diff --git a/backend/database/cabi_local.sql b/backend/database/cabi_local.sql index 2268f4a07..fe882bb56 100644 --- a/backend/database/cabi_local.sql +++ b/backend/database/cabi_local.sql @@ -705,6 +705,81 @@ CREATE TABLE `club_registration` COLLATE = utf8mb4_general_ci; +DROP TABLE IF EXISTS `item`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `item` +( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `price` bigint(20) NOT NULL, + `sku` varchar(64) NOT NULL, + `type` varchar(64) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci; + +LOCK TABLES `item` WRITE; +/*!40000 ALTER TABLE `item` + DISABLE KEYS */; +INSERT INTO `item` +VALUES (1, 0, 'EXTENSION_PREV', 'EXTENSION'), + (2, -2000, 'EXTENSION_31', 'EXTENSION'), + (3, -1200, 'EXTENSION_15', 'EXTENSION'), + (4, -300, 'EXTENSION_3', 'EXTENSION'), + (5, -6200, 'PENALTY_31', 'PENALTY'), + (6, -1400, 'PENALTY_7', 'PENALTY'), + (7, -600, 'PENALTY_3', 'PENALTY'), + (8, -100, 'SWAP', 'SWAP'), + (9, -100, 'ALARM', 'ALARM'), + (10, 10, 'COIN_COLLECT', 'COIN_COLLECT'), + (11, 2000, 'COIN_FULL_TIME', 'COIN_FULL_TIME'), + (12, 200, 'COIN_REWARD_200', 'COIN_REWARD'), + (13, 500, 'COIN_REWARD_500', 'COIN_REWARD'), + (14, 1000, 'COIN_REWARD_1000', 'COIN_REWARD'), + (15, 2000, 'COIN_REWARD_2000', 'COIN_REWARD'); +/*!40000 ALTER TABLE `item` + ENABLE KEYS */; +UNLOCK TABLES; + + +DROP TABLE IF EXISTS `item_history`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `item_history` +( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `item_id` bigint(20) NOT NULL, + `user_id` bigint(20) NOT NULL, + `purchase_at` datetime(6) NOT NULL, + `used_at` datetime(6) DEFAULT NULL, + PRIMARY KEY (`id`), + CONSTRAINT `item_history_item_id` FOREIGN KEY (`item_id`) REFERENCES `item` (`id`), + CONSTRAINT `item_history_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci; + + +DROP TABLE IF EXISTS `section_alarm`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `section_alarm` +( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `cabinet_place_id` bigint(20) NOT NULL, + `user_id` bigint(20) NOT NULL, + `registered_at` datetime(6) NOT NULL, + `alarmed_at` datetime(6) DEFAULT NULL, + PRIMARY KEY (`id`), + CONSTRAINT `section_alarm_cabinet_place_id` + FOREIGN KEY (`cabinet_place_id`) REFERENCES `cabinet_place` (`id`), + CONSTRAINT `section_alarm_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci; + + /*!40103 SET TIME_ZONE = @OLD_TIME_ZONE */; /*!40101 SET SQL_MODE = @OLD_SQL_MODE */; diff --git a/backend/src/main/java/org/ftclub/cabinet/admin/club/service/AdminClubFacadeService.java b/backend/src/main/java/org/ftclub/cabinet/admin/club/service/AdminClubFacadeService.java index cf2bced7d..ef8b1ffc4 100644 --- a/backend/src/main/java/org/ftclub/cabinet/admin/club/service/AdminClubFacadeService.java +++ b/backend/src/main/java/org/ftclub/cabinet/admin/club/service/AdminClubFacadeService.java @@ -1,6 +1,6 @@ package org.ftclub.cabinet.admin.club.service; -import static org.ftclub.cabinet.user.domain.UserRole.CLUB_ADMIN; +import static org.ftclub.cabinet.club.domain.UserRole.CLUB_ADMIN; import java.util.List; import java.util.Map; @@ -9,6 +9,7 @@ import lombok.RequiredArgsConstructor; import org.ftclub.cabinet.club.domain.Club; import org.ftclub.cabinet.club.domain.ClubRegistration; +import org.ftclub.cabinet.club.domain.UserRole; import org.ftclub.cabinet.club.service.ClubCommandService; import org.ftclub.cabinet.club.service.ClubQueryService; import org.ftclub.cabinet.club.service.ClubRegistrationCommandService; @@ -19,7 +20,6 @@ import org.ftclub.cabinet.log.Logging; import org.ftclub.cabinet.mapper.ClubMapper; import org.ftclub.cabinet.user.domain.User; -import org.ftclub.cabinet.user.domain.UserRole; import org.ftclub.cabinet.user.service.UserQueryService; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; diff --git a/backend/src/main/java/org/ftclub/cabinet/admin/dto/AdminItemHistoryDto.java b/backend/src/main/java/org/ftclub/cabinet/admin/dto/AdminItemHistoryDto.java new file mode 100644 index 000000000..99bab75df --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/admin/dto/AdminItemHistoryDto.java @@ -0,0 +1,15 @@ +package org.ftclub.cabinet.admin.dto; + +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class AdminItemHistoryDto { + + private LocalDateTime purchaseAt; + private LocalDateTime usedAt; + private String itemName; + private String itemDetails; +} diff --git a/backend/src/main/java/org/ftclub/cabinet/admin/dto/AdminItemHistoryPaginationDto.java b/backend/src/main/java/org/ftclub/cabinet/admin/dto/AdminItemHistoryPaginationDto.java new file mode 100644 index 000000000..e5710392e --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/admin/dto/AdminItemHistoryPaginationDto.java @@ -0,0 +1,13 @@ +package org.ftclub.cabinet.admin.dto; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class AdminItemHistoryPaginationDto { + + private List itemHistories; + private Long totalLength; +} diff --git a/backend/src/main/java/org/ftclub/cabinet/admin/item/controller/AdminItemController.java b/backend/src/main/java/org/ftclub/cabinet/admin/item/controller/AdminItemController.java new file mode 100644 index 000000000..f3932160e --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/admin/item/controller/AdminItemController.java @@ -0,0 +1,57 @@ +package org.ftclub.cabinet.admin.item.controller; + +import lombok.RequiredArgsConstructor; +import org.ftclub.cabinet.admin.dto.AdminItemHistoryPaginationDto; +import org.ftclub.cabinet.admin.item.service.AdminItemFacadeService; +import org.ftclub.cabinet.admin.statistics.service.AdminStatisticsFacadeService; +import org.ftclub.cabinet.auth.domain.AuthGuard; +import org.ftclub.cabinet.auth.domain.AuthLevel; +import org.ftclub.cabinet.dto.ItemAssignRequestDto; +import org.ftclub.cabinet.dto.ItemCreateDto; +import org.ftclub.cabinet.log.Logging; +import org.springframework.data.domain.Pageable; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RequestMapping("/v5/admin/items") +@RequiredArgsConstructor +@RestController +@Logging +public class AdminItemController { + + private final AdminItemFacadeService adminItemFacadeService; + private final AdminStatisticsFacadeService adminStatisticsFacadeService; + + + @PostMapping("") + @AuthGuard(level = AuthLevel.ADMIN_ONLY) + public void createItem(@RequestBody ItemCreateDto itemCreateDto) { + adminItemFacadeService.createItem(itemCreateDto.getPrice(), + itemCreateDto.getSku(), itemCreateDto.getType()); + } + + @PostMapping("/assign") + @AuthGuard(level = AuthLevel.ADMIN_ONLY) + public void assignItem(@RequestBody ItemAssignRequestDto itemAssignRequestDto) { + adminItemFacadeService.assignItem(itemAssignRequestDto.getUserIds(), + itemAssignRequestDto.getItemSku()); + } + + /** + * 특정 유저의 아이템 history 조회 + * + * @param userId + * @param pageable + * @return + */ + @GetMapping("/users/{userId}") + @AuthGuard(level = AuthLevel.ADMIN_ONLY) + public AdminItemHistoryPaginationDto getUserItemHistoryPagination( + @PathVariable(value = "userId") Long userId, Pageable pageable) { + return adminItemFacadeService.getUserItemHistories(userId, pageable); + } +} diff --git a/backend/src/main/java/org/ftclub/cabinet/admin/item/service/AdminItemFacadeService.java b/backend/src/main/java/org/ftclub/cabinet/admin/item/service/AdminItemFacadeService.java new file mode 100644 index 000000000..2e98fd711 --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/admin/item/service/AdminItemFacadeService.java @@ -0,0 +1,75 @@ +package org.ftclub.cabinet.admin.item.service; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.ftclub.cabinet.admin.dto.AdminItemHistoryDto; +import org.ftclub.cabinet.admin.dto.AdminItemHistoryPaginationDto; +import org.ftclub.cabinet.dto.ItemPurchaseCountDto; +import org.ftclub.cabinet.dto.ItemStatisticsDto; +import org.ftclub.cabinet.item.domain.Item; +import org.ftclub.cabinet.item.domain.ItemHistory; +import org.ftclub.cabinet.item.domain.ItemType; +import org.ftclub.cabinet.item.domain.Sku; +import org.ftclub.cabinet.item.service.ItemCommandService; +import org.ftclub.cabinet.item.service.ItemHistoryCommandService; +import org.ftclub.cabinet.item.service.ItemHistoryQueryService; +import org.ftclub.cabinet.item.service.ItemPolicyService; +import org.ftclub.cabinet.item.service.ItemQueryService; +import org.ftclub.cabinet.mapper.ItemMapper; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class AdminItemFacadeService { + + private final ItemQueryService itemQueryService; + private final ItemCommandService itemCommandService; + private final ItemHistoryCommandService itemHistoryCommandService; + private final ItemHistoryQueryService itemHistoryQueryService; + private final ItemPolicyService itemPolicyService; + private final ItemMapper itemMapper; + + @Transactional + public void createItem(Integer Price, Sku sku, ItemType type) { + itemCommandService.createItem(Price, sku, type); + } + + @Transactional + public void assignItem(List userIds, Sku sku) { + Item item = itemQueryService.getBySku(sku); + LocalDateTime now = null; + if (item.getPrice() > 0) { + now = LocalDateTime.now(); + } + itemHistoryCommandService.createItemHistories(userIds, item.getId(), now); + } + + @Transactional(readOnly = true) + public AdminItemHistoryPaginationDto getUserItemHistories(Long userId, Pageable pageable) { + Page itemHistoryWithItem = + itemHistoryQueryService.findItemHistoriesByUserIdWithItem(userId, pageable); + + List result = itemHistoryWithItem.stream() + .map(ih -> itemMapper.toAdminItemHistoryDto(ih, ih.getItem())) + .collect(Collectors.toList()); + + return new AdminItemHistoryPaginationDto(result, itemHistoryWithItem.getTotalElements()); + } + + @Transactional(readOnly = true) + public ItemStatisticsDto getItemPurchaseStatistics() { + List itemsOnSale = itemQueryService.getUseItemIds(); + List result = itemsOnSale.stream() + .map(item -> { + int userCount = itemHistoryQueryService.findPurchaseCountByItemId(item.getId()); + return itemMapper.toItemPurchaseCountDto(item, userCount); + }).collect(Collectors.toList()); + + return new ItemStatisticsDto(result); + } +} diff --git a/backend/src/main/java/org/ftclub/cabinet/admin/statistics/controller/AdminItemStatisticsController.java b/backend/src/main/java/org/ftclub/cabinet/admin/statistics/controller/AdminItemStatisticsController.java new file mode 100644 index 000000000..71232d964 --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/admin/statistics/controller/AdminItemStatisticsController.java @@ -0,0 +1,84 @@ +package org.ftclub.cabinet.admin.statistics.controller; + +import java.time.LocalDateTime; +import lombok.RequiredArgsConstructor; +import org.ftclub.cabinet.admin.item.service.AdminItemFacadeService; +import org.ftclub.cabinet.admin.statistics.service.AdminStatisticsFacadeService; +import org.ftclub.cabinet.auth.domain.AuthGuard; +import org.ftclub.cabinet.auth.domain.AuthLevel; +import org.ftclub.cabinet.dto.CoinCollectStatisticsDto; +import org.ftclub.cabinet.dto.CoinStaticsDto; +import org.ftclub.cabinet.dto.ItemStatisticsDto; +import org.ftclub.cabinet.dto.TotalCoinAmountDto; +import org.ftclub.cabinet.log.Logging; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.format.annotation.DateTimeFormat.ISO; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/v5/admin/statistics") +@RequiredArgsConstructor +@Logging +public class AdminItemStatisticsController { + + private final AdminStatisticsFacadeService adminStatisticsFacadeService; + private final AdminItemFacadeService adminItemFacadeService; + + /** + * 특정 연도, 월의 동전 줍기 횟수를 횟수 별로 통계를 내서 반환 + * + * @param year + * @param month 조회를 원하는 기간 + * @return + */ + @GetMapping("/coins/collect") + @AuthGuard(level = AuthLevel.ADMIN_ONLY) + public CoinCollectStatisticsDto getCoinCollectCountByMonth( + @RequestParam("year") Integer year, + @RequestParam("month") Integer month) { + return adminStatisticsFacadeService.getCoinCollectCountByMonth(year, month); + } + + /** + * 전체 기간동안 동전의 발행량 및 사용량 반환 + * + * @return + */ + @GetMapping("/coins") + @AuthGuard(level = AuthLevel.ADMIN_ONLY) + public TotalCoinAmountDto getTotalCoinAmount() { + return adminStatisticsFacadeService.getTotalCoinAmount(); + } + + /** + * 아이템별 구매 인원 조회 + * + * @return + */ + @GetMapping("/items") + @AuthGuard(level = AuthLevel.ADMIN_ONLY) + public ItemStatisticsDto getItemPurchaseStatistics() { + return adminItemFacadeService.getItemPurchaseStatistics(); + } + + /** + * 특정 기간동안 재화 사용량 및 발행량 조회 + * + * @param startDate + * @param endDate 조회를 원하는 기간 + * @return + */ + @GetMapping("/coins/use") + @AuthGuard(level = AuthLevel.ADMIN_ONLY) + public CoinStaticsDto getCoinStaticsDto( + @RequestParam("startDate") @DateTimeFormat(iso = ISO.DATE_TIME) LocalDateTime startDate, + @RequestParam("endDate") @DateTimeFormat(iso = ISO.DATE_TIME) LocalDateTime endDate) { + return adminStatisticsFacadeService.getCoinStaticsDto(startDate.toLocalDate(), + endDate.toLocalDate()); + + } + +} diff --git a/backend/src/main/java/org/ftclub/cabinet/admin/statistics/service/AdminStatisticsFacadeService.java b/backend/src/main/java/org/ftclub/cabinet/admin/statistics/service/AdminStatisticsFacadeService.java index 4add56569..f3fc592e8 100644 --- a/backend/src/main/java/org/ftclub/cabinet/admin/statistics/service/AdminStatisticsFacadeService.java +++ b/backend/src/main/java/org/ftclub/cabinet/admin/statistics/service/AdminStatisticsFacadeService.java @@ -1,28 +1,46 @@ package org.ftclub.cabinet.admin.statistics.service; +import static java.util.stream.Collectors.groupingBy; import static org.ftclub.cabinet.cabinet.domain.CabinetStatus.AVAILABLE; import static org.ftclub.cabinet.cabinet.domain.CabinetStatus.BROKEN; import static org.ftclub.cabinet.cabinet.domain.CabinetStatus.FULL; import static org.ftclub.cabinet.cabinet.domain.CabinetStatus.OVERDUE; +import static org.ftclub.cabinet.item.domain.Sku.COIN_COLLECT; +import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.Comparator; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; +import java.util.stream.IntStream; import lombok.RequiredArgsConstructor; import org.ftclub.cabinet.cabinet.service.CabinetQueryService; import org.ftclub.cabinet.dto.BlockedUserPaginationDto; import org.ftclub.cabinet.dto.CabinetFloorStatisticsResponseDto; +import org.ftclub.cabinet.dto.CoinAmountDto; +import org.ftclub.cabinet.dto.CoinCollectStatisticsDto; +import org.ftclub.cabinet.dto.CoinCollectedCountDto; +import org.ftclub.cabinet.dto.CoinStaticsDto; import org.ftclub.cabinet.dto.LentsStatisticsResponseDto; import org.ftclub.cabinet.dto.OverdueUserCabinetDto; import org.ftclub.cabinet.dto.OverdueUserCabinetPaginationDto; +import org.ftclub.cabinet.dto.TotalCoinAmountDto; import org.ftclub.cabinet.dto.UserBlockedInfoDto; import org.ftclub.cabinet.exception.ExceptionStatus; import org.ftclub.cabinet.exception.ServiceException; +import org.ftclub.cabinet.item.domain.ItemHistory; +import org.ftclub.cabinet.item.service.ItemHistoryQueryService; +import org.ftclub.cabinet.item.service.ItemQueryService; +import org.ftclub.cabinet.item.service.ItemRedisService; import org.ftclub.cabinet.lent.domain.LentHistory; import org.ftclub.cabinet.lent.service.LentQueryService; import org.ftclub.cabinet.log.LogLevel; import org.ftclub.cabinet.log.Logging; import org.ftclub.cabinet.mapper.CabinetMapper; +import org.ftclub.cabinet.mapper.ItemMapper; import org.ftclub.cabinet.mapper.UserMapper; import org.ftclub.cabinet.user.domain.BanHistory; import org.ftclub.cabinet.user.service.BanHistoryQueryService; @@ -48,6 +66,10 @@ public class AdminStatisticsFacadeService { private final CabinetMapper cabinetMapper; private final UserMapper userMapper; + private final ItemHistoryQueryService itemHistoryQueryService; + private final ItemQueryService itemQueryService; + private final ItemMapper itemMapper; + private final ItemRedisService itemRedisService; /** * 현재 가용중인 모든 사물함의 현황을 반환합니다. @@ -114,4 +136,88 @@ public OverdueUserCabinetPaginationDto getOverdueUsers(Pageable pageable) { ).collect(Collectors.toList()); return cabinetMapper.toOverdueUserCabinetPaginationDto(result, (long) lentHistories.size()); } + + /** + * 특정 연도, 월의 동전 줍기 횟수를 횟수 별로 통계를 내서 반환 + * + * @param year 조회를 원하는 기간 + * @param month 조회를 원하는 기간 + * @return 동전 줍기 횟수 통계 + */ + public CoinCollectStatisticsDto getCoinCollectCountByMonth(Integer year, Integer month) { + Long itemId = itemQueryService.getBySku(COIN_COLLECT).getId(); + List coinCollectedInfoByMonth = + itemHistoryQueryService.findCoinCollectedInfoByMonth(itemId, year, month); + Map coinCollectCountByUser = coinCollectedInfoByMonth.stream() + .collect(groupingBy(ItemHistory::getUserId, Collectors.counting())); + + int[] coinCollectArray = new int[31]; + coinCollectCountByUser.forEach((userId, coinCount) -> + coinCollectArray[coinCount.intValue() - 1]++); + + List coinCollectedCountDto = IntStream.rangeClosed(0, + 30) // 1부터 30까지의 범위로 스트림 생성 + .mapToObj(i -> new CoinCollectedCountDto(i + 1, + coinCollectArray[i])) // 각 인덱스와 해당하는 배열 값으로 CoinCollectedCountDto 생성 + .collect(Collectors.toList()); // 리스트로 변환하여 반환 + + return new CoinCollectStatisticsDto(coinCollectedCountDto); + } + + /** + * 전체 기간동안 동전의 발행량 및 사용량 반환 + * + * @return 동전의 발행량 및 사용량 + */ + public TotalCoinAmountDto getTotalCoinAmount() { + long totalCoinSupply = itemRedisService.getTotalCoinSupply(); + long totalCoinUsage = itemRedisService.getTotalCoinUsage(); + + // 재화 총 사용량, 현재 총 보유량 (총 공급량 - 총 사용량) 반환 + return new TotalCoinAmountDto(totalCoinUsage, totalCoinSupply + totalCoinUsage); + } + + /** + * 특정 기간동안 재화 사용량 및 발행량 조회 + * + * @param startDate 조회를 원하는 기간 + * @param endDate 조회를 원하는 기간 + * @return 재화 사용량 및 발행량 + */ + public CoinStaticsDto getCoinStaticsDto(LocalDate startDate, LocalDate endDate) { + Map issuedAmount = new LinkedHashMap<>(); + Map usedAmount = new LinkedHashMap<>(); + int dayDifference = (int) ChronoUnit.DAYS.between(startDate, endDate) + 1; + IntStream.range(0, dayDifference) + .mapToObj(startDate::plusDays) + .forEach(date -> { + issuedAmount.put(date, 0L); + usedAmount.put(date, 0L); + }); + + List usedCoins = + itemHistoryQueryService.findUsedCoinHistoryBetween(startDate, endDate); + + usedCoins.forEach(ih -> { + Long price = ih.getItem().getPrice(); + LocalDate date = ih.getPurchaseAt().toLocalDate(); + + if (price > 0) { + issuedAmount.put(date, issuedAmount.get(date) + price); + } else { + usedAmount.put(date, usedAmount.get(date) - price); + } + }); + List issueCoin = convertMapToList(issuedAmount); + List usedCoin = convertMapToList(usedAmount); + + return new CoinStaticsDto(issueCoin, usedCoin); + } + + List convertMapToList(Map map) { + return map.entrySet().stream() + .map(entry -> itemMapper.toCoinAmountDto(entry.getKey(), entry.getValue())) + .sorted(Comparator.comparing(CoinAmountDto::getDate)) + .collect(Collectors.toList()); + } } diff --git a/backend/src/main/java/org/ftclub/cabinet/alarm/config/AlarmProperties.java b/backend/src/main/java/org/ftclub/cabinet/alarm/config/AlarmProperties.java index 4dd3ae0a1..556f30571 100644 --- a/backend/src/main/java/org/ftclub/cabinet/alarm/config/AlarmProperties.java +++ b/backend/src/main/java/org/ftclub/cabinet/alarm/config/AlarmProperties.java @@ -95,6 +95,19 @@ public class AlarmProperties { @Value("${cabinet.alarm.slack.announcement.template}") private String announcementSlackTemplate; + /*==================== section alarm ========================*/ + @Value("${cabinet.alarm.mail.sectionAlarm.subject}") + private String sectionAlarmSubject; + + @Value("${cabinet.alarm.mail.sectionAlarm.template}") + private String sectionAlarmMailTemplateUrl; + + @Value("${cabinet.alarm.fcm.sectionAlarm.template}") + private String sectionAlarmFcmTemplate; + + @Value("${cabinet.alarm.slack.sectionAlarm.template}") + private String sectionAlarmSlackTemplate; + /*======================== term =============================*/ @Value("${cabinet.alarm.overdue-term.week-before}") private Long overdueTermWeekBefore; diff --git a/backend/src/main/java/org/ftclub/cabinet/alarm/domain/AlarmEvent.java b/backend/src/main/java/org/ftclub/cabinet/alarm/domain/AlarmEvent.java index 4e7d97587..a0100f4e1 100644 --- a/backend/src/main/java/org/ftclub/cabinet/alarm/domain/AlarmEvent.java +++ b/backend/src/main/java/org/ftclub/cabinet/alarm/domain/AlarmEvent.java @@ -6,6 +6,7 @@ @Getter @ToString public class AlarmEvent { + private final Long receiverId; private final Alarm alarm; @@ -14,7 +15,7 @@ private AlarmEvent(Long receiverId, Alarm alarm) { this.alarm = alarm; } - public static AlarmEvent of(Long id, Alarm alarm) { - return new AlarmEvent(id, alarm); + public static AlarmEvent of(Long receiverId, Alarm alarm) { + return new AlarmEvent(receiverId, alarm); } } diff --git a/backend/src/main/java/org/ftclub/cabinet/alarm/domain/AlarmItem.java b/backend/src/main/java/org/ftclub/cabinet/alarm/domain/AlarmItem.java new file mode 100644 index 000000000..e0d73e1dc --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/alarm/domain/AlarmItem.java @@ -0,0 +1,14 @@ +package org.ftclub.cabinet.alarm.domain; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.ToString; + +@Getter +@ToString +@RequiredArgsConstructor +public class AlarmItem implements ItemUsage { + + private final Long userId; + private final Long cabinetPlaceId; +} diff --git a/backend/src/main/java/org/ftclub/cabinet/alarm/domain/AvailableSectionAlarm.java b/backend/src/main/java/org/ftclub/cabinet/alarm/domain/AvailableSectionAlarm.java new file mode 100644 index 000000000..42e9818fc --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/alarm/domain/AvailableSectionAlarm.java @@ -0,0 +1,14 @@ +package org.ftclub.cabinet.alarm.domain; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; +import org.ftclub.cabinet.cabinet.domain.Location; + +@Getter +@ToString +@AllArgsConstructor +public class AvailableSectionAlarm implements Alarm { + + private final Location location; +} diff --git a/backend/src/main/java/org/ftclub/cabinet/alarm/domain/ExtensionItem.java b/backend/src/main/java/org/ftclub/cabinet/alarm/domain/ExtensionItem.java new file mode 100644 index 000000000..76787c888 --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/alarm/domain/ExtensionItem.java @@ -0,0 +1,13 @@ +package org.ftclub.cabinet.alarm.domain; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class ExtensionItem implements ItemUsage { + + private final Long userId; + private final Integer days; + +} diff --git a/backend/src/main/java/org/ftclub/cabinet/alarm/domain/ItemUsage.java b/backend/src/main/java/org/ftclub/cabinet/alarm/domain/ItemUsage.java new file mode 100644 index 000000000..7aa8d74c2 --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/alarm/domain/ItemUsage.java @@ -0,0 +1,5 @@ +package org.ftclub.cabinet.alarm.domain; + +public interface ItemUsage { + +} diff --git a/backend/src/main/java/org/ftclub/cabinet/alarm/domain/PenaltyItem.java b/backend/src/main/java/org/ftclub/cabinet/alarm/domain/PenaltyItem.java new file mode 100644 index 000000000..32782c51f --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/alarm/domain/PenaltyItem.java @@ -0,0 +1,12 @@ +package org.ftclub.cabinet.alarm.domain; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class PenaltyItem implements ItemUsage { + + private final Long userId; + private final Integer days; +} diff --git a/backend/src/main/java/org/ftclub/cabinet/alarm/domain/SwapItem.java b/backend/src/main/java/org/ftclub/cabinet/alarm/domain/SwapItem.java new file mode 100644 index 000000000..621ab18a5 --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/alarm/domain/SwapItem.java @@ -0,0 +1,13 @@ +package org.ftclub.cabinet.alarm.domain; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class SwapItem implements ItemUsage { + + private final Long userId; + private final Long newCabinetId; + +} diff --git a/backend/src/main/java/org/ftclub/cabinet/alarm/handler/AlarmEventHandler.java b/backend/src/main/java/org/ftclub/cabinet/alarm/handler/AlarmEventListener.java similarity index 98% rename from backend/src/main/java/org/ftclub/cabinet/alarm/handler/AlarmEventHandler.java rename to backend/src/main/java/org/ftclub/cabinet/alarm/handler/AlarmEventListener.java index 135d5c956..77e8cd3e7 100644 --- a/backend/src/main/java/org/ftclub/cabinet/alarm/handler/AlarmEventHandler.java +++ b/backend/src/main/java/org/ftclub/cabinet/alarm/handler/AlarmEventListener.java @@ -14,7 +14,7 @@ @Component @RequiredArgsConstructor @Log4j2 -public class AlarmEventHandler { +public class AlarmEventListener { private final UserQueryService userQueryService; private final SlackAlarmSender slackAlarmSender; diff --git a/backend/src/main/java/org/ftclub/cabinet/alarm/handler/EmailAlarmSender.java b/backend/src/main/java/org/ftclub/cabinet/alarm/handler/EmailAlarmSender.java index c7a2cb890..d8006b6bc 100644 --- a/backend/src/main/java/org/ftclub/cabinet/alarm/handler/EmailAlarmSender.java +++ b/backend/src/main/java/org/ftclub/cabinet/alarm/handler/EmailAlarmSender.java @@ -9,6 +9,7 @@ import org.ftclub.cabinet.alarm.domain.Alarm; import org.ftclub.cabinet.alarm.domain.AlarmEvent; import org.ftclub.cabinet.alarm.domain.AnnouncementAlarm; +import org.ftclub.cabinet.alarm.domain.AvailableSectionAlarm; import org.ftclub.cabinet.alarm.domain.ExtensionExpirationImminentAlarm; import org.ftclub.cabinet.alarm.domain.ExtensionIssuanceAlarm; import org.ftclub.cabinet.alarm.domain.LentExpirationAlarm; @@ -38,10 +39,6 @@ public class EmailAlarmSender { @Async public void send(User user, AlarmEvent alarmEvent) { log.info("Email Alarm Event : user = {}, alarmEvent = {}", user, alarmEvent); - if (!gmailProperties.getIsProduction()) { - log.debug("개발 환경이므로 메일을 보내지 않습니다."); - return; - } MailDto mailDto = parseMessageToMailDto(user.getName(), alarmEvent.getAlarm()); try { @@ -68,6 +65,8 @@ private MailDto parseMessageToMailDto(String name, Alarm alarm) { (ExtensionExpirationImminentAlarm) alarm, context); } else if (alarm instanceof AnnouncementAlarm) { return generateAnnouncementAlarm((AnnouncementAlarm) alarm, context); + } else if (alarm instanceof AvailableSectionAlarm) { + return generateSectionAlarm((AvailableSectionAlarm) alarm, context); } else { throw ExceptionStatus.NOT_FOUND_ALARM.asServiceException(); } @@ -125,6 +124,16 @@ private MailDto generateLentSuccessAlarm(LentSuccessAlarm alarm, Context context alarmProperties.getLentSuccessMailTemplateUrl(), context); } + @NotNull + private MailDto generateSectionAlarm(AvailableSectionAlarm alarm, Context context) { + String building = alarm.getLocation().getBuilding(); + Integer floor = alarm.getLocation().getFloor(); + String section = alarm.getLocation().getSection(); + context.setVariable("location", building + " " + floor + "층 " + section + "구역"); + return new MailDto(alarmProperties.getSectionAlarmSubject(), + alarmProperties.getSectionAlarmMailTemplateUrl(), context); + } + private void sendMessage(String email, MailDto mailDto) throws MessagingException { log.info("send Message : email = {}, mailDto = {}", email, mailDto); MimeMessage message = javaMailSender.createMimeMessage(); diff --git a/backend/src/main/java/org/ftclub/cabinet/alarm/handler/ItemUsageHandler.java b/backend/src/main/java/org/ftclub/cabinet/alarm/handler/ItemUsageHandler.java new file mode 100644 index 000000000..c00c4d387 --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/alarm/handler/ItemUsageHandler.java @@ -0,0 +1,43 @@ +package org.ftclub.cabinet.alarm.handler; + +import lombok.RequiredArgsConstructor; +import org.ftclub.cabinet.alarm.domain.AlarmItem; +import org.ftclub.cabinet.alarm.domain.ExtensionItem; +import org.ftclub.cabinet.alarm.domain.ItemUsage; +import org.ftclub.cabinet.alarm.domain.PenaltyItem; +import org.ftclub.cabinet.alarm.domain.SwapItem; +import org.ftclub.cabinet.exception.ExceptionStatus; +import org.ftclub.cabinet.item.service.ItemFacadeService; +import org.ftclub.cabinet.lent.service.LentFacadeService; +import org.ftclub.cabinet.user.service.UserFacadeService; +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +@Component +@RequiredArgsConstructor +public class ItemUsageHandler { + + private final LentFacadeService lentFacadeService; + private final UserFacadeService userFacadeService; + private final ItemFacadeService itemFacadeService; + + @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT) + public void handleItemUsage(ItemUsage itemUsage) { + if (itemUsage instanceof SwapItem) { + SwapItem swapItem = (SwapItem) itemUsage; + lentFacadeService.swapPrivateCabinet(swapItem.getUserId(), swapItem.getNewCabinetId()); + } else if (itemUsage instanceof PenaltyItem) { + PenaltyItem penaltyItem = (PenaltyItem) itemUsage; + userFacadeService.reduceBanDays(penaltyItem.getUserId(), penaltyItem.getDays()); + } else if (itemUsage instanceof ExtensionItem) { + ExtensionItem extensionItem = (ExtensionItem) itemUsage; + lentFacadeService.plusExtensionDays(extensionItem.getUserId(), extensionItem.getDays()); + } else if (itemUsage instanceof AlarmItem) { + AlarmItem alarmItem = (AlarmItem) itemUsage; + itemFacadeService.addSectionAlarm(alarmItem.getUserId(), alarmItem.getCabinetPlaceId()); + } else { + throw ExceptionStatus.NOT_FOUND_ITEM.asServiceException(); + } + } +} diff --git a/backend/src/main/java/org/ftclub/cabinet/alarm/handler/PushAlarmSender.java b/backend/src/main/java/org/ftclub/cabinet/alarm/handler/PushAlarmSender.java index de642539f..635425d49 100644 --- a/backend/src/main/java/org/ftclub/cabinet/alarm/handler/PushAlarmSender.java +++ b/backend/src/main/java/org/ftclub/cabinet/alarm/handler/PushAlarmSender.java @@ -10,6 +10,7 @@ import org.ftclub.cabinet.alarm.domain.Alarm; import org.ftclub.cabinet.alarm.domain.AlarmEvent; import org.ftclub.cabinet.alarm.domain.AnnouncementAlarm; +import org.ftclub.cabinet.alarm.domain.AvailableSectionAlarm; import org.ftclub.cabinet.alarm.domain.ExtensionExpirationImminentAlarm; import org.ftclub.cabinet.alarm.domain.ExtensionIssuanceAlarm; import org.ftclub.cabinet.alarm.domain.LentExpirationAlarm; @@ -60,6 +61,8 @@ private FCMDto parseMessage(Alarm alarm) { (ExtensionExpirationImminentAlarm) alarm); } else if (alarm instanceof AnnouncementAlarm) { return generateAnnouncementAlarm(); + } else if (alarm instanceof AvailableSectionAlarm) { + return generateAvailableSectionAlarm((AvailableSectionAlarm) alarm); } else { throw ExceptionStatus.NOT_FOUND_ALARM.asServiceException(); } @@ -122,6 +125,16 @@ private FCMDto generateLentSuccessAlarm(LentSuccessAlarm alarm) { return new FCMDto(title, body); } + private FCMDto generateAvailableSectionAlarm(AvailableSectionAlarm alarm) { + String building = alarm.getLocation().getBuilding(); + Integer floor = alarm.getLocation().getFloor(); + String section = alarm.getLocation().getSection(); + String title = alarmProperties.getSectionAlarmSubject(); + String body = String.format(alarmProperties.getSectionAlarmSlackTemplate(), + building + " " + floor + "층 " + section + "구역"); + return new FCMDto(title, body); + } + private void sendMessage(String token, FCMDto fcmDto) { log.info("send Message : token = {}, fcmDto = {}", token, fcmDto); Message message = Message.builder() diff --git a/backend/src/main/java/org/ftclub/cabinet/alarm/handler/SectionAlarmManager.java b/backend/src/main/java/org/ftclub/cabinet/alarm/handler/SectionAlarmManager.java new file mode 100644 index 000000000..fa43002ca --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/alarm/handler/SectionAlarmManager.java @@ -0,0 +1,71 @@ +package org.ftclub.cabinet.alarm.handler; + +import static java.util.stream.Collectors.groupingBy; +import static java.util.stream.Collectors.mapping; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.ftclub.cabinet.alarm.domain.AlarmEvent; +import org.ftclub.cabinet.alarm.domain.AvailableSectionAlarm; +import org.ftclub.cabinet.cabinet.domain.Cabinet; +import org.ftclub.cabinet.cabinet.domain.CabinetStatus; +import org.ftclub.cabinet.cabinet.domain.Location; +import org.ftclub.cabinet.cabinet.service.CabinetQueryService; +import org.ftclub.cabinet.item.service.SectionAlarmCommandService; +import org.ftclub.cabinet.item.service.SectionAlarmQueryService; +import org.ftclub.cabinet.lent.service.LentRedisService; +import org.ftclub.cabinet.log.LogLevel; +import org.ftclub.cabinet.log.Logging; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Component +@RequiredArgsConstructor +@Logging(level = LogLevel.DEBUG) +public class SectionAlarmManager { + + private final SectionAlarmQueryService sectionAlarmQueryService; + private final SectionAlarmCommandService sectionAlarmCommandService; + private final CabinetQueryService cabinetQueryService; + private final LentRedisService lentRedisService; + private final ApplicationEventPublisher eventPublisher; + + + @Transactional + public void sendSectionAlarm() { + LocalDateTime now = LocalDateTime.now(); + LocalDateTime from = LocalDate.now().atStartOfDay(); + + // 오늘 열리는 모든 사물함 조회 + List allPendingCabinets = + cabinetQueryService.findAllPendingCabinets(CabinetStatus.PENDING); + // 반납일이 오늘 이전인 사물함만 필터링 + Map> locationCabinetMap = allPendingCabinets.stream() + .filter(cabinet -> + lentRedisService.getPreviousEndedAt(cabinet.getId()).isBefore(from)) + .collect(groupingBy(cabinet -> cabinet.getCabinetPlace().getLocation(), + mapping(cabinet -> cabinet, Collectors.toList()))); + + // 사용되지 않은 알람권 조회 + List alarmIds = new ArrayList<>(); + sectionAlarmQueryService.getUnsentAlarms().forEach(alarm -> { + Location location = alarm.getCabinetPlace().getLocation(); + // 오늘 열리는 사물함인 경우에만 알람 이벤트 생성 + if (locationCabinetMap.containsKey(location)) { + alarmIds.add(alarm.getId()); + eventPublisher.publishEvent( + AlarmEvent.of(alarm.getUserId(), new AvailableSectionAlarm(location))); + } + }); + // 사용된 알림권 업데이트 + if (!alarmIds.isEmpty()) { + sectionAlarmCommandService.updateAlarmSend(alarmIds, now); + } + } +} diff --git a/backend/src/main/java/org/ftclub/cabinet/alarm/handler/SlackAlarmSender.java b/backend/src/main/java/org/ftclub/cabinet/alarm/handler/SlackAlarmSender.java index 3d6a4177f..715f7a99c 100644 --- a/backend/src/main/java/org/ftclub/cabinet/alarm/handler/SlackAlarmSender.java +++ b/backend/src/main/java/org/ftclub/cabinet/alarm/handler/SlackAlarmSender.java @@ -7,6 +7,7 @@ import org.ftclub.cabinet.alarm.domain.Alarm; import org.ftclub.cabinet.alarm.domain.AlarmEvent; import org.ftclub.cabinet.alarm.domain.AnnouncementAlarm; +import org.ftclub.cabinet.alarm.domain.AvailableSectionAlarm; import org.ftclub.cabinet.alarm.domain.ExtensionExpirationImminentAlarm; import org.ftclub.cabinet.alarm.domain.ExtensionIssuanceAlarm; import org.ftclub.cabinet.alarm.domain.LentExpirationAlarm; @@ -58,6 +59,8 @@ private SlackDto parseMessage(Alarm alarm) { return generateExtensionExpirationImminent((ExtensionExpirationImminentAlarm) alarm); } else if (alarm instanceof AnnouncementAlarm) { return generateAnnouncementAlarm(); + } else if (alarm instanceof AvailableSectionAlarm) { + return generateAvailableSectionAlarm((AvailableSectionAlarm) alarm); } else { throw ExceptionStatus.NOT_FOUND_ALARM.asServiceException(); } @@ -113,5 +116,12 @@ private SlackDto generateAnnouncementAlarm() { return new SlackDto(body); } - + private SlackDto generateAvailableSectionAlarm(AvailableSectionAlarm alarm) { + String building = alarm.getLocation().getBuilding(); + Integer floor = alarm.getLocation().getFloor(); + String section = alarm.getLocation().getSection(); + String body = String.format(alarmProperties.getSectionAlarmSlackTemplate(), + building + " " + floor + "층 " + section + "구역"); + return new SlackDto(body); + } } diff --git a/backend/src/main/java/org/ftclub/cabinet/cabinet/controller/CabinetController.java b/backend/src/main/java/org/ftclub/cabinet/cabinet/controller/CabinetController.java index 39059a418..0257bf866 100644 --- a/backend/src/main/java/org/ftclub/cabinet/cabinet/controller/CabinetController.java +++ b/backend/src/main/java/org/ftclub/cabinet/cabinet/controller/CabinetController.java @@ -9,8 +9,10 @@ import org.ftclub.cabinet.dto.CabinetInfoResponseDto; import org.ftclub.cabinet.dto.CabinetPendingResponseDto; import org.ftclub.cabinet.dto.CabinetsPerSectionResponseDto; +import org.ftclub.cabinet.dto.UserSessionDto; import org.ftclub.cabinet.exception.ControllerException; import org.ftclub.cabinet.log.Logging; +import org.ftclub.cabinet.user.domain.UserSession; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; @@ -49,9 +51,11 @@ public List getBuildingFloorsResponse() { @GetMapping("/buildings/{building}/floors/{floor}") @AuthGuard(level = AuthLevel.USER_OR_ADMIN) public List getCabinetsPerSection( + @UserSession UserSessionDto user, @PathVariable("building") String building, @PathVariable("floor") Integer floor) { - return cabinetFacadeService.getCabinetsPerSection(building, floor); + return cabinetFacadeService.getCabinetsPerSection(building, floor, + (user == null ? null : user.getUserId())); } /** diff --git a/backend/src/main/java/org/ftclub/cabinet/cabinet/domain/Location.java b/backend/src/main/java/org/ftclub/cabinet/cabinet/domain/Location.java index 1783f1be6..3ef7d20e5 100644 --- a/backend/src/main/java/org/ftclub/cabinet/cabinet/domain/Location.java +++ b/backend/src/main/java/org/ftclub/cabinet/cabinet/domain/Location.java @@ -1,5 +1,7 @@ package org.ftclub.cabinet.cabinet.domain; +import javax.persistence.Column; +import javax.persistence.Embeddable; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; @@ -8,9 +10,6 @@ import org.ftclub.cabinet.exception.ExceptionStatus; import org.ftclub.cabinet.utils.ExceptionUtil; -import javax.persistence.Column; -import javax.persistence.Embeddable; - /** * 건물, 층, 구역에 대한 정보입니다. */ @@ -36,11 +35,33 @@ protected Location(String building, Integer floor, String section) { public static Location of(String building, Integer floor, String section) { Location location = new Location(building, floor, section); - ExceptionUtil.throwIfFalse(location.isValid(), new DomainException(ExceptionStatus.INVALID_ARGUMENT)); + ExceptionUtil.throwIfFalse(location.isValid(), + new DomainException(ExceptionStatus.INVALID_ARGUMENT)); return location; } private boolean isValid() { return (this.building != null && this.floor > 0 && this.section != null); } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + Location location = (Location) obj; + return building.equals(location.building) && floor.equals(location.floor) + && section.equals(location.section); + } + + @Override + public int hashCode() { + int hash = 17 * 31 + building.hashCode(); + hash = 31 * hash + floor.hashCode(); + hash = 31 * hash + section.hashCode(); + return hash; + } } diff --git a/backend/src/main/java/org/ftclub/cabinet/cabinet/repository/CabinetRepository.java b/backend/src/main/java/org/ftclub/cabinet/cabinet/repository/CabinetRepository.java index 8efe292b2..13cd5d0cd 100644 --- a/backend/src/main/java/org/ftclub/cabinet/cabinet/repository/CabinetRepository.java +++ b/backend/src/main/java/org/ftclub/cabinet/cabinet/repository/CabinetRepository.java @@ -4,6 +4,7 @@ import java.util.Optional; import javax.persistence.LockModeType; import org.ftclub.cabinet.cabinet.domain.Cabinet; +import org.ftclub.cabinet.cabinet.domain.CabinetPlace; import org.ftclub.cabinet.cabinet.domain.CabinetStatus; import org.ftclub.cabinet.cabinet.domain.LentType; import org.springframework.data.domain.Page; @@ -210,4 +211,10 @@ void updateStatusAndTitleAndMemoByCabinetIdsIn(@Param("cabinetIds") List c List findAllByStatus(CabinetStatus cabinetStatus); + + @Query("SELECT p " + + "FROM CabinetPlace p " + + "WHERE p.location.building = :building AND p.location.floor = :floor AND p.location.section = :section ") + Optional findCabinetPlaceInfoByLocation(@Param("building") String building, + @Param("floor") Integer floor, @Param("section") String section); } diff --git a/backend/src/main/java/org/ftclub/cabinet/cabinet/service/CabinetFacadeService.java b/backend/src/main/java/org/ftclub/cabinet/cabinet/service/CabinetFacadeService.java index 0f6e3b461..f4c38f8b8 100644 --- a/backend/src/main/java/org/ftclub/cabinet/cabinet/service/CabinetFacadeService.java +++ b/backend/src/main/java/org/ftclub/cabinet/cabinet/service/CabinetFacadeService.java @@ -6,6 +6,7 @@ import static org.ftclub.cabinet.cabinet.domain.CabinetStatus.AVAILABLE; import static org.ftclub.cabinet.cabinet.domain.CabinetStatus.PENDING; +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Collections; @@ -13,15 +14,14 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.ftclub.cabinet.cabinet.domain.Cabinet; import org.ftclub.cabinet.cabinet.domain.CabinetStatus; import org.ftclub.cabinet.cabinet.domain.Grid; import org.ftclub.cabinet.cabinet.domain.LentType; -import org.ftclub.cabinet.club.domain.Club; import org.ftclub.cabinet.club.domain.ClubLentHistory; -import org.ftclub.cabinet.club.service.ClubQueryService; import org.ftclub.cabinet.dto.ActiveCabinetInfoEntities; import org.ftclub.cabinet.dto.BuildingFloorsDto; import org.ftclub.cabinet.dto.CabinetDto; @@ -34,6 +34,7 @@ import org.ftclub.cabinet.dto.LentHistoryDto; import org.ftclub.cabinet.dto.LentHistoryPaginationDto; import org.ftclub.cabinet.exception.ExceptionStatus; +import org.ftclub.cabinet.item.service.SectionAlarmQueryService; import org.ftclub.cabinet.lent.domain.LentHistory; import org.ftclub.cabinet.lent.service.ClubLentQueryService; import org.ftclub.cabinet.lent.service.LentQueryService; @@ -59,8 +60,8 @@ public class CabinetFacadeService { private final LentQueryService lentQueryService; private final LentRedisService lentRedisService; private final UserQueryService userQueryService; - private final ClubQueryService clubQueryService; private final ClubLentQueryService clubLentQueryService; + private final SectionAlarmQueryService sectionAlarmQueryService; private final CabinetMapper cabinetMapper; private final LentMapper lentMapper; @@ -75,8 +76,7 @@ public List getBuildingFloorsResponse() { List allBuildings = cabinetQueryService.findAllBuildings(); return allBuildings.stream() .map(building -> cabinetMapper.toBuildingFloorsDto(building, - cabinetQueryService.findAllFloorsByBuilding(building)) - ) + cabinetQueryService.findAllFloorsByBuilding(building))) .collect(Collectors.toList()); } @@ -99,8 +99,8 @@ public CabinetInfoResponseDto getCabinetInfo(Long cabinetId) { LocalDateTime sessionExpiredAt = lentRedisService.getSessionExpired(cabinetId); return cabinetMapper.toCabinetInfoResponseDto(cabinet, lentDtos, sessionExpiredAt); } - List cabinetActiveLentHistories = lentQueryService.findCabinetActiveLentHistories( - cabinetId); + List cabinetActiveLentHistories = + lentQueryService.findCabinetActiveLentHistories(cabinetId); List lentDtos = cabinetActiveLentHistories.stream() .map(lentHistory -> lentMapper.toLentDto(lentHistory.getUser(), lentHistory)) .collect(Collectors.toList()); @@ -109,8 +109,7 @@ public CabinetInfoResponseDto getCabinetInfo(Long cabinetId) { List usersInCabinet = lentRedisService.findUsersInCabinet(cabinetId); List users = userQueryService.findUsers(usersInCabinet); users.forEach(user -> lentDtos.add( - LentDto.builder().userId(user.getId()).name(user.getName()).build() - )); + LentDto.builder().userId(user.getId()).name(user.getName()).build())); } LocalDateTime sessionExpiredAt = lentRedisService.getSessionExpired(cabinetId); @@ -127,29 +126,43 @@ public CabinetInfoResponseDto getCabinetInfo(Long cabinetId) { */ @Transactional(readOnly = true) public List getCabinetsPerSection(String building, - Integer floor) { + Integer floor, Long userId) { + // 건물, 층에 있는 현재 대여 중인 사물함 정보, 해당 사물함에 대여 중인 유저 정보, 해당 대여 기록 조회 List activeCabinetInfos = cabinetQueryService.findActiveCabinetInfoEntities(building, floor); + // 조회한 사물함, 유저, 대여기록 리스트를 사물함 기준으로 대여기록 그룹화(Map) Map> cabinetLentHistories = activeCabinetInfos.stream(). collect(groupingBy(ActiveCabinetInfoEntities::getCabinet, mapping(ActiveCabinetInfoEntities::getLentHistory, Collectors.toList()))); + // 건물, 층에 있는 모든 사물함 정보 조회 List allCabinetsOnSection = cabinetQueryService.findAllCabinetsByBuildingAndFloor(building, floor); + // 동아리 사물함 대여 정보들을 조회하여 사물함 ID를 기준으로 그룹화(Map) Map> clubLentMap = clubLentQueryService.findAllActiveLentHistoriesWithClub().stream() .collect(groupingBy(ClubLentHistory::getCabinetId)); + // 층, 건물에 따른 유저가 알람 등록한 section 조회 + Set unsetAlarmSection = + sectionAlarmQueryService.getUnsentAlarm(userId, building, floor).stream() + .map(alarm -> alarm.getCabinetPlace().getLocation().getSection()) + .collect(Collectors.toSet()); + + // 층, 건물에 있는 사물함을 순회하며, visibleNum으로 정렬하고, 섹션별로 사물함 정보를 그룹화 Map> cabinetPreviewsBySection = new LinkedHashMap<>(); allCabinetsOnSection.stream() .sorted(Comparator.comparing(Cabinet::getVisibleNum)) .forEach(cabinet -> { String section = cabinet.getCabinetPlace().getLocation().getSection(); + // 동아리 사물함이라면, if (cabinet.getLentType().equals(LentType.CLUB)) { + // 동아리 사물함이 대여 중인 아닌 경우 빈 이름으로 Dto 생성, if (!clubLentMap.containsKey(cabinet.getId())) { cabinetPreviewsBySection.computeIfAbsent(section, k -> new ArrayList<>()) .add(cabinetMapper.toCabinetPreviewDto(cabinet, 0, null)); } else { + // 대여 중인 경우 대여기록을 가져와서 사물함 title을 가져와 Dto 생성 clubLentMap.get(cabinet.getId()).stream() .map(c -> c.getClub().getName()) .findFirst().ifPresent(clubName -> cabinetPreviewsBySection @@ -158,18 +171,22 @@ public List getCabinetsPerSection(String building } return; } + // 사물함 대여기록을 조회 및 사물함 제목을 가져옴 List lentHistories = cabinetLentHistories.getOrDefault(cabinet, Collections.emptyList()); String title = getCabinetTitle(cabinet, lentHistories); + // 사물함과 대여기록, title로 Dto 생성 cabinetPreviewsBySection.computeIfAbsent(section, k -> new ArrayList<>()) .add(cabinetMapper.toCabinetPreviewDto(cabinet, lentHistories.size(), title)); }); - + // 생성한 Dto를 섹션별로 묶어서 반환 return cabinetPreviewsBySection.entrySet().stream() - .map(entry -> cabinetMapper.toCabinetsPerSectionResponseDto(entry.getKey(), - entry.getValue())) - .collect(Collectors.toList()); + .map(entry -> { + String section = entry.getKey(); + return cabinetMapper.toCabinetsPerSectionResponseDto(section, entry.getValue(), + unsetAlarmSection.contains(section)); + }).collect(Collectors.toList()); } /** @@ -196,30 +213,41 @@ private String getCabinetTitle(Cabinet cabinet, List lentHistories) */ @Transactional(readOnly = true) public CabinetPendingResponseDto getAvailableCabinets(String building) { + // 현재 시간 및 어제 13시 00분 00초 시간 설정 final LocalDateTime now = LocalDateTime.now(); - final LocalDateTime yesterday = now.minusDays(1).withHour(13).withMinute(0).withSecond(0); + final LocalDate yesterday = now.minusDays(1).toLocalDate(); + + // 빌딩에 있는 CLUB 대여 타입이 아니면서, AVAILABLE, PENDING 상태인 사물함 조회 List availableCabinets = cabinetQueryService.findCabinetsNotLentTypeAndStatus( building, LentType.CLUB, List.of(AVAILABLE, PENDING)); + // 그 중 PENDING 상태인 사물함들만 ID를 리스트로 변환 List cabinetIds = availableCabinets.stream() .filter(cabinet -> cabinet.isStatus(PENDING)) .map(Cabinet::getId).collect(Collectors.toList()); + Map> lentHistoriesMap; + // 현재 시간이 13시 이전이면 어제 반납된 사물함을 조회 if (now.getHour() < 13) { - lentHistoriesMap = lentQueryService.findPendingLentHistoriesOnDate( - yesterday.toLocalDate(), cabinetIds) - .stream().collect(groupingBy(LentHistory::getCabinetId)); + lentHistoriesMap = + lentQueryService.findAvailableLentHistoriesOnDate(yesterday, cabinetIds) + .stream().collect(groupingBy(LentHistory::getCabinetId)); } else { + // 13시 이후면 현재 PENDING 인 사물함들을 조회 lentHistoriesMap = lentQueryService.findCabinetLentHistories(cabinetIds) .stream().collect(groupingBy(LentHistory::getCabinetId)); } + + // 층별로 사물함 정보를 그룹화 Map> cabinetFloorMap = cabinetQueryService.findAllFloorsByBuilding(building).stream() .collect(toMap(key -> key, value -> new ArrayList<>())); availableCabinets.forEach(cabinet -> { Integer floor = cabinet.getCabinetPlace().getLocation().getFloor(); + // AVAILABLE 상태인 사물함은 바로 추가 if (cabinet.isStatus(AVAILABLE)) { cabinetFloorMap.get(floor).add(cabinetMapper.toCabinetPreviewDto(cabinet, 0, null)); } + // PENDING 상태인 사물함이면서 오픈 예정으로 보여주어야 하는 사물함 추가 if (cabinet.isStatus(PENDING) && lentHistoriesMap.containsKey(cabinet.getId())) { lentHistoriesMap.get(cabinet.getId()).stream() .map(LentHistory::getEndedAt) @@ -256,10 +284,9 @@ public CabinetPaginationDto getCabinetPaginationByStatus(CabinetStatus status, */ @Transactional(readOnly = true) public LentHistoryPaginationDto getLentHistoryPagination(Long cabinetId, Pageable pageable) { - Page lentHistories = lentQueryService.findCabinetLentHistoriesWithUserAndCabinet( - cabinetId, pageable); + Page lentHistories = + lentQueryService.findCabinetLentHistoriesWithUserAndCabinet(cabinetId, pageable); List result = lentHistories.stream() - .sorted(Comparator.comparing(LentHistory::getStartedAt).reversed()) .map(lh -> lentMapper.toLentHistoryDto(lh, lh.getUser(), lh.getCabinet())) .collect(Collectors.toList()); return lentMapper.toLentHistoryPaginationDto(result, lentHistories.getTotalElements()); @@ -340,19 +367,6 @@ public void updateCabinetBundleStatus(List cabinetIds, CabinetStatus statu } } - /** - * [ADMIN] 사물함에 동아리 유저를 대여 시킵니다. {inheritDoc} - * - * @param clubId 대여할 유저 ID - * @param cabinetId 대여할 사물함 ID - * @param statusNote 상태 메모 - */ - @Transactional - public void updateClub(Long clubId, Long cabinetId, String statusNote) { - Cabinet cabinet = cabinetQueryService.getUserActiveCabinetForUpdate(cabinetId); - Club club = clubQueryService.getClub(clubId); - } - /** * [ADMIN] 사물함id 로 사물함을 찾아서, 상태를 변경시킵니다 * diff --git a/backend/src/main/java/org/ftclub/cabinet/cabinet/service/CabinetQueryService.java b/backend/src/main/java/org/ftclub/cabinet/cabinet/service/CabinetQueryService.java index 875b62fb0..122ecdf07 100644 --- a/backend/src/main/java/org/ftclub/cabinet/cabinet/service/CabinetQueryService.java +++ b/backend/src/main/java/org/ftclub/cabinet/cabinet/service/CabinetQueryService.java @@ -4,6 +4,7 @@ import java.util.Optional; import lombok.RequiredArgsConstructor; import org.ftclub.cabinet.cabinet.domain.Cabinet; +import org.ftclub.cabinet.cabinet.domain.CabinetPlace; import org.ftclub.cabinet.cabinet.domain.CabinetStatus; import org.ftclub.cabinet.cabinet.domain.LentType; import org.ftclub.cabinet.cabinet.repository.CabinetRepository; @@ -207,4 +208,21 @@ public Page findAllByStatus(CabinetStatus status, Pageable pageable) { public List findAllPendingCabinets(CabinetStatus cabinetStatus) { return cabinetRepository.findAllByStatus(cabinetStatus); } + + + /** + * 사물함 위치 정보로 CabinetPlaceId를 가져옵니다. + * + * @param building + * @param floor + * @param section + * @return + */ + public CabinetPlace getCabinetPlaceInfoByLocation(String building, Integer floor, + String section) { + return cabinetRepository.findCabinetPlaceInfoByLocation(building, floor, section) + .orElseThrow(ExceptionStatus.NOT_FOUND_SECTION::asServiceException); + + } + } diff --git a/backend/src/main/java/org/ftclub/cabinet/club/domain/ClubRegistration.java b/backend/src/main/java/org/ftclub/cabinet/club/domain/ClubRegistration.java index 333800b7a..291435f8a 100644 --- a/backend/src/main/java/org/ftclub/cabinet/club/domain/ClubRegistration.java +++ b/backend/src/main/java/org/ftclub/cabinet/club/domain/ClubRegistration.java @@ -20,7 +20,6 @@ import org.ftclub.cabinet.exception.DomainException; import org.ftclub.cabinet.exception.ExceptionStatus; import org.ftclub.cabinet.user.domain.User; -import org.ftclub.cabinet.user.domain.UserRole; import org.ftclub.cabinet.utils.ExceptionUtil; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; diff --git a/backend/src/main/java/org/ftclub/cabinet/user/domain/UserRole.java b/backend/src/main/java/org/ftclub/cabinet/club/domain/UserRole.java similarity index 76% rename from backend/src/main/java/org/ftclub/cabinet/user/domain/UserRole.java rename to backend/src/main/java/org/ftclub/cabinet/club/domain/UserRole.java index f377b3669..b05c1c902 100644 --- a/backend/src/main/java/org/ftclub/cabinet/user/domain/UserRole.java +++ b/backend/src/main/java/org/ftclub/cabinet/club/domain/UserRole.java @@ -1,4 +1,4 @@ -package org.ftclub.cabinet.user.domain; +package org.ftclub.cabinet.club.domain; public enum UserRole { diff --git a/backend/src/main/java/org/ftclub/cabinet/club/repository/ClubRegistrationRepoitory.java b/backend/src/main/java/org/ftclub/cabinet/club/repository/ClubRegistrationRepoitory.java index 695a083ab..91a997e91 100644 --- a/backend/src/main/java/org/ftclub/cabinet/club/repository/ClubRegistrationRepoitory.java +++ b/backend/src/main/java/org/ftclub/cabinet/club/repository/ClubRegistrationRepoitory.java @@ -4,7 +4,7 @@ import java.util.List; import java.util.Optional; import org.ftclub.cabinet.club.domain.ClubRegistration; -import org.ftclub.cabinet.user.domain.UserRole; +import org.ftclub.cabinet.club.domain.UserRole; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; diff --git a/backend/src/main/java/org/ftclub/cabinet/club/service/ClubFacadeService.java b/backend/src/main/java/org/ftclub/cabinet/club/service/ClubFacadeService.java index 9fc807736..317377294 100644 --- a/backend/src/main/java/org/ftclub/cabinet/club/service/ClubFacadeService.java +++ b/backend/src/main/java/org/ftclub/cabinet/club/service/ClubFacadeService.java @@ -1,6 +1,6 @@ package org.ftclub.cabinet.club.service; -import static org.ftclub.cabinet.user.domain.UserRole.CLUB_ADMIN; +import static org.ftclub.cabinet.club.domain.UserRole.CLUB_ADMIN; import java.util.List; import java.util.Map; @@ -11,6 +11,7 @@ import org.ftclub.cabinet.club.domain.Club; import org.ftclub.cabinet.club.domain.ClubLentHistory; import org.ftclub.cabinet.club.domain.ClubRegistration; +import org.ftclub.cabinet.club.domain.UserRole; import org.ftclub.cabinet.dto.ClubInfoDto; import org.ftclub.cabinet.dto.ClubInfoPaginationDto; import org.ftclub.cabinet.dto.ClubInfoResponseDto; @@ -21,7 +22,6 @@ import org.ftclub.cabinet.log.Logging; import org.ftclub.cabinet.mapper.ClubMapper; import org.ftclub.cabinet.user.domain.User; -import org.ftclub.cabinet.user.domain.UserRole; import org.ftclub.cabinet.user.service.UserQueryService; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; diff --git a/backend/src/main/java/org/ftclub/cabinet/club/service/ClubPolicyService.java b/backend/src/main/java/org/ftclub/cabinet/club/service/ClubPolicyService.java index 0b28c52a1..19e8760a9 100644 --- a/backend/src/main/java/org/ftclub/cabinet/club/service/ClubPolicyService.java +++ b/backend/src/main/java/org/ftclub/cabinet/club/service/ClubPolicyService.java @@ -2,10 +2,10 @@ import java.util.List; import lombok.RequiredArgsConstructor; +import org.ftclub.cabinet.club.domain.UserRole; import org.ftclub.cabinet.exception.ExceptionStatus; import org.ftclub.cabinet.log.LogLevel; import org.ftclub.cabinet.log.Logging; -import org.ftclub.cabinet.user.domain.UserRole; import org.springframework.stereotype.Service; @Service diff --git a/backend/src/main/java/org/ftclub/cabinet/club/service/ClubRegistrationCommandService.java b/backend/src/main/java/org/ftclub/cabinet/club/service/ClubRegistrationCommandService.java index e5c272555..57932098a 100644 --- a/backend/src/main/java/org/ftclub/cabinet/club/service/ClubRegistrationCommandService.java +++ b/backend/src/main/java/org/ftclub/cabinet/club/service/ClubRegistrationCommandService.java @@ -2,10 +2,10 @@ import lombok.RequiredArgsConstructor; import org.ftclub.cabinet.club.domain.ClubRegistration; +import org.ftclub.cabinet.club.domain.UserRole; import org.ftclub.cabinet.club.repository.ClubRegistrationRepoitory; import org.ftclub.cabinet.log.LogLevel; import org.ftclub.cabinet.log.Logging; -import org.ftclub.cabinet.user.domain.UserRole; import org.springframework.stereotype.Service; @Service diff --git a/backend/src/main/java/org/ftclub/cabinet/club/service/ClubRegistrationQueryService.java b/backend/src/main/java/org/ftclub/cabinet/club/service/ClubRegistrationQueryService.java index 3bc38ec8f..700843de0 100644 --- a/backend/src/main/java/org/ftclub/cabinet/club/service/ClubRegistrationQueryService.java +++ b/backend/src/main/java/org/ftclub/cabinet/club/service/ClubRegistrationQueryService.java @@ -1,7 +1,7 @@ package org.ftclub.cabinet.club.service; -import static org.ftclub.cabinet.user.domain.UserRole.CLUB_ADMIN; +import static org.ftclub.cabinet.club.domain.UserRole.CLUB_ADMIN; import java.util.List; import java.util.Optional; diff --git a/backend/src/main/java/org/ftclub/cabinet/config/ArgumentResolverConfig.java b/backend/src/main/java/org/ftclub/cabinet/config/ArgumentResolverConfig.java new file mode 100644 index 000000000..1060e1521 --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/config/ArgumentResolverConfig.java @@ -0,0 +1,20 @@ +package org.ftclub.cabinet.config; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.ftclub.cabinet.user.domain.UserSessionArgumentResolver; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +@RequiredArgsConstructor +public class ArgumentResolverConfig implements WebMvcConfigurer { + + private final UserSessionArgumentResolver userSessionArgumentResolver; + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(userSessionArgumentResolver); + } +} diff --git a/backend/src/main/java/org/ftclub/cabinet/dto/ActiveCabinetInfoDto.java b/backend/src/main/java/org/ftclub/cabinet/dto/ActiveCabinetInfoDto.java index 6bf9a599b..e712e3a8a 100644 --- a/backend/src/main/java/org/ftclub/cabinet/dto/ActiveCabinetInfoDto.java +++ b/backend/src/main/java/org/ftclub/cabinet/dto/ActiveCabinetInfoDto.java @@ -1,5 +1,6 @@ package org.ftclub.cabinet.dto; +import java.time.LocalDateTime; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.ToString; @@ -7,9 +8,6 @@ import org.ftclub.cabinet.cabinet.domain.Grid; import org.ftclub.cabinet.cabinet.domain.LentType; import org.ftclub.cabinet.cabinet.domain.Location; -import org.ftclub.cabinet.user.domain.UserRole; - -import java.time.LocalDateTime; /** * 현재 대여 기록의 모든 연관관계를 담는 내부 계층 간 DTO @@ -18,6 +16,7 @@ @Getter @ToString public class ActiveCabinetInfoDto { + private final Long cabinetId; private final Long lentHistoryId; private final Long userId; @@ -25,7 +24,6 @@ public class ActiveCabinetInfoDto { private final String email; private final LocalDateTime blackholedAt; private final LocalDateTime deletedAt; - private final UserRole role; private final LocalDateTime startedAt; private final LocalDateTime expiredAt; private final LocalDateTime endedAt; diff --git a/backend/src/main/java/org/ftclub/cabinet/dto/CabinetsPerSectionResponseDto.java b/backend/src/main/java/org/ftclub/cabinet/dto/CabinetsPerSectionResponseDto.java index eeec57211..3b14c03b2 100644 --- a/backend/src/main/java/org/ftclub/cabinet/dto/CabinetsPerSectionResponseDto.java +++ b/backend/src/main/java/org/ftclub/cabinet/dto/CabinetsPerSectionResponseDto.java @@ -15,4 +15,5 @@ public class CabinetsPerSectionResponseDto { private final String section; private final List cabinets; + private final boolean alarmRegistered; } diff --git a/backend/src/main/java/org/ftclub/cabinet/dto/CoinAmountDto.java b/backend/src/main/java/org/ftclub/cabinet/dto/CoinAmountDto.java new file mode 100644 index 000000000..ecc45f449 --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/dto/CoinAmountDto.java @@ -0,0 +1,13 @@ +package org.ftclub.cabinet.dto; + +import java.time.LocalDate; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class CoinAmountDto { + + private LocalDate date; + private Long amount; +} diff --git a/backend/src/main/java/org/ftclub/cabinet/dto/CoinCollectStatisticsDto.java b/backend/src/main/java/org/ftclub/cabinet/dto/CoinCollectStatisticsDto.java new file mode 100644 index 000000000..13d866fee --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/dto/CoinCollectStatisticsDto.java @@ -0,0 +1,15 @@ +package org.ftclub.cabinet.dto; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +@AllArgsConstructor +public class CoinCollectStatisticsDto { + + private List coinCollectStatistics; + +} diff --git a/backend/src/main/java/org/ftclub/cabinet/dto/CoinCollectedCountDto.java b/backend/src/main/java/org/ftclub/cabinet/dto/CoinCollectedCountDto.java new file mode 100644 index 000000000..d893e604a --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/dto/CoinCollectedCountDto.java @@ -0,0 +1,14 @@ +package org.ftclub.cabinet.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +@AllArgsConstructor +public class CoinCollectedCountDto { + + private Integer coinCount; + private Integer userCount; +} diff --git a/backend/src/main/java/org/ftclub/cabinet/dto/CoinCollectionRewardResponseDto.java b/backend/src/main/java/org/ftclub/cabinet/dto/CoinCollectionRewardResponseDto.java new file mode 100644 index 000000000..147e9e730 --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/dto/CoinCollectionRewardResponseDto.java @@ -0,0 +1,11 @@ +package org.ftclub.cabinet.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class CoinCollectionRewardResponseDto { + + private Integer reward; +} diff --git a/backend/src/main/java/org/ftclub/cabinet/dto/CoinHistoryDto.java b/backend/src/main/java/org/ftclub/cabinet/dto/CoinHistoryDto.java new file mode 100644 index 000000000..023469920 --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/dto/CoinHistoryDto.java @@ -0,0 +1,19 @@ +package org.ftclub.cabinet.dto; + +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@AllArgsConstructor +public class CoinHistoryDto { + + private LocalDateTime date; + private Integer amount; + private String history; + private String itemDetails; +} diff --git a/backend/src/main/java/org/ftclub/cabinet/dto/CoinHistoryPaginationDto.java b/backend/src/main/java/org/ftclub/cabinet/dto/CoinHistoryPaginationDto.java new file mode 100644 index 000000000..a9fb02a14 --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/dto/CoinHistoryPaginationDto.java @@ -0,0 +1,15 @@ +package org.ftclub.cabinet.dto; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +@AllArgsConstructor +public class CoinHistoryPaginationDto { + + private List result; + private Long totalLength; +} diff --git a/backend/src/main/java/org/ftclub/cabinet/dto/CoinMonthlyCollectionDto.java b/backend/src/main/java/org/ftclub/cabinet/dto/CoinMonthlyCollectionDto.java new file mode 100644 index 000000000..cf9c35955 --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/dto/CoinMonthlyCollectionDto.java @@ -0,0 +1,14 @@ +package org.ftclub.cabinet.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +@AllArgsConstructor +public class CoinMonthlyCollectionDto { + + private Long monthlyCoinCount; + private boolean todayCoinCollection; +} diff --git a/backend/src/main/java/org/ftclub/cabinet/dto/CoinStaticsDto.java b/backend/src/main/java/org/ftclub/cabinet/dto/CoinStaticsDto.java new file mode 100644 index 000000000..beef66087 --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/dto/CoinStaticsDto.java @@ -0,0 +1,13 @@ +package org.ftclub.cabinet.dto; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class CoinStaticsDto { + + private List issuedCoin; + private List usedCoin; +} diff --git a/backend/src/main/java/org/ftclub/cabinet/dto/ItemAssignPaginationDto.java b/backend/src/main/java/org/ftclub/cabinet/dto/ItemAssignPaginationDto.java new file mode 100644 index 000000000..a441b91d2 --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/dto/ItemAssignPaginationDto.java @@ -0,0 +1,13 @@ +package org.ftclub.cabinet.dto; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class ItemAssignPaginationDto { + + Long total; + private List items; +} diff --git a/backend/src/main/java/org/ftclub/cabinet/dto/ItemAssignRequestDto.java b/backend/src/main/java/org/ftclub/cabinet/dto/ItemAssignRequestDto.java new file mode 100644 index 000000000..20770c617 --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/dto/ItemAssignRequestDto.java @@ -0,0 +1,18 @@ +package org.ftclub.cabinet.dto; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.ftclub.cabinet.item.domain.Sku; + +@Getter +@ToString +@NoArgsConstructor +@AllArgsConstructor +public class ItemAssignRequestDto { + + private Sku itemSku; + private List userIds; +} diff --git a/backend/src/main/java/org/ftclub/cabinet/dto/ItemAssignResponseDto.java b/backend/src/main/java/org/ftclub/cabinet/dto/ItemAssignResponseDto.java new file mode 100644 index 000000000..d1b104750 --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/dto/ItemAssignResponseDto.java @@ -0,0 +1,16 @@ +package org.ftclub.cabinet.dto; + +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.ftclub.cabinet.item.domain.Sku; + +@Getter +@AllArgsConstructor +public class ItemAssignResponseDto { + + private Sku itemSku; // sku + private String itemName; // itemType + private String itemDetails; // sku.description + private LocalDateTime issuedDate; // itemHistory -> purchasedAt +} diff --git a/backend/src/main/java/org/ftclub/cabinet/dto/ItemCreateDto.java b/backend/src/main/java/org/ftclub/cabinet/dto/ItemCreateDto.java new file mode 100644 index 000000000..63ddfbe05 --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/dto/ItemCreateDto.java @@ -0,0 +1,21 @@ +package org.ftclub.cabinet.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import org.ftclub.cabinet.item.domain.ItemType; +import org.ftclub.cabinet.item.domain.Sku; + +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +public class ItemCreateDto { + + private Integer price; + private Sku sku; + private ItemType type; +} diff --git a/backend/src/main/java/org/ftclub/cabinet/dto/ItemDetailsDto.java b/backend/src/main/java/org/ftclub/cabinet/dto/ItemDetailsDto.java new file mode 100644 index 000000000..08502179d --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/dto/ItemDetailsDto.java @@ -0,0 +1,16 @@ +package org.ftclub.cabinet.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; +import org.ftclub.cabinet.item.domain.Sku; + +@Getter +@ToString +@AllArgsConstructor +public class ItemDetailsDto { + + private Sku itemSku; + private Integer itemPrice; + private String itemDetails; // 연장권 종류 - 3, 15, 31일, 페널티 종류 - 3, 7, 31일 +} diff --git a/backend/src/main/java/org/ftclub/cabinet/dto/ItemDto.java b/backend/src/main/java/org/ftclub/cabinet/dto/ItemDto.java new file mode 100644 index 000000000..9b128b514 --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/dto/ItemDto.java @@ -0,0 +1,21 @@ +package org.ftclub.cabinet.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import org.ftclub.cabinet.item.domain.ItemType; +import org.ftclub.cabinet.item.domain.Sku; + +@Getter +@Setter +@ToString +@AllArgsConstructor +public class ItemDto { + + private Sku itemSku; + private String itemName; + private ItemType itemType; + private Integer itemPrice; + private String itemDetails; +} diff --git a/backend/src/main/java/org/ftclub/cabinet/dto/ItemHistoryDto.java b/backend/src/main/java/org/ftclub/cabinet/dto/ItemHistoryDto.java new file mode 100644 index 000000000..1520360ee --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/dto/ItemHistoryDto.java @@ -0,0 +1,17 @@ +package org.ftclub.cabinet.dto; + +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@AllArgsConstructor +public class ItemHistoryDto { + + private LocalDateTime date; + private ItemDto itemDto; +} diff --git a/backend/src/main/java/org/ftclub/cabinet/dto/ItemHistoryPaginationDto.java b/backend/src/main/java/org/ftclub/cabinet/dto/ItemHistoryPaginationDto.java new file mode 100644 index 000000000..f6ed8f0aa --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/dto/ItemHistoryPaginationDto.java @@ -0,0 +1,15 @@ +package org.ftclub.cabinet.dto; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +@AllArgsConstructor +public class ItemHistoryPaginationDto { + + private List result; + private Long totalLength; +} diff --git a/backend/src/main/java/org/ftclub/cabinet/dto/ItemPurchaseCountDto.java b/backend/src/main/java/org/ftclub/cabinet/dto/ItemPurchaseCountDto.java new file mode 100644 index 000000000..7dc75c7c3 --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/dto/ItemPurchaseCountDto.java @@ -0,0 +1,13 @@ +package org.ftclub.cabinet.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class ItemPurchaseCountDto { + + private String itemName; + private String itemDetails; + private int userCount; +} diff --git a/backend/src/main/java/org/ftclub/cabinet/dto/ItemResponseDto.java b/backend/src/main/java/org/ftclub/cabinet/dto/ItemResponseDto.java new file mode 100644 index 000000000..e0b953368 --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/dto/ItemResponseDto.java @@ -0,0 +1,12 @@ +package org.ftclub.cabinet.dto; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class ItemResponseDto { + + private final List result; +} diff --git a/backend/src/main/java/org/ftclub/cabinet/dto/ItemStatisticsDto.java b/backend/src/main/java/org/ftclub/cabinet/dto/ItemStatisticsDto.java new file mode 100644 index 000000000..da2dbce66 --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/dto/ItemStatisticsDto.java @@ -0,0 +1,13 @@ +package org.ftclub.cabinet.dto; + + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class ItemStatisticsDto { + + private List items; +} diff --git a/backend/src/main/java/org/ftclub/cabinet/dto/ItemStoreDto.java b/backend/src/main/java/org/ftclub/cabinet/dto/ItemStoreDto.java new file mode 100644 index 000000000..83f45d8b8 --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/dto/ItemStoreDto.java @@ -0,0 +1,26 @@ +package org.ftclub.cabinet.dto; + +import java.util.Comparator; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import org.ftclub.cabinet.item.domain.ItemType; + +@Getter +@Setter +@ToString +@AllArgsConstructor +public class ItemStoreDto { + + private String itemName; + private ItemType itemType; + private String description; + private List items; + + //SKU 기준 오름차순 정렬 + public void sortBySkuASC() { + items.sort(Comparator.comparing(ItemDetailsDto::getItemSku)); + } +} diff --git a/backend/src/main/java/org/ftclub/cabinet/dto/ItemStoreResponseDto.java b/backend/src/main/java/org/ftclub/cabinet/dto/ItemStoreResponseDto.java new file mode 100644 index 000000000..e69bfb5d5 --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/dto/ItemStoreResponseDto.java @@ -0,0 +1,16 @@ +package org.ftclub.cabinet.dto; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@ToString +@AllArgsConstructor +@NoArgsConstructor +public class ItemStoreResponseDto { + + private List items; +} diff --git a/backend/src/main/java/org/ftclub/cabinet/dto/ItemUseRequestDto.java b/backend/src/main/java/org/ftclub/cabinet/dto/ItemUseRequestDto.java new file mode 100644 index 000000000..46b3d3db8 --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/dto/ItemUseRequestDto.java @@ -0,0 +1,21 @@ +package org.ftclub.cabinet.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import lombok.NoArgsConstructor; +import org.ftclub.cabinet.item.domain.ValidItemUse; + + +@Getter +@AllArgsConstructor +@NoArgsConstructor +@ValidItemUse +public class ItemUseRequestDto { + + private Long newCabinetId; // 이사권 사용 시 + + private String building; + private Integer floor; + private String section; +} diff --git a/backend/src/main/java/org/ftclub/cabinet/dto/MyItemResponseDto.java b/backend/src/main/java/org/ftclub/cabinet/dto/MyItemResponseDto.java new file mode 100644 index 000000000..897e05cc1 --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/dto/MyItemResponseDto.java @@ -0,0 +1,17 @@ +package org.ftclub.cabinet.dto; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +@AllArgsConstructor +public class MyItemResponseDto { + + List extensionItems; + List swapItems; + List alarmItems; + List penaltyItems; +} diff --git a/backend/src/main/java/org/ftclub/cabinet/dto/MyProfileResponseDto.java b/backend/src/main/java/org/ftclub/cabinet/dto/MyProfileResponseDto.java index 9a3c12a19..4811ba0ea 100644 --- a/backend/src/main/java/org/ftclub/cabinet/dto/MyProfileResponseDto.java +++ b/backend/src/main/java/org/ftclub/cabinet/dto/MyProfileResponseDto.java @@ -1,12 +1,11 @@ package org.ftclub.cabinet.dto; -import lombok.AllArgsConstructor; -import lombok.Getter; -import org.ftclub.cabinet.alarm.dto.AlarmTypeResponseDto; - import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.Locale; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.ftclub.cabinet.alarm.dto.AlarmTypeResponseDto; /** * 내 프로필 정보와 대여 중인 사물함의 ID를 반환하는 DTO입니다. @@ -25,4 +24,5 @@ public class MyProfileResponseDto { private final LentExtensionResponseDto lentExtensionResponseDto; private final AlarmTypeResponseDto alarmTypes; private final Boolean isDeviceTokenExpired; + private final Long coins; } diff --git a/backend/src/main/java/org/ftclub/cabinet/dto/TotalCoinAmountDto.java b/backend/src/main/java/org/ftclub/cabinet/dto/TotalCoinAmountDto.java new file mode 100644 index 000000000..8ca2d65c4 --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/dto/TotalCoinAmountDto.java @@ -0,0 +1,15 @@ +package org.ftclub.cabinet.dto; + +import lombok.Getter; + +@Getter +public class TotalCoinAmountDto { + + private Long used; + private Long unused; + + public TotalCoinAmountDto(Long used, Long unused) { + this.used = -1 * used; + this.unused = unused; + } +} diff --git a/backend/src/main/java/org/ftclub/cabinet/exception/ExceptionStatus.java b/backend/src/main/java/org/ftclub/cabinet/exception/ExceptionStatus.java index 26f2fa8ce..55ac3ae7a 100644 --- a/backend/src/main/java/org/ftclub/cabinet/exception/ExceptionStatus.java +++ b/backend/src/main/java/org/ftclub/cabinet/exception/ExceptionStatus.java @@ -17,6 +17,7 @@ public enum ExceptionStatus { NOT_FOUND_CABINET(HttpStatus.NOT_FOUND, "사물함이 존재하지 않습니다."), NOT_FOUND_LENT_HISTORY(HttpStatus.NOT_FOUND, "대여한 사물함이 존재하지 않습니다."), NOT_FOUND_CLUB(HttpStatus.NOT_FOUND, "동아리가 존재하지 않습니다."), + NOT_FOUND_ITEM(HttpStatus.NOT_FOUND, "아이템이 존재하지 않습니다"), LENT_CLUB(HttpStatus.I_AM_A_TEAPOT, "동아리 전용 사물함입니다"), LENT_NOT_CLUB(HttpStatus.I_AM_A_TEAPOT, "동아리 전용 사물함이 아닙니다"), LENT_EXPIRE_IMMINENT(HttpStatus.I_AM_A_TEAPOT, "만료가 임박한 공유 사물함입니다\n해당 사물함은 대여할 수 없습니다"), @@ -32,6 +33,7 @@ public enum ExceptionStatus { LENT_ALREADY_EXISTED(HttpStatus.BAD_REQUEST, "이미 대여중인 사물함이 있습니다"), USER_ALREADY_EXISTED(HttpStatus.BAD_REQUEST, "이미 존재하는 유저입니다"), ADMIN_ALREADY_EXISTED(HttpStatus.BAD_REQUEST, "이미 존재하는 어드민입니다"), + COIN_COLLECTION_ALREADY_EXIST(HttpStatus.BAD_REQUEST, "오늘은 이미 동전줍기를 수행했습니다."), NOT_CLUB_USER(HttpStatus.BAD_REQUEST, "동아리 유저가 아닙니다"), INVALID_ARGUMENT(HttpStatus.BAD_REQUEST, "유효하지 않은 입력입니다"), INVALID_STATUS(HttpStatus.BAD_REQUEST, "유효하지 않은 상태변경입니다"), @@ -63,9 +65,9 @@ public enum ExceptionStatus { INVALID_LENT_TYPE(HttpStatus.BAD_REQUEST, "사물함의 대여 타입이 유효하지 않습니다."), NOT_FOUND_BUILDING(HttpStatus.NOT_FOUND, "빌딩이 존재하지 않습니다."), SWAP_EXPIRE_IMMINENT(HttpStatus.I_AM_A_TEAPOT, "현재 사물함의 대여 기간의 만료가 임박해 사물함을 이동 할 수 없습니다."), - SWAP_LIMIT_EXCEEDED(HttpStatus.I_AM_A_TEAPOT, "사물함 이사 횟수 제한을 초과했습니다.\n 일주일에 1회만 이사할 수 있습니다."), SWAP_RECORD_NOT_FOUND(HttpStatus.NOT_FOUND, "이사하기 기능을 사용한 기록이 없습니다."), SWAP_SAME_CABINET(HttpStatus.BAD_REQUEST, "같은 사물함으로 이사할 수 없습니다."), + SWAP_LIMIT_EXCEEDED(HttpStatus.BAD_REQUEST, "이사하기 기능을 이미 사용했습니다."), INVALID_CLUB(HttpStatus.BAD_REQUEST, "동아리가 맞지 않습니다."), NOT_CLUB_MASTER(HttpStatus.BAD_REQUEST, "동아리 장이 아닙니다."), INVALID_CLUB_MASTER(HttpStatus.BAD_REQUEST, "동아리에 동아리 장이 없습니다."), @@ -76,7 +78,12 @@ public enum ExceptionStatus { NOT_FOUND_FORM(HttpStatus.NOT_FOUND, "신청서가 존재하지 않습니다."), INVALID_FORM_ID(HttpStatus.BAD_REQUEST, "잘못된 신청번호입니다."), INVALID_LOCATION(HttpStatus.BAD_REQUEST, "잘못된 장소입니다."), - ; + INVALID_ITEM_USE_REQUEST(HttpStatus.BAD_REQUEST, "잘못된 아이템 사용 요청입니다."), + ITEM_NOT_ON_SALE(HttpStatus.BAD_REQUEST, "구매할 수 없는 아이템입니다."), + NOT_ENOUGH_COIN(HttpStatus.BAD_REQUEST, "보유한 코인이 아이템 가격보다 적습니다."), + INVALID_JWT_TOKEN(HttpStatus.BAD_REQUEST, "토큰이 없거나, 유효하지 않은 JWT 토큰입니다."), + NOT_FOUND_SECTION(HttpStatus.BAD_REQUEST, "사물함 구역 정보를 찾을 수 없습니다."), + ITEM_NOT_OWNED(HttpStatus.BAD_REQUEST, "해당 아이템을 보유하고 있지 않습니다"); final private int statusCode; final private String message; diff --git a/backend/src/main/java/org/ftclub/cabinet/item/controller/ItemController.java b/backend/src/main/java/org/ftclub/cabinet/item/controller/ItemController.java new file mode 100644 index 000000000..328bede18 --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/item/controller/ItemController.java @@ -0,0 +1,140 @@ +package org.ftclub.cabinet.item.controller; + +import javax.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.ftclub.cabinet.auth.domain.AuthGuard; +import org.ftclub.cabinet.auth.domain.AuthLevel; +import org.ftclub.cabinet.dto.CoinCollectionRewardResponseDto; +import org.ftclub.cabinet.dto.CoinHistoryPaginationDto; +import org.ftclub.cabinet.dto.CoinMonthlyCollectionDto; +import org.ftclub.cabinet.dto.ItemHistoryPaginationDto; +import org.ftclub.cabinet.dto.ItemStoreResponseDto; +import org.ftclub.cabinet.dto.ItemUseRequestDto; +import org.ftclub.cabinet.dto.MyItemResponseDto; +import org.ftclub.cabinet.dto.UserSessionDto; +import org.ftclub.cabinet.item.domain.CoinHistoryType; +import org.ftclub.cabinet.item.domain.Sku; +import org.ftclub.cabinet.item.service.ItemFacadeService; +import org.ftclub.cabinet.log.Logging; +import org.ftclub.cabinet.user.domain.UserSession; +import org.springframework.data.domain.Pageable; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/v5/items") +@Logging +public class ItemController { + + private final ItemFacadeService itemFacadeService; + + /** + * 전체 아이템 목록 조회 + * + * @return + */ + @GetMapping("") + @AuthGuard(level = AuthLevel.USER_OR_ADMIN) + public ItemStoreResponseDto getAllItems() { + return itemFacadeService.getAllItems(); + } + + /** + * 특정 아이템 구매 요청 + * + * @param user + * @param sku + */ + @PostMapping("/{sku}/purchase") + @AuthGuard(level = AuthLevel.USER_ONLY) + public void purchaseItem(@UserSession UserSessionDto user, + @PathVariable Sku sku) { + itemFacadeService.purchaseItem(user.getUserId(), sku); + } + + /** + * 유저의 아이템 구매, 사용 내역 목록 조회 + * + * @param user + * @param pageable + * @return + */ + @GetMapping("/history") + @AuthGuard(level = AuthLevel.USER_ONLY) + public ItemHistoryPaginationDto getItemHistory(@UserSession UserSessionDto user, + Pageable pageable) { + return itemFacadeService.getItemHistory(user.getUserId(), pageable); + } + + /** + * 유저가 보유하고 있는 아이템 목록 조회 + * + * @param user + * @return + */ + @GetMapping("/me") + @AuthGuard(level = AuthLevel.USER_ONLY) + public MyItemResponseDto getMyItems(@UserSession UserSessionDto user) { + return itemFacadeService.getMyItems(user); + } + + /** + * 유저의 동전 줍기 내역 반환 + * + * @param user + * @param type ALL, EARN, USE + * @param pageable + * @return + */ + @GetMapping("/coin/history") + @AuthGuard(level = AuthLevel.USER_ONLY) + public CoinHistoryPaginationDto getCoinHistory(@UserSession UserSessionDto user, + @RequestParam CoinHistoryType type, Pageable pageable) { + return itemFacadeService.getCoinHistory(user.getUserId(), type, pageable); + } + + /** + * 한달 간 동전 줍기 횟수, 당일 동전줍기 요청 유무 + * + * @param user 유저 세션 + * @return + */ + @GetMapping("/coin") + @AuthGuard(level = AuthLevel.USER_ONLY) + public CoinMonthlyCollectionDto getCoinMonthlyCollectionCount( + @UserSession UserSessionDto user) { + return itemFacadeService.getCoinCollectionCountInMonth(user.getUserId()); + } + + /** + * 동전 줍기 요청 + * + * @param user 유저 세션 + */ + @PostMapping("/coin") + @AuthGuard(level = AuthLevel.USER_ONLY) + public CoinCollectionRewardResponseDto collectCoin(@UserSession UserSessionDto user) { + return itemFacadeService.collectCoinAndIssueReward(user.getUserId()); + } + + /** + * 아이템 사용 요청 + * + * @param user 유저 세션 + * @param sku 아이템 고유 식별 값 + * @param data sku 에 따라 다르게 필요한 정보 + */ + @PostMapping("{sku}/use") + @AuthGuard(level = AuthLevel.USER_ONLY) + public void useItem(@UserSession UserSessionDto user, + @PathVariable("sku") Sku sku, + @Valid @RequestBody ItemUseRequestDto data) { + itemFacadeService.useItem(user.getUserId(), sku, data); + } +} diff --git a/backend/src/main/java/org/ftclub/cabinet/item/domain/CoinHistoryType.java b/backend/src/main/java/org/ftclub/cabinet/item/domain/CoinHistoryType.java new file mode 100644 index 000000000..c4199bae4 --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/item/domain/CoinHistoryType.java @@ -0,0 +1,11 @@ +package org.ftclub.cabinet.item.domain; + +public enum CoinHistoryType { + ALL, + EARN, + USE; + + public boolean isValid() { + return this.equals(ALL) || this.equals(EARN) || this.equals(USE); + } +} diff --git a/backend/src/main/java/org/ftclub/cabinet/item/domain/Item.java b/backend/src/main/java/org/ftclub/cabinet/item/domain/Item.java new file mode 100644 index 000000000..2b3cd7d4d --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/item/domain/Item.java @@ -0,0 +1,88 @@ +package org.ftclub.cabinet.item.domain; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.ftclub.cabinet.exception.ExceptionStatus; + +@Entity +@Table(name = "ITEM") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@ToString +@Getter +public class Item { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "ID") + private Long id; + + /** + * 상품 타입 + */ + @Column(name = "Type", nullable = false) + @Enumerated(value = EnumType.STRING) + private ItemType type; + + /** + * 상품 고유 코드 + */ + @Column(name = "SKU", unique = true, nullable = false) + @Enumerated(value = EnumType.STRING) + private Sku sku; + + /** + * 상품 가격 + */ + @Column(name = "PRICE", nullable = false) + private Long price; + + + protected Item(long price, Sku sku, ItemType type) { + this.price = price; + this.sku = sku; + this.type = type; + } + + /** + * @param price 상품 가격 + * @param sku 상품 코드 + * @return 상품 객체 {@link Item} + */ + public static Item of(long price, Sku sku, ItemType type) { + Item item = new Item(price, sku, type); + if (!item.isValid()) { + throw ExceptionStatus.INVALID_ARGUMENT.asDomainException(); + } + return item; + } + + /** + * name, sku, description 의 null 이 아닌지 확인합니다. + * + * @return 유효한 인스턴스 여부 + */ + private boolean isValid() { + return sku.isValid() && type.isValid() && type.isValid(); + } + + @Override + public boolean equals(final Object other) { + if (this == other) { + return true; + } + if (!(other instanceof Item)) { + return false; + } + return (this.id.equals(((Item) other).id)); + } +} diff --git a/backend/src/main/java/org/ftclub/cabinet/item/domain/ItemHistory.java b/backend/src/main/java/org/ftclub/cabinet/item/domain/ItemHistory.java new file mode 100644 index 000000000..2784a5f72 --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/item/domain/ItemHistory.java @@ -0,0 +1,111 @@ +package org.ftclub.cabinet.item.domain; + +import static javax.persistence.FetchType.LAZY; + +import java.time.LocalDateTime; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EntityListeners; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.ftclub.cabinet.exception.ExceptionStatus; +import org.ftclub.cabinet.user.domain.User; +import org.hibernate.annotations.BatchSize; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +@Entity +@Table(name = "ITEM_HISTORY") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@ToString(exclude = {"user", "item"}) +@EntityListeners(AuditingEntityListener.class) +@Getter +@BatchSize(size = 25) +public class ItemHistory { + + @Id + @Column(name = "ID") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + /** + * 아이템 구매 일자 + */ + @CreatedDate + @Column(name = "PURCHASE_AT", nullable = false, updatable = false) + private LocalDateTime purchaseAt; + + /** + * 아이템 사용 일자 + */ + @Column(name = "USED_AT") + private LocalDateTime usedAt; + + /** + * 아이템 소유자 + */ + @JoinColumn(name = "USER_ID", nullable = false, updatable = false, insertable = false) + @ManyToOne(fetch = LAZY) + private User user; + + /** + * 사용한 아이템 + */ + @JoinColumn(name = "ITEM_ID", nullable = false, updatable = false, insertable = false) + @ManyToOne(fetch = LAZY) + private Item item; + + /** + * 사용한 아이템 ID + */ + @Column(name = "ITEM_ID", nullable = false) + private Long itemId; + + /** + * 사용한 사용자 ID + */ + @Column(name = "USER_ID", nullable = false) + private Long userId; + + + protected ItemHistory(long userId, long itemId, LocalDateTime usedAt) { + this.userId = userId; + this.itemId = itemId; + this.usedAt = usedAt; + } + + /** + * @param userId 아이템을 사용한 유저 ID + * @param itemId 사용된 아이템 ID + * @param usedAt 아이템 사용일자 + * @return 아이템 히스토리 객체 {@link ItemHistory} + */ + public static ItemHistory of(long userId, long itemId, LocalDateTime usedAt) { + ItemHistory itemHistory = new ItemHistory(userId, itemId, usedAt); + if (!itemHistory.isValid()) { + throw ExceptionStatus.INVALID_ARGUMENT.asDomainException(); + } + return itemHistory; + } + + /** + * 사용자 ID, 아이템 ID, 사용일자의 null 이 아닌지 확인합니다. + * + * @return 유효한 인스턴스 여부 + */ + private boolean isValid() { + return this.userId != null && this.itemId != null; + } + + public void updateUsedAt() { + this.usedAt = LocalDateTime.now(); + } +} diff --git a/backend/src/main/java/org/ftclub/cabinet/item/domain/ItemType.java b/backend/src/main/java/org/ftclub/cabinet/item/domain/ItemType.java new file mode 100644 index 000000000..d7de1893a --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/item/domain/ItemType.java @@ -0,0 +1,37 @@ +package org.ftclub.cabinet.item.domain; + +import lombok.Getter; + +@Getter +public enum ItemType { + + EXTENSION("연장권", + "현재 대여 중인 사물함의 반납 기한을 3일, 15일 또는 30일 연장할 수 있습니다."), + PENALTY("페널티 감면권", + "사물함 이용 중 발생한 페널티 일수를 감소시켜 사물함 사용 제한 기간을 줄일 수 있습니다."), + SWAP("이사권" + , "현재 대여 중인 사물함의 반납 기한을 유지하면서 다른 사물함으로 이동할 수 있습니다."), + ALARM("알림 등록권", + "원하는 사물함 구역의 개인 사물함 자리가 빈 경우, 다음 날 오픈 5분 전 알림을 받을 수 있습니다."), + + COIN_COLLECT("동전 줍기", + "누군가가 매일 흘리는 동전을 주워보세요\uD83D\uDCB0\n동전은 하루에 한 번씩 획득할 수 있습니다"), + COIN_REWARD("동전 줍기 20일 보상", + "20일 동안 매일 동전을 주웠다면, 보너스 동전을 받습니다\uD83D\uDCB0"), + COIN_FULL_TIME("42 출석 보상", + "42 서울에 열심히 출석했다면, 보상을 받을 수 있습니다\uD83D\uDCB0"), + ; + + private final String name; + private final String description; + + ItemType(String name, String description) { + this.name = name; + this.description = description; + } + + public boolean isValid() { + return this.equals(EXTENSION) || this.equals(PENALTY) || this.equals(SWAP) + || this.equals(ALARM); + } +} diff --git a/backend/src/main/java/org/ftclub/cabinet/item/domain/ItemUseValidator.java b/backend/src/main/java/org/ftclub/cabinet/item/domain/ItemUseValidator.java new file mode 100644 index 000000000..8d323913a --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/item/domain/ItemUseValidator.java @@ -0,0 +1,48 @@ +package org.ftclub.cabinet.item.domain; + +import io.netty.util.internal.StringUtil; +import java.util.Map; +import javax.servlet.http.HttpServletRequest; +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import org.ftclub.cabinet.dto.ItemUseRequestDto; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.servlet.HandlerMapping; + +public class ItemUseValidator implements ConstraintValidator { + + private static final String PATH_VARIABLE_NAME = "sku"; + private static final String SKU_SWAP_NAME = "SWAP"; + private static final String SKU_ALARM_NAME = "ALARM"; + + @Override + public boolean isValid(ItemUseRequestDto itemUseRequestDto, + ConstraintValidatorContext constraintValidatorContext) { + + ServletRequestAttributes requestAttributes = + (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + HttpServletRequest request = requestAttributes.getRequest(); + Map pathVariables = + (Map) request.getAttribute( + HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); + + String sku = pathVariables.get(PATH_VARIABLE_NAME); + if (sku.equals(SKU_SWAP_NAME) && itemUseRequestDto.getNewCabinetId() == null) { + return false; + } + if (sku.equals(SKU_ALARM_NAME) && !isValidAlarmData(itemUseRequestDto)) { + return false; + } + return true; + } + + private boolean isValidAlarmData(ItemUseRequestDto dto) { + if (dto.getFloor() == null + || StringUtil.isNullOrEmpty(dto.getSection()) + || StringUtil.isNullOrEmpty(dto.getBuilding())) { + return false; + } + return true; + } +} diff --git a/backend/src/main/java/org/ftclub/cabinet/item/domain/SectionAlarm.java b/backend/src/main/java/org/ftclub/cabinet/item/domain/SectionAlarm.java new file mode 100644 index 000000000..79166cbbb --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/item/domain/SectionAlarm.java @@ -0,0 +1,108 @@ +package org.ftclub.cabinet.item.domain; + + +import static javax.persistence.FetchType.LAZY; + +import java.time.LocalDateTime; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EntityListeners; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.ftclub.cabinet.cabinet.domain.CabinetPlace; +import org.ftclub.cabinet.exception.ExceptionStatus; +import org.ftclub.cabinet.user.domain.User; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +@Entity +@Table(name = "SECTION_ALARM") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@ToString(exclude = {"user", "cabinetPlace"}) +@EntityListeners(AuditingEntityListener.class) +public class SectionAlarm { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + + /** + * 알람 등록 시간 + */ + @CreatedDate + @Column(name = "REGISTERED_AT", nullable = false, updatable = false) + private LocalDateTime registeredAt; + + /** + * 알람 발생 시간 + */ + @Column(name = "ALARMED_AT") + private LocalDateTime alarmedAt; + + /** + * 대여하는 유저 + */ + @Column(name = "USER_ID", nullable = false) + private Long userId; + + /** + * 알람 등록된 관심 사물함 영역 + */ + @Column(name = "CABINET_PLACE_ID", nullable = false) + private Long cabinetPlaceId; + + @JoinColumn(name = "USER_ID", nullable = false, insertable = false, updatable = false) + @ManyToOne(fetch = LAZY) + private User user; + + @JoinColumn(name = "CABINET_PLACE_ID", nullable = false, insertable = false, updatable = false) + @ManyToOne(fetch = LAZY) + private CabinetPlace cabinetPlace; + + protected SectionAlarm(Long userId, Long cabinetPlaceId) { + this.userId = userId; + this.cabinetPlaceId = cabinetPlaceId; + } + + /** + * @param userId 알람 발생 유저 + * @param cabinetPlaceId 알람 발생 사물함 영역 + * @return 인자 정보를 담고있는 {@link SectionAlarm} + */ + public static SectionAlarm of(Long userId, Long cabinetPlaceId) { + SectionAlarm sectionAlarm = new SectionAlarm(userId, cabinetPlaceId); + if (!sectionAlarm.isValid()) { + throw ExceptionStatus.INVALID_ARGUMENT.asDomainException(); + } + return sectionAlarm; + } + + /** + * registeredAt, userId, cabinetPlaceId, alarmType 의 null 이 아닌지 확인합니다. + * + * @return 유효한 인스턴스 여부 + */ + private boolean isValid() { + return this.userId != null && this.cabinetPlaceId != null; + } + + @Override + public boolean equals(final Object other) { + if (this == other) { + return true; + } + if (!(other instanceof SectionAlarm)) { + return false; + } + return (this.id.equals(((SectionAlarm) other).id)); + } +} diff --git a/backend/src/main/java/org/ftclub/cabinet/item/domain/Sku.java b/backend/src/main/java/org/ftclub/cabinet/item/domain/Sku.java new file mode 100644 index 000000000..fc8dff04a --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/item/domain/Sku.java @@ -0,0 +1,59 @@ +package org.ftclub.cabinet.item.domain; + +import lombok.Getter; +import org.ftclub.cabinet.exception.ExceptionStatus; + +@Getter +public enum Sku { + + EXTENSION_PREV("출석 연장권 보상"), + EXTENSION_3("3일"), + EXTENSION_15("15일"), + EXTENSION_31("31일"), + + PENALTY_3("3일"), + PENALTY_7("7일"), + PENALTY_31("31일"), + + SWAP("이사권"), + ALARM("알림 등록권"), + + COIN_COLLECT("동전 줍기"), + COIN_FULL_TIME("42 출석 보상"), + COIN_REWARD_200("동전 줍기 20일 보상"), + COIN_REWARD_500("동전 줍기 20일 보상"), + COIN_REWARD_1000("동전 줍기 20일 보상"), + COIN_REWARD_2000("동전 줍기 20일 보상"), + ; + + private final String details; + + Sku(String details) { + this.details = details; + } + + public boolean isValid() { + return this.equals(EXTENSION_3) || this.equals(EXTENSION_15) || this.equals(EXTENSION_31) + || this.equals(PENALTY_3) || this.equals(PENALTY_7) || this.equals(PENALTY_31) + || this.equals(SWAP) || this.equals(ALARM) || this.equals(COIN_COLLECT) + || this.equals(COIN_FULL_TIME) || this.equals(COIN_REWARD_200) + || this.equals(COIN_REWARD_500) || this.equals(COIN_REWARD_1000) + || this.equals(COIN_REWARD_2000); + } + + public Integer getDays() { + if (this.equals(EXTENSION_3) || this.equals(PENALTY_3)) { + return 3; + } + if (this.equals(EXTENSION_31) || this.equals(PENALTY_31)) { + return 31; + } + if (this.equals(EXTENSION_15)) { + return 15; + } + if (this.equals(PENALTY_7)) { + return 7; + } + throw ExceptionStatus.NOT_FOUND_ITEM.asDomainException(); + } +} diff --git a/backend/src/main/java/org/ftclub/cabinet/item/domain/ValidItemUse.java b/backend/src/main/java/org/ftclub/cabinet/item/domain/ValidItemUse.java new file mode 100644 index 000000000..6019c186f --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/item/domain/ValidItemUse.java @@ -0,0 +1,21 @@ +package org.ftclub.cabinet.item.domain; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import javax.validation.Constraint; +import javax.validation.Payload; + +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = ItemUseValidator.class) +public @interface ValidItemUse { + + String message() default "잘못된 아이템 사용 요청입니다"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} + diff --git a/backend/src/main/java/org/ftclub/cabinet/item/repository/ItemHistoryRepository.java b/backend/src/main/java/org/ftclub/cabinet/item/repository/ItemHistoryRepository.java new file mode 100644 index 000000000..8eded2a0f --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/item/repository/ItemHistoryRepository.java @@ -0,0 +1,93 @@ +package org.ftclub.cabinet.item.repository; + +import java.time.LocalDate; +import java.util.List; +import org.ftclub.cabinet.item.domain.ItemHistory; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +@Repository +public interface ItemHistoryRepository extends JpaRepository { + + @Query("SELECT ih " + + "FROM ItemHistory ih " + + "WHERE ih.userId = :userId " + + "AND ih.itemId IN (:itemIds) " + + "ORDER BY ih.purchaseAt DESC") + Page findAllByUserIdAndItemIdIn(@Param("userId") Long userId, + Pageable pageable, @Param("itemIds") List itemIds); + + @Query(value = "SELECT ih " + + "FROM ItemHistory ih " + + "JOIN FETCH ih.item i " + + "WHERE ih.userId = :userId " + + "AND ih.usedAt IS NOT NULL " + + "AND i.price < 0 " + + "ORDER BY ih.usedAt DESC", + countQuery = "SELECT COUNT(ih) " + + "FROM ItemHistory ih " + + "JOIN ih.item i " + + "WHERE ih.userId = :userId " + + "AND ih.usedAt IS NOT NULL " + + "AND i.price < 0") + Page findAllByUserIdOnMinusPriceItemsWithSubQuery( + @Param("userId") Long userId, Pageable pageable); + + @EntityGraph(attributePaths = "item") + Page findAllByUserIdOrderByPurchaseAtDesc(Long userId, Pageable pageable); + + @EntityGraph(attributePaths = "item") + List findAllByUserId(Long userId); + + @Query(value = "SELECT SUM(i.price) " + + "FROM ItemHistory ih " + + "JOIN ih.item i " + + "WHERE i.price < 0") + Long getPriceSumOnMinusPriceItems(); + + @Query(value = "SELECT SUM(i.price) " + + "FROM ItemHistory ih " + + "JOIN ih.item i " + + "WHERE i.price > 0") + Long getPriceSumOnPlusPriceItems(); + + @Query("SELECT ih " + + "FROM ItemHistory ih " + + "JOIN FETCH ih.item " + + "WHERE ih.userId = :userId " + + "AND ih.usedAt IS NULL " + ) + List getAllUnusedItemHistoryByUser(@Param("userId") Long userId); + + List findAllByUserIdAndItemIdAndUsedAtIsNull(Long userId, Long itemId); + + + @Query("SELECT ih " + + "FROM ItemHistory ih " + + "WHERE ih.itemId = :itemId " + + "AND YEAR(ih.purchaseAt) = :year " + + "AND MONTH(ih.purchaseAt) = :month " + ) + List findCoinCollectInfoByIdAtYearAndMonth(@Param("itemId") Long itemId, + @Param("year") Integer year, @Param("month") Integer month); + + @Query("SELECT COUNT(ih) " + + "FROM ItemHistory ih " + + "WHERE ih.itemId = :itemId") + int getCountByItemIds(@Param("itemId") Long itemId); + + @Query("SELECT ih " + + "FROM ItemHistory ih " + + "JOIN FETCH ih.item " + + "WHERE DATE(ih.purchaseAt) >= DATE(:start) " + + "AND DATE(ih.purchaseAt) <= DATE(:end)" + ) + List findAllUsedAtIsNotNullBetween( + @Param("start") LocalDate startDate, + @Param("end") LocalDate endDate); +} diff --git a/backend/src/main/java/org/ftclub/cabinet/item/repository/ItemRedis.java b/backend/src/main/java/org/ftclub/cabinet/item/repository/ItemRedis.java new file mode 100644 index 000000000..49f43a457 --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/item/repository/ItemRedis.java @@ -0,0 +1,143 @@ +package org.ftclub.cabinet.item.repository; + + +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.temporal.TemporalAdjusters; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import org.ftclub.cabinet.log.LogLevel; +import org.ftclub.cabinet.log.Logging; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +@Component +@Logging(level = LogLevel.DEBUG) +public class ItemRedis { + + private static final String COIN_COUNT_KEY_SUFFIX = ":coinAmount"; + private static final String COIN_COLLECT_KEY_SUFFIX = ":coinCollect"; + private static final String COIN_COLLECT_COUNT_KEY_SUFFIX = ":coinCollectCount"; + private static final String TOTAL_COIN_SUPPLY_KEY_SUFFIX = "totalCoinSupply"; + private static final String TOTAL_COIN_USAGE_KEY_SUFFIX = "totalCoinUsage"; + + + private final RedisTemplate coinTemplate; + + @Autowired + public ItemRedis(RedisTemplate coinTemplate) { + this.coinTemplate = coinTemplate; + } + + public String getCoinAmount(String userId) { + return coinTemplate.opsForValue().get(userId + COIN_COUNT_KEY_SUFFIX); + } + + public void saveCoinAmount(String userId, String coinCount) { + coinTemplate.opsForValue().set(userId + COIN_COUNT_KEY_SUFFIX, coinCount); + } + + /** + * 전체 동전 발행량 반환 + * + * @return + */ + public String getTotalCoinSupply() { + return coinTemplate.opsForValue().get(TOTAL_COIN_SUPPLY_KEY_SUFFIX); + } + + /** + * 전체 동전 사용량 반환 + * + * @return + */ + public String getTotalcoinUsage() { + return coinTemplate.opsForValue().get(TOTAL_COIN_USAGE_KEY_SUFFIX); + } + + /** + * 전체 동전 발행량 저장 + * + * @param coinSupply + */ + + public void saveTotalCoinSupply(String coinSupply) { + coinTemplate.opsForValue().set(TOTAL_COIN_SUPPLY_KEY_SUFFIX, coinSupply); + } + + /** + * 전체 동전 사용량 저장 + * + * @param coinUsage + */ + public void saveTotalCoinUsage(String coinUsage) { + coinTemplate.opsForValue().set(TOTAL_COIN_USAGE_KEY_SUFFIX, coinUsage); + } + + + /** + * 하루동안 유지되는 redis를 탐색하여 동전줍기를 했는지 검수 + * + * @param userId + * @return + */ + public boolean isCoinCollected(String userId) { + Boolean isCollected = coinTemplate.hasKey(userId + COIN_COLLECT_KEY_SUFFIX); + return Objects.nonNull(isCollected) && isCollected; + } + + /** + * 한 달 동안 동전 줍기를 행한 횟수 반환 + * + * @param userId + * @return + */ + public String getCoinCollectionCount(String userId) { + String key = userId + COIN_COLLECT_COUNT_KEY_SUFFIX; + return coinTemplate.opsForValue().get(key); + } + + /** + * 하루동안 유지되는 코인 줍기 + *

+ * 현재 시간부터 자정까지의 남은 시간을 초로 계산하여 expireTime 으로 저장합니다. + * + * @param userId + */ + public void collectCoin(String userId) { + String key = userId + COIN_COLLECT_KEY_SUFFIX; + coinTemplate.opsForValue().set(key, "collected"); + + LocalDateTime todayEnd = LocalDate.now().atStartOfDay().plusDays(1); + LocalDateTime now = LocalDateTime.now(); + Duration duration = Duration.between(now, todayEnd); + + long expireTime = duration.getSeconds(); + coinTemplate.expire(key, expireTime, TimeUnit.SECONDS); + } + + /** + * 한 달 동안 유지되는 코인 줍기 + *

+ * 값이 존재한다면 횟수 증가, 없다면 1로 설정 후 당월의 마지막을 expire 로 설정 + * + * @param userId + */ + public void addCoinCollectionCount(String userId) { + String key = userId + COIN_COLLECT_COUNT_KEY_SUFFIX; + Long currentCount = coinTemplate.opsForValue().increment(key, 1); + + if (currentCount == 1) { + LocalDate today = LocalDate.now(); + LocalDate lastDayOfMonth = today.with(TemporalAdjusters.lastDayOfMonth()); + LocalDateTime endOfMonth = lastDayOfMonth.atTime(23, 59, 59); + LocalDateTime now = LocalDateTime.now(); + + Duration between = Duration.between(now, endOfMonth); + long expireTime = between.getSeconds(); + coinTemplate.expire(key, expireTime, TimeUnit.SECONDS); + } + } +} diff --git a/backend/src/main/java/org/ftclub/cabinet/item/repository/ItemRepository.java b/backend/src/main/java/org/ftclub/cabinet/item/repository/ItemRepository.java new file mode 100644 index 000000000..6c2f95377 --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/item/repository/ItemRepository.java @@ -0,0 +1,35 @@ +package org.ftclub.cabinet.item.repository; + +import java.util.List; +import java.util.Optional; +import org.ftclub.cabinet.item.domain.Item; +import org.ftclub.cabinet.item.domain.Sku; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +@Repository +public interface ItemRepository extends JpaRepository { + + @Query("SELECT i " + + "FROM Item i " + + "WHERE i.price >= 0") + List findAllByPricePositive(); + + @Query("SELECT i " + + "FROM Item i " + + "WHERE i.price < 0") + List findAllByPriceNegative(); + + /** + * SKU (상품고유번호)로 상품 조회 + * + * @param sku + * @return + */ + @Query("SELECT i " + + "FROM Item i " + + "WHERE i.sku = :sku") + Optional findBySku(@Param("sku") Sku sku); +} diff --git a/backend/src/main/java/org/ftclub/cabinet/item/repository/SectionAlarmRepository.java b/backend/src/main/java/org/ftclub/cabinet/item/repository/SectionAlarmRepository.java new file mode 100644 index 000000000..1077be50f --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/item/repository/SectionAlarmRepository.java @@ -0,0 +1,39 @@ +package org.ftclub.cabinet.item.repository; + +import java.time.LocalDateTime; +import java.util.List; +import org.ftclub.cabinet.item.domain.SectionAlarm; +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +@Repository +public interface SectionAlarmRepository extends JpaRepository { + + + @EntityGraph(attributePaths = {"cabinetPlace"}) + List findAllByAlarmedAtIsNull(); + + @Modifying(flushAutomatically = true, clearAutomatically = true) + @Query("UPDATE SectionAlarm s " + + "SET s.alarmedAt = :date " + + "WHERE s.id IN (:ids)") + void updateAlarmedAtBulk(@Param("ids") List ids, @Param("date") LocalDateTime date); + + @Query("SELECT s " + + "FROM SectionAlarm s " + + "JOIN FETCH s.cabinetPlace " + + "WHERE s.userId = :userId " + + "AND s.alarmedAt IS NULL " + + "AND s.cabinetPlaceId in (" + + " SELECT cp FROM CabinetPlace cp " + + " WHERE cp.location.building = :building " + + " AND cp.location.floor = :floor)") + List findAllByUserIdAndCabinetPlaceAndAlarmedAtIsNull( + @Param("userId") Long userId, + @Param("building") String building, + @Param("floor") Integer floor); +} diff --git a/backend/src/main/java/org/ftclub/cabinet/item/service/ItemCommandService.java b/backend/src/main/java/org/ftclub/cabinet/item/service/ItemCommandService.java new file mode 100644 index 000000000..360ba83b4 --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/item/service/ItemCommandService.java @@ -0,0 +1,24 @@ +package org.ftclub.cabinet.item.service; + +import lombok.RequiredArgsConstructor; +import org.ftclub.cabinet.item.domain.Item; +import org.ftclub.cabinet.item.domain.ItemType; +import org.ftclub.cabinet.item.domain.Sku; +import org.ftclub.cabinet.item.repository.ItemRepository; +import org.ftclub.cabinet.log.LogLevel; +import org.ftclub.cabinet.log.Logging; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional +@Logging(level = LogLevel.DEBUG) +public class ItemCommandService { + + private final ItemRepository itemRepository; + + public void createItem(Integer price, Sku sku, ItemType type) { + itemRepository.save(Item.of(price, sku, type)); + } +} diff --git a/backend/src/main/java/org/ftclub/cabinet/item/service/ItemFacadeService.java b/backend/src/main/java/org/ftclub/cabinet/item/service/ItemFacadeService.java new file mode 100644 index 000000000..90ffd825e --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/item/service/ItemFacadeService.java @@ -0,0 +1,320 @@ +package org.ftclub.cabinet.item.service; + +import static java.util.stream.Collectors.groupingBy; +import static java.util.stream.Collectors.mapping; + +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ThreadLocalRandom; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.ftclub.cabinet.alarm.domain.AlarmItem; +import org.ftclub.cabinet.alarm.domain.ExtensionItem; +import org.ftclub.cabinet.alarm.domain.ItemUsage; +import org.ftclub.cabinet.alarm.domain.PenaltyItem; +import org.ftclub.cabinet.alarm.domain.SwapItem; +import org.ftclub.cabinet.cabinet.domain.CabinetPlace; +import org.ftclub.cabinet.cabinet.service.CabinetQueryService; +import org.ftclub.cabinet.dto.CoinCollectionRewardResponseDto; +import org.ftclub.cabinet.dto.CoinHistoryDto; +import org.ftclub.cabinet.dto.CoinHistoryPaginationDto; +import org.ftclub.cabinet.dto.CoinMonthlyCollectionDto; +import org.ftclub.cabinet.dto.ItemDetailsDto; +import org.ftclub.cabinet.dto.ItemDto; +import org.ftclub.cabinet.dto.ItemHistoryDto; +import org.ftclub.cabinet.dto.ItemHistoryPaginationDto; +import org.ftclub.cabinet.dto.ItemStoreDto; +import org.ftclub.cabinet.dto.ItemStoreResponseDto; +import org.ftclub.cabinet.dto.ItemUseRequestDto; +import org.ftclub.cabinet.dto.MyItemResponseDto; +import org.ftclub.cabinet.dto.UserBlackHoleEvent; +import org.ftclub.cabinet.dto.UserSessionDto; +import org.ftclub.cabinet.exception.ExceptionStatus; +import org.ftclub.cabinet.item.domain.CoinHistoryType; +import org.ftclub.cabinet.item.domain.Item; +import org.ftclub.cabinet.item.domain.ItemHistory; +import org.ftclub.cabinet.item.domain.ItemType; +import org.ftclub.cabinet.item.domain.Sku; +import org.ftclub.cabinet.log.LogLevel; +import org.ftclub.cabinet.log.Logging; +import org.ftclub.cabinet.mapper.ItemMapper; +import org.ftclub.cabinet.user.domain.User; +import org.ftclub.cabinet.user.service.UserQueryService; +import org.ftclub.cabinet.utils.lock.LockUtil; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + + +@Service +@RequiredArgsConstructor +@Logging(level = LogLevel.DEBUG) +public class ItemFacadeService { + + private final ItemQueryService itemQueryService; + private final ItemHistoryQueryService itemHistoryQueryService; + private final ItemHistoryCommandService itemHistoryCommandService; + private final ItemRedisService itemRedisService; + private final UserQueryService userQueryService; + private final SectionAlarmCommandService sectionAlarmCommandService; + private final CabinetQueryService cabinetQueryService; + private final ItemMapper itemMapper; + private final ItemPolicyService itemPolicyService; + private final ApplicationEventPublisher eventPublisher; + + + /** + * 모든 아이템 리스트 반환 + * + * @return 전체 아이템 리스트 + */ + @Transactional + public ItemStoreResponseDto getAllItems() { + List allItems = itemQueryService.getAllItems(); + Map> itemMap = allItems.stream() + .filter(item -> item.getPrice() < 0) + .collect(groupingBy(Item::getType, + mapping(itemMapper::toItemDetailsDto, Collectors.toList()))); + List result = itemMap.entrySet().stream() + .map(entry -> { + ItemStoreDto itemStoreDto = itemMapper.toItemStoreDto(entry.getKey(), + entry.getValue()); + itemStoreDto.sortBySkuASC(); + return itemStoreDto; + }) + .collect(Collectors.toList()); + return new ItemStoreResponseDto(result); + } + + /** + * 유저의 보유 아이템 반환 + * + * @param user 유저 정보 + * @return 유저의 보유 아이템 + */ + @Transactional(readOnly = true) + public MyItemResponseDto getMyItems(UserSessionDto user) { + List userItemHistories = itemHistoryQueryService.findAllItemHistoryByUser( + user.getUserId()); + + Map> itemMap = userItemHistories.stream() + .map(ItemHistory::getItem) + .filter(item -> item.getPrice() < 0) + .collect(groupingBy(Item::getType, + mapping(itemMapper::toItemDto, Collectors.toList()))); + + List extensionItems = itemMap.getOrDefault(ItemType.EXTENSION, + Collections.emptyList()); + List swapItems = itemMap.getOrDefault(ItemType.SWAP, Collections.emptyList()); + List alarmItems = itemMap.getOrDefault(ItemType.ALARM, Collections.emptyList()); + List penaltyItems = itemMap.getOrDefault(ItemType.PENALTY, + Collections.emptyList()); + + return itemMapper.toMyItemResponseDto(extensionItems, swapItems, alarmItems, penaltyItems); + } + + + @Transactional(readOnly = true) + public ItemHistoryPaginationDto getItemHistory(Long userId, Pageable pageable) { + Page itemHistories = + itemHistoryQueryService.findItemHistoryWithItem(userId, pageable); + List result = itemHistories.stream() + .map(ih -> itemMapper.toItemHistoryDto(ih, itemMapper.toItemDto(ih.getItem()))) + .collect(Collectors.toList()); + return itemMapper.toItemHistoryPaginationDto(result, itemHistories.getTotalElements()); + } + + @Transactional(readOnly = true) + public CoinHistoryPaginationDto getCoinHistory(Long userId, CoinHistoryType type, + Pageable pageable) { + + Set items = new HashSet<>(); + if (type.equals(CoinHistoryType.EARN) || type.equals(CoinHistoryType.ALL)) { + items.addAll(itemQueryService.getEarnItemIds()); + } + if (type.equals(CoinHistoryType.USE) || type.equals(CoinHistoryType.ALL)) { + items.addAll(itemQueryService.getUseItemIds()); + } + List itemIds = items.stream().map(Item::getId).collect(Collectors.toList()); + Page coinHistories = + itemHistoryQueryService.findCoinHistory(userId, pageable, itemIds); + + Map itemMap = items.stream() + .collect(Collectors.toMap(Item::getId, item -> item)); + List result = coinHistories.stream() + .map(ih -> itemMapper.toCoinHistoryDto(ih, itemMap.get(ih.getItemId()))) + .sorted(Comparator.comparing(CoinHistoryDto::getDate, Comparator.reverseOrder())) + .collect(Collectors.toList()); + return itemMapper.toCoinHistoryPaginationDto(result, coinHistories.getTotalElements()); + } + + /** + * itemRedisService 를 통해 동전 줍기 정보 생성 + * + * @param userId redis 의 고유 key 를 만들 userId + * @return 동전 줍기 정보 + */ + @Transactional(readOnly = true) + public CoinMonthlyCollectionDto getCoinCollectionCountInMonth(Long userId) { + Long coinCollectionCountInMonth = + itemRedisService.getCoinCollectionCountInMonth(userId); + boolean isCollectedInToday = itemRedisService.isCoinCollected(userId); + + return itemMapper.toCoinMonthlyCollectionDto(coinCollectionCountInMonth, + isCollectedInToday); + } + + /** + * 당일 중복해서 동전줍기를 요청했는지 검수 후 + *

+ * 당일 동전 줍기 체크 및 한 달 동전줍기 횟수 증가 + *

+ * 당일 동전 줍기 리워드 지급 및 지정된 출석 일수 달성 시 랜덤 리워드 지급 + * + * @param userId redis 의 고유 key 를 만들 userId + */ + @Transactional + public CoinCollectionRewardResponseDto collectCoinAndIssueReward(Long userId) { + + // 코인 줍기 횟수 (당일, 한 달) 갱신 + boolean isChecked = itemRedisService.isCoinCollected(userId); + itemPolicyService.verifyIsAlreadyCollectedCoin(isChecked); + itemRedisService.collectCoin(userId); + + // DB에 코인 저장 + Item coinCollect = itemQueryService.getBySku(Sku.COIN_COLLECT); + int reward = (int) (coinCollect.getPrice().longValue()); + itemHistoryCommandService.createItemHistory(userId, coinCollect.getId()); + + // 출석 일자에 따른 랜덤 리워드 지급 + Long coinCollectionCountInMonth = + itemRedisService.getCoinCollectionCountInMonth(userId); + if (itemPolicyService.isRewardable(coinCollectionCountInMonth)) { + ThreadLocalRandom random = ThreadLocalRandom.current(); + int randomPercentage = random.nextInt(100); + Sku coinSku = itemPolicyService.getRewardSku(randomPercentage); + Item coinReward = itemQueryService.getBySku(coinSku); + + itemHistoryCommandService.createItemHistory(userId, coinReward.getId()); + reward += coinReward.getPrice(); + } + + // Redis에 코인 변화량 저장 + saveCoinChangeOnRedis(userId, reward); + + return new CoinCollectionRewardResponseDto(reward); + } + + private void saveCoinChangeOnRedis(Long userId, final int reward) { + LockUtil.lockRedisCoin(userId, () -> { + // Redis에 유저 리워드 저장 + long coins = itemRedisService.getCoinCount(userId); + itemRedisService.saveCoinCount(userId, coins + reward); + + // Redis에 전체 코인 발행량 저장 + long totalCoinSupply = itemRedisService.getTotalCoinSupply(); + itemRedisService.saveTotalCoinSupply(totalCoinSupply + reward); + }); + } + + + /** + * 아이템 사용 + * + * @param userId 사용자 아이디 + * @param sku 아이템 sku + * @param data 아이템 사용 요청 데이터 + */ + @Transactional + public void useItem(Long userId, Sku sku, ItemUseRequestDto data) { + itemPolicyService.verifyDataFieldBySku(sku, data); + User user = userQueryService.getUser(userId); + if (user.isBlackholed()) { + // 이벤트를 발생시켰는데 동기로직이다..? + // TODO: 근데 그 이벤트가 뭘 하는지 이 코드 흐름에서는 알 수 없다..? + eventPublisher.publishEvent(UserBlackHoleEvent.of(user)); + } + Item item = itemQueryService.getBySku(sku); + List itemInInventory = + itemHistoryQueryService.findUnusedItemsInUserInventory(user.getId(), item.getId()); + ItemHistory oldestItemHistory = + itemPolicyService.verifyNotEmptyAndFindOldest(itemInInventory); + ItemUsage itemUsage = getItemUsage(userId, item, data); + + eventPublisher.publishEvent(itemUsage); + oldestItemHistory.updateUsedAt(); + } + + /** + * itemType 에 따른 구현체 반환 + * + * @param userId 사용자 아이디 + * @param item 아이템 + * @param data 아이템 사용 요청 데이터 + * @return 아이템 사용 구현체 + */ + private ItemUsage getItemUsage(Long userId, Item item, ItemUseRequestDto data) { + // 연장권, 이사권, 페널티 + if (item.getType().equals(ItemType.SWAP)) { + return new SwapItem(userId, data.getNewCabinetId()); + } + if (item.getType().equals(ItemType.EXTENSION)) { + return new ExtensionItem(userId, item.getSku().getDays()); + } + if (item.getType().equals(ItemType.PENALTY)) { + return new PenaltyItem(userId, item.getSku().getDays()); + } + if (item.getType().equals(ItemType.ALARM)) { + CabinetPlace cabinetPlaceInfo = cabinetQueryService.getCabinetPlaceInfoByLocation( + data.getBuilding(), data.getFloor(), data.getSection()); + return new AlarmItem(userId, cabinetPlaceInfo.getId()); + } + throw ExceptionStatus.NOT_FOUND_ITEM.asServiceException(); + } + + /** + * user가 아이템 구매 요청 + * + * @param userId 사용자 아이디 + * @param sku 아이템 sku + */ + @Transactional + public void purchaseItem(Long userId, Sku sku) { + // 유저가 블랙홀인지 확인 + User user = userQueryService.getUser(userId); + if (user.isBlackholed()) { + eventPublisher.publishEvent(UserBlackHoleEvent.of(user)); + } + + Item item = itemQueryService.getBySku(sku); + long price = item.getPrice(); + long userCoin = itemRedisService.getCoinCount(userId); + + // 아이템 Policy 검증 + itemPolicyService.verifyOnSale(price); + itemPolicyService.verifyIsAffordable(userCoin, price); + + // 아이템 구매 처리 + itemHistoryCommandService.createItemHistory(user.getId(), item.getId()); + + LockUtil.lockRedisCoin(userId, () -> { + // 코인 차감 + itemRedisService.saveCoinCount(userId, userCoin + price); + + // 전체 코인 사용량 저장 + long totalCoinUsage = itemRedisService.getTotalCoinUsage(); + itemRedisService.saveTotalCoinUsage(totalCoinUsage + price); + }); + } + + @Transactional + public void addSectionAlarm(Long userId, Long cabinetPlaceId) { + sectionAlarmCommandService.addSectionAlarm(userId, cabinetPlaceId); + } +} diff --git a/backend/src/main/java/org/ftclub/cabinet/item/service/ItemHistoryCommandService.java b/backend/src/main/java/org/ftclub/cabinet/item/service/ItemHistoryCommandService.java new file mode 100644 index 000000000..e375a8518 --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/item/service/ItemHistoryCommandService.java @@ -0,0 +1,31 @@ +package org.ftclub.cabinet.item.service; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.ftclub.cabinet.item.domain.ItemHistory; +import org.ftclub.cabinet.item.repository.ItemHistoryRepository; +import org.ftclub.cabinet.log.LogLevel; +import org.ftclub.cabinet.log.Logging; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +@Logging(level = LogLevel.DEBUG) +public class ItemHistoryCommandService { + + private final ItemHistoryRepository itemHistoryRepository; + + public void createItemHistory(Long userId, Long itemId) { + ItemHistory itemHistory = ItemHistory.of(userId, itemId, null); + itemHistoryRepository.save(itemHistory); + } + + public void createItemHistories(List userIds, Long itemId, LocalDateTime usedAt) { + List itemHistories = userIds.stream() + .map(userId -> ItemHistory.of(userId, itemId, usedAt)) + .collect(Collectors.toList()); + itemHistoryRepository.saveAll(itemHistories); + } +} diff --git a/backend/src/main/java/org/ftclub/cabinet/item/service/ItemHistoryQueryService.java b/backend/src/main/java/org/ftclub/cabinet/item/service/ItemHistoryQueryService.java new file mode 100644 index 000000000..4409404d6 --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/item/service/ItemHistoryQueryService.java @@ -0,0 +1,55 @@ +package org.ftclub.cabinet.item.service; + +import java.time.LocalDate; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.ftclub.cabinet.item.domain.ItemHistory; +import org.ftclub.cabinet.item.repository.ItemHistoryRepository; +import org.ftclub.cabinet.log.LogLevel; +import org.ftclub.cabinet.log.Logging; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +@Logging(level = LogLevel.DEBUG) +public class ItemHistoryQueryService { + + private final ItemHistoryRepository itemHistoryRepository; + + public List findAllItemHistoryByUser(Long userId) { + return itemHistoryRepository.getAllUnusedItemHistoryByUser(userId); + } + + public Page findItemHistoryWithItem(Long userId, Pageable pageable) { + return itemHistoryRepository.findAllByUserIdOnMinusPriceItemsWithSubQuery(userId, pageable); + } + + public Page findItemHistoriesByUserIdWithItem(Long userId, Pageable pageable) { + return itemHistoryRepository.findAllByUserIdOrderByPurchaseAtDesc(userId, pageable); + } + + public Page findCoinHistory(Long userId, Pageable pageable, List itemIds) { + return itemHistoryRepository.findAllByUserIdAndItemIdIn(userId, pageable, itemIds); + } + + public List findUnusedItemsInUserInventory(Long userId, Long itemId) { + return itemHistoryRepository.findAllByUserIdAndItemIdAndUsedAtIsNull(userId, itemId); + } + + public int findPurchaseCountByItemId(Long itemId) { + return itemHistoryRepository.getCountByItemIds(itemId); + } + + public List findUsedCoinHistoryBetween( + LocalDate startDate, + LocalDate endDate) { + return itemHistoryRepository.findAllUsedAtIsNotNullBetween(startDate, endDate); + } + + public List findCoinCollectedInfoByMonth(Long itemId, Integer year, + Integer month) { + return itemHistoryRepository.findCoinCollectInfoByIdAtYearAndMonth(itemId, year, month); + } +} diff --git a/backend/src/main/java/org/ftclub/cabinet/item/service/ItemPolicyService.java b/backend/src/main/java/org/ftclub/cabinet/item/service/ItemPolicyService.java new file mode 100644 index 000000000..f27d4bf75 --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/item/service/ItemPolicyService.java @@ -0,0 +1,67 @@ +package org.ftclub.cabinet.item.service; + +import java.util.Comparator; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.ftclub.cabinet.dto.ItemUseRequestDto; +import org.ftclub.cabinet.exception.ExceptionStatus; +import org.ftclub.cabinet.item.domain.ItemHistory; +import org.ftclub.cabinet.item.domain.Sku; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class ItemPolicyService { + + private static final Long REWARD_COUNT = 20L; + + public void verifyIsAffordable(long userCoin, long itemPrice) { + if (userCoin <= 0 || userCoin < -itemPrice) { + throw ExceptionStatus.NOT_ENOUGH_COIN.asServiceException(); + } + } + + public void verifyIsAlreadyCollectedCoin(boolean isChecked) { + if (isChecked) { + throw ExceptionStatus.COIN_COLLECTION_ALREADY_EXIST.asServiceException(); + } + } + + public ItemHistory verifyNotEmptyAndFindOldest(List itemInInventory) { + return itemInInventory.stream() + .min(Comparator.comparing(ItemHistory::getPurchaseAt)) + .orElseThrow(ExceptionStatus.ITEM_NOT_OWNED::asServiceException); + } + + public void verifyOnSale(long price) { + if (price >= 0) { + throw ExceptionStatus.ITEM_NOT_ON_SALE.asServiceException(); + } + } + + public void verifyDataFieldBySku(Sku sku, ItemUseRequestDto data) { + if (sku.equals(Sku.ALARM) && (data.getBuilding() == null || data.getFloor() == null + || data.getSection() == null)) { + throw ExceptionStatus.INVALID_ITEM_USE_REQUEST.asServiceException(); + } + if (sku.equals(Sku.SWAP) && data.getNewCabinetId() == null) { + throw ExceptionStatus.INVALID_ITEM_USE_REQUEST.asServiceException(); + } + } + + public boolean isRewardable(Long monthlyCoinCount) { + return monthlyCoinCount.equals(REWARD_COUNT); + } + + public Sku getRewardSku(int randomPercentage) { + if (randomPercentage < 50) { + return Sku.COIN_REWARD_200; + } else if (randomPercentage < 80) { + return Sku.COIN_REWARD_500; + } else if (randomPercentage < 95) { + return Sku.COIN_REWARD_1000; + } else { + return Sku.COIN_REWARD_2000; + } + } +} diff --git a/backend/src/main/java/org/ftclub/cabinet/item/service/ItemQueryService.java b/backend/src/main/java/org/ftclub/cabinet/item/service/ItemQueryService.java new file mode 100644 index 000000000..3a746a767 --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/item/service/ItemQueryService.java @@ -0,0 +1,37 @@ +package org.ftclub.cabinet.item.service; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.ftclub.cabinet.exception.ExceptionStatus; +import org.ftclub.cabinet.item.domain.Item; +import org.ftclub.cabinet.item.domain.Sku; +import org.ftclub.cabinet.item.repository.ItemRepository; +import org.ftclub.cabinet.log.LogLevel; +import org.ftclub.cabinet.log.Logging; +import org.springframework.stereotype.Service; + + +@Service +@RequiredArgsConstructor +@Logging(level = LogLevel.DEBUG) +public class ItemQueryService { + + private final ItemRepository itemRepository; + + public List getAllItems() { + return itemRepository.findAll(); + } + + public List getEarnItemIds() { + return itemRepository.findAllByPricePositive(); + } + + public List getUseItemIds() { + return itemRepository.findAllByPriceNegative(); + } + + public Item getBySku(Sku sku) { + return itemRepository.findBySku(sku) + .orElseThrow(ExceptionStatus.NOT_FOUND_ITEM::asServiceException); + } +} diff --git a/backend/src/main/java/org/ftclub/cabinet/item/service/ItemRedisService.java b/backend/src/main/java/org/ftclub/cabinet/item/service/ItemRedisService.java new file mode 100644 index 000000000..b7487a74b --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/item/service/ItemRedisService.java @@ -0,0 +1,120 @@ +package org.ftclub.cabinet.item.service; + +import lombok.RequiredArgsConstructor; +import org.ftclub.cabinet.item.repository.ItemHistoryRepository; +import org.ftclub.cabinet.item.repository.ItemRedis; +import org.ftclub.cabinet.log.LogLevel; +import org.ftclub.cabinet.log.Logging; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +@Logging(level = LogLevel.DEBUG) +public class ItemRedisService { + + private final ItemRedis itemRedis; + private final ItemHistoryRepository itemHistoryRepository; + + public long getCoinCount(Long userId) { + String userIdString = userId.toString(); + String coinCount = itemRedis.getCoinAmount(userIdString); + if (coinCount == null) { + long coin = itemHistoryRepository.findAllByUserId(userId).stream() + .mapToLong(ih -> ih.getItem().getPrice()) + .reduce(Long::sum).orElse(0L); + itemRedis.saveCoinAmount(userIdString, Long.toString(coin)); + return coin; + } + return Integer.parseInt(coinCount); + } + + public void saveCoinCount(Long userId, long coinCount) { + itemRedis.saveCoinAmount(userId.toString(), String.valueOf(coinCount)); + } + + public boolean isCoinCollected(Long userId) { + return itemRedis.isCoinCollected(userId.toString()); + } + + /** + * 전체 기간 동전 발행량 반환 + * + * @return 전체 기간 동전 발행량 + */ + public long getTotalCoinSupply() { + String totalCoinSupply = itemRedis.getTotalCoinSupply(); + if (totalCoinSupply == null) { + Long coin = itemHistoryRepository.getPriceSumOnPlusPriceItems(); + if (coin == null) { + coin = 0L; + } + itemRedis.saveTotalCoinSupply(Long.toString(coin)); + return coin; + } + return Long.parseLong(totalCoinSupply); + } + + /** + * 전체 기간 동전 사용량 반환 + * + * @return 전체 기간 동전 사용량 + */ + public long getTotalCoinUsage() { + String totalCoinUsage = itemRedis.getTotalcoinUsage(); + if (totalCoinUsage == null) { + Long coin = itemHistoryRepository.getPriceSumOnMinusPriceItems(); + if (coin == null) { + coin = 0L; + } + itemRedis.saveTotalCoinUsage(Long.toString(coin)); + return coin; + } + return Long.parseLong(totalCoinUsage); + } + + /** + * 전체 기간 동전 발행량 저장 + * + * @param coin 동전 발행량 + */ + public void saveTotalCoinSupply(long coin) { + itemRedis.saveTotalCoinSupply(String.valueOf(coin)); + } + + /** + * 전체 기간 동전 사용량 저장 + * + * @param coin 동전 사용량 + */ + public void saveTotalCoinUsage(long coin) { + itemRedis.saveTotalCoinUsage(String.valueOf(coin)); + } + + /** + * 당일 동전 줍기 처리 및 한 달 동안 유지되는 동전 줍기 횟수 증가 기능 + * + * @param userId 유저 아이디 + */ + public void collectCoin(Long userId) { + itemRedis.collectCoin(userId.toString()); + itemRedis.addCoinCollectionCount(userId.toString()); + } + + /** + * userId를 문자열로 변경하여 redis 내에서 조회 + *

+ * 존재하지 않는다면 캐싱 처리 없이 0을 반환, 존재한다면 Long 으로 변환하여 반환합니다. + * + * @param userId 유저 아이디 + * @return 동전 줍기 횟수 + */ + public Long getCoinCollectionCountInMonth(Long userId) { + String userIdToString = userId.toString(); + String coinCollectionCount = itemRedis.getCoinCollectionCount(userIdToString); + + if (coinCollectionCount == null) { + return 0L; + } + return Long.parseLong(coinCollectionCount); + } +} diff --git a/backend/src/main/java/org/ftclub/cabinet/item/service/SectionAlarmCommandService.java b/backend/src/main/java/org/ftclub/cabinet/item/service/SectionAlarmCommandService.java new file mode 100644 index 000000000..e6772ad8b --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/item/service/SectionAlarmCommandService.java @@ -0,0 +1,28 @@ +package org.ftclub.cabinet.item.service; + +import java.time.LocalDateTime; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.ftclub.cabinet.item.domain.SectionAlarm; +import org.ftclub.cabinet.item.repository.SectionAlarmRepository; +import org.ftclub.cabinet.log.LogLevel; +import org.ftclub.cabinet.log.Logging; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +@Logging(level = LogLevel.DEBUG) +public class SectionAlarmCommandService { + + private final SectionAlarmRepository sectionAlarmRepository; + + + public void addSectionAlarm(Long userId, Long cabinetPlaceId) { + SectionAlarm sectionAlarm = SectionAlarm.of(userId, cabinetPlaceId); + sectionAlarmRepository.save(sectionAlarm); + } + + public void updateAlarmSend(List sectionAlarmIds, LocalDateTime sentDate) { + sectionAlarmRepository.updateAlarmedAtBulk(sectionAlarmIds, sentDate); + } +} diff --git a/backend/src/main/java/org/ftclub/cabinet/item/service/SectionAlarmQueryService.java b/backend/src/main/java/org/ftclub/cabinet/item/service/SectionAlarmQueryService.java new file mode 100644 index 000000000..131370b8c --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/item/service/SectionAlarmQueryService.java @@ -0,0 +1,27 @@ +package org.ftclub.cabinet.item.service; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.ftclub.cabinet.item.domain.SectionAlarm; +import org.ftclub.cabinet.item.repository.SectionAlarmRepository; +import org.ftclub.cabinet.log.LogLevel; +import org.ftclub.cabinet.log.Logging; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +@Logging(level = LogLevel.DEBUG) +public class SectionAlarmQueryService { + + private final SectionAlarmRepository sectionAlarmRepository; + + + public List getUnsentAlarms() { + return sectionAlarmRepository.findAllByAlarmedAtIsNull(); + } + + public List getUnsentAlarm(Long userId, String building, Integer floor) { + return sectionAlarmRepository.findAllByUserIdAndCabinetPlaceAndAlarmedAtIsNull( + userId, building, floor); + } +} diff --git a/backend/src/main/java/org/ftclub/cabinet/lent/domain/LentPolicyStatus.java b/backend/src/main/java/org/ftclub/cabinet/lent/domain/LentPolicyStatus.java index 2cc8af857..947fd636b 100644 --- a/backend/src/main/java/org/ftclub/cabinet/lent/domain/LentPolicyStatus.java +++ b/backend/src/main/java/org/ftclub/cabinet/lent/domain/LentPolicyStatus.java @@ -3,8 +3,7 @@ /** * 대여 상태 */ -public enum -LentPolicyStatus { +public enum LentPolicyStatus { /** * 빌릴 수 있음 */ @@ -56,6 +55,8 @@ BLACKHOLED_USER, SWAP_EXPIREDAT_IMMINENT, INVALID_LENT_TYPE, INVALID_ARGUMENT, - INVALID_EXPIREDAT, SWAP_SAME_CABINET, SWAP_LIMIT_EXCEEDED, LENT_NOT_CLUB, + INVALID_EXPIREDAT, SWAP_SAME_CABINET, LENT_NOT_CLUB, + + SWAP_LIMIT_EXCEEDED } \ No newline at end of file diff --git a/backend/src/main/java/org/ftclub/cabinet/lent/repository/LentRedis.java b/backend/src/main/java/org/ftclub/cabinet/lent/repository/LentRedis.java index c335511ca..d623372c0 100644 --- a/backend/src/main/java/org/ftclub/cabinet/lent/repository/LentRedis.java +++ b/backend/src/main/java/org/ftclub/cabinet/lent/repository/LentRedis.java @@ -28,14 +28,18 @@ public class LentRedis { private static final String SHADOW_KEY_SUFFIX = ":shadow"; private static final String CABINET_KEY_SUFFIX = ":cabinetSession"; private static final String VALUE_KEY_SUFFIX = ":userSession"; + private static final String PREVIOUS_USER_SUFFIX = ":previousUser"; + private static final String PREVIOUS_ENDED_AT_SUFFIX = ":previousEndedAt"; private static final String SWAP_KEY_SUFFIX = ":swap"; private final HashOperations shareCabinetTemplate; private final ValueOperations userCabinetTemplate; private final RedisTemplate shadowKeyTemplate; //조금 더 많은 기능을 지원 - private final ValueOperations previousUserTemplate; // 조회랑 생성 한정 기능 + + private final ValueOperations previousTemplate; // 조회랑 생성 한정 기능 + private final RedisTemplate swapTemplate; private final CabinetProperties cabinetProperties; @@ -44,13 +48,13 @@ public class LentRedis { public LentRedis(RedisTemplate valueHashRedisTemplate, RedisTemplate valueRedisTemplate, RedisTemplate shadowKeyTemplate, - RedisTemplate previousUserTemplate, + RedisTemplate previousTemplate, RedisTemplate swapTemplate, CabinetProperties cabinetProperties) { this.userCabinetTemplate = valueRedisTemplate.opsForValue(); this.shareCabinetTemplate = valueHashRedisTemplate.opsForHash(); this.shadowKeyTemplate = shadowKeyTemplate; - this.previousUserTemplate = previousUserTemplate.opsForValue(); + this.previousTemplate = previousTemplate.opsForValue(); this.swapTemplate = swapTemplate; this.cabinetProperties = cabinetProperties; } @@ -238,7 +242,7 @@ public LocalDateTime getCabinetExpiredAt(String cabinetId) { * @param userName 유저 이름 */ public void setPreviousUserName(String cabinetId, String userName) { - previousUserTemplate.set(cabinetId + PREVIOUS_USER_SUFFIX, userName); + previousTemplate.set(cabinetId + PREVIOUS_USER_SUFFIX, userName); } /** @@ -248,30 +252,38 @@ public void setPreviousUserName(String cabinetId, String userName) { * @return 유저 이름 */ public String getPreviousUserName(String cabinetId) { - return previousUserTemplate.get(cabinetId + PREVIOUS_USER_SUFFIX); + return previousTemplate.get(cabinetId + PREVIOUS_USER_SUFFIX); } - /*---------------------------------------- Swap -----------------------------------------*/ - /** - * swap 하려는 유저가 이전에 swap 한 이력의 여부를 조회합니다. + * 특정 사물함에 대한 이전 대여 종료 시각을 설정합니다. * - * @param userId 유저 ID - * @return true or false + * @param cabinetId 사물함 id + * @param endedAt 종료 시각 */ - public boolean isExistPreviousSwap(String userId) { - Boolean isExist = swapTemplate.hasKey(userId + SWAP_KEY_SUFFIX); - return Objects.nonNull(isExist) && isExist; + public void setPreviousEndedAt(String cabinetId, String endedAt) { + previousTemplate.set(cabinetId + PREVIOUS_ENDED_AT_SUFFIX, endedAt); } /** - * 유저가 swap 가능한 시각을 조회합니다. + * 특정 사물함에 대한 이전 대여 종료 시각을 가져옵니다. * - * @param userId 유저 ID - * @return swap 가능한 시각 + * @param cabinetId 사물함 id + * @return 종료 시각 */ - public LocalDateTime getSwapExpiredTime(String userId) { - if (!this.isExistPreviousSwap(userId)) { + public String getPreviousEndedAt(String cabinetId) { + return previousTemplate.get(cabinetId + PREVIOUS_ENDED_AT_SUFFIX); + } + + /*-----------------------------------SWAP-----------------------------------*/ + + public boolean isExistSwapRecord(String userId) { + Boolean isExist = swapTemplate.hasKey(userId + SWAP_KEY_SUFFIX); + return Objects.nonNull(isExist) && isExist; + } + + public LocalDateTime getSwapExpiredAt(String userId) { + if (!this.isExistSwapRecord(userId)) { return null; } String swapKey = userId + SWAP_KEY_SUFFIX; @@ -280,11 +292,6 @@ public LocalDateTime getSwapExpiredTime(String userId) { return LocalDateTime.now().plusSeconds(expire); } - /** - * swap 하는 유저의 swap 이력을 저장합니다. 기한을 설정합니다 - * - * @param userId 유저 ID - */ public void setSwap(String userId) { final String swapKey = userId + SWAP_KEY_SUFFIX; swapTemplate.opsForValue().set(swapKey, USER_SWAPPED); diff --git a/backend/src/main/java/org/ftclub/cabinet/lent/repository/LentRepository.java b/backend/src/main/java/org/ftclub/cabinet/lent/repository/LentRepository.java index b4ba806bc..5063ad871 100644 --- a/backend/src/main/java/org/ftclub/cabinet/lent/repository/LentRepository.java +++ b/backend/src/main/java/org/ftclub/cabinet/lent/repository/LentRepository.java @@ -174,7 +174,8 @@ int countReturnFromStartDateToEndDate(@Param("startDate") LocalDateTime startDat + "LEFT JOIN FETCH lh.user u " + "LEFT JOIN FETCH lh.cabinet c " + "LEFT JOIN FETCH c.cabinetPlace cp " - + "WHERE lh.cabinetId = :cabinetId ", + + "WHERE lh.cabinetId = :cabinetId " + + "ORDER BY lh.startedAt DESC", countQuery = "SELECT count(lh) " + "FROM LentHistory lh " + "WHERE lh.cabinetId = :cabinetId ") @@ -255,8 +256,8 @@ List findAllByCabinetIdsEndedAtEqualDate(@Param("date") LocalDate d * cabinet 정보를 Join하여 가져옵니다. *

* - * @param userId - * @return + * @param userId 찾으려는 user id + * @return 반납하지 않은 {@link LentHistory}의 {@link Optional} */ @Query("SELECT lh " + "FROM LentHistory lh " @@ -265,6 +266,8 @@ List findAllByCabinetIdsEndedAtEqualDate(@Param("date") LocalDate d + "WHERE lh.userId = :userId AND lh.endedAt IS NULL") Optional findByUserIdAndEndedAtIsNullJoinCabinet(@Param("userId") Long userId); + Optional findFirstByCabinetIdOrderByEndedAtDesc(Long cabinetId); + /** * 특정 유저들의 아직 반납하지 않은 대여 기록을 가져옵니다. *

diff --git a/backend/src/main/java/org/ftclub/cabinet/lent/service/LentFacadeService.java b/backend/src/main/java/org/ftclub/cabinet/lent/service/LentFacadeService.java index 9050ea7a7..62a615afd 100644 --- a/backend/src/main/java/org/ftclub/cabinet/lent/service/LentFacadeService.java +++ b/backend/src/main/java/org/ftclub/cabinet/lent/service/LentFacadeService.java @@ -31,10 +31,13 @@ import org.ftclub.cabinet.mapper.LentMapper; import org.ftclub.cabinet.user.domain.BanHistory; import org.ftclub.cabinet.user.domain.BanType; +import org.ftclub.cabinet.user.domain.LentExtension; +import org.ftclub.cabinet.user.domain.LentExtensionType; import org.ftclub.cabinet.user.domain.User; import org.ftclub.cabinet.user.service.BanHistoryCommandService; import org.ftclub.cabinet.user.service.BanHistoryQueryService; import org.ftclub.cabinet.user.service.BanPolicyService; +import org.ftclub.cabinet.user.service.LentExtensionCommandService; import org.ftclub.cabinet.user.service.UserQueryService; import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.domain.Page; @@ -65,6 +68,7 @@ public class LentFacadeService { private final LentMapper lentMapper; private final CabinetMapper cabinetMapper; + private final LentExtensionCommandService lentExtensionCommandService; /** @@ -418,4 +422,25 @@ public void swapPrivateCabinet(Long userId, Long newCabinetId) { lentRedisService.setSwapRecord(userId); } + + /** + * 아이템 사용 로직 + *

+ * 연체중이지 않고, 대여중인 상태라면 일자만큼 연장 수치를 늘려줍니다. + *

+ * 살 때 발급시키고 쓸 때 사용하는게 맞나, 쓸 때 바로 발급시키고 사용하는게 맞나.. + * + * @param userId + * @param days + */ + @Transactional + public void plusExtensionDays(Long userId, Integer days) { + Cabinet cabinet = cabinetQueryService.getUserActiveCabinet(userId); + + LentExtension lentExtensionByItem = lentExtensionCommandService.createLentExtensionByItem( + userId, LentExtensionType.ALL, days); + List lentHistories = lentQueryService.findCabinetActiveLentHistories( + cabinet.getId()); + lentExtensionCommandService.useLentExtension(lentExtensionByItem, lentHistories); + } } diff --git a/backend/src/main/java/org/ftclub/cabinet/lent/service/LentPolicyService.java b/backend/src/main/java/org/ftclub/cabinet/lent/service/LentPolicyService.java index eeaa438f5..7fe2abbbb 100644 --- a/backend/src/main/java/org/ftclub/cabinet/lent/service/LentPolicyService.java +++ b/backend/src/main/java/org/ftclub/cabinet/lent/service/LentPolicyService.java @@ -63,11 +63,6 @@ private void handlePolicyStatus(LentPolicyStatus status, LocalDateTime policyDat DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")); throw new CustomExceptionStatus(ExceptionStatus.SHARE_CODE_TRIAL_EXCEEDED, unbannedAtString).asCustomServiceException(); - case SWAP_LIMIT_EXCEEDED: - unbannedAtString = policyDate.format( - DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")); - throw new CustomExceptionStatus(ExceptionStatus.SWAP_LIMIT_EXCEEDED, - unbannedAtString).asCustomServiceException(); case BLACKHOLED_USER: throw ExceptionStatus.BLACKHOLED_USER.asServiceException(); case PENDING_CABINET: @@ -80,6 +75,11 @@ private void handlePolicyStatus(LentPolicyStatus status, LocalDateTime policyDat throw ExceptionStatus.INVALID_ARGUMENT.asServiceException(); case SWAP_SAME_CABINET: throw ExceptionStatus.SWAP_SAME_CABINET.asServiceException(); + case SWAP_LIMIT_EXCEEDED: + unbannedAtString = policyDate.format( + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")); + throw new CustomExceptionStatus(ExceptionStatus.SWAP_LIMIT_EXCEEDED, + unbannedAtString).asCustomServiceException(); case NOT_USER: case INTERNAL_ERROR: default: diff --git a/backend/src/main/java/org/ftclub/cabinet/lent/service/LentQueryService.java b/backend/src/main/java/org/ftclub/cabinet/lent/service/LentQueryService.java index 442d123a2..3a594f1e5 100644 --- a/backend/src/main/java/org/ftclub/cabinet/lent/service/LentQueryService.java +++ b/backend/src/main/java/org/ftclub/cabinet/lent/service/LentQueryService.java @@ -193,7 +193,8 @@ public List findOverdueLentHistories(LocalDateTime now, Pageable pa * @param cabinetIds 찾으려는 cabinet id {@link List} * @return 기준 날짜보다 반납 기한이 나중인 대여 기록 {@link List} */ - public List findPendingLentHistoriesOnDate(LocalDate date, List cabinetIds) { + public List findAvailableLentHistoriesOnDate(LocalDate date, + List cabinetIds) { return lentRepository.findAllByCabinetIdsEndedAtEqualDate(date, cabinetIds); } diff --git a/backend/src/main/java/org/ftclub/cabinet/lent/service/LentRedisService.java b/backend/src/main/java/org/ftclub/cabinet/lent/service/LentRedisService.java index 1d019f73d..b65a4c2c0 100644 --- a/backend/src/main/java/org/ftclub/cabinet/lent/service/LentRedisService.java +++ b/backend/src/main/java/org/ftclub/cabinet/lent/service/LentRedisService.java @@ -1,9 +1,11 @@ package org.ftclub.cabinet.lent.service; import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.Comparator; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.ftclub.cabinet.config.CabinetProperties; @@ -225,31 +227,48 @@ public void setPreviousUserName(Long cabinetId, String userName) { } /** - * 정책 기한 이내에 swap 기록이 있는지 확인합니다. + * 사물함의 이전 대여 종료 시간을 가져옵니다. * - * @param userId 유저 이름 - * @return + * @param cabinetId 사물함 id + * @return 이전 대여 종료 시간 */ - public boolean isExistSwapRecord(Long userId) { - return lentRedis.isExistPreviousSwap(String.valueOf(userId)); + public LocalDateTime getPreviousEndedAt(Long cabinetId) { + LocalDateTime previousEndedAt; + String previousEndedAtString = lentRedis.getPreviousEndedAt(cabinetId.toString()); + if (Objects.isNull(previousEndedAtString)) { + Optional cabinetLastLentHistory = + lentRepository.findFirstByCabinetIdOrderByEndedAtDesc(cabinetId); + previousEndedAt = cabinetLastLentHistory.map(LentHistory::getEndedAt).orElse(null); + if (Objects.nonNull(previousEndedAt)) { + lentRedis.setPreviousEndedAt(cabinetId.toString(), previousEndedAt.toString()); + } + } else { + DateTimeFormatter dateFormatter = + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSS"); + previousEndedAt = LocalDateTime.parse(previousEndedAtString, dateFormatter); + } + return previousEndedAt; } /** - * swap에 성공한 유저를 등록합니다. + * 사물함의 이전 대여 종료 시간을 설정합니다. * - * @param userId swap 기능을 사용한 유저 이름 + * @param cabinetId 사물함 id + * @param endedAt 이전 대여 종료 시간 */ - public void setSwapRecord(Long userId) { - lentRedis.setSwap(String.valueOf(userId)); + public void setPreviousEndedAt(Long cabinetId, LocalDateTime endedAt) { + lentRedis.setPreviousEndedAt(cabinetId.toString(), endedAt.toString()); } - /** - * swap 기능이 사용가능한 시간을 리턴합니다. - * - * @param userId 유저 이름 - * @return 사용 가능한 시간 - */ public LocalDateTime getSwapExpiredAt(Long userId) { - return lentRedis.getSwapExpiredTime(String.valueOf(userId)); + return lentRedis.getSwapExpiredAt(String.valueOf(userId)); + } + + public boolean isExistSwapRecord(Long userId) { + return lentRedis.isExistSwapRecord(String.valueOf(userId)); + } + + public void setSwapRecord(Long userId) { + lentRedis.setSwap(String.valueOf(userId)); } } diff --git a/backend/src/main/java/org/ftclub/cabinet/mapper/CabinetMapper.java b/backend/src/main/java/org/ftclub/cabinet/mapper/CabinetMapper.java index da0075454..d2c61098d 100644 --- a/backend/src/main/java/org/ftclub/cabinet/mapper/CabinetMapper.java +++ b/backend/src/main/java/org/ftclub/cabinet/mapper/CabinetMapper.java @@ -1,7 +1,32 @@ package org.ftclub.cabinet.mapper; +import static org.mapstruct.NullValueMappingStrategy.RETURN_DEFAULT; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; import org.ftclub.cabinet.cabinet.domain.Cabinet; -import org.ftclub.cabinet.dto.*; +import org.ftclub.cabinet.dto.ActiveCabinetInfoDto; +import org.ftclub.cabinet.dto.ActiveCabinetInfoEntities; +import org.ftclub.cabinet.dto.BuildingFloorsDto; +import org.ftclub.cabinet.dto.CabinetDto; +import org.ftclub.cabinet.dto.CabinetFloorStatisticsResponseDto; +import org.ftclub.cabinet.dto.CabinetInfoPaginationDto; +import org.ftclub.cabinet.dto.CabinetInfoResponseDto; +import org.ftclub.cabinet.dto.CabinetPaginationDto; +import org.ftclub.cabinet.dto.CabinetPendingResponseDto; +import org.ftclub.cabinet.dto.CabinetPreviewDto; +import org.ftclub.cabinet.dto.CabinetSimpleDto; +import org.ftclub.cabinet.dto.CabinetSimplePaginationDto; +import org.ftclub.cabinet.dto.CabinetsPerSectionResponseDto; +import org.ftclub.cabinet.dto.LentDto; +import org.ftclub.cabinet.dto.LentsStatisticsResponseDto; +import org.ftclub.cabinet.dto.MyCabinetResponseDto; +import org.ftclub.cabinet.dto.OverdueUserCabinetDto; +import org.ftclub.cabinet.dto.OverdueUserCabinetPaginationDto; +import org.ftclub.cabinet.dto.UserBlockedInfoDto; +import org.ftclub.cabinet.dto.UserCabinetDto; +import org.ftclub.cabinet.dto.UserCabinetPaginationDto; import org.ftclub.cabinet.lent.domain.LentHistory; import org.ftclub.cabinet.user.domain.User; import org.mapstruct.Mapper; @@ -9,12 +34,6 @@ import org.mapstruct.factory.Mappers; import org.springframework.stereotype.Component; -import java.time.LocalDateTime; -import java.util.List; -import java.util.Map; - -import static org.mapstruct.NullValueMappingStrategy.RETURN_DEFAULT; - //@NullableMapper @Mapper(componentModel = "spring", nullValueMappingStrategy = RETURN_DEFAULT, @@ -34,7 +53,7 @@ public interface CabinetMapper { @Mapping(target = "cabinetId", source = "lentHistory.cabinetId") @Mapping(target = "location", source = "cabinet.cabinetPlace.location") OverdueUserCabinetDto toOverdueUserCabinetDto(LentHistory lentHistory, User user, - Cabinet cabinet, Long overdueDays); + Cabinet cabinet, Long overdueDays); UserCabinetDto toUserCabinetDto(UserBlockedInfoDto userInfo, CabinetDto cabinetInfo); @@ -46,40 +65,41 @@ OverdueUserCabinetDto toOverdueUserCabinetDto(LentHistory lentHistory, User user @Mapping(target = "userId", source = "lentHistory.userId") @Mapping(target = "location", source = "cabinet.cabinetPlace.location") ActiveCabinetInfoDto toActiveCabinetInfoDto(Cabinet cabinet, LentHistory lentHistory, - User user); + User user); @Mapping(target = "cabinet", source = "cabinet") @Mapping(target = "lentHistory", source = "lentHistory") @Mapping(target = "user", source = "user") ActiveCabinetInfoEntities toActiveCabinetInfoEntitiesDto(Cabinet cabinet, - LentHistory lentHistory, User user); + LentHistory lentHistory, User user); /*--------------------------------Wrapped DTO--------------------------------*/ //TO do : cabinetPlace러 바꾸기 CabinetsPerSectionResponseDto toCabinetsPerSectionResponseDto(String section, - List cabinets); + List cabinets, + boolean alarmRegistered); @Mapping(target = "cabinetId", source = "cabinet.id") @Mapping(target = "location", source = "cabinet.cabinetPlace.location") CabinetInfoResponseDto toCabinetInfoResponseDto(Cabinet cabinet, List lents, - LocalDateTime sessionExpiredAt); + LocalDateTime sessionExpiredAt); @Mapping(target = "totalLength", source = "totalLength") CabinetPaginationDto toCabinetPaginationDtoList(List result, - Long totalLength); + Long totalLength); OverdueUserCabinetPaginationDto toOverdueUserCabinetPaginationDto( List result, Long totalLength); UserCabinetPaginationDto toUserCabinetPaginationDto(List result, - Long totalLength); + Long totalLength); @Mapping(target = "cabinetId", source = "cabinet.id") @Mapping(target = "location", source = "cabinet.cabinetPlace.location") @Mapping(target = "shareCode", source = "sessionShareCode") MyCabinetResponseDto toMyCabinetResponseDto(Cabinet cabinet, List lents, - String sessionShareCode, LocalDateTime sessionExpiredAt, String previousUserName); + String sessionShareCode, LocalDateTime sessionExpiredAt, String previousUserName); @Mapping(target = "cabinetId", source = "cabinet.id") CabinetPreviewDto toCabinetPreviewDto(Cabinet cabinet, Integer userCount, String name); @@ -99,8 +119,8 @@ CabinetPendingResponseDto toCabinetPendingResponseDto( Map> cabinetInfoResponseDtos); CabinetFloorStatisticsResponseDto toCabinetFloorStatisticsResponseDto(Integer floor, - Integer total, Integer used, Integer overdue, Integer unused, Integer disabled); + Integer total, Integer used, Integer overdue, Integer unused, Integer disabled); LentsStatisticsResponseDto toLentsStatisticsResponseDto(LocalDateTime startDate, - LocalDateTime endDate, int lentStartCount, int lentEndCount); + LocalDateTime endDate, int lentStartCount, int lentEndCount); } diff --git a/backend/src/main/java/org/ftclub/cabinet/mapper/ItemMapper.java b/backend/src/main/java/org/ftclub/cabinet/mapper/ItemMapper.java new file mode 100644 index 000000000..71939ee98 --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/mapper/ItemMapper.java @@ -0,0 +1,93 @@ +package org.ftclub.cabinet.mapper; + +import static org.mapstruct.NullValueMappingStrategy.RETURN_DEFAULT; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import org.ftclub.cabinet.admin.dto.AdminItemHistoryDto; +import org.ftclub.cabinet.dto.CoinAmountDto; +import org.ftclub.cabinet.dto.CoinHistoryDto; +import org.ftclub.cabinet.dto.CoinHistoryPaginationDto; +import org.ftclub.cabinet.dto.CoinMonthlyCollectionDto; +import org.ftclub.cabinet.dto.ItemAssignResponseDto; +import org.ftclub.cabinet.dto.ItemDetailsDto; +import org.ftclub.cabinet.dto.ItemDto; +import org.ftclub.cabinet.dto.ItemHistoryDto; +import org.ftclub.cabinet.dto.ItemHistoryPaginationDto; +import org.ftclub.cabinet.dto.ItemPurchaseCountDto; +import org.ftclub.cabinet.dto.ItemStoreDto; +import org.ftclub.cabinet.dto.MyItemResponseDto; +import org.ftclub.cabinet.item.domain.Item; +import org.ftclub.cabinet.item.domain.ItemHistory; +import org.ftclub.cabinet.item.domain.ItemType; +import org.ftclub.cabinet.item.domain.Sku; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; +import org.springframework.stereotype.Component; + +@Mapper(componentModel = "spring", + nullValueMappingStrategy = RETURN_DEFAULT, + nullValueMapMappingStrategy = RETURN_DEFAULT, + nullValueIterableMappingStrategy = RETURN_DEFAULT) +@Component +public interface ItemMapper { + + ItemMapper INSTANCE = Mappers.getMapper(ItemMapper.class); + + @Mapping(target = "date", source = "itemHistory.purchaseAt") + @Mapping(target = "amount", source = "item.price") + @Mapping(target = "history", source = "item.type.name") + @Mapping(target = "itemDetails", source = "item.sku.details") + CoinHistoryDto toCoinHistoryDto(ItemHistory itemHistory, Item item); + + @Mapping(target = "itemSku", source = "item.sku") + @Mapping(target = "itemName", source = "item.type.name") + @Mapping(target = "itemType", source = "item.type") + @Mapping(target = "itemPrice", source = "item.price") + @Mapping(target = "itemDetails", source = "item.sku.details") + ItemDto toItemDto(Item item); + + @Mapping(target = "itemName", source = "item.type.name") + @Mapping(target = "itemDetails", source = "item.sku.details") + AdminItemHistoryDto toAdminItemHistoryDto(ItemHistory itemHistory, Item item); + + @Mapping(target = "itemSku", source = "item.sku") + @Mapping(target = "itemPrice", source = "item.price") + @Mapping(target = "itemDetails", source = "item.sku.details") + ItemDetailsDto toItemDetailsDto(Item item); + + @Mapping(target = "date", source = "itemHistory.usedAt") + ItemHistoryDto toItemHistoryDto(ItemHistory itemHistory, ItemDto itemDto); + + + MyItemResponseDto toMyItemResponseDto(List extensionItems, List swapItems, + List alarmItems, List penaltyItems); + + @Mapping(target = "itemName", source = "itemType.name") + @Mapping(target = "itemType", source = "itemType") + @Mapping(target = "description", source = "itemType.description") + ItemStoreDto toItemStoreDto(ItemType itemType, List items); + + ItemHistoryPaginationDto toItemHistoryPaginationDto(List result, + Long totalLength); + + CoinHistoryPaginationDto toCoinHistoryPaginationDto(List result, + Long totalLength); + + CoinMonthlyCollectionDto toCoinMonthlyCollectionDto(Long monthlyCoinCount, + boolean todayCoinCollection); + + @Mapping(target = "itemSku", source = "sku") + @Mapping(target = "itemDetails", source = "sku.details") + @Mapping(target = "itemName", source = "itemType.name") + ItemAssignResponseDto toItemAssignResponseDto(Sku sku, ItemType itemType, + LocalDateTime issuedDate); + + @Mapping(target = "itemName", source = "item.type.name") + @Mapping(target = "itemDetails", source = "item.sku.details") + ItemPurchaseCountDto toItemPurchaseCountDto(Item item, int userCount); + + CoinAmountDto toCoinAmountDto(LocalDate date, Long amount); +} diff --git a/backend/src/main/java/org/ftclub/cabinet/mapper/UserMapper.java b/backend/src/main/java/org/ftclub/cabinet/mapper/UserMapper.java index f1f7fdd4e..e4720701c 100644 --- a/backend/src/main/java/org/ftclub/cabinet/mapper/UserMapper.java +++ b/backend/src/main/java/org/ftclub/cabinet/mapper/UserMapper.java @@ -1,8 +1,20 @@ package org.ftclub.cabinet.mapper; +import static org.mapstruct.NullValueMappingStrategy.RETURN_DEFAULT; +import static org.mapstruct.NullValueMappingStrategy.RETURN_NULL; + +import java.util.List; import org.ftclub.cabinet.alarm.dto.AlarmTypeResponseDto; import org.ftclub.cabinet.cabinet.domain.Cabinet; -import org.ftclub.cabinet.dto.*; +import org.ftclub.cabinet.dto.BlockedUserPaginationDto; +import org.ftclub.cabinet.dto.ClubUserListDto; +import org.ftclub.cabinet.dto.LentExtensionPaginationDto; +import org.ftclub.cabinet.dto.LentExtensionResponseDto; +import org.ftclub.cabinet.dto.MyProfileResponseDto; +import org.ftclub.cabinet.dto.UserBlockedInfoDto; +import org.ftclub.cabinet.dto.UserProfileDto; +import org.ftclub.cabinet.dto.UserProfilePaginationDto; +import org.ftclub.cabinet.dto.UserSessionDto; import org.ftclub.cabinet.user.domain.BanHistory; import org.ftclub.cabinet.user.domain.LentExtension; import org.ftclub.cabinet.user.domain.User; @@ -10,11 +22,6 @@ import org.mapstruct.Mapping; import org.springframework.stereotype.Component; -import java.util.List; - -import static org.mapstruct.NullValueMappingStrategy.RETURN_DEFAULT; -import static org.mapstruct.NullValueMappingStrategy.RETURN_NULL; - //@NullableMapper @Mapper(componentModel = "spring", nullValueMappingStrategy = RETURN_NULL, @@ -34,15 +41,16 @@ public interface UserMapper { @Mapping(target = "userId", source = "user.userId") @Mapping(target = "name", source = "user.name") @Mapping(target = "cabinetId", source = "cabinet.id") - MyProfileResponseDto toMyProfileResponseDto(UserSessionDto user, Cabinet cabinet, - BanHistory banHistory, LentExtensionResponseDto lentExtensionResponseDto, - AlarmTypeResponseDto alarmTypes, boolean isDeviceTokenExpired); + MyProfileResponseDto toMyProfileResponseDto(UserSessionDto user, + Cabinet cabinet, BanHistory banHistory, + LentExtensionResponseDto lentExtensionResponseDto, + AlarmTypeResponseDto alarmTypes, boolean isDeviceTokenExpired, Long coins); BlockedUserPaginationDto toBlockedUserPaginationDto(List result, - Long totalLength); + Long totalLength); UserProfilePaginationDto toUserProfilePaginationDto(List result, - Long totalLength); + Long totalLength); ClubUserListDto toClubUserListDto(List result, Long totalLength); @@ -50,5 +58,5 @@ UserProfilePaginationDto toUserProfilePaginationDto(List result, LentExtensionResponseDto toLentExtensionResponseDto(LentExtension lentExtension); LentExtensionPaginationDto toLentExtensionPaginationDto(List result, - Long totalLength); + Long totalLength); } diff --git a/backend/src/main/java/org/ftclub/cabinet/presentation/controller/PresentationController.java b/backend/src/main/java/org/ftclub/cabinet/presentation/controller/PresentationController.java index 971665968..3bb1902e3 100644 --- a/backend/src/main/java/org/ftclub/cabinet/presentation/controller/PresentationController.java +++ b/backend/src/main/java/org/ftclub/cabinet/presentation/controller/PresentationController.java @@ -34,9 +34,9 @@ public class PresentationController { @PostMapping("/form") @AuthGuard(level = AuthLevel.USER_ONLY) public void createPresentationForm( - @UserSession UserSessionDto user, - @Valid @RequestBody PresentationFormRequestDto dto) { - presentationService.createPresentationFrom(user.getUserId(), dto); + @UserSession UserSessionDto user, + @Valid @RequestBody PresentationFormRequestDto dto) { + presentationService.createPresentationForm(user.getUserId(), dto); } @GetMapping("/form/invalid-date") @@ -47,17 +47,17 @@ public InvalidDateResponseDto getInvalidDate() { @GetMapping("") @AuthGuard(level = AuthLevel.USER_ONLY) public PresentationMainData getMainData( - @RequestParam(value = "pastFormCount") Integer pastFormCount, - @RequestParam(value = "upcomingFormCount") Integer upcomingFormCount) { + @RequestParam(value = "pastFormCount") Integer pastFormCount, + @RequestParam(value = "upcomingFormCount") Integer upcomingFormCount) { return presentationService.getPastAndUpcomingPresentations(pastFormCount, - upcomingFormCount); + upcomingFormCount); } @GetMapping("/schedule") public PresentationFormResponseDto getPresentationSchedule( - @RequestParam(value = "yearMonth") - @DateTimeFormat(pattern = "yyyy-MM") - YearMonth yearMonth) { + @RequestParam(value = "yearMonth") + @DateTimeFormat(pattern = "yyyy-MM") + YearMonth yearMonth) { return presentationService.getUserPresentationSchedule(yearMonth); } @@ -71,8 +71,8 @@ public PresentationFormResponseDto getPresentationSchedule( @GetMapping("/me/histories") @AuthGuard(level = AuthLevel.USER_ONLY) public PresentationMyPagePaginationDto getUserPresentation( - @UserSession UserSessionDto user, - Pageable pageable + @UserSession UserSessionDto user, + Pageable pageable ) { return presentationService.getUserPresentations(user.getUserId(), pageable); } diff --git a/backend/src/main/java/org/ftclub/cabinet/presentation/service/PresentationService.java b/backend/src/main/java/org/ftclub/cabinet/presentation/service/PresentationService.java index 4c08f928f..0f1457070 100644 --- a/backend/src/main/java/org/ftclub/cabinet/presentation/service/PresentationService.java +++ b/backend/src/main/java/org/ftclub/cabinet/presentation/service/PresentationService.java @@ -18,7 +18,6 @@ import org.ftclub.cabinet.exception.ExceptionStatus; import org.ftclub.cabinet.mapper.PresentationMapper; import org.ftclub.cabinet.presentation.domain.Presentation; -import org.ftclub.cabinet.presentation.domain.PresentationLocation; import org.ftclub.cabinet.presentation.domain.PresentationStatus; import org.ftclub.cabinet.presentation.repository.PresentationRepository; import org.ftclub.cabinet.user.domain.User; @@ -54,13 +53,13 @@ public class PresentationService { * @param dto 신청서 작성에 필요한 정보들 */ @Transactional - public void createPresentationFrom(Long userId, PresentationFormRequestDto dto) { + public void createPresentationForm(Long userId, PresentationFormRequestDto dto) { presentationPolicyService.verifyReservationDate(dto.getDateTime()); Presentation presentation = - Presentation.of(dto.getCategory(), dto.getDateTime(), - dto.getPresentationTime(), dto.getSubject(), dto.getSummary(), - dto.getDetail()); + Presentation.of(dto.getCategory(), dto.getDateTime(), + dto.getPresentationTime(), dto.getSubject(), dto.getSummary(), + dto.getDetail()); User user = userQueryService.getUser(userId); presentation.setUser(user); @@ -79,10 +78,10 @@ public InvalidDateResponseDto getInvalidDate() { LocalDateTime end = start.plusMonths(MAX_MONTH); List invalidDates = - presentationQueryService.getRegisteredPresentations(start, end) - .stream() - .map(Presentation::getDateTime) - .collect(Collectors.toList()); + presentationQueryService.getRegisteredPresentations(start, end) + .stream() + .map(Presentation::getDateTime) + .collect(Collectors.toList()); return new InvalidDateResponseDto(invalidDates); } @@ -98,16 +97,16 @@ public List getLatestPastPresentations(int count) { LocalDateTime limit = now.atStartOfDay(); LocalDateTime start = limit.minusYears(10); PageRequest pageRequest = PageRequest.of(DEFAULT_PAGE, count, - Sort.by(DATE_TIME).descending()); + Sort.by(DATE_TIME).descending()); List presentations = - presentationQueryService.getPresentationsBetweenWithPageRequest(start, limit, - pageRequest); + presentationQueryService.getPresentationsBetweenWithPageRequest(start, limit, + pageRequest); return presentations.stream() - .filter(presentation -> - presentation.getPresentationStatus().equals(PresentationStatus.DONE)) - .collect(Collectors.toList()); + .filter(presentation -> + presentation.getPresentationStatus().equals(PresentationStatus.DONE)) + .collect(Collectors.toList()); } /** @@ -121,15 +120,15 @@ public List getLatestUpcomingPresentations(int count) { LocalDateTime start = now.atStartOfDay(); LocalDateTime end = start.plusMonths(MAX_MONTH); PageRequest pageRequest = PageRequest.of(DEFAULT_PAGE, count, - Sort.by(DATE_TIME).ascending()); + Sort.by(DATE_TIME).ascending()); List presentations = presentationQueryService. - getPresentationsBetweenWithPageRequest(start, end, pageRequest); + getPresentationsBetweenWithPageRequest(start, end, pageRequest); return presentations.stream() - .filter(presentation -> - presentation.getPresentationStatus().equals(PresentationStatus.EXPECTED)) - .collect(Collectors.toList()); + .filter(presentation -> + presentation.getPresentationStatus().equals(PresentationStatus.EXPECTED)) + .collect(Collectors.toList()); } /** @@ -140,17 +139,17 @@ public List getLatestUpcomingPresentations(int count) { * @return */ public PresentationMainData getPastAndUpcomingPresentations( - int pastFormCount, int upcomingFormCount) { + int pastFormCount, int upcomingFormCount) { List pastPresentations = getLatestPastPresentations(pastFormCount); List upcomingPresentations = getLatestUpcomingPresentations( - upcomingFormCount); + upcomingFormCount); List past = pastPresentations.stream() - .map(presentationMapper::toPresentationFormDataDto) - .collect(Collectors.toList()); + .map(presentationMapper::toPresentationFormDataDto) + .collect(Collectors.toList()); List upcoming = upcomingPresentations.stream() - .map(presentationMapper::toPresentationFormDataDto) - .collect(Collectors.toList()); + .map(presentationMapper::toPresentationFormDataDto) + .collect(Collectors.toList()); return presentationMapper.toPresentationMainData(past, upcoming); } @@ -164,12 +163,13 @@ public PresentationMainData getPastAndUpcomingPresentations( public PresentationFormResponseDto getUserPresentationSchedule(YearMonth yearMonth) { List result = - presentationQueryService.getPresentationsByYearMonth(yearMonth) - .stream() - .filter(presentation -> - !presentation.getPresentationStatus().equals(PresentationStatus.CANCEL)) - .map(presentationMapper::toPresentationFormDataDto) - .collect(Collectors.toList()); + presentationQueryService.getPresentationsByYearMonth(yearMonth) + .stream() + .filter(presentation -> + !presentation.getPresentationStatus() + .equals(PresentationStatus.CANCEL)) + .map(presentationMapper::toPresentationFormDataDto) + .collect(Collectors.toList()); return new PresentationFormResponseDto(result); } @@ -177,10 +177,10 @@ public PresentationFormResponseDto getUserPresentationSchedule(YearMonth yearMon public PresentationFormResponseDto getAdminPresentationSchedule(YearMonth yearMonth) { List result = - presentationQueryService.getPresentationsByYearMonth(yearMonth) - .stream() - .map(presentationMapper::toPresentationFormDataDto) - .collect(Collectors.toList()); + presentationQueryService.getPresentationsByYearMonth(yearMonth) + .stream() + .map(presentationMapper::toPresentationFormDataDto) + .collect(Collectors.toList()); return new PresentationFormResponseDto(result); } @@ -199,8 +199,8 @@ public PresentationFormResponseDto getAdminPresentationSchedule(YearMonth yearMo @Transactional public void updatePresentationByFormId(Long formId, PresentationUpdateDto dto) { Presentation presentationToUpdate = - presentationRepository.findById(formId) - .orElseThrow(ExceptionStatus.INVALID_FORM_ID::asServiceException); + presentationRepository.findById(formId) + .orElseThrow(ExceptionStatus.INVALID_FORM_ID::asServiceException); //날짜 변경시에만 유효성 검증 if (!presentationToUpdate.getDateTime().isEqual(dto.getDateTime())) { presentationPolicyService.verifyReservationDate(dto.getDateTime()); @@ -218,11 +218,11 @@ public void updatePresentationByFormId(Long formId, PresentationUpdateDto dto) { */ public PresentationMyPagePaginationDto getUserPresentations(Long userId, Pageable pageable) { Page presentations = presentationQueryService.getPresentationsById(userId, - pageable); + pageable); List result = presentations.stream() - .map(presentationMapper::toPresentationMyPageDto) - .collect(Collectors.toList()); + .map(presentationMapper::toPresentationMyPageDto) + .collect(Collectors.toList()); return new PresentationMyPagePaginationDto(result, presentations.getTotalElements()); } diff --git a/backend/src/main/java/org/ftclub/cabinet/user/domain/BanHistory.java b/backend/src/main/java/org/ftclub/cabinet/user/domain/BanHistory.java index 9a5b32928..0e6b3b04e 100644 --- a/backend/src/main/java/org/ftclub/cabinet/user/domain/BanHistory.java +++ b/backend/src/main/java/org/ftclub/cabinet/user/domain/BanHistory.java @@ -15,7 +15,6 @@ import javax.persistence.ManyToOne; import javax.persistence.Table; import javax.validation.constraints.NotNull; - import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @@ -56,7 +55,7 @@ public class BanHistory { private User user; protected BanHistory(LocalDateTime bannedAt, LocalDateTime unbannedAt, BanType banType, - Long userId) { + Long userId) { this.bannedAt = bannedAt; this.unbannedAt = unbannedAt; this.banType = banType; @@ -64,10 +63,10 @@ protected BanHistory(LocalDateTime bannedAt, LocalDateTime unbannedAt, BanType b } public static BanHistory of(LocalDateTime bannedAt, LocalDateTime unbannedAt, BanType banType, - Long userId) { + Long userId) { BanHistory banHistory = new BanHistory(bannedAt, unbannedAt, banType, userId); ExceptionUtil.throwIfFalse(banHistory.isValid(), - new DomainException(ExceptionStatus.INVALID_ARGUMENT)); + new DomainException(ExceptionStatus.INVALID_ARGUMENT)); return banHistory; } @@ -90,4 +89,8 @@ public boolean equals(Object o) { public boolean isBanned(LocalDateTime date) { return date.isBefore(unbannedAt); } + + public void updateUnbannedAt(LocalDateTime localDateTime) { + this.unbannedAt = localDateTime; + } } \ No newline at end of file diff --git a/backend/src/main/java/org/ftclub/cabinet/user/domain/UserAspect.java b/backend/src/main/java/org/ftclub/cabinet/user/domain/UserAspect.java deleted file mode 100644 index db4698ac8..000000000 --- a/backend/src/main/java/org/ftclub/cabinet/user/domain/UserAspect.java +++ /dev/null @@ -1,63 +0,0 @@ -package org.ftclub.cabinet.user.domain; - -import com.fasterxml.jackson.core.JsonProcessingException; -import java.time.LocalDateTime; -import javax.servlet.http.HttpServletRequest; -import lombok.RequiredArgsConstructor; -import lombok.extern.log4j.Log4j2; -import org.aspectj.lang.ProceedingJoinPoint; -import org.aspectj.lang.annotation.Around; -import org.aspectj.lang.annotation.Aspect; -import org.ftclub.cabinet.auth.domain.CookieManager; -import org.ftclub.cabinet.auth.service.TokenValidator; -import org.ftclub.cabinet.config.JwtProperties; -import org.ftclub.cabinet.dto.UserSessionDto; -import org.ftclub.cabinet.exception.ExceptionStatus; -import org.ftclub.cabinet.user.service.UserQueryService; -import org.springframework.stereotype.Component; -import org.springframework.web.context.request.RequestContextHolder; -import org.springframework.web.context.request.ServletRequestAttributes; - -@Component -@Aspect -@RequiredArgsConstructor -@Log4j2 -public class UserAspect { - - //private final UserMapper ... - //private final UserService ... - //컨트롤러가 아니므로 Facade를 주입받지는 않지만, 서비스와 매퍼를 주입받아서 UserSessionDto를 생성해 줌. - private final CookieManager cookieManager; - private final TokenValidator tokenValidator; - private final JwtProperties jwtProperties; - private final UserQueryService userQueryService; - - @Around("execution(* *(.., @UserSession (*), ..))") - public Object setUserSessionDto(ProceedingJoinPoint joinPoint) - throws Throwable { - HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()) - .getRequest(); - //@User를 쓰려면 반드시 첫 매개변수에 UserSessionDto로 설정해주어야 함. - Object[] args = joinPoint.getArgs(); - if (!args[0].getClass().equals(UserSessionDto.class)) { - log.error("User not found"); - throw ExceptionStatus.UNAUTHORIZED.asControllerException(); - } - args[0] = getUserSessionDtoByRequest(request); - return joinPoint.proceed(args); - } - - // ToDo: 수정 필요 - public UserSessionDto getUserSessionDtoByRequest(HttpServletRequest req) - throws JsonProcessingException { - String name = tokenValidator.getPayloadJson( - cookieManager.getCookieValue(req, jwtProperties.getMainTokenName())).get("name") - .asText(); - User user = userQueryService.findUserByName(name) - .orElseThrow(ExceptionStatus.NOT_FOUND_USER::asServiceException); - //ToDo: name을 기준으로 service에게 정보를 받고, 매핑한다. - // name과 email은 우선 구현했으나 수정이 필요함. - return new UserSessionDto(user.getId(), name, user.getEmail(), 1, 1, LocalDateTime.now(), - true); - } -} diff --git a/backend/src/main/java/org/ftclub/cabinet/user/domain/UserSessionArgumentResolver.java b/backend/src/main/java/org/ftclub/cabinet/user/domain/UserSessionArgumentResolver.java new file mode 100644 index 000000000..616ff2be2 --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/user/domain/UserSessionArgumentResolver.java @@ -0,0 +1,68 @@ +package org.ftclub.cabinet.user.domain; + +import io.netty.util.internal.StringUtil; +import javax.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.ftclub.cabinet.auth.domain.CookieManager; +import org.ftclub.cabinet.auth.service.TokenValidator; +import org.ftclub.cabinet.config.JwtProperties; +import org.ftclub.cabinet.dto.UserSessionDto; +import org.ftclub.cabinet.exception.ExceptionStatus; +import org.ftclub.cabinet.user.service.UserQueryService; +import org.jetbrains.annotations.NotNull; +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +@Slf4j +@Component +@RequiredArgsConstructor +public class UserSessionArgumentResolver implements HandlerMethodArgumentResolver { + + private final TokenValidator tokenValidator; + private final UserQueryService userQueryService; + private final CookieManager cookieManager; + private final JwtProperties jwtProperties; + + + @Override + public boolean supportsParameter(MethodParameter parameter) { + boolean hasUserSessionAnnotation = parameter.hasParameterAnnotation(UserSession.class); + boolean hasUserSessionType = + UserSessionDto.class.isAssignableFrom(parameter.getParameterType()); + + return hasUserSessionAnnotation && hasUserSessionType; + } + + @Override + public Object resolveArgument(@NotNull MethodParameter parameter, + ModelAndViewContainer mavContainer, + @NotNull NativeWebRequest webRequest, + WebDataBinderFactory binderFactory) throws Exception { + + HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); + + String token = cookieManager.getCookieValue(request, jwtProperties.getMainTokenName()); + if (StringUtil.isNullOrEmpty(token)) { + token = cookieManager.getCookieValue(request, jwtProperties.getAdminTokenName()); + if (token != null) { + return null; + } else { + throw ExceptionStatus.INVALID_JWT_TOKEN.asControllerException(); + } + } + String name = tokenValidator.getPayloadJson(token).get("name").asText(); + if (StringUtil.isNullOrEmpty(name)) { + throw ExceptionStatus.INVALID_JWT_TOKEN.asControllerException(); + } + User user = userQueryService.findUserByName(name) + .orElseThrow(ExceptionStatus.NOT_FOUND_USER::asServiceException); + + return new UserSessionDto(user.getId(), name, user.getEmail(), 1, 1, + user.getBlackholedAt(), false); + } +} diff --git a/backend/src/main/java/org/ftclub/cabinet/user/service/BanHistoryCommandService.java b/backend/src/main/java/org/ftclub/cabinet/user/service/BanHistoryCommandService.java index 802beac2c..adfb3b7bd 100644 --- a/backend/src/main/java/org/ftclub/cabinet/user/service/BanHistoryCommandService.java +++ b/backend/src/main/java/org/ftclub/cabinet/user/service/BanHistoryCommandService.java @@ -42,4 +42,17 @@ public void deleteRecentBanHistory(BanHistory banHistory, LocalDateTime now) { banHistoryRepository.delete(banHistory); } } + + @Transactional + public void updateBanDate(BanHistory recentBanHistory, LocalDateTime reducedBanDate) { + LocalDateTime newUnbannedAt = findMaxUnbannedAt(reducedBanDate, LocalDateTime.now()); + recentBanHistory.updateUnbannedAt(newUnbannedAt); + } + + private LocalDateTime findMaxUnbannedAt(LocalDateTime reducedBanDate, LocalDateTime now) { + if (reducedBanDate.isAfter(now)) { + return reducedBanDate; + } + return now; + } } diff --git a/backend/src/main/java/org/ftclub/cabinet/user/service/BanHistoryQueryService.java b/backend/src/main/java/org/ftclub/cabinet/user/service/BanHistoryQueryService.java index 7ec65e770..ca248339c 100644 --- a/backend/src/main/java/org/ftclub/cabinet/user/service/BanHistoryQueryService.java +++ b/backend/src/main/java/org/ftclub/cabinet/user/service/BanHistoryQueryService.java @@ -5,6 +5,7 @@ import java.util.List; import java.util.Optional; import lombok.RequiredArgsConstructor; +import org.ftclub.cabinet.exception.ExceptionStatus; import org.ftclub.cabinet.log.LogLevel; import org.ftclub.cabinet.log.Logging; import org.ftclub.cabinet.user.domain.BanHistory; @@ -23,9 +24,9 @@ public class BanHistoryQueryService { public Optional findRecentActiveBanHistory(Long userId, LocalDateTime now) { List banHistories = banHistoryRepository.findByUserId(userId); return banHistories.stream() - .filter(history -> history.getUnbannedAt().isAfter(now)) - .sorted(Comparator.comparing(BanHistory::getUnbannedAt, Comparator.reverseOrder())) - .findFirst(); + .filter(history -> history.getUnbannedAt().isAfter(now)) + .sorted(Comparator.comparing(BanHistory::getUnbannedAt, Comparator.reverseOrder())) + .findFirst(); } public List findActiveBanHistories(Long userId, LocalDateTime date) { @@ -39,4 +40,12 @@ public List findActiveBanHistories(List userIds, LocalDateTime public Page findActiveBanHistories(LocalDateTime now, Pageable pageable) { return banHistoryRepository.findPaginationActiveBanHistoriesJoinUser(pageable, now); } + + public BanHistory getRecentBanHistory(Long userId) { + List activeBanHistories = + banHistoryRepository.findByUserIdAndUnbannedAt(userId, LocalDateTime.now()); + return activeBanHistories.stream() + .max(Comparator.comparing(BanHistory::getBannedAt)) + .orElseThrow(ExceptionStatus.NOT_FOUND_BAN_HISTORY::asServiceException); + } } diff --git a/backend/src/main/java/org/ftclub/cabinet/user/service/LentExtensionCommandService.java b/backend/src/main/java/org/ftclub/cabinet/user/service/LentExtensionCommandService.java index 41ddff61f..044cc03fb 100644 --- a/backend/src/main/java/org/ftclub/cabinet/user/service/LentExtensionCommandService.java +++ b/backend/src/main/java/org/ftclub/cabinet/user/service/LentExtensionCommandService.java @@ -19,6 +19,7 @@ @Transactional public class LentExtensionCommandService { + private static final String LENT_EXTENSION_ITEM = "lentExtensionItem"; private final LentExtensionRepository lentExtensionRepository; private final LentExtensionPolicy policy; @@ -31,11 +32,19 @@ public class LentExtensionCommandService { * @return 생성된 연장권 */ public LentExtension createLentExtension(Long userId, LentExtensionType type, - LocalDateTime expiredAt) { + LocalDateTime expiredAt) { LentExtension lentExtension = LentExtension.of(policy.getDefaultName(), - policy.getDefaultExtensionTerm(), - policy.getExpiry(expiredAt), - type, userId); + policy.getDefaultExtensionTerm(), + policy.getExpiry(expiredAt), + type, userId); + return lentExtensionRepository.save(lentExtension); + } + + public LentExtension createLentExtensionByItem(Long userId, LentExtensionType type, + Integer extensionDay) { + LentExtension lentExtension = LentExtension.of(LENT_EXTENSION_ITEM, extensionDay, + LocalDateTime.now().plusYears(100), + type, userId); return lentExtensionRepository.save(lentExtension); } @@ -48,7 +57,7 @@ public LentExtension createLentExtension(Long userId, LentExtensionType type, public void useLentExtension(LentExtension lentExtension, List lentHistories) { lentExtension.use(); lentHistories.forEach(lentHistory -> - lentHistory.setExpiredAt( - lentHistory.getExpiredAt().plusDays(lentExtension.getExtensionPeriod()))); + lentHistory.setExpiredAt( + lentHistory.getExpiredAt().plusDays(lentExtension.getExtensionPeriod()))); } } diff --git a/backend/src/main/java/org/ftclub/cabinet/user/service/LentExtensionManager.java b/backend/src/main/java/org/ftclub/cabinet/user/service/LentExtensionManager.java index 75778b2a1..40b618173 100644 --- a/backend/src/main/java/org/ftclub/cabinet/user/service/LentExtensionManager.java +++ b/backend/src/main/java/org/ftclub/cabinet/user/service/LentExtensionManager.java @@ -1,16 +1,20 @@ package org.ftclub.cabinet.user.service; -import java.time.LocalDateTime; import java.util.List; import java.util.stream.Collectors; import javax.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.ftclub.cabinet.dto.UserMonthDataDto; +import org.ftclub.cabinet.item.domain.Item; +import org.ftclub.cabinet.item.domain.Sku; +import org.ftclub.cabinet.item.service.ItemHistoryCommandService; +import org.ftclub.cabinet.item.service.ItemQueryService; +import org.ftclub.cabinet.item.service.ItemRedisService; import org.ftclub.cabinet.log.LogLevel; import org.ftclub.cabinet.log.Logging; import org.ftclub.cabinet.occupiedtime.OccupiedTimeManager; -import org.ftclub.cabinet.user.domain.LentExtensionType; import org.ftclub.cabinet.user.domain.User; +import org.ftclub.cabinet.utils.lock.LockUtil; import org.springframework.stereotype.Service; @Service @@ -18,7 +22,9 @@ @Logging(level = LogLevel.DEBUG) public class LentExtensionManager { - private final LentExtensionCommandService lentExtensionCommandService; + private final ItemHistoryCommandService itemHistoryCommandService; + private final ItemQueryService itemQueryService; + private final ItemRedisService itemRedisService; private final OccupiedTimeManager occupiedTimeManager; private final UserQueryService userQueryService; @@ -28,17 +34,36 @@ public class LentExtensionManager { */ @Transactional public void issueLentExtension() { - UserMonthDataDto[] userLastMonthOccupiedTime = occupiedTimeManager.getUserLastMonthOccupiedTime(); - List userMonthDataDtos = occupiedTimeManager.filterToMetUserMonthlyTime( - userLastMonthOccupiedTime); + UserMonthDataDto[] userLastMonthOccupiedTime = + occupiedTimeManager.getUserLastMonthOccupiedTime(); + List userMonthDataDtos = + occupiedTimeManager.filterToMetUserMonthlyTime(userLastMonthOccupiedTime); List userNames = userMonthDataDtos.stream().map(UserMonthDataDto::getLogin) .collect(Collectors.toList()); - LocalDateTime now = LocalDateTime.now(); List users = userQueryService.findAllUsersByNames(userNames); - users.forEach(user -> lentExtensionCommandService.createLentExtension(user.getId(), - LentExtensionType.ALL, - LocalDateTime.of(now.getYear(), now.getMonth(), - now.getMonth().length(now.toLocalDate().isLeapYear()), 23, 59, 0))); + Item coinRewardItem = itemQueryService.getBySku(Sku.COIN_FULL_TIME); + users.forEach(user -> { + Long userId = user.getId(); + LockUtil.lockRedisCoin(userId, () -> + saveCoinChangeOnRedis(userId, coinRewardItem.getPrice())); + itemHistoryCommandService.createItemHistory(userId, coinRewardItem.getId()); + }); + } + + /** + * 재화 변동량을 Redis에 저장합니다. + * + * @param userId 유저 아이디 + * @param price 변동량 + */ + private void saveCoinChangeOnRedis(Long userId, long price) { + // 유저 재화 변동량 Redis에 저장 + long userCoinCount = itemRedisService.getCoinCount(userId); + itemRedisService.saveCoinCount(userId, userCoinCount + price); + + // 전체 재화 변동량 Redis에 저장 + long totalCoinSupply = itemRedisService.getTotalCoinSupply(); + itemRedisService.saveTotalCoinSupply(totalCoinSupply + price); } } diff --git a/backend/src/main/java/org/ftclub/cabinet/user/service/UserFacadeService.java b/backend/src/main/java/org/ftclub/cabinet/user/service/UserFacadeService.java index 8aa3e22aa..e1d7767af 100644 --- a/backend/src/main/java/org/ftclub/cabinet/user/service/UserFacadeService.java +++ b/backend/src/main/java/org/ftclub/cabinet/user/service/UserFacadeService.java @@ -17,6 +17,7 @@ import org.ftclub.cabinet.dto.UpdateDeviceTokenRequestDto; import org.ftclub.cabinet.dto.UserSessionDto; import org.ftclub.cabinet.exception.ExceptionStatus; +import org.ftclub.cabinet.item.service.ItemRedisService; import org.ftclub.cabinet.lent.domain.LentHistory; import org.ftclub.cabinet.lent.service.LentQueryService; import org.ftclub.cabinet.log.LogLevel; @@ -45,6 +46,9 @@ public class UserFacadeService { private final UserMapper userMapper; private final FCMTokenRedisService fcmTokenRedisService; private final FirebaseConfig firebaseConfig; + private final BanHistoryCommandService banHistoryCommandService; + private final ItemRedisService itemRedisService; + /** * 유저의 프로필을 가져옵니다. @@ -54,19 +58,21 @@ public class UserFacadeService { */ @Transactional(readOnly = true) public MyProfileResponseDto getProfile(UserSessionDto user) { - Cabinet cabinet = cabinetQueryService.findUserActiveCabinet(user.getUserId()); - BanHistory banHistory = banHistoryQueryService.findRecentActiveBanHistory(user.getUserId(), + Long userId = user.getUserId(); + Cabinet cabinet = cabinetQueryService.findUserActiveCabinet(userId); + BanHistory banHistory = banHistoryQueryService.findRecentActiveBanHistory(userId, LocalDateTime.now()).orElse(null); LentExtension lentExtension = lentExtensionQueryService.findActiveLentExtension( - user.getUserId()); + userId); LentExtensionResponseDto lentExtensionResponseDto = userMapper.toLentExtensionResponseDto( lentExtension); - User currentUser = userQueryService.getUser(user.getUserId()); + User currentUser = userQueryService.getUser(userId); AlarmTypeResponseDto userAlarmTypes = currentUser.getAlarmTypes(); boolean isDeviceTokenExpired = userAlarmTypes.isPush() && fcmTokenRedisService.findByUserName(user.getName()).isEmpty(); + Long coins = itemRedisService.getCoinCount(userId); return userMapper.toMyProfileResponseDto(user, cabinet, banHistory, - lentExtensionResponseDto, userAlarmTypes, isDeviceTokenExpired); + lentExtensionResponseDto, userAlarmTypes, isDeviceTokenExpired, coins); } /** @@ -135,5 +141,19 @@ public void updateDeviceToken(UserSessionDto userSessionDto, Duration.ofDays(firebaseConfig.getDeviceTokenExpiryDays()) ); } + + /** + * 가장 최근 밴 당한 날짜에서 일자만큼 차감 후 업데이트 + * + * @param userId + * @param days + */ + @Transactional + public void reduceBanDays(Long userId, Integer days) { + BanHistory recentBanHistory = banHistoryQueryService.getRecentBanHistory(userId); + LocalDateTime reducedUnbannedAt = recentBanHistory.getUnbannedAt().minusDays(days); + banHistoryCommandService.updateBanDate(recentBanHistory, reducedUnbannedAt); + } + } diff --git a/backend/src/main/java/org/ftclub/cabinet/utils/annotations/NullableMapper.java b/backend/src/main/java/org/ftclub/cabinet/utils/annotations/NullableMapper.java deleted file mode 100644 index ecfd121ac..000000000 --- a/backend/src/main/java/org/ftclub/cabinet/utils/annotations/NullableMapper.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.ftclub.cabinet.utils.annotations; - -import static org.mapstruct.NullValueMappingStrategy.RETURN_DEFAULT; -import static org.mapstruct.NullValueMappingStrategy.RETURN_NULL; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; -import org.mapstruct.Mapper; - -/** - * NPE를 방지하고, 빈 값들을 매핑할 수 있는 mapper 입니다. - *

- * 단일 객체가 null인 경우, null을 매핑합니다. - *

- * Map, Iterable가 비어있거나, null인 경우 빈 Map 또는 Iterable을 매핑합니다. - */ -@Mapper(componentModel = "spring", - nullValueMappingStrategy = RETURN_NULL, - nullValueMapMappingStrategy = RETURN_DEFAULT, - nullValueIterableMappingStrategy = RETURN_DEFAULT) -@Target({java.lang.annotation.ElementType.TYPE}) -@Retention(RetentionPolicy.RUNTIME) -public @interface NullableMapper { - -} diff --git a/backend/src/main/java/org/ftclub/cabinet/utils/lock/IntegerLock.java b/backend/src/main/java/org/ftclub/cabinet/utils/lock/IntegerLock.java new file mode 100644 index 000000000..538465ab1 --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/utils/lock/IntegerLock.java @@ -0,0 +1,7 @@ +package org.ftclub.cabinet.utils.lock; + +@FunctionalInterface +public interface IntegerLock { + + int invoke(); +} diff --git a/backend/src/main/java/org/ftclub/cabinet/utils/lock/LockUtil.java b/backend/src/main/java/org/ftclub/cabinet/utils/lock/LockUtil.java new file mode 100644 index 000000000..52817601c --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/utils/lock/LockUtil.java @@ -0,0 +1,16 @@ +package org.ftclub.cabinet.utils.lock; + +import java.util.concurrent.ConcurrentHashMap; + +public abstract class LockUtil { + + private final static ConcurrentHashMap redisCoinLock = new ConcurrentHashMap<>(); + + public static void lockRedisCoin(Long userId, VoidLock functionalInterface) { + Object lock = redisCoinLock.computeIfAbsent(userId, v -> new Object()); + synchronized (lock) { + functionalInterface.invoke(); + } + redisCoinLock.remove(userId); + } +} diff --git a/backend/src/main/java/org/ftclub/cabinet/utils/lock/VoidLock.java b/backend/src/main/java/org/ftclub/cabinet/utils/lock/VoidLock.java new file mode 100644 index 000000000..16b31d8d5 --- /dev/null +++ b/backend/src/main/java/org/ftclub/cabinet/utils/lock/VoidLock.java @@ -0,0 +1,7 @@ +package org.ftclub.cabinet.utils.lock; + +@FunctionalInterface +public interface VoidLock { + + void invoke(); +} diff --git a/backend/src/main/java/org/ftclub/cabinet/utils/release/ReleaseManager.java b/backend/src/main/java/org/ftclub/cabinet/utils/release/ReleaseManager.java index 8c8e760a9..6f4bf2778 100644 --- a/backend/src/main/java/org/ftclub/cabinet/utils/release/ReleaseManager.java +++ b/backend/src/main/java/org/ftclub/cabinet/utils/release/ReleaseManager.java @@ -35,7 +35,7 @@ private Set getAllPendedYesterdayCabinet() { .map(Cabinet::getId).collect(Collectors.toList()); Set todayReturnedSet = lentQueryService.findCabinetLentHistories(cabinetIds) .stream() - .filter(lh -> lh.getEndedAt().isAfter(from)) + .filter(lh -> lh.getEndedAt() != null && lh.getEndedAt().isAfter(from)) .map(LentHistory::getCabinetId) .collect(Collectors.toSet()); diff --git a/backend/src/main/java/org/ftclub/cabinet/utils/scheduler/SystemScheduler.java b/backend/src/main/java/org/ftclub/cabinet/utils/scheduler/SystemScheduler.java index dce80e8d8..24d3fa8cb 100644 --- a/backend/src/main/java/org/ftclub/cabinet/utils/scheduler/SystemScheduler.java +++ b/backend/src/main/java/org/ftclub/cabinet/utils/scheduler/SystemScheduler.java @@ -7,6 +7,7 @@ import lombok.extern.log4j.Log4j2; import org.ftclub.cabinet.alarm.discord.DiscordScheduleAlarmMessage; import org.ftclub.cabinet.alarm.discord.DiscordWebHookMessenger; +import org.ftclub.cabinet.alarm.handler.SectionAlarmManager; import org.ftclub.cabinet.dto.ActiveLentHistoryDto; import org.ftclub.cabinet.dto.UserBlackHoleEvent; import org.ftclub.cabinet.exception.FtClubCabinetException; @@ -48,6 +49,7 @@ private void errorHandle(Exception e, DiscordScheduleAlarmMessage message) { } } + private final SectionAlarmManager sectionAlarmManager; /** * 매일 자정마다 대여 기록을 확인하여, 연체 메일 발송 및 휴학생 처리를 트리거 @@ -176,4 +178,10 @@ public void lentExtensionIssue() { // List userMonthDataDtos = occupiedTimeManager.metLimitTimeUser(occupiedTimeManager.getUserLastMonthOccupiedTime()); // userService.updateUserExtensible(userMonthDataDtos); // } + + @Scheduled(cron = "${cabinet.schedule.cron.section-alarm-time}") + public void sectionAlarm() { + log.info("called sectionAlarm"); + sectionAlarmManager.sendSectionAlarm(); + } } diff --git a/backend/src/main/resources/templates/mail/lentsuccess.html b/backend/src/main/resources/templates/mail/lentsuccess.html index 1be585ffd..dbe1068c1 100644 --- a/backend/src/main/resources/templates/mail/lentsuccess.html +++ b/backend/src/main/resources/templates/mail/lentsuccess.html @@ -1,33 +1,41 @@ - + - - - - - Document - - + + + + + Document + + +

+

🚨 사물함 대여 성공 알림 🚨

+
+
+
+ 님, 의 사물함 대여에 + 성공했습니다.

-
-

🚨 사물함 대여 성공 알림 🚨

-
-
-
- 님, 의 사물함 대여에 성공했습니다.

+ 대여 기간은 까지 이며 기한 만료 전 반납 + 부탁드립니다.

- 대여 기간은 까지 이며 기한 만료 전 반납 부탁드립니다.

- - 연체한 일 수의 제곱 일수 만큼 패널티가 주어집니다.
- 패널티 일수 만큼 사물함 이용이 불가합니다.
- 반복적인 연체 발생 시 TIG가 부여될 수 있습니다.
-
-
-
- 사물함 대여 서비스 바로가기 ➡ https://cabi.42seoul.io
- 사물함 서비스 관련 문의사항 ➡ https://42born2code.slack.com/archives/C02V6GE8LD7 -
-
- - - \ No newline at end of file + 연체한 일 수의 제곱 일수 만큼 페널티가 주어집니다.
+ 페널티 일수 만큼 사물함 이용이 불가합니다.
+ 반복적인 연체 발생 시 TIG가 부여될 수 있습니다.
+
+
+
+ 사물함 대여 서비스 바로가기 ➡ + https://cabi.42seoul.io
+ 사물함 서비스 관련 문의사항 ➡ + https://42born2code.slack.com/archives/C02V6GE8LD7 +
+
+ + diff --git a/backend/src/main/resources/templates/mail/overdue.html b/backend/src/main/resources/templates/mail/overdue.html index ef1010c4b..7d9068712 100644 --- a/backend/src/main/resources/templates/mail/overdue.html +++ b/backend/src/main/resources/templates/mail/overdue.html @@ -1,35 +1,43 @@ - + - - - - - Document - - + + + + + Document + + +
+

🚨 사물함 대여 연체 알림 🚨

+
+
+
+ 님, 이용 중인 사물함이 연체되었습니다. +

-
-

🚨 사물함 대여 연체 알림 🚨

-
-
-
- 님, 이용 중인 사물함이 연체되었습니다.

+ 현재 일 연체 되었으며, 확인 후 반납 + 부탁드립니다.

+ 짐을 비우지 않을 경우, 해당 짐은 분실물 처리가 될 수 있습니다.
+ 이 과정에서 발생하는 분실물에 대한 책임은 사용자 본인에게 있습니다.

- 현재 일 연체 되었으며, 확인 후 반납 부탁드립니다.

- 짐을 비우지 않을 경우, 해당 짐은 분실물 처리가 될 수 있습니다.
- 이 과정에서 발생하는 분실물에 대한 책임은 사용자 본인에게 있습니다.

- - 연체한 일 수의 제곱 일수 만큼 패널티가 주어집니다.
- 패널티 일수 만큼 사물함 이용이 불가합니다.
- 반복적인 연체 발생 시 TIG가 부여될 수 있습니다.
-
-
-
- 사물함 대여 서비스 바로가기 ➡ https://cabi.42seoul.io
- 사물함 서비스 관련 문의사항 ➡ https://42born2code.slack.com/archives/C02V6GE8LD7 -
-
- - - \ No newline at end of file + 연체한 일 수의 제곱 일수 만큼 페널티가 주어집니다.
+ 페널티 일수 만큼 사물함 이용이 불가합니다.
+ 반복적인 연체 발생 시 TIG가 부여될 수 있습니다.
+
+
+
+ 사물함 대여 서비스 바로가기 ➡ + https://cabi.42seoul.io
+ 사물함 서비스 관련 문의사항 ➡ + https://42born2code.slack.com/archives/C02V6GE8LD7 +
+
+ + diff --git a/backend/src/main/resources/templates/mail/sectionAlarm.html b/backend/src/main/resources/templates/mail/sectionAlarm.html new file mode 100644 index 000000000..9699eb3f1 --- /dev/null +++ b/backend/src/main/resources/templates/mail/sectionAlarm.html @@ -0,0 +1,31 @@ + + + + + + + Document + + + +
+

🚨 알림 등록 영역 사물함 오픈 예정 알림 🚨

+
+
+
+ 님, 알림 등록하신 의 사물함이 오픈 예정입니다.

+ + 사물함 오픈 예정 시간은 매일 13시입니다.
+ 이용에 참고 바랍니다.

+
+
+
+ 사물함 대여 서비스 바로가기 ➡ https://cabi.42seoul.io
+ 사물함 서비스 관련 문의사항 ➡ https://42born2code.slack.com/archives/C02V6GE8LD7 +
+
+ + + \ No newline at end of file diff --git a/backend/src/main/resources/templates/mail/soonoverdue.html b/backend/src/main/resources/templates/mail/soonoverdue.html index 23acc4676..8b71b62f3 100644 --- a/backend/src/main/resources/templates/mail/soonoverdue.html +++ b/backend/src/main/resources/templates/mail/soonoverdue.html @@ -1,36 +1,45 @@ - + - - - - - Document - - + + + + + Document + + +
+

🚨 사물함 대여 기간 만료 예정 알림 🚨

+
+
+
+ 님, 사물함 사용 기간이 곧 만료될 예정입니다.

-
-

🚨 사물함 대여 기간 만료 예정 알림 🚨

-
-
-
- 님, 사물함 사용 기간이 곧 만료될 예정입니다.

+ 대여 기간은 까지 이며 기한 만료 전 반납 + 부탁드립니다.

+ 반납 전, 사물함의 짐을 반드시 비운 후에 반납해주시기 바랍니다.
+ 짐을 비우지 않을 경우, 해당 짐은 분실물 처리가 될 수 있습니다.
+ 이 과정에서 발생하는 분실물에 대한 책임은 사용자 본인에게 있습니다.

- 대여 기간은 까지 이며 기한 만료 전 반납 부탁드립니다.

- 반납 전, 사물함의 짐을 반드시 비운 후에 반납해주시기 바랍니다.
- 짐을 비우지 않을 경우, 해당 짐은 분실물 처리가 될 수 있습니다.
- 이 과정에서 발생하는 분실물에 대한 책임은 사용자 본인에게 있습니다.

- - 연체한 일 수의 제곱 일수 만큼 패널티가 주어집니다.
- 패널티 일수 만큼 사물함 이용이 불가합니다.
- 반복적인 연체 발생 시 TIG가 부여될 수 있습니다.
- 직접 반납하기 어려우신 경우 지인을 통해서라도 꼭 연체 전 반납 부탁드립니다!

-
-
-
- 사물함 대여 서비스 바로가기 ➡ https://cabi.42seoul.io
- 사물함 서비스 관련 문의사항 ➡ https://42born2code.slack.com/archives/C02V6GE8LD7 -
-
- - \ No newline at end of file + 연체한 일 수의 제곱 일수 만큼 페널티가 주어집니다.
+ 페널티 일수 만큼 사물함 이용이 불가합니다.
+ 반복적인 연체 발생 시 TIG가 부여될 수 있습니다.
+ 직접 반납하기 어려우신 경우 지인을 통해서라도 꼭 연체 전 반납 + 부탁드립니다!

+
+
+
+ 사물함 대여 서비스 바로가기 ➡ + https://cabi.42seoul.io
+ 사물함 서비스 관련 문의사항 ➡ + https://42born2code.slack.com/archives/C02V6GE8LD7 +
+
+ + diff --git a/backend/src/test/java/org/ftclub/cabinet/cabinet/service/CabinetFacadeServiceTest.java b/backend/src/test/java/org/ftclub/cabinet/cabinet/service/CabinetFacadeServiceTest.java index ea4d780bd..ca6f1239f 100644 --- a/backend/src/test/java/org/ftclub/cabinet/cabinet/service/CabinetFacadeServiceTest.java +++ b/backend/src/test/java/org/ftclub/cabinet/cabinet/service/CabinetFacadeServiceTest.java @@ -3,6 +3,7 @@ import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import java.util.List; import javax.transaction.Transactional; @@ -39,7 +40,7 @@ public class CabinetFacadeServiceTest { assertEquals(LentType.PRIVATE, cabinet.getLentType()); assertEquals(1, cabinet.getVisibleNum().intValue()); assertEquals(Location.of("새롬관", 2, "Oasis"), cabinet.getLocation()); - assertEquals(null, cabinet.getTitle()); + assertNull(cabinet.getTitle()); } @Test @@ -65,9 +66,9 @@ public class CabinetFacadeServiceTest { assertEquals("user7", lentsOfCabinet.get(0).getName()); } - @Test - public void 사물함_섹션_정보_가져오기() { - System.out.println( - "cabinetFacadeService = " + cabinetFacadeService.getCabinetsPerSection("새롬관", 2)); - } +// @Test +// public void 사물함_섹션_정보_가져오기() { +// System.out.println( +// "cabinetFacadeService = " + cabinetFacadeService.getCabinetsPerSection("새롬관", 2)); +// } } diff --git a/backend/src/test/java/org/ftclub/cabinet/mapper/CabinetMapperTest.java b/backend/src/test/java/org/ftclub/cabinet/mapper/CabinetMapperTest.java index 73b1c6da4..143d7d3bc 100644 --- a/backend/src/test/java/org/ftclub/cabinet/mapper/CabinetMapperTest.java +++ b/backend/src/test/java/org/ftclub/cabinet/mapper/CabinetMapperTest.java @@ -1,18 +1,27 @@ package org.ftclub.cabinet.mapper; -import org.ftclub.cabinet.cabinet.domain.*; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.time.LocalDateTime; +import java.util.List; +import org.ftclub.cabinet.cabinet.domain.Cabinet; +import org.ftclub.cabinet.cabinet.domain.CabinetPlace; +import org.ftclub.cabinet.cabinet.domain.CabinetStatus; +import org.ftclub.cabinet.cabinet.domain.Grid; +import org.ftclub.cabinet.cabinet.domain.LentType; +import org.ftclub.cabinet.cabinet.domain.Location; +import org.ftclub.cabinet.cabinet.domain.MapArea; +import org.ftclub.cabinet.cabinet.domain.SectionFormation; import org.ftclub.cabinet.cabinet.repository.CabinetRepository; -import org.ftclub.cabinet.dto.*; +import org.ftclub.cabinet.dto.BuildingFloorsDto; +import org.ftclub.cabinet.dto.CabinetDto; +import org.ftclub.cabinet.dto.CabinetInfoResponseDto; +import org.ftclub.cabinet.dto.LentDto; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import java.time.LocalDateTime; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; - @SpringBootTest class CabinetMapperTest { @@ -71,23 +80,23 @@ void toCabinetInfoResponseDto() { assertEquals(lentDtos, cabinetInfoResponseDto.getLents()); } - @Test - void toCabinetsPerSectionResponseDto() { - String section = "testSection"; - CabinetPreviewDto cabinetPreviewDto1 = new CabinetPreviewDto(1L, 2, "title", - LentType.SHARE, 3, CabinetStatus.AVAILABLE, 1, null); - CabinetPreviewDto cabinetPreviewDto2 = new CabinetPreviewDto(2L, 5, "title", - LentType.SHARE, 3, CabinetStatus.AVAILABLE, 1, null); - CabinetPreviewDto cabinetPreviewDto3 = new CabinetPreviewDto(3L, 6, "title", - LentType.SHARE, 3, CabinetStatus.AVAILABLE, 1, null); - CabinetPreviewDto cabinetPreviewDto4 = new CabinetPreviewDto(4L, 7, "title", - LentType.SHARE, 3, CabinetStatus.AVAILABLE, 1, null); - List cabinetPreviewDtos = List.of(cabinetPreviewDto1, cabinetPreviewDto2, - cabinetPreviewDto3, cabinetPreviewDto4); - CabinetsPerSectionResponseDto cabinetsPerSectionResponseDto = cabinetMapper.toCabinetsPerSectionResponseDto( - section, cabinetPreviewDtos); - System.out.println("cabinetsPerSectionResponseDto = " + cabinetsPerSectionResponseDto); - assertEquals(section, cabinetsPerSectionResponseDto.getSection()); - assertEquals(cabinetPreviewDtos, cabinetsPerSectionResponseDto.getCabinets()); - } +// @Test +// void toCabinetsPerSectionResponseDto() { +// String section = "testSection"; +// CabinetPreviewDto cabinetPreviewDto1 = new CabinetPreviewDto(1L, 2, "title", +// LentType.SHARE, 3, CabinetStatus.AVAILABLE, 1, null); +// CabinetPreviewDto cabinetPreviewDto2 = new CabinetPreviewDto(2L, 5, "title", +// LentType.SHARE, 3, CabinetStatus.AVAILABLE, 1, null); +// CabinetPreviewDto cabinetPreviewDto3 = new CabinetPreviewDto(3L, 6, "title", +// LentType.SHARE, 3, CabinetStatus.AVAILABLE, 1, null); +// CabinetPreviewDto cabinetPreviewDto4 = new CabinetPreviewDto(4L, 7, "title", +// LentType.SHARE, 3, CabinetStatus.AVAILABLE, 1, null); +// List cabinetPreviewDtos = List.of(cabinetPreviewDto1, cabinetPreviewDto2, +// cabinetPreviewDto3, cabinetPreviewDto4); +// CabinetsPerSectionResponseDto cabinetsPerSectionResponseDto = cabinetMapper.toCabinetsPerSectionResponseDto( +// section, cabinetPreviewDtos); +// System.out.println("cabinetsPerSectionResponseDto = " + cabinetsPerSectionResponseDto); +// assertEquals(section, cabinetsPerSectionResponseDto.getSection()); +// assertEquals(cabinetPreviewDtos, cabinetsPerSectionResponseDto.getCabinets()); +// } } \ No newline at end of file diff --git a/backend/src/test/java/org/ftclub/cabinet/user/repository/UserOptionalFetcherTest.java b/backend/src/test/java/org/ftclub/cabinet/user/repository/UserOptionalFetcherTest.java index c1e39c2b6..375a2fb2c 100644 --- a/backend/src/test/java/org/ftclub/cabinet/user/repository/UserOptionalFetcherTest.java +++ b/backend/src/test/java/org/ftclub/cabinet/user/repository/UserOptionalFetcherTest.java @@ -6,7 +6,7 @@ //import java.time.LocalDateTime; // //import org.ftclub.cabinet.user.domain.User; -//import org.ftclub.cabinet.user.domain.UserRole; +//import org.ftclub.cabinet.club.domain.UserRole; //import org.junit.jupiter.api.DisplayName; //import org.junit.jupiter.api.Test; //import org.springframework.beans.factory.annotation.Autowired; diff --git a/config b/config index 8d4f9e4f0..2f4778c4b 160000 --- a/config +++ b/config @@ -1 +1 @@ -Subproject commit 8d4f9e4f0f297a73ea6676c582267a265715ce5d +Subproject commit 2f4778c4b0932e488f7e63cd5893e7244b0d7e32 diff --git a/delete_local_branchs.sh b/delete_local_branchs.sh new file mode 100755 index 000000000..6170ac32b --- /dev/null +++ b/delete_local_branchs.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +# 기본 브랜치 이름 (필요에 따라 변경) +default_branch="main" + +# 원격 브랜치 목록 가져오기 +remote_branches=$(git branch -r | sed 's/ *origin\///') + +# 로컬 브랜치 목록 가져오기 (기본 브랜치 제외) +local_branches=$(git branch | grep -v " $default_branch$" | grep -v "\*") + +# 로컬 브랜치 중 원격에 없는 브랜치 찾기 및 삭제 +for branch in $local_branches; { + branch_name=$(echo $branch | sed 's/ //g') # 앞뒤 공백 제거 + if ! echo "$remote_branches" | grep -qw "$branch_name"; then + echo "Deleting local branch: $branch_name" + git branch -d "$branch_name" + fi +} + diff --git a/frontend/index.html b/frontend/index.html index d46975a5f..768d40f31 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -3,7 +3,7 @@ } /> } /> } /> + } /> + } /> + } /> + } /> }> } /> @@ -67,6 +76,7 @@ function App(): React.ReactElement { } /> } /> } /> + } /> => { } }; +const axiosCoinCheck = "/v5/items/coin"; +export const axiosCoinCheckGet = async (): Promise => { + try { + const response = await instance.get(axiosCoinCheck); + return response; + } catch (error) { + throw error; + } +}; + +export const axiosCoinCheckPost = async (): Promise => { + try { + const response = await instance.post(axiosCoinCheck); + return response; + } catch (error) { + throw error; + } +}; + +const axiosCoinLogURL = "/v5/items/coin/history"; +export const axiosCoinLog = async ( + type: CoinLogToggleType, + page: number, + size: number +): Promise => { + try { + const response = await instance.get(axiosCoinLogURL, { + params: { type: type, page: page, size: size }, + }); + return response; + } catch (error) { + throw error; + } +}; + +const axiosMyItemsURL = "/v5/items/me"; +export const axiosMyItems = async (): Promise => { + try { + const response = await instance.get(axiosMyItemsURL); + return response; + } catch (error) { + throw error; + } +}; + +const axiosItemHistoryURL = "/v5/items/history"; +export const axiosGetItemUsageHistory = async ( + page: number, + size: number +): Promise => { + if (page === null || size === null) return; + try { + const response = await instance.get(axiosItemHistoryURL, { + params: { page: page, size: size }, + }); + return response.data; + } catch (error) { + throw error; + } +}; + +const axiosItemsURL = "/v5/items"; +export const axiosItems = async (): Promise => { + try { + const response = await instance.get(axiosItemsURL); + return response; + } catch (error) { + throw error; + } +}; + +const axiosBuyItemURL = "/v5/items/"; +export const axiosBuyItem = async (sku: String): Promise => { + try { + const response = await instance.post(axiosBuyItemURL + sku + "/purchase"); + return response; + } catch (error) { + throw error; + } +}; + +export const axiosUseItem = async ( + sku: String, + newCabinetId: number | null, + building: string | null, + floor: number | null, + section: string | null +): Promise => { + try { + const response = await instance.post(axiosBuyItemURL + sku + "/use", { + newCabinetId: newCabinetId, + building: building, + floor: floor, + section: section, + }); + return response; + } catch (error) { + throw error; + } +}; + // Admin API const axiosAdminAuthLoginURL = "/v4/admin/auth/login"; export const axiosAdminAuthLogin = async ( @@ -665,6 +767,65 @@ export const axiosLentClubCabinet = async ( } }; +const axiosStatisticsCoinURL = "/v5/admin/statistics/coins"; +export const axiosStatisticsCoin = async () => { + try { + const response = await instance.get(axiosStatisticsCoinURL); + return response; + } catch (error) { + throw error; + } +}; + +const axiosCoinUseStatisticsURL = "/v5/admin/statistics/coins/use"; +export const axiosCoinUseStatistics = async ( + startDate: Date, + endDate: Date +): Promise => { + try { + const response = await instance.get(axiosCoinUseStatisticsURL, { + params: { startDate, endDate }, + }); + return response; + } catch (error) { + throw error; + } +}; + +const axiosStatisticsItemURL = "/v5/admin/statistics/coins"; +export const axiosStatisticsItem = async () => { + try { + const response = await instance.get(axiosStatisticsItemURL); + return response; + } catch (error) { + throw error; + } +}; +const axiosStatisticsTotalItemUseURL = "/v5/admin/statistics/items"; +export const axiosStatisticsTotalItemUse = async () => { + try { + const response = await instance.get(axiosStatisticsTotalItemUseURL); + return response; + } catch (error) { + throw error; + } +}; + +const axiosCoinCollectStatisticsURL = "/v5/admin/statistics/coins/collect"; +export const axiosCoinCollectStatistics = async ( + year: number, + month: number +): Promise => { + try { + const response = await instance.get(axiosCoinCollectStatisticsURL, { + params: { year: year, month: month }, + }); + return response; + } catch (error) { + throw error; + } +}; + const axiosLentShareIdURL = "/v4/lent/cabinets/share/"; export const axiosLentShareId = async ( cabinetId: number | null, @@ -734,3 +895,51 @@ export const axiosSendSlackNotificationToChannel = async ( throw error; } }; + +const axiosItemAssignURL = "v5/admin/items/assign"; +export const axiosItemAssign = async ( + itemSku: string, + userIds: number[] +): Promise => { + try { + const response = await instance.post(axiosItemAssignURL, { + itemSku, + userIds, + }); + return response; + } catch (error) { + throw error; + } +}; + +const axiosItemAssignListURL = "/v5/admin/items/assign"; +export const axiosItemAssignList = async ( + page: number, + size: number +): Promise => { + if (page === null || size === null) return; + try { + const response = await instance.get(axiosItemAssignListURL, { + params: { page: page, size: size }, + }); + return response.data; + } catch (error) { + throw error; + } +}; + +const axiosGetUserItemsURL = "/v5/admin/items/users/"; +export const axiosGetUserItems = async ( + userId: number, + page: number, + size: number +): Promise => { + try { + const response = await instance.get(`${axiosGetUserItemsURL}${userId}`, { + params: { page, size }, + }); + return response; + } catch (error) { + throw error; + } +}; diff --git a/frontend/src/Cabinet/assets/css/media.css b/frontend/src/Cabinet/assets/css/media.css index 967c663b5..9d6b56011 100644 --- a/frontend/src/Cabinet/assets/css/media.css +++ b/frontend/src/Cabinet/assets/css/media.css @@ -45,6 +45,36 @@ } } +/* mapInfo */ +#mapInfo.on { + transform: translateX(0%); +} + +/* storeInfo */ +#storeInfo.on { + transform: translateX(0%); +} + +/* itemInfo */ +#itemInfo.on { + transform: translateX(0%); +} + +/* ClubMemberInfoArea(rightSection) */ +/* #clubMemberInfoArea { + position: fixed; + top: 120px; + right: 0; + height: calc(100% - 120px); + z-index: 9; + transform: translateX(120%); + transition: transform 0.3s ease-in-out; + box-shadow: 0 0 40px 0 var(--bg-shadow); + } */ +#clubMemberInfoArea.on { + transform: translateX(0%); +} + /* search */ @media (max-width: 768px) { #searchBar { diff --git a/frontend/src/Cabinet/assets/data/ColorTheme.ts b/frontend/src/Cabinet/assets/data/ColorTheme.ts index 7df79f904..36aeb2958 100644 --- a/frontend/src/Cabinet/assets/data/ColorTheme.ts +++ b/frontend/src/Cabinet/assets/data/ColorTheme.ts @@ -8,29 +8,30 @@ const lightValues = css` --sys-presentation-main-color: var(--ref-blue-400); /* component variable */ + --white-text-with-bg-color: var(--ref-white); + --card-content-bg-color: var(--ref-white); --bg-color: var(--ref-white); + --card-bg-color: var(--ref-gray-100); + --color-picker-hash-bg-color: var(--ref-gray-200); + --capsule-btn-border-color: var(--ref-gray-200); + --map-floor-color: var(--ref-gray-200); + --presentation-no-event-past-color: var(--ref-gray-200); + --inventory-item-title-border-btm-color: var(--ref-gray-300); + --service-man-title-border-btm-color: var(--ref-gray-300); --line-color: var(--ref-gray-400); + --light-gray-line-btn-color: var(--ref-gray-400); + --gray-line-btn-color: var(--ref-gray-500); + --coin-log-date-color: var(--ref-gray-530); + --pie-chart-label-text-color: var(--ref-gray-600); + --notion-btn-text-color: var(--ref-gray-800); --normal-text-color: var(--ref-black); - --white-text-with-bg-color: var(--ref-white); - --card-content-bg-color: var(--ref-white); + --button-line-color: var(--sys-main-color); - --capsule-btn-border-color: var(--ref-gray-200); --capsule-btn-hover-bg-color: var(--ref-transparent-purple-100); - --presentation-no-event-past-color: var(--ref-gray-200); --presentation-no-event-cur-color: var(--bg-color); --color-picker-bg-color: var(--bg-color); - --color-picker-hash-bg-color: var(--ref-gray-200); - --color-picker-input-color: var(--ref-gray-500); --extension-card-active-btn-color: var(--sys-main-color); - --light-gray-line-btn-color: var(--ref-gray-400); - --card-bg-color: var(--ref-gray-100); - --map-floor-color: var(--ref-gray-200); - --service-man-title-border-btm-color: var(--ref-gray-300); - --toggle-switch-off-bg-color: var(--ref-gray-400); --presentation-card-speaker-name-color: var(--ref-gray-450); - --gray-line-btn-color: var(--ref-gray-500); - --pie-chart-label-text-color: var(--ref-gray-600); - --notion-btn-text-color: var(--ref-gray-800); --table-even-row-bg-color: var(--ref-purple-100); --presentation-table-even-row-bg-color: var(--ref-blue-100); --presentation-dropdown-select-color: var(--ref-blue-150); @@ -57,43 +58,48 @@ const lightValues = css` // 다크모드 변수 const darkValues = css` + /* system variable */ + --sys-main-color: var(--ref-purple-600); + --sys-default-main-color: var(--ref-purple-600); + --sys-default-mine-color: var(--ref-green-200); + --sys-presentation-main-color: var(--ref-blue-430); + + /* component variable */ + --white-text-with-bg-color: var(--ref-gray-100); + --card-content-bg-color: var(--ref-gray-550); --bg-color: var(--ref-gray-900); + --card-bg-color: var(--ref-gray-700); + --color-picker-hash-bg-color: var(--ref-gray-550); + --capsule-btn-border-color: var(--ref-gray-600); + --map-floor-color: var(--ref-gray-700); + --presentation-no-event-past-color: var(--ref-gray-900); + --inventory-item-title-border-btm-color: var(--ref-gray-500); --line-color: var(--ref-gray-500); + --service-man-title-border-btm-color: var(--ref-gray-600); + --light-gray-line-btn-color: var(--ref-gray-600); + --gray-line-btn-color: var(--ref-gray-400); + --coin-log-date-color: var(--ref-gray-400); + --pie-chart-label-text-color: var(--ref-gray-300); + --notion-btn-text-color: var(--ref-gray-200); --normal-text-color: var(--ref-gray-100); - --white-text-with-bg-color: var(--ref-gray-100); - --card-content-bg-color: var(--ref-gray-550); + --button-line-color: var(--sys-main-color); - --capsule-btn-border-color: var(--ref-gray-600); --capsule-btn-hover-bg-color: var(--ref-transparent-purple-200); - --presentation-no-event-past-color: var(--bg-color); --presentation-no-event-cur-color: var(--ref-gray-600); --color-picker-bg-color: var(--ref-gray-530); - --color-picker-hash-bg-color: var(--ref-gray-550); - --color-picker-input-color: var(--ref-gray-400); --extension-card-active-btn-color: var(--gray-line-btn-color); - --light-gray-line-btn-color: var(--service-man-title-border-btm-color); - --card-bg-color: var(--ref-gray-700); - --map-floor-color: var(--ref-gray-700); - --service-man-title-border-btm-color: var(--ref-gray-600); --presentation-card-speaker-name-color: var(--ref-gray-450); - --toggle-switch-off-bg-color: var(--ref-gray-500); - --gray-line-btn-color: var(--ref-gray-400); - --pie-chart-label-text-color: var(--ref-gray-300); - --notion-btn-text-color: var(--ref-gray-200); --table-even-row-bg-color: var(--ref-purple-700); --presentation-table-even-row-bg-color: var(--ref-blue-500); --presentation-dropdown-select-color: var(--ref-blue-470); - --sys-main-color: var(--ref-purple-600); - --sys-default-main-color: var(--ref-purple-600); - --sys-default-mine-color: var(--ref-green-200); - --sys-presentation-main-color: var(--ref-blue-430); - + /* cabinet */ --mine-color: var(--ref-green-200); --available-color: var(--sys-main-color); --pending-color: var(--sys-main-color); --expired-color: var(--ref-red-200); + /* shadow */ --modal-bg-shadow-color: var(--ref-black-shadow-300); --hover-box-shadow-color: var(--ref-black-shadow-400); --login-card-border-shadow-color: var(--ref-black-shadow-300); @@ -110,7 +116,7 @@ const darkValues = css` export const GlobalStyle = createGlobalStyle` :root { ${lightValues} - [color-theme="DARK"] { + [display-style="DARK"] { ${darkValues} } } diff --git a/frontend/src/Cabinet/assets/data/ManualContent.ts b/frontend/src/Cabinet/assets/data/ManualContent.ts index 7e2867301..7e92e6370 100644 --- a/frontend/src/Cabinet/assets/data/ManualContent.ts +++ b/frontend/src/Cabinet/assets/data/ManualContent.ts @@ -1,8 +1,13 @@ +import { ReactComponent as ClockImg } from "@/Cabinet/assets/images/clock.svg"; +import { ReactComponent as ClubIcon } from "@/Cabinet/assets/images/clubIcon.svg"; +import { ReactComponent as ExtensionIcon } from "@/Cabinet/assets/images/extension.svg"; +import { ReactComponent as PrivateIcon } from "@/Cabinet/assets/images/privateIcon.svg"; +import { ReactComponent as ShareIcon } from "@/Cabinet/assets/images/shareIcon.svg"; import ContentStatus from "@/Cabinet/types/enum/content.status.enum"; interface ContentStatusData { contentTitle: string; - imagePath: string; + iconComponent: React.FunctionComponent> | null; background: string; rentalPeriod?: string; capacity?: string; @@ -13,7 +18,7 @@ interface ContentStatusData { export const manualContentData: Record = { [ContentStatus.PRIVATE]: { contentTitle: "개인 사물함", - imagePath: "/src/Cabinet/assets/images/privateIcon.svg", + iconComponent: PrivateIcon, background: "linear-gradient(to bottom, var(--ref-purple-400), var(--ref-purple-600))", rentalPeriod: `${import.meta.env.VITE_PRIVATE_LENT_PERIOD}일`, @@ -35,7 +40,7 @@ export const manualContentData: Record = { }, [ContentStatus.SHARE]: { contentTitle: "공유 사물함", - imagePath: "/src/Cabinet/assets/images/shareIcon.svg", + iconComponent: ShareIcon, background: "linear-gradient(to bottom, var(--ref-blue-200), var(--ref-blue-300))", rentalPeriod: `${import.meta.env.VITE_SHARE_LENT_PERIOD}일 + n * ${ @@ -65,7 +70,7 @@ export const manualContentData: Record = { }, [ContentStatus.CLUB]: { contentTitle: "동아리 사물함", - imagePath: "/src/Cabinet/assets/images/clubIcon.svg", + iconComponent: ClubIcon, background: "linear-gradient(to bottom, var(--ref-pink-100), var(--ref-pink-200))", rentalPeriod: "상세내용 참조", @@ -93,7 +98,7 @@ export const manualContentData: Record = { }, [ContentStatus.PENDING]: { contentTitle: "오픈예정", - imagePath: "", + iconComponent: null, background: "var(--sys-main-color)", contentText: `◦ 상세 내용
사물함 반납 시, 해당 사물함은 오픈예정 상태로 변경됩니다.
@@ -109,7 +114,7 @@ export const manualContentData: Record = { }, [ContentStatus.IN_SESSION]: { contentTitle: "대기중", - imagePath: "/src/Cabinet/assets/images/clock.svg", + iconComponent: ClockImg, background: "var(--card-bg-color)", contentText: `◦ 상세 내용
공유 사물함 대여 시 10분간의 대기 시간이 발생합니다.
@@ -129,7 +134,7 @@ export const manualContentData: Record = { }, [ContentStatus.EXTENSION]: { contentTitle: "연장권 이용방법 안내서", - imagePath: "/src/Cabinet/assets/images/extension.svg", + iconComponent: ExtensionIcon, background: "var(--card-bg-color)", contentText: `◦ 연장권 취득 조건
diff --git a/frontend/src/Cabinet/assets/data/SlackAlarm.ts b/frontend/src/Cabinet/assets/data/SlackAlarm.ts index 123470f87..c89bb73cf 100644 --- a/frontend/src/Cabinet/assets/data/SlackAlarm.ts +++ b/frontend/src/Cabinet/assets/data/SlackAlarm.ts @@ -68,7 +68,7 @@ export const SlackAlarmTemplates: ISlackAlarmTemplate[] = [ :happy_ccabi: 이전 사용자의 짐과 관련한 문의는 데스크에 문의 부탁드립니다! :pushpin: 공유 사물함을 대여했는데 비밀번호는 어디서 알 수 있을까요? :happy_ccabi: 같이 사용하는 사람이 있다면 대여 내역에서 공유 메모에 적혀 있을 수 있습니다. 또는 함께 사용하는 분에게 여쭤보세요! - :pushpin: 사물함을 연체 했는데 패널티는 무엇인가요? + :pushpin: 사물함을 연체 했는데 페널티는 무엇인가요? :happy_ccabi: 연체일만큼 누적 연체일이 증가하고, 누적일 만큼 대여가 불가능합니다:face_holding_back_tears:`, }, { diff --git a/frontend/src/Cabinet/assets/data/mapPositionData.ts b/frontend/src/Cabinet/assets/data/mapPositionData.ts index 72287ecd8..542e63253 100644 --- a/frontend/src/Cabinet/assets/data/mapPositionData.ts +++ b/frontend/src/Cabinet/assets/data/mapPositionData.ts @@ -297,3 +297,9 @@ export const mapPostionData: IFloorSectionsInfo = { }, ], }; + +export const clubSectionsData = [ + "Cluster X - 1", + "Cluster X - 2", + "Cluster X - 3", +]; diff --git a/frontend/src/Cabinet/assets/data/maps.ts b/frontend/src/Cabinet/assets/data/maps.ts index 41f6f425e..8706697ba 100644 --- a/frontend/src/Cabinet/assets/data/maps.ts +++ b/frontend/src/Cabinet/assets/data/maps.ts @@ -1,8 +1,17 @@ import { ReactComponent as ClubIcon } from "@/Cabinet/assets/images/clubIcon.svg"; +import { ReactComponent as ExtensionImg } from "@/Cabinet/assets/images/extension.svg"; import { ReactComponent as PrivateIcon } from "@/Cabinet/assets/images/privateIcon.svg"; import { ReactComponent as ShareIcon } from "@/Cabinet/assets/images/shareIcon.svg"; +import { ReactComponent as AlarmImg } from "@/Cabinet/assets/images/storeAlarm.svg"; +import { ReactComponent as SwapImg } from "@/Cabinet/assets/images/storeMove.svg"; +import { ReactComponent as PenaltyImg } from "@/Cabinet/assets/images/storePenalty.svg"; import CabinetStatus from "@/Cabinet/types/enum/cabinet.status.enum"; import CabinetType from "@/Cabinet/types/enum/cabinet.type.enum"; +import { + StoreExtensionType, + StoreItemType, + StorePenaltyType, +} from "@/Cabinet/types/enum/store.enum"; export enum additionalModalType { MODAL_RETURN = "MODAL_RETURN", @@ -141,7 +150,7 @@ export const modalPropsMap = { }, MODAL_OVERDUE_PENALTY: { type: "error", - title: "패널티 안내", + title: "페널티 안내", confirmMessage: "오늘 하루동안 보지않기", }, MODAL_INVITATION_CODE: { @@ -169,6 +178,7 @@ export const modalPropsMap = { title: "이사하기", confirmMessage: "네, 이사할게요", }, + MODAL_CLUB_ADD_MEM: { type: "confirm", title: "동아리 멤버 추가", @@ -194,6 +204,16 @@ export const modalPropsMap = { title: "비밀번호 수적", confirmMessage: "입력", }, + MODAL_STORE_SWAP: { + type: "confirm", + title: "이사권 사용 안내", + confirmMessage: "네, 사용할게요", + }, + MODAL_SECTION_ALERT: { + type: "confirm", + title: "알림 등록권 사용 안내", + confirmMessage: "네, 사용할게요", + }, }; export const cabinetFilterMap = { @@ -221,3 +241,29 @@ export const cabinetTypeLabelMap = { [CabinetType.PRIVATE]: "개인 사물함", [CabinetType.SHARE]: "공유 사물함", }; + +export const ItemIconMap = { + [StoreItemType.EXTENSION]: ExtensionImg, + [StoreItemType.SWAP]: SwapImg, + [StoreItemType.ALARM]: AlarmImg, + [StoreItemType.PENALTY]: PenaltyImg, +}; + +export const ItemTypeLabelMap = { + [StoreItemType.EXTENSION]: "연장권", + [StoreItemType.SWAP]: "이사권", + [StoreItemType.ALARM]: "알림 등록권", + [StoreItemType.PENALTY]: "페널티 감면권", +}; + +export const ItemTypePenaltyMap = { + [StorePenaltyType.PENALTY_3]: "3일", + [StorePenaltyType.PENALTY_7]: "7일", + [StorePenaltyType.PENALTY_31]: "31일", +}; + +export const ItemTypeExtensionMap = { + [StoreExtensionType.EXTENSION_3]: "3일", + [StoreExtensionType.EXTENSION_15]: "15일", + [StoreExtensionType.EXTENSION_31]: "31일", +}; diff --git a/frontend/src/Cabinet/assets/images/clubIcon.svg b/frontend/src/Cabinet/assets/images/clubIcon.svg index 2f9b7a73d..c5b48c59a 100644 --- a/frontend/src/Cabinet/assets/images/clubIcon.svg +++ b/frontend/src/Cabinet/assets/images/clubIcon.svg @@ -1,7 +1,7 @@ - - - - - - + + + + + + diff --git a/frontend/src/Cabinet/assets/images/coinIcon.svg b/frontend/src/Cabinet/assets/images/coinIcon.svg new file mode 100644 index 000000000..7e1fb4f12 --- /dev/null +++ b/frontend/src/Cabinet/assets/images/coinIcon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/src/Cabinet/assets/images/crown.svg b/frontend/src/Cabinet/assets/images/crown.svg index 9bed5a2d3..537db45f4 100644 --- a/frontend/src/Cabinet/assets/images/crown.svg +++ b/frontend/src/Cabinet/assets/images/crown.svg @@ -1,3 +1,3 @@ - - + + diff --git a/frontend/src/Cabinet/assets/images/dollar-circle.svg b/frontend/src/Cabinet/assets/images/dollar-circle.svg new file mode 100644 index 000000000..3fc1d9cf2 --- /dev/null +++ b/frontend/src/Cabinet/assets/images/dollar-circle.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/src/Cabinet/assets/images/dropdownChevron.svg b/frontend/src/Cabinet/assets/images/dropdownChevron.svg index 2917ac0ff..efcdd6b14 100644 --- a/frontend/src/Cabinet/assets/images/dropdownChevron.svg +++ b/frontend/src/Cabinet/assets/images/dropdownChevron.svg @@ -1,3 +1,3 @@ - + diff --git a/frontend/src/Cabinet/assets/images/extension.svg b/frontend/src/Cabinet/assets/images/extension.svg index 3611f0a48..478a4c1f0 100644 --- a/frontend/src/Cabinet/assets/images/extension.svg +++ b/frontend/src/Cabinet/assets/images/extension.svg @@ -1,4 +1,4 @@ - + diff --git a/frontend/src/Cabinet/assets/images/filledHeart.svg b/frontend/src/Cabinet/assets/images/filledHeart.svg new file mode 100644 index 000000000..d2a85e863 --- /dev/null +++ b/frontend/src/Cabinet/assets/images/filledHeart.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/Cabinet/assets/images/leader.svg b/frontend/src/Cabinet/assets/images/leader.svg deleted file mode 100644 index 627d7d003..000000000 --- a/frontend/src/Cabinet/assets/images/leader.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/src/Cabinet/assets/images/lineHeart.svg b/frontend/src/Cabinet/assets/images/lineHeart.svg new file mode 100644 index 000000000..fd45be7a7 --- /dev/null +++ b/frontend/src/Cabinet/assets/images/lineHeart.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/Cabinet/assets/images/privateIcon.svg b/frontend/src/Cabinet/assets/images/privateIcon.svg index 90935b75a..4517cc27a 100644 --- a/frontend/src/Cabinet/assets/images/privateIcon.svg +++ b/frontend/src/Cabinet/assets/images/privateIcon.svg @@ -1,4 +1,4 @@ - + diff --git a/frontend/src/Cabinet/assets/images/shareIcon.svg b/frontend/src/Cabinet/assets/images/shareIcon.svg index 6ecff41ea..1bdfd8f4d 100644 --- a/frontend/src/Cabinet/assets/images/shareIcon.svg +++ b/frontend/src/Cabinet/assets/images/shareIcon.svg @@ -1,4 +1,4 @@ - + diff --git a/frontend/src/Cabinet/assets/images/storeAlarm.svg b/frontend/src/Cabinet/assets/images/storeAlarm.svg new file mode 100644 index 000000000..fdec88cdb --- /dev/null +++ b/frontend/src/Cabinet/assets/images/storeAlarm.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/src/Cabinet/assets/images/storeCoin.svg b/frontend/src/Cabinet/assets/images/storeCoin.svg new file mode 100644 index 000000000..2fe40696a --- /dev/null +++ b/frontend/src/Cabinet/assets/images/storeCoin.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/src/Cabinet/assets/images/storeCoinCheckFin.svg b/frontend/src/Cabinet/assets/images/storeCoinCheckFin.svg new file mode 100644 index 000000000..2e5c1f371 --- /dev/null +++ b/frontend/src/Cabinet/assets/images/storeCoinCheckFin.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/frontend/src/Cabinet/assets/images/storeCoinCheckOff.svg b/frontend/src/Cabinet/assets/images/storeCoinCheckOff.svg new file mode 100644 index 000000000..557b98e8e --- /dev/null +++ b/frontend/src/Cabinet/assets/images/storeCoinCheckOff.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/src/Cabinet/assets/images/storeCoinCheckOn.svg b/frontend/src/Cabinet/assets/images/storeCoinCheckOn.svg new file mode 100644 index 000000000..51cb824a0 --- /dev/null +++ b/frontend/src/Cabinet/assets/images/storeCoinCheckOn.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/frontend/src/Cabinet/assets/images/storeCoinNav.svg b/frontend/src/Cabinet/assets/images/storeCoinNav.svg new file mode 100644 index 000000000..206c5d49b --- /dev/null +++ b/frontend/src/Cabinet/assets/images/storeCoinNav.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/src/Cabinet/assets/images/storeExtension.svg b/frontend/src/Cabinet/assets/images/storeExtension.svg new file mode 100644 index 000000000..91c16096c --- /dev/null +++ b/frontend/src/Cabinet/assets/images/storeExtension.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/src/Cabinet/assets/images/storeIconGray.svg b/frontend/src/Cabinet/assets/images/storeIconGray.svg new file mode 100644 index 000000000..5400d92fb --- /dev/null +++ b/frontend/src/Cabinet/assets/images/storeIconGray.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/frontend/src/Cabinet/assets/images/storeMove.svg b/frontend/src/Cabinet/assets/images/storeMove.svg new file mode 100644 index 000000000..4500e4867 --- /dev/null +++ b/frontend/src/Cabinet/assets/images/storeMove.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/frontend/src/Cabinet/assets/images/storePenalty.svg b/frontend/src/Cabinet/assets/images/storePenalty.svg new file mode 100644 index 000000000..158760b33 --- /dev/null +++ b/frontend/src/Cabinet/assets/images/storePenalty.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/src/Cabinet/components/AdminInfo/Chart/CoinUseLineChart.tsx b/frontend/src/Cabinet/components/AdminInfo/Chart/CoinUseLineChart.tsx new file mode 100644 index 000000000..80eb28c0b --- /dev/null +++ b/frontend/src/Cabinet/components/AdminInfo/Chart/CoinUseLineChart.tsx @@ -0,0 +1,131 @@ +import { ResponsiveLine } from "@nivo/line"; +import styled from "styled-components"; +import { ICoinStatisticsDto } from "@/Cabinet/types/dto/admin.dto"; +import { CoinUseDateType, CoinUseType } from "@/Cabinet/types/enum/store.enum"; + +const CoinUseLineChart = ({ + toggleType, + coinToggleType, + totalCoinUseData, +}: { + toggleType: CoinUseDateType; + coinToggleType: CoinUseType; + totalCoinUseData: ICoinStatisticsDto | undefined; +}) => { + if (totalCoinUseData === undefined) { + return null; + } + const formattedData = [ + { + id: "issuedCoin", + data: + totalCoinUseData?.issuedCoin?.map((item) => ({ + x: item.date, + y: item.amount, + })) || [], + }, + { + id: "usedCoin", + data: + totalCoinUseData?.usedCoin?.map((item) => ({ + x: item.date, + y: item.amount, + })) || [], + }, + ]; + + // 발행코인, 미사용 코인, 사용코인 나눠서 보내주는 함수 + const filteredData = formattedData.filter( + (data) => data.id === coinToggleType + ); + + return ( + <> + + { + return ( + + +

+ date : {point.point.data.xFormatted} +

+

+ , coin : {point.point.data.yFormatted} +

+ + ); + }} + isInteractive={true} + animate={true} + data={filteredData} + margin={{ top: 50, right: 60, bottom: 50, left: 60 }} + // xFormat="time:%b %d" + // %b -> 영어로 달 표시 + xFormat="time:%m.%d" + xScale={{ + format: "%Y-%m-%d", + precision: "day", + type: "time", + useUTC: false, + }} + yScale={{ + type: "linear", + min: 0, + max: "auto", + }} + yFormat=" >0" + // curve="cardinal" + curve="monotoneX" + axisTop={null} + colors={["var(--sys-main-color)"]} + axisRight={null} + axisBottom={{ + format: "%m.%d", + legendOffset: -12, + tickValues: `every 1 ${toggleType.toLowerCase()}`, + }} + axisLeft={{ + legendOffset: 12, + }} + enableGridX={false} + pointSize={0} + enableArea={true} + useMesh={true} + /> + + + ); +}; + +const LineChartStyled = styled.div` + height: 90%; + width: 90%; + display: flex; + justify-content: center; + align-items: center; +`; + +const ToolTipStyled = styled.div<{ color: string }>` + height: 24px; + background-color: var(--bg-color); + box-shadow: var(--left-nav-border-shadow-color) 0 1px 2px; + color: var(--normal-text-color); + display: flex; + align-items: center; + padding: 5px 9px; + border-radius: 2px; + + & > span { + display: block; + width: 12px; + height: 12px; + background-color: ${(props) => props.color}; + margin-right: 8px; + } +`; + +export default CoinUseLineChart; diff --git a/frontend/src/Cabinet/components/AdminInfo/Chart/ItemBarChart.tsx b/frontend/src/Cabinet/components/AdminInfo/Chart/ItemBarChart.tsx new file mode 100644 index 000000000..a85bfb3c7 --- /dev/null +++ b/frontend/src/Cabinet/components/AdminInfo/Chart/ItemBarChart.tsx @@ -0,0 +1,163 @@ +import { ResponsiveBar } from "@nivo/bar"; +import { useEffect, useState } from "react"; +import styled from "styled-components"; +import { IItemUseCountDto } from "@/Cabinet/types/dto/admin.dto"; + +interface ITransformedItem { + item: string; + [key: string]: number | string; +} + +const transformData = (itemArr: IItemUseCountDto[]): ITransformedItem[] => { + const transformedData: ITransformedItem[] = []; + + itemArr.forEach((item) => { + const { itemName, itemDetails, userCount } = item; + const existingItem = transformedData.find( + (transformed) => transformed.item === itemName + ); + + if (existingItem) { + existingItem[`${itemName}-${itemDetails}`] = userCount; + } else { + const newItem: ITransformedItem = { + item: itemName, + }; + + if (itemName === itemDetails) { + newItem[itemDetails] = userCount; + } else { + newItem[`${itemName}-${itemDetails}`] = userCount; + } + + transformedData.push(newItem); + } + }); + + return transformedData; +}; + +const ItemBarChart = ({ data }: { data: IItemUseCountDto[] }) => { + const itemData: ITransformedItem[] = transformData(data); + return ( + + + + e.id + ": " + e.formattedValue + " in item: " + e.indexValue + } + minValue={0} + /> + + + + + ); +}; + +const ItemBarChartStyled = styled.div` + height: 90%; + width: 100%; + display: flex; + justify-content: center; + align-items: center; + @media screen and (min-width: 768px) { + padding-right: 60px; + padding-left: 60px; + } +`; + +const ResponsiveBarStyled = styled.div` + height: 100%; + width: 100%; +`; + +const ItemLegendsStyled = styled.div` + height: 100%; + width: 0%; + background-color: #d5d5ff; +`; + +export default ItemBarChart; + +// const itemList: IItemUseCountDto[] = [ +// { itemName: "연장권", itemDetails: "출석 연장권 보상", userCount: 0 }, +// { itemName: "연장권", itemDetails: "31일", userCount: 530 }, +// { itemName: "연장권", itemDetails: "15일", userCount: 22 }, +// { itemName: "연장권", itemDetails: "3일", userCount: 3}, +// { itemName: "페널티 감면권", itemDetails: "31일", userCount: 1 }, +// { itemName: "페널티 감면권", itemDetails: "7일", userCount: 30 }, +// { itemName: "페널티 감면권", itemDetails: "3일", userCount: 10 }, +// { itemName: "이사권", itemDetails: "이사권", userCount: 60 }, +// { itemName: "알림 등록권", itemDetails: "알림 등록권", userCount: 100 }, +// ]; diff --git a/frontend/src/Cabinet/components/AdminInfo/Chart/PieChartCoin.tsx b/frontend/src/Cabinet/components/AdminInfo/Chart/PieChartCoin.tsx new file mode 100644 index 000000000..5ee1ccfbb --- /dev/null +++ b/frontend/src/Cabinet/components/AdminInfo/Chart/PieChartCoin.tsx @@ -0,0 +1,117 @@ +import { ResponsivePie } from "@nivo/pie"; +import styled from "styled-components"; + +// make sure parent container have a defined height when using +// responsive component, otherwise height will be 0 and +// no chart will be rendered. +// website examples showcase many properties, +// you'll often use just a few of them. + +interface ICoinInfo { + used: number; + unused: number; +} + +type CoinStatus = { + [used: string]: number; + unused: number; +}; + +type TextMap = { + [used: string]: string; + unused: string; +}; + +const convert = (data: ICoinInfo[]) => { + const textMap: TextMap = { + used: "사용", + unused: "보유", + }; + const obj: CoinStatus = data.reduce( + (acc, cur) => { + acc.used += cur.used; + acc.unused += cur.unused; + return acc; + }, + { used: 0, unused: 0 } as CoinStatus + ); + return Object.keys(obj).map((key: string) => ({ + id: textMap[key], + value: obj[key], + })); +}; + +const PieChartCoin = ({ data }: { data: ICoinInfo[] }) => { + return ( + + + + ); +}; + +const PieChartStyled = styled.div` + height: 90%; + width: 90%; + display: flex; + justify-content: center; + align-items: center; +`; +export default PieChartCoin; diff --git a/frontend/src/Cabinet/components/AdminInfo/Chart/StoreHorizontalBarChart.tsx b/frontend/src/Cabinet/components/AdminInfo/Chart/StoreHorizontalBarChart.tsx new file mode 100644 index 000000000..b35a86480 --- /dev/null +++ b/frontend/src/Cabinet/components/AdminInfo/Chart/StoreHorizontalBarChart.tsx @@ -0,0 +1,143 @@ +import { ResponsiveBar } from "@nivo/bar"; +import styled from "styled-components"; +import { ICoinCollectInfoDto } from "@/Cabinet/types/dto/store.dto"; + +const keys = ["5회", "10회", "15회", "20회", "전체"]; + +interface IConvertedData { + cnt: string; + [key: string]: number | string; +} + +const convert = (data: ICoinCollectInfoDto[]) => { + let userTotalPerCnt = 0; + let userTotal = 0; + let ary: IConvertedData[] = []; + + data.forEach((cur) => { + userTotalPerCnt += cur.userCount; + + // NOTE : 정책에 따라 변경 + // 현재 정책 - 0~5, 6~10, 11~15, 16~20회 + if ( + cur.coinCount % 5 === 0 && + cur.coinCount / 5 >= 1 && + cur.coinCount / 5 <= 4 + ) { + ary = [ + ...ary, + { + cnt: cur.coinCount + "회", + [cur.coinCount + "회"]: userTotalPerCnt, + }, + ]; + userTotal += userTotalPerCnt; + userTotalPerCnt = 0; + } + }); + + return [...ary, { cnt: "전체", ["전체"]: userTotal }]; +}; + +const StoreHorizontalBarChart = ({ data }: { data: ICoinCollectInfoDto[] }) => { + return ( + + { + return ( + + + {point.data.cnt + ":"} {point.value}명 + + ); + }} + theme={{ + legends: { text: { fontSize: "14px" } }, + labels: { text: { fontSize: "14px", fill: "var(--ref-gray-500)" } }, + axis: { ticks: { text: { fontSize: "14px" } } }, + textColor: "var(--normal-text-color)", + }} + margin={{ top: 20, right: 70, bottom: 80, left: 80 }} + colors={[ + "var(--ref-purple-200)", + "var(--ref-purple-300)", + "var(--sys-main-color)", + "var(--ref-purple-650)", + "var(--ref-purple-690)", + "var(--ref-purple-200)", + ]} + legends={[ + { + dataFrom: "keys", + anchor: "bottom", + direction: "row", + itemWidth: 56, + itemHeight: 0, + itemTextColor: "var(--gray-line-btn-color)", + translateX: 0, + translateY: 44, + itemsSpacing: 2, + itemDirection: "top-to-bottom", + symbolSize: 12, + effects: [ + { + on: "hover", + style: { + itemTextColor: "var(--normal-text-color)", + }, + }, + ], + }, + ]} + labelSkipWidth={1} + /> + + ); +}; + +const HalfPieChartStyled = styled.div` + height: 80%; + width: 90%; + display: flex; + justify-content: center; + align-items: center; + + @media screen and (max-width: 768px) { + width: 100%; + } +`; + +const ToolTipStyled = styled.div<{ color: string }>` + height: 24px; + background-color: var(--bg-color); + box-shadow: var(--left-nav-border-shadow-color) 0 1px 2px; + color: var(--normal-text-color); + display: flex; + align-items: center; + padding: 5px 9px; + border-radius: 2px; + + & > span { + display: block; + width: 12px; + height: 12px; + background-color: ${(props) => props.color}; + margin-right: 8px; + } + & > strong { + padding-left: 4px; + } +`; + +export default StoreHorizontalBarChart; diff --git a/frontend/src/Cabinet/components/AdminInfo/Table/AdminTable.tsx b/frontend/src/Cabinet/components/AdminInfo/Table/AdminTable.tsx index 68bc71ed0..968e7e752 100644 --- a/frontend/src/Cabinet/components/AdminInfo/Table/AdminTable.tsx +++ b/frontend/src/Cabinet/components/AdminInfo/Table/AdminTable.tsx @@ -104,7 +104,7 @@ const TheadStyled = styled.thead` height: 45px; line-height: 45px; background-color: var(--sys-main-color); - color: var(--bg-color); + color: var(--white-text-with-bg-color); `; const TbodyStyled = styled.tbody` diff --git a/frontend/src/Cabinet/components/Available/FloorContainer.tsx b/frontend/src/Cabinet/components/Available/FloorContainer.tsx index 0f91d0992..875309a41 100644 --- a/frontend/src/Cabinet/components/Available/FloorContainer.tsx +++ b/frontend/src/Cabinet/components/Available/FloorContainer.tsx @@ -3,8 +3,9 @@ import { useLocation } from "react-router-dom"; import styled from "styled-components"; import AdminCabinetListItem from "@/Cabinet/components/CabinetList/CabinetListItem/AdminCabinetListItem"; import CabinetListItem from "@/Cabinet/components/CabinetList/CabinetListItem/CabinetListItem"; -import { CabinetPreviewInfo } from "@/Cabinet/types/dto/cabinet.dto"; +import UnavailableDataInfo from "@/Cabinet/components/Common/UnavailableDataInfo"; import { ReactComponent as SelectImg } from "@/Cabinet/assets/images/select.svg"; +import { CabinetPreviewInfo } from "@/Cabinet/types/dto/cabinet.dto"; // 하나의 층에 대한 타이틀과 캐비넷 리스트를 담고 있는 컴포넌트 const FloorContainer = ({ @@ -21,13 +22,12 @@ const FloorContainer = ({ const toggle = () => { setIsToggled(!isToggled); }; - return (

{floorNumber}층

{pendingCabinetsList.length !== 0 ? ( @@ -41,13 +41,14 @@ const FloorContainer = ({ )} ) : ( - -

해당 층에는 사용 가능한 사물함이 없습니다

- noAvailable -
+ !isToggled && ( + + + + ) )}
); @@ -67,6 +68,7 @@ const FloorTitleStyled = styled.div<{ isToggled: boolean }>` padding-right: 5px; border-bottom: 1.5px solid var(--service-man-title-border-btm-color); cursor: pointer; + button { all: initial; cursor: inherit; @@ -75,6 +77,10 @@ const FloorTitleStyled = styled.div<{ isToggled: boolean }>` transform: ${(props) => props.isToggled ? "rotate(180deg)" : "rotate(0deg)"}; } + + & > button > svg > path { + stroke: var(--gray-line-btn-color); + } `; const FloorCabinetsContainerStyled = styled.div<{ isToggled: boolean }>` @@ -89,21 +95,11 @@ const FloorCabinetsContainerStyled = styled.div<{ isToggled: boolean }>` } `; -const NoAvailableCabinetMessageStyled = styled.div<{ isToggled: boolean }>` - display: ${(props) => (props.isToggled ? "none" : "flex")}; - align-items: center; - margin-top: 20px; - margin-left: 5px; - p { - color: var(--gray-line-btn-color); - line-height: 1.5; - word-break: keep-all; - } - img { - width: 30px; - aspect-ratio: 1 / 1; - margin-left: 8px; - } +const UnavailableCabinetMsgWrapperStyled = styled.div` + width: 100%; + display: flex; + padding-top: 20px; + padding-left: 5px; `; export default FloorContainer; diff --git a/frontend/src/Cabinet/components/CabinetInfoArea/AdminCabinetInfoArea.tsx b/frontend/src/Cabinet/components/CabinetInfoArea/AdminCabinetInfoArea.tsx index 8caf51754..018fb5d33 100644 --- a/frontend/src/Cabinet/components/CabinetInfoArea/AdminCabinetInfoArea.tsx +++ b/frontend/src/Cabinet/components/CabinetInfoArea/AdminCabinetInfoArea.tsx @@ -12,6 +12,7 @@ import { TAdminModalState, } from "@/Cabinet/components/CabinetInfoArea/CabinetInfoArea.container"; import ButtonContainer from "@/Cabinet/components/Common/Button"; +import SelectInduction from "@/Cabinet/components/Common/SelectInduction"; import ClubLentModal from "@/Cabinet/components/Modals/LentModal/ClubLentModal"; import AdminReturnModal from "@/Cabinet/components/Modals/ReturnModal/AdminReturnModal"; import StatusModalContainer from "@/Cabinet/components/Modals/StatusModal/StatusModal.container"; @@ -66,15 +67,10 @@ const AdminCabinetInfoArea: React.FC<{ (multiSelectTargetInfo && targetCabinetInfoList!.length < 1) ) return ( - - - - - - 사물함/유저를
- 선택해주세요 -
-
+ ); // 다중 선택 모드 진입 후 캐비넷을 하나 이상 선택했을 시 if (multiSelectTargetInfo) { @@ -196,14 +192,6 @@ const AdminCabinetInfoArea: React.FC<{ ); }; -const NotSelectedStyled = styled.div` - height: 100%; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; -`; - const CabinetDetailAreaStyled = styled.div` height: 100%; display: flex; @@ -212,17 +200,6 @@ const CabinetDetailAreaStyled = styled.div` align-items: center; `; -const CabiLogoStyled = styled.div` - width: 35px; - height: 35px; - margin-bottom: 10px; - svg { - .logo_svg__currentPath { - fill: var(--sys-main-color); - } - } -`; - const CabinetTypeIconStyled = styled.div` width: 24px; height: 24px; @@ -233,6 +210,11 @@ const CabinetTypeIconStyled = styled.div` & path { stroke: var(--normal-text-color); } + + & > svg { + width: 24px; + height: 24px; + } `; const LinkTextStyled = styled.div` diff --git a/frontend/src/Cabinet/components/CabinetInfoArea/CabinetInfoArea.tsx b/frontend/src/Cabinet/components/CabinetInfoArea/CabinetInfoArea.tsx index 09be27808..8ec38283a 100644 --- a/frontend/src/Cabinet/components/CabinetInfoArea/CabinetInfoArea.tsx +++ b/frontend/src/Cabinet/components/CabinetInfoArea/CabinetInfoArea.tsx @@ -7,6 +7,7 @@ import { } from "@/Cabinet/components/CabinetInfoArea/CabinetInfoArea.container"; import CountTimeContainer from "@/Cabinet/components/CabinetInfoArea/CountTime/CountTime.container"; import ButtonContainer from "@/Cabinet/components/Common/Button"; +import SelectInduction from "@/Cabinet/components/Common/SelectInduction"; import CancelModal from "@/Cabinet/components/Modals/CancelModal/CancelModal"; import ExtendModal from "@/Cabinet/components/Modals/ExtendModal/ExtendModal"; import InvitationCodeModalContainer from "@/Cabinet/components/Modals/InvitationCodeModal/InvitationCodeModal.container"; @@ -23,7 +24,7 @@ import { cabinetStatusColorMap, } from "@/Cabinet/assets/data/maps"; import alertImg from "@/Cabinet/assets/images/cautionSign.svg"; -import { ReactComponent as ExtensionImg } from "@/Cabinet/assets/images/extensionTicket.svg"; +import { ReactComponent as ExtensionImg } from "@/Cabinet/assets/images/extension.svg"; import { ReactComponent as LogoImg } from "@/Cabinet/assets/images/logo.svg"; import CabinetStatus from "@/Cabinet/types/enum/cabinet.status.enum"; @@ -50,11 +51,8 @@ const CabinetInfoArea: React.FC<{ closeModal, isSwappable, }) => { - const isExtensionVisible = - isMine && - isExtensible && - selectedCabinetInfo && - selectedCabinetInfo.status !== "IN_SESSION"; + const isExtensionVisible = isMine && selectedCabinetInfo; + // selectedCabinetInfo.status !== "IN_SESSION"; const isHoverBoxVisible = selectedCabinetInfo && selectedCabinetInfo.lentsLength <= 1 && @@ -64,15 +62,10 @@ const CabinetInfoArea: React.FC<{ : null; return selectedCabinetInfo === null ? ( - - - - - - 사물함을
- 선택해주세요 -
-
+ ) : ( @@ -193,13 +186,10 @@ const CabinetInfoArea: React.FC<{ selectedCabinetInfo.lentType === "SHARE" } > - - {"연장권 사용하기"} + + + + 연장권 사용하기 )} {isExtensionVisible && isHoverBoxVisible && ( @@ -272,14 +262,6 @@ const CabinetInfoArea: React.FC<{ ); }; -const NotSelectedStyled = styled.div` - height: 100%; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; -`; - const CabinetDetailAreaStyled = styled.div` height: 100%; max-width: 330px; @@ -289,17 +271,6 @@ const CabinetDetailAreaStyled = styled.div` align-items: center; `; -const CabiLogoStyled = styled.div` - width: 35px; - height: 35px; - margin-bottom: 10px; - svg { - .logo_svg__currentPath { - fill: var(--sys-main-color); - } - } -`; - const CabinetTypeIconStyled = styled.div` width: 24px; height: 24px; @@ -310,6 +281,11 @@ const CabinetTypeIconStyled = styled.div` & path { stroke: var(--normal-text-color); } + + & > svg { + width: 24px; + height: 24px; + } `; const TextStyled = styled.p<{ fontSize: string; fontColor: string }>` @@ -460,6 +436,21 @@ const ButtonContainerStyled = styled.button` @media (max-height: 745px) { margin-bottom: 8px; } + + & > span { + height: 20px; + line-height: 18px; + } +`; + +const ExtensionImgStyled = styled.div` + width: 24px; + height: 24px; + margin-right: 10px; + + & > svg > path { + stroke: var(--sys-main-color); + } `; export default CabinetInfoArea; diff --git a/frontend/src/Cabinet/components/CabinetInfoArea/CountTime/CountTime.container.tsx b/frontend/src/Cabinet/components/CabinetInfoArea/CountTime/CountTime.container.tsx index b69de2ce9..55ae40cf3 100644 --- a/frontend/src/Cabinet/components/CabinetInfoArea/CountTime/CountTime.container.tsx +++ b/frontend/src/Cabinet/components/CabinetInfoArea/CountTime/CountTime.container.tsx @@ -12,14 +12,13 @@ import { axiosCabinetById, axiosMyLentInfo, } from "@/Cabinet/api/axios/axios.custom"; +import { padTo2Digits } from "@/Cabinet/utils/dateUtils"; const returnCountTime = (countDown: number) => { - const minutes = Math.floor((countDown % (1000 * 60 * 60)) / (1000 * 60)) - .toString() - .padStart(2, "0"); - const seconds = Math.floor((countDown % (1000 * 60)) / 1000) - .toString() - .padStart(2, "0"); + const minutes = padTo2Digits( + Math.floor((countDown % (1000 * 60 * 60)) / (1000 * 60)) + ); + const seconds = padTo2Digits(Math.floor((countDown % (1000 * 60)) / 1000)); return [minutes, seconds]; }; diff --git a/frontend/src/Cabinet/components/CabinetList/CabinetListItem/AdminCabinetListItem.tsx b/frontend/src/Cabinet/components/CabinetList/CabinetListItem/AdminCabinetListItem.tsx index 5fba4ff25..186e0732b 100644 --- a/frontend/src/Cabinet/components/CabinetList/CabinetListItem/AdminCabinetListItem.tsx +++ b/frontend/src/Cabinet/components/CabinetList/CabinetListItem/AdminCabinetListItem.tsx @@ -17,6 +17,7 @@ import { } from "@/Cabinet/types/dto/cabinet.dto"; import CabinetStatus from "@/Cabinet/types/enum/cabinet.status.enum"; import CabinetType from "@/Cabinet/types/enum/cabinet.type.enum"; +import CabinetDetailAreaType from "@/Cabinet/types/enum/cabinetDetailArea.type.enum"; import { axiosCabinetById } from "@/Cabinet/api/axios/axios.custom"; import useMenu from "@/Cabinet/hooks/useMenu"; import useMultiSelect from "@/Cabinet/hooks/useMultiSelect"; @@ -28,7 +29,7 @@ const AdminCabinetListItem = (props: CabinetPreviewInfo): JSX.Element => { const setTargetCabinetInfo = useSetRecoilState( targetCabinetInfoState ); - const setSelectedTypeOnSearch = useSetRecoilState( + const setSelectedTypeOnSearch = useSetRecoilState( selectedTypeOnSearchState ); const { openCabinet, closeCabinet } = useMenu(); @@ -64,7 +65,7 @@ const AdminCabinetListItem = (props: CabinetPreviewInfo): JSX.Element => { } setCurrentCabinetId(cabinetId); - setSelectedTypeOnSearch("CABINET"); + setSelectedTypeOnSearch(CabinetDetailAreaType.CABINET); async function getData(cabinetId: number) { try { const { data } = await axiosCabinetById(cabinetId); @@ -253,7 +254,11 @@ const CabinetIconContainerStyled = styled.div<{ & > svg > path { stroke: ${(props) => cabinetFilterMap[props.status]}; - transform: scale(0.7); + } + + & > svg { + width: 16px; + height: 16px; } `; diff --git a/frontend/src/Cabinet/components/CabinetList/CabinetListItem/CabinetListItem.tsx b/frontend/src/Cabinet/components/CabinetList/CabinetListItem/CabinetListItem.tsx index 4aaac3ccd..f65ec3cba 100644 --- a/frontend/src/Cabinet/components/CabinetList/CabinetListItem/CabinetListItem.tsx +++ b/frontend/src/Cabinet/components/CabinetList/CabinetListItem/CabinetListItem.tsx @@ -292,7 +292,11 @@ const CabinetIconContainerStyled = styled.div<{ & > svg > path { stroke: ${(props) => cabinetFilterMap[props.status]}; - transform: scale(0.7); + } + + & > svg { + width: 16px; + height: 16px; } `; diff --git a/frontend/src/Cabinet/components/Card/Card.tsx b/frontend/src/Cabinet/components/Card/Card.tsx index e5357800a..17bb10f24 100644 --- a/frontend/src/Cabinet/components/Card/Card.tsx +++ b/frontend/src/Cabinet/components/Card/Card.tsx @@ -19,6 +19,7 @@ interface CardProps { gridArea: string; width?: string; height?: string; + cardType?: string; } const Card = ({ @@ -29,11 +30,12 @@ const Card = ({ height = "163px", buttons = ([] = []), children, + cardType, }: CardProps) => { return ( {(title || buttons.length > 0) && ( - + {title && {title}} {onClickToolTip && } @@ -77,12 +79,13 @@ export const CardStyled = styled.div<{ grid-area: ${(props) => props.gridArea}; `; -export const CardHeaderStyled = styled.div` +export const CardHeaderStyled = styled.div<{ cardType?: string }>` width: 100%; display: flex; justify-content: space-between; align-items: center; - padding: 20px 20px 10px 30px; + padding: ${(props) => + props.cardType === "store" ? "12px 18px 12px 18px" : "20px 20px 10px 30px"}; `; const CardTitleWrapperStyled = styled.div` diff --git a/frontend/src/Cabinet/components/Card/ClubCabinetInfoCard/ClubCabinetInfoCard.tsx b/frontend/src/Cabinet/components/Card/ClubCabinetInfoCard/ClubCabinetInfoCard.tsx index 6a421da3c..51b4f4a3e 100644 --- a/frontend/src/Cabinet/components/Card/ClubCabinetInfoCard/ClubCabinetInfoCard.tsx +++ b/frontend/src/Cabinet/components/Card/ClubCabinetInfoCard/ClubCabinetInfoCard.tsx @@ -6,7 +6,7 @@ import { ContentDetailStyled, } from "@/Cabinet/components/Card/CardStyles"; import ClubPasswordModalContainer from "@/Cabinet/components/Modals/ClubModal/ClubPasswordModal.container"; -import { ReactComponent as LeaderIcon } from "@/Cabinet/assets/images/leader.svg"; +import { ReactComponent as LeaderIcon } from "@/Cabinet/assets/images/crown.svg"; import { ReactComponent as LockIcon } from "@/Cabinet/assets/images/lock.svg"; import { ClubInfoResponseDto } from "@/Cabinet/types/dto/club.dto"; @@ -165,7 +165,6 @@ const CabinetIconStyled = styled.div` & > svg > path { stroke: var(--normal-text-color); - transform: scale(1.1); } `; diff --git a/frontend/src/Cabinet/components/Card/ClubNoticeCard/ClubNoticeCard.tsx b/frontend/src/Cabinet/components/Card/ClubNoticeCard/ClubNoticeCard.tsx index c64f5b266..0d733e3dc 100644 --- a/frontend/src/Cabinet/components/Card/ClubNoticeCard/ClubNoticeCard.tsx +++ b/frontend/src/Cabinet/components/Card/ClubNoticeCard/ClubNoticeCard.tsx @@ -81,7 +81,7 @@ const ClubNoticeTextStyled = styled.div` } ::-webkit-scrollbar-thumb { - background: var(--toggle-switch-off-bg-color); + background: var(--line-color); border-radius: 50px; border: 6px solid transparent; background-clip: padding-box; diff --git a/frontend/src/Cabinet/components/Card/DisplayStyleCard/ColorTheme/ColorTheme.tsx b/frontend/src/Cabinet/components/Card/DisplayStyleCard/ColorTheme/ColorTheme.tsx deleted file mode 100644 index bc2284cd3..000000000 --- a/frontend/src/Cabinet/components/Card/DisplayStyleCard/ColorTheme/ColorTheme.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import styled from "styled-components"; -import { ReactComponent as MonitorMobileIcon } from "@/Cabinet/assets/images/monitorMobile.svg"; -import { ReactComponent as MoonIcon } from "@/Cabinet/assets/images/moon.svg"; -import { ReactComponent as SunIcon } from "@/Cabinet/assets/images/sun.svg"; -import { ColorThemeToggleType } from "@/Cabinet/types/enum/colorTheme.type.enum"; - -interface ItoggleItemSeparated { - name: string; - key: string; - icon: React.ComponentType>; -} - -const toggleList: ItoggleItemSeparated[] = [ - { - name: "라이트", - key: ColorThemeToggleType.LIGHT, - icon: SunIcon, - }, - { - name: "다크", - key: ColorThemeToggleType.DARK, - icon: MoonIcon, - }, - { - name: "기기설정", - key: ColorThemeToggleType.DEVICE, - icon: MonitorMobileIcon, - }, -]; - -const ColorTheme = ({ - colorThemeToggle, - handleColorThemeButtonClick, -}: { - colorThemeToggle: ColorThemeToggleType; - handleColorThemeButtonClick: (colorThemeToggleType: string) => void; -}) => { - return ( - <> - - {toggleList.map((item) => { - const ColorThemeIcon = item.icon; - return ( - handleColorThemeButtonClick(item.key)} - > - {ColorThemeIcon && } - {item.name} - - ); - })} - - - ); -}; - -const ButtonsWrapperStyled = styled.div` - display: flex; - justify-content: center; - align-items: center; - border-radius: 10px; - justify-content: space-between; - padding: 0 16px; -`; - -const ButtonStyled = styled.button<{ - isClicked: boolean; -}>` - display: flex; - justify-content: space-between; - align-items: center; - flex-direction: column; - min-width: 50px; - width: 90px; - min-width: 50px; - border-radius: 10px; - font-size: 1rem; - height: 90px; - font-weight: 500; - background-color: ${(props) => - props.isClicked ? "var(--sys-main-color)" : "var(--card-bg-color)"}; - color: ${(props) => - props.isClicked - ? "var(--white-text-with-bg-color)" - : "var(--normal-text-color)"}; - padding: 12px 0 16px 0; - - & > svg { - width: 30px; - height: 30px; - } - - & > svg > path { - stroke: ${(props) => - props.isClicked - ? "var(--white-text-with-bg-color)" - : "var(--normal-text-color)"}; - } -`; - -export default ColorTheme; diff --git a/frontend/src/Cabinet/components/Card/DisplayStyleCard/DisplayStyleCard.container.tsx b/frontend/src/Cabinet/components/Card/DisplayStyleCard/DisplayStyleCard.container.tsx index 256acd97a..472e4251f 100644 --- a/frontend/src/Cabinet/components/Card/DisplayStyleCard/DisplayStyleCard.container.tsx +++ b/frontend/src/Cabinet/components/Card/DisplayStyleCard/DisplayStyleCard.container.tsx @@ -1,205 +1,84 @@ import { useEffect, useState } from "react"; import DisplayStyleCard from "@/Cabinet/components/Card/DisplayStyleCard/DisplayStyleCard"; -import ColorType from "@/Cabinet/types/enum/color.type.enum"; import { - ColorThemeToggleType, - ColorThemeType, -} from "@/Cabinet/types/enum/colorTheme.type.enum"; + DisplayStyleToggleType, + DisplayStyleType, +} from "@/Cabinet/types/enum/displayStyle.type.enum"; -// 로컬스토리지의 color-theme-toggle 값에 따라 ColorThemeType 반환 -export const getInitialColorTheme = ( - savedColorThemeToggle: ColorThemeToggleType, +// 로컬스토리지의 display-style-toggle 값에 따라 DisplayStyleType 반환 +export const getInitialDisplayStyle = ( + savedDisplayStyleToggle: DisplayStyleToggleType, darkModeQuery: MediaQueryList ) => { // 라이트 / 다크 버튼 - if (savedColorThemeToggle === ColorThemeToggleType.LIGHT) - return ColorThemeType.LIGHT; - else if (savedColorThemeToggle === ColorThemeToggleType.DARK) - return ColorThemeType.DARK; + if (savedDisplayStyleToggle === DisplayStyleToggleType.LIGHT) + return DisplayStyleType.LIGHT; + else if (savedDisplayStyleToggle === DisplayStyleToggleType.DARK) + return DisplayStyleType.DARK; // 디바이스 버튼 if (darkModeQuery.matches) { - return ColorThemeType.DARK; + return DisplayStyleType.DARK; } - return ColorThemeType.LIGHT; + return DisplayStyleType.LIGHT; }; const DisplayStyleCardContainer = () => { - const savedMainColor = - localStorage.getItem("main-color") || "var(--sys-default-main-color)"; - const savedSubColor = - localStorage.getItem("sub-color") || "var(--sys-default-sub-color)"; - const savedMineColor = - localStorage.getItem("mine-color") || "var(--sys-default-mine-color)"; - - const [mainColor, setMainColor] = useState(savedMainColor); - const [subColor, setSubColor] = useState(savedSubColor); - const [mineColor, setMineColor] = useState(savedMineColor); - - const [showColorPicker, setShowColorPicker] = useState(false); - const body: HTMLElement = document.body; - const root: HTMLElement = document.documentElement; - - const [selectedColorType, setSelectedColorType] = useState( - ColorType.MAIN + const savedDisplayStyleToggle = + (localStorage.getItem("display-style-toggle") as DisplayStyleToggleType) || + DisplayStyleToggleType.DEVICE; + var darkModeQuery = window.matchMedia("(prefers-color-scheme: dark)"); + const initialDisplayStyle = getInitialDisplayStyle( + savedDisplayStyleToggle, + darkModeQuery + ); + const [darkMode, setDarkMode] = useState( + initialDisplayStyle as DisplayStyleType + ); + const [toggleType, setToggleType] = useState( + savedDisplayStyleToggle ); - const handlePointColorChange = ( - mainColor: { hex: string }, - colorType: string - ) => { - const selectedColor: string = mainColor.hex; - if (colorType === ColorType.MAIN) { - setMainColor(selectedColor); - } else if (colorType === ColorType.SUB) { - setSubColor(selectedColor); - } else if (colorType === ColorType.MINE) { - setMineColor(selectedColor); - } - }; - - const setColorsAndLocalStorage = ( - main: string, - sub: string, - mine: string, - toggleType: ColorThemeToggleType - ) => { - setMainColor(main); - setSubColor(sub); - setMineColor(mine); - body.style.setProperty("--sys-main-color", main); - body.style.setProperty("--sys-sub-color", sub); - body.style.setProperty("--mine-color", mine); - root.style.setProperty("--sys-main-color", main); - root.style.setProperty("--sys-sub-color", sub); - root.style.setProperty("--mine-color", mine); - localStorage.setItem("main-color", main); - localStorage.setItem("sub-color", sub); - localStorage.setItem("mine-color", mine); - + const setColorsAndLocalStorage = (toggleType: DisplayStyleToggleType) => { setToggleType(toggleType); - localStorage.setItem("color-theme-toggle", toggleType); - }; - - const handleReset = () => { - setColorsAndLocalStorage( - "var(--sys-default-main-color)", - "var(--sys-default-sub-color)", - "var(--sys-default-mine-color)", - ColorThemeToggleType.DEVICE - ); - }; - - const handleSave = () => { - setColorsAndLocalStorage(mainColor, subColor, mineColor, toggleType); - setShowColorPicker(!showColorPicker); - }; - - const handleCancel = () => { - setColorsAndLocalStorage( - savedMainColor, - savedSubColor, - savedMineColor, - savedColorThemeToggle - ); - setShowColorPicker(!showColorPicker); + localStorage.setItem("display-style-toggle", toggleType); }; - const handlePointColorButtonClick = (pointColorType: string) => { - setSelectedColorType(pointColorType); - setShowColorPicker(true); - }; - - const handleColorThemeButtonClick = (colorThemeToggleType: string) => { - if (toggleType === colorThemeToggleType) return; + const handleDisplayStyleButtonClick = (displayStyleToggleType: string) => { + if (toggleType === displayStyleToggleType) return; setToggleType( - colorThemeToggleType as React.SetStateAction + displayStyleToggleType as React.SetStateAction ); - setShowColorPicker(true); + setColorsAndLocalStorage(displayStyleToggleType as DisplayStyleToggleType); }; - const savedColorThemeToggle = - (localStorage.getItem("color-theme-toggle") as ColorThemeToggleType) || - ColorThemeToggleType.DEVICE; - var darkModeQuery = window.matchMedia("(prefers-color-scheme: dark)"); - const initialColorTheme = getInitialColorTheme( - savedColorThemeToggle, - darkModeQuery - ); - const [darkMode, setDarkMode] = useState( - initialColorTheme as ColorThemeType - ); - const [toggleType, setToggleType] = useState( - savedColorThemeToggle - ); - useEffect(() => { darkModeQuery.addEventListener("change", (event) => - setDarkMode(event.matches ? ColorThemeType.DARK : ColorThemeType.LIGHT) + setDarkMode( + event.matches ? DisplayStyleType.DARK : DisplayStyleType.LIGHT + ) ); }, []); useEffect(() => { - document.body.setAttribute("color-theme", darkMode); + document.body.setAttribute("display-style", darkMode); }, [darkMode]); useEffect(() => { - if (toggleType === ColorThemeToggleType.LIGHT) { - setDarkMode(ColorThemeType.LIGHT); - } else if (toggleType === ColorThemeToggleType.DARK) { - setDarkMode(ColorThemeType.DARK); + if (toggleType === DisplayStyleToggleType.LIGHT) { + setDarkMode(DisplayStyleType.LIGHT); + } else if (toggleType === DisplayStyleToggleType.DARK) { + setDarkMode(DisplayStyleType.DARK); } else { setDarkMode( - darkModeQuery.matches ? ColorThemeType.DARK : ColorThemeType.LIGHT + darkModeQuery.matches ? DisplayStyleType.DARK : DisplayStyleType.LIGHT ); } }, [toggleType]); - useEffect(() => { - body.style.setProperty("--sys-main-color", mainColor); - body.style.setProperty("--sys-sub-color", subColor); - body.style.setProperty("--mine-color", mineColor); - root.style.setProperty("--sys-main-color", mainColor); - root.style.setProperty("--sys-sub-color", subColor); - root.style.setProperty("--mine-color", mineColor); - const confirmBeforeUnload = (e: BeforeUnloadEvent) => { - if ( - mainColor !== savedMainColor || - subColor !== savedSubColor || - mineColor !== savedMineColor || - toggleType !== savedColorThemeToggle - ) { - e.returnValue = - "변경된 색상이 저장되지 않을 수 있습니다. 페이지를 나가시겠습니까?"; - } - }; - window.addEventListener("beforeunload", confirmBeforeUnload); - return () => { - window.removeEventListener("beforeunload", confirmBeforeUnload); - }; - }, [ - mainColor, - mineColor, - savedMainColor, - savedMineColor, - subColor, - savedSubColor, - toggleType, - ]); - return ( ); }; diff --git a/frontend/src/Cabinet/components/Card/DisplayStyleCard/DisplayStyleCard.tsx b/frontend/src/Cabinet/components/Card/DisplayStyleCard/DisplayStyleCard.tsx index 541975511..f24601027 100644 --- a/frontend/src/Cabinet/components/Card/DisplayStyleCard/DisplayStyleCard.tsx +++ b/frontend/src/Cabinet/components/Card/DisplayStyleCard/DisplayStyleCard.tsx @@ -1,110 +1,126 @@ import styled from "styled-components"; import Card from "@/Cabinet/components/Card/Card"; import { CardContentWrapper } from "@/Cabinet/components/Card/CardStyles"; -import ColorTheme from "@/Cabinet/components/Card/DisplayStyleCard/ColorTheme/ColorTheme"; -import PointColor from "@/Cabinet/components/Card/DisplayStyleCard/PointColor/PointColor"; -import { ColorThemeToggleType } from "@/Cabinet/types/enum/colorTheme.type.enum"; +import { ReactComponent as MonitorMobileIcon } from "@/Cabinet/assets/images/monitorMobile.svg"; +import { ReactComponent as MoonIcon } from "@/Cabinet/assets/images/moon.svg"; +import { ReactComponent as SunIcon } from "@/Cabinet/assets/images/sun.svg"; +import { DisplayStyleToggleType } from "@/Cabinet/types/enum/displayStyle.type.enum"; interface DisplayStyleProps { - showColorPicker: boolean; - handlePointColorChange: (mainColor: { hex: string }, type: string) => void; - handleReset: () => void; - handleSave: () => void; - handleCancel: () => void; - mainColor: string; - subColor: string; - mineColor: string; - handlePointColorButtonClick: (colorType: string) => void; - selectedColorType: string; - colorThemeToggle: ColorThemeToggleType; - handleColorThemeButtonClick: (colorThemeToggleType: string) => void; + displayStyleToggle: DisplayStyleToggleType; + handleDisplayStyleButtonClick: (DisplayStyleToggleType: string) => void; } +interface IToggleItemSeparated { + name: string; + key: string; + icon: React.ComponentType>; +} + +const toggleList: IToggleItemSeparated[] = [ + { + name: "라이트", + key: DisplayStyleToggleType.LIGHT, + icon: SunIcon, + }, + { + name: "다크", + key: DisplayStyleToggleType.DARK, + icon: MoonIcon, + }, + { + name: "기기설정", + key: DisplayStyleToggleType.DEVICE, + icon: MonitorMobileIcon, + }, +]; + const DisplayStyleCard = ({ - showColorPicker, - handlePointColorChange, - handleReset, - handleSave, - handleCancel, - mainColor, - subColor, - mineColor, - handlePointColorButtonClick, - selectedColorType, - colorThemeToggle, - handleColorThemeButtonClick, + displayStyleToggle, + handleDisplayStyleButtonClick, }: DisplayStyleProps) => { return ( <> - {showColorPicker && } - + <> - - - - + + {toggleList.map((item) => { + const DisplayStyleIcon = item.icon; + return ( + handleDisplayStyleButtonClick(item.key)} + > + {DisplayStyleIcon && } + {item.name} + + ); + })} + - + ); }; -const BackgroundOverlayStyled = styled.div` - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: var(--modal-bg-shadow-color); -`; - -const ThemeColorCardWrapper = styled.div` +const DisplayStyleCardWrapper = styled.div` z-index: 1; align-self: start; `; +const ButtonsWrapperStyled = styled.div` + display: flex; + justify-content: center; + align-items: center; + border-radius: 10px; + justify-content: space-between; + padding: 0 16px; +`; + +const ButtonStyled = styled.button<{ + isClicked: boolean; +}>` + display: flex; + justify-content: space-between; + align-items: center; + flex-direction: column; + min-width: 50px; + width: 90px; + min-width: 50px; + border-radius: 10px; + font-size: 1rem; + height: 90px; + font-weight: 500; + background-color: ${(props) => + props.isClicked ? "var(--sys-main-color)" : "var(--card-bg-color)"}; + color: ${(props) => + props.isClicked + ? "var(--white-text-with-bg-color)" + : "var(--normal-text-color)"}; + padding: 12px 0 16px 0; + + & > svg { + width: 30px; + height: 30px; + } + + & > svg > path { + stroke: ${(props) => + props.isClicked + ? "var(--white-text-with-bg-color)" + : "var(--normal-text-color)"}; + } +`; + export default DisplayStyleCard; diff --git a/frontend/src/Cabinet/components/Card/DisplayStyleCard/colorThemeInitializer.ts b/frontend/src/Cabinet/components/Card/DisplayStyleCard/colorThemeInitializer.ts deleted file mode 100644 index 6a1fa6574..000000000 --- a/frontend/src/Cabinet/components/Card/DisplayStyleCard/colorThemeInitializer.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { getInitialColorTheme } from "@/Cabinet/components/Card/DisplayStyleCard/DisplayStyleCard.container"; -import { - ColorThemeToggleType, - ColorThemeType, -} from "@/Cabinet/types/enum/colorTheme.type.enum"; - -(function () { - const isClient = typeof window !== "undefined"; - if (isClient) { - const savedColorThemeToggle = - (localStorage.getItem("color-theme-toggle") as ColorThemeToggleType) || - ColorThemeToggleType.DEVICE; - - const darkModeQuery = window.matchMedia("(prefers-color-scheme: dark)"); - - const colorMode = getInitialColorTheme( - savedColorThemeToggle, - darkModeQuery - ); - - document.documentElement.style.setProperty( - "background-color", - colorMode === ColorThemeType.DARK ? "#1f1f1f" : "#ffffff" - ); - // NOTE : 새로고침 깜박임 현상 방지 - // 이 코드가 실행중일땐 전역변수가 아직 정의가 안된 상태라 전역변수 대신 hex code 사용 - - document.addEventListener("DOMContentLoaded", function () { - document.body.setAttribute("color-theme", colorMode); - }); - } -})(); diff --git a/frontend/src/Cabinet/components/Card/DisplayStyleCard/displayStyleInitializer.ts b/frontend/src/Cabinet/components/Card/DisplayStyleCard/displayStyleInitializer.ts new file mode 100644 index 000000000..3302a8b48 --- /dev/null +++ b/frontend/src/Cabinet/components/Card/DisplayStyleCard/displayStyleInitializer.ts @@ -0,0 +1,33 @@ +import { getInitialDisplayStyle } from "@/Cabinet/components/Card/DisplayStyleCard/DisplayStyleCard.container"; +import { + DisplayStyleToggleType, + DisplayStyleType, +} from "@/Cabinet/types/enum/displayStyle.type.enum"; + +(function () { + const isClient = typeof window !== "undefined"; + if (isClient) { + const savedDisplayStyleToggle = + (localStorage.getItem( + "display-style-toggle" + ) as DisplayStyleToggleType) || DisplayStyleToggleType.DEVICE; + + const darkModeQuery = window.matchMedia("(prefers-color-scheme: dark)"); + + const colorMode = getInitialDisplayStyle( + savedDisplayStyleToggle, + darkModeQuery + ); + + document.documentElement.style.setProperty( + "background-color", + colorMode === DisplayStyleType.DARK ? "#1f1f1f" : "#ffffff" + ); + // NOTE : 새로고침 깜박임 현상 방지 + // 이 코드가 실행중일땐 전역변수가 아직 정의가 안된 상태라 전역변수 대신 hex code 사용 + + document.addEventListener("DOMContentLoaded", function () { + document.body.setAttribute("display-style", colorMode); + }); + } +})(); diff --git a/frontend/src/Cabinet/components/Card/ExtensionCard/ExtensionCard.tsx b/frontend/src/Cabinet/components/Card/ExtensionCard/ExtensionCard.tsx index 1aec4650e..b65e7153a 100644 --- a/frontend/src/Cabinet/components/Card/ExtensionCard/ExtensionCard.tsx +++ b/frontend/src/Cabinet/components/Card/ExtensionCard/ExtensionCard.tsx @@ -1,3 +1,4 @@ +import { useState } from "react"; import Card, { IButtonProps } from "@/Cabinet/components/Card/Card"; import { CardContentStyled, @@ -8,7 +9,6 @@ import { import { NotificationModal } from "@/Cabinet/components/Modals/NotificationModal/NotificationModal"; import { LentExtensionDto } from "@/Cabinet/types/dto/lent.dto"; import { formatDate } from "@/Cabinet/utils/dateUtils"; -import { useState } from "react"; interface ExtensionProps { extensionInfo: LentExtensionDto | null; diff --git a/frontend/src/Cabinet/components/Card/LentInfoCard/LentInfoCard.container.tsx b/frontend/src/Cabinet/components/Card/LentInfoCard/LentInfoCard.container.tsx index c0f48e9d4..3dca53ca1 100644 --- a/frontend/src/Cabinet/components/Card/LentInfoCard/LentInfoCard.container.tsx +++ b/frontend/src/Cabinet/components/Card/LentInfoCard/LentInfoCard.container.tsx @@ -1,12 +1,14 @@ +import { useEffect, useState } from "react"; +import { useRecoilValue, useSetRecoilState } from "recoil"; +import { myCabinetInfoState, userState } from "@/Cabinet/recoil/atoms"; import LentInfoCard from "@/Cabinet/components/Card/LentInfoCard/LentInfoCard"; import { getDefaultCabinetInfo } from "@/Cabinet/components/TopNav/TopNavButtonGroup/TopNavButtonGroup"; -import { myCabinetInfoState } from "@/Cabinet/recoil/atoms"; import { CabinetInfo } from "@/Cabinet/types/dto/cabinet.dto"; import { LentDto } from "@/Cabinet/types/dto/lent.dto"; +import { IItemTimeRemaining } from "@/Cabinet/types/dto/store.dto"; import CabinetStatus from "@/Cabinet/types/enum/cabinet.status.enum"; import CabinetType from "@/Cabinet/types/enum/cabinet.type.enum"; -import { getRemainingTime } from "@/Cabinet/utils/dateUtils"; -import { useRecoilValue } from "recoil"; +import { getRemainingTime, getTimeRemaining } from "@/Cabinet/utils/dateUtils"; export interface MyCabinetInfo { name: string | null; @@ -73,6 +75,10 @@ const LentInfoCardContainer = ({ unbannedAt: Date | null | undefined; }) => { const myCabinetInfo = useRecoilValue(myCabinetInfoState); + const [isPenaltyUser, setIsPenaltyUser] = useState(true); + const [remainPenaltyPeriod, setRemainPenaltyPeriod] = + useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); let dateUsed, dateLeft, expireDate; if (name && myCabinetInfo.lents) { @@ -102,7 +108,39 @@ const LentInfoCardContainer = ({ status: myCabinetInfo.status || "", }; - return ; + const onCLickPenaltyButton = () => { + setIsModalOpen(true); + }; + const handleCloseModal = () => { + setIsModalOpen(false); + }; + + useEffect(() => { + if (unbannedAt == null) { + setIsPenaltyUser(false); + } else { + setRemainPenaltyPeriod(getTimeRemaining(unbannedAt)); + } + }, [unbannedAt]); + + return ( + + ); }; export default LentInfoCardContainer; diff --git a/frontend/src/Cabinet/components/Card/LentInfoCard/LentInfoCard.tsx b/frontend/src/Cabinet/components/Card/LentInfoCard/LentInfoCard.tsx index 527b46c00..3c88ed753 100644 --- a/frontend/src/Cabinet/components/Card/LentInfoCard/LentInfoCard.tsx +++ b/frontend/src/Cabinet/components/Card/LentInfoCard/LentInfoCard.tsx @@ -1,5 +1,5 @@ import styled from "styled-components"; -import Card from "@/Cabinet/components/Card/Card"; +import Card, { IButtonProps } from "@/Cabinet/components/Card/Card"; import { CardContentStyled, CardContentWrapper, @@ -8,8 +8,10 @@ import { } from "@/Cabinet/components/Card/CardStyles"; import { MyCabinetInfo } from "@/Cabinet/components/Card/LentInfoCard/LentInfoCard.container"; import { cabinetIconComponentMap } from "@/Cabinet/assets/data/maps"; +import { IItemTimeRemaining } from "@/Cabinet/types/dto/store.dto"; import CabinetStatus from "@/Cabinet/types/enum/cabinet.status.enum"; import { formatDate } from "@/Cabinet/utils/dateUtils"; +import StoreBuyPenalty from "../../Modals/StoreModal/StoreBuyPenaltyModal"; const calculateFontSize = (userCount: number): string => { const baseSize = 1; @@ -26,94 +28,111 @@ const calculateFontSize = (userCount: number): string => { const LentInfoCard = ({ cabinetInfo, unbannedAt, + button, + isModalOpen, + remainPenaltyPeriod, + onClose, }: { cabinetInfo: MyCabinetInfo; unbannedAt: Date | null | undefined; + button: IButtonProps | undefined; + isModalOpen: boolean; + remainPenaltyPeriod: IItemTimeRemaining | null; + onClose: () => void; }) => { const CabinetIcon = cabinetIconComponentMap[cabinetInfo.lentType]; return ( - - <> - - - {cabinetInfo.visibleNum !== 0 - ? cabinetInfo.visibleNum - : !!unbannedAt - ? "!" - : "-"} - - - + + <> + + - {cabinetInfo.floor !== 0 - ? cabinetInfo.floor + "층 - " + cabinetInfo.section - : "대여 중이 아닌 사용자"} - - - - - - + {cabinetInfo.visibleNum !== 0 + ? cabinetInfo.visibleNum + : !!unbannedAt + ? "!" + : "-"} + + - {cabinetInfo.userNameList} + {cabinetInfo.floor !== 0 + ? cabinetInfo.floor + "층 - " + cabinetInfo.section + : "대여 중이 아닌 사용자"} - - - - - - 사용 기간 - - {cabinetInfo?.isLented && cabinetInfo.status != "IN_SESSION" - ? `${cabinetInfo.dateUsed}일` - : "-"} - - - - - {cabinetInfo?.status === "OVERDUE" ? "연체 기간" : "남은 기간"} - - - {cabinetInfo?.expireDate ? `${cabinetInfo.dateLeft}일` : "-"} - - - - - {!!unbannedAt ? "패널티 종료 일자" : "종료 일자"} - - - {!!unbannedAt - ? formatDate(new Date(unbannedAt), ".") - : cabinetInfo?.expireDate - ? formatDate(new Date(cabinetInfo?.expireDate), ".") - : "-"} - - - - - - 이전 대여자 - - {cabinetInfo?.previousUserName || "-"} - - - - - + + + + + + + {cabinetInfo.userNameList} + + + + + + + 사용 기간 + + {cabinetInfo?.isLented && cabinetInfo.status != "IN_SESSION" + ? `${cabinetInfo.dateUsed}일` + : "-"} + + + + + {cabinetInfo?.status === "OVERDUE" ? "연체 기간" : "남은 기간"} + + + {cabinetInfo?.expireDate ? `${cabinetInfo.dateLeft}일` : "-"} + + + + + {!!unbannedAt ? "페널티 종료 일자" : "종료 일자"} + + + {!!unbannedAt + ? formatDate(new Date(unbannedAt), ".") + : cabinetInfo?.expireDate + ? formatDate(new Date(cabinetInfo?.expireDate), ".") + : "-"} + + + + + + 이전 대여자 + + {cabinetInfo?.previousUserName || "-"} + + + + + + {isModalOpen && ( + + )} + ); }; @@ -188,7 +207,6 @@ const CabinetIconStyled = styled.div` & > svg > path { stroke: var(--normal-text-color); - transform: scale(0.8); } `; diff --git a/frontend/src/Cabinet/components/Card/DisplayStyleCard/PointColor/ColorPicker.tsx b/frontend/src/Cabinet/components/Card/PointColorCard/ColorPicker.tsx similarity index 93% rename from frontend/src/Cabinet/components/Card/DisplayStyleCard/PointColor/ColorPicker.tsx rename to frontend/src/Cabinet/components/Card/PointColorCard/ColorPicker.tsx index 0227cec50..9cef69a2a 100644 --- a/frontend/src/Cabinet/components/Card/DisplayStyleCard/PointColor/ColorPicker.tsx +++ b/frontend/src/Cabinet/components/Card/PointColorCard/ColorPicker.tsx @@ -1,6 +1,6 @@ import { TwitterPicker } from "react-color"; import styled from "styled-components"; -import { GetCustomColorsValues } from "@/Cabinet/components/Card/DisplayStyleCard/colorInfo"; +import { GetCustomColorsValues } from "@/Cabinet/components/Card/PointColorCard/colorInfo"; interface ColorPickerProps { color: string; @@ -24,7 +24,7 @@ const ColorPicker = ({ color, onChange }: ColorPickerProps) => { input: { boxShadow: "var(--color-picker-hash-bg-color) 0px 0px 0px 1px inset", - color: "var(--color-picker-input-color)", + color: "var(--gray-line-btn-color)", }, hash: { background: "var(--color-picker-hash-bg-color)", diff --git a/frontend/src/Cabinet/components/Card/DisplayStyleCard/PointColor/PointColor.tsx b/frontend/src/Cabinet/components/Card/PointColorCard/PointColor.tsx similarity index 91% rename from frontend/src/Cabinet/components/Card/DisplayStyleCard/PointColor/PointColor.tsx rename to frontend/src/Cabinet/components/Card/PointColorCard/PointColor.tsx index fd7bb11e7..eae1723e1 100644 --- a/frontend/src/Cabinet/components/Card/DisplayStyleCard/PointColor/PointColor.tsx +++ b/frontend/src/Cabinet/components/Card/PointColorCard/PointColor.tsx @@ -3,8 +3,8 @@ import { CardContentStyled, ContentInfoStyled, } from "@/Cabinet/components/Card/CardStyles"; -import ColorPicker from "@/Cabinet/components/Card/DisplayStyleCard/PointColor/ColorPicker"; -import { pointColorData } from "@/Cabinet/components/Card/DisplayStyleCard/colorInfo"; +import ColorPicker from "@/Cabinet/components/Card/PointColorCard/ColorPicker"; +import { pointColorData } from "@/Cabinet/components/Card/PointColorCard/colorInfo"; interface PointColorProps { showColorPicker: boolean; diff --git a/frontend/src/Cabinet/components/Card/PointColorCard/PointColorCard.container.tsx b/frontend/src/Cabinet/components/Card/PointColorCard/PointColorCard.container.tsx new file mode 100644 index 000000000..034fd2d49 --- /dev/null +++ b/frontend/src/Cabinet/components/Card/PointColorCard/PointColorCard.container.tsx @@ -0,0 +1,127 @@ +import { useEffect, useState } from "react"; +import PointColorCard from "@/Cabinet/components/Card/PointColorCard/PointColorCard"; +import ColorType from "@/Cabinet/types/enum/color.type.enum"; + +const PointColorCardContainer = () => { + const savedMainColor = + localStorage.getItem("main-color") || "var(--sys-default-main-color)"; + const savedSubColor = + localStorage.getItem("sub-color") || "var(--sys-default-sub-color)"; + const savedMineColor = + localStorage.getItem("mine-color") || "var(--sys-default-mine-color)"; + + const [mainColor, setMainColor] = useState(savedMainColor); + const [subColor, setSubColor] = useState(savedSubColor); + const [mineColor, setMineColor] = useState(savedMineColor); + + const [showColorPicker, setShowColorPicker] = useState(false); + const body: HTMLElement = document.body; + const root: HTMLElement = document.documentElement; + + const [selectedColorType, setSelectedColorType] = useState( + ColorType.MAIN + ); + + const handlePointColorChange = ( + mainColor: { hex: string }, + colorType: string + ) => { + const selectedColor: string = mainColor.hex; + if (colorType === ColorType.MAIN) { + setMainColor(selectedColor); + } else if (colorType === ColorType.SUB) { + setSubColor(selectedColor); + } else if (colorType === ColorType.MINE) { + setMineColor(selectedColor); + } + }; + + const setColorsAndLocalStorage = ( + main: string, + sub: string, + mine: string + ) => { + setMainColor(main); + setSubColor(sub); + setMineColor(mine); + body.style.setProperty("--sys-main-color", main); + body.style.setProperty("--sys-sub-color", sub); + body.style.setProperty("--mine-color", mine); + root.style.setProperty("--sys-main-color", main); + root.style.setProperty("--sys-sub-color", sub); + root.style.setProperty("--mine-color", mine); + localStorage.setItem("main-color", main); + localStorage.setItem("sub-color", sub); + localStorage.setItem("mine-color", mine); + }; + + const handleReset = () => { + setColorsAndLocalStorage( + "var(--sys-default-main-color)", + "var(--sys-default-sub-color)", + "var(--sys-default-mine-color)" + ); + }; + + const handleSave = () => { + setColorsAndLocalStorage(mainColor, subColor, mineColor); + setShowColorPicker(!showColorPicker); + }; + + const handleCancel = () => { + setColorsAndLocalStorage(savedMainColor, savedSubColor, savedMineColor); + setShowColorPicker(!showColorPicker); + }; + + const handlePointColorButtonClick = (pointColorType: string) => { + setSelectedColorType(pointColorType); + setShowColorPicker(true); + }; + + useEffect(() => { + body.style.setProperty("--sys-main-color", mainColor); + body.style.setProperty("--sys-sub-color", subColor); + body.style.setProperty("--mine-color", mineColor); + root.style.setProperty("--sys-main-color", mainColor); + root.style.setProperty("--sys-sub-color", subColor); + root.style.setProperty("--mine-color", mineColor); + const confirmBeforeUnload = (e: BeforeUnloadEvent) => { + if ( + mainColor !== savedMainColor || + subColor !== savedSubColor || + mineColor !== savedMineColor + ) { + e.returnValue = + "변경된 색상이 저장되지 않을 수 있습니다. 페이지를 나가시겠습니까?"; + } + }; + window.addEventListener("beforeunload", confirmBeforeUnload); + return () => { + window.removeEventListener("beforeunload", confirmBeforeUnload); + }; + }, [ + mainColor, + mineColor, + savedMainColor, + savedMineColor, + subColor, + savedSubColor, + ]); + + return ( + + ); +}; + +export default PointColorCardContainer; diff --git a/frontend/src/Cabinet/components/Card/PointColorCard/PointColorCard.tsx b/frontend/src/Cabinet/components/Card/PointColorCard/PointColorCard.tsx new file mode 100644 index 000000000..a95e4af09 --- /dev/null +++ b/frontend/src/Cabinet/components/Card/PointColorCard/PointColorCard.tsx @@ -0,0 +1,98 @@ +import styled from "styled-components"; +import Card from "@/Cabinet/components/Card/Card"; +import { CardContentWrapper } from "@/Cabinet/components/Card/CardStyles"; +import PointColor from "@/Cabinet/components/Card/PointColorCard/PointColor"; + +interface PointColorProps { + showColorPicker: boolean; + handlePointColorChange: (mainColor: { hex: string }, type: string) => void; + handleReset: () => void; + handleSave: () => void; + handleCancel: () => void; + mainColor: string; + subColor: string; + mineColor: string; + handlePointColorButtonClick: (colorType: string) => void; + selectedColorType: string; +} + +const PointColorCard = ({ + showColorPicker, + handlePointColorChange, + handleReset, + handleSave, + handleCancel, + mainColor, + subColor, + mineColor, + handlePointColorButtonClick, + selectedColorType, +}: PointColorProps) => { + return ( + <> + {showColorPicker && } + + + <> + + + + + + + + ); +}; + +const BackgroundOverlayStyled = styled.div` + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: var(--modal-bg-shadow-color); +`; + +const PointColorCardWrapper = styled.div` + z-index: 1; + align-self: start; +`; + +export default PointColorCard; diff --git a/frontend/src/Cabinet/components/Card/DisplayStyleCard/colorInfo.ts b/frontend/src/Cabinet/components/Card/PointColorCard/colorInfo.ts similarity index 97% rename from frontend/src/Cabinet/components/Card/DisplayStyleCard/colorInfo.ts rename to frontend/src/Cabinet/components/Card/PointColorCard/colorInfo.ts index b301c1157..f4ad1336a 100644 --- a/frontend/src/Cabinet/components/Card/DisplayStyleCard/colorInfo.ts +++ b/frontend/src/Cabinet/components/Card/PointColorCard/colorInfo.ts @@ -29,7 +29,7 @@ export const pointColorData: ColorData[] = [ ]; export const customColors = [ - "--custom-pink", + "--custom-pink-200", "--custom-orange", "--custom-yellow", "--custom-green-100", diff --git a/frontend/src/Cabinet/components/Card/StoreItemCard/StoreItemCard.tsx b/frontend/src/Cabinet/components/Card/StoreItemCard/StoreItemCard.tsx new file mode 100644 index 000000000..717e0c2df --- /dev/null +++ b/frontend/src/Cabinet/components/Card/StoreItemCard/StoreItemCard.tsx @@ -0,0 +1,145 @@ +import styled from "styled-components"; +import Card from "@/Cabinet/components/Card/Card"; +import type { IButtonProps } from "@/Cabinet/components/Card/Card"; +import { ItemIconMap } from "@/Cabinet/assets/data/maps"; +import { ReactComponent as CoinImg } from "@/Cabinet/assets/images/coinIcon.svg"; +import { IItemDetail } from "@/Cabinet/types/dto/store.dto"; +import { StoreItemType } from "@/Cabinet/types/enum/store.enum"; + +const convertToItemType = (itemType: string) => { + switch (itemType) { + case "EXTENSION": + return StoreItemType.EXTENSION; + case "SWAP": + return StoreItemType.SWAP; + case "ALARM": + return StoreItemType.ALARM; + case "PENALTY": + return StoreItemType.PENALTY; + default: + return StoreItemType.EXTENSION; + } +}; + +const StoreItemCard = ({ + item, + button, +}: { + item: IItemDetail; + button: IButtonProps; +}) => { + const ItemIcon = ItemIconMap[convertToItemType(item.itemType)]; + return ( + + + + + + + + + + + + {item.items[0].itemPrice * -1} + + + {item.description} + + + + ); +}; + +const WrapperStyled = styled.div` + font-size: 15px; +`; + +const SectionStyled = styled.div` + display: flex; + height: 80px; + width: 90%; + align-items: center; +`; + +const BlockStyled = styled.div` + display: flex; + flex-direction: column; + margin-right: 15px; + justify-content: center; +`; + +const IconBlockStyled = styled.div` + width: 53px; + height: 53px; + border-radius: 10px; + background-color: var(--sys-main-color); + margin-bottom: 5px; + display: flex; + justify-content: center; + align-items: center; +`; + +const PriseBlockStyled = styled.div` + width: 53px; + height: 22px; + background-color: var(--card-content-bg-color); + border-radius: 5px; + font-size: 12px; + color: var(--sys-main-color); + display: flex; + justify-content: center; + align-items: center; + > span { + margin-left: 3px; + font-weight: 600; + } + + & > svg { + width: 14px; + height: 14px; + } + & > svg > path { + stroke: var(--sys-main-color); + stroke-width: 2px; + } +`; + +const ItemDetailStyled = styled.div` + width: 100%; + height: 100%; + background-color: var(--card-content-bg-color); + font-size: var(--size-base); + word-wrap: normal; + padding: 10px 16px; + line-height: 1.4; + border-radius: 10px; +`; + +const ItemIconStyled = styled.div<{ itemType: StoreItemType }>` + display: flex; + justify-content: center; + align-items: center; + width: 32px; + height: 32px; + + & > svg { + width: 32px; + height: 32px; + } + + & > svg > path { + stroke: var(--white-text-with-bg-color); + stroke-width: ${(props) => + props.itemType === StoreItemType.EXTENSION ? "2.8px" : "1.5px"}; + } +`; + +export default StoreItemCard; diff --git a/frontend/src/Cabinet/components/Club/ClubInfo.tsx b/frontend/src/Cabinet/components/Club/ClubInfo.tsx index 9e373495c..d26f04cbc 100644 --- a/frontend/src/Cabinet/components/Club/ClubInfo.tsx +++ b/frontend/src/Cabinet/components/Club/ClubInfo.tsx @@ -6,7 +6,7 @@ import ClubCabinetInfoCard from "@/Cabinet/components/Card/ClubCabinetInfoCard/C import ClubNoticeCard from "@/Cabinet/components/Card/ClubNoticeCard/ClubNoticeCard"; import ClubMemberListContainer from "@/Cabinet/components/Club/ClubMemberList/ClubMemberList.container"; import LoadingAnimation from "@/Cabinet/components/Common/LoadingAnimation"; -import { ReactComponent as SadCcabi } from "@/Cabinet/assets/images/sadCcabi.svg"; +import UnavailableDataInfo from "@/Cabinet/components/Common/UnavailableDataInfo"; import { ClubInfoResponseDto } from "@/Cabinet/types/dto/club.dto"; import useClubInfo from "@/Cabinet/hooks/useClubInfo"; import useMenu from "@/Cabinet/hooks/useMenu"; @@ -31,12 +31,9 @@ const ClubInfo = () => { {clubInfo === undefined ? ( ) : clubInfo === STATUS_400_BAD_REQUEST ? ( - - 동아리 사물함이 없어요 - - - - + <> + + ) : ( 동아리 정보 @@ -55,34 +52,6 @@ const ClubInfo = () => { ); }; -const EmptyClubCabinetTextStyled = styled.div` - width: 100%; - height: 100%; - display: flex; - justify-content: center; - align-items: center; - font-size: 1.125rem; - color: var(--gray-line-btn-color); -`; - -const SadCcabiStyled = styled.div` - display: flex; - margin-left: 5px; - width: 30px; - height: 30px; - margin-left: 8px; - padding-top: 3px; - - & > svg { - width: 30px; - height: 30px; - } - - & > svg > path { - fill: var(--gray-line-btn-color); - } -`; - const ClubInfoWrapperStyled = styled.div` display: flex; flex-direction: column; diff --git a/frontend/src/Cabinet/components/Club/ClubMemberInfoArea/ClubMemberInfoArea.tsx b/frontend/src/Cabinet/components/Club/ClubMemberInfoArea/ClubMemberInfoArea.tsx index cf41d2628..7aaa96f81 100644 --- a/frontend/src/Cabinet/components/Club/ClubMemberInfoArea/ClubMemberInfoArea.tsx +++ b/frontend/src/Cabinet/components/Club/ClubMemberInfoArea/ClubMemberInfoArea.tsx @@ -4,15 +4,14 @@ import { TClubModalState, } from "@/Cabinet/components/Club/ClubMemberInfoArea/ClubMemberInfoArea.container"; import Button from "@/Cabinet/components/Common/Button"; +import SelectInduction from "@/Cabinet/components/Common/SelectInduction"; import DeleteClubMemberModal from "@/Cabinet/components/Modals/ClubModal/DeleteClubMemberModal"; import MandateClubMemberModal from "@/Cabinet/components/Modals/ClubModal/MandateClubMemberModal"; import { - cabinetIconSrcMap, cabinetLabelColorMap, cabinetStatusColorMap, } from "@/Cabinet/assets/data/maps"; -import { ReactComponent as LeaderIcon } from "@/Cabinet/assets/images/leader.svg"; -import { ReactComponent as LogoImg } from "@/Cabinet/assets/images/logo.svg"; +import { ReactComponent as LeaderIcon } from "@/Cabinet/assets/images/crown.svg"; import { ReactComponent as UserImg } from "@/Cabinet/assets/images/privateIcon.svg"; import { ClubCabinetInfo, @@ -51,18 +50,10 @@ const ClubMemberInfoArea = ({ <> {selectedClubCabinetInfo === null ? ( - - - - - - 동아리를
- 선택해주세요 -
-
+ ) : ( <> @@ -120,24 +111,6 @@ const ClubMemberInfoArea = ({ ); }; -const NotSelectedStyled = styled.div` - height: 100%; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; -`; - -const CabiLogoStyled = styled.div` - width: 35px; - height: 35px; - margin-bottom: 10px; - svg { - .logo_svg__currentPath { - fill: var(--sys-main-color); - } - } -`; const ClubMemberInfoAreaStyled = styled.div` position: fixed; top: 120px; @@ -215,13 +188,11 @@ const ClubMemberIconStyled = styled.div<{ isMasterSelected: boolean }>` & > svg { width: 24px; - height: 24px; + height: ${(props) => (props.isMasterSelected ? "20px" : "24px")}; } & > svg > path { stroke: var(--normal-text-color); - transform: ${(props) => - props.isMasterSelected ? "scale(1.3)" : "scale(1.0)"}; } `; diff --git a/frontend/src/Cabinet/components/Club/ClubMemberList/ClubMemberList.tsx b/frontend/src/Cabinet/components/Club/ClubMemberList/ClubMemberList.tsx index 69c1c0179..1d772bb24 100644 --- a/frontend/src/Cabinet/components/Club/ClubMemberList/ClubMemberList.tsx +++ b/frontend/src/Cabinet/components/Club/ClubMemberList/ClubMemberList.tsx @@ -152,6 +152,11 @@ const UserCountIconStyled = styled.div` & > svg > path { stroke: var(--normal-text-color); } + + & > svg { + width: 24px; + height: 24px; + } `; const AddMemberCardStyled = styled.div` diff --git a/frontend/src/Cabinet/components/Club/ClubMemberList/ClubMemberListItem/ClubMemberListItem.tsx b/frontend/src/Cabinet/components/Club/ClubMemberList/ClubMemberListItem/ClubMemberListItem.tsx index be6e04df5..8c2e2d9ac 100644 --- a/frontend/src/Cabinet/components/Club/ClubMemberList/ClubMemberListItem/ClubMemberListItem.tsx +++ b/frontend/src/Cabinet/components/Club/ClubMemberList/ClubMemberListItem/ClubMemberListItem.tsx @@ -86,7 +86,6 @@ const MemberListItemStyled = styled.div<{ isMaster: boolean }>` props.isMaster ? "var(--white-text-with-bg-color)" : "var(--normal-text-color)"}; - transform: ${(props) => (props.isMaster ? "" : "scale(0.7)")}; } `; diff --git a/frontend/src/Cabinet/components/Common/ClubListDropdown.tsx b/frontend/src/Cabinet/components/Common/ClubListDropdown.tsx index 1c1e16001..8ae9c35a8 100644 --- a/frontend/src/Cabinet/components/Common/ClubListDropdown.tsx +++ b/frontend/src/Cabinet/components/Common/ClubListDropdown.tsx @@ -71,7 +71,7 @@ const ClubListDropdSelectionBoxStyled = styled.div<{ isOpen: boolean }>` position: relative; display: flex; align-items: center; - border: 1px solid var(--toggle-switch-off-bg-color); + border: 1px solid var(--line-color); width: 100%; height: 60px; border-radius: 10px; @@ -114,7 +114,7 @@ const ClubListDropdItemStyled = styled.div<{ isSelected: boolean }>` align-items: center; background-color: ${({ isSelected }) => isSelected ? "var(--map-floor-color)" : "var(--bg-color)"}; - border: 1px solid var(--toggle-switch-off-bg-color); + border: 1px solid var(--line-color); border-width: 0px 1px 1px 1px; width: 100%; height: 60px; diff --git a/frontend/src/Cabinet/components/Common/Dropdown.tsx b/frontend/src/Cabinet/components/Common/Dropdown.tsx index 8b47b4cce..c9eb3f121 100644 --- a/frontend/src/Cabinet/components/Common/Dropdown.tsx +++ b/frontend/src/Cabinet/components/Common/Dropdown.tsx @@ -1,13 +1,15 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import styled, { css } from "styled-components"; -import { ReactComponent as ClubIcon } from "@/Cabinet/assets/images/clubIcon.svg"; -import { ReactComponent as PrivateIcon } from "@/Cabinet/assets/images/privateIcon.svg"; -import { ReactComponent as ShareIcon } from "@/Cabinet/assets/images/shareIcon.svg"; +import { cabinetIconComponentMap } from "@/Cabinet/assets/data/maps"; +import { ReactComponent as DropdownChevronIcon } from "@/Cabinet/assets/images/dropdownChevron.svg"; +import CabinetType from "@/Cabinet/types/enum/cabinet.type.enum"; export interface IDropdownOptions { name: string; value: any; imageSrc?: string; + isDisabled?: boolean; + hasNoOptions?: boolean; } export interface IDropdownProps { @@ -15,51 +17,72 @@ export interface IDropdownProps { defaultValue: string; defaultImageSrc?: string; onChangeValue?: (param: any) => any; + isOpen: boolean; + setIsOpen: React.Dispatch>; + closeOtherDropdown?: () => void; } -const Dropdown = ({ options, defaultValue, onChangeValue }: IDropdownProps) => { +const Dropdown = ({ + options, + defaultValue, + onChangeValue, + defaultImageSrc, + isOpen, + setIsOpen, + closeOtherDropdown, +}: IDropdownProps) => { const [currentName, setCurrentName] = useState(defaultValue); - const [isOpen, setIsOpen] = useState(false); const idx: number = options.findIndex((op) => op.name === currentName); const selectedIdx: number = idx === -1 ? 0 : idx; + const DefaultOptionIcon = + defaultImageSrc && + cabinetIconComponentMap[options[selectedIdx].value as CabinetType]; + useEffect(() => { + setCurrentName(defaultValue); + }, [defaultValue]); return ( { + if (options[selectedIdx].isDisabled) return; setIsOpen(!isOpen); + closeOtherDropdown && closeOtherDropdown(); }} - isOpen={isOpen} > - {options[selectedIdx] && options[selectedIdx].imageSrc && ( + {DefaultOptionIcon && ( - {options[selectedIdx].value === "PRIVATE" && } - {options[selectedIdx].value === "CLUB" && } - {options[selectedIdx].value === "SHARE" && } + )}

{currentName}

- + + +
- {options?.map((option) => { + {options.map((option) => { + const OptionIcon = + cabinetIconComponentMap[option.value as CabinetType]; return ( { - setCurrentName(option.name); - setIsOpen(false); - if (onChangeValue) { - onChangeValue(option.value); + if (!option.isDisabled) { + setCurrentName(option.name); + setIsOpen(false); + if (onChangeValue) { + onChangeValue(option.value); + } } }} isSelected={option.name === currentName} + isDisabled={option.isDisabled} + hasNoOptions={option.hasNoOptions} > {option.imageSrc && ( - {option.value === "PRIVATE" && } - {option.value === "CLUB" && } - {option.value === "SHARE" && } + )}

{option.name}

@@ -76,13 +99,14 @@ const DropdownContainerStyled = styled.div` flex-direction: column; width: 100%; position: relative; + cursor: pointer; `; -const DropdownSelectionBoxStyled = styled.div<{ isOpen: boolean }>` +const DropdownSelectionBoxStyled = styled.div` position: relative; display: flex; align-items: center; - border: 1px solid var(--toggle-switch-off-bg-color); + border: 1px solid var(--line-color); width: 100%; height: 60px; border-radius: 10px; @@ -90,19 +114,6 @@ const DropdownSelectionBoxStyled = styled.div<{ isOpen: boolean }>` padding-left: 20px; font-size: 1.125rem; color: var(--sys-main-color); - & > img { - filter: contrast(0.6); - width: 14px; - height: 8px; - position: absolute; - top: 45%; - left: 85%; - ${({ isOpen }) => - isOpen === true && - css` - transform: scaleY(-1); - `} - } `; const DropdownItemContainerStyled = styled.div<{ isVisible: boolean }>` @@ -119,29 +130,41 @@ const DropdownItemContainerStyled = styled.div<{ isVisible: boolean }>` `} `; -const DropdownItemStyled = styled.div<{ isSelected: boolean }>` +const DropdownItemStyled = styled.div<{ + isSelected: boolean; + isDisabled?: boolean; + hasNoOptions?: boolean; +}>` position: relative; display: flex; align-items: center; background-color: ${({ isSelected }) => isSelected ? "var(--map-floor-color)" : "var(--bg-color)"}; - border: 1px solid var(--toggle-switch-off-bg-color); + border: 1px solid var(--line-color); border-width: 0px 1px 1px 1px; width: 100%; height: 60px; text-align: start; padding-left: 20px; font-size: 1.125rem; - color: ${({ isSelected }) => - isSelected ? "var(--sys-main-color)" : "var(--normal-text-color)"}; - cursor: pointer; + color: ${( + { isSelected, isDisabled } // 비활성화 된 항목은 --capsule-btn-border-color 로 띄우고 클릭 못하게 + ) => + isDisabled + ? "var(--capsule-btn-border-color)" + : isSelected + ? "var(--sys-main-color)" + : "var(--normal-text-color)"}; + cursor: ${({ isDisabled }) => (isDisabled ? "not-allowed" : "pointer")}; &:first-child { border-radius: 10px 10px 0px 0px; border-width: 1px 1px 1px 1px; } &:last-child { - border-radius: 0px 0px 10px 10px; + border-radius: ${(props) => + props.hasNoOptions ? "10px" : "0px 0px 10px 10px"}; } + &:hover { background-color: var(--map-floor-color); } @@ -155,9 +178,28 @@ const OptionsImgStyled = styled.div<{ isSelected?: boolean }>` width: 18px; height: 18px; } + & > svg > path { stroke: var(--normal-text-color); - transform: scale(0.8); } `; + +const DropdownSelectionBoxIconStyled = styled.div<{ isOpen: boolean }>` + width: 14px; + height: 8px; + display: flex; + position: absolute; + top: 45%; + left: 85%; + ${({ isOpen }) => + isOpen === true && + css` + transform: scaleY(-1); + `} + + & > svg > path { + stroke: var(--line-color); + } +`; + export default Dropdown; diff --git a/frontend/src/Cabinet/components/Common/MultiSelectFilterButton.tsx b/frontend/src/Cabinet/components/Common/MultiSelectFilterButton.tsx index 9a2a71785..76355fa3b 100644 --- a/frontend/src/Cabinet/components/Common/MultiSelectFilterButton.tsx +++ b/frontend/src/Cabinet/components/Common/MultiSelectFilterButton.tsx @@ -50,7 +50,7 @@ const FilterTextWrapperStyled = styled.div<{ isClicked: boolean }>` justify-content: center; align-items: center; color: ${({ isClicked }) => - isClicked ? "var(--sys-main-color)" : "var(--toggle-switch-off-bg-color)"}; + isClicked ? "var(--sys-main-color)" : "var(--line-color)"}; font-size: 1rem; `; diff --git a/frontend/src/Cabinet/components/Common/SelectInduction.tsx b/frontend/src/Cabinet/components/Common/SelectInduction.tsx new file mode 100644 index 000000000..e6c36903b --- /dev/null +++ b/frontend/src/Cabinet/components/Common/SelectInduction.tsx @@ -0,0 +1,43 @@ +import styled from "styled-components"; +import { ReactComponent as LogoImg } from "@/Cabinet/assets/images/logo.svg"; + +const SelectInduction = ({ msg }: { msg: string }) => { + return ( + + + + + {msg} + + ); +}; + +const WrapperStyled = styled.div` + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +`; + +const CabiLogoStyled = styled.div` + width: 35px; + height: 35px; + margin-bottom: 10px; + svg { + .logo_svg__currentPath { + fill: var(--sys-main-color); + } + } +`; + +const MsgStyled = styled.p` + font-size: 1.125rem; + font-weight: 400; + line-height: 28px; + color: var(--gray-line-btn-color); + text-align: center; + white-space: pre-line; +`; + +export default SelectInduction; diff --git a/frontend/src/Cabinet/components/Common/Selector.tsx b/frontend/src/Cabinet/components/Common/Selector.tsx index 170761990..ea90c4b35 100644 --- a/frontend/src/Cabinet/components/Common/Selector.tsx +++ b/frontend/src/Cabinet/components/Common/Selector.tsx @@ -1,18 +1,17 @@ -import PillButton from "@/Cabinet/components/Common/PillButton"; import styled from "styled-components"; +import PillButton from "@/Cabinet/components/Common/PillButton"; interface ISelectorProps { - iconSrc?: string; + icon?: React.FunctionComponent>; selectList: { key: number; value: string }[]; onClickSelect: any; } -const Selector = ({ iconSrc, selectList, onClickSelect }: ISelectorProps) => { +const Selector = ({ icon, selectList, onClickSelect }: ISelectorProps) => { + const Icon = icon; return ( - - - + {Icon && } {selectList && selectList.map((elem) => { return ( @@ -42,6 +41,10 @@ const IconWrapperStyled = styled.div` width: 24px; height: 24px; margin-bottom: 12px; + + & > svg > path { + stroke: var(--normal-text-color); + } `; export default Selector; diff --git a/frontend/src/Cabinet/components/Common/ToggleSwitch.tsx b/frontend/src/Cabinet/components/Common/ToggleSwitch.tsx index 8d50baf2d..49c068d39 100644 --- a/frontend/src/Cabinet/components/Common/ToggleSwitch.tsx +++ b/frontend/src/Cabinet/components/Common/ToggleSwitch.tsx @@ -57,9 +57,7 @@ const ToggleSwitchStyled = styled.label<{ display: inline-block; position: relative; background: ${(props) => - props.checked - ? "var(--sys-main-color)" - : "var(--toggle-switch-off-bg-color)"}; + props.checked ? "var(--sys-main-color)" : "var(--line-color)"}; width: 56px; height: 28px; border-radius: 50px; diff --git a/frontend/src/Cabinet/components/Common/UnavailableDataInfo.tsx b/frontend/src/Cabinet/components/Common/UnavailableDataInfo.tsx new file mode 100644 index 000000000..b5f703049 --- /dev/null +++ b/frontend/src/Cabinet/components/Common/UnavailableDataInfo.tsx @@ -0,0 +1,68 @@ +import styled from "styled-components"; +import { ReactComponent as SadCabiIcon } from "@/Cabinet/assets/images/sadCcabi.svg"; + +const UnavailableDataInfo = ({ + msg, + height, + fontSize, + iconWidth, + iconHeight, +}: { + msg: string; + height?: string; + fontSize?: string; + iconWidth?: string; + iconHeight?: string; +}) => { + return ( + + + {msg} + + + + + + ); +}; + +const EmptyWrapperStyled = styled.div` + display: flex; + justify-content: center; + align-items: center; + height: 100%; +`; + +const EmptyItemUsageLogTextStyled = styled.div<{ + height: string | undefined; + fontSize: string | undefined; +}>` + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + font-size: ${(props) => (props.fontSize ? props.fontSize : "1.125rem")}; + line-height: 1.75rem; + color: var(--gray-line-btn-color); + height: ${(props) => props.height && props.height}; +`; + +const SadCcabiIconStyled = styled.div<{ + width: string | undefined; + height: string | undefined; +}>` + width: ${(props) => (props.width ? props.width : "30px")}; + height: ${(props) => (props.height ? props.height : "30px")}; + margin-left: 10px; + + & > svg { + width: ${(props) => (props.width ? props.width : "30px")}; + height: ${(props) => (props.height ? props.height : "30px")}; + } + + & > svg > path { + fill: var(--normal-text-color); + } +`; + +export default UnavailableDataInfo; diff --git a/frontend/src/Cabinet/components/Common/WarningNotification.tsx b/frontend/src/Cabinet/components/Common/WarningNotification.tsx index 6e00574f1..455d9c8dc 100644 --- a/frontend/src/Cabinet/components/Common/WarningNotification.tsx +++ b/frontend/src/Cabinet/components/Common/WarningNotification.tsx @@ -1,5 +1,6 @@ import React from "react"; import styled from "styled-components"; +import { ReactComponent as WarningIcon } from "@/Cabinet/assets/images/warningTriangleIcon.svg"; export interface WarningNotificationProps { isVisible: boolean; @@ -12,25 +13,14 @@ const WarningNotification: React.FC = ({ }: WarningNotificationProps) => { return ( - + + + {message} ); }; -const WarningIcon = styled.div<{ isVisible: boolean }>` - display: ${({ isVisible }) => (isVisible ? "block" : "none")}; - background-image: url("/src/Cabinet/assets/images/warningTriangleIcon.svg"); - width: 24px; - height: 24px; - margin: 0px auto; - opacity: 0.6; - cursor: pointer; - &:hover { - opacity: 1; - } -`; - const WarningBox = styled.div` position: relative; margin: 10px auto; @@ -50,15 +40,27 @@ const WarningBox = styled.div` padding 0.5s ease-in-out; `; +const IconWrapperStyled = styled.div<{ isVisible: boolean }>` + display: ${({ isVisible }) => (isVisible ? "block" : "none")}; + width: 24px; + height: 24px; + cursor: pointer; + margin: 0px auto; + opacity: 0.6; + &:hover { + opacity: 1; + } +`; + const WarningWrapper = styled.div<{ isVisible: boolean }>` display: ${({ isVisible }) => (isVisible ? "block" : "none")}; position: relative; width: 100%; height: 24px; justify-content: center; - & ${WarningIcon}:hover + ${WarningBox} { + & ${IconWrapperStyled}:hover + ${WarningBox} { visibility: visible; - color: var(--normal-text-color); + color: var(--white-text-with-bg-color); background-color: var(--tooltip-shadow-color); &:before { border-color: transparent transparent var(--tooltip-shadow-color) diff --git a/frontend/src/Cabinet/components/Home/ManualContentBox.tsx b/frontend/src/Cabinet/components/Home/ManualContentBox.tsx index 1af0d4d15..5f4885e30 100644 --- a/frontend/src/Cabinet/components/Home/ManualContentBox.tsx +++ b/frontend/src/Cabinet/components/Home/ManualContentBox.tsx @@ -1,12 +1,7 @@ import styled, { css, keyframes } from "styled-components"; import { manualContentData } from "@/Cabinet/assets/data/ManualContent"; -import { ReactComponent as ClockImg } from "@/Cabinet/assets/images/clock.svg"; -import { ReactComponent as ClubIcon } from "@/Cabinet/assets/images/clubIcon.svg"; -import { ReactComponent as ExtensionIcon } from "@/Cabinet/assets/images/extension.svg"; import { ReactComponent as ManualPeopleImg } from "@/Cabinet/assets/images/manualPeople.svg"; import { ReactComponent as MoveBtnImg } from "@/Cabinet/assets/images/moveButton.svg"; -import { ReactComponent as PrivateIcon } from "@/Cabinet/assets/images/privateIcon.svg"; -import { ReactComponent as ShareIcon } from "@/Cabinet/assets/images/shareIcon.svg"; import ContentStatus from "@/Cabinet/types/enum/content.status.enum"; interface MaunalContentBoxProps { @@ -22,24 +17,17 @@ const MaunalContentBox = ({ contentStatus }: MaunalContentBoxProps) => { contentStatus={contentStatus} > {contentStatus === ContentStatus.EXTENSION && ( - + )} - {contentStatus === ContentStatus.PRIVATE && ( - - )} - {contentStatus === ContentStatus.SHARE && ( - - )} - {contentStatus === ContentStatus.CLUB && ( - - )} - {contentStatus === ContentStatus.EXTENSION && ( - - )} - - {contentStatus === ContentStatus.IN_SESSION && ( - + {contentStatus !== ContentStatus.IN_SESSION && + contentData.iconComponent && ( + )} + + {contentStatus === ContentStatus.IN_SESSION && + contentData.iconComponent && ( + + )}

{contentData.contentTitle}

@@ -49,9 +37,9 @@ const MaunalContentBox = ({ contentStatus }: MaunalContentBoxProps) => { const Rotation = keyframes` to { - transform : rotate(360deg) + transform : rotate(360deg) } -`; + `; const MaunalContentBoxStyled = styled.div<{ background: string; @@ -76,6 +64,7 @@ const MaunalContentBoxStyled = styled.div<{ margin-right: 10px; margin-top: 160px; animation: ${Rotation} 1s linear infinite; + stroke: var(--sys-main-color); } .contentImg { @@ -87,10 +76,6 @@ const MaunalContentBoxStyled = styled.div<{ props.contentStatus === ContentStatus.EXTENSION ? "var(--normal-text-color)" : "var(--white-text-with-bg-color)"}; - transform: ${(props) => - props.contentStatus === ContentStatus.EXTENSION - ? "scale(1.4)" - : "scale(3.3)"}; } } @@ -101,6 +86,7 @@ const MaunalContentBoxStyled = styled.div<{ position: absolute; right: 100px; bottom: 30px; + fill: var(--sys-main-color); } ${({ contentStatus }) => diff --git a/frontend/src/Cabinet/components/Home/ServiceManual.tsx b/frontend/src/Cabinet/components/Home/ServiceManual.tsx index 29da0ccf1..94f766f70 100644 --- a/frontend/src/Cabinet/components/Home/ServiceManual.tsx +++ b/frontend/src/Cabinet/components/Home/ServiceManual.tsx @@ -147,7 +147,7 @@ const NotionBtn = styled.button` font-size: 0.875rem; color: var(--notion-btn-text-color); background: var(--bg-color); - border: 1px solid var(--toggle-switch-off-bg-color); + border: 1px solid var(--line-color); :hover { color: var(--normal-text-color); font-weight: 400; diff --git a/frontend/src/Cabinet/components/ItemLog/AdminItemProvideLog.container.tsx b/frontend/src/Cabinet/components/ItemLog/AdminItemProvideLog.container.tsx new file mode 100644 index 000000000..1cca6cf96 --- /dev/null +++ b/frontend/src/Cabinet/components/ItemLog/AdminItemProvideLog.container.tsx @@ -0,0 +1,112 @@ +import { useEffect, useState } from "react"; +import AdminItemProvideLog from "@/Cabinet/components/ItemLog/AdminItemProvideLog"; +import { ItemLogResponseType } from "@/Cabinet/types/dto/admin.dto"; +import useMenu from "@/Cabinet/hooks/useMenu"; +import { STATUS_400_BAD_REQUEST } from "@/Cabinet/constants/StatusCode"; + +const AdminItemProvideLogContainer = () => { + const { closeStore } = useMenu(); + const [logs, setLogs] = useState({ + itemHistories: [], + totalLength: 0, + }); + const [page, setPage] = useState(0); + const [totalPage, setTotalPage] = useState(-1); + const [needsUpdate, setNeedsUpdate] = useState(false); + const size = 8; + + const mockItemHistories = [ + { + items: [ + { + itemSku: "SKU1001", + itemName: "이사권", + itemDetails: "이사권", + issuedDate: "2024-05-28T10:00:00", + }, + { + itemSku: "SKU1002", + itemName: "알림 등록권", + itemDetails: "알림 등록권", + issuedDate: "2024-05-28T11:00:00", + }, + { + itemSku: "SKU1003", + itemName: "페널티권", + itemDetails: "31일", + issuedDate: "2024-05-28T12:00:00", + }, + { + itemSku: "SKU1004", + itemName: "연장권", + itemDetails: "15일", + issuedDate: "2024-05-28T13:00:00", + }, + ], + }, + ]; + + async function getData(page: number) { + try { + const items = mockItemHistories[0].items; + const startIndex = page * size; + const endIndex = startIndex + size; + const paginatedItems = items.slice(startIndex, endIndex); + + const paginatedData = { + itemHistories: paginatedItems, + totalLength: items.length, + }; + + setLogs(paginatedData); + setTotalPage(Math.ceil(paginatedData.totalLength / size)); + } catch (error) { + console.error("Failed to fetch data:", error); + setLogs({ itemHistories: [], totalLength: 0 }); + } + } + + useEffect(() => { + getData(page); + }, [page]); + + useEffect(() => { + if (needsUpdate) { + getData(page); + setNeedsUpdate(false); + } + }, [needsUpdate, page]); + + const onClickPrev = () => { + if (page > 0) { + setPage((prev) => prev - 1); + setNeedsUpdate(true); + } + }; + + const onClickNext = () => { + if (page < totalPage - 1) { + setPage((prev) => prev + 1); + setNeedsUpdate(true); + } + }; + + const closeAndResetLogPage = () => { + closeStore(); + setPage(0); + setNeedsUpdate(true); + }; + + return ( + + ); +}; + +export default AdminItemProvideLogContainer; diff --git a/frontend/src/Cabinet/components/ItemLog/AdminItemProvideLog.tsx b/frontend/src/Cabinet/components/ItemLog/AdminItemProvideLog.tsx new file mode 100644 index 000000000..b97f6dc4a --- /dev/null +++ b/frontend/src/Cabinet/components/ItemLog/AdminItemProvideLog.tsx @@ -0,0 +1,124 @@ +import styled, { css } from "styled-components"; +import AdminItemProvideTable from "@/Cabinet/components/ItemLog/ItemLogTable/AdminItemProvideTable"; +import { IItemLog } from "@/Cabinet/types/dto/admin.dto"; + +const AdminItemProvideLog = ({ + closeItem, + logs, + page, + totalPage, + onClickPrev, + onClickNext, +}: IItemLog) => { + return ( + + + + + + + + + + + + + + + + + + + + ); +}; + +const AdminItemUsageLogStyled = styled.div` + width: 100%; + position: relative; +`; + +const ButtonContainerStyled = styled.div` + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + opacity: 0.3; + display: flex; + justify-content: space-between; + align-items: center; + margin: 0 auto; + overflow: hidden; + &:hover > .logPageButton { + opacity: 1; + } +`; + +const PageButtonStyled = styled.div<{ + page: number; + totalPage: number; + type: string; +}>` + cursor: pointer; + width: 40px; + height: 100%; + border-radius: 10px; + position: absolute; + opacity: 0.5; + transition: opacity 0.5s; + background: linear-gradient( + to left, + transparent, + var(--page-btn-shadow-color) + ); + display: ${({ page, totalPage, type }) => { + if (type == "prev" && page == 0) return "none"; + if (type == "next" && (totalPage == 0 || page == totalPage - 1)) + return "none"; + return "block"; + }}; + ${({ type }) => + type === "prev" + ? css` + left: 0; + ` + : css` + right: 0; + transform: rotate(-180deg); + `} +`; + +const ImgCenterStyled = styled.div` + display: flex; + align-items: center; + justify-content: center; + height: 100%; +`; + +const ImageStyled = styled.div` + width: 40px; + margin-right: 4px; + filter: brightness(0%); + border-radius: 50%; +`; + +export default AdminItemProvideLog; diff --git a/frontend/src/Cabinet/components/ItemLog/AdminItemUsageLog.container.tsx b/frontend/src/Cabinet/components/ItemLog/AdminItemUsageLog.container.tsx new file mode 100644 index 000000000..ff26aba0c --- /dev/null +++ b/frontend/src/Cabinet/components/ItemLog/AdminItemUsageLog.container.tsx @@ -0,0 +1,79 @@ +import { useEffect, useState } from "react"; +import { useRecoilState } from "recoil"; +import { targetUserInfoState } from "@/Cabinet/recoil/atoms"; +import AdminItemUsageLog from "@/Cabinet/components/ItemLog/AdminItemUsageLog"; +import { ItemLogResponseType } from "@/Cabinet/types/dto/admin.dto"; +import { axiosGetUserItems } from "@/Cabinet/api/axios/axios.custom"; +import useMenu from "@/Cabinet/hooks/useMenu"; + +const AdminItemUsageLogContainer = () => { + const { closeStore } = useMenu(); + const [userId, setUserId] = useState(0); + const [targetUserInfo] = useRecoilState(targetUserInfoState); + const [logs, setLogs] = useState({ + itemHistories: [], + totalLength: 0, + }); + const [page, setPage] = useState(0); + const [totalPage, setTotalPage] = useState(-1); + const [needsUpdate, setNeedsUpdate] = useState(true); + const size = 8; + + async function getData(page: number) { + try { + const paginatedData = await axiosGetUserItems( + targetUserInfo.userId!, + page, + size + ); + setLogs({ + itemHistories: paginatedData.data.itemHistories, + totalLength: paginatedData.data.totalLength, + }); + setTotalPage(Math.ceil(paginatedData.data.totalLength / size)); + } catch { + setLogs({ itemHistories: [], totalLength: 0 }); + setTotalPage(1); + } + } + + useEffect(() => { + if (needsUpdate) { + getData(page); + setNeedsUpdate(false); + } + }, [needsUpdate, page]); + + const onClickPrev = () => { + if (page > 0) { + setPage((prev) => prev - 1); + setNeedsUpdate(true); + } + }; + + const onClickNext = () => { + if (page < totalPage - 1) { + setPage((prev) => prev + 1); + setNeedsUpdate(true); + } + }; + + const closeAndResetLogPage = () => { + closeStore(); + setPage(0); + setNeedsUpdate(true); + }; + + return ( + + ); +}; + +export default AdminItemUsageLogContainer; diff --git a/frontend/src/Cabinet/components/ItemLog/AdminItemUsageLog.tsx b/frontend/src/Cabinet/components/ItemLog/AdminItemUsageLog.tsx new file mode 100644 index 000000000..164924250 --- /dev/null +++ b/frontend/src/Cabinet/components/ItemLog/AdminItemUsageLog.tsx @@ -0,0 +1,124 @@ +import styled, { css } from "styled-components"; +import AdminItemLogTable from "@/Cabinet/components/ItemLog/ItemLogTable/AdminItemLogTable"; +import { IItemLog } from "@/Cabinet/types/dto/admin.dto"; + +const AdminItemUsageLog = ({ + closeItem, + logs, + page, + totalPage, + onClickPrev, + onClickNext, +}: IItemLog) => { + return ( + + + + + + + + + + + + + + + + + + + + ); +}; + +const AdminItemUsageLogStyled = styled.div` + width: 100%; + position: relative; +`; + +const ButtonWrapperStyled = styled.div` + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + opacity: 0.3; + display: flex; + justify-content: space-between; + align-items: center; + margin: 0 auto; + overflow: hidden; + &:hover > .logPageButton { + opacity: 1; + } +`; + +const PageButtonStyled = styled.div<{ + page: number; + totalPage: number; + type: string; +}>` + cursor: pointer; + width: 40px; + height: 100%; + border-radius: 10px; + position: absolute; + opacity: 0.5; + transition: opacity 0.5s; + background: linear-gradient( + to left, + transparent, + var(--page-btn-shadow-color) + ); + display: ${({ page, totalPage, type }) => { + if (type == "prev" && page == 0) return "none"; + if (type == "next" && (totalPage == 0 || page == totalPage - 1)) + return "none"; + return "block"; + }}; + ${({ type }) => + type === "prev" + ? css` + left: 0; + ` + : css` + right: 0; + transform: rotate(-180deg); + `} +`; + +const ImgCenterStyled = styled.div` + display: flex; + align-items: center; + justify-content: center; + height: 100%; +`; + +const ImageStyled = styled.div` + width: 40px; + margin-right: 4px; + filter: brightness(0%); + border-radius: 50%; +`; + +export default AdminItemUsageLog; diff --git a/frontend/src/Cabinet/components/ItemLog/ItemLogTable/AdminItemLogTable.tsx b/frontend/src/Cabinet/components/ItemLog/ItemLogTable/AdminItemLogTable.tsx new file mode 100644 index 000000000..07a754416 --- /dev/null +++ b/frontend/src/Cabinet/components/ItemLog/ItemLogTable/AdminItemLogTable.tsx @@ -0,0 +1,113 @@ +import styled from "styled-components"; +import LoadingAnimation from "@/Cabinet/components/Common/LoadingAnimation"; +import { ItemLogResponseType } from "@/Cabinet/types/dto/admin.dto"; +import { STATUS_400_BAD_REQUEST } from "@/Cabinet/constants/StatusCode"; + +const dateOptions: Intl.DateTimeFormatOptions = { + year: "2-digit", + month: "2-digit", + day: "2-digit", +}; + +const AdminItemLogTable = ({ itemLog }: { itemLog: ItemLogResponseType }) => { + if (!itemLog) return ; + return ( + + + + + 발급일 + 아이템 + 사용일 + + + {itemLog !== STATUS_400_BAD_REQUEST && + Array.isArray(itemLog.itemHistories) && ( + + {itemLog.itemHistories.map( + ({ purchaseAt, itemName, itemDetails, usedAt }, idx) => ( + + + {new Date(purchaseAt ?? "").toLocaleString( + "ko-KR", + dateOptions + )} + + + {itemName !== itemDetails + ? `${itemName} - ${itemDetails}` + : itemName} + + + {usedAt + ? new Date(usedAt).toLocaleString("ko-KR", dateOptions) + : "-"} + + + ) + )} + + )} + + {(itemLog === STATUS_400_BAD_REQUEST || + itemLog.totalLength === undefined || + itemLog.totalLength === 0) && ( + 아이템 내역이 없습니다. + )} + + ); +}; + +const LogTableWrapperstyled = styled.div` + width: 100%; + max-width: 800px; + border-radius: 10px; + overflow: hidden; + margin: 0 auto; + box-shadow: 0 0 10px 0 var(--table-border-shadow-color-100); +`; + +const LogTableStyled = styled.table` + width: 100%; + background: var(--bg-color); + overflow: scroll; +`; + +const TheadStyled = styled.thead` + width: 100%; + height: 50px; + line-height: 50px; + background-color: var(--sys-main-color); + color: var(--white-text-with-bg-color); +`; + +const TbodyStyled = styled.tbody` + & > tr { + font-size: 11px; + text-align: center; + height: 50px; + } + & > tr > td { + height: 50px; + line-height: 50px; + width: 33.3%; + } + & > tr:nth-child(2n) { + background: var(--table-even-row-bg-color); + } +`; + +const EmptyLogStyled = styled.div` + width: 100%; + text-align: center; + font-size: 1rem; + padding: 20px 0; +`; + +export default AdminItemLogTable; diff --git a/frontend/src/Cabinet/components/ItemLog/ItemLogTable/AdminItemProvideTable.tsx b/frontend/src/Cabinet/components/ItemLog/ItemLogTable/AdminItemProvideTable.tsx new file mode 100644 index 000000000..d9d5c2930 --- /dev/null +++ b/frontend/src/Cabinet/components/ItemLog/ItemLogTable/AdminItemProvideTable.tsx @@ -0,0 +1,111 @@ +import styled from "styled-components"; +import LoadingAnimation from "@/Cabinet/components/Common/LoadingAnimation"; +import { ItemLogResponseType } from "@/Cabinet/types/dto/admin.dto"; +import { STATUS_400_BAD_REQUEST } from "@/Cabinet/constants/StatusCode"; + +const dateOptions: Intl.DateTimeFormatOptions = { + year: "2-digit", + month: "2-digit", + day: "2-digit", +}; + +const AdminItemProvideTable = ({ + itemLog, +}: { + itemLog: ItemLogResponseType; +}) => { + if (itemLog === undefined) return ; + + return ( + + + + + 지급일 + 아이템 + + + {itemLog !== STATUS_400_BAD_REQUEST && ( + + {itemLog.itemHistories.map( + ({ issuedDate, itemName, itemDetails, usedAt }, idx) => ( + + + {issuedDate + ? new Date(issuedDate).toLocaleString( + "ko-KR", + dateOptions + ) + : ""} + + + {itemName !== itemDetails + ? `${itemName} - ${itemDetails}` + : itemName} + + + ) + )} + + )} + + {itemLog === STATUS_400_BAD_REQUEST && ( + 아이템 사용기록이 없습니다. + )} + + ); +}; + +const LogTableWrapperstyled = styled.div` + width: 100%; + max-width: 800px; + border-radius: 10px; + overflow: hidden; + margin: 0 auto; + box-shadow: 0 0 10px 0 var(--table-border-shadow-color-100); +`; + +const LogTableStyled = styled.table` + width: 100%; + background: var(--bg-color); + overflow: scroll; +`; + +const TheadStyled = styled.thead` + width: 100%; + height: 50px; + line-height: 50px; + background-color: var(--sys-main-color); + color: var(--white-text-with-bg-color); +`; + +const TbodyStyled = styled.tbody` + & > tr { + font-size: small; + text-align: center; + height: 50px; + } + & > tr > td { + height: 50px; + line-height: 50px; + width: 33.3%; + } + & > tr:nth-child(2n) { + background: var(--table-even-row-bg-color); + } +`; + +const EmptyLogStyled = styled.div` + width: 100%; + text-align: center; + font-size: 1rem; + padding: 20px 0; +`; + +export default AdminItemProvideTable; diff --git a/frontend/src/Cabinet/components/LeftNav/LeftSectionNav/LeftSectionNavClubs.tsx b/frontend/src/Cabinet/components/LeftNav/LeftClubNav/LeftClubNav.tsx similarity index 92% rename from frontend/src/Cabinet/components/LeftNav/LeftSectionNav/LeftSectionNavClubs.tsx rename to frontend/src/Cabinet/components/LeftNav/LeftClubNav/LeftClubNav.tsx index 5224eb6a8..09d363114 100644 --- a/frontend/src/Cabinet/components/LeftNav/LeftSectionNav/LeftSectionNavClubs.tsx +++ b/frontend/src/Cabinet/components/LeftNav/LeftClubNav/LeftClubNav.tsx @@ -6,13 +6,11 @@ import { ClubPaginationResponseDto, ClubResponseDto, } from "@/Cabinet/types/dto/club.dto"; -import useMenu from "@/Cabinet/hooks/useMenu"; -const LeftSectionNavClubs = () => { +const LeftClubNav = ({ closeLeftNav }: { closeLeftNav: () => void }) => { const clubList = useRecoilValue(myClubListState); const [targetClubInfo, setTargetClubInfo] = useRecoilState(targetClubInfoState); - const { closeLeftNav } = useMenu(); return ( <> @@ -48,6 +46,7 @@ const ClubLeftNavOptionStyled = styled.div` padding: 32px 10px 32px; border-right: 1px solid var(--line-color); font-weight: 300; + font-size: var(--size-base); position: relative; font-size: var(--size-base); & hr { @@ -67,4 +66,4 @@ const ListTitleStyled = styled.div` font-weight: 500; `; -export default LeftSectionNavClubs; +export default LeftClubNav; diff --git a/frontend/src/Cabinet/components/LeftNav/LeftMainNav/LeftMainNav.container.tsx b/frontend/src/Cabinet/components/LeftNav/LeftMainNav/LeftMainNav.container.tsx index 90dfce091..f5ed614ab 100644 --- a/frontend/src/Cabinet/components/LeftNav/LeftMainNav/LeftMainNav.container.tsx +++ b/frontend/src/Cabinet/components/LeftNav/LeftMainNav/LeftMainNav.container.tsx @@ -12,8 +12,10 @@ import { currentFloorNumberState, currentMapFloorState, currentSectionNameState, + isCurrentSectionRenderState, myCabinetInfoState, numberOfAdminWorkState, + selectedTypeOnSearchState, } from "@/Cabinet/recoil/atoms"; import { currentBuildingFloorState } from "@/Cabinet/recoil/selectors"; import LeftMainNav from "@/Cabinet/components/LeftNav/LeftMainNav/LeftMainNav"; @@ -21,6 +23,7 @@ import { CabinetInfoByBuildingFloorDto, MyCabinetInfoResponseDto, } from "@/Cabinet/types/dto/cabinet.dto"; +import CabinetDetailAreaType from "@/Cabinet/types/enum/cabinetDetailArea.type.enum"; import { axiosCabinetByBuildingFloor } from "@/Cabinet/api/axios/axios.custom"; import { removeCookie } from "@/Cabinet/api/react_cookie/cookies"; import useMenu from "@/Cabinet/hooks/useMenu"; @@ -45,6 +48,10 @@ const LeftMainNavContainer = ({ isAdmin }: { isAdmin?: boolean }) => { const numberOfAdminWork = useRecoilValue(numberOfAdminWorkState); const navigator = useNavigate(); const { pathname } = useLocation(); + const [isCurrentSectionRender] = useRecoilState(isCurrentSectionRenderState); + const setSelectedTypeOnSearch = useSetRecoilState( + selectedTypeOnSearchState + ); useEffect(() => { if (currentFloor === undefined) { @@ -79,11 +86,13 @@ const LeftMainNavContainer = ({ isAdmin }: { isAdmin?: boolean }) => { myCabinetInfo?.cabinetId, numberOfAdminWork, myCabinetInfo?.status, + isCurrentSectionRender, ]); const onClickFloorButton = (floor: number) => { setCurrentFloor(floor); setCurrentMapFloor(floor); + setSelectedTypeOnSearch(CabinetDetailAreaType.CABINET); if (!pathname.includes("main")) { if (floor === currentFloor) { axiosCabinetByBuildingFloor(currentBuilding, currentFloor).then( @@ -118,7 +127,12 @@ const LeftMainNavContainer = ({ isAdmin }: { isAdmin?: boolean }) => { navigator("slack-notification"); closeAll(); }; - + + const onClickStoreButton = (): void => { + navigator("store"); + closeAll(); + }; + const onClickMainClubButton = () => { navigator("clubs"); closeAll(); @@ -152,7 +166,7 @@ const LeftMainNavContainer = ({ isAdmin }: { isAdmin?: boolean }) => { resetCurrentSection(); navigator("/login"); }; - + return ( { onClickMainClubButton={onClickMainClubButton} onClickProfileButton={onClickProfileButton} onClickAvailableButton={onClickAvailableButton} + onClickStoreButton={onClickStoreButton} isAdmin={isAdmin} /> ); diff --git a/frontend/src/Cabinet/components/LeftNav/LeftMainNav/LeftMainNav.tsx b/frontend/src/Cabinet/components/LeftNav/LeftMainNav/LeftMainNav.tsx index 548466036..2251e3ca4 100644 --- a/frontend/src/Cabinet/components/LeftNav/LeftMainNav/LeftMainNav.tsx +++ b/frontend/src/Cabinet/components/LeftNav/LeftMainNav/LeftMainNav.tsx @@ -1,10 +1,10 @@ import styled from "styled-components"; import { ReactComponent as LogoutImg } from "@/Cabinet/assets/images/close-square.svg"; -import { ReactComponent as CulbImg } from "@/Cabinet/assets/images/clubIconGray.svg"; +import { ReactComponent as ClubImg } from "@/Cabinet/assets/images/clubIconGray.svg"; import { ReactComponent as ProfileImg } from "@/Cabinet/assets/images/profile-circle.svg"; -import { ReactComponent as SearchImg } from "@/Cabinet/assets/images/search.svg"; import { ReactComponent as SlackNotiImg } from "@/Cabinet/assets/images/slack-notification.svg"; import { ReactComponent as SlackImg } from "@/Cabinet/assets/images/slack.svg"; +import { ReactComponent as StoreImg } from "@/Cabinet/assets/images/storeIconGray.svg"; interface ILeftMainNav { pathname: string; @@ -20,6 +20,7 @@ interface ILeftMainNav { onClickMainClubButton: React.MouseEventHandler; onClickProfileButton: React.MouseEventHandler; onClickAvailableButton: React.MouseEventHandler; + onClickStoreButton: React.MouseEventHandler; isAdmin?: boolean; } @@ -32,11 +33,11 @@ const LeftMainNav = ({ onClickFloorButton, onClickLogoutButton, onClickSlackNotiButton, - onClickSearchButton, onClickAdminClubButton, onClickMainClubButton, onClickProfileButton, onClickAvailableButton, + onClickStoreButton, isAdmin, }: ILeftMainNav) => { return ( @@ -89,25 +90,25 @@ const LeftMainNav = ({ <> - - Noti + + Store - - Search + + Noti - + Club - + Logout )} {!isAdmin && currentBuildingName === "새롬관" && ( <> + + + Store + - + Clubs - + Profile @@ -198,6 +210,8 @@ const TopBtnStyled = styled.li` width: 100%; height: 48px; line-height: 48px; + /* font-size: var(--size-base); */ + font-size: var(--size-base); font-weight: 300; margin-bottom: 2.5vh; border-radius: 10px; @@ -227,7 +241,7 @@ const BottomSectionStyled = styled.section` margin: 0 auto; width: 56px; height: 1px; - background-color: var(--toggle-switch-off-bg-color); + background-color: var(--line-color); } `; @@ -240,6 +254,7 @@ const BottomBtnStyled = styled.li` width: 100%; min-height: 48px; line-height: 1.125rem; + font-size: var(--size-base); font-weight: 300; margin-top: 2.5vh; border-radius: 10px; @@ -268,8 +283,11 @@ const BottomBtnStyled = styled.li` stroke: var(--button-line-color); } } - svg { + & > svg { margin: 0 auto; + width: 24px; + height: 24px; + stroke: var(--gray-line-btn-color); } @media (hover: hover) and (pointer: fine) { &:hover { diff --git a/frontend/src/Cabinet/components/LeftNav/LeftNav.tsx b/frontend/src/Cabinet/components/LeftNav/LeftNav.tsx index e7f68f4ce..409446e82 100644 --- a/frontend/src/Cabinet/components/LeftNav/LeftNav.tsx +++ b/frontend/src/Cabinet/components/LeftNav/LeftNav.tsx @@ -1,15 +1,38 @@ +import { useNavigate } from "react-router-dom"; import styled from "styled-components"; +import LeftClubNav from "@/Cabinet/components/LeftNav/LeftClubNav/LeftClubNav"; import LeftMainNavContainer from "@/Cabinet/components/LeftNav/LeftMainNav/LeftMainNav.container"; -import LeftSectionNavContainer from "@/Cabinet/components/LeftNav/LeftSectionNav/LeftSectionNav.container"; +import LeftProfileNav from "@/Cabinet/components/LeftNav/LeftProfileNav/LeftProfileNav"; +import LeftSectionNav from "@/Cabinet/components/LeftNav/LeftSectionNav/LeftSectionNav"; +import LeftStoreNav from "@/Cabinet/components/LeftNav/LeftStoreNav/LeftStoreNav"; +import useMenu from "@/Cabinet/hooks/useMenu"; const LeftNav: React.FC<{ isVisible: boolean; isAdmin?: boolean; }> = ({ isAdmin, isVisible }) => { + const navigator = useNavigate(); + const { closeLeftNav } = useMenu(); + const isProfilePage: boolean = location.pathname.includes("profile"); + const isMainClubPage: boolean = location.pathname === "/clubs"; + const isMainStorePage: boolean = location.pathname.includes("store"); + + const onClickRedirectButton = (location: string) => { + closeLeftNav(); + navigator(location); + }; + return ( - + {isVisible && } + {isProfilePage && ( + + )} + {isMainClubPage && } + {isMainStorePage && !isAdmin && ( + + )} ); }; diff --git a/frontend/src/Cabinet/components/LeftNav/LeftProfileNav/LeftProfileNav.tsx b/frontend/src/Cabinet/components/LeftNav/LeftProfileNav/LeftProfileNav.tsx new file mode 100644 index 000000000..cc18f9c29 --- /dev/null +++ b/frontend/src/Cabinet/components/LeftNav/LeftProfileNav/LeftProfileNav.tsx @@ -0,0 +1,118 @@ +import { useLocation } from "react-router-dom"; +import styled from "styled-components"; +import { FloorSectionStyled } from "@/Cabinet/components/LeftNav/LeftSectionNav/LeftSectionNav"; +import { ReactComponent as LinkImg } from "@/Cabinet/assets/images/link.svg"; + +const LeftProfileNav = ({ + onClickRedirectButton, +}: { + onClickRedirectButton: (location: string) => void; +}) => { + const { pathname } = useLocation(); + + const onClickSlack = () => { + window.open( + "https://42born2code.slack.com/archives/C02V6GE8LD7", + "_blank", + "noopener noreferrer" + ); + }; + + const onClickClubForm = () => { + window.open( + "https://docs.google.com/forms/d/e/1FAIpQLSfp-d7qq8gTvmQe5i6Gtv_mluNSICwuv5pMqeTBqt9NJXXP7w/closedform", + "_blank", + "noopener noreferrer" + ); + }; + + return ( + + onClickRedirectButton("profile")} + > + 내 정보 + + onClickRedirectButton("profile/log")} + > + 대여 기록 + +
+ onClickSlack()} + title="슬랙 캐비닛 채널 새창으로 열기" + > + 문의하기 + + + onClickClubForm()} + title="동아리 사물함 사용 신청서 새창으로 열기" + > + 동아리 신청서 + + +
+ ); +}; + +const ProfileLeftNavOptionStyled = styled.div` + display: block; + min-width: 240px; + height: 100%; + padding: 32px 10px; + border-right: 1px solid var(--line-color); + font-weight: 300; + font-size: var(--size-base); + position: relative; + & hr { + width: 80%; + height: 1px; + background-color: var(--inventory-item-title-border-btm-color); + border: 0; + margin-top: 20px; + margin-bottom: 20px; + } +`; + +const SectionLinkStyled = styled.div` + width: 100%; + height: 40px; + line-height: 40px; + text-indent: 20px; + margin: 2px 0; + padding-right: 30px; + cursor: pointer; + display: flex; + align-items: center; + color: var(--gray-line-btn-color); + + #linknImg { + width: 15px; + height: 15px; + margin-left: auto; + } + + @media (hover: hover) and (pointer: fine) { + &:hover { + color: var(--sys-main-color); + } + &:hover img { + filter: invert(33%) sepia(55%) saturate(3554%) hue-rotate(230deg) + brightness(99%) contrast(107%); + } + } +`; + +export default LeftProfileNav; diff --git a/frontend/src/Cabinet/components/LeftNav/LeftSectionNav/LeftSectionNav.container.tsx b/frontend/src/Cabinet/components/LeftNav/LeftSectionNav/LeftSectionNav.container.tsx deleted file mode 100644 index ee4171ec2..000000000 --- a/frontend/src/Cabinet/components/LeftNav/LeftSectionNav/LeftSectionNav.container.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { useLocation, useNavigate } from "react-router-dom"; -import { useRecoilState, useRecoilValue } from "recoil"; -import { currentSectionNameState } from "@/Cabinet/recoil/atoms"; -import { currentFloorSectionState } from "@/Cabinet/recoil/selectors"; -import LeftSectionNav from "@/Cabinet/components/LeftNav/LeftSectionNav/LeftSectionNav"; -import useMenu from "@/Cabinet/hooks/useMenu"; - -const LeftSectionNavContainer = ({ isVisible }: { isVisible: boolean }) => { - const floorSection = useRecoilValue>(currentFloorSectionState); - const [currentFloorSection, setCurrentFloorSection] = useRecoilState( - currentSectionNameState - ); - const navigator = useNavigate(); - const { pathname } = useLocation(); - const { closeLeftNav } = useMenu(); - const isProfilePage: boolean = location.pathname.includes("profile"); - const isMainClubPage: boolean = location.pathname === "/clubs"; - - const onClickSection = (section: string) => { - closeLeftNav(); - setCurrentFloorSection(section); - }; - - const onClickProfile = () => { - closeLeftNav(); - navigator("profile"); - }; - - const onClickLentLogButton = () => { - closeLeftNav(); - navigator("profile/log"); - }; - - const onClickSlack = () => { - window.open( - "https://42born2code.slack.com/archives/C02V6GE8LD7", - "_blank", - "noopener noreferrer" - ); - }; - - const onClickClubForm = () => { - window.open( - "https://docs.google.com/forms/d/e/1FAIpQLSfp-d7qq8gTvmQe5i6Gtv_mluNSICwuv5pMqeTBqt9NJXXP7w/closedform", - "_blank", - "noopener noreferrer" - ); - }; - - return ( - - ); -}; - -export default LeftSectionNavContainer; diff --git a/frontend/src/Cabinet/components/LeftNav/LeftSectionNav/LeftSectionNav.tsx b/frontend/src/Cabinet/components/LeftNav/LeftSectionNav/LeftSectionNav.tsx index 7deadb65b..5939a9405 100644 --- a/frontend/src/Cabinet/components/LeftNav/LeftSectionNav/LeftSectionNav.tsx +++ b/frontend/src/Cabinet/components/LeftNav/LeftSectionNav/LeftSectionNav.tsx @@ -1,113 +1,67 @@ +import { useLocation } from "react-router-dom"; +import { useRecoilValue } from "recoil"; +import { useRecoilState } from "recoil"; import styled from "styled-components"; +import { currentSectionNameState } from "@/Cabinet/recoil/atoms"; +import { currentFloorSectionState } from "@/Cabinet/recoil/selectors"; import CabinetColorTable from "@/Cabinet/components/LeftNav/CabinetColorTable/CabinetColorTable"; -import LeftSectionNavClubs from "@/Cabinet/components/LeftNav/LeftSectionNav/LeftSectionNavClubs"; -import { ReactComponent as LinkImg } from "@/Cabinet/assets/images/link.svg"; +import { clubSectionsData } from "@/Cabinet/assets/data/mapPositionData"; +import { ReactComponent as FilledHeartIcon } from "@/Cabinet/assets/images/filledHeart.svg"; +import { ReactComponent as LineHeartIcon } from "@/Cabinet/assets/images/lineHeart.svg"; +import { ICurrentSectionInfo } from "@/Cabinet/types/dto/cabinet.dto"; -interface ILeftSectionNav { - isVisible: boolean; - onClickSection: Function; - currentFloorSection: string; - floorSection: string[]; - isProfile: boolean; - onClickProfile: Function; - pathname: string; - onClickLentLogButton: Function; - onClickSlack: Function; - onClickClubForm: Function; - isClub: boolean; -} +const LeftSectionNav = ({ closeLeftNav }: { closeLeftNav: () => void }) => { + const floorSection = useRecoilValue>( + currentFloorSectionState + ); + const [currentFloorSection, setCurrentFloorSection] = useRecoilState( + currentSectionNameState + ); + const { pathname } = useLocation(); + const isAdmin = pathname.includes("admin"); -const LeftSectionNav = ({ - isVisible, - currentFloorSection, - onClickSection, - floorSection, - isProfile, - onClickProfile, - pathname, - onClickLentLogButton, - onClickSlack, - onClickClubForm, - isClub, -}: ILeftSectionNav) => { return ( - <> - - {floorSection.map((section: string, index: number) => ( + + {floorSection.map((section: ICurrentSectionInfo, index: number) => { + const isClubSection = clubSectionsData.find((clubSection) => { + return clubSection === section.sectionName; + }) + ? true + : false; + return ( onClickSection(section)} + onClick={() => { + closeLeftNav(); + setCurrentFloorSection(section.sectionName); + }} > - {section} + {section.sectionName} + + {!isAdmin && + !isClubSection && + (section.alarmRegistered ? ( + + ) : ( + + ))} + - ))} - - - - - onClickProfile()} - > - 내 정보 - - onClickLentLogButton()} - > - 대여 기록 - -
- onClickSlack()} - title="슬랙 캐비닛 채널 새창으로 열기" - > - 문의하기 - - - onClickClubForm()} - title="동아리 사물함 사용 신청서 새창으로 열기" - > - 동아리 신청서 - - -
- {isClub && } - + ); + })} + +
); }; -const LeftNavOptionStyled = styled.div<{ - isVisible: boolean; -}>` - display: ${(props) => (props.isVisible ? "block" : "none")}; - min-width: 240px; - height: 100%; - padding: 32px 10px; - border-right: 1px solid var(--line-color); - font-weight: 300; - position: relative; +const LeftNavOptionStyled = styled.div` font-size: var(--size-base); -`; - -const ProfileLeftNavOptionStyled = styled.div<{ - isProfile: boolean; -}>` - display: ${(props) => (props.isProfile ? "block" : "none")}; + display: block; min-width: 240px; height: 100%; padding: 32px 10px; @@ -115,14 +69,6 @@ const ProfileLeftNavOptionStyled = styled.div<{ font-weight: 300; position: relative; font-size: var(--size-base); - & hr { - width: 80%; - height: 1px; - background-color: var(--service-man-title-border-btm-color); - border: 0; - margin-top: 20px; - margin-bottom: 20px; - } `; export const FloorSectionStyled = styled.div` @@ -134,6 +80,9 @@ export const FloorSectionStyled = styled.div` color: var(--gray-line-btn-color); margin: 2px 0; cursor: pointer; + display: flex; + justify-content: space-between; + align-items: center; @media (hover: hover) and (pointer: fine) { &:hover { background-color: var(--sys-main-color); @@ -142,32 +91,15 @@ export const FloorSectionStyled = styled.div` } `; -const SectionLinkStyled = styled.div` - width: 100%; - height: 40px; - line-height: 40px; - text-indent: 20px; - margin: 2px 0; - padding-right: 30px; - cursor: pointer; +const IconWrapperStyled = styled.div` + height: 14px; + width: 14px; + margin-right: 12px; display: flex; - align-items: center; - color: var(--gray-line-btn-color); - - #linknImg { - width: 15px; - height: 15px; - margin-left: auto; - } - @media (hover: hover) and (pointer: fine) { - &:hover { - color: var(--button-line-color); - - svg { - stroke: var(--button-line-color); - } - } + & > svg { + height: 14px; + width: 14px; } `; diff --git a/frontend/src/Cabinet/components/LeftNav/LeftStoreNav/LeftStoreNav.tsx b/frontend/src/Cabinet/components/LeftNav/LeftStoreNav/LeftStoreNav.tsx new file mode 100644 index 000000000..15bfbcebb --- /dev/null +++ b/frontend/src/Cabinet/components/LeftNav/LeftStoreNav/LeftStoreNav.tsx @@ -0,0 +1,152 @@ +import { useEffect, useState } from "react"; +import { useLocation } from "react-router-dom"; +import { useRecoilState } from "recoil"; +import styled from "styled-components"; +import { userState } from "@/Cabinet/recoil/atoms"; +import { ReactComponent as CoinIcon } from "@/Cabinet/assets/images/coinIcon.svg"; + +interface IStorePageItem { + name: string; + route: string; +} + +const storePages: IStorePageItem[] = [ + { name: "까비상점", route: "/store" }, + { name: "인벤토리", route: "/store/inventory" }, + { name: "아이템 사용내역", route: "/store/item-use-log" }, + { name: "코인 내역", route: "/store/coin-log" }, +]; + +const LeftStoreNav = ({ + onClickRedirectButton, +}: { + onClickRedirectButton: (location: string) => void; +}) => { + const location = useLocation(); + const [userInfo] = useRecoilState(userState); + + const getCurrentPageName = () => { + const matchingPage = storePages.find( + (page) => page.route === location.pathname + ); + return matchingPage ? matchingPage.name : storePages[0].name; + }; + + const [currentPage, setCurrentPage] = useState(getCurrentPageName()); + + useEffect(() => { + const handleRouteChange = () => { + setCurrentPage(getCurrentPageName()); + }; + + handleRouteChange(); + window.addEventListener("popstate", handleRouteChange); + + return () => { + window.removeEventListener("popstate", handleRouteChange); + }; + }, [location]); + + return ( + <> + + + 코인 + + + + + + {userInfo.coins} 까비 + + + +
+ {storePages.map((item: IStorePageItem) => ( + { + setCurrentPage(item.name); + onClickRedirectButton(item.route); + }} + > + {item.name} + + ))} +
+ + ); +}; + +const StoreLeftNavOptionStyled = styled.div` + min-width: 240px; + height: 100%; + padding: 32px 10px; + border-right: 1px solid var(--line-color); + font-weight: 300; + font-size: var(--size-base); + position: relative; + & hr { + width: 80%; + height: 1px; + background-color: var(--inventory-item-title-border-btm-color); + border: 0; + margin-top: 20px; + margin-bottom: 20px; + } +`; + +const CoinCountStyled = styled.div` + display: flex; + width: 100%; + padding: 0px 20px 0px 20px; + align-items: center; + justify-content: space-between; + color: var(--gray-line-btn-color); +`; + +const UserCoinsWrapperStyled = styled.div` + display: flex; + align-items: center; +`; + +const CoinTextStyled = styled.span` + margin-left: 5px; + & span { + font-weight: 800; + } +`; + +const StoreSectionStyled = styled.div` + width: 100%; + height: 40px; + line-height: 40px; + border-radius: 10px; + text-indent: 20px; + color: var(--gray-line-btn-color); + margin: 2px 0; + cursor: pointer; + &.leftNavButtonActive { + background-color: var(--sys-main-color); + color: var(--white-text-with-bg-color); + } + &:hover { + background-color: var(--sys-main-color); + color: var(--white-text-with-bg-color); + } +`; + +const CoinIconStyled = styled.div` + width: 20px; + height: 20px; + & > svg > path { + stroke: var(--sys-main-color); + } +`; + +export default LeftStoreNav; diff --git a/frontend/src/Cabinet/components/LentLog/AdminLentLog.tsx b/frontend/src/Cabinet/components/LentLog/AdminLentLog.tsx index ee0942a27..142b25c0f 100644 --- a/frontend/src/Cabinet/components/LentLog/AdminLentLog.tsx +++ b/frontend/src/Cabinet/components/LentLog/AdminLentLog.tsx @@ -70,7 +70,8 @@ const TitleContainer = styled.div` display: flex; justify-content: space-between; align-items: flex-start; - margin-bottom: 30px; + margin-top: 15px; + margin-bottom: 25px; `; const TitleStyled = styled.h1<{ isClick: boolean }>` diff --git a/frontend/src/Cabinet/components/LentLog/LogTable/LogTable.tsx b/frontend/src/Cabinet/components/LentLog/LogTable/LogTable.tsx index 4faea564f..87f822758 100644 --- a/frontend/src/Cabinet/components/LentLog/LogTable/LogTable.tsx +++ b/frontend/src/Cabinet/components/LentLog/LogTable/LogTable.tsx @@ -61,7 +61,6 @@ const LogTableWrapperstyled = styled.div` max-width: 800px; border-radius: 10px; overflow: hidden; - margin: 60px 0 0 0; box-shadow: 0 0 10px 0 var(--table-border-shadow-color-100); `; diff --git a/frontend/src/Cabinet/components/Login/AdminLoginTemplate.tsx b/frontend/src/Cabinet/components/Login/AdminLoginTemplate.tsx index a0962fddc..38c54f415 100644 --- a/frontend/src/Cabinet/components/Login/AdminLoginTemplate.tsx +++ b/frontend/src/Cabinet/components/Login/AdminLoginTemplate.tsx @@ -258,7 +258,7 @@ const CardInputStyled = styled.input<{ isFocus: boolean }>` border: ${(props) => props.isFocus ? "1px solid var(--sys-main-color)" - : "1px solid var(--toggle-switch-off-bg-color)"}; + : "1px solid var(--line-color)"}; color: var(--normal-text-color); `; diff --git a/frontend/src/Cabinet/components/MapInfo/MapItem/MapItem.tsx b/frontend/src/Cabinet/components/MapInfo/MapItem/MapItem.tsx index 804d812eb..deb52f4a2 100644 --- a/frontend/src/Cabinet/components/MapInfo/MapItem/MapItem.tsx +++ b/frontend/src/Cabinet/components/MapInfo/MapItem/MapItem.tsx @@ -56,7 +56,7 @@ const ItemStyled = styled.div<{ cursor: ${({ info }) => (info.type === "floorInfo" ? "default" : "pointer")}; color: ${({ info }) => info.type === "floorInfo" - ? "var(--toggle-switch-off-bg-color)" + ? "var(--line-color)" : "var(--white-text-with-bg-color)"}; display: flex; justify-content: center; @@ -72,7 +72,7 @@ const ItemStyled = styled.div<{ ? "var(--sys-main-color)" : info.type === "floorInfo" ? "transparent" - : "var(--toggle-switch-off-bg-color)"}; + : "var(--line-color)"}; &:hover { opacity: ${({ info }) => (info.type === "cabinet" ? 0.9 : 1)}; } diff --git a/frontend/src/Cabinet/components/Modals/BanModal/BanModal.tsx b/frontend/src/Cabinet/components/Modals/BanModal/BanModal.tsx index 838f8fb59..5690e0184 100644 --- a/frontend/src/Cabinet/components/Modals/BanModal/BanModal.tsx +++ b/frontend/src/Cabinet/components/Modals/BanModal/BanModal.tsx @@ -1,24 +1,24 @@ +import React, { useState } from "react"; +import { useSetRecoilState } from "recoil"; import { - axiosDeleteCurrentBanLog, - axiosGetBannedUserList, -} from "@/Cabinet/api/axios/axios.custom"; -import { additionalModalType, modalPropsMap } from "@/Cabinet/assets/data/maps"; + bannedUserListState, + isCurrentSectionRenderState, + numberOfAdminWorkState, + targetUserInfoState, +} from "@/Cabinet/recoil/atoms"; import Modal, { IModalContents } from "@/Cabinet/components/Modals/Modal"; import ModalPortal from "@/Cabinet/components/Modals/ModalPortal"; import { FailResponseModal, SuccessResponseModal, } from "@/Cabinet/components/Modals/ResponseModal/ResponseModal"; -import { - bannedUserListState, - isCurrentSectionRenderState, - numberOfAdminWorkState, - targetUserInfoState, -} from "@/Cabinet/recoil/atoms"; +import { additionalModalType, modalPropsMap } from "@/Cabinet/assets/data/maps"; import IconType from "@/Cabinet/types/enum/icon.type.enum"; +import { + axiosDeleteCurrentBanLog, + axiosGetBannedUserList, +} from "@/Cabinet/api/axios/axios.custom"; import { handleBannedUserList } from "@/Cabinet/utils/tableUtils"; -import React, { useState } from "react"; -import { useSetRecoilState } from "recoil"; const BanModal: React.FC<{ userId: number | null; @@ -40,7 +40,7 @@ const BanModal: React.FC<{ 해제하시겠습니까?`; const tryReturnRequest = async (e: React.MouseEvent) => { try { - // 패널티 해제 API 호출 + // 페널티 해제 API 호출 await axiosDeleteCurrentBanLog(props.userId); setIsCurrentSectionRender(true); setModalTitle("해제되었습니다"); diff --git a/frontend/src/Cabinet/components/Modals/ClubModal/AddClubMemberModal.tsx b/frontend/src/Cabinet/components/Modals/ClubModal/AddClubMemberModal.tsx index c65d3b549..0d46eba64 100644 --- a/frontend/src/Cabinet/components/Modals/ClubModal/AddClubMemberModal.tsx +++ b/frontend/src/Cabinet/components/Modals/ClubModal/AddClubMemberModal.tsx @@ -113,7 +113,7 @@ const ContentItemInputStyled = styled.input` cursor: "input"; color: "var(--normal-text-color)"; &::placeholder { - color: "var(--toggle-switch-off-bg-color)"; + color: "var(--line-color)"; } `; diff --git a/frontend/src/Cabinet/components/Modals/ClubModal/ClubModal.tsx b/frontend/src/Cabinet/components/Modals/ClubModal/ClubModal.tsx index 43faeca45..e6976a345 100644 --- a/frontend/src/Cabinet/components/Modals/ClubModal/ClubModal.tsx +++ b/frontend/src/Cabinet/components/Modals/ClubModal/ClubModal.tsx @@ -282,7 +282,7 @@ const ContentItemInputStyled = styled.input` color: var(--normal-text-color); &::placeholder { - color: var(--toggle-switch-off-bg-color); + color: var(--line-color); } `; diff --git a/frontend/src/Cabinet/components/Modals/ExtendModal/ExtendModal.tsx b/frontend/src/Cabinet/components/Modals/ExtendModal/ExtendModal.tsx index 60ebafbe3..7e243fbfe 100644 --- a/frontend/src/Cabinet/components/Modals/ExtendModal/ExtendModal.tsx +++ b/frontend/src/Cabinet/components/Modals/ExtendModal/ExtendModal.tsx @@ -1,15 +1,6 @@ -import { - axiosCabinetById, - axiosMyLentInfo, - axiosUseExtension, // axiosExtend, // TODO: 연장권 api 생성 후 연결해야 함 -} from "@/Cabinet/api/axios/axios.custom"; -import { additionalModalType, modalPropsMap } from "@/Cabinet/assets/data/maps"; -import Modal, { IModalContents } from "@/Cabinet/components/Modals/Modal"; -import ModalPortal from "@/Cabinet/components/Modals/ModalPortal"; -import { - FailResponseModal, - SuccessResponseModal, -} from "@/Cabinet/components/Modals/ResponseModal/ResponseModal"; +import React, { useEffect, useState } from "react"; +import { useRecoilState, useSetRecoilState } from "recoil"; +import styled from "styled-components"; import { currentCabinetIdState, isCurrentSectionRenderState, @@ -17,11 +8,30 @@ import { targetCabinetInfoState, userState, } from "@/Cabinet/recoil/atoms"; +import Dropdown from "@/Cabinet/components/Common/Dropdown"; +import Modal, { IModalContents } from "@/Cabinet/components/Modals/Modal"; +import ModalPortal from "@/Cabinet/components/Modals/ModalPortal"; +import { + FailResponseModal, + SuccessResponseModal, +} from "@/Cabinet/components/Modals/ResponseModal/ResponseModal"; +import { IInventoryInfo } from "@/Cabinet/components/Store/Inventory/Inventory"; +import { additionalModalType, modalPropsMap } from "@/Cabinet/assets/data/maps"; import { MyCabinetInfoResponseDto } from "@/Cabinet/types/dto/cabinet.dto"; import IconType from "@/Cabinet/types/enum/icon.type.enum"; +import { + axiosCabinetById, + axiosMyItems, + axiosMyLentInfo, + axiosUseItem, +} from "@/Cabinet/api/axios/axios.custom"; import { getExtendedDateString } from "@/Cabinet/utils/dateUtils"; -import React, { useState } from "react"; -import { useRecoilState, useRecoilValue, useSetRecoilState } from "recoil"; + +const extensionPeriod = [ + { sku: "EXTENSION_3", period: "3일", day: 3 }, + { sku: "EXTENSION_15", period: "15일", day: 15 }, + { sku: "EXTENSION_31", period: "31일", day: 31 }, +]; const ExtendModal: React.FC<{ onClose: () => void; @@ -30,7 +40,10 @@ const ExtendModal: React.FC<{ const [showResponseModal, setShowResponseModal] = useState(false); const [hasErrorOnResponse, setHasErrorOnResponse] = useState(false); const [modalTitle, setModalTitle] = useState(""); - const currentCabinetId = useRecoilValue(currentCabinetIdState); + const [modalContents, setModalContents] = useState(null); + const [extensionDate, setExtensionDate] = useState(3); + const [isOpen, setIsOpen] = useState(false); + const [currentCabinetId] = useRecoilState(currentCabinetIdState); const [myInfo, setMyInfo] = useRecoilState(userState); const [myLentInfo, setMyLentInfo] = useRecoilState(myCabinetInfoState); @@ -40,7 +53,9 @@ const ExtendModal: React.FC<{ ); const formattedExtendedDate = getExtendedDateString( myLentInfo.lents[0].expiredAt, - myInfo.lentExtensionResponseDto?.extensionPeriod + // myInfo.lentExtensionResponseDto?.extensionPeriod + extensionDate + //내가 선택한 옵션의 연장 기간을 number 로 넘겨주기 ); const extensionExpiredDate = getExtendedDateString( myInfo.lentExtensionResponseDto?.expiredAt, @@ -49,24 +64,123 @@ const ExtendModal: React.FC<{ const extendDetail = `사물함 연장권 사용 시, 대여 기간이 ${formattedExtendedDate} 23:59으로 연장됩니다. - 연장권 사용은 취소할 수 없습니다. - 연장권을 사용하시겠습니까?`; + 연장권 사용은 취소할 수 없습니다.`; const extendInfoDetail = `사물함을 대여하시면 연장권 사용이 가능합니다. 연장권은 ${extensionExpiredDate} 23:59 이후 만료됩니다.`; + const getModalTitle = (cabinetId: number | null) => { return cabinetId === null ? modalPropsMap[additionalModalType.MODAL_OWN_EXTENSION].title : modalPropsMap[additionalModalType.MODAL_USE_EXTENSION].title; }; + const getModalDetail = (cabinetId: number | null) => { return cabinetId === null ? extendInfoDetail : extendDetail; }; + const getModalProceedBtnText = (cabinetId: number | null) => { return cabinetId === null ? modalPropsMap[additionalModalType.MODAL_OWN_EXTENSION].confirmMessage : modalPropsMap[additionalModalType.MODAL_USE_EXTENSION].confirmMessage; }; - const tryExtendRequest = async (e: React.MouseEvent) => { + + // 연장권 보유 여부 확인하는 부분 + const [myItems, setMyItems] = useState(null); + const [selectedOption, setSelectedOption] = useState(0); + + const findMyExtension = (period: string) => { + return !myItems?.extensionItems.some((item) => item.itemDetails === period); + }; + + // 연장권이 하나라도 없다면 true + const checkExtension = () => { + return ( + findMyExtension("3일") && + findMyExtension("15일") && + findMyExtension("31일") + ); + }; + + const getDefault = () => { + if (!findMyExtension(extensionPeriod[0].period)) return 3; + if (!findMyExtension(extensionPeriod[1].period)) return 15; + if (!findMyExtension(extensionPeriod[2].period)) return 31; + else return 0; + }; + const getDefaultOption = (option: number) => { + if (option == 3) return 0; + else if (option == 15) return 1; + else return 2; + }; + + const getMyItems = async () => { + try { + const response = await axiosMyItems(); + setMyItems(response.data); + } catch (error: any) { + console.error("Error getting inventory:", error); + } + }; + + useEffect(() => { + getMyItems(); + }, []); + + useEffect(() => { + if (checkExtension() == true) { + setShowResponseModal(true); + setHasErrorOnResponse(true); + setModalContents( + `현재 연장권을 보유하고 있지 않습니다. +연장권은 까비 상점에서 구매하실 수 있습니다.` + ); + } else { + setShowResponseModal(false); + setHasErrorOnResponse(false); + } + setExtensionDate(getDefault()); + }, [myItems]); + + useEffect(() => { + setSelectedOption(getDefaultOption(extensionDate)); + }, [extensionDate]); + + + const handleDropdownChange = (option: number) => { + setSelectedOption(option); + setExtensionDate(extensionPeriod[option].day); + }; + + const extensionDropdownProps = { + options: [ + { + name: extensionPeriod[0].period, + value: 0, + isDisabled: findMyExtension(extensionPeriod[0].period), + }, + { + name: extensionPeriod[1].period, + value: 1, + isDisabled: findMyExtension(extensionPeriod[1].period), + }, + { + name: extensionPeriod[2].period, + value: 2, + isDisabled: findMyExtension(extensionPeriod[2].period), + }, + ], + defaultValue: findMyExtension(extensionPeriod[0].period) + ? findMyExtension(extensionPeriod[1].period) + ? extensionPeriod[2].period + : extensionPeriod[1].period + : extensionPeriod[0].period, + onChangeValue: handleDropdownChange, + isOpen: isOpen, + setIsOpen: setIsOpen, + }; + + const extensionItemUse = async (item: string) => { + // 아이템 사용 if (currentCabinetId === 0 || myInfo.cabinetId === null) { setHasErrorOnResponse(true); setModalTitle("현재 대여중인 사물함이 없습니다."); @@ -74,14 +188,17 @@ const ExtendModal: React.FC<{ return; } try { - await axiosUseExtension(); + await axiosUseItem(item, null, null, null, null); setMyInfo({ ...myInfo, cabinetId: currentCabinetId, lentExtensionResponseDto: null, }); setIsCurrentSectionRender(true); - setModalTitle("연장되었습니다"); + setModalTitle("연장권 사용완료"); + setModalContents( + `대여 기간이 ${formattedExtendedDate}으로 연장되었습니다.` + ); try { const { data } = await axiosCabinetById(currentCabinetId); setTargetCabinetInfo(data); @@ -96,14 +213,23 @@ const ExtendModal: React.FC<{ } } catch (error: any) { setHasErrorOnResponse(true); - error.response - ? setModalTitle(error.response.data.message) - : setModalTitle(error.data.message); + if (error.response.status === 400) { + setModalTitle("연장권 사용실패"); + setModalContents( + `현재 연장권을 보유하고 있지 않습니다. + 연장권은 까비 상점에서 구매하실 수 있습니다.` + ); + } else + error.response + ? setModalTitle(error.response.data.message) + : setModalTitle(error.data.message); } finally { setShowResponseModal(true); } }; + + const extendModalContents: IModalContents = { type: myInfo.cabinetId === null ? "penaltyBtn" : "hasProceedBtn", title: getModalTitle(myInfo.cabinetId), @@ -114,9 +240,20 @@ const ExtendModal: React.FC<{ ? async (e: React.MouseEvent) => { props.onClose(); } - : tryExtendRequest, + : async () => { + extensionItemUse(extensionPeriod[selectedOption].sku); + }, closeModal: props.onClose, iconType: IconType.CHECKICON, + renderAdditionalComponent: () => ( + <> + + 연장권 타입 + + + + + ), }; return ( @@ -126,11 +263,15 @@ const ExtendModal: React.FC<{ (hasErrorOnResponse ? ( ) : ( ))} @@ -138,4 +279,26 @@ const ExtendModal: React.FC<{ ); }; +const ModalContainerStyled = styled.div` + padding: 10px 20px 0 20px; +`; + +const ModalDropdownNameStyled = styled.div` + display: flex; + margin: 10px 10px 15px 5px; + font-size: 18px; +`; + +const ModalDetailStyled = styled.div` + width: 100%; + height: 100%; + margin-top: 30px; + > p { + margin: 10px; + > span { + font-weight: 600; + } + } +`; + export default ExtendModal; diff --git a/frontend/src/Cabinet/components/Modals/ManualModal/ManualModal.tsx b/frontend/src/Cabinet/components/Modals/ManualModal/ManualModal.tsx index 2acebafdd..494d11383 100644 --- a/frontend/src/Cabinet/components/Modals/ManualModal/ManualModal.tsx +++ b/frontend/src/Cabinet/components/Modals/ManualModal/ManualModal.tsx @@ -2,11 +2,7 @@ import React from "react"; import { useState } from "react"; import styled, { keyframes } from "styled-components"; import { manualContentData } from "@/Cabinet/assets/data/ManualContent"; -import { ReactComponent as ClubIcon } from "@/Cabinet/assets/images/clubIcon.svg"; -import { ReactComponent as ExtensionIcon } from "@/Cabinet/assets/images/extension.svg"; import { ReactComponent as MoveBtnImg } from "@/Cabinet/assets/images/moveButton.svg"; -import { ReactComponent as PrivateIcon } from "@/Cabinet/assets/images/privateIcon.svg"; -import { ReactComponent as ShareIcon } from "@/Cabinet/assets/images/shareIcon.svg"; import ContentStatus from "@/Cabinet/types/enum/content.status.enum"; interface ModalProps { @@ -60,17 +56,8 @@ const ManualModal: React.FC = ({ {hasImage && ( - {contentStatus === ContentStatus.PRIVATE && ( - - )} - {contentStatus === ContentStatus.SHARE && ( - - )} - {contentStatus === ContentStatus.CLUB && ( - - )} - {contentStatus === ContentStatus.EXTENSION && ( - + {contentData.iconComponent && ( + )} {isCabinetType && ( @@ -339,10 +326,6 @@ const ContentImgStyled = styled.div<{ props.contentStatus === ContentStatus.EXTENSION ? "var(--normal-text-color)" : "var(--white-text-with-bg-color)"}; - transform: ${(props) => - props.contentStatus === ContentStatus.EXTENSION - ? "scale(1.4)" - : "scale(3.3)"}; } } `; diff --git a/frontend/src/Cabinet/components/Modals/MemoModal/MemoModal.tsx b/frontend/src/Cabinet/components/Modals/MemoModal/MemoModal.tsx index aaec64b5d..9d90cb626 100644 --- a/frontend/src/Cabinet/components/Modals/MemoModal/MemoModal.tsx +++ b/frontend/src/Cabinet/components/Modals/MemoModal/MemoModal.tsx @@ -6,14 +6,14 @@ import CabinetType from "@/Cabinet/types/enum/cabinet.type.enum"; export interface MemoModalInterface { cabinetType: CabinetType; - cabinetTitle: string | null; + cabinetTitle: string; cabinetMemo: string; } interface MemoModalContainerInterface { memoModalObj: MemoModalInterface; onClose: React.MouseEventHandler; - onSave: (newTitle: string | null, newMemo: string) => void; + onSave: (newTitle: string, newMemo: string) => void; } const MAX_INPUT_LENGTH = 14; @@ -33,6 +33,7 @@ const MemoModal = ({ newTitle.current.select(); } }; + const handleClickSave = (e: React.MouseEvent) => { //사물함 제목, 사물함 비밀메모 update api 호출 // onClose(e); @@ -40,7 +41,7 @@ const MemoModal = ({ if (newTitle.current!.value) { onSave(newTitle.current!.value, newMemo.current!.value); } else { - onSave(null, newMemo.current!.value); + onSave("", newMemo.current!.value); } setMode("read"); }; @@ -100,12 +101,12 @@ const MemoModal = ({ ? onClose : () => { setMode("read"); - if (cabinetTitle) newTitle.current!.value = cabinetTitle; + newTitle.current!.value = cabinetTitle; newMemo.current!.value = cabinetMemo; } } text={mode === "read" ? "닫기" : "취소"} - theme={mode === "read" ? "light-grayLine" : "line"} + theme={mode === "read" ? "grayLine" : "line"} /> @@ -183,9 +184,7 @@ const ContentItemInputStyled = styled.input<{ mode === "read" ? "var(--sys-main-color)" : "var(--normal-text-color)"}; &::placeholder { color: ${({ mode }) => - mode === "read" - ? "var(--sys-main-color)" - : "var(--toggle-switch-off-bg-color)"}; + mode === "read" ? "var(--sys-main-color)" : "var(--line-color)"}; } `; diff --git a/frontend/src/Cabinet/components/Modals/Modal.tsx b/frontend/src/Cabinet/components/Modals/Modal.tsx index a2256af07..a854f03a8 100644 --- a/frontend/src/Cabinet/components/Modals/Modal.tsx +++ b/frontend/src/Cabinet/components/Modals/Modal.tsx @@ -1,11 +1,11 @@ import React, { ReactElement } from "react"; +import { useNavigate } from "react-router-dom"; import styled, { css } from "styled-components"; import AdminClubLogContainer from "@/Cabinet/components/Club/AdminClubLog.container"; import Button from "@/Cabinet/components/Common/Button"; import { ReactComponent as CheckIcon } from "@/Cabinet/assets/images/checkIcon.svg"; import { ReactComponent as ErrorIcon } from "@/Cabinet/assets/images/errorIcon.svg"; import { ReactComponent as NotificationIcon } from "@/Cabinet/assets/images/notificationSign.svg"; -import IconType from "@/Cabinet/types/enum/icon.type.enum"; import useMultiSelect from "@/Cabinet/hooks/useMultiSelect"; /** @@ -24,6 +24,8 @@ import useMultiSelect from "@/Cabinet/hooks/useMultiSelect"; * @property {boolean} isClubLentModal : 동아리 (CLUB) 대여 모달인지 여부 * @property {boolean} isLoading : 로딩중 요청 버튼 비활성화 감지를 위한 변수 * @property {boolean} isCheckIcon : checkIcon인지 errorIcon인지 감지를 위한 변수 + * @property {string} urlTitle : 모달에서 링크로 이동할 url의 제목 + * @property {string} url : 모달에서 링크로 이동할 url 값 */ export interface IModalContents { type: string; @@ -38,6 +40,8 @@ export interface IModalContents { isClubLentModal?: boolean; isLoading?: boolean; iconType?: string; + urlTitle?: string | null; + url?: string | null; } const Modal: React.FC<{ modalContents: IModalContents }> = (props) => { @@ -54,8 +58,11 @@ const Modal: React.FC<{ modalContents: IModalContents }> = (props) => { isClubLentModal, isLoading, iconType, + urlTitle, + url, } = props.modalContents; const { isMultiSelect, closeMultiSelectMode } = useMultiSelect(); + const navigator = useNavigate(); return ( <> @@ -91,11 +98,6 @@ const Modal: React.FC<{ modalContents: IModalContents }> = (props) => { {renderAdditionalComponent && renderAdditionalComponent()} {type === "hasProceedBtn" && ( -