diff --git a/src/main/java/com/campus/campus/domain/councilpost/application/dto/response/GetActivePartnershipListForUserResponse.java b/src/main/java/com/campus/campus/domain/councilpost/application/dto/response/GetActivePartnershipListForUserResponse.java new file mode 100644 index 00000000..ef1091e3 --- /dev/null +++ b/src/main/java/com/campus/campus/domain/councilpost/application/dto/response/GetActivePartnershipListForUserResponse.java @@ -0,0 +1,18 @@ +package com.campus.campus.domain.councilpost.application.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +public record GetActivePartnershipListForUserResponse( + @Schema(description = "제휴글 id", example = "1") + Long postId, + + @Schema(description = "제휴글 이름", example = "투썸 제휴") + String title, + + @Schema(description = "제휴 장소", example = "투썸 플레이스") + String place, + + @Schema(description = "썸네일 image url", example = "https://www.example.com.png") + String thumbnailImageUrl +) { +} diff --git a/src/main/java/com/campus/campus/domain/councilpost/application/dto/response/GetPostListForCouncilResponse.java b/src/main/java/com/campus/campus/domain/councilpost/application/dto/response/GetPostListForCouncilResponse.java new file mode 100644 index 00000000..294333fd --- /dev/null +++ b/src/main/java/com/campus/campus/domain/councilpost/application/dto/response/GetPostListForCouncilResponse.java @@ -0,0 +1,32 @@ +package com.campus.campus.domain.councilpost.application.dto.response; + +import java.time.LocalDateTime; + +import com.campus.campus.domain.councilpost.domain.entity.PostCategory; +import com.campus.campus.domain.councilpost.domain.entity.ThumbnailIcon; + +import io.swagger.v3.oas.annotations.media.Schema; + +public record GetPostListForCouncilResponse( + @Schema(description = "게시글 id", example = "1") + Long postId, + + @Schema(description = "게시글 카테고리", example = "EVENT") + PostCategory category, + + @Schema(description = "게시글 이름", example = "중간고사 간식생사") + String title, + + @Schema(description = "장소", example = "310관 1층") + String place, + + @Schema(description = "시간(끝나는 시간 or 행사날짜", example = "2026-01-10T18:00:00") + LocalDateTime dateTime, + + @Schema(description = "썸네일 image url", example = "https://www.example.com.png") + String thumbnailImageUrl, + + @Schema(description = "썸네일 아이콘", example = "FOOD") + ThumbnailIcon thumbnailIcon +) { +} diff --git a/src/main/java/com/campus/campus/domain/councilpost/application/dto/response/GetUpcomingEventListForCouncilResponse.java b/src/main/java/com/campus/campus/domain/councilpost/application/dto/response/GetUpcomingEventListForCouncilResponse.java new file mode 100644 index 00000000..cd62e6ec --- /dev/null +++ b/src/main/java/com/campus/campus/domain/councilpost/application/dto/response/GetUpcomingEventListForCouncilResponse.java @@ -0,0 +1,29 @@ +package com.campus.campus.domain.councilpost.application.dto.response; + +import java.time.LocalDateTime; + +import com.campus.campus.domain.councilpost.domain.entity.PostCategory; +import com.campus.campus.domain.councilpost.domain.entity.ThumbnailIcon; + +import io.swagger.v3.oas.annotations.media.Schema; + +public record GetUpcomingEventListForCouncilResponse( + @Schema(description = "게시글 id", example = "1") + Long postId, + + @Schema(description = "게시글 카테고리", example = "EVENT") + PostCategory category, + + @Schema(description = "게시글 이름", example = "중간고사 간식생사") + String title, + + @Schema(description = "장소", example = "310관 1층") + String place, + + @Schema(description = "시간(끝나는 시간 or 행사날짜", example = "2026-01-10T18:00:00") + LocalDateTime dateTime, + + @Schema(description = "썸네일 아이콘", example = "FOOD") + ThumbnailIcon thumbnailIcon +) { +} diff --git a/src/main/java/com/campus/campus/domain/councilpost/application/mapper/StudentCouncilPostMapper.java b/src/main/java/com/campus/campus/domain/councilpost/application/mapper/StudentCouncilPostMapper.java index 29c7ae89..895f99ef 100644 --- a/src/main/java/com/campus/campus/domain/councilpost/application/mapper/StudentCouncilPostMapper.java +++ b/src/main/java/com/campus/campus/domain/councilpost/application/mapper/StudentCouncilPostMapper.java @@ -7,6 +7,9 @@ import org.springframework.stereotype.Component; import com.campus.campus.domain.council.domain.entity.StudentCouncil; +import com.campus.campus.domain.councilpost.application.dto.response.GetPostListForCouncilResponse; +import com.campus.campus.domain.councilpost.application.dto.response.GetUpcomingEventListForCouncilResponse; +import com.campus.campus.domain.councilpost.application.dto.response.GetActivePartnershipListForUserResponse; import com.campus.campus.domain.councilpost.application.dto.response.PostListItemResponse; import com.campus.campus.domain.councilpost.application.dto.request.PostRequest; import com.campus.campus.domain.councilpost.application.dto.response.PostResponse; @@ -19,7 +22,7 @@ @RequiredArgsConstructor public class StudentCouncilPostMapper { - public PostListItemResponse toPostListItemResponse(StudentCouncilPost post, Long currentUserId) { + public PostListItemResponse toPostListItemResponse(StudentCouncilPost post, Long councilId) { return new PostListItemResponse( post.getId(), post.getCategory(), @@ -30,7 +33,39 @@ public PostListItemResponse toPostListItemResponse(StudentCouncilPost post, Long : post.getEndDateTime(), post.getThumbnailImageUrl(), post.getThumbnailIcon(), - post.isWrittenByCouncil(currentUserId) + post.isWrittenByCouncil(councilId) + ); + } + + public GetPostListForCouncilResponse toGetPostListForCouncilResponse(StudentCouncilPost post) { + return new GetPostListForCouncilResponse( + post.getId(), + post.getCategory(), + post.getTitle(), + post.getPlace(), + post.isEvent() ? post.getStartDateTime() : post.getEndDateTime(), + post.getThumbnailImageUrl(), + post.getThumbnailIcon() + ); + } + + public GetUpcomingEventListForCouncilResponse toGetUpcomingEventListForCouncilResponse(StudentCouncilPost post) { + return new GetUpcomingEventListForCouncilResponse( + post.getId(), + post.getCategory(), + post.getTitle(), + post.getPlace(), + post.getStartDateTime(), + post.getThumbnailIcon() + ); + } + + public GetActivePartnershipListForUserResponse toGetActivePartnershipListForUserResponse(StudentCouncilPost post) { + return new GetActivePartnershipListForUserResponse( + post.getId(), + post.getTitle(), + post.getPlace(), + post.getThumbnailImageUrl() ); } @@ -59,7 +94,8 @@ public PostResponse toPostResponse(StudentCouncilPost post, List images, return builder.build(); } - public StudentCouncilPost createStudentCouncilPost(StudentCouncil writer, PostRequest dto, LocalDateTime startDateTime, + public StudentCouncilPost createStudentCouncilPost(StudentCouncil writer, PostRequest dto, + LocalDateTime startDateTime, LocalDateTime endDateTime) { return StudentCouncilPost.builder() .writer(writer) diff --git a/src/main/java/com/campus/campus/domain/councilpost/application/service/UserPostService.java b/src/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostForUserService.java similarity index 84% rename from src/main/java/com/campus/campus/domain/councilpost/application/service/UserPostService.java rename to src/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostForUserService.java index b4d1045c..f5f788bc 100644 --- a/src/main/java/com/campus/campus/domain/councilpost/application/service/UserPostService.java +++ b/src/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostForUserService.java @@ -12,6 +12,7 @@ import org.springframework.transaction.annotation.Transactional; import com.campus.campus.domain.council.domain.entity.CouncilType; +import com.campus.campus.domain.councilpost.application.dto.response.GetActivePartnershipListForUserResponse; import com.campus.campus.domain.councilpost.application.dto.response.PostListItemResponse; import com.campus.campus.domain.councilpost.application.dto.response.PostResponse; import com.campus.campus.domain.councilpost.application.exception.CollegeNotSetException; @@ -34,7 +35,8 @@ @Service @RequiredArgsConstructor @Slf4j -public class UserPostService { +@Transactional(readOnly = true) +public class StudentCouncilPostForUserService { private static final int UPCOMING_HOURS = 72; private static final ZoneId KST = ZoneId.of("Asia/Seoul"); @@ -44,7 +46,6 @@ public class UserPostService { private final UserRepository userRepository; private final PostAccessPolicy postAccessPolicy; - @Transactional(readOnly = true) public Page findSchoolPosts(PostCategory category, int page, int size, Long userId) { User user = userRepository.findByIdWithAcademicInfo(userId).orElseThrow(UserNotFoundException::new); @@ -56,7 +57,6 @@ public Page findSchoolPosts(PostCategory category, int pag return posts.map(post -> studentCouncilPostMapper.toPostListItemResponse(post, userId)); } - @Transactional(readOnly = true) public Page findCollegePosts(PostCategory category, int page, int size, Long userId) { User user = userRepository.findByIdWithAcademicInfo(userId).orElseThrow(UserNotFoundException::new); @@ -72,7 +72,6 @@ public Page findCollegePosts(PostCategory category, int pa return posts.map(post -> studentCouncilPostMapper.toPostListItemResponse(post, userId)); } - @Transactional(readOnly = true) public Page findMajorPosts(PostCategory category, int page, int size, Long userId) { User user = userRepository.findByIdWithAcademicInfo(userId).orElseThrow(UserNotFoundException::new); @@ -84,7 +83,6 @@ public Page findMajorPosts(PostCategory category, int page return posts.map(post -> studentCouncilPostMapper.toPostListItemResponse(post, userId)); } - @Transactional(readOnly = true) public Page findUpcomingSchoolEvents72h(int page, int size, Long userId) { User user = userRepository.findByIdWithAcademicInfo(userId).orElseThrow(UserNotFoundException::new); @@ -105,7 +103,6 @@ public Page findUpcomingSchoolEvents72h(int page, int size return posts.map(post -> studentCouncilPostMapper.toPostListItemResponse(post, userId)); } - @Transactional(readOnly = true) public Page findUpcomingCollegeEvents72h(int page, int size, Long userId) { User user = userRepository.findByIdWithAcademicInfo(userId).orElseThrow(UserNotFoundException::new); @@ -134,7 +131,6 @@ public Page findUpcomingCollegeEvents72h(int page, int siz return posts.map(post -> studentCouncilPostMapper.toPostListItemResponse(post, userId)); } - @Transactional(readOnly = true) public Page findUpcomingMajorEvents72h(int page, int size, Long userId) { User user = userRepository.findByIdWithAcademicInfo(userId).orElseThrow(UserNotFoundException::new); @@ -163,7 +159,6 @@ public Page findUpcomingMajorEvents72h(int page, int size, return posts.map(post -> studentCouncilPostMapper.toPostListItemResponse(post, userId)); } - @Transactional(readOnly = true) public PostResponse findById(Long postId, Long userId) { User user = userRepository.findByIdWithAcademicInfo(userId).orElseThrow(UserNotFoundException::new); @@ -180,4 +175,42 @@ public PostResponse findById(Long postId, Long userId) { return studentCouncilPostMapper.toPostResponse(post, imageUrls, userId); } + + public List findActivePartnershipForUser(CouncilType councilType, + Long userId) { + User user = userRepository.findByIdWithAcademicInfo(userId) + .orElseThrow(UserNotFoundException::new); + + if (user.getSchool() == null) { + return List.of(); + } + + Long schoolId = user.getSchool().getSchoolId(); + Long collegeId = null; + Long majorId = null; + + if (CouncilType.COLLEGE_COUNCIL.equals(councilType)) { + if (user.getCollege() == null) { + return List.of(); + } + collegeId = user.getCollege().getCollegeId(); + } + + if (CouncilType.MAJOR_COUNCIL.equals(councilType)) { + if (user.getMajor() == null) { + return List.of(); + } + majorId = user.getMajor().getMajorId(); + } + + LocalDateTime now = LocalDateTime.now(KST); + Pageable partnershipCount = PageRequest.of(0, 3); + + List partnerships = studentCouncilPostRepository.findRandomActivePartnerships(schoolId, + councilType, PostCategory.PARTNERSHIP, collegeId, majorId, now, partnershipCount); + + return partnerships.stream() + .map(studentCouncilPostMapper::toGetActivePartnershipListForUserResponse) + .toList(); + } } diff --git a/src/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostService.java b/src/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostService.java index 207c06b7..446edee7 100644 --- a/src/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostService.java +++ b/src/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostService.java @@ -12,10 +12,12 @@ import org.springframework.transaction.annotation.Transactional; import com.campus.campus.domain.council.application.exception.StudentCouncilNotFoundException; +import com.campus.campus.domain.council.domain.entity.CouncilType; import com.campus.campus.domain.council.domain.entity.StudentCouncil; import com.campus.campus.domain.council.domain.repository.StudentCouncilRepository; +import com.campus.campus.domain.councilpost.application.dto.response.GetPostListForCouncilResponse; +import com.campus.campus.domain.councilpost.application.dto.response.GetUpcomingEventListForCouncilResponse; import com.campus.campus.domain.councilpost.application.dto.response.NormalizedDateTime; -import com.campus.campus.domain.councilpost.application.dto.response.PostListItemResponse; import com.campus.campus.domain.councilpost.application.dto.request.PostRequest; import com.campus.campus.domain.councilpost.application.dto.response.PostResponse; import com.campus.campus.domain.councilpost.application.exception.NotPostWriterException; @@ -100,34 +102,44 @@ public PostResponse findById(Long postId, Long currentUserId) { } @Transactional(readOnly = true) - public Page findAll(PostCategory category, int page, int size, Long currentUserId) { - Pageable pageable = PageRequest.of(Math.max(page - 1, 0), size, Sort.by(Sort.Direction.DESC, "createdAt")); + public Page findPostListForCouncil(Long councilId, PostCategory category, + int page, int size) { + studentCouncilRepository.findByIdAndManagerApprovedIsTrueAndDeletedAtIsNull(councilId) + .orElseThrow(StudentCouncilNotFoundException::new); - Page posts = (category == null) - ? postRepository.findAll(pageable) - : postRepository.findAllByCategory(category, pageable); + Sort sort; + if (category == PostCategory.EVENT) { + sort = Sort.by(Sort.Direction.ASC, "startDateTime"); + } else { + sort = Sort.by(Sort.Direction.ASC, "endDateTime"); + } - return posts.map(post -> - studentCouncilPostMapper.toPostListItemResponse(post, currentUserId) - ); + Pageable pageable = PageRequest.of(Math.max(page - 1, 0), size, sort); + + LocalDateTime now = LocalDateTime.now(); + + Page posts = postRepository.findPostsByCouncilAndFilters(councilId, category, now, + pageable); + + return posts.map(studentCouncilPostMapper::toGetPostListForCouncilResponse); } @Transactional(readOnly = true) - public Page findUpcomingEvents(int page, int size, Long currentUserId) { - Pageable pageable = PageRequest.of( - Math.max(page - 1, 0), - size, - Sort.by(Sort.Direction.ASC, "startDateTime") - ); + public Page findUpcomingEventsForCouncil(Long councilId, + int page, int size) { + Pageable pageable = PageRequest.of(Math.max(page - 1, 0), size, + Sort.by(Sort.Direction.ASC, "startDateTime")); + + studentCouncilRepository.findByIdAndManagerApprovedIsTrueAndDeletedAtIsNull(councilId) + .orElseThrow(StudentCouncilNotFoundException::new); LocalDateTime now = LocalDateTime.now(); LocalDateTime limit = now.plusHours(UPCOMING_EVENT_WINDOW_HOURS); - Page posts = postRepository.findUpcomingEvents(PostCategory.EVENT, now, limit, pageable); + Page posts = postRepository.findUpcomingEventsByCouncil(councilId, PostCategory.EVENT, + now, limit, pageable); - return posts.map(post -> - studentCouncilPostMapper.toPostListItemResponse(post, currentUserId) - ); + return posts.map(studentCouncilPostMapper::toGetUpcomingEventListForCouncilResponse); } @Transactional diff --git a/src/main/java/com/campus/campus/domain/councilpost/domain/repository/StudentCouncilPostRepository.java b/src/main/java/com/campus/campus/domain/councilpost/domain/repository/StudentCouncilPostRepository.java index 125c59fa..1898f73b 100644 --- a/src/main/java/com/campus/campus/domain/councilpost/domain/repository/StudentCouncilPostRepository.java +++ b/src/main/java/com/campus/campus/domain/councilpost/domain/repository/StudentCouncilPostRepository.java @@ -1,6 +1,7 @@ package com.campus.campus.domain.councilpost.domain.repository; import java.time.LocalDateTime; +import java.util.List; import java.util.Optional; import org.springframework.data.domain.Page; @@ -16,8 +17,6 @@ public interface StudentCouncilPostRepository extends JpaRepository { - Page findAllByCategory(PostCategory category, Pageable pageable); - @Query(""" SELECT p FROM StudentCouncilPost p JOIN FETCH p.writer w @@ -29,19 +28,69 @@ public interface StudentCouncilPostRepository extends JpaRepository findByIdWithFullInfo(@Param("postId") Long postId); + @EntityGraph(attributePaths = {"writer", "writer.school", "writer.college", "writer.major"}) @Query(""" - SELECT p - FROM StudentCouncilPost p - WHERE p.category = :category - AND p.startDateTime BETWEEN :now AND :limit + SELECT p FROM StudentCouncilPost p + JOIN p.writer w + WHERE w.id = :councilId + AND p.category = :category + AND p.startDateTime BETWEEN :now AND :limit + AND w.deletedAt IS NULL + ORDER BY p.startDateTime ASC """) - Page findUpcomingEvents( + Page findUpcomingEventsByCouncil( + @Param("councilId") Long councilId, @Param("category") PostCategory category, @Param("now") LocalDateTime now, @Param("limit") LocalDateTime limit, Pageable pageable ); + @EntityGraph(attributePaths = {"writer", "writer.school", "writer.college", "writer.major"}) + @Query(""" + SELECT p FROM StudentCouncilPost p + JOIN p.writer w + WHERE w.id = :councilId + AND (:category IS NULL OR p.category = :category) + AND ( + (p.category = com.campus.campus.domain.councilpost.domain.entity.PostCategory.EVENT + AND p.startDateTime >= :now) + OR (p.category = com.campus.campus.domain.councilpost.domain.entity.PostCategory.PARTNERSHIP + AND p.endDateTime >= :now) + ) + AND w.deletedAt IS NULL + """) + Page findPostsByCouncilAndFilters(@Param("councilId") Long councilId, + @Param("category") PostCategory category, + @Param("now") LocalDateTime now, + Pageable pageable + ); + + @EntityGraph(attributePaths = {"writer", "writer.school", "writer.college", "writer.major"}) + @Query(""" + SELECT p FROM StudentCouncilPost p + JOIN p.writer w + LEFT JOIN w.college c + LEFT JOIN w.major m + WHERE w.school.schoolId = :schoolId + AND w.councilType = :councilType + AND (:collegeId IS NULL OR c.collegeId = :collegeId) + AND (:majorId IS NULL OR m.majorId = :majorId) + AND p.category = :category + AND :now BETWEEN p.startDateTime AND p.endDateTime + AND w.deletedAt IS NULL + ORDER BY function('RAND') + """) + List findRandomActivePartnerships( + @Param("schoolId") Long schoolId, + @Param("councilType") CouncilType councilType, + @Param("category") PostCategory category, + @Param("collegeId") Long collegeId, + @Param("majorId") Long majorId, + @Param("now") LocalDateTime now, + Pageable pageable + ); + @EntityGraph(attributePaths = {"writer", "writer.school", "writer.college", "writer.major"}) @Query(""" diff --git a/src/main/java/com/campus/campus/domain/councilpost/presentation/StudentCouncilPostController.java b/src/main/java/com/campus/campus/domain/councilpost/presentation/StudentCouncilPostController.java index 70db8fb5..81099218 100644 --- a/src/main/java/com/campus/campus/domain/councilpost/presentation/StudentCouncilPostController.java +++ b/src/main/java/com/campus/campus/domain/councilpost/presentation/StudentCouncilPostController.java @@ -12,8 +12,10 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import com.campus.campus.domain.council.domain.entity.CouncilType; import com.campus.campus.domain.councilpost.application.dto.request.PostRequest; -import com.campus.campus.domain.councilpost.application.dto.response.PostListItemResponse; +import com.campus.campus.domain.councilpost.application.dto.response.GetPostListForCouncilResponse; +import com.campus.campus.domain.councilpost.application.dto.response.GetUpcomingEventListForCouncilResponse; import com.campus.campus.domain.councilpost.application.dto.response.PostResponse; import com.campus.campus.domain.councilpost.application.service.StudentCouncilPostService; import com.campus.campus.domain.councilpost.domain.entity.PostCategory; @@ -21,7 +23,6 @@ import com.campus.campus.global.common.response.CommonResponse; 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.ExampleObject; import io.swagger.v3.oas.annotations.media.Schema; @@ -30,7 +31,8 @@ import lombok.RequiredArgsConstructor; @RestController -@RequestMapping("/student-council/posts") +@RequestMapping("/student-councils/posts") +@PreAuthorize("hasRole('COUNCIL')") @Tag(name = "Student Council Post", description = "학생회(COUNCIL) 권한 전용 제휴/행사 게시글 관리 API") @RequiredArgsConstructor public class StudentCouncilPostController { @@ -38,7 +40,6 @@ public class StudentCouncilPostController { private final StudentCouncilPostService postService; @PostMapping - @PreAuthorize("hasRole('COUNCIL')") @Operation( summary = "학생회 제휴/행사 게시글 생성", description = @@ -111,7 +112,6 @@ public CommonResponse createPost( } @PatchMapping("/{postId}") - @PreAuthorize("hasRole('COUNCIL')") @Operation(summary = "학생회 게시글 수정") public CommonResponse updatePost( @CurrentCouncilId Long councilId, @@ -124,7 +124,6 @@ public CommonResponse updatePost( } @DeleteMapping("/{postId}") - @PreAuthorize("hasRole('COUNCIL')") @Operation(summary = "학생회 게시글 삭제") public CommonResponse deletePost(@CurrentCouncilId Long councilId, @PathVariable Long postId) { postService.delete(councilId, postId); @@ -133,44 +132,37 @@ public CommonResponse deletePost(@CurrentCouncilId Long councilId, @PathVa } @GetMapping("/{postId}") - @Operation(summary = "학생회 게시글 단건 조회") + @Operation(summary = "학생회 게시글 상세 조회") public CommonResponse getPost(@PathVariable Long postId, @CurrentCouncilId Long councilId) { - PostResponse responseDto = - postService.findById(postId, councilId); + PostResponse responseDto = postService.findById(postId, councilId); return CommonResponse.success(StudentCouncilPostResponseCode.POST_READ_SUCCESS, responseDto); } @GetMapping - @Operation( - summary = "학생회 게시글 목록 조회 (필터링 포함)", - description = "전체 게시글 혹은 제휴(PARTNERSHIP), 행사(EVENT) 카테고리별로 필터링하여 목록을 조회합니다." - ) - public CommonResponse> getPostList( - @Parameter( - description = "필터링할 카테고리 (미선택 시 전체 조회)", - example = "PARTNERSHIP", - schema = @Schema(implementation = PostCategory.class) - ) + @Operation(summary = "학생회 제휴/행사 목록 조회 (학생회 홈화면, 로그인한 학생회 작성한 글만)") + public CommonResponse> getPostListForCouncil( @RequestParam(required = false) PostCategory category, @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "3") int size, - @CurrentCouncilId(required = false) Long councilId + @CurrentCouncilId Long councilId ) { - Page responseDto = postService.findAll(category, page, size, councilId); + Page response = postService.findPostListForCouncil(councilId, + category, page, size); - return CommonResponse.success(StudentCouncilPostResponseCode.POST_LIST_READ_SUCCESS, responseDto); + return CommonResponse.success(StudentCouncilPostResponseCode.POST_LIST_READ_SUCCESS, response); } @GetMapping("/events/upcoming") - @Operation(summary = "72시간 이내 행사 게시글 조회") - public CommonResponse> getUpcomingEvents( + @Operation(summary = "학생회 72시간 이내 행사 조회 (학생회 홈화면, 로그인한 학생회 작성한 글만)") + public CommonResponse> getUpcomingEventsForCouncil( @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "3") int size, - @CurrentCouncilId(required = false) Long councilId + @CurrentCouncilId Long councilId ) { - return CommonResponse.success(StudentCouncilPostResponseCode.POST_LIST_READ_SUCCESS, - postService.findUpcomingEvents(page, size, councilId) - ); + Page response = postService.findUpcomingEventsForCouncil(councilId, + page, size); + + return CommonResponse.success(StudentCouncilPostResponseCode.POST_LIST_READ_SUCCESS, response); } } diff --git a/src/main/java/com/campus/campus/domain/councilpost/presentation/UserPostController.java b/src/main/java/com/campus/campus/domain/councilpost/presentation/StudentCouncilPostForUserController.java similarity index 81% rename from src/main/java/com/campus/campus/domain/councilpost/presentation/UserPostController.java rename to src/main/java/com/campus/campus/domain/councilpost/presentation/StudentCouncilPostForUserController.java index 9a082d51..51e43a60 100644 --- a/src/main/java/com/campus/campus/domain/councilpost/presentation/UserPostController.java +++ b/src/main/java/com/campus/campus/domain/councilpost/presentation/StudentCouncilPostForUserController.java @@ -1,15 +1,20 @@ package com.campus.campus.domain.councilpost.presentation; +import java.util.List; + import org.springframework.data.domain.Page; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import com.campus.campus.domain.council.domain.entity.CouncilType; +import com.campus.campus.domain.councilpost.application.dto.response.GetActivePartnershipListForUserResponse; import com.campus.campus.domain.councilpost.application.dto.response.PostListItemResponse; import com.campus.campus.domain.councilpost.application.dto.response.PostResponse; -import com.campus.campus.domain.councilpost.application.service.UserPostService; +import com.campus.campus.domain.councilpost.application.service.StudentCouncilPostForUserService; import com.campus.campus.domain.councilpost.domain.entity.PostCategory; import com.campus.campus.global.annotation.CurrentUserId; import com.campus.campus.global.common.response.CommonResponse; @@ -20,13 +25,14 @@ import lombok.extern.slf4j.Slf4j; @RestController -@RequestMapping("/users/posts") -@Tag(name = "Student Post", description = "사용자가 속한 대학/단과대/전공의 제휴/행사 게시글 목록 조회 API") +@PreAuthorize("hasRole('USER')") +@RequestMapping("/users/student-council/posts") +@Tag(name = "Student Council Post For User", description = "사용자(USER) 권한 전용 사용자가 속한 대학/단과대/전공의 제휴/행사 게시글 목록 조회 API") @RequiredArgsConstructor @Slf4j -public class UserPostController { +public class StudentCouncilPostForUserController { - private final UserPostService postService; + private final StudentCouncilPostForUserService postService; @GetMapping("/school") @Operation( @@ -128,4 +134,16 @@ public CommonResponse> getUpcomingMajorEvents( Page responseDto = postService.findUpcomingMajorEvents72h(page, size, userId); return CommonResponse.success(StudentCouncilPostResponseCode.POST_LIST_READ_SUCCESS, responseDto); } + + @GetMapping("/partnerships/active") + @Operation(summary = "학생회 타입별 현재 이용 가능한 제휴 (일반 유저 전용 로직, 홈 화면)") + public CommonResponse> getActivePartnershipListForUser( + @RequestParam CouncilType councilType, + @CurrentUserId Long userId + ) { + List responses = postService.findActivePartnershipForUser(councilType, + userId); + + return CommonResponse.success(StudentCouncilPostResponseCode.POST_LIST_READ_SUCCESS, responses); + } } diff --git a/src/main/java/com/campus/campus/domain/user/application/dto/request/CampusNicknameUpdateRequest.java b/src/main/java/com/campus/campus/domain/user/application/dto/request/CampusNicknameUpdateRequest.java new file mode 100644 index 00000000..1e4a71aa --- /dev/null +++ b/src/main/java/com/campus/campus/domain/user/application/dto/request/CampusNicknameUpdateRequest.java @@ -0,0 +1,11 @@ +package com.campus.campus.domain.user.application.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; + +public record CampusNicknameUpdateRequest( + @Schema(description = "서비스 내 닉네임", example = "캠퍼스러버") + @NotBlank + String campusNickname +) { +} diff --git a/src/main/java/com/campus/campus/domain/user/application/dto/request/UserWithdrawRequest.java b/src/main/java/com/campus/campus/domain/user/application/dto/request/UserWithdrawRequest.java index b21498a1..14f0a49d 100644 --- a/src/main/java/com/campus/campus/domain/user/application/dto/request/UserWithdrawRequest.java +++ b/src/main/java/com/campus/campus/domain/user/application/dto/request/UserWithdrawRequest.java @@ -4,8 +4,8 @@ import jakarta.validation.constraints.NotBlank; public record UserWithdrawRequest( - @Schema(description = "닉네임", example = "한승현") - @NotBlank(message = "닉네임을 입력해주세요.") + @Schema(description = "카카오 이름", example = "한승현") + @NotBlank(message = "카카오 이름을 입력해주세요.") String nickname ) { } diff --git a/src/main/java/com/campus/campus/domain/user/application/dto/response/UserInfoResponse.java b/src/main/java/com/campus/campus/domain/user/application/dto/response/UserInfoResponse.java new file mode 100644 index 00000000..82e03900 --- /dev/null +++ b/src/main/java/com/campus/campus/domain/user/application/dto/response/UserInfoResponse.java @@ -0,0 +1,21 @@ +package com.campus.campus.domain.user.application.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +public record UserInfoResponse( + @Schema(description = "유저 id", example = "1") + Long userId, + + @Schema(description = "유저 닉네임(campusNickname 설정했으면 campusNickname, 아니면, nickname") + String nickname, + + @Schema(description = "학교 이름", example = "가천대학교") + String schoolName, + + @Schema(description = "단과대 이름", example = "IT융합대학") + String collegeName, + + @Schema(description = "학과 이름", example = "컴퓨터공학과") + String majorName +) { +} diff --git a/src/main/java/com/campus/campus/domain/user/application/exception/ErrorCode.java b/src/main/java/com/campus/campus/domain/user/application/exception/ErrorCode.java index 16364446..f7193fc1 100644 --- a/src/main/java/com/campus/campus/domain/user/application/exception/ErrorCode.java +++ b/src/main/java/com/campus/campus/domain/user/application/exception/ErrorCode.java @@ -13,7 +13,8 @@ public enum ErrorCode implements ErrorCodeInterface { USER_NOT_FOUND(2100, HttpStatus.NOT_FOUND, "존재하지 않는 사용자입니다."), USER_NOT_FIRST_LOGIN(2101, HttpStatus.BAD_REQUEST, "최초 로그인한 사용가 아닙니다."), NICKNAME_NOT_MATCH(2102, HttpStatus.BAD_REQUEST, "닉네임이 일치하지 않습니다."), - USER_SIGNUP_FORBIDDEN_NOW(2103, HttpStatus.FORBIDDEN, "현재 유저 회원가입할 수 없는 계정입니다."); + USER_SIGNUP_FORBIDDEN_NOW(2103, HttpStatus.FORBIDDEN, "현재 유저 회원가입할 수 없는 계정입니다."), + NICKNAME_ALREADY_EXISTS(2104, HttpStatus.CONFLICT, "이미 사용 중인 닉네임입니다."); private final int code; private final HttpStatus status; diff --git a/src/main/java/com/campus/campus/domain/user/application/exception/NicknameAlreadyExistsException.java b/src/main/java/com/campus/campus/domain/user/application/exception/NicknameAlreadyExistsException.java new file mode 100644 index 00000000..d045c132 --- /dev/null +++ b/src/main/java/com/campus/campus/domain/user/application/exception/NicknameAlreadyExistsException.java @@ -0,0 +1,9 @@ +package com.campus.campus.domain.user.application.exception; + +import com.campus.campus.global.common.exception.ApplicationException; + +public class NicknameAlreadyExistsException extends ApplicationException { + public NicknameAlreadyExistsException() { + super(ErrorCode.NICKNAME_ALREADY_EXISTS); + } +} diff --git a/src/main/java/com/campus/campus/domain/user/application/mapper/UserMapper.java b/src/main/java/com/campus/campus/domain/user/application/mapper/UserMapper.java index bf399ae7..f237a762 100644 --- a/src/main/java/com/campus/campus/domain/user/application/mapper/UserMapper.java +++ b/src/main/java/com/campus/campus/domain/user/application/mapper/UserMapper.java @@ -3,6 +3,7 @@ import org.springframework.stereotype.Component; import com.campus.campus.domain.user.application.dto.response.UserFirstProfileResponse; +import com.campus.campus.domain.user.application.dto.response.UserInfoResponse; import com.campus.campus.domain.user.domain.entity.User; import lombok.RequiredArgsConstructor; @@ -26,4 +27,14 @@ public UserFirstProfileResponse toUserFirstProfileResponse(User user) { .majorName(user.getMajor().getMajorName()) .build(); } + + public UserInfoResponse toUserInfoResponse(User user) { + return new UserInfoResponse( + user.getId(), + user.getCampusNickname() == null ? user.getNickname() : user.getCampusNickname(), + user.getSchool().getSchoolName(), + user.getCollege().getCollegeName(), + user.getMajor().getMajorName() + ); + } } diff --git a/src/main/java/com/campus/campus/domain/user/application/service/UserService.java b/src/main/java/com/campus/campus/domain/user/application/service/UserService.java index 7d7a9346..9d8ecfcd 100644 --- a/src/main/java/com/campus/campus/domain/user/application/service/UserService.java +++ b/src/main/java/com/campus/campus/domain/user/application/service/UserService.java @@ -11,8 +11,11 @@ import com.campus.campus.domain.school.domain.entity.School; import com.campus.campus.domain.school.domain.repository.MajorRepository; import com.campus.campus.domain.school.domain.repository.SchoolRepository; +import com.campus.campus.domain.user.application.dto.request.CampusNicknameUpdateRequest; import com.campus.campus.domain.user.application.dto.request.UserProfileRequest; import com.campus.campus.domain.user.application.dto.response.UserFirstProfileResponse; +import com.campus.campus.domain.user.application.dto.response.UserInfoResponse; +import com.campus.campus.domain.user.application.exception.NicknameAlreadyExistsException; import com.campus.campus.domain.user.application.exception.UserNotFirstLoginException; import com.campus.campus.domain.user.application.exception.UserNotFoundException; import com.campus.campus.domain.user.application.mapper.UserMapper; @@ -54,4 +57,24 @@ public UserFirstProfileResponse writeUserProfile(Long userId, UserProfileRequest return userMapper.toUserFirstProfileResponse(user); } + + @Transactional + public void updateCampusNickname(Long userId, CampusNicknameUpdateRequest nicknameUpdateRequest) { + User user = userRepository.findByIdAndDeletedAtIsNull(userId) + .orElseThrow(UserNotFoundException::new); + + if (userRepository.existsByCampusNicknameAndIdNot(nicknameUpdateRequest.campusNickname(), userId)) { + throw new NicknameAlreadyExistsException(); + } + + user.updateCampusNickname(nicknameUpdateRequest.campusNickname()); + userRepository.save(user); + } + + public UserInfoResponse getUserInfo(Long userId) { + User user = userRepository.findByIdAndDeletedAtIsNull(userId) + .orElseThrow(UserNotFoundException::new); + + return userMapper.toUserInfoResponse(user); + } } diff --git a/src/main/java/com/campus/campus/domain/user/domain/entity/User.java b/src/main/java/com/campus/campus/domain/user/domain/entity/User.java index 14afd03a..ac97f60f 100644 --- a/src/main/java/com/campus/campus/domain/user/domain/entity/User.java +++ b/src/main/java/com/campus/campus/domain/user/domain/entity/User.java @@ -46,6 +46,9 @@ public class User extends BaseEntity { @Column(name = "profile_image") private String profileImage; + @Column(name = "campus_nickname") + private String campusNickname; + @Column(name = "deleted_at") private LocalDateTime deletedAt; @@ -67,6 +70,10 @@ public void updateProfile(School school, College college, Major major) { this.major = major; } + public void updateCampusNickname(String campusNickname) { + this.campusNickname = campusNickname; + } + public void delete(LocalDateTime now) { this.deletedAt = now; } diff --git a/src/main/java/com/campus/campus/domain/user/domain/repository/UserRepository.java b/src/main/java/com/campus/campus/domain/user/domain/repository/UserRepository.java index 71aa3787..c24d5593 100644 --- a/src/main/java/com/campus/campus/domain/user/domain/repository/UserRepository.java +++ b/src/main/java/com/campus/campus/domain/user/domain/repository/UserRepository.java @@ -19,6 +19,8 @@ public interface UserRepository extends JpaRepository { boolean existsByIdAndDeletedAtIsNull(Long userId); + boolean existsByCampusNicknameAndIdNot(String campusNickname, Long userId); + List findAllByDeletedAtIsNotNullAndDeletedAtBefore(LocalDateTime softDeleteDate); @Query(""" diff --git a/src/main/java/com/campus/campus/domain/user/presentation/UserController.java b/src/main/java/com/campus/campus/domain/user/presentation/UserController.java index d897b950..7b5e1991 100644 --- a/src/main/java/com/campus/campus/domain/user/presentation/UserController.java +++ b/src/main/java/com/campus/campus/domain/user/presentation/UserController.java @@ -1,12 +1,15 @@ package com.campus.campus.domain.user.presentation; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import com.campus.campus.domain.user.application.dto.request.CampusNicknameUpdateRequest; import com.campus.campus.domain.user.application.dto.request.UserProfileRequest; import com.campus.campus.domain.user.application.dto.response.UserFirstProfileResponse; +import com.campus.campus.domain.user.application.dto.response.UserInfoResponse; import com.campus.campus.domain.user.application.service.UserService; import com.campus.campus.global.annotation.CurrentUserId; import com.campus.campus.global.common.response.CommonResponse; @@ -29,4 +32,21 @@ public CommonResponse createUserProfile(@CurrentUserId return CommonResponse.success(UserResponseCode.FIRST_PROFILE_WRITE, userFirstProfileResponse); } + + @PatchMapping("/change/nickname") + @Operation(summary = "사용자 서비스 내 닉네임 변경") + public CommonResponse updateCampusNickname(@CurrentUserId Long userId, + @Valid @RequestBody CampusNicknameUpdateRequest nicknameUpdateRequest) { + userService.updateCampusNickname(userId, nicknameUpdateRequest); + + return CommonResponse.success(UserResponseCode.NICKNAME_UPDATE_SUCCESS); + } + + @GetMapping + @Operation(summary = "사용자 정보 조회(홈 화면)") + public CommonResponse getUserInfo(@CurrentUserId Long userId) { + UserInfoResponse userInfoResponse = userService.getUserInfo(userId); + + return CommonResponse.success(UserResponseCode.GET_USER_INFO_SUCCESS, userInfoResponse); + } } diff --git a/src/main/java/com/campus/campus/domain/user/presentation/UserResponseCode.java b/src/main/java/com/campus/campus/domain/user/presentation/UserResponseCode.java index 562ab1bf..e1a8abd9 100644 --- a/src/main/java/com/campus/campus/domain/user/presentation/UserResponseCode.java +++ b/src/main/java/com/campus/campus/domain/user/presentation/UserResponseCode.java @@ -12,7 +12,9 @@ public enum UserResponseCode implements ResponseCodeInterface { LOGIN_SUCCESS(200, HttpStatus.OK, "로그인에 성공했습니다."), FIRST_PROFILE_WRITE(200, HttpStatus.OK, " 프로필(학교 정보) 입력에 성공했습니다."), - WITHDRAW_SUCCESS(200, HttpStatus.OK, "회원탈퇴에 성공했습니다."); + NICKNAME_UPDATE_SUCCESS(200, HttpStatus.OK, "닉네임 변경에 성공했습니다."), + WITHDRAW_SUCCESS(200, HttpStatus.OK, "회원탈퇴에 성공했습니다."), + GET_USER_INFO_SUCCESS(200, HttpStatus.OK, "유저 정보 조회에 성공했습니다."); private final int code; private final HttpStatus status; diff --git a/src/main/java/com/campus/campus/global/auth/application/dto/OauthLoginResponse.java b/src/main/java/com/campus/campus/global/auth/application/dto/OauthLoginResponse.java index edcf5cd0..bc25bdb9 100644 --- a/src/main/java/com/campus/campus/global/auth/application/dto/OauthLoginResponse.java +++ b/src/main/java/com/campus/campus/global/auth/application/dto/OauthLoginResponse.java @@ -12,6 +12,9 @@ public record OauthLoginResponse( @Schema(description = "사용자 이름(닉네임)", example = "김잇타") String nickname, + @Schema(description = "사용자 서비스 내 닉네임", example = "망포동 피바라기") + String campusNickname, + @Schema(description = "사용자 id", example = "1") Long userId, diff --git a/src/main/java/com/campus/campus/global/auth/application/mapper/LoginMapper.java b/src/main/java/com/campus/campus/global/auth/application/mapper/LoginMapper.java index 3333a0af..b7e9cf70 100644 --- a/src/main/java/com/campus/campus/global/auth/application/mapper/LoginMapper.java +++ b/src/main/java/com/campus/campus/global/auth/application/mapper/LoginMapper.java @@ -17,6 +17,7 @@ public OauthLoginResponse toOauthLoginResponse(User user, String accessToken, St accessToken, refreshToken, user.getNickname(), + user.getCampusNickname(), user.getId(), user.getKakaoId(), user.getEmail(),