diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 2904076..da129a7 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,14 +1,16 @@ -name: Deploy to ECR and EC2 +name: CI/CD Pipeline on: push: - branches: [ main ] - pull_request: - branches: [ main ] + branches: [ main, develop ] jobs: + deploy: + name: Deploy to EC2 runs-on: ubuntu-latest + # develop 또는 main에 푸시될 때만 실행 + if: github.event_name == 'push' steps: - name: Checkout code @@ -43,17 +45,10 @@ jobs: username: ubuntu key: ${{ secrets.EC2_SSH_KEY }} script: | - # ECR 로그인 aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin ${{ steps.login-ecr.outputs.registry }} - - # 최신 이미지 pull docker pull ${{ steps.login-ecr.outputs.registry }}/stitch-api:latest - - # 기존 컨테이너 중지 및 제거 docker stop stitch-api-container || true docker rm stitch-api-container || true - - # 새 컨테이너 실행 docker run -d \ --name stitch-api-container \ -p 8080:8080 \ @@ -72,8 +67,6 @@ jobs: -e SPRING_MAIL_PASSWORD="${{ secrets.SPRING_MAIL_PASSWORD }}" \ -e UNIVCERT_API_KEY="${{ secrets.UNIVCERT_API_KEY }}" \ ${{ steps.login-ecr.outputs.registry }}/stitch-api:latest - - # 컨테이너 시작 확인 sleep 10 docker logs stitch-api-container --tail 20 @@ -84,9 +77,14 @@ jobs: username: ubuntu key: ${{ secrets.EC2_SSH_KEY }} script: | - # 컨테이너 상태 확인 docker ps -f name=stitch-api-container - - # 애플리케이션 시작 대기 및 로그 확인 sleep 30 - docker logs stitch-api-container --tail 10 \ No newline at end of file + docker logs stitch-api-container --tail 10 + PUBLIC_IP=$(curl -s ifconfig.me) + echo "======================================" + echo "🚀 Deployment Completed!" + echo "======================================" + echo "Branch: ${{ github.ref_name }}" + echo "Swagger UI: http://${PUBLIC_IP}:8080/swagger-ui.html" + echo "Website: https://stitch-study.site" + echo "======================================" \ No newline at end of file diff --git a/stitch-api/src/main/java/se/sowl/stitchapi/campus/controller/CampusController.java b/stitch-api/src/main/java/se/sowl/stitchapi/campus/controller/CampusController.java index b6a3ccf..19e3445 100644 --- a/stitch-api/src/main/java/se/sowl/stitchapi/campus/controller/CampusController.java +++ b/stitch-api/src/main/java/se/sowl/stitchapi/campus/controller/CampusController.java @@ -21,7 +21,7 @@ public class CampusController { private final CampusService campusService; @Operation(summary = "캠퍼스 목록 조회", description = "모든 캠퍼스의 목록을 조회합니다.") - @GetMapping("/list") + @GetMapping public CommonResponse> getCampusList(){ List campusListResponse = campusService.getAllCampuses(); return CommonResponse.ok(campusListResponse); diff --git a/stitch-api/src/main/java/se/sowl/stitchapi/major/contoller/MajorController.java b/stitch-api/src/main/java/se/sowl/stitchapi/major/contoller/MajorController.java index 7b8458f..e00e740 100644 --- a/stitch-api/src/main/java/se/sowl/stitchapi/major/contoller/MajorController.java +++ b/stitch-api/src/main/java/se/sowl/stitchapi/major/contoller/MajorController.java @@ -2,16 +2,16 @@ import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import se.sowl.stitchapi.common.CommonResponse; -import se.sowl.stitchapi.major.dto.request.MajorRequest; import se.sowl.stitchapi.major.dto.response.MajorDetailResponse; import se.sowl.stitchapi.major.dto.response.MajorListResponse; import se.sowl.stitchapi.major.dto.response.MajorResponse; import se.sowl.stitchapi.major.service.MajorService; +import se.sowl.stitchdomain.user.domain.CustomOAuth2User; import java.util.List; @@ -24,25 +24,31 @@ public class MajorController { private final MajorService majorService; @Operation(summary = "전공 목록 조회") - @GetMapping("/list") + @GetMapping public CommonResponse> getMajorList() { List majorList = majorService.getAllMajors(); return CommonResponse.ok(majorList); } @Operation(summary = "전공 상세 조회") - @GetMapping("/detail") + @GetMapping("/{majorId}") public CommonResponse getMajorDetail( - @Parameter(description = "전공 ID", required = true) - @RequestParam("majorId") Long majorId) { + @PathVariable Long majorId + ) { MajorDetailResponse response = majorService.getMajorDetail(majorId); return CommonResponse.ok(response); } @Operation(summary = "전공 선택") - @PostMapping("/select") - public CommonResponse selectMajor(@RequestBody MajorRequest request) { - MajorResponse response = majorService.selectMajor(request); + @PutMapping("/{majorId}") + public CommonResponse selectMajor( + @PathVariable Long majorId, + @AuthenticationPrincipal CustomOAuth2User currentUser + ) { + MajorResponse response = majorService.selectMajor( + majorId, + currentUser.getUserId() + ); return CommonResponse.ok(response); } } \ No newline at end of file diff --git a/stitch-api/src/main/java/se/sowl/stitchapi/major/dto/request/MajorRequest.java b/stitch-api/src/main/java/se/sowl/stitchapi/major/dto/request/MajorRequest.java deleted file mode 100644 index 2a0d6a6..0000000 --- a/stitch-api/src/main/java/se/sowl/stitchapi/major/dto/request/MajorRequest.java +++ /dev/null @@ -1,19 +0,0 @@ -package se.sowl.stitchapi.major.dto.request; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@NoArgsConstructor -@AllArgsConstructor -@Builder -@Schema(description = "전공 선택 요청") -public class MajorRequest { - @Schema(description = "전공 ID", example = "1") - private Long majorId; - @Schema(description = "사용자 ID", example = "12345") - private Long userId; -} diff --git a/stitch-api/src/main/java/se/sowl/stitchapi/major/service/MajorService.java b/stitch-api/src/main/java/se/sowl/stitchapi/major/service/MajorService.java index 8dc064f..d807de7 100644 --- a/stitch-api/src/main/java/se/sowl/stitchapi/major/service/MajorService.java +++ b/stitch-api/src/main/java/se/sowl/stitchapi/major/service/MajorService.java @@ -5,7 +5,6 @@ import org.springframework.transaction.annotation.Transactional; import se.sowl.stitchapi.exception.MajorException; import se.sowl.stitchapi.exception.UserException; -import se.sowl.stitchapi.major.dto.request.MajorRequest; import se.sowl.stitchapi.major.dto.response.MajorDetailResponse; import se.sowl.stitchapi.major.dto.response.MajorListResponse; import se.sowl.stitchapi.major.dto.response.MajorResponse; @@ -43,8 +42,8 @@ public MajorDetailResponse getMajorDetail(Long majorId){ @Transactional - public MajorResponse selectMajor(MajorRequest request) { - User user = userRepository.findById(request.getUserId()) + public MajorResponse selectMajor(Long majorId, Long userId) { + User user = userRepository.findById(userId) .orElseThrow(UserException.UserNotFoundException::new); if (!user.isCampusCertified()) { @@ -54,7 +53,7 @@ public MajorResponse selectMajor(MajorRequest request) { UserCamInfo userCamInfo = userCamInfoRepository.findByUser(user) .orElseThrow(UserException.UserCamInfoNotFoundException::new); - Major newMajor = findMajorById(request.getMajorId()); + Major newMajor = findMajorById(majorId); // 새 전공 설정 diff --git a/stitch-api/src/main/java/se/sowl/stitchapi/notification/controller/NotificationController.java b/stitch-api/src/main/java/se/sowl/stitchapi/notification/controller/NotificationController.java index 8021e92..3df00c7 100644 --- a/stitch-api/src/main/java/se/sowl/stitchapi/notification/controller/NotificationController.java +++ b/stitch-api/src/main/java/se/sowl/stitchapi/notification/controller/NotificationController.java @@ -1,17 +1,22 @@ package se.sowl.stitchapi.notification.controller; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import se.sowl.stitchapi.common.CommonResponse; import se.sowl.stitchapi.notification.dto.NotificationListResponse; import se.sowl.stitchapi.notification.dto.NotificationResponse; import se.sowl.stitchapi.notification.service.NotificationService; +import se.sowl.stitchdomain.user.domain.CustomOAuth2User; import java.util.List; @RestController @RequestMapping("/api/notifications") @RequiredArgsConstructor +@Tag(name = "Notification", description = "알림 관련 API") public class NotificationController { private final NotificationService notificationService; @@ -21,11 +26,14 @@ public class NotificationController { * - 최신순으로 정렬된 알림 목록을 반환 * - 읽음/안읽음 상태 포함 */ - @GetMapping("/list") + @Operation(summary = "사용자의 알림 목록 조회", description = "최신순으로 정렬된 알림 목록을 반환합니다.") + @GetMapping public CommonResponse> getUserNotifications( - @RequestParam("userCamInfoId") Long userCamInfoId + @AuthenticationPrincipal CustomOAuth2User currentUser ){ - List responses = notificationService.getUserNotifications(userCamInfoId); + List responses = notificationService.getUserNotifications( + currentUser.getUserCamInfoId() + ); return CommonResponse.ok(responses); } @@ -34,11 +42,14 @@ public CommonResponse> getUserNotifications( * - 알림 뱃지 표시용 * - 헤더나 네비게이션 바에서 활용 */ + @Operation(summary = "읽지 않은 알림 개수 조회", description = "알림 뱃지 표시용") @GetMapping("/unread-count") public CommonResponse getUnreadNotificationCount( - @RequestParam("userCamInfoId") Long userCamInfoId + @AuthenticationPrincipal CustomOAuth2User currentUser ){ - int count = notificationService.getUnreadNotificationCount(userCamInfoId); + int count = notificationService.getUnreadNotificationCount( + currentUser.getUserCamInfoId() + ); return CommonResponse.ok(count); } @@ -47,12 +58,16 @@ public CommonResponse getUnreadNotificationCount( * - 알림 클릭 시 상세 정보 확인 * - 권한 검증 포함 (본인 알림만 조회 가능) */ - @GetMapping("/detail") + @Operation(summary = "특정 알림 상세 조회", description = "알림 클릭 시 상세 정보 확인 (본인 알림만 조회 가능)") + @GetMapping("/{notificationId}") public CommonResponse getNotificationDetail( - @RequestParam("notificationId") Long notificationId, - @RequestParam("userCamInfoId") Long userCamInfoId + @PathVariable Long notificationId, + @AuthenticationPrincipal CustomOAuth2User currentUser ){ - NotificationResponse response = notificationService.getNotificationDetail(notificationId, userCamInfoId); + NotificationResponse response = notificationService.getNotificationDetail( + notificationId, + currentUser.getUserCamInfoId() + ); return CommonResponse.ok(response); } @@ -61,12 +76,16 @@ public CommonResponse getNotificationDetail( * - 특정 알림 하나를 읽음 상태로 변경 * - 알림 클릭 시 자동으로 읽음 처리되도록 활용 */ - @PostMapping("/read") + @Operation(summary = "개별 알림 읽음 처리", description = "특정 알림 하나를 읽음 상태로 변경") + @PutMapping("/{notificationId}/read") public CommonResponse markOneRead( - @RequestParam("notificationId") Long notificationId, - @RequestParam("userCamInfoId") Long userCamInfoId + @PathVariable Long notificationId, + @AuthenticationPrincipal CustomOAuth2User currentUser ){ - NotificationResponse response = notificationService.markOneAsRead(notificationId, userCamInfoId); + NotificationResponse response = notificationService.markOneAsRead( + notificationId, + currentUser.getUserCamInfoId() + ); return CommonResponse.ok(response); } @@ -75,11 +94,12 @@ public CommonResponse markOneRead( * - "모두 읽음" 버튼 기능 * - 읽지 않은 모든 알림을 일괄 읽음 처리 */ - @PostMapping("/read/all") + @Operation(summary = "모든 알림 읽음 처리", description = "읽지 않은 모든 알림을 일괄 읽음 처리") + @PutMapping("/read-all") public CommonResponse markAllRead( - @RequestParam("userCamInfoId") Long userCamInfoId + @AuthenticationPrincipal CustomOAuth2User currentUser ){ - notificationService.markAllAsRead(userCamInfoId); + notificationService.markAllAsRead(currentUser.getUserCamInfoId()); return CommonResponse.ok(null); } @@ -88,12 +108,16 @@ public CommonResponse markAllRead( * - 개별 알림을 완전히 삭제 * - 알림 목록에서 제거하고 싶을 때 사용 */ - @DeleteMapping("/delete") + @Operation(summary = "알림 삭제", description = "개별 알림을 완전히 삭제") + @DeleteMapping("/{notificationId}") public CommonResponse deleteNotification( - @RequestParam("notificationId") Long notificationId, - @RequestParam("userCamInfoId") Long userCamInfoId + @PathVariable Long notificationId, + @AuthenticationPrincipal CustomOAuth2User currentUser ){ - notificationService.deleteNotification(notificationId, userCamInfoId); + notificationService.deleteNotification( + notificationId, + currentUser.getUserCamInfoId() + ); return CommonResponse.ok(null); } @@ -103,6 +127,7 @@ public CommonResponse deleteNotification( * - 스터디 리더/관리자에게 전송 * - 실시간 SSE 알림과 함께 DB 저장 */ + @Operation(summary = "스터디 가입 신청 알림 생성", description = "내부 API - Service에서 호출") @PostMapping("/study-apply") public CommonResponse createStudyApplyNotification( @RequestParam("receiverId") Long receiverId, @@ -118,6 +143,7 @@ public CommonResponse createStudyApplyNotification( * - 신청자에게 전송 * - 승인 완료 후 자동으로 호출 */ + @Operation(summary = "스터디 가입 승인 알림 생성", description = "내부 API - Service에서 호출") @PostMapping("/study-approve") public CommonResponse createStudyApproveNotification( @RequestParam("studyMemberId") Long studyMemberId @@ -132,6 +158,7 @@ public CommonResponse createStudyApproveNotification( * - 신청자에게 전송 * - 거절 처리 후 자동으로 호출 */ + @Operation(summary = "스터디 가입 거절 알림 생성", description = "내부 API - Service에서 호출") @PostMapping("/study-reject") public CommonResponse createStudyRejectNotification( @RequestParam("studyMemberId") Long studyMemberId @@ -146,6 +173,7 @@ public CommonResponse createStudyRejectNotification( * - 게시글 작성자에게 전송 (자신이 단 댓글 제외) * - 댓글 작성 후 자동으로 호출 */ + @Operation(summary = "새 댓글 알림 생성", description = "내부 API - Service에서 호출") @PostMapping("/study-comment") public CommonResponse createNewCommentNotification( @RequestParam("commentId") Long commentId diff --git a/stitch-api/src/main/java/se/sowl/stitchapi/study/controller/StudyMemberController.java b/stitch-api/src/main/java/se/sowl/stitchapi/study/controller/StudyMemberController.java index 3d69efa..c0e1570 100644 --- a/stitch-api/src/main/java/se/sowl/stitchapi/study/controller/StudyMemberController.java +++ b/stitch-api/src/main/java/se/sowl/stitchapi/study/controller/StudyMemberController.java @@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import se.sowl.stitchapi.common.CommonResponse; import se.sowl.stitchapi.study.dto.request.ChangeLeaderRequest; @@ -11,6 +12,7 @@ import se.sowl.stitchapi.study.dto.response.MyStudyResponse; import se.sowl.stitchapi.study.dto.response.StudyMemberResponse; import se.sowl.stitchapi.study.service.StudyMemberService; +import se.sowl.stitchdomain.user.domain.CustomOAuth2User; import java.util.List; @@ -26,88 +28,77 @@ public class StudyMemberController { @PostMapping("/apply") public CommonResponse applyStudyMember( @RequestBody StudyMemberApplyRequest request, - @Parameter(description = "학교 인증자 ID", required = true, example = "1") - @RequestParam("userCamInfoId") Long userCamInfoId + @AuthenticationPrincipal CustomOAuth2User currentUser ){ - StudyMemberResponse response = studyMemberService.applyStudyMember(request, userCamInfoId); + StudyMemberResponse response = studyMemberService.applyStudyMember(request, currentUser.getUserCamInfoId()); return CommonResponse.ok(response); } @Operation(summary = "스터디 가입 승인", description = "스터디 가입 신청을 승인합니다. (리더만 가능)") - @PostMapping("/approve") + @PostMapping("/{studyMemberId}/approve") public CommonResponse approveStudyMember( - @Parameter(description = "스터디 멤버 ID", required = true, example = "1") - @RequestParam("studyMemberId") Long studyMemberId, - @Parameter(description = "학교 인증자 ID", required = true, example = "1") - @RequestParam("userCamInfoId") Long userCamInfoId + @PathVariable Long studyMemberId, + @AuthenticationPrincipal CustomOAuth2User currentUser ){ - StudyMemberResponse response = studyMemberService.approveStudyMember(studyMemberId, userCamInfoId); + StudyMemberResponse response = studyMemberService.approveStudyMember(studyMemberId, currentUser.getUserCamInfoId()); return CommonResponse.ok(response); } @Operation(summary = "스터디 가입 거절", description = "스터디 가입 신청을 거절합니다. (리더만 가능)") - @PostMapping("/reject") + @PostMapping("/{studyMemberId}/reject") public CommonResponse rejectStudyMember( - @Parameter(description = "스터디 멤버 ID", required = true, example = "1") - @RequestParam("studyMemberId") Long studyMemberId, - @Parameter(description = "학교 인증자 ID", required = true, example = "1") - @RequestParam("userCamInfoId") Long userCamInfoId + @PathVariable Long studyMemberId, + @AuthenticationPrincipal CustomOAuth2User currentUser ){ - studyMemberService.rejectStudyMember(studyMemberId, userCamInfoId); + studyMemberService.rejectStudyMember(studyMemberId, currentUser.getUserCamInfoId()); return CommonResponse.ok(null); } @Operation(summary = "스터디 멤버 목록 조회", description = "승인된 스터디 멤버 목록을 조회합니다.") - @GetMapping("/list") + @GetMapping("/studies/{studyPostId}") public CommonResponse> getStudyMember( - @Parameter(description = "스터디 게시글 ID", required = true, example = "1") - @RequestParam("studyPostId") Long studyPostId) { + @PathVariable Long studyPostId + ) { List responses = studyMemberService.getStudyMembers(studyPostId); return CommonResponse.ok(responses); } @Operation(summary = "스터디 탈퇴", description = "스터디에서 탈퇴합니다. (리더는 탈퇴 불가)") - @PostMapping("/leave") + @DeleteMapping("/studies/{studyPostId}/leave") public CommonResponse leaveStudyMember( - @Parameter(description = "스터디 게시글 ID", required = true, example = "1") - @RequestParam("studyPostId") Long studyPostId, - @Parameter(description = "학교 인증자 ID", required = true, example = "1") - @RequestParam("userCamInfoId") Long userCamInfoId + @PathVariable Long studyPostId, + @AuthenticationPrincipal CustomOAuth2User currentUser ){ - studyMemberService.leaveStudyMember(studyPostId, userCamInfoId); + studyMemberService.leaveStudyMember(studyPostId, currentUser.getUserCamInfoId()); return CommonResponse.ok(null); } @Operation(summary = "리더 변경", description = "스터디 리더를 변경합니다. (현재 리더만 가능)") - @PostMapping("/change-leader") + @PutMapping("/leader") public CommonResponse changeLeader( @RequestBody ChangeLeaderRequest request, - @Parameter(description = "학교 인증자 ID", required = true, example = "1") - @RequestParam("userCamInfoId") Long userCamInfoId + @AuthenticationPrincipal CustomOAuth2User currentUser ) { - StudyMemberResponse response = studyMemberService.changeLeader(request, userCamInfoId); + StudyMemberResponse response = studyMemberService.changeLeader(request, currentUser.getUserCamInfoId()); return CommonResponse.ok(response); } @Operation(summary = "스터디 신청자 목록 조회", description = "내 스터디의 신청자 목록을 조회합니다. (리더만 가능)") - @GetMapping("/applicants") + @GetMapping("/studies/{studyPostId}/applicants") public CommonResponse> getStudyApplicants( - @Parameter(description = "스터디 게시글 ID", required = true, example = "1") - @RequestParam("studyPostId") Long studyPostId, - @Parameter(description = "학교 인증자 ID", required = true, example = "1") - @RequestParam("userCamInfoId") Long userCamInfoId + @PathVariable Long studyPostId, + @AuthenticationPrincipal CustomOAuth2User currentUser ) { - List responses = studyMemberService.getApplicantsMyStudy(studyPostId, userCamInfoId); + List responses = studyMemberService.getApplicantsMyStudy(studyPostId, currentUser.getUserCamInfoId()); return CommonResponse.ok(responses); } @Operation(summary = "내가 관련된 모든 스터디 목록", description = "내가 관련된 모든 스터디 목록을 조회합니다. (신청, 승인 상태 모두 포함)") @GetMapping("/my-studies") public CommonResponse> getMyStudies( - @Parameter(description = "학교 인증자 ID", required = true, example = "1") - @RequestParam("userCamInfoId") Long userCamInfoId + @AuthenticationPrincipal CustomOAuth2User currentUser ) { - List responses = studyMemberService.getMyJoinedStudies(userCamInfoId); + List responses = studyMemberService.getMyJoinedStudies(currentUser.getUserCamInfoId()); return CommonResponse.ok(responses); } diff --git a/stitch-api/src/main/java/se/sowl/stitchapi/study/controller/StudyPostCommentController.java b/stitch-api/src/main/java/se/sowl/stitchapi/study/controller/StudyPostCommentController.java index 14e80f3..8aafc6c 100644 --- a/stitch-api/src/main/java/se/sowl/stitchapi/study/controller/StudyPostCommentController.java +++ b/stitch-api/src/main/java/se/sowl/stitchapi/study/controller/StudyPostCommentController.java @@ -1,21 +1,21 @@ package se.sowl.stitchapi.study.controller; import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.transaction.annotation.Transactional; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import se.sowl.stitchapi.common.CommonResponse; import se.sowl.stitchapi.study.dto.request.StudyPostCommentRequest; import se.sowl.stitchapi.study.dto.response.StudyPostCommentResponse; import se.sowl.stitchapi.study.service.StudyPostCommentService; +import se.sowl.stitchdomain.user.domain.CustomOAuth2User; import java.util.List; @RestController -@RequestMapping("/api/studyComments") +@RequestMapping("/api/study-comments") @RequiredArgsConstructor @Tag(name = "StudyPostComment", description = "스터디 게시글 댓글 API") public class StudyPostCommentController { @@ -23,71 +23,61 @@ public class StudyPostCommentController { private final StudyPostCommentService studyPostCommentService; @Operation(summary = "스터디 게시글 댓글 조회", description = "특정 스터디 게시글에 대한 댓글을 조회합니다.") - @GetMapping("/list") + @GetMapping("/studies/{studyPostId}") @PreAuthorize("isAuthenticated()") public CommonResponse> getCommentsByPostId( - @Parameter(description = "스터디 게시글 ID", required = true, example = "1") - @RequestParam("studyPostId") Long studyPostId + @PathVariable Long studyPostId ){ List response = studyPostCommentService.getCommentsByPostId(studyPostId); return CommonResponse.ok(response); } @Operation(summary = "스터디 게시글 댓글 작성", description = "새로운 스터디 게시글 댓글을 작성합니다.") - @PostMapping("/create") + @PostMapping public CommonResponse createStudyComments( @RequestBody StudyPostCommentRequest studyPostCommentRequest, - @Parameter(description = "학교 인증자 ID", required = true, example = "1") - @RequestParam("userCamInfoId") Long userCamInfoId - ){ - StudyPostCommentResponse response = studyPostCommentService.createStudyComments(studyPostCommentRequest, userCamInfoId); + @AuthenticationPrincipal CustomOAuth2User currentUser + ){ + StudyPostCommentResponse response = studyPostCommentService.createStudyComments(studyPostCommentRequest, currentUser.getUserCamInfoId()); return CommonResponse.ok(response); } @Operation(summary = "스터디 게시글 댓글 수정", description = "기존 스터디 게시글 댓글을 수정합니다.") - @PostMapping("/update") + @PutMapping("/{commentId}") public CommonResponse updateStudyPostComments( + @PathVariable Long commentId, @RequestBody StudyPostCommentRequest studyPostCommentRequest, - @Parameter(description = "댓글 ID", required = true, example = "1") - @RequestParam("commentId") Long commentId, - @Parameter(description = "학교 인증자 ID", required = true, example = "1") - @RequestParam("userCamInfoId") Long userCamInfoId + @AuthenticationPrincipal CustomOAuth2User currentUser ){ - StudyPostCommentResponse response = studyPostCommentService.updateStudyPostComment(commentId, studyPostCommentRequest, userCamInfoId); + StudyPostCommentResponse response = studyPostCommentService.updateStudyPostComment(commentId, studyPostCommentRequest, currentUser.getUserCamInfoId()); return CommonResponse.ok(response); } @Operation(summary = "스터디 게시글 댓글 삭제", description = "기존 스터디 게시글 댓글을 삭제합니다.") - @PostMapping("/delete") + @DeleteMapping("/{commentId}") public CommonResponse deleteStudyPostComments( - @Parameter(description = "댓글 ID", required = true, example = "1") - @RequestParam("commentId") Long commentId, - @Parameter(description = "학교 인증자 ID", required = true, example = "1") - @RequestParam("userCamInfoId") Long userCamInfoId + @PathVariable Long commentId, + @AuthenticationPrincipal CustomOAuth2User currentUser ){ - studyPostCommentService.deleteStudyPostComment(commentId, userCamInfoId); + studyPostCommentService.deleteStudyPostComment(commentId, currentUser.getUserCamInfoId()); return CommonResponse.ok(null); } @Operation(summary = "스터디 게시글 댓글 개수 조회", description = "특정 스터디 게시글에 대한 댓글 개수를 조회합니다.") - @GetMapping("/count") - @PreAuthorize("isAuthenticated()") + @GetMapping("/studies/{studyPostId}/count") public CommonResponse getCommentCount( - @Parameter(description = "스터디 게시글 ID", required = true, example = "1") - @RequestParam("studyPostId") Long studyPostId + @PathVariable Long studyPostId ){ int commentCount = studyPostCommentService.getCommentCount(studyPostId); return CommonResponse.ok(commentCount); } @Operation(summary = "내가 작성한 댓글 조회", description = "사용자가 작성한 모든 스터디 게시글 댓글을 조회합니다.") - @GetMapping("/myComments") - @PreAuthorize("isAuthenticated()") + @GetMapping("/my-comments") public CommonResponse> getMyComments( - @Parameter(description = "학교 인증자 ID", required = true, example = "1") - @RequestParam("userCamInfoId") Long userCamInfoId + @AuthenticationPrincipal CustomOAuth2User currentUser ){ - List response = studyPostCommentService.getMyComments(userCamInfoId); + List response = studyPostCommentService.getMyComments(currentUser.getUserCamInfoId()); return CommonResponse.ok(response); } -} +} \ No newline at end of file diff --git a/stitch-api/src/main/java/se/sowl/stitchapi/study/controller/StudyPostController.java b/stitch-api/src/main/java/se/sowl/stitchapi/study/controller/StudyPostController.java index 9eafc2f..94523a8 100644 --- a/stitch-api/src/main/java/se/sowl/stitchapi/study/controller/StudyPostController.java +++ b/stitch-api/src/main/java/se/sowl/stitchapi/study/controller/StudyPostController.java @@ -5,6 +5,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import se.sowl.stitchapi.common.CommonResponse; import se.sowl.stitchapi.study.dto.request.StudyPostRequest; @@ -12,6 +13,7 @@ import se.sowl.stitchapi.study.dto.response.StudyPostListResponse; import se.sowl.stitchapi.study.dto.response.StudyPostResponse; import se.sowl.stitchapi.study.service.StudyPostService; +import se.sowl.stitchdomain.user.domain.CustomOAuth2User; import java.util.List; @@ -24,55 +26,48 @@ public class StudyPostController { private final StudyPostService studyPostService; @Operation(summary = "스터디 게시글 생성") - @PostMapping("/create") + @PostMapping public CommonResponse createStudyPost( @RequestBody StudyPostRequest studyPostRequest, - @Parameter(description = "학교 인증자 ID", required = true, example = "1") - @RequestParam("userCamInfoId") Long userCamInfoId - ){ - StudyPostResponse response = studyPostService.createStudyPost(studyPostRequest, userCamInfoId); + @AuthenticationPrincipal CustomOAuth2User currentUser + ){ + StudyPostResponse response = studyPostService.createStudyPost(studyPostRequest, currentUser.getUserCamInfoId()); return CommonResponse.ok(response); } @Operation(summary = "스터디 게시글 상세 조회") - @GetMapping("/detail") + @GetMapping("/{studyPostId}") public CommonResponse getStudyPostDetail( - @Parameter(description = "스터디 게시글 ID", required = true, example = "1") - @RequestParam("studyPostId") Long studyPostId, - @Parameter(description = "학교 인증자 ID", required = true, example = "1") - @RequestParam("userCamInfoId") Long userCamInfoId + @PathVariable Long studyPostId, + @AuthenticationPrincipal CustomOAuth2User currentUser ){ - StudyPostDetailResponse response = studyPostService.getStudyPostDetail(studyPostId, userCamInfoId); + StudyPostDetailResponse response = studyPostService.getStudyPostDetail(studyPostId, currentUser.getUserCamInfoId()); return CommonResponse.ok(response); } @Operation(summary = "스터디 게시글 수정") - @PostMapping("/update") + @PutMapping("/{studyPostId}") public CommonResponse updateStudyPost( + @PathVariable Long studyPostId, @RequestBody StudyPostRequest studyPostRequest, - @Parameter(description = "스터디 게시글 ID", required = true, example = "1") - @RequestParam("studyPostId") Long studyPostId, - @Parameter(description = "학교 인증자 ID", required = true, example = "1") - @RequestParam("userCamInfoId") Long userCamInfoId + @AuthenticationPrincipal CustomOAuth2User currentUser ){ - StudyPostResponse response = studyPostService.updateStudyPost(studyPostRequest, studyPostId ,userCamInfoId); + StudyPostResponse response = studyPostService.updateStudyPost(studyPostRequest, studyPostId , currentUser.getUserCamInfoId()); return CommonResponse.ok(response); } @Operation(summary = "스터디 게시글 삭제") - @PostMapping("/delete") + @DeleteMapping("/{studyPostId}") public CommonResponse deleteStudyPost( - @Parameter(description = "스터디 게시글 ID", required = true, example = "1") - @RequestParam("studyPostId") Long studyPostId, - @Parameter(description = "학교 인증자 ID", required = true, example = "1") - @RequestParam("userCamInfoId") Long userCamInfoId + @PathVariable Long studyPostId, + @AuthenticationPrincipal CustomOAuth2User currentUser ){ - studyPostService.deleteStudyPost(studyPostId, userCamInfoId); + studyPostService.deleteStudyPost(studyPostId, currentUser.getUserCamInfoId()); return CommonResponse.ok(null); } @Operation(summary = "스터디 게시글 목록 조회") - @GetMapping("/list") + @GetMapping @PreAuthorize("isAuthenticated()") public CommonResponse> getStudyPostList(){ List postLists = studyPostService.getStudyPostLists(); diff --git a/stitch-api/src/test/java/se/sowl/stitchapi/major/contoller/MajorControllerTest.java b/stitch-api/src/test/java/se/sowl/stitchapi/major/contoller/MajorControllerTest.java deleted file mode 100644 index 5562dfe..0000000 --- a/stitch-api/src/test/java/se/sowl/stitchapi/major/contoller/MajorControllerTest.java +++ /dev/null @@ -1,153 +0,0 @@ -package se.sowl.stitchapi.major.contoller; - -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.http.MediaType; -import org.springframework.security.test.context.support.WithMockUser; -import org.springframework.test.web.servlet.MockMvc; -import se.sowl.stitchapi.exception.MajorException; -import se.sowl.stitchapi.major.dto.request.MajorRequest; -import se.sowl.stitchapi.major.dto.response.MajorDetailResponse; -import se.sowl.stitchapi.major.dto.response.MajorListResponse; -import se.sowl.stitchapi.major.dto.response.MajorResponse; -import se.sowl.stitchapi.major.service.MajorService; - -import java.util.Arrays; -import java.util.List; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; - -@WebMvcTest(MajorController.class) -class MajorControllerTest { - - @Autowired - private MockMvc mockMvc; - - @MockBean - private MajorService majorService; - - private List majorList; - - @BeforeEach - void setUp(){ - MajorListResponse major1 = new MajorListResponse(1L, "컴퓨터공학과", null); - MajorListResponse major2 = new MajorListResponse(2L, "전자공학과", null); - MajorListResponse major3 = new MajorListResponse(3L, "기계공학과", null); - - majorList = Arrays.asList(major1, major2, major3); - - } - - @Test - @DisplayName("GET /api/majors/list") - @WithMockUser - void getMajorList() throws Exception{ - //given - - //when - when(majorService.getAllMajors()).thenReturn(majorList); - - //then - mockMvc.perform(get("/api/majors/list") - .with(csrf()) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.code").value("SUCCESS")) - .andExpect(jsonPath("$.message").value("성공")) - .andExpect(jsonPath("$.result").isArray()) - .andExpect(jsonPath("$.result.length()").value(3)) - .andExpect(jsonPath("$.result[0].name").value("컴퓨터공학과")) - .andExpect(jsonPath("$.result[1].name").value("전자공학과")) - .andExpect(jsonPath("$.result[2].name").value("기계공학과")) - .andDo(print()); - } - - @Nested - @DisplayName("전공 상세 조회") - class getMajorDetail{ - - @Test - @DisplayName("GET /api/majors/detail - 전공 상세 조회 성공") - @WithMockUser - void getMajorDetailSuccess() throws Exception{ - //given - MajorDetailResponse majorDetail = new MajorDetailResponse(1L, "컴퓨터공학과", null); - - //when - when(majorService.getMajorDetail(1L)).thenReturn(majorDetail); - - //then - mockMvc.perform(get("/api/majors/detail") - .param("majorId", "1") - .with(csrf()) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.code").value("SUCCESS")) - .andExpect(jsonPath("$.message").value("성공")) - .andExpect(jsonPath("$.result.name").value("컴퓨터공학과")) - .andDo(print()); - } - - @Test - @DisplayName("GET /api/majors/detail - 전공 상세 조회 실패") - @WithMockUser - void getMajorDetailFail() throws Exception{ - //given - - //when - when(majorService.getMajorDetail(9999L)).thenThrow(new IllegalArgumentException("존재하지 않는 전공입니다.")); - - //then - mockMvc.perform(get("/api/majors/detail") - .param("majorId", "9999") - .with(csrf()) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.code").value("FAIL")) - .andExpect(jsonPath("$.message").value("존재하지 않는 전공입니다.")) - .andDo(print()); - } - } - - @Nested - @DisplayName("전공 선택") - @WithMockUser - class selectMajor{ - - @Test - @DisplayName("POST /api/majors/select - 전공 선택 성공") - void selectMajorSuccess() throws Exception{ - //given - MajorRequest majorRequest = new MajorRequest(1L, 1L); - - MajorResponse majorResponse = new MajorResponse(1L, "컴퓨터공학과", null); - - //when - when(majorService.selectMajor(any(MajorRequest.class))).thenReturn(majorResponse); - - //then - mockMvc.perform(post("/api/majors/select") - .with(csrf()) - .contentType(MediaType.APPLICATION_JSON) - .content(new ObjectMapper().writeValueAsString(majorRequest))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.code").value("SUCCESS")) - .andExpect(jsonPath("$.message").value("성공")) - .andExpect(jsonPath("$.result.name").value("컴퓨터공학과")) - .andDo(print()); - } - } -} \ No newline at end of file diff --git a/stitch-api/src/test/java/se/sowl/stitchapi/major/service/MajorServiceTest.java b/stitch-api/src/test/java/se/sowl/stitchapi/major/service/MajorServiceTest.java deleted file mode 100644 index bc83cd8..0000000 --- a/stitch-api/src/test/java/se/sowl/stitchapi/major/service/MajorServiceTest.java +++ /dev/null @@ -1,197 +0,0 @@ -package se.sowl.stitchapi.major.service; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.transaction.annotation.Transactional; -import se.sowl.stitchapi.exception.MajorException; -import se.sowl.stitchapi.major.dto.request.MajorRequest; -import se.sowl.stitchapi.major.dto.response.MajorDetailResponse; -import se.sowl.stitchapi.major.dto.response.MajorListResponse; -import se.sowl.stitchapi.major.dto.response.MajorResponse; -import se.sowl.stitchdomain.school.domain.Campus; -import se.sowl.stitchdomain.school.domain.Major; -import se.sowl.stitchdomain.school.repository.CampusRepository; -import se.sowl.stitchdomain.school.repository.MajorRepository; -import se.sowl.stitchdomain.user.domain.User; -import se.sowl.stitchdomain.user.domain.UserCamInfo; -import se.sowl.stitchdomain.user.repository.UserCamInfoRepository; -import se.sowl.stitchdomain.user.repository.UserRepository; - -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; - -@SpringBootTest -@Transactional -class MajorServiceTest { - - @Autowired - private MajorService majorService; - - @Autowired - private MajorRepository majorRepository; - - @Autowired - private UserRepository userRepository; - - @Autowired - private UserCamInfoRepository userCamInfoRepository; - - @Autowired - private CampusRepository campusRepository; - - private User testUser; - - private Campus testCampus; - - private UserCamInfo testUserCamInfo; - - private Major testMajor; - - private Major anotherMajor; - - @BeforeEach - void setUp() { - testMajor = Major.builder() - .name("컴퓨터공학과") - .build(); - testMajor = majorRepository.save(testMajor); - - anotherMajor = Major.builder() - .name("경영학과") - .build(); - anotherMajor = majorRepository.save(anotherMajor); - - testCampus = Campus.builder() - .name("테스트 캠퍼스") - .domain("test.ac.kr") - .build(); - testCampus = campusRepository.save(testCampus); - - testUser = User.builder() - .name("테스트 유저") - .email("test@email.com") - .nickname("test") - .provider("kakao") - .campusCertified(true) - .build(); - testUser = userRepository.save(testUser); - - testUserCamInfo = UserCamInfo.builder() - .user(testUser) - .campus(testCampus) - .campusEmail("test@skhu.ac.kr") - .build(); - testUserCamInfo = userCamInfoRepository.save(testUserCamInfo); - } - - - @Nested - @DisplayName("모든 전공 조회 테스트") - class GetAllMajorsTest{ - @Test - @DisplayName("모든 전공 조회 성공") - void getAllMajorsSuccess(){ - majorRepository.deleteAll(); - - //given - for (int i = 1; i <= 5; i++) { - Major major = Major.builder() - .name("테스트 전공" + i) - .build(); - majorRepository.save(major); - } - - //when - List majorResponses = majorService.getAllMajors(); - - //then - assertEquals(5, majorResponses.size()); - } - } - - @Nested - @DisplayName("전공 상세 조회 테스트") - class GetMajorDetailTest{ - @Test - @DisplayName("전공 상세 조회 성공") - void getMajorDetailSuccess(){ - //given - Long majorId = testMajor.getId(); - - //when - MajorDetailResponse majorResponse = majorService.getMajorDetail(majorId); - - //then - assertEquals(testMajor.getName(), majorResponse.getName()); - } - - @Test - @DisplayName("존재하지 않는 전공 조회 시 예외 발생") - void getMajorDetailFail(){ - //given - Long nonExistentMajorId = 99999L; - - //when & then - assertThrows(MajorException.MajorNotFoundException.class, () -> majorService.getMajorDetail(nonExistentMajorId)); - } - } - - @Nested - @DisplayName("전공 선택 테스트") - class SelectMajorTest{ - - @Test - @DisplayName("처음 전공 선택 성공") - void selectFirstMajorSuccess(){ - //given - MajorRequest request = new MajorRequest(testMajor.getId(), testUser.getId()); - - //when - MajorResponse response = majorService.selectMajor(request); - - //then - assertNotNull(response); - assertEquals(testMajor.getId(), response.getId()); - assertEquals(testMajor.getName(), response.getName()); - - // DB에 실제로 반영되었는지 확인 - UserCamInfo savedUserCamInfo = userCamInfoRepository.findByUser(testUser) - .orElseThrow(); - assertNotNull(savedUserCamInfo.getMajor()); - assertEquals(testMajor.getId(), savedUserCamInfo.getMajor().getId()); - } - - @Test - @DisplayName("이미 선택한 전공이 있는 경우 전공 변경 성공") - void changeMajorSuccess(){ - //given - testUserCamInfo.setMajor(testMajor); - userCamInfoRepository.save(testUserCamInfo); - - MajorRequest request = new MajorRequest(anotherMajor.getId(), testUser.getId()); - - //when - MajorResponse response = majorService.selectMajor(request); - - //then - assertNotNull(response); - assertEquals(anotherMajor.getId(), response.getId()); - assertEquals(anotherMajor.getName(), response.getName()); - - // DB에 실제로 반영되었는지 확인 - UserCamInfo savedUserCamInfo = userCamInfoRepository.findByUser(testUser) - .orElseThrow(); - assertNotNull(savedUserCamInfo.getMajor()); - assertEquals(anotherMajor.getId(), savedUserCamInfo.getMajor().getId()); - - // 이전 전공과의 관계가 제거되었는지 확인 - Major oldMajor = majorRepository.findById(testMajor.getId()).orElseThrow(); - assertFalse(oldMajor.getUserCamInfos().contains(savedUserCamInfo)); - } - } -} \ No newline at end of file diff --git a/stitch-domain/src/main/java/se/sowl/stitchdomain/user/domain/CustomOAuth2User.java b/stitch-domain/src/main/java/se/sowl/stitchdomain/user/domain/CustomOAuth2User.java index f47dd35..292cc0f 100644 --- a/stitch-domain/src/main/java/se/sowl/stitchdomain/user/domain/CustomOAuth2User.java +++ b/stitch-domain/src/main/java/se/sowl/stitchdomain/user/domain/CustomOAuth2User.java @@ -32,4 +32,17 @@ public Long getUserId() { } public String getEmail() {return user.getEmail();} + + // 추가적인 사용자 정보 메서드 + public Long getUserCamInfoId() { + if (user.getUserCamInfo() != null) { + return user.getUserCamInfo().getId(); + } + return null; + } + + // 추가적인 사용자 정보 메서드(학교 인증 관련) + public boolean isCampusCertified() { + return user.isCampusCertified(); + } }