diff --git a/.gradle/8.13/checksums/checksums.lock b/.gradle/8.13/checksums/checksums.lock index 943f8b0..a319497 100644 Binary files a/.gradle/8.13/checksums/checksums.lock and b/.gradle/8.13/checksums/checksums.lock differ diff --git a/.gradle/8.13/checksums/md5-checksums.bin b/.gradle/8.13/checksums/md5-checksums.bin index 47ef681..f47d8b2 100644 Binary files a/.gradle/8.13/checksums/md5-checksums.bin and b/.gradle/8.13/checksums/md5-checksums.bin differ diff --git a/.gradle/8.13/checksums/sha1-checksums.bin b/.gradle/8.13/checksums/sha1-checksums.bin index 35da322..e7cd065 100644 Binary files a/.gradle/8.13/checksums/sha1-checksums.bin and b/.gradle/8.13/checksums/sha1-checksums.bin differ diff --git a/.gradle/8.13/executionHistory/executionHistory.lock b/.gradle/8.13/executionHistory/executionHistory.lock index 2e74c8c..5c34500 100644 Binary files a/.gradle/8.13/executionHistory/executionHistory.lock and b/.gradle/8.13/executionHistory/executionHistory.lock differ diff --git a/.gradle/8.13/fileHashes/fileHashes.bin b/.gradle/8.13/fileHashes/fileHashes.bin index 1a348dd..4c8dcf5 100644 Binary files a/.gradle/8.13/fileHashes/fileHashes.bin and b/.gradle/8.13/fileHashes/fileHashes.bin differ diff --git a/.gradle/8.13/fileHashes/fileHashes.lock b/.gradle/8.13/fileHashes/fileHashes.lock index 4a6a1af..805ecaa 100644 Binary files a/.gradle/8.13/fileHashes/fileHashes.lock and b/.gradle/8.13/fileHashes/fileHashes.lock differ diff --git a/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/.gradle/buildOutputCleanup/buildOutputCleanup.lock index 8bc68f8..ef3c451 100644 Binary files a/.gradle/buildOutputCleanup/buildOutputCleanup.lock and b/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ diff --git a/build.gradle b/build.gradle index 9592344..f4c1f54 100644 --- a/build.gradle +++ b/build.gradle @@ -28,10 +28,12 @@ dependencies { //implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' + implementation 'io.minio:minio:8.5.7' //testImplementation 'org.springframework.security:spring-security-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } diff --git a/src/main/java/study/goorm/domain/cloth/api/ClothRestController.java b/src/main/java/study/goorm/domain/cloth/api/ClothRestController.java new file mode 100644 index 0000000..148c6b6 --- /dev/null +++ b/src/main/java/study/goorm/domain/cloth/api/ClothRestController.java @@ -0,0 +1,96 @@ +package study.goorm.domain.cloth.api; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import study.goorm.domain.cloth.application.ClothService; +import study.goorm.domain.cloth.dto.ClothRequestDTO; +import study.goorm.domain.cloth.dto.ClothResponseDTO; +import study.goorm.domain.model.enums.ClothSort; +import study.goorm.domain.model.exception.annotation.CheckPage; +import study.goorm.domain.model.exception.annotation.CheckPageSize; +import study.goorm.global.common.response.BaseResponse; +import study.goorm.global.error.code.status.SuccessStatus; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/cloth") //원래 cloth-였는데 cloth로 바꿨음 +@Validated +public class ClothRestController { + + private final ClothService clothService; + + @GetMapping("/{cloth-id}/edit-view") + @Operation(summary = "특정 Cloth에 대한 정보를 수정용으로 조회하는 API",description = "Path Variable로 clothId를 던져주세요.") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "CLOTH_200", description = "OK, 성공적으로 조회되었습니다."), + }) + public BaseResponse getClothEditView( + @PathVariable(name = "cloth-id") Long clothId + ){ + ClothResponseDTO.ClothEditViewResult result=clothService.getClothEditView(clothId); + return BaseResponse.onSuccess(SuccessStatus.CLOTH_VIEW_SUCCESS,result); + } + + + + @GetMapping("/closet-view") + @Operation(summary = "유저의 옷장을 조회하는 API",description = "query string으로 sort,page,size를 넘겨주세요.") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "CLOTH_200",description = "OK, 성공적으로 조회되었습니다.") + }) + @Parameters({ + @Parameter(name = "clokey-id", description = "클로키 유저의 clokey id, query string 입니다."), + @Parameter(name = "sort", description = "정렬(Sort) ENUM 값 { WEAR, NOT_WEAR, LATEST, OLDEST }, query string 입니다."), + @Parameter(name = "page", description = "페이지 값, query string 입니다."), + @Parameter(name = "size", description = "페이지에 표시할 요소 개수 값, query string 입니다.") + }) + public BaseResponsegetMemberCloset( + @RequestParam(value="clokey-id") String clokeyId, + @RequestParam ClothSort sort, + @RequestParam @CheckPage int page, + @RequestParam @CheckPageSize int size + ){ + + ClothResponseDTO.MemberClosetResult result=clothService.getMemberCloset(clokeyId,sort,page-1,size); + return BaseResponse.onSuccess(SuccessStatus.CLOTH_VIEW_SUCCESS,result); + + } + + + + @PostMapping(value = "",consumes= MediaType.MULTIPART_FORM_DATA_VALUE) + @Operation(summary = "새로운 옷을 생성하는 API", description = "request body에 ClothCreateRequest 형식의 데이터를 전달해주세요.") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "CLOTH_201",description = "CREATED,성공적으로 생성되었습니다."), + + }) + public BaseResponse createCloth( + @RequestPart("clothCreateRequest") @Valid ClothRequestDTO.ClothCreateRequest clothCreateRequest, + @RequestPart("imageFile") MultipartFile imageFile + ){ + ClothResponseDTO.ClothCreateResult result=clothService.createCloth(clothCreateRequest,imageFile); + return BaseResponse.onSuccess(SuccessStatus.CLOTH_CREATED,result); + } + + @DeleteMapping("/{cloth-id}") + @Operation(summary = "특정 옷을 삭제하는 API",description = "path variable로 cloth_id를 넘겨주세요.") + @Parameters({@Parameter(name = "cloth-id",description = "옷의 id,path variable입니다.")}) + public BaseResponse deleteCloth ( + @PathVariable(value="cloth-id") Long clothId + ){ + clothService.deleteCloth(clothId); + return BaseResponse.onSuccess(SuccessStatus.CLOTH_DELETED,null); + } + + + +} + diff --git a/src/main/java/study/goorm/domain/cloth/application/ClothImageQueryService.java b/src/main/java/study/goorm/domain/cloth/application/ClothImageQueryService.java new file mode 100644 index 0000000..f1a8e71 --- /dev/null +++ b/src/main/java/study/goorm/domain/cloth/application/ClothImageQueryService.java @@ -0,0 +1,9 @@ +package study.goorm.domain.cloth.application; + +import study.goorm.domain.cloth.domain.entity.Cloth; + +import java.util.Map; + +public interface ClothImageQueryService { + Map getFirstImageUrlMap(Iterable clothes); +} diff --git a/src/main/java/study/goorm/domain/cloth/application/ClothImageQueryServiceImpl.java b/src/main/java/study/goorm/domain/cloth/application/ClothImageQueryServiceImpl.java new file mode 100644 index 0000000..43128fa --- /dev/null +++ b/src/main/java/study/goorm/domain/cloth/application/ClothImageQueryServiceImpl.java @@ -0,0 +1,34 @@ +package study.goorm.domain.cloth.application; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import study.goorm.domain.cloth.domain.entity.Cloth; +import study.goorm.domain.cloth.domain.entity.ClothImage; +import study.goorm.domain.cloth.domain.repository.ClothImageRepository; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +@Service +@RequiredArgsConstructor +public class ClothImageQueryServiceImpl implements ClothImageQueryService { + private final ClothImageRepository clothImageRepository; + + @Override + public Map getFirstImageUrlMap(Iterable clothes){ + List clothIds= StreamSupport.stream(clothes.spliterator(),false) + .map(Cloth::getId) + .toList(); + + List firstImages=clothImageRepository.findFirstImagesByClothIds(clothIds); + + return firstImages.stream() + .collect(Collectors.toMap( + image->image.getCloth().getId(), + ClothImage::getImageUrl + )); + + } +} diff --git a/src/main/java/study/goorm/domain/cloth/application/ClothService.java b/src/main/java/study/goorm/domain/cloth/application/ClothService.java new file mode 100644 index 0000000..822b1c1 --- /dev/null +++ b/src/main/java/study/goorm/domain/cloth/application/ClothService.java @@ -0,0 +1,14 @@ +package study.goorm.domain.cloth.application; + +import org.springframework.web.multipart.MultipartFile; +import study.goorm.domain.cloth.dto.ClothRequestDTO; +import study.goorm.domain.cloth.dto.ClothResponseDTO; +import study.goorm.domain.model.enums.ClothSort; + +public interface ClothService { + ClothResponseDTO.ClothEditViewResult getClothEditView(Long clothId); + ClothResponseDTO.MemberClosetResult getMemberCloset(String clokeyId, ClothSort sort,int page,int size); + ClothResponseDTO.ClothCreateResult createCloth(ClothRequestDTO.ClothCreateRequest clothCreateResult ,MultipartFile image); + void deleteCloth(Long clothId); + +} diff --git a/src/main/java/study/goorm/domain/cloth/application/ClothServiceImpl.java b/src/main/java/study/goorm/domain/cloth/application/ClothServiceImpl.java new file mode 100644 index 0000000..f7efd3e --- /dev/null +++ b/src/main/java/study/goorm/domain/cloth/application/ClothServiceImpl.java @@ -0,0 +1,138 @@ +package study.goorm.domain.cloth.application; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; +import study.goorm.domain.cloth.converter.ClothConverter; +import study.goorm.domain.cloth.domain.entity.Category; +import study.goorm.domain.cloth.domain.entity.Cloth; +import study.goorm.domain.cloth.domain.entity.ClothImage; +import study.goorm.domain.cloth.domain.repository.CategoryRepository; +import study.goorm.domain.cloth.domain.repository.ClothImageRepository; +import study.goorm.domain.cloth.domain.repository.ClothRepository; +import study.goorm.domain.cloth.dto.ClothRequestDTO; +import study.goorm.domain.cloth.dto.ClothResponseDTO; +import study.goorm.domain.cloth.exception.ClothException; +import study.goorm.domain.folder.domain.repository.ClothFolderRepository; +import study.goorm.domain.history.domain.repository.HistoryClothRepository; +import study.goorm.domain.member.domain.entity.Member; +import study.goorm.domain.member.domain.exception.MemberException; +import study.goorm.domain.member.domain.repository.MemberRepository; +import study.goorm.domain.model.enums.ClothSort; +import study.goorm.global.error.code.status.ErrorStatus; + +import java.util.List; +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class ClothServiceImpl implements ClothService { + + private final ClothRepository clothRepository; + private final ClothImageRepository clothImageRepository; + private final MemberRepository memberRepository; + private final ClothImageQueryService clothImageQueryService; + private final CategoryRepository categoryRepository; + private final ClothFolderRepository clothFolderRepository; + private final HistoryClothRepository historyClothRepository; + + @Override + @Transactional(readOnly = true) + public ClothResponseDTO.ClothEditViewResult getClothEditView(Long clothId){ + + Cloth cloth = clothRepository.findById(clothId) + .orElseThrow(() -> new ClothException(ErrorStatus.NO_SUCH_CLOTH)); + + List clothImageUrls=clothImageRepository.findAllByCloth(cloth); + + String firstImageUrl=clothImageUrls.stream() + .findFirst() + .map(ClothImage::getImageUrl) + .orElseThrow(()->new ClothException(ErrorStatus.NO_ClOTH_IMAGE)); + + + + return ClothConverter.toClothEditViewResult(cloth,firstImageUrl); + } + + @Override + @Transactional(readOnly = true) + public ClothResponseDTO.MemberClosetResult getMemberCloset(String clokeyId, ClothSort sort, int page, int size){ + Member member=memberRepository.findByClokeyId(clokeyId) + .orElseThrow(()->new MemberException(ErrorStatus.NO_SUCH_MEMBER)); + + PageRequest pageRequest=PageRequest .of(page,size); + Page clothes; + + if(sort.equals(ClothSort.LATEST)){ + clothes=clothRepository.findByMemberOrderByCreatedAtDesc(member,pageRequest); + } + else if (sort.equals(ClothSort.OLDEST)){ + clothes=clothRepository.findByMemberOrderByCreatedAtAsc(member,pageRequest); + + } + else if(sort.equals(ClothSort.WEAR)){ + clothes=clothRepository.findByMemberOrderByWearNumDesc(member,pageRequest); + + } + else{ + clothes=clothRepository.findByMemberOrderByWearNumAsc(member,pageRequest); + } + Map firstImagesOfCloth=clothImageQueryService.getFirstImageUrlMap(clothes); + + return ClothConverter.toMemberClosetResult(member,firstImagesOfCloth,clothes); + } + + @Override + @Transactional + public ClothResponseDTO.ClothCreateResult createCloth(ClothRequestDTO.ClothCreateRequest clothCreateResult, MultipartFile image){ + Member member=memberRepository.findById(clothCreateResult.getMemberId()) + .orElseThrow(()->new MemberException(ErrorStatus.NO_SUCH_MEMBER)); + + Category category=categoryRepository.findById(clothCreateResult.getCategoryId()) + .orElseThrow(()->new ClothException(ErrorStatus.NO_SUCH_CATEGORY)); + + Cloth newCloth=Cloth.builder() + .name(clothCreateResult.getName()) + .wearNum(0) + .season(clothCreateResult.getSeasons()) + .tempUpperBound(clothCreateResult.getTempUpperBound()) + .tempLowerBound(clothCreateResult.getTempLowerBound()) + .thicknessLevel(clothCreateResult.getThicknessLevel()) + .clothUrl(clothCreateResult.getClothUrl()) + .brand(clothCreateResult.getBrand()) + .category(category) + .member(member) + .build(); + + clothRepository.save(newCloth); + + ClothImage newClothImage=ClothImage.builder() + .cloth(newCloth) + .imageUrl("아직 S3를 구현하지 않아서 url이 없어용") + .build(); + clothImageRepository.save(newClothImage); + + return ClothConverter.toClothCreateResult(newCloth); + + + } + + @Override + @Transactional + public void deleteCloth(Long clothId){ + Cloth cloth=clothRepository.findById(clothId) + .orElseThrow(()->new ClothException(ErrorStatus.NO_SUCH_CLOTH)); + //매핑 테이블 삭제 + clothImageRepository.deleteAllByCloth(cloth); + clothFolderRepository.deleteAllByCloth(cloth); + historyClothRepository.deleteAllByCloth(cloth); + + //최종 옷 삭제 + clothRepository.delete(cloth); + } + +} diff --git a/src/main/java/study/goorm/domain/cloth/converter/ClothConverter.java b/src/main/java/study/goorm/domain/cloth/converter/ClothConverter.java new file mode 100644 index 0000000..ec948e3 --- /dev/null +++ b/src/main/java/study/goorm/domain/cloth/converter/ClothConverter.java @@ -0,0 +1,62 @@ +package study.goorm.domain.cloth.converter; + + +import org.springframework.data.domain.Page; +import study.goorm.domain.cloth.domain.entity.Cloth; +import study.goorm.domain.cloth.dto.ClothResponseDTO; +import study.goorm.domain.member.domain.entity.Member; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + + +public class ClothConverter { + public static ClothResponseDTO.ClothEditViewResult toClothEditViewResult(Cloth cloth, String clothImageUrl){ + return ClothResponseDTO.ClothEditViewResult.builder() + .id(cloth.getId()) + .brand(cloth.getBrand()) + .categoryId(cloth.getCategory().getId()) + .clothUrl(cloth.getClothUrl()) + .imageUrl(clothImageUrl) + .name(cloth.getName()) + .seasons(cloth.getSeason()) + .tempLowerBound(cloth.getTempLowerBound()) + .tempUpperBound(cloth.getTempUpperBound()) + .thicknessLevel(cloth.getThicknessLevel()) + .build(); + + } + public static ClothResponseDTO.MemberClosetResult toMemberClosetResult(Member member, Map firstImagesOfCloth, Page clothes){ + return ClothResponseDTO.MemberClosetResult.builder() + .nickName(member.getNickname()) + .clothPreviewListResult(toClothPreviewListResult(firstImagesOfCloth,clothes)) + .build(); + } + private static ClothResponseDTO.ClothPreviewListResult toClothPreviewListResult(Map firstImagesOfCloth,Page clothes){ + return ClothResponseDTO.ClothPreviewListResult.builder() + .clothPreviews(toClothPreview(firstImagesOfCloth,clothes)) + .isFirst(clothes.isFirst()) + .isLast(clothes.isLast()) + .totalElements(clothes.getTotalElements()) + .totalPage(clothes.getTotalPages()) + .build(); + } + private static List toClothPreview(Map firstImagesOfCloth, Page clothes){ + return clothes.stream() + .map(cloth->ClothResponseDTO.ClothPreview.builder() + .id(cloth.getId()) + .name(cloth.getName()) + .wearNum(cloth.getWearNum()) + .imageUrl(firstImagesOfCloth.get(cloth.getId())) + .build()) + .collect(Collectors.toList()); + } + + public static ClothResponseDTO.ClothCreateResult toClothCreateResult(Cloth cloth){ + return ClothResponseDTO.ClothCreateResult.builder() + .id(cloth.getId()) + .build(); + } + +} diff --git a/src/main/java/study/goorm/domain/cloth/domain/entity/Cloth.java b/src/main/java/study/goorm/domain/cloth/domain/entity/Cloth.java index fd8ad15..2bfe3ae 100644 --- a/src/main/java/study/goorm/domain/cloth/domain/entity/Cloth.java +++ b/src/main/java/study/goorm/domain/cloth/domain/entity/Cloth.java @@ -64,4 +64,9 @@ public class Cloth extends BaseEntity { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "member_id", nullable = false) private Member member; + + //hyowon + public void increaseWearCount(){ + this.wearNum++; + } } \ No newline at end of file diff --git a/src/main/java/study/goorm/domain/cloth/domain/repository/CategoryRepository.java b/src/main/java/study/goorm/domain/cloth/domain/repository/CategoryRepository.java index 66bc9fd..efd5455 100644 --- a/src/main/java/study/goorm/domain/cloth/domain/repository/CategoryRepository.java +++ b/src/main/java/study/goorm/domain/cloth/domain/repository/CategoryRepository.java @@ -2,6 +2,8 @@ import org.springframework.data.jpa.repository.JpaRepository; import study.goorm.domain.cloth.domain.entity.Category; +import study.goorm.domain.cloth.domain.entity.Cloth; public interface CategoryRepository extends JpaRepository { + } diff --git a/src/main/java/study/goorm/domain/cloth/domain/repository/ClothImageRepository.java b/src/main/java/study/goorm/domain/cloth/domain/repository/ClothImageRepository.java index 1b5a7f1..a665717 100644 --- a/src/main/java/study/goorm/domain/cloth/domain/repository/ClothImageRepository.java +++ b/src/main/java/study/goorm/domain/cloth/domain/repository/ClothImageRepository.java @@ -1,7 +1,27 @@ package study.goorm.domain.cloth.domain.repository; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import study.goorm.domain.cloth.domain.entity.Cloth; import study.goorm.domain.cloth.domain.entity.ClothImage; +import java.util.List; + public interface ClothImageRepository extends JpaRepository { + void deleteAllByCloth(Cloth cloth); + List findAllByCloth(Cloth cloth); + + @Query(value = """ + SELECT ci.* + FROM cloth_image ci + INNER JOIN( + SELECT cloth_id, MIN(id) AS min_id + FROM cloth_image + WHERE cloth_id IN (:clothIds) + GROUP BY cloth_id + ) AS firsts ON ci.id=firsts.min_id +""",nativeQuery = true) + List findFirstImagesByClothIds(@Param("clothIds") List clothIds); + } diff --git a/src/main/java/study/goorm/domain/cloth/domain/repository/ClothRepository.java b/src/main/java/study/goorm/domain/cloth/domain/repository/ClothRepository.java index e27b013..ee14452 100644 --- a/src/main/java/study/goorm/domain/cloth/domain/repository/ClothRepository.java +++ b/src/main/java/study/goorm/domain/cloth/domain/repository/ClothRepository.java @@ -1,7 +1,16 @@ package study.goorm.domain.cloth.domain.repository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import study.goorm.domain.cloth.domain.entity.Cloth; +import study.goorm.domain.member.domain.entity.Member; + public interface ClothRepository extends JpaRepository { + Page findByMemberOrderByWearNumAsc(Member member, Pageable pageable); + Page findByMemberOrderByWearNumDesc(Member member, Pageable pageable); + Page findByMemberOrderByCreatedAtAsc(Member member, Pageable pageable); + Page findByMemberOrderByCreatedAtDesc(Member member, Pageable pageable); + } diff --git a/src/main/java/study/goorm/domain/cloth/dto/ClothRequestDTO.java b/src/main/java/study/goorm/domain/cloth/dto/ClothRequestDTO.java new file mode 100644 index 0000000..28a5b9b --- /dev/null +++ b/src/main/java/study/goorm/domain/cloth/dto/ClothRequestDTO.java @@ -0,0 +1,38 @@ +package study.goorm.domain.cloth.dto; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import study.goorm.domain.cloth.exception.annotation.CheckLowerUpperTempBound; +import study.goorm.domain.model.enums.Season; +import study.goorm.domain.model.enums.ThicknessLevel; + +import java.util.List; + +public class ClothRequestDTO { + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + @CheckLowerUpperTempBound + public static class ClothCreateRequest{ + private Long memberId; + private Long categoryId; + private String name; + private List seasons; + @Max(40) + @Min(-20) + private Integer tempUpperBound; + @Max(40) + @Min(-20) + private Integer tempLowerBound; + private ThicknessLevel thicknessLevel; + private String clothUrl; + private String brand; + + } +} diff --git a/src/main/java/study/goorm/domain/cloth/dto/ClothResponseDTO.java b/src/main/java/study/goorm/domain/cloth/dto/ClothResponseDTO.java new file mode 100644 index 0000000..15e991e --- /dev/null +++ b/src/main/java/study/goorm/domain/cloth/dto/ClothResponseDTO.java @@ -0,0 +1,72 @@ +package study.goorm.domain.cloth.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import study.goorm.domain.model.enums.Season; +import study.goorm.domain.model.enums.ThicknessLevel; +import java.util.List; + +public class ClothResponseDTO { + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class ClothEditViewResult{ + private Long id; + private String name; + private List seasons; + private int tempUpperBound; + private int tempLowerBound; + private ThicknessLevel thicknessLevel; + private String clothUrl; + private String brand; + private String imageUrl; + private Long categoryId; + + } + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class MemberClosetResult{ + private String nickName; + private ClothPreviewListResult clothPreviewListResult; + } + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class ClothPreviewListResult{ + private List clothPreviews; + private int totalPage; + private long totalElements; + private Boolean isFirst; + private Boolean isLast; + } + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class ClothPreview{ + private Long id; + private String name; + private String imageUrl; + private int wearNum; + } + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class ClothCreateResult{ + private Long id; + } + + +} diff --git a/src/main/java/study/goorm/domain/cloth/exception/ClothException.java b/src/main/java/study/goorm/domain/cloth/exception/ClothException.java new file mode 100644 index 0000000..e4e7be9 --- /dev/null +++ b/src/main/java/study/goorm/domain/cloth/exception/ClothException.java @@ -0,0 +1,12 @@ +package study.goorm.domain.cloth.exception; + +import study.goorm.global.error.code.BaseErrorCode; +import study.goorm.global.exception.GeneralException; + +public class ClothException extends GeneralException { + + public ClothException(BaseErrorCode code) { + super(code); + } + +} diff --git a/src/main/java/study/goorm/domain/cloth/exception/annotation/CheckLowerUpperTempBound.java b/src/main/java/study/goorm/domain/cloth/exception/annotation/CheckLowerUpperTempBound.java new file mode 100644 index 0000000..bee7914 --- /dev/null +++ b/src/main/java/study/goorm/domain/cloth/exception/annotation/CheckLowerUpperTempBound.java @@ -0,0 +1,18 @@ +package study.goorm.domain.cloth.exception.annotation; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import study.goorm.domain.cloth.exception.validator.CheckLowerUpperTempBoundValidator; + +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy = CheckLowerUpperTempBoundValidator.class) +@Target({ElementType.TYPE, ElementType.METHOD,ElementType.FIELD,ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface CheckLowerUpperTempBound { + + String message() default "상한 온도는 하한 온도보다 높아야 합니다."; + Class[] groups() default {}; + Class[] payload() default {}; +} diff --git a/src/main/java/study/goorm/domain/cloth/exception/validator/CheckLowerUpperTempBoundValidator.java b/src/main/java/study/goorm/domain/cloth/exception/validator/CheckLowerUpperTempBoundValidator.java new file mode 100644 index 0000000..f0971ea --- /dev/null +++ b/src/main/java/study/goorm/domain/cloth/exception/validator/CheckLowerUpperTempBoundValidator.java @@ -0,0 +1,32 @@ +package study.goorm.domain.cloth.exception.validator; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import study.goorm.domain.cloth.dto.ClothRequestDTO; +import study.goorm.domain.cloth.exception.annotation.CheckLowerUpperTempBound; +import study.goorm.global.error.code.status.ErrorStatus; + +@Component +@RequiredArgsConstructor +public class CheckLowerUpperTempBoundValidator implements ConstraintValidator { + + @Override + public void initialize(CheckLowerUpperTempBound constraintAnnotation){ + ConstraintValidator.super.initialize(constraintAnnotation); + } + + @Override + public boolean isValid(ClothRequestDTO.ClothCreateRequest request, ConstraintValidatorContext context) { + boolean isValid=request.getTempLowerBound()<=request.getTempUpperBound(); + + if(!isValid){ + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(ErrorStatus.LOWER_TEMP_BIGGER_THAN_UPPER_TEMP.toString()).addConstraintViolation(); + + + } + return isValid; + } +} diff --git a/src/main/java/study/goorm/domain/folder/domain/repository/ClothFolderRepository.java b/src/main/java/study/goorm/domain/folder/domain/repository/ClothFolderRepository.java index 143587d..f135bb0 100644 --- a/src/main/java/study/goorm/domain/folder/domain/repository/ClothFolderRepository.java +++ b/src/main/java/study/goorm/domain/folder/domain/repository/ClothFolderRepository.java @@ -1,7 +1,9 @@ package study.goorm.domain.folder.domain.repository; import org.springframework.data.jpa.repository.JpaRepository; +import study.goorm.domain.cloth.domain.entity.Cloth; import study.goorm.domain.folder.domain.entity.ClothFolder; public interface ClothFolderRepository extends JpaRepository { + void deleteAllByCloth(Cloth cloth); } diff --git a/src/main/java/study/goorm/domain/history/api/HistoryRestController.java b/src/main/java/study/goorm/domain/history/api/HistoryRestController.java new file mode 100644 index 0000000..2599774 --- /dev/null +++ b/src/main/java/study/goorm/domain/history/api/HistoryRestController.java @@ -0,0 +1,152 @@ +package study.goorm.domain.history.api; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import lombok.RequiredArgsConstructor; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.http.MediaType; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import study.goorm.domain.history.application.HistoryService; +import study.goorm.domain.history.dto.HistoryRequestDTO; +import study.goorm.domain.history.dto.HistoryResponseDTO; +import study.goorm.global.common.response.BaseResponse; +import study.goorm.global.error.code.status.SuccessStatus; + +import java.time.YearMonth; +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/histories") +@Validated +public class HistoryRestController { + private final HistoryService historyService; + + @GetMapping("/monthly") + @Operation(summary ="월별 기록을 조회하는 API",description = "Query Parameter로 clokeyId와 month(YYYY-MM)를 전달해주세요. clokeyId를 생략하면 본인기록을 조회합니다.") + @ApiResponses({ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "HISTORY_200", description = "OK, 성공적으로 조회되었습니다.")}) + public BaseResponse getMonthlyHistoryView( + @RequestParam(required = false) String clokeyId, + @RequestParam @DateTimeFormat(pattern = "yyyy-MM") YearMonth month + ){ + HistoryResponseDTO.HistoryMonthlyViewResult result=historyService.getMonthlyHistoryView(clokeyId,month); + return BaseResponse.onSuccess(SuccessStatus.HISTORY_MONTHLY_VIEW_SUCCESS, result); + } + + @GetMapping("/{history-id}") + @Operation(summary ="일별 기록을 조회하는 API",description = "Path Variable로 historyId를 던져주세요.") + @ApiResponses({ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "HISTORY_200", description = "OK, 성공적으로 조회되었습니다.")}) + public BaseResponse getDailyHistoryView( + @PathVariable(name="history-id") Long historyId + ){ + HistoryResponseDTO.HistoryDailyViewResult result=historyService.getDailyHistoryView(historyId); + return BaseResponse.onSuccess(SuccessStatus.HISTORY_DAILY_VIEW_SUCCESS, result); + } + + @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @Operation(summary = "기록을 추가하는 API",description = "request body에 HistoryCreateRequest 형식의 데이터를 전달해주세요.") + @ApiResponses({ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "HISTORY_201", description = "CREATED, 기록이 성공적으로 추가되었습니다."),}) + public BaseResponse createHistory( + @RequestPart("historyCreateRequest")HistoryRequestDTO.HistoryCreateRequest historyCreateRequest, + @RequestPart("imageFile") List imageFiles + + ){ + HistoryResponseDTO.HistoryCreateResult result=historyService.createHistory(historyCreateRequest,imageFiles); + return BaseResponse.onSuccess(SuccessStatus.HISTORY_CREATED,result); + } + + + + @PatchMapping(value = "/{history-id}",consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @Operation(summary = "기록을 수정하는 API",description = "request body에 HistoryUpdateRequest 형식의 데이터를 전달해주세요.") + @ApiResponses({ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "HISTORY_200", description = "UPDATED, 기록이 성공적으로 수정되었습니다."),}) + public BaseResponse updateHistory( + @Parameter(description = "수정할 기록ID") @PathVariable Long historyId, + @RequestPart("historyUpdateRequest")HistoryRequestDTO.HistoryUpdateRequest historyUpdateRequest, + @RequestPart("imageFile")List imageFiles + + ){ + HistoryResponseDTO.HistoryUpdateResult result=historyService.updateHistory(historyId ,historyUpdateRequest ,imageFiles); + return BaseResponse.onSuccess(SuccessStatus.HISTORY_UPDATED,result); + } + + @DeleteMapping("/{history-id}") + @Operation(summary="특정 기록을 삭제하는 API",description = "path variable로 history_id를 넘겨주세요.") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "HISTORY_200", description = "기록이 성공적으로 삭제되었습니다."), + }) + @Parameters({ + @Parameter(name = "history-id", description = "기록의 id, path variable 입니다.") + }) + public BaseResponse deleteHistory( + @PathVariable(value = "history-id") Long historyId + ) { + historyService.deleteHistory(historyId); + return BaseResponse.onSuccess(SuccessStatus.HISTORY_DELETED, null); + } + + + + @PostMapping("/like") + @Operation(summary="좋아요 누르기 / 취소 기능 API",description = "좋아요 기능입니다.") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "HISTORY_200", description = "좋아요 상태가 성공적으로 변경되었습니다."), + }) + public BaseResponse likeHistory( + @RequestPart("likeRequest")HistoryRequestDTO.LikeRequest likeRequest + + ){ + HistoryResponseDTO.LikeResult result=historyService.likeHistory(likeRequest); + return BaseResponse.onSuccess(SuccessStatus.LIKE_UPDATED,result); + } + + + + @GetMapping("/{history-id}/likes") + @Operation(summary="특정 게시물에 좋아요를 누른 유저들의 정보를 확인 API",description = "path variable로 history_id를 넘겨주세요.") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "HISTORY_200", description = "좋아요 상태가 성공적으로 변경되었습니다."), + }) + @Parameters({ + @Parameter(name = "history-id", description = "기록의 id, path variable 입니다.") + }) + public BaseResponse getLikedUsers( + @PathVariable(value = "history-id") Long historyId + ){ + HistoryResponseDTO.LikedUsersResult result=historyService.likedUser(historyId); + return BaseResponse.onSuccess(SuccessStatus.LIKED_USERS_VIEW_SUCCESS, result); + + } + + @PostMapping("/{historyId}/comments") + @Operation(summary="댓글을 작성하는 API",description = "path variable로 history_id를 넘겨주세요.") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "HISTORY_200", description = "댓글이 성공적으로 추가되었습니다."), + }) + @Parameters({ + @Parameter(name = "history-id", description = "기록의 id, path variable 입니다.") + }) + public BaseResponse postWriteComment( + @PathVariable(value = "history-id") Long historyId, + @RequestPart("likeRequest")HistoryRequestDTO.WriteCommentRequest request + ){ + HistoryResponseDTO.writeCommentResult result=historyService.writeComment(historyId,request); + return BaseResponse.onSuccess(SuccessStatus.COMMENT_CREATED,result); + } + + + + + + + + + + + + +} diff --git a/src/main/java/study/goorm/domain/history/application/HistoryImageQueryService.java b/src/main/java/study/goorm/domain/history/application/HistoryImageQueryService.java new file mode 100644 index 0000000..9cdbd6e --- /dev/null +++ b/src/main/java/study/goorm/domain/history/application/HistoryImageQueryService.java @@ -0,0 +1,10 @@ +package study.goorm.domain.history.application; + +import study.goorm.domain.history.domain.entity.History; + +import java.util.List; +import java.util.Map; + +public interface HistoryImageQueryService { + Map getFirstImageUrlMap(List histories); +} diff --git a/src/main/java/study/goorm/domain/history/application/HistoryImageQueryServiceImpl.java b/src/main/java/study/goorm/domain/history/application/HistoryImageQueryServiceImpl.java new file mode 100644 index 0000000..8c014e2 --- /dev/null +++ b/src/main/java/study/goorm/domain/history/application/HistoryImageQueryServiceImpl.java @@ -0,0 +1,32 @@ +package study.goorm.domain.history.application; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import study.goorm.domain.history.domain.entity.History; +import study.goorm.domain.history.domain.entity.HistoryImage; +import study.goorm.domain.history.domain.repository.HistoryImageRepository; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class HistoryImageQueryServiceImpl implements HistoryImageQueryService { + private final HistoryImageRepository historyImageRepository; + + public Map getFirstImageUrlMap(List histories) { + List historyIds=histories.stream() + .map(History::getId) + .toList(); + List firstImages=historyImageRepository.findFirstImagesByHistoryIds(historyIds); + + return firstImages.stream() + .collect(Collectors.toMap( + image->image.getHistory().getId(), + HistoryImage::getImageUrl + )); + } + + +} diff --git a/src/main/java/study/goorm/domain/history/application/HistoryService.java b/src/main/java/study/goorm/domain/history/application/HistoryService.java new file mode 100644 index 0000000..36f09c2 --- /dev/null +++ b/src/main/java/study/goorm/domain/history/application/HistoryService.java @@ -0,0 +1,31 @@ +package study.goorm.domain.history.application; + +import org.springframework.data.domain.Page; +import org.springframework.web.multipart.MultipartFile; +import study.goorm.domain.history.dto.HistoryRequestDTO; +import study.goorm.domain.history.dto.HistoryResponseDTO; + + +import java.time.YearMonth; +import java.util.List; + +public interface HistoryService { + HistoryResponseDTO.HistoryDailyViewResult getDailyHistoryView(Long historyId); + HistoryResponseDTO.HistoryMonthlyViewResult getMonthlyHistoryView(String clokeyId, YearMonth month); + HistoryResponseDTO.HistoryUpdateResult updateHistory(Long historyId,HistoryRequestDTO.HistoryUpdateRequest historyUpdateRequest,List imageFiles); + public void deleteHistory(Long historyId); + + HistoryResponseDTO.HistoryCreateResult createHistory(HistoryRequestDTO.HistoryCreateRequest historyCreateRequest, List imageFiles); + + HistoryResponseDTO.LikeResult likeHistory(HistoryRequestDTO.LikeRequest likeRequest); + + HistoryResponseDTO.LikedUsersResult likedUser(Long historyId); + + HistoryResponseDTO.writeCommentResult writeComment(Long historyId,HistoryRequestDTO.WriteCommentRequest request); + + + + + +} + diff --git a/src/main/java/study/goorm/domain/history/application/HistoryServiceImpl.java b/src/main/java/study/goorm/domain/history/application/HistoryServiceImpl.java new file mode 100644 index 0000000..04d5422 --- /dev/null +++ b/src/main/java/study/goorm/domain/history/application/HistoryServiceImpl.java @@ -0,0 +1,376 @@ +package study.goorm.domain.history.application; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; +import study.goorm.domain.cloth.domain.entity.Cloth; +import study.goorm.domain.cloth.domain.repository.ClothRepository; +import study.goorm.domain.history.converter.HistoryConverter; +import study.goorm.domain.history.domain.entity.*; +import study.goorm.domain.history.domain.repository.*; +import study.goorm.domain.history.dto.HistoryRequestDTO; +import study.goorm.domain.history.dto.HistoryResponseDTO; +import study.goorm.domain.history.exception.HistoryException; +import study.goorm.domain.member.domain.entity.Member; +import study.goorm.domain.member.domain.repository.MemberRepository; +import study.goorm.global.common.utils.MinioUploader; +import study.goorm.global.error.code.status.ErrorStatus; + +import java.time.LocalDate; +import java.time.YearMonth; +import java.util.List; +import java.util.Map; + + +@Service +@RequiredArgsConstructor +public class HistoryServiceImpl implements HistoryService { + private final HistoryRepository historyRepository; + private final CommentRepository commentRepository; + private final HistoryClothRepository historyClothRepository; + private final HashtagHistoryRepository historyHashtagRepository; + private final HashtagHistoryRepository hashtagHistoryRepository; + private final HistoryImageRepository historyImageRepository; + private final MemberRepository memberRepository; + private final HistoryImageQueryService historyImageQueryService; + private final ClothRepository clothRepository; + private final HashtagRepository hashtagRepository; + private final MinioUploader minioUploader; + private final MemberLikeRepository memberLikeRepository; + private final followRepository followRepository; + + + @Override + @Transactional(readOnly = true) + public HistoryResponseDTO.HistoryMonthlyViewResult getMonthlyHistoryView(String clokeyId, YearMonth month){ + + String effectiveClokeyId=(clokeyId==null) ? "clo001" : clokeyId; + + Member member=memberRepository.findByClokeyId(effectiveClokeyId) + .orElseThrow(()->new HistoryException(ErrorStatus.NO_SUCH_HISTORY_MEMBER)); + + LocalDate start=month.atDay(1); + LocalDate end=month.atEndOfMonth(); + List histories=historyRepository.findByMemberIdAndHistoryDateBetween(member.getId(),start,end); + Map imageUrlMap=historyImageQueryService.getFirstImageUrlMap(histories); + return HistoryConverter.toHistoryMonthlyViewResult(histories,member,imageUrlMap); + + + + } + + @Override + @Transactional(readOnly = true) + public HistoryResponseDTO.HistoryDailyViewResult getDailyHistoryView(Long historyId){ + + History history=historyRepository.findById(historyId) + .orElseThrow(()->new HistoryException(ErrorStatus.NO_SUCH_HISTORY)); + + //List hashtag와 List clothes 차이 + List hashtag = historyHashtagRepository.findAllByHistory_Id(historyId)//n+1발생할것임->fetchjoin hashtagtable같이 조인해서 가져오기 or hashtag repository에 jpql로 로직짜기!! + .stream() + .map(hc->hc.getHashtag().getName()) + .toList(); + + + List clothes=historyClothRepository.findAllByHistory(history) + .stream() + .map(hc->hc.getCloth().getName()) + .toList(); + + int commentCount=commentRepository.countByHistory(history); + + + boolean liked= false; + return HistoryConverter.toHistoryDailyViewResult(history,hashtag,clothes,commentCount,liked); + } + + //코드참고함 + @Override + public HistoryResponseDTO.HistoryCreateResult createHistory( + HistoryRequestDTO.HistoryCreateRequest historyCreateRequest,List imageFiles + ) + + { + Member member=memberRepository.findById(1L) + .orElseThrow(()->new HistoryException(ErrorStatus.NO_SUCH_MEMBER)); + LocalDate date; + try{ + date= historyCreateRequest.getDate(); + + } catch(Exception e){ + throw new HistoryException(ErrorStatus.INVALID_HISTORY_DATE_FORTMAT); + } + History history = HistoryConverter.toHistoryEntity(historyCreateRequest, member); + historyRepository.save(history); + + if(imageFiles==null || imageFiles.isEmpty()){ + throw new HistoryException(ErrorStatus.EMPTY_HISTORY_IMAGE); + } + //minioUploader로 나중에 대체하기 + for(MultipartFile imageFile:imageFiles){ + HistoryImage image = HistoryImage.builder() + .history(history) + .imageUrl("지금은 url이 없음") + .build(); + historyImageRepository.save(image); + + } + + List clothIds=historyCreateRequest.getClothes(); + if(clothIds==null || clothIds.isEmpty()){ + throw new HistoryException(ErrorStatus.EMPTY_CLOTH); + } + if(clothIds.size()!= clothIds.stream().distinct().count()){ + throw new HistoryException(ErrorStatus.DUPLICATE_CLOTH); + } + for(Long clothId:clothIds){ + Cloth cloth = clothRepository.findById(clothId) + .orElseThrow(() -> new HistoryException(ErrorStatus.INVALID_CLOTH)); + cloth.increaseWearCount(); + HistoryCloth hc = HistoryCloth.builder(). + history(history) + .cloth(cloth) + .build(); + historyClothRepository.save(hc); + } + + List hashtags=historyCreateRequest.getHashtags(); + if(hashtags==null || hashtags.isEmpty()){ + throw new HistoryException(ErrorStatus.EMPTY_HASHTAGS); + } + if(hashtags.size()!= hashtags.stream().distinct().count()){ + throw new HistoryException(ErrorStatus.DUPLICATE_HASHTAGS); + } + for(String tag:hashtags){ + Hashtag hashtag = hashtagRepository.findByName(tag) + .orElseGet(() -> hashtagRepository.save(Hashtag.builder().name(tag).build())); + HashtagHistory hh = HashtagHistory.builder() + .history(history) + .hashtag(hashtag) + .build(); + hashtagHistoryRepository.save(hh); + + } + + + + return HistoryConverter.toHistoryCreateResult(history); + } + + //코드참고함 + @Override + public HistoryResponseDTO.HistoryUpdateResult updateHistory( + Long historyId,HistoryRequestDTO.HistoryUpdateRequest historyUpdateRequest,List imageFiles + ) { + + Member member = memberRepository.findById(1L) + .orElseThrow(() -> new HistoryException(ErrorStatus.NO_SUCH_MEMBER)); + History history = historyRepository.findById(historyId) + .orElseThrow(() -> new HistoryException(ErrorStatus.NO_SUCH_HISTORY)); + + if (!history.getMember().getId().equals(member.getId())) { + throw new HistoryException(ErrorStatus.NO_AUTHORITY_HISTORY); + } + history.updateContent(historyUpdateRequest.getContent()); + if (historyUpdateRequest.getContent() != null + && historyUpdateRequest.getContent().length() > 200) { + throw new HistoryException(ErrorStatus.CONTENT_LENGTH_EXCEEDED); + } + if (imageFiles != null && imageFiles.size() > 10) { + throw new HistoryException(ErrorStatus.TOO_MANY_IMAGES); + } + historyImageRepository.deleteAllByHistoryId(historyId); + +// if(imageFiles!=null && !imageFiles.isEmpty()){ +// for(MultipartFile imageFile:imageFiles){ +// String url=minioUploader.uploadImage(file); +// historyImageRepository.save(HistoryImage.builder() +// .history(history) +// .imageUrl(url) +// .build()); +// } +// } else{ +// throw new HistoryException(ErrorStatus.EMPTY_HISTORY_IMAGE); +// } + + historyClothRepository.deleteAllByHistoryId(historyId); + List clothes = historyUpdateRequest.getClothes(); + if (clothes.size() != clothes.stream().distinct().count()) { + throw new HistoryException(ErrorStatus.DUPLICATE_CLOTH); + } + for (Long clothId : clothes) { + Cloth cloth = clothRepository.findById(clothId) + .orElseThrow(() -> new HistoryException(ErrorStatus.INVALID_CLOTH)); + if (!cloth.getMember().getId().equals(member.getId())) { + throw new HistoryException(ErrorStatus.INVALID_CLOTH); + } + cloth.increaseWearCount(); + historyClothRepository.save(HistoryCloth.builder() + .history(history) + .cloth(cloth) + .build()); + } + + hashtagHistoryRepository.deleteAllByHistoryId(historyId); + List hashtags = historyUpdateRequest.getHashtags(); + if (hashtags != null && !hashtags.isEmpty()) { + if (hashtags.size() != hashtags.stream().distinct().count()) { + throw new HistoryException(ErrorStatus.DUPLICATE_HASHTAGS); + } + for (String tag : hashtags) { + Hashtag hashtag = hashtagRepository.findByName(tag) + .orElseGet(() -> hashtagRepository.save(Hashtag.builder().name(tag).build())); + hashtagHistoryRepository.save(HashtagHistory.builder() + .history(history) + .hashtag(hashtag) + .build()); + } + } + history.updateContent(historyUpdateRequest.getContent()); + + return HistoryConverter.toHistoryUpdateResult(history); + + } + + @Override + @Transactional + public void deleteHistory(Long historyId) { + History history=historyRepository.findById(historyId) + .orElseThrow(()->new HistoryException(ErrorStatus.NO_SUCH_HISTORY)); + + hashtagHistoryRepository.deleteAllByHistory(history); + historyClothRepository.deleteAllByHistory(history); + historyImageRepository.deleteAllByHistory(history); + + historyRepository.delete(history); + } + + @Override + @Transactional(readOnly = true) + public HistoryResponseDTO.LikeResult likeHistory(HistoryRequestDTO.LikeRequest likeRequest){ + History history=historyRepository.findById(likeRequest.getHistoryId()) + .orElseThrow(()->new HistoryException(ErrorStatus.NO_SUCH_HISTORY)); + Member member=memberRepository.findById(1L) + .orElseThrow(()->new HistoryException(ErrorStatus.NO_SUCH_MEMBER)); + boolean isLiked=memberLikeRepository.existsByMemberAndHistory(member,history); + + if(likeRequest.isLiked()!=isLiked){ + throw new HistoryException(ErrorStatus.LIKE_STATE_MISMATCH); + } + + if(isLiked){ + MemberLike like=memberLikeRepository.findByMemberAndHistory(member,history) + .orElseThrow(()->new HistoryException(ErrorStatus.LIKE_NOT_FOUND)); + }else{ + MemberLike like=MemberLike.builder() + .member(member) + .history(history) + .build(); + memberLikeRepository.save(like); + history.setLikes(history.getLikes()+1); + } + + Long likeCount=memberLikeRepository.countByHistory(history); + boolean newState=!isLiked; + + + return HistoryConverter.toHistoryLikeResult(history,newState,likeCount); + + } + + + + @Override + @Transactional(readOnly = true) + public HistoryResponseDTO.LikedUsersResult likedUser(Long historyId){ + History history=historyRepository.findById(historyId) + .orElseThrow(()->new HistoryException(ErrorStatus.NO_SUCH_HISTORY)); + //실제 로그인한 1L대신 사용자id로 써야할것 + Member member=memberRepository.findById(1L) + .orElseThrow(()->new HistoryException(ErrorStatus.NO_SUCH_MEMBER)); + + List likes = memberLikeRepository.findAllByHistory(history); + + //코드 참고 + List likedUsers=likes.stream() + .map(like->{ + Member likedMember=like.getMember(); + boolean isFollowing = followRepository.existsByFollowerAndFollowing(member, likedMember); + boolean me=likedMember.getId().equals(member.getId()); + + return HistoryResponseDTO.LikedUser.builder() + .memberId(likedMember.getId()) + .nickName(likedMember.getNickname()) + .imageUrl(likedMember.getProfileImageUrl()) + .followStatus(isFollowing) + .me(me) + .clokeyId(likedMember.getClokeyId()) + .build(); + }) + .toList(); + + return HistoryConverter.toLikedUsersResult(likedUsers); + + } + + + + @Override + @Transactional(readOnly = true) + public HistoryResponseDTO.writeCommentResult writeComment( + Long historyId,HistoryRequestDTO.WriteCommentRequest request + ){ + + History history=historyRepository.findById(historyId) + .orElseThrow(()->new HistoryException(ErrorStatus.NO_SUCH_HISTORY)); + + Member member=memberRepository.findById(1L) + .orElseThrow(()->new HistoryException(ErrorStatus.NO_SUCH_MEMBER)); + + + String content=request.getContent(); + if(content==null || content.isEmpty()){ + throw new HistoryException(ErrorStatus.COMMENT_CONTENT_EMPTY); + } + if(content.length()>50){ + throw new HistoryException(ErrorStatus.COMMENT_CONTENT_TOO_LONG); + } + + Comment newComment=Comment.builder() + .content(content) + .history(history) + .member(member) + .history(history) + .build(); + commentRepository.save(newComment); + return HistoryConverter.toHistoryWriteCommentResult(newComment); + + } + + + + + + + + + + + + + + + + + + + + + +} + diff --git a/src/main/java/study/goorm/domain/history/converter/HistoryConverter.java b/src/main/java/study/goorm/domain/history/converter/HistoryConverter.java new file mode 100644 index 0000000..619d6de --- /dev/null +++ b/src/main/java/study/goorm/domain/history/converter/HistoryConverter.java @@ -0,0 +1,170 @@ +package study.goorm.domain.history.converter; + + +import study.goorm.domain.history.domain.entity.Comment; +import study.goorm.domain.history.domain.entity.History; +import study.goorm.domain.history.dto.HistoryRequestDTO; +import study.goorm.domain.history.dto.HistoryResponseDTO; +import study.goorm.domain.member.domain.entity.Member; +import study.goorm.domain.model.enums.Visibility; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +//List에서 List로 바꿈 +public class HistoryConverter { + + + public static HistoryResponseDTO.HistoryMonthlyViewResult toHistoryMonthlyViewResult( + List histories, Member member, Map imageUrlMap + ) { + + List historyItems = histories.stream() + .map(history -> new HistoryResponseDTO.HistoryMonthlyViewResult.HistoryItem( + history.getId(), + history.getHistoryDate(), + imageUrlMap.getOrDefault(history.getId(), null) // 존재하지 않으면 null + )) + .collect(Collectors.toList()); + + return HistoryResponseDTO.HistoryMonthlyViewResult.builder() + .memberId(member.getId()) + .nickname(member.getNickname()) + .histories(historyItems) + .build(); + } + + public static HistoryResponseDTO.HistoryDailyViewResult toHistoryDailyViewResult( + History history, List hashtag, List clothes, int commentCount, boolean liked + ) { + return HistoryResponseDTO.HistoryDailyViewResult.builder() + .memberId(history.getMember().getId()) + .historyId(history.getId()) + .memberImageUrl(history.getMember().getProfileImageUrl()) + .nickName(history.getMember().getNickname()) + .clokeyId(history.getMember().getClokeyId()) + .contents(history.getContent()) + .hashtags(hashtag) + .likeCount(history.getLikes()) + .commentCount(commentCount) + .date(history.getHistoryDate()) + .clothes(clothes) + .liked(liked) + .build(); + + } + + + //코드 참고 + public static History toHistoryEntity(HistoryRequestDTO.HistoryCreateRequest historyRequestDTO,Member member) { + return History.builder() + .member(member) + .historyDate(historyRequestDTO.getDate()) + .likes(0) + .content(historyRequestDTO.getContent()) + .build(); + } + + public static HistoryResponseDTO.HistoryCreateResult toHistoryCreateResult( + History history + ) + { + return HistoryResponseDTO.HistoryCreateResult.builder() + .historyId(history.getId()) + .build(); + } + + + + public static HistoryResponseDTO.HistoryUpdateResult toHistoryUpdateResult(History history){ + return HistoryResponseDTO.HistoryUpdateResult.builder() + .historyId(history.getId()) + .build(); + } + + + + //안쓰이나 남겨둠 + public static HistoryRequestDTO.HistoryCreateRequest toHistoryCreateRequest( + History history,List clothes, List hashtags + ) { + return HistoryRequestDTO.HistoryCreateRequest.builder() + .content(history.getContent()) + .clothes(clothes) + .hashtags(hashtags) + .date(history.getHistoryDate()) + .build(); + } + + + //안쓰이나 남겨둠 + public static HistoryRequestDTO.HistoryUpdateRequest toHistoryUpdateRequest( + History history, List clothes, List hashtags, Visibility visibility) { + return HistoryRequestDTO.HistoryUpdateRequest.builder() + .content(history.getContent()) + .clothes(clothes) + .hashtags(hashtags) + .visibility(visibility.toString()) + .build(); + } + + public static HistoryResponseDTO.LikeResult toHistoryLikeResult( + History history,boolean newState, Long likeCount + + ){ + return HistoryResponseDTO.LikeResult.builder() + .historyId(history.getId()) + .isLiked(newState) + .likeCount(likeCount) + .build(); + + } + + public static HistoryResponseDTO.LikedUser toHistoryLikedUser( + Member member, boolean me, boolean followStatus + ){ + return HistoryResponseDTO.LikedUser.builder() + .memberId(member.getId()) + .clokeyId(member.getClokeyId()) + .nickName(member.getNickname()) + .imageUrl(member.getProfileImageUrl()) + .me(me) + .followStatus(followStatus) + .build(); + + } + + public static HistoryResponseDTO.LikedUsersResult toLikedUsersResult( + List likedUsers + ){ + + return HistoryResponseDTO.LikedUsersResult.builder() + .likedUsers(likedUsers) + .build(); + + } + + + public static HistoryResponseDTO.writeCommentResult toHistoryWriteCommentResult( + Comment comment + ){ + return HistoryResponseDTO.writeCommentResult.builder() + .commentId(comment.getId()) + .build(); + } + + + + + + + + + + + + + + +} diff --git a/src/main/java/study/goorm/domain/history/domain/entity/Follow.java b/src/main/java/study/goorm/domain/history/domain/entity/Follow.java new file mode 100644 index 0000000..94d0115 --- /dev/null +++ b/src/main/java/study/goorm/domain/history/domain/entity/Follow.java @@ -0,0 +1,16 @@ +package study.goorm.domain.history.domain.entity; + +import jakarta.persistence.*; +import study.goorm.domain.member.domain.entity.Member; + +@Entity +public class Follow { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + @ManyToOne + private Member follower; + + @ManyToOne + private Member following; +} diff --git a/src/main/java/study/goorm/domain/history/domain/entity/History.java b/src/main/java/study/goorm/domain/history/domain/entity/History.java index 2667b0c..422c541 100644 --- a/src/main/java/study/goorm/domain/history/domain/entity/History.java +++ b/src/main/java/study/goorm/domain/history/domain/entity/History.java @@ -37,4 +37,12 @@ public class History extends BaseEntity { @JoinColumn(name = "member_id", nullable = false) private Member member; + public void updateContent(String content) { + this.content = content; + } + + public void setLikes(int likes) { + this.likes = likes; + } + } diff --git a/src/main/java/study/goorm/domain/history/domain/entity/HistoryImage.java b/src/main/java/study/goorm/domain/history/domain/entity/HistoryImage.java index 93aec04..ebcba89 100644 --- a/src/main/java/study/goorm/domain/history/domain/entity/HistoryImage.java +++ b/src/main/java/study/goorm/domain/history/domain/entity/HistoryImage.java @@ -15,7 +15,7 @@ public class HistoryImage extends BaseEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(nullable = false, unique = true) + @Column(nullable = false, unique = true,length=1000) private String imageUrl; @ManyToOne(fetch = FetchType.LAZY) diff --git a/src/main/java/study/goorm/domain/history/domain/repository/CommentRepository.java b/src/main/java/study/goorm/domain/history/domain/repository/CommentRepository.java index 3ce29c9..f6c662a 100644 --- a/src/main/java/study/goorm/domain/history/domain/repository/CommentRepository.java +++ b/src/main/java/study/goorm/domain/history/domain/repository/CommentRepository.java @@ -1,7 +1,18 @@ package study.goorm.domain.history.domain.repository; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import study.goorm.domain.history.domain.entity.Comment; +import study.goorm.domain.history.domain.entity.History; + +import java.util.List; public interface CommentRepository extends JpaRepository { + int countByHistory(History history); + + + + + } diff --git a/src/main/java/study/goorm/domain/history/domain/repository/HashtagHistoryRepository.java b/src/main/java/study/goorm/domain/history/domain/repository/HashtagHistoryRepository.java index 571b65b..79f849c 100644 --- a/src/main/java/study/goorm/domain/history/domain/repository/HashtagHistoryRepository.java +++ b/src/main/java/study/goorm/domain/history/domain/repository/HashtagHistoryRepository.java @@ -2,6 +2,13 @@ import org.springframework.data.jpa.repository.JpaRepository; import study.goorm.domain.history.domain.entity.HashtagHistory; +import study.goorm.domain.history.domain.entity.History; + +import java.util.List; public interface HashtagHistoryRepository extends JpaRepository { + List findAllByHistory_Id(Long historyId); + + void deleteAllByHistory(History history); + void deleteAllByHistoryId(Long historyId); } diff --git a/src/main/java/study/goorm/domain/history/domain/repository/HashtagRepository.java b/src/main/java/study/goorm/domain/history/domain/repository/HashtagRepository.java index d149c62..d37eaed 100644 --- a/src/main/java/study/goorm/domain/history/domain/repository/HashtagRepository.java +++ b/src/main/java/study/goorm/domain/history/domain/repository/HashtagRepository.java @@ -3,5 +3,8 @@ import org.springframework.data.jpa.repository.JpaRepository; import study.goorm.domain.history.domain.entity.Hashtag; +import java.util.Optional; + public interface HashtagRepository extends JpaRepository { + Optional findByName(String name); } diff --git a/src/main/java/study/goorm/domain/history/domain/repository/HistoryClothRepository.java b/src/main/java/study/goorm/domain/history/domain/repository/HistoryClothRepository.java index 1042019..44c5799 100644 --- a/src/main/java/study/goorm/domain/history/domain/repository/HistoryClothRepository.java +++ b/src/main/java/study/goorm/domain/history/domain/repository/HistoryClothRepository.java @@ -1,7 +1,21 @@ package study.goorm.domain.history.domain.repository; import org.springframework.data.jpa.repository.JpaRepository; +import study.goorm.domain.cloth.domain.entity.Cloth; +import study.goorm.domain.history.domain.entity.History; import study.goorm.domain.history.domain.entity.HistoryCloth; +import java.util.List; +import java.util.Optional; + public interface HistoryClothRepository extends JpaRepository { + void deleteAllByCloth(Cloth cloth); + + Optional findAllByHistory_Id(Long historyId); + + List findAllByHistory(History history); + + void deleteAllByHistory(History history); + + void deleteAllByHistoryId(Long historyId); } diff --git a/src/main/java/study/goorm/domain/history/domain/repository/HistoryImageRepository.java b/src/main/java/study/goorm/domain/history/domain/repository/HistoryImageRepository.java index 52881cb..8da7c8b 100644 --- a/src/main/java/study/goorm/domain/history/domain/repository/HistoryImageRepository.java +++ b/src/main/java/study/goorm/domain/history/domain/repository/HistoryImageRepository.java @@ -1,7 +1,32 @@ package study.goorm.domain.history.domain.repository; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import study.goorm.domain.history.domain.entity.History; import study.goorm.domain.history.domain.entity.HistoryImage; +import java.util.List; + public interface HistoryImageRepository extends JpaRepository { + void deleteAllByHistory(History history); + + List findFirstImagesByHistory(History history); + + + History history(History history); + + @Query(""" + SELECT hi FROM HistoryImage hi + WHERE hi.id IN ( + SELECT MIN(hi2.id) + FROM HistoryImage hi2 + WHERE hi2.history.id IN :historyIds + GROUP BY hi2.history.id + ) +""") + List findFirstImagesByHistoryIds(@Param("historyIds") List historyIds); + void deleteAllByHistoryId(Long historyId); + + } diff --git a/src/main/java/study/goorm/domain/history/domain/repository/HistoryRepository.java b/src/main/java/study/goorm/domain/history/domain/repository/HistoryRepository.java index 5f66987..ccd486b 100644 --- a/src/main/java/study/goorm/domain/history/domain/repository/HistoryRepository.java +++ b/src/main/java/study/goorm/domain/history/domain/repository/HistoryRepository.java @@ -1,7 +1,33 @@ package study.goorm.domain.history.domain.repository; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; import study.goorm.domain.history.domain.entity.History; +import study.goorm.domain.history.dto.HistoryResponseDTO; + +import java.awt.print.Pageable; +import java.time.LocalDate; +import java.time.YearMonth; +import java.util.List; +import java.util.Optional; public interface HistoryRepository extends JpaRepository { + + + List findByMemberIdAndHistoryDateBetween(Long memberId, LocalDate historyDateAfter, LocalDate historyDateBefore); + + @Query("SELECT h FROM History h JOIN FETCH h.member WHERE h.id=:historyId") + Optional findByIdWithMember(@Param("historyId") Long historyId); + + + + + + + + + + } diff --git a/src/main/java/study/goorm/domain/history/domain/repository/MemberLikeRepository.java b/src/main/java/study/goorm/domain/history/domain/repository/MemberLikeRepository.java index b9f1634..6845bb8 100644 --- a/src/main/java/study/goorm/domain/history/domain/repository/MemberLikeRepository.java +++ b/src/main/java/study/goorm/domain/history/domain/repository/MemberLikeRepository.java @@ -1,7 +1,19 @@ package study.goorm.domain.history.domain.repository; import org.springframework.data.jpa.repository.JpaRepository; +import study.goorm.domain.history.domain.entity.History; import study.goorm.domain.history.domain.entity.MemberLike; +import study.goorm.domain.member.domain.entity.Member; + +import java.util.List; +import java.util.Optional; public interface MemberLikeRepository extends JpaRepository { + boolean existsByMemberAndHistory(Member member, History history); + Optional findByMemberAndHistory(Member member, History history); + Long countByHistory(History history); + + List findAllByHistoryId(Long historyId); + + List findAllByHistory(History history); } diff --git a/src/main/java/study/goorm/domain/history/domain/repository/followRepository.java b/src/main/java/study/goorm/domain/history/domain/repository/followRepository.java new file mode 100644 index 0000000..93d5197 --- /dev/null +++ b/src/main/java/study/goorm/domain/history/domain/repository/followRepository.java @@ -0,0 +1,9 @@ +package study.goorm.domain.history.domain.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import study.goorm.domain.history.domain.entity.Follow; +import study.goorm.domain.member.domain.entity.Member; + +public interface followRepository extends JpaRepository { + boolean existsByFollowerAndFollowing(Member follower, Member following); +} diff --git a/src/main/java/study/goorm/domain/history/dto/HistoryRequestDTO.java b/src/main/java/study/goorm/domain/history/dto/HistoryRequestDTO.java new file mode 100644 index 0000000..3c7a017 --- /dev/null +++ b/src/main/java/study/goorm/domain/history/dto/HistoryRequestDTO.java @@ -0,0 +1,61 @@ +package study.goorm.domain.history.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; +import java.util.List; + + +public class HistoryRequestDTO { + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class HistoryUpdateRequest{ + private String content; + private List clothes; + private List hashtags; + private String visibility; + } + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class HistoryCreateRequest{ + private String content; + private List clothes; + private List hashtags; + private LocalDate date; + } + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class LikeRequest{ + private Long historyId; + private boolean liked; + } + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class WriteCommentRequest{ + private String content; + private Long commentId; + + } + + + + + +} + + diff --git a/src/main/java/study/goorm/domain/history/dto/HistoryResponseDTO.java b/src/main/java/study/goorm/domain/history/dto/HistoryResponseDTO.java new file mode 100644 index 0000000..7a13c22 --- /dev/null +++ b/src/main/java/study/goorm/domain/history/dto/HistoryResponseDTO.java @@ -0,0 +1,145 @@ +package study.goorm.domain.history.dto; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import study.goorm.domain.cloth.domain.entity.ClothImage; + + + + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + +public class HistoryResponseDTO { + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class HistoryMonthlyViewResult{ + private Long memberId; + private String nickname; + private List histories; + + @Getter + @AllArgsConstructor + public static class HistoryItem{ + private Long historyId; + private LocalDate date; + private String imageUrl; + } + } + + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class HistoryDailyViewResult{ + private Long memberId; + private String memberImageUrl; + private String nickName; + private String clokeyId; + private String contents; + private List images; + private List hashtags; + private int likeCount; + private Boolean liked; + private LocalDate date; + private List clothes; + private Long ClothId; + private int commentCount; + private Long historyId; + + + } + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class HistoryCreateResult{ + private Long historyId; + } + + + + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class HistoryUpdateResult{ + private Long historyId; + + } + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class LikeResult{ + private Long historyId; + private boolean isLiked; + private long likeCount; + } + + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class LikedUser{ + private Long memberId; + private String clokeyId; + private String nickName; + private boolean followStatus; + private String imageUrl; + private Boolean me; + + } + + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class LikedUsersResult{ + private List likedUsers; + } + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class writeCommentResult{ + private Long commentId; + } + + @Builder + @Getter + @AllArgsConstructor + @NoArgsConstructor + public static class HistoryCommentProjectionDTO { + private Long commentId; + private String content; + private boolean isRoot; + private Long parentId; + private String clokeyId; + private String nickname; + private String profileImageUrl; + private LocalDateTime createdAt; + } + + + + + + + + + + +} diff --git a/src/main/java/study/goorm/domain/history/exception/HistoryException.java b/src/main/java/study/goorm/domain/history/exception/HistoryException.java new file mode 100644 index 0000000..b3307a2 --- /dev/null +++ b/src/main/java/study/goorm/domain/history/exception/HistoryException.java @@ -0,0 +1,11 @@ +package study.goorm.domain.history.exception; + +import study.goorm.global.error.code.BaseErrorCode; +import study.goorm.global.exception.GeneralException; + +public class HistoryException extends GeneralException { + public HistoryException(BaseErrorCode code) { + super(code); + } + +} diff --git a/src/main/java/study/goorm/domain/member/domain/exception/MemberException.java b/src/main/java/study/goorm/domain/member/domain/exception/MemberException.java new file mode 100644 index 0000000..e5ccfa4 --- /dev/null +++ b/src/main/java/study/goorm/domain/member/domain/exception/MemberException.java @@ -0,0 +1,12 @@ +package study.goorm.domain.member.domain.exception; + +import study.goorm.global.error.code.BaseErrorCode; +import study.goorm.global.exception.GeneralException; + +public class MemberException extends GeneralException { + + public MemberException(BaseErrorCode code) { + super(code); + } + +} diff --git a/src/main/java/study/goorm/domain/member/domain/repository/MemberRepository.java b/src/main/java/study/goorm/domain/member/domain/repository/MemberRepository.java index d095876..61f4aa2 100644 --- a/src/main/java/study/goorm/domain/member/domain/repository/MemberRepository.java +++ b/src/main/java/study/goorm/domain/member/domain/repository/MemberRepository.java @@ -1,7 +1,13 @@ package study.goorm.domain.member.domain.repository; import org.springframework.data.jpa.repository.JpaRepository; +import study.goorm.domain.cloth.domain.entity.Cloth; import study.goorm.domain.member.domain.entity.Member; +import java.util.Optional; + public interface MemberRepository extends JpaRepository { + + Optional findByClokeyId(String clokeyId); + } diff --git a/src/main/java/study/goorm/domain/member/domain/repository/MemberTermRepository.java b/src/main/java/study/goorm/domain/member/domain/repository/MemberTermRepository.java index 92c726c..1af3a38 100644 --- a/src/main/java/study/goorm/domain/member/domain/repository/MemberTermRepository.java +++ b/src/main/java/study/goorm/domain/member/domain/repository/MemberTermRepository.java @@ -1,7 +1,9 @@ package study.goorm.domain.member.domain.repository; import org.springframework.data.jpa.repository.JpaRepository; +import study.goorm.domain.cloth.domain.entity.Cloth; import study.goorm.domain.member.domain.entity.MemberTerm; public interface MemberTermRepository extends JpaRepository { + } diff --git a/src/main/java/study/goorm/domain/member/domain/repository/TermRepository.java b/src/main/java/study/goorm/domain/member/domain/repository/TermRepository.java index bcc6edb..13651e2 100644 --- a/src/main/java/study/goorm/domain/member/domain/repository/TermRepository.java +++ b/src/main/java/study/goorm/domain/member/domain/repository/TermRepository.java @@ -1,7 +1,9 @@ package study.goorm.domain.member.domain.repository; import org.springframework.data.jpa.repository.JpaRepository; +import study.goorm.domain.cloth.domain.entity.Cloth; import study.goorm.domain.member.domain.entity.Term; public interface TermRepository extends JpaRepository { + } diff --git a/src/main/java/study/goorm/domain/model/enums/ClothSort.java b/src/main/java/study/goorm/domain/model/enums/ClothSort.java new file mode 100644 index 0000000..ba569b0 --- /dev/null +++ b/src/main/java/study/goorm/domain/model/enums/ClothSort.java @@ -0,0 +1,9 @@ +package study.goorm.domain.model.enums; + +public enum ClothSort { + + WEAR, + NOT_WEAR, + LATEST, + OLDEST +} diff --git a/src/main/java/study/goorm/domain/model/enums/Visibility.java b/src/main/java/study/goorm/domain/model/enums/Visibility.java new file mode 100644 index 0000000..080be15 --- /dev/null +++ b/src/main/java/study/goorm/domain/model/enums/Visibility.java @@ -0,0 +1,5 @@ +package study.goorm.domain.model.enums; + +public enum Visibility { + PUBLIC,PRIAVTE +} diff --git a/src/main/java/study/goorm/domain/model/exception/annotation/CheckPage.java b/src/main/java/study/goorm/domain/model/exception/annotation/CheckPage.java new file mode 100644 index 0000000..9411134 --- /dev/null +++ b/src/main/java/study/goorm/domain/model/exception/annotation/CheckPage.java @@ -0,0 +1,20 @@ +package study.goorm.domain.model.exception.annotation; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import study.goorm.domain.model.exception.validator.CheckPageValidator; + +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy = CheckPageValidator.class) +@Target({ElementType.METHOD, ElementType.FIELD,ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface CheckPage { + + String message() default "페이지는 1이상 부터 입력이 가능합니다."; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/src/main/java/study/goorm/domain/model/exception/annotation/CheckPageSize.java b/src/main/java/study/goorm/domain/model/exception/annotation/CheckPageSize.java new file mode 100644 index 0000000..7b7cfaf --- /dev/null +++ b/src/main/java/study/goorm/domain/model/exception/annotation/CheckPageSize.java @@ -0,0 +1,19 @@ +package study.goorm.domain.model.exception.annotation; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import study.goorm.domain.model.exception.validator.CheckPageSizeValidator; + +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy= CheckPageSizeValidator.class) +@Target({ElementType.METHOD,ElementType.FIELD,ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface CheckPageSize { + String message() default "페이지 크기는 1이상 부터 입력이 가능합니다."; + + Class[] groups() default {}; + Class[] payload() default {}; + +} diff --git a/src/main/java/study/goorm/domain/model/exception/validator/CheckPageSizeValidator.java b/src/main/java/study/goorm/domain/model/exception/validator/CheckPageSizeValidator.java new file mode 100644 index 0000000..b01f662 --- /dev/null +++ b/src/main/java/study/goorm/domain/model/exception/validator/CheckPageSizeValidator.java @@ -0,0 +1,30 @@ +package study.goorm.domain.model.exception.validator; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import study.goorm.domain.model.exception.annotation.CheckPageSize; +import study.goorm.global.error.code.status.ErrorStatus; + +@Component +@RequiredArgsConstructor +public class CheckPageSizeValidator implements ConstraintValidator { + + @Override + public void initialize(CheckPageSize constraintAnnotation) { + ConstraintValidator.super.initialize(constraintAnnotation); + } + + @Override + public boolean isValid(Integer pageSize, ConstraintValidatorContext context){ + boolean isValid = pageSize>=1; + + if(!isValid){ + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(ErrorStatus.PAGE_SIZE_UNDER_ONE.toString()).addConstraintViolation(); + + } + return isValid; + } +} diff --git a/src/main/java/study/goorm/domain/model/exception/validator/CheckPageValidator.java b/src/main/java/study/goorm/domain/model/exception/validator/CheckPageValidator.java new file mode 100644 index 0000000..ec4cf1f --- /dev/null +++ b/src/main/java/study/goorm/domain/model/exception/validator/CheckPageValidator.java @@ -0,0 +1,30 @@ +package study.goorm.domain.model.exception.validator; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import study.goorm.domain.model.exception.annotation.CheckPage; +import study.goorm.global.error.code.status.ErrorStatus; + +@Component +@RequiredArgsConstructor +public class CheckPageValidator implements ConstraintValidator { + + @Override + public void initialize(CheckPage constraintAnnotation){ + ConstraintValidator.super.initialize(constraintAnnotation); + } + @Override + public boolean isValid(Integer page, ConstraintValidatorContext context){ + boolean isValid=page>=1; + if(!isValid){ + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(ErrorStatus.PAGE_UNDER_ONE.toString()).addConstraintViolation(); + + + } + return isValid; + } + +} diff --git a/src/main/java/study/goorm/global/common/utils/MinioUploader.java b/src/main/java/study/goorm/global/common/utils/MinioUploader.java new file mode 100644 index 0000000..6125f47 --- /dev/null +++ b/src/main/java/study/goorm/global/common/utils/MinioUploader.java @@ -0,0 +1,49 @@ +package study.goorm.global.common.utils; + +import lombok.RequiredArgsConstructor; +import lombok.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import java.util.UUID; + +@Component +@RequiredArgsConstructor +public class MinioUploader { + +// private final MinioClient minioClient; +// +// @Value("${minio.bucket}") +// private String bucket; +// +// public String uploadImage(MultipartFile file) { +// String objectName= UUID.randomUUID() + "_" + file.getOriginalFilename(); +// +// try{ +// minioClient.putObject( +// putObjectArgs.builder() +// .bucket(bucket) +// .object(objectName) +// .stream(file.getInputStream(),file.getSize(),-1) +// .contentType(file.getContentType) +// .build() +// ); +// return minioClient.getPresignedObjectUrl( +// GetPresignObjectUrlArgs.builder() +// .method(Method.GET) +// .bucket(bucket) +// .object(objectName) +// .expire(60*30) +// .build() +// ); +// +// +// +// }catch(Exception e) +// { +// throw new RuntimeException("MiniO 이미지 업로드 실패",e); +// +// } +// +// } +} diff --git a/src/main/java/study/goorm/global/config/MinioConfig.java b/src/main/java/study/goorm/global/config/MinioConfig.java new file mode 100644 index 0000000..3348d4c --- /dev/null +++ b/src/main/java/study/goorm/global/config/MinioConfig.java @@ -0,0 +1,29 @@ +package study.goorm.global.config; + +import lombok.Getter; +import lombok.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@Getter +public class MinioConfig { + +// @Value("${minio.url}") +// private String url; +// +// @Value("${minio.url}") +// private String accessKey; +// +// @Value("${minio.url}") +// private String secretKey; +// +// @Bean +// public MinioClient minioClient(){ +// return MinioClient.builder() +// .endpoint(url) +// .credentials(accessKey,secretKey) +// .build(); +// } + +} diff --git a/src/main/java/study/goorm/global/config/SwaggerConfig.java b/src/main/java/study/goorm/global/config/SwaggerConfig.java new file mode 100644 index 0000000..5d267d4 --- /dev/null +++ b/src/main/java/study/goorm/global/config/SwaggerConfig.java @@ -0,0 +1,54 @@ +package study.goorm.global.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.servers.Server; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; + +import java.util.ArrayList; +import java.util.List; + +@Configuration +public class SwaggerConfig { + /* + Multipart+JSON동시에 Swagger에서 테스트할떄 + content-type null -> application/octet-stream으로 처리되어 Jackson에서 인식 못하는 문제 해결 + */ + + @Autowired + public void configureMessageConverter(MappingJackson2HttpMessageConverter converter) { + List supportMediaTypes = new ArrayList<>(converter.getSupportedMediaTypes()); + supportMediaTypes.add(MediaType.APPLICATION_OCTET_STREAM); + converter.setSupportedMediaTypes(supportMediaTypes); + } + + @Bean + public OpenAPI goormStudyAPI(){ + Info info = new Info() + .title("GOORM API") + .description("GOORM API 명세서") + .version("1.0.0"); + + String jwtSchemeName="JWT TOKEN"; + SecurityRequirement securityRequirement=new SecurityRequirement().addList(jwtSchemeName); + Components components = new Components() + .addSecuritySchemes(jwtSchemeName,new SecurityScheme() + .name(jwtSchemeName) + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT")); + return new OpenAPI() + .addServersItem(new Server().url("/")) + .info(info) + .addSecurityItem(securityRequirement) + .components(components); + + } +} diff --git a/src/main/java/study/goorm/global/error/code/status/ErrorStatus.java b/src/main/java/study/goorm/global/error/code/status/ErrorStatus.java index 5e1f9b3..0be17da 100644 --- a/src/main/java/study/goorm/global/error/code/status/ErrorStatus.java +++ b/src/main/java/study/goorm/global/error/code/status/ErrorStatus.java @@ -14,7 +14,57 @@ public enum ErrorStatus implements BaseErrorCode { _INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON500", "서버 에러, 관리자에게 문의 바랍니다."), _BAD_REQUEST(HttpStatus.BAD_REQUEST, "COMMON400", "잘못된 요청입니다."), _UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "COMMON401", "인증이 필요합니다."), - _FORBIDDEN(HttpStatus.FORBIDDEN, "COMMON403", "금지된 요청입니다."); + _FORBIDDEN(HttpStatus.FORBIDDEN, "COMMON403", "금지된 요청입니다."), + + //Cloth + NO_SUCH_CLOTH(HttpStatus.BAD_REQUEST,"CLOTH_4001","옷이 존재하지 않습니다"), + + //bug data + NO_ClOTH_IMAGE(HttpStatus.BAD_REQUEST,"CLOTH_4002","옷의 사진이 존재하지 않습니다."), + + //Member + NO_SUCH_MEMBER(HttpStatus.BAD_REQUEST,"MEMBER_4001","멤버가 존재하지 않습니다."), + + //Page + PAGE_UNDER_ONE(HttpStatus.BAD_REQUEST,"PAGE_4001","페이지는 1이상으로 입력해야 합니다."), + PAGE_SIZE_UNDER_ONE(HttpStatus.BAD_REQUEST,"PAGE_4002","페이지 사이즈는 1이상으로 입력해야합니다."), + + NO_SUCH_CATEGORY(HttpStatus.BAD_REQUEST,"CLOTH_4003","카테고리가 존재하지 않습니다."), + + LOWER_TEMP_BIGGER_THAN_UPPER_TEMP(HttpStatus.BAD_REQUEST,"CLOTH_4004","옷의 하한 온도가 상한 온도 보다 높습니다."), + + + //History + NO_SUCH_HISTORY(HttpStatus.NOT_FOUND, "HISTORY_4002","존재하지 않는 기록ID입니다"), + //hyowon + INVALID_HISTORY_DATE_FORTMAT(HttpStatus.BAD_REQUEST,"HISTORY_4001","잘못된 날짜 형식입니다."), + EMPTY_HISTORY_IMAGE(HttpStatus.BAD_REQUEST,"HISTORY_4004","기록의 이미지는 필수입니다."), + EMPTY_CLOTH(HttpStatus.BAD_REQUEST,"CLOTH_4005","등록할 옷이 비어있습니다."), + DUPLICATE_CLOTH(HttpStatus.BAD_REQUEST,"CLOTH_4006","중복되는 옷이 있습니다."), + INVALID_CLOTH(HttpStatus.BAD_REQUEST,"CLOTH_4007","존재하지 않거나 소유하지 않은 옷id입니다."), + + NO_AUTHORITY_HISTORY(HttpStatus.FORBIDDEN,"HISTORY_4006","기록에 접근 권한이 없습니다."), + NO_SUCH_HISTORY_MEMBER(HttpStatus.BAD_REQUEST,"MEMBER_4001","존재하지 않는 ID멤버입니다."), + CONTENT_LENGTH_EXCEEDED(HttpStatus.BAD_REQUEST,"HISTORY_4007","내용은 200자 초과 불과합니다"), + TOO_MANY_IMAGES(HttpStatus.BAD_REQUEST,"HISTORY_4008","이미지는 최대 10장까지만 업로드할 수 있습니다."), + + + //Like + LIKE_STATE_MISMATCH(HttpStatus.BAD_REQUEST,"LIKE_4001","유효하지 않은 좋아요상태입니다"), + LIKE_NOT_FOUND(HttpStatus.BAD_REQUEST,"LIKE_4002","Like를 찾지 못했습니다."), + + //HASHTAG + EMPTY_HASHTAGS(HttpStatus.BAD_REQUEST,"HASHTAG_4001","해쉬태그가 없습니다."), + DUPLICATE_HASHTAGS(HttpStatus.BAD_REQUEST,"HASHTAG_4002","중복되는 해시태그가 있습니다."), + + + COMMENT_CONTENT_EMPTY(HttpStatus.BAD_REQUEST,"COMMENT_4001","댓글이 비어있습니다."), + COMMENT_CONTENT_TOO_LONG(HttpStatus.BAD_REQUEST,"COMMENT_4002","댓글 글자수 초과입니다."), + ; + + + + private final HttpStatus httpStatus; private final String code; diff --git a/src/main/java/study/goorm/global/error/code/status/SuccessStatus.java b/src/main/java/study/goorm/global/error/code/status/SuccessStatus.java index 71d54d6..38a691b 100644 --- a/src/main/java/study/goorm/global/error/code/status/SuccessStatus.java +++ b/src/main/java/study/goorm/global/error/code/status/SuccessStatus.java @@ -11,7 +11,28 @@ public enum SuccessStatus implements BaseCode { //Common - OK(HttpStatus.OK, "COMMON_200", "성공입니다."); + OK(HttpStatus.OK, "COMMON_200", "성공입니다."), + + //Cloth + CLOTH_VIEW_SUCCESS(HttpStatus.OK,"CLOTH_200","옷이 성공적으로 조회되었습니다."), + + CLOTH_CREATED(HttpStatus.CREATED, "CLOTH_201","옷이 성공적으로 생성되었습니다."), + + CLOTH_DELETED(HttpStatus.NO_CONTENT,"CLOTH_202","옷이 성공적으로 삭제되었습니다."), + + HISTORY_DAILY_VIEW_SUCCESS(HttpStatus.OK,"HISTORY_200","성공적으로 조회되었습니다."), + HISTORY_MONTHLY_VIEW_SUCCESS(HttpStatus.OK,"HISTORY_201","월별기록이 성공적으로 조회되었습니다."), + + HISTORY_UPDATED(HttpStatus.OK,"HISTORY_200","기록이 성공적으로 수정되었습니다."), + HISTORY_CREATED(HttpStatus.OK,"HISTORY_201","기록이 성공적으로 추가되었습니다."), + + HISTORY_DELETED(HttpStatus.NO_CONTENT,"HISTORY_200","기록이 성공적으로 삭제되었습니다."), + LIKE_UPDATED(HttpStatus.OK,"LIKE_200","좋아요가 성공적으로 수정되었습니다"), + LIKED_USERS_VIEW_SUCCESS(HttpStatus.OK,"LIKE_201","좋아요 누른 유저들 성공적으로 확인되었습니다."), + + COMMENT_CREATED(HttpStatus.OK,"COMMENT_201","댓글이 성공적으로 추가되었습니다"),; + + private final HttpStatus httpStatus; private final String code; diff --git a/src/main/java/study/goorm/global/exception/GlobalExceptionAdvice.java b/src/main/java/study/goorm/global/exception/GlobalExceptionAdvice.java index 05f6cf3..2568ee7 100644 --- a/src/main/java/study/goorm/global/exception/GlobalExceptionAdvice.java +++ b/src/main/java/study/goorm/global/exception/GlobalExceptionAdvice.java @@ -3,7 +3,8 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.ConstraintViolationException; import lombok.extern.slf4j.Slf4j; -import org.hibernate.TypeMismatchException; + +import org.springframework.beans.TypeMismatchException; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatusCode; @@ -28,18 +29,17 @@ @RestControllerAdvice(annotations = {RestController.class}) public class GlobalExceptionAdvice extends ResponseEntityExceptionHandler { - //주석처리한 코드는 override가 안됨, getPropertyName()도 문제가 있음 -// @Override -// protected ResponseEntity handleTypeMismatch( -// TypeMismatchException e, -// HttpHeaders headers, -// HttpStatusCode status, -// WebRequest request) { -// String errorMessage = e.getPropertyName() + ": 올바른 값이 아닙니다."; -// -// return handleExceptionInternalMessage(e, headers, request, errorMessage); -// } + @Override + protected ResponseEntity handleTypeMismatch( + TypeMismatchException e, + HttpHeaders headers, + HttpStatusCode status, + WebRequest request) { + String errorMessage = e.getPropertyName() + ": 올바른 값이 아닙니다."; + + return handleExceptionInternalMessage(e, headers, request, errorMessage); + } @Override protected ResponseEntity handleMissingServletRequestParameter( @@ -77,7 +77,7 @@ public ResponseEntity handleMethodArgumentNotValid( WebRequest request) { Map errors = new LinkedHashMap<>(); - + //필드에러처리 e.getBindingResult().getFieldErrors().stream() .forEach( fieldError -> { @@ -94,6 +94,18 @@ public ResponseEntity handleMethodArgumentNotValid( (existingErrorMessage, newErrorMessage) -> existingErrorMessage + ", " + newErrorMessage); }); + //클래스 레벨 에러 처리(ObjectError) + e.getBindingResult().getGlobalErrors().forEach(objectError ->{ + String objectName=objectError.getObjectName(); + String errorMessage; + try{ + errorMessage = Optional.ofNullable(ErrorStatus.valueOf(objectError.getDefaultMessage()).getMessage()).orElse(""); + } catch (IllegalArgumentException ex) { + errorMessage = Optional.ofNullable(ErrorStatus.valueOf(objectError.getDefaultMessage()).getMessage()).orElse(""); + } + errors.merge("message :",errorMessage, + (existingErrorMessage, newErrorMessage) ->existingErrorMessage+ ", " + newErrorMessage); + } ); return handleExceptionInternalArgs( e, HttpHeaders.EMPTY, ErrorStatus.valueOf("_BAD_REQUEST"), request, errors); diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 252c411..305fc74 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -26,3 +26,8 @@ logging: org.hibernate.SQL: DEBUG org.hibernate.type.descriptor.sql.BasicBinder: TRACE org.springframework.jdbc.core: DEBUG +#minio: +# url:http://localhost:9000 +# access-key: +# secret-key: +# bucket: