diff --git a/.gitignore b/.gitignore index 1e0ebc66..754acf39 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,12 @@ # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 +# docker +db/** + +## shell script +dev-restart.sh + # User-specific stuff .idea/**/workspace.xml .idea/**/tasks.xml diff --git a/application/src/main/java/org/depromeet/spot/application/block/BlockReadController.java b/application/src/main/java/org/depromeet/spot/application/block/BlockReadController.java new file mode 100644 index 00000000..6358e420 --- /dev/null +++ b/application/src/main/java/org/depromeet/spot/application/block/BlockReadController.java @@ -0,0 +1,48 @@ +package org.depromeet.spot.application.block; + +import java.util.List; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; + +import org.depromeet.spot.application.block.dto.response.BlockCodeInfoResponse; +import org.depromeet.spot.usecase.port.in.block.BlockReadUsecase; +import org.depromeet.spot.usecase.port.in.block.BlockReadUsecase.BlockCodeInfo; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; + +@RestController +@Tag(name = "블록") +@RequiredArgsConstructor +@RequestMapping("/api/v1") +public class BlockReadController { + + private final BlockReadUsecase blockReadUsecase; + + @ResponseStatus(HttpStatus.OK) + @GetMapping("/stadiums/{stadiumId}/sections/{sectionId}/blocks") + @Operation(summary = "특정 야구 경기장 특정 구역 내의 블록 리스트를 조회한다.") + public List findCodeInfosByStadium( + @PathVariable("stadiumId") + @NotNull + @Positive + @Parameter(name = "stadiumId", description = "야구 경기장 PK", required = true) + final Long stadiumId, + @PathVariable("sectionId") + @NotNull + @Positive + @Parameter(name = "sectionId", description = "구역 PK", required = true) + final Long sectionId) { + List infos = blockReadUsecase.findCodeInfosByStadium(stadiumId, sectionId); + return infos.stream().map(BlockCodeInfoResponse::from).toList(); + } +} diff --git a/application/src/main/java/org/depromeet/spot/application/block/dto/response/BlockCodeInfoResponse.java b/application/src/main/java/org/depromeet/spot/application/block/dto/response/BlockCodeInfoResponse.java new file mode 100644 index 00000000..21d75bdd --- /dev/null +++ b/application/src/main/java/org/depromeet/spot/application/block/dto/response/BlockCodeInfoResponse.java @@ -0,0 +1,10 @@ +package org.depromeet.spot.application.block.dto.response; + +import org.depromeet.spot.usecase.port.in.block.BlockReadUsecase.BlockCodeInfo; + +public record BlockCodeInfoResponse(Long id, String code) { + + public static BlockCodeInfoResponse from(BlockCodeInfo blockCodeInfo) { + return new BlockCodeInfoResponse(blockCodeInfo.getId(), blockCodeInfo.getCode()); + } +} diff --git a/application/src/main/java/org/depromeet/spot/application/common/dto/RgbCodeRequest.java b/application/src/main/java/org/depromeet/spot/application/common/dto/RgbCodeRequest.java new file mode 100644 index 00000000..7335f27f --- /dev/null +++ b/application/src/main/java/org/depromeet/spot/application/common/dto/RgbCodeRequest.java @@ -0,0 +1,9 @@ +package org.depromeet.spot.application.common.dto; + +import org.depromeet.spot.domain.common.RgbCode; + +public record RgbCodeRequest(Integer red, Integer green, Integer blue) { + public RgbCode toDomain() { + return RgbCode.builder().red(red).green(green).blue(blue).build(); + } +} diff --git a/application/src/main/java/org/depromeet/spot/application/common/dto/RgbCodeResponse.java b/application/src/main/java/org/depromeet/spot/application/common/dto/RgbCodeResponse.java new file mode 100644 index 00000000..f703317b --- /dev/null +++ b/application/src/main/java/org/depromeet/spot/application/common/dto/RgbCodeResponse.java @@ -0,0 +1,13 @@ +package org.depromeet.spot.application.common.dto; + +import org.depromeet.spot.domain.common.RgbCode; + +import lombok.Builder; + +@Builder +public record RgbCodeResponse(Integer red, Integer green, Integer blue) { + + public static RgbCodeResponse from(RgbCode rgbCode) { + return new RgbCodeResponse(rgbCode.getRed(), rgbCode.getGreen(), rgbCode.getBlue()); + } +} diff --git a/application/src/main/java/org/depromeet/spot/application/member/dto/response/MemberResponse.java b/application/src/main/java/org/depromeet/spot/application/member/dto/response/MemberResponse.java index 06ab8203..469e981c 100644 --- a/application/src/main/java/org/depromeet/spot/application/member/dto/response/MemberResponse.java +++ b/application/src/main/java/org/depromeet/spot/application/member/dto/response/MemberResponse.java @@ -5,6 +5,6 @@ public record MemberResponse(Long id, String name) { public static MemberResponse from(Member member) { - return new MemberResponse(member.getId(), member.getName()); + return new MemberResponse(member.getUserId(), member.getName()); } } diff --git a/application/src/main/java/org/depromeet/spot/application/review/ReviewReadController.java b/application/src/main/java/org/depromeet/spot/application/review/ReviewReadController.java new file mode 100644 index 00000000..d4a0c9d7 --- /dev/null +++ b/application/src/main/java/org/depromeet/spot/application/review/ReviewReadController.java @@ -0,0 +1,58 @@ +package org.depromeet.spot.application.review; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.PositiveOrZero; + +import org.depromeet.spot.application.review.dto.response.ReviewListResponse; +import org.depromeet.spot.domain.review.ReviewListResult; +import org.depromeet.spot.usecase.port.in.review.ReviewReadUsecase; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; + +@RestController +@Tag(name = "리뷰") +@RequiredArgsConstructor +@RequestMapping("/api/v1") +public class ReviewReadController { + + private final ReviewReadUsecase reviewReadUsecase; + + @ResponseStatus(HttpStatus.OK) + @GetMapping("/stadiums/{stadiumId}/blocks/{blockId}/reviews") + @Operation(summary = "특정 야구장의 특정 블록에 대한 리뷰 목록을 조회한다.") + public ReviewListResponse findReviewsByBlockId( + @PathVariable("stadiumId") + @NotNull + @Positive + @Parameter(description = "야구장 PK", required = true) + Long stadiumId, + @PathVariable("blockId") + @NotNull + @Positive + @Parameter(description = "블록 PK", required = true) + Long blockId, + @RequestParam(required = false) @Parameter(description = "열 ID (필터링)") Long rowId, + @RequestParam(required = false) @Parameter(description = "좌석 번호 (필터링)") Long seatNumber, + @RequestParam(defaultValue = "0") + @PositiveOrZero + @Parameter(description = "시작 위치 (기본값: 0)") + int offset, + @RequestParam(defaultValue = "10") + @Positive + @Max(50) + @Parameter(description = "조회할 리뷰 수 (기본값: 10, 최대: 50)") + int limit) { + + ReviewListResult result = + reviewReadUsecase.findReviewsByBlockId( + stadiumId, blockId, rowId, seatNumber, offset, limit); + return ReviewListResponse.from(result); + } +} diff --git a/application/src/main/java/org/depromeet/spot/application/review/dto/request/ReviewBlockRequest.java b/application/src/main/java/org/depromeet/spot/application/review/dto/request/ReviewBlockRequest.java new file mode 100644 index 00000000..507544b8 --- /dev/null +++ b/application/src/main/java/org/depromeet/spot/application/review/dto/request/ReviewBlockRequest.java @@ -0,0 +1,12 @@ +package org.depromeet.spot.application.review.dto.request; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.PositiveOrZero; + +import io.swagger.v3.oas.annotations.Parameter; + +public record ReviewBlockRequest( + @Parameter(description = "열 ID (필터링)") Long rowId, + @Parameter(description = "좌석 번호 (필터링)") Long seatNumber, + @PositiveOrZero @Parameter(description = "시작 위치 (기본값: 0)") int offset, + @Max(50) @Parameter(description = "조회할 리뷰 수 (기본값: 10, 최대: 50)") int limit) {} diff --git a/application/src/main/java/org/depromeet/spot/application/review/dto/response/ReviewListResponse.java b/application/src/main/java/org/depromeet/spot/application/review/dto/response/ReviewListResponse.java new file mode 100644 index 00000000..49809377 --- /dev/null +++ b/application/src/main/java/org/depromeet/spot/application/review/dto/response/ReviewListResponse.java @@ -0,0 +1,43 @@ +package org.depromeet.spot.application.review.dto.response; + +import java.util.List; +import java.util.stream.Collectors; + +import org.depromeet.spot.domain.review.ReviewListResult; + +public record ReviewListResponse( + List keywords, + List reviews, + Long totalCount, + int filteredCount, + int offset, + int limit, + boolean hasMore, + FilterInfo filter) { + public static ReviewListResponse from(ReviewListResult result) { + List reviewResponses = + result.reviews().stream().map(ReviewResponse::from).collect(Collectors.toList()); + + List keywordResponses = + result.topKeywords().stream() + .map(kc -> new KeywordCountResponse(kc.content(), kc.count())) + .collect(Collectors.toList()); + + boolean hasMore = (result.offset() + result.reviews().size()) < result.totalCount(); + FilterInfo filter = new FilterInfo(null, null); // Assuming no filter info for now + + return new ReviewListResponse( + keywordResponses, + reviewResponses, + result.totalCount(), + result.reviews().size(), + result.offset(), + result.limit(), + hasMore, + filter); + } + + record KeywordCountResponse(String content, Long count) {} + + record FilterInfo(Long rowId, Integer seatNumber) {} +} diff --git a/application/src/main/java/org/depromeet/spot/application/review/dto/response/ReviewResponse.java b/application/src/main/java/org/depromeet/spot/application/review/dto/response/ReviewResponse.java new file mode 100644 index 00000000..66257481 --- /dev/null +++ b/application/src/main/java/org/depromeet/spot/application/review/dto/response/ReviewResponse.java @@ -0,0 +1,60 @@ +package org.depromeet.spot.application.review.dto.response; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +import org.depromeet.spot.domain.review.Review; +import org.depromeet.spot.domain.review.ReviewImage; +import org.depromeet.spot.domain.review.ReviewKeyword; + +public record ReviewResponse( + Long id, + Long userId, + Long blockId, + Long seatId, + Long rowId, + Long seatNumber, + LocalDateTime date, + String content, + LocalDateTime createdAt, + LocalDateTime updatedAt, + List images, + List keywords) { + public static ReviewResponse from(Review review) { + return new ReviewResponse( + review.getId(), + review.getUserId(), + review.getBlockId(), + review.getSeatId(), + review.getRowId(), + review.getSeatNumber(), + review.getDateTime(), + review.getContent(), + review.getCreatedAt(), + review.getUpdatedAt(), + mapToImageResponses(review.getImages()), + mapToKeywordResponses(review.getKeywords())); + } + + private static List mapToImageResponses(List images) { + return images.stream() + .map(image -> new ImageResponse(image.getId(), image.getUrl())) + .collect(Collectors.toList()); + } + + private static List mapToKeywordResponses(List keywords) { + return keywords.stream() + .map( + keyword -> + new KeywordResponse( + keyword.getId(), + keyword.getKeywordId(), + keyword.getIsPositive())) + .collect(Collectors.toList()); + } + + record ImageResponse(Long id, String url) {} + + record KeywordResponse(Long id, Long keywordId, Boolean isPositive) {} +} diff --git a/application/src/main/java/org/depromeet/spot/application/section/SectionReadController.java b/application/src/main/java/org/depromeet/spot/application/section/SectionReadController.java new file mode 100644 index 00000000..c28f59da --- /dev/null +++ b/application/src/main/java/org/depromeet/spot/application/section/SectionReadController.java @@ -0,0 +1,41 @@ +package org.depromeet.spot.application.section; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; + +import org.depromeet.spot.application.section.dto.response.StadiumSectionsResponse; +import org.depromeet.spot.usecase.port.in.section.SectionReadUsecase; +import org.depromeet.spot.usecase.port.in.section.SectionReadUsecase.StadiumSections; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; + +@RestController +@Tag(name = "구역") +@RequiredArgsConstructor +@RequestMapping("/api/v1") +public class SectionReadController { + + private final SectionReadUsecase sectionReadUsecase; + + @ResponseStatus(HttpStatus.OK) + @GetMapping("/stadiums/{stadiumId}/sections") + @Operation(summary = "특정 야구 경기장 내의 모든 구역 정보를 구장 좌석 배치도와 함께 조회한다.") + public StadiumSectionsResponse findAllByStadium( + @PathVariable("stadiumId") + @NotNull + @Positive + @Parameter(name = "stadiumId", description = "야구 경기장 PK", required = true) + final Long stadiumId) { + StadiumSections sectionInfo = sectionReadUsecase.findAllByStadium(stadiumId); + return StadiumSectionsResponse.from(sectionInfo); + } +} diff --git a/application/src/main/java/org/depromeet/spot/application/section/dto/response/SectionInfoResponse.java b/application/src/main/java/org/depromeet/spot/application/section/dto/response/SectionInfoResponse.java new file mode 100644 index 00000000..062a2cc3 --- /dev/null +++ b/application/src/main/java/org/depromeet/spot/application/section/dto/response/SectionInfoResponse.java @@ -0,0 +1,19 @@ +package org.depromeet.spot.application.section.dto.response; + +import org.depromeet.spot.domain.common.RgbCode; +import org.depromeet.spot.usecase.port.in.section.SectionReadUsecase.SectionInfo; + +import lombok.Builder; + +@Builder +public record SectionInfoResponse(Long id, String name, String alias, RgbCode color) { + + public static SectionInfoResponse from(SectionInfo sectionInfo) { + return SectionInfoResponse.builder() + .id(sectionInfo.getId()) + .name(sectionInfo.getName()) + .alias(sectionInfo.getAlias()) + .color(sectionInfo.getColor()) + .build(); + } +} diff --git a/application/src/main/java/org/depromeet/spot/application/section/dto/response/StadiumSectionsResponse.java b/application/src/main/java/org/depromeet/spot/application/section/dto/response/StadiumSectionsResponse.java new file mode 100644 index 00000000..d8dc65f1 --- /dev/null +++ b/application/src/main/java/org/depromeet/spot/application/section/dto/response/StadiumSectionsResponse.java @@ -0,0 +1,14 @@ +package org.depromeet.spot.application.section.dto.response; + +import java.util.List; + +import org.depromeet.spot.usecase.port.in.section.SectionReadUsecase.StadiumSections; + +public record StadiumSectionsResponse(String seatChart, List sectionList) { + + public static StadiumSectionsResponse from(StadiumSections stadiumSections) { + List infos = + stadiumSections.getSectionList().stream().map(SectionInfoResponse::from).toList(); + return new StadiumSectionsResponse(stadiumSections.getSeatChart(), infos); + } +} diff --git a/application/src/main/java/org/depromeet/spot/application/stadium/StadiumReadController.java b/application/src/main/java/org/depromeet/spot/application/stadium/StadiumReadController.java new file mode 100644 index 00000000..4eb01c74 --- /dev/null +++ b/application/src/main/java/org/depromeet/spot/application/stadium/StadiumReadController.java @@ -0,0 +1,65 @@ +package org.depromeet.spot.application.stadium; + +import java.util.List; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; + +import org.depromeet.spot.application.stadium.dto.response.StadiumHomeTeamInfoResponse; +import org.depromeet.spot.application.stadium.dto.response.StadiumInfoWithSeatChartResponse; +import org.depromeet.spot.application.stadium.dto.response.StadiumNameInfoResponse; +import org.depromeet.spot.usecase.port.in.stadium.StadiumReadUsecase; +import org.depromeet.spot.usecase.port.in.stadium.StadiumReadUsecase.StadiumHomeTeamInfo; +import org.depromeet.spot.usecase.port.in.stadium.StadiumReadUsecase.StadiumInfoWithSeatChart; +import org.depromeet.spot.usecase.port.in.stadium.StadiumReadUsecase.StadiumNameInfo; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; + +@RestController +@Tag(name = "경기장") +@RequiredArgsConstructor +@RequestMapping("/api/v1/stadiums") +public class StadiumReadController { + + private final StadiumReadUsecase stadiumReadUsecase; + + @GetMapping() + @ResponseStatus(HttpStatus.OK) + @Operation(summary = "SPOT에서 관리하는 모든 야구 경기장 정보를 조회한다.") + public List findAllStadiums() { + List infos = stadiumReadUsecase.findAllStadiums(); + return infos.stream().map(StadiumHomeTeamInfoResponse::from).toList(); + } + + @GetMapping("/names") + @ResponseStatus(HttpStatus.OK) + @Operation(summary = "SPOT에서 관리하는 모든 야구 경기장의 이름들을 조회한다.") + public List findAllNames() { + List infos = stadiumReadUsecase.findAllNames(); + return infos.stream() + .map(t -> new StadiumNameInfoResponse(t.getId(), t.getName())) + .toList(); + } + + @GetMapping("/{stadiumId}") + @ResponseStatus(HttpStatus.OK) + @Operation(summary = "특정 야구 경기장의 상세 정보를 좌석 배치도와 함께 조회한다.") + public StadiumInfoWithSeatChartResponse findWithSeatChartById( + @PathVariable("stadiumId") + @NotNull + @Positive + @Parameter(name = "stadiumId", description = "야구 경기장 PK", required = true) + final Long stadiumId) { + StadiumInfoWithSeatChart info = stadiumReadUsecase.findWithSeatChartById(stadiumId); + return StadiumInfoWithSeatChartResponse.from(info); + } +} diff --git a/application/src/main/java/org/depromeet/spot/application/stadium/dto/response/HomeTeamInfoResponse.java b/application/src/main/java/org/depromeet/spot/application/stadium/dto/response/HomeTeamInfoResponse.java new file mode 100644 index 00000000..1ece6284 --- /dev/null +++ b/application/src/main/java/org/depromeet/spot/application/stadium/dto/response/HomeTeamInfoResponse.java @@ -0,0 +1,20 @@ +package org.depromeet.spot.application.stadium.dto.response; + +import org.depromeet.spot.application.common.dto.RgbCodeResponse; +import org.depromeet.spot.domain.common.RgbCode; +import org.depromeet.spot.usecase.port.in.team.ReadStadiumHomeTeamUsecase.HomeTeamInfo; + +public record HomeTeamInfoResponse(Long id, String alias, RgbCodeResponse color) { + + public static HomeTeamInfoResponse from(HomeTeamInfo homeTeamInfo) { + RgbCode rgbCode = homeTeamInfo.getRgbCode(); + RgbCodeResponse rgbCodeRes = + RgbCodeResponse.builder() + .red(rgbCode.getRed()) + .blue(rgbCode.getBlue()) + .green(rgbCode.getGreen()) + .build(); + + return new HomeTeamInfoResponse(homeTeamInfo.getId(), homeTeamInfo.getAlias(), rgbCodeRes); + } +} diff --git a/application/src/main/java/org/depromeet/spot/application/stadium/dto/response/StadiumHomeTeamInfoResponse.java b/application/src/main/java/org/depromeet/spot/application/stadium/dto/response/StadiumHomeTeamInfoResponse.java new file mode 100644 index 00000000..e200dd33 --- /dev/null +++ b/application/src/main/java/org/depromeet/spot/application/stadium/dto/response/StadiumHomeTeamInfoResponse.java @@ -0,0 +1,29 @@ +package org.depromeet.spot.application.stadium.dto.response; + +import java.util.List; + +import org.depromeet.spot.usecase.port.in.stadium.StadiumReadUsecase.StadiumHomeTeamInfo; + +import lombok.Builder; + +@Builder +public record StadiumHomeTeamInfoResponse( + Long id, + String name, + String thumbnail, + List homeTeams, + boolean isActive) { + public static StadiumHomeTeamInfoResponse from(StadiumHomeTeamInfo stadiumHomeTeamInfo) { + List homeTeams = + stadiumHomeTeamInfo.getHomeTeams().stream() + .map(HomeTeamInfoResponse::from) + .toList(); + + return new StadiumHomeTeamInfoResponse( + stadiumHomeTeamInfo.getId(), + stadiumHomeTeamInfo.getName(), + stadiumHomeTeamInfo.getThumbnail(), + homeTeams, + stadiumHomeTeamInfo.isActive()); + } +} diff --git a/application/src/main/java/org/depromeet/spot/application/stadium/dto/response/StadiumInfoWithSeatChartResponse.java b/application/src/main/java/org/depromeet/spot/application/stadium/dto/response/StadiumInfoWithSeatChartResponse.java new file mode 100644 index 00000000..613ac65a --- /dev/null +++ b/application/src/main/java/org/depromeet/spot/application/stadium/dto/response/StadiumInfoWithSeatChartResponse.java @@ -0,0 +1,26 @@ +package org.depromeet.spot.application.stadium.dto.response; + +import java.util.List; + +import org.depromeet.spot.usecase.port.in.stadium.StadiumReadUsecase.StadiumInfoWithSeatChart; + +import lombok.Builder; + +@Builder +public record StadiumInfoWithSeatChartResponse( + Long id, String name, String seatChartWithLabel, List homeTeams) { + + public static StadiumInfoWithSeatChartResponse from( + StadiumInfoWithSeatChart stadiumInfoWithSeatChart) { + List homeTeams = + stadiumInfoWithSeatChart.getHomeTeams().stream() + .map(HomeTeamInfoResponse::from) + .toList(); + + return new StadiumInfoWithSeatChartResponse( + stadiumInfoWithSeatChart.getId(), + stadiumInfoWithSeatChart.getName(), + stadiumInfoWithSeatChart.getSeatChartWithLabel(), + homeTeams); + } +} diff --git a/application/src/main/java/org/depromeet/spot/application/stadium/dto/response/StadiumNameInfoResponse.java b/application/src/main/java/org/depromeet/spot/application/stadium/dto/response/StadiumNameInfoResponse.java new file mode 100644 index 00000000..7606a081 --- /dev/null +++ b/application/src/main/java/org/depromeet/spot/application/stadium/dto/response/StadiumNameInfoResponse.java @@ -0,0 +1,3 @@ +package org.depromeet.spot.application.stadium.dto.response; + +public record StadiumNameInfoResponse(Long id, String name) {} diff --git a/application/src/main/java/org/depromeet/spot/application/team/CreateBaseballTeamController.java b/application/src/main/java/org/depromeet/spot/application/team/CreateBaseballTeamController.java new file mode 100644 index 00000000..b5ccdeb2 --- /dev/null +++ b/application/src/main/java/org/depromeet/spot/application/team/CreateBaseballTeamController.java @@ -0,0 +1,37 @@ +package org.depromeet.spot.application.team; + +import java.util.List; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; + +import org.depromeet.spot.application.team.dto.request.CreateBaseballTeamReq; +import org.depromeet.spot.domain.team.BaseballTeam; +import org.depromeet.spot.usecase.port.in.team.CreateBaseballTeamUsecase; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; + +@RestController +@Tag(name = "야구 팀") +@RequiredArgsConstructor +@RequestMapping("/api/v1") +public class CreateBaseballTeamController { + + private final CreateBaseballTeamUsecase createBaseballTeamUsecase; + + @PostMapping("/baseball-teams") + @ResponseStatus(HttpStatus.CREATED) + @Operation(summary = "신규 야구 팀(구단) 정보를 생성한다.") + public void create(@RequestBody @Valid @NotEmpty List requests) { + List teams = requests.stream().map(CreateBaseballTeamReq::toDomain).toList(); + createBaseballTeamUsecase.saveAll(teams); + } +} diff --git a/application/src/main/java/org/depromeet/spot/application/team/ReadBaseballTeamController.java b/application/src/main/java/org/depromeet/spot/application/team/ReadBaseballTeamController.java new file mode 100644 index 00000000..d6f264b7 --- /dev/null +++ b/application/src/main/java/org/depromeet/spot/application/team/ReadBaseballTeamController.java @@ -0,0 +1,33 @@ +package org.depromeet.spot.application.team; + +import java.util.List; + +import org.depromeet.spot.application.team.dto.response.BaseballTeamLogoRes; +import org.depromeet.spot.domain.team.BaseballTeam; +import org.depromeet.spot.usecase.port.in.team.ReadBaseballTeamUsecase; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; + +@RestController +@Tag(name = "야구 팀") +@RequiredArgsConstructor +@RequestMapping("/api/v1") +public class ReadBaseballTeamController { + + private final ReadBaseballTeamUsecase readBaseballTeamUsecase; + + @GetMapping("/baseball-teams") + @ResponseStatus(HttpStatus.OK) + @Operation(summary = "SPOT에서 관리하는 모든 야구 팀 정보를 조회한다.") + public List findAll() { + List infos = readBaseballTeamUsecase.findAll(); + return infos.stream().map(BaseballTeamLogoRes::from).toList(); + } +} diff --git a/application/src/main/java/org/depromeet/spot/application/team/dto/request/CreateBaseballTeamReq.java b/application/src/main/java/org/depromeet/spot/application/team/dto/request/CreateBaseballTeamReq.java new file mode 100644 index 00000000..e8979094 --- /dev/null +++ b/application/src/main/java/org/depromeet/spot/application/team/dto/request/CreateBaseballTeamReq.java @@ -0,0 +1,27 @@ +package org.depromeet.spot.application.team.dto.request; + +import jakarta.validation.constraints.NotBlank; + +import org.depromeet.spot.application.common.dto.RgbCodeRequest; +import org.depromeet.spot.domain.team.BaseballTeam; +import org.hibernate.validator.constraints.Length; + +public record CreateBaseballTeamReq( + @NotBlank(message = "구단명을 입력해주세요.") + @Length(max = 20, message = "구단명은 최대 20글자 까지만 입력할 수 있습니다.") + String name, + @NotBlank(message = "구단 별칭을 입력해주세요.") + @Length(max = 10, message = "구단 별칭은 최대 10글자 까지만 입력할 수 있습니다.") + String alias, + @NotBlank(message = "구단 로고를 입력해주세요.") String logo, + RgbCodeRequest rgbCode) { + + public BaseballTeam toDomain() { + return BaseballTeam.builder() + .name(name) + .alias(alias) + .logo(logo) + .labelRgbCode(rgbCode.toDomain()) + .build(); + } +} diff --git a/application/src/main/java/org/depromeet/spot/application/team/dto/response/BaseballTeamLogoRes.java b/application/src/main/java/org/depromeet/spot/application/team/dto/response/BaseballTeamLogoRes.java new file mode 100644 index 00000000..b66b3e17 --- /dev/null +++ b/application/src/main/java/org/depromeet/spot/application/team/dto/response/BaseballTeamLogoRes.java @@ -0,0 +1,10 @@ +package org.depromeet.spot.application.team.dto.response; + +import org.depromeet.spot.domain.team.BaseballTeam; + +public record BaseballTeamLogoRes(Long id, String name, String logo) { + + public static BaseballTeamLogoRes from(BaseballTeam team) { + return new BaseballTeamLogoRes(team.getId(), team.getName(), team.getLogo()); + } +} diff --git a/common/src/main/java/org/depromeet/spot/common/exception/review/ReviewErrorCode.java b/common/src/main/java/org/depromeet/spot/common/exception/review/ReviewErrorCode.java new file mode 100644 index 00000000..f0a44f03 --- /dev/null +++ b/common/src/main/java/org/depromeet/spot/common/exception/review/ReviewErrorCode.java @@ -0,0 +1,27 @@ +package org.depromeet.spot.common.exception.review; + +import org.depromeet.spot.common.exception.ErrorCode; +import org.springframework.http.HttpStatus; + +import lombok.Getter; + +@Getter +public enum ReviewErrorCode implements ErrorCode { + REVIEW_NOT_FOUND(HttpStatus.NOT_FOUND, "RV001", "요청한 리뷰를 찾을 수 없습니다."), + INVALID_REVIEW_DATA(HttpStatus.BAD_REQUEST, "RV002", "유효하지 않은 리뷰 데이터입니다."); + + private final HttpStatus status; + private final String code; + private String message; + + ReviewErrorCode(HttpStatus status, String code, String message) { + this.status = status; + this.code = code; + this.message = message; + } + + public ReviewErrorCode appended(Object o) { + message = message + " {" + o.toString() + "}"; + return this; + } +} diff --git a/common/src/main/java/org/depromeet/spot/common/exception/review/ReviewException.java b/common/src/main/java/org/depromeet/spot/common/exception/review/ReviewException.java new file mode 100644 index 00000000..a766d2c2 --- /dev/null +++ b/common/src/main/java/org/depromeet/spot/common/exception/review/ReviewException.java @@ -0,0 +1,29 @@ +package org.depromeet.spot.common.exception.review; + +import org.depromeet.spot.common.exception.BusinessException; + +public abstract class ReviewException extends BusinessException { + protected ReviewException(ReviewErrorCode errorCode) { + super(errorCode); + } + + public static class ReviewNotFoundException extends ReviewException { + public ReviewNotFoundException() { + super(ReviewErrorCode.REVIEW_NOT_FOUND); + } + + public ReviewNotFoundException(String s) { + super(ReviewErrorCode.REVIEW_NOT_FOUND.appended(s)); + } + } + + public static class InvalidReviewDataException extends ReviewException { + public InvalidReviewDataException() { + super(ReviewErrorCode.INVALID_REVIEW_DATA); + } + + public InvalidReviewDataException(String str) { + super(ReviewErrorCode.INVALID_REVIEW_DATA.appended(str)); + } + } +} diff --git a/common/src/main/java/org/depromeet/spot/common/exception/section/SectionErrorCode.java b/common/src/main/java/org/depromeet/spot/common/exception/section/SectionErrorCode.java new file mode 100644 index 00000000..65435715 --- /dev/null +++ b/common/src/main/java/org/depromeet/spot/common/exception/section/SectionErrorCode.java @@ -0,0 +1,28 @@ +package org.depromeet.spot.common.exception.section; + +import org.depromeet.spot.common.exception.ErrorCode; +import org.springframework.http.HttpStatus; + +import lombok.Getter; + +@Getter +public enum SectionErrorCode implements ErrorCode { + SECTION_NOT_FOUND(HttpStatus.NOT_FOUND, "SE001", "요청 구역이 존재하지 않습니다."), + SECTION_NOT_BELONG_TO_STADIUM(HttpStatus.BAD_REQUEST, "SE002", "요청 경기장의 구역이 아닙니다."), + ; + + private final HttpStatus status; + private final String code; + private String message; + + SectionErrorCode(HttpStatus status, String code, String message) { + this.status = status; + this.code = code; + this.message = message; + } + + public SectionErrorCode appended(Object o) { + message = message + " {" + o.toString() + "}"; + return this; + } +} diff --git a/common/src/main/java/org/depromeet/spot/common/exception/section/SectionException.java b/common/src/main/java/org/depromeet/spot/common/exception/section/SectionException.java new file mode 100644 index 00000000..053b5f02 --- /dev/null +++ b/common/src/main/java/org/depromeet/spot/common/exception/section/SectionException.java @@ -0,0 +1,22 @@ +package org.depromeet.spot.common.exception.section; + +import org.depromeet.spot.common.exception.BusinessException; + +public abstract class SectionException extends BusinessException { + + protected SectionException(SectionErrorCode errorCode) { + super(errorCode); + } + + public static class SectionNotFoundException extends SectionException { + public SectionNotFoundException() { + super(SectionErrorCode.SECTION_NOT_FOUND); + } + } + + public static class SectionNotBelongStadiumException extends SectionException { + public SectionNotBelongStadiumException() { + super(SectionErrorCode.SECTION_NOT_BELONG_TO_STADIUM); + } + } +} diff --git a/common/src/main/java/org/depromeet/spot/common/exception/stadium/StadiumErrorCode.java b/common/src/main/java/org/depromeet/spot/common/exception/stadium/StadiumErrorCode.java new file mode 100644 index 00000000..5651a862 --- /dev/null +++ b/common/src/main/java/org/depromeet/spot/common/exception/stadium/StadiumErrorCode.java @@ -0,0 +1,27 @@ +package org.depromeet.spot.common.exception.stadium; + +import org.depromeet.spot.common.exception.ErrorCode; +import org.springframework.http.HttpStatus; + +import lombok.Getter; + +@Getter +public enum StadiumErrorCode implements ErrorCode { + STADIUM_NOT_FOUND(HttpStatus.NOT_FOUND, "ST001", "요청 경기장이 존재하지 않습니다."), + ; + + private final HttpStatus status; + private final String code; + private String message; + + StadiumErrorCode(HttpStatus status, String code, String message) { + this.status = status; + this.code = code; + this.message = message; + } + + public StadiumErrorCode appended(Object o) { + message = message + " {" + o.toString() + "}"; + return this; + } +} diff --git a/common/src/main/java/org/depromeet/spot/common/exception/stadium/StadiumException.java b/common/src/main/java/org/depromeet/spot/common/exception/stadium/StadiumException.java new file mode 100644 index 00000000..59321c89 --- /dev/null +++ b/common/src/main/java/org/depromeet/spot/common/exception/stadium/StadiumException.java @@ -0,0 +1,20 @@ +package org.depromeet.spot.common.exception.stadium; + +import org.depromeet.spot.common.exception.BusinessException; + +public abstract class StadiumException extends BusinessException { + + protected StadiumException(StadiumErrorCode errorCode) { + super(errorCode); + } + + public static class StadiumNotFoundException extends StadiumException { + public StadiumNotFoundException() { + super(StadiumErrorCode.STADIUM_NOT_FOUND); + } + + public StadiumNotFoundException(String str) { + super(StadiumErrorCode.STADIUM_NOT_FOUND.appended(str)); + } + } +} diff --git a/common/src/main/java/org/depromeet/spot/common/exception/team/TeamErrorCode.java b/common/src/main/java/org/depromeet/spot/common/exception/team/TeamErrorCode.java new file mode 100644 index 00000000..71e4194e --- /dev/null +++ b/common/src/main/java/org/depromeet/spot/common/exception/team/TeamErrorCode.java @@ -0,0 +1,31 @@ +package org.depromeet.spot.common.exception.team; + +import org.depromeet.spot.common.exception.ErrorCode; +import org.springframework.http.HttpStatus; + +import lombok.Getter; + +@Getter +public enum TeamErrorCode implements ErrorCode { + BASEBALL_TEAM_NOT_FOUND(HttpStatus.NOT_FOUND, "T001", "요청 구단이 존재하지 않습니다."), + INVALID_TEAM_NAME_NOT_FOUND(HttpStatus.BAD_REQUEST, "T002", "구단명이 잘못되었습니다."), + INVALID_TEAM_ALIAS_NOT_FOUND(HttpStatus.BAD_REQUEST, "T003", "구단 별칭이 잘못되었습니다."), + DUPLICATE_TEAM_NAME(HttpStatus.CONFLICT, "T004", "이미 등록된 구단입니다."), + EMPTY_TEAM_LOGO(HttpStatus.BAD_REQUEST, "T005", "구단 로고를 등록해주세요."), + ; + + private final HttpStatus status; + private final String code; + private String message; + + TeamErrorCode(HttpStatus status, String code, String message) { + this.status = status; + this.code = code; + this.message = message; + } + + public TeamErrorCode appended(Object o) { + message = message + " {" + o.toString() + "}"; + return this; + } +} diff --git a/common/src/main/java/org/depromeet/spot/common/exception/team/TeamException.java b/common/src/main/java/org/depromeet/spot/common/exception/team/TeamException.java new file mode 100644 index 00000000..b217581e --- /dev/null +++ b/common/src/main/java/org/depromeet/spot/common/exception/team/TeamException.java @@ -0,0 +1,40 @@ +package org.depromeet.spot.common.exception.team; + +import org.depromeet.spot.common.exception.BusinessException; + +public abstract class TeamException extends BusinessException { + + protected TeamException(TeamErrorCode errorCode) { + super(errorCode); + } + + public static class BaseballTeamNotFoundException extends TeamException { + public BaseballTeamNotFoundException() { + super(TeamErrorCode.BASEBALL_TEAM_NOT_FOUND); + } + } + + public static class InvalidBaseballTeamNameException extends TeamException { + public InvalidBaseballTeamNameException() { + super(TeamErrorCode.INVALID_TEAM_NAME_NOT_FOUND); + } + } + + public static class InvalidBaseballAliasNameException extends TeamException { + public InvalidBaseballAliasNameException() { + super(TeamErrorCode.INVALID_TEAM_ALIAS_NOT_FOUND); + } + } + + public static class DuplicateTeamNameException extends TeamException { + public DuplicateTeamNameException() { + super(TeamErrorCode.DUPLICATE_TEAM_NAME); + } + } + + public static class EmptyTeamLogoException extends TeamException { + public EmptyTeamLogoException() { + super(TeamErrorCode.EMPTY_TEAM_LOGO); + } + } +} diff --git a/common/src/main/java/org/depromeet/spot/common/exception/util/UtilErrorCode.java b/common/src/main/java/org/depromeet/spot/common/exception/util/UtilErrorCode.java new file mode 100644 index 00000000..306dab85 --- /dev/null +++ b/common/src/main/java/org/depromeet/spot/common/exception/util/UtilErrorCode.java @@ -0,0 +1,27 @@ +package org.depromeet.spot.common.exception.util; + +import org.depromeet.spot.common.exception.ErrorCode; +import org.springframework.http.HttpStatus; + +import lombok.Getter; + +@Getter +public enum UtilErrorCode implements ErrorCode { + INVALID_RGB_CODE(HttpStatus.BAD_REQUEST, "UT001", "잘못된 RGB 코드 값 입니다."), + ; + + private final HttpStatus status; + private final String code; + private String message; + + UtilErrorCode(HttpStatus status, String code, String message) { + this.status = status; + this.code = code; + this.message = message; + } + + public UtilErrorCode appended(Object o) { + message = message + " {" + o.toString() + "}"; + return this; + } +} diff --git a/common/src/main/java/org/depromeet/spot/common/exception/util/UtilException.java b/common/src/main/java/org/depromeet/spot/common/exception/util/UtilException.java new file mode 100644 index 00000000..c484035e --- /dev/null +++ b/common/src/main/java/org/depromeet/spot/common/exception/util/UtilException.java @@ -0,0 +1,15 @@ +package org.depromeet.spot.common.exception.util; + +import org.depromeet.spot.common.exception.BusinessException; + +public abstract class UtilException extends BusinessException { + protected UtilException(UtilErrorCode errorCode) { + super(errorCode); + } + + public static class InvalidRgbCodeException extends UtilException { + public InvalidRgbCodeException() { + super(UtilErrorCode.INVALID_RGB_CODE); + } + } +} diff --git a/docker-compose.yml b/docker-compose.yml index 027dd8cf..ed72f3b5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -41,4 +41,4 @@ services: - ~/.gradle:/root/.gradle command: ./gradlew :application:bootRun env_file: - - .env + - .env \ No newline at end of file diff --git a/domain/src/main/java/org/depromeet/spot/domain/block/Block.java b/domain/src/main/java/org/depromeet/spot/domain/block/Block.java new file mode 100644 index 00000000..38b78fe4 --- /dev/null +++ b/domain/src/main/java/org/depromeet/spot/domain/block/Block.java @@ -0,0 +1,21 @@ +package org.depromeet.spot.domain.block; + +import lombok.Getter; + +@Getter +public class Block { + + private final Long id; + private final Long stadiumId; + private final Long sectionId; + private final String code; + private final Integer maxRows; + + public Block(Long id, Long stadiumId, Long sectionId, String code, Integer maxRows) { + this.id = id; + this.stadiumId = stadiumId; + this.sectionId = sectionId; + this.code = code; + this.maxRows = maxRows; + } +} diff --git a/domain/src/main/java/org/depromeet/spot/domain/block/BlockRow.java b/domain/src/main/java/org/depromeet/spot/domain/block/BlockRow.java new file mode 100644 index 00000000..df0d19d3 --- /dev/null +++ b/domain/src/main/java/org/depromeet/spot/domain/block/BlockRow.java @@ -0,0 +1,19 @@ +package org.depromeet.spot.domain.block; + +import lombok.Getter; + +@Getter +public class BlockRow { + + private final Long id; + private final Long blockId; + private final Long number; + private final Long maxSeats; + + public BlockRow(Long id, Long blockId, Long number, Long max_seats) { + this.id = id; + this.blockId = blockId; + this.number = number; + this.maxSeats = max_seats; + } +} diff --git a/domain/src/main/java/org/depromeet/spot/domain/block/BlockTopKeyword.java b/domain/src/main/java/org/depromeet/spot/domain/block/BlockTopKeyword.java new file mode 100644 index 00000000..1e186b8b --- /dev/null +++ b/domain/src/main/java/org/depromeet/spot/domain/block/BlockTopKeyword.java @@ -0,0 +1,13 @@ +package org.depromeet.spot.domain.block; + +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class BlockTopKeyword { + private final Long id; + private final Long blockId; + private final Long keywordId; + private final Long count; +} diff --git a/domain/src/main/java/org/depromeet/spot/domain/common/RgbCode.java b/domain/src/main/java/org/depromeet/spot/domain/common/RgbCode.java new file mode 100644 index 00000000..052f9938 --- /dev/null +++ b/domain/src/main/java/org/depromeet/spot/domain/common/RgbCode.java @@ -0,0 +1,28 @@ +package org.depromeet.spot.domain.common; + +import org.depromeet.spot.common.exception.util.UtilException.InvalidRgbCodeException; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class RgbCode { + + private final Integer red; + private final Integer green; + private final Integer blue; + + public RgbCode(Integer red, Integer green, Integer blue) { + isValidRgb(red, green, blue); + this.red = red; + this.green = green; + this.blue = blue; + } + + private static void isValidRgb(Integer red, Integer green, Integer blue) { + if (red == null || green == null || blue == null) { + throw new InvalidRgbCodeException(); + } + } +} diff --git a/domain/src/main/java/org/depromeet/spot/domain/member/Member.java b/domain/src/main/java/org/depromeet/spot/domain/member/Member.java index 7c141f54..c931dcd9 100644 --- a/domain/src/main/java/org/depromeet/spot/domain/member/Member.java +++ b/domain/src/main/java/org/depromeet/spot/domain/member/Member.java @@ -1,15 +1,52 @@ package org.depromeet.spot.domain.member; +import java.time.LocalDateTime; + import lombok.Getter; @Getter public class Member { - private final Long id; + private final Long userId; + private final String email; private final String name; + private final String nickname; + private final String phoneNumber; + private final Integer level; + private final String profileImage; + private final String snsProvider; + private final String idToken; + private final String myTeam; + private final Integer role; + private final LocalDateTime createdAt; + private final LocalDateTime deletedAt; - public Member(Long id, String name) { - this.id = id; + public Member( + Long userId, + String email, + String name, + String nickname, + String phoneNumber, + Integer level, + String profileImage, + String snsProvider, + String idToken, + String myTeam, + Integer role, + LocalDateTime createdAt, + LocalDateTime deletedAt) { + this.userId = userId; + this.email = email; this.name = name; + this.nickname = nickname; + this.phoneNumber = phoneNumber; + this.level = level; + this.profileImage = profileImage; + this.snsProvider = snsProvider; + this.idToken = idToken; + this.myTeam = myTeam; + this.role = role; + this.createdAt = createdAt; + this.deletedAt = deletedAt; } } diff --git a/domain/src/main/java/org/depromeet/spot/domain/review/Keyword.java b/domain/src/main/java/org/depromeet/spot/domain/review/Keyword.java new file mode 100644 index 00000000..90d0dd65 --- /dev/null +++ b/domain/src/main/java/org/depromeet/spot/domain/review/Keyword.java @@ -0,0 +1,12 @@ +package org.depromeet.spot.domain.review; + +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class Keyword { + + private final Long id; + private final String content; +} diff --git a/domain/src/main/java/org/depromeet/spot/domain/review/KeywordCount.java b/domain/src/main/java/org/depromeet/spot/domain/review/KeywordCount.java new file mode 100644 index 00000000..da5a01b6 --- /dev/null +++ b/domain/src/main/java/org/depromeet/spot/domain/review/KeywordCount.java @@ -0,0 +1,3 @@ +package org.depromeet.spot.domain.review; + +public record KeywordCount(String content, Long count) {} diff --git a/domain/src/main/java/org/depromeet/spot/domain/review/Review.java b/domain/src/main/java/org/depromeet/spot/domain/review/Review.java new file mode 100644 index 00000000..b96e33d8 --- /dev/null +++ b/domain/src/main/java/org/depromeet/spot/domain/review/Review.java @@ -0,0 +1,28 @@ +package org.depromeet.spot.domain.review; + +import java.time.LocalDateTime; +import java.util.List; + +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class Review { + + private final Long id; + private final Long userId; + private final Long stadiumId; + private final Long blockId; + private final Long seatId; + private final Long rowId; + private final Long seatNumber; + + private final LocalDateTime dateTime; // 시간은 미표기 + private final String content; + private final LocalDateTime createdAt; + private final LocalDateTime updatedAt; + private final LocalDateTime deletedAt; + private final List images; + private final List keywords; +} diff --git a/domain/src/main/java/org/depromeet/spot/domain/review/ReviewImage.java b/domain/src/main/java/org/depromeet/spot/domain/review/ReviewImage.java new file mode 100644 index 00000000..a2034140 --- /dev/null +++ b/domain/src/main/java/org/depromeet/spot/domain/review/ReviewImage.java @@ -0,0 +1,17 @@ +package org.depromeet.spot.domain.review; + +import java.time.LocalDateTime; + +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class ReviewImage { + + private final Long id; + private final Long reviewId; + private final String url; + private final LocalDateTime createdAt; + private final LocalDateTime deletedAt; +} diff --git a/domain/src/main/java/org/depromeet/spot/domain/review/ReviewKeyword.java b/domain/src/main/java/org/depromeet/spot/domain/review/ReviewKeyword.java new file mode 100644 index 00000000..143fd4ba --- /dev/null +++ b/domain/src/main/java/org/depromeet/spot/domain/review/ReviewKeyword.java @@ -0,0 +1,14 @@ +package org.depromeet.spot.domain.review; + +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class ReviewKeyword { + + private final Long id; + private final Long reviewId; + private final Long keywordId; + private final Boolean isPositive; +} diff --git a/domain/src/main/java/org/depromeet/spot/domain/review/ReviewListResult.java b/domain/src/main/java/org/depromeet/spot/domain/review/ReviewListResult.java new file mode 100644 index 00000000..668705d8 --- /dev/null +++ b/domain/src/main/java/org/depromeet/spot/domain/review/ReviewListResult.java @@ -0,0 +1,10 @@ +package org.depromeet.spot.domain.review; + +import java.util.List; + +public record ReviewListResult( + List reviews, + List topKeywords, + Long totalCount, + int offset, + int limit) {} diff --git a/domain/src/main/java/org/depromeet/spot/domain/seat/Seat.java b/domain/src/main/java/org/depromeet/spot/domain/seat/Seat.java new file mode 100644 index 00000000..2e145604 --- /dev/null +++ b/domain/src/main/java/org/depromeet/spot/domain/seat/Seat.java @@ -0,0 +1,24 @@ +package org.depromeet.spot.domain.seat; + +import lombok.Getter; + +@Getter +public class Seat { + + private final Long id; + private final Long stadiumId; + private final Long sectionId; + private final Long blockId; + private final Long rowId; + private final Integer seatNumber; + + public Seat( + Long id, Long stadiumId, Long sectionId, Long blockId, Long rowId, Integer seatNumber) { + this.id = id; + this.stadiumId = stadiumId; + this.sectionId = sectionId; + this.blockId = blockId; + this.rowId = rowId; + this.seatNumber = seatNumber; + } +} diff --git a/domain/src/main/java/org/depromeet/spot/domain/section/Section.java b/domain/src/main/java/org/depromeet/spot/domain/section/Section.java new file mode 100644 index 00000000..99ba01a8 --- /dev/null +++ b/domain/src/main/java/org/depromeet/spot/domain/section/Section.java @@ -0,0 +1,25 @@ +package org.depromeet.spot.domain.section; + +import org.depromeet.spot.domain.common.RgbCode; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class Section { + + private final Long id; + private final Long stadiumId; + private final String name; + private final String alias; + private final RgbCode labelRgbCode; + + public Section(Long id, Long stadiumId, String name, String alias, RgbCode labelRgbCode) { + this.id = id; + this.stadiumId = stadiumId; + this.name = name; + this.alias = alias; + this.labelRgbCode = labelRgbCode; + } +} diff --git a/domain/src/main/java/org/depromeet/spot/domain/stadium/Stadium.java b/domain/src/main/java/org/depromeet/spot/domain/stadium/Stadium.java new file mode 100644 index 00000000..d8ed72b2 --- /dev/null +++ b/domain/src/main/java/org/depromeet/spot/domain/stadium/Stadium.java @@ -0,0 +1,18 @@ +package org.depromeet.spot.domain.stadium; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +@AllArgsConstructor +public class Stadium { + + private final Long id; + private final String name; + private final String mainImage; + private final String seatingChartImage; + private final String labeledSeatingChartImage; + private final boolean isActive; +} diff --git a/domain/src/main/java/org/depromeet/spot/domain/team/BaseballTeam.java b/domain/src/main/java/org/depromeet/spot/domain/team/BaseballTeam.java new file mode 100644 index 00000000..2b28d4a7 --- /dev/null +++ b/domain/src/main/java/org/depromeet/spot/domain/team/BaseballTeam.java @@ -0,0 +1,57 @@ +package org.depromeet.spot.domain.team; + +import org.depromeet.spot.common.exception.team.TeamException.EmptyTeamLogoException; +import org.depromeet.spot.common.exception.team.TeamException.InvalidBaseballAliasNameException; +import org.depromeet.spot.common.exception.team.TeamException.InvalidBaseballTeamNameException; +import org.depromeet.spot.domain.common.RgbCode; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class BaseballTeam { + + private final Long id; + private final String name; + private final String alias; + private final String logo; + private final RgbCode labelRgbCode; + + private static final int MAX_NAME_LENGTH = 20; + private static final int MAX_ALIAS_LENGTH = 10; + + public BaseballTeam(Long id, String name, String alias, String logo, RgbCode labelRgbCode) { + checkValidName(name); + checkValidAlias(alias); + checkValidLogo(logo); + + this.id = id; + this.name = name; + this.alias = alias; + this.logo = logo; + this.labelRgbCode = labelRgbCode; + } + + private void checkValidName(final String name) { + if (isNullOrBlank(name) || name.length() > MAX_NAME_LENGTH) { + throw new InvalidBaseballTeamNameException(); + } + } + + private void checkValidAlias(final String alias) { + if (isNullOrBlank(alias) || alias.length() > MAX_ALIAS_LENGTH) { + throw new InvalidBaseballAliasNameException(); + } + } + + private void checkValidLogo(final String logo) { + if (isNullOrBlank(logo)) { + throw new EmptyTeamLogoException(); + } + } + + private boolean isNullOrBlank(String str) { + return str == null || str.isBlank(); + } +} diff --git a/domain/src/main/java/org/depromeet/spot/domain/team/StadiumHomeTeam.java b/domain/src/main/java/org/depromeet/spot/domain/team/StadiumHomeTeam.java new file mode 100644 index 00000000..885de611 --- /dev/null +++ b/domain/src/main/java/org/depromeet/spot/domain/team/StadiumHomeTeam.java @@ -0,0 +1,13 @@ +package org.depromeet.spot.domain.team; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class StadiumHomeTeam { + + private final Long id; + private final Long stadiumId; + private final Long teamId; +} diff --git a/domain/src/test/java/org/depromeet/spot/domain/common/RgbCodeTest.java b/domain/src/test/java/org/depromeet/spot/domain/common/RgbCodeTest.java new file mode 100644 index 00000000..dee4aac7 --- /dev/null +++ b/domain/src/test/java/org/depromeet/spot/domain/common/RgbCodeTest.java @@ -0,0 +1,30 @@ +package org.depromeet.spot.domain.common; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +import java.util.stream.Stream; + +import org.depromeet.spot.common.exception.util.UtilException.InvalidRgbCodeException; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class RgbCodeTest { + + @ParameterizedTest + @MethodSource("provideRgbValues") + void rgb_범위_외의_값으로_RgbCode를_생성할_수_없다(Integer red, Integer green, Integer blue) { + // given + // when + // then + assertThatThrownBy(() -> new RgbCode(red, green, blue)) + .isInstanceOf(InvalidRgbCodeException.class); + } + + private static Stream provideRgbValues() { + return Stream.of( + Arguments.of(null, 255, 255), + Arguments.of(255, null, 255), + Arguments.of(255, 255, null)); + } +} diff --git a/domain/src/test/java/org/depromeet/spot/domain/team/BaseballTeamTest.java b/domain/src/test/java/org/depromeet/spot/domain/team/BaseballTeamTest.java new file mode 100644 index 00000000..076f4985 --- /dev/null +++ b/domain/src/test/java/org/depromeet/spot/domain/team/BaseballTeamTest.java @@ -0,0 +1,111 @@ +package org.depromeet.spot.domain.team; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +import org.depromeet.spot.common.exception.team.TeamException.EmptyTeamLogoException; +import org.depromeet.spot.common.exception.team.TeamException.InvalidBaseballAliasNameException; +import org.depromeet.spot.common.exception.team.TeamException.InvalidBaseballTeamNameException; +import org.depromeet.spot.domain.common.RgbCode; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +class BaseballTeamTest { + + @ParameterizedTest + @NullAndEmptySource + void name이_null_또는_blank일_때_BaseballTeam을_생성할_수_없다(String name) { + // given + RgbCode rgbCode = new RgbCode(100, 100, 100); + + // when + // then + assertAll( + () -> + assertThatThrownBy( + () -> new BaseballTeam(1L, name, "alias", "logo", rgbCode)) + .isInstanceOf(InvalidBaseballTeamNameException.class), + () -> + assertThatThrownBy( + () -> + BaseballTeam.builder() + .name(name) + .alias("alias") + .logo("logo") + .build()) + .isInstanceOf(InvalidBaseballTeamNameException.class)); + } + + @Test + void name_길이는_20글자를_초과할_수_없다() { + // given + final String name = "012345678901234567890"; + RgbCode rgbCode = new RgbCode(100, 100, 100); + + // when + // then + assertThatThrownBy(() -> new BaseballTeam(1L, name, "alias", "logo", rgbCode)) + .isInstanceOf(InvalidBaseballTeamNameException.class); + } + + @ParameterizedTest + @NullAndEmptySource + void alias가_null_또는_blank일_때_BaseballTeam을_생성할_수_없다(String alias) { + // given + RgbCode rgbCode = new RgbCode(100, 100, 100); + + // when + // then + assertAll( + () -> + assertThatThrownBy( + () -> new BaseballTeam(1L, "name", alias, "logo", rgbCode)) + .isInstanceOf(InvalidBaseballAliasNameException.class), + () -> + assertThatThrownBy( + () -> + BaseballTeam.builder() + .name("name") + .alias(alias) + .logo("logo") + .build()) + .isInstanceOf(InvalidBaseballAliasNameException.class)); + } + + @Test + void alias_길이는_10글자를_초과할_수_없다() { + // given + final String alias = "01234567890"; + RgbCode rgbCode = new RgbCode(100, 100, 100); + + // when + // then + assertThatThrownBy(() -> new BaseballTeam(1L, "name", alias, "logo", rgbCode)) + .isInstanceOf(InvalidBaseballAliasNameException.class); + } + + @ParameterizedTest + @NullAndEmptySource + void logo가_null_또는_blank일_때_BaseballTeam을_생성할_수_없다(String logo) { + // given + RgbCode rgbCode = new RgbCode(100, 100, 100); + + // when + // then + assertAll( + () -> + assertThatThrownBy( + () -> new BaseballTeam(1L, "name", "alias", logo, rgbCode)) + .isInstanceOf(EmptyTeamLogoException.class), + () -> + assertThatThrownBy( + () -> + BaseballTeam.builder() + .name("name") + .alias("alias") + .logo(logo) + .build()) + .isInstanceOf(EmptyTeamLogoException.class)); + } +} diff --git a/infrastructure/jpa/build.gradle.kts b/infrastructure/jpa/build.gradle.kts index c0ad6e3d..6643e45c 100644 --- a/infrastructure/jpa/build.gradle.kts +++ b/infrastructure/jpa/build.gradle.kts @@ -1,10 +1,12 @@ dependencies { + implementation(project(":common")) implementation(project(":domain")) implementation(project(":usecase")) // spring implementation("org.springframework.boot:spring-boot-starter-data-jpa:_") + // mysql runtimeOnly("com.mysql:mysql-connector-j") // queryDSL diff --git a/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/block/entity/BlockEntity.java b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/block/entity/BlockEntity.java new file mode 100644 index 00000000..18eafbf3 --- /dev/null +++ b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/block/entity/BlockEntity.java @@ -0,0 +1,39 @@ +package org.depromeet.spot.jpa.block.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; + +import org.depromeet.spot.domain.block.Block; +import org.depromeet.spot.jpa.common.entity.BaseEntity; + +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "blocks") +@NoArgsConstructor +@AllArgsConstructor +public class BlockEntity extends BaseEntity { + + @Column(name = "stadium_id", nullable = false) + private Long stadiumId; + + @Column(name = "section_id", nullable = false) + private Long sectionId; + + @Column(name = "code", nullable = false, length = 20) + private String code; + + @Column(name = "max_rows", nullable = false) + private Integer maxRows; + + public static BlockEntity from(Block block) { + return new BlockEntity( + block.getStadiumId(), block.getSectionId(), block.getCode(), block.getMaxRows()); + } + + public Block toDomain() { + return new Block(this.getId(), stadiumId, sectionId, code, maxRows); + } +} diff --git a/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/block/entity/BlockRowEntity.java b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/block/entity/BlockRowEntity.java new file mode 100644 index 00000000..e3d2f490 --- /dev/null +++ b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/block/entity/BlockRowEntity.java @@ -0,0 +1,35 @@ +package org.depromeet.spot.jpa.block.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; + +import org.depromeet.spot.domain.block.BlockRow; +import org.depromeet.spot.jpa.common.entity.BaseEntity; + +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "block_rows") +@NoArgsConstructor +@AllArgsConstructor +public class BlockRowEntity extends BaseEntity { + @Column(name = "block_id", nullable = false) + private Long blockId; + + @Column(name = "number", nullable = false) + private Long number; + + @Column(name = "max_seats", nullable = false) + private Long maxSeats; + + public static BlockRowEntity from(BlockRow blockRow) { + return new BlockRowEntity( + blockRow.getBlockId(), blockRow.getNumber(), blockRow.getMaxSeats()); + } + + public BlockRow toDomain() { + return new BlockRow(this.getId(), blockId, number, maxSeats); + } +} diff --git a/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/block/entity/BlockTopKeywordEntity.java b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/block/entity/BlockTopKeywordEntity.java new file mode 100644 index 00000000..0bbbb284 --- /dev/null +++ b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/block/entity/BlockTopKeywordEntity.java @@ -0,0 +1,43 @@ +package org.depromeet.spot.jpa.block.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; + +import org.depromeet.spot.domain.block.BlockTopKeyword; +import org.depromeet.spot.jpa.common.entity.BaseEntity; + +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "block_top_keywords") +@NoArgsConstructor +@AllArgsConstructor +public class BlockTopKeywordEntity extends BaseEntity { + + @Column(name = "block_id", nullable = false) + private Long blockId; + + @Column(name = "keyword_id", nullable = false) + private Long keywordId; + + @Column(name = "count", nullable = false) + private Long count; + + public static BlockTopKeywordEntity from(BlockTopKeyword blockTopKeyword) { + return new BlockTopKeywordEntity( + blockTopKeyword.getBlockId(), + blockTopKeyword.getKeywordId(), + blockTopKeyword.getCount()); + } + + public BlockTopKeyword toDomain() { + return BlockTopKeyword.builder() + .id(this.getId()) + .blockId(blockId) + .keywordId(keywordId) + .count(count) + .build(); + } +} diff --git a/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/block/repository/BlockJpaRepository.java b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/block/repository/BlockJpaRepository.java new file mode 100644 index 00000000..8e7aed5e --- /dev/null +++ b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/block/repository/BlockJpaRepository.java @@ -0,0 +1,11 @@ +package org.depromeet.spot.jpa.block.repository; + +import java.util.List; + +import org.depromeet.spot.jpa.block.entity.BlockEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface BlockJpaRepository extends JpaRepository { + + List findAllBySectionId(Long sectionId); +} diff --git a/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/block/repository/BlockRepositoryImpl.java b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/block/repository/BlockRepositoryImpl.java new file mode 100644 index 00000000..764abf62 --- /dev/null +++ b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/block/repository/BlockRepositoryImpl.java @@ -0,0 +1,23 @@ +package org.depromeet.spot.jpa.block.repository; + +import java.util.List; + +import org.depromeet.spot.domain.block.Block; +import org.depromeet.spot.jpa.block.entity.BlockEntity; +import org.depromeet.spot.usecase.port.out.block.BlockRepository; +import org.springframework.stereotype.Repository; + +import lombok.RequiredArgsConstructor; + +@Repository +@RequiredArgsConstructor +public class BlockRepositoryImpl implements BlockRepository { + + private final BlockJpaRepository blockJpaRepository; + + @Override + public List findAllBySection(final Long sectionId) { + List entities = blockJpaRepository.findAllBySectionId(sectionId); + return entities.stream().map(BlockEntity::toDomain).toList(); + } +} diff --git a/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/common/entity/BaseEntity.java b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/common/entity/BaseEntity.java new file mode 100644 index 00000000..7ab09ace --- /dev/null +++ b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/common/entity/BaseEntity.java @@ -0,0 +1,64 @@ +package org.depromeet.spot.jpa.common.entity; + +import java.time.LocalDateTime; + +import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.MappedSuperclass; +import jakarta.persistence.PrePersist; +import jakarta.persistence.PreUpdate; + +import org.hibernate.annotations.Where; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@Getter +@SuperBuilder +@MappedSuperclass +@AllArgsConstructor +@Where(clause = "deleted_at is null") +@EntityListeners(AuditingEntityListener.class) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public abstract class BaseEntity { + + @Id + @Column(name = "id", nullable = false) + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(updatable = false, name = "created_at") + private LocalDateTime createdAt; + + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + @Column(name = "deleted_at") + private LocalDateTime deletedAt; + + @PrePersist + public void prePersist() { + this.createdAt = LocalDateTime.now(); + this.updatedAt = LocalDateTime.now(); + } + + @PreUpdate + public void preUpdate() { + this.updatedAt = LocalDateTime.now(); + } + + public boolean isDeleted() { + return null != this.deletedAt; + } + + public void delete() { + this.deletedAt = LocalDateTime.now(); + } +} diff --git a/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/config/QueryDslConfig.java b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/config/QueryDslConfig.java index 5b64ca95..974753fa 100644 --- a/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/config/QueryDslConfig.java +++ b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/config/QueryDslConfig.java @@ -6,6 +6,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import com.querydsl.jpa.JPQLTemplates; import com.querydsl.jpa.impl.JPAQueryFactory; @Configuration @@ -15,6 +16,6 @@ public class QueryDslConfig { @Bean public JPAQueryFactory queryFactory() { - return new JPAQueryFactory(entityManager); + return new JPAQueryFactory(JPQLTemplates.DEFAULT, entityManager); } } diff --git a/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/member/entity/MemberEntity.java b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/member/entity/MemberEntity.java index f8d00adf..fd59477a 100644 --- a/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/member/entity/MemberEntity.java +++ b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/member/entity/MemberEntity.java @@ -1,41 +1,79 @@ package org.depromeet.spot.jpa.member.entity; -/* JPA 설정 확인용 샘플 엔티티. 실제 피처 개발 시작할 때 삭제 예정! */ - import jakarta.persistence.Column; import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; import jakarta.persistence.Table; import org.depromeet.spot.domain.member.Member; +import org.depromeet.spot.jpa.common.entity.BaseEntity; +import lombok.AllArgsConstructor; import lombok.NoArgsConstructor; @Entity -@Table(name = "member") +@Table(name = "members") @NoArgsConstructor -public class MemberEntity { +@AllArgsConstructor +public class MemberEntity extends BaseEntity { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "id", nullable = false) - private Long id; + @Column(name = "email", nullable = false, unique = true, length = 50) + private String email; - @Column(name = "name", nullable = false) + @Column(name = "name", nullable = false, length = 20) private String name; - public MemberEntity(Long id, String name) { - this.id = id; - this.name = name; - } + @Column(name = "nickname", nullable = false, unique = true, length = 10) + private String nickname; + + @Column(name = "phone_number", nullable = false, unique = true, length = 13) + private String phoneNumber; + + @Column(name = "level", nullable = false) + private Integer level; + + @Column(name = "profile_image", length = 255) + private String profileImage; + + @Column(name = "sns_provider", nullable = false, length = 20) + private String snsProvider; + + @Column(name = "id_token", nullable = false, unique = true, length = 255) + private String idToken; + + @Column(name = "my_team", nullable = false, length = 10) + private String myTeam; + + @Column(name = "role", nullable = false) + private Integer role; public static MemberEntity from(Member member) { - return new MemberEntity(member.getId(), member.getName()); + return new MemberEntity( + member.getEmail(), + member.getName(), + member.getNickname(), + member.getPhoneNumber(), + member.getLevel(), + member.getProfileImage(), + member.getSnsProvider(), + member.getIdToken(), + member.getMyTeam(), + member.getRole()); } public Member toDomain() { - return new Member(id, name); + return new Member( + this.getId(), + email, + name, + nickname, + phoneNumber, + level, + profileImage, + snsProvider, + idToken, + myTeam, + role, + this.getCreatedAt(), + this.getDeletedAt()); } } diff --git a/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/review/entity/KeywordEntity.java b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/review/entity/KeywordEntity.java new file mode 100644 index 00000000..be5220eb --- /dev/null +++ b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/review/entity/KeywordEntity.java @@ -0,0 +1,29 @@ +package org.depromeet.spot.jpa.review.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; + +import org.depromeet.spot.domain.review.Keyword; +import org.depromeet.spot.jpa.common.entity.BaseEntity; + +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "keywords") +@NoArgsConstructor +@AllArgsConstructor +public class KeywordEntity extends BaseEntity { + + @Column(name = "content", nullable = false, length = 50) + private String content; + + public static KeywordEntity from(Keyword keyword) { + return new KeywordEntity(keyword.getContent()); + } + + public Keyword toDomain() { + return Keyword.builder().id(this.getId()).content(content).build(); + } +} diff --git a/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/review/entity/ReviewEntity.java b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/review/entity/ReviewEntity.java new file mode 100644 index 00000000..ea81c209 --- /dev/null +++ b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/review/entity/ReviewEntity.java @@ -0,0 +1,103 @@ +package org.depromeet.spot.jpa.review.entity; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; + +import org.depromeet.spot.domain.review.Review; +import org.depromeet.spot.jpa.common.entity.BaseEntity; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "reviews") +@NoArgsConstructor +@AllArgsConstructor +@Getter +public class ReviewEntity extends BaseEntity { + + @Column(name = "user_id", nullable = false) + private Long userId; + + @Column(name = "stadium_id", nullable = false) + private Long stadiumId; + + @Column(name = "block_id", nullable = false) + private Long blockId; + + @Column(name = "row_id", nullable = false) + private Long rowId; + + @Column(name = "seat_number", nullable = false) + private Long seatNumber; + + @Column(name = "date_time", nullable = false) + private LocalDateTime dateTime; + + @Column(name = "content", length = 300) + private String content; + + @Column(name = "deleted_at") + private LocalDateTime deletedAt; + + public static Review createReviewWithDetails( + ReviewEntity entity, + List images, + List keywords) { + return Review.builder() + .id(entity.getId()) + .userId(entity.getUserId()) + .stadiumId(entity.getStadiumId()) + .blockId(entity.getBlockId()) + .rowId(entity.getRowId()) + .seatNumber(entity.getSeatNumber()) + .dateTime(entity.getDateTime()) + .content(entity.getContent()) + .createdAt(entity.getCreatedAt()) + .updatedAt(entity.getUpdatedAt()) + .deletedAt(entity.getDeletedAt()) + .images( + images.stream() + .map(ReviewImageEntity::toDomain) + .collect(Collectors.toList())) + .keywords( + keywords.stream() + .map(ReviewKeywordEntity::toDomain) + .collect(Collectors.toList())) + .build(); + } + + public static ReviewEntity from(Review review) { + return new ReviewEntity( + review.getUserId(), + review.getStadiumId(), + review.getBlockId(), + review.getRowId(), + review.getSeatNumber(), + review.getDateTime(), + review.getContent(), + review.getDeletedAt()); + } + + public Review toDomain() { + return Review.builder() + .id(this.getId()) + .userId(userId) + .stadiumId(stadiumId) + .blockId(blockId) + .rowId(rowId) + .seatNumber(seatNumber) + .dateTime(dateTime) + .content(content) + .createdAt(this.getCreatedAt()) + .updatedAt(this.getUpdatedAt()) + .deletedAt(deletedAt) + .build(); + } +} diff --git a/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/review/entity/ReviewImageEntity.java b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/review/entity/ReviewImageEntity.java new file mode 100644 index 00000000..30789b82 --- /dev/null +++ b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/review/entity/ReviewImageEntity.java @@ -0,0 +1,44 @@ +package org.depromeet.spot.jpa.review.entity; + +import java.time.LocalDateTime; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; + +import org.depromeet.spot.domain.review.ReviewImage; +import org.depromeet.spot.jpa.common.entity.BaseEntity; + +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "review_images") +@NoArgsConstructor +@AllArgsConstructor +public class ReviewImageEntity extends BaseEntity { + + @Column(name = "review_id", nullable = false) + private Long reviewId; + + @Column(name = "url", nullable = false, length = 255) + private String url; + + @Column(name = "deleted_at") + private LocalDateTime deletedAt; + + public static ReviewImageEntity from(ReviewImage reviewImage) { + return new ReviewImageEntity( + reviewImage.getReviewId(), reviewImage.getUrl(), reviewImage.getDeletedAt()); + } + + public ReviewImage toDomain() { + return ReviewImage.builder() + .id(this.getId()) + .reviewId(reviewId) + .url(url) + .createdAt(this.getCreatedAt()) + .deletedAt(deletedAt) + .build(); + } +} diff --git a/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/review/entity/ReviewKeywordEntity.java b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/review/entity/ReviewKeywordEntity.java new file mode 100644 index 00000000..d97b5d5e --- /dev/null +++ b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/review/entity/ReviewKeywordEntity.java @@ -0,0 +1,43 @@ +package org.depromeet.spot.jpa.review.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; + +import org.depromeet.spot.domain.review.ReviewKeyword; +import org.depromeet.spot.jpa.common.entity.BaseEntity; + +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "review_keywords") +@NoArgsConstructor +@AllArgsConstructor +public class ReviewKeywordEntity extends BaseEntity { + + @Column(name = "review_id", nullable = false) + private Long reviewId; + + @Column(name = "keyword_id", nullable = false) + private Long keywordId; + + @Column(name = "is_positive", nullable = false) + private Boolean isPositive; + + public static ReviewKeywordEntity from(ReviewKeyword reviewKeyword) { + return new ReviewKeywordEntity( + reviewKeyword.getReviewId(), + reviewKeyword.getKeywordId(), + reviewKeyword.getIsPositive()); + } + + public ReviewKeyword toDomain() { + return ReviewKeyword.builder() + .id(this.getId()) + .reviewId(reviewId) + .keywordId(keywordId) + .isPositive(isPositive) + .build(); + } +} diff --git a/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/review/repository/ReviewCustomRepository.java b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/review/repository/ReviewCustomRepository.java new file mode 100644 index 00000000..66750121 --- /dev/null +++ b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/review/repository/ReviewCustomRepository.java @@ -0,0 +1,107 @@ +package org.depromeet.spot.jpa.review.repository; + +import static org.depromeet.spot.jpa.review.entity.QKeywordEntity.keywordEntity; +import static org.depromeet.spot.jpa.review.entity.QReviewEntity.reviewEntity; +import static org.depromeet.spot.jpa.review.entity.QReviewImageEntity.reviewImageEntity; +import static org.depromeet.spot.jpa.review.entity.QReviewKeywordEntity.reviewKeywordEntity; + +import java.util.List; + +import org.depromeet.spot.domain.review.KeywordCount; +import org.depromeet.spot.jpa.review.entity.ReviewEntity; +import org.depromeet.spot.jpa.review.entity.ReviewImageEntity; +import org.depromeet.spot.jpa.review.entity.ReviewKeywordEntity; +import org.springframework.stereotype.Repository; + +import com.querydsl.core.types.Projections; +import com.querydsl.jpa.impl.JPAQueryFactory; + +import lombok.RequiredArgsConstructor; + +@Repository +@RequiredArgsConstructor +public class ReviewCustomRepository { + + private final JPAQueryFactory queryFactory; + + public List findByBlockIdWithFilters( + Long stadiumId, Long blockId, Long rowId, Long seatNumber, int offset, int limit) { + return queryFactory + .selectFrom(reviewEntity) + .where( + reviewEntity + .stadiumId + .eq(stadiumId) + .and(reviewEntity.blockId.eq(blockId)) + .and(rowId != null ? reviewEntity.rowId.eq(rowId) : null) + .and( + seatNumber != null + ? reviewEntity.seatNumber.eq(seatNumber) + : null) + .and(reviewEntity.deletedAt.isNull())) + .orderBy(reviewEntity.createdAt.desc()) + .offset(offset) + .limit(limit) + .fetch(); + } + + public long countByBlockIdWithFilters( + Long stadiumId, Long blockId, Long rowId, Long seatNumber) { + return queryFactory + .selectFrom(reviewEntity) + .where( + reviewEntity + .stadiumId + .eq(stadiumId) + .and(reviewEntity.blockId.eq(blockId)) + .and(rowId != null ? reviewEntity.rowId.eq(rowId) : null) + .and( + seatNumber != null + ? reviewEntity.seatNumber.eq(seatNumber) + : null) + .and(reviewEntity.deletedAt.isNull())) + .fetchCount(); + } + + public List findTopKeywordsByBlockId(Long stadiumId, Long blockId, int limit) { + return queryFactory + .select( + Projections.constructor( + KeywordCount.class, + keywordEntity.content, + reviewKeywordEntity.count().as("count"))) + .from(reviewKeywordEntity) + .join(keywordEntity) + .on(reviewKeywordEntity.keywordId.eq(keywordEntity.id)) + .join(reviewEntity) + .on(reviewKeywordEntity.reviewId.eq(reviewEntity.id)) + .where( + reviewEntity + .stadiumId + .eq(stadiumId) + .and(reviewEntity.blockId.eq(blockId)) + .and(reviewEntity.deletedAt.isNull())) + .groupBy(keywordEntity.id, keywordEntity.content) + .orderBy(reviewKeywordEntity.count().desc()) + .limit(limit) + .fetch(); + } + + public List findImagesByReviewIds(List reviewIds) { + return queryFactory + .selectFrom(reviewImageEntity) + .where( + reviewImageEntity + .reviewId + .in(reviewIds) + .and(reviewImageEntity.deletedAt.isNull())) + .fetch(); + } + + public List findKeywordsByReviewIds(List reviewIds) { + return queryFactory + .selectFrom(reviewKeywordEntity) + .where(reviewKeywordEntity.reviewId.in(reviewIds)) + .fetch(); + } +} diff --git a/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/review/repository/ReviewRepositoryImpl.java b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/review/repository/ReviewRepositoryImpl.java new file mode 100644 index 00000000..5759ad88 --- /dev/null +++ b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/review/repository/ReviewRepositoryImpl.java @@ -0,0 +1,49 @@ +package org.depromeet.spot.jpa.review.repository; + +import java.util.List; +import java.util.stream.Collectors; + +import org.depromeet.spot.domain.review.KeywordCount; +import org.depromeet.spot.domain.review.Review; +import org.depromeet.spot.jpa.review.entity.ReviewEntity; +import org.depromeet.spot.jpa.review.entity.ReviewImageEntity; +import org.depromeet.spot.jpa.review.entity.ReviewKeywordEntity; +import org.depromeet.spot.usecase.port.out.review.ReviewRepository; +import org.springframework.stereotype.Repository; + +import lombok.RequiredArgsConstructor; + +@Repository +@RequiredArgsConstructor +public class ReviewRepositoryImpl implements ReviewRepository { + private final ReviewCustomRepository reviewCustomRepository; + + @Override + public List findByBlockId( + Long stadiumId, Long blockId, Long rowId, Long seatNumber, int offset, int limit) { + List reviews = + reviewCustomRepository.findByBlockIdWithFilters( + stadiumId, blockId, rowId, seatNumber, offset, limit); + return reviews.stream().map(this::fetchReviewDetails).collect(Collectors.toList()); + } + + @Override + public Long countByBlockId(Long stadiumId, Long blockId, Long rowId, Long seatNumber) { + return reviewCustomRepository.countByBlockIdWithFilters( + stadiumId, blockId, rowId, seatNumber); + } + + @Override + public List findTopKeywordsByBlockId(Long stadiumId, Long blockId, int limit) { + return reviewCustomRepository.findTopKeywordsByBlockId(stadiumId, blockId, limit); + } + + private Review fetchReviewDetails(ReviewEntity reviewEntity) { + List images = + reviewCustomRepository.findImagesByReviewIds(List.of(reviewEntity.getId())); + List keywords = + reviewCustomRepository.findKeywordsByReviewIds(List.of(reviewEntity.getId())); + + return ReviewEntity.createReviewWithDetails(reviewEntity, images, keywords); + } +} diff --git a/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/seat/entity/SeatEntity.java b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/seat/entity/SeatEntity.java new file mode 100644 index 00000000..0f9cea16 --- /dev/null +++ b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/seat/entity/SeatEntity.java @@ -0,0 +1,46 @@ +package org.depromeet.spot.jpa.seat.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; + +import org.depromeet.spot.domain.seat.Seat; +import org.depromeet.spot.jpa.common.entity.BaseEntity; + +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "seats") +@NoArgsConstructor +@AllArgsConstructor +public class SeatEntity extends BaseEntity { + + @Column(name = "stadium_id", nullable = false) + private Long stadiumId; + + @Column(name = "section_id", nullable = false) + private Long sectionId; + + @Column(name = "block_id", nullable = false) + private Long blockId; + + @Column(name = "row_id", nullable = false) + private Long rowId; + + @Column(name = "seat_number", nullable = false) + private Integer seatNumber; + + public static SeatEntity from(Seat seat) { + return new SeatEntity( + seat.getStadiumId(), + seat.getSectionId(), + seat.getBlockId(), + seat.getRowId(), + seat.getSeatNumber()); + } + + public Seat toDomain() { + return new Seat(this.getId(), stadiumId, sectionId, blockId, rowId, seatNumber); + } +} diff --git a/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/section/entity/SectionEntity.java b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/section/entity/SectionEntity.java new file mode 100644 index 00000000..eba0428c --- /dev/null +++ b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/section/entity/SectionEntity.java @@ -0,0 +1,53 @@ +package org.depromeet.spot.jpa.section.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; + +import org.depromeet.spot.domain.common.RgbCode; +import org.depromeet.spot.domain.section.Section; +import org.depromeet.spot.jpa.common.entity.BaseEntity; + +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "sections") +@NoArgsConstructor +@AllArgsConstructor +public class SectionEntity extends BaseEntity { + + @Column(name = "stadium_id", nullable = false) + private Long stadiumId; + + @Column(name = "name", nullable = false, length = 20) + private String name; + + @Column(name = "alias", length = 20) + private String alias; + + @Column(name = "red") + private Integer red; + + @Column(name = "green") + private Integer green; + + @Column(name = "blue") + private Integer blue; + + public static SectionEntity from(Section section) { + RgbCode labelRgbCode = section.getLabelRgbCode(); + return new SectionEntity( + section.getStadiumId(), + section.getName(), + section.getAlias(), + labelRgbCode.getRed(), + labelRgbCode.getGreen(), + labelRgbCode.getBlue()); + } + + public Section toDomain() { + RgbCode rgbCode = RgbCode.builder().red(red).green(green).blue(blue).build(); + return new Section(this.getId(), stadiumId, name, alias, rgbCode); + } +} diff --git a/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/section/repository/SectionJpaRepository.java b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/section/repository/SectionJpaRepository.java new file mode 100644 index 00000000..96cce20e --- /dev/null +++ b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/section/repository/SectionJpaRepository.java @@ -0,0 +1,13 @@ +package org.depromeet.spot.jpa.section.repository; + +import java.util.List; + +import org.depromeet.spot.jpa.section.entity.SectionEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface SectionJpaRepository extends JpaRepository { + + List findAllByStadiumId(Long stadiumId); + + boolean existsByStadiumIdAndId(Long stadiumId, Long id); +} diff --git a/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/section/repository/SectionRepositoryImpl.java b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/section/repository/SectionRepositoryImpl.java new file mode 100644 index 00000000..97c20a0b --- /dev/null +++ b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/section/repository/SectionRepositoryImpl.java @@ -0,0 +1,34 @@ +package org.depromeet.spot.jpa.section.repository; + +import java.util.List; + +import org.depromeet.spot.domain.section.Section; +import org.depromeet.spot.jpa.section.entity.SectionEntity; +import org.depromeet.spot.usecase.port.out.section.SectionRepository; +import org.springframework.stereotype.Repository; + +import lombok.RequiredArgsConstructor; + +@Repository +@RequiredArgsConstructor +public class SectionRepositoryImpl implements SectionRepository { + + private final SectionJpaRepository sectionJpaRepository; + + @Override + public List
findAllByStadium(final Long stadiumId) { + List entities = sectionJpaRepository.findAllByStadiumId(stadiumId); + return entities.stream().map(SectionEntity::toDomain).toList(); + } + + @Override + public Section save(Section section) { + // TODO: test를 위해 추가 -> 구역 생성 티켓 작업할 때 구현 예정 + return null; + } + + @Override + public boolean existsInStadium(Long stadiumId, Long sectionId) { + return sectionJpaRepository.existsByStadiumIdAndId(stadiumId, sectionId); + } +} diff --git a/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/stadium/entity/StadiumEntity.java b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/stadium/entity/StadiumEntity.java new file mode 100644 index 00000000..1041d101 --- /dev/null +++ b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/stadium/entity/StadiumEntity.java @@ -0,0 +1,53 @@ +package org.depromeet.spot.jpa.stadium.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; + +import org.depromeet.spot.domain.stadium.Stadium; +import org.depromeet.spot.jpa.common.entity.BaseEntity; + +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "stadiums") +@NoArgsConstructor +@AllArgsConstructor +public class StadiumEntity extends BaseEntity { + + @Column(name = "name", nullable = false, length = 50) + private String name; + + @Column(name = "main_image", length = 255) + private String mainImage; + + @Column(name = "seating_chart_image", length = 255) + private String seatingChartImage; + + @Column(name = "labeled_seating_chart_image", length = 255) + private String labeledSeatingChartImage; + + @Column(name = "is_active", nullable = false) + private boolean isActive; + + public static StadiumEntity from(Stadium stadium) { + return new StadiumEntity( + stadium.getName(), + stadium.getMainImage(), + stadium.getSeatingChartImage(), + stadium.getLabeledSeatingChartImage(), + stadium.isActive()); + } + + public Stadium toDomain() { + return Stadium.builder() + .id(this.getId()) + .name(name) + .mainImage(mainImage) + .seatingChartImage(seatingChartImage) + .labeledSeatingChartImage(labeledSeatingChartImage) + .isActive(isActive) + .build(); + } +} diff --git a/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/stadium/repository/StadiumJpaRepository.java b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/stadium/repository/StadiumJpaRepository.java new file mode 100644 index 00000000..6caacf4a --- /dev/null +++ b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/stadium/repository/StadiumJpaRepository.java @@ -0,0 +1,6 @@ +package org.depromeet.spot.jpa.stadium.repository; + +import org.depromeet.spot.jpa.stadium.entity.StadiumEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface StadiumJpaRepository extends JpaRepository {} diff --git a/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/stadium/repository/StadiumRepositoryImpl.java b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/stadium/repository/StadiumRepositoryImpl.java new file mode 100644 index 00000000..a3f896ee --- /dev/null +++ b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/stadium/repository/StadiumRepositoryImpl.java @@ -0,0 +1,43 @@ +package org.depromeet.spot.jpa.stadium.repository; + +import java.util.List; + +import org.depromeet.spot.common.exception.stadium.StadiumException.StadiumNotFoundException; +import org.depromeet.spot.domain.stadium.Stadium; +import org.depromeet.spot.jpa.stadium.entity.StadiumEntity; +import org.depromeet.spot.usecase.port.out.stadium.StadiumRepository; +import org.springframework.stereotype.Repository; + +import lombok.RequiredArgsConstructor; + +@Repository +@RequiredArgsConstructor +public class StadiumRepositoryImpl implements StadiumRepository { + + private final StadiumJpaRepository stadiumJpaRepository; + + @Override + public Stadium findById(final Long id) { + return stadiumJpaRepository + .findById(id) + .orElseThrow(() -> new StadiumNotFoundException(id + "의 경기장은 존재하지 않습니다.")) + .toDomain(); + } + + @Override + public List findAll() { + List entities = stadiumJpaRepository.findAll(); + return entities.stream().map(StadiumEntity::toDomain).toList(); + } + + @Override + public Stadium save(Stadium stadium) { + // TODO: test를 위해 추가 -> 구장 저장 API 티켓때 구현 예정 + return null; + } + + @Override + public boolean existsById(final Long id) { + return stadiumJpaRepository.existsById(id); + } +} diff --git a/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/team/entity/BaseballTeamEntity.java b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/team/entity/BaseballTeamEntity.java new file mode 100644 index 00000000..de8d4723 --- /dev/null +++ b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/team/entity/BaseballTeamEntity.java @@ -0,0 +1,53 @@ +package org.depromeet.spot.jpa.team.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; + +import org.depromeet.spot.domain.common.RgbCode; +import org.depromeet.spot.domain.team.BaseballTeam; +import org.depromeet.spot.jpa.common.entity.BaseEntity; + +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "baseball_teams") +@NoArgsConstructor +@AllArgsConstructor +public class BaseballTeamEntity extends BaseEntity { + + @Column(name = "name", nullable = false, length = 20) + private String name; + + @Column(name = "alias", nullable = false, length = 10) + private String alias; + + @Column(name = "logo", nullable = false, length = 255) + private String logo; + + @Column(name = "red") + private Integer red; + + @Column(name = "green") + private Integer green; + + @Column(name = "blue") + private Integer blue; + + public static BaseballTeamEntity from(BaseballTeam baseballTeam) { + RgbCode labelRgbCode = baseballTeam.getLabelRgbCode(); + return new BaseballTeamEntity( + baseballTeam.getName(), + baseballTeam.getAlias(), + baseballTeam.getLogo(), + labelRgbCode.getRed(), + labelRgbCode.getGreen(), + labelRgbCode.getBlue()); + } + + public BaseballTeam toDomain() { + RgbCode labelRgbCode = RgbCode.builder().red(red).green(green).blue(blue).build(); + return new BaseballTeam(this.getId(), name, alias, logo, labelRgbCode); + } +} diff --git a/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/team/entity/StadiumHomeTeamEntity.java b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/team/entity/StadiumHomeTeamEntity.java new file mode 100644 index 00000000..a98fee4e --- /dev/null +++ b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/team/entity/StadiumHomeTeamEntity.java @@ -0,0 +1,33 @@ +package org.depromeet.spot.jpa.team.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; + +import org.depromeet.spot.domain.team.StadiumHomeTeam; +import org.depromeet.spot.jpa.common.entity.BaseEntity; + +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "stadium_home_teams") +@NoArgsConstructor +@AllArgsConstructor +public class StadiumHomeTeamEntity extends BaseEntity { + + @Column(name = "stadium_id", nullable = false) + private Long stadiumId; + + @Column(name = "team_id", nullable = false) + private Long teamId; + + public static StadiumHomeTeamEntity from(StadiumHomeTeam stadiumHomeTeam) { + return new StadiumHomeTeamEntity( + stadiumHomeTeam.getStadiumId(), stadiumHomeTeam.getTeamId()); + } + + public StadiumHomeTeam toDomain() { + return new StadiumHomeTeam(this.getId(), stadiumId, teamId); + } +} diff --git a/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/team/repository/BaseballTeamJdbcRepository.java b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/team/repository/BaseballTeamJdbcRepository.java new file mode 100644 index 00000000..a54ecb84 --- /dev/null +++ b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/team/repository/BaseballTeamJdbcRepository.java @@ -0,0 +1,41 @@ +package org.depromeet.spot.jpa.team.repository; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.List; + +import org.depromeet.spot.domain.team.BaseballTeam; +import org.springframework.jdbc.core.BatchPreparedStatementSetter; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; + +import lombok.RequiredArgsConstructor; + +@Repository +@RequiredArgsConstructor +public class BaseballTeamJdbcRepository { + + private final JdbcTemplate jdbcTemplate; + + public void createBaseballTeams(List teams) { + jdbcTemplate.batchUpdate( + "insert into baseball_teams" + + "(name, alias, logo, red, green, blue) values (?, ?, ?, ?, ?, ?)", + new BatchPreparedStatementSetter() { + @Override + public void setValues(PreparedStatement ps, int i) throws SQLException { + ps.setString(1, teams.get(i).getName()); + ps.setString(2, teams.get(i).getAlias()); + ps.setString(3, teams.get(i).getLogo()); + ps.setInt(4, teams.get(i).getLabelRgbCode().getRed()); + ps.setInt(5, teams.get(i).getLabelRgbCode().getGreen()); + ps.setInt(6, teams.get(i).getLabelRgbCode().getBlue()); + } + + @Override + public int getBatchSize() { + return teams.size(); + } + }); + } +} diff --git a/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/team/repository/BaseballTeamJpaRepository.java b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/team/repository/BaseballTeamJpaRepository.java new file mode 100644 index 00000000..242c3837 --- /dev/null +++ b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/team/repository/BaseballTeamJpaRepository.java @@ -0,0 +1,10 @@ +package org.depromeet.spot.jpa.team.repository; + +import java.util.List; + +import org.depromeet.spot.jpa.team.entity.BaseballTeamEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface BaseballTeamJpaRepository extends JpaRepository { + boolean existsByNameIn(List names); +} diff --git a/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/team/repository/BaseballTeamRepositoryImpl.java b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/team/repository/BaseballTeamRepositoryImpl.java new file mode 100644 index 00000000..13cb6db4 --- /dev/null +++ b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/team/repository/BaseballTeamRepositoryImpl.java @@ -0,0 +1,75 @@ +package org.depromeet.spot.jpa.team.repository; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.depromeet.spot.common.exception.team.TeamException.BaseballTeamNotFoundException; +import org.depromeet.spot.domain.stadium.Stadium; +import org.depromeet.spot.domain.team.BaseballTeam; +import org.depromeet.spot.jpa.stadium.entity.StadiumEntity; +import org.depromeet.spot.jpa.team.entity.BaseballTeamEntity; +import org.depromeet.spot.usecase.port.out.team.BaseballTeamRepository; +import org.springframework.stereotype.Repository; + +import lombok.RequiredArgsConstructor; + +@Repository +@RequiredArgsConstructor +public class BaseballTeamRepositoryImpl implements BaseballTeamRepository { + + private final StadiumHomeTeamCustomRepository stadiumHomeTeamCustomRepository; + private final BaseballTeamJpaRepository baseballTeamJpaRepository; + private final BaseballTeamJdbcRepository baseballTeamJdbcRepository; + + @Override + public BaseballTeam findById(final Long id) { + BaseballTeamEntity entity = + baseballTeamJpaRepository + .findById(id) + .orElseThrow(BaseballTeamNotFoundException::new); + return entity.toDomain(); + } + + @Override + public List findAll() { + List entities = baseballTeamJpaRepository.findAll(); + return entities.stream().map(BaseballTeamEntity::toDomain).toList(); + } + + @Override + public List findAllHomeTeamByStadium(final Long stadiumId) { + List homeTeamEntities = + stadiumHomeTeamCustomRepository.findAllHomeTeamByStadium(stadiumId); + return homeTeamEntities.stream().map(BaseballTeamEntity::toDomain).toList(); + } + + @Override + public Map> findAllStadiumHomeTeam() { + Map> stadiumHomeTeamMap = + stadiumHomeTeamCustomRepository.findAllStadiumHomeTeam(); + return stadiumHomeTeamMap.entrySet().stream() + .collect( + Collectors.toMap( + entry -> entry.getKey().toDomain(), + entry -> + entry.getValue().stream() + .map(BaseballTeamEntity::toDomain) + .toList())); + } + + @Override + public void saveAll(List teams) { + baseballTeamJdbcRepository.createBaseballTeams(teams); + } + + @Override + public void createHomeTeam(Long stadiumId, List teamIds) { + // TODO: 홈 팀 등록할 때 구현 예정 + } + + @Override + public boolean existsByNameIn(List names) { + return baseballTeamJpaRepository.existsByNameIn(names); + } +} diff --git a/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/team/repository/StadiumHomeTeamCustomRepository.java b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/team/repository/StadiumHomeTeamCustomRepository.java new file mode 100644 index 00000000..de841ed5 --- /dev/null +++ b/infrastructure/jpa/src/main/java/org/depromeet/spot/jpa/team/repository/StadiumHomeTeamCustomRepository.java @@ -0,0 +1,45 @@ +package org.depromeet.spot.jpa.team.repository; + +import static com.querydsl.core.group.GroupBy.groupBy; +import static com.querydsl.core.group.GroupBy.list; +import static org.depromeet.spot.jpa.stadium.entity.QStadiumEntity.stadiumEntity; +import static org.depromeet.spot.jpa.team.entity.QBaseballTeamEntity.baseballTeamEntity; +import static org.depromeet.spot.jpa.team.entity.QStadiumHomeTeamEntity.stadiumHomeTeamEntity; + +import java.util.List; +import java.util.Map; + +import org.depromeet.spot.jpa.stadium.entity.StadiumEntity; +import org.depromeet.spot.jpa.team.entity.BaseballTeamEntity; +import org.springframework.stereotype.Repository; + +import com.querydsl.jpa.impl.JPAQueryFactory; + +import lombok.RequiredArgsConstructor; + +@Repository +@RequiredArgsConstructor +public class StadiumHomeTeamCustomRepository { + + private final JPAQueryFactory queryFactory; + + public List findAllHomeTeamByStadium(final Long stadiumId) { + return queryFactory + .select(baseballTeamEntity) + .from(stadiumHomeTeamEntity) + .join(baseballTeamEntity) + .on(stadiumHomeTeamEntity.teamId.eq(baseballTeamEntity.id)) + .where(stadiumHomeTeamEntity.stadiumId.eq(stadiumId)) + .fetch(); + } + + public Map> findAllStadiumHomeTeam() { + return queryFactory + .from(stadiumHomeTeamEntity) + .join(stadiumEntity) + .on(stadiumHomeTeamEntity.stadiumId.eq(stadiumEntity.id)) + .join(baseballTeamEntity) + .on(stadiumHomeTeamEntity.teamId.eq(baseballTeamEntity.id)) + .transform(groupBy(stadiumEntity).as(list(baseballTeamEntity))); + } +} diff --git a/infrastructure/jpa/src/main/resources/application-jpa.yaml b/infrastructure/jpa/src/main/resources/application-jpa.yaml index dbe6c8a5..b7782cd2 100644 --- a/infrastructure/jpa/src/main/resources/application-jpa.yaml +++ b/infrastructure/jpa/src/main/resources/application-jpa.yaml @@ -31,5 +31,3 @@ decorator: datasource: p6spy: enable-logging: true - -# 필요한 경우 추가 설정 \ No newline at end of file diff --git a/infrastructure/jpa/src/main/resources/data.sql b/infrastructure/jpa/src/main/resources/data.sql new file mode 100644 index 00000000..385293f3 --- /dev/null +++ b/infrastructure/jpa/src/main/resources/data.sql @@ -0,0 +1,59 @@ +-- Stadiums +INSERT INTO stadiums (id, name, main_image, seating_chart_image, labeled_seating_chart_image, is_active) +VALUES (1, '잠실 야구 경기장', 'main_image_a.jpg', 'seating_chart_a.jpg', 'labeled_seating_chart_a.jpg', 1), + (2, '김포 야구 경기장', 'main_image_b.jpg', 'seating_chart_b.jpg', 'labeled_seating_chart_b.jpg', 1), + (3, '부산 야구 경기장', 'main_image_c.jpg', 'seating_chart_c.jpg', 'labeled_seating_chart_c.jpg', 0); + +-- Baseball Teams +INSERT INTO baseball_teams (id, name, alias, logo, red, green, blue) +VALUES (1, 'Team A', 'A', 'logo_a.png', 255, 0, 0), + (2, 'Team B', 'B', 'logo_b.png', 0, 255, 0), + (3, 'Team C', 'C', 'logo_c.png', 0, 0, 255); + +-- Stadium Home Teams +INSERT INTO stadium_home_teams (id, stadium_id, team_id) +VALUES (1, 1, 1), + (2, 2, 2), + (3, 3, 3), + (4, 1, 2); + +-- Stadium Sections +INSERT INTO sections (id, stadium_id, name, alias, red, green, blue) +VALUES (1, 1, '오렌지석', '응원석', 255, 255, 255), + (2, 1, '네이비석', '프리미엄석', 100, 100, 100), + (3, 1, '레드석', null, 100, 0, 0); + +-- Reviews +INSERT INTO reviews (id, user_id, stadium_id, block_id, row_id, seat_number, date_time, content, created_at, updated_at) +VALUES +(1, 1, 1, 1, 1, 10, '2023-07-15 14:00:00', '좋은 경기였습니다!', '2023-07-15 18:00:00', '2023-07-15 18:00:00'), +(2, 2, 1, 1, 2, 15, '2023-07-16 15:00:00', '시야가 좋았어요', '2023-07-16 19:00:00', '2023-07-16 19:00:00'), +(3, 3, 1, 2, 1, 5, '2023-07-17 16:00:00', '다음에 또 오고 싶어요', '2023-07-17 20:00:00', '2023-07-17 20:00:00'); + +-- Review Images +INSERT INTO review_images (id, review_id, url, created_at) +VALUES (1, 1, 'review1_image1.jpg', '2023-07-15 18:00:00'), + (2, 1, 'review1_image2.jpg', '2023-07-15 18:00:00'), + (3, 2, 'review2_image1.jpg', '2023-07-16 19:00:00'), + (4, 3, 'review3_image1.jpg', '2023-07-17 20:00:00'); + +-- Keywords +INSERT INTO keywords (id, content) +VALUES (1, '좋아요'), + (2, '시야 좋음'), + (3, '재방문 의사'), + (4, '편안함'); + +-- Review Keywords +INSERT INTO review_keywords (id, review_id, keyword_id, is_positive) +VALUES (1, 1, 1, 1), + (2, 1, 4, 1), + (3, 2, 2, 1), + (4, 3, 3, 1); + +-- Block Top Keywords +INSERT INTO block_top_keywords (id, block_id, keyword_id, count) +VALUES (1, 1, 1, 10), + (2, 1, 2, 8), + (3, 2, 3, 5), + (4, 2, 4, 3); \ No newline at end of file diff --git a/infrastructure/ncp/src/test/java/org/depromeet/spot/ncp/objectstorage/FileNameGeneratorTest.java b/infrastructure/ncp/src/test/java/org/depromeet/spot/ncp/objectstorage/FileNameGeneratorTest.java index 3e4489b2..8ceb1e46 100644 --- a/infrastructure/ncp/src/test/java/org/depromeet/spot/ncp/objectstorage/FileNameGeneratorTest.java +++ b/infrastructure/ncp/src/test/java/org/depromeet/spot/ncp/objectstorage/FileNameGeneratorTest.java @@ -25,10 +25,12 @@ void init() { ImageExtension extension = ImageExtension.JPG; // when - final String fileName = fileNameGenerator.createReviewFileName(userId, extension); + final String folderName = "review-images"; + final String fileName = + fileNameGenerator.createReviewFileName(userId, extension, folderName); // then - assertThat(fileName).isEqualTo("REVIEW/user/1/2024-07-09T21:00/jpg"); + assertThat(fileName).isEqualTo("review-images/REVIEW_user_1_2024-07-09T21:00.jpg"); } @Test @@ -37,9 +39,10 @@ void init() { StadiumSeatMediaExtension extension = StadiumSeatMediaExtension.SVG; // when - final String fileName = fileNameGenerator.createStadiumFileName(extension); + final String folderName = "stadium-images"; + final String fileName = fileNameGenerator.createStadiumFileName(extension, folderName); // then - assertThat(fileName).isEqualTo("STADIUM/2024-07-09T21:00/svg"); + assertThat(fileName).isEqualTo("stadium-images/STADIUM_2024-07-09T21:00.svg"); } } diff --git a/usecase/src/main/java/org/depromeet/spot/usecase/port/in/block/BlockReadUsecase.java b/usecase/src/main/java/org/depromeet/spot/usecase/port/in/block/BlockReadUsecase.java new file mode 100644 index 00000000..1d439fac --- /dev/null +++ b/usecase/src/main/java/org/depromeet/spot/usecase/port/in/block/BlockReadUsecase.java @@ -0,0 +1,18 @@ +package org.depromeet.spot.usecase.port.in.block; + +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +public interface BlockReadUsecase { + + List findCodeInfosByStadium(Long stadiumId, Long sectionId); + + @Getter + @AllArgsConstructor + class BlockCodeInfo { + private final Long id; + private final String code; + } +} diff --git a/usecase/src/main/java/org/depromeet/spot/usecase/port/in/review/ReviewReadUsecase.java b/usecase/src/main/java/org/depromeet/spot/usecase/port/in/review/ReviewReadUsecase.java new file mode 100644 index 00000000..e16f7eaa --- /dev/null +++ b/usecase/src/main/java/org/depromeet/spot/usecase/port/in/review/ReviewReadUsecase.java @@ -0,0 +1,8 @@ +package org.depromeet.spot.usecase.port.in.review; + +import org.depromeet.spot.domain.review.ReviewListResult; + +public interface ReviewReadUsecase { + ReviewListResult findReviewsByBlockId( + Long stadiumId, Long blockId, Long rowId, Long seatNumber, int offset, int limit); +} diff --git a/usecase/src/main/java/org/depromeet/spot/usecase/port/in/section/SectionReadUsecase.java b/usecase/src/main/java/org/depromeet/spot/usecase/port/in/section/SectionReadUsecase.java new file mode 100644 index 00000000..727a10aa --- /dev/null +++ b/usecase/src/main/java/org/depromeet/spot/usecase/port/in/section/SectionReadUsecase.java @@ -0,0 +1,43 @@ +package org.depromeet.spot.usecase.port.in.section; + +import java.util.List; + +import org.depromeet.spot.domain.common.RgbCode; +import org.depromeet.spot.domain.section.Section; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +public interface SectionReadUsecase { + + StadiumSections findAllByStadium(Long stadiumId); + + boolean existsInStadium(Long stadiumId, Long sectionId); + + @Getter + @AllArgsConstructor + class StadiumSections { + private final String seatChart; + private final List sectionList; + } + + @Getter + @Builder + @AllArgsConstructor + class SectionInfo { + private final Long id; + private final String name; + private final String alias; + private final RgbCode color; + + public static SectionInfo from(Section section) { + return SectionInfo.builder() + .id(section.getId()) + .name(section.getName()) + .alias(section.getAlias()) + .color(section.getLabelRgbCode()) + .build(); + } + } +} diff --git a/usecase/src/main/java/org/depromeet/spot/usecase/port/in/stadium/StadiumReadUsecase.java b/usecase/src/main/java/org/depromeet/spot/usecase/port/in/stadium/StadiumReadUsecase.java new file mode 100644 index 00000000..7557c46e --- /dev/null +++ b/usecase/src/main/java/org/depromeet/spot/usecase/port/in/stadium/StadiumReadUsecase.java @@ -0,0 +1,50 @@ +package org.depromeet.spot.usecase.port.in.stadium; + +import java.util.List; + +import org.depromeet.spot.domain.stadium.Stadium; +import org.depromeet.spot.usecase.port.in.team.ReadStadiumHomeTeamUsecase.HomeTeamInfo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +public interface StadiumReadUsecase { + + List findAllStadiums(); + + List findAllNames(); + + StadiumInfoWithSeatChart findWithSeatChartById(Long id); + + Stadium findById(Long id); + + boolean existsById(Long id); + + @Getter + @AllArgsConstructor + class StadiumHomeTeamInfo { + private final Long id; + private final String name; + private final List homeTeams; + private final String thumbnail; + private final boolean isActive; + } + + @Getter + @Builder + @AllArgsConstructor + class StadiumInfoWithSeatChart { + private final Long id; + private final String name; + private final List homeTeams; + private final String seatChartWithLabel; + } + + @Getter + @AllArgsConstructor + class StadiumNameInfo { + private final Long id; + private final String name; + } +} diff --git a/usecase/src/main/java/org/depromeet/spot/usecase/port/in/team/CreateBaseballTeamUsecase.java b/usecase/src/main/java/org/depromeet/spot/usecase/port/in/team/CreateBaseballTeamUsecase.java new file mode 100644 index 00000000..ca544e0b --- /dev/null +++ b/usecase/src/main/java/org/depromeet/spot/usecase/port/in/team/CreateBaseballTeamUsecase.java @@ -0,0 +1,10 @@ +package org.depromeet.spot.usecase.port.in.team; + +import java.util.List; + +import org.depromeet.spot.domain.team.BaseballTeam; + +public interface CreateBaseballTeamUsecase { + + void saveAll(List teams); +} diff --git a/usecase/src/main/java/org/depromeet/spot/usecase/port/in/team/ReadBaseballTeamUsecase.java b/usecase/src/main/java/org/depromeet/spot/usecase/port/in/team/ReadBaseballTeamUsecase.java new file mode 100644 index 00000000..700045de --- /dev/null +++ b/usecase/src/main/java/org/depromeet/spot/usecase/port/in/team/ReadBaseballTeamUsecase.java @@ -0,0 +1,10 @@ +package org.depromeet.spot.usecase.port.in.team; + +import java.util.List; + +import org.depromeet.spot.domain.team.BaseballTeam; + +public interface ReadBaseballTeamUsecase { + + List findAll(); +} diff --git a/usecase/src/main/java/org/depromeet/spot/usecase/port/in/team/ReadStadiumHomeTeamUsecase.java b/usecase/src/main/java/org/depromeet/spot/usecase/port/in/team/ReadStadiumHomeTeamUsecase.java new file mode 100644 index 00000000..78b1236e --- /dev/null +++ b/usecase/src/main/java/org/depromeet/spot/usecase/port/in/team/ReadStadiumHomeTeamUsecase.java @@ -0,0 +1,26 @@ +package org.depromeet.spot.usecase.port.in.team; + +import java.util.List; +import java.util.Map; + +import org.depromeet.spot.domain.common.RgbCode; +import org.depromeet.spot.domain.stadium.Stadium; +import org.depromeet.spot.domain.team.BaseballTeam; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +public interface ReadStadiumHomeTeamUsecase { + + List findByStadium(Long stadiumId); + + Map> findAllStadiumHomeTeam(); + + @Getter + @AllArgsConstructor + class HomeTeamInfo { + private final Long id; + private final String alias; + private final RgbCode rgbCode; + } +} diff --git a/usecase/src/main/java/org/depromeet/spot/usecase/port/out/block/BlockRepository.java b/usecase/src/main/java/org/depromeet/spot/usecase/port/out/block/BlockRepository.java new file mode 100644 index 00000000..b146f7d9 --- /dev/null +++ b/usecase/src/main/java/org/depromeet/spot/usecase/port/out/block/BlockRepository.java @@ -0,0 +1,10 @@ +package org.depromeet.spot.usecase.port.out.block; + +import java.util.List; + +import org.depromeet.spot.domain.block.Block; + +public interface BlockRepository { + + List findAllBySection(Long sectionId); +} diff --git a/usecase/src/main/java/org/depromeet/spot/usecase/port/out/review/ReviewRepository.java b/usecase/src/main/java/org/depromeet/spot/usecase/port/out/review/ReviewRepository.java new file mode 100644 index 00000000..6d9bc45b --- /dev/null +++ b/usecase/src/main/java/org/depromeet/spot/usecase/port/out/review/ReviewRepository.java @@ -0,0 +1,15 @@ +package org.depromeet.spot.usecase.port.out.review; + +import java.util.List; + +import org.depromeet.spot.domain.review.KeywordCount; +import org.depromeet.spot.domain.review.Review; + +public interface ReviewRepository { + List findByBlockId( + Long stadiumId, Long blockId, Long rowId, Long seatNumber, int offset, int limit); + + Long countByBlockId(Long stadiumId, Long blockId, Long rowId, Long seatNumber); + + List findTopKeywordsByBlockId(Long stadiumId, Long blockId, int limit); +} diff --git a/usecase/src/main/java/org/depromeet/spot/usecase/port/out/section/SectionRepository.java b/usecase/src/main/java/org/depromeet/spot/usecase/port/out/section/SectionRepository.java new file mode 100644 index 00000000..9e960c1b --- /dev/null +++ b/usecase/src/main/java/org/depromeet/spot/usecase/port/out/section/SectionRepository.java @@ -0,0 +1,14 @@ +package org.depromeet.spot.usecase.port.out.section; + +import java.util.List; + +import org.depromeet.spot.domain.section.Section; + +public interface SectionRepository { + + List
findAllByStadium(Long stadiumId); + + Section save(Section section); + + boolean existsInStadium(Long stadiumId, Long sectionId); +} diff --git a/usecase/src/main/java/org/depromeet/spot/usecase/port/out/stadium/StadiumRepository.java b/usecase/src/main/java/org/depromeet/spot/usecase/port/out/stadium/StadiumRepository.java new file mode 100644 index 00000000..3f52f796 --- /dev/null +++ b/usecase/src/main/java/org/depromeet/spot/usecase/port/out/stadium/StadiumRepository.java @@ -0,0 +1,15 @@ +package org.depromeet.spot.usecase.port.out.stadium; + +import java.util.List; + +import org.depromeet.spot.domain.stadium.Stadium; + +public interface StadiumRepository { + Stadium findById(Long id); + + List findAll(); + + Stadium save(Stadium stadium); + + boolean existsById(Long id); +} diff --git a/usecase/src/main/java/org/depromeet/spot/usecase/port/out/team/BaseballTeamRepository.java b/usecase/src/main/java/org/depromeet/spot/usecase/port/out/team/BaseballTeamRepository.java new file mode 100644 index 00000000..f578e032 --- /dev/null +++ b/usecase/src/main/java/org/depromeet/spot/usecase/port/out/team/BaseballTeamRepository.java @@ -0,0 +1,23 @@ +package org.depromeet.spot.usecase.port.out.team; + +import java.util.List; +import java.util.Map; + +import org.depromeet.spot.domain.stadium.Stadium; +import org.depromeet.spot.domain.team.BaseballTeam; + +public interface BaseballTeamRepository { + BaseballTeam findById(Long id); + + List findAll(); + + List findAllHomeTeamByStadium(Long stadiumId); + + Map> findAllStadiumHomeTeam(); + + void saveAll(List teams); + + void createHomeTeam(Long stadiumId, List teamIds); + + boolean existsByNameIn(List names); +} diff --git a/usecase/src/main/java/org/depromeet/spot/usecase/service/block/BlockReadService.java b/usecase/src/main/java/org/depromeet/spot/usecase/service/block/BlockReadService.java new file mode 100644 index 00000000..4e0336cc --- /dev/null +++ b/usecase/src/main/java/org/depromeet/spot/usecase/service/block/BlockReadService.java @@ -0,0 +1,35 @@ +package org.depromeet.spot.usecase.service.block; + +import java.util.List; + +import org.depromeet.spot.common.exception.section.SectionException.SectionNotBelongStadiumException; +import org.depromeet.spot.common.exception.stadium.StadiumException.StadiumNotFoundException; +import org.depromeet.spot.domain.block.Block; +import org.depromeet.spot.usecase.port.in.block.BlockReadUsecase; +import org.depromeet.spot.usecase.port.in.section.SectionReadUsecase; +import org.depromeet.spot.usecase.port.in.stadium.StadiumReadUsecase; +import org.depromeet.spot.usecase.port.out.block.BlockRepository; +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class BlockReadService implements BlockReadUsecase { + + private final BlockRepository blockRepository; + private final StadiumReadUsecase stadiumReadUsecase; + private final SectionReadUsecase sectionReadUsecase; + + @Override + public List findCodeInfosByStadium(final Long stadiumId, final Long sectionId) { + if (!stadiumReadUsecase.existsById(stadiumId)) { + throw new StadiumNotFoundException(); + } + if (!sectionReadUsecase.existsInStadium(stadiumId, sectionId)) { + throw new SectionNotBelongStadiumException(); + } + List blocks = blockRepository.findAllBySection(sectionId); + return blocks.stream().map(b -> new BlockCodeInfo(b.getId(), b.getCode())).toList(); + } +} diff --git a/usecase/src/main/java/org/depromeet/spot/usecase/service/member/MemberService.java b/usecase/src/main/java/org/depromeet/spot/usecase/service/member/MemberService.java index bbd6d37b..5d5cf60a 100644 --- a/usecase/src/main/java/org/depromeet/spot/usecase/service/member/MemberService.java +++ b/usecase/src/main/java/org/depromeet/spot/usecase/service/member/MemberService.java @@ -18,7 +18,10 @@ public class MemberService implements MemberUsecase { @Override public Member create(final String name) { - var member = new Member(null, name); + var member = + new Member( + null, null, name, null, null, null, null, null, null, null, null, null, + null); return memberRepository.save(member); } diff --git a/usecase/src/main/java/org/depromeet/spot/usecase/service/review/ReviewReadService.java b/usecase/src/main/java/org/depromeet/spot/usecase/service/review/ReviewReadService.java new file mode 100644 index 00000000..892b3256 --- /dev/null +++ b/usecase/src/main/java/org/depromeet/spot/usecase/service/review/ReviewReadService.java @@ -0,0 +1,33 @@ +package org.depromeet.spot.usecase.service.review; + +import java.util.List; + +import org.depromeet.spot.domain.review.KeywordCount; +import org.depromeet.spot.domain.review.Review; +import org.depromeet.spot.domain.review.ReviewListResult; +import org.depromeet.spot.usecase.port.in.review.ReviewReadUsecase; +import org.depromeet.spot.usecase.port.out.review.ReviewRepository; +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class ReviewReadService implements ReviewReadUsecase { + private final ReviewRepository reviewRepository; + + private static final int TOP_KEYWORDS_LIMIT = 5; + + @Override + public ReviewListResult findReviewsByBlockId( + Long stadiumId, Long blockId, Long rowId, Long seatNumber, int offset, int limit) { + List reviews = + reviewRepository.findByBlockId( + stadiumId, blockId, rowId, seatNumber, offset, limit); + Long totalCount = reviewRepository.countByBlockId(stadiumId, blockId, rowId, seatNumber); + List topKeywords = + reviewRepository.findTopKeywordsByBlockId(stadiumId, blockId, TOP_KEYWORDS_LIMIT); + + return new ReviewListResult(reviews, topKeywords, totalCount, offset, limit); + } +} diff --git a/usecase/src/main/java/org/depromeet/spot/usecase/service/section/SectionReadService.java b/usecase/src/main/java/org/depromeet/spot/usecase/service/section/SectionReadService.java new file mode 100644 index 00000000..03f55aed --- /dev/null +++ b/usecase/src/main/java/org/depromeet/spot/usecase/service/section/SectionReadService.java @@ -0,0 +1,35 @@ +package org.depromeet.spot.usecase.service.section; + +import java.util.List; + +import org.depromeet.spot.domain.section.Section; +import org.depromeet.spot.domain.stadium.Stadium; +import org.depromeet.spot.usecase.port.in.section.SectionReadUsecase; +import org.depromeet.spot.usecase.port.in.stadium.StadiumReadUsecase; +import org.depromeet.spot.usecase.port.out.section.SectionRepository; +import org.springframework.stereotype.Service; + +import lombok.Builder; +import lombok.RequiredArgsConstructor; + +@Service +@Builder +@RequiredArgsConstructor +public class SectionReadService implements SectionReadUsecase { + + private final StadiumReadUsecase stadiumReadUsecase; + private final SectionRepository sectionRepository; + + @Override + public StadiumSections findAllByStadium(final Long stadiumId) { + Stadium stadium = stadiumReadUsecase.findById(stadiumId); + List
sections = sectionRepository.findAllByStadium(stadiumId); + List sectionInfos = sections.stream().map(SectionInfo::from).toList(); + return new StadiumSections(stadium.getSeatingChartImage(), sectionInfos); + } + + @Override + public boolean existsInStadium(final Long stadiumId, final Long sectionId) { + return sectionRepository.existsInStadium(stadiumId, sectionId); + } +} diff --git a/usecase/src/main/java/org/depromeet/spot/usecase/service/stadium/StadiumReadService.java b/usecase/src/main/java/org/depromeet/spot/usecase/service/stadium/StadiumReadService.java new file mode 100644 index 00000000..4b062ae3 --- /dev/null +++ b/usecase/src/main/java/org/depromeet/spot/usecase/service/stadium/StadiumReadService.java @@ -0,0 +1,82 @@ +package org.depromeet.spot.usecase.service.stadium; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.depromeet.spot.domain.stadium.Stadium; +import org.depromeet.spot.domain.team.BaseballTeam; +import org.depromeet.spot.usecase.port.in.stadium.StadiumReadUsecase; +import org.depromeet.spot.usecase.port.in.team.ReadStadiumHomeTeamUsecase; +import org.depromeet.spot.usecase.port.in.team.ReadStadiumHomeTeamUsecase.HomeTeamInfo; +import org.depromeet.spot.usecase.port.out.stadium.StadiumRepository; +import org.springframework.stereotype.Service; + +import lombok.Builder; +import lombok.RequiredArgsConstructor; + +@Service +@Builder +@RequiredArgsConstructor +public class StadiumReadService implements StadiumReadUsecase { + + private final ReadStadiumHomeTeamUsecase readStadiumHomeTeamUsecase; + private final StadiumRepository stadiumRepository; + + @Override + public List findAllStadiums() { + Map> stadiumHomeTeams = + readStadiumHomeTeamUsecase.findAllStadiumHomeTeam(); + return stadiumHomeTeams.entrySet().stream() + .map( + entry -> { + Stadium stadium = entry.getKey(); + List teams = entry.getValue(); + List homeTeamInfos = + teams.stream() + .map( + t -> + new HomeTeamInfo( + t.getId(), + t.getAlias(), + t.getLabelRgbCode())) + .collect(Collectors.toList()); + + return new StadiumHomeTeamInfo( + stadium.getId(), + stadium.getName(), + homeTeamInfos, + stadium.getMainImage(), + stadium.isActive()); + }) + .toList(); + } + + @Override + public List findAllNames() { + List stadiums = stadiumRepository.findAll(); + return stadiums.stream().map(s -> new StadiumNameInfo(s.getId(), s.getName())).toList(); + } + + @Override + public StadiumInfoWithSeatChart findWithSeatChartById(final Long id) { + Stadium stadium = stadiumRepository.findById(id); + List homeTeams = readStadiumHomeTeamUsecase.findByStadium(id); + return StadiumInfoWithSeatChart.builder() + .id(stadium.getId()) + .name(stadium.getName()) + .homeTeams(homeTeams) + .seatChartWithLabel(stadium.getLabeledSeatingChartImage()) + .build(); + } + + @Override + public Stadium findById(final Long id) { + return stadiumRepository.findById(id); + } + + @Override + public boolean existsById(final Long id) { + return stadiumRepository.existsById(id); + } +} diff --git a/usecase/src/main/java/org/depromeet/spot/usecase/service/team/CreateBaseballTeamService.java b/usecase/src/main/java/org/depromeet/spot/usecase/service/team/CreateBaseballTeamService.java new file mode 100644 index 00000000..2c9414af --- /dev/null +++ b/usecase/src/main/java/org/depromeet/spot/usecase/service/team/CreateBaseballTeamService.java @@ -0,0 +1,29 @@ +package org.depromeet.spot.usecase.service.team; + +import java.util.List; + +import org.depromeet.spot.common.exception.team.TeamException.DuplicateTeamNameException; +import org.depromeet.spot.domain.team.BaseballTeam; +import org.depromeet.spot.usecase.port.in.team.CreateBaseballTeamUsecase; +import org.depromeet.spot.usecase.port.out.team.BaseballTeamRepository; +import org.springframework.stereotype.Service; + +import lombok.Builder; +import lombok.RequiredArgsConstructor; + +@Service +@Builder +@RequiredArgsConstructor +public class CreateBaseballTeamService implements CreateBaseballTeamUsecase { + + private final BaseballTeamRepository baseballTeamRepository; + + @Override + public void saveAll(List teams) { + List names = teams.stream().map(BaseballTeam::getName).toList(); + if (baseballTeamRepository.existsByNameIn(names)) { + throw new DuplicateTeamNameException(); + } + baseballTeamRepository.saveAll(teams); + } +} diff --git a/usecase/src/main/java/org/depromeet/spot/usecase/service/team/ReadBaseballTeamService.java b/usecase/src/main/java/org/depromeet/spot/usecase/service/team/ReadBaseballTeamService.java new file mode 100644 index 00000000..c7e69932 --- /dev/null +++ b/usecase/src/main/java/org/depromeet/spot/usecase/service/team/ReadBaseballTeamService.java @@ -0,0 +1,24 @@ +package org.depromeet.spot.usecase.service.team; + +import java.util.List; + +import org.depromeet.spot.domain.team.BaseballTeam; +import org.depromeet.spot.usecase.port.in.team.ReadBaseballTeamUsecase; +import org.depromeet.spot.usecase.port.out.team.BaseballTeamRepository; +import org.springframework.stereotype.Service; + +import lombok.Builder; +import lombok.RequiredArgsConstructor; + +@Service +@Builder +@RequiredArgsConstructor +public class ReadBaseballTeamService implements ReadBaseballTeamUsecase { + + private final BaseballTeamRepository baseballTeamRepository; + + @Override + public List findAll() { + return baseballTeamRepository.findAll(); + } +} diff --git a/usecase/src/main/java/org/depromeet/spot/usecase/service/team/ReadStadiumHomeTeamService.java b/usecase/src/main/java/org/depromeet/spot/usecase/service/team/ReadStadiumHomeTeamService.java new file mode 100644 index 00000000..1ad2dbc4 --- /dev/null +++ b/usecase/src/main/java/org/depromeet/spot/usecase/service/team/ReadStadiumHomeTeamService.java @@ -0,0 +1,32 @@ +package org.depromeet.spot.usecase.service.team; + +import java.util.List; +import java.util.Map; + +import org.depromeet.spot.domain.stadium.Stadium; +import org.depromeet.spot.domain.team.BaseballTeam; +import org.depromeet.spot.usecase.port.in.team.ReadStadiumHomeTeamUsecase; +import org.depromeet.spot.usecase.port.out.team.BaseballTeamRepository; +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class ReadStadiumHomeTeamService implements ReadStadiumHomeTeamUsecase { + + private final BaseballTeamRepository baseballTeamRepository; + + @Override + public List findByStadium(final Long stadiumId) { + List teams = baseballTeamRepository.findAllHomeTeamByStadium(stadiumId); + return teams.stream() + .map(t -> new HomeTeamInfo(t.getId(), t.getAlias(), t.getLabelRgbCode())) + .toList(); + } + + @Override + public Map> findAllStadiumHomeTeam() { + return baseballTeamRepository.findAllStadiumHomeTeam(); + } +} diff --git a/usecase/src/test/java/org/depromeet/spot/usecase/service/fake/FakeBaseballTeamRepository.java b/usecase/src/test/java/org/depromeet/spot/usecase/service/fake/FakeBaseballTeamRepository.java new file mode 100644 index 00000000..5db0b739 --- /dev/null +++ b/usecase/src/test/java/org/depromeet/spot/usecase/service/fake/FakeBaseballTeamRepository.java @@ -0,0 +1,95 @@ +package org.depromeet.spot.usecase.service.fake; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; + +import org.depromeet.spot.common.exception.team.TeamException.BaseballTeamNotFoundException; +import org.depromeet.spot.domain.stadium.Stadium; +import org.depromeet.spot.domain.team.BaseballTeam; +import org.depromeet.spot.usecase.port.out.team.BaseballTeamRepository; + +public class FakeBaseballTeamRepository implements BaseballTeamRepository { + + private final AtomicLong autoGeneratedId = new AtomicLong(0); + private final List data = Collections.synchronizedList(new ArrayList<>()); + private final Map> homeTeamMap = + Collections.synchronizedMap(new HashMap<>()); + + @Override + public BaseballTeam findById(Long id) { + return getById(id).orElseThrow(BaseballTeamNotFoundException::new); + } + + private Optional getById(Long id) { + return data.stream().filter(team -> team.getId().equals(id)).findAny(); + } + + @Override + public List findAll() { + return data; + } + + @Override + public List findAllHomeTeamByStadium(Long stadiumId) { + return homeTeamMap.entrySet().stream() + .filter(entry -> stadiumId.equals(entry.getKey().getId())) + .flatMap(entry -> entry.getValue().stream()) + .collect(Collectors.toList()); + } + + @Override + public Map> findAllStadiumHomeTeam() { + return homeTeamMap; + } + + public BaseballTeam save(BaseballTeam team) { + if (team.getId() == null || team.getId() == 0) { + BaseballTeam newTeam = + BaseballTeam.builder() + .id(autoGeneratedId.incrementAndGet()) + .name(team.getName()) + .alias(team.getAlias()) + .logo(team.getLogo()) + .labelRgbCode(team.getLabelRgbCode()) + .build(); + data.add(newTeam); + return newTeam; + } else { + data.removeIf(item -> Objects.equals(item.getId(), team.getId())); + data.add(team); + return team; + } + } + + @Override + public void saveAll(List teams) { + teams.forEach(this::save); + } + + @Override + public void createHomeTeam(Long stadiumId, List teamIds) { + Stadium stadium = Stadium.builder().id(stadiumId).build(); + List newTeams = + teamIds.stream().map(t -> BaseballTeam.builder().id(t).build()).toList(); + + homeTeamMap.merge( + stadium, + newTeams, + (existingTeams, teams) -> { + existingTeams.addAll(teams); + return existingTeams; + }); + } + + @Override + public boolean existsByNameIn(List names) { + return data.stream().map(BaseballTeam::getName).anyMatch(names::contains); + } +} diff --git a/usecase/src/test/java/org/depromeet/spot/usecase/service/fake/FakeReviewRepository.java b/usecase/src/test/java/org/depromeet/spot/usecase/service/fake/FakeReviewRepository.java new file mode 100644 index 00000000..8ab8257b --- /dev/null +++ b/usecase/src/test/java/org/depromeet/spot/usecase/service/fake/FakeReviewRepository.java @@ -0,0 +1,86 @@ +package org.depromeet.spot.usecase.service.fake; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.depromeet.spot.common.exception.review.ReviewException; +import org.depromeet.spot.domain.review.KeywordCount; +import org.depromeet.spot.domain.review.Review; +import org.depromeet.spot.domain.review.ReviewKeyword; +import org.depromeet.spot.usecase.port.out.review.ReviewRepository; + +public class FakeReviewRepository implements ReviewRepository { + + private final List data = new ArrayList<>(); + + @Override + public List findByBlockId( + Long stadiumId, Long blockId, Long rowId, Long seatNumber, int offset, int limit) { + List filteredReviews = + data.stream() + .filter( + review -> + review.getStadiumId().equals(stadiumId) + && review.getBlockId().equals(blockId)) + .filter(review -> rowId == null || review.getRowId().equals(rowId)) + .filter( + review -> + seatNumber == null + || review.getSeatNumber().equals(seatNumber)) + .skip(offset) + .limit(limit) + .collect(Collectors.toList()); + + if (filteredReviews.isEmpty()) { + throw new ReviewException.ReviewNotFoundException( + "No review found for blockId:" + blockId); + } + + return filteredReviews; + } + + @Override + public Long countByBlockId(Long stadiumId, Long blockId, Long rowId, Long seatNumber) { + return data.stream() + .filter( + review -> + review.getStadiumId().equals(stadiumId) + && review.getBlockId().equals(blockId)) + .filter(review -> rowId == null || review.getRowId().equals(rowId)) + .filter(review -> seatNumber == null || review.getSeatNumber().equals(seatNumber)) + .count(); + } + + @Override + public List findTopKeywordsByBlockId(Long stadiumId, Long blockId, int limit) { + Map keywordCounts = + data.stream() + .filter( + review -> + review.getStadiumId().equals(stadiumId) + && review.getBlockId().equals(blockId)) + .flatMap( + review -> + review.getKeywords() != null + ? review.getKeywords().stream() + : Stream.empty()) + .collect( + Collectors.groupingBy( + ReviewKeyword::getKeywordId, Collectors.counting())); + + return keywordCounts.entrySet().stream() + .map(entry -> new KeywordCount(entry.getKey().toString(), entry.getValue())) + .sorted(Comparator.comparing(KeywordCount::count).reversed()) + .limit(limit) + .collect(Collectors.toList()); + } + + public void save(Review review) { + data.add(review); + } + + public void clear() { + data.clear(); + } +} diff --git a/usecase/src/test/java/org/depromeet/spot/usecase/service/fake/FakeSectionRepository.java b/usecase/src/test/java/org/depromeet/spot/usecase/service/fake/FakeSectionRepository.java new file mode 100644 index 00000000..ea7e0480 --- /dev/null +++ b/usecase/src/test/java/org/depromeet/spot/usecase/service/fake/FakeSectionRepository.java @@ -0,0 +1,48 @@ +package org.depromeet.spot.usecase.service.fake; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicLong; + +import org.depromeet.spot.domain.section.Section; +import org.depromeet.spot.usecase.port.out.section.SectionRepository; + +public class FakeSectionRepository implements SectionRepository { + + private final AtomicLong autoGeneratedId = new AtomicLong(0); + private final List
data = Collections.synchronizedList(new ArrayList<>()); + + @Override + public List
findAllByStadium(Long stadiumId) { + return data.stream().filter(section -> section.getStadiumId().equals(stadiumId)).toList(); + } + + @Override + public Section save(Section section) { + if (section.getId() == null || section.getId() == 0) { + Section newSection = + Section.builder() + .id(autoGeneratedId.incrementAndGet()) + .stadiumId(section.getStadiumId()) + .name(section.getName()) + .alias(section.getAlias()) + .labelRgbCode(section.getLabelRgbCode()) + .build(); + data.add(newSection); + return newSection; + } else { + data.removeIf(item -> Objects.equals(item.getId(), section.getId())); + data.add(section); + return section; + } + } + + @Override + public boolean existsInStadium(Long stadiumId, Long sectionId) { + return data.stream() + .filter(section -> section.getStadiumId().equals(sectionId)) + .anyMatch(section -> section.getId().equals(sectionId)); + } +} diff --git a/usecase/src/test/java/org/depromeet/spot/usecase/service/fake/FakeStadiumRepository.java b/usecase/src/test/java/org/depromeet/spot/usecase/service/fake/FakeStadiumRepository.java new file mode 100644 index 00000000..a3003010 --- /dev/null +++ b/usecase/src/test/java/org/depromeet/spot/usecase/service/fake/FakeStadiumRepository.java @@ -0,0 +1,58 @@ +package org.depromeet.spot.usecase.service.fake; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicLong; + +import org.depromeet.spot.common.exception.stadium.StadiumException.StadiumNotFoundException; +import org.depromeet.spot.domain.stadium.Stadium; +import org.depromeet.spot.usecase.port.out.stadium.StadiumRepository; + +public class FakeStadiumRepository implements StadiumRepository { + + private final AtomicLong autoGeneratedId = new AtomicLong(0); + private final List data = Collections.synchronizedList(new ArrayList<>()); + + @Override + public Stadium findById(Long id) { + return getById(id).orElseThrow(StadiumNotFoundException::new); + } + + private Optional getById(Long id) { + return data.stream().filter(stadium -> stadium.getId().equals(id)).findAny(); + } + + @Override + public List findAll() { + return data; + } + + @Override + public Stadium save(Stadium stadium) { + if (stadium.getId() == null || stadium.getId() == 0) { + Stadium newStadium = + Stadium.builder() + .id(autoGeneratedId.incrementAndGet()) + .name(stadium.getName()) + .mainImage(stadium.getMainImage()) + .seatingChartImage(stadium.getSeatingChartImage()) + .labeledSeatingChartImage(stadium.getLabeledSeatingChartImage()) + .isActive(stadium.isActive()) + .build(); + data.add(newStadium); + return newStadium; + } else { + data.removeIf(item -> Objects.equals(item.getId(), stadium.getId())); + data.add(stadium); + return stadium; + } + } + + @Override + public boolean existsById(final Long id) { + return data.stream().anyMatch(stadium -> stadium.getId().equals(id)); + } +} diff --git a/usecase/src/test/java/org/depromeet/spot/usecase/service/review/ReviewReadServiceTest.java b/usecase/src/test/java/org/depromeet/spot/usecase/service/review/ReviewReadServiceTest.java new file mode 100644 index 00000000..f4154890 --- /dev/null +++ b/usecase/src/test/java/org/depromeet/spot/usecase/service/review/ReviewReadServiceTest.java @@ -0,0 +1,118 @@ +package org.depromeet.spot.usecase.service.review; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Collections; + +import org.depromeet.spot.common.exception.review.ReviewException; +import org.depromeet.spot.domain.review.Review; +import org.depromeet.spot.domain.review.ReviewKeyword; +import org.depromeet.spot.domain.review.ReviewListResult; +import org.depromeet.spot.usecase.service.fake.FakeReviewRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class ReviewReadServiceTest { + + private ReviewReadService reviewReadService; + private FakeReviewRepository fakeReviewRepository; + + private ReviewKeyword createReviewKeyword( + Long id, Long reviewId, Long keywordId, Boolean isPositive) { + return ReviewKeyword.builder() + .id(id) + .reviewId(reviewId) + .keywordId(keywordId) + .isPositive(isPositive) + .build(); + } + + @BeforeEach + void setUp() { + fakeReviewRepository = new FakeReviewRepository(); + reviewReadService = new ReviewReadService(fakeReviewRepository); + } + + @Test + void findReviewsByBlockId는_블록별_리뷰를_정상적으로_반환한다() { + // Given + Review review1 = + Review.builder() + .id(1L) + .stadiumId(1L) + .blockId(1L) + .rowId(1L) + .seatNumber(1L) + .content("Great game!") + .keywords(Collections.singletonList(createReviewKeyword(1L, 1L, 1L, true))) + .build(); + Review review2 = + Review.builder() + .id(2L) + .stadiumId(1L) + .blockId(1L) + .rowId(2L) + .seatNumber(2L) + .content("Amazing view!") + .keywords( + Collections.singletonList( + createReviewKeyword(2L, 2L, 2L, true))) // 다른 keywordId 사용 + .build(); + fakeReviewRepository.save(review1); + fakeReviewRepository.save(review2); + + // When + ReviewListResult result = reviewReadService.findReviewsByBlockId(1L, 1L, null, null, 0, 10); + + // Then + assertEquals(2, result.reviews().size()); + assertEquals(2, result.totalCount()); + assertEquals(2, result.topKeywords().size()); + } + + @Test + void findReviewsByBlockId는_조회된_리뷰가_없을_시_NotFoundException_반환한다() { + // Given + Long nonExistentStadiumId = 999L; + Long nonExistentBlockId = 999L; + + // When & Then + assertThatThrownBy( + () -> + reviewReadService.findReviewsByBlockId( + nonExistentStadiumId, + nonExistentBlockId, + null, + null, + 0, + 10)) + .isInstanceOf(ReviewException.ReviewNotFoundException.class) + .hasMessageContaining("No review found for blockId:" + nonExistentBlockId); + } + + @Test + void findReviewsByBlockId는_offset과_limit로_잘_필터링한다() { + // Given + for (int i = 0; i < 20; i++) { + Review review = + Review.builder() + .id((long) i) + .stadiumId(1L) + .blockId(1L) + .rowId(1L) + .seatNumber((long) i) + .content("Review " + i) + .keywords(Collections.emptyList()) // 빈 리스트로 초기화 + .build(); + fakeReviewRepository.save(review); + } + + // When + ReviewListResult result = reviewReadService.findReviewsByBlockId(1L, 1L, null, null, 5, 10); + + // Then + assertEquals(10, result.reviews().size()); + assertEquals(20, result.totalCount()); + } +} diff --git a/usecase/src/test/java/org/depromeet/spot/usecase/service/section/SectionReadServiceTest.java b/usecase/src/test/java/org/depromeet/spot/usecase/service/section/SectionReadServiceTest.java new file mode 100644 index 00000000..90ae1ff9 --- /dev/null +++ b/usecase/src/test/java/org/depromeet/spot/usecase/service/section/SectionReadServiceTest.java @@ -0,0 +1,106 @@ +package org.depromeet.spot.usecase.service.section; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; + +import org.depromeet.spot.common.exception.stadium.StadiumException.StadiumNotFoundException; +import org.depromeet.spot.domain.common.RgbCode; +import org.depromeet.spot.domain.section.Section; +import org.depromeet.spot.domain.stadium.Stadium; +import org.depromeet.spot.usecase.port.in.section.SectionReadUsecase.SectionInfo; +import org.depromeet.spot.usecase.port.in.section.SectionReadUsecase.StadiumSections; +import org.depromeet.spot.usecase.port.in.team.ReadStadiumHomeTeamUsecase; +import org.depromeet.spot.usecase.service.fake.FakeSectionRepository; +import org.depromeet.spot.usecase.service.fake.FakeStadiumRepository; +import org.depromeet.spot.usecase.service.stadium.StadiumReadService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class SectionReadServiceTest { + + private SectionReadService sectionReadService; + private StadiumReadService stadiumReadService; + private ReadStadiumHomeTeamUsecase readStadiumHomeTeamUsecase; + + @BeforeEach + void init() { + FakeStadiumRepository fakeStadiumRepository = new FakeStadiumRepository(); + FakeSectionRepository fakeSectionRepository = new FakeSectionRepository(); + + this.stadiumReadService = + StadiumReadService.builder() + .readStadiumHomeTeamUsecase(readStadiumHomeTeamUsecase) + .stadiumRepository(fakeStadiumRepository) + .build(); + + this.sectionReadService = + SectionReadService.builder() + .stadiumReadUsecase(stadiumReadService) + .sectionRepository(fakeSectionRepository) + .build(); + + Stadium stadium = + Stadium.builder() + .id(1L) + .name("잠실 야구 경기장") + .mainImage("mainImage1.png") + .seatingChartImage("seatingChartImage1.png") + .labeledSeatingChartImage("labeledSeatingChartImage1.png") + .isActive(true) + .build(); + fakeStadiumRepository.save(stadium); + + Section section1 = + Section.builder() + .id(1L) + .stadiumId(1L) + .name("오렌지석") + .alias("응원석") + .labelRgbCode(new RgbCode(0, 0, 0)) + .build(); + Section section2 = + Section.builder() + .id(2L) + .stadiumId(1L) + .name("레드석") + .alias(null) + .labelRgbCode(new RgbCode(110, 100, 0)) + .build(); + fakeSectionRepository.save(section1); + fakeSectionRepository.save(section2); + } + + @Test + void findAllByStadium_는_주어진_경기장의_좌석배치도와_구역_정보를_반환한다() { + // given + final Long stadiumId = 1L; + + // when + StadiumSections results = sectionReadService.findAllByStadium(stadiumId); + + // then + assertEquals("seatingChartImage1.png", results.getSeatChart()); + + List sectionList = results.getSectionList(); + assertThat(sectionList).hasSize(2); + assertThat(sectionList) + .anyMatch( + section -> section.getId().equals(1L) && section.getName().equals("오렌지석")); + assertThat(sectionList) + .anyMatch(section -> section.getId().equals(2L) && section.getName().equals("레드석")); + } + + @Test + void findAllByStadium_는_존재하지_않는_경기장_요청에_예외를_반환한다() { + // given + final Long stadiumId = -999L; + + // when + // then + assertThatThrownBy(() -> sectionReadService.findAllByStadium(stadiumId)) + .isInstanceOf(StadiumNotFoundException.class); + } +} diff --git a/usecase/src/test/java/org/depromeet/spot/usecase/service/stadium/StadiumReadServiceTest.java b/usecase/src/test/java/org/depromeet/spot/usecase/service/stadium/StadiumReadServiceTest.java new file mode 100644 index 00000000..7e42f1f8 --- /dev/null +++ b/usecase/src/test/java/org/depromeet/spot/usecase/service/stadium/StadiumReadServiceTest.java @@ -0,0 +1,85 @@ +package org.depromeet.spot.usecase.service.stadium; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.depromeet.spot.common.exception.stadium.StadiumException.StadiumNotFoundException; +import org.depromeet.spot.domain.stadium.Stadium; +import org.depromeet.spot.usecase.port.in.team.ReadStadiumHomeTeamUsecase; +import org.depromeet.spot.usecase.service.fake.FakeStadiumRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class StadiumReadServiceTest { + + private StadiumReadService stadiumReadService; + private ReadStadiumHomeTeamUsecase readStadiumHomeTeamUsecase; + + @BeforeEach + void init() { + FakeStadiumRepository fakeStadiumRepository = new FakeStadiumRepository(); + + this.stadiumReadService = + StadiumReadService.builder() + .readStadiumHomeTeamUsecase(readStadiumHomeTeamUsecase) + .stadiumRepository(fakeStadiumRepository) + .build(); + + Stadium stadium1 = + Stadium.builder() + .id(1L) + .name("잠실 야구 경기장") + .mainImage("mainImage1.png") + .seatingChartImage("seatingChartImage1.png") + .labeledSeatingChartImage("labeledSeatingChartImage1.png") + .isActive(true) + .build(); + + Stadium stadium2 = + Stadium.builder() + .id(2L) + .name("부산 야구 경기장") + .mainImage("mainImage2.png") + .seatingChartImage("seatingChartImage2.png") + .labeledSeatingChartImage("labeledSeatingChartImage2.png") + .isActive(false) + .build(); + + fakeStadiumRepository.save(stadium1); + fakeStadiumRepository.save(stadium2); + } + + @Test + void findById는_요청_경기장을_반환한다() { + // given + final Long stadiumId = 1L; + + // when + Stadium stadium = stadiumReadService.findById(stadiumId); + + // then + assertAll( + () -> assertEquals(1L, stadium.getId()), + () -> assertEquals("잠실 야구 경기장", stadium.getName()), + () -> assertEquals("mainImage1.png", stadium.getMainImage()), + () -> assertEquals("seatingChartImage1.png", stadium.getSeatingChartImage()), + () -> + assertEquals( + "labeledSeatingChartImage1.png", + stadium.getLabeledSeatingChartImage()), + () -> assertTrue(stadium.isActive())); + } + + @Test + void findById는_존재하지_않는_경기장_요청에_예외를_반환한다() { + // given + final Long stadiumId = -999L; + + // when + // then + assertThatThrownBy(() -> stadiumReadService.findById(stadiumId)) + .isInstanceOf(StadiumNotFoundException.class); + } +} diff --git a/usecase/src/test/java/org/depromeet/spot/usecase/service/team/CreateBaseballTeamServiceTest.java b/usecase/src/test/java/org/depromeet/spot/usecase/service/team/CreateBaseballTeamServiceTest.java new file mode 100644 index 00000000..4d26f637 --- /dev/null +++ b/usecase/src/test/java/org/depromeet/spot/usecase/service/team/CreateBaseballTeamServiceTest.java @@ -0,0 +1,55 @@ +package org.depromeet.spot.usecase.service.team; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; + +import org.depromeet.spot.common.exception.team.TeamException.DuplicateTeamNameException; +import org.depromeet.spot.domain.common.RgbCode; +import org.depromeet.spot.domain.team.BaseballTeam; +import org.depromeet.spot.usecase.service.fake.FakeBaseballTeamRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class CreateBaseballTeamServiceTest { + + private CreateBaseballTeamService createBaseballTeamService; + + @BeforeEach + void init() { + FakeBaseballTeamRepository fakeBaseballTeamRepository = new FakeBaseballTeamRepository(); + this.createBaseballTeamService = + CreateBaseballTeamService.builder() + .baseballTeamRepository(fakeBaseballTeamRepository) + .build(); + + BaseballTeam team = + BaseballTeam.builder() + .id(1L) + .name("두산 베어스") + .alias("두산") + .logo("logo1.png") + .labelRgbCode(new RgbCode(0, 0, 0)) + .build(); + fakeBaseballTeamRepository.save(team); + } + + @Test + void 이미_존재하는_이름의_구단을_중복_저장할_수_없다() { + // given + BaseballTeam team = + BaseballTeam.builder() + .id(1L) + .name("두산 베어스") + .alias("두산") + .logo("logo1.png") + .labelRgbCode(new RgbCode(0, 0, 0)) + .build(); + List teams = List.of(team); + + // when + // then + assertThatThrownBy(() -> createBaseballTeamService.saveAll(teams)) + .isInstanceOf(DuplicateTeamNameException.class); + } +} diff --git a/usecase/src/test/java/org/depromeet/spot/usecase/service/team/ReadBaseballTeamServiceTest.java b/usecase/src/test/java/org/depromeet/spot/usecase/service/team/ReadBaseballTeamServiceTest.java new file mode 100644 index 00000000..dcf55ede --- /dev/null +++ b/usecase/src/test/java/org/depromeet/spot/usecase/service/team/ReadBaseballTeamServiceTest.java @@ -0,0 +1,69 @@ +package org.depromeet.spot.usecase.service.team; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import java.util.List; + +import org.depromeet.spot.domain.common.RgbCode; +import org.depromeet.spot.domain.team.BaseballTeam; +import org.depromeet.spot.usecase.service.fake.FakeBaseballTeamRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class ReadBaseballTeamServiceTest { + + private ReadBaseballTeamService baseballTeamReadService; + + @BeforeEach + void init() { + FakeBaseballTeamRepository fakeBaseballTeamRepository = new FakeBaseballTeamRepository(); + this.baseballTeamReadService = + ReadBaseballTeamService.builder() + .baseballTeamRepository(fakeBaseballTeamRepository) + .build(); + + BaseballTeam team1 = + BaseballTeam.builder() + .id(1L) + .name("두산 베어스") + .alias("두산") + .logo("logo1.png") + .labelRgbCode(new RgbCode(0, 0, 0)) + .build(); + BaseballTeam team2 = + BaseballTeam.builder() + .id(2L) + .name("SSG 랜더스") + .alias("SSG") + .logo("logo2.png") + .labelRgbCode(new RgbCode(100, 0, 0)) + .build(); + + fakeBaseballTeamRepository.save(team1); + fakeBaseballTeamRepository.save(team2); + } + + @Test + void findAll_은_모든_구단을_반환한다() { + // given + // when + List teams = baseballTeamReadService.findAll(); + + // then + assertAll( + () -> assertThat(teams).hasSize(2), + () -> + assertThat(teams) + .anyMatch( + team -> + team.getId().equals(1L) + && team.getName().equals("두산 베어스")), + () -> + assertThat(teams) + .anyMatch( + team -> + team.getId().equals(2L) + && team.getName().equals("SSG 랜더스"))); + } +}