diff --git a/.github/ISSUE_TEMPLATE/ui_improvement.yml b/.github/ISSUE_TEMPLATE/ui_improvement.yml new file mode 100644 index 000000000..f34b2ba71 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/ui_improvement.yml @@ -0,0 +1,46 @@ +name: "UI 개선 건의" +description: UI 개선 건의 작성 기본 양식입니다. +labels: ["ui"] +body: + - type: markdown + attributes: + value: | + 작성 예시 : "[FE] 지도-E/V 버튼 눌렀을때 나타나는 로고 색 다크모드 적용" + - type: textarea + id: ui-description + attributes: + label: UI 문제점 설명 + description: 문제가 언제/어떻게 발생했는지 명확하게 적어주세요. + placeholder: 설명을 적어주세요. + validations: + required: true + - type: textarea + id: reproduction + attributes: + label: 재현 방법 + description: 문제가 재현되는 상황을 설명해주세요. + placeholder: 설명을 적어주세요. + validations: + required: true + - type: textarea + id: improvement + attributes: + label: 기대하는 UI + description: 기대하는 UI에 대해서 설명해주세요. + placeholder: 설명을 적어주세요. + validations: + required: true + - type: textarea + id: system-info + attributes: + label: 시스템 환경 (선택 사항) + description: 현재 버그가 발생한 시스템 환경을 적어주세요. + render: shell + placeholder: OS, 브라우저 등을 적어주세요. + validations: + required: false + - type: textarea + id: additional-context + attributes: + label: 추가 건의사항 + description: Cabi에 바라시는 점이나, 하시고 싶은 말씀을 적어주세요. diff --git a/README.md b/README.md index 6075e13fb..245ff01e0 100644 --- a/README.md +++ b/README.md @@ -75,8 +75,8 @@
## 🕸️ 인프라 구조도 -![Untitled](https://github.com/innovationacademy-kr/Cabi/assets/83565255/165c1529-6164-4988-9495-6bc2ba3ef0ab) +![Untitled](https://github.com/innovationacademy-kr/Cabi/assets/83565255/165c1529-6164-4988-9495-6bc2ba3ef0ab) ## 🛠 기술 스택 @@ -175,6 +175,9 @@ | [ 🐶 surlee](https://github.com/Elineely) | [ 🐣 hyowchoi](https://github.com/chyo1/) | [ 👽 sohyupar](https://github.com/saewoo1) | [🦝 jimchoi](https://github.com/jimchoi9) | [ 🛼 jeekim](https://github.com/jnkeniaem) | [🍾 miyu](https://github.com/Minkyu01) | [🧸 gykoh](https://github.com/gykoh42) | | ----------------------------------------- | ----------------------------------------- | ------------------------------------------ | ----------------------------------------- | ------------------------------------------ | -------------------------------------- | -------------------------------------- | +| [ 🐰 jihykim2](https://github.com/jihyunk03) | [⛄️ seonmiki](https://github.com/seonmiki) | [ 🎱 junsbae ](https://github.com/wet6123) | +| -------------------------------------------- | ------------------------------------------- | ------------------------------------------ | + | | | --------------------------------------------------------------------------------------------------------------------------------------------------------- | 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 index be9967d4d..d8c456f03 100644 --- 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 @@ -18,7 +18,8 @@ import org.ftclub.cabinet.item.service.ItemQueryService; import org.ftclub.cabinet.item.service.ItemRedisService; import org.ftclub.cabinet.mapper.ItemMapper; -import org.ftclub.cabinet.utils.lock.LockUtil; +import org.ftclub.cabinet.user.service.UserCommandService; +import org.ftclub.cabinet.user.service.UserQueryService; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; @@ -35,6 +36,8 @@ public class AdminItemFacadeService { private final ItemMapper itemMapper; private final ItemRedisService itemRedisService; + private final UserCommandService userCommandService; + private final UserQueryService userQueryService; @Transactional public void createItem(Integer Price, Sku sku, ItemType type) { @@ -44,16 +47,18 @@ public void createItem(Integer Price, Sku sku, ItemType type) { @Transactional public void assignItem(List userIds, Sku sku) { Item item = itemQueryService.getBySku(sku); + Long price = item.getPrice(); LocalDateTime now = null; - if (item.getPrice() > 0) { + if (price > 0) { now = LocalDateTime.now(); - userIds.forEach(userId -> LockUtil.lockRedisCoin(userId, () -> { - long coinAmount = itemRedisService.getCoinAmount(userId); + userIds.forEach(userId -> { + long coinAmount = userQueryService.getUser(userId).getCoin(); itemRedisService.saveCoinCount(userId, coinAmount + item.getPrice()); long totalCoinSupply = itemRedisService.getTotalCoinSupply(); itemRedisService.saveTotalCoinSupply(totalCoinSupply + item.getPrice()); - })); + userCommandService.updateCoinAmount(userId, price); + }); } itemHistoryCommandService.createItemHistories(userIds, item.getId(), now); } diff --git a/backend/src/main/java/org/ftclub/cabinet/admin/lent/service/AdminLentFacadeService.java b/backend/src/main/java/org/ftclub/cabinet/admin/lent/service/AdminLentFacadeService.java index dbaf15cc5..1b743c5ee 100644 --- a/backend/src/main/java/org/ftclub/cabinet/admin/lent/service/AdminLentFacadeService.java +++ b/backend/src/main/java/org/ftclub/cabinet/admin/lent/service/AdminLentFacadeService.java @@ -74,7 +74,6 @@ public void endUserLent(List userIds) { LocalDateTime now = LocalDateTime.now(); List lentHistories = lentQueryService.findUserActiveLentHistoriesInCabinetForUpdate(userIds.get(0)); - System.out.println("lentHistories = " + lentHistories); if (lentHistories.isEmpty()) { Long cabinetId = lentRedisService.findCabinetJoinedUser(userIds.get(0)); if (cabinetId != null) { diff --git a/backend/src/main/java/org/ftclub/cabinet/alarm/fcm/repository/FCMTokenRedis.java b/backend/src/main/java/org/ftclub/cabinet/alarm/fcm/repository/FCMTokenRedis.java index 4516580e2..3b52513ee 100644 --- a/backend/src/main/java/org/ftclub/cabinet/alarm/fcm/repository/FCMTokenRedis.java +++ b/backend/src/main/java/org/ftclub/cabinet/alarm/fcm/repository/FCMTokenRedis.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import java.time.Duration; +import java.util.Objects; import java.util.Optional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -26,7 +27,8 @@ public class FCMTokenRedis { */ public Optional findByKey(String key, Class type) { String serializedValue = redisTemplate.opsForValue().get(key + KEY_PREFIX); - if (serializedValue == null) { + final String NULL = "null"; + if (Objects.isNull(serializedValue) || serializedValue.equals(NULL)) { return Optional.empty(); } try { diff --git a/backend/src/main/java/org/ftclub/cabinet/auth/service/AuthFacadeService.java b/backend/src/main/java/org/ftclub/cabinet/auth/service/AuthFacadeService.java index bd4dce194..76668a0d6 100644 --- a/backend/src/main/java/org/ftclub/cabinet/auth/service/AuthFacadeService.java +++ b/backend/src/main/java/org/ftclub/cabinet/auth/service/AuthFacadeService.java @@ -81,6 +81,10 @@ public void handleUserLogin(HttpServletRequest req, HttpServletResponse res, Str FtProfile profile = userOauthService.getProfileByCode(code); User user = userQueryService.findUser(profile.getIntraName()) .orElseGet(() -> userCommandService.createUserByFtProfile(profile)); + // 블랙홀이 API에서 가져온 날짜와 다르다면 갱신 + if (!user.isSameBlackholedAt(profile.getBlackHoledAt())) { + userCommandService.updateUserBlackholeStatus(user.getId(), profile.getBlackHoledAt()); + } String token = tokenProvider.createUserToken(user, LocalDateTime.now()); Cookie cookie = cookieManager.cookieOf(TokenProvider.USER_TOKEN_NAME, token); cookieManager.setCookieToClient(res, cookie, "/", req.getServerName()); 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 f4c38f8b8..61152c7ad 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 @@ -144,7 +144,7 @@ public List getCabinetsPerSection(String building // 층, 건물에 따른 유저가 알람 등록한 section 조회 Set unsetAlarmSection = - sectionAlarmQueryService.getUnsentAlarm(userId, building, floor).stream() + sectionAlarmQueryService.findUnsentAlarm(userId, building, floor).stream() .map(alarm -> alarm.getCabinetPlace().getLocation().getSection()) .collect(Collectors.toSet()); 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 55ac3ae7a..39c570da2 100644 --- a/backend/src/main/java/org/ftclub/cabinet/exception/ExceptionStatus.java +++ b/backend/src/main/java/org/ftclub/cabinet/exception/ExceptionStatus.java @@ -55,8 +55,8 @@ public enum ExceptionStatus { CLUB_HAS_LENT_CABINET(HttpStatus.NOT_ACCEPTABLE, "대여 중인 사물함을 반납 후 삭제할 수 있습니다."), HANEAPI_ERROR(HttpStatus.BAD_GATEWAY, "24HANE API 통신에 에러가 있습니다."), EXTENSION_NOT_FOUND(HttpStatus.BAD_REQUEST, "연장권이 존재하지 않습니다."), + EXTENSION_LENT_DELAYED(HttpStatus.FORBIDDEN, "연장권은 연체된 사물함에 사용할 수 없습니다."), EXTENSION_SOLO_IN_SHARE_NOT_ALLOWED(HttpStatus.UNAUTHORIZED, "연장권은 1명일 때 사용할 수 없습니다."), - EXTENSION_LENT_DELAYED(HttpStatus.UNAUTHORIZED, "연장권은 연체된 사물함에 사용할 수 없습니다."), MAIL_BAD_GATEWAY(HttpStatus.BAD_GATEWAY, "메일 전송 중 에러가 발생했습니다"), SLACK_REQUEST_BAD_GATEWAY(HttpStatus.BAD_GATEWAY, "슬랙 인증 중 에러가 발생했습니다."), SLACK_MESSAGE_SEND_BAD_GATEWAY(HttpStatus.BAD_GATEWAY, "슬랙 메세지 전송 중 에러가 발생했습니다."), @@ -83,7 +83,8 @@ public enum ExceptionStatus { 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, "해당 아이템을 보유하고 있지 않습니다"); + ITEM_NOT_OWNED(HttpStatus.BAD_REQUEST, "해당 아이템을 보유하고 있지 않습니다"), + ITEM_USE_DUPLICATED(HttpStatus.FORBIDDEN, "아이템이 중복 사용되었습니다."); final private int statusCode; final private String message; 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 index 9848e75ba..da5d04cc5 100644 --- a/backend/src/main/java/org/ftclub/cabinet/item/domain/ItemType.java +++ b/backend/src/main/java/org/ftclub/cabinet/item/domain/ItemType.java @@ -6,7 +6,7 @@ public enum ItemType { EXTENSION("연장권", - "현재 대여 중인 사물함의 반납 기한을 3일, 15일 또는 30일 연장할 수 있습니다."), + "현재 대여 중인 사물함의 반납 기한을 3일, 15일 또는 31일 연장할 수 있습니다."), PENALTY("페널티 감면권", "사물함 이용 중 발생한 페널티 일수를 감소시켜 사물함 사용 제한 기간을 줄일 수 있습니다."), SWAP("이사권" 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 index 8eded2a0f..edcbafb10 100644 --- a/backend/src/main/java/org/ftclub/cabinet/item/repository/ItemHistoryRepository.java +++ b/backend/src/main/java/org/ftclub/cabinet/item/repository/ItemHistoryRepository.java @@ -27,14 +27,14 @@ Page findAllByUserIdAndItemIdIn(@Param("userId") Long userId, + "JOIN FETCH ih.item i " + "WHERE ih.userId = :userId " + "AND ih.usedAt IS NOT NULL " - + "AND i.price < 0 " + + "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") + + "AND i.price <= 0") Page findAllByUserIdOnMinusPriceItemsWithSubQuery( @Param("userId") Long userId, Pageable pageable); 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 index 1077be50f..ce3d99225 100644 --- a/backend/src/main/java/org/ftclub/cabinet/item/repository/SectionAlarmRepository.java +++ b/backend/src/main/java/org/ftclub/cabinet/item/repository/SectionAlarmRepository.java @@ -36,4 +36,9 @@ List findAllByUserIdAndCabinetPlaceAndAlarmedAtIsNull( @Param("userId") Long userId, @Param("building") String building, @Param("floor") Integer floor); + + List findAllByUserIdAndCabinetPlaceIdOrderByIdDesc( + @Param("userId") Long userId, + @Param("cabinetPlaceId") Long cabinetPlaceId); + } 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 index b95987f0c..b6f0e5546 100644 --- a/backend/src/main/java/org/ftclub/cabinet/item/service/ItemFacadeService.java +++ b/backend/src/main/java/org/ftclub/cabinet/item/service/ItemFacadeService.java @@ -43,7 +43,9 @@ 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.UserCommandService; import org.ftclub.cabinet.user.service.UserQueryService; +import org.ftclub.cabinet.utils.blackhole.manager.BlackholeManager; import org.ftclub.cabinet.utils.lock.LockUtil; import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.domain.Page; @@ -57,17 +59,20 @@ @Logging(level = LogLevel.DEBUG) public class ItemFacadeService { + private final ItemMapper itemMapper; private final ItemQueryService itemQueryService; private final ItemHistoryQueryService itemHistoryQueryService; private final ItemHistoryCommandService itemHistoryCommandService; private final ItemRedisService itemRedisService; + private final ItemPolicyService itemPolicyService; private final UserQueryService userQueryService; - private final SectionAlarmCommandService sectionAlarmCommandService; + private final UserCommandService userCommandService; private final CabinetQueryService cabinetQueryService; - private final ItemMapper itemMapper; - private final ItemPolicyService itemPolicyService; - private final ApplicationEventPublisher eventPublisher; + private final SectionAlarmCommandService sectionAlarmCommandService; + private final SectionAlarmQueryService sectionAlarmQueryService; + private final BlackholeManager blackholeManager; + private final ApplicationEventPublisher eventPublisher; /** * 모든 아이템 리스트 반환 @@ -190,7 +195,7 @@ public CoinCollectionRewardResponseDto collectCoinAndIssueReward(Long userId) { // DB에 코인 저장 Item coinCollect = itemQueryService.getBySku(Sku.COIN_COLLECT); int reward = (int) (coinCollect.getPrice().longValue()); - itemHistoryCommandService.createItemHistory(userId, coinCollect.getId()); + itemHistoryCommandService.createCoinItemHistory(userId, coinCollect.getId()); // 출석 일자에 따른 랜덤 리워드 지급 Long coinCollectionCountInMonth = @@ -201,12 +206,13 @@ public CoinCollectionRewardResponseDto collectCoinAndIssueReward(Long userId) { Sku coinSku = itemPolicyService.getRewardSku(randomPercentage); Item coinReward = itemQueryService.getBySku(coinSku); - itemHistoryCommandService.createItemHistory(userId, coinReward.getId()); + itemHistoryCommandService.createCoinItemHistory(userId, coinReward.getId()); reward += coinReward.getPrice(); } // Redis에 코인 변화량 저장 saveCoinChangeOnRedis(userId, reward); + userCommandService.updateCoinAmount(userId, (long) reward); return new CoinCollectionRewardResponseDto(reward); } @@ -214,8 +220,8 @@ public CoinCollectionRewardResponseDto collectCoinAndIssueReward(Long userId) { private void saveCoinChangeOnRedis(Long userId, final int reward) { LockUtil.lockRedisCoin(userId, () -> { // Redis에 유저 리워드 저장 - long coins = itemRedisService.getCoinAmount(userId); - itemRedisService.saveCoinCount(userId, coins + reward); +// long coins = itemRedisService.getCoinAmount(userId); +// itemRedisService.saveCoinCount(userId, coins + reward); // Redis에 전체 코인 발행량 저장 long totalCoinSupply = itemRedisService.getTotalCoinSupply(); @@ -235,11 +241,11 @@ private void saveCoinChangeOnRedis(Long userId, final int reward) { 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)); + throw ExceptionStatus.BLACKHOLED_USER.asServiceException(); } + Item item = itemQueryService.getBySku(sku); List itemInInventory = itemHistoryQueryService.findUnusedItemsInUserInventory(user.getId(), item.getId()); @@ -294,7 +300,10 @@ public void purchaseItem(Long userId, Sku sku) { Item item = itemQueryService.getBySku(sku); long price = item.getPrice(); - long userCoin = itemRedisService.getCoinAmount(userId); + + // 유저의 보유 재화량 +// long userCoin = itemRedisService.getCoinAmount(userId); + long userCoin = user.getCoin(); // 아이템 Policy 검증 itemPolicyService.verifyOnSale(price); @@ -311,10 +320,19 @@ public void purchaseItem(Long userId, Sku sku) { long totalCoinUsage = itemRedisService.getTotalCoinUsage(); itemRedisService.saveTotalCoinUsage(totalCoinUsage + price); }); + userCommandService.updateCoinAmount(userId, price); } @Transactional public void addSectionAlarm(Long userId, Long cabinetPlaceId) { + verifyDuplicateSection(userId, cabinetPlaceId); sectionAlarmCommandService.addSectionAlarm(userId, cabinetPlaceId); } + + private void verifyDuplicateSection(Long userId, Long cabinetPlaceId) { + if (sectionAlarmQueryService.findUnsentAlarmDesc(userId, + cabinetPlaceId).isPresent()) { + throw ExceptionStatus.ITEM_USE_DUPLICATED.asServiceException(); + } + } } 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 index e375a8518..2863043c4 100644 --- a/backend/src/main/java/org/ftclub/cabinet/item/service/ItemHistoryCommandService.java +++ b/backend/src/main/java/org/ftclub/cabinet/item/service/ItemHistoryCommandService.java @@ -28,4 +28,9 @@ public void createItemHistories(List userIds, Long itemId, LocalDateTime u .collect(Collectors.toList()); itemHistoryRepository.saveAll(itemHistories); } + + public void createCoinItemHistory(Long userId, Long itemId) { + ItemHistory coinCollectItemHistory = ItemHistory.of(userId, itemId, LocalDateTime.now()); + itemHistoryRepository.save(coinCollectItemHistory); + } } 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 index 9f1713dcd..794166fed 100644 --- a/backend/src/main/java/org/ftclub/cabinet/item/service/ItemRedisService.java +++ b/backend/src/main/java/org/ftclub/cabinet/item/service/ItemRedisService.java @@ -15,19 +15,6 @@ public class ItemRedisService { private final ItemRedis itemRedis; private final ItemHistoryRepository itemHistoryRepository; - public long getCoinAmount(Long userId) { - String userIdString = userId.toString(); - String coinAmount = itemRedis.getCoinAmount(userIdString); - if (coinAmount == 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(coinAmount); - } - public void saveCoinCount(Long userId, long coinCount) { itemRedis.saveCoinAmount(userId.toString(), String.valueOf(coinCount)); } @@ -117,4 +104,21 @@ public Long getCoinCollectionCountInMonth(Long userId) { } return Long.parseLong(coinCollectionCount); } + + /** + * Redis에 저장되어있는 현재 User가 가진 Coin 양 반환 + */ + @Deprecated + public long getCoinAmount(Long userId) { + String userIdString = userId.toString(); + String coinAmount = itemRedis.getCoinAmount(userIdString); + if (coinAmount == 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(coinAmount); + } } 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 index 131370b8c..2f56ea3c9 100644 --- a/backend/src/main/java/org/ftclub/cabinet/item/service/SectionAlarmQueryService.java +++ b/backend/src/main/java/org/ftclub/cabinet/item/service/SectionAlarmQueryService.java @@ -1,6 +1,7 @@ package org.ftclub.cabinet.item.service; import java.util.List; +import java.util.Optional; import lombok.RequiredArgsConstructor; import org.ftclub.cabinet.item.domain.SectionAlarm; import org.ftclub.cabinet.item.repository.SectionAlarmRepository; @@ -20,8 +21,15 @@ public List getUnsentAlarms() { return sectionAlarmRepository.findAllByAlarmedAtIsNull(); } - public List getUnsentAlarm(Long userId, String building, Integer floor) { + public List findUnsentAlarm(Long userId, String building, Integer floor) { return sectionAlarmRepository.findAllByUserIdAndCabinetPlaceAndAlarmedAtIsNull( userId, building, floor); } + + public Optional findUnsentAlarmDesc(Long userId, Long cabinetPlaceId) { + return sectionAlarmRepository.findAllByUserIdAndCabinetPlaceIdOrderByIdDesc( + userId, cabinetPlaceId).stream() + .filter(sectionAlarm -> sectionAlarm.getAlarmedAt() == null).findFirst(); + + } } 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 947fd636b..196edfeea 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 @@ -56,7 +56,7 @@ public enum LentPolicyStatus { INVALID_LENT_TYPE, INVALID_ARGUMENT, INVALID_EXPIREDAT, SWAP_SAME_CABINET, LENT_NOT_CLUB, - - SWAP_LIMIT_EXCEEDED + SWAP_LIMIT_EXCEEDED, + OVERDUE_CABINET_EXTEND } \ No newline at end of file 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 62a615afd..9d53e29a9 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 @@ -441,6 +441,9 @@ public void plusExtensionDays(Long userId, Integer days) { userId, LentExtensionType.ALL, days); List lentHistories = lentQueryService.findCabinetActiveLentHistories( cabinet.getId()); + lentHistories.forEach( + lentHistory -> lentPolicyService.verifyExtendable(lentHistory.getExpiredAt()) + ); 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 7fe2abbbb..c26140638 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 @@ -43,6 +43,8 @@ private void handlePolicyStatus(LentPolicyStatus status, LocalDateTime policyDat throw ExceptionStatus.LENT_FULL.asServiceException(); case OVERDUE_CABINET: throw ExceptionStatus.LENT_EXPIRED.asServiceException(); + case OVERDUE_CABINET_EXTEND: + throw ExceptionStatus.EXTENSION_LENT_DELAYED.asServiceException(); case LENT_CLUB: throw ExceptionStatus.LENT_CLUB.asServiceException(); case LENT_NOT_CLUB: @@ -59,10 +61,7 @@ private void handlePolicyStatus(LentPolicyStatus status, LocalDateTime policyDat throw new CustomExceptionStatus(ExceptionStatus.ALL_BANNED_USER, unbannedAtString).asCustomServiceException(); case SHARE_BANNED_USER: - unbannedAtString = policyDate.format( - DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")); - throw new CustomExceptionStatus(ExceptionStatus.SHARE_CODE_TRIAL_EXCEEDED, - unbannedAtString).asCustomServiceException(); + throw ExceptionStatus.SHARE_CODE_TRIAL_EXCEEDED.asServiceException(); case BLACKHOLED_USER: throw ExceptionStatus.BLACKHOLED_USER.asServiceException(); case PENDING_CABINET: @@ -310,4 +309,17 @@ public void verifySwappable(boolean existSwapRecord, LocalDateTime swapExpiredAt handlePolicyStatus(LentPolicyStatus.SWAP_LIMIT_EXCEEDED, swapExpiredAt); } } + + /** + * 연장권 사용 시, 연체된 사물함에 사용하는지 확인합니다 + * + * @param expiredAt 대여만료 날짜 + */ + public void verifyExtendable(LocalDateTime expiredAt) { + LentPolicyStatus status = LentPolicyStatus.FINE; + if (expiredAt.isBefore(LocalDateTime.now())) { + status = LentPolicyStatus.OVERDUE_CABINET_EXTEND; + } + handlePolicyStatus(status, null); + } } 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 b65a4c2c0..6d2b57f0c 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 @@ -160,6 +160,8 @@ public void deleteUserInCabinet(Long cabinetId, Long userId) { lentRedis.deleteUserInCabinet(cabinetIdString, userIdString); if (lentRedis.countUserInCabinet(cabinetIdString) == 0) { lentRedis.deleteShadowKey(cabinetIdString); + clearCabinetSession(cabinetId); + } } diff --git a/backend/src/main/java/org/ftclub/cabinet/ping/PingController.java b/backend/src/main/java/org/ftclub/cabinet/ping/PingController.java index ba8f5ebf0..6805cde2a 100644 --- a/backend/src/main/java/org/ftclub/cabinet/ping/PingController.java +++ b/backend/src/main/java/org/ftclub/cabinet/ping/PingController.java @@ -4,8 +4,12 @@ import lombok.extern.log4j.Log4j2; import org.ftclub.cabinet.auth.domain.AuthGuard; import org.ftclub.cabinet.auth.domain.AuthLevel; +import org.ftclub.cabinet.item.service.ItemCommandService; +import org.ftclub.cabinet.item.service.ItemRedisService; import org.ftclub.cabinet.lent.service.LentFacadeService; import org.ftclub.cabinet.user.service.LentExtensionManager; +import org.ftclub.cabinet.user.service.UserCommandService; +import org.ftclub.cabinet.user.service.UserQueryService; import org.ftclub.cabinet.utils.overdue.manager.OverdueManager; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -21,6 +25,11 @@ public class PingController { private final OverdueManager overdueManager; private final LentExtensionManager lentExtensionManager; + private final ItemRedisService itemRedisService; + private final ItemCommandService itemCommandService; + private final UserQueryService userQueryService; + private final UserCommandService userCommandService; + @RequestMapping public String ping() { return "pong"; diff --git a/backend/src/main/java/org/ftclub/cabinet/user/domain/User.java b/backend/src/main/java/org/ftclub/cabinet/user/domain/User.java index f0870a694..c4aa3a738 100644 --- a/backend/src/main/java/org/ftclub/cabinet/user/domain/User.java +++ b/backend/src/main/java/org/ftclub/cabinet/user/domain/User.java @@ -61,11 +61,15 @@ public class User { @Column(name = "PUSH_ALARM", columnDefinition = "boolean default false") private boolean pushAlarm; + @NotNull + @Column(name = "COIN") + private Long coin; protected User(String name, String email, LocalDateTime blackholedAt) { this.name = name; this.email = email; this.blackholedAt = blackholedAt; + this.coin = 0L; // this.role = userRole; setDefaultAlarmStatus(); } @@ -143,4 +147,24 @@ public void changeAlarmStatus(UpdateAlarmRequestDto updateAlarmRequestDto) { public boolean isBlackholed() { return blackholedAt != null && blackholedAt.isBefore(LocalDateTime.now()); } + + /** + * this.blackholedAt과 전달인자 blackholedAt 중 어느 하나가 null인 경우 false를 반환 + * + * @param blackholedAt + * @return + */ + public boolean isSameBlackholedAt(LocalDateTime blackholedAt) { + if (this.blackholedAt == null || blackholedAt == null) { + return this.blackholedAt == null && blackholedAt == null; + } + + return this.blackholedAt.isEqual(blackholedAt); + } + + + public void addCoin(Long reward) { + this.coin += reward; + } + } diff --git a/backend/src/main/java/org/ftclub/cabinet/user/repository/UserRepository.java b/backend/src/main/java/org/ftclub/cabinet/user/repository/UserRepository.java index 11e7da9bf..0a384bb73 100644 --- a/backend/src/main/java/org/ftclub/cabinet/user/repository/UserRepository.java +++ b/backend/src/main/java/org/ftclub/cabinet/user/repository/UserRepository.java @@ -15,6 +15,9 @@ @Repository public interface UserRepository extends JpaRepository { + @Query("SELECT u FROM User u WHERE u.id = :userId") + User getById(@Param("userId") Long userId); + /** * 유저 고유 아이디로 유저를 가져옵니다. * @@ -46,7 +49,7 @@ public interface UserRepository extends JpaRepository { * @param name 유저 이름 * @return {@link User} */ - @Query("SELECT u FROM User u WHERE u.name = :name AND u.deletedAt IS NULL") + @Query("SELECT u FROM User u WHERE u.name = :name") Optional findByName(@Param("name") String name); /** @@ -134,4 +137,7 @@ public interface UserRepository extends JpaRepository { */ @Query("SELECT u FROM User u WHERE u.id IN :userIds AND u.deletedAt IS NULL") Page findPaginationByIds(@Param("userIds") List userIds, Pageable pageable); + + @Query("SELECT u FROM User u WHERE u.deletedAt IS NULL") + List findAllDeletedAtIsNull(); } diff --git a/backend/src/main/java/org/ftclub/cabinet/user/service/BanPolicyService.java b/backend/src/main/java/org/ftclub/cabinet/user/service/BanPolicyService.java index f856b75c1..3df7b51f7 100644 --- a/backend/src/main/java/org/ftclub/cabinet/user/service/BanPolicyService.java +++ b/backend/src/main/java/org/ftclub/cabinet/user/service/BanPolicyService.java @@ -20,7 +20,7 @@ public BanType verifyBan(LocalDateTime endedAt, LocalDateTime expiredAt) { public LocalDateTime getUnBannedAt(LocalDateTime endedAt, LocalDateTime expiredAt) { long recentBanDays = DateUtil.calculateTwoDateDiffCeil(expiredAt, endedAt); - double squaredBanDays = Math.pow(recentBanDays, 2); + double squaredBanDays = Math.min(Math.pow(recentBanDays, 2), 180); return endedAt.plusDays((long) squaredBanDays); } } 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 a8d7b6e73..cb569e574 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 @@ -14,7 +14,6 @@ import org.ftclub.cabinet.log.Logging; import org.ftclub.cabinet.occupiedtime.OccupiedTimeManager; import org.ftclub.cabinet.user.domain.User; -import org.ftclub.cabinet.utils.lock.LockUtil; import org.springframework.stereotype.Service; @Service @@ -43,11 +42,11 @@ public void issueLentExtension() { List users = userQueryService.findAllUsersByNames(userNames); 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()); + user.addCoin(coinRewardItem.getPrice()); + itemHistoryCommandService.createCoinItemHistory(userId, coinRewardItem.getId()); }); } @@ -59,8 +58,8 @@ public void issueLentExtension() { */ private void saveCoinChangeOnRedis(Long userId, long price) { // 유저 재화 변동량 Redis에 저장 - long userCoinCount = itemRedisService.getCoinAmount(userId); - itemRedisService.saveCoinCount(userId, userCoinCount + price); +// long userCoinCount = itemRedisService.getCoinAmount(userId); +// itemRedisService.saveCoinCount(userId, userCoinCount + price); // 전체 재화 변동량 Redis에 저장 long totalCoinSupply = itemRedisService.getTotalCoinSupply(); diff --git a/backend/src/main/java/org/ftclub/cabinet/user/service/UserCommandService.java b/backend/src/main/java/org/ftclub/cabinet/user/service/UserCommandService.java index 5da70b657..994e9f7b8 100644 --- a/backend/src/main/java/org/ftclub/cabinet/user/service/UserCommandService.java +++ b/backend/src/main/java/org/ftclub/cabinet/user/service/UserCommandService.java @@ -103,10 +103,16 @@ public void updateAlarmStatus(User user, UpdateAlarmRequestDto updateAlarmReques * @param userId 유저의 ID * @param blackholedAt 변경할 blackholedAt */ - public void updateUserBlackholedAtById(Long userId, LocalDateTime blackholedAt) { - User user = userRepository.findById(userId) - .orElseThrow(ExceptionStatus.NOT_FOUND_USER::asServiceException); + public void updateUserBlackholeStatus(Long userId, LocalDateTime blackholedAt) { + User user = userRepository.getById(userId); user.changeBlackholedAt(blackholedAt); + user.setDeletedAt(null); userRepository.save(user); } + + public void updateCoinAmount(Long userId, Long reward) { + User user = userRepository.findById(userId) + .orElseThrow(ExceptionStatus.NOT_FOUND_USER::asServiceException); + user.addCoin(reward); + } } 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 f7d04f82f..66a4054c7 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 @@ -70,7 +70,7 @@ public MyProfileResponseDto getProfile(UserSessionDto user) { AlarmTypeResponseDto userAlarmTypes = currentUser.getAlarmTypes(); boolean isDeviceTokenExpired = userAlarmTypes.isPush() && fcmTokenRedisService.findByUserName(user.getName()).isEmpty(); - Long coins = itemRedisService.getCoinAmount(userId); + Long coins = currentUser.getCoin(); return userMapper.toMyProfileResponseDto(user, cabinet, banHistory, lentExtensionResponseDto, userAlarmTypes, isDeviceTokenExpired, coins); } diff --git a/backend/src/main/java/org/ftclub/cabinet/user/service/UserQueryService.java b/backend/src/main/java/org/ftclub/cabinet/user/service/UserQueryService.java index 0d4fdec40..9415da5ae 100644 --- a/backend/src/main/java/org/ftclub/cabinet/user/service/UserQueryService.java +++ b/backend/src/main/java/org/ftclub/cabinet/user/service/UserQueryService.java @@ -20,8 +20,9 @@ public class UserQueryService { private final UserRepository userRepository; + /** - * 유저를 가져옵니다. + * deletedAt 이 없는 활성화된 유저를 가져옵니다. * * @param userId 유저의 ID * @return 유저 객체를 반환합니다. @@ -155,6 +156,7 @@ public List findUsersAtNoRiskOfBlackhole() { */ public List findAllUsersByNames(List userNames) { return userRepository.findAllUsersInNames(userNames); - } + + } diff --git a/backend/src/main/java/org/ftclub/cabinet/utils/blackhole/manager/BlackholeManager.java b/backend/src/main/java/org/ftclub/cabinet/utils/blackhole/manager/BlackholeManager.java index b1a0a4d27..5414778b1 100644 --- a/backend/src/main/java/org/ftclub/cabinet/utils/blackhole/manager/BlackholeManager.java +++ b/backend/src/main/java/org/ftclub/cabinet/utils/blackhole/manager/BlackholeManager.java @@ -70,7 +70,7 @@ public void handleBlackHole(UserBlackHoleEvent dto) { if (!userRecentIntraProfile.getRole().isInCursus()) { terminateInvalidUser(dto, now); } - userCommandService.updateUserBlackholedAtById(dto.getUserId(), + userCommandService.updateUserBlackholeStatus(dto.getUserId(), userRecentIntraProfile.getBlackHoledAt()); } catch (HttpClientErrorException e) { HttpStatus status = e.getStatusCode(); @@ -94,19 +94,21 @@ public void handleBlackHole(UserBlackHoleEvent dto) { } } - private boolean isBlackholed(LocalDateTime blackholedAt) { - if (blackholedAt == null) { - return false; - } - return blackholedAt.isBefore(LocalDateTime.now()); - } public void blackholeUpdate(User user) { - if (isBlackholed(user.getBlackholedAt())) { - return; - } FtProfile userRecentIntraProfile = getUserRecentIntraProfile(user.getName()); - userCommandService.updateUserBlackholedAtById(user.getId(), + userCommandService.updateUserBlackholeStatus(user.getId(), userRecentIntraProfile.getBlackHoledAt()); } + + private boolean isBlackholeRemains(LocalDateTime blackholedAt) { + return blackholedAt == null || blackholedAt.isAfter(LocalDateTime.now()); + } + + public boolean isBlackholedUser(User user) { + FtProfile userRecentIntraProfile = getUserRecentIntraProfile(user.getName()); + return isBlackholeRemains(userRecentIntraProfile.getBlackHoledAt()); + } + + } diff --git a/backend/src/test/java/org/ftclub/cabinet/user/domain/UserTest.java b/backend/src/test/java/org/ftclub/cabinet/user/domain/UserTest.java index bc2fed3da..08000d32f 100644 --- a/backend/src/test/java/org/ftclub/cabinet/user/domain/UserTest.java +++ b/backend/src/test/java/org/ftclub/cabinet/user/domain/UserTest.java @@ -1,10 +1,35 @@ package org.ftclub.cabinet.user.domain; +import java.time.LocalDateTime; import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; @RequiredArgsConstructor public class UserTest { + @Test + void isSameBlackholedAt_test_both_null() { + User test = User.of("test", "aewf@naver.com", null); + boolean sameBlackholedAt = test.isSameBlackholedAt(null); + Assertions.assertTrue(sameBlackholedAt); + } + + @Test + void isSameBlackholedAt_test_one_null() { + User test = User.of("test", "aewf@naver.com", null); + boolean sameBlackholedAt = test.isSameBlackholedAt(LocalDateTime.now()); + Assertions.assertFalse(sameBlackholedAt); + } + + @Test + void isSameBlackholedAt_test_same() { + LocalDateTime now = LocalDateTime.now(); + User test = User.of("test", "aewf@naver.com", now); + boolean sameBlackholedAt = test.isSameBlackholedAt(now); + Assertions.assertTrue(sameBlackholedAt); + } + // @Test // @DisplayName("유저 생성 성공 - 일반적인 이메일 형식") // void of_성공_일반적인_이메일_형식() { diff --git a/config b/config index 17a625f08..0d3244f5b 160000 --- a/config +++ b/config @@ -1 +1 @@ -Subproject commit 17a625f087c040b00938e9369e12c0732fc4bb7b +Subproject commit 0d3244f5b700c2b735c13a73ed4e8868139b144f diff --git a/frontend/.prettierrc b/frontend/.prettierrc index d55a2894b..5f131ea44 100644 --- a/frontend/.prettierrc +++ b/frontend/.prettierrc @@ -8,6 +8,8 @@ "arrowParens": "always", "importOrder": [ "", + "^@/.*\\.css$", + "^@/App", "^@/Cabinet/recoil/(.*)$", "^@/Cabinet/pages/(.*)$", "^@/Cabinet/components/(.*)$", diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 1e2b3b809..f8f391019 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,8 +12,9 @@ "@nivo/core": "^0.80.0", "@nivo/line": "^0.80.0", "@nivo/pie": "^0.80.0", + "@sentry/react": "^8.18.0", "@types/react-color": "^3.0.7", - "axios": "^1.2.1", + "axios": "^1.7.2", "firebase": "^10.4.0", "react": "^18.2.0", "react-color": "^2.19.3", @@ -38,6 +39,7 @@ "@typescript-eslint/eslint-plugin": "^5.46.1", "@typescript-eslint/parser": "^5.46.1", "@vitejs/plugin-react": "^3.0.0", + "babel-plugin-styled-components": "^2.1.4", "date-fns": "^3.6.0", "eslint": "^8.30.0", "eslint-config-airbnb": "^19.0.4", @@ -65,7 +67,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", - "dev": true, "dependencies": { "@jridgewell/gen-mapping": "^0.1.0", "@jridgewell/trace-mapping": "^0.3.9" @@ -90,7 +91,6 @@ "version": "7.23.2", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.2.tgz", "integrity": "sha512-0S9TQMmDHlqAZ2ITT95irXKfxN9bncq8ZCoJhun3nHL/lLUxd2NKBJYoNGWH7S0hz6fRQwWlAWn/ILM0C70KZQ==", - "dev": true, "engines": { "node": ">=6.9.0" } @@ -99,7 +99,6 @@ "version": "7.23.2", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.2.tgz", "integrity": "sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ==", - "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.22.13", @@ -179,7 +178,6 @@ "version": "7.22.15", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", - "dev": true, "dependencies": { "@babel/compat-data": "^7.22.9", "@babel/helper-validator-option": "^7.22.15", @@ -195,7 +193,6 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, "dependencies": { "yallist": "^3.0.2" } @@ -203,8 +200,7 @@ "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" }, "node_modules/@babel/helper-create-class-features-plugin": { "version": "7.22.15", @@ -320,7 +316,6 @@ "version": "7.23.0", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.0.tgz", "integrity": "sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw==", - "dev": true, "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-module-imports": "^7.22.15", @@ -351,7 +346,6 @@ "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", - "dev": true, "engines": { "node": ">=6.9.0" } @@ -394,7 +388,6 @@ "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", - "dev": true, "dependencies": { "@babel/types": "^7.22.5" }, @@ -445,7 +438,6 @@ "version": "7.22.15", "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==", - "dev": true, "engines": { "node": ">=6.9.0" } @@ -468,7 +460,6 @@ "version": "7.23.2", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.2.tgz", "integrity": "sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ==", - "dev": true, "dependencies": { "@babel/template": "^7.22.15", "@babel/traverse": "^7.23.2", @@ -667,7 +658,6 @@ "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz", "integrity": "sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -2826,7 +2816,6 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", - "dev": true, "dependencies": { "@jridgewell/set-array": "^1.0.0", "@jridgewell/sourcemap-codec": "^1.4.10" @@ -3252,6 +3241,126 @@ } } }, + "node_modules/@sentry-internal/browser-utils": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.18.0.tgz", + "integrity": "sha512-1R7QXp7Gu6ovJGWvGjbgHcDcvDstsQba3miHtUCyDSH9kXtnAVLCAItDkseetFh+JLsjBXf3QFi2H3HPY4hRCw==", + "dependencies": { + "@sentry/core": "8.18.0", + "@sentry/types": "8.18.0", + "@sentry/utils": "8.18.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry-internal/feedback": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-8.18.0.tgz", + "integrity": "sha512-on6+4ZRkfdnsNgXecGQ6ME8aO26VTzkuM6y/kNN+bG2hSdxsmuU957B4x1Z5wEXiOWswuf3rhqGepg8JIdPkMQ==", + "dependencies": { + "@sentry/core": "8.18.0", + "@sentry/types": "8.18.0", + "@sentry/utils": "8.18.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry-internal/replay": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-8.18.0.tgz", + "integrity": "sha512-cCLib/HjD8UR0fB2F5hV6KsFBD6yTOEsi67RBllm5gT5vJt87VYoPliF6O7mmMNw8TWkQ0uc5laKld3q9ph+ug==", + "dependencies": { + "@sentry-internal/browser-utils": "8.18.0", + "@sentry/core": "8.18.0", + "@sentry/types": "8.18.0", + "@sentry/utils": "8.18.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry-internal/replay-canvas": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-8.18.0.tgz", + "integrity": "sha512-fcuLJBrhw3Ql8sU8veUgDCRYo6toQldFU807cpYphQ0uEw2oVZwNNPDQSu1651Ykvp0P/x+9hk/jjJxMohrO9g==", + "dependencies": { + "@sentry-internal/replay": "8.18.0", + "@sentry/core": "8.18.0", + "@sentry/types": "8.18.0", + "@sentry/utils": "8.18.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry/browser": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-8.18.0.tgz", + "integrity": "sha512-E2w9u76JcjxcmgvroJrB7bcbG5oBCYI/pME1CtprBgZSS9mMYDsyBe6JKqGHdw2wvT3xNxNtkm7hf1O6+3NWUQ==", + "dependencies": { + "@sentry-internal/browser-utils": "8.18.0", + "@sentry-internal/feedback": "8.18.0", + "@sentry-internal/replay": "8.18.0", + "@sentry-internal/replay-canvas": "8.18.0", + "@sentry/core": "8.18.0", + "@sentry/types": "8.18.0", + "@sentry/utils": "8.18.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry/core": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.18.0.tgz", + "integrity": "sha512-8moEMC3gp4W6mH9w5amb/zrYk6bNW8WGgcLRMCs5rguxny8YP5i8ISOJ0T0LP9x/RxSK/6xix5D2bzI/5ECzlw==", + "dependencies": { + "@sentry/types": "8.18.0", + "@sentry/utils": "8.18.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry/react": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@sentry/react/-/react-8.18.0.tgz", + "integrity": "sha512-ckCKdxmeFdfR6moE/Aiq+cJyQuCUKoUqU/++xZwqVbgecuImsk4s7CzzpX9T6JoYK7jqru2SvuRSiwcdtLN6AQ==", + "dependencies": { + "@sentry/browser": "8.18.0", + "@sentry/core": "8.18.0", + "@sentry/types": "8.18.0", + "@sentry/utils": "8.18.0", + "hoist-non-react-statics": "^3.3.2" + }, + "engines": { + "node": ">=14.18" + }, + "peerDependencies": { + "react": "^16.14.0 || 17.x || 18.x || 19.x" + } + }, + "node_modules/@sentry/types": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-8.18.0.tgz", + "integrity": "sha512-5J+uOqptnmAnW3Rk31AHIqW36Wzvlo3UOM+p2wjSYGrC/PgcE47Klzr+w4UcOhN6AZqefalGd3vaUXz9NaFdRg==", + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry/utils": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-8.18.0.tgz", + "integrity": "sha512-7wq7cgaeSIGJncl9/2VMu81ZN5ep4lp4H1/+O8+xUxOmnPb/05ZZcbn9/VxVQvIoqZSZdwCLPeBz6PEVukvokA==", + "dependencies": { + "@sentry/types": "8.18.0" + }, + "engines": { + "node": ">=14.18" + } + }, "node_modules/@sinclair/typebox": { "version": "0.25.21", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.21.tgz", @@ -4696,11 +4805,11 @@ } }, "node_modules/axios": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.2.1.tgz", - "integrity": "sha512-I88cFiGu9ryt/tfVEi4kX2SITsvDddTajXTOFmt2uK1ZVA8LytjtdeyefdQWEf5PU8w+4SSJDoYnggflB5tW4A==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", "dependencies": { - "follow-redirects": "^1.15.0", + "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } @@ -4751,25 +4860,20 @@ } }, "node_modules/babel-plugin-styled-components": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.0.7.tgz", - "integrity": "sha512-i7YhvPgVqRKfoQ66toiZ06jPNA3p6ierpfUuEWxNF+fV27Uv5gxBkf8KZLHUCc1nFA9j6+80pYoIpqCeyW3/bA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.1.4.tgz", + "integrity": "sha512-Xgp9g+A/cG47sUyRwwYxGM4bR/jDRg5N6it/8+HxCnbT5XNKSKDT9xm4oag/osgqjC2It/vH0yXsomOG6k558g==", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.16.0", - "@babel/helper-module-imports": "^7.16.0", - "babel-plugin-syntax-jsx": "^6.18.0", - "lodash": "^4.17.11", - "picomatch": "^2.3.0" + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-module-imports": "^7.22.5", + "@babel/plugin-syntax-jsx": "^7.22.5", + "lodash": "^4.17.21", + "picomatch": "^2.3.1" }, "peerDependencies": { "styled-components": ">= 2" } }, - "node_modules/babel-plugin-syntax-jsx": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", - "integrity": "sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw==" - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -4808,7 +4912,6 @@ "version": "4.22.1", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", - "dev": true, "funding": [ { "type": "opencollective", @@ -4897,7 +5000,6 @@ "version": "1.0.30001551", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001551.tgz", "integrity": "sha512-vtBAez47BoGMMzlbYhfXrMV1kvRF2WP/lqiMuDu1Sb4EE4LKEgjopFDSRtZfdVnslNRpOqV/woE+Xgrwj6VQlg==", - "dev": true, "funding": [ { "type": "opencollective", @@ -5071,8 +5173,7 @@ "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" }, "node_modules/cookie": { "version": "0.4.2", @@ -5615,8 +5716,7 @@ "node_modules/electron-to-chromium": { "version": "1.4.559", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.559.tgz", - "integrity": "sha512-iS7KhLYCSJbdo3rUSkhDTVuFNCV34RKs2UaB9Ecr7VlqzjjWW//0nfsFF5dtDmyXlZQaDYYtID5fjtC/6lpRug==", - "dev": true + "integrity": "sha512-iS7KhLYCSJbdo3rUSkhDTVuFNCV34RKs2UaB9Ecr7VlqzjjWW//0nfsFF5dtDmyXlZQaDYYtID5fjtC/6lpRug==" }, "node_modules/emoji-regex": { "version": "9.2.2", @@ -6598,9 +6698,9 @@ "dev": true }, "node_modules/follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "funding": [ { "type": "individual", @@ -6648,6 +6748,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, "hasInstallScript": true, "optional": true, "os": [ @@ -6694,7 +6795,6 @@ "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, "engines": { "node": ">=6.9.0" } @@ -7953,7 +8053,6 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, "bin": { "json5": "lib/cli.js" }, @@ -8321,8 +8420,7 @@ "node_modules/node-releases": { "version": "2.0.13", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", - "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", - "dev": true + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==" }, "node_modules/nth-check": { "version": "2.1.1", @@ -8637,8 +8735,7 @@ "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -9299,7 +9396,6 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, "bin": { "semver": "bin/semver.js" } @@ -9968,7 +10064,6 @@ "version": "1.0.13", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", - "dev": true, "funding": [ { "type": "opencollective", diff --git a/frontend/package.json b/frontend/package.json index 273f5be30..6940a7a7b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -18,8 +18,9 @@ "@nivo/core": "^0.80.0", "@nivo/line": "^0.80.0", "@nivo/pie": "^0.80.0", + "@sentry/react": "^8.18.0", "@types/react-color": "^3.0.7", - "axios": "^1.2.1", + "axios": "^1.7.2", "firebase": "^10.4.0", "react": "^18.2.0", "react-color": "^2.19.3", @@ -44,6 +45,7 @@ "@typescript-eslint/eslint-plugin": "^5.46.1", "@typescript-eslint/parser": "^5.46.1", "@vitejs/plugin-react": "^3.0.0", + "babel-plugin-styled-components": "^2.1.4", "date-fns": "^3.6.0", "eslint": "^8.30.0", "eslint-config-airbnb": "^19.0.4", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index cf3b35972..4dc792545 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,4 +1,5 @@ import PageTracker from "@/api/analytics/PageTracker"; +import * as Sentry from "@sentry/react"; import React, { Suspense, lazy } from "react"; import { BrowserRouter, Route, Routes } from "react-router-dom"; import AvailablePage from "@/Cabinet/pages/AvailablePage"; @@ -15,6 +16,7 @@ import PostLogin from "@/Cabinet/pages/PostLogin"; import ProfilePage from "@/Cabinet/pages/ProfilePage"; import StoreMainPage from "@/Cabinet/pages/StoreMainPage"; import AdminMainPage from "@/Cabinet/pages/admin/AdminMainPage"; +import AdminSlackNotiPage from "@/Cabinet/pages/admin/AdminSlackNotiPage"; import AdminStorePage from "@/Cabinet/pages/admin/AdminStorePage"; import LoadingAnimation from "@/Cabinet/components/Common/LoadingAnimation"; import DetailPage from "@/Presentation/pages/DetailPage"; @@ -23,7 +25,6 @@ import PresentationLayout from "@/Presentation/pages/Layout"; import PresentationLogPage from "@/Presentation/pages/LogPage"; import RegisterPage from "@/Presentation/pages/RegisterPage"; import AdminPresentationLayout from "@/Presentation/pages/admin/AdminLayout"; -import AdminSlackNotiPage from "@/Cabinet/pages/admin/AdminSlackNotiPage"; const NotFoundPage = lazy(() => import("@/Cabinet/pages/NotFoundPage")); const LoginFailurePage = lazy(() => import("@/Cabinet/pages/LoginFailurePage")); @@ -37,17 +38,15 @@ const AdminLoginFailurePage = lazy( () => import("@/Cabinet/pages/admin/AdminLoginFailurePage") ); const AdminHomePage = lazy(() => import("@/Cabinet/pages/admin/AdminHomePage")); -// const AdminSlackNotiPage = lazy( -// () => import("@/Cabinet/pages/admin/AdminSlackNotiPage") -// ); function App(): React.ReactElement { + const SentryRoutes = Sentry.withSentryReactRouterV6Routing(Routes); return ( {/* GA4 Page Tracking Component */} }> - + } /> }> } /> @@ -91,7 +90,7 @@ function App(): React.ReactElement { element={} /> } /> - + ); diff --git a/frontend/src/Cabinet/api/axios/axios.custom.ts b/frontend/src/Cabinet/api/axios/axios.custom.ts index abd63565a..514135b75 100644 --- a/frontend/src/Cabinet/api/axios/axios.custom.ts +++ b/frontend/src/Cabinet/api/axios/axios.custom.ts @@ -1,9 +1,12 @@ +import { captureException } from "@sentry/react"; import { AlarmInfo } from "@/Cabinet/types/dto/alarm.dto"; import { ClubUserDto } from "@/Cabinet/types/dto/lent.dto"; import CabinetStatus from "@/Cabinet/types/enum/cabinet.status.enum"; import CabinetType from "@/Cabinet/types/enum/cabinet.type.enum"; +import ErrorType from "@/Cabinet/types/enum/error.type.enum"; import { CoinLogToggleType } from "@/Cabinet/types/enum/store.enum"; import instance from "@/Cabinet/api/axios/axios.instance"; +import { logAxiosError } from "@/Cabinet/api/axios/axios.log"; const axiosLogoutUrl = "/v4/auth/logout"; export const axiosLogout = async (): Promise => { @@ -25,36 +28,6 @@ export const axiosMyInfo = async (): Promise => { } }; -const axiosMyExtensionsInfoURL = "/v4/users/me/lent-extensions"; -export const axiosMyExtensionsInfo = async (): Promise => { - try { - const response = await instance.get(axiosMyExtensionsInfoURL); - return response; - } catch (error) { - throw error; - } -}; - -const axiosActiveExtensionInfoURL = "/v4/users/me/lent-extensions/active"; -export const axiosActiveExtensionInfo = async (): Promise => { - try { - const response = await instance.get(axiosActiveExtensionInfoURL); - return response; - } catch (error) { - throw error; - } -}; - -const axiosUseExtensionURL = "/v4/users/me/lent-extensions"; -export const axiosUseExtension = async (): Promise => { - try { - const response = await instance.post(axiosUseExtensionURL); - return response; - } catch (error) { - throw error; - } -}; - const axiosUpdateAlarmURL = "/v4/users/me/alarms"; export const axiosUpdateAlarm = async (alarm: AlarmInfo): Promise => { try { @@ -111,7 +84,7 @@ export const axiosGetClubInfo = async ( const axiosAddClubMemberURL = "/v4/clubs"; export const axiosAddClubMember = async ( clubId: number, - name: String + name: string ): Promise => { // TODO : 예외처리? try { @@ -241,6 +214,7 @@ export const axiosLentId = async (cabinetId: number | null): Promise => { const response = await instance.post(`${axiosLentIdURL}${cabinetId}`); return response; } catch (error) { + logAxiosError(error, ErrorType.LENT, "개인사물함 대여 중 오류 발생"); throw error; } }; @@ -251,17 +225,7 @@ export const axiosMyLentInfo = async (): Promise => { const response = await instance.get(axiosMyLentInfoURL); return response; } catch (error) { - throw error; - } -}; - -const axiosSwapIdURL = "/v4/lent/swap/"; -export const axiosSwapId = async (cabinetId: number | null): Promise => { - if (cabinetId === null) return; - try { - const response = await instance.post(`${axiosSwapIdURL}${cabinetId}`); - return response; - } catch (error) { + logAxiosError(error, ErrorType.LENT, "내 대여 정보 불러오는 중 오류 발생"); throw error; } }; @@ -278,6 +242,11 @@ export const axiosUpdateMyCabinetInfo = async ( }); return response; } catch (error) { + logAxiosError( + error, + ErrorType.LENT, + "내 사물함 정보 업데이트 중 오류 발생" + ); throw error; } }; @@ -288,6 +257,7 @@ export const axiosReturn = async (): Promise => { const response = await instance.patch(axiosReturnURL); return response; } catch (error) { + logAxiosError(error, ErrorType.RETURN, "사물함 반납 중 오류 발생"); throw error; } }; @@ -299,6 +269,11 @@ export const axiosSendCabinetPassword = async (password: string) => { cabinetMemo: password, }); } catch (error) { + logAxiosError( + error, + ErrorType.RETURN, + "3층 사물함 비밀번호 재설정 & 반납 중 오류 발생" + ); throw error; } }; @@ -311,16 +286,7 @@ export const axiosMyLentLog = async (page: number): Promise => { }); return response; } catch (error) { - throw error; - } -}; - -const axiosExtendLentPeriodURL = "/v4/lent/cabinets/extend"; -export const axiosExtendLentPeriod = async (): Promise => { - try { - const response = await instance.patch(axiosExtendLentPeriodURL); - return response; - } catch (error) { + logAxiosError(error, ErrorType.LENT, "내 대여 기록 불러오는 중 오류 발생"); throw error; } }; @@ -331,6 +297,11 @@ export const axiosCoinCheckGet = async (): Promise => { const response = await instance.get(axiosCoinCheck); return response; } catch (error) { + logAxiosError( + error, + ErrorType.STORE, + "동전 줍기 정보 불러오는 중 오류 발생" + ); throw error; } }; @@ -340,6 +311,7 @@ export const axiosCoinCheckPost = async (): Promise => { const response = await instance.post(axiosCoinCheck); return response; } catch (error) { + logAxiosError(error, ErrorType.STORE, "동전 줍기 중 오류 발생"); throw error; } }; @@ -356,6 +328,7 @@ export const axiosCoinLog = async ( }); return response; } catch (error) { + logAxiosError(error, ErrorType.STORE, "코인내역 불러오는중 오류 발생"); throw error; } }; @@ -366,6 +339,7 @@ export const axiosMyItems = async (): Promise => { const response = await instance.get(axiosMyItemsURL); return response; } catch (error) { + logAxiosError(error, ErrorType.STORE, "보유아이템 불러오는중 오류 발생"); throw error; } }; @@ -382,6 +356,11 @@ export const axiosGetItemUsageHistory = async ( }); return response.data; } catch (error) { + logAxiosError( + error, + ErrorType.STORE, + "아이템 사용내역 불러오는중 오류 발생" + ); throw error; } }; @@ -392,22 +371,28 @@ export const axiosItems = async (): Promise => { const response = await instance.get(axiosItemsURL); return response; } catch (error) { + logAxiosError( + error, + ErrorType.STORE, + "상점 아이템 목록 불러오는중 오류 발생" + ); throw error; } }; const axiosBuyItemURL = "/v5/items/"; -export const axiosBuyItem = async (sku: String): Promise => { +export const axiosBuyItem = async (sku: string): Promise => { try { const response = await instance.post(axiosBuyItemURL + sku + "/purchase"); return response; } catch (error) { + logAxiosError(error, ErrorType.STORE, "아이템 구매중 오류 발생"); throw error; } }; export const axiosUseItem = async ( - sku: String, + sku: string, newCabinetId: number | null, building: string | null, floor: number | null, @@ -422,6 +407,7 @@ export const axiosUseItem = async ( }); return response; } catch (error) { + logAxiosError(error, ErrorType.STORE, "아이템 사용중 오류 발생"); throw error; } }; @@ -773,6 +759,12 @@ export const axiosStatisticsCoin = async () => { const response = await instance.get(axiosStatisticsCoinURL); return response; } catch (error) { + logAxiosError( + error, + ErrorType.STORE, + "전체 재화 현황 데이터 불러오는중 오류 발생", + true + ); throw error; } }; @@ -788,25 +780,28 @@ export const axiosCoinUseStatistics = async ( }); return response; } catch (error) { + logAxiosError( + error, + ErrorType.STORE, + "재화 사용 통계 데이터 불러오는중 오류 발생", + true + ); 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) { + logAxiosError( + error, + ErrorType.STORE, + "아이템 사용 통계 데이터 불러오는중 오류 발생", + true + ); throw error; } }; @@ -822,6 +817,12 @@ export const axiosCoinCollectStatistics = async ( }); return response; } catch (error) { + logAxiosError( + error, + ErrorType.STORE, + "동전 줍기 통계 데이터 불러오는중 오류 발생", + true + ); throw error; } }; @@ -838,6 +839,10 @@ export const axiosLentShareId = async ( }); return response; } catch (error) { + captureException(error, { + level: "error", + extra: { type: "대여 에러" }, + }); throw error; } }; @@ -851,6 +856,10 @@ export const axiosCancel = async (cabinetId: number | null): Promise => { const response = await instance.patch(`${axiosCancelURL}${cabinetId}`); return response; } catch (error) { + captureException(error, { + level: "error", + extra: { type: "대여 에러" }, + }); throw error; } }; @@ -908,22 +917,7 @@ export const axiosItemAssign = async ( }); 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) { + logAxiosError(error, ErrorType.STORE, "아이템 지급 중 오류 발생", true); throw error; } }; @@ -940,6 +934,12 @@ export const axiosGetUserItems = async ( }); return response; } catch (error) { + logAxiosError( + error, + ErrorType.STORE, + "유저 아이템 내역 불러오는중 오류 발생", + true + ); throw error; } }; diff --git a/frontend/src/Cabinet/api/axios/axios.instance.ts b/frontend/src/Cabinet/api/axios/axios.instance.ts index a5a583f3e..2642905f4 100644 --- a/frontend/src/Cabinet/api/axios/axios.instance.ts +++ b/frontend/src/Cabinet/api/axios/axios.instance.ts @@ -1,6 +1,7 @@ -import axios from "axios"; +import axios, { HttpStatusCode } from "axios"; +import ErrorType from "@/Cabinet/types/enum/error.type.enum"; +import { logAxiosError } from "@/Cabinet/api/axios/axios.log"; import { getCookie, removeCookie } from "@/Cabinet/api/react_cookie/cookies"; -import { STATUS_401_UNAUTHORIZED } from "@/Cabinet/constants/StatusCode"; axios.defaults.withCredentials = true; @@ -11,9 +12,7 @@ const instance = axios.create({ instance.interceptors.request.use(async (config) => { const token = getCookie("admin_access_token") ?? getCookie("access_token"); - config.headers = { - Authorization: `Bearer ${token}`, - }; + config.headers.set("Authorization", `Bearer ${token}`); return config; }); @@ -23,7 +22,7 @@ instance.interceptors.response.use( }, (error) => { // access_token unauthorized - if (error.response?.status === STATUS_401_UNAUTHORIZED) { + if (error.response?.status === HttpStatusCode.Unauthorized) { if (import.meta.env.VITE_IS_LOCAL === "true") { removeCookie("admin_access_token", { path: "/", @@ -39,9 +38,11 @@ instance.interceptors.response.use( } window.location.href = "login"; alert(error.response.data.message); + } else if (error.response?.status === HttpStatusCode.InternalServerError) { + logAxiosError(error, ErrorType.INTERNAL_SERVER_ERROR, "서버 에러"); } return Promise.reject(error); } ); -export default instance; \ No newline at end of file +export default instance; diff --git a/frontend/src/Cabinet/api/axios/axios.log.ts b/frontend/src/Cabinet/api/axios/axios.log.ts new file mode 100644 index 000000000..19d81ed13 --- /dev/null +++ b/frontend/src/Cabinet/api/axios/axios.log.ts @@ -0,0 +1,26 @@ +import { captureException } from "@sentry/react"; +import ErrorType from "@/Cabinet/types/enum/error.type.enum"; +import { getCookie } from "@/Cabinet/api/react_cookie/cookies"; + +const token = getCookie("admin_access_token") ?? getCookie("access_token"); +const payload = token?.split(".")[1]; +const userId = payload ? JSON.parse(window.atob(payload)).name : "Unknown"; + +export const logAxiosError = ( + error: any, + type: ErrorType, + errorMsg: string, + isAdmin = false +) => { + error.message = (isAdmin ? "[Admin] " : "") + errorMsg; + captureException(error, { + tags: { + userId: userId, + api: error.response?.config.url, + httpMethod: error.config?.method?.toUpperCase(), + httpStatusCode: (error.response?.status ?? "").toString(), + }, + level: "error", + extra: { type: type }, + }); +}; diff --git a/frontend/src/Cabinet/assets/data/ManualContent.ts b/frontend/src/Cabinet/assets/data/ManualContent.ts index 7e92e6370..8b39bebca 100644 --- a/frontend/src/Cabinet/assets/data/ManualContent.ts +++ b/frontend/src/Cabinet/assets/data/ManualContent.ts @@ -1,9 +1,13 @@ +import { ItemIconMap } from "@/Cabinet/assets/data/maps"; import { ReactComponent as ClockImg } from "@/Cabinet/assets/images/clock.svg"; import { ReactComponent as ClubIcon } from "@/Cabinet/assets/images/clubIcon.svg"; +import { ReactComponent as DollarImg } from "@/Cabinet/assets/images/coinDolar.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 { ReactComponent as StoreImg } from "@/Cabinet/assets/images/storeIconGray.svg"; import ContentStatus from "@/Cabinet/types/enum/content.status.enum"; +import { StoreItemType } from "@/Cabinet/types/enum/store.enum"; interface ContentStatusData { contentTitle: string; @@ -15,6 +19,12 @@ interface ContentStatusData { pointColor: string; } +interface ItemStatusData { + icon: React.FunctionComponent>; + title: string; + content: string; +} + export const manualContentData: Record = { [ContentStatus.PRIVATE]: { contentTitle: "개인 사물함", @@ -153,4 +163,107 @@ export const manualContentData: Record = { `, pointColor: "var(--normal-text-color)", }, + [ContentStatus.COIN]: { + contentTitle: `코인 사용법`, + iconComponent: DollarImg, + background: "var(--card-bg-color)", + contentText: `◦ 160시간 출석하기
+
+ 42 출석 160시간을 채운다면 다음달 2일에 2000까비가 지급됩니다.
+
+
+ ◦ 동전줍기
+
+ Cabi 홈페이지에 접속해, 하루에 한 번 동전을 주울 수 있습니다.
+ 한 달 동안 20개의 코인을 모두 줍는다면 랜덤 보상이 주어집니다.
+
+
+ ◦ 수요지식회 발표하기
+
+ 수요지식회 발표자 분께 1000까비를 지급해 드립니다.
+ 수요지식회 신청은 왼쪽 상단의수요지식회 홈페이지에서 신청할 수 있습니다.
+
+ + `, + pointColor: "var(--sys-default-main-color)", + }, + [ContentStatus.STORE]: { + contentTitle: "까비 상점 OPEN!", + iconComponent: StoreImg, + background: + "linear-gradient(to bottom, var(--ref-purple-400), var(--ref-purple-600))", + contentText: ` + `, + pointColor: "var(--white-text-with-bg-color)", + }, +}; + +export const manualItemsData: Record = { + [StoreItemType.EXTENSION]: { + icon: ItemIconMap.EXTENSION, + title: "연장권", + content: ` + store 탭을 눌러 연장권 구매하기 버튼을 클릭 후 3일, 15일, 31일 단위로 구매할 수 있습니다.
+ 구매한 아이템은 인벤토리 탭에서 확인할 수 있습니다.
+
+ ◦ 사용방법
+
+ 사물함을 대여한 상태로, 상단 오른쪽의 상자 아이콘을 누르면 현재 자신의 사물함의 정보를 볼 수 있습니다.
+ 연장권 사용하기 버튼을 눌러 보유한 연장권 중 원하는 타입을 선택 후 사용합니다.
+ 이미 사용한 연장권은 취소할 수 없습니다.
+ 공유사물함의 모든 인원이 연장권을 사용할 수 있지만, 남은 인원이 한 명인 경우 연장권을 사용할 수 없습니다. +
+ `, + }, + [StoreItemType.SWAP]: { + icon: ItemIconMap.SWAP, + title: "이사권", + content: ` + + 기존 일주일에 한 번 가능했던 이사하기 기능을 제한 없이 자유롭게 사용할 수 있습니다.
+ 현재 이용중인 사물함의 대여 기간을 유지한 채 다른 사물함으로 이사할 수 있습니다.
+ store 탭에서 구매할 수 있으며, 인벤토리 탭에서 구매한 아이템을 확인할 수 있습니다. +
+ ◦ 사용방법
+
+ 개인 사물함을 이용중인 사용자만 이사권을 사용할 수 있습니다.
+ 아이템을 보유한 상태로 비어있는 개인 사물함을 눌렀을 때 이사하기 버튼이 활성화됩니다.
+ 이미 사용한 이사권은 취소할 수 없습니다.
+ +
+ `, + }, + [StoreItemType.ALARM]: { + icon: ItemIconMap.ALARM, + title: "알림 등록권", + content: ` + 내가 원하는 섹션에 빈 자리가 나온다면 알림을 받을 수 있습니다.
+ 개인사물함에 대해서만 알림을 받을 수 있습니다.
+ store 탭에서 구매할 수 있으며, 인벤토리 탭에서 구매한 아이템을 확인할 수 있습니다.
+
+ ◦ 사용방법
+
+ 아이템 구매 후 원하는 섹션으로 이동해 우측 상단의 하트 아이콘을 클릭합니다.
+ 사용한 알림 등록권은 섹션을 변경하거나 취소할 수 없습니다.
+ 알림등록권은 1회 알림 후 소멸됩니다. +
+ + `, + }, + [StoreItemType.PENALTY]: { + icon: ItemIconMap.PENALTY, + title: "페널티 감면권", + content: ` + 페널티 감면권은 7일, 15일, 31일 단위로 구매할 수 있습니다.
+ 구매한 아이템은 인벤토리 탭에서 확인할 수 있습니다.
+
+ ◦ 사용방법
+
+ 사물함 대여 불가 페널티가 부과된 유저라면 Profile 탭을 눌러 대여정보 상자 상단의
+ 페널티 감면권 사용하기 버튼이 활성화됩니다.
+ 버튼을 눌러 내가 보유한 페널티 감면권을 선택하면, 남은 페널티 기간을 확인하실 수 있습니다.
+ 연체된 사물함을 아직 반납하지 않았다면, 우선 반납하기 버튼을 눌러야 사용 버튼이 활성화됩니다. +
+ `, + }, }; diff --git a/frontend/src/Cabinet/assets/data/SlackAlarm.ts b/frontend/src/Cabinet/assets/data/SlackAlarm.ts index c89bb73cf..3def9e28e 100644 --- a/frontend/src/Cabinet/assets/data/SlackAlarm.ts +++ b/frontend/src/Cabinet/assets/data/SlackAlarm.ts @@ -23,112 +23,165 @@ export const SlackAlarmTemplates: ISlackAlarmTemplate[] = [ { title: "점검 시작", content: `:alert::alert::alert::alert::alert::alert: - :happy_ccabi: 안녕하세요. Cabi 팀입니다! :happy_ccabi: - :sad_ccabi: 서비스 개선을 위해, 서버를 점검하게 되었습니다. :sad_ccabi: - :file_cabinet: 서비스 개선과 관련한 사항은 Cabi 채널 에서, :file_cabinet: - :hammer_and_wrench: 보관물 관련 사항은 *데스크에 직접 문의*해주세요! :hammer_and_wrench: - 점검 일자 : 2024년 04월 02일 (화요일) - 점검 시간 : 15시 10분 ~ 완료 공지 시점까지 +:happy_ccabi: 안녕하세요. Cabi 팀입니다! :happy_ccabi: +:sad_ccabi: 서비스 개선을 위해, 서버를 점검하게 되었습니다. :sad_ccabi: +:file_cabinet: 서비스 개선과 관련한 사항은 Cabi 채널 에서, :file_cabinet: +:hammer_and_wrench: 보관물 관련 사항은 *데스크에 직접 문의* 해주세요! :hammer_and_wrench: - :party-dinosaur::party-dinosaur::party-dinosaur::party-dinosaur:잠시만 기다려주세요! :party-dinosaur::party-dinosaur::party-dinosaur::party-dinosaur:`, +점검 일자 : 2024년 04월 02일 (화요일) +점검 시간 : 15시 10분 ~ 완료 공지 시점까지 + +:party-dinosaur::party-dinosaur::party-dinosaur::party-dinosaur:잠시만 기다려주세요! :party-dinosaur::party-dinosaur::party-dinosaur::party-dinosaur:`, }, { title: "점검 완료", content: `:dancing_kirby::dancing_kirby::dancing_kirby::dancing_kirby::dancing_kirby::dancing_kirby: - 안녕하세요. Cabi 팀입니다! :happy_ccabi: - 현시간부로 서비스 이용이 정상화되었습니다. - :portal_blue_parrot: 서비스는 cabi.42seoul.io 를 이용해주시면 됩니다. :portal_orange_parrot: - :file_cabinet: 서비스 개선과 관련한 사항은 Cabi 채널 문의주세요! :file_cabinet: - :party-dinosaur::party-dinosaur::party-dinosaur::party-dinosaur:기다려주셔서 감사합니다! :party-dinosaur::party-dinosaur::party-dinosaur::party-dinosaur:`, + +안녕하세요. Cabi 팀입니다! :happy_ccabi: +현시간부로 서비스 이용이 정상화되었습니다. + +:portal_blue_parrot: 서비스는 cabi.42seoul.io 를 이용해주시면 됩니다. :portal_orange_parrot: +:file_cabinet: 서비스 개선과 관련한 사항은 Cabi 채널 문의주세요! :file_cabinet: + +:party-dinosaur::party-dinosaur::party-dinosaur::party-dinosaur:기다려주셔서 감사합니다! :party-dinosaur::party-dinosaur::party-dinosaur::party-dinosaur:`, }, { title: "업데이트", content: `:dancing_kirby::dancing_kirby::dancing_kirby::dancing_kirby::dancing_kirby::dancing_kirby: - :happy_ccabi:동아리 장분들의 동아리 기능 사용 방법:happy_ccabi: - =============================================== - 내용 - =============================================== - :point_right: 서비스는 cabi.42seoul.io 를 이용해주시면 됩니다. :point_left:`, + +:happy_ccabi:동아리 장분들의 동아리 기능 사용 방법:happy_ccabi: +=============================================== +내용 +=============================================== + +:point_right: 서비스는 cabi.42seoul.io 를 이용해주시면 됩니다. :point_left:`, }, { title: "이용 안내서", content: `:file_cabinet: Cabi 이용 안내서 :file_cabinet: - :embarrassed_cabi: 42seoul의 사물함 대여 서비스를 운영중인 Cabi 팀입니다.:embarrassed_cabi: - 자세한 이용 방법은 Cabi 가입 후 홈페이지의 이용 안내서를 참고해 주세요! - :point_right: https://cabi.42seoul.io/home - :alert: Cabi FAQ :alert: - :pushpin: 사물함의 물리적인 문제가 있습니다 (고장 났거나 잠겨있는 경우) - :happy_ccabi: 사물함의 물리적인 문제는 데스크에 문의 부탁드립니다! - :pushpin: 사물함 비밀번호를 모릅니다 (잊어버렸습니다). - :happy_ccabi: 저희 서비스에서 대여한 화면과 슬랙 화면을 준비해서 데스크에 문의해주시기 바랍니다! - :pushpin: 사물함을 닫으려는데 빨간 열쇠 표시가 뜨면서 경고음이 나고 잠기지 않습니다. - :happy_ccabi: 사물함 안이 꽉 차거나 제대로 닫히지 않은 경우에 발생하는데, 문을 누른 상태로 비밀번호를 입력해 보시고, 그래도 되지 않는다면 데스크에 문의 부탁드립니다! - :pushpin: 사물함 대여 후 사용하려고 했더니 안에 짐이 가득 차 있습니다. - :happy_ccabi: 이전 사용자의 짐과 관련한 문의는 데스크에 문의 부탁드립니다! - :pushpin: 공유 사물함을 대여했는데 비밀번호는 어디서 알 수 있을까요? - :happy_ccabi: 같이 사용하는 사람이 있다면 대여 내역에서 공유 메모에 적혀 있을 수 있습니다. 또는 함께 사용하는 분에게 여쭤보세요! - :pushpin: 사물함을 연체 했는데 페널티는 무엇인가요? - :happy_ccabi: 연체일만큼 누적 연체일이 증가하고, 누적일 만큼 대여가 불가능합니다:face_holding_back_tears:`, + + +:embarrassed_cabi: 42seoul의 사물함 대여 서비스를 운영중인 Cabi 팀입니다.:embarrassed_cabi: +자세한 이용 방법은 Cabi 가입 후 홈페이지의 이용 안내서를 참고해 주세요! +:point_right: https://cabi.42seoul.io/home + + +:alert: Cabi FAQ :alert: + +:pushpin: 사물함의 물리적인 문제가 있습니다 (고장 났거나 잠겨있는 경우) +:happy_ccabi: 사물함의 물리적인 문제는 데스크에 문의 부탁드립니다! + +:pushpin: 사물함 비밀번호를 모릅니다 (잊어버렸습니다). +:happy_ccabi: 저희 서비스에서 대여한 화면과 슬랙 화면을 준비해서 데스크에 문의해주시기 바랍니다! + +:pushpin: 사물함을 닫으려는데 빨간 열쇠 표시가 뜨면서 경고음이 나고 잠기지 않습니다. +:happy_ccabi: 사물함 안이 꽉 차거나 제대로 닫히지 않은 경우에 발생하는데, 문을 누른 상태로 비밀번호를 입력해 보시고, 그래도 되지 않는다면 데스크에 문의 부탁드립니다! + +:pushpin: 사물함 대여 후 사용하려고 했더니 안에 짐이 가득 차 있습니다. +:happy_ccabi: 이전 사용자의 짐과 관련한 문의는 데스크에 문의 부탁드립니다! + +:pushpin: 공유 사물함을 대여했는데 비밀번호는 어디서 알 수 있을까요? +:happy_ccabi: 같이 사용하는 사람이 있다면 대여 내역에서 공유 메모에 적혀 있을 수 있습니다. 또는 함께 사용하는 분에게 여쭤보세요! + +:pushpin: 사물함을 연체 했는데 페널티는 무엇인가요? +:happy_ccabi: 연체일만큼 누적 연체일이 증가하고, 누적일 만큼 대여가 불가능합니다:face_holding_back_tears:`, }, { title: "모집 공고", - content: `:embarrassed_cabi::embarrassed_cabi::embarrassed_cabi: :embarrassed_cabi::embarrassed_cabi::embarrassed_cabi: - 안녕하세요 Cabi 팀입니다! - 새로운 팀원 모집 공고를 올립니다:yesyes: - 많은 관심 부탁드립니다! - ---------------------------------------------------- - :dancing_kirby:모집개요:dancing_kirby: - 오늘 {날짜} 부터, {날짜} 23:59까지! - 까비 팀의 프론트엔드 / 백엔드 6기 신청을 받습니다! - 폼 작성 - 간단한 미팅 - 최종 발표 예정입니다! - ... - ...까비에 할애할 수 있으신 분! - - 열정 넘치시는 분! - :four_leaf_clover::four_leaf_clover::four_leaf_clover::four_leaf_clover::four_leaf_clover::four_leaf_clover::four_leaf_clover::four_leaf_clover::four_leaf_clover: - :four_leaf_clover::arrow_right:지금 바로 지원하기:arrow_left::four_leaf_clover: - :four_leaf_clover::four_leaf_clover::four_leaf_clover::four_leaf_clover::four_leaf_clover::four_leaf_clover::four_leaf_clover::four_leaf_clover::four_leaf_clover: - :man-bowing: 상세한 정보는 구글 폼을 참고해주시고, 이외에 모집과 관련한 문의는 - @jpark2 에게 DM 부탁드립니다! :man-bowing:`, + content: `:white_check_mark: 까비 소개 링크, 구글폼 링크 추가 :white_check_mark: + +:embarrassed_cabi::embarrassed_cabi::embarrassed_cabi: :embarrassed_cabi::embarrassed_cabi::embarrassed_cabi: + +안녕하세요 Cabi 팀입니다! +새로운 팀원 모집 공고를 올립니다:yesyes: +많은 관심 부탁드립니다! +---------------------------------------------------- +:dancing_kirby:모집개요:dancing_kirby: +오늘 {날짜} 부터, {날짜} 23:59까지! +까비 팀의 프론트엔드 / 백엔드 n기 신청을 받습니다! +폼 작성 - 간단한 미팅 - 최종 발표 예정입니다! + + +:file_cabinet: 저희 서비스는요…:file_cabinet: +사물함 대여 서비스: 42서울의 사물함 400여 개를 편리하게 대여 및 반납할 수 있는 서비스입니다. +효율적이고 원활한 서비스: 제한된 자원으로 많은 사용자가 원활… (더보기) + + +:hammer_and_wrench: 사용중인 기술 스택 :hammer_and_wrench: +프론트: :typescript: / :reactjs: / Recoil +백: :java: / Spring / Spring Boot / JPA / QueryDSL / MariaDB / Redis +인프라: :aws: EC2, RDS, S3, CloudFront, CodeDeploy / Docker / Github Actions / Pinpoint APM / Grafana / Prometheus +:star::star::star::star:위에 있는 기술들을 모르셔도 상관 없습니다!!:star::star::star::star: + + +:thumbsup_ccabi: 까비 팀에 합류하시면... :thumbsup_ccabi: +월 약 1300명의 유저가 사용하는 서비스를 직접 기획 / 운영 / 개발하기! +메인, 어드민 서비스 유지 및 보수! +친절하고 유쾌한 까비 팀원들과 친해지기! +체계적인 협업 컨벤션과 코드 리뷰! +트러블 슈팅과 기술 아카이브로 포트폴리오 해결! +42Seoul Cabinet System Developer 칭호까지! + + +:cat_thumbs: 다음과 같은 아이템을 염두하고 있어요! :cat_thumbs: +재화 시스템 - 기존 상점 페이지에 새로운 아이템 추가 및 고도화 +수요지식회 - Cabi 페이지에 합쳐진 수요지식회 페이지 분리 및 후기 기능 추가 +성능 최적화 - Lighthouse 지표 개선 + + +:pushpin: 이런 분을 우대합니다! +- 블랙홀이 넉넉하신 분! +- 많은 시간을 까비에 할애할 수 있으신 분! +- 열정 넘치시는 분! + + +:four_leaf_clover::four_leaf_clover::four_leaf_clover::four_leaf_clover::four_leaf_clover::four_leaf_clover::four_leaf_clover::four_leaf_clover::four_leaf_clover: +:four_leaf_clover::arrow_right:지금 바로 지원하기:arrow_left::four_leaf_clover: +:four_leaf_clover::four_leaf_clover::four_leaf_clover::four_leaf_clover::four_leaf_clover::four_leaf_clover::four_leaf_clover::four_leaf_clover::four_leaf_clover: + +:man-bowing: 상세한 정보는 구글 폼을 참고해주시고, 이외에 모집과 관련한 문의는 +@담당자 에게 DM 부탁드립니다! :man-bowing:`, }, { title: "동아리 사물함", content: `:happy_ccabi: 안녕하세요 사물함 서비스를 운영중인 Cabi 팀입니다 :happy_ccabi: - 이번 동아리 사물함 모집을 공지드립니다! - 기존 {날짜} 에 신청하셨던 분들도 재신청해주시기를 바랍니다. - 신청 링크 : {링크} - 내용은 아래와 같습니다. - ----------------------------------------------------- - < :hourglass_flowing_sand: 모집 기간 > - 2024년 동아리 사물함 신청은 일주일 동안 진행됩니다. - 3월 15일(금) 부터 - ~ 3월 22일(금) 23:59 까지 입니다. - *이 시간대 외의 신청은 유효하지 않은 신청으로 간주됩니다:disappointed_relieved: - ----------------------------------------------------- - < :pushpin: 선발 기준 > - [스프레드 시트에 등록된] 42 Seoul 동아리 리스트에 포함된 동아리 - 신청자 수가 사물함보다 많을경우 추첨으로 진행됩니다. - ----------------------------------------------------- - < :mantelpiece_clock: 발표일 > - 10월 20일 오후 중 발표 예정입니다! - 해당 발표 결과는 슬랙의 ‘42seoul_club_cabinet’, ‘42seoul_global_random’ 채널에 공지 드릴 예정입니다. - 사물함 배정이 완료된 후 동아리 사물함 대표분들에게, 슬랙 DM으로 메시지가 전송될 예정입니다. - ----------------------------------------------------- - < :man-tipping-hand: 유의사항 > - 1. 기존에 동아리 사물함을 사용중이셨다면, 새로이 사용하는 동아리들의 원활한 이용을 위해서 - 3월 22일(금)까지 사물함을 비워주시기 바랍니다! - 2. 위 모든 내용은 상황에 따라 변경될 수 있으며 차후에도 변경될 수 있습니다. - 이 때에는 재공지될 예정입니다! - ----------------------------------------------------- - < :telephone_receiver: 문의사항 > - 슬랙의 ‘42seoul_club_cabinet’ 채널에 문의해주시면 됩니다! :sunglasses:`, + +이번 동아리 사물함 모집을 공지드립니다! +기존 {날짜} 에 신청하셨던 분들도 재신청해주시기를 바랍니다. +신청 링크 : {링크} +내용은 아래와 같습니다. +----------------------------------------------------- +< :hourglass_flowing_sand: 모집 기간 > +2024년 동아리 사물함 신청은 일주일 동안 진행됩니다. +3월 15일(금) 부터 +~ 3월 22일(금) 23:59 까지 입니다. +*이 시간대 외의 신청은 유효하지 않은 신청으로 간주됩니다:disappointed_relieved: +----------------------------------------------------- +< :pushpin: 선발 기준 > +[스프레드 시트에 등록된] 42 Seoul 동아리 리스트에 포함된 동아리 +신청자 수가 사물함보다 많을경우 추첨으로 진행됩니다. +----------------------------------------------------- +< :mantelpiece_clock: 발표일 > +10월 20일 오후 중 발표 예정입니다! +해당 발표 결과는 슬랙의 ‘42seoul_club_cabinet’, ‘42seoul_global_random’ 채널에 공지 드릴 예정입니다. +사물함 배정이 완료된 후 동아리 사물함 대표분들에게, 슬랙 DM으로 메시지가 전송될 예정입니다. +----------------------------------------------------- +< :man-tipping-hand: 유의사항 > +1. 기존에 동아리 사물함을 사용중이셨다면, 새로이 사용하는 동아리들의 원활한 이용을 위해서 +3월 22일(금)까지 사물함을 비워주시기 바랍니다! +2. 위 모든 내용은 상황에 따라 변경될 수 있으며 차후에도 변경될 수 있습니다. +이 때에는 재공지될 예정입니다! +----------------------------------------------------- +< :telephone_receiver: 문의사항 > +슬랙의 ‘42seoul_club_cabinet’ 채널에 문의해주시면 됩니다! :sunglasses:`, }, { title: "연체", content: `[CABI] 안녕하세요! :embarrassed_cabi: - 현재 이용 중이신 사물함이 연체인 것으로 확인되어 연락드립니다. - 장기간 연체시 서비스 이용에 대한 페널티, 혹은 :tig:가 부여될 수 있음을 인지해주세요! - 사물함의 대여 기간을 확인하신 후 반납 부탁드립니다. - 항상 저희 서비스를 이용해 주셔서 감사합니다:)`, + 현재 이용 중이신 사물함이 *연체* 된 것으로 확인되어 연락드립니다. + *3주(21일) 이상 연체 시 미회수된 개인 물품은 폐기될 수 있음을 인지해 주세요!* + 사물함의 대여 기간을 확인하신 후 반납 부탁드립니다. + 항상 저희 서비스를 이용해 주셔서 감사합니다:)`, }, ]; diff --git a/frontend/src/Cabinet/assets/data/maps.ts b/frontend/src/Cabinet/assets/data/maps.ts index 8706697ba..a1bfdfc83 100644 --- a/frontend/src/Cabinet/assets/data/maps.ts +++ b/frontend/src/Cabinet/assets/data/maps.ts @@ -1,5 +1,13 @@ +import { css } from "styled-components"; +import { + coinBoxStyles, + extensionBoxStyles, + inSessionBoxStyles, + pendingBoxStyles, + storeBoxStyles, +} from "@/Cabinet/components/Home/ManualContentBoxStyles"; import { ReactComponent as ClubIcon } from "@/Cabinet/assets/images/clubIcon.svg"; -import { ReactComponent as ExtensionImg } from "@/Cabinet/assets/images/extension.svg"; +import { ReactComponent as ExtensionImg } from "@/Cabinet/assets/images/storeExtension.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"; @@ -7,6 +15,7 @@ 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 ContentStatus from "@/Cabinet/types/enum/content.status.enum"; import { StoreExtensionType, StoreItemType, @@ -267,3 +276,16 @@ export const ItemTypeExtensionMap = { [StoreExtensionType.EXTENSION_15]: "15일", [StoreExtensionType.EXTENSION_31]: "31일", }; + +export const ContentStatusStylesMap: { + [key in ContentStatus]: any; +} = { + [ContentStatus.EXTENSION]: extensionBoxStyles, + [ContentStatus.STORE]: storeBoxStyles, + [ContentStatus.COIN]: coinBoxStyles, + [ContentStatus.PENDING]: pendingBoxStyles, + [ContentStatus.IN_SESSION]: inSessionBoxStyles, + [ContentStatus.PRIVATE]: css``, + [ContentStatus.SHARE]: css``, + [ContentStatus.CLUB]: css``, +}; diff --git a/frontend/src/Cabinet/assets/images/coinDolar.svg b/frontend/src/Cabinet/assets/images/coinDolar.svg new file mode 100644 index 000000000..72f985b8f --- /dev/null +++ b/frontend/src/Cabinet/assets/images/coinDolar.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/src/Cabinet/assets/images/storeAlarm.svg b/frontend/src/Cabinet/assets/images/storeAlarm.svg index fdec88cdb..deb44a952 100644 --- a/frontend/src/Cabinet/assets/images/storeAlarm.svg +++ b/frontend/src/Cabinet/assets/images/storeAlarm.svg @@ -1,4 +1,4 @@ - - - + + + diff --git a/frontend/src/Cabinet/assets/images/storeExtension.svg b/frontend/src/Cabinet/assets/images/storeExtension.svg index 91c16096c..d97f6f116 100644 --- a/frontend/src/Cabinet/assets/images/storeExtension.svg +++ b/frontend/src/Cabinet/assets/images/storeExtension.svg @@ -1,6 +1,6 @@ - - - - - + + + + + diff --git a/frontend/src/Cabinet/assets/images/storeMove.svg b/frontend/src/Cabinet/assets/images/storeMove.svg index 4500e4867..f87ec2c31 100644 --- a/frontend/src/Cabinet/assets/images/storeMove.svg +++ b/frontend/src/Cabinet/assets/images/storeMove.svg @@ -1,7 +1,7 @@ - - - - - - + + + + + + diff --git a/frontend/src/Cabinet/assets/images/storePenalty.svg b/frontend/src/Cabinet/assets/images/storePenalty.svg index 158760b33..9838cbd18 100644 --- a/frontend/src/Cabinet/assets/images/storePenalty.svg +++ b/frontend/src/Cabinet/assets/images/storePenalty.svg @@ -1,5 +1,5 @@ - - - - + + + + diff --git a/frontend/src/Cabinet/components/CabinetInfoArea/CabinetInfoArea.tsx b/frontend/src/Cabinet/components/CabinetInfoArea/CabinetInfoArea.tsx index 8ec38283a..9159f504c 100644 --- a/frontend/src/Cabinet/components/CabinetInfoArea/CabinetInfoArea.tsx +++ b/frontend/src/Cabinet/components/CabinetInfoArea/CabinetInfoArea.tsx @@ -9,13 +9,13 @@ import CountTimeContainer from "@/Cabinet/components/CabinetInfoArea/CountTime/C 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"; import LentModal from "@/Cabinet/components/Modals/LentModal/LentModal"; import MemoModalContainer from "@/Cabinet/components/Modals/MemoModal/MemoModal.container"; import PasswordCheckModalContainer from "@/Cabinet/components/Modals/PasswordCheckModal/PasswordCheckModal.container"; import ReturnModal from "@/Cabinet/components/Modals/ReturnModal/ReturnModal"; -import SwapModal from "@/Cabinet/components/Modals/SwapModal/SwapModal"; +import ExtendModal from "@/Cabinet/components/Modals/StoreModal/ExtendModal"; +import SwapModal from "@/Cabinet/components/Modals/StoreModal/SwapModal"; import UnavailableModal from "@/Cabinet/components/Modals/UnavailableModal/UnavailableModal"; import { additionalModalType, diff --git a/frontend/src/Cabinet/components/CabinetInfoArea/CountTime/CodeAndTime.tsx b/frontend/src/Cabinet/components/CabinetInfoArea/CountTime/CodeAndTime.tsx index c3c013be9..5535b3852 100644 --- a/frontend/src/Cabinet/components/CabinetInfoArea/CountTime/CodeAndTime.tsx +++ b/frontend/src/Cabinet/components/CabinetInfoArea/CountTime/CodeAndTime.tsx @@ -5,6 +5,7 @@ import { myCabinetInfoState } from "@/Cabinet/recoil/atoms"; import alertImg from "@/Cabinet/assets/images/cautionSign.svg"; import { ReactComponent as ClockImg } from "@/Cabinet/assets/images/clock.svg"; import { MyCabinetInfoResponseDto } from "@/Cabinet/types/dto/cabinet.dto"; +import useDebounce from "@/Cabinet/hooks/useDebounce"; interface CountTimeProps { minutes: string; @@ -17,13 +18,18 @@ const CodeAndTime = ({ minutes, seconds, isTimeOver }: CountTimeProps) => { useRecoilValue(myCabinetInfoState); const code = myCabinetInfo.shareCode + ""; const [copySuccess, setCopySuccess] = useState(false); + const { debounce } = useDebounce(); const handleCopyClick = () => { navigator.clipboard.writeText(code).then(() => { setCopySuccess(true); - setTimeout(() => { - setCopySuccess(false); - }, 2000); + debounce( + "codeCopyClick", + () => { + setCopySuccess(false); + }, + 2000 + ); }); }; diff --git a/frontend/src/Cabinet/components/CabinetList/CabinetList.container.tsx b/frontend/src/Cabinet/components/CabinetList/CabinetList.container.tsx index 64515e003..1e9dd4fc2 100644 --- a/frontend/src/Cabinet/components/CabinetList/CabinetList.container.tsx +++ b/frontend/src/Cabinet/components/CabinetList/CabinetList.container.tsx @@ -1,6 +1,7 @@ import React from "react"; -import { useRecoilValue } from "recoil"; +import { useRecoilState, useRecoilValue } from "recoil"; import { + currentFloorSectionNamesState, currentSectionNameState, isMultiSelectState, } from "@/Cabinet/recoil/atoms"; @@ -30,6 +31,10 @@ const CabinetListContainer = ({ const currentSectionName = useRecoilValue(currentSectionNameState); const isMultiSelect = useRecoilValue(isMultiSelectState); const { handleSelectAll, containsAllCabinets } = useMultiSelect(); + const [currentFloorSectionNames] = useRecoilState( + currentFloorSectionNamesState + ); + return ( {isMultiSelect && ( @@ -45,7 +50,9 @@ const CabinetListContainer = ({ /> )} - + {currentFloorSectionNames.includes(currentSectionName) && ( + + )} { }; const DisplayStyleCardWrapper = styled.div` - z-index: 1; align-self: start; `; diff --git a/frontend/src/Cabinet/components/Card/ExtensionCard/ExtensionCard.container.tsx b/frontend/src/Cabinet/components/Card/ExtensionCard/ExtensionCard.container.tsx deleted file mode 100644 index 18958f830..000000000 --- a/frontend/src/Cabinet/components/Card/ExtensionCard/ExtensionCard.container.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { useRecoilState, useSetRecoilState } from "recoil"; -import { - currentCabinetIdState, - myCabinetInfoState, - targetCabinetInfoState, - userState, -} from "@/Cabinet/recoil/atoms"; -import ExtensionCard from "@/Cabinet/components/Card/ExtensionCard/ExtensionCard"; -import { CabinetInfo } from "@/Cabinet/types/dto/cabinet.dto"; -import { LentDto, LentExtensionDto } from "@/Cabinet/types/dto/lent.dto"; -import { axiosCabinetById } from "@/Cabinet/api/axios/axios.custom"; -import useMenu from "@/Cabinet/hooks/useMenu"; - -const ExtensionCardContainer = ({ - extensionInfo, -}: { - extensionInfo: LentExtensionDto | null; -}) => { - const { toggleCabinet, openCabinet, closeAll } = useMenu(); - const [myInfo, setMyInfo] = useRecoilState(userState); - const [currentCabinetId, setCurrentCabinetId] = useRecoilState( - currentCabinetIdState - ); - const [myCabinetInfo, setMyCabinetInfo] = useRecoilState(myCabinetInfoState); - const setTargetCabinetInfo = useSetRecoilState( - targetCabinetInfoState - ); - async function setTargetCabinetInfoToMyCabinet() { - setCurrentCabinetId(myInfo.cabinetId); - setMyInfo((prev) => ({ ...prev, cabinetId: null })); - try { - if (!myCabinetInfo?.cabinetId) return; - const { data } = await axiosCabinetById(myCabinetInfo.cabinetId); - if (data.lents.length === 0 && myInfo.cabinetId !== null) { - setMyInfo((prev) => ({ ...prev, cabinetId: null })); - } else { - const doesNameExist = data.lents.some( - (lent: LentDto) => lent.name === myInfo.name - ); - if (doesNameExist) { - setTargetCabinetInfo(data); - setCurrentCabinetId(data.cabinetId); - setMyInfo((prev) => ({ ...prev, cabinetId: data.cabinetId })); - } - } - } catch (error) { - console.log(error); - } - } - const clickMyCabinet = () => { - if (!!extensionInfo && !!myInfo.cabinetId) { - if (myInfo.cabinetId === null && !myCabinetInfo?.cabinetId) { - setTargetCabinetInfoToMyCabinet(); - toggleCabinet(); - } else if (currentCabinetId !== myInfo.cabinetId) { - setTargetCabinetInfoToMyCabinet(); - openCabinet(); - } else { - toggleCabinet(); - } - } - }; - return ( - clickMyCabinet(), - isClickable: !!extensionInfo && !!myInfo.cabinetId, - isExtensible: !!extensionInfo, - }} - /> - ); -}; - -export default ExtensionCardContainer; diff --git a/frontend/src/Cabinet/components/Card/ExtensionCard/ExtensionCard.tsx b/frontend/src/Cabinet/components/Card/ExtensionCard/ExtensionCard.tsx deleted file mode 100644 index e35446a69..000000000 --- a/frontend/src/Cabinet/components/Card/ExtensionCard/ExtensionCard.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { useState } from "react"; -import Card, { IButtonProps } from "@/Cabinet/components/Card/Card"; -import { - CardContentStyled, - CardContentWrapper, - ContentDetailStyled, - ContentInfoStyled, -} from "@/Cabinet/components/Card/CardStyles"; -import { NotificationModal } from "@/Cabinet/components/Modals/NotificationModal/NotificationModal"; -import { LentExtensionDto } from "@/Cabinet/types/dto/lent.dto"; -import { formatDate } from "@/Cabinet/utils/dateUtils"; - -interface ExtensionProps { - extensionInfo: LentExtensionDto | null; - button: IButtonProps; -} - -const NotificationModalDetail = `연장권은 매월 2일 제공되며,
이전에 받은 연장권은 사용이 불가능 합니다.
24HANE 기준 160시간을 출석한 경우,
연장권이 부여됩니다.`; - -const ExtensionCard = ({ extensionInfo, button }: ExtensionProps) => { - const [showNotificationModal, setShowNotificationModal] = - useState(false); - return ( - <> - { - setShowNotificationModal(true); - }} - gridArea={"extension"} - width={"350px"} - height={"183px"} - buttons={[button]} - > - - - 사용 기한 - - {!!extensionInfo - ? formatDate(new Date(extensionInfo.expiredAt), ".", 4, 2, 2) - : "-"} - - - - 연장 기간 - - {!!extensionInfo ? extensionInfo.extensionPeriod + "일" : "-"} - - - - - {showNotificationModal && ( - setShowNotificationModal(false)} - /> - )} - - ); -}; - -export default ExtensionCard; diff --git a/frontend/src/Cabinet/components/Card/LentInfoCard/LentInfoCard.tsx b/frontend/src/Cabinet/components/Card/LentInfoCard/LentInfoCard.tsx index 80ab11f97..b3cc0195b 100644 --- a/frontend/src/Cabinet/components/Card/LentInfoCard/LentInfoCard.tsx +++ b/frontend/src/Cabinet/components/Card/LentInfoCard/LentInfoCard.tsx @@ -7,11 +7,11 @@ import { ContentInfoStyled, } from "@/Cabinet/components/Card/CardStyles"; import { MyCabinetInfo } from "@/Cabinet/components/Card/LentInfoCard/LentInfoCard.container"; +import PenaltyModal from "@/Cabinet/components/Modals/StoreModal/PenaltyModal"; 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 PenaltyModal from "@/Cabinet/components/Modals//PenaltyModal/PenaltyModal"; const calculateFontSize = (userCount: number): string => { const baseSize = 1; diff --git a/frontend/src/Cabinet/components/Card/StoreItemCard/StoreItemCard.tsx b/frontend/src/Cabinet/components/Card/StoreItemCard/StoreItemCard.tsx index 717e0c2df..17b021574 100644 --- a/frontend/src/Cabinet/components/Card/StoreItemCard/StoreItemCard.tsx +++ b/frontend/src/Cabinet/components/Card/StoreItemCard/StoreItemCard.tsx @@ -137,8 +137,6 @@ const ItemIconStyled = styled.div<{ itemType: StoreItemType }>` & > svg > path { stroke: var(--white-text-with-bg-color); - stroke-width: ${(props) => - props.itemType === StoreItemType.EXTENSION ? "2.8px" : "1.5px"}; } `; diff --git a/frontend/src/Cabinet/components/Club/AdminClubLog.container.tsx b/frontend/src/Cabinet/components/Club/AdminClubLog.container.tsx index 95b7235f9..67ddd1873 100644 --- a/frontend/src/Cabinet/components/Club/AdminClubLog.container.tsx +++ b/frontend/src/Cabinet/components/Club/AdminClubLog.container.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from "react"; import AdminClubLog from "@/Cabinet/components/Club/AdminClubLog"; import { ClubLogResponseType, ClubUserDto } from "@/Cabinet/types/dto/lent.dto"; import { axiosGetClubUserLog } from "@/Cabinet/api/axios/axios.custom"; -import { STATUS_400_BAD_REQUEST } from "@/Cabinet/constants/StatusCode"; +import { HttpStatusCode } from "axios"; const AdminClubLogContainer = (props: any) => { const [logs, setLogs] = useState(undefined); @@ -20,7 +20,7 @@ const AdminClubLogContainer = (props: any) => { } else setTotalPage(Math.ceil(result.data.totalLength / size)); setLogs(result.data.result); } catch { - setLogs(STATUS_400_BAD_REQUEST); + setLogs(HttpStatusCode.BadRequest); setTotalPage(1); } }; diff --git a/frontend/src/Cabinet/components/Club/ClubInfo.tsx b/frontend/src/Cabinet/components/Club/ClubInfo.tsx index d26f04cbc..141635837 100644 --- a/frontend/src/Cabinet/components/Club/ClubInfo.tsx +++ b/frontend/src/Cabinet/components/Club/ClubInfo.tsx @@ -1,3 +1,4 @@ +import { HttpStatusCode } from "axios"; import { useEffect, useState } from "react"; import { useRecoilState } from "recoil"; import styled from "styled-components"; @@ -10,7 +11,6 @@ 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"; -import { STATUS_400_BAD_REQUEST } from "@/Cabinet/constants/StatusCode"; const ClubInfo = () => { const [myInfo] = useRecoilState(userState); @@ -20,7 +20,7 @@ const ClubInfo = () => { useEffect(() => { closeAll(); - if (clubInfo && clubInfo !== STATUS_400_BAD_REQUEST) { + if (clubInfo && clubInfo !== HttpStatusCode.BadRequest) { let clubInfoTest = clubInfo as ClubInfoResponseDto; if (clubInfoTest.clubMaster.userName === myInfo.name) setIsMaster(true); } @@ -30,7 +30,7 @@ const ClubInfo = () => { <> {clubInfo === undefined ? ( - ) : clubInfo === STATUS_400_BAD_REQUEST ? ( + ) : clubInfo === HttpStatusCode.BadRequest ? ( <> diff --git a/frontend/src/Cabinet/components/Club/ClubLogTable.tsx b/frontend/src/Cabinet/components/Club/ClubLogTable.tsx index b99d219f3..59f3f610b 100644 --- a/frontend/src/Cabinet/components/Club/ClubLogTable.tsx +++ b/frontend/src/Cabinet/components/Club/ClubLogTable.tsx @@ -1,9 +1,9 @@ +import { HttpStatusCode } from "axios"; import { useRecoilState } from "recoil"; import styled from "styled-components"; import { selectedClubInfoState } from "@/Cabinet/recoil/atoms"; import LoadingAnimation from "@/Cabinet/components/Common/LoadingAnimation"; import { ClubLogResponseType, ClubUserDto } from "@/Cabinet/types/dto/lent.dto"; -import { STATUS_400_BAD_REQUEST } from "@/Cabinet/constants/StatusCode"; const ClubLogTable = ({ ClubList }: { ClubList: ClubLogResponseType }) => { const [selectedClubInfo, setSelectedClubInfo] = useRecoilState( @@ -20,7 +20,7 @@ const ClubLogTable = ({ ClubList }: { ClubList: ClubLogResponseType }) => { return ( <> - {ClubList !== STATUS_400_BAD_REQUEST && ClubList.length !== 0 ? ( + {ClubList !== HttpStatusCode.BadRequest && ClubList.length !== 0 ? ( diff --git a/frontend/src/Cabinet/components/Club/ClubMemberList/ClubMemberList.container.tsx b/frontend/src/Cabinet/components/Club/ClubMemberList/ClubMemberList.container.tsx index 0df8e737c..44c6344cd 100644 --- a/frontend/src/Cabinet/components/Club/ClubMemberList/ClubMemberList.container.tsx +++ b/frontend/src/Cabinet/components/Club/ClubMemberList/ClubMemberList.container.tsx @@ -1,11 +1,12 @@ import { useEffect, useState } from "react"; -import { useRecoilState, useRecoilValue } from "recoil"; -import { targetClubUserInfoState, userState } from "@/Cabinet/recoil/atoms"; +import { useRecoilState } from "recoil"; +import { targetClubUserInfoState } from "@/Cabinet/recoil/atoms"; import ClubMemberList from "@/Cabinet/components/Club/ClubMemberList/ClubMemberList"; import { ClubInfoResponseDto, ClubUserResponseDto, } from "@/Cabinet/types/dto/club.dto"; +import useDebounce from "@/Cabinet/hooks/useDebounce"; import useMenu from "@/Cabinet/hooks/useMenu"; export type TClubMemberModalState = "addModal"; @@ -27,7 +28,6 @@ const ClubMemberListContainer = ({ }: ClubMemberListContainerProps) => { const [moreButton, setMoreButton] = useState(true); const [members, setMembers] = useState([]); - // const [sortMembers, setSortMembers] = useState([]); const [clubModal, setClubModal] = useState({ addModal: false, }); @@ -36,12 +36,16 @@ const ClubMemberListContainer = ({ const [targetClubUser, setTargetClubUser] = useRecoilState( targetClubUserInfoState ); - + const { debounce } = useDebounce(); const clickMoreButton = () => { setIsLoading(true); - setTimeout(() => { - setPage(page + 1); - }, 100); + debounce( + "clubMemberList", + () => { + setPage(page + 1); + }, + 300 + ); }; const selectClubMemberOnClick = (member: ClubUserResponseDto) => { diff --git a/frontend/src/Cabinet/components/Home/ManualContentBox.tsx b/frontend/src/Cabinet/components/Home/ManualContentBox.tsx index 5f4885e30..2f973569d 100644 --- a/frontend/src/Cabinet/components/Home/ManualContentBox.tsx +++ b/frontend/src/Cabinet/components/Home/ManualContentBox.tsx @@ -1,18 +1,19 @@ import styled, { css, keyframes } from "styled-components"; import { manualContentData } from "@/Cabinet/assets/data/ManualContent"; +import { ContentStatusStylesMap } from "@/Cabinet/assets/data/maps"; import { ReactComponent as ManualPeopleImg } from "@/Cabinet/assets/images/manualPeople.svg"; import { ReactComponent as MoveBtnImg } from "@/Cabinet/assets/images/moveButton.svg"; import ContentStatus from "@/Cabinet/types/enum/content.status.enum"; -interface MaunalContentBoxProps { +interface ManualContentBoxProps { contentStatus: ContentStatus; } -const MaunalContentBox = ({ contentStatus }: MaunalContentBoxProps) => { +const ManualContentBox = ({ contentStatus }: ManualContentBoxProps) => { const contentData = manualContentData[contentStatus]; return ( - @@ -23,15 +24,13 @@ const MaunalContentBox = ({ contentStatus }: MaunalContentBoxProps) => { contentData.iconComponent && ( )} + - {contentStatus === ContentStatus.IN_SESSION && - contentData.iconComponent && ( - - )}

{contentData.contentTitle}

+ -
+ ); }; @@ -41,7 +40,7 @@ const Rotation = keyframes` } `; -const MaunalContentBoxStyled = styled.div<{ +const ManualContentBoxStyled = styled.div<{ background: string; contentStatus: ContentStatus; }>` @@ -59,12 +58,14 @@ const MaunalContentBoxStyled = styled.div<{ font-weight: bold; cursor: pointer; + ${(props) => ContentStatusStylesMap[props.contentStatus]} + .clockImg { width: 35px; margin-right: 10px; margin-top: 160px; animation: ${Rotation} 1s linear infinite; - stroke: var(--sys-main-color); + stroke: var(--sys-default-main-color); } .contentImg { @@ -73,8 +74,8 @@ const MaunalContentBoxStyled = styled.div<{ & > path { stroke: ${(props) => - props.contentStatus === ContentStatus.EXTENSION - ? "var(--normal-text-color)" + props.contentStatus === ContentStatus.COIN + ? "var(--sys-default-main-color)" : "var(--white-text-with-bg-color)"}; } } @@ -86,44 +87,17 @@ const MaunalContentBoxStyled = styled.div<{ position: absolute; right: 100px; bottom: 30px; - fill: var(--sys-main-color); + fill: var(--sys-def-main-color); } - ${({ contentStatus }) => - contentStatus === ContentStatus.PENDING && - css` - border: 5px double var(--sys-main-color); - box-shadow: inset 0px 0px 0px 5px var(--bg-color); - `} - - ${({ contentStatus }) => - contentStatus === ContentStatus.IN_SESSION && - css` - border: 5px solid var(--sys-main-color); - color: var(--sys-main-color); - `} - - ${({ contentStatus }) => - contentStatus === ContentStatus.EXTENSION && - css` - width: 900px; - color: var(--normal-text-color); - @media screen and (max-width: 1000px) { - width: 280px; - .peopleImg { - display: none; - } - font-size: 21px; - } - `} - - p { + p { margin-top: 90px; ${({ contentStatus }) => (contentStatus === ContentStatus.PENDING || + contentStatus === ContentStatus.COIN || contentStatus === ContentStatus.IN_SESSION) && css` - margin-top: 160px; + color: var(--sys-default-main-color); `} } @@ -134,34 +108,42 @@ const MaunalContentBoxStyled = styled.div<{ right: 35px; bottom: 35px; stroke: ${(props) => - props.contentStatus === ContentStatus.IN_SESSION - ? "var(--sys-main-color)" + props.contentStatus === ContentStatus.COIN + ? "var(--sys-default-main-color)" : props.contentStatus === ContentStatus.EXTENSION ? "var(--normal-text-color)" : "var(--white-text-with-bg-color)"}; cursor: pointer; } - :hover { + &:hover { transition: all 0.3s ease-in-out; ${({ contentStatus }) => contentStatus === ContentStatus.PENDING ? css` - border: 5px double var(--sys-main-color); - box-shadow: inset 0px 0px 0px 5px var(--bg-color), - 10px 10px 25px 0 var(--left-nav-border-shadow-color); + border: 5px double var(--sys-default-main-color); + box-shadow: inset 0px 0px 0px 5px var(--bg-color); + filter: drop-shadow( + 10px 10px 10px var(--left-nav-border-shadow-color) + ); ` + : contentStatus === ContentStatus.STORE + ? css`` // No box-shadow or filter for STORE status : css` - box-shadow: 10px 10px 25px 0 var(--left-nav-border-shadow-color); + filter: drop-shadow( + 10px 10px 10px var(--left-nav-border-shadow-color) + ); `} + p { transition: all 0.3s ease-in-out; transform: translateY(-5px); ${({ contentStatus }) => (contentStatus === ContentStatus.PENDING || + contentStatus === ContentStatus.COIN || contentStatus === ContentStatus.IN_SESSION) && css` - margin-top: 155px; + color: var(--sys-default-main-color); `} } .clockImg { @@ -174,6 +156,11 @@ const MaunalContentBoxStyled = styled.div<{ const ContentTextStyled = styled.div` display: flex; align-items: center; + + & > span { + font-weight: 400; + font-size: 1rem; + } `; -export default MaunalContentBox; +export default ManualContentBox; diff --git a/frontend/src/Cabinet/components/Home/ManualContentBoxStyles.ts b/frontend/src/Cabinet/components/Home/ManualContentBoxStyles.ts new file mode 100644 index 000000000..99c3489d6 --- /dev/null +++ b/frontend/src/Cabinet/components/Home/ManualContentBoxStyles.ts @@ -0,0 +1,97 @@ +import { css } from "styled-components"; + +export const extensionBoxStyles = css` + width: 900px; + color: var(--normal-text-color); + @media screen and (max-width: 1000px) { + width: 280px; + font-size: 21px; + } +`; + +export const storeBoxStyles = css` + width: 620px; + height: 280px; + position: relative; + background: linear-gradient( + to bottom, + var(--ref-purple-400), + var(--ref-purple-600) + ); + border-radius: 40px; + clip-path: path( + "M 0 163.33 + A 23.33 23.33 1 0 0 0 116.67 + L 0 0 + L 396.56 0 + L 413.354 15.67 + L 430.148 0 + L 620 0 + L 620 280 + L 430.148 280 + L 413.354 264.33 + L 396.56 280 + L 0 280 + Z" + ); + /* Explanation of path: + - M 0 175: Move to (0, 175) + - A 25 25 1 0 0 0 125: Draw an arc with radius 25, starting from (0, 175) to (0, 125) // radius-x, radius-y, x-axis-rotation, large-arc-flag, sweep-flag, x, y + - L 0 0: Draw a line from (0, 125) to (0, 0) + - L 396.56 0: Draw a line from (0, 0) to (396.56, 0) + - L 413.354 16.794: Draw a line from (396.56, 0) to (413.354, 16.794) + - L 430.148 0: Draw a line from (413.354, 16.794) to (430.148, 0) + - L 620 0: Draw a line from (430.148, 0) to (620, 0) + - L 620 300: Draw a line from (620, 0) to (620, 300) + - L 430.148 300: Draw a line from (620, 300) to (430.148, 300) + - L 413.354 283.206: Draw a line from (430.148, 300) to (413.354, 283.206) + - L 396.56 300: Draw a line from (413.354, 283.206) to (396.56, 300) + - L 0 300: Draw a line from (396.56, 300) to (0, 300) + - Z: Close the path + */ + &:after { + content: ""; + position: absolute; + top: 25px; + right: 32.99%; /* 2/3 point */ + height: 100%; + width: 4px; + background-image: linear-gradient( + to bottom, + white 33%, + rgba(255, 255, 255, 0) 0% + ); + background-position: right; + background-size: 10px 30px; + background-repeat: repeat-y; + } + @media screen and (max-width: 1100px) { + width: 280px; + font-size: 21px; + clip-path: none; + + &:after { + display: none; + } + } +`; + +export const coinBoxStyles = css` + border: 5px solid var(--sys-default-main-color); + color: var(--sys-main-color); +`; + +export const pendingBoxStyles = css` + border: 6px double var(--sys-main-color); + box-shadow: inset 0px 0px 0px 5px var(--bg-color); +`; + +export const inSessionBoxStyles = css` + border: 5px solid var(--sys-main-color); + color: var(--sys-main-color); +`; + +export const privateBoxStyles = css` + /* border: 5px solid var(--sys-main-color); + color: var(--sys-main-color); */ +`; diff --git a/frontend/src/Cabinet/components/Home/ServiceManual.tsx b/frontend/src/Cabinet/components/Home/ServiceManual.tsx index 94f766f70..8416dd658 100644 --- a/frontend/src/Cabinet/components/Home/ServiceManual.tsx +++ b/frontend/src/Cabinet/components/Home/ServiceManual.tsx @@ -1,7 +1,8 @@ import { useState } from "react"; import styled from "styled-components"; -import MaunalContentBox from "@/Cabinet/components/Home/ManualContentBox"; +import ManualContentBox from "@/Cabinet/components/Home/ManualContentBox"; import ManualModal from "@/Cabinet/components/Modals/ManualModal/ManualModal"; +import { ReactComponent as LinkImg } from "@/Cabinet/assets/images/link.svg"; import ContentStatus from "@/Cabinet/types/enum/content.status.enum"; const ServiceManual = ({ @@ -20,7 +21,7 @@ const ServiceManual = ({ }; const openNotionLink = () => { - window.open("https://cabi.oopy.io/0bbb08a2-241c-444b-8a96-6b33c3796451"); + window.open("https://cabi.oopy.io/115f29ec-ef8e-4748-a8a6-0d0341def33c"); }; return ( @@ -30,68 +31,60 @@ const ServiceManual = ({ Cabi 이용 안내서 - 상세보기 + 상세보기 +

- 가능성의 확장 + 당신의 사물함
- 개인, 공유, 동아리 사물함. + 당신의 방식으로,

+
openModal(ContentStatus.PRIVATE)} + onClick={() => openModal(ContentStatus.COIN)} > - -
-
openModal(ContentStatus.SHARE)} - > - -
-
openModal(ContentStatus.CLUB)} - > - + +

new

+ +
openModal(ContentStatus.STORE)} + > + +

new

+
+
+

- 공정한 대여를 위한 + 가능성의 확장
- 새로운 사물함 서비스. + 개인, 공유, 동아리 사물함.

openModal(ContentStatus.PENDING)} + onClick={() => openModal(ContentStatus.PRIVATE)} > - -

new

+
openModal(ContentStatus.IN_SESSION)} + onClick={() => openModal(ContentStatus.SHARE)} > - -

