diff --git a/src/main/java/com/first/flash/climbing/favorite/application/MemberFavoriteGymService.java b/src/main/java/com/first/flash/climbing/favorite/application/MemberFavoriteGymService.java new file mode 100644 index 00000000..f736b2c7 --- /dev/null +++ b/src/main/java/com/first/flash/climbing/favorite/application/MemberFavoriteGymService.java @@ -0,0 +1,40 @@ +package com.first.flash.climbing.favorite.application; + +import com.first.flash.climbing.favorite.application.dto.MemberFavoriteGymResponseDto; +import com.first.flash.climbing.favorite.domain.MemberFavoriteGym; +import com.first.flash.climbing.favorite.domain.MemberFavoriteGymRepository; +import com.first.flash.climbing.gym.domian.ClimbingGymIdConfirmRequestedEvent; +import com.first.flash.global.event.Events; +import com.first.flash.global.util.AuthUtil; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class MemberFavoriteGymService { + + private final MemberFavoriteGymRepository memberFavoriteGymRepository; + + public MemberFavoriteGymResponseDto toggleMemberFavoriteGym(final Long gymId) { + Events.raise(ClimbingGymIdConfirmRequestedEvent.of(gymId)); + UUID memberId = AuthUtil.getId(); + Optional favoriteGym = memberFavoriteGymRepository.findByMemberIdAndGymId(memberId, gymId); + + if (favoriteGym.isPresent()) { + memberFavoriteGymRepository.delete(favoriteGym.get()); + } else { + MemberFavoriteGym memberFavoriteGym = MemberFavoriteGym.createDefault(memberId, gymId); + memberFavoriteGymRepository.save(memberFavoriteGym); + } + return MemberFavoriteGymResponseDto.toDtoByStatus(favoriteGym.isPresent()); + } + + public List findFavoriteGymIdsByMemberId(final UUID memberId) { + return memberFavoriteGymRepository.findByMemberId(memberId).stream() + .map(MemberFavoriteGym::getGymId) + .toList(); + } +} diff --git a/src/main/java/com/first/flash/climbing/favorite/application/dto/MemberFavoriteGymResponseDto.java b/src/main/java/com/first/flash/climbing/favorite/application/dto/MemberFavoriteGymResponseDto.java new file mode 100644 index 00000000..ed1e5529 --- /dev/null +++ b/src/main/java/com/first/flash/climbing/favorite/application/dto/MemberFavoriteGymResponseDto.java @@ -0,0 +1,11 @@ +package com.first.flash.climbing.favorite.application.dto; + +public record MemberFavoriteGymResponseDto(String message) { + + public static MemberFavoriteGymResponseDto toDtoByStatus(final boolean present) { + if (present) { + return new MemberFavoriteGymResponseDto("즐겨찾기에서 제거되었습니다."); + } + return new MemberFavoriteGymResponseDto("즐겨찾기에 추가되었습니다."); + } +} diff --git a/src/main/java/com/first/flash/climbing/favorite/domain/MemberFavoriteGym.java b/src/main/java/com/first/flash/climbing/favorite/domain/MemberFavoriteGym.java new file mode 100644 index 00000000..a0a7e040 --- /dev/null +++ b/src/main/java/com/first/flash/climbing/favorite/domain/MemberFavoriteGym.java @@ -0,0 +1,33 @@ +package com.first.flash.climbing.favorite.domain; + +import com.first.flash.global.domain.BaseEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import java.util.UUID; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Entity +@NoArgsConstructor +@Getter +@ToString +public class MemberFavoriteGym extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private UUID memberId; + private Long gymId; + + protected MemberFavoriteGym(final UUID memberId, final Long gymId) { + this.memberId = memberId; + this.gymId = gymId; + } + + public static MemberFavoriteGym createDefault(final UUID memberId, final Long gymId) { + return new MemberFavoriteGym(memberId, gymId); + } +} diff --git a/src/main/java/com/first/flash/climbing/favorite/domain/MemberFavoriteGymRepository.java b/src/main/java/com/first/flash/climbing/favorite/domain/MemberFavoriteGymRepository.java new file mode 100644 index 00000000..28205336 --- /dev/null +++ b/src/main/java/com/first/flash/climbing/favorite/domain/MemberFavoriteGymRepository.java @@ -0,0 +1,18 @@ +package com.first.flash.climbing.favorite.domain; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public interface MemberFavoriteGymRepository { + + MemberFavoriteGym save(final MemberFavoriteGym memberFavoriteGym); + + Optional findById(final Long id); + + List findByMemberId(final UUID memberId); + + Optional findByMemberIdAndGymId(final UUID memberId, final Long gymId); + + void delete(final MemberFavoriteGym memberFavoriteGym); +} diff --git a/src/main/java/com/first/flash/climbing/favorite/infrastructure/MemberFavoriteGymJpaRepository.java b/src/main/java/com/first/flash/climbing/favorite/infrastructure/MemberFavoriteGymJpaRepository.java new file mode 100644 index 00000000..de034077 --- /dev/null +++ b/src/main/java/com/first/flash/climbing/favorite/infrastructure/MemberFavoriteGymJpaRepository.java @@ -0,0 +1,20 @@ +package com.first.flash.climbing.favorite.infrastructure; + +import com.first.flash.climbing.favorite.domain.MemberFavoriteGym; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MemberFavoriteGymJpaRepository extends JpaRepository { + + MemberFavoriteGym save(final MemberFavoriteGym memberFavoriteGym); + + Optional findById(final Long id); + + List findByMemberId(final UUID memberId); + + Optional findByMemberIdAndGymId(final UUID memberId, final Long gymId); + + void delete(final MemberFavoriteGym memberFavoriteGym); +} diff --git a/src/main/java/com/first/flash/climbing/favorite/infrastructure/MemberFavoriteGymRepositoryImpl.java b/src/main/java/com/first/flash/climbing/favorite/infrastructure/MemberFavoriteGymRepositoryImpl.java new file mode 100644 index 00000000..8fbddaf2 --- /dev/null +++ b/src/main/java/com/first/flash/climbing/favorite/infrastructure/MemberFavoriteGymRepositoryImpl.java @@ -0,0 +1,42 @@ +package com.first.flash.climbing.favorite.infrastructure; + +import com.first.flash.climbing.favorite.domain.MemberFavoriteGym; +import com.first.flash.climbing.favorite.domain.MemberFavoriteGymRepository; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class MemberFavoriteGymRepositoryImpl implements MemberFavoriteGymRepository { + + private final MemberFavoriteGymJpaRepository memberFavoriteGymJpaRepository; + + @Override + public MemberFavoriteGym save(final MemberFavoriteGym memberFavoriteGym) { + return memberFavoriteGymJpaRepository.save(memberFavoriteGym); + } + + @Override + public Optional findById(final Long id) { + return memberFavoriteGymJpaRepository.findById(id); + } + + @Override + public List findByMemberId(final UUID memberId) { + return memberFavoriteGymJpaRepository.findByMemberId(memberId); + } + + @Override + public Optional findByMemberIdAndGymId(final UUID memberId, + final Long gymId) { + return memberFavoriteGymJpaRepository.findByMemberIdAndGymId(memberId, gymId); + } + + @Override + public void delete(final MemberFavoriteGym memberFavoriteGym) { + memberFavoriteGymJpaRepository.delete(memberFavoriteGym); + } +} diff --git a/src/main/java/com/first/flash/climbing/favorite/ui/MemberFavoriteGymController.java b/src/main/java/com/first/flash/climbing/favorite/ui/MemberFavoriteGymController.java new file mode 100644 index 00000000..13d5256f --- /dev/null +++ b/src/main/java/com/first/flash/climbing/favorite/ui/MemberFavoriteGymController.java @@ -0,0 +1,38 @@ +package com.first.flash.climbing.favorite.ui; + +import com.first.flash.climbing.favorite.application.MemberFavoriteGymService; +import com.first.flash.climbing.favorite.application.dto.MemberFavoriteGymResponseDto; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "favorite", description = "즐겨찾기 API") +@RestController +@RequiredArgsConstructor +public class MemberFavoriteGymController { + + private final MemberFavoriteGymService memberFavoriteGymService; + + @Operation(summary = "클라이밍장 즐겨찾기 생성/삭제", description = "클라이밍장 id로 즐겨찾기 토글") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "즐겨찾기 생성 및 삭제", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = MemberFavoriteGymResponseDto.class))), + @ApiResponse(responseCode = "400", description = "유효하지 않은 요청 형식", + content = @Content(mediaType = "application/json")) + }) + @PutMapping("/favorites/{gymId}") + public ResponseEntity toggleMemberFavoriteGym( + @PathVariable final Long gymId) { + return ResponseEntity.status(HttpStatus.OK) + .body(memberFavoriteGymService.toggleMemberFavoriteGym(gymId)); + } +} diff --git a/src/main/java/com/first/flash/climbing/gym/application/ClimbingGymService.java b/src/main/java/com/first/flash/climbing/gym/application/ClimbingGymService.java index 35ef6600..45ea8660 100644 --- a/src/main/java/com/first/flash/climbing/gym/application/ClimbingGymService.java +++ b/src/main/java/com/first/flash/climbing/gym/application/ClimbingGymService.java @@ -1,14 +1,18 @@ package com.first.flash.climbing.gym.application; +import com.first.flash.climbing.favorite.application.MemberFavoriteGymService; import com.first.flash.climbing.gym.application.dto.ClimbingGymCreateRequestDto; import com.first.flash.climbing.gym.application.dto.ClimbingGymCreateResponseDto; import com.first.flash.climbing.gym.application.dto.ClimbingGymDetailResponseDto; -import com.first.flash.climbing.gym.application.dto.ClimbingGymResponseDto; +import com.first.flash.climbing.gym.infrastructure.dto.ClimbingGymResponseDto; import com.first.flash.climbing.gym.domian.ClimbingGym; import com.first.flash.climbing.gym.domian.ClimbingGymRepository; import com.first.flash.climbing.gym.domian.vo.Difficulty; import com.first.flash.climbing.gym.exception.exceptions.ClimbingGymNotFoundException; +import com.first.flash.climbing.gym.infrastructure.dto.SectorInfoResponseDto; +import com.first.flash.global.util.AuthUtil; import java.util.List; +import java.util.UUID; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -19,6 +23,7 @@ public class ClimbingGymService { private final ClimbingGymRepository climbingGymRepository; + private final MemberFavoriteGymService memberFavoriteGymService; @Transactional public ClimbingGymCreateResponseDto save(final ClimbingGymCreateRequestDto createRequestDto) { @@ -32,21 +37,21 @@ public ClimbingGym findClimbingGymById(final Long id) { } public List findAllClimbingGyms() { - return climbingGymRepository.findAll().stream() - .map(ClimbingGymResponseDto::toDto) - .toList(); + UUID memberId = AuthUtil.getId(); + List favoriteGymIds = memberFavoriteGymService.findFavoriteGymIdsByMemberId(memberId); + return climbingGymRepository.findAllWithFavorites(favoriteGymIds); } public ClimbingGymDetailResponseDto findClimbingGymDetail(final Long id) { ClimbingGym climbingGym = findClimbingGymById(id); - List sectorNames = findSectorNamesById(id); + List sectorNames = findSectorNamesById(id); List difficultyNames = getDifficultyNames(climbingGym); return new ClimbingGymDetailResponseDto(climbingGym.getGymName(), climbingGym.getMapImageUrl(), climbingGym.getCalendarImageUrl(), difficultyNames, sectorNames); } - private List findSectorNamesById(final Long id) { + private List findSectorNamesById(final Long id) { return climbingGymRepository.findGymSectorNamesById(id); } diff --git a/src/main/java/com/first/flash/climbing/gym/application/dto/ClimbingGymDetailResponseDto.java b/src/main/java/com/first/flash/climbing/gym/application/dto/ClimbingGymDetailResponseDto.java index f2f1f822..fb9260ef 100644 --- a/src/main/java/com/first/flash/climbing/gym/application/dto/ClimbingGymDetailResponseDto.java +++ b/src/main/java/com/first/flash/climbing/gym/application/dto/ClimbingGymDetailResponseDto.java @@ -1,10 +1,11 @@ package com.first.flash.climbing.gym.application.dto; +import com.first.flash.climbing.gym.infrastructure.dto.SectorInfoResponseDto; import java.util.List; public record ClimbingGymDetailResponseDto(String gymName, String mapImageUrl, String calendarImageUrl, List difficulties, - List sectors) { + List sectors) { } diff --git a/src/main/java/com/first/flash/climbing/gym/application/dto/ClimbingGymResponseDto.java b/src/main/java/com/first/flash/climbing/gym/application/dto/ClimbingGymResponseDto.java deleted file mode 100644 index 5f024d7c..00000000 --- a/src/main/java/com/first/flash/climbing/gym/application/dto/ClimbingGymResponseDto.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.first.flash.climbing.gym.application.dto; - -import com.first.flash.climbing.gym.domian.ClimbingGym; - -public record ClimbingGymResponseDto(Long id, String gymName, String thumbnailUrl) { - - public static ClimbingGymResponseDto toDto(final ClimbingGym gym) { - return new ClimbingGymResponseDto(gym.getId(), gym.getGymName(), gym.getThumbnailUrl()); - } -} diff --git a/src/main/java/com/first/flash/climbing/gym/domian/ClimbingGymRepository.java b/src/main/java/com/first/flash/climbing/gym/domian/ClimbingGymRepository.java index e5e665f0..2367d4ed 100644 --- a/src/main/java/com/first/flash/climbing/gym/domian/ClimbingGymRepository.java +++ b/src/main/java/com/first/flash/climbing/gym/domian/ClimbingGymRepository.java @@ -1,5 +1,7 @@ package com.first.flash.climbing.gym.domian; +import com.first.flash.climbing.gym.infrastructure.dto.ClimbingGymResponseDto; +import com.first.flash.climbing.gym.infrastructure.dto.SectorInfoResponseDto; import java.util.List; import java.util.Optional; @@ -9,7 +11,7 @@ public interface ClimbingGymRepository { Optional findById(final Long id); - List findAll(); + List findAllWithFavorites(final List favoriteGymIds); - List findGymSectorNamesById(final Long id); + List findGymSectorNamesById(final Long id); } diff --git a/src/main/java/com/first/flash/climbing/gym/infrastructure/ClimbingGymJpaRepository.java b/src/main/java/com/first/flash/climbing/gym/infrastructure/ClimbingGymJpaRepository.java index f5a9a619..49749c71 100644 --- a/src/main/java/com/first/flash/climbing/gym/infrastructure/ClimbingGymJpaRepository.java +++ b/src/main/java/com/first/flash/climbing/gym/infrastructure/ClimbingGymJpaRepository.java @@ -1,7 +1,6 @@ package com.first.flash.climbing.gym.infrastructure; import com.first.flash.climbing.gym.domian.ClimbingGym; -import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; @@ -10,6 +9,4 @@ public interface ClimbingGymJpaRepository extends JpaRepository findById(final Long id); - - List findAll(); } diff --git a/src/main/java/com/first/flash/climbing/gym/infrastructure/ClimbingGymQueryDslRepository.java b/src/main/java/com/first/flash/climbing/gym/infrastructure/ClimbingGymQueryDslRepository.java index 215c7d32..010fdda1 100644 --- a/src/main/java/com/first/flash/climbing/gym/infrastructure/ClimbingGymQueryDslRepository.java +++ b/src/main/java/com/first/flash/climbing/gym/infrastructure/ClimbingGymQueryDslRepository.java @@ -1,7 +1,12 @@ package com.first.flash.climbing.gym.infrastructure; +import static com.first.flash.climbing.gym.domian.QClimbingGym.climbingGym; import static com.first.flash.climbing.sector.domain.QSector.sector; +import com.first.flash.climbing.gym.infrastructure.dto.ClimbingGymResponseDto; +import com.first.flash.climbing.gym.infrastructure.dto.SectorInfoResponseDto; +import com.querydsl.core.types.Projections; +import com.querydsl.core.types.dsl.CaseBuilder; import com.querydsl.jpa.impl.JPAQueryFactory; import java.util.List; import lombok.RequiredArgsConstructor; @@ -13,12 +18,29 @@ public class ClimbingGymQueryDslRepository { private final JPAQueryFactory jpaQueryFactory; - public List findSortedSectorNamesByGymId(final Long gymId) { - return jpaQueryFactory.select(sector.sectorName.name) + public List findSortedSectorNamesByGymId(final Long gymId) { + return jpaQueryFactory.select( + Projections.constructor(SectorInfoResponseDto.class, sector.id, + sector.sectorName.name, sector.selectedImageUrl)) .from(sector) .where(sector.gymId.eq(gymId), sector.removalInfo.isExpired.isFalse()) .distinct() .orderBy(sector.sectorName.name.asc()) .fetch(); } + + public List findAllWithFavorites(final List favoriteGymIds) { + return jpaQueryFactory.select( + Projections.constructor(ClimbingGymResponseDto.class, climbingGym.id, + climbingGym.gymName, climbingGym.thumbnailUrl, climbingGym.id.in(favoriteGymIds)) + ) + .from(climbingGym) + .orderBy( + new CaseBuilder() + .when(climbingGym.id.in(favoriteGymIds)).then(1) + .otherwise(0).desc(), + climbingGym.gymName.asc() + ) + .fetch(); + } } diff --git a/src/main/java/com/first/flash/climbing/gym/infrastructure/ClimbingGymRepositoryImpl.java b/src/main/java/com/first/flash/climbing/gym/infrastructure/ClimbingGymRepositoryImpl.java index ea93b6f0..4946d6f1 100644 --- a/src/main/java/com/first/flash/climbing/gym/infrastructure/ClimbingGymRepositoryImpl.java +++ b/src/main/java/com/first/flash/climbing/gym/infrastructure/ClimbingGymRepositoryImpl.java @@ -2,6 +2,8 @@ import com.first.flash.climbing.gym.domian.ClimbingGym; import com.first.flash.climbing.gym.domian.ClimbingGymRepository; +import com.first.flash.climbing.gym.infrastructure.dto.ClimbingGymResponseDto; +import com.first.flash.climbing.gym.infrastructure.dto.SectorInfoResponseDto; import java.util.List; import java.util.Optional; import lombok.RequiredArgsConstructor; @@ -25,12 +27,12 @@ public Optional findById(final Long id) { } @Override - public List findAll() { - return climbingGymJpaRepository.findAll(); - } + public List findAllWithFavorites(final List favoriteGymIds){ + return climbingGymQueryDslRepository.findAllWithFavorites(favoriteGymIds); + }; @Override - public List findGymSectorNamesById(final Long id) { + public List findGymSectorNamesById(final Long id) { return climbingGymQueryDslRepository.findSortedSectorNamesByGymId(id); } } diff --git a/src/main/java/com/first/flash/climbing/gym/infrastructure/dto/ClimbingGymResponseDto.java b/src/main/java/com/first/flash/climbing/gym/infrastructure/dto/ClimbingGymResponseDto.java new file mode 100644 index 00000000..09c2fbc5 --- /dev/null +++ b/src/main/java/com/first/flash/climbing/gym/infrastructure/dto/ClimbingGymResponseDto.java @@ -0,0 +1,11 @@ +package com.first.flash.climbing.gym.infrastructure.dto; + +import com.first.flash.climbing.gym.domian.ClimbingGym; +import java.util.List; + +public record ClimbingGymResponseDto(Long id, String gymName, String thumbnailUrl, boolean isFavorite) { + + public static Object toDto(final ClimbingGym gym, final List favoriteGymIds) { + return new ClimbingGymResponseDto(gym.getId(), gym.getGymName(), gym.getThumbnailUrl(), favoriteGymIds.contains(gym.getId())); + } +} diff --git a/src/main/java/com/first/flash/climbing/gym/infrastructure/dto/SectorInfoResponseDto.java b/src/main/java/com/first/flash/climbing/gym/infrastructure/dto/SectorInfoResponseDto.java new file mode 100644 index 00000000..3d2540d5 --- /dev/null +++ b/src/main/java/com/first/flash/climbing/gym/infrastructure/dto/SectorInfoResponseDto.java @@ -0,0 +1,5 @@ +package com.first.flash.climbing.gym.infrastructure.dto; + +public record SectorInfoResponseDto(Long id, String name, String selectedImageUrl) { + +} diff --git a/src/main/java/com/first/flash/climbing/gym/ui/ClimbingGymController.java b/src/main/java/com/first/flash/climbing/gym/ui/ClimbingGymController.java index 5c4707a8..55e34459 100644 --- a/src/main/java/com/first/flash/climbing/gym/ui/ClimbingGymController.java +++ b/src/main/java/com/first/flash/climbing/gym/ui/ClimbingGymController.java @@ -4,7 +4,7 @@ import com.first.flash.climbing.gym.application.dto.ClimbingGymCreateRequestDto; import com.first.flash.climbing.gym.application.dto.ClimbingGymCreateResponseDto; import com.first.flash.climbing.gym.application.dto.ClimbingGymDetailResponseDto; -import com.first.flash.climbing.gym.application.dto.ClimbingGymResponseDto; +import com.first.flash.climbing.gym.infrastructure.dto.ClimbingGymResponseDto; import com.first.flash.climbing.gym.domian.vo.Difficulty; import com.first.flash.climbing.gym.exception.exceptions.DuplicateDifficultyLevelException; import com.first.flash.climbing.gym.exception.exceptions.DuplicateDifficultyNameException; @@ -27,7 +27,6 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @Tag(name = "gyms", description = "클라이밍장 조회 API") diff --git a/src/main/java/com/first/flash/climbing/hold/application/HoldService.java b/src/main/java/com/first/flash/climbing/hold/application/HoldService.java new file mode 100644 index 00000000..32150b05 --- /dev/null +++ b/src/main/java/com/first/flash/climbing/hold/application/HoldService.java @@ -0,0 +1,50 @@ +package com.first.flash.climbing.hold.application; + +import com.first.flash.climbing.hold.application.dto.HoldCreateRequestDto; +import com.first.flash.climbing.hold.application.dto.HoldResponseDto; +import com.first.flash.climbing.hold.application.dto.HoldsResponseDto; +import com.first.flash.climbing.hold.domain.Hold; +import com.first.flash.climbing.hold.domain.HoldRepository; +import com.first.flash.climbing.hold.exception.exceptions.HoldNotFoundException; +import com.first.flash.climbing.sector.application.dto.SectorCreateRequestDto; +import com.first.flash.climbing.sector.application.dto.SectorDetailResponseDto; +import com.first.flash.climbing.sector.application.dto.SectorsDetailResponseDto; +import com.first.flash.climbing.sector.domain.Sector; +import com.first.flash.climbing.sector.domain.SectorInfo; +import com.first.flash.climbing.sector.exception.exceptions.SectorNotFoundException; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class HoldService { + + private final HoldRepository holdRepository; + + public Hold findById(final Long id) { + return holdRepository.findById(id) + .orElseThrow(() -> new HoldNotFoundException(id)); + } + + public HoldsResponseDto findAllHolds() { + List holdsResponse = holdRepository.findAll() + .stream() + .map(HoldResponseDto::toDto) + .toList(); + + return new HoldsResponseDto(holdsResponse); + } + + @Transactional + public HoldResponseDto saveHold(final HoldCreateRequestDto createRequestDto) { + Hold hold = createHold(createRequestDto); + return HoldResponseDto.toDto(holdRepository.save(hold)); + } + + private Hold createHold(final HoldCreateRequestDto createRequestDto) { + return Hold.of(createRequestDto.colorName(), createRequestDto.colorCode()); + } +} diff --git a/src/main/java/com/first/flash/climbing/hold/application/dto/HoldCreateRequestDto.java b/src/main/java/com/first/flash/climbing/hold/application/dto/HoldCreateRequestDto.java new file mode 100644 index 00000000..8eb98f1c --- /dev/null +++ b/src/main/java/com/first/flash/climbing/hold/application/dto/HoldCreateRequestDto.java @@ -0,0 +1,10 @@ +package com.first.flash.climbing.hold.application.dto; + +import jakarta.validation.constraints.NotNull; +import java.time.LocalDate; + +public record HoldCreateRequestDto( + @NotNull(message = "홀드 색상 이름 정보는 비어있을 수 없습니다.") String colorName, + @NotNull(message = "홀드 색상 코드 정보는 비어있을 수 없습니다.") String colorCode) { + +} diff --git a/src/main/java/com/first/flash/climbing/hold/application/dto/HoldResponseDto.java b/src/main/java/com/first/flash/climbing/hold/application/dto/HoldResponseDto.java new file mode 100644 index 00000000..2b6c5732 --- /dev/null +++ b/src/main/java/com/first/flash/climbing/hold/application/dto/HoldResponseDto.java @@ -0,0 +1,10 @@ +package com.first.flash.climbing.hold.application.dto; + +import com.first.flash.climbing.hold.domain.Hold; + +public record HoldResponseDto(Long id, String colorName, String colorCode) { + + public static HoldResponseDto toDto(final Hold hold) { + return new HoldResponseDto(hold.getId(), hold.getColorName(), hold.getColorCode()); + } +} diff --git a/src/main/java/com/first/flash/climbing/hold/application/dto/HoldsResponseDto.java b/src/main/java/com/first/flash/climbing/hold/application/dto/HoldsResponseDto.java new file mode 100644 index 00000000..1f6d215f --- /dev/null +++ b/src/main/java/com/first/flash/climbing/hold/application/dto/HoldsResponseDto.java @@ -0,0 +1,7 @@ +package com.first.flash.climbing.hold.application.dto; + +import java.util.List; + +public record HoldsResponseDto(List holdList) { + +} diff --git a/src/main/java/com/first/flash/climbing/hold/domain/Hold.java b/src/main/java/com/first/flash/climbing/hold/domain/Hold.java new file mode 100644 index 00000000..0644f76c --- /dev/null +++ b/src/main/java/com/first/flash/climbing/hold/domain/Hold.java @@ -0,0 +1,31 @@ +package com.first.flash.climbing.hold.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Entity +@NoArgsConstructor +@Getter +@ToString +public class Hold { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private String colorName; + private String colorCode; + + protected Hold(final String colorName, final String colorCode) { + this.colorName = colorName; + this.colorCode = colorCode; + } + + public static Hold of(final String colorName, final String colorCode) { + return new Hold(colorName, colorCode); + } +} diff --git a/src/main/java/com/first/flash/climbing/hold/domain/HoldRepository.java b/src/main/java/com/first/flash/climbing/hold/domain/HoldRepository.java new file mode 100644 index 00000000..2bb02724 --- /dev/null +++ b/src/main/java/com/first/flash/climbing/hold/domain/HoldRepository.java @@ -0,0 +1,14 @@ +package com.first.flash.climbing.hold.domain; + +import com.first.flash.climbing.sector.domain.Sector; +import java.util.List; +import java.util.Optional; + +public interface HoldRepository { + + Hold save(final Hold hold); + + Optional findById(final Long id); + + List findAll(); +} diff --git a/src/main/java/com/first/flash/climbing/hold/exception/HoldExceptionHandler.java b/src/main/java/com/first/flash/climbing/hold/exception/HoldExceptionHandler.java new file mode 100644 index 00000000..73054ba1 --- /dev/null +++ b/src/main/java/com/first/flash/climbing/hold/exception/HoldExceptionHandler.java @@ -0,0 +1,25 @@ +package com.first.flash.climbing.hold.exception; + +import com.first.flash.climbing.hold.exception.exceptions.HoldNotFoundException; +import com.first.flash.global.dto.ErrorResponseDto; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +@ControllerAdvice +public class HoldExceptionHandler { + + @ExceptionHandler(HoldNotFoundException.class) + public ResponseEntity handleHoldNotFoundException( + final HoldNotFoundException exception) { + return getResponseWithStatus(HttpStatus.NOT_FOUND, exception); + } + + private ResponseEntity getResponseWithStatus(final HttpStatus httpStatus, + final RuntimeException exception) { + ErrorResponseDto errorResponse = new ErrorResponseDto(exception.getMessage()); + return ResponseEntity.status(httpStatus) + .body(errorResponse); + } +} diff --git a/src/main/java/com/first/flash/climbing/hold/exception/exceptions/HoldNotFoundException.java b/src/main/java/com/first/flash/climbing/hold/exception/exceptions/HoldNotFoundException.java new file mode 100644 index 00000000..c5485321 --- /dev/null +++ b/src/main/java/com/first/flash/climbing/hold/exception/exceptions/HoldNotFoundException.java @@ -0,0 +1,15 @@ +package com.first.flash.climbing.hold.exception.exceptions; + +import com.first.flash.climbing.sector.exception.exceptions.SectorNotFoundException; +import com.first.flash.global.dto.ErrorResponseDto; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +public class HoldNotFoundException extends RuntimeException { + + public HoldNotFoundException(final Long id) { + super(String.format("아이디가 %s인 홀드를 찾을 수 없습니다.", id)); + } +} diff --git a/src/main/java/com/first/flash/climbing/hold/infrastructure/HoldJpaRepository.java b/src/main/java/com/first/flash/climbing/hold/infrastructure/HoldJpaRepository.java new file mode 100644 index 00000000..a78cedcb --- /dev/null +++ b/src/main/java/com/first/flash/climbing/hold/infrastructure/HoldJpaRepository.java @@ -0,0 +1,15 @@ +package com.first.flash.climbing.hold.infrastructure; + +import com.first.flash.climbing.hold.domain.Hold; +import java.util.List; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface HoldJpaRepository extends JpaRepository { + + Hold save(final Hold hold); + + Optional findById(final Long id); + + List findAll(); +} diff --git a/src/main/java/com/first/flash/climbing/hold/infrastructure/HoldRepositoryImpl.java b/src/main/java/com/first/flash/climbing/hold/infrastructure/HoldRepositoryImpl.java new file mode 100644 index 00000000..177ca752 --- /dev/null +++ b/src/main/java/com/first/flash/climbing/hold/infrastructure/HoldRepositoryImpl.java @@ -0,0 +1,30 @@ +package com.first.flash.climbing.hold.infrastructure; + +import com.first.flash.climbing.hold.domain.Hold; +import com.first.flash.climbing.hold.domain.HoldRepository; +import java.util.List; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class HoldRepositoryImpl implements HoldRepository { + + private final HoldJpaRepository holdJpaRepository; + + @Override + public Hold save(Hold hold) { + return holdJpaRepository.save(hold); + } + + @Override + public Optional findById(Long id) { + return holdJpaRepository.findById(id); + } + + @Override + public List findAll() { + return holdJpaRepository.findAll(); + } +} diff --git a/src/main/java/com/first/flash/climbing/hold/ui/HoldController.java b/src/main/java/com/first/flash/climbing/hold/ui/HoldController.java new file mode 100644 index 00000000..01d9a3d9 --- /dev/null +++ b/src/main/java/com/first/flash/climbing/hold/ui/HoldController.java @@ -0,0 +1,52 @@ +package com.first.flash.climbing.hold.ui; + +import com.first.flash.climbing.hold.application.HoldService; +import com.first.flash.climbing.hold.application.dto.HoldCreateRequestDto; +import com.first.flash.climbing.hold.application.dto.HoldResponseDto; +import com.first.flash.climbing.hold.application.dto.HoldsResponseDto; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +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.RestController; + +@Tag(name = "holds", description = "홀드 정보 관리 API") +@RestController +@RequestMapping +@RequiredArgsConstructor +public class HoldController { + + private final HoldService holdService; + + @Operation(summary = "모든 홀드 정보 조회", description = "모든 홀드 정보를 리스트로 반환") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "성공적으로 홀드를 조회", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = HoldsResponseDto.class))), + }) + @GetMapping("holds") + public ResponseEntity findAllHolds() { + return ResponseEntity.ok(holdService.findAllHolds()); + } + + @Operation(summary = "홀드 정보 생성", description = "홀드 정보를 생성") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "홀드 정보를 생성", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = HoldResponseDto.class))), + }) + @PostMapping("holds") + public ResponseEntity createHold( + @Valid @RequestBody final HoldCreateRequestDto createRequestDto) { + return ResponseEntity.status(HttpStatus.CREATED) + .body(holdService.saveHold(createRequestDto)); + } +} \ No newline at end of file diff --git a/src/main/java/com/first/flash/climbing/problem/application/ProblemEventHandler.java b/src/main/java/com/first/flash/climbing/problem/application/ProblemEventHandler.java index 44c3a089..83e5add5 100644 --- a/src/main/java/com/first/flash/climbing/problem/application/ProblemEventHandler.java +++ b/src/main/java/com/first/flash/climbing/problem/application/ProblemEventHandler.java @@ -1,12 +1,14 @@ package com.first.flash.climbing.problem.application; import com.first.flash.climbing.problem.domain.ProblemIdConfirmRequestedEvent; +import com.first.flash.climbing.sector.domain.SectorFixedInfoUpdatedEvent; import com.first.flash.climbing.sector.domain.SectorExpiredEvent; import com.first.flash.climbing.sector.domain.SectorInfoUpdatedEvent; import com.first.flash.climbing.sector.domain.SectorRemovalDateUpdatedEvent; import com.first.flash.climbing.solution.domain.PerceivedDifficultySetEvent; import com.first.flash.climbing.solution.domain.SolutionDeletedEvent; import com.first.flash.climbing.solution.domain.SolutionSavedEvent; +import com.first.flash.climbing.solution.domain.SolutionUpdatedEvent; import lombok.RequiredArgsConstructor; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; @@ -22,7 +24,8 @@ public class ProblemEventHandler { @EventListener @Transactional public void changeRemovalDate(final SectorRemovalDateUpdatedEvent event) { - problemsService.changeRemovalDate(event.getSectorId(), event.getRemovalDate()); + problemsService.changeRemovalDate(event.getSectorId(), event.getRemovalDate(), + event.isExpired()); } @EventListener @@ -34,20 +37,36 @@ public void expireProblem(final SectorExpiredEvent event) { @EventListener @Transactional public void updateProblemSolutionInfo(final SolutionSavedEvent event) { + problemsService.changeThumbnailInfo(event.getProblemId(), event.getSolutionId(), + event.getThumbnailImageUrl(), event.getUploader()); problemsService.updateProblemSolutionInfo(event.getProblemId()); } + @EventListener + @Transactional + public void updateThumbnailInfo(final SolutionUpdatedEvent event) { + problemsService.changeAllThumbnailInfo(event.getSolutionId(), event.getThumbnailImageUrl(), + event.getUploader()); + } + @EventListener @Transactional public void updateProblemDeletedSolutionInfo(final SolutionDeletedEvent event) { - problemsService.updateProblemDeletedSolutionInfo(event.getProblemId(), event.getPerceivedDifficulty()); + problemsService.updateProblemDeletedSolutionInfo(event.getProblemId(), + event.getPerceivedDifficulty()); } @EventListener @Transactional public void updateQueryProblemInfo(final SectorInfoUpdatedEvent event) { problemsService.updateQueryProblemInfo(event.getId(), event.getSectorName(), - event.getSettingDate()); + event.getSettingDate(), event.isExpired()); + } + + @EventListener + @Transactional + public void updateQueryProblemFixedInfo(final SectorFixedInfoUpdatedEvent event) { + problemsService.updateQueryProblemFixedInfo(event.getSectorIds(), event.getSectorName()); } @EventListener @@ -59,6 +78,9 @@ public void confirmProblemId(final ProblemIdConfirmRequestedEvent event) { @EventListener @Transactional public void updatePerceivedDifficulty(final PerceivedDifficultySetEvent event) { - problemsService.addPerceivedDifficulty(event.getProblemId(), event.getPerceivedDifficulty()); + problemsService.addPerceivedDifficulty(event.getProblemId(), + event.getPerceivedDifficulty()); } + + } diff --git a/src/main/java/com/first/flash/climbing/problem/application/ProblemReadService.java b/src/main/java/com/first/flash/climbing/problem/application/ProblemReadService.java index 1dac8299..0e3a18e2 100644 --- a/src/main/java/com/first/flash/climbing/problem/application/ProblemReadService.java +++ b/src/main/java/com/first/flash/climbing/problem/application/ProblemReadService.java @@ -4,7 +4,9 @@ import static com.first.flash.climbing.problem.infrastructure.paging.ProblemSortBy.VIEWS; import com.first.flash.climbing.gym.domian.ClimbingGymIdConfirmRequestedEvent; +import com.first.flash.climbing.problem.application.dto.DuplicateProblemsResponseDto; import com.first.flash.climbing.problem.application.dto.ProblemDetailResponseDto; +import com.first.flash.climbing.problem.application.dto.ProblemResponseDto; import com.first.flash.climbing.problem.application.dto.ProblemsResponseDto; import com.first.flash.climbing.problem.domain.Problem; import com.first.flash.climbing.problem.domain.ProblemRepository; @@ -66,6 +68,13 @@ public QueryProblem findQueryProblemById(final UUID problemId) { () -> new QueryProblemNotFoundException(problemId)); } + public DuplicateProblemsResponseDto findDuplicateProblems(final Long sectorId, final Long holdId, + final String difficulty) { + List duplicateProblems = queryProblemRepository.findBySectorIdAndHoldIdAndDifficulty(sectorId, holdId, difficulty); + + return DuplicateProblemsResponseDto.of(duplicateProblems); + } + private void validateExpiration(final Problem problem, final QueryProblem queryProblem) { if (problem.isExpired() || queryProblem.isExpired()) { throw new QueryProblemExpiredException(problem.getId()); diff --git a/src/main/java/com/first/flash/climbing/problem/application/ProblemsHoldService.java b/src/main/java/com/first/flash/climbing/problem/application/ProblemsHoldService.java new file mode 100644 index 00000000..9abb42d3 --- /dev/null +++ b/src/main/java/com/first/flash/climbing/problem/application/ProblemsHoldService.java @@ -0,0 +1,35 @@ +package com.first.flash.climbing.problem.application; + +import com.first.flash.climbing.hold.application.HoldService; +import com.first.flash.climbing.hold.domain.Hold; +import com.first.flash.climbing.problem.application.dto.ProblemDetailResponseDto; +import com.first.flash.climbing.problem.domain.Problem; +import com.first.flash.climbing.problem.domain.QueryProblem; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ProblemsHoldService { + + private final ProblemReadService problemReadService; + private final HoldService holdService; + + @Transactional + public ProblemDetailResponseDto updateHold(final UUID problemId, + final Long holdId) { + Hold hold = holdService.findById(holdId); + + Problem problem = problemReadService.findProblemById(problemId); + QueryProblem queryProblem = problemReadService.findQueryProblemById(problemId); + + problem.updateHoldInfo(hold.getId()); + queryProblem.updateHoldInfo(hold.getId(), hold.getColorName(), hold.getColorCode()); + + return ProblemDetailResponseDto.of(queryProblem); + } + +} diff --git a/src/main/java/com/first/flash/climbing/problem/application/ProblemsSaveService.java b/src/main/java/com/first/flash/climbing/problem/application/ProblemsSaveService.java index d8a09380..346767e2 100644 --- a/src/main/java/com/first/flash/climbing/problem/application/ProblemsSaveService.java +++ b/src/main/java/com/first/flash/climbing/problem/application/ProblemsSaveService.java @@ -2,6 +2,8 @@ import com.first.flash.climbing.gym.application.ClimbingGymService; import com.first.flash.climbing.gym.domian.ClimbingGym; +import com.first.flash.climbing.hold.application.HoldService; +import com.first.flash.climbing.hold.domain.Hold; import com.first.flash.climbing.problem.application.dto.ProblemCreateResponseDto; import com.first.flash.climbing.problem.domain.Problem; import com.first.flash.climbing.problem.domain.ProblemRepository; @@ -24,6 +26,7 @@ public class ProblemsSaveService { private final QueryProblemRepository queryProblemRepository; private final ClimbingGymService climbingGymService; private final SectorService sectorService; + private final HoldService holdService; private final ProblemsCreateService problemsCreateService; @Transactional @@ -31,10 +34,11 @@ public ProblemCreateResponseDto saveProblems(final Long gymId, final Long sector final ProblemCreateRequestDto createRequestDto) { ClimbingGym climbingGym = climbingGymService.findClimbingGymById(gymId); Sector sector = sectorService.findById(sectorId); + Hold hold = holdService.findById(createRequestDto.holdId()); Problem problem = problemsCreateService.createProblem(climbingGym, sector, createRequestDto); QueryProblem queryProblem = problemsCreateService.createQueryProblem(climbingGym, - sector, problem); + sector, problem, hold); problemRepository.save(problem); queryProblemRepository.save(queryProblem); return ProblemCreateResponseDto.toDto(problem); diff --git a/src/main/java/com/first/flash/climbing/problem/application/ProblemsService.java b/src/main/java/com/first/flash/climbing/problem/application/ProblemsService.java index 0685d7ce..f6b92015 100644 --- a/src/main/java/com/first/flash/climbing/problem/application/ProblemsService.java +++ b/src/main/java/com/first/flash/climbing/problem/application/ProblemsService.java @@ -1,9 +1,13 @@ package com.first.flash.climbing.problem.application; import com.first.flash.climbing.problem.application.dto.ProblemDetailResponseDto; +import com.first.flash.climbing.problem.application.dto.ProblemResponseDto; +import com.first.flash.climbing.problem.application.dto.ProblemsResponseDto; +import com.first.flash.climbing.problem.domain.Problem; import com.first.flash.climbing.problem.domain.ProblemRepository; import com.first.flash.climbing.problem.domain.QueryProblem; import com.first.flash.climbing.problem.domain.QueryProblemRepository; +import com.first.flash.climbing.problem.infrastructure.dto.ThumbnailSolutionDto; import java.time.LocalDate; import java.util.List; import java.util.UUID; @@ -21,8 +25,8 @@ public class ProblemsService { private final ProblemReadService problemReadService; @Transactional - public void changeRemovalDate(final Long sectorId, final LocalDate removalDate) { - queryProblemRepository.updateRemovalDateBySectorId(sectorId, removalDate); + public void changeRemovalDate(final Long sectorId, final LocalDate removalDate, final boolean isExpired) { + queryProblemRepository.updateRemovalDateBySectorId(sectorId, removalDate, isExpired); } @Transactional @@ -30,6 +34,30 @@ public void expireProblems(final List expiredSectorsIds) { queryProblemRepository.expireProblemsBySectorIds(expiredSectorsIds); problemRepository.expireProblemsBySectorIds(expiredSectorsIds); } + + @Transactional + public void changeThumbnailInfo(final UUID problemId, final Long solutionId, + final String thumbnailImageUrl, final String uploader) { + Problem problem = problemReadService.findProblemById(problemId); + QueryProblem queryProblem = problemReadService.findQueryProblemById(problemId); + + if (queryProblem.getHasSolution()) { + return; + } + + problem.setThumbnailInfo(solutionId, thumbnailImageUrl, uploader); + queryProblem.setThumbnailInfo(solutionId, thumbnailImageUrl, uploader); + } + + @Transactional + public void changeAllThumbnailInfo(final Long solutionId, final String thumbnailImageUrl, final String uploader) { + List problems = problemRepository.findProblemsByThumbnailSolutionId(solutionId); + List queryProblems = queryProblemRepository.findProblemsByThumbnailSolutionId( + solutionId); + + problems.forEach(problem -> problem.setThumbnailInfo(solutionId, thumbnailImageUrl, uploader)); + queryProblems.forEach(queryProblem -> queryProblem.setThumbnailInfo(solutionId, thumbnailImageUrl, uploader)); + } @Transactional public void updateProblemSolutionInfo(final UUID problemId) { @@ -38,16 +66,29 @@ public void updateProblemSolutionInfo(final UUID problemId) { } @Transactional - public void updateProblemDeletedSolutionInfo(final UUID problemId, final Integer perceivedDifficulty) { + public void updateProblemDeletedSolutionInfo(final UUID problemId, + final Integer perceivedDifficulty) { + Problem problem = problemReadService.findProblemById(problemId); QueryProblem queryProblem = problemReadService.findQueryProblemById(problemId); + queryProblem.decrementSolutionCount(); + if (!queryProblem.getHasSolution()) { + problemRepository.deleteByProblemId(problemId); + queryProblemRepository.deleteByProblemId(problemId); + return; + } + + ThumbnailSolutionDto dto = problemRepository.findNextSolution(problemId); + problem.setThumbnailInfo(dto.solutionId(), dto.thumbnailImageUrl(), dto.uploader()); + queryProblem.setThumbnailInfo(dto.solutionId(), dto.thumbnailImageUrl(), dto.uploader()); + queryProblem.subtractPerceivedDifficulty(perceivedDifficulty); } @Transactional public void updateQueryProblemInfo(final Long sectorId, final String sectorName, - final LocalDate settingDate) { - queryProblemRepository.updateQueryProblemInfo(sectorId, sectorName, settingDate); + final LocalDate settingDate, final boolean isExpired) { + queryProblemRepository.updateQueryProblemInfo(sectorId, sectorName, settingDate, isExpired); } @Transactional @@ -57,9 +98,15 @@ public void addPerceivedDifficulty(final UUID problemId, final Integer perceived } @Transactional - public ProblemDetailResponseDto setPerceivedDifficulty(final UUID problemId, final Integer perceivedDifficulty) { + public ProblemDetailResponseDto setPerceivedDifficulty(final UUID problemId, + final Integer perceivedDifficulty) { QueryProblem queryProblem = problemReadService.findQueryProblemById(problemId); queryProblem.setPerceivedDifficulty(perceivedDifficulty); return ProblemDetailResponseDto.of(queryProblem); } + + public void updateQueryProblemFixedInfo(final List sectorIds, final String sectorName) { + queryProblemRepository.updateSectorNameBySectorIds(sectorIds, sectorName); + } + } diff --git a/src/main/java/com/first/flash/climbing/problem/application/dto/DuplicateProblemsResponseDto.java b/src/main/java/com/first/flash/climbing/problem/application/dto/DuplicateProblemsResponseDto.java new file mode 100644 index 00000000..b4646e57 --- /dev/null +++ b/src/main/java/com/first/flash/climbing/problem/application/dto/DuplicateProblemsResponseDto.java @@ -0,0 +1,14 @@ +package com.first.flash.climbing.problem.application.dto; + +import com.first.flash.climbing.problem.domain.QueryProblem; +import com.first.flash.global.paging.Meta; +import java.util.List; + +public record DuplicateProblemsResponseDto(List problems) { + + public static DuplicateProblemsResponseDto of(final List queryProblems) { + return new DuplicateProblemsResponseDto(queryProblems.stream() + .map(ProblemResponseDto::toDto) + .toList()); + } +} diff --git a/src/main/java/com/first/flash/climbing/problem/application/dto/ProblemCreateResponseDto.java b/src/main/java/com/first/flash/climbing/problem/application/dto/ProblemCreateResponseDto.java index eace0f4d..99e59dae 100644 --- a/src/main/java/com/first/flash/climbing/problem/application/dto/ProblemCreateResponseDto.java +++ b/src/main/java/com/first/flash/climbing/problem/application/dto/ProblemCreateResponseDto.java @@ -8,7 +8,8 @@ @Builder public record ProblemCreateResponseDto(UUID id, String imageUrl, Integer views, Boolean isExpired, DifficultyInfo difficultyInfo, Long optionalWeight, - Long gymId, Long sectorId, String imageSource + Long gymId, Long sectorId, String imageSource, + Long thumbnailSolutionId, Long holdId ) { public static ProblemCreateResponseDto toDto(final Problem problem) { @@ -22,6 +23,8 @@ public static ProblemCreateResponseDto toDto(final Problem problem) { .gymId(problem.getGymId()) .sectorId(problem.getSectorId()) .imageSource(problem.getImageSource()) + .thumbnailSolutionId(problem.getThumbnailSolutionId()) + .holdId(problem.getHoldId()) .build(); } } diff --git a/src/main/java/com/first/flash/climbing/problem/application/dto/ProblemDetailResponseDto.java b/src/main/java/com/first/flash/climbing/problem/application/dto/ProblemDetailResponseDto.java index b3d8a606..175cf9c5 100644 --- a/src/main/java/com/first/flash/climbing/problem/application/dto/ProblemDetailResponseDto.java +++ b/src/main/java/com/first/flash/climbing/problem/application/dto/ProblemDetailResponseDto.java @@ -7,13 +7,14 @@ public record ProblemDetailResponseDto(UUID id, String sector, String difficulty, LocalDate settingDate, LocalDate removalDate, boolean isFakeRemovalDate, boolean hasSolution, + String holdColorCode, String imageUrl, String gymName, String imageSource, Boolean isHoney) { public static ProblemDetailResponseDto of(final QueryProblem queryProblem) { return new ProblemDetailResponseDto(queryProblem.getId(), queryProblem.getSectorName(), queryProblem.getDifficultyName(), queryProblem.getSettingDate(), queryProblem.getRemovalDate(), queryProblem.getIsFakeRemovalDate(), - queryProblem.getHasSolution(), queryProblem.getImageUrl(), queryProblem.getGymName(), - queryProblem.getImageSource(), queryProblem.isHoney()); + queryProblem.getHasSolution(), queryProblem.getHoldColorCode(), queryProblem.getImageUrl(), + queryProblem.getGymName(), queryProblem.getImageSource(), queryProblem.isHoney()); } } diff --git a/src/main/java/com/first/flash/climbing/problem/application/dto/ProblemHoldRequestDto.java b/src/main/java/com/first/flash/climbing/problem/application/dto/ProblemHoldRequestDto.java new file mode 100644 index 00000000..82407017 --- /dev/null +++ b/src/main/java/com/first/flash/climbing/problem/application/dto/ProblemHoldRequestDto.java @@ -0,0 +1,8 @@ +package com.first.flash.climbing.problem.application.dto; + +import jakarta.validation.constraints.NotNull; + +public record ProblemHoldRequestDto( + @NotNull(message = "변경할 홀드 id는 필수입니다.") Long holdId) { + +} diff --git a/src/main/java/com/first/flash/climbing/problem/application/dto/ProblemResponseDto.java b/src/main/java/com/first/flash/climbing/problem/application/dto/ProblemResponseDto.java index fa0659ae..f74b4780 100644 --- a/src/main/java/com/first/flash/climbing/problem/application/dto/ProblemResponseDto.java +++ b/src/main/java/com/first/flash/climbing/problem/application/dto/ProblemResponseDto.java @@ -6,12 +6,13 @@ public record ProblemResponseDto(UUID id, String sector, String difficulty, LocalDate settingDate, LocalDate removalDate, boolean hasSolution, String imageUrl, - Boolean isHoney, Integer solutionCount) { + String holdColorCode, Boolean isHoney, Integer solutionCount) { public static ProblemResponseDto toDto(QueryProblem queryProblem) { return new ProblemResponseDto(queryProblem.getId(), queryProblem.getSectorName(), queryProblem.getDifficultyName(), queryProblem.getSettingDate(), queryProblem.getRemovalDate(), queryProblem.getHasSolution(), - queryProblem.getImageUrl(), queryProblem.isHoney(), queryProblem.getSolutionCount()); + queryProblem.getImageUrl(), queryProblem.getHoldColorCode(), + queryProblem.isHoney(), queryProblem.getSolutionCount()); } } diff --git a/src/main/java/com/first/flash/climbing/problem/domain/Problem.java b/src/main/java/com/first/flash/climbing/problem/domain/Problem.java index f01cff00..b5d24701 100644 --- a/src/main/java/com/first/flash/climbing/problem/domain/Problem.java +++ b/src/main/java/com/first/flash/climbing/problem/domain/Problem.java @@ -10,6 +10,7 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.Setter; import lombok.ToString; @Entity @@ -35,10 +36,12 @@ public class Problem { private Long gymId; private Long sectorId; private String imageSource; + private Long thumbnailSolutionId; + private Long holdId; public static Problem createDefault(final UUID id, final String imageUrl, final String difficultyName, final Integer difficultyLevel, final Long gymId, - final Long sectorId, final String imageSource) { + final Long sectorId, final String imageSource, final Long thumbnailSolutionId, final Long holdId) { return Problem.builder() .id(id) .imageUrl(imageUrl) @@ -49,6 +52,8 @@ public static Problem createDefault(final UUID id, final String imageUrl, .gymId(gymId) .sectorId(sectorId) .imageSource(imageSource) + .thumbnailSolutionId(thumbnailSolutionId) + .holdId(holdId) .build(); } @@ -59,4 +64,16 @@ public void view() { public boolean isExpired() { return isExpired; } + + public void setThumbnailInfo(final Long thumbnailSolutionId, final String imageUrl, + final String imageSource) { + this.thumbnailSolutionId = thumbnailSolutionId; + this.imageUrl = imageUrl; + this.imageSource = imageSource; + } + + public void updateHoldInfo(final Long holdId) { + this.holdId = holdId; + } + } diff --git a/src/main/java/com/first/flash/climbing/problem/domain/ProblemRepository.java b/src/main/java/com/first/flash/climbing/problem/domain/ProblemRepository.java index 4ec5e862..36ae7b41 100644 --- a/src/main/java/com/first/flash/climbing/problem/domain/ProblemRepository.java +++ b/src/main/java/com/first/flash/climbing/problem/domain/ProblemRepository.java @@ -1,5 +1,7 @@ package com.first.flash.climbing.problem.domain; +import com.first.flash.climbing.problem.infrastructure.dto.ThumbnailSolutionDto; +import com.first.flash.climbing.solution.domain.Solution; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -11,4 +13,10 @@ public interface ProblemRepository { Optional findById(final UUID id); void expireProblemsBySectorIds(final List expiredSectorsIds); + + void deleteByProblemId(final UUID problemId); + + ThumbnailSolutionDto findNextSolution(final UUID problemId); + + List findProblemsByThumbnailSolutionId(final Long solutionId); } diff --git a/src/main/java/com/first/flash/climbing/problem/domain/ProblemsCreateService.java b/src/main/java/com/first/flash/climbing/problem/domain/ProblemsCreateService.java index 650529fc..6f85b972 100644 --- a/src/main/java/com/first/flash/climbing/problem/domain/ProblemsCreateService.java +++ b/src/main/java/com/first/flash/climbing/problem/domain/ProblemsCreateService.java @@ -2,6 +2,7 @@ import com.first.flash.climbing.gym.domian.ClimbingGym; import com.first.flash.climbing.gym.domian.vo.Difficulty; +import com.first.flash.climbing.hold.domain.Hold; import com.first.flash.climbing.problem.domain.dto.ProblemCreateRequestDto; import com.first.flash.climbing.problem.util.UUIDGenerator; import com.first.flash.climbing.sector.domain.Sector; @@ -26,11 +27,12 @@ public Problem createProblem(final ClimbingGym climbingGym, final Sector sector, UUID generatedUUID = uuidGenerator.generate(); return Problem.createDefault(generatedUUID, createRequestDto.imageUrl(), - difficulty.getName(), difficulty.getLevel(), climbingGym.getId(), sector.getId(), createRequestDto.imageSource()); + difficulty.getName(), difficulty.getLevel(), climbingGym.getId(), sector.getId(), + createRequestDto.imageSource(), createRequestDto.thumbnailSolutionId(), createRequestDto.holdId()); } public QueryProblem createQueryProblem(final ClimbingGym climbingGym, final Sector sector, - final Problem problem) { + final Problem problem, final Hold hold) { return QueryProblem.builder() .id(problem.getId()) .imageUrl(problem.getImageUrl()) @@ -51,6 +53,10 @@ public QueryProblem createQueryProblem(final ClimbingGym climbingGym, final Sect .sectorName(sector.getSectorName().getName()) .settingDate(sector.getSettingDate()) .removalDate(sector.getRemovalDate()) + .thumbnailSolutionId(problem.getThumbnailSolutionId()) + .holdId(hold.getId()) + .holdColorName(hold.getColorName()) + .holdColorCode(hold.getColorCode()) .build(); } } diff --git a/src/main/java/com/first/flash/climbing/problem/domain/QueryProblem.java b/src/main/java/com/first/flash/climbing/problem/domain/QueryProblem.java index 65583af8..51b39fb7 100644 --- a/src/main/java/com/first/flash/climbing/problem/domain/QueryProblem.java +++ b/src/main/java/com/first/flash/climbing/problem/domain/QueryProblem.java @@ -58,6 +58,10 @@ public class QueryProblem { private String sectorName; private LocalDate settingDate; private LocalDate removalDate; + private Long thumbnailSolutionId; + private Long holdId; + private String holdColorName; + private String holdColorCode; public boolean isExpired() { return isExpired; @@ -94,6 +98,19 @@ public Boolean isHoney() { return perceivedDifficulty < 0; } + public void setThumbnailInfo(final Long thumbnailSolutionId, final String imageUrl, + final String imageSource) { + this.thumbnailSolutionId = thumbnailSolutionId; + this.imageUrl = imageUrl; + this.imageSource = imageSource; + } + + public void updateHoldInfo(final Long holdId, final String holdColorName, final String holdColorCode) { + this.holdId = holdId; + this.holdColorName = holdColorName; + this.holdColorCode = holdColorCode; + } + private void enableSolution() { if (!hasSolution) { hasSolution = true; @@ -109,4 +126,5 @@ private void calculateRecommendationValue() { (STANDARD_VIEW_COUNT + difficultyLevel * DIFFICULTY_LEVEL_WEIGHT) * solutionCount + optionalWeight; } + } diff --git a/src/main/java/com/first/flash/climbing/problem/domain/QueryProblemRepository.java b/src/main/java/com/first/flash/climbing/problem/domain/QueryProblemRepository.java index b0aea189..e4782f2b 100644 --- a/src/main/java/com/first/flash/climbing/problem/domain/QueryProblemRepository.java +++ b/src/main/java/com/first/flash/climbing/problem/domain/QueryProblemRepository.java @@ -17,10 +17,18 @@ List findAll(final ProblemCursor preProblemCursor, final ProblemSo final Long gymId, final List difficulty, final List sector, final Boolean hasSolution, final Boolean isHoney); - void updateRemovalDateBySectorId(final Long sectorId, final LocalDate removalDate); + void updateRemovalDateBySectorId(final Long sectorId, final LocalDate removalDate, final boolean isExpired); void expireProblemsBySectorIds(final List expiredSectorsIds); void updateQueryProblemInfo(final Long sectorId, final String sectorName, - final LocalDate settingDate); + final LocalDate settingDate, final boolean isExpired); + + void updateSectorNameBySectorIds(final List sectorIds, final String sectorName); + + void deleteByProblemId(final UUID problemId); + + List findBySectorIdAndHoldIdAndDifficulty(Long sectorId, Long holdId, String difficulty); + + List findProblemsByThumbnailSolutionId(Long solutionId); } diff --git a/src/main/java/com/first/flash/climbing/problem/domain/dto/ProblemCreateRequestDto.java b/src/main/java/com/first/flash/climbing/problem/domain/dto/ProblemCreateRequestDto.java index 61b2b033..e966ad9b 100644 --- a/src/main/java/com/first/flash/climbing/problem/domain/dto/ProblemCreateRequestDto.java +++ b/src/main/java/com/first/flash/climbing/problem/domain/dto/ProblemCreateRequestDto.java @@ -1,10 +1,13 @@ package com.first.flash.climbing.problem.domain.dto; import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; public record ProblemCreateRequestDto( - @NotEmpty(message = "이미지 URL은 필수입니다.") String imageUrl, + String imageUrl, + String imageSource, + Long thumbnailSolutionId, @NotEmpty(message = "난이도는 필수입니다.") String difficulty, - @NotEmpty(message = "이미지 출처는 필수입니다.") String imageSource) { + @NotNull(message = "홀드 아이디는 필수입니다.") Long holdId) { } diff --git a/src/main/java/com/first/flash/climbing/problem/infrastructure/ProblemJpaRepository.java b/src/main/java/com/first/flash/climbing/problem/infrastructure/ProblemJpaRepository.java index 1df977ce..2ee70f56 100644 --- a/src/main/java/com/first/flash/climbing/problem/infrastructure/ProblemJpaRepository.java +++ b/src/main/java/com/first/flash/climbing/problem/infrastructure/ProblemJpaRepository.java @@ -1,6 +1,7 @@ package com.first.flash.climbing.problem.infrastructure; import com.first.flash.climbing.problem.domain.Problem; +import java.util.List; import java.util.Optional; import java.util.UUID; import org.springframework.data.jpa.repository.JpaRepository; @@ -10,4 +11,8 @@ public interface ProblemJpaRepository extends JpaRepository { Problem save(final Problem problem); Optional findById(final UUID id); + + void deleteById(final UUID id); + + List findProblemsByThumbnailSolutionId(final Long solutionId); } diff --git a/src/main/java/com/first/flash/climbing/problem/infrastructure/ProblemQueryDslRepository.java b/src/main/java/com/first/flash/climbing/problem/infrastructure/ProblemQueryDslRepository.java index 3a48e71a..29962971 100644 --- a/src/main/java/com/first/flash/climbing/problem/infrastructure/ProblemQueryDslRepository.java +++ b/src/main/java/com/first/flash/climbing/problem/infrastructure/ProblemQueryDslRepository.java @@ -2,8 +2,12 @@ import static com.first.flash.climbing.problem.domain.QProblem.problem; +import com.first.flash.climbing.problem.infrastructure.dto.ThumbnailSolutionDto; +import com.first.flash.climbing.solution.domain.QSolution; +import com.querydsl.core.types.Projections; import com.querydsl.jpa.impl.JPAQueryFactory; import java.util.List; +import java.util.UUID; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; @@ -19,4 +23,18 @@ public void expireProblemsBySectorIds(final List expiredSectorsIds) { .where(problem.sectorId.in(expiredSectorsIds)) .execute(); } + + public ThumbnailSolutionDto findNextSolution(UUID problemId) { + QSolution solution = QSolution.solution; + + return queryFactory.select(Projections.constructor(ThumbnailSolutionDto.class, + solution.id, + solution.uploaderDetail.uploader, + solution.solutionDetail.thumbnailImageUrl)) + .from(solution) + .where(solution.problemId.eq(problemId)) + .orderBy(solution.createdAt.asc()) + .limit(1) + .fetchOne(); + } } diff --git a/src/main/java/com/first/flash/climbing/problem/infrastructure/ProblemRepositoryImpl.java b/src/main/java/com/first/flash/climbing/problem/infrastructure/ProblemRepositoryImpl.java index 1412aed0..6e176191 100644 --- a/src/main/java/com/first/flash/climbing/problem/infrastructure/ProblemRepositoryImpl.java +++ b/src/main/java/com/first/flash/climbing/problem/infrastructure/ProblemRepositoryImpl.java @@ -2,6 +2,8 @@ import com.first.flash.climbing.problem.domain.Problem; import com.first.flash.climbing.problem.domain.ProblemRepository; +import com.first.flash.climbing.problem.infrastructure.dto.ThumbnailSolutionDto; +import com.first.flash.climbing.solution.domain.Solution; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -29,4 +31,19 @@ public Optional findById(final UUID id) { public void expireProblemsBySectorIds(final List expiredSectorsIds) { queryDslRepository.expireProblemsBySectorIds(expiredSectorsIds); } + + @Override + public void deleteByProblemId(final UUID problemId) { + jpaRepository.deleteById(problemId); + } + + @Override + public ThumbnailSolutionDto findNextSolution(final UUID problemId) { + return queryDslRepository.findNextSolution(problemId); + } + + @Override + public List findProblemsByThumbnailSolutionId(final Long solutionId) { + return jpaRepository.findProblemsByThumbnailSolutionId(solutionId); + } } diff --git a/src/main/java/com/first/flash/climbing/problem/infrastructure/QueryProblemJpaRepository.java b/src/main/java/com/first/flash/climbing/problem/infrastructure/QueryProblemJpaRepository.java index ca0b0637..29e7da6f 100644 --- a/src/main/java/com/first/flash/climbing/problem/infrastructure/QueryProblemJpaRepository.java +++ b/src/main/java/com/first/flash/climbing/problem/infrastructure/QueryProblemJpaRepository.java @@ -1,6 +1,7 @@ package com.first.flash.climbing.problem.infrastructure; import com.first.flash.climbing.problem.domain.QueryProblem; +import java.util.List; import java.util.Optional; import java.util.UUID; import org.springframework.data.jpa.repository.JpaRepository; @@ -10,4 +11,10 @@ public interface QueryProblemJpaRepository extends JpaRepository findById(final UUID id); + + void deleteById(final UUID id); + + List findBySectorIdAndHoldIdAndDifficultyName(final Long sectorId, final Long holdId, final String difficulty); + + List findProblemsByThumbnailSolutionId(final Long solutionId); } diff --git a/src/main/java/com/first/flash/climbing/problem/infrastructure/QueryProblemQueryDslRepository.java b/src/main/java/com/first/flash/climbing/problem/infrastructure/QueryProblemQueryDslRepository.java index 508292e0..e40ec488 100644 --- a/src/main/java/com/first/flash/climbing/problem/infrastructure/QueryProblemQueryDslRepository.java +++ b/src/main/java/com/first/flash/climbing/problem/infrastructure/QueryProblemQueryDslRepository.java @@ -1,9 +1,11 @@ package com.first.flash.climbing.problem.infrastructure; +import static com.first.flash.climbing.hold.domain.QHold.hold; import static com.first.flash.climbing.problem.domain.QQueryProblem.queryProblem; import static com.first.flash.climbing.problem.infrastructure.paging.ProblemSortBy.DIFFICULTY; import static com.first.flash.climbing.problem.infrastructure.paging.ProblemSortBy.VIEWS; +import com.first.flash.climbing.hold.domain.Hold; import com.first.flash.climbing.problem.domain.QueryProblem; import com.first.flash.climbing.problem.infrastructure.paging.ProblemCursor; import com.first.flash.climbing.problem.infrastructure.paging.ProblemSortBy; @@ -23,21 +25,25 @@ public class QueryProblemQueryDslRepository { private final JPAQueryFactory queryFactory; - public List findAll(final ProblemCursor prevProblemCursor, final ProblemSortBy problemSortBy, final int size, + public List findAll(final ProblemCursor prevProblemCursor, + final ProblemSortBy problemSortBy, final int size, final Long gymId, final List difficulty, final List sector, final Boolean hasSolution, final Boolean isHoney) { return queryFactory .selectFrom(queryProblem) - .where(notExpired(), cursorCondition(prevProblemCursor), inGym(gymId), inSectors(sector), + .where(notExpired(), cursorCondition(prevProblemCursor), inGym(gymId), + inSectors(sector), inDifficulties(difficulty), hasSolution(hasSolution), isHoneyCondition(isHoney)) .orderBy(sortItem(problemSortBy), queryProblem.id.desc()) .limit(size) .fetch(); } - public void updateRemovalDateBySectorId(final Long sectorId, final LocalDate removalDate) { + public void updateRemovalDateBySectorId(final Long sectorId, final LocalDate removalDate, + final boolean isExpired) { queryFactory.update(queryProblem) .set(queryProblem.removalDate, removalDate) + .set(queryProblem.isExpired, isExpired) .set(queryProblem.isFakeRemovalDate, false) .where(queryProblem.sectorId.eq(sectorId)) .execute(); @@ -51,10 +57,11 @@ public void expireProblemsBySectorIds(final List expiredSectorsIds) { } public void updateQueryProblemInfo(final Long sectorId, final String sectorName, - final LocalDate settingDate) { + final LocalDate settingDate, final boolean isExpired) { queryFactory.update(queryProblem) .set(queryProblem.sectorName, sectorName) .set(queryProblem.settingDate, settingDate) + .set(queryProblem.isExpired, isExpired) .where(queryProblem.sectorId.eq(sectorId)) .execute(); } @@ -133,4 +140,11 @@ private BooleanExpression hasSolution(final Boolean hasSolution) { } return queryProblem.hasSolution.eq(hasSolution); } + + public void updateSectorNameBySectorIds(final List sectorIds, final String sectorName) { + queryFactory.update(queryProblem) + .set(queryProblem.sectorName, sectorName) + .where(queryProblem.sectorId.in(sectorIds)) + .execute(); + } } diff --git a/src/main/java/com/first/flash/climbing/problem/infrastructure/QueryProblemRepositoryImpl.java b/src/main/java/com/first/flash/climbing/problem/infrastructure/QueryProblemRepositoryImpl.java index b804ae28..33be9dca 100644 --- a/src/main/java/com/first/flash/climbing/problem/infrastructure/QueryProblemRepositoryImpl.java +++ b/src/main/java/com/first/flash/climbing/problem/infrastructure/QueryProblemRepositoryImpl.java @@ -29,7 +29,8 @@ public Optional findById(final UUID id) { } @Override - public List findAll(final ProblemCursor prevProblemCursor, final ProblemSortBy problemSortBy, final int size, + public List findAll(final ProblemCursor prevProblemCursor, + final ProblemSortBy problemSortBy, final int size, final Long gymId, final List difficulty, final List sector, final Boolean hasSolution, final Boolean isHoney) { return queryProblemQueryDslRepository.findAll(prevProblemCursor, problemSortBy, size, @@ -37,8 +38,8 @@ public List findAll(final ProblemCursor prevProblemCursor, final P } @Override - public void updateRemovalDateBySectorId(final Long sectorId, final LocalDate removalDate) { - queryProblemQueryDslRepository.updateRemovalDateBySectorId(sectorId, removalDate); + public void updateRemovalDateBySectorId(final Long sectorId, final LocalDate removalDate, final boolean isExpired) { + queryProblemQueryDslRepository.updateRemovalDateBySectorId(sectorId, removalDate, isExpired); } @Override @@ -48,7 +49,29 @@ public void expireProblemsBySectorIds(final List expiredSectorsIds) { @Override public void updateQueryProblemInfo(final Long sectorId, final String sectorName, - final LocalDate settingDate) { - queryProblemQueryDslRepository.updateQueryProblemInfo(sectorId, sectorName, settingDate); + final LocalDate settingDate, final boolean isExpired) { + queryProblemQueryDslRepository.updateQueryProblemInfo(sectorId, sectorName, settingDate, + isExpired); + } + + @Override + public void updateSectorNameBySectorIds(final List sectorIds, final String sectorName) { + queryProblemQueryDslRepository.updateSectorNameBySectorIds(sectorIds, sectorName); + } + + @Override + public void deleteByProblemId(UUID problemId) { + jpaRepository.deleteById(problemId); + } + + @Override + public List findBySectorIdAndHoldIdAndDifficulty(Long sectorId, Long holdId, + String difficulty) { + return jpaRepository.findBySectorIdAndHoldIdAndDifficultyName(sectorId, holdId, difficulty); + } + + @Override + public List findProblemsByThumbnailSolutionId(Long solutionId) { + return jpaRepository.findProblemsByThumbnailSolutionId(solutionId); } } diff --git a/src/main/java/com/first/flash/climbing/problem/infrastructure/dto/ThumbnailSolutionDto.java b/src/main/java/com/first/flash/climbing/problem/infrastructure/dto/ThumbnailSolutionDto.java new file mode 100644 index 00000000..38b1b612 --- /dev/null +++ b/src/main/java/com/first/flash/climbing/problem/infrastructure/dto/ThumbnailSolutionDto.java @@ -0,0 +1,7 @@ +package com.first.flash.climbing.problem.infrastructure.dto; + +import java.util.UUID; + +public record ThumbnailSolutionDto(Long solutionId, String uploader, String thumbnailImageUrl) { + +} diff --git a/src/main/java/com/first/flash/climbing/problem/ui/ProblemController.java b/src/main/java/com/first/flash/climbing/problem/ui/ProblemController.java index fd4a6ce5..0e63598c 100644 --- a/src/main/java/com/first/flash/climbing/problem/ui/ProblemController.java +++ b/src/main/java/com/first/flash/climbing/problem/ui/ProblemController.java @@ -1,10 +1,13 @@ package com.first.flash.climbing.problem.ui; import com.first.flash.climbing.problem.application.ProblemReadService; +import com.first.flash.climbing.problem.application.ProblemsHoldService; import com.first.flash.climbing.problem.application.ProblemsSaveService; import com.first.flash.climbing.problem.application.ProblemsService; +import com.first.flash.climbing.problem.application.dto.DuplicateProblemsResponseDto; import com.first.flash.climbing.problem.application.dto.ProblemCreateResponseDto; import com.first.flash.climbing.problem.application.dto.ProblemDetailResponseDto; +import com.first.flash.climbing.problem.application.dto.ProblemHoldRequestDto; import com.first.flash.climbing.problem.application.dto.ProblemPerceivedDifficultyRequestDto; import com.first.flash.climbing.problem.application.dto.ProblemsResponseDto; import com.first.flash.climbing.problem.domain.dto.ProblemCreateRequestDto; @@ -40,6 +43,7 @@ public class ProblemController { private final ProblemsSaveService problemsSaveService; private final ProblemReadService problemReadService; private final ProblemsService problemsService; + private final ProblemsHoldService problemsHoldService; @Operation(summary = "문제 생성", description = "특정 섹터에 문제 생성") @ApiResponses(value = { @@ -56,7 +60,7 @@ public class ProblemController { @ExampleObject(name = "난이도 없음", value = "{\"error\": \"이름이 핑크인 난이도를 찾을 수 없습니다.\"}") })) }) - @PostMapping("/admin/gyms/{gymId}/sectors/{sectorId}/problems") + @PostMapping("/gyms/{gymId}/sectors/{sectorId}/problems") public ResponseEntity saveProblems( @PathVariable("gymId") final Long gymId, @PathVariable("sectorId") final Long sectorId, @@ -123,4 +127,40 @@ public ResponseEntity changePerceivedDifficulty( @Valid @RequestBody final ProblemPerceivedDifficultyRequestDto requestDto) { return ResponseEntity.ok(problemsService.setPerceivedDifficulty(problemId, requestDto.perceivedDifficulty())); } + + @Operation(summary = "문제 홀드색 수정", description = "특정 문제의 홀드색 수정") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "성공적으로 문제 정보 수정함", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = ProblemDetailResponseDto.class))), + @ApiResponse(responseCode = "400", description = "유효하지 않은 요청 형식", + content = @Content(mediaType = "application/json", examples = { + @ExampleObject(name = "요청값 누락", value = "{\"perceivedDifficulty\": \"변경할 홀드 id는 필수입니다.\"}") + })), + @ApiResponse(responseCode = "404", description = "리소스를 찾을 수 없음", + content = @Content(mediaType = "application/json", examples = { + @ExampleObject(name = "문제 없음", value = "{\"error\": \"아이디가 0190c558-9063-7050-b4fc-eb421e3236b3인 문제를 찾을 수 없습니다.\"}") + })) + }) + @PatchMapping("/admin/problems/{problemId}/holds") + public ResponseEntity changeHold( + @PathVariable final UUID problemId, + @Valid @RequestBody final ProblemHoldRequestDto requestDto) { + return ResponseEntity.ok(problemsHoldService.updateHold(problemId, requestDto.holdId())); + } + + @Operation(summary = "중복된 문제 조회", description = "sectorId, holdColorId, difficulty로 중복된 문제를 조회") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "성공적으로 중복된 문제 조회", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = DuplicateProblemsResponseDto.class))) + }) + @GetMapping("/problems/duplicate") + public ResponseEntity findDuplicateProblems( + @RequestParam("sectorId") final Long sectorId, + @RequestParam("holdColorId") final Long holdId, + @RequestParam("difficulty") final String difficulty) { + + DuplicateProblemsResponseDto response = problemReadService.findDuplicateProblems(sectorId, holdId, difficulty); + + return ResponseEntity.ok(response); + } } diff --git a/src/main/java/com/first/flash/climbing/sector/application/SectorService.java b/src/main/java/com/first/flash/climbing/sector/application/SectorService.java index ce7fffc7..63a3106f 100644 --- a/src/main/java/com/first/flash/climbing/sector/application/SectorService.java +++ b/src/main/java/com/first/flash/climbing/sector/application/SectorService.java @@ -1,21 +1,28 @@ package com.first.flash.climbing.sector.application; import com.first.flash.climbing.gym.domian.ClimbingGymIdConfirmRequestedEvent; +import com.first.flash.climbing.sector.application.dto.SectorInfosDetailResponseDto; +import com.first.flash.climbing.sector.domain.SectorFixedInfoUpdatedEvent; +import com.first.flash.climbing.sector.infrastructure.dto.UpdateSectorsDto; import com.first.flash.climbing.sector.application.dto.SectorCreateRequestDto; import com.first.flash.climbing.sector.application.dto.SectorDetailResponseDto; +import com.first.flash.climbing.sector.application.dto.SectorInfoCreateRequestDto; +import com.first.flash.climbing.sector.application.dto.SectorInfoDetailResponseDto; import com.first.flash.climbing.sector.application.dto.SectorUpdateRemovalDateRequestDto; import com.first.flash.climbing.sector.application.dto.SectorUpdateRequestDto; import com.first.flash.climbing.sector.application.dto.SectorsDetailResponseDto; import com.first.flash.climbing.sector.domain.Sector; import com.first.flash.climbing.sector.domain.SectorExpiredEvent; +import com.first.flash.climbing.sector.domain.SectorInfo; +import com.first.flash.climbing.sector.domain.SectorInfoRepository; import com.first.flash.climbing.sector.domain.SectorInfoUpdatedEvent; import com.first.flash.climbing.sector.domain.SectorRemovalDateUpdatedEvent; import com.first.flash.climbing.sector.domain.SectorRepository; +import com.first.flash.climbing.sector.exception.exceptions.SectorInfoNotFoundException; import com.first.flash.climbing.sector.exception.exceptions.SectorNotFoundException; import com.first.flash.global.event.Events; import java.time.LocalDate; import java.util.List; -import java.util.Objects; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -26,15 +33,23 @@ public class SectorService { private final SectorRepository sectorRepository; + private final SectorInfoRepository sectorInfoRepository; @Transactional - public SectorDetailResponseDto saveSector(final Long gymId, + public SectorDetailResponseDto saveSector(final Long sectorInfoId, final SectorCreateRequestDto createRequestDto) { + SectorInfo sectorInfo = findSectorInfoById(sectorInfoId); + Sector sector = createSector(sectorInfo, createRequestDto); + return SectorDetailResponseDto.toDto(sectorRepository.save(sector)); + } - Sector sector = createSectorByDto(gymId, createRequestDto); + @Transactional + public SectorInfoDetailResponseDto saveSectorInfo(final Long gymId, + final SectorInfoCreateRequestDto createRequestDto) { + SectorInfo sectorInfo = SectorInfo.createDefault(createRequestDto.name(), + createRequestDto.adminName(), gymId, createRequestDto.selectedImageUrl()); Events.raise(ClimbingGymIdConfirmRequestedEvent.of(gymId)); - - return SectorDetailResponseDto.toDto(sectorRepository.save(sector)); + return SectorInfoDetailResponseDto.toDto(sectorInfoRepository.save(sectorInfo)); } @Transactional @@ -43,7 +58,7 @@ public SectorDetailResponseDto updateSectorRemovalDate(final Long sectorId, Sector sector = findById(sectorId); LocalDate removalDate = sectorUpdateRemovalDateRequestDto.removalDate(); sector.updateRemovalDate(removalDate); - Events.raise(SectorRemovalDateUpdatedEvent.of(sectorId, removalDate)); + Events.raise(SectorRemovalDateUpdatedEvent.of(sectorId, removalDate, sector.isExpired())); return SectorDetailResponseDto.toDto(sector); } @@ -59,14 +74,26 @@ public SectorDetailResponseDto updateSector( final SectorUpdateRequestDto updateRequestDto) { Sector foundSector = findById(sectorId); foundSector.updateSector(updateRequestDto.sectorName(), updateRequestDto.adminSectorName(), - updateRequestDto.settingDate(), - updateRequestDto.removalDate(), updateRequestDto.gymId()); + updateRequestDto.settingDate(), updateRequestDto.removalDate(), + updateRequestDto.selectedImageUrl()); Events.raise(SectorInfoUpdatedEvent.of(foundSector.getId(), updateRequestDto.sectorName(), - updateRequestDto.settingDate())); - Events.raise(ClimbingGymIdConfirmRequestedEvent.of(updateRequestDto.gymId())); + updateRequestDto.settingDate(), foundSector.isExpired())); return SectorDetailResponseDto.toDto(foundSector); } + @Transactional + public SectorInfoDetailResponseDto updateSectorInfo(final Long sectorInfoId, + final SectorInfoCreateRequestDto updateRequestDto) { + SectorInfo sectorInfo = findSectorInfoById(sectorInfoId); + sectorInfo.updateSectorInfo(updateRequestDto.name(), updateRequestDto.adminName(), + updateRequestDto.selectedImageUrl()); + UpdateSectorsDto updateSectorsDto = UpdateSectorsDto.toDto(sectorInfo); + sectorRepository.updateSectors(sectorInfoId, updateSectorsDto); + List sectorIds = sectorRepository.findSectorIdsBySectorInfoId(sectorInfoId); + Events.raise(SectorFixedInfoUpdatedEvent.of(sectorIds, sectorInfo)); + return SectorInfoDetailResponseDto.toDto(sectorInfo); + } + public Sector findById(final Long id) { return sectorRepository.findById(id) .orElseThrow(() -> new SectorNotFoundException(id)); @@ -82,19 +109,26 @@ public SectorsDetailResponseDto findAllSectors() { return new SectorsDetailResponseDto(sectorsResponse); } - private Sector createSectorByDto(final Long gymId, - final SectorCreateRequestDto createRequestDto) { - if (hasNoRemovalDate(createRequestDto)) { - return Sector.createExceptRemovalDate(createRequestDto.name(), - createRequestDto.adminName(), createRequestDto.settingDate(), gymId); - } + public SectorInfosDetailResponseDto findAllSectorInfos() { + List sectorInfosResponse = sectorInfoRepository + .findAll() + .stream() + .map( + SectorInfoDetailResponseDto::toDto) + .toList(); + return new SectorInfosDetailResponseDto(sectorInfosResponse); + } - return Sector.createDefault(createRequestDto.name(), - createRequestDto.adminName(), createRequestDto.settingDate(), - createRequestDto.removalDate(), gymId); + private Sector createSector(final SectorInfo sectorInfo, + final SectorCreateRequestDto createRequestDto) { + return Sector.of(sectorInfo.getSectorName(), createRequestDto.settingDate(), + createRequestDto.removalDate(), sectorInfo.getGymId(), + sectorInfo.getSelectedImageUrl(), sectorInfo.getId()); } - private static boolean hasNoRemovalDate(final SectorCreateRequestDto createRequestDto) { - return Objects.isNull(createRequestDto.removalDate()); + private SectorInfo findSectorInfoById(final Long sectorInfoId) { + return sectorInfoRepository.findById(sectorInfoId) + .orElseThrow( + () -> new SectorInfoNotFoundException(sectorInfoId)); } } diff --git a/src/main/java/com/first/flash/climbing/sector/application/dto/SectorCreateRequestDto.java b/src/main/java/com/first/flash/climbing/sector/application/dto/SectorCreateRequestDto.java index 7472783c..821eece0 100644 --- a/src/main/java/com/first/flash/climbing/sector/application/dto/SectorCreateRequestDto.java +++ b/src/main/java/com/first/flash/climbing/sector/application/dto/SectorCreateRequestDto.java @@ -1,13 +1,10 @@ package com.first.flash.climbing.sector.application.dto; -import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import java.time.LocalDate; public record SectorCreateRequestDto( - @NotEmpty(message = "섹터 이름은 필수입니다.") String name, - @NotEmpty(message = "섹터 관리 이름은 필수입니다.") String adminName, @NotNull(message = "세팅일 정보는 비어있을 수 없습니다.") LocalDate settingDate, LocalDate removalDate) { -} \ No newline at end of file +} diff --git a/src/main/java/com/first/flash/climbing/sector/application/dto/SectorInfoCreateRequestDto.java b/src/main/java/com/first/flash/climbing/sector/application/dto/SectorInfoCreateRequestDto.java new file mode 100644 index 00000000..2a76ff3c --- /dev/null +++ b/src/main/java/com/first/flash/climbing/sector/application/dto/SectorInfoCreateRequestDto.java @@ -0,0 +1,9 @@ +package com.first.flash.climbing.sector.application.dto; + +import jakarta.validation.constraints.NotEmpty; + +public record SectorInfoCreateRequestDto(@NotEmpty(message = "섹터 이름은 필수입니다.") String name, + @NotEmpty(message = "섹터 관리 이름은 필수입니다.") String adminName, + String selectedImageUrl) { + +} diff --git a/src/main/java/com/first/flash/climbing/sector/application/dto/SectorInfoDetailResponseDto.java b/src/main/java/com/first/flash/climbing/sector/application/dto/SectorInfoDetailResponseDto.java new file mode 100644 index 00000000..efc8f819 --- /dev/null +++ b/src/main/java/com/first/flash/climbing/sector/application/dto/SectorInfoDetailResponseDto.java @@ -0,0 +1,13 @@ +package com.first.flash.climbing.sector.application.dto; + +import com.first.flash.climbing.sector.domain.SectorInfo; +import com.first.flash.climbing.sector.domain.vo.SectorName; + +public record SectorInfoDetailResponseDto(Long id, SectorName sectorName, String selectedImageUrl, + Long gymId) { + + public static SectorInfoDetailResponseDto toDto(final SectorInfo sectorInfo) { + return new SectorInfoDetailResponseDto(sectorInfo.getId(), sectorInfo.getSectorName(), + sectorInfo.getSelectedImageUrl(), sectorInfo.getGymId()); + } +} diff --git a/src/main/java/com/first/flash/climbing/sector/application/dto/SectorInfosDetailResponseDto.java b/src/main/java/com/first/flash/climbing/sector/application/dto/SectorInfosDetailResponseDto.java new file mode 100644 index 00000000..ba8cedf2 --- /dev/null +++ b/src/main/java/com/first/flash/climbing/sector/application/dto/SectorInfosDetailResponseDto.java @@ -0,0 +1,11 @@ +package com.first.flash.climbing.sector.application.dto; + +import java.util.List; + +public record SectorInfosDetailResponseDto(List sectorInfosResponse) { + + public static SectorInfosDetailResponseDto toDto( + final List sectorInfosResponse) { + return new SectorInfosDetailResponseDto(sectorInfosResponse); + } +} diff --git a/src/main/java/com/first/flash/climbing/sector/application/dto/SectorUpdateRequestDto.java b/src/main/java/com/first/flash/climbing/sector/application/dto/SectorUpdateRequestDto.java index ec0a810d..f95265ee 100644 --- a/src/main/java/com/first/flash/climbing/sector/application/dto/SectorUpdateRequestDto.java +++ b/src/main/java/com/first/flash/climbing/sector/application/dto/SectorUpdateRequestDto.java @@ -9,11 +9,6 @@ public record SectorUpdateRequestDto( @NotEmpty(message = "섹터 관리 이름은 필수입니다.") String adminSectorName, @NotNull(message = "세팅일 정보는 비어있을 수 없습니다.") LocalDate settingDate, @NotNull(message = "탈거일 정보는 비어있을 수 없습니다.") LocalDate removalDate, - @NotNull(message = "클라이밍장 ID는 비어있을 수 없습니다.") Long gymId) { + String selectedImageUrl) { - public static SectorUpdateRequestDto of(final String sectorName, final String adminSectorName, - final LocalDate settingDate, final LocalDate removalDate, final Long gymId) { - return new SectorUpdateRequestDto(sectorName, adminSectorName, settingDate, removalDate, - gymId); - } -} \ No newline at end of file +} diff --git a/src/main/java/com/first/flash/climbing/sector/domain/Sector.java b/src/main/java/com/first/flash/climbing/sector/domain/Sector.java index a40ccd64..dd05d812 100644 --- a/src/main/java/com/first/flash/climbing/sector/domain/Sector.java +++ b/src/main/java/com/first/flash/climbing/sector/domain/Sector.java @@ -8,7 +8,9 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import java.time.LocalDate; +import java.util.Objects; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.ToString; @@ -17,6 +19,7 @@ @NoArgsConstructor @AllArgsConstructor @Getter +@Builder @ToString public class Sector { @@ -26,27 +29,46 @@ public class Sector { private SectorName sectorName; private LocalDate settingDate; private RemovalInfo removalInfo; + private String selectedImageUrl; private Long gymId; + private Long sectorInfoId; protected Sector(final SectorName sectorName, final LocalDate settingDate, - final RemovalInfo removalInfo, final Long gymId) { + final RemovalInfo removalInfo, final Long gymId, final String selectedImageUrl, + final Long sectorInfoId) { this.sectorName = sectorName; this.settingDate = settingDate; this.removalInfo = removalInfo; + this.selectedImageUrl = selectedImageUrl; this.gymId = gymId; + this.sectorInfoId = sectorInfoId; + } + + public static Sector of(final SectorName sectorName, final LocalDate settingDate, + final LocalDate removalDate, final Long gymId, + final String selectedImageUrl, final Long sectorInfoId) { + if (hasNoRemovalDate(removalDate)) { + return createExceptRemovalDate(sectorName.getName(), sectorName.getAdminName(), + settingDate, gymId, selectedImageUrl, sectorInfoId); + } + + return createDefault(sectorName.getName(), sectorName.getAdminName(), settingDate, + removalDate, gymId, selectedImageUrl, sectorInfoId); } public static Sector createExceptRemovalDate(final String sectorName, - final String adminSectorName, final LocalDate settingDate, final Long gymId) { + final String adminSectorName, final LocalDate settingDate, final Long gymId, + final String selectedImageUrl, final Long sectorInfoId) { return new Sector(SectorName.of(sectorName, adminSectorName), settingDate, - RemovalInfo.createBySettingDate(settingDate), gymId); + RemovalInfo.createBySettingDate(settingDate), gymId, selectedImageUrl, sectorInfoId); } public static Sector createDefault(final String sectorName, final String adminSectorName, - final LocalDate settingDate, final LocalDate removalDate, final Long gymId) { + final LocalDate settingDate, final LocalDate removalDate, final Long gymId, + final String selectedImageUrl, final Long sectorInfoId) { validateRemovalDate(settingDate, removalDate); return new Sector(SectorName.of(sectorName, adminSectorName), settingDate, - RemovalInfo.createDefault(removalDate), gymId); + RemovalInfo.createDefault(removalDate), gymId, selectedImageUrl, sectorInfoId); } public LocalDate getRemovalDate() { @@ -55,16 +77,20 @@ public LocalDate getRemovalDate() { public void updateRemovalDate(final LocalDate removalDate) { validateRemovalDate(settingDate, removalDate); - removalInfo = RemovalInfo.createDefault(removalDate); + removalInfo = RemovalInfo.createByNewRemovalDate(removalDate); } public void updateSector(final String sectorName, final String adminSectorName, - final LocalDate settingDate, final LocalDate removalDate, final Long gymId) { + final LocalDate settingDate, final LocalDate removalDate, final String selectedImageUrl) { validateRemovalDate(settingDate, removalDate); this.sectorName = SectorName.of(sectorName, adminSectorName); this.settingDate = settingDate; - this.removalInfo = RemovalInfo.createDefault(removalDate); - this.gymId = gymId; + this.removalInfo = RemovalInfo.createByNewRemovalDate(removalDate); + this.selectedImageUrl = selectedImageUrl; + } + + public boolean isExpired() { + return removalInfo.getIsExpired(); } private static void validateRemovalDate(final LocalDate settingDate, @@ -73,4 +99,8 @@ private static void validateRemovalDate(final LocalDate settingDate, throw new InvalidRemovalDateException(); } } + + private static boolean hasNoRemovalDate(final LocalDate removalDate) { + return Objects.isNull(removalDate); + } } diff --git a/src/main/java/com/first/flash/climbing/sector/domain/SectorFixedInfoUpdatedEvent.java b/src/main/java/com/first/flash/climbing/sector/domain/SectorFixedInfoUpdatedEvent.java new file mode 100644 index 00000000..9aa6eec8 --- /dev/null +++ b/src/main/java/com/first/flash/climbing/sector/domain/SectorFixedInfoUpdatedEvent.java @@ -0,0 +1,18 @@ +package com.first.flash.climbing.sector.domain; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class SectorFixedInfoUpdatedEvent { + + private List sectorIds; + private String sectorName; + + public static SectorFixedInfoUpdatedEvent of(final List sectorIds, + final SectorInfo sectorInfo) { + return new SectorFixedInfoUpdatedEvent(sectorIds, sectorInfo.getSectorName().getName()); + } +} diff --git a/src/main/java/com/first/flash/climbing/sector/domain/SectorInfo.java b/src/main/java/com/first/flash/climbing/sector/domain/SectorInfo.java new file mode 100644 index 00000000..36da6bd0 --- /dev/null +++ b/src/main/java/com/first/flash/climbing/sector/domain/SectorInfo.java @@ -0,0 +1,44 @@ +package com.first.flash.climbing.sector.domain; + +import com.first.flash.climbing.sector.domain.vo.SectorName; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Entity +@NoArgsConstructor +@AllArgsConstructor +@Getter +@ToString +public class SectorInfo { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private String selectedImageUrl; + private SectorName sectorName; + private Long gymId; + + protected SectorInfo(final String selectedImageUrl, final SectorName sectorName, + final Long gymId) { + this.selectedImageUrl = selectedImageUrl; + this.sectorName = sectorName; + this.gymId = gymId; + } + + public static SectorInfo createDefault(final String sectorName, final String adminSectorName, + final Long gymId, final String selectedImageUrl) { + return new SectorInfo(selectedImageUrl, SectorName.of(sectorName, adminSectorName), gymId); + } + + public void updateSectorInfo(final String sectorName, final String adminSectorName, + final String selectedImageUrl) { + this.sectorName = SectorName.of(sectorName, adminSectorName); + this.selectedImageUrl = selectedImageUrl; + } +} diff --git a/src/main/java/com/first/flash/climbing/sector/domain/SectorInfoRepository.java b/src/main/java/com/first/flash/climbing/sector/domain/SectorInfoRepository.java new file mode 100644 index 00000000..3f538ff6 --- /dev/null +++ b/src/main/java/com/first/flash/climbing/sector/domain/SectorInfoRepository.java @@ -0,0 +1,13 @@ +package com.first.flash.climbing.sector.domain; + +import java.util.List; +import java.util.Optional; + +public interface SectorInfoRepository { + + SectorInfo save(SectorInfo sectorInfo); + + Optional findById(Long id); + + List findAll(); +} diff --git a/src/main/java/com/first/flash/climbing/sector/domain/SectorInfoUpdatedEvent.java b/src/main/java/com/first/flash/climbing/sector/domain/SectorInfoUpdatedEvent.java index cdcf1dc2..da8f8893 100644 --- a/src/main/java/com/first/flash/climbing/sector/domain/SectorInfoUpdatedEvent.java +++ b/src/main/java/com/first/flash/climbing/sector/domain/SectorInfoUpdatedEvent.java @@ -11,9 +11,10 @@ public class SectorInfoUpdatedEvent { private Long id; private String sectorName; private LocalDate settingDate; + private boolean isExpired; public static SectorInfoUpdatedEvent of(final Long id, final String sectorName, - final LocalDate settingDate) { - return new SectorInfoUpdatedEvent(id, sectorName, settingDate); + final LocalDate settingDate, final boolean isExpired) { + return new SectorInfoUpdatedEvent(id, sectorName, settingDate, isExpired); } } diff --git a/src/main/java/com/first/flash/climbing/sector/domain/SectorRemovalDateUpdatedEvent.java b/src/main/java/com/first/flash/climbing/sector/domain/SectorRemovalDateUpdatedEvent.java index a02bdada..62855233 100644 --- a/src/main/java/com/first/flash/climbing/sector/domain/SectorRemovalDateUpdatedEvent.java +++ b/src/main/java/com/first/flash/climbing/sector/domain/SectorRemovalDateUpdatedEvent.java @@ -11,9 +11,10 @@ public class SectorRemovalDateUpdatedEvent extends Event { private Long sectorId; private LocalDate removalDate; + private boolean isExpired; public static SectorRemovalDateUpdatedEvent of( - final Long sectorId, final LocalDate removalDate) { - return new SectorRemovalDateUpdatedEvent(sectorId, removalDate); + final Long sectorId, final LocalDate removalDate, final boolean isExpired) { + return new SectorRemovalDateUpdatedEvent(sectorId, removalDate, isExpired); } } diff --git a/src/main/java/com/first/flash/climbing/sector/domain/SectorRepository.java b/src/main/java/com/first/flash/climbing/sector/domain/SectorRepository.java index 7b662e4d..80f0821c 100644 --- a/src/main/java/com/first/flash/climbing/sector/domain/SectorRepository.java +++ b/src/main/java/com/first/flash/climbing/sector/domain/SectorRepository.java @@ -1,5 +1,6 @@ package com.first.flash.climbing.sector.domain; +import com.first.flash.climbing.sector.infrastructure.dto.UpdateSectorsDto; import java.util.List; import java.util.Optional; @@ -12,4 +13,8 @@ public interface SectorRepository { List updateExpiredSector(); List findAll(); + + void updateSectors(final Long sectorInfoId, final UpdateSectorsDto updateSectorsDto); + + List findSectorIdsBySectorInfoId(Long sectorInfoId); } diff --git a/src/main/java/com/first/flash/climbing/sector/domain/vo/RemovalInfo.java b/src/main/java/com/first/flash/climbing/sector/domain/vo/RemovalInfo.java index 0e308ab0..aa555463 100644 --- a/src/main/java/com/first/flash/climbing/sector/domain/vo/RemovalInfo.java +++ b/src/main/java/com/first/flash/climbing/sector/domain/vo/RemovalInfo.java @@ -35,4 +35,11 @@ public static RemovalInfo createBySettingDate(final LocalDate settingDate) { public static RemovalInfo createDefault(final LocalDate removalDate) { return new RemovalInfo(removalDate, false, false); } + + public static RemovalInfo createByNewRemovalDate(final LocalDate removalDate) { + if (removalDate.isBefore(LocalDate.now())) { + return new RemovalInfo(removalDate, false, true); + } + return new RemovalInfo(removalDate, false, false); + } } diff --git a/src/main/java/com/first/flash/climbing/sector/exception/SectorExceptionHandler.java b/src/main/java/com/first/flash/climbing/sector/exception/SectorExceptionHandler.java index 5f9041ab..e6de5464 100644 --- a/src/main/java/com/first/flash/climbing/sector/exception/SectorExceptionHandler.java +++ b/src/main/java/com/first/flash/climbing/sector/exception/SectorExceptionHandler.java @@ -1,6 +1,7 @@ package com.first.flash.climbing.sector.exception; import com.first.flash.climbing.sector.exception.exceptions.InvalidRemovalDateException; +import com.first.flash.climbing.sector.exception.exceptions.SectorInfoNotFoundException; import com.first.flash.climbing.sector.exception.exceptions.SectorNotFoundException; import com.first.flash.global.dto.ErrorResponseDto; import org.springframework.http.HttpStatus; @@ -17,6 +18,12 @@ public ResponseEntity handleSectorNotFoundException( return getResponseWithStatus(HttpStatus.NOT_FOUND, exception); } + @ExceptionHandler(SectorInfoNotFoundException.class) + public ResponseEntity handleSectorInfoNotFoundException( + final SectorInfoNotFoundException exception) { + return getResponseWithStatus(HttpStatus.NOT_FOUND, exception); + } + @ExceptionHandler(InvalidRemovalDateException.class) public ResponseEntity handleInvalidRemovalDateException( final InvalidRemovalDateException exception) { diff --git a/src/main/java/com/first/flash/climbing/sector/exception/exceptions/SectorInfoNotFoundException.java b/src/main/java/com/first/flash/climbing/sector/exception/exceptions/SectorInfoNotFoundException.java new file mode 100644 index 00000000..c216cc31 --- /dev/null +++ b/src/main/java/com/first/flash/climbing/sector/exception/exceptions/SectorInfoNotFoundException.java @@ -0,0 +1,8 @@ +package com.first.flash.climbing.sector.exception.exceptions; + +public class SectorInfoNotFoundException extends RuntimeException { + + public SectorInfoNotFoundException(final Long id) { + super(String.format("아이디가 %s인 섹터 정보를 찾을 수 없습니다.", id)); + } +} diff --git a/src/main/java/com/first/flash/climbing/sector/infrastructure/SectorInfoJpaRepository.java b/src/main/java/com/first/flash/climbing/sector/infrastructure/SectorInfoJpaRepository.java new file mode 100644 index 00000000..616b8942 --- /dev/null +++ b/src/main/java/com/first/flash/climbing/sector/infrastructure/SectorInfoJpaRepository.java @@ -0,0 +1,15 @@ +package com.first.flash.climbing.sector.infrastructure; + +import com.first.flash.climbing.sector.domain.SectorInfo; +import java.util.List; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface SectorInfoJpaRepository extends JpaRepository { + + SectorInfo save(final SectorInfo sector); + + Optional findById(final Long id); + + List findAll(); +} diff --git a/src/main/java/com/first/flash/climbing/sector/infrastructure/SectorInfoRepositoryImpl.java b/src/main/java/com/first/flash/climbing/sector/infrastructure/SectorInfoRepositoryImpl.java new file mode 100644 index 00000000..cf55a1ac --- /dev/null +++ b/src/main/java/com/first/flash/climbing/sector/infrastructure/SectorInfoRepositoryImpl.java @@ -0,0 +1,30 @@ +package com.first.flash.climbing.sector.infrastructure; + +import com.first.flash.climbing.sector.domain.SectorInfo; +import com.first.flash.climbing.sector.domain.SectorInfoRepository; +import java.util.List; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class SectorInfoRepositoryImpl implements SectorInfoRepository { + + private final SectorInfoJpaRepository jpaRepository; + + @Override + public SectorInfo save(final SectorInfo sectorInfo) { + return jpaRepository.save(sectorInfo); + } + + @Override + public Optional findById(final Long id) { + return jpaRepository.findById(id); + } + + @Override + public List findAll() { + return jpaRepository.findAll(); + } +} diff --git a/src/main/java/com/first/flash/climbing/sector/infrastructure/SectorQueryDslRepository.java b/src/main/java/com/first/flash/climbing/sector/infrastructure/SectorQueryDslRepository.java index 9700beeb..e6acd233 100644 --- a/src/main/java/com/first/flash/climbing/sector/infrastructure/SectorQueryDslRepository.java +++ b/src/main/java/com/first/flash/climbing/sector/infrastructure/SectorQueryDslRepository.java @@ -2,6 +2,7 @@ import static com.first.flash.climbing.sector.domain.QSector.sector; +import com.first.flash.climbing.sector.infrastructure.dto.UpdateSectorsDto; import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.jpa.impl.JPAQueryFactory; import java.time.LocalDate; @@ -37,4 +38,21 @@ private BooleanExpression isExpired() { sector.removalInfo.removalDate .before(LocalDate.now())); } + + public void updateSectors(final Long sectorInfoId, + final UpdateSectorsDto updateSectorsDto) { + List sectorIds = findSectorIdsBySectorInfoId(sectorInfoId); + jpaQueryFactory.update(sector) + .set(sector.sectorName.name, updateSectorsDto.name()) + .set(sector.sectorName.adminName, updateSectorsDto.adminName()) + .where(sector.id.in(sectorIds)) + .execute(); + } + + public List findSectorIdsBySectorInfoId(final Long sectorInfoId) { + return jpaQueryFactory.select(sector.id) + .from(sector) + .where(sector.sectorInfoId.eq(sectorInfoId)) + .fetch(); + } } diff --git a/src/main/java/com/first/flash/climbing/sector/infrastructure/SectorRepositoryImpl.java b/src/main/java/com/first/flash/climbing/sector/infrastructure/SectorRepositoryImpl.java index 5fc5d5d8..995d8c6c 100644 --- a/src/main/java/com/first/flash/climbing/sector/infrastructure/SectorRepositoryImpl.java +++ b/src/main/java/com/first/flash/climbing/sector/infrastructure/SectorRepositoryImpl.java @@ -2,6 +2,7 @@ import com.first.flash.climbing.sector.domain.Sector; import com.first.flash.climbing.sector.domain.SectorRepository; +import com.first.flash.climbing.sector.infrastructure.dto.UpdateSectorsDto; import java.util.List; import java.util.Optional; import lombok.RequiredArgsConstructor; @@ -33,4 +34,15 @@ public List updateExpiredSector() { public List findAll() { return sectorJpaRepository.findAll(); } + + @Override + public void updateSectors(final Long sectorInfoId, + final UpdateSectorsDto updateSectorsDto) { + sectorQueryDslRepository.updateSectors(sectorInfoId, updateSectorsDto); + } + + @Override + public List findSectorIdsBySectorInfoId(final Long sectorInfoId) { + return sectorQueryDslRepository.findSectorIdsBySectorInfoId(sectorInfoId); + } } diff --git a/src/main/java/com/first/flash/climbing/sector/infrastructure/dto/UpdateSectorsDto.java b/src/main/java/com/first/flash/climbing/sector/infrastructure/dto/UpdateSectorsDto.java new file mode 100644 index 00000000..9a42f27c --- /dev/null +++ b/src/main/java/com/first/flash/climbing/sector/infrastructure/dto/UpdateSectorsDto.java @@ -0,0 +1,11 @@ +package com.first.flash.climbing.sector.infrastructure.dto; + +import com.first.flash.climbing.sector.domain.SectorInfo; + +public record UpdateSectorsDto(String name, String adminName, String selectedImageUrl) { + + public static UpdateSectorsDto toDto(final SectorInfo sectorInfo) { + return new UpdateSectorsDto(sectorInfo.getSectorName().getName(), + sectorInfo.getSectorName().getAdminName(), sectorInfo.getSelectedImageUrl()); + } +} diff --git a/src/main/java/com/first/flash/climbing/sector/ui/SectorController.java b/src/main/java/com/first/flash/climbing/sector/ui/SectorController.java index 082e11b1..72836fa3 100644 --- a/src/main/java/com/first/flash/climbing/sector/ui/SectorController.java +++ b/src/main/java/com/first/flash/climbing/sector/ui/SectorController.java @@ -3,6 +3,9 @@ import com.first.flash.climbing.sector.application.SectorService; import com.first.flash.climbing.sector.application.dto.SectorCreateRequestDto; import com.first.flash.climbing.sector.application.dto.SectorDetailResponseDto; +import com.first.flash.climbing.sector.application.dto.SectorInfoCreateRequestDto; +import com.first.flash.climbing.sector.application.dto.SectorInfoDetailResponseDto; +import com.first.flash.climbing.sector.application.dto.SectorInfosDetailResponseDto; import com.first.flash.climbing.sector.application.dto.SectorUpdateRemovalDateRequestDto; import com.first.flash.climbing.sector.application.dto.SectorUpdateRequestDto; import com.first.flash.climbing.sector.application.dto.SectorsDetailResponseDto; @@ -44,6 +47,37 @@ public ResponseEntity findAllSectors() { return ResponseEntity.ok(sectorService.findAllSectors()); } + @Operation(summary = "모든 섹터 고정 정보 조회", description = "모든 섹터 정보를 리스트로 반환") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "성공적으로 섹터를 조회", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = SectorInfosDetailResponseDto.class))), + }) + @GetMapping("sectorInfos") + public ResponseEntity findAllSectorInfos() { + return ResponseEntity.ok(sectorService.findAllSectorInfos()); + } + + @Operation(summary = "섹터 고정 정보 생성", description = "특정 클라이밍장의 새로운 섹터 고정 정보 생성") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "성공적으로 섹터를 생성함", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = SectorInfoDetailResponseDto.class))), + @ApiResponse(responseCode = "400", description = "유효하지 않은 요청 형식", + content = @Content(mediaType = "application/json", examples = { + @ExampleObject(name = "요청값 누락", value = "{\"name\": \"섹터 이름은 필수입니다.\"}"), + })), + @ApiResponse(responseCode = "404", description = "클라이밍장을 찾을 수 없음", + content = @Content(mediaType = "application/json", examples = { + @ExampleObject(name = "클라이밍장 없음", value = "{\"error\": \"아이디가 1인 클라이밍장을 찾을 수 없습니다.\"}") + })) + }) + @PostMapping("admin/gyms/{gymId}/sectorInfos") + public ResponseEntity createSectorInfo( + @PathVariable final Long gymId, + @Valid @RequestBody final SectorInfoCreateRequestDto sectorInfoCreateRequestDto) { + return ResponseEntity.status(HttpStatus.CREATED) + .body(sectorService.saveSectorInfo(gymId, sectorInfoCreateRequestDto)); + } + @Operation(summary = "섹터 갱신(생성)", description = "특정 클라이밍장에 새로운 섹터 생성") @ApiResponses(value = { @ApiResponse(responseCode = "201", description = "성공적으로 섹터를 생성함", @@ -58,11 +92,12 @@ public ResponseEntity findAllSectors() { @ExampleObject(name = "클라이밍장 없음", value = "{\"error\": \"아이디가 1인 클라이밍장을 찾을 수 없습니다.\"}") })) }) - @PostMapping("admin/gyms/{gymId}/sectors") - public ResponseEntity createSector(@PathVariable final Long gymId, + @PostMapping("admin/sectorInfos/{sectorInfoId}/sectors") + public ResponseEntity createSector( + @PathVariable final Long sectorInfoId, @Valid @RequestBody final SectorCreateRequestDto sectorCreateRequestDto) { return ResponseEntity.status(HttpStatus.CREATED) - .body(sectorService.saveSector(gymId, sectorCreateRequestDto)); + .body(sectorService.saveSector(sectorInfoId, sectorCreateRequestDto)); } @Operation(summary = "섹터 탈거일 수정", description = "특정 섹터의 탈거일 정보 수정") @@ -100,7 +135,6 @@ public ResponseEntity updateSectorRemovalDate( @ApiResponse(responseCode = "404", description = "리소스를 찾을 수 없음", content = @Content(mediaType = "application/json", examples = { @ExampleObject(name = "섹터 없음", value = "{\"error\": \"아이디가 1인 섹터를 찾을 수 없습니다.\"}"), - @ExampleObject(name = "클라이밍장 없음", value = "{\"error\": \"아이디가 1인 클라이밍장을 찾을 수 없습니다.\"}") })) }) @PutMapping("admin/sectors/{sectorId}") @@ -109,4 +143,24 @@ public ResponseEntity updateSector( @Valid @RequestBody final SectorUpdateRequestDto updateRequestDto) { return ResponseEntity.ok(sectorService.updateSector(sectorId, updateRequestDto)); } + + @Operation(summary = "섹터 전체 수정", description = "특정 섹터의 정보 수정") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "성공적으로 섹터 정보 수정함", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = SectorInfoDetailResponseDto.class))), + @ApiResponse(responseCode = "400", description = "유효하지 않은 요청 형식", + content = @Content(mediaType = "application/json", examples = { + @ExampleObject(name = "요청값 누락", value = "{\"name\": \"섹터 이름은 필수입니다.\"}"), + })), + @ApiResponse(responseCode = "404", description = "리소스를 찾을 수 없음", + content = @Content(mediaType = "application/json", examples = { + @ExampleObject(name = "섹터 없음", value = "{\"error\": \"아이디가 1인 섹터 정보를 찾을 수 없습니다.\"}"), + })) + }) + @PutMapping("admin/sectorInfos/{sectorInfoId}") + public ResponseEntity updateSectorInfo( + @PathVariable final Long sectorInfoId, + @Valid @RequestBody final SectorInfoCreateRequestDto updateRequestDto) { + return ResponseEntity.ok(sectorService.updateSectorInfo(sectorInfoId, updateRequestDto)); + } } diff --git a/src/main/java/com/first/flash/climbing/solution/application/SolutionSaveService.java b/src/main/java/com/first/flash/climbing/solution/application/SolutionSaveService.java index 3d3e89ce..f1ea5eca 100644 --- a/src/main/java/com/first/flash/climbing/solution/application/SolutionSaveService.java +++ b/src/main/java/com/first/flash/climbing/solution/application/SolutionSaveService.java @@ -34,14 +34,18 @@ public SolutionWriteResponseDto saveSolution(final UUID problemId, PerceivedDifficulty perceivedDifficulty = createRequestDto.perceivedDifficulty(); Solution solution = Solution.of(member.getNickName(), createRequestDto.review(), - member.getInstagramId(), createRequestDto.videoUrl(), problemId, member.getId(), + member.getInstagramId(), createRequestDto.thumbnailImageUrl(), createRequestDto.solvedDate(), + createRequestDto.videoUrl(), problemId, member.getId(), member.getProfileImageUrl(), perceivedDifficulty, member.getHeight(), member.getReach(), member.getGender()); Events.raise(PerceivedDifficultySetEvent.of(solution.getProblemId(), perceivedDifficulty.getValue())); Solution savedSolution = solutionRepository.save(solution); - Events.raise(SolutionSavedEvent.of(savedSolution.getProblemId())); + Events.raise(SolutionSavedEvent.of(savedSolution.getProblemId(), savedSolution.getId(), + savedSolution.getSolutionDetail().getThumbnailImageUrl(), + savedSolution.getUploaderDetail().getInstagramId())); + return SolutionWriteResponseDto.toDto(savedSolution); } @@ -61,14 +65,17 @@ public SolutionWriteResponseDto saveUnregisteredMemberSolution(final UUID proble PerceivedDifficulty perceivedDifficulty = requestDto.perceivedDifficulty(); Solution solution = Solution.of(requestDto.nickName(), requestDto.review(), - requestDto.instagramId(), requestDto.videoUrl(), problemId, member.getId(), + requestDto.instagramId(), requestDto.thumbnailImageUrl(), requestDto.solvedDate(), + requestDto.videoUrl(), problemId, member.getId(), requestDto.profileImageUrl(), perceivedDifficulty, member.getHeight(), member.getReach(), member.getGender()); Solution savedSolution = solutionRepository.save(solution); Events.raise(PerceivedDifficultySetEvent.of(solution.getProblemId(), perceivedDifficulty.getValue())); - Events.raise(SolutionSavedEvent.of(savedSolution.getProblemId())); + Events.raise(SolutionSavedEvent.of(savedSolution.getProblemId(), savedSolution.getId(), + savedSolution.getSolutionDetail().getThumbnailImageUrl(), + savedSolution.getUploaderDetail().getInstagramId())); return SolutionWriteResponseDto.toDto(savedSolution); } } diff --git a/src/main/java/com/first/flash/climbing/solution/application/SolutionService.java b/src/main/java/com/first/flash/climbing/solution/application/SolutionService.java index 630afc0e..61cb0481 100644 --- a/src/main/java/com/first/flash/climbing/solution/application/SolutionService.java +++ b/src/main/java/com/first/flash/climbing/solution/application/SolutionService.java @@ -12,6 +12,7 @@ import com.first.flash.climbing.solution.domain.Solution; import com.first.flash.climbing.solution.domain.SolutionDeletedEvent; import com.first.flash.climbing.solution.domain.SolutionRepository; +import com.first.flash.climbing.solution.domain.SolutionUpdatedEvent; import com.first.flash.climbing.solution.domain.dto.SolutionResponseDto; import com.first.flash.climbing.solution.exception.exceptions.SolutionAccessDeniedException; import com.first.flash.climbing.solution.exception.exceptions.SolutionNotFoundException; @@ -79,8 +80,11 @@ public SolutionWriteResponseDto updateContent(final Long id, PerceivedDifficulty oldPerceivedDifficulty = solution.getSolutionDetail().getPerceivedDifficulty(); int difficultyDifference = newPerceivedDifficulty.calculateDifferenceFrom(oldPerceivedDifficulty); - solution.updateContentInfo(requestDto.review(), requestDto.videoUrl(), newPerceivedDifficulty); + solution.updateContentInfo(requestDto.review(), requestDto.videoUrl(), + requestDto.thumbnailImageUrl(), requestDto.solvedDate(), newPerceivedDifficulty); + Events.raise(SolutionUpdatedEvent.of(solution.getId(), requestDto.thumbnailImageUrl(), + solution.getUploaderDetail().getUploader())); Events.raise(PerceivedDifficultySetEvent.of( solution.getProblemId(), difficultyDifference diff --git a/src/main/java/com/first/flash/climbing/solution/application/dto/SolutionUpdateRequestDto.java b/src/main/java/com/first/flash/climbing/solution/application/dto/SolutionUpdateRequestDto.java index 427c2c5e..d24b9108 100644 --- a/src/main/java/com/first/flash/climbing/solution/application/dto/SolutionUpdateRequestDto.java +++ b/src/main/java/com/first/flash/climbing/solution/application/dto/SolutionUpdateRequestDto.java @@ -5,10 +5,13 @@ import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; +import java.time.LocalDate; public record SolutionUpdateRequestDto( + @NotEmpty(message = "썸네일 URL은 필수입니다.") String thumbnailImageUrl, @NotEmpty(message = "비디오 URL은 필수입니다.") String videoUrl, @Size(max = 500, message = "리뷰는 최대 500자까지 가능합니다.") String review, + @NotNull(message = "풀이 일자 정보는 필수입니다.") LocalDate solvedDate, @NotNull(message = "체감 난이도는 필수입니다.") @ValidEnum(enumClass = PerceivedDifficulty.class) PerceivedDifficulty perceivedDifficulty) { diff --git a/src/main/java/com/first/flash/climbing/solution/application/dto/SolutionWriteResponseDto.java b/src/main/java/com/first/flash/climbing/solution/application/dto/SolutionWriteResponseDto.java index b6c7360d..0049a22d 100644 --- a/src/main/java/com/first/flash/climbing/solution/application/dto/SolutionWriteResponseDto.java +++ b/src/main/java/com/first/flash/climbing/solution/application/dto/SolutionWriteResponseDto.java @@ -4,10 +4,12 @@ import com.first.flash.climbing.solution.domain.vo.SolutionDetail; import com.first.flash.climbing.solution.domain.vo.UploaderDetail; import com.first.flash.global.util.AuthUtil; +import java.time.LocalDate; import java.util.UUID; public record SolutionWriteResponseDto(Long id, String uploader, String review, String instagramId, - String videoUrl, UUID uploaderId, Boolean isUploader, + String thumbnailImageUrl, String videoUrl, LocalDate solvedDate, + UUID uploaderId, Boolean isUploader, String profileImageUrl) { public static SolutionWriteResponseDto toDto(final Solution solution) { @@ -18,7 +20,8 @@ public static SolutionWriteResponseDto toDto(final Solution solution) { return new SolutionWriteResponseDto(solution.getId(), uploaderDetail.getUploader(), solutionDetail.getReview(), uploaderDetail.getInstagramId(), - solutionDetail.getVideoUrl(), uploaderDetail.getUploaderId(), isUploader, + solutionDetail.getThumbnailImageUrl(), solutionDetail.getVideoUrl(), + solutionDetail.getSolvedDate(), uploaderDetail.getUploaderId(), isUploader, uploaderDetail.getProfileImageUrl()); } } diff --git a/src/main/java/com/first/flash/climbing/solution/application/dto/UnregisteredMemberSolutionCreateRequest.java b/src/main/java/com/first/flash/climbing/solution/application/dto/UnregisteredMemberSolutionCreateRequest.java index ead35334..e4a133fa 100644 --- a/src/main/java/com/first/flash/climbing/solution/application/dto/UnregisteredMemberSolutionCreateRequest.java +++ b/src/main/java/com/first/flash/climbing/solution/application/dto/UnregisteredMemberSolutionCreateRequest.java @@ -4,12 +4,15 @@ import com.first.flash.global.annotation.ValidEnum; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; +import java.time.LocalDate; public record UnregisteredMemberSolutionCreateRequest( @NotEmpty(message = "닉네임은 필수입니다.") String nickName, @NotEmpty(message = "인스타그램 아이디는 필수입니다.") String instagramId, String review, String profileImageUrl, + @NotEmpty(message = "썸네일 URL은 필수입니다.") String thumbnailImageUrl, @NotEmpty(message = "비디오 URL은 필수입니다.") String videoUrl, + @NotNull(message = "풀이 일자 정보는 필수입니다.") LocalDate solvedDate, @NotNull(message = "체감 난이도는 필수입니다.") @ValidEnum(enumClass = PerceivedDifficulty.class) PerceivedDifficulty perceivedDifficulty) { diff --git a/src/main/java/com/first/flash/climbing/solution/domain/Solution.java b/src/main/java/com/first/flash/climbing/solution/domain/Solution.java index 0049a0f8..23051f4c 100644 --- a/src/main/java/com/first/flash/climbing/solution/domain/Solution.java +++ b/src/main/java/com/first/flash/climbing/solution/domain/Solution.java @@ -11,6 +11,7 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.OneToMany; +import java.time.LocalDate; import java.util.ArrayList; import java.util.List; import java.util.UUID; @@ -41,12 +42,14 @@ public class Solution extends BaseEntity { private List comments = new ArrayList<>(); protected Solution(final String uploader, final String review, final String instagramId, + final String thumbnailImageUrl, final LocalDate solvedDate, final String videoUrl, final UUID problemId, final UUID uploaderId, final String profileImageUrl, final PerceivedDifficulty perceivedDifficulty, final Double uploaderHeight, final Double uploaderReach, final Gender uploaderGender) { - this.solutionDetail = SolutionDetail.of(review, videoUrl, perceivedDifficulty); + this.solutionDetail = SolutionDetail.of(review, thumbnailImageUrl, videoUrl, + solvedDate, perceivedDifficulty); this.uploaderDetail = UploaderDetail.of(uploaderId, uploader, instagramId, profileImageUrl, uploaderHeight, uploaderReach, uploaderGender); this.optionalWeight = DEFAULT_OPTIONAL_WEIGHT; @@ -54,16 +57,20 @@ protected Solution(final String uploader, final String review, final String inst } public static Solution of(final String uploader, final String review, final String instagramId, + final String thumbnailImageUrl, final LocalDate solvedDate, final String videoUrl, final UUID problemId, final UUID uploaderId, final String profileImageUrl, final PerceivedDifficulty perceivedDifficulty, - final Double uploaderHeight, final Double uploaderReach, final Gender uploaderGender) { + final Double uploaderHeight, + final Double uploaderReach, final Gender uploaderGender) { - return new Solution(uploader, review, instagramId, videoUrl, problemId, uploaderId, + return new Solution(uploader, review, instagramId, thumbnailImageUrl, solvedDate, videoUrl, + problemId, uploaderId, profileImageUrl, perceivedDifficulty, uploaderHeight, uploaderReach, uploaderGender); } - public void updateContentInfo(final String review, final String videoUrl, - final PerceivedDifficulty perceivedDifficulty) { - this.solutionDetail = SolutionDetail.of(review, videoUrl, perceivedDifficulty); + public void updateContentInfo(final String review, final String videoUrl, final String thumbnailImageUrl, + final LocalDate solvedDate, final PerceivedDifficulty perceivedDifficulty) { + this.solutionDetail = SolutionDetail.of(review, thumbnailImageUrl, videoUrl, + solvedDate, perceivedDifficulty); } } diff --git a/src/main/java/com/first/flash/climbing/solution/domain/SolutionSavedEvent.java b/src/main/java/com/first/flash/climbing/solution/domain/SolutionSavedEvent.java index 789244cd..46978903 100644 --- a/src/main/java/com/first/flash/climbing/solution/domain/SolutionSavedEvent.java +++ b/src/main/java/com/first/flash/climbing/solution/domain/SolutionSavedEvent.java @@ -10,8 +10,12 @@ public class SolutionSavedEvent extends Event { private UUID problemId; + private Long solutionId; + private String thumbnailImageUrl; + private String uploader; - public static SolutionSavedEvent of(final UUID problemId) { - return new SolutionSavedEvent(problemId); + public static SolutionSavedEvent of(final UUID problemId, Long solutionId, + String thumbnailImageUrl, String uploader) { + return new SolutionSavedEvent(problemId, solutionId, thumbnailImageUrl, uploader); } } diff --git a/src/main/java/com/first/flash/climbing/solution/domain/SolutionUpdatedEvent.java b/src/main/java/com/first/flash/climbing/solution/domain/SolutionUpdatedEvent.java new file mode 100644 index 00000000..cc4423e6 --- /dev/null +++ b/src/main/java/com/first/flash/climbing/solution/domain/SolutionUpdatedEvent.java @@ -0,0 +1,20 @@ +package com.first.flash.climbing.solution.domain; + +import com.first.flash.global.event.Event; +import java.util.UUID; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class SolutionUpdatedEvent extends Event { + + private Long solutionId; + private String thumbnailImageUrl; + private String uploader; + + public static SolutionUpdatedEvent of(final Long solutionId, final String thumbnailImageUrl, + final String uploader) { + return new SolutionUpdatedEvent(solutionId, thumbnailImageUrl, uploader); + } +} diff --git a/src/main/java/com/first/flash/climbing/solution/domain/dto/SolutionCreateRequestDto.java b/src/main/java/com/first/flash/climbing/solution/domain/dto/SolutionCreateRequestDto.java index 621ea90b..56ddef22 100644 --- a/src/main/java/com/first/flash/climbing/solution/domain/dto/SolutionCreateRequestDto.java +++ b/src/main/java/com/first/flash/climbing/solution/domain/dto/SolutionCreateRequestDto.java @@ -3,16 +3,23 @@ import com.first.flash.climbing.solution.domain.PerceivedDifficulty; import com.first.flash.global.annotation.ValidEnum; import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; +import java.time.LocalDate; public record SolutionCreateRequestDto( + @NotEmpty(message = "썸네일 URL은 필수입니다.") String thumbnailImageUrl, @NotEmpty(message = "비디오 URL은 필수입니다.") String videoUrl, @Size(max = 500, message = "리뷰는 최대 500자까지 가능합니다.") String review, + @NotNull(message = "풀이 일자 정보는 필수입니다.") LocalDate solvedDate, @ValidEnum(enumClass = PerceivedDifficulty.class) PerceivedDifficulty perceivedDifficulty) { - public SolutionCreateRequestDto(final String videoUrl, final String review, final PerceivedDifficulty perceivedDifficulty) { + public SolutionCreateRequestDto(final String thumbnailImageUrl, final String videoUrl, + final String review, final LocalDate solvedDate, final PerceivedDifficulty perceivedDifficulty) { + this.thumbnailImageUrl = thumbnailImageUrl; this.videoUrl = videoUrl; this.review = review; + this.solvedDate = solvedDate; this.perceivedDifficulty = perceivedDifficulty != null ? perceivedDifficulty : PerceivedDifficulty.NORMAL; } } diff --git a/src/main/java/com/first/flash/climbing/solution/domain/vo/SolutionDetail.java b/src/main/java/com/first/flash/climbing/solution/domain/vo/SolutionDetail.java index 6fba659d..2206bc5d 100644 --- a/src/main/java/com/first/flash/climbing/solution/domain/vo/SolutionDetail.java +++ b/src/main/java/com/first/flash/climbing/solution/domain/vo/SolutionDetail.java @@ -4,6 +4,7 @@ import com.first.flash.climbing.solution.domain.PerceivedDifficultyConverter; import jakarta.persistence.Convert; import jakarta.persistence.Embeddable; +import java.time.LocalDate; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; @@ -17,17 +18,23 @@ public class SolutionDetail { private String review; + private String thumbnailImageUrl; private String videoUrl; + private LocalDate solvedDate; @Convert(converter = PerceivedDifficultyConverter.class) private PerceivedDifficulty perceivedDifficulty; - protected SolutionDetail(final String review, final String videoUrl, final PerceivedDifficulty perceivedDifficulty) { + protected SolutionDetail(final String review, final String thumbnailImageUrl, final String videoUrl, + final LocalDate solvedDate, final PerceivedDifficulty perceivedDifficulty) { this.review = review; + this.thumbnailImageUrl = thumbnailImageUrl; this.videoUrl = videoUrl; + this.solvedDate = solvedDate; this.perceivedDifficulty = perceivedDifficulty; } - public static SolutionDetail of(final String review, final String videoUrl, final PerceivedDifficulty perceivedDifficulty) { - return new SolutionDetail(review, videoUrl, perceivedDifficulty); + public static SolutionDetail of(final String review, final String thumbnailImageUrl, final String videoUrl, + final LocalDate solvedDate, final PerceivedDifficulty perceivedDifficulty) { + return new SolutionDetail(review, thumbnailImageUrl, videoUrl, solvedDate, perceivedDifficulty); } } diff --git a/src/main/java/com/first/flash/climbing/solution/infrastructure/SolutionQueryDslRepository.java b/src/main/java/com/first/flash/climbing/solution/infrastructure/SolutionQueryDslRepository.java index 620b6e28..3774c7a8 100644 --- a/src/main/java/com/first/flash/climbing/solution/infrastructure/SolutionQueryDslRepository.java +++ b/src/main/java/com/first/flash/climbing/solution/infrastructure/SolutionQueryDslRepository.java @@ -48,8 +48,8 @@ public List findByUploaderId(final UUID uploaderId, final List difficulty) { return jpaQueryFactory.select(Projections.constructor(MySolutionDto.class, solution.id, queryProblem.gymName, queryProblem.sectorName, - queryProblem.difficultyName, queryProblem.imageUrl, solutionComment.count(), - solution.createdAt + queryProblem.difficultyName, solution.solutionDetail.thumbnailImageUrl, + solutionComment.count(), solution.solutionDetail.solvedDate, solution.updatedAt )) .from(solution) .innerJoin(queryProblem) @@ -85,6 +85,8 @@ public DetailSolutionDto findDetailSolutionById(final Long solutionId) { queryProblem.sectorName, solution.solutionDetail.review, queryProblem.difficultyName, solutionComment.count(), solution.solutionDetail.perceivedDifficulty, + solution.solutionDetail.thumbnailImageUrl, queryProblem.holdColorCode, + solution.solutionDetail.solvedDate, queryProblem.removalDate, queryProblem.settingDate, solution.createdAt )) .from(solution) diff --git a/src/main/java/com/first/flash/climbing/solution/infrastructure/dto/DetailSolutionDto.java b/src/main/java/com/first/flash/climbing/solution/infrastructure/dto/DetailSolutionDto.java index cd20c0e7..0351cb6b 100644 --- a/src/main/java/com/first/flash/climbing/solution/infrastructure/dto/DetailSolutionDto.java +++ b/src/main/java/com/first/flash/climbing/solution/infrastructure/dto/DetailSolutionDto.java @@ -6,5 +6,6 @@ public record DetailSolutionDto(Long solutionId, String videoUrl, String gymName, String sectorName, String review, String difficultyName, Long commentsCount, PerceivedDifficulty perceivedDifficulty, + String thumbnailImageUrl, String holdColorCode, LocalDate solvedDate, LocalDate removalDate, LocalDate settingDate, LocalDateTime uploadedAt) { } diff --git a/src/main/java/com/first/flash/climbing/solution/infrastructure/dto/MySolutionDto.java b/src/main/java/com/first/flash/climbing/solution/infrastructure/dto/MySolutionDto.java index 91aed245..6cbe6bd0 100644 --- a/src/main/java/com/first/flash/climbing/solution/infrastructure/dto/MySolutionDto.java +++ b/src/main/java/com/first/flash/climbing/solution/infrastructure/dto/MySolutionDto.java @@ -1,9 +1,10 @@ package com.first.flash.climbing.solution.infrastructure.dto; +import java.time.LocalDate; import java.time.LocalDateTime; public record MySolutionDto(Long solutionId, String gymName, String sectorName, - String difficultyName, String problemImageUrl, Long commentsCount, - LocalDateTime uploadedAt) { + String difficultyName, String thumbnailImageUrl, Long commentsCount, + LocalDate solvedDate, LocalDateTime uploadedAt) { } diff --git a/src/test/java/com/first/flash/climbing/sector/domain/SectorTest.java b/src/test/java/com/first/flash/climbing/sector/domain/SectorTest.java index c4860bf4..b3921d39 100644 --- a/src/test/java/com/first/flash/climbing/sector/domain/SectorTest.java +++ b/src/test/java/com/first/flash/climbing/sector/domain/SectorTest.java @@ -9,6 +9,7 @@ class SectorTest { + private static final String DEFAULT_SECTOR_IMAGE = "example image url"; private static final Long DEFAULT_GYM_ID = 1L; @Test @@ -18,7 +19,7 @@ class SectorTest { // when & then assertThatThrownBy(() -> Sector.createDefault("test", "test", settingDate, - settingDate.minusDays(1), DEFAULT_GYM_ID)) + settingDate.minusDays(1), DEFAULT_GYM_ID, DEFAULT_SECTOR_IMAGE)) .isInstanceOf(InvalidRemovalDateException.class); } @@ -27,7 +28,7 @@ class SectorTest { // given LocalDate settingDate = LocalDate.now(); Sector sector = Sector.createExceptRemovalDate("test", "test", settingDate, - DEFAULT_GYM_ID); + DEFAULT_GYM_ID, DEFAULT_SECTOR_IMAGE); // when sector.updateRemovalDate(settingDate.plusDays(1)); @@ -42,7 +43,7 @@ class SectorTest { // given LocalDate settingDate = LocalDate.now(); Sector sector = Sector.createExceptRemovalDate("test", "test", settingDate, - DEFAULT_GYM_ID); + DEFAULT_GYM_ID, DEFAULT_SECTOR_IMAGE); // when & then assertThatThrownBy(() -> sector.updateRemovalDate(settingDate.minusDays(1))) diff --git a/src/test/java/com/first/flash/climbing/sector/fixture/SectorFixture.java b/src/test/java/com/first/flash/climbing/sector/fixture/SectorFixture.java index bf0fcd80..0cd9fef7 100644 --- a/src/test/java/com/first/flash/climbing/sector/fixture/SectorFixture.java +++ b/src/test/java/com/first/flash/climbing/sector/fixture/SectorFixture.java @@ -6,17 +6,18 @@ public class SectorFixture { + private static final String DEFAULT_SECTOR_IMAGE = "example image url"; private final static Long DEFAULT_PLUS_DAYS = 30L; public static Sector createDefault(final Long gymId, final LocalDate settingDate) { return Sector.createDefault("sector 1", "admin sector 1", - settingDate, settingDate.plusDays(DEFAULT_PLUS_DAYS), gymId); + settingDate, settingDate.plusDays(DEFAULT_PLUS_DAYS), gymId, DEFAULT_SECTOR_IMAGE); } public static Sector createDefaultExceptRemovalDate(final Long gymId, final LocalDate settingDate) { return Sector.createExceptRemovalDate("sector 1", "admin sector 1", - settingDate, gymId); + settingDate, gymId, DEFAULT_SECTOR_IMAGE); } public static SectorCreateRequestDto createDefaultRequestDtoExceptRemovalDate(