diff --git a/src/main/java/stackpot/stackpot/feed/controller/FeedController.java b/src/main/java/stackpot/stackpot/feed/controller/FeedController.java index 37541fbb..66a3fa68 100644 --- a/src/main/java/stackpot/stackpot/feed/controller/FeedController.java +++ b/src/main/java/stackpot/stackpot/feed/controller/FeedController.java @@ -16,6 +16,7 @@ import stackpot.stackpot.feed.entity.enums.Category; import stackpot.stackpot.feed.service.FeedQueryService; import stackpot.stackpot.feed.service.FeedCommandService; +import stackpot.stackpot.user.dto.response.UserMyPageResponseDto; import java.util.Map; @@ -137,37 +138,7 @@ public ResponseEntity> toggleLike(@PathVariable Long feedId) { "message", isLiked ? "좋아요를 눌렀습니다." : "좋아요를 취소했습니다." ))); } - @GetMapping("/{userId}") - @Operation( - summary = "사용자별 Feed 조회 API", - description = "사용자의 feed를 조회합니다." - ) - public ResponseEntity> getFeedsByUserId( - @Parameter(description = "사용자 ID", example = "1") - @PathVariable("userId") Long userId, - - @Parameter(description = "커서", example = "100", required = false) - @RequestParam(value = "cursor", required = false) Long cursor, - - @Parameter(description = "페이지 크기", example = "10") - @RequestParam(value = "size", defaultValue = "10") int size - ) { - FeedResponseDto.FeedPreviewList feedPreviewList = feedQueryService.getFeedsByUserId(userId, cursor, size); - return ResponseEntity.ok(ApiResponse.onSuccess(feedPreviewList)); - } - - @Operation(summary = "나의 Feed 조회 API") - @GetMapping("/my-feeds") - @ApiErrorCodeExamples({ - ErrorStatus.USER_NOT_FOUND - }) - public ResponseEntity> getFeeds( - @RequestParam(name = "cursor", required = false) Long cursor, - @RequestParam(name = "size", defaultValue = "10") int size) { - FeedResponseDto.FeedPreviewList feedPreviewList = feedQueryService.getFeeds(cursor, size); - return ResponseEntity.ok(ApiResponse.onSuccess(feedPreviewList)); - } @PostMapping("/series") @Operation(summary = "시리즈 생성/삭제 동기화 API", diff --git a/src/main/java/stackpot/stackpot/feed/converter/FeedConverter.java b/src/main/java/stackpot/stackpot/feed/converter/FeedConverter.java index 07587d94..d64cf8f9 100644 --- a/src/main/java/stackpot/stackpot/feed/converter/FeedConverter.java +++ b/src/main/java/stackpot/stackpot/feed/converter/FeedConverter.java @@ -33,7 +33,7 @@ public FeedResponseDto.FeedDto feedDto(Feed feed, Boolean isOwner, Boolean isLik return FeedResponseDto.FeedDto.builder() .feedId(feed.getFeedId()) .writerId(feed.getUser().getId()) - .writer(feed.getUser().getNickname() + " 씨앗") + .writer(feed.getUser().getNickname() + " 새싹") .writerRole(feed.getUser().getRole()) .title(feed.getTitle()) .content(feed.getContent()) @@ -61,7 +61,7 @@ public FeedResponseDto.CreatedFeedDto createFeedDto(Feed feed) { .title(feed.getTitle()) .content(feed.getContent()) .writerId(feed.getUser().getId()) - .writer(feed.getUser().getNickname()+ " 씨앗") + .writer(feed.getUser().getNickname()+ " 새싹") .writerRole(feed.getUser().getRole()) .categories(feed.getCategories().stream() .map(Enum::name) @@ -91,7 +91,7 @@ public FeedSearchResponseDto toSearchDto(Feed feed) { .feedId(feed.getFeedId()) .title(feed.getTitle()) .content(feed.getContent()) - .creatorNickname(feed.getUser().getNickname()+" 씨앗") + .creatorNickname(feed.getUser().getNickname()+" 새싹") .creatorRole(mapRoleName(String.valueOf(feed.getUser().getRole()))) .createdAt(DateFormatter.koreanFormatter(feed.getCreatedAt())) .likeCount(feed.getLikeCount()) // 좋아요 개수 포함 @@ -110,7 +110,7 @@ public FeedResponseDto.AuthorizedFeedDto toAuthorizedFeedDto(Feed feed, boolean FeedResponseDto.CreatedFeedDto createdDto = FeedResponseDto.CreatedFeedDto.builder() .feedId(feed.getFeedId()) .writerId(feed.getUser().getId()) - .writer(feed.getUser().getNickname()+" 씨앗") + .writer(feed.getUser().getNickname()+" 새싹") .writerRole(feed.getUser().getRole()) .title(feed.getTitle()) .content(feed.getContent()) diff --git a/src/main/java/stackpot/stackpot/feed/service/FeedQueryService.java b/src/main/java/stackpot/stackpot/feed/service/FeedQueryService.java index 479b1ffb..70d2f0ab 100644 --- a/src/main/java/stackpot/stackpot/feed/service/FeedQueryService.java +++ b/src/main/java/stackpot/stackpot/feed/service/FeedQueryService.java @@ -2,15 +2,16 @@ import stackpot.stackpot.feed.entity.Feed; import stackpot.stackpot.feed.dto.FeedResponseDto; +import stackpot.stackpot.user.dto.response.UserMyPageResponseDto; import java.util.Map; public interface FeedQueryService { FeedResponseDto.FeedPreviewList getPreViewFeeds(String category, String sort, Long cursor, int limit); FeedResponseDto.AuthorizedFeedDto getFeed(Long feedId); - FeedResponseDto.FeedPreviewList getFeedsByUserId(Long userId, Long nextCursor, int pageSize); + UserMyPageResponseDto getFeedsByUserId(Long userId, Long nextCursor, int pageSize); // FeedResponseDto.FeedPreviewList searchByUserIdByKeyword(Long userId, Long nextCursor, int pageSize); - FeedResponseDto.FeedPreviewList getFeeds(Long nextCursor, int pageSize); +UserMyPageResponseDto getFeeds(Long nextCursor, int pageSize); Map getMySeries(); Long getLikeCount(Long feedId); Feed getFeedByFeedId(Long feedId); diff --git a/src/main/java/stackpot/stackpot/feed/service/FeedQueryServiceImpl.java b/src/main/java/stackpot/stackpot/feed/service/FeedQueryServiceImpl.java index 8dc33ca5..46d4cdd4 100644 --- a/src/main/java/stackpot/stackpot/feed/service/FeedQueryServiceImpl.java +++ b/src/main/java/stackpot/stackpot/feed/service/FeedQueryServiceImpl.java @@ -30,6 +30,7 @@ import stackpot.stackpot.feed.repository.SeriesRepository; import stackpot.stackpot.notification.service.NotificationCommandService; import stackpot.stackpot.save.repository.FeedSaveRepository; +import stackpot.stackpot.user.dto.response.UserMyPageResponseDto; import stackpot.stackpot.user.entity.User; import stackpot.stackpot.user.repository.UserRepository; @@ -179,7 +180,7 @@ public FeedResponseDto.AuthorizedFeedDto getFeed(Long feedId) { } @Transactional - public FeedResponseDto.FeedPreviewList getFeedsByUserId(Long userId, Long nextCursor, int pageSize) { + public UserMyPageResponseDto getFeedsByUserId(Long userId, Long nextCursor, int pageSize) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); boolean isAuthenticated = authentication != null && !(authentication instanceof AnonymousAuthenticationToken) @@ -201,13 +202,9 @@ public FeedResponseDto.FeedPreviewList getFeedsByUserId(Long userId, Long nextCu : List.of(); Pageable pageable = PageRequest.of(0, pageSize, Sort.by(Sort.Direction.DESC, "feedId")); - List feeds; - - if (nextCursor == null) { - feeds = feedRepository.findByUser_Id(userId, pageable); - } else { - feeds = feedRepository.findByUserIdAndFeedIdBefore(userId, nextCursor, pageable); - } + List feeds = (nextCursor == null) + ? feedRepository.findByUser_Id(userId, pageable) + : feedRepository.findByUserIdAndFeedIdBefore(userId, nextCursor, pageable); List feedDtos = feeds.stream() .map(feed -> { @@ -220,18 +217,23 @@ public FeedResponseDto.FeedPreviewList getFeedsByUserId(Long userId, Long nextCu }) .collect(Collectors.toList()); - Long nextCursorResult = (!feeds.isEmpty() && feeds.size() >= pageSize) - ? feeds.get(feeds.size() - 1).getFeedId() - : null; + // 조회 대상 유저 정보 + User targetUser = userRepository.findById(userId) + .orElseThrow(() -> new UserHandler(ErrorStatus.USER_NOT_FOUND)); - return FeedResponseDto.FeedPreviewList.builder() + List seriesComments = targetUser.getSeriesList().stream() + .map(Series::getComment) + .collect(Collectors.toList()); + + return UserMyPageResponseDto.builder() + .id(targetUser.getId()) + .seriesComments(seriesComments) .feeds(feedDtos) - .nextCursor(nextCursorResult) .build(); } @Override - public FeedResponseDto.FeedPreviewList getFeeds(Long nextCursor, int pageSize) { + public UserMyPageResponseDto getFeeds(Long nextCursor, int pageSize) { User user = authService.getCurrentUser(); Pageable pageable = PageRequest.of(0, pageSize, Sort.by(Sort.Direction.DESC, "feedId")); @@ -253,13 +255,14 @@ public FeedResponseDto.FeedPreviewList getFeeds(Long nextCursor, int pageSize) { }) .collect(Collectors.toList()); - Long nextCursorResult = (!feeds.isEmpty() && feeds.size() >= pageSize) - ? feeds.get(feeds.size() - 1).getFeedId() - : null; + List seriesComments = user.getSeriesList().stream() + .map(Series::getComment) + .collect(Collectors.toList()); - return FeedResponseDto.FeedPreviewList.builder() + return UserMyPageResponseDto.builder() + .id(user.getId()) + .seriesComments(seriesComments) .feeds(feedDtos) - .nextCursor(nextCursorResult) .build(); } diff --git a/src/main/java/stackpot/stackpot/pot/controller/PotController.java b/src/main/java/stackpot/stackpot/pot/controller/PotController.java index 26bc1ef6..1c2deb07 100644 --- a/src/main/java/stackpot/stackpot/pot/controller/PotController.java +++ b/src/main/java/stackpot/stackpot/pot/controller/PotController.java @@ -21,6 +21,7 @@ import java.util.List; import java.util.Map; +import java.util.stream.Collectors; @Tag(name = "Pot Management", description = "팟 관리 API") @RestController @@ -69,13 +70,12 @@ public ResponseEntity>> @Operation( summary = "모든 팟 조회 API", description = """ - - Role: FRONTEND / BACKEND / DESIGN / PLANNING / (NULL) - 만약 null인 경우 모든 role에 대해서 조회합니다. - - onlyMine: true인 경우 로그인한 사용자가 만든 팟 중 모집 중(RECRUITING)인 팟만 조회합니다. - false 또는 null인 경우 전체 팟 목록에서 조건에 맞는 팟을 조회합니다.""" +- recruitmentRoles: 여러 역할(FRONTEND, BACKEND 등)을 리스트로 받습니다. + ex) /api/pots?recruitmentRoles=BACKEND&recruitmentRoles=FRONTEND +- onlyMine: true인 경우 로그인한 사용자가 만든 팟 중 모집 중(RECRUITING)인 팟만 조회합니다.""" ) public ResponseEntity>> getPots( - @RequestParam(required = false) String recruitmentRole, + @RequestParam(required = false) List recruitmentRoles, @RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer size, @RequestParam(required = false) Boolean onlyMine) { @@ -84,12 +84,14 @@ public ResponseEntity>> getPots( throw new EnumHandler(ErrorStatus.INVALID_PAGE); } - Role roleEnum = null; - if (recruitmentRole != null && !recruitmentRole.isEmpty()) { - roleEnum = Role.valueOf(recruitmentRole.trim().toUpperCase()); + List roleEnums = null; + if (recruitmentRoles != null && !recruitmentRoles.isEmpty()) { + roleEnums = recruitmentRoles.stream() + .map(role -> Role.valueOf(role.trim().toUpperCase())) + .collect(Collectors.toList()); } - Map response = potQueryService.getAllPotsWithPaging(roleEnum, page, size, onlyMine); + Map response = potQueryService.getAllPotsWithPaging(roleEnums, page, size, onlyMine); return ResponseEntity.ok(ApiResponse.onSuccess(response)); } diff --git a/src/main/java/stackpot/stackpot/pot/converter/PotApplicationConverter.java b/src/main/java/stackpot/stackpot/pot/converter/PotApplicationConverter.java index 2499226e..59621ad4 100644 --- a/src/main/java/stackpot/stackpot/pot/converter/PotApplicationConverter.java +++ b/src/main/java/stackpot/stackpot/pot/converter/PotApplicationConverter.java @@ -45,7 +45,7 @@ public PotApplicationResponseDto toDto(PotApplication entity) { .applicationId(entity.getApplicationId()) .potRole(roleInfo) .userId(entity.getUser().getId()) - .userNickname(entity.getUser().getNickname() + " 씨앗") + .userNickname(entity.getUser().getNickname() + " 새싹") .build(); } diff --git a/src/main/java/stackpot/stackpot/pot/converter/PotConverter.java b/src/main/java/stackpot/stackpot/pot/converter/PotConverter.java index 9891721b..a0acc7b7 100644 --- a/src/main/java/stackpot/stackpot/pot/converter/PotConverter.java +++ b/src/main/java/stackpot/stackpot/pot/converter/PotConverter.java @@ -72,7 +72,7 @@ public PotPreviewResponseDto toPrviewDto(User user, Pot pot, List recrui return PotPreviewResponseDto.builder() .userId(user.getId()) .userRole(user.getRole().name()) - .userNickname(user.getNickname() + " 씨앗") + .userNickname(user.getNickname() + " 새싹") .potId(pot.getPotId()) .potName(pot.getPotName()) .potContent(pot.getPotContent()) diff --git a/src/main/java/stackpot/stackpot/pot/converter/PotDetailConverter.java b/src/main/java/stackpot/stackpot/pot/converter/PotDetailConverter.java index f7870257..201a72b0 100644 --- a/src/main/java/stackpot/stackpot/pot/converter/PotDetailConverter.java +++ b/src/main/java/stackpot/stackpot/pot/converter/PotDetailConverter.java @@ -40,7 +40,7 @@ public PotDetailResponseDto toPotDetailResponseDto(User user, Pot pot, String re return PotDetailResponseDto.builder() .userId(user.getId()) .userRole(user.getRole().name()) - .userNickname(user.getNickname() + " 씨앗") + .userNickname(user.getNickname() + " 새싹") .isOwner(isOwner) .potId(pot.getPotId()) .potName(pot.getPotName()) diff --git a/src/main/java/stackpot/stackpot/pot/repository/PotRepository.java b/src/main/java/stackpot/stackpot/pot/repository/PotRepository.java index 42b14aa6..5dfea427 100644 --- a/src/main/java/stackpot/stackpot/pot/repository/PotRepository.java +++ b/src/main/java/stackpot/stackpot/pot/repository/PotRepository.java @@ -65,14 +65,16 @@ public interface PotRepository extends JpaRepository { "ORDER BY COUNT(pa.applicationId) DESC, p.createdAt DESC") Page findAllOrderByApplicantsCountDesc(Pageable pageable); - /// 특정 Role을 기준으로 지원자 수 많은 순 정렬 + /// 여러 Role을 기준으로 지원자 수 많은 순 정렬 @Query("SELECT p FROM Pot p " + "LEFT JOIN PotRecruitmentDetails prd ON p = prd.pot " + "LEFT JOIN PotApplication pa ON p = pa.pot " + - "WHERE prd.recruitmentRole = :recruitmentRole " + + "WHERE prd.recruitmentRole IN :roles " + "GROUP BY p " + "ORDER BY COUNT(pa.applicationId) DESC, p.createdAt DESC") - Page findByRecruitmentRoleOrderByApplicantsCountDesc(@Param("recruitmentRole") Role recruitmentRole, Pageable pageable); + Page findByRecruitmentRolesInOrderByApplicantsCountDesc(@Param("roles") List roles, Pageable pageable); List findByPotMembers_UserIdOrderByCreatedAtDesc(Long userId); + + } diff --git a/src/main/java/stackpot/stackpot/pot/service/pot/PotQueryService.java b/src/main/java/stackpot/stackpot/pot/service/pot/PotQueryService.java index f596229b..93c4fb1b 100644 --- a/src/main/java/stackpot/stackpot/pot/service/pot/PotQueryService.java +++ b/src/main/java/stackpot/stackpot/pot/service/pot/PotQueryService.java @@ -15,7 +15,7 @@ public interface PotQueryService { List getAppliedPots(); PotSummaryResponseDTO getPotSummary(Long potId); CursorPageResponse getUserCompletedPots(Long userId, Long cursor, int size); - Map getAllPotsWithPaging(Role role, int page, int size, Boolean onlyMine); + Map getAllPotsWithPaging(List roles, int page, int size, Boolean onlyMine); Map getMyRecruitingPotsWithPaging(Integer page, Integer size); Pot getPotByPotId(Long potId); } diff --git a/src/main/java/stackpot/stackpot/pot/service/pot/PotQueryServiceImpl.java b/src/main/java/stackpot/stackpot/pot/service/pot/PotQueryServiceImpl.java index cde3492f..50928554 100644 --- a/src/main/java/stackpot/stackpot/pot/service/pot/PotQueryServiceImpl.java +++ b/src/main/java/stackpot/stackpot/pot/service/pot/PotQueryServiceImpl.java @@ -246,7 +246,7 @@ public CursorPageResponse getUserCompletedPots(Long use } @Override - public Map getAllPotsWithPaging(Role role, int page, int size, Boolean onlyMine) { + public Map getAllPotsWithPaging(List roles, int page, int size, Boolean onlyMine) { User user = null; // 로그인 여부와 무관하게 인증된 사용자인 경우 user 정보 가져오기 @@ -265,9 +265,11 @@ public Map getAllPotsWithPaging(Role role, int page, int size, B if (onlyMine != null && onlyMine && user != null) { potPage = potRepository.findByUserIdAndPotStatus(user.getId(), "RECRUITING", pageable); } else { - potPage = (role == null) - ? potRepository.findAllOrderByApplicantsCountDesc(pageable) - : potRepository.findByRecruitmentRoleOrderByApplicantsCountDesc(role, pageable); + if (roles == null || roles.isEmpty()) { + potPage = potRepository.findAllOrderByApplicantsCountDesc(pageable); + } else { + potPage = potRepository.findByRecruitmentRolesInOrderByApplicantsCountDesc(roles, pageable); + } } List pots = potPage.getContent(); @@ -285,11 +287,10 @@ public Map getAllPotsWithPaging(Role role, int page, int size, B ? potMemberRepository.findPotIdsByUserIdAndPotIds(user.getId(), potIds) : Collections.emptySet(); - List content = pots.stream() .map(pot -> { Long potId = pot.getPotId(); - List roles = pot.getRecruitmentDetails().stream() + List potRoles = pot.getRecruitmentDetails().stream() .map(rd -> String.valueOf(rd.getRecruitmentRole())) .collect(Collectors.toList()); @@ -297,7 +298,7 @@ public Map getAllPotsWithPaging(Role role, int page, int size, B int saveCount = potSaveCountMap.getOrDefault(potId, 0); boolean isMember = memberPotIds.contains(potId); - return potConverter.toPrviewDto(pot.getUser(), pot, roles, isSaved, saveCount, isMember); + return potConverter.toPrviewDto(pot.getUser(), pot, potRoles, isSaved, saveCount, isMember); }) .collect(Collectors.toList()); @@ -310,7 +311,6 @@ public Map getAllPotsWithPaging(Role role, int page, int size, B return response; } - @Override public Pot getPotByPotId(Long potId) { return potRepository.findById(potId).orElseThrow(() -> new PotHandler(ErrorStatus.POT_NOT_FOUND)); diff --git a/src/main/java/stackpot/stackpot/save/repository/FeedSaveRepository.java b/src/main/java/stackpot/stackpot/save/repository/FeedSaveRepository.java index c0d2e1a8..4d4e8c87 100644 --- a/src/main/java/stackpot/stackpot/save/repository/FeedSaveRepository.java +++ b/src/main/java/stackpot/stackpot/save/repository/FeedSaveRepository.java @@ -10,8 +10,7 @@ import stackpot.stackpot.feed.entity.mapping.FeedSave; import stackpot.stackpot.user.entity.User; -import java.util.List; -import java.util.Optional; +import java.util.*; @Repository public interface FeedSaveRepository extends JpaRepository { @@ -30,6 +29,23 @@ public interface FeedSaveRepository extends JpaRepository { @Query("SELECT COUNT(fs) FROM FeedSave fs WHERE fs.feed.feedId = :feedId") long countByFeedId(@Param("feedId") Long feedId); + @Query(""" + SELECT fs.feed.feedId, COUNT(fs) + FROM FeedSave fs + WHERE fs.feed.feedId IN :feedIds + GROUP BY fs.feed.feedId + """) + List countByFeedIdsRaw(@Param("feedIds") List feedIds); + + default Map countByFeedIds(List feedIds) { + if (feedIds == null || feedIds.isEmpty()) return Collections.emptyMap(); + Map map = new HashMap<>(); + for (Object[] row : countByFeedIdsRaw(feedIds)) { + map.put((Long) row[0], (Long) row[1]); + } + return map; + } + } diff --git a/src/main/java/stackpot/stackpot/search/service/SearchServiceImpl.java b/src/main/java/stackpot/stackpot/search/service/SearchServiceImpl.java index 8dabfa58..c8a5c695 100644 --- a/src/main/java/stackpot/stackpot/search/service/SearchServiceImpl.java +++ b/src/main/java/stackpot/stackpot/search/service/SearchServiceImpl.java @@ -53,23 +53,22 @@ public Page searchPots(String keyword, Pageable pageable) .map(Pot::getPotId) .collect(Collectors.toList()); - User user = null; Long userId = null; try { - user = authService.getCurrentUser(); + User user = authService.getCurrentUser(); userId = user.getId(); - } catch (Exception e) { - // 비로그인 - } + } catch (Exception ignored) { /* 비로그인 */ } + // 배치 집계(이미 배치 쿼리 사용 중이라 OK) Map potSaveCountMap = potSaveRepository.countSavesByPotIds(potIds); + + // List -> Set (contains O(1)) Set savedPotIds = (userId != null) - ? potSaveRepository.findPotIdsByUserIdAndPotIds(userId, potIds) + ? new HashSet<>(potSaveRepository.findPotIdsByUserIdAndPotIds(userId, potIds)) : Collections.emptySet(); - // 참여 여부 isMember 확인 Set memberPotIds = (userId != null) - ? potMemberRepository.findPotIdsByUserIdAndPotIds(userId, potIds) + ? new HashSet<>(potMemberRepository.findPotIdsByUserIdAndPotIds(userId, potIds)) : Collections.emptySet(); return pots.map(pot -> { @@ -86,47 +85,50 @@ public Page searchPots(String keyword, Pageable pageable) }); } + @Override @Transactional(readOnly = true) public Page searchFeeds(String keyword, Pageable pageable) { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - boolean isAuthenticated = authentication != null && - !(authentication instanceof AnonymousAuthenticationToken) && - authentication.isAuthenticated(); + var authentication = SecurityContextHolder.getContext().getAuthentication(); + boolean isAuthenticated = authentication != null + && !(authentication instanceof AnonymousAuthenticationToken) + && authentication.isAuthenticated(); + + User user = isAuthenticated ? authService.getCurrentUser() : null; + Long userId = (user != null) ? user.getId() : null; - User user; - Long userId = null; - if (isAuthenticated) { - user = authService.getCurrentUser(); - userId = user.getId(); - } else { - user = null; - } - // 키워드 기반 검색 Page feeds = feedRepository.findByTitleContainingOrContentContainingOrderByCreatedAtDesc( keyword, keyword, pageable ); - // 좋아요/저장 정보 조회 - List likedFeedIds = (userId != null) - ? feedLikeRepository.findFeedIdsByUserId(userId) - : Collections.emptyList(); + // === N+1 제거: 한 번에 집계/조회 === + List feedIds = feeds.getContent().stream() + .map(Feed::getFeedId) + .collect(Collectors.toList()); - List savedFeedIds = (userId != null) - ? feedSaveRepository.findFeedIdsByUserId(userId) - : Collections.emptyList(); + // 저장 수 배치 집계 + Map saveCountMap = feedSaveRepository.countByFeedIds(feedIds); + + // List -> Set (contains O(1)) + Set likedFeedIds = (userId != null) + ? new HashSet<>(feedLikeRepository.findFeedIdsByUserId(userId)) + : Collections.emptySet(); + + Set savedFeedIds = (userId != null) + ? new HashSet<>(feedSaveRepository.findFeedIdsByUserId(userId)) + : Collections.emptySet(); - // 각 Feed를 FeedDto로 변환 return feeds.map(feed -> { boolean isOwner = user != null && Objects.equals(user.getId(), feed.getUser().getId()); - Boolean isLiked = likedFeedIds.contains(feed.getFeedId()); - Boolean isSaved = savedFeedIds.contains(feed.getFeedId()); - int saveCount = feedSaveRepository.countByFeed(feed); + Boolean isLiked = (userId != null) ? likedFeedIds.contains(feed.getFeedId()) : null; + Boolean isSaved = (userId != null) ? savedFeedIds.contains(feed.getFeedId()) : null; + int saveCount = saveCountMap.getOrDefault(feed.getFeedId(), 0L).intValue(); return feedConverter.feedDto(feed, isOwner, isLiked, isSaved, saveCount); }); } + } diff --git a/src/main/java/stackpot/stackpot/user/controller/UserController.java b/src/main/java/stackpot/stackpot/user/controller/UserController.java index 6cb590bb..2cc20af0 100644 --- a/src/main/java/stackpot/stackpot/user/controller/UserController.java +++ b/src/main/java/stackpot/stackpot/user/controller/UserController.java @@ -1,6 +1,7 @@ package stackpot.stackpot.user.controller; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.tags.Tag; @@ -15,6 +16,7 @@ import stackpot.stackpot.apiPayload.ApiResponse; import stackpot.stackpot.apiPayload.code.status.ErrorStatus; import stackpot.stackpot.common.swagger.ApiErrorCodeExamples; +import stackpot.stackpot.feed.service.FeedQueryService; import stackpot.stackpot.pot.dto.CompletedPotRequestDto; import stackpot.stackpot.pot.dto.PotResponseDto; import stackpot.stackpot.pot.service.pot.MyPotService; @@ -26,7 +28,6 @@ import stackpot.stackpot.user.dto.request.UserUpdateRequestDto; import stackpot.stackpot.user.dto.response.*; import stackpot.stackpot.user.entity.enums.Provider; -import stackpot.stackpot.user.entity.enums.Role; import stackpot.stackpot.user.service.UserCommandService; import stackpot.stackpot.user.service.UserQueryService; import stackpot.stackpot.user.service.oauth.GoogleService; @@ -50,7 +51,7 @@ public class UserController { private final MyPotService myPotService; private final PotCommandService potCommandService; private final UserQueryService userQueryService; - + private final FeedQueryService feedQueryService; @GetMapping("/login/token") @Operation( @@ -189,10 +190,10 @@ public ResponseEntity> signup(@Valid @Request @GetMapping("/nickname") @Operation( summary = "닉네임 생성 API", - description = "닉네임 생성 시 역할별로 닉네임이 생성됩니다. 기존 유저와 중복되지 않는 닉네임이 생성됩니다." + description = "새싹 관련 닉네임이 생성됩니다. 기존 유저와 중복되지 않는 닉네임이 생성됩니다." ) - public ResponseEntity> nickname(@RequestParam("role") Role role) { - NicknameResponseDto nickName = userCommandService.createNickname(role); + public ResponseEntity> nickname() { + NicknameResponseDto nickName = userCommandService.createNickname(); return ResponseEntity.ok(ApiResponse.onSuccess(nickName)); } @@ -399,37 +400,46 @@ public ResponseEntity> deleteMyDescription() { return ResponseEntity.noContent().build(); } - //삭제 예정 - @GetMapping("/mypages") + + + @GetMapping("/{userId}/feeds") @Operation( - summary = "나의 마이페이지 조회 API", - description = "토큰을 통해 자신의 [정보 조회 API + 피드 + 끓인 팟] 모두를 제공하는 API로 마이페이지 전체의 정보를 제공하는 API입니다. dataType = pot / feed / (null : pot + feed)" + summary = "사용자별 피드 조회 API", + description = "userId에 해당하는 사용자의 시리즈 코멘트와 피드를 반환합니다. 피드는 커서 기반 페이지네이션을 지원합니다." ) @ApiErrorCodeExamples({ ErrorStatus.USER_NOT_FOUND, - ErrorStatus.USER_ALREADY_WITHDRAWN, + ErrorStatus.USER_ALREADY_WITHDRAWN }) - public ResponseEntity> usersMypages( - @RequestParam(name = "dataType", required = false) String dataType){ - UserMyPageResponseDto userDetails = userCommandService.getMypages(dataType); - return ResponseEntity.ok(ApiResponse.onSuccess(userDetails)); + public ResponseEntity> getFeedsByUserId( + @Parameter(description = "사용자 ID", example = "1") + @PathVariable("userId") Long userId, + + @Parameter(description = "커서", example = "100", required = false) + @RequestParam(value = "cursor", required = false) Long cursor, + + @Parameter(description = "페이지 크기", example = "10") + @RequestParam(value = "size", defaultValue = "10") int size + ) { + UserMyPageResponseDto mypage = feedQueryService.getFeedsByUserId(userId, cursor, size); + return ResponseEntity.ok(ApiResponse.onSuccess(mypage)); } - //삭제 예정 - @GetMapping("/{userId}/mypages") @Operation( - summary = "사용자별 마이페이지 조회 API", - description = "userId를 통해 사용자의 [정보 조회 API + 피드 + 끓인 팟] 모두를 제공하는 API로 마이페이지 전체의 정보를 제공하는 API입니다.\n"+"dataType = pot / feed / (null : pot + feed)" + summary = "나의 피드 조회 API", + description = "로그인한 사용자의 시리즈 코멘트와 피드를 반환합니다. 피드는 커서 기반 페이지네이션을 지원합니다." ) + @GetMapping("/feeds") @ApiErrorCodeExamples({ ErrorStatus.USER_NOT_FOUND, - ErrorStatus.USER_ALREADY_WITHDRAWN, - ErrorStatus._BAD_REQUEST + ErrorStatus.USER_ALREADY_WITHDRAWN }) - public ResponseEntity> getUserMypage( - @PathVariable("userId") Long userId, - @RequestParam(name = "dataType", required = false) String dataType) { - UserMyPageResponseDto response = userCommandService.getUserMypage(userId, dataType); - return ResponseEntity.ok(ApiResponse.onSuccess(response)); + public ResponseEntity> getFeeds( + @RequestParam(name = "cursor", required = false) Long cursor, + @RequestParam(name = "size", defaultValue = "10") int size + ) { + UserMyPageResponseDto mypage = feedQueryService.getFeeds(cursor, size); + return ResponseEntity.ok(ApiResponse.onSuccess(mypage)); } + } diff --git a/src/main/java/stackpot/stackpot/user/converter/UserConverter.java b/src/main/java/stackpot/stackpot/user/converter/UserConverter.java index 8d445520..466e8a5d 100644 --- a/src/main/java/stackpot/stackpot/user/converter/UserConverter.java +++ b/src/main/java/stackpot/stackpot/user/converter/UserConverter.java @@ -18,7 +18,6 @@ public static User toUser(UserRequestDto.JoinDto request) { List interests = request.getInterest(); return User.builder() - .kakaoId(request.getKakaoId()) .interests(interests) .role(Role.valueOf(String.valueOf(request.getRole()))) .build(); @@ -37,10 +36,7 @@ public static UserResponseDto.Userdto toDto(User user) { throw new IllegalStateException("User ID is null"); } - // 역할명을 변환하여 닉네임에 추가 - String roleName = user.getRole() != null ? user.getRole().name() : "멤버"; - String nicknameWithRole = user.getNickname() + " " + Role.toVegetable(roleName); - + String nicknameWithRole = user.getNickname() + " 새싹"; List interests = user.getInterests(); @@ -65,9 +61,7 @@ public static UserResponseDto.UserInfoDto toUserInfo(User user) { throw new IllegalStateException("User ID is null"); } - // 역할명을 변환하여 닉네임에 추가 - String roleName = user.getRole() != null ? user.getRole().name() : "멤버"; - String nicknameWithRole = user.getNickname() + " " + Role.toVegetable(roleName); + String nicknameWithRole = user.getNickname() + " 새싹"; return UserResponseDto.UserInfoDto.builder() .id(user.getId()) diff --git a/src/main/java/stackpot/stackpot/user/converter/UserMypageConverter.java b/src/main/java/stackpot/stackpot/user/converter/UserMypageConverter.java index e5f12210..b13f460d 100644 --- a/src/main/java/stackpot/stackpot/user/converter/UserMypageConverter.java +++ b/src/main/java/stackpot/stackpot/user/converter/UserMypageConverter.java @@ -23,16 +23,13 @@ @Component @RequiredArgsConstructor public class UserMypageConverter { - private final PotMemberRepository potMemberRepository; private final FeedConverter feedConverter; - private final MyPotConverter myPotConverter; - private final PotMemberBadgeRepository potMemberBadgeRepository; private final FeedLikeRepository feedLikeRepository; private final FeedSaveRepository feedSaveRepository; - public UserMyPageResponseDto toDto(User user, List completedPots, List feeds) { + public UserMyPageResponseDto toDto(User user, List feeds) { // 현재 유저가 좋아요 누른 피드 ID 목록 List likedFeedIds = feedLikeRepository.findFeedIdsByUserId(user.getId()); @@ -40,47 +37,16 @@ public UserMyPageResponseDto toDto(User user, List completedPots, List savedFeedIds = feedSaveRepository.findFeedIdsByUserId(user.getId()); + // 시리즈 코멘트 List seriesComments = user.getSeriesList().stream() .map(Series::getComment) .collect(Collectors.toList()); + return UserMyPageResponseDto.builder() .id(user.getId()) - .nickname(user.getNickname() + Role.toVegetable(String.valueOf(user.getRole()))) - .role(user.getRole()) - .userTemperature(user.getUserTemperature()) - .userIntroduction(user.getUserIntroduction()) .seriesComments(seriesComments) - .completedPots(completedPots.stream() - .map(pot -> { - List roleCounts = potMemberRepository.findRoleCountsByPotId(pot.getPotId()); - Map roleCountsMap = roleCounts.stream() - .collect(Collectors.toMap( - roleCount -> ((Role) roleCount[0]).name(), - roleCount -> ((Long) roleCount[1]).intValue() - )); - - String formattedMembers = roleCountsMap.entrySet().stream() - .map(entry -> Role.toKoreanName(entry.getKey()) + "(" + entry.getValue() + ")") - .collect(Collectors.joining(", ")); - Role userPotRole = pot.getUser().getId().equals(user.getId()) ? - pot.getUser().getRole() : - potMemberRepository.findRoleByUserId(pot.getPotId(), user.getId()) - .orElse(pot.getUser().getRole()); - - List myBadges = potMemberBadgeRepository - .findByPotMember_Pot_PotIdAndPotMember_User_Id(pot.getPotId(), user.getId()) - .stream() - .map(potMemberBadge -> new BadgeDto( - potMemberBadge.getBadge().getBadgeId(), - potMemberBadge.getBadge().getName() - )) - .collect(Collectors.toList()); - - return myPotConverter.toCompletedPotBadgeResponseDto(pot, formattedMembers, userPotRole, myBadges); - }) - .collect(Collectors.toList())) .feeds(feeds.stream() .map(feed -> { @@ -94,4 +60,5 @@ public UserMyPageResponseDto toDto(User user, List completedPots, List interest; - - - @Schema(description = "카카오 아이디") - String kakaoId; } } diff --git a/src/main/java/stackpot/stackpot/user/dto/response/UserMyPageResponseDto.java b/src/main/java/stackpot/stackpot/user/dto/response/UserMyPageResponseDto.java index eb8a789f..7b9c7666 100644 --- a/src/main/java/stackpot/stackpot/user/dto/response/UserMyPageResponseDto.java +++ b/src/main/java/stackpot/stackpot/user/dto/response/UserMyPageResponseDto.java @@ -18,21 +18,6 @@ public class UserMyPageResponseDto { @Schema(description = "유저 아이디") private Long id; - @Schema(description = "닉네임") - private String nickname; - - @Schema(description = "역할") - private Role role; - - @Schema(description = "유저 온도") - private Integer userTemperature; - - @Schema(description = "유저 소개") - private String userIntroduction; - - @Schema(description = "끓인 팟") - private List completedPots; - @Schema(description = "시리즈 이름") private List seriesComments; diff --git a/src/main/java/stackpot/stackpot/user/entity/enums/Role.java b/src/main/java/stackpot/stackpot/user/entity/enums/Role.java index 8743e772..dc356dc1 100644 --- a/src/main/java/stackpot/stackpot/user/entity/enums/Role.java +++ b/src/main/java/stackpot/stackpot/user/entity/enums/Role.java @@ -4,11 +4,11 @@ @Getter public enum Role { - DEFAULT("씨앗", "씨앗"), BACKEND("양파", "백엔드"), FRONTEND("버섯", "프론트엔드"), DESIGN("브로콜리", "디자인"), PLANNING("당근", "기획"), + DEFAULT("새싹", "새싹"), UNKNOWN("UNKNOWN", "알 수 없음"); private final String vegetable; diff --git a/src/main/java/stackpot/stackpot/user/service/UserCommandService.java b/src/main/java/stackpot/stackpot/user/service/UserCommandService.java index 5f9b28bf..d917bf36 100644 --- a/src/main/java/stackpot/stackpot/user/service/UserCommandService.java +++ b/src/main/java/stackpot/stackpot/user/service/UserCommandService.java @@ -15,13 +15,13 @@ public interface UserCommandService { UserResponseDto.UserInfoDto getMyUsers(); UserResponseDto.UserInfoDto getUsers(Long UserId); - UserMyPageResponseDto getMypages(String dataType); + UserMyPageResponseDto getMypages(); - UserMyPageResponseDto getUserMypage(Long userId, String dataType); + UserMyPageResponseDto getUserMypage(Long userId); UserResponseDto.Userdto updateUserProfile(UserUpdateRequestDto requestDto); - NicknameResponseDto createNickname(Role role); + NicknameResponseDto createNickname(); TokenServiceResponse saveNickname(String nickname); diff --git a/src/main/java/stackpot/stackpot/user/service/UserCommandServiceImpl.java b/src/main/java/stackpot/stackpot/user/service/UserCommandServiceImpl.java index aea4885e..e9f27bca 100644 --- a/src/main/java/stackpot/stackpot/user/service/UserCommandServiceImpl.java +++ b/src/main/java/stackpot/stackpot/user/service/UserCommandServiceImpl.java @@ -85,7 +85,6 @@ public UserSignUpResponseDto joinUser(UserRequestDto.JoinDto request) { tempUser.setInterest(request.getInterest()); tempUser.setRole(request.getRole()); - tempUser.setKakaoId(request.getKakaoId()); tempUserRepository.save(tempUser); @@ -167,45 +166,35 @@ public UserResponseDto.UserInfoDto getUsers(Long UserId) { } - public UserMyPageResponseDto getMypages(String dataType) { + public UserMyPageResponseDto getMypages() { User user = authService.getCurrentUser(); - //탈퇴한 사용자 - if(user.getRole() == Role.UNKNOWN){ - log.error("탈퇴한 유저에 대한 요청입니다. {}",user.getUserId()); + // 탈퇴한 사용자 + if (user.getRole() == Role.UNKNOWN) { + log.error("탈퇴한 유저에 대한 요청입니다. {}", user.getUserId()); throw new UserHandler(ErrorStatus.USER_ALREADY_WITHDRAWN); } - return getMypageByUser(user.getId(), dataType); + return getMypageByUser(user.getId()); } - public UserMyPageResponseDto getUserMypage(Long userId, String dataType) { - return getMypageByUser(userId, dataType); + public UserMyPageResponseDto getUserMypage(Long userId) { + return getMypageByUser(userId); } - private UserMyPageResponseDto getMypageByUser(Long userId, String dataType) { - List completedPots = List.of(); - List feeds = List.of(); - + private UserMyPageResponseDto getMypageByUser(Long userId) { User user = userRepository.findById(userId) .orElseThrow(() -> new UserHandler(ErrorStatus.USER_NOT_FOUND)); - if(user.getRole() == Role.UNKNOWN){ + if (user.getRole() == Role.UNKNOWN) { throw new UserHandler(ErrorStatus.USER_ALREADY_WITHDRAWN); } - if (dataType == null || dataType.isBlank()) { - completedPots = potRepository.findByUserIdAndPotStatus(userId, "COMPLETED"); - feeds = feedRepository.findByUser_Id(userId); - } else if ("pot".equalsIgnoreCase(dataType)) { - completedPots = potRepository.findByUserIdAndPotStatus(userId, "COMPLETED"); - } else if ("feed".equalsIgnoreCase(dataType)) { - feeds = feedRepository.findByUser_Id(userId); - } else { - log.error("pot, feed의 요청이 잘 못 되었습니다."); - throw new GeneralException(ErrorStatus._BAD_REQUEST); - } - return userMypageConverter.toDto(user, completedPots, feeds); + // Feed만 조회 + List feeds = feedRepository.findByUser_Id(userId); + + + return userMypageConverter.toDto(user, feeds); } @@ -237,12 +226,12 @@ public UserResponseDto.Userdto updateUserProfile(UserUpdateRequestDto requestDto @Override @Transactional - public NicknameResponseDto createNickname(Role role) { + public NicknameResponseDto createNickname() { String nickname; while (true) { // 닉네임 생성 - String prompt = getPromptByRole(role); + String prompt = getPromptForNewbie(); nickname = potSummarizationService.summarizeText(prompt, 15); // 중복 검사 @@ -254,7 +243,7 @@ public NicknameResponseDto createNickname(Role role) { log.debug("사용중인 닉네임 입니다.{}", nickname); } } - return new NicknameResponseDto(nickname + " " + Role.toVegetable(role.toString())); + return new NicknameResponseDto(nickname + " 새싹"); } @Override @@ -501,119 +490,24 @@ public String logout(String aToken, String refreshToken) { return "로그아웃이 성공적으로 완료되었습니다."; } - private String getPromptByRole(Role role) { - switch (role) { - case BACKEND: - return "너는 백엔드 개발자 전용 닉네임 생성기야. " + - "백엔드 개발자는 서버를 다루고, API를 만들고, 때때로 버그와 싸우는 존재야. " + - "이들의 일상적인 특성과 분위기를 반영한 닉네임을 만들어줘. " + - "닉네임은 반드시 **수식어만 포함**해야 하고, **뒤에는 어떤 단어도 붙이면 안 돼.** " + - "**특히 '버섯', '브로콜리', '양파', '당근'을 수식어 끝에 붙이지 마.** " + - "닉네임은 15자 이내여야 하고, 특수문자와 숫자는 포함하지 마. " + - "예시:\n" + - "- 묵묵한 해결사\n" + - "- 밤을 지배하는\n" + - "- 디버깅 철학자\n" + - "- 재부팅이 답이야\n" + - "- 커피로 살아가는\n" + - "- 감자칩과 코드\n" + - "- 무한 루프 탈출러\n" + - "- 배포의 순간 떨리는\n" + - "- 예상치 못한 오류의 친구\n" + - "- 로그에 의존하는 삶\n" + - "- 밤을 지새우는\n" + - "- 코드 속에 사는\n" + - "- 로딩 중인\n" + - "- 서버를 지키는\n" + - "- 로그를 수집하는\n" + - "- 언제나 배포 준비\n" + - "- 메모리 절약 장인\n" + - "- 예외를 쫓는\n" + - "이제 백엔드 개발자의 감성과 특징을 반영한 닉네임 수식어 하나를 만들어줘."; - - case FRONTEND: - return "너는 프론트엔드 개발자 전용 닉네임 생성기야. " + - "프론트엔드 개발자들은 UI/UX, 디자인, 자바스크립트, 반응형 웹을 다루지. " + - "그래서 그들의 일상을 반영한 닉네임을 만들어야 해. " + - "닉네임은 반드시 수식어만 포함해야 하며, 뒤에는 어떤 단어도 붙이면 안 돼. " + - "**절대 '버섯', '브로콜리', '양파', '당근'을 수식어 끝에 붙이지 마.** " + - "닉네임은 15자 이내여야 하고, 특수문자와 숫자는 포함하지 마. " + - "예시:\n" + - "- 픽셀 하나까지 보는\n" + - "- 다크모드를 사랑하는\n" + - "- 마우스를 덜 쓰는\n" + - "- CSS와 함께 춤을\n" + - "- 반응형에 진심인\n" + - "- UI 디테일 집착러\n" + - "- 모니터를 끌 수 없는\n" + - "- 화면을 디자인하는\n" + - "- 색상 코드 장인\n" + - "- 픽셀 맞추기 장인\n" + - "- 감성적인 디버거\n" + - "- 버튼 하나에 2시간\n" + - "- 컬러팔레트 탐험가\n" + - "- 디자인과 현실 사이\n" + - "- 알림창을 사랑하는\n" + - "- CSS 트라우마 보유자\n" + - "- 예쁘면 다 용서됨\n" + - "이제 프론트엔드 개발자의 특징을 반영한 닉네임 수식어 하나를 만들어줘."; - - case DESIGN: - return "너는 디자이너 전용 닉네임 생성기야. " + - "디자이너들은 색감, UI/UX, 레이아웃, 그래픽을 다루지. " + - "그래서 그들의 일상을 반영한 닉네임을 만들어야 해. " + - "닉네임은 반드시 수식어만 포함해야 하며, 뒤에는 어떤 단어도 붙이면 안 돼. " + - "**절대 '버섯', '브로콜리', '양파', '당근'을 수식어 끝에 붙이지 마.** " + - "닉네임은 15자 이내여야 하고, 특수문자와 숫자는 포함하지 마. " + - "예시:\n" + - "- 색감 조합 마법사\n" + - "- 폰트 정렬의 고수\n" + - "- 직관적 레이아웃 탐험가\n" + - "- 감각적인 픽셀러\n" + - "- 그리드 계산 장인\n" + - "- 비율 감각이 뛰어난\n" + - "- 포토샵 브러쉬 장인\n" + - "- 스타일 가이드의 전설\n" + - "- 시각적 균형의 추구자\n" + - "- UI를 사랑하는\n" + - "이제 디자이너의 특징을 반영한 닉네임 수식어 하나를 만들어줘."; - - case PLANNING: - return "너는 기획자 전용 닉네임 생성기야. " + - "기획자들은 프로젝트 관리, 일정 조율, 요구사항 정의 등을 다루지. " + - "그래서 그들의 일상을 반영한 닉네임을 만들어야 해. " + - "닉네임은 반드시 수식어만 포함해야 하며, 뒤에는 어떤 단어도 붙이면 안 돼. " + - "**절대 '버섯', '브로콜리', '양파', '당근'을 수식어 끝에 붙이지 마.** " + - "닉네임은 15자 이내여야 하고, 특수문자와 숫자는 포함하지 마. " + - "예시:\n" + - "- 일정 조율의 달인\n" + - "- 요구사항 정리왕\n" + - "- 문서 마스터\n" + - "- 회의 천재\n" + - "- 일정 예측가\n" + - "- 스프린트 조종자\n" + - "- 기획 감각 천재\n" + - "- 협업 마스터\n" + - "- 마감 기한을 지키는\n" + - "- 모든 걸 계획하는\n" + - "- 문서를 정리하는\n" + - "- 회의를 조율하는\n" + - "- 협업을 최적화하는\n" + - "- 서비스의 길을 여는\n" + - "- 일정 조율의 달인\n" + - "- 프로젝트를 설계하는\n" + - "- 아이디어를 수집하는\n" + - "- 보고서를 사랑하는\n" + - "이제 기획자의 특징을 반영한 닉네임 수식어 하나를 만들어줘."; - - default: - return "너는 개발자 전용 닉네임 생성기야. " + - "닉네임은 개발자들이 일상에서 자주 겪는 특징, 습관, 성격을 반영한 수식어로 만들어야 해. " + - "닉네임은 반드시 수식어만 포함해야 하며, 뒤에는 어떤 단어도 붙이면 안 돼. " + - "**절대 '버섯', '브로콜리', '양파', '당근'을 수식어 끝에 붙이지 마.** " + - "닉네임은 15자 이내여야 하고, 특수문자와 숫자는 포함하지 마. " + - "이제 개발자의 일상을 반영한 닉네임 수식어 하나를 만들어줘."; - } + private String getPromptForNewbie() { + return "너는 '프로젝트를 시작하고 싶은 사람'을 위한 닉네임 수식어 생성기야. " + + "특히 '새싹'처럼 아직 작지만 앞으로 성장할 가능성이 있는 사람의 감성을 담아야 해. " + + "배움, 도전, 시작, 성장, 열정, 호기심 같은 분위기를 담아서 만들어줘. " + + "닉네임은 반드시 **수식어만 포함**해야 하고, **뒤에는 어떤 단어도 붙이면 안 돼.** " + + "**특히 '버섯', '브로콜리', '양파', '당근', '새싹' 같은 단어는 쓰지 마.** " + + "닉네임은 15자 이내여야 하고, 특수문자와 숫자는 포함하지 마. " + + "예시:\n" + + "- 도전을 즐기는\n" + + "- 배우고 싶은\n" + + "- 아이디어가 자라는\n" + + "- 가능성을 품은\n" + + "- 첫걸음을 내딛는\n" + + "- 매일 조금씩 성장하는\n" + + "- 호기심으로 가득한\n" + + "- 열정이 피어나는\n" + + "- 아직 미완성이지만 빛나는\n" + + "이제 '프로젝트를 시작하고 싶은 새싹'의 감성을 담은 닉네임 수식어 하나를 만들어줘."; } @Transactional