new

+
-
-

- 사물함을 더 오래 -
- 사용할 수 있는 방법. -

-
openModal(ContentStatus.EXTENSION)} + onClick={() => openModal(ContentStatus.CLUB)} > - +
@@ -107,6 +100,15 @@ const ServiceManual = ({ ); }; +const TicketWrapperStyled = styled.div` + width: 620px; + &:hover { + transition: all 0.3s ease-in-out; + transform: translateY(-5px); + filter: drop-shadow(10px 10px 10px var(--left-nav-border-shadow-color)); + } +`; + const WrapperStyled = styled.div` display: flex; flex-direction: column; @@ -148,9 +150,30 @@ const NotionBtn = styled.button` color: var(--notion-btn-text-color); background: var(--bg-color); border: 1px solid var(--line-color); - :hover { - color: var(--normal-text-color); - font-weight: 400; + display: flex; + align-items: center; + justify-content: center; + @media (hover: hover) and (pointer: fine) { + &:hover { + color: var(--normal-text-color); + font-weight: 400; + + #linknImg > path { + stroke: var(--normal-text-color); + stroke-width: 1.2px; + } + } + } + + & > span { + line-height: 14px; + height: 16px; + } + + & > #linknImg { + width: 12px; + height: 12px; + margin-left: 4px; } `; diff --git a/frontend/src/Cabinet/components/ItemLog/AdminItemProvideLog.container.tsx b/frontend/src/Cabinet/components/ItemLog/AdminItemProvideLog.container.tsx index 1cca6cf96..388d18a46 100644 --- a/frontend/src/Cabinet/components/ItemLog/AdminItemProvideLog.container.tsx +++ b/frontend/src/Cabinet/components/ItemLog/AdminItemProvideLog.container.tsx @@ -2,7 +2,6 @@ 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(); diff --git a/frontend/src/Cabinet/components/ItemLog/ItemLogTable/AdminItemLogTable.tsx b/frontend/src/Cabinet/components/ItemLog/ItemLogTable/AdminItemLogTable.tsx index f406f572c..f50767083 100644 --- a/frontend/src/Cabinet/components/ItemLog/ItemLogTable/AdminItemLogTable.tsx +++ b/frontend/src/Cabinet/components/ItemLog/ItemLogTable/AdminItemLogTable.tsx @@ -1,8 +1,8 @@ +import { HttpStatusCode } from "axios"; import styled from "styled-components"; import LoadingAnimation from "@/Cabinet/components/Common/LoadingAnimation"; import { ItemLogResponseType } from "@/Cabinet/types/dto/admin.dto"; import { formatDate } from "@/Cabinet/utils/dateUtils"; -import { STATUS_400_BAD_REQUEST } from "@/Cabinet/constants/StatusCode"; const AdminItemLogTable = ({ itemLog }: { itemLog: ItemLogResponseType }) => { if (!itemLog) return ; @@ -16,7 +16,7 @@ const AdminItemLogTable = ({ itemLog }: { itemLog: ItemLogResponseType }) => { 사용일
- {itemLog !== STATUS_400_BAD_REQUEST && + {itemLog !== HttpStatusCode.BadRequest && Array.isArray(itemLog.itemHistories) && ( {itemLog.itemHistories.map( @@ -47,7 +47,7 @@ const AdminItemLogTable = ({ itemLog }: { itemLog: ItemLogResponseType }) => { )}
- {(itemLog === STATUS_400_BAD_REQUEST || + {(itemLog === HttpStatusCode.BadRequest || itemLog.totalLength === undefined || itemLog.totalLength === 0) && ( 아이템 내역이 없습니다. diff --git a/frontend/src/Cabinet/components/ItemLog/ItemLogTable/AdminItemProvideTable.tsx b/frontend/src/Cabinet/components/ItemLog/ItemLogTable/AdminItemProvideTable.tsx index cdb30a9fe..098417faa 100644 --- a/frontend/src/Cabinet/components/ItemLog/ItemLogTable/AdminItemProvideTable.tsx +++ b/frontend/src/Cabinet/components/ItemLog/ItemLogTable/AdminItemProvideTable.tsx @@ -1,8 +1,8 @@ +import { HttpStatusCode } from "axios"; import styled from "styled-components"; import LoadingAnimation from "@/Cabinet/components/Common/LoadingAnimation"; import { ItemLogResponseType } from "@/Cabinet/types/dto/admin.dto"; import { formatDate } from "@/Cabinet/utils/dateUtils"; -import { STATUS_400_BAD_REQUEST } from "@/Cabinet/constants/StatusCode"; const AdminItemProvideTable = ({ itemLog, @@ -20,7 +20,7 @@ const AdminItemProvideTable = ({ 아이템 - {itemLog !== STATUS_400_BAD_REQUEST && ( + {itemLog !== HttpStatusCode.BadRequest && ( {itemLog.itemHistories.map( ({ issuedDate, itemName, itemDetails, usedAt }, idx) => ( @@ -47,7 +47,7 @@ const AdminItemProvideTable = ({ )} - {itemLog === STATUS_400_BAD_REQUEST && ( + {itemLog === HttpStatusCode.BadRequest && ( 아이템 사용기록이 없습니다. )}
diff --git a/frontend/src/Cabinet/components/LeftNav/LeftMainNav/LeftMainNav.container.tsx b/frontend/src/Cabinet/components/LeftNav/LeftMainNav/LeftMainNav.container.tsx index f5ed614ab..0f99c12f3 100644 --- a/frontend/src/Cabinet/components/LeftNav/LeftMainNav/LeftMainNav.container.tsx +++ b/frontend/src/Cabinet/components/LeftNav/LeftMainNav/LeftMainNav.container.tsx @@ -10,6 +10,7 @@ import { currentBuildingNameState, currentFloorCabinetState, currentFloorNumberState, + currentFloorSectionNamesState, currentMapFloorState, currentSectionNameState, isCurrentSectionRenderState, @@ -52,6 +53,8 @@ const LeftMainNavContainer = ({ isAdmin }: { isAdmin?: boolean }) => { const setSelectedTypeOnSearch = useSetRecoilState( selectedTypeOnSearchState ); + const [currentFloorSectionNames, setCurrentFloorSectionNames] = + useRecoilState(currentFloorSectionNamesState); useEffect(() => { if (currentFloor === undefined) { @@ -64,6 +67,7 @@ const LeftMainNavContainer = ({ isAdmin }: { isAdmin?: boolean }) => { const sections = response.data.map( (data: CabinetInfoByBuildingFloorDto) => data.section ); + setCurrentFloorSectionNames(sections); let currentSectionFromPersist = undefined; const recoilPersist = localStorage.getItem("recoil-persist"); if (recoilPersist) { @@ -72,6 +76,7 @@ const LeftMainNavContainer = ({ isAdmin }: { isAdmin?: boolean }) => { currentSectionFromPersist = recoilPersistObj.CurrentSection; } } + currentSectionFromPersist && sections.includes(currentSectionFromPersist) ? setCurrentSection(currentSectionFromPersist) diff --git a/frontend/src/Cabinet/components/LeftNav/LeftProfileNav/LeftProfileNav.tsx b/frontend/src/Cabinet/components/LeftNav/LeftProfileNav/LeftProfileNav.tsx index cc18f9c29..c1184208b 100644 --- a/frontend/src/Cabinet/components/LeftNav/LeftProfileNav/LeftProfileNav.tsx +++ b/frontend/src/Cabinet/components/LeftNav/LeftProfileNav/LeftProfileNav.tsx @@ -107,10 +107,10 @@ const SectionLinkStyled = styled.div` @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%); + + #linknImg > path { + stroke: var(--sys-main-color); + } } } `; diff --git a/frontend/src/Cabinet/components/LentLog/AdminCabinetLentLog.container.tsx b/frontend/src/Cabinet/components/LentLog/AdminCabinetLentLog.container.tsx index d001b3cae..486c08a5c 100644 --- a/frontend/src/Cabinet/components/LentLog/AdminCabinetLentLog.container.tsx +++ b/frontend/src/Cabinet/components/LentLog/AdminCabinetLentLog.container.tsx @@ -1,3 +1,4 @@ +import { HttpStatusCode } from "axios"; import { useEffect, useState } from "react"; import { useRecoilValue } from "recoil"; import { currentCabinetIdState } from "@/Cabinet/recoil/atoms"; @@ -5,7 +6,6 @@ import AdminCabinetLentLog from "@/Cabinet/components/LentLog/AdminCabinetLentLo import { LentLogResponseType } from "@/Cabinet/types/dto/lent.dto"; import { axiosGetCabinetLentLog } from "@/Cabinet/api/axios/axios.custom"; import useMenu from "@/Cabinet/hooks/useMenu"; -import { STATUS_400_BAD_REQUEST } from "@/Cabinet/constants/StatusCode"; const AdminCabinetLentLogContainer = () => { const { closeLent } = useMenu(); @@ -20,7 +20,7 @@ const AdminCabinetLentLogContainer = () => { setTotalPage(Math.ceil(result.data.totalLength / 10)); setLogs(result.data.result); } catch { - setLogs(STATUS_400_BAD_REQUEST); + setLogs(HttpStatusCode.BadRequest); setTotalPage(1); } } diff --git a/frontend/src/Cabinet/components/LentLog/AdminUserLentLog.container.tsx b/frontend/src/Cabinet/components/LentLog/AdminUserLentLog.container.tsx index 637e1cc8a..07ae9a158 100644 --- a/frontend/src/Cabinet/components/LentLog/AdminUserLentLog.container.tsx +++ b/frontend/src/Cabinet/components/LentLog/AdminUserLentLog.container.tsx @@ -1,3 +1,4 @@ +import { HttpStatusCode } from "axios"; import { useEffect, useState } from "react"; import { useRecoilValue } from "recoil"; import { targetUserInfoState } from "@/Cabinet/recoil/atoms"; @@ -5,7 +6,6 @@ import AdminUserLentLog from "@/Cabinet/components/LentLog/AdminUserLentLog"; import { LentLogResponseType } from "@/Cabinet/types/dto/lent.dto"; import { axiosGetUserLentLog } from "@/Cabinet/api/axios/axios.custom"; import useMenu from "@/Cabinet/hooks/useMenu"; -import { STATUS_400_BAD_REQUEST } from "@/Cabinet/constants/StatusCode"; const AdminUserLentLogContainer = () => { const { closeLent } = useMenu(); @@ -20,7 +20,7 @@ const AdminUserLentLogContainer = () => { setTotalPage(Math.ceil(result.data.totalLength / 10)); setLogs(result.data.result); } catch { - setLogs(STATUS_400_BAD_REQUEST); + setLogs(HttpStatusCode.BadRequest); setTotalPage(1); } } diff --git a/frontend/src/Cabinet/components/LentLog/LentLog.container.tsx b/frontend/src/Cabinet/components/LentLog/LentLog.container.tsx index 7ebed068b..45f66a218 100644 --- a/frontend/src/Cabinet/components/LentLog/LentLog.container.tsx +++ b/frontend/src/Cabinet/components/LentLog/LentLog.container.tsx @@ -1,10 +1,10 @@ +import { HttpStatusCode } from "axios"; import { useEffect, useState } from "react"; import LentLog from "@/Cabinet/components/LentLog/LentLog"; import { LentLogResponseType } from "@/Cabinet/types/dto/lent.dto"; import { axiosMyLentLog } from "@/Cabinet/api/axios/axios.custom"; import useMenu from "@/Cabinet/hooks/useMenu"; import { getTotalPage } from "@/Cabinet/utils/paginationUtils"; -import { STATUS_400_BAD_REQUEST } from "@/Cabinet/constants/StatusCode"; const LentLogContainer = () => { const { closeLent } = useMenu(); @@ -17,7 +17,7 @@ const LentLogContainer = () => { setTotalPage(getTotalPage(result.data.totalLength, 10)); setLogs(result.data.result); } catch { - setLogs(STATUS_400_BAD_REQUEST); + setLogs(HttpStatusCode.BadRequest); } } useEffect(() => { diff --git a/frontend/src/Cabinet/components/LentLog/LogTable/AdminCabinetLogTable.tsx b/frontend/src/Cabinet/components/LentLog/LogTable/AdminCabinetLogTable.tsx index 2208bc57c..f4c09b9c5 100644 --- a/frontend/src/Cabinet/components/LentLog/LogTable/AdminCabinetLogTable.tsx +++ b/frontend/src/Cabinet/components/LentLog/LogTable/AdminCabinetLogTable.tsx @@ -1,8 +1,8 @@ +import { HttpStatusCode } from "axios"; import styled from "styled-components"; import LoadingAnimation from "@/Cabinet/components/Common/LoadingAnimation"; import { LentLogResponseType } from "@/Cabinet/types/dto/lent.dto"; import { formatDate } from "@/Cabinet/utils/dateUtils"; -import { STATUS_400_BAD_REQUEST } from "@/Cabinet/constants/StatusCode"; const AdminCabinetLogTable = ({ lentLog, @@ -21,7 +21,7 @@ const AdminCabinetLogTable = ({ 반납일 - {lentLog !== STATUS_400_BAD_REQUEST && ( + {lentLog !== HttpStatusCode.BadRequest && ( {lentLog.map( ({ floor, section, name, startedAt, endedAt }, idx) => ( @@ -45,7 +45,7 @@ const AdminCabinetLogTable = ({ )} - {(lentLog === STATUS_400_BAD_REQUEST || lentLog.length === 0) && ( + {(lentLog === HttpStatusCode.BadRequest || lentLog.length === 0) && ( 대여기록이 없습니다. )} diff --git a/frontend/src/Cabinet/components/LentLog/LogTable/LogTable.tsx b/frontend/src/Cabinet/components/LentLog/LogTable/LogTable.tsx index 9aa25e3f8..47b0d1b9b 100644 --- a/frontend/src/Cabinet/components/LentLog/LogTable/LogTable.tsx +++ b/frontend/src/Cabinet/components/LentLog/LogTable/LogTable.tsx @@ -1,8 +1,8 @@ +import { HttpStatusCode } from "axios"; import styled from "styled-components"; import LoadingAnimation from "@/Cabinet/components/Common/LoadingAnimation"; import { LentLogResponseType } from "@/Cabinet/types/dto/lent.dto"; import { formatDate } from "@/Cabinet/utils/dateUtils"; -import { STATUS_400_BAD_REQUEST } from "@/Cabinet/constants/StatusCode"; const LogTable = ({ lentHistory }: { lentHistory: LentLogResponseType }) => { if (lentHistory === undefined) return ; @@ -17,7 +17,7 @@ const LogTable = ({ lentHistory }: { lentHistory: LentLogResponseType }) => { 반납일 - {lentHistory !== STATUS_400_BAD_REQUEST && ( + {lentHistory !== HttpStatusCode.BadRequest && ( {lentHistory.map( ({ floor, section, visibleNum, startedAt, endedAt }, idx) => ( @@ -43,7 +43,7 @@ const LogTable = ({ lentHistory }: { lentHistory: LentLogResponseType }) => { )} - {lentHistory === STATUS_400_BAD_REQUEST || + {lentHistory === HttpStatusCode.BadRequest || (lentHistory.length === 0 && ( 대여기록이 없습니다. ))} diff --git a/frontend/src/Cabinet/components/MapInfo/MapGrid/MapGrid.tsx b/frontend/src/Cabinet/components/MapInfo/MapGrid/MapGrid.tsx index 1f7c894d7..889128910 100644 --- a/frontend/src/Cabinet/components/MapInfo/MapGrid/MapGrid.tsx +++ b/frontend/src/Cabinet/components/MapInfo/MapGrid/MapGrid.tsx @@ -16,6 +16,7 @@ const MapGrid = ({ floor }: { floor: number }) => { setIsCurrentSectionRender(true); setSection(section); }; + return ( {floor && diff --git a/frontend/src/Cabinet/components/Modals/LentModal/LentModal.tsx b/frontend/src/Cabinet/components/Modals/LentModal/LentModal.tsx index 614cfaafa..556af4a8e 100644 --- a/frontend/src/Cabinet/components/Modals/LentModal/LentModal.tsx +++ b/frontend/src/Cabinet/components/Modals/LentModal/LentModal.tsx @@ -46,7 +46,9 @@ const LentModal: React.FC<{ const formattedExpireDate = getExpireDateString(props.lentType); const privateLentDetail = `대여기간은 ${formattedExpireDate} 23:59까지 입니다. - 귀중품 분실 및 메모 내용의 유출에 책임지지 않습니다.`; + 귀중품 분실 및 메모 내용의 유출에 책임지지 않습니다. + 3주(21일) 이상 연체 시 사물함은 강제 반납되며 + 미회수된 개인 물품은 폐기될 수 있습니다.`; const shareLentDetail = `대여 후 ${ 10 // import.meta.env.VITE_SHARE_LENT_COUNTDOWN // TODO: .env 에 등록하기 @@ -54,7 +56,9 @@ const LentModal: React.FC<{ 공유 인원 (2인~4인) 이 충족되지 않으면, 공유 사물함의 대여가 취소됩니다. “메모 내용”은 공유 인원끼리 공유됩니다. - 귀중품 분실 및 메모 내용의 유출에 책임지지 않습니다.`; + 귀중품 분실 및 메모 내용의 유출에 책임지지 않습니다. + 3주(21일) 이상 연체 시 사물함은 강제 반납되며 + 미회수된 개인 물품은 폐기될 수 있습니다.`; const tryLentRequest = async (e: React.MouseEvent) => { setIsLoading(true); try { diff --git a/frontend/src/Cabinet/components/Modals/ManualModal/ManualModal.tsx b/frontend/src/Cabinet/components/Modals/ManualModal/ManualModal.tsx index 494d11383..a238f8fb1 100644 --- a/frontend/src/Cabinet/components/Modals/ManualModal/ManualModal.tsx +++ b/frontend/src/Cabinet/components/Modals/ManualModal/ManualModal.tsx @@ -1,9 +1,13 @@ import React from "react"; import { useState } from "react"; import styled, { keyframes } from "styled-components"; -import { manualContentData } from "@/Cabinet/assets/data/ManualContent"; +import { + manualContentData, + manualItemsData, +} from "@/Cabinet/assets/data/ManualContent"; import { ReactComponent as MoveBtnImg } from "@/Cabinet/assets/images/moveButton.svg"; import ContentStatus from "@/Cabinet/types/enum/content.status.enum"; +import { StoreItemType } from "@/Cabinet/types/enum/store.enum"; interface ModalProps { contentStatus: ContentStatus; @@ -15,6 +19,9 @@ const ManualModal: React.FC = ({ setIsModalOpen, }) => { const [modalIsOpen, setModalIsOpen] = useState(true); + const [selectedItem, setSelectedItem] = useState( + StoreItemType.EXTENSION + ); const contentData = manualContentData[contentStatus]; const isCabinetType = @@ -28,6 +35,10 @@ const ManualModal: React.FC = ({ contentStatus === ContentStatus.SHARE || contentStatus === ContentStatus.CLUB; + const handleIconClick = (index: StoreItemType) => { + setSelectedItem(index); + }; + const closeModal = () => { if (modalIsOpen) { setModalIsOpen(false); @@ -76,12 +87,40 @@ const ManualModal: React.FC = ({ )} )} - {contentData.contentTitle} - -
-
+ {contentStatus === ContentStatus.STORE && ( + <> + + {Object.entries(manualItemsData).map(([key, item]) => ( + handleIconClick(key as StoreItemType)} + className={selectedItem === key ? "selected" : ""} + color={contentData.pointColor} + > + + + ))} + + {manualItemsData[selectedItem].title} + +
+
+ + )} + {contentStatus !== ContentStatus.STORE && ( + <> + {contentData.contentTitle} + +
+
+ + )} @@ -144,9 +183,10 @@ const ModalWrapper = styled.div<{ border-radius: 40px 40px 0 0; border: ${(props) => props.contentStatus === ContentStatus.PENDING - ? "5px double var(--sys-main-color)" - : props.contentStatus === ContentStatus.IN_SESSION - ? "5px solid var(--sys-main-color)" + ? "5px double var(--sys-default-main-color)" + : props.contentStatus === ContentStatus.IN_SESSION || + props.contentStatus === ContentStatus.COIN + ? "5px solid var(--sys-default-main-color)" : "none"}; box-shadow: ${(props) => props.contentStatus === ContentStatus.PENDING && @@ -166,8 +206,9 @@ const ModalContent = styled.div<{ display: flex; flex-direction: column; color: ${(props) => - props.contentStatus === ContentStatus.IN_SESSION - ? "var(--sys-main-color)" + props.contentStatus === ContentStatus.IN_SESSION || + props.contentStatus === ContentStatus.COIN + ? "var(--sys-default-main-color)" : props.contentStatus === ContentStatus.EXTENSION ? "var(--normal-text-color)" : "var(--white-text-with-bg-color)"}; @@ -188,8 +229,9 @@ const ModalContent = styled.div<{ } .moveButton { stroke: ${(props) => - props.contentStatus === ContentStatus.IN_SESSION - ? "var(--sys-main-color)" + props.contentStatus === ContentStatus.IN_SESSION || + props.contentStatus === ContentStatus.COIN + ? "var(--sys-default-main-color)" : props.contentStatus === ContentStatus.EXTENSION ? "var(--normal-text-color)" : "var(--white-text-with-bg-color)"}; @@ -210,8 +252,9 @@ const CloseButton = styled.div<{ svg { transform: scaleX(-1); stroke: ${(props) => - props.contentStatus === ContentStatus.IN_SESSION - ? "var(--sys-main-color)" + props.contentStatus === ContentStatus.IN_SESSION || + props.contentStatus === ContentStatus.COIN + ? "var(--sys-default-main-color)" : props.contentStatus === ContentStatus.EXTENSION ? "var(--normal-text-color)" : "var(--bg-color)"}; @@ -269,7 +312,7 @@ const BoxInfo2 = styled.div` } `; -const ManualContentStyeld = styled.div<{ +const ManualContentStyled = styled.div<{ color: string; }>` margin: 40px 0 0 20px; @@ -330,4 +373,45 @@ const ContentImgStyled = styled.div<{ } `; +const ItemContentsStyled = styled.div` + width: 45%; + height: 90px; + display: flex; + margin-bottom: 30px; +`; + +const ItemIconStyled = styled.div<{ + color: string; +}>` + width: 80px; + height: 80px; + margin-right: 10px; + display: flex; + justify-content: center; + align-items: center; + + & > svg { + padding: 4px; + width: 80px; + height: 80px; + cursor: pointer; + stroke-width: 50px; + & > path { + transform: scale(2); + stroke: var(--ref-purple-690); + } + } + + &:hover:not(.selected), + &.selected > svg { + width: 80px; + height: 80px; + filter: drop-shadow(0px 5px 3px var(--hover-box-shadow-color)); + transition: all 0.2s ease; + & > path { + stroke: ${(props) => props.color}; + } + } +`; + export default ManualModal; diff --git a/frontend/src/Cabinet/components/Modals/Modal.tsx b/frontend/src/Cabinet/components/Modals/Modal.tsx index a854f03a8..545b606bb 100644 --- a/frontend/src/Cabinet/components/Modals/Modal.tsx +++ b/frontend/src/Cabinet/components/Modals/Modal.tsx @@ -6,6 +6,7 @@ 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 useMenu from "@/Cabinet/hooks/useMenu"; import useMultiSelect from "@/Cabinet/hooks/useMultiSelect"; /** @@ -63,6 +64,7 @@ const Modal: React.FC<{ modalContents: IModalContents }> = (props) => { } = props.modalContents; const { isMultiSelect, closeMultiSelectMode } = useMultiSelect(); const navigator = useNavigate(); + const { closeAll } = useMenu(); return ( <> @@ -127,6 +129,7 @@ const Modal: React.FC<{ modalContents: IModalContents }> = (props) => { {url && urlTitle && ( { + closeAll(); navigator(url); }} > diff --git a/frontend/src/Cabinet/components/Modals/ExtendModal/ExtendModal.tsx b/frontend/src/Cabinet/components/Modals/StoreModal/ExtendModal.tsx similarity index 86% rename from frontend/src/Cabinet/components/Modals/ExtendModal/ExtendModal.tsx rename to frontend/src/Cabinet/components/Modals/StoreModal/ExtendModal.tsx index 7ac32e373..74a2241c7 100644 --- a/frontend/src/Cabinet/components/Modals/ExtendModal/ExtendModal.tsx +++ b/frontend/src/Cabinet/components/Modals/StoreModal/ExtendModal.tsx @@ -22,6 +22,7 @@ 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 { IItemDetail, IItemStore } from "@/Cabinet/types/dto/store.dto"; +import CabinetStatus from "@/Cabinet/types/enum/cabinet.status.enum"; import IconType from "@/Cabinet/types/enum/icon.type.enum"; import { axiosCabinetById, @@ -45,11 +46,11 @@ const ExtendModal: React.FC<{ const [items, setItems] = useState([]); const [myItems, setMyItems] = useState(null); const [selectedOption, setSelectedOption] = useState(""); - // const [extensionItems, setExtensionItems] = useState([]); const [myExtensionItems, setMyExtensionItems] = useState([]); const [itemDropdownOptions, setItemDropdownOptions] = useState< IDropdownOptions[] >([]); + const [url, setUrl] = useState(null); const [currentCabinetId] = useRecoilState(currentCabinetIdState); const [myInfo, setMyInfo] = useRecoilState(userState); const [myLentInfo, setMyLentInfo] = @@ -74,8 +75,10 @@ const ExtendModal: React.FC<{ 연장권 사용은 취소할 수 없습니다.`; const extendInfoDetail = `사물함을 대여하시면 연장권 사용이 가능합니다. 연장권은 ${extensionExpiredDate} 23:59 이후 만료됩니다.`; - const noExtension = `현재 연장권을 보유하고 있지 않습니다. + const noItemMsg = `현재 연장권을 보유하고 있지 않습니다. 연장권은 까비 상점에서 구매하실 수 있습니다.`; + const overdueMsg = "연체 중에는 연장권을 사용하실 수 없습니다."; + const defaultFailureModalTitle = "연장권 사용실패"; useEffect(() => { fetchData(); @@ -85,7 +88,7 @@ const ExtendModal: React.FC<{ if (myItems?.extensionItems.length === 0) { setShowResponseModal(true); setHasErrorOnResponse(true); - setModalContents(noExtension); + setModalContents(noItemMsg); } else { setShowResponseModal(false); setHasErrorOnResponse(false); @@ -97,22 +100,21 @@ const ExtendModal: React.FC<{ } if (items.length) { const sortedItems = sortItems(items); - const dropdownOptions: IDropdownOptions[] = getItemDropDownOption(sortedItems[0]); + const dropdownOptions: IDropdownOptions[] = getItemDropDownOption( + sortedItems[0] + ); -// 새로운 항목 생성 -const newOption = { - name: "출석 연장권 보상", - value: "EXTENSION_PREV", - isDisabled: findMyItem("EXTENSION_PREV"), -}; + const extensionPrevOption = { + name: "출석 연장권 보상", + value: "EXTENSION_PREV", + isDisabled: findMyItem("EXTENSION_PREV"), + }; -// 새로운 항목을 dropdownOptions 배열의 마지막에 추가 -dropdownOptions.push(newOption); + dropdownOptions.push(extensionPrevOption); - // setExtensionItems(sortedItems[0].items); setItemDropdownOptions(dropdownOptions); -} -}, [myItems]); + } + }, [myItems]); const fetchData = async () => { try { @@ -128,10 +130,9 @@ dropdownOptions.push(newOption); }; const findMyItem = (period: string) => { - return !myItems?.extensionItems.some((item) => item.itemSku === period); + return !myItems?.extensionItems.some((item) => item.itemSku === period); }; - const getItemDropDownOption = (curItem: IItemDetail): IDropdownOptions[] => { if (curItem) { return curItem.items.map((item) => ({ @@ -143,7 +144,6 @@ dropdownOptions.push(newOption); return []; }; - // Modal related functions const getModalTitle = (cabinetId: number | null) => { return cabinetId === null ? modalPropsMap[additionalModalType.MODAL_OWN_EXTENSION].title @@ -191,7 +191,15 @@ dropdownOptions.push(newOption); const extensionItemUse = async (item: string) => { if (currentCabinetId === 0 || myInfo.cabinetId === null) { setHasErrorOnResponse(true); - setModalTitle("현재 대여중인 사물함이 없습니다."); + setModalTitle(defaultFailureModalTitle); + setModalContents("현재 대여중인 사물함이 없습니다."); + setShowResponseModal(true); + return; + } + if (myLentInfo.status === CabinetStatus.OVERDUE) { + setHasErrorOnResponse(true); + setModalTitle(defaultFailureModalTitle); + setModalContents(overdueMsg); setShowResponseModal(true); return; } @@ -218,8 +226,12 @@ dropdownOptions.push(newOption); } catch (error: any) { setHasErrorOnResponse(true); if (error.response.status === 400) { - setModalTitle("연장권 사용실패"); - setModalContents(noExtension); + setModalTitle(defaultFailureModalTitle); + setModalContents(noItemMsg); + setUrl("/store"); + } else if (error.response.status === 403) { + setModalTitle(defaultFailureModalTitle); + setModalContents(overdueMsg); } else { setModalTitle(error.response?.data.message || error.data.message); } @@ -263,7 +275,7 @@ dropdownOptions.push(newOption); modalTitle={modalTitle} modalContents={modalContents} closeModal={props.onClose} - url={"/store"} + url={url} urlTitle={"까비상점으로 이동"} /> ) : ( diff --git a/frontend/src/Cabinet/components/Modals/PenaltyModal/PenaltyModal.tsx b/frontend/src/Cabinet/components/Modals/StoreModal/PenaltyModal.tsx similarity index 98% rename from frontend/src/Cabinet/components/Modals/PenaltyModal/PenaltyModal.tsx rename to frontend/src/Cabinet/components/Modals/StoreModal/PenaltyModal.tsx index 65d503de0..952ac7baf 100644 --- a/frontend/src/Cabinet/components/Modals/PenaltyModal/PenaltyModal.tsx +++ b/frontend/src/Cabinet/components/Modals/StoreModal/PenaltyModal.tsx @@ -12,13 +12,13 @@ import { FailResponseModal, SuccessResponseModal, } from "@/Cabinet/components/Modals/ResponseModal/ResponseModal"; +import { IInventoryInfo } from "@/Cabinet/components/Store/Inventory/Inventory"; import { IItemDetail, IItemTimeRemaining, IStoreItem, } from "@/Cabinet/types/dto/store.dto"; import { UserDto } from "@/Cabinet/types/dto/user.dto"; -import { StorePenaltyType } from "@/Cabinet/types/enum/store.enum"; import { axiosItems, axiosMyInfo, @@ -26,7 +26,6 @@ import { axiosUseItem, } from "@/Cabinet/api/axios/axios.custom"; import { formatDate, formatDateTime } from "@/Cabinet/utils/dateUtils"; -import { IInventoryInfo } from "../../Store/Inventory/Inventory"; interface PenaltyModalProps { onClose: () => void; @@ -187,8 +186,6 @@ const PenaltyModal: React.FC = ({ } }; - - const modalContents: IModalContents = { type: "hasProceedBtn", iconType: "CHECK", diff --git a/frontend/src/Cabinet/components/Modals/SectionAlertModal/SectionAlertModal.tsx b/frontend/src/Cabinet/components/Modals/StoreModal/SectionAlertModal.tsx similarity index 100% rename from frontend/src/Cabinet/components/Modals/SectionAlertModal/SectionAlertModal.tsx rename to frontend/src/Cabinet/components/Modals/StoreModal/SectionAlertModal.tsx diff --git a/frontend/src/Cabinet/components/Modals/SwapModal/SwapModal.tsx b/frontend/src/Cabinet/components/Modals/StoreModal/SwapModal.tsx similarity index 97% rename from frontend/src/Cabinet/components/Modals/SwapModal/SwapModal.tsx rename to frontend/src/Cabinet/components/Modals/StoreModal/SwapModal.tsx index 7eaaee11e..3d6afba32 100644 --- a/frontend/src/Cabinet/components/Modals/SwapModal/SwapModal.tsx +++ b/frontend/src/Cabinet/components/Modals/StoreModal/SwapModal.tsx @@ -16,10 +16,10 @@ import { import { modalPropsMap } from "@/Cabinet/assets/data/maps"; import { MyCabinetInfoResponseDto } from "@/Cabinet/types/dto/cabinet.dto"; import IconType from "@/Cabinet/types/enum/icon.type.enum"; -import { axiosUseItem } from "@/Cabinet/api/axios/axios.custom"; import { axiosCabinetById, axiosMyLentInfo, + axiosUseItem, } from "@/Cabinet/api/axios/axios.custom"; const SwapModal: React.FC<{ @@ -74,7 +74,7 @@ const SwapModal: React.FC<{ } } catch (error: any) { setModalTitle("이사권 사용실패"); - if (error.response.ststus === 400) { + if (error.response.status === 400) { setModalContent( "현재 이사권을 보유하고 있지 않습니다.\n이사권은 까비상점에서 구매하실 수 있습니다." ); diff --git a/frontend/src/Cabinet/components/Search/SearchCabinetDetails.tsx b/frontend/src/Cabinet/components/Search/SearchCabinetDetails.tsx index 8861c7450..b092bb807 100644 --- a/frontend/src/Cabinet/components/Search/SearchCabinetDetails.tsx +++ b/frontend/src/Cabinet/components/Search/SearchCabinetDetails.tsx @@ -49,9 +49,7 @@ const SearchCabinetDetails = (props: ISearchDetail) => { ); const { openCabinet, closeCabinet } = useMenu(); const CabinetIcon = - cabinetIconComponentMap[ - cabinetInfo?.lentType || CabinetType.PRIVATE - ]; + cabinetIconComponentMap[cabinetInfo?.lentType || CabinetType.PRIVATE]; const clickSearchItem = () => { if ( @@ -150,7 +148,6 @@ const SearchCabinetDetails = (props: ISearchDetail) => { ); }; - const WrapperStyled = styled.div` width: 350px; height: 110px; @@ -252,4 +249,3 @@ const ButtonWrapper = styled.div` `; export default SearchCabinetDetails; - diff --git a/frontend/src/Cabinet/components/Search/SearchNoCabinetDetails.tsx b/frontend/src/Cabinet/components/Search/SearchNoCabinetDetails.tsx index 455a58aed..6456dfc49 100644 --- a/frontend/src/Cabinet/components/Search/SearchNoCabinetDetails.tsx +++ b/frontend/src/Cabinet/components/Search/SearchNoCabinetDetails.tsx @@ -145,7 +145,6 @@ const SearchNoCabinetDetails = (props: ISearchDetail) => { ); }; - const WrapperStyled = styled.div` width: 350px; height: 110px; diff --git a/frontend/src/Cabinet/components/Store/Admin/UserStoreInfoArea/UserStoreInfoArea.tsx b/frontend/src/Cabinet/components/Store/Admin/UserStoreInfoArea/UserStoreInfoArea.tsx index a7c79642e..1b2247d1b 100644 --- a/frontend/src/Cabinet/components/Store/Admin/UserStoreInfoArea/UserStoreInfoArea.tsx +++ b/frontend/src/Cabinet/components/Store/Admin/UserStoreInfoArea/UserStoreInfoArea.tsx @@ -5,7 +5,6 @@ import { targetUserInfoState } from "@/Cabinet/recoil/atoms"; import AdminItemUsageLogPage from "@/Cabinet/pages/admin/AdminItemUsageLogPage"; import ButtonContainer from "@/Cabinet/components/Common/Button"; import SelectInduction from "@/Cabinet/components/Common/SelectInduction"; -import AdminLentLog from "@/Cabinet/components/LentLog/AdminLentLog"; import AdminItemProvisionModal from "@/Cabinet/components/Modals/StoreModal/AdminItemProvisionModal"; import { ReactComponent as LogoIcon } from "@/Cabinet/assets/images/logo.svg"; import useMenu from "@/Cabinet/hooks/useMenu"; diff --git a/frontend/src/Cabinet/components/Store/CoinLog/CoinLog.tsx b/frontend/src/Cabinet/components/Store/CoinLog/CoinLog.tsx index b6d867c8c..425692936 100644 --- a/frontend/src/Cabinet/components/Store/CoinLog/CoinLog.tsx +++ b/frontend/src/Cabinet/components/Store/CoinLog/CoinLog.tsx @@ -11,6 +11,7 @@ import { ReactComponent as CoinIcon } from "@/Cabinet/assets/images/coinIcon.svg import { ReactComponent as Select } from "@/Cabinet/assets/images/selectMaincolor.svg"; import { CoinLogToggleType } from "@/Cabinet/types/enum/store.enum"; import { axiosCoinLog } from "@/Cabinet/api/axios/axios.custom"; +import useDebounce from "@/Cabinet/hooks/useDebounce"; import { formatDate } from "@/Cabinet/utils/dateUtils"; const toggleList: toggleItem[] = [ @@ -44,10 +45,11 @@ const CoinLog = () => { const [userInfo] = useRecoilState(userState); const size = 5; // NOTE : size 만큼 데이터 불러옴 + const { debounce } = useDebounce(); - const getCoinLog = async (type: CoinLogToggleType) => { + const getCoinLog = async () => { try { - const response = await axiosCoinLog(type, page, size); + const response = await axiosCoinLog(toggleType, page, size); if (page === 0) { setCoinLogs(response.data.result); } else { @@ -71,9 +73,7 @@ const CoinLog = () => { }; useEffect(() => { - setTimeout(() => { - getCoinLog(toggleType); - }, 333); + debounce("coinLog", getCoinLog, 100); }, [page, toggleType]); useEffect(() => { diff --git a/frontend/src/Cabinet/components/Store/Inventory/InventoryItem.tsx b/frontend/src/Cabinet/components/Store/Inventory/InventoryItem.tsx index e15a09d89..340134de8 100644 --- a/frontend/src/Cabinet/components/Store/Inventory/InventoryItem.tsx +++ b/frontend/src/Cabinet/components/Store/Inventory/InventoryItem.tsx @@ -170,8 +170,7 @@ const ItemIconStyled = styled.div<{ itemType: StoreItemType }>` & > svg > path { stroke: var(--sys-main-color); - stroke-width: ${(props) => - props.itemType === StoreItemType.EXTENSION ? "2.8px" : "1.5px"}; + stroke-width: "1.5px"; } `; diff --git a/frontend/src/Cabinet/components/Store/ItemUsageLog/ItemLogBlock.tsx b/frontend/src/Cabinet/components/Store/ItemUsageLog/ItemLogBlock.tsx index b82eacb00..b6a2fae71 100644 --- a/frontend/src/Cabinet/components/Store/ItemUsageLog/ItemLogBlock.tsx +++ b/frontend/src/Cabinet/components/Store/ItemUsageLog/ItemLogBlock.tsx @@ -50,14 +50,13 @@ const IconBlockStyled = styled.div<{ itemName: string }>` svg { width: 40px; height: 40px; + transform-origin: center; } - & > svg > path { + & > svg > * { + transform: scale(1.25); stroke: var(--white-text-with-bg-color); - stroke-width: ${(props) => - props.itemName === ItemTypeLabelMap[StoreItemType.EXTENSION] - ? "3px" - : "1.5px"}; + stroke-width: "1.5px"; } `; diff --git a/frontend/src/Cabinet/components/Store/StoreCoinPick.tsx b/frontend/src/Cabinet/components/Store/StoreCoinPick.tsx index 52c130ea8..4bbaddf56 100644 --- a/frontend/src/Cabinet/components/Store/StoreCoinPick.tsx +++ b/frontend/src/Cabinet/components/Store/StoreCoinPick.tsx @@ -14,10 +14,12 @@ const StoreCoinPick = () => { height={"320px"} > <> - + + +

누군가가 매일 흘리는 동전을 주워보세요💰

-

동전은 하루에 한 번씩 획득할 수 있습니다

+

동전은 하루에 한 번씩 주울 수 있습니다

toggleStore()}> 동전 주우러가기 @@ -27,6 +29,11 @@ const StoreCoinPick = () => { ); }; +const CoinAnimationStyled = styled.div` + min-width: 140px; + min-height: 140px; +`; + const CoinSummary = styled.div` background-color: var(--card-content-bg-color); font-size: var(--size-base); diff --git a/frontend/src/Cabinet/components/UserCabinetInfoArea/UserCabinetInfoArea.tsx b/frontend/src/Cabinet/components/UserCabinetInfoArea/UserCabinetInfoArea.tsx index 67840970f..10071a05d 100644 --- a/frontend/src/Cabinet/components/UserCabinetInfoArea/UserCabinetInfoArea.tsx +++ b/frontend/src/Cabinet/components/UserCabinetInfoArea/UserCabinetInfoArea.tsx @@ -59,7 +59,7 @@ const UserCabinetInfoArea: React.FC<{ - + {selectedUserInfo.name} diff --git a/frontend/src/Cabinet/constants/StatusCode.ts b/frontend/src/Cabinet/constants/StatusCode.ts deleted file mode 100644 index 6452cf27b..000000000 --- a/frontend/src/Cabinet/constants/StatusCode.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const STATUS_400_BAD_REQUEST = 400; -export const STATUS_401_UNAUTHORIZED = 401; diff --git a/frontend/src/Cabinet/hooks/useClubInfo.ts b/frontend/src/Cabinet/hooks/useClubInfo.ts index 152676f21..35e47e52e 100644 --- a/frontend/src/Cabinet/hooks/useClubInfo.ts +++ b/frontend/src/Cabinet/hooks/useClubInfo.ts @@ -1,3 +1,4 @@ +import { HttpStatusCode } from "axios"; import { useEffect, useRef, useState } from "react"; import { useRecoilState, useSetRecoilState } from "recoil"; import { @@ -11,7 +12,6 @@ import { } from "@/Cabinet/types/dto/club.dto"; import { axiosGetClubInfo } from "@/Cabinet/api/axios/axios.custom"; import useMenu from "@/Cabinet/hooks/useMenu"; -import { STATUS_400_BAD_REQUEST } from "@/Cabinet/constants/StatusCode"; const useClubInfo = () => { const [clubState, setClubState] = useState({ clubId: 0, page: 0 }); @@ -54,7 +54,7 @@ const useClubInfo = () => { }, 500); } catch { setTimeout(() => { - setClubInfo(STATUS_400_BAD_REQUEST); + setClubInfo(HttpStatusCode.BadRequest); }, 500); } }; diff --git a/frontend/src/Cabinet/hooks/useMenu.ts b/frontend/src/Cabinet/hooks/useMenu.ts index 1a47e474a..7e0a31949 100644 --- a/frontend/src/Cabinet/hooks/useMenu.ts +++ b/frontend/src/Cabinet/hooks/useMenu.ts @@ -200,7 +200,9 @@ const useMenu = () => { }; const closeUserStore = () => { - if (document.getElementById("itemInfo")?.classList.contains("on") == true) { + if ( + document.getElementById("itemInfo")?.classList.contains("on") === true + ) { document.getElementById("itemInfo")?.classList.remove("on"); } }; diff --git a/frontend/src/Cabinet/pages/AvailablePage.tsx b/frontend/src/Cabinet/pages/AvailablePage.tsx index 9cd4291a4..9ec0be173 100644 --- a/frontend/src/Cabinet/pages/AvailablePage.tsx +++ b/frontend/src/Cabinet/pages/AvailablePage.tsx @@ -29,6 +29,9 @@ const toggleList: toggleItem[] = [ { name: "공유", key: AvailableCabinetsType.SHARE }, ]; +/* TODO: DISABLED_FLOOR 을 환경변수로 넣기 */ +export const DISABLED_FLOOR = ["4"]; + const AvailablePage = () => { const [toggleType, setToggleType] = useState( AvailableCabinetsType.ALL @@ -98,10 +101,6 @@ const AvailablePage = () => { useEffect(() => { deleteRecoilPersistFloorSection(); - setTimeout(() => { - // 새로고침 광클 방지를 위한 초기 로딩 딜레이 - setIsLoaded(true); - }, 500); }, []); useEffect(() => { @@ -156,14 +155,16 @@ const AvailablePage = () => { /> - {isLoaded && cabinets ? ( - Object.entries(cabinets).map(([key, value]) => ( - - )) + {Object.keys(cabinets).length ? ( + Object.entries(cabinets) + .filter(([key, _]) => !DISABLED_FLOOR.includes(key)) + .map(([key, value]) => ( + + )) ) : ( )} diff --git a/frontend/src/Cabinet/pages/ItemUsageLogPage.tsx b/frontend/src/Cabinet/pages/ItemUsageLogPage.tsx index 32a1c2553..455e6d119 100644 --- a/frontend/src/Cabinet/pages/ItemUsageLogPage.tsx +++ b/frontend/src/Cabinet/pages/ItemUsageLogPage.tsx @@ -7,6 +7,7 @@ import { ItemIconMap } from "@/Cabinet/assets/data/maps"; import { ReactComponent as DropdownChevron } from "@/Cabinet/assets/images/dropdownChevron.svg"; import { StoreItemType } from "@/Cabinet/types/enum/store.enum"; import { axiosGetItemUsageHistory } from "@/Cabinet/api/axios/axios.custom"; +import useDebounce from "@/Cabinet/hooks/useDebounce"; const mapItemNameToType = (itemName: string): StoreItemType => { switch (itemName) { @@ -19,7 +20,7 @@ const mapItemNameToType = (itemName: string): StoreItemType => { case "페널티 감면권": return StoreItemType.PENALTY; default: - return StoreItemType.PENALTY; + return StoreItemType.EXTENSION; } }; @@ -55,8 +56,9 @@ const ItemUsageLogPage = () => { const [isMoreBtnLoading, setIsMoreBtnLoading] = useState(true); const [isLoading, setIsLoading] = useState(true); const size = 5; + const { debounce } = useDebounce(); - const getItemUsageLog = async (page: number, size: number) => { + const getItemUsageLog = async () => { try { const data = await axiosGetItemUsageHistory(page, size); const newLogs = createLogEntries(data); @@ -72,13 +74,19 @@ const ItemUsageLogPage = () => { useEffect(() => { setTimeout(() => { - getItemUsageLog(page, size); + getItemUsageLog(); }, 333); }, [page]); const handleMoreClick = () => { - setPage((prev) => prev + 1); setIsMoreBtnLoading(true); + debounce( + "itemUsageLog", + () => { + setPage((prev) => prev + 1); + }, + 333 + ); }; return ( @@ -111,7 +119,6 @@ const ItemUsageLogPage = () => { {isMoreBtnLoading ? ( diff --git a/frontend/src/Cabinet/pages/LogPage.tsx b/frontend/src/Cabinet/pages/LogPage.tsx index d89840775..9d157d0ad 100644 --- a/frontend/src/Cabinet/pages/LogPage.tsx +++ b/frontend/src/Cabinet/pages/LogPage.tsx @@ -1,10 +1,10 @@ +import { HttpStatusCode } from "axios"; import { useEffect, useState } from "react"; import styled from "styled-components"; import LogTable from "@/Cabinet/components/LentLog/LogTable/LogTable"; import { LentHistoryDto } from "@/Cabinet/types/dto/lent.dto"; import { LentLogResponseType } from "@/Cabinet/types/dto/lent.dto"; import { axiosMyLentLog } from "@/Cabinet/api/axios/axios.custom"; -import { STATUS_400_BAD_REQUEST } from "@/Cabinet/constants/StatusCode"; const LogPage = () => { const [lentLog, setLentLog] = useState(undefined); @@ -18,7 +18,7 @@ const LogPage = () => { }, 500); } catch { setTimeout(() => { - setLentLog(STATUS_400_BAD_REQUEST); + setLentLog(HttpStatusCode.BadRequest); }, 500); } }; diff --git a/frontend/src/Cabinet/pages/MainPage.tsx b/frontend/src/Cabinet/pages/MainPage.tsx index 070e57387..c98123f84 100644 --- a/frontend/src/Cabinet/pages/MainPage.tsx +++ b/frontend/src/Cabinet/pages/MainPage.tsx @@ -6,14 +6,16 @@ import { currentBuildingNameState, currentCabinetIdState, currentFloorNumberState, + currentFloorSectionNamesState, currentSectionNameState, isCurrentSectionRenderState, targetCabinetInfoState, } from "@/Cabinet/recoil/atoms"; import { currentFloorSectionState } from "@/Cabinet/recoil/selectors"; +import { DISABLED_FLOOR } from "@/Cabinet/pages/AvailablePage"; import CabinetListContainer from "@/Cabinet/components/CabinetList/CabinetList.container"; import LoadingAnimation from "@/Cabinet/components/Common/LoadingAnimation"; -import SectionAlertModal from "@/Cabinet/components/Modals/SectionAlertModal/SectionAlertModal"; +import SectionAlertModal from "@/Cabinet/components/Modals/StoreModal/SectionAlertModal"; import SectionPaginationContainer from "@/Cabinet/components/SectionPagination/SectionPagination.container"; import { clubSectionsData } from "@/Cabinet/assets/data/mapPositionData"; import { ReactComponent as FilledHeartIcon } from "@/Cabinet/assets/images/filledHeart.svg"; @@ -52,6 +54,9 @@ const MainPage = () => { const [isCurrentSectionRender, setIsCurrentSectionRender] = useRecoilState( isCurrentSectionRenderState ); + const [currentFloorSectionNames] = useRecoilState( + currentFloorSectionNamesState + ); useEffect(() => { if (!currentFloor) { @@ -70,11 +75,9 @@ const MainPage = () => { }, []); useEffect(() => { - const clubSection = clubSectionsData.find((section) => { + const clubSection = !!clubSectionsData.find((section) => { return section === currentSectionName; - }) - ? true - : false; + }); setIsClubSection(clubSection); }, [currentSectionName]); @@ -119,29 +122,25 @@ const MainPage = () => { setIsCurrentSectionRender(false); }; - return ( - <> - {isLoading && } - { - touchStartPosX.current = e.changedTouches[0].screenX; - touchStartPosY.current = e.changedTouches[0].screenY; - }} - onTouchEnd={(e: React.TouchEvent) => { - swipeSection( - e.changedTouches[0].screenX, - e.changedTouches[0].screenY - ); - }} - > - - {!isClubSection && ( + return isLoading ? ( + + ) : ( + { + touchStartPosX.current = e.changedTouches[0].screenX; + touchStartPosY.current = e.changedTouches[0].screenY; + }} + onTouchEnd={(e: React.TouchEvent) => { + swipeSection(e.changedTouches[0].screenX, e.changedTouches[0].screenY); + }} + > + + {currentFloorSectionNames.includes(currentSectionName) && + !isClubSection && ( {sectionList[currentSectionIndex]?.alarmRegistered === true ? ( @@ -150,32 +149,31 @@ const MainPage = () => { )} )} - - - - - {currentSectionName !== SectionType.elevator && - currentSectionName !== SectionType.stairs && ( - - 새로고침 - - )} - - {showSectionAlertModal && ( - - )} - - + + + + + {currentSectionName !== SectionType.elevator && + currentSectionName !== SectionType.stairs && ( + + 새로고침 + + )} + + {showSectionAlertModal && ( + + )} + ); }; @@ -222,7 +220,11 @@ const IconWrapperStyled = styled.div<{ disabled: boolean }>` } `; -const AlertStyled = styled.div` +const AlertStyled = styled.div<{ currentFloor: number }>` + visibility: ${(props) => + DISABLED_FLOOR.includes(props.currentFloor.toString()) + ? "hidden" + : "visible"}; height: 30px; display: flex; justify-content: end; diff --git a/frontend/src/Cabinet/pages/PostLogin.tsx b/frontend/src/Cabinet/pages/PostLogin.tsx index c840badad..ef31dbbd8 100644 --- a/frontend/src/Cabinet/pages/PostLogin.tsx +++ b/frontend/src/Cabinet/pages/PostLogin.tsx @@ -46,6 +46,7 @@ const PostLogin = (): JSX.Element => { let time = setTimeout(() => { navigate("/home"); }, 600); + return () => { clearTimeout(time); }; diff --git a/frontend/src/Cabinet/pages/admin/AdminMainPage.tsx b/frontend/src/Cabinet/pages/admin/AdminMainPage.tsx index de410c157..adc495cb1 100644 --- a/frontend/src/Cabinet/pages/admin/AdminMainPage.tsx +++ b/frontend/src/Cabinet/pages/admin/AdminMainPage.tsx @@ -93,44 +93,43 @@ const AdminMainPage = () => { else moveToRightSection(); }; - return ( - <> - {isLoading && } - { - touchStartPosX.current = e.changedTouches[0].screenX; - touchStartPosY.current = e.changedTouches[0].screenY; - }} - onTouchEnd={(e: React.TouchEvent) => { - swipeSection( - e.changedTouches[0].screenX, - e.changedTouches[0].screenY - ); - }} - > - - - - - - + return isLoading ? ( + + ) : ( + { + touchStartPosX.current = e.changedTouches[0].screenX; + touchStartPosY.current = e.changedTouches[0].screenY; + }} + onTouchEnd={(e: React.TouchEvent) => { + swipeSection( + e.changedTouches[0].screenX, + e.changedTouches[0].screenY + ); + }} + > + + + + + + - - 새로고침 - - - - + + 새로고침 + + + ); }; diff --git a/frontend/src/Cabinet/pages/admin/AdminSlackNotiPage.tsx b/frontend/src/Cabinet/pages/admin/AdminSlackNotiPage.tsx index 97885d4af..0fc2ad296 100644 --- a/frontend/src/Cabinet/pages/admin/AdminSlackNotiPage.tsx +++ b/frontend/src/Cabinet/pages/admin/AdminSlackNotiPage.tsx @@ -1,5 +1,5 @@ import { useRef, useState } from "react"; -import styled from "styled-components"; +import styled, { css } from "styled-components"; import { FailResponseModal, SuccessResponseModal, @@ -16,6 +16,12 @@ import { axiosSendSlackNotificationToUser, } from "@/Cabinet/api/axios/axios.custom"; +const hoverAndClickedBtnStyles = css` + background: var(--capsule-btn-hover-bg-color); + color: var(--sys-main-color); + border: 1px solid var(--sys-main-color); +`; + const AdminSlackNotiPage = () => { const receiverInputRef = useRef(null); const msgTextAreaRef = useRef(null); @@ -24,6 +30,8 @@ const AdminSlackNotiPage = () => { const [modalContent, setModalContent] = useState(""); const [modalTitle, setModalTitle] = useState(""); const [isLoading, setIsLoading] = useState(false); + const [channelBtnIdx, setChannelBtnIdx] = useState(-1); + const [templateBtnIdx, setTemplateBtnIdx] = useState(-1); const renderReceiverInput = (title: string) => { if (receiverInputRef.current) receiverInputRef.current.value = title; @@ -40,6 +48,8 @@ const AdminSlackNotiPage = () => { const initializeInputandTextArea = () => { if (receiverInputRef.current) receiverInputRef.current.value = ""; if (msgTextAreaRef.current) msgTextAreaRef.current.value = ""; + if (channelBtnIdx > -1) setChannelBtnIdx(-1); + if (templateBtnIdx > -1) setTemplateBtnIdx(-1); }; const handleSubmitButton = async () => { @@ -88,7 +98,11 @@ const AdminSlackNotiPage = () => { return ( renderReceiverInput(channel.title)} + onClick={() => { + renderReceiverInput(channel.title); + if (channelBtnIdx !== idx) setChannelBtnIdx(idx); + }} + channelBtnIsClicked={channelBtnIdx === idx} > {channel.title} @@ -104,7 +118,11 @@ const AdminSlackNotiPage = () => { return ( renderTemplateTextArea(template.title)} + onClick={() => { + renderTemplateTextArea(template.title); + if (templateBtnIdx !== idx) setTemplateBtnIdx(idx); + }} + templateBtnIsClicked={templateBtnIdx === idx} > {template.title} @@ -209,7 +227,10 @@ const CapsuleWrappingStyled = styled.div` flex-wrap: wrap; `; -const CapsuleButtonStyled = styled.span` +const CapsuleButtonStyled = styled.span<{ + channelBtnIsClicked?: boolean; + templateBtnIsClicked?: boolean; +}>` display: flex; justify-content: center; align-items: center; @@ -220,10 +241,14 @@ const CapsuleButtonStyled = styled.span` cursor: pointer; :hover { - background: var(--capsule-btn-hover-bg-color); - color: var(--sys-main-color); - border: 1px solid var(--sys-main-color); + ${hoverAndClickedBtnStyles} } + + ${({ channelBtnIsClicked, templateBtnIsClicked }) => + (channelBtnIsClicked || templateBtnIsClicked) && + css` + ${hoverAndClickedBtnStyles} + `} `; const FormWappingStyled = styled.div` diff --git a/frontend/src/Cabinet/types/dto/admin.dto.ts b/frontend/src/Cabinet/types/dto/admin.dto.ts index d3b97ee9b..6fa19f058 100644 --- a/frontend/src/Cabinet/types/dto/admin.dto.ts +++ b/frontend/src/Cabinet/types/dto/admin.dto.ts @@ -1,4 +1,4 @@ -import { STATUS_400_BAD_REQUEST } from "@/Cabinet/constants/StatusCode"; +import { HttpStatusCode } from "axios"; export interface BannedUserDto { userId: number; @@ -78,7 +78,7 @@ export interface ItemLogResponse { export type ItemLogResponseType = | ItemLogResponse - | typeof STATUS_400_BAD_REQUEST + | HttpStatusCode.BadRequest | undefined; export interface IItemUseCountDto { diff --git a/frontend/src/Cabinet/types/dto/club.dto.ts b/frontend/src/Cabinet/types/dto/club.dto.ts index 379462796..eba012415 100644 --- a/frontend/src/Cabinet/types/dto/club.dto.ts +++ b/frontend/src/Cabinet/types/dto/club.dto.ts @@ -1,8 +1,8 @@ -import { STATUS_400_BAD_REQUEST } from "@/Cabinet/constants/StatusCode"; +import { HttpStatusCode } from "axios"; export type ClubListReponseType = | ClubPaginationResponseDto - | typeof STATUS_400_BAD_REQUEST + | HttpStatusCode.BadRequest | undefined; export interface ClubPaginationResponseDto { @@ -18,7 +18,7 @@ export interface ClubResponseDto { export type ClubInfoResponseType = | ClubInfoResponseDto - | typeof STATUS_400_BAD_REQUEST + | HttpStatusCode.BadRequest | undefined; export interface ClubCabinetInfo { diff --git a/frontend/src/Cabinet/types/dto/lent.dto.ts b/frontend/src/Cabinet/types/dto/lent.dto.ts index 05f483c44..23ebf6814 100644 --- a/frontend/src/Cabinet/types/dto/lent.dto.ts +++ b/frontend/src/Cabinet/types/dto/lent.dto.ts @@ -1,4 +1,4 @@ -import { STATUS_400_BAD_REQUEST } from "@/Cabinet/constants/StatusCode"; +import { HttpStatusCode } from "axios"; /** * @interface @@ -31,7 +31,7 @@ export interface LentHistoryDto { export type LentLogResponseType = | LentHistoryDto[] - | typeof STATUS_400_BAD_REQUEST + | HttpStatusCode.BadRequest | undefined; export interface ILentLog { @@ -51,7 +51,7 @@ export interface ClubUserDto { export type ClubLogResponseType = | ClubUserDto[] - | typeof STATUS_400_BAD_REQUEST + | HttpStatusCode.BadRequest | undefined; export interface IClubLog { diff --git a/frontend/src/Cabinet/types/enum/content.status.enum.ts b/frontend/src/Cabinet/types/enum/content.status.enum.ts index cb348bd85..535867bca 100644 --- a/frontend/src/Cabinet/types/enum/content.status.enum.ts +++ b/frontend/src/Cabinet/types/enum/content.status.enum.ts @@ -5,6 +5,8 @@ export enum ContentStatus { PENDING = "PENDING", IN_SESSION = "IN_SESSION", EXTENSION = "EXTENSION", + COIN = "COIN", + STORE = "STORE", } export default ContentStatus; diff --git a/frontend/src/Cabinet/types/enum/error.type.enum.ts b/frontend/src/Cabinet/types/enum/error.type.enum.ts new file mode 100644 index 000000000..f77f8438b --- /dev/null +++ b/frontend/src/Cabinet/types/enum/error.type.enum.ts @@ -0,0 +1,8 @@ +enum ErrorType { + LENT = "LENT", + RETURN = "RETURN", + STORE = "STORE", + INTERNAL_SERVER_ERROR = "INTERNAL_SERVER_ERROR", +} + +export default ErrorType; diff --git a/frontend/src/Presentation/components/LeftNav/LeftMainNav/LeftMainNav.container.tsx b/frontend/src/Presentation/components/LeftNav/LeftMainNav/LeftMainNav.container.tsx index 367f83995..6bebb4647 100644 --- a/frontend/src/Presentation/components/LeftNav/LeftMainNav/LeftMainNav.container.tsx +++ b/frontend/src/Presentation/components/LeftNav/LeftMainNav/LeftMainNav.container.tsx @@ -30,9 +30,9 @@ const LeftMainNavContainer = ({ isAdmin }: { isAdmin?: boolean }) => { const onClickPresentationDetailButton = () => { if (isAdmin) { - navigator("/presentation/detail"); + navigator("/admin/presentation/detail"); } else { - navigator("detail"); + navigator("/presentation/detail"); } closeAll(); }; diff --git a/frontend/src/Presentation/components/PresentationLog/LogTable.tsx b/frontend/src/Presentation/components/PresentationLog/LogTable.tsx index 923beb42e..ba86a64f7 100644 --- a/frontend/src/Presentation/components/PresentationLog/LogTable.tsx +++ b/frontend/src/Presentation/components/PresentationLog/LogTable.tsx @@ -1,7 +1,7 @@ +import { HttpStatusCode } from "axios"; import styled from "styled-components"; import LoadingAnimation from "@/Cabinet/components/Common/LoadingAnimation"; import { formatDate } from "@/Cabinet/utils/dateUtils"; -import { STATUS_400_BAD_REQUEST } from "@/Cabinet/constants/StatusCode"; import { PresentationLocationLabelMap, PresentationStatusTypeLabelMap, @@ -26,7 +26,7 @@ const LogTable = ({ 상태 - {presentationHistory !== STATUS_400_BAD_REQUEST && ( + {presentationHistory !== HttpStatusCode.BadRequest && ( {presentationHistory.map( ( @@ -50,7 +50,7 @@ const LogTable = ({ )} - {presentationHistory === STATUS_400_BAD_REQUEST || + {presentationHistory === HttpStatusCode.BadRequest || (presentationHistory.length === 0 && ( 발표기록이 없습니다. ))} diff --git a/frontend/src/Presentation/pages/LogPage.tsx b/frontend/src/Presentation/pages/LogPage.tsx index 5bbff678c..86b226140 100644 --- a/frontend/src/Presentation/pages/LogPage.tsx +++ b/frontend/src/Presentation/pages/LogPage.tsx @@ -1,6 +1,6 @@ +import { HttpStatusCode } from "axios"; import { useEffect, useState } from "react"; import styled from "styled-components"; -import { STATUS_400_BAD_REQUEST } from "@/Cabinet/constants/StatusCode"; import LogTable from "@/Presentation/components/PresentationLog/LogTable"; import { PresentationHistoryDto, @@ -21,7 +21,7 @@ const PresentationLogPage = () => { }, 500); } catch { setTimeout(() => { - setPresentationLog(STATUS_400_BAD_REQUEST); + setPresentationLog(HttpStatusCode.BadRequest); }, 500); } }; diff --git a/frontend/src/Presentation/types/dto/presentation.dto.ts b/frontend/src/Presentation/types/dto/presentation.dto.ts index 2d995f5c3..390d287a9 100644 --- a/frontend/src/Presentation/types/dto/presentation.dto.ts +++ b/frontend/src/Presentation/types/dto/presentation.dto.ts @@ -1,4 +1,4 @@ -import { STATUS_400_BAD_REQUEST } from "@/Cabinet/constants/StatusCode"; +import { HttpStatusCode } from "axios"; import { PresentationCategoryType, PresentationLocation, @@ -18,7 +18,7 @@ export interface PresentationHistoryDto { export type PresentationHistoryResponseType = | PresentationHistoryDto[] - | typeof STATUS_400_BAD_REQUEST + | HttpStatusCode.BadRequest | undefined; export interface IPresentationInfo { diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 47dddd284..1f4fddbe7 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,17 +1,62 @@ -import React from "react"; +import * as Sentry from "@sentry/react"; +import React, { useEffect } from "react"; import ReactDOM from "react-dom/client"; +import { + createRoutesFromChildren, + matchRoutes, + useLocation, + useNavigationType, +} from "react-router-dom"; import { RecoilRoot } from "recoil"; +import "@/Cabinet/assets/css/media.css"; +import "@/Cabinet/assets/css/reset.css"; +import "@/index.css"; +import App from "@/App"; import { GlobalStyle } from "@/Cabinet/assets/data/ColorTheme"; -import App from "./App"; -import "./Cabinet/assets/css/media.css"; -import "./Cabinet/assets/css/reset.css"; -import "./index.css"; + +Sentry.init({ + dsn: import.meta.env.VITE_SENTRY_DSN, + environment: + import.meta.env.VITE_IS_LOCAL === "true" ? "local" : "production", + release: "^8.18.0", + integrations: [ + // See docs for support of different versions of variation of react router + // https://docs.sentry.io/platforms/javascript/guides/react/configuration/integrations/react-router/ + Sentry.reactRouterV6BrowserTracingIntegration({ + useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + }), + Sentry.replayIntegration(), + ], + + // Set tracesSampleRate to 1.0 to capture 100% + // of transactions for tracing. + tracesSampleRate: 1.0, + + // Set `tracePropagationTargets` to control for which URLs trace propagation should be enabled + // tracePropagationTargets: [/^\//, /^https:\/\/yourserver\.io\/api/], + tracePropagationTargets: [ + "localhost", + /^https:\/\/cabi\.42seoul\.io/, + /^https:\/\/dev\.cabi\.42seoul\.io/, + /^https:\/\/api\.cabi\.42seoul\.io/, + /^https:\/\/api-dev\.cabi\.42seoul\.io/, + ], + + // Capture Replay for 100% of all sessions, + // plus for 100% of sessions with an error + replaysSessionSampleRate: 1.0, + replaysOnErrorSampleRate: 1.0, +}); ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( - // - - - - - // + + + + + + ); diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index d1621437f..7a1007709 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -8,7 +8,19 @@ import { defineConfig } from "vite"; // https://vitejs.dev/config/ export default defineConfig({ plugins: [ - react(), + react({ + babel: { + plugins: [ + [ + "babel-plugin-styled-components", + { + displayName: true, + fileName: false, + }, + ], + ], + }, + }), visualizer({ open: true, gzipSize: true, template: "treemap" }), svgr(), ],