diff --git a/backend/src/main/java/com/hallyugo/hallyugo/content/controller/ContentController.java b/backend/src/main/java/com/hallyugo/hallyugo/content/controller/ContentController.java index ee44fa9..2b27ffc 100644 --- a/backend/src/main/java/com/hallyugo/hallyugo/content/controller/ContentController.java +++ b/backend/src/main/java/com/hallyugo/hallyugo/content/controller/ContentController.java @@ -1,6 +1,7 @@ package com.hallyugo.hallyugo.content.controller; -import com.hallyugo.hallyugo.content.domain.ContentResponseDto; +import com.hallyugo.hallyugo.content.domain.response.ContentForMapResponseDto; +import com.hallyugo.hallyugo.content.domain.response.ContentResponseDto; import com.hallyugo.hallyugo.content.service.ContentService; import java.util.List; import java.util.Map; @@ -12,26 +13,50 @@ import org.springframework.web.bind.annotation.RestController; @RequiredArgsConstructor -@RequestMapping("/api/v1/content") +@RequestMapping("/api/v1/") @RestController public class ContentController { private final ContentService contentService; - @GetMapping("/initial") + @GetMapping("/content/initial") public ResponseEntity>> getRandomContents() { Map> result = contentService.getRandomContents(); return ResponseEntity.ok(result); } - @GetMapping(params = "category") + @GetMapping(value = "/content", params = "category") public ResponseEntity> getContentsByCategory(@RequestParam String category) { List result = contentService.getContentsByCategory(category); return ResponseEntity.ok(result); } - @GetMapping(params = "keyword") + @GetMapping(value = "/content", params = "keyword") public ResponseEntity> getContentsByKeyword(@RequestParam String keyword) { List result = contentService.getContentsByKeyword(keyword); return ResponseEntity.ok(result); } + + @GetMapping(value = "/location", params = "content_id") + public ResponseEntity getContentWithLocationsAndImages( + @RequestParam(name = "content_id") Long contentId + ) { + ContentForMapResponseDto result = contentService.getContentWithLocationsAndImages(contentId); + return ResponseEntity.ok(result); + } + + @GetMapping(value = "/location", params = "category") + public ResponseEntity> getContentsWithLocationsAndImagesByCategory( + @RequestParam(name = "category") String category + ) { + List result = contentService.getContentsWithLocationsAndImagesByCategory(category); + return ResponseEntity.ok(result); + } + + @GetMapping(value = "/location", params = "keyword") + public ResponseEntity> searchContentsWithLocationsAndImagesByKeyword( + @RequestParam(name = "keyword") String keyword + ) { + List result = contentService.getContentsWithLocationsAndImagesByKeyword(keyword); + return ResponseEntity.ok(result); + } } \ No newline at end of file diff --git a/backend/src/main/java/com/hallyugo/hallyugo/content/domain/Content.java b/backend/src/main/java/com/hallyugo/hallyugo/content/domain/Content.java index b2cf074..2c317ce 100644 --- a/backend/src/main/java/com/hallyugo/hallyugo/content/domain/Content.java +++ b/backend/src/main/java/com/hallyugo/hallyugo/content/domain/Content.java @@ -1,7 +1,5 @@ package com.hallyugo.hallyugo.content.domain; -import com.hallyugo.hallyugo.location.domain.Location; -import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EntityListeners; @@ -10,11 +8,8 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; -import jakarta.persistence.OneToMany; import jakarta.persistence.Table; import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Getter; @@ -55,9 +50,6 @@ public class Content { @LastModifiedDate private LocalDateTime updatedAt; - @OneToMany(mappedBy = "content", cascade = CascadeType.ALL, orphanRemoval = true) - private List locations = new ArrayList<>(); - public Content(Category category, String title, String description, String contentImageUrl, String hashtag) { this.category = category; this.title = title; diff --git a/backend/src/main/java/com/hallyugo/hallyugo/content/domain/response/ContentForMapResponseDto.java b/backend/src/main/java/com/hallyugo/hallyugo/content/domain/response/ContentForMapResponseDto.java new file mode 100644 index 0000000..b60fb49 --- /dev/null +++ b/backend/src/main/java/com/hallyugo/hallyugo/content/domain/response/ContentForMapResponseDto.java @@ -0,0 +1,41 @@ +package com.hallyugo.hallyugo.content.domain.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.hallyugo.hallyugo.content.domain.Category; +import com.hallyugo.hallyugo.content.domain.Content; +import com.hallyugo.hallyugo.location.domain.response.LocationWithImagesResponseDto; +import java.util.List; +import lombok.Getter; + +public class ContentForMapResponseDto { + + @JsonProperty + private Long id; + + @JsonProperty + private Category category; + + @JsonProperty + private String title; + + @JsonProperty + private String hashtag; + + @JsonProperty + @Getter + private List locations; + + private ContentForMapResponseDto(Content content, List locations) { + this.id = content.getId(); + this.category = content.getCategory(); + this.title = content.getTitle(); + this.hashtag = content.getHashtag(); + this.locations = locations; + } + + public static ContentForMapResponseDto toDto(Content content, + List locationWithImages) { + return new ContentForMapResponseDto(content, locationWithImages); + } + +} diff --git a/backend/src/main/java/com/hallyugo/hallyugo/content/domain/ContentResponseDto.java b/backend/src/main/java/com/hallyugo/hallyugo/content/domain/response/ContentResponseDto.java similarity index 84% rename from backend/src/main/java/com/hallyugo/hallyugo/content/domain/ContentResponseDto.java rename to backend/src/main/java/com/hallyugo/hallyugo/content/domain/response/ContentResponseDto.java index e66cd04..89ba8e0 100644 --- a/backend/src/main/java/com/hallyugo/hallyugo/content/domain/ContentResponseDto.java +++ b/backend/src/main/java/com/hallyugo/hallyugo/content/domain/response/ContentResponseDto.java @@ -1,6 +1,8 @@ -package com.hallyugo.hallyugo.content.domain; +package com.hallyugo.hallyugo.content.domain.response; import com.fasterxml.jackson.annotation.JsonProperty; +import com.hallyugo.hallyugo.content.domain.Category; +import com.hallyugo.hallyugo.content.domain.Content; public class ContentResponseDto { diff --git a/backend/src/main/java/com/hallyugo/hallyugo/content/service/ContentService.java b/backend/src/main/java/com/hallyugo/hallyugo/content/service/ContentService.java index 6b70a67..8067911 100644 --- a/backend/src/main/java/com/hallyugo/hallyugo/content/service/ContentService.java +++ b/backend/src/main/java/com/hallyugo/hallyugo/content/service/ContentService.java @@ -1,12 +1,18 @@ package com.hallyugo.hallyugo.content.service; +import com.hallyugo.hallyugo.common.exception.EntityNotFoundException; +import com.hallyugo.hallyugo.common.exception.ExceptionCode; import com.hallyugo.hallyugo.content.domain.Category; import com.hallyugo.hallyugo.content.domain.Content; -import com.hallyugo.hallyugo.content.domain.ContentResponseDto; +import com.hallyugo.hallyugo.content.domain.response.ContentForMapResponseDto; +import com.hallyugo.hallyugo.content.domain.response.ContentResponseDto; import com.hallyugo.hallyugo.content.repository.ContentRepository; +import com.hallyugo.hallyugo.location.domain.response.LocationWithImagesResponseDto; +import com.hallyugo.hallyugo.location.service.LocationService; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -18,6 +24,7 @@ public class ContentService { private static final int PAGE_NUMBER = 0; private static final int INITIAL_CONTENTS_SIZE_PER_CATEGORY = 2; private final ContentRepository contentRepository; + private final LocationService locationService; public Map> getRandomContents() { Map> result = new HashMap<>(); @@ -45,4 +52,34 @@ public List getContentsByKeyword(String keyword) { List contents = contentRepository.findByTitleContainingIgnoreCase(keyword); return contents.stream().map(ContentResponseDto::toDto).toList(); } + + public ContentForMapResponseDto getContentWithLocationsAndImages(Long contentId) { + Content content = contentRepository.findById(contentId) + .orElseThrow(() -> new EntityNotFoundException(ExceptionCode.ENTITY_NOT_FOUND)); + + List locationsWithImages = + locationService.getLocationsWithImagesByContentId(contentId); + + return ContentForMapResponseDto.toDto(content, locationsWithImages); + } + + public List getContentsWithLocationsAndImagesByCategory(String category) { + List contents = contentRepository.findByCategory(Category.valueOf(category)); + + List contentDtos = contents.stream() + .map(content -> getContentWithLocationsAndImages(content.getId())).collect(Collectors.toList()); + + return contentDtos; + } + + public List getContentsWithLocationsAndImagesByKeyword(String keyword) { + List contents = contentRepository.findByTitleContainingIgnoreCase(keyword); + + return contents.stream().map(content -> { + List locationsWithImages = + locationService.getLocationsWithImagesByContentId(content.getId()); + return ContentForMapResponseDto.toDto(content, locationsWithImages); + }).toList(); + } + } diff --git a/backend/src/main/java/com/hallyugo/hallyugo/favorite/controller/FavoriteController.java b/backend/src/main/java/com/hallyugo/hallyugo/favorite/controller/FavoriteController.java index 52a725c..55758d4 100644 --- a/backend/src/main/java/com/hallyugo/hallyugo/favorite/controller/FavoriteController.java +++ b/backend/src/main/java/com/hallyugo/hallyugo/favorite/controller/FavoriteController.java @@ -6,7 +6,13 @@ import com.hallyugo.hallyugo.user.domain.User; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; @RestController @RequiredArgsConstructor @@ -23,4 +29,22 @@ public ResponseEntity getUserFavorite( FavoriteResponseDto result = favoriteService.getFavoritesByUser(user, limit); return ResponseEntity.ok(result); } + + @PostMapping("/favorite/on?location_id={locationId}") + public ResponseEntity increaseFavoriteCount( + @AuthUser User user, + @PathVariable Long locationId + ) { + favoriteService.increaseFavoriteCountAndSave(user, locationId); + return ResponseEntity.ok().build(); + } + + @DeleteMapping("/favorite/off?location_id={locationId}") + public ResponseEntity decreaseFavoriteCount( + @AuthUser User user, + @PathVariable Long locationId + ) { + favoriteService.decreaseFavoriteCountAndDelete(user, locationId); + return ResponseEntity.ok().build(); + } } diff --git a/backend/src/main/java/com/hallyugo/hallyugo/favorite/domain/EntityType.java b/backend/src/main/java/com/hallyugo/hallyugo/favorite/domain/EntityType.java deleted file mode 100644 index 8afc505..0000000 --- a/backend/src/main/java/com/hallyugo/hallyugo/favorite/domain/EntityType.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.hallyugo.hallyugo.favorite.domain; - -public enum EntityType { - CONTENT, - LOCATION -} diff --git a/backend/src/main/java/com/hallyugo/hallyugo/favorite/domain/response/FavoriteResponseItem.java b/backend/src/main/java/com/hallyugo/hallyugo/favorite/domain/response/FavoriteResponseItem.java index 3109afd..33edd2a 100644 --- a/backend/src/main/java/com/hallyugo/hallyugo/favorite/domain/response/FavoriteResponseItem.java +++ b/backend/src/main/java/com/hallyugo/hallyugo/favorite/domain/response/FavoriteResponseItem.java @@ -1,12 +1,10 @@ package com.hallyugo.hallyugo.favorite.domain.response; import com.fasterxml.jackson.annotation.JsonProperty; -import com.hallyugo.hallyugo.favorite.domain.EntityType; +import java.time.LocalDateTime; import lombok.Data; import lombok.NoArgsConstructor; -import java.time.LocalDateTime; - @Data @NoArgsConstructor public class FavoriteResponseItem { diff --git a/backend/src/main/java/com/hallyugo/hallyugo/favorite/repository/FavoriteRepository.java b/backend/src/main/java/com/hallyugo/hallyugo/favorite/repository/FavoriteRepository.java index 2228299..98c8844 100644 --- a/backend/src/main/java/com/hallyugo/hallyugo/favorite/repository/FavoriteRepository.java +++ b/backend/src/main/java/com/hallyugo/hallyugo/favorite/repository/FavoriteRepository.java @@ -1,10 +1,13 @@ package com.hallyugo.hallyugo.favorite.repository; import com.hallyugo.hallyugo.favorite.domain.Favorite; -import org.springframework.data.jpa.repository.JpaRepository; - import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; public interface FavoriteRepository extends JpaRepository { List findByUserId(Long userId); + + boolean existsByUserIdAndLocationId(Long userId, Long locationId); + + void deleteByUserIdAndLocationId(Long userId, Long locationId); } diff --git a/backend/src/main/java/com/hallyugo/hallyugo/favorite/service/FavoriteService.java b/backend/src/main/java/com/hallyugo/hallyugo/favorite/service/FavoriteService.java index 5a13eff..cd47ba0 100644 --- a/backend/src/main/java/com/hallyugo/hallyugo/favorite/service/FavoriteService.java +++ b/backend/src/main/java/com/hallyugo/hallyugo/favorite/service/FavoriteService.java @@ -11,11 +11,11 @@ import com.hallyugo.hallyugo.location.domain.Location; import com.hallyugo.hallyugo.location.repository.LocationRepository; import com.hallyugo.hallyugo.user.domain.User; +import jakarta.transaction.Transactional; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import java.util.List; - @Service @RequiredArgsConstructor public class FavoriteService { @@ -63,4 +63,41 @@ private FavoriteResponseItem createFavoriteResponseItem(Favorite favorite) { } return item; } + + @Transactional + public void increaseFavoriteCountAndSave(User user, Long locationId) { + // 전달된 locationId를 이용해 Location 객체 조회 + Location location = locationRepository.findById(locationId) + .orElseThrow(() -> new EntityNotFoundException(ExceptionCode.ENTITY_NOT_FOUND)); + + if (!favoriteRepository.existsByUserIdAndLocationId(user.getId(), locationId)) { + // 해당 객체의 favoriteCount 1 증가 + location.increaseFavoriteCount(); + + // favoriteCount가 갱신된 Location 객체 저장 + locationRepository.save(location); + + // Favorite 객체 생성 후 저장 + Favorite favorite = new Favorite(user, location); + favoriteRepository.save(favorite); + } + } + + @Transactional + public void decreaseFavoriteCountAndDelete(User user, Long locationId) { + // 전달된 locationId를 이용해 Location 객체 조회 + Location location = locationRepository.findById(locationId) + .orElseThrow(() -> new EntityNotFoundException(ExceptionCode.ENTITY_NOT_FOUND)); + + if (favoriteRepository.existsByUserIdAndLocationId(user.getId(), locationId)) { + // 해당 객체의 favoriteCount 1 감소 + location.decreaseFavoriteCount(); + + // favoriteCount가 갱신된 Location 객체 저장 + locationRepository.save(location); + + // Favorite 객체 삭제 + favoriteRepository.deleteByUserIdAndLocationId(user.getId(), locationId); + } + } } \ No newline at end of file diff --git a/backend/src/main/java/com/hallyugo/hallyugo/image/domain/response/ImageResponseDto.java b/backend/src/main/java/com/hallyugo/hallyugo/image/domain/response/ImageResponseDto.java new file mode 100644 index 0000000..08863cb --- /dev/null +++ b/backend/src/main/java/com/hallyugo/hallyugo/image/domain/response/ImageResponseDto.java @@ -0,0 +1,52 @@ +package com.hallyugo.hallyugo.image.domain.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.hallyugo.hallyugo.image.domain.Image; +import java.time.LocalDateTime; +import java.util.Objects; + +public class ImageResponseDto { + + @JsonProperty + private Long id; + + @JsonProperty("image_url") + private String imageUrl; + + @JsonProperty + private String description; + + @JsonProperty("created_at") + private LocalDateTime createdAt; + + private ImageResponseDto(Image image) { + this.id = image.getId(); + this.imageUrl = image.getImageUrl(); + this.description = image.getDescription(); + this.createdAt = image.getCreatedAt(); + } + + public static ImageResponseDto toDto(Image image) { + return new ImageResponseDto(image); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + ImageResponseDto that = (ImageResponseDto) obj; + + return Objects.equals(id, that.id); + } + +} diff --git a/backend/src/main/java/com/hallyugo/hallyugo/image/service/ImageService.java b/backend/src/main/java/com/hallyugo/hallyugo/image/service/ImageService.java new file mode 100644 index 0000000..9a0f4a6 --- /dev/null +++ b/backend/src/main/java/com/hallyugo/hallyugo/image/service/ImageService.java @@ -0,0 +1,18 @@ +package com.hallyugo.hallyugo.image.service; + +import com.hallyugo.hallyugo.image.domain.response.ImageResponseDto; +import com.hallyugo.hallyugo.image.repository.ImageRepository; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class ImageService { + private final ImageRepository imageRepository; + + public List getImagesByLocationId(Long locationId) { + return imageRepository.findByLocationId(locationId).stream() + .map(ImageResponseDto::toDto).toList(); + } +} diff --git a/backend/src/main/java/com/hallyugo/hallyugo/location/domain/Location.java b/backend/src/main/java/com/hallyugo/hallyugo/location/domain/Location.java index 3c77687..b78f8ef 100644 --- a/backend/src/main/java/com/hallyugo/hallyugo/location/domain/Location.java +++ b/backend/src/main/java/com/hallyugo/hallyugo/location/domain/Location.java @@ -1,8 +1,6 @@ package com.hallyugo.hallyugo.location.domain; import com.hallyugo.hallyugo.content.domain.Content; -import com.hallyugo.hallyugo.image.domain.Image; -import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EntityListeners; @@ -12,12 +10,9 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; -import jakarta.persistence.OneToMany; import jakarta.persistence.Table; import java.math.BigDecimal; import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Getter; @@ -35,7 +30,8 @@ @Entity public class Location { - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne(fetch = FetchType.LAZY) @@ -59,9 +55,11 @@ public class Location { private String pose; + @Column(name = "created_at") @CreatedDate private LocalDateTime createdAt; + @Column(name = "updated_at") @LastModifiedDate private LocalDateTime updatedAt; @@ -76,12 +74,11 @@ public Location(String title, BigDecimal latitude, BigDecimal longitude, String this.pose = pose; } - public void setContent(Content content) { - if (this.content != null) { - this.content.getLocations().remove(this); - } + public void increaseFavoriteCount() { + this.favoriteCount++; + } - this.content = content; - content.getLocations().add(this); + public void decreaseFavoriteCount() { + this.favoriteCount--; } } \ No newline at end of file diff --git a/backend/src/main/java/com/hallyugo/hallyugo/location/domain/response/LocationWithImagesResponseDto.java b/backend/src/main/java/com/hallyugo/hallyugo/location/domain/response/LocationWithImagesResponseDto.java new file mode 100644 index 0000000..f2cfc47 --- /dev/null +++ b/backend/src/main/java/com/hallyugo/hallyugo/location/domain/response/LocationWithImagesResponseDto.java @@ -0,0 +1,86 @@ +package com.hallyugo.hallyugo.location.domain.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.hallyugo.hallyugo.image.domain.response.ImageResponseDto; +import com.hallyugo.hallyugo.location.domain.Location; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import lombok.Getter; + +public class LocationWithImagesResponseDto { + + @JsonProperty + private Long id; + + @JsonProperty + private String title; + + @JsonProperty + private BigDecimal latitude; + + @JsonProperty + private BigDecimal longitude; + + @JsonProperty + private String description; + + @JsonProperty("video_link") + private String videoLink; + + @JsonProperty("favorite_cnt") + private Long favoriteCount; + + @JsonProperty + private String pose; + + @JsonProperty("created_at") + private LocalDateTime createdAt; + + @JsonProperty("updated_at") + private LocalDateTime updatedAt; + + @JsonProperty + @Getter + private List images = new ArrayList<>(); + + private LocationWithImagesResponseDto(Location location, List images) { + this.id = location.getId(); + this.title = location.getTitle(); + this.latitude = location.getLatitude(); + this.longitude = location.getLongitude(); + this.description = location.getDescription(); + this.videoLink = location.getVideoLink(); + this.favoriteCount = location.getFavoriteCount(); + this.pose = location.getPose(); + this.createdAt = location.getCreatedAt(); + this.updatedAt = location.getUpdatedAt(); + this.images = images; + } + + public static LocationWithImagesResponseDto toDto(Location location, List images) { + return new LocationWithImagesResponseDto(location, images); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + LocationWithImagesResponseDto that = (LocationWithImagesResponseDto) obj; + + return Objects.equals(id, that.id); + } + +} diff --git a/backend/src/main/java/com/hallyugo/hallyugo/location/repository/LocationRepository.java b/backend/src/main/java/com/hallyugo/hallyugo/location/repository/LocationRepository.java index d55a750..8b04057 100644 --- a/backend/src/main/java/com/hallyugo/hallyugo/location/repository/LocationRepository.java +++ b/backend/src/main/java/com/hallyugo/hallyugo/location/repository/LocationRepository.java @@ -1,7 +1,12 @@ package com.hallyugo.hallyugo.location.repository; import com.hallyugo.hallyugo.location.domain.Location; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; public interface LocationRepository extends JpaRepository { -} + + List findByContentId(Long contentId); + + List findByTitleContainingIgnoreCase(String keyword); +} \ No newline at end of file diff --git a/backend/src/main/java/com/hallyugo/hallyugo/location/service/LocationService.java b/backend/src/main/java/com/hallyugo/hallyugo/location/service/LocationService.java new file mode 100644 index 0000000..4cb7fa1 --- /dev/null +++ b/backend/src/main/java/com/hallyugo/hallyugo/location/service/LocationService.java @@ -0,0 +1,26 @@ +package com.hallyugo.hallyugo.location.service; + +import com.hallyugo.hallyugo.image.domain.response.ImageResponseDto; +import com.hallyugo.hallyugo.image.service.ImageService; +import com.hallyugo.hallyugo.location.domain.response.LocationWithImagesResponseDto; +import com.hallyugo.hallyugo.location.repository.LocationRepository; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class LocationService { + private final LocationRepository locationRepository; + private final ImageService imageService; + + public List getLocationsWithImagesByContentId(Long contentId) { + return locationRepository.findByContentId(contentId).stream() + .map(location -> { + List images = imageService.getImagesByLocationId(location.getId()); + return LocationWithImagesResponseDto.toDto(location, images); + }) + .toList(); + } + +} diff --git a/backend/src/main/java/com/hallyugo/hallyugo/stamp/controller/StampController.java b/backend/src/main/java/com/hallyugo/hallyugo/stamp/controller/StampController.java index f66e2e4..4867224 100644 --- a/backend/src/main/java/com/hallyugo/hallyugo/stamp/controller/StampController.java +++ b/backend/src/main/java/com/hallyugo/hallyugo/stamp/controller/StampController.java @@ -26,4 +26,13 @@ public ResponseEntity getUserStamp( StampResponseDto result = stampService.getStampsByUser(user, limit); return ResponseEntity.ok(result); } + + @GetMapping("/stamp/location") + public ResponseEntity getLocationStamp( + @AuthUser User user, + @RequestParam(name = "location_id") Long locationId + ) { + StampResponseDto result = stampService.getStampsByUserAndLocation(user, locationId); + return ResponseEntity.ok(result); + } } \ No newline at end of file diff --git a/backend/src/main/java/com/hallyugo/hallyugo/stamp/domain/response/StampResponseDto.java b/backend/src/main/java/com/hallyugo/hallyugo/stamp/domain/response/StampResponseDto.java index 55ce9f3..c304233 100644 --- a/backend/src/main/java/com/hallyugo/hallyugo/stamp/domain/response/StampResponseDto.java +++ b/backend/src/main/java/com/hallyugo/hallyugo/stamp/domain/response/StampResponseDto.java @@ -1,11 +1,10 @@ package com.hallyugo.hallyugo.stamp.domain.response; import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; import lombok.Data; import lombok.NoArgsConstructor; -import java.util.List; - @Data @NoArgsConstructor public class StampResponseDto { @@ -15,4 +14,13 @@ public class StampResponseDto { @JsonProperty("stamps") private List stamps; + + private StampResponseDto(List stamps) { + this.total = stamps.size(); + this.stamps = stamps; + } + + public static StampResponseDto toDto(List stamps) { + return new StampResponseDto(stamps); + } } \ No newline at end of file diff --git a/backend/src/main/java/com/hallyugo/hallyugo/stamp/repository/StampRepository.java b/backend/src/main/java/com/hallyugo/hallyugo/stamp/repository/StampRepository.java index 77fd120..cb18e7e 100644 --- a/backend/src/main/java/com/hallyugo/hallyugo/stamp/repository/StampRepository.java +++ b/backend/src/main/java/com/hallyugo/hallyugo/stamp/repository/StampRepository.java @@ -1,12 +1,13 @@ package com.hallyugo.hallyugo.stamp.repository; import com.hallyugo.hallyugo.stamp.domain.Stamp; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; -import java.util.List; - @Repository public interface StampRepository extends JpaRepository { List findByUserId(Long userId); + + List findByUserIdAndLocationId(Long userId, Long locationId); } diff --git a/backend/src/main/java/com/hallyugo/hallyugo/stamp/service/StampService.java b/backend/src/main/java/com/hallyugo/hallyugo/stamp/service/StampService.java index 5cdef5d..a70cc99 100644 --- a/backend/src/main/java/com/hallyugo/hallyugo/stamp/service/StampService.java +++ b/backend/src/main/java/com/hallyugo/hallyugo/stamp/service/StampService.java @@ -5,11 +5,10 @@ import com.hallyugo.hallyugo.stamp.domain.response.StampResponseItem; import com.hallyugo.hallyugo.stamp.repository.StampRepository; import com.hallyugo.hallyugo.user.domain.User; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import java.util.List; - @Service @RequiredArgsConstructor public class StampService { @@ -30,4 +29,13 @@ public StampResponseDto getStampsByUser(User user, int limit) { result.setStamps(stampResponseItems); return result; } + + public StampResponseDto getStampsByUserAndLocation(User user, Long locationId) { + List stamps = stampRepository.findByUserIdAndLocationId(user.getId(), locationId); + + List stampResponseItems = stamps.stream() + .map(StampResponseItem::toDto).toList(); + + return StampResponseDto.toDto(stampResponseItems); + } } \ No newline at end of file diff --git a/backend/src/test/java/com/hallyugo/hallyugo/HallyugoApplicationTests.java b/backend/src/test/java/com/hallyugo/hallyugo/HallyugoApplicationTests.java index 9ae8628..d4ffd26 100644 --- a/backend/src/test/java/com/hallyugo/hallyugo/HallyugoApplicationTests.java +++ b/backend/src/test/java/com/hallyugo/hallyugo/HallyugoApplicationTests.java @@ -8,8 +8,8 @@ @ActiveProfiles("test") class HallyugoApplicationTests { - @Test - void contextLoads() { - } + @Test + void contextLoads() { + } } diff --git a/backend/src/test/java/com/hallyugo/hallyugo/content/controller/ContentControllerTest.java b/backend/src/test/java/com/hallyugo/hallyugo/content/controller/ContentControllerTest.java index 4dd82dd..6470db9 100644 --- a/backend/src/test/java/com/hallyugo/hallyugo/content/controller/ContentControllerTest.java +++ b/backend/src/test/java/com/hallyugo/hallyugo/content/controller/ContentControllerTest.java @@ -8,7 +8,7 @@ import com.hallyugo.hallyugo.auth.AuthUserArgumentResolver; import com.hallyugo.hallyugo.content.domain.Category; import com.hallyugo.hallyugo.content.domain.Content; -import com.hallyugo.hallyugo.content.domain.ContentResponseDto; +import com.hallyugo.hallyugo.content.domain.response.ContentResponseDto; import com.hallyugo.hallyugo.content.service.ContentService; import java.util.ArrayList; import java.util.Arrays; diff --git a/backend/src/test/java/com/hallyugo/hallyugo/content/service/ContentServiceSqlTest.java b/backend/src/test/java/com/hallyugo/hallyugo/content/service/ContentServiceSqlTest.java new file mode 100644 index 0000000..eab4287 --- /dev/null +++ b/backend/src/test/java/com/hallyugo/hallyugo/content/service/ContentServiceSqlTest.java @@ -0,0 +1,97 @@ +package com.hallyugo.hallyugo.content.service; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.hallyugo.hallyugo.content.domain.response.ContentForMapResponseDto; +import com.hallyugo.hallyugo.content.repository.ContentRepository; +import jakarta.transaction.Transactional; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; + +@Transactional +@ActiveProfiles("test") +@Sql("/data.sql") +@SpringBootTest +public class ContentServiceSqlTest { + + @Autowired + private ContentRepository contentRepository; + + @Autowired + private ContentService contentService; + + @DisplayName("특정 콘텐츠와 연관된 위치와 각 위치에 대한 이미지를 조회할 수 있어야 한다.") + @Test + void 콘텐츠_위치_이미지_리스트_조회_성공_테스트() { + // given + int size = 10; + Long contentId = 1L; + + // when + ContentForMapResponseDto result = contentService.getContentWithLocationsAndImages(contentId); + + // then + assertEquals(size, result.getLocations().size()); + assertEquals(2, result.getLocations().get(0).getImages().size()); + assertEquals(1, result.getLocations().get(1).getImages().size()); + assertEquals(0, result.getLocations().get(2).getImages().size()); + } + + @DisplayName("카테고리를 이용해 콘텐츠와 연관된 위치와 각 위치에 대한 이미지를 조회할 수 있어야 한다.") + @Test + void 카테고리_콘텐츠_위치_이미지_리스트_조회_성공_테스트() { + // given + String category = "K_POP"; + int contentSize = 1; + int locationSize = 10; + + // when + List result = contentService.getContentsWithLocationsAndImagesByCategory(category); + + // then + assertEquals(contentSize, result.size()); + assertEquals(locationSize, result.getFirst().getLocations().size()); + assertEquals(2, result.getFirst().getLocations().get(0).getImages().size()); + assertEquals(1, result.getFirst().getLocations().get(1).getImages().size()); + assertEquals(0, result.getFirst().getLocations().get(2).getImages().size()); + } + + @DisplayName("키워드로 콘텐츠와 연관된 위치와 각 위치에 대한 이미지를 검색할 수 있어야 한다.") + @Test + void 키워드_콘텐츠_위치_이미지_리스트_검색_성공_테스트() { + // given + String keyword = "1"; + int expectedContentDtoSize = 1; + int expectedLocationWithImagesDtoSize = 10; + + // when + List result = contentService.getContentsWithLocationsAndImagesByKeyword(keyword); + + // then + assertEquals(expectedContentDtoSize, result.size()); + assertEquals(expectedLocationWithImagesDtoSize, result.getFirst().getLocations().size()); + } + + @DisplayName("키워드로 콘텐츠와 연관된 위치와 각 위치에 대한 이미지를 검색할 수 있어야 한다.") + @Test + void 키워드_콘텐츠_위치_이미지_리스트_검색_성공_테스트2() { + // given + String keyword = "title"; + int expectedContentDtoSize = 2; + int expectedLocationWithImagesDtoSize1 = 10; + int expectedLocationWithImagesDtoSize2 = 4; + + // when + List result = contentService.getContentsWithLocationsAndImagesByKeyword(keyword); + + // then + assertEquals(expectedContentDtoSize, result.size()); + assertEquals(expectedLocationWithImagesDtoSize1, result.getFirst().getLocations().size()); + assertEquals(expectedLocationWithImagesDtoSize2, result.getLast().getLocations().size()); + } +} diff --git a/backend/src/test/java/com/hallyugo/hallyugo/content/service/ContentServiceTest.java b/backend/src/test/java/com/hallyugo/hallyugo/content/service/ContentServiceTest.java index ab2eeb1..e0f6bed 100644 --- a/backend/src/test/java/com/hallyugo/hallyugo/content/service/ContentServiceTest.java +++ b/backend/src/test/java/com/hallyugo/hallyugo/content/service/ContentServiceTest.java @@ -2,7 +2,7 @@ import com.hallyugo.hallyugo.content.domain.Category; import com.hallyugo.hallyugo.content.domain.Content; -import com.hallyugo.hallyugo.content.domain.ContentResponseDto; +import com.hallyugo.hallyugo.content.domain.response.ContentResponseDto; import com.hallyugo.hallyugo.content.repository.ContentRepository; import jakarta.transaction.Transactional; import java.util.ArrayList; @@ -15,10 +15,8 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.ValueSource; -import org.redisson.api.RedissonClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.ActiveProfiles; @Transactional @@ -29,9 +27,6 @@ class ContentServiceTest { private static final int INITIAL_CONTENTS_SIZE_PER_CATEGORY = 2; private static final int TOTAL_CONTENTS_SIZE_PER_CATEGORY = 3; - @MockBean - private RedissonClient redissonClient; - @Autowired private ContentRepository contentRepository; @@ -115,4 +110,5 @@ private List createContentsByCategory(Category category) { return contentsByCategory; } + } \ No newline at end of file diff --git a/backend/src/test/java/com/hallyugo/hallyugo/favorite/service/FavoriteServiceTest.java b/backend/src/test/java/com/hallyugo/hallyugo/favorite/service/FavoriteServiceTest.java index 5b04064..801bf6e 100644 --- a/backend/src/test/java/com/hallyugo/hallyugo/favorite/service/FavoriteServiceTest.java +++ b/backend/src/test/java/com/hallyugo/hallyugo/favorite/service/FavoriteServiceTest.java @@ -1,9 +1,16 @@ package com.hallyugo.hallyugo.favorite.service; +import static org.assertj.core.api.Assertions.assertThat; + import com.hallyugo.hallyugo.favorite.domain.response.FavoriteResponseDto; import com.hallyugo.hallyugo.favorite.domain.response.FavoriteResponseItem; +import com.hallyugo.hallyugo.location.domain.Location; +import com.hallyugo.hallyugo.location.repository.LocationRepository; import com.hallyugo.hallyugo.user.domain.User; import com.hallyugo.hallyugo.user.repository.UserRepository; +import java.util.List; +import java.util.Optional; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -12,11 +19,6 @@ import org.springframework.test.context.jdbc.Sql; import org.springframework.transaction.annotation.Transactional; -import static org.assertj.core.api.Assertions.assertThat; - -import java.util.List; -import java.util.Optional; - @Transactional @ActiveProfiles("test") @Sql("/data.sql") @@ -29,6 +31,9 @@ public class FavoriteServiceTest { @Autowired private UserRepository userRepository; + @Autowired + private LocationRepository locationRepository; + @DisplayName("사용자의 즐겨찾기 조회 - 제한 없음") @Test void getFavoritesByUserTest_noLimit() { @@ -77,4 +82,80 @@ void getFavoriteDetailsTest() { assertThat(item.getImage()).isNotEmpty(); } } + + @DisplayName("좋아요 버튼이 눌리지 않은 상태에서 버튼을 누르면 좋아요 개수가 1 증가한다.") + @Test + void 좋아요_버튼_클릭_시_좋아요_개수_1_증가_테스트() { + // given + Long userId = 1L; + Long locationId = 3L; + User user = userRepository.findById(userId).get(); + Location location = locationRepository.findById(locationId).get(); + Long initialFavoriteCount = location.getFavoriteCount(); + + // when + favoriteService.increaseFavoriteCountAndSave(user, locationId); + + // then + Assertions.assertThat(initialFavoriteCount + 1).isEqualTo( + locationRepository.findById(locationId).get().getFavoriteCount() + ); + } + + @DisplayName("좋아요 버튼이 눌린 상태에서 버튼을 누르면 좋아요 개수가 그대로 유지된다.") + @Test + void 좋아요_버튼_클릭_시_좋아요_개수_유지_테스트() { + // given + Long userId = 1L; + Long locationId = 1L; + User user = userRepository.findById(userId).get(); + Location location = locationRepository.findById(locationId).get(); + Long initialFavoriteCount = location.getFavoriteCount(); + + // when + favoriteService.increaseFavoriteCountAndSave(user, locationId); + + // then + Assertions.assertThat(initialFavoriteCount).isEqualTo( + locationRepository.findById(locationId).get().getFavoriteCount() + ); + } + + @DisplayName("좋아요 버튼이 눌린 상태에서 버튼을 취소하면 좋아요 개수가 1 감소한다.") + @Test + void 좋아요_버튼_언클릭_시_좋아요_개수_1_감소_테스트() { + // given + Long userId = 1L; + Long locationId = 1L; + User user = userRepository.findById(userId).get(); + Location location = locationRepository.findById(locationId).get(); + Long initialFavoriteCount = location.getFavoriteCount(); + + // when + favoriteService.decreaseFavoriteCountAndDelete(user, locationId); + + // then + Assertions.assertThat(initialFavoriteCount - 1).isEqualTo( + locationRepository.findById(locationId).get().getFavoriteCount() + ); + } + + @DisplayName("좋아요 버튼이 눌리지 않은 상태에서 버튼을 취소하면 좋아요 개수가 그대로 유지된다.") + @Test + void 좋아요_버튼_언클릭_시_좋아요_개수_유지_테스트() { + // given + Long userId = 1L; + Long locationId = 3L; + User user = userRepository.findById(userId).get(); + Location location = locationRepository.findById(locationId).get(); + Long initialFavoriteCount = location.getFavoriteCount(); + + // when + favoriteService.decreaseFavoriteCountAndDelete(user, locationId); + + // then + Assertions.assertThat(initialFavoriteCount).isEqualTo( + locationRepository.findById(locationId).get().getFavoriteCount() + ); + } } \ No newline at end of file diff --git a/backend/src/test/java/com/hallyugo/hallyugo/stamp/service/StampServiceTest.java b/backend/src/test/java/com/hallyugo/hallyugo/stamp/service/StampServiceTest.java index 10b2573..43c68c6 100644 --- a/backend/src/test/java/com/hallyugo/hallyugo/stamp/service/StampServiceTest.java +++ b/backend/src/test/java/com/hallyugo/hallyugo/stamp/service/StampServiceTest.java @@ -1,8 +1,12 @@ package com.hallyugo.hallyugo.stamp.service; +import static org.junit.jupiter.api.Assertions.assertEquals; + import com.hallyugo.hallyugo.stamp.domain.response.StampResponseDto; import com.hallyugo.hallyugo.stamp.repository.StampRepository; import com.hallyugo.hallyugo.user.domain.User; +import com.hallyugo.hallyugo.user.repository.UserRepository; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -10,8 +14,6 @@ import org.springframework.test.context.jdbc.Sql; import org.springframework.transaction.annotation.Transactional; -import static org.junit.jupiter.api.Assertions.assertEquals; - @Transactional @ActiveProfiles("test") @Sql("/data.sql") @@ -24,6 +26,9 @@ public class StampServiceTest { @Autowired private StampRepository stampRepository; + @Autowired + private UserRepository userRepository; + @Test public void testGetStampsByUser_withLimitGreaterThanAvailableStamps() { // Given @@ -53,7 +58,7 @@ public void testGetStampsByUser_withLimitLessThanAvailableStamps() { @Test public void testGetStampsByUser_noStamps() { // Given - User user = new User("test", "test", "test" ); // 스탬프가 없는 사용자 생성 + User user = new User("test", "test", "test"); // 스탬프가 없는 사용자 생성 // When StampResponseDto response = stampService.getStampsByUser(user, 10); // limit 10 @@ -62,4 +67,20 @@ public void testGetStampsByUser_noStamps() { assertEquals(0, response.getTotal()); // 전체 스탬프 수는 0 assertEquals(0, response.getStamps().size()); // 반환된 스탬프 개수는 0 } + + @DisplayName("사용자가 특정 위치에서 얻은 스탬프를 조회할 수 있어야 한다.") + @Test + public void 위치_스탬프_조회_성공_테스트() { + // given + Long userId = 1L; + Long locationId = 3L; + User user = userRepository.findById(userId).get(); + + // when + StampResponseDto result = stampService.getStampsByUserAndLocation(user, locationId); + + // then + assertEquals(1, result.getTotal()); + assertEquals(1, result.getStamps().size()); + } } diff --git a/frontend/app/(auth)/login/page.tsx b/frontend/app/(auth)/login/page.tsx index b262511..50265c5 100644 --- a/frontend/app/(auth)/login/page.tsx +++ b/frontend/app/(auth)/login/page.tsx @@ -1,17 +1,68 @@ +"use client"; + +import { useState } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; +import { signIn } from "next-auth/react"; export default function SignInPage() { + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(""); + + const onSubmit = async (event: React.FormEvent): Promise => { + event.preventDefault(); + + try { + const res = await signIn("credentials", { + redirect: false, + username, + password, + }); + + if (res && !res.error) { + console.log("Signed in: ", res); + window.location.href = "/"; + } else { + setError("Failed to sign in. Please check your credentials."); + } + } catch (error) { + //console.error("Failed to sign in: ", error); + setError("An unexpected error occurred. Please try again.: " + error); + } + }; + return (
- - - - +
+ setUsername(e.target.value)} + /> + setPassword(e.target.value)} + /> + + + {error &&

{error}

} +
); } diff --git a/frontend/app/(auth)/signup/page.tsx b/frontend/app/(auth)/signup/page.tsx index 6176bdd..ec639b9 100644 --- a/frontend/app/(auth)/signup/page.tsx +++ b/frontend/app/(auth)/signup/page.tsx @@ -1,17 +1,68 @@ +"use client"; + +import { useState } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; +import { baseUrl } from "@/lib/constants"; export default function SignupPage() { + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [nickname, setNickname] = useState(""); + const [error, setError] = useState(""); + + const onSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + try { + const res = await fetch(`${baseUrl}/api/v1/auth/signin`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ username, password, nickname }), + }); + if (res.ok) { + alert("Sign up successful!"); + window.location.href = "./login"; + } else { + setError("Failed to sign up: " + res.statusText); + } + } catch (error) { + setError("Failed to sign up: " + error); + } + }; + return ( -
- - - - -
+
+
+ setNickname(e.target.value)} + /> + setUsername(e.target.value)} + /> + setPassword(e.target.value)} + /> + + {error &&

{error}

} +
+
); } diff --git a/frontend/app/_components/AuthProvider.tsx b/frontend/app/_components/AuthProvider.tsx new file mode 100644 index 0000000..7749c6c --- /dev/null +++ b/frontend/app/_components/AuthProvider.tsx @@ -0,0 +1,12 @@ +"use client"; + +import React from "react"; +import { SessionProvider } from "next-auth/react"; + +interface Props { + children: React.ReactNode; +} + +export default function AuthProvider({ children }: Props): JSX.Element { + return {children}; +} diff --git a/frontend/app/_components/Carousel.tsx b/frontend/app/_components/Carousel.tsx index ef428ce..8aee32e 100644 --- a/frontend/app/_components/Carousel.tsx +++ b/frontend/app/_components/Carousel.tsx @@ -5,26 +5,52 @@ import { Carousel, CarouselContent, CarouselItem, + CarouselPrevious, + CarouselNext, } from "@/components/ui/carousel"; +import carouselLogo from "@/public/icons/carousel-logo.png"; +import Image from "next/image"; +import itzy from "@/public/itzy.png"; export default function CarousselMain() { return ( - {Array.from({ length: 3 }).map((_, index) => ( - -
- - - - Carousel {index + 1} - - - -
-
- ))} + +
+ + +
+
+

HallyuGo

+

+ Map App for K-culuture Lover +

+
+ init carousel +
+
+
+
+
+ + + +
+ itzy +
+

ITZY

+

- NOT SHY!

+
+
+
+
+
+
+ + +
); } diff --git a/frontend/app/_components/ContentCard.tsx b/frontend/app/_components/ContentCard.tsx index 69a3428..2875dc2 100644 --- a/frontend/app/_components/ContentCard.tsx +++ b/frontend/app/_components/ContentCard.tsx @@ -1,26 +1,38 @@ +import Image from "next/image"; import Link from "next/link"; export default function ContentCard({ + id, title, description, image, hashtags, }: { + id: number; title: string; description: string; - image?: string; + image: string; link?: string; hashtags?: string; }) { return ( - +

{title}

{description}

{hashtags}

-
{image}
+
+ {image} +
); diff --git a/frontend/app/_components/ContentsBox.tsx b/frontend/app/_components/ContentsBox.tsx index 4d9ba58..1c8140c 100644 --- a/frontend/app/_components/ContentsBox.tsx +++ b/frontend/app/_components/ContentsBox.tsx @@ -1,30 +1,24 @@ -import { Button } from "@/components/ui/button"; -import ContentCard from "./ContentCard"; +import Link from "next/link"; -export default function ContentsBox({ title }: { title: string }) { +export default function ContentsBox({ + title, + children, +}: { + title: string; + children: React.ReactNode; +}) { return (

{title}

- -
-
- - +
+
{children}
); } diff --git a/frontend/app/_components/Header.tsx b/frontend/app/_components/Header.tsx index 751de2d..218c26a 100644 --- a/frontend/app/_components/Header.tsx +++ b/frontend/app/_components/Header.tsx @@ -29,7 +29,7 @@ export default function Header() { {currentPath === "/map" && } - + {currentPath === "/my" && } diff --git a/frontend/app/_components/Map.tsx b/frontend/app/_components/Map.tsx deleted file mode 100644 index 1074e19..0000000 --- a/frontend/app/_components/Map.tsx +++ /dev/null @@ -1,22 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; -import { Coordinates } from "@/app/types/naverMaps"; -import Map from "./NaverMap"; -import MapSkeleton from "./MapSkeleton"; - -export default function MapContainer() { - const [loc, setLoc] = useState(); - - const initLocation = () => { - navigator.geolocation.getCurrentPosition((position) => { - setLoc([position.coords.longitude, position.coords.latitude]); - }); - }; - - useEffect(() => { - initLocation(); - }, []); - - return loc ? : ; -} diff --git a/frontend/app/_components/NaverMap.tsx b/frontend/app/_components/NaverMap.tsx index 93143e5..377efb1 100644 --- a/frontend/app/_components/NaverMap.tsx +++ b/frontend/app/_components/NaverMap.tsx @@ -1,37 +1,30 @@ "use client"; -import Script from "next/script"; -import { Coordinates, NaverMap } from "@/app/types/naverMaps"; -import { useCallback, useRef } from "react"; +import { + Container as MapDiv, + NaverMap, + Marker, + useNavermaps, +} from "react-naver-maps"; -const mapId = "naver-map"; +function MyMap() { + // instead of window.naver.maps + const navermaps = useNavermaps(); -export default function Map({ loc }: { loc: Coordinates }) { - const mapRef = useRef(); - - const initializeMap = useCallback(() => { - const mapOptions = { - center: new window.naver.maps.LatLng(loc), - zoom: 15, - scaleControl: true, - mapDataControl: true, - logoControlOptions: { - position: naver.maps.Position.BOTTOM_LEFT, - }, - }; - const map = new window.naver.maps.Map(mapId, mapOptions); - mapRef.current = map; - }, [loc]); + return ( + + + + ); +} +export default function SimpleMap() { return ( - <> - -
- + + + ); } diff --git a/frontend/app/_components/PhotoUploader.tsx b/frontend/app/_components/PhotoUploader.tsx new file mode 100644 index 0000000..587e6b1 --- /dev/null +++ b/frontend/app/_components/PhotoUploader.tsx @@ -0,0 +1,67 @@ +import React, { useRef } from "react"; +import { MdAddToPhotos } from "react-icons/md"; + +const PhotoUploader = () => { + const inputRef = useRef(null); + + const handleClick = () => { + inputRef.current?.click(); + }; + + const handleFileChange = (event: React.ChangeEvent) => { + const files = event.target.files; + const gallery = document.getElementById("photoGallery"); + + if (gallery) gallery.innerHTML = ""; // 기존 사진 초기화 + + if (files) { + Array.from(files).forEach((file) => { + if (file.type.startsWith("image/")) { + const reader = new FileReader(); + reader.onload = (e) => { + const img = document.createElement("img"); + img.src = e.target?.result as string; + img.style.width = "150px"; + img.style.margin = "10px"; + img.style.borderRadius = "8px"; + img.style.boxShadow = "0 4px 6px rgba(0, 0, 0, 0.1)"; + if (gallery) gallery.appendChild(img); + }; + reader.readAsDataURL(file); + } + }); + } + }; + + return ( +
+ {/* 클릭 트리거 */} +
+

ADD

+ +
+ + {/* 숨겨진 파일 입력 */} + + + {/* 갤러리 */} +
+
+ ); +}; + +export default PhotoUploader; diff --git a/frontend/app/_components/SearchBar.tsx b/frontend/app/_components/SearchBar.tsx index 04fbf40..fa997a9 100644 --- a/frontend/app/_components/SearchBar.tsx +++ b/frontend/app/_components/SearchBar.tsx @@ -1,14 +1,29 @@ +"use client"; + import { Input } from "@/components/ui/input"; import { IoSearch } from "react-icons/io5"; +import { useRouter } from "next/navigation"; // next/navigation에서 useRouter 가져오기 +import { useState } from "react"; export default function SearchBar() { + const [keyword, setKeyword] = useState(""); + const router = useRouter(); + + const onSubmit = (e: React.FormEvent) => { + e.preventDefault(); + router.push(`/search?keyword=${keyword}`); + }; + return (
- +
+ setKeyword(e.target.value)} + /> +
); } diff --git a/frontend/app/_components/UnderBar.tsx b/frontend/app/_components/UnderBar.tsx index 6b5cc78..0651434 100644 --- a/frontend/app/_components/UnderBar.tsx +++ b/frontend/app/_components/UnderBar.tsx @@ -5,7 +5,7 @@ export default function UnderBar({ isMy = false }: { isMy?: boolean }) { return ( diff --git a/frontend/app/api/auth/[...nextauth]/route.ts b/frontend/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..896d551 --- /dev/null +++ b/frontend/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1 @@ +export { GET, POST } from "@/lib/auth/route"; diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx index 1a5f8d3..d5d66b5 100644 --- a/frontend/app/layout.tsx +++ b/frontend/app/layout.tsx @@ -2,6 +2,8 @@ import type { Metadata } from "next"; import localFont from "next/font/local"; import "./globals.css"; import Script from "next/script"; +import AuthProvider from "./_components/AuthProvider"; +import { NavermapsProvider } from "react-naver-maps"; const geistSans = localFont({ src: "./fonts/GeistVF.woff", @@ -37,10 +39,11 @@ export default function RootLayout({ type="text/javascript" src={`https://oapi.map.naver.com/openapi/v3/maps.js?ncpClientId=${process.env.NEXT_PUBLIC_MAP_CLIENT_ID}&submodules=geocoder`} /> + - {children} + {children} ); diff --git a/frontend/app/map/[id]/_components/DialogueBox.tsx b/frontend/app/map/[id]/_components/DialogueBox.tsx new file mode 100644 index 0000000..52ac45d --- /dev/null +++ b/frontend/app/map/[id]/_components/DialogueBox.tsx @@ -0,0 +1,34 @@ +import { ReactNode } from "react"; +import { + Dialog, + DialogTitle, + DialogContent, + DialogTrigger, +} from "@/components/ui/dialog"; +import Image from "next/image"; + +interface DialogueBoxProps { + children: ReactNode; +} + +export function DialogueBox({ children }: DialogueBoxProps) { + return ( + + {children} + + +
+ photo +
+
+ stampImage + {} + +
+
+ USERNAME visited on DATE +
+
+
+ ); +} diff --git a/frontend/app/map/[id]/page.tsx b/frontend/app/map/[id]/page.tsx index dde9c9d..e42e883 100644 --- a/frontend/app/map/[id]/page.tsx +++ b/frontend/app/map/[id]/page.tsx @@ -3,6 +3,7 @@ import { CiHeart } from "react-icons/ci"; import Image from "next/image"; import kheart from "@/public/kheart_gray.png"; import { Button } from "@/components/ui/button"; +import { DialogueBox } from "./_components/DialogueBox"; export default async function Page() { return ( @@ -30,19 +31,20 @@ export default async function Page() { function StampSection() { return ( -
- kheart -

- You haven't visited this place yet!

Please enable GPS and - collect your stamp! -

-
+ +
+ kheart +

+ You haven't visited this place yet!
Please enable GPS and collect your stamp! +

+
+
); } function ProofShoots() { return ( -
+

Proof Shoots

+
+ +
+ + + + +
+
+ )} + ); } const MyList = () => { const total = 91; // TODO: fetch from API return ( -
-
+
+
+ <MyListModal /> </div> - <div className="flex flex-col gap-3"> + <div className="flex flex-col gap-3 rounded-xl border border-neutral-100 p-4"> <LocationCard title="Bongsuyuk" photo="photohere" @@ -48,17 +84,53 @@ const MyList = () => { }; const MyProofShot = () => { - const total = 21; + const [total, setTotal] = useState(0); + const [proofshots, setProofshots] = useState<ProofShot["proof_shoots"]>([]); + interface ProofShot { + total: number; + proof_shoots: { + id: string; + location_id: string; + category: string; + title: string; + description: string; + image: string; + created_at: string; + }[]; + } + + // 데이터를 가져오는 함수 + const getProofShots = async () => { + try { + const data: ProofShot = await fetcherWithAuth + .get(`api/v1/user/proof-shot`) + .json(); + setProofshots(data.proof_shoots); + setTotal(data.total); + } catch (error) { + console.error("Failed to fetch proof shots:", error); + } + }; + + useEffect(() => { + // 컴포넌트가 렌더링될 때 데이터 가져오기 + getProofShots(); + }, []); // 빈 배열은 처음 마운트 시에만 실행됨 + return ( - <div className="flex w-full flex-col gap-5 pl-1 pr-1"> + <div className="flex w-full flex-col gap-5 pr-1"> <Title title="My ProofShots" total={total} /> <div className="grid grid-cols-3 gap-1"> - {Array.from({ length: 10 }).map((_, index) => ( + {proofshots?.map((proofshot) => ( <div - key={index} + key={proofshot.id} // 각 항목에 고유한 키를 사용 className="flex aspect-square items-center justify-center bg-neutral-200 pl-1 pr-1" > - photo {index + 1} + <Image + src={proofshot.image} + alt={proofshot.title} + className="object-cover" + /> </div> ))} </div> @@ -67,22 +139,58 @@ const MyProofShot = () => { }; const MyStamps = () => { - const total = 10; // TODO: fetch from API + interface Stamp { + total: number; + stamps: { + id: string; + location_id: string; + category: "K_POP" | "DRAMA" | "MOVIE" | "NOVEL"; + title: string; + created_at: string; + }[]; + } + + const [total, setTotal] = useState(0); + const [stamps, setStamps] = useState<Stamp["stamps"]>([]); + + const getStamps = async () => { + try { + const data: Stamp = await fetcherWithAuth.get(`api/v1/user/stamp`).json(); + setStamps(data.stamps); + setTotal(data.total); + } catch (error) { + console.error("Failed to fetch stamps:", error); + } + }; + + useEffect(() => { + getStamps(); + }, []); + return ( <div> <Title title="My Stamps" total={total} /> - <div className="mx-auto grid aspect-square max-w-xl grid-cols-3 gap-x-3 gap-y-3"> - <Stamp title="Bongsuyuk" date="July 7, 2023" /> - <Stamp title="SKKU" date="July 7, 2023" /> - <Stamp title="Busan" date="July 7, 2023" /> - <Stamp title="SquidGame" date="July 7, 2023" /> - <Stamp title="Namsan" date="July 7, 2023" /> - <Stamp title="Theglory" date="July 7, 2023" /> - <Stamp title="Bongsuyuk" date="July 7, 2023" /> - <Stamp title="Bongsuyuk" date="July 7, 2023" /> - <Stamp title="Bongsuyuk" date="July 7, 2023" /> - <Stamp title="Bongsuyuk" date="July 7, 2023" /> - <Stamp title="Bongsuyuk" date="July 7, 2023" /> + <div className="mx-auto mt-6 grid aspect-square max-w-xl grid-cols-3 gap-x-3 gap-y-3"> + {stamps?.map((stamp) => ( + <Stamp + key={stamp.id} + title={stamp.title} + date={stamp.created_at} + category={stamp.category} + /> + ))} + + <Stamp title="Bongsuyuk" date="July 7, 2023" category="K_POP" /> + <Stamp title="SKKU" date="July 7, 2023" category="DRAMA" /> + <Stamp title="Busan" date="July 7, 2023" category="MOVIE" /> + <Stamp title="SquidGame" date="July 7, 2023" category="DRAMA" /> + <Stamp title="Namsan" date="July 7, 2023" category="MOVIE" /> + <Stamp title="Theglory" date="July 7, 2023" category="MOVIE" /> + <Stamp title="Bongsuyuk" date="July 7, 2023" category="DRAMA" /> + <Stamp title="Bongsuyuk" date="July 7, 2023" category="K_POP" /> + <Stamp title="Bongsuyuk" date="July 7, 2023" category="DRAMA" /> + <Stamp title="Bongsuyuk" date="July 7, 2023" category="K_POP" /> + <Stamp title="Bongsuyuk" date="July 7, 2023" category="NOVEL" /> </div> </div> ); @@ -104,9 +212,9 @@ const MyStats = () => { const Title = ({ title, total }: { title: string; total?: number }) => { return ( - <div className="mb-5 flex justify-between"> + <div className="flex justify-between"> <p className="text-xl font-bold">{title}</p> - {total && ( + {total !== undefined && ( <div className="flex gap-2 self-end"> <p className="font-semibold">TOTAL </p> <p className="font-semibold text-blue-500">{total}</p> @@ -115,3 +223,65 @@ const Title = ({ title, total }: { title: string; total?: number }) => { </div> ); }; + +const RedirectModal = () => { + return ( + <> + <div className="absolute inset-0 z-50 flex items-center justify-center bg-black/50"> + <div className="w-80 rounded-lg bg-white p-8 shadow-xl"> + <h2 className="mb-4 text-xl font-bold text-gray-800">My Page</h2> + <p className="mb-6 text-gray-600">Please sign in to continue.</p> + <div className="flex justify-around"> + <Link href="/login"> + <button className="rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"> + Sign In + </button> + </Link> + <Link href="/"> + <button className="rounded bg-gray-200 px-4 py-2 text-gray-800 hover:bg-gray-300"> + Back to Home + </button> + </Link> + </div> + </div> + </div> + <style jsx global>{` + body { + overflow: hidden; + } + `}</style> + </> + ); +}; + +const MyListModal = () => { + return ( + <div className="flex justify-end"> + <Dialog> + <DialogTrigger className="self-end rounded-md border-2 border-blue-200 bg-blue-50 px-3 py-1 text-sm text-black hover:bg-blue-400"> + View All + </DialogTrigger> + <DialogContent className="h-[80%] w-[90%] max-w-lg rounded-lg bg-white p-6 shadow-lg sm:w-[60%]"> + <DialogHeader className="mb-4"> + <DialogTitle className="text-lg font-semibold text-gray-700"> + Location Details + </DialogTitle> + </DialogHeader> + <ScrollArea className="h-full w-full overflow-y-auto"> + <div className="flex flex-col gap-4"> + {[...Array(10)].map((_, index) => ( + <LocationCard + key={index} + title={`Location ${index + 1}`} + photo={`photo ${index + 1}`} + description="This is a sample description for the location." + address="39-16, Ingye-ro 94beon-gil, Paldal-gu, Suwon-si, Gyeonggi-do" + /> + ))} + </div> + </ScrollArea> + </DialogContent> + </Dialog> + </div> + ); +}; diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index 8288dd0..ce51936 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -2,8 +2,28 @@ import Header from "./_components/Header"; import ContentsBox from "./_components/ContentsBox"; import CarouselMain from "./_components/Carousel"; import SearchBar from "./_components/SearchBar"; +import ContentCard from "./_components/ContentCard"; +import { fetcher } from "@/lib/utils"; -export default function Home() { +export interface ContentResponseDto { + id: number; + category: string; + title: string; + description: string; + content_image_url: string; + hashtag: string; +} + +// Map 구조를 위한 타입 정의 +export interface ContentMap { + K_POP: ContentResponseDto[]; + DRAMA: ContentResponseDto[]; + MOVIE: ContentResponseDto[]; + NOVEL: ContentResponseDto[]; +} + +export default async function Home() { + const data: ContentMap = await fetcher.get("api/v1/content/initial").json(); return ( <div className="flex flex-col gap-4"> <Header /> @@ -13,10 +33,58 @@ export default function Home() { </div> <main className="flex flex-col gap-16"> - <ContentsBox title="K-Pop" /> - <ContentsBox title="Drama" /> - <ContentsBox title="Movie" /> - <ContentsBox title="Novel" /> + <ContentsBox title="K-Pop"> + {data.K_POP?.map((content) => ( + <ContentCard + id={content.id} + key={content.id} + title={content.title} + description={content.description} + hashtags={content.hashtag} + link={`/content/${content.id}`} + image={content.content_image_url} + /> + ))} + </ContentsBox> + <ContentsBox title="Drama"> + {data.DRAMA?.map((content) => ( + <ContentCard + id={content.id} + key={content.id} + title={content.title} + description={content.description} + hashtags={content.hashtag} + link={`/content/${content.id}`} + image={content.content_image_url} + /> + ))} + </ContentsBox> + <ContentsBox title="Movie"> + {data.MOVIE?.map((content) => ( + <ContentCard + id={content.id} + key={content.id} + title={content.title} + description={content.description} + hashtags={content.hashtag} + link={`/content/${content.id}`} + image={content.content_image_url} + /> + ))} + </ContentsBox> + <ContentsBox title="Novel"> + {data.NOVEL?.map((content) => ( + <ContentCard + id={content.id} + key={content.id} + title={content.title} + description={content.description} + hashtags={content.hashtag} + link={`/content/${content.id}`} + image={content.content_image_url} + /> + ))} + </ContentsBox> </main> </div> ); diff --git a/frontend/app/search/page.tsx b/frontend/app/search/page.tsx new file mode 100644 index 0000000..3969006 --- /dev/null +++ b/frontend/app/search/page.tsx @@ -0,0 +1,83 @@ +"use client"; + +import { fetcher } from "@/lib/utils"; +import ContentCard from "../_components/ContentCard"; +import { useSearchParams } from "next/navigation"; +import { useEffect, useState } from "react"; +import Header from "../_components/Header"; +import { Skeleton } from "@/components/ui/skeleton"; +import SearchBar from "../_components/SearchBar"; + +interface ContentResponseDto { + id: number; + category: string; + title: string; + description: string; + content_image_url: string; + hashtag: string; +} + +const getData = async (content: string) => { + const data: ContentResponseDto[] = await fetcher + .get(`api/v1/content?keyword=${encodeURIComponent(content)}`) + .json(); + return data; +}; + +export default function SeeMore() { + // next/navigation에서 useSearchParams 훅 사용 + const searchParams = useSearchParams(); + const content = searchParams.get("keyword"); + + const [data, setData] = useState<ContentResponseDto[] | null>(null); + + useEffect(() => { + if (!content) { + return; + } + getData(content).then((data) => { + setData(data); + }); + }, [content]); + + if (!data) { + return ( + <div className="flex flex-col gap-4"> + <Skeleton className="h-10 w-1/2" /> + <Skeleton className="h-10 w-1/2" /> + <Skeleton className="h-48 w-full" /> + <Skeleton className="h-48 w-full" /> + <Skeleton className="h-48 w-full" /> + </div> + ); + } + + return ( + <div> + <Header /> + <div className="mt-10 flex flex-col"> + <div className="flex flex-col gap-10"> + <SearchBar /> + + <div className="flex flex-col gap-5"> + {content ? ( + data.map((content) => ( + <ContentCard + id={content.id} + key={content.id} + title={content.title} + description={content.description} + hashtags={content.hashtag} + link={`/content/${content.id}`} + image={content.content_image_url} + /> + )) + ) : ( + <div className="self-center">No content.</div> + )} + </div> + </div> + </div> + </div> + ); +} diff --git a/frontend/app/seemore/page.tsx b/frontend/app/seemore/page.tsx new file mode 100644 index 0000000..992c074 --- /dev/null +++ b/frontend/app/seemore/page.tsx @@ -0,0 +1,81 @@ +"use client"; + +import { fetcher } from "@/lib/utils"; +import ContentCard from "../_components/ContentCard"; +import { useSearchParams } from "next/navigation"; +import { useEffect, useState } from "react"; +import Header from "../_components/Header"; +import { Skeleton } from "@/components/ui/skeleton"; + +interface ContentResponseDto { + id: number; + category: string; + title: string; + description: string; + content_image_url: string; + hashtag: string; +} + +const getData = async (content: string) => { + const data: ContentResponseDto[] = await fetcher + .get(`api/v1/content?category=${encodeURIComponent(content)}`) + .json(); + return data; +}; + +export default function SeeMore() { + // next/navigation에서 useSearchParams 훅 사용 + const searchParams = useSearchParams(); + const content = searchParams.get("category"); + + const [data, setData] = useState<ContentResponseDto[] | null>(null); + + useEffect(() => { + if (!content) { + return; + } + getData(content).then((data) => { + setData(data); + }); + }, [content]); + + if (!content) { + return <div>No content specified</div>; + } + + if (!data) { + return ( + <div className="flex flex-col gap-4"> + <Skeleton className="h-10 w-1/2" /> + <Skeleton className="h-10 w-1/2" /> + <Skeleton className="h-48 w-full" /> + <Skeleton className="h-48 w-full" /> + <Skeleton className="h-48 w-full" /> + </div> + ); + } + + return ( + <div> + <Header /> + <div className="mt-10 flex flex-col"> + <p className="mb-5 ml-2 text-lg font-bold"> + All About The {content} ⭐️ + </p> + <div className="flex flex-col gap-5"> + {data.map((content) => ( + <ContentCard + id={content.id} + key={content.id} + title={content.title} + description={content.description} + hashtags={content.hashtag} + link={`/content/${content.id}`} + image={content.content_image_url} + /> + ))} + </div> + </div> + </div> + ); +} diff --git a/frontend/components/ui/card.tsx b/frontend/components/ui/card.tsx index 77e9fb7..1baceb9 100644 --- a/frontend/components/ui/card.tsx +++ b/frontend/components/ui/card.tsx @@ -1,6 +1,6 @@ -import * as React from "react" +import * as React from "react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const Card = React.forwardRef< HTMLDivElement, @@ -10,12 +10,12 @@ const Card = React.forwardRef< ref={ref} className={cn( "rounded-xl border bg-card text-card-foreground shadow", - className + className, )} {...props} /> -)) -Card.displayName = "Card" +)); +Card.displayName = "Card"; const CardHeader = React.forwardRef< HTMLDivElement, @@ -26,8 +26,8 @@ const CardHeader = React.forwardRef< className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} /> -)) -CardHeader.displayName = "CardHeader" +)); +CardHeader.displayName = "CardHeader"; const CardTitle = React.forwardRef< HTMLParagraphElement, @@ -38,8 +38,8 @@ const CardTitle = React.forwardRef< className={cn("font-semibold leading-none tracking-tight", className)} {...props} /> -)) -CardTitle.displayName = "CardTitle" +)); +CardTitle.displayName = "CardTitle"; const CardDescription = React.forwardRef< HTMLParagraphElement, @@ -50,16 +50,16 @@ const CardDescription = React.forwardRef< className={cn("text-sm text-muted-foreground", className)} {...props} /> -)) -CardDescription.displayName = "CardDescription" +)); +CardDescription.displayName = "CardDescription"; const CardContent = React.forwardRef< HTMLDivElement, React.HTMLAttributes<HTMLDivElement> >(({ className, ...props }, ref) => ( - <div ref={ref} className={cn("p-6 pt-0", className)} {...props} /> -)) -CardContent.displayName = "CardContent" + <div ref={ref} className={cn("p-6 pb-0 pt-0", className)} {...props} /> +)); +CardContent.displayName = "CardContent"; const CardFooter = React.forwardRef< HTMLDivElement, @@ -67,10 +67,17 @@ const CardFooter = React.forwardRef< >(({ className, ...props }, ref) => ( <div ref={ref} - className={cn("flex items-center p-6 pt-0", className)} + className={cn("flex items-center p-6 pb-0 pt-0", className)} {...props} /> -)) -CardFooter.displayName = "CardFooter" +)); +CardFooter.displayName = "CardFooter"; -export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardDescription, + CardContent, +}; diff --git a/frontend/components/ui/carousel.tsx b/frontend/components/ui/carousel.tsx index f9b6840..d31c47c 100644 --- a/frontend/components/ui/carousel.tsx +++ b/frontend/components/ui/carousel.tsx @@ -1,45 +1,45 @@ -"use client" +"use client"; -import * as React from "react" -import { ArrowLeftIcon, ArrowRightIcon } from "@radix-ui/react-icons" +import * as React from "react"; +import { ArrowLeftIcon, ArrowRightIcon } from "@radix-ui/react-icons"; import useEmblaCarousel, { type UseEmblaCarouselType, -} from "embla-carousel-react" +} from "embla-carousel-react"; -import { cn } from "@/lib/utils" -import { Button } from "@/components/ui/button" +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; -type CarouselApi = UseEmblaCarouselType[1] -type UseCarouselParameters = Parameters<typeof useEmblaCarousel> -type CarouselOptions = UseCarouselParameters[0] -type CarouselPlugin = UseCarouselParameters[1] +type CarouselApi = UseEmblaCarouselType[1]; +type UseCarouselParameters = Parameters<typeof useEmblaCarousel>; +type CarouselOptions = UseCarouselParameters[0]; +type CarouselPlugin = UseCarouselParameters[1]; type CarouselProps = { - opts?: CarouselOptions - plugins?: CarouselPlugin - orientation?: "horizontal" | "vertical" - setApi?: (api: CarouselApi) => void -} + opts?: CarouselOptions; + plugins?: CarouselPlugin; + orientation?: "horizontal" | "vertical"; + setApi?: (api: CarouselApi) => void; +}; type CarouselContextProps = { - carouselRef: ReturnType<typeof useEmblaCarousel>[0] - api: ReturnType<typeof useEmblaCarousel>[1] - scrollPrev: () => void - scrollNext: () => void - canScrollPrev: boolean - canScrollNext: boolean -} & CarouselProps + carouselRef: ReturnType<typeof useEmblaCarousel>[0]; + api: ReturnType<typeof useEmblaCarousel>[1]; + scrollPrev: () => void; + scrollNext: () => void; + canScrollPrev: boolean; + canScrollNext: boolean; +} & CarouselProps; -const CarouselContext = React.createContext<CarouselContextProps | null>(null) +const CarouselContext = React.createContext<CarouselContextProps | null>(null); function useCarousel() { - const context = React.useContext(CarouselContext) + const context = React.useContext(CarouselContext); if (!context) { - throw new Error("useCarousel must be used within a <Carousel />") + throw new Error("useCarousel must be used within a <Carousel />"); } - return context + return context; } const Carousel = React.forwardRef< @@ -56,69 +56,69 @@ const Carousel = React.forwardRef< children, ...props }, - ref + ref, ) => { const [carouselRef, api] = useEmblaCarousel( { ...opts, axis: orientation === "horizontal" ? "x" : "y", }, - plugins - ) - const [canScrollPrev, setCanScrollPrev] = React.useState(false) - const [canScrollNext, setCanScrollNext] = React.useState(false) + plugins, + ); + const [canScrollPrev, setCanScrollPrev] = React.useState(false); + const [canScrollNext, setCanScrollNext] = React.useState(false); const onSelect = React.useCallback((api: CarouselApi) => { if (!api) { - return + return; } - setCanScrollPrev(api.canScrollPrev()) - setCanScrollNext(api.canScrollNext()) - }, []) + setCanScrollPrev(api.canScrollPrev()); + setCanScrollNext(api.canScrollNext()); + }, []); const scrollPrev = React.useCallback(() => { - api?.scrollPrev() - }, [api]) + api?.scrollPrev(); + }, [api]); const scrollNext = React.useCallback(() => { - api?.scrollNext() - }, [api]) + api?.scrollNext(); + }, [api]); const handleKeyDown = React.useCallback( (event: React.KeyboardEvent<HTMLDivElement>) => { if (event.key === "ArrowLeft") { - event.preventDefault() - scrollPrev() + event.preventDefault(); + scrollPrev(); } else if (event.key === "ArrowRight") { - event.preventDefault() - scrollNext() + event.preventDefault(); + scrollNext(); } }, - [scrollPrev, scrollNext] - ) + [scrollPrev, scrollNext], + ); React.useEffect(() => { if (!api || !setApi) { - return + return; } - setApi(api) - }, [api, setApi]) + setApi(api); + }, [api, setApi]); React.useEffect(() => { if (!api) { - return + return; } - onSelect(api) - api.on("reInit", onSelect) - api.on("select", onSelect) + onSelect(api); + api.on("reInit", onSelect); + api.on("select", onSelect); return () => { - api?.off("select", onSelect) - } - }, [api, onSelect]) + api?.off("select", onSelect); + }; + }, [api, onSelect]); return ( <CarouselContext.Provider @@ -145,16 +145,16 @@ const Carousel = React.forwardRef< {children} </div> </CarouselContext.Provider> - ) - } -) -Carousel.displayName = "Carousel" + ); + }, +); +Carousel.displayName = "Carousel"; const CarouselContent = React.forwardRef< HTMLDivElement, React.HTMLAttributes<HTMLDivElement> >(({ className, ...props }, ref) => { - const { carouselRef, orientation } = useCarousel() + const { carouselRef, orientation } = useCarousel(); return ( <div ref={carouselRef} className="overflow-hidden"> @@ -163,20 +163,20 @@ const CarouselContent = React.forwardRef< className={cn( "flex", orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col", - className + className, )} {...props} /> </div> - ) -}) -CarouselContent.displayName = "CarouselContent" + ); +}); +CarouselContent.displayName = "CarouselContent"; const CarouselItem = React.forwardRef< HTMLDivElement, React.HTMLAttributes<HTMLDivElement> >(({ className, ...props }, ref) => { - const { orientation } = useCarousel() + const { orientation } = useCarousel(); return ( <div @@ -186,19 +186,19 @@ const CarouselItem = React.forwardRef< className={cn( "min-w-0 shrink-0 grow-0 basis-full", orientation === "horizontal" ? "pl-4" : "pt-4", - className + className, )} {...props} /> - ) -}) -CarouselItem.displayName = "CarouselItem" + ); +}); +CarouselItem.displayName = "CarouselItem"; const CarouselPrevious = React.forwardRef< HTMLButtonElement, React.ComponentProps<typeof Button> >(({ className, variant = "outline", size = "icon", ...props }, ref) => { - const { orientation, scrollPrev, canScrollPrev } = useCarousel() + const { orientation, scrollPrev, canScrollPrev } = useCarousel(); return ( <Button @@ -206,11 +206,11 @@ const CarouselPrevious = React.forwardRef< variant={variant} size={size} className={cn( - "absolute h-8 w-8 rounded-full", + "absolute h-8 w-8 rounded-full border-none bg-transparent", orientation === "horizontal" ? "-left-12 top-1/2 -translate-y-1/2" : "-top-12 left-1/2 -translate-x-1/2 rotate-90", - className + className, )} disabled={!canScrollPrev} onClick={scrollPrev} @@ -219,15 +219,15 @@ const CarouselPrevious = React.forwardRef< <ArrowLeftIcon className="h-4 w-4" /> <span className="sr-only">Previous slide</span> </Button> - ) -}) -CarouselPrevious.displayName = "CarouselPrevious" + ); +}); +CarouselPrevious.displayName = "CarouselPrevious"; const CarouselNext = React.forwardRef< HTMLButtonElement, React.ComponentProps<typeof Button> >(({ className, variant = "outline", size = "icon", ...props }, ref) => { - const { orientation, scrollNext, canScrollNext } = useCarousel() + const { orientation, scrollNext, canScrollNext } = useCarousel(); return ( <Button @@ -235,11 +235,11 @@ const CarouselNext = React.forwardRef< variant={variant} size={size} className={cn( - "absolute h-8 w-8 rounded-full", + "absolute h-8 w-8 rounded-full border-none bg-transparent", orientation === "horizontal" ? "-right-12 top-1/2 -translate-y-1/2" : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90", - className + className, )} disabled={!canScrollNext} onClick={scrollNext} @@ -248,9 +248,9 @@ const CarouselNext = React.forwardRef< <ArrowRightIcon className="h-4 w-4" /> <span className="sr-only">Next slide</span> </Button> - ) -}) -CarouselNext.displayName = "CarouselNext" + ); +}); +CarouselNext.displayName = "CarouselNext"; export { type CarouselApi, @@ -259,4 +259,4 @@ export { CarouselItem, CarouselPrevious, CarouselNext, -} +}; diff --git a/frontend/components/ui/dialog.tsx b/frontend/components/ui/dialog.tsx new file mode 100644 index 0000000..b5aaef7 --- /dev/null +++ b/frontend/components/ui/dialog.tsx @@ -0,0 +1,121 @@ +"use client" + +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { cn } from "@/lib/utils" +import { Cross2Icon } from "@radix-ui/react-icons" + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = DialogPrimitive.Portal + +const DialogClose = DialogPrimitive.Close + +const DialogOverlay = React.forwardRef< + React.ElementRef<typeof DialogPrimitive.Overlay>, + React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay> +>(({ className, ...props }, ref) => ( + <DialogPrimitive.Overlay + ref={ref} + className={cn( + "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", + className + )} + {...props} + /> +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef<typeof DialogPrimitive.Content>, + React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> +>(({ className, children, ...props }, ref) => ( + <DialogPortal> + <DialogOverlay /> + <DialogPrimitive.Content + ref={ref} + className={cn( + "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg", + className + )} + {...props} + > + {children} + <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"> + <Cross2Icon className="h-4 w-4" /> + <span className="sr-only">Close</span> + </DialogPrimitive.Close> + </DialogPrimitive.Content> + </DialogPortal> +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes<HTMLDivElement>) => ( + <div + className={cn( + "flex flex-col space-y-1.5 text-center sm:text-left", + className + )} + {...props} + /> +) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes<HTMLDivElement>) => ( + <div + className={cn( + "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", + className + )} + {...props} + /> +) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef<typeof DialogPrimitive.Title>, + React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title> +>(({ className, ...props }, ref) => ( + <DialogPrimitive.Title + ref={ref} + className={cn( + "text-lg font-semibold leading-none tracking-tight", + className + )} + {...props} + /> +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef<typeof DialogPrimitive.Description>, + React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description> +>(({ className, ...props }, ref) => ( + <DialogPrimitive.Description + ref={ref} + className={cn("text-sm text-muted-foreground", className)} + {...props} + /> +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogTrigger, + DialogClose, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/frontend/components/ui/drawer.tsx b/frontend/components/ui/drawer.tsx index 6a0ef53..f5510c1 100644 --- a/frontend/components/ui/drawer.tsx +++ b/frontend/components/ui/drawer.tsx @@ -1,9 +1,9 @@ -"use client" +"use client"; -import * as React from "react" -import { Drawer as DrawerPrimitive } from "vaul" +import * as React from "react"; +import { Drawer as DrawerPrimitive } from "vaul"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const Drawer = ({ shouldScaleBackground = true, @@ -12,15 +12,16 @@ const Drawer = ({ <DrawerPrimitive.Root shouldScaleBackground={shouldScaleBackground} {...props} + modal={false} /> -) -Drawer.displayName = "Drawer" +); +Drawer.displayName = "Drawer"; -const DrawerTrigger = DrawerPrimitive.Trigger +const DrawerTrigger = DrawerPrimitive.Trigger; -const DrawerPortal = DrawerPrimitive.Portal +const DrawerPortal = DrawerPrimitive.Portal; -const DrawerClose = DrawerPrimitive.Close +const DrawerClose = DrawerPrimitive.Close; const DrawerOverlay = React.forwardRef< React.ElementRef<typeof DrawerPrimitive.Overlay>, @@ -31,8 +32,8 @@ const DrawerOverlay = React.forwardRef< className={cn("fixed inset-0 z-50 bg-black/80", className)} {...props} /> -)) -DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName +)); +DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName; const DrawerContent = React.forwardRef< React.ElementRef<typeof DrawerPrimitive.Content>, @@ -44,7 +45,7 @@ const DrawerContent = React.forwardRef< ref={ref} className={cn( "fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background", - className + className, )} {...props} > @@ -52,8 +53,8 @@ const DrawerContent = React.forwardRef< {children} </DrawerPrimitive.Content> </DrawerPortal> -)) -DrawerContent.displayName = "DrawerContent" +)); +DrawerContent.displayName = "DrawerContent"; const DrawerHeader = ({ className, @@ -63,8 +64,8 @@ const DrawerHeader = ({ className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)} {...props} /> -) -DrawerHeader.displayName = "DrawerHeader" +); +DrawerHeader.displayName = "DrawerHeader"; const DrawerFooter = ({ className, @@ -74,8 +75,8 @@ const DrawerFooter = ({ className={cn("mt-auto flex flex-col gap-2 p-4", className)} {...props} /> -) -DrawerFooter.displayName = "DrawerFooter" +); +DrawerFooter.displayName = "DrawerFooter"; const DrawerTitle = React.forwardRef< React.ElementRef<typeof DrawerPrimitive.Title>, @@ -85,12 +86,12 @@ const DrawerTitle = React.forwardRef< ref={ref} className={cn( "text-lg font-semibold leading-none tracking-tight", - className + className, )} {...props} /> -)) -DrawerTitle.displayName = DrawerPrimitive.Title.displayName +)); +DrawerTitle.displayName = DrawerPrimitive.Title.displayName; const DrawerDescription = React.forwardRef< React.ElementRef<typeof DrawerPrimitive.Description>, @@ -101,8 +102,8 @@ const DrawerDescription = React.forwardRef< className={cn("text-sm text-muted-foreground", className)} {...props} /> -)) -DrawerDescription.displayName = DrawerPrimitive.Description.displayName +)); +DrawerDescription.displayName = DrawerPrimitive.Description.displayName; export { Drawer, @@ -115,4 +116,4 @@ export { DrawerFooter, DrawerTitle, DrawerDescription, -} +}; diff --git a/frontend/lib/auth.ts b/frontend/lib/auth.ts index a313af1..7fbe820 100644 --- a/frontend/lib/auth.ts +++ b/frontend/lib/auth.ts @@ -1,107 +1,9 @@ -import { ACCESS_TOKEN_EXPIRE_TIME } from "@/lib/constants"; -import { fetcher } from "@/lib/utils"; -import { - getServerSession, - type NextAuthOptions, - type Session, - type User, -} from "next-auth"; -import type { JWT } from "next-auth/jwt"; -import CredentialsProvider from "next-auth/providers/credentials"; -import { getSession } from "next-auth/react"; - -const getAuthTokenFromJson = async (res: Response) => { - const data = await res.json(); - return { - accessToken: data.accessToken, - refreshToken: data.refreshToken, - accessTokenExpires: Date.now() + ACCESS_TOKEN_EXPIRE_TIME - 30 * 1000, // 29 minutes 30 seconds - refreshTokenExpires: Date.now() + 1000 * 60 * 60 * 24 - 30 * 1000, // 23 hours 59 minutes 30 seconds - }; -}; - -export const authOptions: NextAuthOptions = { - providers: [ - CredentialsProvider({ - name: "Credentials", - credentials: { - username: { label: "Username", type: "text" }, - password: { label: "Password", type: "password" }, - }, - async authorize(credentials) { - // Log in API request - const res = await fetcher.post("/api/v1/auth/login", { - json: { - username: credentials?.username, - password: credentials?.password, - nickname: null, - }, - }); - - if (res.ok) { - // Extract tokens from JSON response - const { - accessToken, - refreshToken, - refreshTokenExpires, - accessTokenExpires, - } = await getAuthTokenFromJson(res); +import { getServerSession } from "next-auth"; - // Fetch user data - const userRes = await fetcher.get("/api/v1/auth/user", { - headers: { - Authorization: `Bearer ${accessToken}`, - }, - }); - - if (userRes.ok) { - const user: User = await userRes.json(); - return { - username: user.username, - accessToken, - refreshToken, - accessTokenExpires, - refreshTokenExpires, - } as User; - } - } - - // Return null if login fails - return null; - }, - }), - ], - session: { - strategy: "jwt", - maxAge: 24 * 60 * 60, // 24 hours - }, - callbacks: { - jwt: async ({ token, user }: { token: JWT; user?: User }) => { - if (user) { - token.username = user.username; - token.accessToken = user.accessToken; - token.refreshToken = user.refreshToken; - token.accessTokenExpires = user.accessTokenExpires; - token.refreshTokenExpires = user.refreshTokenExpires; - } - return token; - }, - session: async ({ session, token }: { session: Session; token: JWT }) => { - session.user = { - username: token.username, - }; - session.token = { - accessToken: token.accessToken, - refreshToken: token.refreshToken, - accessTokenExpires: token.accessTokenExpires, - refreshTokenExpires: token.refreshTokenExpires, - }; - return session; - }, - }, -}; +import { getSession } from "next-auth/react"; +import { options } from "./auth/options"; export const auth = async () => typeof window !== "undefined" ? await getSession() - : await getServerSession(authOptions); + : await getServerSession(options); diff --git a/frontend/lib/auth/options.ts b/frontend/lib/auth/options.ts index 275c072..ed9f852 100644 --- a/frontend/lib/auth/options.ts +++ b/frontend/lib/auth/options.ts @@ -12,7 +12,7 @@ export const options: NextAuthOptions = { password: { label: "Password", type: "password" }, }, async authorize(credentials) { - const res = await fetch("/api/v1/auth/login", { + const res = await fetch("http://hallyugo.com:8080/api/v1/auth/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(credentials), @@ -20,8 +20,12 @@ export const options: NextAuthOptions = { const user = await res.json(); if (res.ok && user) { - return user; + return { + ...user, + username: credentials?.username, + }; } + console.error("Failed to log in: ", user); return null; }, }), @@ -30,6 +34,7 @@ export const options: NextAuthOptions = { session: { strategy: "jwt", maxAge: 24 * 60 * 60, // 24 hours + updateAge: 24 * 60 * 60, // 24 hours }, callbacks: { @@ -37,14 +42,18 @@ export const options: NextAuthOptions = { if (user) { token.accessToken = user.accessToken; token.refreshToken = user.refreshToken; + token.accessTokenExpires = user.exp; + token.refreshTokenExpires = + Number(user.exp) + 7 * 24 * 60 * 60 - 30 * 60; + // 7 days } + return token; }, async session({ session, token }: { session: Session; token: JWT }) { session.user = { username: token.username, - role: token.role, }; session.token = { @@ -52,6 +61,7 @@ export const options: NextAuthOptions = { refreshToken: token.refreshToken, accessTokenExpires: token.accessTokenExpires, refreshTokenExpires: token.refreshTokenExpires, + exp: token.exp, }; return session; diff --git a/frontend/lib/constants.ts b/frontend/lib/constants.ts index 86511e4..48470f2 100644 --- a/frontend/lib/constants.ts +++ b/frontend/lib/constants.ts @@ -1,4 +1,4 @@ -export const baseUrl = "http://localhost:3000"; //api base url +export const baseUrl = "http://hallyugo.com:8080"; //api base url const MILLSECONDS_PER_MINUTE = 60000; /** diff --git a/frontend/middleware.ts b/frontend/middleware.ts index 7d80d7f..41d67c5 100644 --- a/frontend/middleware.ts +++ b/frontend/middleware.ts @@ -18,11 +18,7 @@ const sessionCookieName = process.env.NEXTAUTH_URL?.startsWith("https://") export const middleware = async (req: NextRequest) => { const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET }); - - // Redirect unauthenticated users trying to access restricted pages - if (req.nextUrl.pathname.startsWith("/my") && !token) { - return NextResponse.redirect(new URL("/my", req.url)); - } + //console.log("token", token); // Reissue access token if expired if (token && token.accessTokenExpires <= Date.now()) { diff --git a/frontend/next-auth.d.ts b/frontend/next-auth.d.ts index d1e8af8..efe5ba0 100644 --- a/frontend/next-auth.d.ts +++ b/frontend/next-auth.d.ts @@ -8,6 +8,7 @@ interface Token { refreshToken: string; accessTokenExpires: number; refreshTokenExpires: number; + exp: number; } declare module "next-auth" { diff --git a/frontend/next.config.mjs b/frontend/next.config.mjs index 8065657..7128be0 100644 --- a/frontend/next.config.mjs +++ b/frontend/next.config.mjs @@ -1,5 +1,3 @@ -// next.config.mjs - import withPWA from "next-pwa"; /** @type {import('next').NextConfig} */ @@ -9,11 +7,14 @@ const nextConfig = { compiler: { removeConsole: process.env.NODE_ENV !== "development", // Remove console.log in production }, + images: { + domains: ["hallyugo-s3.s3.ap-northeast-2.amazonaws.com"], // Allowed domains for images + }, }; export default withPWA({ - dest: "public", // destination directory for the PWA files - disable: process.env.NODE_ENV === "development", // disable PWA in the development environment - register: true, // register the PWA service worker - skipWaiting: true, // skip waiting for service worker activation + dest: "public", // Destination directory for the PWA files + disable: process.env.NODE_ENV === "development", // Disable PWA in the development environment + register: true, // Register the PWA service worker + skipWaiting: true, // Skip waiting for service worker activation })(nextConfig); diff --git a/frontend/package.json b/frontend/package.json index c46c307..4ab5d72 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -30,11 +30,13 @@ "react-hook-form": "^7.53.2", "react-icons": "^5.3.0", "react-modal-sheet": "^3.3.0", + "react-naver-maps": "^0.1.3", "react-resizable-panels": "^2.1.6", "react-spring-bottom-sheet": "^3.4.1", "tailwind-merge": "^2.5.4", "tailwindcss-animate": "^1.0.7", - "vaul": "^1.1.1" + "vaul": "^1.1.1", + "zustand": "^5.0.1" }, "devDependencies": { "@types/next-pwa": "^5", diff --git a/frontend/public/carousel1.png b/frontend/public/carousel1.png new file mode 100644 index 0000000..bcb349c Binary files /dev/null and b/frontend/public/carousel1.png differ diff --git a/frontend/public/icons/carousel-btn-1.png b/frontend/public/icons/carousel-btn-1.png new file mode 100644 index 0000000..8ffac63 Binary files /dev/null and b/frontend/public/icons/carousel-btn-1.png differ diff --git a/frontend/public/icons/carousel-logo.png b/frontend/public/icons/carousel-logo.png new file mode 100644 index 0000000..499cfaa Binary files /dev/null and b/frontend/public/icons/carousel-logo.png differ diff --git a/frontend/public/icons/proof_DRAMA.png b/frontend/public/icons/proof_DRAMA.png new file mode 100644 index 0000000..a81665b Binary files /dev/null and b/frontend/public/icons/proof_DRAMA.png differ diff --git a/frontend/public/icons/proof_K_POP.png b/frontend/public/icons/proof_K_POP.png new file mode 100644 index 0000000..2215aeb Binary files /dev/null and b/frontend/public/icons/proof_K_POP.png differ diff --git a/frontend/public/icons/proof_MOVIE.png b/frontend/public/icons/proof_MOVIE.png new file mode 100644 index 0000000..8e3d9a2 Binary files /dev/null and b/frontend/public/icons/proof_MOVIE.png differ diff --git a/frontend/public/icons/proof_NOVEL.png b/frontend/public/icons/proof_NOVEL.png new file mode 100644 index 0000000..e57b383 Binary files /dev/null and b/frontend/public/icons/proof_NOVEL.png differ diff --git a/frontend/public/itzy.png b/frontend/public/itzy.png new file mode 100644 index 0000000..73ee5fd Binary files /dev/null and b/frontend/public/itzy.png differ diff --git a/frontend/yarn.lock b/frontend/yarn.lock index d7dac10..8a4c114 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -910,6 +910,13 @@ "@babel/types" "^7.4.4" esutils "^2.0.2" +"@babel/runtime@^7.1.2", "@babel/runtime@^7.20.13": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.0.tgz#8600c2f595f277c60815256418b85356a65173c1" + integrity sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/runtime@^7.11.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.8.4": version "7.25.7" resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.7.tgz" @@ -917,13 +924,6 @@ dependencies: regenerator-runtime "^0.14.0" -"@babel/runtime@^7.20.13": - version "7.26.0" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.0.tgz#8600c2f595f277c60815256418b85356a65173c1" - integrity sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw== - dependencies: - regenerator-runtime "^0.14.0" - "@babel/template@^7.25.7": version "7.25.7" resolved "https://registry.npmjs.org/@babel/template/-/template-7.25.7.tgz" @@ -1165,7 +1165,7 @@ "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.25" -"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.4.15": version "1.5.0" resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz" integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== @@ -1348,7 +1348,7 @@ "@radix-ui/react-dialog@^1.1.1", "@radix-ui/react-dialog@^1.1.2": version "1.1.2" - resolved "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.2.tgz" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.1.2.tgz#d9345575211d6f2d13e209e84aec9a8584b54d6c" integrity sha512-Yj4dZtqa2o+kG61fzB0H2qUvmwBA2oyQroGLyNtBj1beo1khoQ3q1a2AO8rrQYjd8256CO9+N8L9tvsS+bnIyA== dependencies: "@radix-ui/primitive" "1.1.0" @@ -1625,6 +1625,11 @@ "@types/minimatch" "*" "@types/node" "*" +"@types/js-cookie@^2.2.6": + version "2.2.7" + resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-2.2.7.tgz#226a9e31680835a6188e887f3988e60c04d3f6a3" + integrity sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA== + "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8": version "7.0.15" resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz" @@ -1823,6 +1828,11 @@ resolved "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== +"@xobotyi/scrollbar-width@^1.9.5": + version "1.9.5" + resolved "https://registry.yarnpkg.com/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz#80224a6919272f405b87913ca13b92929bdf3c4d" + integrity sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ== + "@xstate/react@^1.2.0": version "1.6.3" resolved "https://registry.npmjs.org/@xstate/react/-/react-1.6.3.tgz" @@ -2194,6 +2204,11 @@ camelcase-css@^2.0.1: resolved "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz" integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== +camelcase@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + caniuse-lite@^1.0.30001406, caniuse-lite@^1.0.30001579, caniuse-lite@^1.0.30001663: version "1.0.30001667" resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001667.tgz" @@ -2342,6 +2357,13 @@ cookie@^0.7.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== +copy-to-clipboard@^3.3.1: + version "3.3.3" + resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz#55ac43a1db8ae639a4bd99511c148cdd1b83a1b0" + integrity sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA== + dependencies: + toggle-selection "^1.0.6" + core-js-compat@^3.38.0, core-js-compat@^3.38.1: version "3.38.1" resolved "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.38.1.tgz" @@ -2363,12 +2385,27 @@ crypto-random-string@^2.0.0: resolved "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz" integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== +css-in-js-utils@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz#640ae6a33646d401fc720c54fc61c42cd76ae2bb" + integrity sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A== + dependencies: + hyphenate-style-name "^1.0.3" + +css-tree@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d" + integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q== + dependencies: + mdn-data "2.0.14" + source-map "^0.6.1" + cssesc@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz" integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== -csstype@^3.0.2: +csstype@^3.0.2, csstype@^3.1.2: version "3.1.3" resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz" integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== @@ -2583,6 +2620,13 @@ enhanced-resolve@^5.15.0: graceful-fs "^4.2.4" tapable "^2.2.0" +error-stack-parser@^2.0.6: + version "2.1.4" + resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-2.1.4.tgz#229cb01cdbfa84440bfa91876285b94680188286" + integrity sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ== + dependencies: + stackframe "^1.3.4" + es-abstract@^1.17.5, es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.1, es-abstract@^1.23.2, es-abstract@^1.23.3: version "1.23.3" resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz" @@ -2972,11 +3016,21 @@ fast-levenshtein@^2.0.6: resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== +fast-shallow-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz#d4dcaf6472440dcefa6f88b98e3251e27f25628b" + integrity sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw== + fast-uri@^3.0.1: version "3.0.2" resolved "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.2.tgz" integrity sha512-GR6f0hD7XXyNJa25Tb9BuIdN0tdr+0BMi6/CJPH3wJO1JjNG3n/VsSw38AwRdKZABm8lGbPfakLRkYzx2V9row== +fastest-stable-stringify@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/fastest-stable-stringify/-/fastest-stable-stringify-2.0.2.tgz#3757a6774f6ec8de40c4e86ec28ea02417214c76" + integrity sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q== + fastq@^1.6.0: version "1.17.1" resolved "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz" @@ -3296,6 +3350,11 @@ hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2: dependencies: function-bind "^1.1.2" +hyphenate-style-name@^1.0.3: + version "1.1.0" + resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz#1797bf50369588b47b72ca6d5e65374607cf4436" + integrity sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw== + idb@^7.0.1: version "7.1.1" resolved "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz" @@ -3332,6 +3391,13 @@ inherits@2: resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +inline-style-prefixer@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/inline-style-prefixer/-/inline-style-prefixer-7.0.1.tgz#9310f3cfa2c6f3901d1480f373981c02691781e8" + integrity sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw== + dependencies: + css-in-js-utils "^3.1.0" + internal-slot@^1.0.4, internal-slot@^1.0.7: version "1.0.7" resolved "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz" @@ -3657,6 +3723,11 @@ jose@^4.15.5, jose@^4.15.9: resolved "https://registry.yarnpkg.com/jose/-/jose-4.15.9.tgz#9b68eda29e9a0614c042fa29387196c7dd800100" integrity sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA== +js-cookie@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8" + integrity sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ== + "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" @@ -3787,6 +3858,11 @@ lines-and-columns@^1.1.6: resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== +load-script@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/load-script/-/load-script-2.0.0.tgz#40821aaa59e9bbe7be2e28b6ab053e6f44330fa1" + integrity sha512-km6cyoPW4rM22JMGb+SHUKPMZVDpUaMpMAKrv8UHWllIxc/qjgMGHD91nY+5hM+/NFs310OZ2pqQeJKs7HqWPA== + loader-utils@^2.0.4: version "2.0.4" resolved "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz" @@ -3815,16 +3891,41 @@ lodash.debounce@^4.0.8: resolved "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz" integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== +lodash.isempty@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.isempty/-/lodash.isempty-4.4.0.tgz#6f86cbedd8be4ec987be9aaf33c9684db1b31e7e" + integrity sha512-oKMuF3xEeqDltrGMfDxAPGIVMSSRv8tbRSODbrs4KGsRRLEhrW8N8Rd4DRgB2+621hY8A8XwwrTVhXWpxFvMzg== + +lodash.mapkeys@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.mapkeys/-/lodash.mapkeys-4.6.0.tgz#df2cfa231d7c57c7a8ad003abdad5d73d3ea5195" + integrity sha512-0Al+hxpYvONWtg+ZqHpa/GaVzxuN3V7Xeo2p+bY06EaK/n+Y9R7nBePPN2o1LxmL0TWQSwP8LYZ008/hc9JzhA== + lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== +lodash.omit@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.omit/-/lodash.omit-4.5.0.tgz#6eb19ae5a1ee1dd9df0b969e66ce0b7fa30b5e60" + integrity sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg== + +lodash.pick@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3" + integrity sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q== + lodash.sortby@^4.7.0: version "4.7.0" resolved "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz" integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA== +lodash.upperfirst@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz#1365edf431480481ef0d1c68957a5ed99d49f7ce" + integrity sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg== + lodash@^4.17.20: version "4.17.21" resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" @@ -3875,6 +3976,11 @@ make-dir@^3.0.2, make-dir@^3.1.0: dependencies: semver "^6.0.0" +mdn-data@2.0.14: + version "2.0.14" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" + integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz" @@ -3938,6 +4044,20 @@ mz@^2.7.0: object-assign "^4.0.1" thenify-all "^1.0.0" +nano-css@^5.6.2: + version "5.6.2" + resolved "https://registry.yarnpkg.com/nano-css/-/nano-css-5.6.2.tgz#584884ddd7547278f6d6915b6805069742679a32" + integrity sha512-+6bHaC8dSDGALM1HJjOHVXpuastdu2xFoZlC77Jh4cg+33Zcgm+Gxd+1xsnpZK14eyHObSp82+ll5y3SX75liw== + dependencies: + "@jridgewell/sourcemap-codec" "^1.4.15" + css-tree "^1.1.2" + csstype "^3.1.2" + fastest-stable-stringify "^2.0.2" + inline-style-prefixer "^7.0.1" + rtl-css-js "^1.16.1" + stacktrace-js "^2.0.2" + stylis "^4.3.0" + nanoid@^3.3.6, nanoid@^3.3.7: version "3.3.7" resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz" @@ -4381,7 +4501,7 @@ pretty-format@^3.8.0: resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-3.8.0.tgz#bfbed56d5e9a776645f4b1ff7aa1a3ac4fa3c385" integrity sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew== -prop-types@^15.5.8, prop-types@^15.8.1: +prop-types@^15.5.8, prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -4442,6 +4562,22 @@ react-modal-sheet@^3.3.0: dependencies: "@react-aria/utils" "3.25.3" +react-naver-maps@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/react-naver-maps/-/react-naver-maps-0.1.3.tgz#242da4556799e7a854cc0fe5f061c188919f32c7" + integrity sha512-J2AD+MMn33NQ0RH3ldn4kG0ehV9Ao+16/w2kw4XilWl/xWI8mu2DnHUJNaxWBu7S0JSEIsFJuUp7JfZ48wH8kw== + dependencies: + camelcase "^5.3.1" + load-script "^2.0.0" + lodash.isempty "^4.4.0" + lodash.mapkeys "^4.6.0" + lodash.omit "^4.5.0" + lodash.pick "^4.4.0" + lodash.upperfirst "^4.3.1" + prop-types "^15.7.2" + react-use "^17.3.1" + suspend-react "^0.0.8" + react-remove-scroll-bar@^2.3.6: version "2.3.6" resolved "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz" @@ -4468,7 +4604,7 @@ react-resizable-panels@^2.1.6: react-spring-bottom-sheet@^3.4.1: version "3.4.1" - resolved "https://registry.npmjs.org/react-spring-bottom-sheet/-/react-spring-bottom-sheet-3.4.1.tgz" + resolved "https://registry.yarnpkg.com/react-spring-bottom-sheet/-/react-spring-bottom-sheet-3.4.1.tgz#9a4f90b1c0af17eb4a22a606a5efc5d6e62c7b0c" integrity sha512-yDFqiPMm/fjefjnOe6Q9zxccbCl6HMUKsK5bWgfGHJIj4zmXVKio5d4icQvmOLuwpuCA2pwv4J6nGWS6fUZidQ== dependencies: "@juggle/resize-observer" "^3.2.0" @@ -4497,11 +4633,36 @@ react-style-singleton@^2.2.1: invariant "^2.2.4" tslib "^2.0.0" +react-universal-interface@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/react-universal-interface/-/react-universal-interface-0.6.2.tgz#5e8d438a01729a4dbbcbeeceb0b86be146fe2b3b" + integrity sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw== + react-use-gesture@^8.0.1: version "8.0.1" resolved "https://registry.npmjs.org/react-use-gesture/-/react-use-gesture-8.0.1.tgz" integrity sha512-CXzUNkulUdgouaAlvAsC5ZVo0fi9KGSBSk81WrE4kOIcJccpANe9zZkAYr5YZZhqpicIFxitsrGVS4wmoMun9A== +react-use@^17.3.1: + version "17.5.1" + resolved "https://registry.yarnpkg.com/react-use/-/react-use-17.5.1.tgz#19fc2ae079775d8450339e9fa8dbe25b17f2263c" + integrity sha512-LG/uPEVRflLWMwi3j/sZqR00nF6JGqTTDblkXK2nzXsIvij06hXl1V/MZIlwj1OKIQUtlh1l9jK8gLsRyCQxMg== + dependencies: + "@types/js-cookie" "^2.2.6" + "@xobotyi/scrollbar-width" "^1.9.5" + copy-to-clipboard "^3.3.1" + fast-deep-equal "^3.1.3" + fast-shallow-equal "^1.0.0" + js-cookie "^2.2.1" + nano-css "^5.6.2" + react-universal-interface "^0.6.2" + resize-observer-polyfill "^1.5.1" + screenfull "^5.1.0" + set-harmonic-interval "^1.0.1" + throttle-debounce "^3.0.1" + ts-easing "^0.2.0" + tslib "^2.1.0" + react@^18: version "18.3.1" resolved "https://registry.npmjs.org/react/-/react-18.3.1.tgz" @@ -4599,6 +4760,11 @@ require-from-string@^2.0.2: resolved "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz" integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== +resize-observer-polyfill@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" + integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg== + resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz" @@ -4663,6 +4829,13 @@ rollup@^2.43.1: optionalDependencies: fsevents "~2.3.2" +rtl-css-js@^1.16.1: + version "1.16.1" + resolved "https://registry.yarnpkg.com/rtl-css-js/-/rtl-css-js-1.16.1.tgz#4b48b4354b0ff917a30488d95100fbf7219a3e80" + integrity sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg== + dependencies: + "@babel/runtime" "^7.1.2" + run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz" @@ -4719,6 +4892,11 @@ schema-utils@^3.1.1: ajv "^6.12.5" ajv-keywords "^3.5.2" +screenfull@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/screenfull/-/screenfull-5.2.0.tgz#6533d524d30621fc1283b9692146f3f13a93d1ba" + integrity sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA== + semver@^6.0.0, semver@^6.3.1: version "6.3.1" resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" @@ -4765,6 +4943,11 @@ set-function-name@^2.0.1, set-function-name@^2.0.2: functions-have-names "^1.2.3" has-property-descriptors "^1.0.2" +set-harmonic-interval@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/set-harmonic-interval/-/set-harmonic-interval-1.0.1.tgz#e1773705539cdfb80ce1c3d99e7f298bb3995249" + integrity sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g== + sharp@^0.33.5: version "0.33.5" resolved "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz" @@ -4851,7 +5034,12 @@ source-map-support@~0.5.20: buffer-from "^1.0.0" source-map "^0.6.0" -source-map@^0.6.0, source-map@~0.6.1: +source-map@0.5.6: + version "0.5.6" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" + integrity sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA== + +source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: version "0.6.1" resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== @@ -4868,6 +5056,35 @@ sourcemap-codec@^1.4.8: resolved "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz" integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== +stack-generator@^2.0.5: + version "2.0.10" + resolved "https://registry.yarnpkg.com/stack-generator/-/stack-generator-2.0.10.tgz#8ae171e985ed62287d4f1ed55a1633b3fb53bb4d" + integrity sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ== + dependencies: + stackframe "^1.3.4" + +stackframe@^1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.3.4.tgz#b881a004c8c149a5e8efef37d51b16e412943310" + integrity sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw== + +stacktrace-gps@^3.0.4: + version "3.1.2" + resolved "https://registry.yarnpkg.com/stacktrace-gps/-/stacktrace-gps-3.1.2.tgz#0c40b24a9b119b20da4525c398795338966a2fb0" + integrity sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ== + dependencies: + source-map "0.5.6" + stackframe "^1.3.4" + +stacktrace-js@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/stacktrace-js/-/stacktrace-js-2.0.2.tgz#4ca93ea9f494752d55709a081d400fdaebee897b" + integrity sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg== + dependencies: + error-stack-parser "^2.0.6" + stack-generator "^2.0.5" + stacktrace-gps "^3.0.4" + stop-iteration-iterator@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz" @@ -5028,6 +5245,11 @@ styled-jsx@5.1.6: dependencies: client-only "0.0.1" +stylis@^4.3.0: + version "4.3.4" + resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.3.4.tgz#ca5c6c4a35c4784e4e93a2a24dc4e9fa075250a4" + integrity sha512-osIBl6BGUmSfDkyH2mB7EFvCJntXDrLhKjHTRj/rK6xLH0yuPrHULDRQzKokSOD4VoorhtKpfcfW1GAntu8now== + sucrase@^3.32.0: version "3.35.0" resolved "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz" @@ -5067,6 +5289,11 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +suspend-react@^0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/suspend-react/-/suspend-react-0.0.8.tgz#b0740c1386b4eb652f17affe4339915ee268bd31" + integrity sha512-ZC3r8Hu1y0dIThzsGw0RLZplnX9yXwfItcvaIzJc2VQVi8TGyGDlu92syMB5ulybfvGLHAI5Ghzlk23UBPF8xg== + tabbable@^5.3.3: version "5.3.3" resolved "https://registry.npmjs.org/tabbable/-/tabbable-5.3.3.tgz" @@ -5170,6 +5397,11 @@ thenify-all@^1.0.0: dependencies: any-promise "^1.0.0" +throttle-debounce@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-3.0.1.tgz#32f94d84dfa894f786c9a1f290e7a645b6a19abb" + integrity sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg== + to-fast-properties@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz" @@ -5182,6 +5414,11 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" +toggle-selection@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32" + integrity sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ== + tr46@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz" @@ -5194,6 +5431,11 @@ ts-api-utils@^1.3.0: resolved "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz" integrity sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ== +ts-easing@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/ts-easing/-/ts-easing-0.2.0.tgz#c8a8a35025105566588d87dbda05dd7fbfa5a4ec" + integrity sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ== + ts-interface-checker@^0.1.9: version "0.1.13" resolved "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz" @@ -5714,3 +5956,8 @@ yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +zustand@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/zustand/-/zustand-5.0.1.tgz#2bdca5e4be172539558ce3974fe783174a48fdcf" + integrity sha512-pRET7Lao2z+n5R/HduXMio35TncTlSW68WsYBq2Lg1ASspsNGjpwLAsij3RpouyV6+kHMwwwzP0bZPD70/Jx/w==