createField(@AuthenticationPrincipal User user,
}
/**
- * 분야 수정 API
- *
- * ADMIN계정만 호출 가능 -> 분야를 수정.
+ *
+ * 분야명을 수정합니다.
*
- * @param fieldIdx 분야 인덱스
- * @param updateFieldRequest 분야 수정 요청 정보
- *
- * @return 분야 수정 결과를 포함하는 BaseResponse
+ * @param user 현재 인증된 관리자 정보
+ * @param fieldIdx 수정할 분야의 식별자
+ * @param updateFieldRequest 새로운 분야명
+ * @return 분야명 수정 결과 메시지
+ * @throws BaseException FIELD_NOT_FOUND: 분야를 찾을 수 없는 경우
*/
@PutMapping("/{fieldIdx}")
@PreAuthorize("hasAuthority('admin:update')")
@@ -82,6 +79,15 @@ public BaseResponse updateField(@AuthenticationPrincipal User user,
return BaseResponse.of(FIELD_UPDATE_OK, fieldService.updateField(user, fieldIdx, updateFieldRequest));
}
+ /**
+ *
+ * 분야를 삭제(비활성화) 처리합니다.
+ *
+ * @param user 현재 인증된 관리자 정보
+ * @param fieldIdx 삭제할 분야의 식별자
+ * @return 분야 삭제 결과 메시지
+ * @throws BaseException FIELD_NOT_FOUND: 분야를 찾을 수 없는 경우
+ */
@DeleteMapping("/{fieldIdx}")
@PreAuthorize("hasAuthority('admin:delete')")
@Operation(summary = "분야 삭제(관리자 전용) API", description = "분야를 삭제합니다.")
diff --git a/src/main/java/inha/git/field/api/service/FieldService.java b/src/main/java/inha/git/field/api/service/FieldService.java
index 06a3b5f1..4a16c105 100644
--- a/src/main/java/inha/git/field/api/service/FieldService.java
+++ b/src/main/java/inha/git/field/api/service/FieldService.java
@@ -10,9 +10,6 @@
public interface FieldService {
List getFields();
String createField(User admin, CreateFieldRequest createFieldRequest);
-
String updateField(User admin, Integer fieldIdx, UpdateFieldRequest updateFieldRequest);
-
-
String deleteField(User admin, Integer fieldIdx);
}
diff --git a/src/main/java/inha/git/field/api/service/FieldServiceImpl.java b/src/main/java/inha/git/field/api/service/FieldServiceImpl.java
index 4b6160e3..75608cbd 100644
--- a/src/main/java/inha/git/field/api/service/FieldServiceImpl.java
+++ b/src/main/java/inha/git/field/api/service/FieldServiceImpl.java
@@ -20,7 +20,8 @@
import static inha.git.common.code.status.ErrorStatus.FIELD_NOT_FOUND;
/**
- * FieldServiceImpl는 FieldService 인터페이스를 구현하는 클래스.
+ * FieldService 인터페이스를 구현하는 서비스 클래스입니다.
+ * 분야의 조회, 생성, 수정, 삭제 등의 비즈니스 로직을 처리합니다.
*/
@Service
@RequiredArgsConstructor
@@ -32,9 +33,9 @@ public class FieldServiceImpl implements FieldService {
private final FieldMapper fieldMapper;
/**
- * 분야 전체 조회
+ * 활성화된 모든 분야를 조회합니다.
*
- * @return 분야 전체 조회 결과
+ * @return 분야 정보 목록 (SearchFieldResponse)
*/
@Override
public List getFields() {
@@ -42,10 +43,11 @@ public List getFields() {
}
/**
- * 분야 생성
+ * 새로운 분야를 생성합니다.
*
- * @param createFieldRequest 분야 생성 요청
- * @return 생성된 분야 이름
+ * @param admin 생성을 요청한 관리자 정보
+ * @param createFieldRequest 생성할 분야 정보
+ * @return 분야 생성 완료 메시지
*/
@Override
@Transactional
@@ -57,11 +59,13 @@ public String createField(User admin, CreateFieldRequest createFieldRequest) {
}
/**
- * 분야 이름 변경
+ * 분야명을 수정합니다.
*
- * @param fieldIdx 분야 인덱스
- * @param updateFieldRequest 분야 이름 변경 요청
- * @return 변경된 분야 이름
+ * @param admin 수정을 요청한 관리자 정보
+ * @param fieldIdx 수정할 분야의 식별자
+ * @param updateFieldRequest 새로운 분야명 정보
+ * @return 분야명 수정 완료 메시지
+ * @throws BaseException FIELD_NOT_FOUND: 분야를 찾을 수 없는 경우
*/
@Override
public String updateField(User admin, Integer fieldIdx, UpdateFieldRequest updateFieldRequest) {
@@ -73,10 +77,12 @@ public String updateField(User admin, Integer fieldIdx, UpdateFieldRequest updat
}
/**
- * 분야 삭제
+ * 분야를 삭제(비활성화) 처리합니다.
*
- * @param fieldIdx 분야 인덱스
- * @return 삭제된 분야 이름
+ * @param admin 삭제를 요청한 관리자 정보
+ * @param fieldIdx 삭제할 분야의 식별자
+ * @return 분야 삭제 완료 메시지
+ * @throws BaseException FIELD_NOT_FOUND: 분야를 찾을 수 없는 경우
*/
@Override
@Transactional
diff --git a/src/main/java/inha/git/mapping/domain/repository/ProjectLikeJpaRepository.java b/src/main/java/inha/git/mapping/domain/repository/ProjectLikeJpaRepository.java
index 5faa2f76..346819fd 100644
--- a/src/main/java/inha/git/mapping/domain/repository/ProjectLikeJpaRepository.java
+++ b/src/main/java/inha/git/mapping/domain/repository/ProjectLikeJpaRepository.java
@@ -5,8 +5,13 @@
import inha.git.mapping.domain.id.ProjectLikeId;
import inha.git.project.domain.Project;
import inha.git.user.domain.User;
+import jakarta.persistence.LockModeType;
import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Lock;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+import java.util.Optional;
/**
@@ -17,4 +22,6 @@ public interface ProjectLikeJpaRepository extends JpaRepository공지를 조회.
- *
- * @param page 페이지 번호
- * @param size 페이지 사이즈
- * @return 공지 조회 결과를 포함하는 BaseResponse>
+ * @param page 조회할 페이지 번호 (1부터 시작)
+ * @param size 페이지당 항목 수
+ * @return 페이징된 공지사항 목록
+ * @throws BaseException INVALID_PAGE: 페이지 번호가 유효하지 않은 경우
+ * INVALID_SIZE: 페이지 크기가 유효하지 않은 경우
*/
@GetMapping
@Operation(summary = "공지 조회 API", description = "공지를 조회합니다.")
public BaseResponse> getNotices(@RequestParam("page") Integer page, @RequestParam("size") Integer size) {
- if (page < 1) {
- throw new BaseException(INVALID_PAGE);
- }
- if (size < 1) {
- throw new BaseException(INVALID_PAGE);
- }
- return BaseResponse.of(NOTICE_SEARCH_OK, noticeService.getNotices(page - 1, size - 1));
+ pagingUtils.validatePage(page);
+ pagingUtils.validateSize(size);
+ return BaseResponse.of(NOTICE_SEARCH_OK, noticeService.getNotices(pagingUtils.toPageIndex(page), pagingUtils.toPageSize(size)));
}
/**
- * 공지 상세 조회 API
- *
- * 공지를 상세 조회.
- *
- * @param noticeIdx 공지 인덱스
+ * 특정 공지사항의 상세 정보를 조회합니다.
*
- * @return 공지 상세 조회 결과를 포함하는 BaseResponse
+ * @param noticeIdx 조회할 공지사항의 식별자
+ * @return 공지사항 상세 정보
+ * @throws BaseException NOTICE_NOT_FOUND: 공지사항을 찾을 수 없는 경우
*/
@GetMapping("/{noticeIdx}")
@Operation(summary = "공지 상세 조회 API", description = "공지를 상세 조회합니다.")
@@ -74,15 +71,12 @@ public BaseResponse getNotice(@PathVariable("noticeIdx") I
}
/**
- * 공지 생성 API
- *
- * 조교, 교수, 관리자만 호출 가능 -> 공지를 생성.
- *
- * @param user 로그인한 사용자 정보
- * @param createNoticeRequest 공지 생성 요청 정보
- * @param attachmentList 첨부파일 리스트
+ * 새로운 공지사항을 생성합니다.
*
- * @return 공지 생성 결과를 포함하는 BaseResponse
+ * @param user 현재 인증된 사용자 정보
+ * @param createNoticeRequest 생성할 공지사항 정보 (제목, 내용)
+ * @param attachmentList 첨부파일 목록 (선택적)
+ * @return 공지사항 생성 결과 메시지
*/
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@PreAuthorize("hasAuthority('assistant:create')")
@@ -95,16 +89,15 @@ public BaseResponse createNotice(@AuthenticationPrincipal User user,
}
/**
- * 공지 수정 API
+ * 기존 공지사항을 수정합니다.
*
- * 조교, 교수, 관리자만 호출 가능 -> 공지를 수정.
- *
- * @param user 로그인한 사용자 정보
- * @param noticeIdx 공지 인덱스
- * @param updateNoticeRequest 공지 수정 요청 정보
- * @param attachmentList 첨부파일 리스트
- *
- * @return 공지 수정 결과를 포함하는 BaseResponse
+ * @param user 현재 인증된 사용자 정보
+ * @param noticeIdx 수정할 공지사항의 식별자
+ * @param updateNoticeRequest 수정할 내용 (제목, 내용)
+ * @param attachmentList 새로운 첨부파일 목록 (선택적)
+ * @return 공지사항 수정 결과 메시지
+ * @throws BaseException NOTICE_NOT_FOUND: 공지사항을 찾을 수 없는 경우
+ * NOTICE_NOT_AUTHORIZED: 수정 권한이 없는 경우
*/
@PutMapping(value = "/{noticeIdx}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@PreAuthorize("hasAuthority('assistant:update')")
@@ -118,14 +111,13 @@ public BaseResponse updateNotice(@AuthenticationPrincipal User user,
}
/**
- * 공지 삭제 API
- *
- * 조교, 교수, 관리자만 호출 가능 -> 공지를 삭제.
- *
- * @param user 로그인한 사용자 정보
- * @param noticeIdx 공지 인덱스
+ * 공지사항을 삭제(비활성화) 처리합니다.
*
- * @return 공지 삭제 결과를 포함하는 BaseResponse
+ * @param user 현재 인증된 사용자 정보
+ * @param noticeIdx 삭제할 공지사항의 식별자
+ * @return 공지사항 삭제 결과 메시지
+ * @throws BaseException NOTICE_NOT_FOUND: 공지사항을 찾을 수 없는 경우
+ * NOTICE_NOT_AUTHORIZED: 삭제 권한이 없는 경우
*/
@DeleteMapping("/{noticeIdx}")
@PreAuthorize("hasAuthority('assistant:delete')")
diff --git a/src/main/java/inha/git/notice/api/service/NoticeServiceImpl.java b/src/main/java/inha/git/notice/api/service/NoticeServiceImpl.java
index f5e763b1..d4d4f381 100644
--- a/src/main/java/inha/git/notice/api/service/NoticeServiceImpl.java
+++ b/src/main/java/inha/git/notice/api/service/NoticeServiceImpl.java
@@ -38,7 +38,8 @@
import static inha.git.common.code.status.ErrorStatus.*;
/**
- * NoticeServiceImpl는 NoticeService 인터페이스를 구현하는 클래스.
+ * 공지사항 관련 비즈니스 로직을 처리하는 서비스 구현체입니다.
+ * 공지사항의 조회, 생성, 수정, 삭제 및 첨부파일 관리 기능을 제공합니다.
*/
@Service
@RequiredArgsConstructor
@@ -55,11 +56,11 @@ public class NoticeServiceImpl implements NoticeService {
/**
- * 공지 조회
+ * 공지사항 목록을 페이징하여 조회합니다.
*
- * @param page 페이지 번호
- * @param size 페이지 사이즈
- * @return 공지 페이지
+ * @param page 조회할 페이지 번호 (0부터 시작)
+ * @param size 페이지당 항목 수
+ * @return 페이징된 공지사항 목록
*/
@Override
@Transactional(readOnly = true)
@@ -68,6 +69,14 @@ public Page getNotices(Integer page, Integer size) {
return noticeQueryRepository.getNotices(pageable);
}
+ /**
+ * 특정 공지사항의 상세 정보를 조회합니다.
+ *
+ * @param noticeIdx 조회할 공지사항 ID
+ * @return 공지사항 상세 정보
+ * @throws BaseException NOT_FIND_USER: 작성자를 찾을 수 없는 경우
+ * NOTICE_NOT_FOUND: 공지사항을 찾을 수 없는 경우
+ */
@Override
@Transactional(readOnly = true)
public SearchNoticeResponse getNotice(Integer noticeIdx) {
@@ -81,12 +90,12 @@ public SearchNoticeResponse getNotice(Integer noticeIdx) {
}
/**
- * 공지 생성
+ * 새로운 공지사항을 생성합니다.
*
- * @param user 사용자
- * @param createNoticeRequest 공지 생성 요청
- * @param attachmentList 첨부 파일 리스트
- * @return 생성된 공지 이름
+ * @param user 생성을 요청한 사용자 정보
+ * @param createNoticeRequest 생성할 공지사항 정보
+ * @param attachmentList 첨부파일 목록 (선택적)
+ * @return 공지사항 생성 완료 메시지
*/
@Override
public String createNotice(User user, CreateNoticeRequest createNoticeRequest, List attachmentList) {
@@ -115,15 +124,15 @@ public String createNotice(User user, CreateNoticeRequest createNoticeRequest, L
}
/**
- * 공지 수정
- *
- * 관리자는 모든 공지를 수정할 수 있고, 공지 작성자는 자신의 공지만 수정할 수 있습니다.
+ * 기존 공지사항을 수정합니다.
*
- * @param user 사용자
- * @param noticeIdx 공지 인덱스
- * @param updateNoticeRequest 공지 수정 요청
- * @param attachmentList 첨부 파일 리스트
- * @return 수정된 공지 이름
+ * @param user 수정을 요청한 사용자 정보
+ * @param noticeIdx 수정할 공지사항 ID
+ * @param updateNoticeRequest 수정할 내용
+ * @param attachmentList 새로운 첨부파일 목록 (선택적)
+ * @return 공지사항 수정 완료 메시지
+ * @throws BaseException NOTICE_NOT_FOUND: 공지사항을 찾을 수 없는 경우
+ * NOTICE_NOT_AUTHORIZED: 수정 권한이 없는 경우
*/
@Override
public String updateNotice(User user, Integer noticeIdx, UpdateNoticeRequest updateNoticeRequest, List attachmentList) {
@@ -168,13 +177,13 @@ public String updateNotice(User user, Integer noticeIdx, UpdateNoticeRequest upd
}
/**
- * 공지 삭제
- *
- * 관리자는 모든 공지를 삭제할 수 있고, 공지 작성자는 자신의 공지만 삭제할 수 있습니다.
+ * 공지사항을 삭제(비활성화) 처리합니다.
*
- * @param user 사용자
- * @param noticeIdx 공지 인덱스
- * @return 삭제된 공지 이름
+ * @param user 삭제를 요청한 사용자 정보
+ * @param noticeIdx 삭제할 공지사항 ID
+ * @return 공지사항 삭제 완료 메시지
+ * @throws BaseException NOTICE_NOT_FOUND: 공지사항을 찾을 수 없는 경우
+ * NOTICE_NOT_AUTHORIZED: 삭제 권한이 없는 경우
*/
@Override
public String deleteNotice(User user, Integer noticeIdx) {
@@ -212,6 +221,11 @@ private Notice findNotice(Integer noticeIdx) {
.orElseThrow(() -> new BaseException(NOTICE_NOT_FOUND));
}
+ /**
+ * 트랜잭션 롤백 시 파일 삭제 로직 등록
+ *
+ * @param zipFilePath 파일 경로
+ */
private void registerRollbackCleanup(String zipFilePath) {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
diff --git a/src/main/java/inha/git/notice/domain/Notice.java b/src/main/java/inha/git/notice/domain/Notice.java
index 281b5f12..7db9a0d0 100644
--- a/src/main/java/inha/git/notice/domain/Notice.java
+++ b/src/main/java/inha/git/notice/domain/Notice.java
@@ -53,7 +53,7 @@ public void setUser(User user) {
user.getNotices().add(this); // 양방향 연관관계 설정
}
- public void setNoticeAttachments(ArrayList noticeAttachments) {
+ public void setNoticeAttachments(List noticeAttachments) {
this.noticeAttachments = noticeAttachments;
noticeAttachments.forEach(noticeAttachment -> noticeAttachment.setNotice(this)); // 양방향 연관관계 설정
}
diff --git a/src/main/java/inha/git/project/api/service/ProjectCommentServiceImpl.java b/src/main/java/inha/git/project/api/service/ProjectCommentServiceImpl.java
index cd6b52bb..422155f5 100644
--- a/src/main/java/inha/git/project/api/service/ProjectCommentServiceImpl.java
+++ b/src/main/java/inha/git/project/api/service/ProjectCommentServiceImpl.java
@@ -23,6 +23,8 @@
import inha.git.utils.IdempotentProvider;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.dao.DataIntegrityViolationException;
+import org.springframework.dao.PessimisticLockingFailureException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -251,23 +253,17 @@ public ReplyCommentResponse deleteReply(User user, Integer replyCommentIdx) {
*/
@Override
public String projectCommentLike(User user, CommentLikeRequest commentLikeRequest) {
-
- idempotentProvider.isValidIdempotent(List.of("projectCommentLike", user.getId().toString(), user.getName(), commentLikeRequest.idx().toString()));
-
-
- ProjectComment projectComment = getProjectComment(commentLikeRequest);
- Project project = projectComment.getProject();
-
- if (!hasAccessToProject(project, user)) {
- throw new BaseException(PROJECT_NOT_PUBLIC);
+ ProjectComment projectComment = getProjectComment(user, commentLikeRequest);
+ try {
+ validLike(projectComment, user, projectCommentLikeJpaRepository.existsByUserAndProjectComment(user, projectComment));
+ projectCommentLikeJpaRepository.save(projectMapper.createProjectCommentLike(user, projectComment));
+ projectComment.setLikeCount(projectComment.getLikeCount() + 1);
+ log.info("프로젝트 댓글 좋아요 완료 - 사용자: {} 프로젝트 댓글 식별자: {} 좋아요 개수: {}", user.getName(), commentLikeRequest.idx(), projectComment.getLikeCount());
+ return commentLikeRequest.idx() + "번 프로젝트 댓글 좋아요 완료";
+ }catch(DataIntegrityViolationException e) {
+ log.error("프로젝트 댓글 좋아요 중복 발생 - 사용자: {}, 프로젝트 ID: {}", user.getName(), commentLikeRequest.idx());
+ throw new BaseException(ALREADY_RECOMMENDED);
}
-
-
- validLike(projectComment, user, projectCommentLikeJpaRepository.existsByUserAndProjectComment(user, projectComment));
- projectCommentLikeJpaRepository.save(projectMapper.createProjectCommentLike(user, projectComment));
- projectComment.setLikeCount(projectComment.getLikeCount() + 1);
- log.info("프로젝트 댓글 좋아요 완료 - 사용자: {} 프로젝트 댓글 식별자: {} 좋아요 개수: {}", user.getName(), commentLikeRequest.idx(), projectComment.getLikeCount());
- return commentLikeRequest.idx() + "번 프로젝트 댓글 좋아요 완료";
}
/**
@@ -279,25 +275,20 @@ public String projectCommentLike(User user, CommentLikeRequest commentLikeReques
*/
@Override
public String projectCommentLikeCancel(User user, CommentLikeRequest commentLikeRequest) {
-
- idempotentProvider.isValidIdempotent(List.of("projectCommentLikeCancel", user.getId().toString(), user.getName(), commentLikeRequest.idx().toString()));
-
-
- ProjectComment projectComment = getProjectComment(commentLikeRequest);
-
- Project project = projectComment.getProject();
-
- if (!hasAccessToProject(project, user)) {
- throw new BaseException(PROJECT_NOT_PUBLIC);
+ ProjectComment projectComment = getProjectComment(user, commentLikeRequest);
+ try {
+ validLikeCancel(projectComment, user, projectCommentLikeJpaRepository.existsByUserAndProjectComment(user, projectComment));
+ projectCommentLikeJpaRepository.deleteByUserAndProjectComment(user, projectComment);
+ if (projectComment.getLikeCount() <= 0) {
+ projectComment.setLikeCount(0);
+ }
+ projectComment.setLikeCount(projectComment.getLikeCount() - 1);
+ log.info("프로젝트 댓글 좋아요 취소 완료 - 사용자: {} 프로젝트 댓글 식별자: {} 좋아요 개수: {}", user.getName(), commentLikeRequest.idx(), projectComment.getLikeCount());
+ return commentLikeRequest.idx() + "번 프로젝트 댓글 좋아요 취소 완료";
+ }catch (DataIntegrityViolationException e) {
+ log.error("프로젝트 댓글 좋아요 취소 중복 발생 - 사용자: {}, 프로젝트 ID: {}", user.getName(), commentLikeRequest.idx());
+ throw new BaseException(PROJECT_NOT_LIKE);
}
- validLikeCancel(projectComment, user, projectCommentLikeJpaRepository.existsByUserAndProjectComment(user, projectComment));
- projectCommentLikeJpaRepository.deleteByUserAndProjectComment(user, projectComment);
- if (projectComment.getLikeCount() <= 0) {
- projectComment.setLikeCount(0);
- }
- projectComment.setLikeCount(projectComment.getLikeCount() - 1);
- log.info("프로젝트 댓글 좋아요 취소 완료 - 사용자: {} 프로젝트 댓글 식별자: {} 좋아요 개수: {}", user.getName(), commentLikeRequest.idx(), projectComment.getLikeCount());
- return commentLikeRequest.idx() + "번 프로젝트 댓글 좋아요 취소 완료";
}
/**
@@ -309,22 +300,17 @@ public String projectCommentLikeCancel(User user, CommentLikeRequest commentLike
*/
@Override
public String projectReplyCommentLike(User user, CommentLikeRequest commentLikeRequest) {
- idempotentProvider.isValidIdempotent(List.of("projectReplyCommentLike", user.getId().toString(), user.getName(), commentLikeRequest.idx().toString()));
-
- ProjectReplyComment projectReplyComment = projectReplyCommentJpaRepository.findByIdAndState(commentLikeRequest.idx(), ACTIVE)
- .orElseThrow(() -> new BaseException(PROJECT_COMMENT_REPLY_NOT_FOUND));
-
- Project project = projectReplyComment.getProjectComment().getProject();
-
- if (!hasAccessToProject(project, user)) {
- throw new BaseException(PROJECT_NOT_PUBLIC);
+ ProjectReplyComment projectReplyComment = getProjectReplyComment(user, commentLikeRequest);
+ try {
+ validReplyLike(projectReplyComment, user, projectReplyCommentLikeJpaRepository.existsByUserAndProjectReplyComment(user, projectReplyComment));
+ projectReplyCommentLikeJpaRepository.save(projectMapper.createProjectReplyCommentLike(user, projectReplyComment));
+ projectReplyComment.setLikeCount(projectReplyComment.getLikeCount() + 1);
+ log.info("프로젝트 대댓글 좋아요 완료 - 사용자: {} 프로젝트 대댓글 식별자: {} 좋아요 개수: {}", user.getName(), commentLikeRequest.idx(), projectReplyComment.getLikeCount());
+ return commentLikeRequest.idx() + "번 프로젝트 대댓글 좋아요 완료";
+ } catch (DataIntegrityViolationException e) {
+ log.error("프로젝트 대댓글 좋아요 중복 발생 - 사용자: {}, 프로젝트 ID: {}", user.getName(), commentLikeRequest.idx());
+ throw new BaseException(ALREADY_RECOMMENDED);
}
-
- validReplyLike(projectReplyComment, user, projectReplyCommentLikeJpaRepository.existsByUserAndProjectReplyComment(user, projectReplyComment));
- projectReplyCommentLikeJpaRepository.save(projectMapper.createProjectReplyCommentLike(user, projectReplyComment));
- projectReplyComment.setLikeCount(projectReplyComment.getLikeCount() + 1);
- log.info("프로젝트 대댓글 좋아요 완료 - 사용자: {} 프로젝트 대댓글 식별자: {} 좋아요 개수: {}", user.getName(), commentLikeRequest.idx(), projectReplyComment.getLikeCount());
- return commentLikeRequest.idx() + "번 프로젝트 대댓글 좋아요 완료";
}
/**
@@ -336,37 +322,22 @@ public String projectReplyCommentLike(User user, CommentLikeRequest commentLikeR
*/
@Override
public String projectReplyCommentLikeCancel(User user, CommentLikeRequest commentLikeRequest) {
- idempotentProvider.isValidIdempotent(List.of("projectReplyCommentLikeCancel", user.getId().toString(), user.getName(), commentLikeRequest.idx().toString()));
-
- ProjectReplyComment projectReplyComment = projectReplyCommentJpaRepository.findByIdAndState(commentLikeRequest.idx(), ACTIVE)
- .orElseThrow(() -> new BaseException(PROJECT_COMMENT_REPLY_NOT_FOUND));
-
- Project project = projectReplyComment.getProjectComment().getProject();
-
- if (!hasAccessToProject(project, user)) {
- throw new BaseException(PROJECT_NOT_PUBLIC);
+ ProjectReplyComment projectReplyComment = getProjectReplyComment(user, commentLikeRequest);
+ try{
+ validReplyLikeCancel(projectReplyComment, user, projectReplyCommentLikeJpaRepository.existsByUserAndProjectReplyComment(user, projectReplyComment));
+ projectReplyCommentLikeJpaRepository.deleteByUserAndProjectReplyComment(user, projectReplyComment);
+ if (projectReplyComment.getLikeCount() <= 0) {
+ projectReplyComment.setLikeCount(0);
+ }
+ projectReplyComment.setLikeCount(projectReplyComment.getLikeCount() - 1);
+ log.info("프로젝트 대댓글 좋아요 취소 완료 - 사용자: {} 프로젝트 대댓글 식별자: {} 좋아요 개수: {}", user.getName(), commentLikeRequest.idx(), projectReplyComment.getLikeCount());
+ return commentLikeRequest.idx() + "번 프로젝트 대댓글 좋아요 취소 완료";
+ } catch (DataIntegrityViolationException e) {
+ log.error("프로젝트 대댓글 좋아요 취소 중복 발생 - 사용자: {}, 프로젝트 ID: {}", user.getName(), commentLikeRequest.idx());
+ throw new BaseException(PROJECT_NOT_LIKE);
}
-
- validReplyLikeCancel(projectReplyComment, user, projectReplyCommentLikeJpaRepository.existsByUserAndProjectReplyComment(user, projectReplyComment));
- projectReplyCommentLikeJpaRepository.deleteByUserAndProjectReplyComment(user, projectReplyComment);
- if (projectReplyComment.getLikeCount() <= 0) {
- projectReplyComment.setLikeCount(0);
- }
- projectReplyComment.setLikeCount(projectReplyComment.getLikeCount() - 1);
- log.info("프로젝트 대댓글 좋아요 취소 완료 - 사용자: {} 프로젝트 대댓글 식별자: {} 좋아요 개수: {}", user.getName(), commentLikeRequest.idx(), projectReplyComment.getLikeCount());
- return commentLikeRequest.idx() + "번 프로젝트 대댓글 좋아요 취소 완료";
}
-
-
-
- /**
- * 댓글 좋아요 정보 유효성 검사
- *
- * @param projectComment 댓글 정보
- * @param user 사용자 정보
- * @param commentLikeJpaRepository 댓글 좋아요 레포지토리
- */
private void validLike(ProjectComment projectComment, User user, boolean commentLikeJpaRepository) {
if (projectComment.getUser().getId().equals(user.getId())) {
log.error("프로젝트 댓글 좋아요 실패 - 사용자: {} 자신의 댓글에 좋아요를 할 수 없습니다.", user.getName());
@@ -378,13 +349,6 @@ private void validLike(ProjectComment projectComment, User user, boolean comment
}
}
- /**
- * 대댓글 좋아요 정보 유효성 검사
- *
- * @param projectReplyComment 대댓글 정보
- * @param user 사용자 정보
- * @param commentLikeJpaRepository 대댓글 좋아요 레포지토리
- */
private void validReplyLike(ProjectReplyComment projectReplyComment, User user, boolean commentLikeJpaRepository) {
if (projectReplyComment.getUser().getId().equals(user.getId())) {
log.error("프로젝트 대댓글 좋아요 실패 - 사용자: {} 자신의 대댓글에 좋아요를 할 수 없습니다.", user.getName());
@@ -396,13 +360,6 @@ private void validReplyLike(ProjectReplyComment projectReplyComment, User user,
}
}
- /**
- * 댓글 좋아요 취소
- *
- * @param user 사용자 정보
- * @param projectComment 좋아요 취소할 댓글 정보
- * @param commentLikeJpaRepository 댓글 좋아요 레포지토리
- */
private void validLikeCancel(ProjectComment projectComment, User user, boolean commentLikeJpaRepository) {
if (projectComment.getUser().getId().equals(user.getId())) {
log.error("프로젝트 댓글 좋아요 취소 실패 - 사용자: {} 자신의 댓글에 좋아요를 취소할 수 없습니다.", user.getName());
@@ -425,14 +382,35 @@ private void validReplyLikeCancel(ProjectReplyComment projectReplyComment, User
}
}
- /**
- * 댓글 좋아요 정보 조회
- *
- * @param commentLikeRequest 댓글 좋아요 정보
- * @return 댓글 좋아요 정보
- */
- private ProjectComment getProjectComment(CommentLikeRequest commentLikeRequest) {
- return projectCommentJpaRepository.findByIdAndState(commentLikeRequest.idx(), ACTIVE)
- .orElseThrow(() -> new BaseException(PROJECT_NOT_FOUND));
+
+ private ProjectComment getProjectComment(User user, CommentLikeRequest commentLikeRequest) {
+ ProjectComment projectComment;
+ try {
+ projectComment = projectCommentJpaRepository.findByIdAndStateWithPessimisticLock(commentLikeRequest.idx(), ACTIVE)
+ .orElseThrow(() -> new BaseException(PROJECT_COMMENT_NOT_FOUND));
+ } catch (PessimisticLockingFailureException e) {
+ log.error("프로젝트 댓글 추천 락 획득 실패 - 사용자: {}, 프로젝트 ID: {}", user.getName(), commentLikeRequest.idx());
+ throw new BaseException(TEMPORARY_UNAVAILABLE);
+ }
+
+ if (!hasAccessToProject(projectComment.getProject(), user)) {
+ throw new BaseException(PROJECT_NOT_PUBLIC);
+ }
+ return projectComment;
+ }
+
+ private ProjectReplyComment getProjectReplyComment(User user, CommentLikeRequest commentLikeRequest) {
+ ProjectReplyComment projectReplyComment;
+ try {
+ projectReplyComment = projectReplyCommentJpaRepository.findByIdAndStateWithPessimisticLock(commentLikeRequest.idx(), ACTIVE)
+ .orElseThrow(() -> new BaseException(PROJECT_COMMENT_REPLY_NOT_FOUND));
+ } catch (PessimisticLockingFailureException e) {
+ log.error("프로젝트 대댓글 추천 락 획득 실패 - 사용자: {}, 프로젝트 ID: {}", user.getName(), commentLikeRequest.idx());
+ throw new BaseException(TEMPORARY_UNAVAILABLE);
+ }
+ if (!hasAccessToProject(projectReplyComment.getProjectComment().getProject(), user)) {
+ throw new BaseException(PROJECT_NOT_PUBLIC);
+ }
+ return projectReplyComment;
}
}
diff --git a/src/main/java/inha/git/project/api/service/ProjectRecommendServiceImpl.java b/src/main/java/inha/git/project/api/service/ProjectRecommendServiceImpl.java
index 643664fd..db4f1a53 100644
--- a/src/main/java/inha/git/project/api/service/ProjectRecommendServiceImpl.java
+++ b/src/main/java/inha/git/project/api/service/ProjectRecommendServiceImpl.java
@@ -10,14 +10,13 @@
import inha.git.project.domain.Project;
import inha.git.project.domain.repository.ProjectJpaRepository;
import inha.git.user.domain.User;
-import inha.git.utils.IdempotentProvider;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.dao.DataIntegrityViolationException;
+import org.springframework.dao.PessimisticLockingFailureException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
-import java.util.List;
-
import static inha.git.common.BaseEntity.State.ACTIVE;
import static inha.git.common.Constant.hasAccessToProject;
import static inha.git.common.code.status.ErrorStatus.*;
@@ -36,7 +35,6 @@ public class ProjectRecommendServiceImpl implements ProjectRecommendService{
private final ProjectLikeJpaRepository projectLikeJpaRepository;
private final FoundingRecommendJpaRepository foundingRecommendJpaRepository;
private final RegistrationRecommendJpaRepository registrationRecommendJpaRepository;
- private final IdempotentProvider idempotentProvider;
/**
@@ -46,24 +44,24 @@ public class ProjectRecommendServiceImpl implements ProjectRecommendService{
* @param recommendRequest 추천할 프로젝트 정보
* @return 추천 성공 메시지
*/
+ @Transactional
@Override
public String createProjectFoundingRecommend(User user, RecommendRequest recommendRequest) {
-
- idempotentProvider.isValidIdempotent(List.of("createProjectFoundingRecommend", user.getId().toString(), user.getName(), recommendRequest.idx().toString()));
-
-
- Project project = getProject(recommendRequest);
-
- if (!hasAccessToProject(project, user)) {
- throw new BaseException(PROJECT_NOT_PUBLIC);
+ Project project = getProject(user, recommendRequest);
+ try {
+ validRecommend(project, user, foundingRecommendJpaRepository.existsByUserAndProject(user, project));
+ foundingRecommendJpaRepository.save(projectMapper.createProjectFoundingRecommend(user, project));
+ project.setFoundRecommendCount(project.getFoundingRecommendCount() + 1);
+ log.info("프로젝트 창업 추천 성공 - 사용자: {} 프로젝트 ID: {} 추천 개수: {}", user.getName(), recommendRequest.idx(), project.getFoundingRecommendCount());
+ return recommendRequest.idx() + "번 프로젝트 창업 추천 완료";
+ } catch (DataIntegrityViolationException e) {
+ log.error("프로젝트 창업 추천 중복 발생 - 사용자: {}, 프로젝트 ID: {}", user.getName(), recommendRequest.idx());
+ throw new BaseException(ALREADY_RECOMMENDED);
}
- validRecommend(project, user, foundingRecommendJpaRepository.existsByUserAndProject(user, project));
- foundingRecommendJpaRepository.save(projectMapper.createProjectFoundingRecommend(user, project));
- project.setFoundRecommendCount(project.getFoundingRecommendCount() + 1);
- log.info("프로젝트 창업 추천 - 사용자: {} 프로젝트 ID: {} 추천 개수: {}", user.getName(), recommendRequest.idx(), project.getFoundingRecommendCount());
- return recommendRequest.idx() + "번 프로젝트 창업 추천 완료";
}
+
+
/**
* 프로젝트 좋아요 추천
*
@@ -73,18 +71,17 @@ public String createProjectFoundingRecommend(User user, RecommendRequest recomme
*/
@Override
public String createProjectLike(User user, RecommendRequest recommendRequest) {
-
- idempotentProvider.isValidIdempotent(List.of("createProjectLike", user.getId().toString(), user.getName(), recommendRequest.idx().toString()));
-
- Project project = getProject(recommendRequest);
- if (!hasAccessToProject(project, user)) {
- throw new BaseException(PROJECT_NOT_PUBLIC);
+ Project project = getProject(user, recommendRequest);
+ try {
+ validLike(project, user, projectLikeJpaRepository.existsByUserAndProject(user, project));
+ projectLikeJpaRepository.save(projectMapper.createProjectLike(user, project));
+ project.setLikeCount(project.getLikeCount() + 1);
+ log.info("프로젝트 좋아요 - 사용자: {} 프로젝트 ID: {} 좋아요 개수: {}", user.getName(), recommendRequest.idx(), project.getLikeCount());
+ return recommendRequest.idx() + "번 프로젝트 창업 추천 완료";
+ } catch (DataIntegrityViolationException e) {
+ log.error("프로젝트 좋아요 추천 중복 발생 - 사용자: {}, 프로젝트 ID: {}", user.getName(), recommendRequest.idx());
+ throw new BaseException(ALREADY_RECOMMENDED);
}
- validLike(project, user, projectLikeJpaRepository.existsByUserAndProject(user, project));
- projectLikeJpaRepository.save(projectMapper.createProjectLike(user, project));
- project.setLikeCount(project.getLikeCount() + 1);
- log.info("프로젝트 좋아요 - 사용자: {} 프로젝트 ID: {} 좋아요 개수: {}", user.getName(), recommendRequest.idx(), project.getLikeCount());
- return recommendRequest.idx() + "번 프로젝트 좋아요 완료";
}
/**
@@ -96,19 +93,17 @@ public String createProjectLike(User user, RecommendRequest recommendRequest) {
*/
@Override
public String createProjectRegistrationRecommend(User user, RecommendRequest recommendRequest) {
-
- idempotentProvider.isValidIdempotent(List.of("createProjectRegistrationRecommend", user.getId().toString(), user.getName(), recommendRequest.idx().toString()));
-
-
- Project project = getProject(recommendRequest);
- if (!hasAccessToProject(project, user)) {
- throw new BaseException(PROJECT_NOT_PUBLIC);
+ Project project = getProject(user, recommendRequest);
+ try {
+ validRecommend(project, user, registrationRecommendJpaRepository.existsByUserAndProject(user, project));
+ registrationRecommendJpaRepository.save(projectMapper.createProjectRegistrationRecommend(user, project));
+ project.setRegistrationRecommendCount(project.getRegistrationRecommendCount() + 1);
+ log.info("프로젝트 등록 추천 - 사용자: {} 프로젝트 ID: {} 추천 개수: {}", user.getName(), recommendRequest.idx(), project.getRegistrationRecommendCount());
+ return recommendRequest.idx() + "번 프로젝트 등록 추천 완료";
+ } catch (DataIntegrityViolationException e) {
+ log.error("프로젝트 등록 추천 중복 발생 - 사용자: {}, 프로젝트 ID: {}", user.getName(), recommendRequest.idx());
+ throw new BaseException(ALREADY_RECOMMENDED);
}
- validRecommend(project, user, registrationRecommendJpaRepository.existsByUserAndProject(user, project));
- registrationRecommendJpaRepository.save(projectMapper.createProjectRegistrationRecommend(user, project));
- project.setRegistrationRecommendCount(project.getRegistrationRecommendCount() + 1);
- log.info("프로젝트 등록 추천 - 사용자: {} 프로젝트 ID: {} 추천 개수: {}", user.getName(), recommendRequest.idx(), project.getRegistrationRecommendCount());
- return recommendRequest.idx() + "번 프로젝트 등록 추천 완료";
}
/**
@@ -120,21 +115,20 @@ public String createProjectRegistrationRecommend(User user, RecommendRequest rec
*/
@Override
public String cancelProjectFoundingRecommend(User user, RecommendRequest recommendRequest) {
-
- idempotentProvider.isValidIdempotent(List.of("cancelProjectFoundingRecommend", user.getId().toString(), user.getName(), recommendRequest.idx().toString()));
-
- Project project = getProject(recommendRequest);
- if (!hasAccessToProject(project, user)) {
- throw new BaseException(PROJECT_NOT_PUBLIC);
- }
- validRecommendCancel(project, user, foundingRecommendJpaRepository.existsByUserAndProject(user, project));
- foundingRecommendJpaRepository.deleteByUserAndProject(user, project);
- if (project.getFoundingRecommendCount() <= 0) {
- project.setFoundRecommendCount(0);
+ Project project = getProject(user, recommendRequest);
+ try {
+ validRecommendCancel(project, user, foundingRecommendJpaRepository.existsByUserAndProject(user, project));
+ foundingRecommendJpaRepository.deleteByUserAndProject(user, project);
+ if (project.getFoundingRecommendCount() <= 0) {
+ project.setFoundRecommendCount(0);
+ }
+ project.setFoundRecommendCount(project.getFoundingRecommendCount() - 1);
+ log.info("프로젝트 창업 추천 취소 - 사용자: {} 프로젝트 ID: {} 추천 개수: {}", user.getName(), recommendRequest.idx(), project.getFoundingRecommendCount());
+ return recommendRequest.idx() + "번 프로젝트 창업 추천 취소 완료";
+ } catch (DataIntegrityViolationException e) {
+ log.error("프로젝트 창업 추천 취소 중복 발생 - 사용자: {}, 프로젝트 ID: {}", user.getName(), recommendRequest.idx());
+ throw new BaseException(PROJECT_NOT_LIKE);
}
- project.setFoundRecommendCount(project.getFoundingRecommendCount() - 1);
- log.info("프로젝트 창업 추천 취소 - 사용자: {} 프로젝트 ID: {} 추천 개수: {}", user.getName(), recommendRequest.idx(), project.getFoundingRecommendCount());
- return recommendRequest.idx() + "번 프로젝트 창업 추천 취소 완료";
}
/**
@@ -146,22 +140,20 @@ public String cancelProjectFoundingRecommend(User user, RecommendRequest recomme
*/
@Override
public String cancelProjectLike(User user, RecommendRequest recommendRequest) {
-
- idempotentProvider.isValidIdempotent(List.of("cancelProjectLike", user.getId().toString(), user.getName(), recommendRequest.idx().toString()));
-
-
- Project project = getProject(recommendRequest);
- if (!hasAccessToProject(project, user)) {
- throw new BaseException(PROJECT_NOT_PUBLIC);
- }
- validLikeCancel(project, user, projectLikeJpaRepository.existsByUserAndProject(user, project));
- projectLikeJpaRepository.deleteByUserAndProject(user, project);
- if (project.getLikeCount() <= 0) {
- project.setLikeCount(0);
+ Project project = getProject(user, recommendRequest);
+ try {
+ validLikeCancel(project, user, projectLikeJpaRepository.existsByUserAndProject(user, project));
+ projectLikeJpaRepository.deleteByUserAndProject(user, project);
+ if (project.getLikeCount() <= 0) {
+ project.setLikeCount(0);
+ }
+ project.setLikeCount(project.getLikeCount() - 1);
+ log.info("프로젝트 좋아요 취소 - 사용자: {} 프로젝트 ID: {} 좋아요 개수: {}", user.getName(), recommendRequest.idx(), project.getLikeCount());
+ return recommendRequest.idx() + "번 프로젝트 좋아요 취소 완료";
+ } catch (DataIntegrityViolationException e) {
+ log.error("프로젝트 좋아요 추천 취소 중복 발생 - 사용자: {}, 프로젝트 ID: {}", user.getName(), recommendRequest.idx());
+ throw new BaseException(PROJECT_NOT_LIKE);
}
- project.setLikeCount(project.getLikeCount() - 1);
- log.info("프로젝트 좋아요 취소 - 사용자: {} 프로젝트 ID: {} 좋아요 개수: {}", user.getName(), recommendRequest.idx(), project.getLikeCount());
- return recommendRequest.idx() + "번 프로젝트 좋아요 취소 완료";
}
/**
@@ -173,27 +165,22 @@ public String cancelProjectLike(User user, RecommendRequest recommendRequest) {
*/
@Override
public String cancelProjectRegistrationRecommend(User user, RecommendRequest recommendRequest) {
-
- idempotentProvider.isValidIdempotent(List.of("cancelProjectRegistrationRecommend", user.getId().toString(), user.getName(), recommendRequest.idx().toString()));
-
-
- Project project = getProject(recommendRequest);
- if (!hasAccessToProject(project, user)) {
- throw new BaseException(PROJECT_NOT_PUBLIC);
- }
- validRecommendCancel(project, user, registrationRecommendJpaRepository.existsByUserAndProject(user, project));
- registrationRecommendJpaRepository.deleteByUserAndProject(user, project);
- if (project.getRegistrationRecommendCount() <= 0) {
- project.setRegistrationRecommendCount(0);
+ Project project = getProject(user, recommendRequest);
+ try {
+ validRecommendCancel(project, user, registrationRecommendJpaRepository.existsByUserAndProject(user, project));
+ registrationRecommendJpaRepository.deleteByUserAndProject(user, project);
+ if (project.getRegistrationRecommendCount() <= 0) {
+ project.setRegistrationRecommendCount(0);
+ }
+ project.setRegistrationRecommendCount(project.getRegistrationRecommendCount() - 1);
+ log.info("프로젝트 등록 추천 취소 - 사용자: {} 프로젝트 ID: {} 추천 개수: {}", user.getName(), recommendRequest.idx(), project.getRegistrationRecommendCount());
+ return recommendRequest.idx() + "번 프로젝트 등록 추천 취소 완료";
+ } catch (DataIntegrityViolationException e) {
+ log.error("프로젝트 등록 추천 취소 중복 발생 - 사용자: {}, 프로젝트 ID: {}", user.getName(), recommendRequest.idx());
+ throw new BaseException(PROJECT_NOT_LIKE);
}
- project.setRegistrationRecommendCount(project.getRegistrationRecommendCount() - 1);
- log.info("프로젝트 등록 추천 취소 - 사용자: {} 프로젝트 ID: {} 추천 개수: {}", user.getName(), recommendRequest.idx(), project.getRegistrationRecommendCount());
- return recommendRequest.idx() + "번 프로젝트 등록 추천 취소 완료";
}
-
-
-
/**
* 추천할 프로젝트가 유효한지 확인
*
@@ -265,17 +252,27 @@ private void validLikeCancel(Project project, User user, boolean patentRecommend
throw new BaseException(PROJECT_NOT_LIKE);
}
}
+
/**
- * 추천할 프로젝트 정보 조회
+ * 프로젝트 정보 조회
*
+ * @param user 로그인한 사용자 정보
* @param recommendRequest 추천할 프로젝트 정보
- * @return 추천할 프로젝트 정보
+ * @return 프로젝트 정보
*/
- private Project getProject(RecommendRequest recommendRequest) {
- return projectJpaRepository.findByIdAndState(recommendRequest.idx(), ACTIVE)
- .orElseThrow(() -> new BaseException(PROJECT_NOT_FOUND));
+ private Project getProject(User user, RecommendRequest recommendRequest) {
+ Project project;
+ try {
+ project = projectJpaRepository.findByIdAndStateWithPessimisticLock(recommendRequest.idx(), ACTIVE)
+ .orElseThrow(() -> new BaseException(PROJECT_NOT_FOUND));
+ } catch (PessimisticLockingFailureException e) {
+ log.error("프로젝트 창업 추천 락 획득 실패 - 사용자: {}, 프로젝트 ID: {}",
+ user.getName(), recommendRequest.idx());
+ throw new BaseException(TEMPORARY_UNAVAILABLE);
+ }
+ if (!hasAccessToProject(project, user)) {
+ throw new BaseException(PROJECT_NOT_PUBLIC);
+ }
+ return project;
}
-
-
-
}
diff --git a/src/main/java/inha/git/project/domain/repository/ProjectCommentJpaRepository.java b/src/main/java/inha/git/project/domain/repository/ProjectCommentJpaRepository.java
index 0510264d..8c2cce98 100644
--- a/src/main/java/inha/git/project/domain/repository/ProjectCommentJpaRepository.java
+++ b/src/main/java/inha/git/project/domain/repository/ProjectCommentJpaRepository.java
@@ -4,7 +4,12 @@
import inha.git.project.domain.Project;
import inha.git.project.domain.ProjectComment;
import inha.git.user.domain.User;
+import jakarta.persistence.LockModeType;
+import jakarta.persistence.QueryHint;
import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Lock;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.jpa.repository.QueryHints;
import java.util.List;
import java.util.Optional;
@@ -19,6 +24,11 @@ public interface ProjectCommentJpaRepository extends JpaRepository findByIdAndState(Integer commentIdx, State state);
+ @Lock(LockModeType.PESSIMISTIC_WRITE)
+ @QueryHints({@QueryHint(name = "javax.persistence.lock.timeout", value = "3000")})
+ @Query("SELECT c FROM ProjectComment c WHERE c.id = :commentIdx AND c.state = :state")
+ Optional findByIdAndStateWithPessimisticLock(Integer commentIdx, State state);
+
List findAllByProjectAndStateOrderByIdAsc(Project project, State state);
}
diff --git a/src/main/java/inha/git/project/domain/repository/ProjectJpaRepository.java b/src/main/java/inha/git/project/domain/repository/ProjectJpaRepository.java
index 9f1632fe..56ac2314 100644
--- a/src/main/java/inha/git/project/domain/repository/ProjectJpaRepository.java
+++ b/src/main/java/inha/git/project/domain/repository/ProjectJpaRepository.java
@@ -3,14 +3,16 @@
import inha.git.common.BaseEntity;
import inha.git.field.domain.Field;
-import inha.git.mapping.domain.ProjectField;
import inha.git.project.domain.Project;
import inha.git.semester.domain.Semester;
import inha.git.user.domain.User;
+import jakarta.persistence.LockModeType;
+import jakarta.persistence.QueryHint;
import org.springframework.data.jpa.repository.JpaRepository;
-
-import java.util.Collection;
-import java.util.List;
+import org.springframework.data.jpa.repository.Lock;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.jpa.repository.QueryHints;
+import org.springframework.data.repository.query.Param;
import java.util.Optional;
@@ -23,4 +25,9 @@ public interface ProjectJpaRepository extends JpaRepository {
Optional findByIdAndState(Integer projectIdx, BaseEntity.State state);
long countByUserAndSemesterAndProjectFields_FieldAndState(User user, Semester semester, Field field, BaseEntity.State state);
+
+ @Lock(LockModeType.PESSIMISTIC_WRITE)
+ @QueryHints({@QueryHint(name = "javax.persistence.lock.timeout", value = "3000")})
+ @Query("SELECT p FROM Project p WHERE p.id = :id AND p.state = :state")
+ Optional findByIdAndStateWithPessimisticLock(@Param("id") Integer id, @Param("state") BaseEntity.State state);
}
diff --git a/src/main/java/inha/git/project/domain/repository/ProjectReplyCommentJpaRepository.java b/src/main/java/inha/git/project/domain/repository/ProjectReplyCommentJpaRepository.java
index 32231e8d..254519f8 100644
--- a/src/main/java/inha/git/project/domain/repository/ProjectReplyCommentJpaRepository.java
+++ b/src/main/java/inha/git/project/domain/repository/ProjectReplyCommentJpaRepository.java
@@ -5,7 +5,12 @@
import inha.git.common.BaseEntity.State;
import inha.git.project.domain.ProjectComment;
import inha.git.project.domain.ProjectReplyComment;
+import jakarta.persistence.LockModeType;
+import jakarta.persistence.QueryHint;
import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Lock;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.jpa.repository.QueryHints;
import java.util.Arrays;
import java.util.List;
@@ -20,6 +25,12 @@ public interface ProjectReplyCommentJpaRepository extends JpaRepository findByIdAndState(Integer replyCommentIdx, State state);
+ @Lock(LockModeType.PESSIMISTIC_WRITE)
+ @QueryHints({@QueryHint(name = "javax.persistence.lock.timeout", value = "3000")})
+ @Query("SELECT c FROM ProjectReplyComment c WHERE c.id = :replyCommentIdx AND c.state = :state")
+ Optional findByIdAndStateWithPessimisticLock(Integer replyCommentIdx, State state);
+
boolean existsByProjectCommentAndState(ProjectComment projectComment, State state);
+
}
diff --git a/src/main/java/inha/git/question/api/controller/QuestionController.java b/src/main/java/inha/git/question/api/controller/QuestionController.java
index f11ad90b..72d67bbc 100644
--- a/src/main/java/inha/git/question/api/controller/QuestionController.java
+++ b/src/main/java/inha/git/question/api/controller/QuestionController.java
@@ -12,6 +12,7 @@
import inha.git.question.api.service.QuestionService;
import inha.git.user.domain.User;
import inha.git.user.domain.enums.Role;
+import inha.git.utils.PagingUtils;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
@@ -26,7 +27,8 @@
import static inha.git.common.code.status.SuccessStatus.*;
/**
- * QuestionController는 question 관련 엔드포인트를 처리.
+ * 질문 관련 API를 처리하는 컨트롤러입니다.
+ * 질문의 조회, 생성, 수정, 삭제 및 좋아요 기능을 제공합니다.
*/
@Slf4j
@Tag(name = "question controller", description = "question 관련 API")
@@ -36,34 +38,29 @@
public class QuestionController {
private final QuestionService questionService;
+ private final PagingUtils pagingUtils;
/**
- * 질문 전체 조회 API
+ * 전체 질문을 페이징하여 조회합니다.
*
- * 질문 전체를 조회합니다.
- *
- * @param page Integer
- * @param size Integer
- * @return 검색된 질문 정보를 포함하는 BaseResponse>
+ * @param page 조회할 페이지 번호 (1부터 시작)
+ * @param size 페이지당 항목 수
+ * @return 페이징된 질문 목록
+ * @throws BaseException INVALID_PAGE: 페이지 번호가 유효하지 않은 경우
+ * INVALID_SIZE: 페이지 크기가 유효하지 않은 경우
*/
@GetMapping
@Operation(summary = "질문 전체 조회 API", description = "질문 전체를 조회합니다.")
public BaseResponse> getQuestions(@RequestParam("page") Integer page, @RequestParam("size") Integer size) {
- if (page < 1) {
- throw new BaseException(INVALID_PAGE);
- }
- if (size < 1) {
- throw new BaseException(INVALID_SIZE);
- }
- return BaseResponse.of(QUESTION_SEARCH_OK, questionService.getQuestions(page - 1, size - 1));
+ pagingUtils.validatePage(page);
+ pagingUtils.validateSize(size);
+ return BaseResponse.of(QUESTION_SEARCH_OK, questionService.getQuestions(pagingUtils.toPageIndex(page), pagingUtils.toPageSize(size)));
}
/**
* 질문 조건 조회 API
*
- * 질문 조건에 맞게 조회합니다.
- *
* @param page Integer
* @param size Integer
* @param searchQuestionCond SearchQuestionCond
@@ -72,13 +69,9 @@ public BaseResponse> getQuestions(@RequestParam("p
@GetMapping("/cond")
@Operation(summary = "질문 조건 조회 API", description = "질문 조건에 맞게 조회합니다.")
public BaseResponse> getCondQuestions(@RequestParam("page") Integer page, @RequestParam("size") Integer size , SearchQuestionCond searchQuestionCond) {
- if (page < 1) {
- throw new BaseException(INVALID_PAGE);
- }
- if (size < 1) {
- throw new BaseException(INVALID_SIZE);
- }
- return BaseResponse.of(QUESTION_SEARCH_OK, questionService.getCondQuestions(searchQuestionCond, page - 1, size - 1));
+ pagingUtils.validatePage(page);
+ pagingUtils.validateSize(size);
+ return BaseResponse.of(QUESTION_SEARCH_OK, questionService.getCondQuestions(searchQuestionCond, pagingUtils.toPageIndex(page), pagingUtils.toPageSize(size)));
}
/**
@@ -98,8 +91,6 @@ public BaseResponse getQuestion(@AuthenticationPrincipal
/**
* 질문 생성(기업제외) API
*
- * 질문을 생성합니다.
- *
* @param user User
* @param createQuestionRequest CreateQuestionRequest
* @return 생성된 질문 정보를 포함하는 BaseResponse
@@ -120,8 +111,6 @@ public BaseResponse createQuestion(
/**
* 질문 수정 API
*
- * 질문을 수정합니다.
- *
* @param user User
* @param questionIdx Integer
* @param updateQuestionRequest UpdateQuestionRequest
@@ -140,8 +129,6 @@ public BaseResponse updateQuestion(
/**
* 질문 삭제 API
*
- * 질문을 삭제합니다.
- *
* @param user User
* @param questionIdx Integer
* @return 삭제된 질문 정보를 포함하는 BaseResponse
@@ -174,8 +161,6 @@ public BaseResponse questionLike(@AuthenticationPrincipal User user,
/**
* 질문 좋아요 취소 API
*
- * 특정 질문에 좋아요를 취소합니다.
- *
* @param user 로그인한 사용자 정보
* @param likeRequest 좋아요할 질문 정보
* @return 좋아요 취소 성공 메시지를 포함하는 BaseResponse
diff --git a/src/main/java/inha/git/question/api/service/QuestionCommentServiceImpl.java b/src/main/java/inha/git/question/api/service/QuestionCommentServiceImpl.java
index 189ab69a..f59f301b 100644
--- a/src/main/java/inha/git/question/api/service/QuestionCommentServiceImpl.java
+++ b/src/main/java/inha/git/question/api/service/QuestionCommentServiceImpl.java
@@ -18,6 +18,8 @@
import inha.git.utils.IdempotentProvider;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.dao.DataIntegrityViolationException;
+import org.springframework.dao.PessimisticLockingFailureException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -233,16 +235,17 @@ public ReplyCommentResponse deleteReplyComment(User user, Integer replyCommentId
*/
@Override
public String questionCommentLike(User user, CommentLikeRequest commentLikeRequest) {
-
- idempotentProvider.isValidIdempotent(List.of("questionCommentLike", user.getId().toString(), user.getName(), commentLikeRequest.idx().toString()));
-
-
- QuestionComment questionComment = getQuestionComment(commentLikeRequest);
- validLike(questionComment, user, questionCommentLikeJpaRepository.existsByUserAndQuestionComment(user, questionComment));
- questionCommentLikeJpaRepository.save(questionMapper.createQuestionCommentLike(user, questionComment));
- questionComment.setLikeCount(questionComment.getLikeCount() + 1);
- log.info("질문 댓글 좋아요 성공 - 사용자: {} 댓글 ID: {} 좋아요 개수: {}", user.getName(), commentLikeRequest.idx(), questionComment.getLikeCount());
- return commentLikeRequest.idx() + "번 질문 댓글 좋아요 완료";
+ QuestionComment questionComment = getQuestionComment(user, commentLikeRequest);
+ try {
+ validLike(questionComment, user, questionCommentLikeJpaRepository.existsByUserAndQuestionComment(user, questionComment));
+ questionCommentLikeJpaRepository.save(questionMapper.createQuestionCommentLike(user, questionComment));
+ questionComment.setLikeCount(questionComment.getLikeCount() + 1);
+ log.info("질문 댓글 좋아요 성공 - 사용자: {} 댓글 ID: {} 좋아요 개수: {}", user.getName(), commentLikeRequest.idx(), questionComment.getLikeCount());
+ return commentLikeRequest.idx() + "번 질문 댓글 좋아요 완료";
+ } catch(DataIntegrityViolationException e) {
+ log.error("질문 댓글 좋아요 중복 발생 - 사용자: {} 댓글 ID: {}", user.getName(), commentLikeRequest.idx());
+ throw new BaseException(ALREADY_LIKE);
+ }
}
/**
@@ -254,19 +257,21 @@ public String questionCommentLike(User user, CommentLikeRequest commentLikeReque
*/
@Override
public String questionCommentLikeCancel(User user, CommentLikeRequest commentLikeRequest) {
-
- idempotentProvider.isValidIdempotent(List.of("questionCommentLikeCancel", user.getId().toString(), user.getName(), commentLikeRequest.idx().toString()));
-
- QuestionComment questionComment = getQuestionComment(commentLikeRequest);
- boolean commentLikeJpaRepository = questionCommentLikeJpaRepository.existsByUserAndQuestionComment(user, questionComment);
- validLikeCancel(questionComment, user, commentLikeJpaRepository);
- questionCommentLikeJpaRepository.deleteByUserAndQuestionComment(user, questionComment);
- if (questionComment.getLikeCount() <= 0) {
- questionComment.setLikeCount(0);
+ QuestionComment questionComment = getQuestionComment(user, commentLikeRequest);
+ try {
+ boolean commentLikeJpaRepository = questionCommentLikeJpaRepository.existsByUserAndQuestionComment(user, questionComment);
+ validLikeCancel(questionComment, user, commentLikeJpaRepository);
+ questionCommentLikeJpaRepository.deleteByUserAndQuestionComment(user, questionComment);
+ if (questionComment.getLikeCount() <= 0) {
+ questionComment.setLikeCount(0);
+ }
+ questionComment.setLikeCount(questionComment.getLikeCount() - 1);
+ log.info("질문 댓글 좋아요 취소 성공 - 사용자: {} 댓글 ID: {} 좋아요 개수: {}", user.getName(), commentLikeRequest.idx(), questionComment.getLikeCount());
+ return commentLikeRequest.idx() + "번 질문 댓글 좋아요 취소 완료";
+ } catch(DataIntegrityViolationException e) {
+ log.error("질문 댓글 좋아요 취소 중복 발생 - 사용자: {} 댓글 ID: {}", user.getName(), commentLikeRequest.idx());
+ throw new BaseException(NOT_LIKE);
}
- questionComment.setLikeCount(questionComment.getLikeCount() - 1);
- log.info("질문 댓글 좋아요 취소 성공 - 사용자: {} 댓글 ID: {} 좋아요 개수: {}", user.getName(), commentLikeRequest.idx(), questionComment.getLikeCount());
- return commentLikeRequest.idx() + "번 질문 댓글 좋아요 취소 완료";
}
/**
@@ -278,16 +283,17 @@ public String questionCommentLikeCancel(User user, CommentLikeRequest commentLik
*/
@Override
public String questionReplyCommentLike(User user, CommentLikeRequest commentLikeRequest) {
-
- idempotentProvider.isValidIdempotent(List.of("questionReplyCommentLike", user.getId().toString(), user.getName(), commentLikeRequest.idx().toString()));
-
- QuestionReplyComment questionReplyComment = questionReplyCommentJpaRepository.findByIdAndState(commentLikeRequest.idx(), ACTIVE)
- .orElseThrow(() -> new BaseException(QUESTION_COMMENT_REPLY_NOT_FOUND));
- validReplyLike(questionReplyComment, user, questionReplyCommentLikeJpaRepository.existsByUserAndQuestionReplyComment(user, questionReplyComment));
- questionReplyCommentLikeJpaRepository.save(questionMapper.createQuestionReplyCommentLike(user, questionReplyComment));
- questionReplyComment.setLikeCount(questionReplyComment.getLikeCount() + 1);
- log.info("질문 대댓글 좋아요 성공 - 사용자: {} 댓글 ID: {} 좋아요 개수: {}", user.getName(), commentLikeRequest.idx(), questionReplyComment.getLikeCount());
- return commentLikeRequest.idx() + "번 질문 대댓글 좋아요 완료";
+ QuestionReplyComment questionReplyComment = getQuestionReplyComment(user, commentLikeRequest);
+ try {
+ validReplyLike(questionReplyComment, user, questionReplyCommentLikeJpaRepository.existsByUserAndQuestionReplyComment(user, questionReplyComment));
+ questionReplyCommentLikeJpaRepository.save(questionMapper.createQuestionReplyCommentLike(user, questionReplyComment));
+ questionReplyComment.setLikeCount(questionReplyComment.getLikeCount() + 1);
+ log.info("질문 대댓글 좋아요 성공 - 사용자: {} 댓글 ID: {} 좋아요 개수: {}", user.getName(), commentLikeRequest.idx(), questionReplyComment.getLikeCount());
+ return commentLikeRequest.idx() + "번 질문 대댓글 좋아요 완료";
+ } catch(DataIntegrityViolationException e) {
+ log.error("질문 대댓글 좋아요 중복 발생 - 사용자: {} 댓글 ID: {}", user.getName(), commentLikeRequest.idx());
+ throw new BaseException(ALREADY_LIKE);
+ }
}
/**
@@ -299,30 +305,23 @@ public String questionReplyCommentLike(User user, CommentLikeRequest commentLike
*/
@Override
public String questionReplyCommentLikeCancel(User user, CommentLikeRequest commentLikeRequest) {
-
- idempotentProvider.isValidIdempotent(List.of("questionReplyCommentLikeCancel", user.getId().toString(), user.getName(), commentLikeRequest.idx().toString()));
-
- QuestionReplyComment questionReplyComment = questionReplyCommentJpaRepository.findByIdAndState(commentLikeRequest.idx(), ACTIVE)
- .orElseThrow(() -> new BaseException(QUESTION_COMMENT_REPLY_NOT_FOUND));
- boolean commentLikeJpaRepository = questionReplyCommentLikeJpaRepository.existsByUserAndQuestionReplyComment(user, questionReplyComment);
- validReplyLikeCancel(questionReplyComment, user, commentLikeJpaRepository);
- questionReplyCommentLikeJpaRepository.deleteByUserAndQuestionReplyComment(user, questionReplyComment);
- if (questionReplyComment.getLikeCount() <= 0) {
- questionReplyComment.setLikeCount(0);
+ QuestionReplyComment questionReplyComment = getQuestionReplyComment(user, commentLikeRequest);
+ try {
+ boolean commentLikeJpaRepository = questionReplyCommentLikeJpaRepository.existsByUserAndQuestionReplyComment(user, questionReplyComment);
+ validReplyLikeCancel(questionReplyComment, user, commentLikeJpaRepository);
+ questionReplyCommentLikeJpaRepository.deleteByUserAndQuestionReplyComment(user, questionReplyComment);
+ if (questionReplyComment.getLikeCount() <= 0) {
+ questionReplyComment.setLikeCount(0);
+ }
+ questionReplyComment.setLikeCount(questionReplyComment.getLikeCount() - 1);
+ log.info("질문 대댓글 좋아요 취소 성공 - 사용자: {} 댓글 ID: {} 좋아요 개수: {}", user.getName(), commentLikeRequest.idx(), questionReplyComment.getLikeCount());
+ return commentLikeRequest.idx() + "번 질문 대댓글 좋아요 취소 완료";
+ } catch(DataIntegrityViolationException e) {
+ log.error("질문 대댓글 좋아요 취소 중복 발생 - 사용자: {} 댓글 ID: {}", user.getName(), commentLikeRequest.idx());
+ throw new BaseException(NOT_LIKE);
}
- questionReplyComment.setLikeCount(questionReplyComment.getLikeCount() - 1);
- log.info("질문 대댓글 좋아요 취소 성공 - 사용자: {} 댓글 ID: {} 좋아요 개수: {}", user.getName(), commentLikeRequest.idx(), questionReplyComment.getLikeCount());
- return commentLikeRequest.idx() + "번 질문 대댓글 좋아요 취소 완료";
}
-
- /**
- * 댓글 좋아요 정보 유효성 검사
- *
- * @param questionComment 댓글 정보
- * @param user 사용자 정보
- * @param commentLikeJpaRepository 댓글 좋아요 레포지토리
- */
private void validLike(QuestionComment questionComment, User user, boolean commentLikeJpaRepository) {
if (questionComment.getUser().getId().equals(user.getId())) {
log.error("내 댓글은 좋아요할 수 없습니다. - 사용자: {} 댓글 ID: {}", user.getName(), questionComment.getId());
@@ -334,13 +333,6 @@ private void validLike(QuestionComment questionComment, User user, boolean comme
}
}
- /**
- * 대댓글 좋아요 정보 유효성 검사
- *
- * @param questionReplyComment 대댓글 정보
- * @param user 사용자 정보
- * @param commentLikeJpaRepository 대댓글 좋아요 레포지토리
- */
private void validReplyLike(QuestionReplyComment questionReplyComment, User user, boolean commentLikeJpaRepository) {
if (questionReplyComment.getUser().getId().equals(user.getId())) {
log.error("내 대댓글은 좋아요할 수 없습니다. - 사용자: {} 댓글 ID: {}", user.getName(), questionReplyComment.getId());
@@ -352,13 +344,6 @@ private void validReplyLike(QuestionReplyComment questionReplyComment, User user
}
}
- /**
- * 댓글 좋아요 취소
- *
- * @param user 사용자 정보
- * @param questionComment 좋아요 취소할 댓글 정보
- * @param commentLikeJpaRepository 댓글 좋아요 레포지토리
- */
private void validLikeCancel(QuestionComment questionComment, User user, boolean commentLikeJpaRepository) {
if (questionComment.getUser().getId().equals(user.getId())) {
log.error("내 댓글은 좋아요할 수 없습니다. - 사용자: {} 댓글 ID: {}", user.getName(), questionComment.getId());
@@ -370,13 +355,6 @@ private void validLikeCancel(QuestionComment questionComment, User user, boolean
}
}
- /**
- * 대댓글 좋아요 취소
- *
- * @param user 사용자 정보
- * @param questionReplyComment 좋아요 취소할 대댓글 정보
- * @param commentLikeJpaRepository 대댓글 좋아요 레포지토리
- */
private void validReplyLikeCancel(QuestionReplyComment questionReplyComment, User user, boolean commentLikeJpaRepository) {
if (questionReplyComment.getUser().getId().equals(user.getId())) {
log.error("내 대댓글은 좋아요할 수 없습니다. - 사용자: {} 댓글 ID: {}", user.getName(), questionReplyComment.getId());
@@ -388,15 +366,28 @@ private void validReplyLikeCancel(QuestionReplyComment questionReplyComment, Use
}
}
- /**
- * 댓글 좋아요 정보 조회
- *
- * @param commentLikeRequest 댓글 좋아요 정보
- * @return 댓글 좋아요 정보
- */
- private QuestionComment getQuestionComment(CommentLikeRequest commentLikeRequest) {
- return questionCommentJpaRepository.findByIdAndState(commentLikeRequest.idx(), ACTIVE)
- .orElseThrow(() -> new BaseException(QUESTION_COMMENT_NOT_FOUND));
+
+ private QuestionComment getQuestionComment(User user, CommentLikeRequest commentLikeRequest) {
+ QuestionComment questionComment;
+ try{
+ questionComment = questionCommentJpaRepository.findByIdAndStateWithPessimisticLock(commentLikeRequest.idx(), ACTIVE)
+ .orElseThrow(() -> new BaseException(QUESTION_COMMENT_NOT_FOUND));
+ } catch (PessimisticLockingFailureException e){
+ log.error("질문 댓글 좋아요 추천 락 획득 실패- 사용자: {} 댓글 ID: {}", user.getName(), commentLikeRequest.idx());
+ throw new BaseException(TEMPORARY_UNAVAILABLE);
+ }
+ return questionComment;
}
-}
+ private QuestionReplyComment getQuestionReplyComment(User user, CommentLikeRequest commentLikeRequest) {
+ QuestionReplyComment questionReplyComment;
+ try{
+ questionReplyComment = questionReplyCommentJpaRepository.findByIdAndStateWithPessimisticLock(commentLikeRequest.idx(), ACTIVE)
+ .orElseThrow(() -> new BaseException(QUESTION_COMMENT_REPLY_NOT_FOUND));
+ } catch (PessimisticLockingFailureException e){
+ log.error("질문 대댓글 좋아요 추천 락 획득 실패- 사용자: {} 댓글 ID: {}", user.getName(), commentLikeRequest.idx());
+ throw new BaseException(TEMPORARY_UNAVAILABLE);
+ }
+ return questionReplyComment;
+ }
+}
diff --git a/src/main/java/inha/git/question/api/service/QuestionServiceImpl.java b/src/main/java/inha/git/question/api/service/QuestionServiceImpl.java
index 79cf538c..bd66ba67 100644
--- a/src/main/java/inha/git/question/api/service/QuestionServiceImpl.java
+++ b/src/main/java/inha/git/question/api/service/QuestionServiceImpl.java
@@ -33,6 +33,8 @@
import inha.git.utils.IdempotentProvider;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.dao.DataIntegrityViolationException;
+import org.springframework.dao.PessimisticLockingFailureException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
@@ -52,7 +54,8 @@
import static inha.git.common.code.status.ErrorStatus.*;
/**
- * QuestionServiceImpl은 question 관련 비즈니스 로직을 처리.
+ * 질문 관련 비즈니스 로직을 처리하는 서비스 구현체입니다.
+ * 질문의 조회, 생성, 수정, 삭제 및 관련 통계 처리를 담당합니다.
*/
@Service
@RequiredArgsConstructor
@@ -73,11 +76,11 @@ public class QuestionServiceImpl implements QuestionService {
private final StatisticsService statisticsService;
/**
- * 질문 전체 조회
+ * 전체 질문을 페이징하여 조회합니다.
*
- * @param page Integer
- * @param size Integer
- * @return Page
+ * @param page 조회할 페이지 번호 (0부터 시작)
+ * @param size 페이지당 항목 수
+ * @return 페이징된 질문 목록
*/
@Override
public Page getQuestions(Integer page, Integer size) {
@@ -104,6 +107,7 @@ public Page getCondQuestions(SearchQuestionCond searchQ
*
* @param questionIdx Integer
* @return SearchQuestionResponse
+ * @throws BaseException QUESTION_NOT_FOUND: 질문을 찾을 수 없는 경우
*/
@Override
public SearchQuestionResponse getQuestion(User user, Integer questionIdx) {
@@ -126,6 +130,10 @@ public SearchQuestionResponse getQuestion(User user, Integer questionIdx) {
* @param user User
* @param createQuestionRequest CreateQuestionRequest
* @return QuestionResponse
+ * @throws BaseException SEMESTER_NOT_FOUND: 학기를 찾을 수 없는 경우
+ * CATEGORY_NOT_FOUND: 카테고리를 찾을 수 없는 경우
+ * FIELD_NOT_FOUND: 필드를 찾을 수 없는 경우
+ * QUESTION_NOT_AUTHORIZED: 질문 수정 권한이 없는 경우
*/
@Override
@Transactional
@@ -157,6 +165,11 @@ public QuestionResponse createQuestion(User user, CreateQuestionRequest createQu
* @param questionIdx Integer
* @param updateQuestionRequest UpdateQuestionRequest
* @return QuestionResponse
+ * @throws BaseException SEMESTER_NOT_FOUND: 학기를 찾을 수 없는 경우
+ * FIELD_NOT_FOUND: 필드를 찾을 수 없는 경우
+ * QUESTION_NOT_AUTHORIZED: 질문 수정 권한이 없는 경우
+ * FIELD_NOT_FOUND: 필드를 찾을 수 없는 경우
+ * QUESTION_NOT_FOUND: 질문을 찾을 수 없는 경우
*/
@Override
@Transactional
@@ -240,6 +253,8 @@ public QuestionResponse updateQuestion(User user, Integer questionIdx, UpdateQue
* @param user User
* @param questionIdx Integer
* @return QuestionResponse
+ * @throws BaseException QUESTION_DELETE_NOT_AUTHORIZED: 질문 삭제 권한이 없는 경우
+ * QUESTION_NOT_FOUND: 질문을 찾을 수 없는 경우
*/
@Override
@Transactional
@@ -267,21 +282,24 @@ public QuestionResponse deleteQuestion(User user, Integer questionIdx) {
* @param user User
* @param likeRequest LikeRequest
* @return String
+ * @throws BaseException QUESTION_NOT_FOUND: 질문을 찾을 수 없는 경우
+ * MY_QUESTION_LIKE: 내 질문은 좋아요할 수 없는 경우
+ * QUESTION_ALREADY_LIKE: 이미 좋아요한 질문인 경우
*/
@Override
@Transactional
public String createQuestionLike(User user, LikeRequest likeRequest) {
-
- idempotentProvider.isValidIdempotent(List.of("createQuestionLike", user.getId().toString(), user.getName(), likeRequest.idx().toString()));
-
-
- Question question = questionJpaRepository.findByIdAndState(likeRequest.idx(), ACTIVE)
- .orElseThrow(() -> new BaseException(QUESTION_NOT_FOUND));
- validLike(question, user, questionLikeJpaRepository.existsByUserAndQuestion(user, question));
- questionLikeJpaRepository.save(questionMapper.createQuestionLike(user, question));
- question.setLikeCount(question.getLikeCount() + 1);
- log.info("질문 좋아요 성공 - 사용자: {} 질문 ID: {} 좋아요 개수 : {}", user.getName(), likeRequest.idx(), question.getLikeCount());
- return likeRequest.idx() + "번 질문 좋아요 완료";
+ Question question = getQuestion(user, likeRequest);
+ try{
+ validLike(question, user, questionLikeJpaRepository.existsByUserAndQuestion(user, question));
+ questionLikeJpaRepository.save(questionMapper.createQuestionLike(user, question));
+ question.setLikeCount(question.getLikeCount() + 1);
+ log.info("질문 좋아요 성공 - 사용자: {} 질문 ID: {} 좋아요 개수 : {}", user.getName(), likeRequest.idx(), question.getLikeCount());
+ return likeRequest.idx() + "번 질문 좋아요 완료";
+ } catch(DataIntegrityViolationException e) {
+ log.error("질문 좋아요 중복 발생 - 사용자: {} 댓글 ID: {}", user.getName(), likeRequest.idx());
+ throw new BaseException(ALREADY_LIKE);
+ }
}
/**
@@ -290,23 +308,27 @@ public String createQuestionLike(User user, LikeRequest likeRequest) {
* @param user User
* @param likeRequest LikeRequest
* @return String
+ * @throws BaseException QUESTION_NOT_FOUND: 질문을 찾을 수 없는 경우
+ * MY_QUESTION_LIKE: 내 질문은 좋아요할 수 없는 경우
+ * QUESTION_NOT_LIKE: 좋아요하지 않은 질문인 경우
+ *
*/
@Override
@Transactional
public String questionLikeCancel(User user, LikeRequest likeRequest) {
-
- idempotentProvider.isValidIdempotent(List.of("questionLikeCancel", user.getId().toString(), user.getName(), likeRequest.idx().toString()));
-
- Question question = questionJpaRepository.findByIdAndState(likeRequest.idx(), ACTIVE)
- .orElseThrow(() -> new BaseException(QUESTION_NOT_FOUND));
- validLikeCancel(question, user, questionLikeJpaRepository.existsByUserAndQuestion(user, question));
- questionLikeJpaRepository.deleteByUserAndQuestion(user, question);
- question.setLikeCount(question.getLikeCount() - 1);
- log.info("질문 좋아요 취소 성공 - 사용자: {} 질문 ID: {} 좋아요 개수 : {}", user.getName(), likeRequest.idx(), question.getLikeCount());
- return likeRequest.idx() + "번 프로젝트 좋아요 취소 완료";
+ Question question = getQuestion(user, likeRequest);
+ try{
+ validLikeCancel(question, user, questionLikeJpaRepository.existsByUserAndQuestion(user, question));
+ questionLikeJpaRepository.deleteByUserAndQuestion(user, question);
+ question.setLikeCount(question.getLikeCount() - 1);
+ log.info("질문 좋아요 취소 성공 - 사용자: {} 질문 ID: {} 좋아요 개수 : {}", user.getName(), likeRequest.idx(), question.getLikeCount());
+ return likeRequest.idx() + "번 프로젝트 좋아요 취소 완료";
+ } catch(DataIntegrityViolationException e) {
+ log.error("질문 좋아요 취소 중복 발생 - 사용자: {} 댓글 ID: {}", user.getName(), likeRequest.idx());
+ throw new BaseException(QUESTION_NOT_LIKE);
+ }
}
-
/**
* 질문 생성시 필드 생성
*
@@ -323,13 +345,6 @@ private List createAndSaveQuestionFields(List fieldIdxLi
}).toList();
}
- /**
- * 좋아요 유효성 검사
- *
- * @param question Question
- * @param user User
- * @param questionLikeJpaRepository 질문 좋아요 레포지토리
- */
private void validLike(Question question, User user, boolean questionLikeJpaRepository) {
if (question.getUser().getId().equals(user.getId())) {
log.error("내 질문은 좋아요할 수 없습니다. - 사용자: {} 질문 ID: {}", user.getName(), question.getId());
@@ -351,4 +366,16 @@ private void validLikeCancel(Question question, User user, boolean questionLikeJ
throw new BaseException(QUESTION_NOT_LIKE);
}
}
+
+ private Question getQuestion(User user, LikeRequest likeRequest) {
+ Question question;
+ try {
+ question = questionJpaRepository.findByIdAndStateWithPessimisticLock(likeRequest.idx(), ACTIVE)
+ .orElseThrow(() -> new BaseException(QUESTION_NOT_FOUND));
+ } catch (PessimisticLockingFailureException e) {
+ log.error("질문 좋아요 추천 락 획득 실패- 사용자: {} 댓글 ID: {}", user.getName(), likeRequest.idx());
+ throw new BaseException(TEMPORARY_UNAVAILABLE);
+ }
+ return question;
+ }
}
diff --git a/src/main/java/inha/git/question/domain/Question.java b/src/main/java/inha/git/question/domain/Question.java
index 9c9ce3b5..512b196a 100644
--- a/src/main/java/inha/git/question/domain/Question.java
+++ b/src/main/java/inha/git/question/domain/Question.java
@@ -8,6 +8,7 @@
import jakarta.persistence.*;
import lombok.*;
+import java.util.ArrayList;
import java.util.List;
@@ -60,7 +61,7 @@ public class Question extends BaseEntity {
private Category category;
@OneToMany(mappedBy = "question", cascade = CascadeType.ALL, orphanRemoval = true)
- private List questionFields;
+ private List questionFields = new ArrayList<>();
public void setLikeCount(int likeCount) {
this.likeCount = likeCount;
@@ -73,4 +74,8 @@ public void increaseCommentCount() {
public void decreaseCommentCount() {
this.commentCount--;
}
+
+ public void setQuestionFields(ArrayList questionFields) {
+ this.questionFields = questionFields;
+ }
}
diff --git a/src/main/java/inha/git/question/domain/QuestionComment.java b/src/main/java/inha/git/question/domain/QuestionComment.java
index c5fea0f1..a34dddf8 100644
--- a/src/main/java/inha/git/question/domain/QuestionComment.java
+++ b/src/main/java/inha/git/question/domain/QuestionComment.java
@@ -6,6 +6,7 @@
import jakarta.persistence.*;
import lombok.*;
+import java.util.ArrayList;
import java.util.List;
@@ -44,9 +45,13 @@ public void setContents(String contents) {
}
@OneToMany(mappedBy = "questionComment", cascade = CascadeType.ALL, orphanRemoval = true)
- private List replies;
+ private List replies = new ArrayList<>();
public void setLikeCount(Integer likeCount) {
this.likeCount = likeCount;
}
+
+ public void setQuestion(Question question) {
+ this.question = question;
+ }
}
diff --git a/src/main/java/inha/git/question/domain/repository/QuestionCommentJpaRepository.java b/src/main/java/inha/git/question/domain/repository/QuestionCommentJpaRepository.java
index 8c96fa41..e12d6883 100644
--- a/src/main/java/inha/git/question/domain/repository/QuestionCommentJpaRepository.java
+++ b/src/main/java/inha/git/question/domain/repository/QuestionCommentJpaRepository.java
@@ -5,7 +5,12 @@
import inha.git.common.BaseEntity.State;
import inha.git.question.domain.Question;
import inha.git.question.domain.QuestionComment;
+import jakarta.persistence.LockModeType;
+import jakarta.persistence.QueryHint;
import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Lock;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.jpa.repository.QueryHints;
import java.util.List;
import java.util.Optional;
@@ -19,6 +24,11 @@ public interface QuestionCommentJpaRepository extends JpaRepository findByIdAndState(Integer commentIdx, State state);
+ @Lock(LockModeType.PESSIMISTIC_WRITE)
+ @QueryHints({@QueryHint(name = "javax.persistence.lock.timeout", value = "3000")})
+ @Query("SELECT c FROM QuestionComment c WHERE c.id = :commentIdx AND c.state = :state")
+ Optional findByIdAndStateWithPessimisticLock(Integer commentIdx, State state);
+
List findAllByQuestionAndStateOrderByIdAsc(Question question, State state);
diff --git a/src/main/java/inha/git/question/domain/repository/QuestionJpaRepository.java b/src/main/java/inha/git/question/domain/repository/QuestionJpaRepository.java
index fb88e2e4..7bf07b34 100644
--- a/src/main/java/inha/git/question/domain/repository/QuestionJpaRepository.java
+++ b/src/main/java/inha/git/question/domain/repository/QuestionJpaRepository.java
@@ -3,7 +3,12 @@
import inha.git.common.BaseEntity.State;
import inha.git.question.domain.Question;
+import jakarta.persistence.LockModeType;
+import jakarta.persistence.QueryHint;
import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Lock;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.jpa.repository.QueryHints;
import java.util.Optional;
@@ -13,4 +18,9 @@
*/
public interface QuestionJpaRepository extends JpaRepository {
Optional findByIdAndState(Integer questionIdx, State state);
+
+ @Lock(LockModeType.PESSIMISTIC_WRITE)
+ @QueryHints({@QueryHint(name = "javax.persistence.lock.timeout", value = "3000")})
+ @Query("SELECT q FROM Question q WHERE q.id = :questionIdx AND q.state = :state")
+ Optional findByIdAndStateWithPessimisticLock(Integer questionIdx, State state);
}
diff --git a/src/main/java/inha/git/question/domain/repository/QuestionReplyCommentJpaRepository.java b/src/main/java/inha/git/question/domain/repository/QuestionReplyCommentJpaRepository.java
index d84f849d..e5401450 100644
--- a/src/main/java/inha/git/question/domain/repository/QuestionReplyCommentJpaRepository.java
+++ b/src/main/java/inha/git/question/domain/repository/QuestionReplyCommentJpaRepository.java
@@ -1,11 +1,15 @@
package inha.git.question.domain.repository;
-import inha.git.common.BaseEntity;
import inha.git.common.BaseEntity.State;
import inha.git.question.domain.QuestionComment;
import inha.git.question.domain.QuestionReplyComment;
+import jakarta.persistence.LockModeType;
+import jakarta.persistence.QueryHint;
import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Lock;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.jpa.repository.QueryHints;
import java.util.Optional;
@@ -18,5 +22,10 @@ public interface QuestionReplyCommentJpaRepository extends JpaRepository findByIdAndState(Integer commentIdx, State state);
+ @Lock(LockModeType.PESSIMISTIC_WRITE)
+ @QueryHints({@QueryHint(name = "javax.persistence.lock.timeout", value = "3000")})
+ @Query("SELECT c FROM QuestionReplyComment c WHERE c.id = :commentIdx AND c.state = :state")
+ Optional findByIdAndStateWithPessimisticLock(Integer commentIdx, State state);
+
boolean existsByQuestionCommentAndState(QuestionComment questionComment, State state);
}
diff --git a/src/main/java/inha/git/semester/controller/SemesterController.java b/src/main/java/inha/git/semester/controller/SemesterController.java
index 16034412..0f475aef 100644
--- a/src/main/java/inha/git/semester/controller/SemesterController.java
+++ b/src/main/java/inha/git/semester/controller/SemesterController.java
@@ -1,6 +1,7 @@
package inha.git.semester.controller;
import inha.git.common.BaseResponse;
+import inha.git.common.exceptions.BaseException;
import inha.git.semester.controller.dto.request.CreateSemesterRequest;
import inha.git.semester.controller.dto.request.UpdateSemesterRequest;
import inha.git.semester.controller.dto.response.SearchSemesterResponse;
@@ -20,7 +21,8 @@
import static inha.git.common.code.status.SuccessStatus.*;
/**
- * SemesterController는 semester 관련 엔드포인트를 처리.
+ * 학기 관련 API를 처리하는 컨트롤러입니다.
+ * 학기의 조회, 생성, 수정, 삭제 기능을 제공합니다.
*/
@Slf4j
@Tag(name = "semester controller", description = "semester 관련 API")
@@ -32,9 +34,9 @@ public class SemesterController {
private final SemesterService semesterService;
/**
- * 학기 전체 조회 API
+ * 전체 학기 목록을 조회합니다.
*
- * @return 학기 전체
+ * @return 학기 목록을 포함한 응답
*/
@GetMapping
@Operation(summary = "학기 전체 조회 API", description = "학기 전체를 조회합니다.")
@@ -44,26 +46,29 @@ public BaseResponse> getSemesters() {
/**
- * 학기 생성 API
+ * 새로운 학기를 생성합니다.
*
- * @param createDepartmentRequest 학기 생성 요청
- * @return 생성된 학기 이름
+ * @param user 현재 인증된 관리자 정보
+ * @param createSemesterRequest 생성할 학기 정보 (학기명)
+ * @return 학기 생성 결과 메시지
*/
@PostMapping
@PreAuthorize("hasAuthority('admin:create')")
@Operation(summary = "학기 생성(관리자 전용) API", description = "학기를 생성합니다.(관리자 전용)")
public BaseResponse createSemester(@AuthenticationPrincipal User user,
- @Validated @RequestBody CreateSemesterRequest createDepartmentRequest) {
- log.info("학기 생성 - 관리자: {} 학기명: {}", user.getName(), createDepartmentRequest.name());
- return BaseResponse.of(SEMESTER_CREATE_OK, semesterService.createSemester(user, createDepartmentRequest));
+ @Validated @RequestBody CreateSemesterRequest createSemesterRequest) {
+ log.info("학기 생성 - 관리자: {} 학기명: {}", user.getName(), createSemesterRequest.name());
+ return BaseResponse.of(SEMESTER_CREATE_OK, semesterService.createSemester(user, createSemesterRequest));
}
/**
- * 학기 수정 API
+ * 학기명을 수정합니다.
*
- * @param semesterIdx 학기 인덱스
- * @param updateSemesterRequest 학기 수정 요청
- * @return 수정된 학기 이름
+ * @param user 현재 인증된 관리자 정보
+ * @param semesterIdx 수정할 학기의 식별자
+ * @param updateSemesterRequest 새로운 학기명
+ * @return 학기명 수정 결과 메시지
+ * @throws BaseException SEMESTER_NOT_FOUND: 학기를 찾을 수 없는 경우
*/
@PutMapping("/{semesterIdx}")
@PreAuthorize("hasAuthority('admin:update')")
@@ -76,9 +81,12 @@ public BaseResponse updateSemester(@AuthenticationPrincipal User user,
}
/**
- * 학기 목록 조회 API
+ * 학기를 삭제(비활성화) 처리합니다.
*
- * @return 학기 목록
+ * @param user 현재 인증된 관리자 정보
+ * @param semesterIdx 삭제할 학기의 식별자
+ * @return 학기 삭제 결과 메시지
+ * @throws BaseException SEMESTER_NOT_FOUND: 학기를 찾을 수 없는 경우
*/
@DeleteMapping("/{semesterIdx}")
@PreAuthorize("hasAuthority('admin:delete')")
diff --git a/src/main/java/inha/git/semester/service/SemesterService.java b/src/main/java/inha/git/semester/service/SemesterService.java
index 17b0a151..8880fa9c 100644
--- a/src/main/java/inha/git/semester/service/SemesterService.java
+++ b/src/main/java/inha/git/semester/service/SemesterService.java
@@ -10,7 +10,7 @@
public interface SemesterService {
List getSemesters();
- String createSemester(User admin, CreateSemesterRequest createDepartmentRequest);
+ String createSemester(User admin, CreateSemesterRequest createSemesterRequest);
String updateSemesterName(User admin, Integer semesterIdx, UpdateSemesterRequest updateSemesterRequest);
String deleteSemester(User admin, Integer semesterIdx);
diff --git a/src/main/java/inha/git/semester/service/SemesterServiceImpl.java b/src/main/java/inha/git/semester/service/SemesterServiceImpl.java
index 104d97c3..0bfd1157 100644
--- a/src/main/java/inha/git/semester/service/SemesterServiceImpl.java
+++ b/src/main/java/inha/git/semester/service/SemesterServiceImpl.java
@@ -22,7 +22,8 @@
import static inha.git.common.code.status.ErrorStatus.SEMESTER_NOT_FOUND;
/**
- * SemesterServiceImpl는 SemesterService 인터페이스를 구현하는 클래스.
+ * 학기 관련 비즈니스 로직을 처리하는 서비스 구현체입니다.
+ * 학기의 조회, 생성, 수정, 삭제 기능을 제공합니다.
*/
@Service
@RequiredArgsConstructor
@@ -35,9 +36,9 @@ public class SemesterServiceImpl implements SemesterService {
/**
- * 학기 전체 조회
+ * 활성화된 모든 학기를 조회합니다.
*
- * @return 학기 전체 조회 결과
+ * @return 학기 정보 목록 (SearchSemesterResponse)
*/
@Override
public List getSemesters() {
@@ -46,25 +47,28 @@ public List getSemesters() {
}
/**
- * 학기 생성
+ * 새로운 학기를 생성합니다.
*
- * @param createDepartmentRequest 학기 생성 요청
- * @return 생성된 학기 이름
+ * @param admin 생성을 요청한 관리자 정보
+ * @param createSemesterRequest 생성할 학기 정보
+ * @return 학기 생성 완료 메시지
*/
@Override
@Transactional
- public String createSemester(User admin, CreateSemesterRequest createDepartmentRequest) {
- Semester semester = semesterJpaRepository.save(semesterMapper.createSemesterRequestToSemester(createDepartmentRequest));
- log.info("학기 생성 성공 - 관리자: {} 학기명: {}", admin.getName(), createDepartmentRequest.name());
+ public String createSemester(User admin, CreateSemesterRequest createSemesterRequest) {
+ Semester semester = semesterJpaRepository.save(semesterMapper.createSemesterRequestToSemester(createSemesterRequest));
+ log.info("학기 생성 성공 - 관리자: {} 학기명: {}", admin.getName(), createSemesterRequest.name());
return semester.getName() + " 학기가 생성되었습니다.";
}
/**
- * 학기 이름 수정
+ * 학기명을 수정합니다.
*
- * @param semesterIdx 학기 인덱스
- * @param updateSemesterRequest 학기 수정 요청
- * @return 수정된 학기 이름
+ * @param admin 수정을 요청한 관리자 정보
+ * @param semesterIdx 수정할 학기의 식별자
+ * @param updateSemesterRequest 새로운 학기명 정보
+ * @return 학기명 수정 완료 메시지
+ * @throws BaseException SEMESTER_NOT_FOUND: 학기를 찾을 수 없는 경우
*/
@Override
@Transactional
@@ -78,10 +82,12 @@ public String updateSemesterName(User admin, Integer semesterIdx, UpdateSemester
}
/**
- * 학기 삭제
+ * 학기를 삭제(비활성화) 처리합니다.
*
- * @param semesterIdx 학기 인덱스
- * @return 삭제된 학기 이름
+ * @param admin 삭제를 요청한 관리자 정보
+ * @param semesterIdx 삭제할 학기의 식별자
+ * @return 학기 삭제 완료 메시지
+ * @throws BaseException SEMESTER_NOT_FOUND: 학기를 찾을 수 없는 경우
*/
@Override
@Transactional
diff --git a/src/main/java/inha/git/user/api/controller/UserController.java b/src/main/java/inha/git/user/api/controller/UserController.java
index fe66589f..9ab5d59f 100644
--- a/src/main/java/inha/git/user/api/controller/UserController.java
+++ b/src/main/java/inha/git/user/api/controller/UserController.java
@@ -19,6 +19,7 @@
import inha.git.user.api.service.StudentService;
import inha.git.user.api.service.UserService;
import inha.git.user.domain.User;
+import inha.git.utils.PagingUtils;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.constraints.NotNull;
@@ -31,11 +32,12 @@
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
-import static inha.git.common.code.status.ErrorStatus.INVALID_PAGE;
import static inha.git.common.code.status.SuccessStatus.*;
/**
- * UserController는 유저 관련 엔드포인트를 처리.
+ * 사용자 관련 API를 처리하는 컨트롤러입니다.
+ * 일반 사용자, 학생, 교수, 기업회원의 회원가입과 정보 관리 기능을 제공합니다.
+ * 사용자 조회, 프로젝트/팀/문제 참여 현황 등의 조회 기능을 포함합니다.
*/
@Slf4j
@Tag(name = "user controller", description = "유저 관련 API")
@@ -48,29 +50,27 @@ public class UserController {
private final StudentService studentService;
private final ProfessorService professorService;
private final CompanyService companyService;
+ private final PagingUtils pagingUtils;
/**
- * 특정 유저 조회 API
+ * 현재 로그인한 사용자의 상세 정보를 조회합니다.
*
- * 특정 유저를 조회.
- *
- * @return 특정 유저 조회 결과를 포함하는 BaseResponse
+ * @param user 현재 인증된 사용자 정보
+ * @return BaseResponse 사용자의 기본 정보와 통계 정보를 포함한 응답
*/
@GetMapping
- @Operation(summary = "특정 유저 조회 API", description = "특정 유저를 조회합니다.")
+ @Operation(summary = "로그인 유저 조회 API", description = "현재 로그인한 유저의 정보를 조회합니다.")
public BaseResponse getLoginUser(@AuthenticationPrincipal User user) {
return BaseResponse.of(MY_PAGE_USER_SEARCH_OK, userService.getUser(user.getId()));
}
/**
- * 특정 유저 조회 API
- *
- * 특정 유저를 조회.
- *
- * @PathVariable userIdx 조회할 유저의 idx
+ * 특정 사용자의 상세 정보를 조회합니다.
*
- * @return 특정 유저 조회 결과를 포함하는 BaseResponse
+ * @param userIdx 조회할 대상 사용자의 식별자
+ * @return BaseResponse 사용자의 기본 정보와 통계 정보를 포함한 응답
+ * @throws BaseException 조회 대상 사용자가 존재하지 않는 경우
*/
@GetMapping("/{userIdx}")
@Operation(summary = "특정 유저 조회 API", description = "특정 유저를 조회합니다.")
@@ -79,120 +79,105 @@ public BaseResponse getUser(@PathVariable("userIdx" ) Intege
}
/**
- * 특정 유저의 프로젝트 조회 API
+ * 특정 사용자가 참여중인 프로젝트 목록을 조회합니다.
*
- * 특정 유저의 프로젝트를 조회.
- *
- * @param user 인증된 유저 정보
- * @param page 페이지 번호
- *
- * @return 특정 유저의 프로젝트 조회 결과를 포함하는 BaseResponse>
+ * @param user 현재 인증된 사용자 정보
+ * @param userIdx 조회할 대상 사용자의 식별자
+ * @param page 조회할 페이지 번호 (1부터 시작)
+ * @return BaseResponse> 프로젝트 목록을 포함한 페이징 응답
+ * @throws BaseException 페이지 번호가 1 미만이거나, 조회 권한이 없는 경우
*/
@GetMapping("/{userIdx}/projects")
@Operation(summary = "특정 유저의 프로젝트 조회 API", description = "특정 유저의 프로젝트를 조회합니다.")
public BaseResponse> getUserProjects(@AuthenticationPrincipal User user,
@PathVariable("userIdx") Integer userIdx,
@RequestParam("page") Integer page) {
- if (page < 1) {
- throw new BaseException(INVALID_PAGE);
- }
- return BaseResponse.of(MY_PAGE_PROJECT_SEARCH_OK, userService.getUserProjects(user, userIdx, page - 1));
+ pagingUtils.validatePage(page);
+ return BaseResponse.of(MY_PAGE_PROJECT_SEARCH_OK, userService.getUserProjects(user, userIdx, pagingUtils.toPageIndex(page)));
}
/**
- * 특정 유저의 질문 조회 API
- *
- * 특정 유저의 질문을 조회.
+ * 특정 사용자가 작성한 질문 목록을 조회합니다.
*
- * @param user 인증된 유저 정보
- * @param page 페이지 번호
- *
- * @return 특정 유저의 질문 조회 결과를 포함하는 BaseResponse>
+ * @param user 현재 인증된 사용자 정보
+ * @param userIdx 조회할 대상 사용자의 식별자
+ * @param page 조회할 페이지 번호 (1부터 시작)
+ * @return BaseResponse> 질문 목록을 포함한 페이징 응답
+ * @throws BaseException 페이지 번호가 1 미만이거나, 조회 권한이 없는 경우
*/
@GetMapping("/{userIdx}/questions")
@Operation(summary = "특정 유저의 질문 조회 API", description = "특정 유저의 질문을 조회합니다.")
public BaseResponse> getUserQuestions(@AuthenticationPrincipal User user,
@PathVariable("userIdx") Integer userIdx,
@RequestParam("page") Integer page) {
- if (page < 1) {
- throw new BaseException(INVALID_PAGE);
- }
- return BaseResponse.of(MY_PAGE_QUESTION_SEARCH_OK, userService.getUserQuestions(user,userIdx, page - 1));
+ pagingUtils.validatePage(page);
+ return BaseResponse.of(MY_PAGE_QUESTION_SEARCH_OK, userService.getUserQuestions(user,userIdx, pagingUtils.toPageIndex(page)));
}
/**
- * 특정 유저의 팀 조회 API
- *
- * 특정 유저의 팀을 조회.
+ * 특정 사용자가 참여중인 팀 목록을 조회합니다.
*
- * @param user 인증된 유저 정보
- * @param page 페이지 번호
- *
- * @return 특정 유저의 팀 조회 결과를 포함하는 BaseResponse>
+ * @param user 현재 인증된 사용자 정보
+ * @param userIdx 조회할 대상 사용자의 식별자
+ * @param page 조회할 페이지 번호 (1부터 시작)
+ * @return BaseResponse> 팀 목록을 포함한 페이징 응답
+ * @throws BaseException 페이지 번호가 1 미만이거나, 조회 권한이 없는 경우
*/
@GetMapping("/{userIdx}/teams")
@Operation(summary = "특정 유저의 팀 조회 API", description = "특정 유저의 팀을 조회합니다.")
public BaseResponse> getUserTeams(@AuthenticationPrincipal User user,
@PathVariable("userIdx") Integer userIdx,
@RequestParam("page") Integer page) {
- if (page < 1) {
- throw new BaseException(INVALID_PAGE);
- }
- return BaseResponse.of(MY_PAGE_TEAM_SEARCH_OK, userService.getUserTeams(user, userIdx, page - 1));
+ pagingUtils.validatePage(page);
+ return BaseResponse.of(MY_PAGE_TEAM_SEARCH_OK, userService.getUserTeams(user, userIdx, pagingUtils.toPageIndex(page)));
}
/**
- * 특정 유저의 참여중인 문제 조회 API
- *
- * 특정 유저의 참여중인 문제를 조회.
+ * 특정 사용자가 참여중인 문제 목록을 조회합니다.
*
- * @param user 인증된 유저 정보
- * @param page 페이지 번호
- *
- * @return 특정 유저의 참여중인 문제 조회 결과를 포함하는 BaseResponse>
+ * @param user 현재 인증된 사용자 정보
+ * @param userIdx 조회할 대상 사용자의 식별자
+ * @param page 조회할 페이지 번호 (1부터 시작)
+ * @return BaseResponse> 문제 목록을 포함한 페이징 응답
+ * @throws BaseException 페이지 번호가 1 미만이거나, 조회 권한이 없는 경우
*/
@GetMapping("/{userIdx}/problems")
@Operation(summary = "특정 유저의 참여중인 문제 조회 API", description = "특정 유저의 참여중인 문제를 조회합니다.")
public BaseResponse> getUserProblems(@AuthenticationPrincipal User user,
@PathVariable("userIdx") Integer userIdx,
@RequestParam("page") Integer page) {
- if (page < 1) {
- throw new BaseException(INVALID_PAGE);
- }
- return BaseResponse.of(MY_PAGE_PROBLEM_SEARCH_OK, userService.getUserProblems(user, userIdx,page - 1));
+ pagingUtils.validatePage(page);
+ return BaseResponse.of(MY_PAGE_PROBLEM_SEARCH_OK, userService.getUserProblems(user, userIdx,pagingUtils.toPageIndex(page)));
}
/**
- * 특정 유저의 신고 조회 API
- *
- * 특정 유저의 신고를 조회.
- *
- * @param user 인증된 유저 정보
- * @param page 페이지 번호
+ * 특정 사용자가 작성한 신고 목록을 조회합니다.
*
- * @return 특정 유저의 신고 조회 결과를 포함하는 BaseResponse>
+ * @param user 현재 인증된 사용자 정보
+ * @param userIdx 조회할 대상 사용자의 식별자
+ * @param page 조회할 페이지 번호 (1부터 시작)
+ * @return BaseResponse> 신고 목록을 포함한 페이징 응답
+ * @throws BaseException 페이지 번호가 1 미만이거나, 조회 권한이 없는 경우
*/
@GetMapping("/{userIdx}/reports")
@Operation(summary = "특정 유저의 신고 조회 API", description = "특정 유저의 신고를 조회합니다.")
public BaseResponse > getUserReports(@AuthenticationPrincipal User user,
@PathVariable("userIdx") Integer userIdx,
@RequestParam("page") Integer page) {
- if (page < 1) {
- throw new BaseException(INVALID_PAGE);
- }
- return BaseResponse.of(MY_PAGE_REPORT_SEARCH_OK, userService.getUserReports(user, userIdx, page - 1));
+ pagingUtils.validatePage(page);
+ return BaseResponse.of(MY_PAGE_REPORT_SEARCH_OK, userService.getUserReports(user, userIdx, pagingUtils.toPageIndex(page)));
}
/**
- * 특정 유저의 버그 제보 조회 API
- *
- * 특정 유저의 버그 제보를 조회.
- *
- * @param user 인증된 유저 정보
- * @param page 페이지 번호
- *
- * @return 특정 유저의 버그 제보 조회 결과를 포함하는 BaseResponse>
+ * 특정 사용자가 작성한 버그 제보 목록을 조회합니다.
+ *
+ * @param user 현재 인증된 사용자 정보
+ * @param userIdx 조회할 대상 사용자의 식별자
+ * @param searchBugReportCond 버그 제보 검색 조건
+ * @param page 조회할 페이지 번호 (1부터 시작)
+ * @return BaseResponse> 버그 제보 목록을 포함한 페이징 응답
+ * @throws BaseException 페이지 번호가 1 미만이거나, 조회 권한이 없는 경우
*/
@GetMapping("/{userIdx}/bug-reports")
@Operation(summary = "특정 유저의 버그 제보 조회 API", description = "특정 유저의 버그 제보를 조회합니다.")
@@ -200,19 +185,16 @@ public BaseResponse > getUserBugReports(@Authenti
@PathVariable("userIdx") Integer userIdx,
@Validated @ModelAttribute SearchBugReportCond searchBugReportCond,
@RequestParam("page") Integer page) {
- if (page < 1) {
- throw new BaseException(INVALID_PAGE);
- }
- return BaseResponse.of(MY_PAGE_BUG_REPORT_SEARCH_OK, userService.getUserBugReports(user, userIdx, searchBugReportCond, page - 1));
+ pagingUtils.validatePage(page);
+ return BaseResponse.of(MY_PAGE_BUG_REPORT_SEARCH_OK, userService.getUserBugReports(user, userIdx, searchBugReportCond, pagingUtils.toPageIndex(page)));
}
+
/**
- * 학생 회원가입 API
+ * 학생 회원가입을 처리합니다.
*
- * 학생 회원가입을 처리.
- *
- * @param studentSignupRequest 학생 회원가입 요청 정보
- *
- * @return 학생 회원가입 결과를 포함하는 BaseResponse
+ * @param studentSignupRequest 학생 회원가입 요청 정보 (이메일, 비밀번호, 이름, 학번, 학과 정보 등)
+ * @return BaseResponse 가입된 학생 정보를 포함한 응답
+ * @throws BaseException 이메일 중복, 유효하지 않은 학과 정보, 이메일 인증 실패 등의 경우
*/
@PostMapping("/student")
@Operation(summary = "학생 회원가입 API", description = "학생 회원가입을 처리합니다.")
@@ -222,13 +204,11 @@ public BaseResponse studentSignup(@Validated @RequestBody
}
/**
- * 교수 회원가입 API
- *
- * 교수 회원가입을 처리.
+ * 교수 회원가입을 처리합니다.
*
- * @param professorSignupRequest 교수 회원가입 요청 정보
- *
- * @return 교수 회원가입 결과를 포함하는 BaseResponse
+ * @param professorSignupRequest 교수 회원가입 요청 정보 (이메일, 비밀번호, 이름, 사번, 학과 정보 등)
+ * @return BaseResponse 가입된 교수 정보를 포함한 응답
+ * @throws BaseException 이메일 중복, 유효하지 않은 학과 정보, 이메일 인증 실패 등의 경우
*/
@PostMapping("/professor")
@Operation(summary = "교수 회원가입 API", description = "교수 회원가입을 처리합니다.")
@@ -238,13 +218,12 @@ public BaseResponse professorSignup(@Validated @Request
}
/**
- * 기업 회원가입 API
- *
- * 기업 회원가입을 처리.
- *
- * @param companySignupRequest 기업 회원가입 요청 정보
+ * 기업 회원가입을 처리합니다.
*
- * @return 기업 회원가입 결과를 포함하는 BaseResponse
+ * @param companySignupRequest 기업 회원가입 요청 정보 (이메일, 비밀번호, 이름, 회사명 등)
+ * @param evidence 사업자등록증 파일
+ * @return BaseResponse 가입된 기업 정보를 포함한 응답
+ * @throws BaseException 이메일 중복, 파일 업로드 실패, 이메일 인증 실패 등의 경우
*/
@PostMapping(value = "/company",consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@Operation(summary = "기업 회원가입 API", description = "기업 회원가입을 처리합니다.")
@@ -256,6 +235,13 @@ public BaseResponse companySignup(
return BaseResponse.of(COMPANY_SIGN_UP_OK, companyService.companySignup(companySignupRequest, evidence));
}
+ /**
+ * 로그인한 사용자의 비밀번호를 변경합니다.
+ *
+ * @param user 현재 인증된 사용자 정보
+ * @param updatePwRequest 변경할 비밀번호 정보
+ * @return BaseResponse 비밀번호가 변경된 사용자 정보를 포함한 응답
+ */
@PutMapping("/pw")
@Operation(summary = "비밀번호 변경 API", description = "비밀번호를 변경합니다.")
public BaseResponse changePassword(@AuthenticationPrincipal User user, @Validated @RequestBody UpdatePwRequest updatePwRequest) {
diff --git a/src/main/java/inha/git/user/api/service/CompanyServiceImpl.java b/src/main/java/inha/git/user/api/service/CompanyServiceImpl.java
index 32fc0257..e29af2aa 100644
--- a/src/main/java/inha/git/user/api/service/CompanyServiceImpl.java
+++ b/src/main/java/inha/git/user/api/service/CompanyServiceImpl.java
@@ -1,6 +1,7 @@
package inha.git.user.api.service;
import inha.git.auth.api.service.MailService;
+import inha.git.common.exceptions.BaseException;
import inha.git.user.api.controller.dto.request.CompanySignupRequest;
import inha.git.user.api.controller.dto.response.CompanySignupResponse;
import inha.git.user.api.mapper.UserMapper;
@@ -19,6 +20,10 @@
import static inha.git.common.Constant.*;
+/**
+ * 기업 관련 비즈니스 로직을 처리하는 서비스 구현체입니다.
+ * 기업 회원가입과 관련된 도메인 로직을 수행합니다.
+ */
@Service
@RequiredArgsConstructor
@Slf4j
@@ -32,11 +37,14 @@ public class CompanyServiceImpl implements CompanyService{
private final MailService mailService;
/**
- * 기업 회원가입
+ * 기업 회원가입을 처리합니다.
*
- * @param companySignupRequest 기업 회원가입 요청 정보
- * @param evidence 기업 등록증
- * @return 기업 회원가입 결과
+ * @param companySignupRequest 기업 회원가입 요청 정보 (이메일, 비밀번호, 이름, 회사명)
+ * @param evidence 사업자등록증 파일
+ * @return CompanySignupResponse 가입된 기업 정보를 포함한 응답
+ * @throws BaseException 다음의 경우에 발생:
+ * - EMAIL_AUTH_NOT_FOUND: 이메일 인증이 완료되지 않은 경우
+ * - FILE_CONVERT & FILE_NOT_FOUND: 파일 업로드 실패한 경우
*/
@Transactional
@Override
diff --git a/src/main/java/inha/git/user/api/service/ProfessorServiceImpl.java b/src/main/java/inha/git/user/api/service/ProfessorServiceImpl.java
index b6460a64..f8f5a5df 100644
--- a/src/main/java/inha/git/user/api/service/ProfessorServiceImpl.java
+++ b/src/main/java/inha/git/user/api/service/ProfessorServiceImpl.java
@@ -23,7 +23,10 @@
import static inha.git.common.Constant.*;
-
+/**
+ * 교수 관련 비즈니스 로직을 처리하는 서비스 구현체입니다.
+ * 교수 회원가입과 관련된 도메인 로직을 수행합니다.
+ */
@Service
@RequiredArgsConstructor
@Slf4j
@@ -51,11 +54,17 @@ public Page getProfessorStudents(String search, Integer p
Pageable pageable = PageRequest.of(page, 10, Sort.by(Sort.Direction.DESC, CREATE_AT));
return professorQueryRepository.searchStudents(search, pageable);
}
+
+
/**
- * 교수 회원가입
+ * 교수 회원가입을 처리합니다.
*
- * @param professorSignupRequest 교수 회원가입 요청 정보
- * @return 교수 회원가입 결과
+ * @param professorSignupRequest 교수 회원가입 요청 정보 (이메일, 비밀번호, 이름, 사번, 학과 정보)
+ * @return ProfessorSignupResponse 가입된 교수 정보를 포함한 응답
+ * @throws BaseException 다음의 경우에 발생:
+ * - INVALID_EMAIL_DOMAIN: 유효하지 않은 이메일 도메인
+ * - EMAIL_AUTH_NOT_FOUND: 이메일 인증이 완료되지 않은 경우
+ * - DEPARTMENT_NOT_FOUND: 존재하지 않는 학과인 경우
*/
@Transactional
@Override
diff --git a/src/main/java/inha/git/user/api/service/StudentServiceImpl.java b/src/main/java/inha/git/user/api/service/StudentServiceImpl.java
index 34dd04bb..b7e77db1 100644
--- a/src/main/java/inha/git/user/api/service/StudentServiceImpl.java
+++ b/src/main/java/inha/git/user/api/service/StudentServiceImpl.java
@@ -18,7 +18,8 @@
/**
- * StudentServiceImpl은 학생 관련 비즈니스 로직을 처리하는 서비스 클래스.
+ * 학생 관련 비즈니스 로직을 처리하는 서비스 구현체입니다.
+ * 학생 회원가입과 관련된 도메인 로직을 수행합니다.
*/
@Service
@RequiredArgsConstructor
@@ -34,10 +35,14 @@ public class StudentServiceImpl implements StudentService{
private final EmailDomainService emailDomainService;
/**
- * 학생 회원가입
+ * 학생 회원가입을 처리합니다.
*
- * @param studentSignupRequest 학생 회원가입 요청 정보
- * @return 학생 회원가입 결과
+ * @param studentSignupRequest 학생 회원가입 요청 정보 (이메일, 비밀번호, 이름, 학번, 학과 정보)
+ * @return StudentSignupResponse 가입된 학생 정보를 포함한 응답
+ * @throws BaseException 다음의 경우에 발생:
+ * - INVALID_EMAIL_DOMAIN: 유효하지 않은 이메일 도메인
+ * - EMAIL_AUTH_NOT_FOUND: 이메일 인증이 완료되지 않은 경우
+ * - DEPARTMENT_NOT_FOUND: 존재하지 않는 학과인 경우
*/
@Transactional
@Override
diff --git a/src/main/java/inha/git/user/domain/User.java b/src/main/java/inha/git/user/domain/User.java
index 73210d05..9e88a8ae 100644
--- a/src/main/java/inha/git/user/domain/User.java
+++ b/src/main/java/inha/git/user/domain/User.java
@@ -10,7 +10,6 @@
import lombok.*;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
-import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.ArrayList;
diff --git a/src/main/java/inha/git/utils/IdempotentProvider.java b/src/main/java/inha/git/utils/IdempotentProvider.java
index 36c1358b..2afcc6ef 100644
--- a/src/main/java/inha/git/utils/IdempotentProvider.java
+++ b/src/main/java/inha/git/utils/IdempotentProvider.java
@@ -24,7 +24,7 @@ public class IdempotentProvider {
/**
* Idempotency 키의 유효성을 검증하는 메서드.
*
- * @param keyElement 키를 구성하는 요소 리스트
+ * @param keyElement 키를 구성하는 isValidIdempotent요소 리스트
*/
public void isValidIdempotent(List keyElement) {
String idempotentKey = this.compactKey(keyElement);
diff --git a/src/main/java/inha/git/utils/PagingUtils.java b/src/main/java/inha/git/utils/PagingUtils.java
new file mode 100644
index 00000000..b0297396
--- /dev/null
+++ b/src/main/java/inha/git/utils/PagingUtils.java
@@ -0,0 +1,63 @@
+package inha.git.utils;
+
+import inha.git.common.exceptions.BaseException;
+import org.springframework.stereotype.Component;
+
+import static inha.git.common.code.status.ErrorStatus.INVALID_PAGE;
+import static inha.git.common.code.status.ErrorStatus.INVALID_SIZE;
+
+/**
+ * 페이징 처리를 위한 유틸리티 클래스입니다.
+ * 페이지 번호와 크기의 유효성 검증 및 변환 기능을 제공합니다.
+ */
+@Component
+public class PagingUtils {
+
+ private static final int MIN_PAGE = 1;
+ private static final int MIN_SIZE = 1;
+
+ /**
+ * 페이지 번호의 유효성을 검증합니다.
+ *
+ * @param page 검증할 페이지 번호
+ * @throws BaseException INVALID_PAGE: 페이지 번호가 최소값보다 작은 경우
+ */
+ public void validatePage(int page) {
+ if (page < MIN_PAGE) {
+ throw new BaseException(INVALID_PAGE);
+ }
+ }
+
+ /**
+ * 페이지 크기의 유효성을 검증합니다.
+ *
+ * @param size 검증할 페이지 크기
+ * @throws BaseException INVALID_SIZE: 페이지 크기가 최소값보다 작은 경우
+ */
+ public void validateSize(int size) {
+ if (size < MIN_SIZE) {
+ throw new BaseException(INVALID_SIZE);
+ }
+ }
+
+ /**
+ * 사용자가 입력한 페이지 번호를 인덱스로 변환합니다.
+ * (예: 페이지 1 → 인덱스 0)
+ *
+ * @param page 변환할 페이지 번호
+ * @return 변환된 페이지 인덱스
+ */
+ public int toPageIndex(int page) {
+ return page - 1;
+ }
+
+ /**
+ * 사용자가 입력한 페이지 크기를 실제 크기로 변환합니다.
+ *
+ * @param size 변환할 페이지 크기
+ * @return 변환된 페이지 크기
+ */
+ public int toPageSize(int size) {
+ return size - 1;
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/inha/git/GitApplicationTests.java b/src/test/java/inha/git/GitApplicationTests.java
index 14033342..b6ffc741 100644
--- a/src/test/java/inha/git/GitApplicationTests.java
+++ b/src/test/java/inha/git/GitApplicationTests.java
@@ -1,13 +1,10 @@
package inha.git;
-import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
+
@SpringBootTest
class GitApplicationTests {
- @Test
- void contextLoads() {
- }
-}
+}
\ No newline at end of file
diff --git a/src/test/java/inha/git/auth/api/controller/AuthControllerTest.java b/src/test/java/inha/git/auth/api/controller/AuthControllerTest.java
new file mode 100644
index 00000000..cac86ea5
--- /dev/null
+++ b/src/test/java/inha/git/auth/api/controller/AuthControllerTest.java
@@ -0,0 +1,230 @@
+package inha.git.auth.api.controller;
+
+import inha.git.auth.api.controller.dto.request.*;
+import inha.git.auth.api.controller.dto.response.FindEmailResponse;
+import inha.git.auth.api.controller.dto.response.LoginResponse;
+import inha.git.auth.api.service.AuthService;
+import inha.git.auth.api.service.MailService;
+import inha.git.common.BaseResponse;
+import inha.git.user.api.controller.dto.response.UserResponse;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.verify;
+
+@DisplayName("인증 컨트롤러 테스트")
+@ExtendWith(MockitoExtension.class)
+class AuthControllerTest {
+
+ @InjectMocks
+ private AuthController authController;
+
+ @Mock
+ private AuthService authService;
+
+ @Mock
+ private MailService mailService;
+
+ @Nested
+ @DisplayName("이메일 인증 테스트")
+ class emailTest {
+
+ @Test
+ @DisplayName("이메일 인증 성공")
+ void mailSend_Success() {
+ // given
+ EmailRequest request = createValidEmailRequest();
+ String expectedResponse = "이메일 전송 완료";
+
+ given(mailService.mailSend(request))
+ .willReturn(expectedResponse);
+
+ // when
+ BaseResponse response = authController.mailSend(request);
+
+ // then
+ assertThat(response.getResult()).isEqualTo(expectedResponse);
+ verify(mailService).mailSend(request);
+ }
+
+ @Test
+ @DisplayName("이메일 인증확인 성공")
+ void mailSendCheck_Success() {
+ // given
+ EmailCheckRequest request = createValidEmailCheckRequest();
+ given(mailService.mailSendCheck(request))
+ .willReturn(true);
+
+ // when
+ BaseResponse response = authController.mailSendCheck(request);
+
+ // then
+ assertThat(response.getResult()).isTrue();
+ verify(mailService).mailSendCheck(request);
+ }
+
+ private EmailRequest createValidEmailRequest() {
+ return new EmailRequest(
+ "test@test.com",
+ 1 // 인증 타입
+ );
+ }
+
+ private EmailCheckRequest createValidEmailCheckRequest() {
+ return new EmailCheckRequest(
+ "test@test.com",
+ 1,
+ "123456"
+ );
+ }
+
+ }
+
+ @Nested
+ @DisplayName("로그인 테스트")
+ class LoginTest {
+
+ @Test
+ @DisplayName("로그인 성공")
+ void login_Success() {
+ // given
+ LoginRequest request = createValidLoginRequest();
+ LoginResponse expectedResponse = new LoginResponse(1, "Bearer test.token");
+
+ given(authService.login(request))
+ .willReturn(expectedResponse);
+
+ // when
+ BaseResponse response = authController.login(request);
+
+ // then
+ assertThat(response.getResult()).isEqualTo(expectedResponse);
+ verify(authService).login(request);
+ }
+
+
+
+ private LoginRequest createValidLoginRequest() {
+ return new LoginRequest(
+ "test@test.com",
+ "password123!"
+ );
+ }
+ }
+
+ @Nested
+ @DisplayName("이메일 찾기 테스트")
+ class FindEmailTest {
+
+ @Test
+ @DisplayName("학번과 이름으로 이메일 찾기 성공")
+ void findEmail_Success() {
+ // given
+ FindEmailRequest request = createValidFindEmailRequest();
+ FindEmailResponse expectedResponse = new FindEmailResponse("test@inha.edu");
+
+ given(authService.findEmail(request))
+ .willReturn(expectedResponse);
+
+ // when
+ BaseResponse response = authController.findEmail(request);
+
+ // then
+ assertThat(response.getResult()).isEqualTo(expectedResponse);
+ verify(authService).findEmail(request);
+ }
+
+
+ private FindEmailRequest createValidFindEmailRequest() {
+ return new FindEmailRequest(
+ "12345678", // 학번
+ "홍길동" // 이름
+ );
+ }
+ }
+
+ @Nested
+ @DisplayName("비밀번호 찾기 테스트")
+ class FindPasswordTest {
+
+ @Test
+ @DisplayName("비밀번호 찾기 이메일 발송 성공")
+ void findPasswordMailSend_Success() {
+ // given
+ FindPasswordRequest request = createValidFindPasswordRequest();
+ String expectedResponse = "이메일 전송 완료";
+
+ given(mailService.findPasswordMailSend(request))
+ .willReturn(expectedResponse);
+
+ // when
+ BaseResponse response = authController.findPasswordMailSend(request);
+
+ // then
+ assertThat(response.getResult()).isEqualTo(expectedResponse);
+ verify(mailService).findPasswordMailSend(request);
+ }
+
+ @Test
+ @DisplayName("비밀번호 찾기 이메일 인증 확인 성공")
+ void findPasswordMailSendCheck_Success() {
+ // given
+ FindPasswordCheckRequest request = createValidFindPasswordCheckRequest();
+ given(mailService.findPasswordMailSendCheck(request))
+ .willReturn(true);
+
+ // when
+ BaseResponse response = authController.findPasswordMailSendCheck(request);
+
+ // then
+ assertThat(response.getResult()).isTrue();
+ verify(mailService).findPasswordMailSendCheck(request);
+ }
+
+ private FindPasswordRequest createValidFindPasswordRequest() {
+ return new FindPasswordRequest(
+ "test@test.com"
+ );
+ }
+
+ private FindPasswordCheckRequest createValidFindPasswordCheckRequest() {
+ return new FindPasswordCheckRequest(
+ "test@test.com",
+ "123456"
+ );
+ }
+ }
+
+ @Nested
+ @DisplayName("비밀번호 변경 테스트")
+ class ChangePasswordTest {
+
+ @Test
+ @DisplayName("비밀번호 변경 성공")
+ void changePassword_Success() {
+ // given
+ ChangePasswordRequest request = new ChangePasswordRequest(
+ "test@test.com",
+ "newPassword123!"
+ );
+ UserResponse expectedResponse = new UserResponse(1);
+
+ given(authService.changePassword(request))
+ .willReturn(expectedResponse);
+
+ // when
+ BaseResponse response = authController.findPassword(request);
+
+ // then
+ assertThat(response.getResult()).isEqualTo(expectedResponse);
+ verify(authService).changePassword(request);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/inha/git/auth/api/service/AuthServiceTest.java b/src/test/java/inha/git/auth/api/service/AuthServiceTest.java
new file mode 100644
index 00000000..e48aa7bb
--- /dev/null
+++ b/src/test/java/inha/git/auth/api/service/AuthServiceTest.java
@@ -0,0 +1,443 @@
+package inha.git.auth.api.service;
+
+import inha.git.auth.api.controller.dto.request.ChangePasswordRequest;
+import inha.git.auth.api.controller.dto.request.LoginRequest;
+import inha.git.auth.api.controller.dto.response.LoginResponse;
+import inha.git.auth.api.mapper.AuthMapper;
+import inha.git.common.exceptions.BaseException;
+import inha.git.user.api.controller.dto.response.UserResponse;
+import inha.git.user.domain.Company;
+import inha.git.user.domain.Professor;
+import inha.git.user.domain.User;
+import inha.git.user.domain.enums.Role;
+import inha.git.user.domain.repository.CompanyJpaRepository;
+import inha.git.user.domain.repository.ProfessorJpaRepository;
+import inha.git.user.domain.repository.UserJpaRepository;
+import inha.git.utils.RedisProvider;
+import inha.git.utils.jwt.JwtProvider;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+import java.time.LocalDateTime;
+import java.util.Optional;
+
+import static inha.git.common.BaseEntity.State.ACTIVE;
+import static inha.git.common.Constant.*;
+import static inha.git.common.code.status.ErrorStatus.*;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.BDDMockito.willDoNothing;
+import static org.mockito.Mockito.*;
+
+@DisplayName("인증 서비스 테스트")
+@ExtendWith(MockitoExtension.class)
+class AuthServiceTest {
+
+ @InjectMocks
+ private AuthServiceImpl authService;
+
+ @Mock
+ private MailService mailService;
+
+ @Mock
+ private UserJpaRepository userJpaRepository;
+
+ @Mock
+ private AuthenticationManager authenticationManager;
+
+ @Mock
+ private ProfessorJpaRepository professorJpaRepository;
+
+ @Mock
+ private CompanyJpaRepository companyJpaRepository;
+
+ @Mock
+ private PasswordEncoder passwordEncoder;
+
+ @Mock
+ private JwtProvider jwtProvider;
+
+ @Mock
+ private AuthMapper authMapper;
+
+ @Mock
+ private RedisProvider redisProvider;
+
+ @Nested
+ @DisplayName("로그인 테스트")
+ class LoginTest {
+
+ @Test
+ @DisplayName("학생 로그인 성공")
+ void login_Success() {
+ // given
+ LoginRequest request = createLoginRequest();
+ User user = createUser(Role.USER);
+ String accessToken = "test.access.token";
+ LoginResponse expectedResponse = createLoginResponse(user, accessToken);
+
+ given(userJpaRepository.findByEmailAndState(request.email(), ACTIVE))
+ .willReturn(Optional.of(user));
+ given(redisProvider.getValueOps(anyString()))
+ .willReturn(null); // 잠금 및 실패 횟수 없음
+ given(jwtProvider.generateToken(user))
+ .willReturn(accessToken);
+ given(authMapper.userToLoginResponse(user, TOKEN_PREFIX + accessToken))
+ .willReturn(expectedResponse);
+
+ // when
+ LoginResponse response = authService.login(request);
+
+ // then
+ assertThat(response).isEqualTo(expectedResponse);
+ verify(authenticationManager).authenticate(any());
+ }
+
+ @Test
+ @DisplayName("교수 로그인 성공")
+ void login_Professor_Success() {
+ // given
+ LoginRequest request = createLoginRequest();
+ User user = createUser(Role.PROFESSOR);
+ Professor professor = createApprovedProfessor(user);
+ String accessToken = "test.access.token";
+ LoginResponse expectedResponse = createLoginResponse(user, accessToken);
+
+ given(userJpaRepository.findByEmailAndState(request.email(), ACTIVE))
+ .willReturn(Optional.of(user));
+ given(redisProvider.getValueOps(anyString()))
+ .willReturn(null);
+ given(professorJpaRepository.findByUserId(user.getId()))
+ .willReturn(Optional.of(professor));
+ given(jwtProvider.generateToken(user))
+ .willReturn(accessToken);
+ given(authMapper.userToLoginResponse(user, TOKEN_PREFIX + accessToken))
+ .willReturn(expectedResponse);
+
+ // when
+ LoginResponse response = authService.login(request);
+
+ // then
+ assertThat(response).isEqualTo(expectedResponse);
+ verify(authenticationManager).authenticate(any());
+ }
+
+ @Test
+ @DisplayName("승인되지 않은 교수 로그인 실패")
+ void login_NotApprovedProfessor_ThrowsException() {
+ // given
+ LoginRequest request = createLoginRequest();
+ User user = createUser(Role.PROFESSOR);
+ Professor professor = createNotApprovedProfessor(user);
+
+ given(userJpaRepository.findByEmailAndState(request.email(), ACTIVE))
+ .willReturn(Optional.of(user));
+ given(redisProvider.getValueOps(anyString()))
+ .willReturn(null);
+ given(professorJpaRepository.findByUserId(user.getId()))
+ .willReturn(Optional.of(professor));
+
+ // when & then
+ assertThrows(BaseException.class, () -> authService.login(request))
+ .getErrorReason()
+ .equals(NOT_APPROVED_USER);
+ }
+
+ @Test
+ @DisplayName("기업 회원 로그인 성공")
+ void login_Company_Success() {
+ // given
+ LoginRequest request = createLoginRequest();
+ User user = createUser(Role.COMPANY);
+ Company company = createTestCompany(user, true); // 승인된 기업
+ String accessToken = "test.access.token";
+ LoginResponse expectedResponse = createLoginResponse(user, accessToken);
+
+ given(userJpaRepository.findByEmailAndState(request.email(), ACTIVE))
+ .willReturn(Optional.of(user));
+ given(redisProvider.getValueOps("lockout:" + request.email()))
+ .willReturn(null);
+ given(companyJpaRepository.findByUserId(user.getId()))
+ .willReturn(Optional.of(company));
+ given(jwtProvider.generateToken(user))
+ .willReturn(accessToken);
+ given(authMapper.userToLoginResponse(user, TOKEN_PREFIX + accessToken))
+ .willReturn(expectedResponse);
+
+ // when
+ LoginResponse response = authService.login(request);
+
+ // then
+ assertThat(response).isEqualTo(expectedResponse);
+ verify(authenticationManager).authenticate(any());
+ }
+
+ @Test
+ @DisplayName("승인되지 않은 기업 회원 로그인 실패")
+ void login_NotApprovedCompany_ThrowsException() {
+ // given
+ LoginRequest request = createLoginRequest();
+ User user = createUser(Role.COMPANY);
+ Company company = createTestCompany(user, false); // 승인되지 않은 기업
+
+ given(userJpaRepository.findByEmailAndState(request.email(), ACTIVE))
+ .willReturn(Optional.of(user));
+ given(redisProvider.getValueOps("lockout:" + request.email()))
+ .willReturn(null);
+ given(companyJpaRepository.findByUserId(user.getId()))
+ .willReturn(Optional.of(company));
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class,
+ () -> authService.login(request));
+
+ assertThat(exception.getErrorReason().getMessage())
+ .isEqualTo(NOT_APPROVED_USER.getMessage());
+ }
+
+ @Test
+ @DisplayName("계정 잠김 상태로 로그인 시도")
+ void login_AccountLocked_ThrowsException() {
+ // given
+ LoginRequest request = createLoginRequest();
+ User user = createUser(Role.USER);
+
+ given(userJpaRepository.findByEmailAndState(request.email(), ACTIVE))
+ .willReturn(Optional.of(user));
+ given(redisProvider.getValueOps("lockout:" + request.email()))
+ .willReturn("LOCKED");
+
+ // when & then
+ assertThrows(BaseException.class, () -> authService.login(request))
+ .getErrorReason()
+ .equals(ACCOUNT_LOCKED);
+ }
+
+ @Test
+ @DisplayName("차단된 사용자 로그인 시도")
+ void login_BlockedUser_ThrowsException() {
+ // given
+ LoginRequest request = createLoginRequest();
+ User user = createBlockedUser();
+
+ given(userJpaRepository.findByEmailAndState(request.email(), ACTIVE))
+ .willReturn(Optional.of(user));
+ given(redisProvider.getValueOps(anyString()))
+ .willReturn(null);
+
+ // when & then
+ assertThrows(BaseException.class, () -> authService.login(request))
+ .getErrorReason()
+ .equals(BLOCKED_USER);
+ }
+
+ @Test
+ @DisplayName("비밀번호 실패 횟수 초과로 계정 잠금")
+ void login_ExceedMaxFailedAttempts_AccountLocked() {
+ // given
+ LoginRequest request = createLoginRequest();
+ User user = createUser(Role.USER);
+
+ given(userJpaRepository.findByEmailAndState(request.email(), ACTIVE))
+ .willReturn(Optional.of(user));
+ given(redisProvider.getValueOps("lockout:" + request.email()))
+ .willReturn(null);
+ given(redisProvider.getValueOps("failedAttempts:" + request.email()))
+ .willReturn(String.valueOf(MAX_FAILED_ATTEMPTS - 1));
+ doThrow(new BadCredentialsException("Invalid credentials"))
+ .when(authenticationManager)
+ .authenticate(any());
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class,
+ () -> authService.login(request));
+
+ assertThat(exception.getErrorReason().getMessage())
+ .isEqualTo(ACCOUNT_LOCKED.getMessage());
+ }
+
+ @Test
+ @DisplayName("존재하지 않는 이메일로 로그인 시도")
+ void login_NonExistentEmail_ThrowsException() {
+ // given
+ LoginRequest request = createLoginRequest();
+ given(userJpaRepository.findByEmailAndState(request.email(), ACTIVE))
+ .willReturn(Optional.empty());
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class,
+ () -> authService.login(request));
+
+ assertThat(exception.getErrorReason().getMessage())
+ .isEqualTo(NOT_FIND_USER.getMessage());
+ }
+
+ @Test
+ @DisplayName("Redis 작업 실패 시 예외 발생")
+ void login_RedisOperationFails_ThrowsException() {
+ // given
+ LoginRequest request = createLoginRequest();
+ User user = createUser(Role.USER);
+
+ given(userJpaRepository.findByEmailAndState(request.email(), ACTIVE))
+ .willReturn(Optional.of(user));
+ given(redisProvider.getValueOps(anyString()))
+ .willThrow(new RuntimeException("Redis connection failed"));
+
+ // when & then
+ assertThrows(RuntimeException.class,
+ () -> authService.login(request));
+ }
+
+ private Company createTestCompany(User user, boolean isApproved) {
+ return Company.builder()
+ .id(1)
+ .user(user)
+ .affiliation("테스트기업")
+ .acceptedAt(isApproved ? LocalDateTime.now() : null)
+ .build();
+ }
+
+ private LoginRequest createLoginRequest() {
+ return new LoginRequest("test@test.com", "password123!");
+ }
+
+ private User createUser(Role role) {
+ return User.builder()
+ .id(1)
+ .email("test@test.com")
+ .pw("encodedPassword")
+ .role(role)
+ .build();
+ }
+
+ private User createBlockedUser() {
+ return User.builder()
+ .id(1)
+ .email("test@test.com")
+ .pw("encodedPassword")
+ .blockedAt(LocalDateTime.now())
+ .build();
+ }
+
+ private Professor createApprovedProfessor(User user) {
+ return Professor.builder()
+ .id(1)
+ .user(user)
+ .acceptedAt(LocalDateTime.now())
+ .build();
+ }
+
+ private Professor createNotApprovedProfessor(User user) {
+ return Professor.builder()
+ .id(1)
+ .user(user)
+ .build();
+ }
+
+ private LoginResponse createLoginResponse(User user, String accessToken) {
+ return new LoginResponse(
+ user.getId(),
+ TOKEN_PREFIX + accessToken
+ );
+ }
+ }
+
+ @Nested
+ @DisplayName("비밀번호 변경 테스트")
+ class ChangePasswordTest {
+
+ @Test
+ @DisplayName("이메일 인증 후 비밀번호 변경 성공")
+ void changePassword_Success() {
+ // given
+ ChangePasswordRequest request = new ChangePasswordRequest(
+ "test@test.com",
+ "newPassword123!"
+ );
+ User user = createUser();
+ UserResponse expectedResponse = new UserResponse(1);
+
+ willDoNothing().given(mailService).emailAuth(anyString(), anyString());
+ given(userJpaRepository.findByEmailAndState(request.email(), ACTIVE))
+ .willReturn(Optional.of(user));
+ given(passwordEncoder.encode(request.pw()))
+ .willReturn("encodedPassword");
+ given(authMapper.userToUserResponse(user))
+ .willReturn(expectedResponse);
+
+ // when
+ UserResponse response = authService.changePassword(request);
+
+ // then
+ assertThat(response).isEqualTo(expectedResponse);
+ verify(mailService).emailAuth(request.email(), PASSWORD_TYPE.toString());
+ verify(passwordEncoder).encode(request.pw());
+ verify(userJpaRepository).findByEmailAndState(request.email(), ACTIVE);
+ }
+
+ @Test
+ @DisplayName("이메일 인증되지 않은 경우 실패")
+ void changePassword_NotAuthenticated_ThrowsException() {
+ // given
+ ChangePasswordRequest request = new ChangePasswordRequest(
+ "test@test.com",
+ "newPassword123!"
+ );
+
+ doThrow(new BaseException(EMAIL_AUTH_NOT_FOUND))
+ .when(mailService)
+ .emailAuth(anyString(), anyString());
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class, () ->
+ authService.changePassword(request));
+
+ assertThat(exception.getErrorReason().getMessage())
+ .isEqualTo(EMAIL_AUTH_NOT_FOUND.getMessage());
+ verify(userJpaRepository, never()).findByEmailAndState(anyString(), any());
+ }
+
+ @Test
+ @DisplayName("존재하지 않는 이메일로 변경 시도")
+ void changePassword_UserNotFound_ThrowsException() {
+ // given
+ ChangePasswordRequest request = new ChangePasswordRequest(
+ "test@test.com",
+ "newPassword123!"
+ );
+
+ willDoNothing().given(mailService).emailAuth(anyString(), anyString());
+ given(userJpaRepository.findByEmailAndState(request.email(), ACTIVE))
+ .willReturn(Optional.empty());
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class, () ->
+ authService.changePassword(request));
+
+ assertThat(exception.getErrorReason().getMessage())
+ .isEqualTo(NOT_FIND_USER.getMessage());
+ verify(passwordEncoder, never()).encode(anyString());
+ }
+
+ private User createUser() {
+ return User.builder()
+ .id(1)
+ .email("test@test.com")
+ .name("홍길동")
+ .build();
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/inha/git/auth/api/service/MailServiceTest.java b/src/test/java/inha/git/auth/api/service/MailServiceTest.java
new file mode 100644
index 00000000..fde4d746
--- /dev/null
+++ b/src/test/java/inha/git/auth/api/service/MailServiceTest.java
@@ -0,0 +1,333 @@
+package inha.git.auth.api.service;
+
+import inha.git.auth.api.controller.dto.request.*;
+import inha.git.auth.api.controller.dto.response.FindEmailResponse;
+import inha.git.auth.api.mapper.AuthMapper;
+import inha.git.common.exceptions.BaseException;
+import inha.git.user.api.service.EmailDomainService;
+import inha.git.user.domain.User;
+import inha.git.user.domain.enums.Role;
+import inha.git.user.domain.repository.UserJpaRepository;
+import inha.git.utils.RedisProvider;
+import jakarta.mail.internet.MimeMessage;
+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.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.mail.javamail.JavaMailSender;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import java.util.Optional;
+
+import static inha.git.common.Constant.PASSWORD_TYPE;
+import static inha.git.common.code.status.ErrorStatus.*;
+import static inha.git.common.code.status.ErrorStatus.NOT_FIND_USER;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.BDDMockito.willDoNothing;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+@DisplayName("메일 서비스 테스트")
+@ExtendWith(MockitoExtension.class)
+class MailServiceTest {
+
+
+ @InjectMocks
+ private MailServiceImpl mailServiceImpl;
+
+ @InjectMocks
+ private AuthServiceImpl authService;
+
+ @Mock
+ private UserJpaRepository userJpaRepository;
+
+
+ @Mock
+ private JavaMailSender javaMailSender;
+
+ @Mock
+ private AuthMapper authMapper;
+
+ @Mock
+ private EmailDomainService emailDomainService;
+
+ @Mock
+ private RedisProvider redisProvider;
+
+ @BeforeEach
+ void setUp() {
+ ReflectionTestUtils.setField(mailServiceImpl, "username", "test@test.com");
+ }
+
+ @Nested
+ @DisplayName("이메일 인증 테스트")
+ class MailSendTest {
+
+ @Test
+ @DisplayName("이메일 인증번호 전송 성공")
+ void mailSend_Success() {
+ // given
+ EmailRequest request = new EmailRequest("test@inha.edu", 1);
+ MimeMessage mimeMessage = mock(MimeMessage.class);
+
+ given(redisProvider.getValueOps(anyString()))
+ .willReturn(null);
+ given(javaMailSender.createMimeMessage())
+ .willReturn(mimeMessage);
+ willDoNothing().given(javaMailSender).send(any(MimeMessage.class));
+ willDoNothing().given(emailDomainService)
+ .validateEmailDomain(anyString(), anyInt()); // Mock 동작 추가
+
+ // when
+ String result = mailServiceImpl.mailSend(request);
+
+ // then
+ assertThat(result).isEqualTo("이메일 전송 완료");
+ verify(javaMailSender).send(any(MimeMessage.class));
+ verify(emailDomainService).validateEmailDomain(request.email(), request.type());
+ verify(redisProvider).setDataExpire(
+ anyString(),
+ anyString(),
+ eq(60 * 3L)
+ );
+ }
+
+ @Test
+ @DisplayName("이메일 인증번호 확인 성공")
+ void mailSendCheck_Success() {
+ // given
+ EmailCheckRequest request = new EmailCheckRequest("test@inha.edu", 1, "123456");
+
+ given(redisProvider.getValueOps(anyString()))
+ .willReturn("123456");
+ willDoNothing().given(emailDomainService)
+ .validateEmailDomain(anyString(), anyInt()); // Mock 동작 추가
+
+ // when
+ Boolean result = mailServiceImpl.mailSendCheck(request);
+
+ // then
+ assertThat(result).isTrue();
+ verify(emailDomainService).validateEmailDomain(request.email(), request.type());
+ verify(redisProvider).setDataExpire(
+ startsWith("verification-"),
+ anyString(),
+ eq(60 * 60L)
+ );
+ }
+
+ @Test
+ @DisplayName("잘못된 인증번호로 확인 시도")
+ void mailSendCheck_InvalidAuthNumber_ThrowsException() {
+ // given
+ EmailCheckRequest request = new EmailCheckRequest("test@inha.edu", 1, "123456");
+
+ given(redisProvider.getValueOps(anyString()))
+ .willReturn("654321");
+ willDoNothing().given(emailDomainService)
+ .validateEmailDomain(anyString(), anyInt()); // Mock 동작 추가
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class, () ->
+ mailServiceImpl.mailSendCheck(request));
+
+ assertThat(exception.getErrorReason().getMessage())
+ .isEqualTo(EMAIL_AUTH_NOT_MATCH.getMessage());
+ }
+ }
+
+ @Nested
+ @DisplayName("이메일 찾기 테스트")
+ class FindEmailTest {
+
+ @Test
+ @DisplayName("유효한 학번과 이름으로 이메일 찾기 성공")
+ void findEmail_ValidUserNumberAndName_Success() {
+ // given
+ FindEmailRequest request = createValidFindEmailRequest();
+ User user = createUser();
+ FindEmailResponse expectedResponse = new FindEmailResponse(user.getEmail());
+
+ given(userJpaRepository.findByUserNumberAndName(request.userNumber(), request.name()))
+ .willReturn(Optional.of(user));
+ given(authMapper.userToFindEmailResponse(user))
+ .willReturn(expectedResponse);
+
+ // when
+ FindEmailResponse response = authService.findEmail(request);
+
+ // then
+ assertThat(response).isEqualTo(expectedResponse);
+ verify(userJpaRepository).findByUserNumberAndName(request.userNumber(), request.name());
+ verify(authMapper).userToFindEmailResponse(user);
+ }
+
+ @Test
+ @DisplayName("존재하지 않는 학번으로 조회시 예외 발생")
+ void findEmail_InvalidUserNumber_ThrowsException() {
+ // given
+ FindEmailRequest request = createValidFindEmailRequest();
+
+ given(userJpaRepository.findByUserNumberAndName(request.userNumber(), request.name()))
+ .willReturn(Optional.empty());
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class, () ->
+ authService.findEmail(request));
+
+ assertThat(exception.getErrorReason().getMessage())
+ .isEqualTo(NOT_FIND_USER.getMessage());
+ }
+
+ @Test
+ @DisplayName("일치하지 않는 이름으로 조회시 예외 발생")
+ void findEmail_InvalidName_ThrowsException() {
+ // given
+ FindEmailRequest request = new FindEmailRequest("12345678", "잘못된이름");
+
+ given(userJpaRepository.findByUserNumberAndName(request.userNumber(), request.name()))
+ .willReturn(Optional.empty());
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class, () ->
+ authService.findEmail(request));
+
+ assertThat(exception.getErrorReason().getMessage())
+ .isEqualTo(NOT_FIND_USER.getMessage());
+ }
+
+ private FindEmailRequest createValidFindEmailRequest() {
+ return new FindEmailRequest(
+ "12345678", // 학번
+ "홍길동" // 이름
+ );
+ }
+
+ private User createUser() {
+ return User.builder()
+ .id(1)
+ .email("test@inha.edu")
+ .name("홍길동")
+ .userNumber("12345678")
+ .role(Role.USER)
+ .build();
+ }
+ }
+
+ @Nested
+ @DisplayName("비밀번호 찾기 이메일 인증 테스트")
+ class FindPasswordMailTest {
+
+ @Test
+ @DisplayName("비밀번호 찾기 이메일 전송 성공")
+ void findPasswordMailSend_Success() {
+ // given
+ FindPasswordRequest request = new FindPasswordRequest("test@test.com");
+ User user = createUser();
+ MimeMessage mimeMessage = mock(MimeMessage.class);
+
+ given(userJpaRepository.findByEmail(request.email()))
+ .willReturn(Optional.of(user));
+ given(redisProvider.getValueOps(anyString()))
+ .willReturn(null);
+ given(javaMailSender.createMimeMessage())
+ .willReturn(mimeMessage);
+ willDoNothing().given(javaMailSender).send(any(MimeMessage.class));
+
+ // when
+ String result = mailServiceImpl.findPasswordMailSend(request);
+
+ // then
+ assertThat(result).isEqualTo("이메일 전송 완료");
+ verify(javaMailSender).send(any(MimeMessage.class));
+ verify(redisProvider).setDataExpire(
+ anyString(),
+ anyString(),
+ eq(60 * 3L)
+ );
+ }
+
+ @Test
+ @DisplayName("존재하지 않는 이메일로 비밀번호 찾기 시도")
+ void findPasswordMailSend_EmailNotFound_ThrowsException() {
+ // given
+ FindPasswordRequest request = new FindPasswordRequest("invalid@test.com");
+
+ given(userJpaRepository.findByEmail(request.email()))
+ .willReturn(Optional.empty());
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class, () ->
+ mailServiceImpl.findPasswordMailSend(request));
+
+ assertThat(exception.getErrorReason().getMessage())
+ .isEqualTo(EMAIL_NOT_FOUND.getMessage());
+ }
+
+ @Test
+ @DisplayName("비밀번호 찾기 인증번호 확인 성공")
+ void findPasswordMailSendCheck_Success() {
+ // given
+ FindPasswordCheckRequest request = new FindPasswordCheckRequest(
+ "test@test.com",
+ "123456"
+ );
+ User user = createUser();
+
+ given(userJpaRepository.findByEmail(request.email()))
+ .willReturn(Optional.of(user));
+ given(redisProvider.getValueOps(request.email() + "-" + PASSWORD_TYPE))
+ .willReturn("123456");
+
+ // when
+ Boolean result = mailServiceImpl.findPasswordMailSendCheck(request);
+
+ // then
+ assertThat(result).isTrue();
+ verify(redisProvider).setDataExpire(
+ anyString(),
+ anyString(),
+ eq(60 * 60L)
+ );
+ }
+
+ @Test
+ @DisplayName("만료된 인증번호로 확인 시도")
+ void findPasswordMailSendCheck_AuthExpired_ThrowsException() {
+ // given
+ FindPasswordCheckRequest request = new FindPasswordCheckRequest(
+ "test@test.com",
+ "123456"
+ );
+ User user = createUser();
+
+ given(userJpaRepository.findByEmail(request.email()))
+ .willReturn(Optional.of(user));
+ given(redisProvider.getValueOps(request.email() + "-" + PASSWORD_TYPE))
+ .willReturn(null);
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class, () ->
+ mailServiceImpl.findPasswordMailSendCheck(request));
+
+ assertThat(exception.getErrorReason().getMessage())
+ .isEqualTo(EMAIL_AUTH_EXPIRED.getMessage());
+ }
+
+ private User createUser() {
+ return User.builder()
+ .id(1)
+ .email("test@test.com")
+ .name("테스트유저")
+ .build();
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/inha/git/category/api/controller/CategoryControllerTest.java b/src/test/java/inha/git/category/api/controller/CategoryControllerTest.java
new file mode 100644
index 00000000..a758af4d
--- /dev/null
+++ b/src/test/java/inha/git/category/api/controller/CategoryControllerTest.java
@@ -0,0 +1,127 @@
+package inha.git.category.api.controller;
+
+import inha.git.category.controller.CategoryController;
+import inha.git.category.controller.dto.request.CreateCategoryRequest;
+import inha.git.category.controller.dto.request.UpdateCategoryRequest;
+import inha.git.category.controller.dto.response.SearchCategoryResponse;
+import inha.git.category.service.CategoryService;
+import inha.git.common.BaseResponse;
+import inha.git.user.domain.User;
+import inha.git.user.domain.enums.Role;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.verify;
+
+@DisplayName("카테고리 컨트롤러 테스트")
+@ExtendWith(MockitoExtension.class)
+class CategoryControllerTest {
+
+ @InjectMocks
+ private CategoryController categoryController;
+
+ @Mock
+ private CategoryService categoryService;
+
+ @Test
+ @DisplayName("카테고리 전체 조회 성공")
+ void getCategories_Success() {
+ // given
+ List expectedResponses = Arrays.asList(
+ new SearchCategoryResponse(1, "교과"),
+ new SearchCategoryResponse(2, "기타"),
+ new SearchCategoryResponse(3, "비교과")
+ );
+
+ given(categoryService.getCategories())
+ .willReturn(expectedResponses);
+
+ // when
+ BaseResponse> response =
+ categoryController.getCategories();
+
+ // then
+ assertThat(response.getResult())
+ .isEqualTo(expectedResponses);
+ verify(categoryService).getCategories();
+ }
+
+ @Test
+ @DisplayName("카테고리 생성 성공")
+ void createCategory_Success() {
+ // given
+ User admin = createAdminUser();
+ CreateCategoryRequest request = new CreateCategoryRequest("신규카테고리");
+ String expectedResponse = "신규카테고리 카테고리가 생성되었습니다.";
+
+ given(categoryService.createCategory(admin, request))
+ .willReturn(expectedResponse);
+
+ // when
+ BaseResponse response =
+ categoryController.createCategory(admin, request);
+
+ // then
+ assertThat(response.getResult()).isEqualTo(expectedResponse);
+ verify(categoryService).createCategory(admin, request);
+ }
+
+ @Test
+ @DisplayName("카테고리 이름 수정 성공")
+ void updateCategory_Success() {
+ // given
+ User admin = createAdminUser();
+ Integer categoryIdx = 1;
+ UpdateCategoryRequest request = new UpdateCategoryRequest("수정된카테고리");
+ String expectedResponse = "수정된카테고리 카테고리 이름이 수정되었습니다.";
+
+ given(categoryService.updateCategoryName(admin, categoryIdx, request))
+ .willReturn(expectedResponse);
+
+ // when
+ BaseResponse response =
+ categoryController.updateCategory(admin, categoryIdx, request);
+
+ // then
+ assertThat(response.getResult()).isEqualTo(expectedResponse);
+ verify(categoryService).updateCategoryName(admin, categoryIdx, request);
+ }
+
+ @Test
+ @DisplayName("카테고리 삭제 성공")
+ void deleteCategory_Success() {
+ // given
+ User admin = createAdminUser();
+ Integer categoryIdx = 1;
+ String expectedResponse = "테스트카테고리 카테고리 삭제되었습니다.";
+
+ given(categoryService.deleteCategory(admin, categoryIdx))
+ .willReturn(expectedResponse);
+
+ // when
+ BaseResponse response =
+ categoryController.deleteCategory(admin, categoryIdx);
+
+ // then
+ assertThat(response.getResult()).isEqualTo(expectedResponse);
+ verify(categoryService).deleteCategory(admin, categoryIdx);
+ }
+ private User createAdminUser() {
+ return User.builder()
+ .id(1)
+ .email("admin@test.com")
+ .name("관리자")
+ .role(Role.ADMIN)
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/inha/git/category/api/service/CategoryServiceTest.java b/src/test/java/inha/git/category/api/service/CategoryServiceTest.java
new file mode 100644
index 00000000..bf001959
--- /dev/null
+++ b/src/test/java/inha/git/category/api/service/CategoryServiceTest.java
@@ -0,0 +1,220 @@
+package inha.git.category.api.service;
+
+import inha.git.category.controller.dto.request.CreateCategoryRequest;
+import inha.git.category.controller.dto.request.UpdateCategoryRequest;
+import inha.git.category.controller.dto.response.SearchCategoryResponse;
+import inha.git.category.domain.Category;
+import inha.git.category.domain.repository.CategoryJpaRepository;
+import inha.git.category.mapper.CategoryMapper;
+import inha.git.category.service.CategoryServiceImpl;
+import inha.git.common.BaseEntity;
+import inha.git.common.exceptions.BaseException;
+import inha.git.user.domain.User;
+import inha.git.user.domain.enums.Role;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.dao.DataIntegrityViolationException;
+import org.springframework.data.domain.Sort;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+import static inha.git.common.BaseEntity.State.ACTIVE;
+import static inha.git.common.BaseEntity.State.INACTIVE;
+import static inha.git.common.code.status.ErrorStatus.CATEGORY_NOT_FOUND;
+import static org.assertj.core.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.verify;
+
+@DisplayName("카테고리 서비스 테스트")
+@ExtendWith(MockitoExtension.class)
+class CategoryServiceTest {
+
+ @InjectMocks
+ private CategoryServiceImpl categoryService;
+
+ @Mock
+ private CategoryJpaRepository categoryJpaRepository;
+
+ @Mock
+ private CategoryMapper categoryMapper;
+
+ @Test
+ @DisplayName("카테고리 전체 조회 성공")
+ void getCategories_Success() {
+ // given
+ List categories = Arrays.asList(
+ createCategory(1, "교과"),
+ createCategory(2, "기타"),
+ createCategory(3, "비교과")
+ );
+
+ List expectedResponses = Arrays.asList(
+ new SearchCategoryResponse(1, "교과"),
+ new SearchCategoryResponse(2, "기타"),
+ new SearchCategoryResponse(3, "비교과")
+ );
+
+ given(categoryJpaRepository.findAllByState(ACTIVE, Sort.by(Sort.Direction.ASC, "name")))
+ .willReturn(categories);
+ given(categoryMapper.categoriesToSearchCategoryResponses(categories))
+ .willReturn(expectedResponses);
+
+ // when
+ List result = categoryService.getCategories();
+
+ // then
+ assertThat(result)
+ .hasSize(3)
+ .isEqualTo(expectedResponses);
+ verify(categoryJpaRepository).findAllByState(ACTIVE, Sort.by(Sort.Direction.ASC, "name"));
+ }
+
+ private Category createCategory(int id, String name) {
+ return Category.builder()
+ .id(id)
+ .name(name)
+ .build();
+ }
+
+ @Test
+ @DisplayName("카테고리 생성 성공")
+ void createCategory_Success() {
+ // given
+ User admin = createAdminUser();
+ CreateCategoryRequest request = new CreateCategoryRequest("신규카테고리");
+ Category category = Category.builder()
+ .id(1)
+ .name("신규카테고리")
+ .build();
+
+ given(categoryMapper.createCategoryRequestToSemester(request))
+ .willReturn(category);
+ given(categoryJpaRepository.save(any(Category.class)))
+ .willReturn(category);
+
+ // when
+ String result = categoryService.createCategory(admin, request);
+
+ // then
+ assertThat(result).isEqualTo("신규카테고리 카테고리가 생성되었습니다.");
+ verify(categoryJpaRepository).save(any(Category.class));
+ verify(categoryMapper).createCategoryRequestToSemester(request);
+ }
+
+ @Test
+ @DisplayName("중복된 카테고리명으로 생성 시도")
+ void createCategory_DuplicateName_ThrowsException() {
+ // given
+ User admin = createAdminUser();
+ CreateCategoryRequest request = new CreateCategoryRequest("기존카테고리");
+ Category category = createCategory(request.name());
+
+ given(categoryMapper.createCategoryRequestToSemester(request))
+ .willReturn(category);
+ given(categoryJpaRepository.save(any(Category.class)))
+ .willThrow(new DataIntegrityViolationException("Duplicate entry"));
+
+ // when & then
+ assertThrows(DataIntegrityViolationException.class, () ->
+ categoryService.createCategory(admin, request));
+ }
+
+ @Test
+ @DisplayName("카테고리 이름 수정 성공")
+ void updateCategoryName_Success() {
+ // given
+ User admin = createAdminUser();
+ Integer categoryIdx = 1;
+ UpdateCategoryRequest request = new UpdateCategoryRequest("수정된카테고리");
+ Category category = createCategory("기존카테고리");
+
+ given(categoryJpaRepository.findByIdAndState(categoryIdx, ACTIVE))
+ .willReturn(Optional.of(category));
+
+ // when
+ String result = categoryService.updateCategoryName(admin, categoryIdx, request);
+
+ // then
+ assertThat(result).isEqualTo("수정된카테고리 카테고리 이름이 수정되었습니다.");
+ assertThat(category.getName()).isEqualTo("수정된카테고리");
+ }
+
+ @Test
+ @DisplayName("존재하지 않는 카테고리 수정 시도")
+ void updateCategoryName_CategoryNotFound_ThrowsException() {
+ // given
+ User admin = createAdminUser();
+ Integer categoryIdx = 999;
+ UpdateCategoryRequest request = new UpdateCategoryRequest("수정된카테고리");
+
+ given(categoryJpaRepository.findByIdAndState(categoryIdx, ACTIVE))
+ .willReturn(Optional.empty());
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class, () ->
+ categoryService.updateCategoryName(admin, categoryIdx, request));
+ assertThat(exception.getErrorReason().getMessage())
+ .isEqualTo(CATEGORY_NOT_FOUND.getMessage());
+ }
+
+ @Test
+ @DisplayName("카테고리 삭제 성공")
+ void deleteCategory_Success() {
+ // given
+ User admin = createAdminUser();
+ Integer categoryIdx = 1;
+ Category category = createCategory("삭제할카테고리");
+
+ given(categoryJpaRepository.findByIdAndState(categoryIdx, ACTIVE))
+ .willReturn(Optional.of(category));
+
+ // when
+ String result = categoryService.deleteCategory(admin, categoryIdx);
+
+ // then
+ assertThat(result).isEqualTo("삭제할카테고리 카테고리 삭제되었습니다.");
+ assertThat(category.getState()).isEqualTo(INACTIVE);
+ assertThat(category.getDeletedAt()).isNotNull();
+ }
+
+ @Test
+ @DisplayName("존재하지 않는 카테고리 삭제 시도")
+ void deleteCategory_CategoryNotFound_ThrowsException() {
+ // given
+ User admin = createAdminUser();
+ Integer categoryIdx = 999;
+
+ given(categoryJpaRepository.findByIdAndState(categoryIdx, ACTIVE))
+ .willReturn(Optional.empty());
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class, () ->
+ categoryService.deleteCategory(admin, categoryIdx));
+ assertThat(exception.getErrorReason().getMessage())
+ .isEqualTo(CATEGORY_NOT_FOUND.getMessage());
+ }
+
+ private Category createCategory(String name) {
+ return Category.builder()
+ .id(1)
+ .name(name)
+ .build();
+ }
+
+ private User createAdminUser() {
+ return User.builder()
+ .id(1)
+ .email("admin@test.com")
+ .name("관리자")
+ .role(Role.ADMIN)
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/inha/git/college/api/controller/CollegeControllerTest.java b/src/test/java/inha/git/college/api/controller/CollegeControllerTest.java
new file mode 100644
index 00000000..9ef7638e
--- /dev/null
+++ b/src/test/java/inha/git/college/api/controller/CollegeControllerTest.java
@@ -0,0 +1,140 @@
+package inha.git.college.api.controller;
+
+import inha.git.college.controller.CollegeController;
+import inha.git.college.controller.dto.request.CreateCollegeRequest;
+import inha.git.college.controller.dto.request.UpdateCollegeRequest;
+import inha.git.college.controller.dto.response.SearchCollegeResponse;
+import inha.git.college.service.CollegeService;
+import inha.git.common.BaseResponse;
+import inha.git.user.domain.User;
+import inha.git.user.domain.enums.Role;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.verify;
+
+@DisplayName("단과대 컨트롤러 테스트")
+@ExtendWith(MockitoExtension.class)
+class CollegeControllerTest {
+
+ @InjectMocks
+ private CollegeController collegeController;
+
+ @Mock
+ private CollegeService collegeService;
+
+ @Test
+ @DisplayName("단과대 전체 조회 성공")
+ void getColleges_Success() {
+ // given
+ List expectedResponses = Arrays.asList(
+ new SearchCollegeResponse(1, "소프트웨어융합대학"),
+ new SearchCollegeResponse(2, "공과대학")
+ );
+
+ given(collegeService.getColleges())
+ .willReturn(expectedResponses);
+
+ // when
+ BaseResponse> response = collegeController.getColleges();
+
+ // then
+ assertThat(response.getResult()).isEqualTo(expectedResponses);
+ verify(collegeService).getColleges();
+ }
+
+ @Test
+ @DisplayName("특정 단과대 조회 성공")
+ void getCollege_Success() {
+ // given
+ Integer departmentIdx = 1;
+ SearchCollegeResponse expectedResponse = new SearchCollegeResponse(1, "소프트웨어융합대학");
+
+
+ given(collegeService.getCollege(departmentIdx))
+ .willReturn(expectedResponse);
+
+ // when
+ BaseResponse response = collegeController.getCollege(departmentIdx);
+
+ // then
+ assertThat(response.getResult()).isEqualTo(expectedResponse);
+ verify(collegeService).getCollege(departmentIdx);
+ }
+
+ @Test
+ @DisplayName("단과대 생성 성공")
+ void createCollege_Success() {
+ // given
+ User admin = createAdminUser();
+ CreateCollegeRequest request = new CreateCollegeRequest("신설단과대학");
+ String expectedResponse = "신설단과대학 단과대가 생성되었습니다.";
+
+ given(collegeService.createCollege(admin, request))
+ .willReturn(expectedResponse);
+
+ // when
+ BaseResponse response = collegeController.createCollege(admin, request);
+
+ // then
+ assertThat(response.getResult()).isEqualTo(expectedResponse);
+ verify(collegeService).createCollege(admin, request);
+ }
+
+ @Test
+ @DisplayName("단과대 수정 성공")
+ void updateCollege_Success() {
+ // given
+ User admin = createAdminUser();
+ Integer collegeIdx = 1;
+ UpdateCollegeRequest request = new UpdateCollegeRequest("수정된단과대학");
+ String expectedResponse = "수정된단과대학 단과대 이름이 변경되었습니다.";
+
+ given(collegeService.updateCollegeName(admin, collegeIdx, request))
+ .willReturn(expectedResponse);
+
+ // when
+ BaseResponse response = collegeController.updateCollege(admin, collegeIdx, request);
+
+ // then
+ assertThat(response.getResult()).isEqualTo(expectedResponse);
+ verify(collegeService).updateCollegeName(admin, collegeIdx, request);
+ }
+
+ @Test
+ @DisplayName("단과대 삭제 성공")
+ void deleteCollege_Success() {
+ // given
+ User admin = createAdminUser();
+ Integer collegeIdx = 1;
+ String expectedResponse = "IT공과대학 단과대가 삭제되었습니다.";
+
+ given(collegeService.deleteCollege(admin, collegeIdx))
+ .willReturn(expectedResponse);
+
+ // when
+ BaseResponse response = collegeController.deleteCollege(admin, collegeIdx);
+
+ // then
+ assertThat(response.getResult()).isEqualTo(expectedResponse);
+ verify(collegeService).deleteCollege(admin, collegeIdx);
+ }
+
+ private User createAdminUser() {
+ return User.builder()
+ .id(1)
+ .email("admin@test.com")
+ .name("관리자")
+ .role(Role.ADMIN)
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/inha/git/college/api/service/CollegeServiceTest.java b/src/test/java/inha/git/college/api/service/CollegeServiceTest.java
new file mode 100644
index 00000000..31652d8e
--- /dev/null
+++ b/src/test/java/inha/git/college/api/service/CollegeServiceTest.java
@@ -0,0 +1,204 @@
+package inha.git.college.api.service;
+
+import inha.git.college.controller.dto.request.CreateCollegeRequest;
+import inha.git.college.controller.dto.request.UpdateCollegeRequest;
+import inha.git.college.controller.dto.response.SearchCollegeResponse;
+import inha.git.college.domain.College;
+import inha.git.college.domain.repository.CollegeJpaRepository;
+import inha.git.college.mapper.CollegeMapper;
+import inha.git.college.service.CollegeServiceImpl;
+import inha.git.common.exceptions.BaseException;
+import inha.git.department.domain.Department;
+import inha.git.department.domain.repository.DepartmentJpaRepository;
+import inha.git.statistics.domain.repository.TotalCollegeStatisticsJpaRepository;
+import inha.git.user.domain.User;
+import inha.git.user.domain.enums.Role;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+import static inha.git.common.BaseEntity.State.ACTIVE;
+import static inha.git.common.BaseEntity.State.INACTIVE;
+import static inha.git.common.code.status.ErrorStatus.DEPARTMENT_NOT_FOUND;
+import static org.assertj.core.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.verify;
+
+@DisplayName("단과대 서비스 테스트")
+@ExtendWith(MockitoExtension.class)
+class CollegeServiceTest {
+
+ @InjectMocks
+ private CollegeServiceImpl collegeService;
+
+ @Mock
+ private CollegeJpaRepository collegeJpaRepository;
+
+ @Mock
+ private TotalCollegeStatisticsJpaRepository totalCollegeStatisticsJpaRepository;
+
+ @Mock
+ private DepartmentJpaRepository departmentJpaRepository;
+
+ @Mock
+ private CollegeMapper collegeMapper;
+
+ @Test
+ @DisplayName("단과대 전체 조회 성공")
+ void getColleges_Success() {
+ // given
+ List colleges = Arrays.asList(
+ createCollege(1, "소프트웨어융합대학"),
+ createCollege(2, "공과대학")
+ );
+ List expectedResponses = Arrays.asList(
+ new SearchCollegeResponse(1, "소프트웨어융합대학"),
+ new SearchCollegeResponse(2, "공과대학")
+ );
+
+ given(collegeJpaRepository.findAllByState(ACTIVE))
+ .willReturn(colleges);
+ given(collegeMapper.collegesToSearchCollegeResponses(colleges))
+ .willReturn(expectedResponses);
+
+ // when
+ List result = collegeService.getColleges();
+
+ // then
+ assertThat(result).isEqualTo(expectedResponses);
+ verify(collegeJpaRepository).findAllByState(ACTIVE);
+ }
+
+ @Test
+ @DisplayName("특정 단과대 조회 성공")
+ void getCollege_Success() {
+ // given
+ Integer departmentIdx = 1;
+ Department department = createDepartment(departmentIdx, "컴퓨터공학과");
+ College college = createCollege(1, "소프트웨어융합대학");
+ SearchCollegeResponse expectedResponse = new SearchCollegeResponse(1, "소프트웨어융합대학");
+
+ given(departmentJpaRepository.findByIdAndState(departmentIdx, ACTIVE))
+ .willReturn(Optional.of(department));
+ given(collegeJpaRepository.findByDepartments_IdAndState(departmentIdx, ACTIVE))
+ .willReturn(Optional.of(college));
+ given(collegeMapper.collegeToSearchCollegeResponse(college))
+ .willReturn(expectedResponse);
+
+ // when
+ SearchCollegeResponse result = collegeService.getCollege(departmentIdx);
+
+ // then
+ assertThat(result).isEqualTo(expectedResponse);
+ }
+
+ @Test
+ @DisplayName("존재하지 않는 학과로 단과대 조회 시 예외 발생")
+ void getCollege_DepartmentNotFound_ThrowsException() {
+ // given
+ Integer departmentIdx = 999;
+
+ given(departmentJpaRepository.findByIdAndState(departmentIdx, ACTIVE))
+ .willReturn(Optional.empty());
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class, () ->
+ collegeService.getCollege(departmentIdx));
+ assertThat(exception.getErrorReason().getMessage())
+ .isEqualTo(DEPARTMENT_NOT_FOUND.getMessage());
+ }
+
+ @Test
+ @DisplayName("단과대 생성 성공")
+ void createCollege_Success() {
+ // given
+ User admin = createAdminUser();
+ CreateCollegeRequest request = new CreateCollegeRequest("신설단과대학");
+ College college = createCollege(1, "신설단과대학");
+
+ given(collegeMapper.createCollegeRequestToCollege(request))
+ .willReturn(college);
+ given(collegeJpaRepository.save(any(College.class)))
+ .willReturn(college);
+
+ // when
+ String result = collegeService.createCollege(admin, request);
+
+ // then
+ assertThat(result).isEqualTo("신설단과대학 단과대가 생성되었습니다.");
+ verify(collegeJpaRepository).save(any(College.class));
+ verify(totalCollegeStatisticsJpaRepository).save(any());
+ }
+
+ @Test
+ @DisplayName("단과대 이름 수정 성공")
+ void updateCollegeName_Success() {
+ // given
+ User admin = createAdminUser();
+ Integer collegeIdx = 1;
+ UpdateCollegeRequest request = new UpdateCollegeRequest("수정된단과대학");
+ College college = createCollege(collegeIdx, "기존단과대학");
+
+ given(collegeJpaRepository.findByIdAndState(collegeIdx, ACTIVE))
+ .willReturn(Optional.of(college));
+
+ // when
+ String result = collegeService.updateCollegeName(admin, collegeIdx, request);
+
+ // then
+ assertThat(result).isEqualTo("수정된단과대학 단과대 이름이 변경되었습니다.");
+ assertThat(college.getName()).isEqualTo("수정된단과대학");
+ }
+
+ @Test
+ @DisplayName("단과대 삭제 성공")
+ void deleteCollege_Success() {
+ // given
+ User admin = createAdminUser();
+ Integer collegeIdx = 1;
+ College college = createCollege(collegeIdx, "삭제할단과대학");
+
+ given(collegeJpaRepository.findByIdAndState(collegeIdx, ACTIVE))
+ .willReturn(Optional.of(college));
+
+ // when
+ String result = collegeService.deleteCollege(admin, collegeIdx);
+
+ // then
+ assertThat(result).isEqualTo("삭제할단과대학 단과대가 삭제되었습니다.");
+ assertThat(college.getState()).isEqualTo(INACTIVE);
+ assertThat(college.getDeletedAt()).isNotNull();
+ }
+
+ private College createCollege(Integer id, String name) {
+ return College.builder()
+ .id(id)
+ .name(name)
+ .build();
+ }
+
+ private Department createDepartment(Integer id, String name) {
+ return Department.builder()
+ .id(id)
+ .name(name)
+ .build();
+ }
+
+ private User createAdminUser() {
+ return User.builder()
+ .id(1)
+ .email("admin@test.com")
+ .name("관리자")
+ .role(Role.ADMIN)
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/inha/git/department/api/controller/DepartmentControllerTest.java b/src/test/java/inha/git/department/api/controller/DepartmentControllerTest.java
new file mode 100644
index 00000000..826ebf98
--- /dev/null
+++ b/src/test/java/inha/git/department/api/controller/DepartmentControllerTest.java
@@ -0,0 +1,123 @@
+package inha.git.department.api.controller;
+
+import inha.git.admin.api.controller.dto.response.SearchDepartmentResponse;
+import inha.git.common.BaseResponse;
+import inha.git.department.api.controller.dto.request.CreateDepartmentRequest;
+import inha.git.department.api.controller.dto.request.UpdateDepartmentRequest;
+import inha.git.department.api.service.DepartmentService;
+import inha.git.user.domain.User;
+import inha.git.user.domain.enums.Role;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.verify;
+
+@DisplayName("학과 컨트롤러 테스트")
+@ExtendWith(MockitoExtension.class)
+class DepartmentControllerTest {
+
+ @InjectMocks
+ private DepartmentController departmentController;
+
+ @Mock
+ private DepartmentService departmentService;
+
+ @Test
+ @DisplayName("학과 전체 조회 성공")
+ void getDepartments_Success() {
+ // given
+ Integer collegeIdx = 1;
+ List expectedResponses = Arrays.asList(
+ new SearchDepartmentResponse(1, "컴퓨터공학과"),
+ new SearchDepartmentResponse(2, "정보통신공학과")
+ );
+ given(departmentService.getDepartments(collegeIdx))
+ .willReturn(expectedResponses);
+
+ // when
+ BaseResponse> response =
+ departmentController.getDepartments(collegeIdx);
+
+ // then
+ assertThat(response.getResult()).isEqualTo(expectedResponses);
+ verify(departmentService).getDepartments(collegeIdx);
+ }
+
+ @Test
+ @DisplayName("학과 생성 성공")
+ void createDepartment_Success() {
+ // given
+ User admin = createAdminUser();
+ CreateDepartmentRequest request = new CreateDepartmentRequest(1,"신설학과");
+ String expectedResponse = "신설학과 학과가 생성되었습니다.";
+
+ given(departmentService.createDepartment(admin, request))
+ .willReturn(expectedResponse);
+
+ // when
+ BaseResponse response = departmentController.createDepartment(admin, request);
+
+ // then
+ assertThat(response.getResult()).isEqualTo(expectedResponse);
+ verify(departmentService).createDepartment(admin, request);
+ }
+
+ @Test
+ @DisplayName("학과명 수정 성공")
+ void updateDepartmentName_Success() {
+ // given
+ User admin = createAdminUser();
+ Integer departmentIdx = 1;
+ UpdateDepartmentRequest request = new UpdateDepartmentRequest("수정된학과");
+ String expectedResponse = "수정된학과 학과 이름이 변경되었습니다.";
+
+ given(departmentService.updateDepartmentName(admin, departmentIdx, request))
+ .willReturn(expectedResponse);
+
+ // when
+ BaseResponse response =
+ departmentController.updateDepartmentName(admin, departmentIdx, request);
+
+ // then
+ assertThat(response.getResult()).isEqualTo(expectedResponse);
+ verify(departmentService).updateDepartmentName(admin, departmentIdx, request);
+ }
+
+ @Test
+ @DisplayName("학과 삭제 성공")
+ void deleteDepartment_Success() {
+ // given
+ User admin = createAdminUser();
+ Integer departmentIdx = 1;
+ String expectedResponse = "컴퓨터공학과 학과가 삭제되었습니다.";
+
+ given(departmentService.deleteDepartment(admin, departmentIdx))
+ .willReturn(expectedResponse);
+
+ // when
+ BaseResponse response =
+ departmentController.deleteDepartment(admin, departmentIdx);
+
+ // then
+ assertThat(response.getResult()).isEqualTo(expectedResponse);
+ verify(departmentService).deleteDepartment(admin, departmentIdx);
+ }
+
+ private User createAdminUser() {
+ return User.builder()
+ .id(1)
+ .email("admin@test.com")
+ .name("관리자")
+ .role(Role.ADMIN)
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/inha/git/department/api/service/DepartmentServiceTest.java b/src/test/java/inha/git/department/api/service/DepartmentServiceTest.java
new file mode 100644
index 00000000..bc76370d
--- /dev/null
+++ b/src/test/java/inha/git/department/api/service/DepartmentServiceTest.java
@@ -0,0 +1,241 @@
+package inha.git.department.api.service;
+
+import inha.git.admin.api.controller.dto.response.SearchDepartmentResponse;
+import inha.git.college.domain.College;
+import inha.git.college.domain.repository.CollegeJpaRepository;
+import inha.git.common.exceptions.BaseException;
+import inha.git.department.api.controller.dto.request.CreateDepartmentRequest;
+import inha.git.department.api.controller.dto.request.UpdateDepartmentRequest;
+import inha.git.department.api.mapper.DepartmentMapper;
+import inha.git.department.domain.Department;
+import inha.git.department.domain.repository.DepartmentJpaRepository;
+import inha.git.statistics.domain.repository.TotalDepartmentStatisticsJpaRepository;
+import inha.git.user.domain.User;
+import inha.git.user.domain.enums.Role;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+import static inha.git.common.BaseEntity.State.ACTIVE;
+import static inha.git.common.BaseEntity.State.INACTIVE;
+import static inha.git.common.code.status.ErrorStatus.COLLEGE_NOT_FOUND;
+import static inha.git.common.code.status.ErrorStatus.DEPARTMENT_NOT_FOUND;
+import static org.assertj.core.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.verify;
+
+@DisplayName("학과 서비스 테스트")
+@ExtendWith(MockitoExtension.class)
+class DepartmentServiceTest {
+
+ @InjectMocks
+ private DepartmentServiceImpl departmentService;
+
+ @Mock
+ private DepartmentJpaRepository departmentJpaRepository;
+
+ @Mock
+ private DepartmentMapper departmentMapper;
+
+ @Mock
+ private TotalDepartmentStatisticsJpaRepository totalDepartmentStatisticsJpaRepository;
+
+ @Mock
+ private CollegeJpaRepository collegeJpaRepository;
+
+ @Test
+ @DisplayName("학과 전체 조회 성공")
+ void getDepartments_Success() {
+ // given
+ List departments = Arrays.asList(
+ createDepartment(1, "컴퓨터공학과"),
+ createDepartment(2, "정보통신공학과")
+ );
+ List expectedResponses = Arrays.asList(
+ new SearchDepartmentResponse(1, "컴퓨터공학과"),
+ new SearchDepartmentResponse(2, "정보통신공학과")
+ );
+
+ given(departmentJpaRepository.findAllByState(ACTIVE))
+ .willReturn(departments);
+ given(departmentMapper.departmentsToSearchDepartmentResponses(departments))
+ .willReturn(expectedResponses);
+
+ // when
+ List result = departmentService.getDepartments(null);
+
+ // then
+ assertThat(result).isEqualTo(expectedResponses);
+ verify(departmentJpaRepository).findAllByState(ACTIVE);
+ }
+
+ @Test
+ @DisplayName("특정 단과대학의 학과 조회 성공")
+ void getDepartments_WithCollegeId_Success() {
+ // given
+ Integer collegeIdx = 1;
+ College college = createCollege(collegeIdx, "공과대학");
+ List departments = Arrays.asList(
+ createDepartment(1, "컴퓨터공학과"),
+ createDepartment(2, "정보통신공학과")
+ );
+ List expectedResponses = Arrays.asList(
+ new SearchDepartmentResponse(1, "컴퓨터공학과"),
+ new SearchDepartmentResponse(2, "정보통신공학과")
+ );
+
+ given(collegeJpaRepository.findByIdAndState(collegeIdx, ACTIVE))
+ .willReturn(Optional.of(college));
+ given(departmentJpaRepository.findAllByCollegeAndState(college, ACTIVE))
+ .willReturn(departments);
+ given(departmentMapper.departmentsToSearchDepartmentResponses(departments))
+ .willReturn(expectedResponses);
+
+ // when
+ List result = departmentService.getDepartments(collegeIdx);
+
+ // then
+ assertThat(result).isEqualTo(expectedResponses);
+ verify(collegeJpaRepository).findByIdAndState(collegeIdx, ACTIVE);
+ verify(departmentJpaRepository).findAllByCollegeAndState(college, ACTIVE);
+ }
+
+ @Test
+ @DisplayName("단과대학이 존재하지 않을 때 예외 발생")
+ void getDepartments_CollegeNotFound_ThrowsException() {
+ // given
+ Integer collegeIdx = 999;
+ given(collegeJpaRepository.findByIdAndState(collegeIdx, ACTIVE))
+ .willReturn(Optional.empty());
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class, () ->
+ departmentService.getDepartments(collegeIdx));
+ assertThat(exception.getErrorReason().getMessage())
+ .isEqualTo(COLLEGE_NOT_FOUND.getMessage());
+ }
+
+ @Test
+ @DisplayName("학과 생성 성공")
+ void createDepartment_Success() {
+ // given
+ User admin = createAdminUser();
+ CreateDepartmentRequest request = new CreateDepartmentRequest(1,"신설학과");
+ College college = createCollege(1, "공과대학");
+ Department department = Department.builder()
+ .id(1)
+ .name("신설학과")
+ .college(college)
+ .build();
+
+ given(collegeJpaRepository.findByIdAndState(request.collegeIdx(), ACTIVE))
+ .willReturn(Optional.of(college));
+ given(departmentMapper.createDepartmentRequestToDepartment(request, college))
+ .willReturn(department);
+ given(departmentJpaRepository.save(any(Department.class)))
+ .willReturn(department);
+
+ // when
+ String result = departmentService.createDepartment(admin, request);
+
+ // then
+ assertThat(result).isEqualTo("신설학과 학과가 생성되었습니다.");
+ verify(departmentJpaRepository).save(any(Department.class));
+ verify(totalDepartmentStatisticsJpaRepository).save(any());
+ }
+
+ @Test
+ @DisplayName("학과명 수정 성공")
+ void updateDepartmentName_Success() {
+ // given
+ User admin = createAdminUser();
+ Integer departmentIdx = 1;
+ UpdateDepartmentRequest request = new UpdateDepartmentRequest("수정된학과");
+ Department department = createDepartment(departmentIdx, "기존학과");
+
+ given(departmentJpaRepository.findByIdAndState(departmentIdx, ACTIVE))
+ .willReturn(Optional.of(department));
+
+ // when
+ String result = departmentService.updateDepartmentName(admin, departmentIdx, request);
+
+ // then
+ assertThat(result).isEqualTo("수정된학과 학과 이름이 변경되었습니다.");
+ assertThat(department.getName()).isEqualTo("수정된학과");
+ }
+
+ @Test
+ @DisplayName("존재하지 않는 학과 수정 시 예외 발생")
+ void updateDepartmentName_DepartmentNotFound_ThrowsException() {
+ // given
+ User admin = createAdminUser();
+ Integer departmentIdx = 999;
+ UpdateDepartmentRequest request = new UpdateDepartmentRequest("수정된학과");
+
+ given(departmentJpaRepository.findByIdAndState(departmentIdx, ACTIVE))
+ .willReturn(Optional.empty());
+
+ // when & then
+ assertThatThrownBy(() ->
+ departmentService.updateDepartmentName(admin, departmentIdx, request))
+ .isInstanceOf(BaseException.class)
+ .extracting("errorReason.message")
+ .isEqualTo(DEPARTMENT_NOT_FOUND.getMessage());
+ }
+
+
+
+ @Test
+ @DisplayName("학과 삭제 성공")
+ void deleteDepartment_Success() {
+ // given
+ User admin = createAdminUser();
+ Integer departmentIdx = 1;
+ Department department = createDepartment(departmentIdx, "삭제할학과");
+
+ given(departmentJpaRepository.findByIdAndState(departmentIdx, ACTIVE))
+ .willReturn(Optional.of(department));
+
+ // when
+ String result = departmentService.deleteDepartment(admin, departmentIdx);
+
+ // then
+ assertThat(result).isEqualTo("삭제할학과 학과가 삭제되었습니다.");
+ assertThat(department.getState()).isEqualTo(INACTIVE);
+ assertThat(department.getDeletedAt()).isNotNull();
+ }
+
+ private College createCollege(Integer id, String name) {
+ return College.builder()
+ .id(id)
+ .name(name)
+ .build();
+ }
+
+ private Department createDepartment(Integer id, String name) {
+ College college = createCollege(id, "테스트단과대학");
+ return Department.builder()
+ .id(id)
+ .name(name)
+ .college(college)
+ .build();
+ }
+
+ private User createAdminUser() {
+ return User.builder()
+ .id(1)
+ .email("admin@test.com")
+ .name("관리자")
+ .role(Role.ADMIN)
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/inha/git/field/api/controller/FieldControllerTest.java b/src/test/java/inha/git/field/api/controller/FieldControllerTest.java
new file mode 100644
index 00000000..af51acdd
--- /dev/null
+++ b/src/test/java/inha/git/field/api/controller/FieldControllerTest.java
@@ -0,0 +1,120 @@
+package inha.git.field.api.controller;
+
+import inha.git.common.BaseResponse;
+import inha.git.field.api.controller.dto.request.CreateFieldRequest;
+import inha.git.field.api.controller.dto.request.UpdateFieldRequest;
+import inha.git.field.api.controller.dto.response.SearchFieldResponse;
+import inha.git.field.api.service.FieldService;
+import inha.git.user.domain.User;
+import inha.git.user.domain.enums.Role;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.verify;
+
+@DisplayName("분야 컨트롤러 테스트")
+@ExtendWith(MockitoExtension.class)
+class FieldControllerTest {
+
+ @InjectMocks
+ private FieldController fieldController;
+
+ @Mock
+ private FieldService fieldService;
+
+ @Test
+ @DisplayName("분야 전체 조회 성공")
+ void getFields_Success() {
+ // given
+ List expectedResponses = Arrays.asList(
+ new SearchFieldResponse(1, "웹"),
+ new SearchFieldResponse(2, "앱")
+ );
+
+ given(fieldService.getFields())
+ .willReturn(expectedResponses);
+
+ // when
+ BaseResponse> response = fieldController.getFields();
+
+ // then
+ assertThat(response.getResult()).isEqualTo(expectedResponses);
+ verify(fieldService).getFields();
+ }
+
+ @Test
+ @DisplayName("분야 생성 성공")
+ void createField_Success() {
+ // given
+ User admin = createAdminUser();
+ CreateFieldRequest request = new CreateFieldRequest("신규분야");
+ String expectedResponse = "신규분야 분야가 생성되었습니다.";
+
+ given(fieldService.createField(admin, request))
+ .willReturn(expectedResponse);
+
+ // when
+ BaseResponse response = fieldController.createField(admin, request);
+
+ // then
+ assertThat(response.getResult()).isEqualTo(expectedResponse);
+ verify(fieldService).createField(admin, request);
+ }
+
+ @Test
+ @DisplayName("분야명 수정 성공")
+ void updateField_Success() {
+ // given
+ User admin = createAdminUser();
+ Integer fieldIdx = 1;
+ UpdateFieldRequest request = new UpdateFieldRequest("수정된분야");
+ String expectedResponse = "수정된분야 분야가 수정되었습니다.";
+
+ given(fieldService.updateField(admin, fieldIdx, request))
+ .willReturn(expectedResponse);
+
+ // when
+ BaseResponse response = fieldController.updateField(admin, fieldIdx, request);
+
+ // then
+ assertThat(response.getResult()).isEqualTo(expectedResponse);
+ verify(fieldService).updateField(admin, fieldIdx, request);
+ }
+
+ @Test
+ @DisplayName("분야 삭제 성공")
+ void deleteField_Success() {
+ // given
+ User admin = createAdminUser();
+ Integer fieldIdx = 1;
+ String expectedResponse = "백엔드 분야가 삭제되었습니다.";
+
+ given(fieldService.deleteField(admin, fieldIdx))
+ .willReturn(expectedResponse);
+
+ // when
+ BaseResponse response = fieldController.deleteField(admin, fieldIdx);
+
+ // then
+ assertThat(response.getResult()).isEqualTo(expectedResponse);
+ verify(fieldService).deleteField(admin, fieldIdx);
+ }
+
+ private User createAdminUser() {
+ return User.builder()
+ .id(1)
+ .email("admin@test.com")
+ .name("관리자")
+ .role(Role.ADMIN)
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/inha/git/field/api/service/FieldServiceTest.java b/src/test/java/inha/git/field/api/service/FieldServiceTest.java
new file mode 100644
index 00000000..cc565d0b
--- /dev/null
+++ b/src/test/java/inha/git/field/api/service/FieldServiceTest.java
@@ -0,0 +1,184 @@
+package inha.git.field.api.service;
+
+import inha.git.common.exceptions.BaseException;
+import inha.git.field.api.controller.dto.request.CreateFieldRequest;
+import inha.git.field.api.controller.dto.request.UpdateFieldRequest;
+import inha.git.field.api.controller.dto.response.SearchFieldResponse;
+import inha.git.field.api.mapper.FieldMapper;
+import inha.git.field.domain.Field;
+import inha.git.field.domain.repository.FieldJpaRepository;
+import inha.git.user.domain.User;
+import inha.git.user.domain.enums.Role;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+import static inha.git.common.BaseEntity.State.ACTIVE;
+import static inha.git.common.BaseEntity.State.INACTIVE;
+import static inha.git.common.code.status.ErrorStatus.FIELD_NOT_FOUND;
+import static org.assertj.core.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.verify;
+
+@DisplayName("분야 서비스 테스트")
+@ExtendWith(MockitoExtension.class)
+class FieldServiceTest {
+
+ @InjectMocks
+ private FieldServiceImpl fieldService;
+
+ @Mock
+ private FieldJpaRepository fieldJpaRepository;
+
+ @Mock
+ private FieldMapper fieldMapper;
+
+ @Test
+ @DisplayName("분야 전체 조회 성공")
+ void getFields_Success() {
+ // given
+ List fields = Arrays.asList(
+ createField(1, "웹"),
+ createField(2, "앱")
+ );
+ List expectedResponses = Arrays.asList(
+ new SearchFieldResponse(1, "웹"),
+ new SearchFieldResponse(2, "앱")
+ );
+
+ given(fieldJpaRepository.findAllByState(ACTIVE))
+ .willReturn(fields);
+ given(fieldMapper.fieldsToSearchFieldResponses(fields))
+ .willReturn(expectedResponses);
+
+ // when
+ List result = fieldService.getFields();
+
+ // then
+ assertThat(result).isEqualTo(expectedResponses);
+ verify(fieldJpaRepository).findAllByState(ACTIVE);
+ }
+
+ @Test
+ @DisplayName("분야 생성 성공")
+ void createField_Success() {
+ // given
+ User admin = createAdminUser();
+ CreateFieldRequest request = new CreateFieldRequest("신규분야");
+ Field field = createField(1, "신규분야");
+
+ given(fieldMapper.createFieldRequestToField(request))
+ .willReturn(field);
+ given(fieldJpaRepository.save(any(Field.class)))
+ .willReturn(field);
+
+ // when
+ String result = fieldService.createField(admin, request);
+
+ // then
+ assertThat(result).isEqualTo("신규분야 분야가 생성되었습니다.");
+ verify(fieldJpaRepository).save(any(Field.class));
+ }
+
+ @Test
+ @DisplayName("분야명 수정 성공")
+ void updateField_Success() {
+ // given
+ User admin = createAdminUser();
+ Integer fieldIdx = 1;
+ UpdateFieldRequest request = new UpdateFieldRequest("수정된분야");
+ Field field = createField(fieldIdx, "기존분야");
+
+ given(fieldJpaRepository.findByIdAndState(fieldIdx, ACTIVE))
+ .willReturn(Optional.of(field));
+
+ // when
+ String result = fieldService.updateField(admin, fieldIdx, request);
+
+ // then
+ assertThat(result).isEqualTo("수정된분야 분야가 수정되었습니다.");
+ assertThat(field.getName()).isEqualTo("수정된분야");
+ }
+
+ @Test
+ @DisplayName("존재하지 않는 분야 수정 시 예외 발생")
+ void updateField_NotFound_ThrowsException() {
+ // given
+ User admin = createAdminUser();
+ Integer fieldIdx = 999;
+ UpdateFieldRequest request = new UpdateFieldRequest("수정된분야");
+
+ given(fieldJpaRepository.findByIdAndState(fieldIdx, ACTIVE))
+ .willReturn(Optional.empty());
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class, () ->
+ fieldService.updateField(admin, fieldIdx, request));
+
+ assertThat(exception.getErrorReason().getMessage())
+ .isEqualTo(FIELD_NOT_FOUND.getMessage());
+ }
+
+ @Test
+ @DisplayName("분야 삭제 성공")
+ void deleteField_Success() {
+ // given
+ User admin = createAdminUser();
+ Integer fieldIdx = 1;
+ Field field = createField(fieldIdx, "삭제할분야");
+
+ given(fieldJpaRepository.findByIdAndState(fieldIdx, ACTIVE))
+ .willReturn(Optional.of(field));
+
+ // when
+ String result = fieldService.deleteField(admin, fieldIdx);
+
+ // then
+ assertThat(result).isEqualTo("삭제할분야 분야가 삭제되었습니다.");
+ assertThat(field.getState()).isEqualTo(INACTIVE);
+ assertThat(field.getDeletedAt()).isNotNull();
+ }
+
+ @Test
+ @DisplayName("존재하지 않는 분야 삭제 시 예외 발생")
+ void deleteField_NotFound_ThrowsException() {
+ // given
+ User admin = createAdminUser();
+ Integer fieldIdx = 999;
+
+ given(fieldJpaRepository.findByIdAndState(fieldIdx, ACTIVE))
+ .willReturn(Optional.empty());
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class, () ->
+ fieldService.deleteField(admin, fieldIdx));
+
+ assertThat(exception.getErrorReason().getMessage())
+ .isEqualTo(FIELD_NOT_FOUND.getMessage());
+ }
+
+ private Field createField(Integer id, String name) {
+ return Field.builder()
+ .id(id)
+ .name(name)
+ .build();
+ }
+
+ private User createAdminUser() {
+ return User.builder()
+ .id(1)
+ .email("admin@test.com")
+ .name("관리자")
+ .role(Role.ADMIN)
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/inha/git/notice/api/controller/NoticeControllerTest.java b/src/test/java/inha/git/notice/api/controller/NoticeControllerTest.java
new file mode 100644
index 00000000..7be63e4c
--- /dev/null
+++ b/src/test/java/inha/git/notice/api/controller/NoticeControllerTest.java
@@ -0,0 +1,195 @@
+package inha.git.notice.api.controller;
+
+import inha.git.common.BaseResponse;
+import inha.git.common.exceptions.BaseException;
+import inha.git.notice.api.controller.dto.request.CreateNoticeRequest;
+import inha.git.notice.api.controller.dto.request.UpdateNoticeRequest;
+import inha.git.notice.api.controller.dto.response.SearchNoticeAttachmentResponse;
+import inha.git.notice.api.controller.dto.response.SearchNoticeResponse;
+import inha.git.notice.api.controller.dto.response.SearchNoticeUserResponse;
+import inha.git.notice.api.controller.dto.response.SearchNoticesResponse;
+import inha.git.notice.api.service.NoticeService;
+import inha.git.user.domain.User;
+import inha.git.user.domain.enums.Role;
+import inha.git.utils.PagingUtils;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import static inha.git.common.code.status.ErrorStatus.INVALID_PAGE;
+import static org.assertj.core.api.Assertions.*;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.verify;
+
+@DisplayName("공지사항 컨트롤러 테스트")
+@ExtendWith(MockitoExtension.class)
+class NoticeControllerTest {
+
+ @InjectMocks
+ private NoticeController noticeController;
+
+ @Mock
+ private NoticeService noticeService;
+
+ @Mock
+ private PagingUtils pagingUtils;
+
+ @Test
+ @DisplayName("공지사항 페이징 조회 성공")
+ void getNotices_Success() {
+ // given
+ Integer page = 1;
+ Integer size = 10;
+ int pageIndex = 0;
+ int pageSize = 9;
+ Page expectedPage = new PageImpl<>(Arrays.asList(
+ new SearchNoticesResponse(1, "공지1", LocalDateTime.now(), false, new SearchNoticeUserResponse(1, "작성자1")),
+ new SearchNoticesResponse(2, "공지2", LocalDateTime.now(), false, new SearchNoticeUserResponse(2, "작성자2"))
+ ));
+
+ given(pagingUtils.toPageIndex(page)).willReturn(pageIndex);
+ given(pagingUtils.toPageSize(size)).willReturn(pageSize);
+ given(noticeService.getNotices(pageIndex, pageSize)).willReturn(expectedPage);
+
+ // when
+ BaseResponse> response = noticeController.getNotices(page, size);
+
+ // then
+ assertThat(response.getResult()).isEqualTo(expectedPage);
+ verify(pagingUtils).validatePage(page);
+ verify(pagingUtils).validateSize(size);
+ verify(noticeService).getNotices(pageIndex, pageSize);
+ }
+
+ @Test
+ @DisplayName("잘못된 페이지 번호로 조회 시 예외 발생")
+ void getNotices_WithInvalidPage_ThrowsException() {
+ // given
+ Integer invalidPage = 0;
+ Integer size = 10;
+
+ doThrow(new BaseException(INVALID_PAGE))
+ .when(pagingUtils).validatePage(invalidPage);
+
+ // when & then
+ assertThatThrownBy(() -> noticeController.getNotices(invalidPage, size))
+ .isInstanceOf(BaseException.class)
+ .hasMessage(INVALID_PAGE.getMessage());
+ }
+
+ @Test
+ @DisplayName("공지사항 상세 조회 성공")
+ void getNotice_Success() {
+ // given
+ Integer noticeIdx = 1;
+ SearchNoticeResponse expectedResponse = createSearchNoticeResponse(noticeIdx);
+
+ given(noticeService.getNotice(noticeIdx))
+ .willReturn(expectedResponse);
+
+ // when
+ BaseResponse response = noticeController.getNotice(noticeIdx);
+
+ // then
+ assertThat(response.getResult()).isEqualTo(expectedResponse);
+ verify(noticeService).getNotice(noticeIdx);
+ }
+
+ @Test
+ @DisplayName("공지사항 생성 성공")
+ void createNotice_Success() {
+ // given
+ User user = createUser(1, "작성자", Role.ASSISTANT);
+ CreateNoticeRequest request = new CreateNoticeRequest("제목", "내용");
+ List attachments = new ArrayList<>();
+ String expectedResponse = "제목 공지가 생성되었습니다.";
+
+ given(noticeService.createNotice(user, request, attachments))
+ .willReturn(expectedResponse);
+
+ // when
+ BaseResponse response = noticeController.createNotice(user, request, attachments);
+
+ // then
+ assertThat(response.getResult()).isEqualTo(expectedResponse);
+ verify(noticeService).createNotice(user, request, attachments);
+ }
+
+ @Test
+ @DisplayName("공지사항 수정 성공")
+ void updateNotice_Success() {
+ // given
+ User user = createUser(1, "작성자", Role.ASSISTANT);
+ Integer noticeIdx = 1;
+ UpdateNoticeRequest request = new UpdateNoticeRequest("수정된제목", "수정된내용");
+ List attachments = new ArrayList<>();
+ String expectedResponse = "수정된제목 공지가 수정되었습니다.";
+
+ given(noticeService.updateNotice(user, noticeIdx, request, attachments))
+ .willReturn(expectedResponse);
+
+ // when
+ BaseResponse response = noticeController.updateNotice(user, noticeIdx, request, attachments);
+
+ // then
+ assertThat(response.getResult()).isEqualTo(expectedResponse);
+ verify(noticeService).updateNotice(user, noticeIdx, request, attachments);
+ }
+
+ @Test
+ @DisplayName("공지사항 삭제 성공")
+ void deleteNotice_Success() {
+ // given
+ User user = createUser(1, "작성자", Role.ASSISTANT);
+ Integer noticeIdx = 1;
+ String expectedResponse = "공지가 삭제되었습니다.";
+
+ given(noticeService.deleteNotice(user, noticeIdx))
+ .willReturn(expectedResponse);
+
+ // when
+ BaseResponse response = noticeController.deleteNotice(user, noticeIdx);
+
+ // then
+ assertThat(response.getResult()).isEqualTo(expectedResponse);
+ verify(noticeService).deleteNotice(user, noticeIdx);
+ }
+
+ private User createUser(Integer id, String name, Role role) {
+ return User.builder()
+ .id(id)
+ .name(name)
+ .role(role)
+ .build();
+ }
+
+ private SearchNoticeResponse createSearchNoticeResponse(Integer id) {
+ SearchNoticeUserResponse userResponse = new SearchNoticeUserResponse(1, "작성자");
+ List attachments = Arrays.asList(
+ new SearchNoticeAttachmentResponse(1, "file1.txt", "/path/to/file1"),
+ new SearchNoticeAttachmentResponse(2, "file2.txt", "/path/to/file2")
+ );
+
+ return new SearchNoticeResponse(
+ id,
+ "테스트 공지",
+ "테스트 내용",
+ true,
+ attachments,
+ LocalDateTime.now(),
+ userResponse
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/inha/git/notice/api/service/NoticeServiceTest.java b/src/test/java/inha/git/notice/api/service/NoticeServiceTest.java
new file mode 100644
index 00000000..5a2d07f8
--- /dev/null
+++ b/src/test/java/inha/git/notice/api/service/NoticeServiceTest.java
@@ -0,0 +1,306 @@
+package inha.git.notice.api.service;
+
+import inha.git.common.exceptions.BaseException;
+import inha.git.notice.api.controller.dto.request.CreateNoticeRequest;
+import inha.git.notice.api.controller.dto.request.UpdateNoticeRequest;
+import inha.git.notice.api.controller.dto.response.SearchNoticeAttachmentResponse;
+import inha.git.notice.api.controller.dto.response.SearchNoticeResponse;
+import inha.git.notice.api.controller.dto.response.SearchNoticeUserResponse;
+import inha.git.notice.api.controller.dto.response.SearchNoticesResponse;
+import inha.git.notice.api.mapper.NoticeMapper;
+import inha.git.notice.domain.Notice;
+import inha.git.notice.domain.NoticeAttachment;
+import inha.git.notice.domain.repository.NoticeAttachmentJpaRepository;
+import inha.git.notice.domain.repository.NoticeJpaRepository;
+import inha.git.notice.domain.repository.NoticeQueryRepository;
+import inha.git.user.domain.User;
+import inha.git.user.domain.enums.Role;
+import inha.git.user.domain.repository.UserJpaRepository;
+import inha.git.utils.IdempotentProvider;
+import inha.git.utils.file.FilePath;
+import jakarta.transaction.Transactional;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockedStatic;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.data.domain.*;
+import org.springframework.http.MediaType;
+import org.springframework.mock.web.MockMultipartFile;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+import static inha.git.common.BaseEntity.State.ACTIVE;
+import static inha.git.common.BaseEntity.State.INACTIVE;
+import static inha.git.common.Constant.CREATE_AT;
+import static inha.git.common.code.status.ErrorStatus.NOTICE_NOT_AUTHORIZED;
+import static inha.git.common.code.status.ErrorStatus.NOTICE_NOT_FOUND;
+import static org.assertj.core.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.*;
+
+@DisplayName("공지사항 서비스 테스트")
+@ExtendWith(MockitoExtension.class)
+class NoticeServiceTest {
+
+ @InjectMocks
+ private NoticeServiceImpl noticeService;
+
+ @Mock
+ private NoticeJpaRepository noticeJpaRepository;
+
+ @Mock
+ private NoticeAttachmentJpaRepository noticeAttachmentRepository;
+
+ @Mock
+ private NoticeMapper noticeMapper;
+
+ @Mock
+ private NoticeQueryRepository noticeQueryRepository;
+
+ @Mock
+ private UserJpaRepository userJpaRepository;
+
+ @Mock
+ private IdempotentProvider idempotentProvider;
+
+ @Mock
+ private FilePath filePath; // FilePath Mock 추가
+
+ @Test
+ @DisplayName("공지사항 페이징 조회 성공")
+ void getNotices_Success() {
+ // given
+ int page = 0;
+ int size = 10;
+ Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, CREATE_AT));
+ Page expectedPage = new PageImpl<>(Arrays.asList(
+ new SearchNoticesResponse(1, "공지1", LocalDateTime.now(), false, new SearchNoticeUserResponse(1, "작성자1")),
+ new SearchNoticesResponse(2, "공지2", LocalDateTime.now(), false, new SearchNoticeUserResponse(2, "작성자2"))
+ ));
+
+ given(noticeQueryRepository.getNotices(pageable))
+ .willReturn(expectedPage);
+
+ // when
+ Page result = noticeService.getNotices(page, size);
+
+ // then
+ assertThat(result).isEqualTo(expectedPage);
+ verify(noticeQueryRepository).getNotices(pageable);
+ }
+
+ //@Test
+ @DisplayName("공지사항 상세 조회 성공")
+ void getNotice_Success() {
+ // given
+ Integer noticeIdx = 1;
+ Notice notice = createNotice(noticeIdx, "테스트 공지");
+ User user = createUser(1, "작성자", Role.ASSISTANT);
+ notice.setUser(user);
+
+ ArrayList attachments = new ArrayList<>();
+ attachments.add(createNoticeAttachment(1, "file1.txt", "/path/to/file1", notice));
+ attachments.add(createNoticeAttachment(2, "file2.txt", "/path/to/file2", notice));
+ notice.setNoticeAttachments(attachments);
+
+ SearchNoticeUserResponse userResponse = new SearchNoticeUserResponse(user.getId(), user.getName());
+ List attachmentResponses = Arrays.asList(
+ new SearchNoticeAttachmentResponse(1, "file1.txt", "/path/to/file1"),
+ new SearchNoticeAttachmentResponse(2, "file2.txt", "/path/to/file2")
+ );
+
+ SearchNoticeResponse expectedResponse = new SearchNoticeResponse(
+ notice.getId(),
+ notice.getTitle(),
+ notice.getContents(),
+ true,
+ attachmentResponses,
+ notice.getCreatedAt(),
+ userResponse
+ );
+
+ given(noticeJpaRepository.findByIdAndState(noticeIdx, ACTIVE))
+ .willReturn(Optional.of(notice));
+ given(userJpaRepository.findById(user.getId()))
+ .willReturn(Optional.of(user));
+ given(noticeMapper.noticeToSearchNoticeResponse(eq(notice), any(), anyList()))
+ .willReturn(expectedResponse);
+
+ // when
+ SearchNoticeResponse result = noticeService.getNotice(noticeIdx);
+
+ // then
+ assertThat(result).isEqualTo(expectedResponse);
+ }
+
+
+ @Test
+ @DisplayName("존재하지 않는 공지사항 조회 시 예외 발생")
+ void getNotice_NotFound_ThrowsException() {
+ // given
+ Integer noticeIdx = 999;
+ given(noticeJpaRepository.findByIdAndState(noticeIdx, ACTIVE))
+ .willReturn(Optional.empty());
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class,
+ () -> noticeService.getNotice(noticeIdx));
+ assertThat(exception.getErrorReason().getMessage())
+ .isEqualTo(NOTICE_NOT_FOUND.getMessage());
+ }
+
+
+ //@Test
+ @DisplayName("첨부파일이 있는 공지사항 생성 성공")
+ void createNotice_WithAttachments_Success() {
+ // given
+ User user = createUser(1, "작성자", Role.ASSISTANT);
+ CreateNoticeRequest request = new CreateNoticeRequest("제목", "내용");
+ Notice notice = createNotice(1, "제목");
+ notice.setUser(user);
+
+ List attachments = Arrays.asList(
+ new MockMultipartFile("file1", "file1.txt", "text/plain", "test content".getBytes()),
+ new MockMultipartFile("file2", "file2.txt", "text/plain", "test content".getBytes())
+ );
+
+ given(noticeMapper.createNoticeRequestToNotice(user, request))
+ .willReturn(notice);
+ given(noticeJpaRepository.save(any(Notice.class)))
+ .willReturn(notice);
+ doNothing().when(idempotentProvider)
+ .isValidIdempotent(anyList());
+
+ // Mock 파일 저장 로직
+ try (MockedStatic filePathMock = mockStatic(FilePath.class)) {
+ filePathMock.when(() -> FilePath.storeFile(any(MultipartFile.class), any()))
+ .thenReturn("/mocked/path/file.txt");
+
+ // when
+ String result = noticeService.createNotice(user, request, attachments);
+
+ // then
+ assertThat(result).isEqualTo("제목 공지가 생성되었습니다.");
+ assertThat(notice.getHasAttachment()).isTrue();
+ verify(noticeJpaRepository).save(notice);
+ verify(noticeAttachmentRepository, times(2)).save(any(NoticeAttachment.class));
+ }
+ }
+
+ @Test
+ @DisplayName("권한이 없는 사용자의 공지사항 수정 시도 시 예외 발생")
+ void updateNotice_WithoutAuthorization_ThrowsException() {
+ // given
+ User unauthorized = createUser(2, "무권한사용자", Role.USER);
+ Integer noticeIdx = 1;
+ Notice notice = createNotice(1, "제목");
+ User originalAuthor = createUser(1, "원작성자", Role.ASSISTANT);
+ notice.setUser(originalAuthor);
+ UpdateNoticeRequest request = new UpdateNoticeRequest("수정된제목", "수정된내용");
+
+ given(noticeJpaRepository.findByIdAndState(noticeIdx, ACTIVE))
+ .willReturn(Optional.of(notice));
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class,
+ () -> noticeService.updateNotice(unauthorized, noticeIdx, request, null));
+ assertThat(exception.getErrorReason().getMessage())
+ .isEqualTo(NOTICE_NOT_AUTHORIZED.getMessage());
+ }
+
+ @Test
+ @DisplayName("관리자의 타인 공지사항 수정 성공")
+ void updateNotice_ByAdmin_Success() {
+ // given
+ User admin = createUser(2, "관리자", Role.ADMIN);
+ Integer noticeIdx = 1;
+ Notice notice = createNotice(1, "제목");
+ notice.setUser(createUser(1, "원작성자", Role.ASSISTANT));
+ UpdateNoticeRequest request = new UpdateNoticeRequest("수정된제목", "수정된내용");
+
+ given(noticeJpaRepository.findByIdAndState(noticeIdx, ACTIVE))
+ .willReturn(Optional.of(notice));
+ given(noticeJpaRepository.save(any(Notice.class)))
+ .willReturn(notice);
+
+ // when
+ String result = noticeService.updateNotice(admin, noticeIdx, request, null);
+
+ // then
+ assertThat(result).isEqualTo("수정된제목 공지가 수정되었습니다.");
+ assertThat(notice.getTitle()).isEqualTo("수정된제목");
+ assertThat(notice.getContents()).isEqualTo("수정된내용");
+ }
+
+ @Test
+ @DisplayName("공지사항 삭제 성공")
+ void deleteNotice_Success() {
+ // given
+ User user = createUser(1, "작성자", Role.ASSISTANT);
+ Integer noticeIdx = 1;
+ Notice notice = createNotice(noticeIdx, "삭제될공지");
+ notice.setUser(user);
+
+ given(noticeJpaRepository.findByIdAndState(noticeIdx, ACTIVE))
+ .willReturn(Optional.of(notice));
+ given(noticeJpaRepository.save(any(Notice.class)))
+ .willReturn(notice);
+
+ // when
+ String result = noticeService.deleteNotice(user, noticeIdx);
+
+ // then
+ assertThat(result).isEqualTo("삭제될공지 공지가 삭제되었습니다.");
+ assertThat(notice.getState()).isEqualTo(INACTIVE);
+ assertThat(notice.getDeletedAt()).isNotNull();
+ }
+
+ private Notice createNotice(Integer id, String title) {
+ Notice notice = Notice.builder()
+ .id(id)
+ .title(title)
+ .contents("테스트 내용")
+ .hasAttachment(false)
+ .build();
+ notice.setNoticeAttachments(new ArrayList<>()); // 빈 ArrayList로 초기화
+ return notice;
+ }
+
+ private NoticeAttachment createNoticeAttachment(Integer id, String originalFileName,
+ String storedFileUrl, Notice notice) {
+ return NoticeAttachment.builder()
+ .id(id)
+ .originalFileName(originalFileName)
+ .storedFileUrl(storedFileUrl)
+ .notice(notice)
+ .build();
+ }
+
+ private User createUser(Integer id, String name, Role role) {
+ return User.builder()
+ .id(id)
+ .name(name)
+ .role(role)
+ .build();
+ }
+
+ private MultipartFile createMockMultipartFile(String filename) {
+ return new MockMultipartFile(
+ "file",
+ filename,
+ MediaType.TEXT_PLAIN_VALUE,
+ "test file content".getBytes()
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/inha/git/question/api/controller/QuestionCommentControllerTest.java b/src/test/java/inha/git/question/api/controller/QuestionCommentControllerTest.java
new file mode 100644
index 00000000..b50eec0a
--- /dev/null
+++ b/src/test/java/inha/git/question/api/controller/QuestionCommentControllerTest.java
@@ -0,0 +1,410 @@
+package inha.git.question.api.controller;
+
+import inha.git.common.BaseResponse;
+import inha.git.common.exceptions.BaseException;
+import inha.git.question.api.controller.dto.request.*;
+import inha.git.question.api.controller.dto.response.CommentResponse;
+import inha.git.question.api.controller.dto.response.ReplyCommentResponse;
+import inha.git.question.api.service.QuestionCommentService;
+import inha.git.user.domain.User;
+import inha.git.user.domain.enums.Role;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+import static inha.git.common.code.status.ErrorStatus.*;
+import static inha.git.common.code.status.SuccessStatus.*;
+import static org.assertj.core.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.BDDMockito.willThrow;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@DisplayName("질문 댓글 컨트롤러 테스트")
+@ExtendWith(MockitoExtension.class)
+class QuestionCommentControllerTest {
+
+ @InjectMocks
+ private QuestionCommentController questionCommentController;
+
+ @Mock
+ private QuestionCommentService questionCommentService;
+
+
+
+ @Test
+ @DisplayName("특정 질문 댓글 + 대댓글 전체 조회 성공")
+ void getAllComments_Success() {
+ // given
+ User user = createTestUser(1, "테스트유저", Role.USER);
+ Integer questionIdx = 100;
+
+ List fakeComments = List.of(
+ new CommentWithRepliesResponse(
+ 1,
+ "댓글 내용",
+ null,
+ LocalDateTime.now(),
+ 3,
+ true,
+ List.of()
+ )
+ );
+
+ given(questionCommentService.getAllCommentsByQuestionIdx(user, questionIdx))
+ .willReturn(fakeComments);
+
+ // when
+ BaseResponse> response =
+ questionCommentController.getAllComments(user, questionIdx);
+
+ // then
+ assertThat(response.getResult()).isEqualTo(fakeComments);
+ assertThat(response.getMessage()).isEqualTo(QUESTION_COMMENT_SEARCH_OK.getMessage());
+ verify(questionCommentService).getAllCommentsByQuestionIdx(user, questionIdx);
+ }
+
+ @Test
+ @DisplayName("존재하지 않는 질문 조회 시 예외 발생")
+ void getAllComments_QuestionNotFound_ThrowsException() {
+ // given
+ User user = createTestUser(1, "테스트유저", Role.USER);
+ Integer invalidQuestionIdx = 999;
+
+ willThrow(new BaseException(QUESTION_NOT_FOUND))
+ .given(questionCommentService)
+ .getAllCommentsByQuestionIdx(user, invalidQuestionIdx);
+
+ // when & then
+ BaseException ex = assertThrows(BaseException.class,
+ () -> questionCommentController.getAllComments(user, invalidQuestionIdx));
+
+ assertThat(ex.getErrorReason().getMessage()).isEqualTo(QUESTION_NOT_FOUND.getMessage());
+ verify(questionCommentService).getAllCommentsByQuestionIdx(user, invalidQuestionIdx);
+ }
+
+
+ @Test
+ @DisplayName("질문 댓글 생성 성공")
+ void createComment_Success() {
+ // given
+ User user = createTestUser(1, "테스트유저", Role.USER);
+ CreateCommentRequest request = new CreateCommentRequest(1, "댓글 내용");
+ CommentResponse expectedResponse = new CommentResponse(10);
+
+ when(questionCommentService.createComment(any(User.class), any(CreateCommentRequest.class)))
+ .thenReturn(expectedResponse);
+
+ // when
+ BaseResponse response = questionCommentController.createComment(user, request);
+
+ // then
+ assertThat(response.getCode()).isEqualTo(QUESTION_COMMENT_CREATE_OK.getCode());
+ assertThat(response.getResult()).isEqualTo(expectedResponse);
+ verify(questionCommentService).createComment(user, request);
+ }
+
+ @Test
+ @DisplayName("존재하지 않는 질문에 댓글 생성 시 예외 발생")
+ void createComment_QuestionNotFound_ThrowsException() {
+ // given
+ User user = createTestUser(1, "테스트유저", Role.USER);
+ CreateCommentRequest request = new CreateCommentRequest(999, "댓글 내용");
+
+ when(questionCommentService.createComment(any(User.class), any(CreateCommentRequest.class)))
+ .thenThrow(new BaseException(QUESTION_NOT_FOUND));
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class,
+ () -> questionCommentController.createComment(user, request));
+
+ assertThat(exception.getErrorReason().getMessage()).isEqualTo(QUESTION_NOT_FOUND.getMessage());
+ verify(questionCommentService).createComment(user, request);
+ }
+
+ @Test
+ @DisplayName("질문 댓글 수정 성공")
+ void updateComment_Success() {
+ // given
+ User user = createTestUser(1, "테스트유저", Role.USER);
+ Integer commentIdx = 1;
+ UpdateCommentRequest request = new UpdateCommentRequest("수정된 댓글 내용");
+ CommentResponse expectedResponse = new CommentResponse(commentIdx);
+
+ // mocking
+ when(questionCommentService.updateComment(user, commentIdx, request))
+ .thenReturn(expectedResponse);
+
+ // when
+ BaseResponse response = questionCommentController.updateComment(user, commentIdx, request);
+
+ // then
+ assertThat(response.getResult()).isEqualTo(expectedResponse);
+ verify(questionCommentService).updateComment(user, commentIdx, request);
+ }
+
+ @Test
+ @DisplayName("질문 댓글 삭제 성공")
+ void deleteComment_Success() {
+ // given
+ User user = createTestUser(1, "테스트유저", Role.USER);
+ Integer commentIdx = 1;
+ CommentResponse expectedResponse = new CommentResponse(commentIdx);
+
+ // mocking
+ when(questionCommentService.deleteComment(user, commentIdx))
+ .thenReturn(expectedResponse);
+
+ // when
+ BaseResponse response = questionCommentController.deleteComment(user, commentIdx);
+
+ // then
+ assertThat(response.getResult()).isEqualTo(expectedResponse);
+ verify(questionCommentService).deleteComment(user, commentIdx);
+ }
+
+ @Test
+ @DisplayName("질문 댓글 답글 생성 성공")
+ void createReplyComment_Success() {
+ // given
+ User user = createTestUser(1, "테스트유저", Role.USER);
+ Integer commentIdx = 1;
+ CreateReplyCommentRequest request = new CreateReplyCommentRequest(
+ commentIdx,
+ "테스트 답글 내용"
+ );
+ ReplyCommentResponse expectedResponse = new ReplyCommentResponse(1);
+
+ // mocking
+ when(questionCommentService.createReplyComment(user, request))
+ .thenReturn(expectedResponse);
+
+ // when
+ BaseResponse response = questionCommentController.createReplyComment(user, request);
+
+ // then
+ assertThat(response.getResult()).isEqualTo(expectedResponse);
+ verify(questionCommentService).createReplyComment(user, request);
+ }
+
+ @Test
+ @DisplayName("존재하지 않는 댓글에 답글 생성 시 예외 발생")
+ void createReplyComment_CommentNotFound_ThrowsException() {
+ // given
+ User user = createTestUser(1, "테스트유저", Role.USER);
+ Integer invalidCommentIdx = 999;
+ CreateReplyCommentRequest request = new CreateReplyCommentRequest(
+ invalidCommentIdx,
+ "테스트 답글 내용"
+ );
+
+ // mocking
+ when(questionCommentService.createReplyComment(user, request))
+ .thenThrow(new BaseException(QUESTION_COMMENT_NOT_FOUND));
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class,
+ () -> questionCommentController.createReplyComment(user, request));
+
+ assertThat(exception.getErrorReason().getMessage())
+ .isEqualTo(QUESTION_COMMENT_NOT_FOUND.getMessage());
+ }
+
+ @Test
+ @DisplayName("대댓글 수정 성공")
+ void updateReplyComment_Success() {
+ // given
+ Integer replyCommentIdx = 1;
+ UpdateCommentRequest request = new UpdateCommentRequest("수정된 대댓글 내용");
+ ReplyCommentResponse expectedResponse = new ReplyCommentResponse(replyCommentIdx);
+
+ // Mocking Service
+ when(questionCommentService.updateReplyComment(any(User.class), eq(replyCommentIdx), eq(request)))
+ .thenReturn(expectedResponse);
+
+ User testUser = createTestUser(1, "테스트 사용자", Role.USER);
+
+ // when
+ BaseResponse response = questionCommentController.updateReplyComment(testUser, replyCommentIdx, request);
+
+ // then
+ assertThat(response.getResult()).isEqualTo(expectedResponse);
+
+ // Verify
+ verify(questionCommentService).updateReplyComment(any(User.class), eq(replyCommentIdx), eq(request));
+ }
+
+ @Test
+ @DisplayName("대댓글 삭제 성공")
+ void deleteReplyComment_Success() {
+ // given
+ Integer replyCommentIdx = 1;
+ ReplyCommentResponse expectedResponse = new ReplyCommentResponse(replyCommentIdx);
+
+ // Mocking Service
+ when(questionCommentService.deleteReplyComment(any(User.class), eq(replyCommentIdx)))
+ .thenReturn(expectedResponse);
+
+ User testUser = createTestUser(1, "테스트 사용자", Role.USER);
+
+ // when
+ BaseResponse response = questionCommentController.deleteReplyComment(testUser, replyCommentIdx);
+
+ // then
+ assertThat(response.getResult()).isEqualTo(expectedResponse);
+
+ // Verify
+ verify(questionCommentService).deleteReplyComment(any(User.class), eq(replyCommentIdx));
+ }
+
+ @Test
+ @DisplayName("존재하지 않는 대댓글 삭제 시 예외 발생")
+ void deleteReplyComment_NotFound_ThrowsException() {
+ // given
+ Integer replyCommentIdx = 999;
+
+ User testUser = createTestUser(1, "테스트 사용자", Role.USER);
+
+ when(questionCommentService.deleteReplyComment(any(User.class), eq(replyCommentIdx)))
+ .thenThrow(new BaseException(QUESTION_COMMENT_REPLY_NOT_FOUND));
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class, () ->
+ questionCommentController.deleteReplyComment(testUser, replyCommentIdx));
+
+ assertThat(exception.getErrorReason().getMessage())
+ .isEqualTo(QUESTION_COMMENT_REPLY_NOT_FOUND.getMessage());
+ }
+
+ @Test
+ @DisplayName("질문 댓글 좋아요 성공")
+ void questionCommentLike_Success() {
+ // given
+ User user = createTestUser(1, "테스트 사용자", Role.USER);
+ CommentLikeRequest request = new CommentLikeRequest(1);
+ String expectedMessage = "1번 질문 댓글 좋아요 완료";
+
+ when(questionCommentService.questionCommentLike(user, request))
+ .thenReturn(expectedMessage);
+
+ // when
+ BaseResponse response = questionCommentController.questionCommentLike(user, request);
+
+ // then
+ assertThat(response.getResult()).isEqualTo(expectedMessage);
+ verify(questionCommentService).questionCommentLike(user, request);
+ }
+
+ @Test
+ @DisplayName("질문 댓글 좋아요 취소 성공")
+ void questionCommentLikeCancel_Success() {
+ // given
+ User user = createTestUser(1, "테스트 사용자", Role.USER);
+ CommentLikeRequest request = new CommentLikeRequest(1);
+ String expectedMessage = "1번 질문 댓글 좋아요 취소 완료";
+
+ when(questionCommentService.questionCommentLikeCancel(user, request))
+ .thenReturn(expectedMessage);
+
+ // when
+ BaseResponse response = questionCommentController.questionCommentLikeCancel(user, request);
+
+ // then
+ assertThat(response.getResult()).isEqualTo(expectedMessage);
+ verify(questionCommentService).questionCommentLikeCancel(user, request);
+ }
+
+ @Test
+ @DisplayName("질문 대댓글 좋아요 성공")
+ void questionReplyCommentLike_Success() {
+ // given
+ User user = createTestUser(1, "테스트 사용자", Role.USER);
+ CommentLikeRequest request = new CommentLikeRequest(1);
+ String expectedMessage = "1번 질문 대댓글 좋아요 완료";
+
+ when(questionCommentService.questionReplyCommentLike(user, request))
+ .thenReturn(expectedMessage);
+
+ // when
+ BaseResponse response = questionCommentController.questionReplyCommentLike(user, request);
+
+ // then
+ assertThat(response.getResult()).isEqualTo(expectedMessage);
+ assertThat(response.getMessage()).isEqualTo(LIKE_SUCCESS.getMessage());
+ verify(questionCommentService).questionReplyCommentLike(user, request);
+ }
+
+ @Test
+ @DisplayName("이미 좋아요한 대댓글에 좋아요 시도 시 예외 발생")
+ void questionReplyCommentLike_AlreadyLiked_ThrowsException() {
+ // given
+ User user = createTestUser(1, "테스트 사용자", Role.USER);
+ CommentLikeRequest request = new CommentLikeRequest(1);
+
+ when(questionCommentService.questionReplyCommentLike(user, request))
+ .thenThrow(new BaseException(ALREADY_LIKE));
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class, () ->
+ questionCommentController.questionReplyCommentLike(user, request));
+
+ assertThat(exception.getErrorReason().getMessage()).isEqualTo(ALREADY_LIKE.getMessage());
+ verify(questionCommentService).questionReplyCommentLike(user, request);
+ }
+
+ @Test
+ @DisplayName("질문 대댓글 좋아요 취소 성공")
+ void questionReplyCommentLikeCancel_Success() {
+ // given
+ User user = createTestUser(1, "테스트 사용자", Role.USER);
+ CommentLikeRequest request = new CommentLikeRequest(1);
+ String expectedMessage = "1번 질문 대댓글 좋아요 취소 완료";
+
+ when(questionCommentService.questionReplyCommentLikeCancel(user, request))
+ .thenReturn(expectedMessage);
+
+ // when
+ BaseResponse response = questionCommentController.questionReplyCommentLikeCancel(user, request);
+
+ // then
+ assertThat(response.getResult()).isEqualTo(expectedMessage);
+ assertThat(response.getMessage()).isEqualTo(LIKE_CANCEL_SUCCESS.getMessage());
+ verify(questionCommentService).questionReplyCommentLikeCancel(user, request);
+ }
+
+ @Test
+ @DisplayName("좋아요하지 않은 대댓글 좋아요 취소 시도 시 예외 발생")
+ void questionReplyCommentLikeCancel_NotLiked_ThrowsException() {
+ // given
+ User user = createTestUser(1, "테스트 사용자", Role.USER);
+ CommentLikeRequest request = new CommentLikeRequest(1);
+
+ when(questionCommentService.questionReplyCommentLikeCancel(user, request))
+ .thenThrow(new BaseException(NOT_LIKE));
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class, () ->
+ questionCommentController.questionReplyCommentLikeCancel(user, request));
+
+ assertThat(exception.getErrorReason().getMessage()).isEqualTo(NOT_LIKE.getMessage());
+ verify(questionCommentService).questionReplyCommentLikeCancel(user, request);
+ }
+
+
+ private User createTestUser(int id, String name, Role role) {
+ return User.builder()
+ .id(id)
+ .name(name)
+ .role(role)
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/inha/git/question/api/controller/QuestionControllerTest.java b/src/test/java/inha/git/question/api/controller/QuestionControllerTest.java
new file mode 100644
index 00000000..65ae4bdf
--- /dev/null
+++ b/src/test/java/inha/git/question/api/controller/QuestionControllerTest.java
@@ -0,0 +1,551 @@
+package inha.git.question.api.controller;
+
+import inha.git.category.controller.dto.response.SearchCategoryResponse;
+import inha.git.common.BaseResponse;
+import inha.git.common.exceptions.BaseException;
+import inha.git.project.api.controller.dto.response.SearchFieldResponse;
+import inha.git.project.api.controller.dto.response.SearchUserResponse;
+import inha.git.question.api.controller.dto.request.CreateQuestionRequest;
+import inha.git.question.api.controller.dto.request.LikeRequest;
+import inha.git.question.api.controller.dto.request.SearchQuestionCond;
+import inha.git.question.api.controller.dto.request.UpdateQuestionRequest;
+import inha.git.question.api.controller.dto.response.QuestionResponse;
+import inha.git.question.api.controller.dto.response.SearchLikeState;
+import inha.git.question.api.controller.dto.response.SearchQuestionResponse;
+import inha.git.question.api.controller.dto.response.SearchQuestionsResponse;
+import inha.git.question.api.service.QuestionService;
+import inha.git.question.domain.Question;
+import inha.git.semester.controller.dto.response.SearchSemesterResponse;
+import inha.git.user.domain.User;
+import inha.git.user.domain.enums.Role;
+import inha.git.utils.PagingUtils;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+
+import java.time.LocalDateTime;
+import java.util.Arrays;
+import java.util.List;
+
+import static inha.git.common.code.status.ErrorStatus.*;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.BDDMockito.willThrow;
+import static org.mockito.Mockito.*;
+
+@DisplayName("질문 컨트롤러 테스트")
+@ExtendWith(MockitoExtension.class)
+class QuestionControllerTest {
+
+ @InjectMocks
+ private QuestionController questionController;
+
+ @Mock
+ private QuestionService questionService;
+
+ @Mock
+ private PagingUtils pagingUtils;
+
+ @Test
+ @DisplayName("질문 전체 조회 성공")
+ void getQuestions_Success() {
+ // given
+ int page = 1;
+ int size = 10;
+ List questions = Arrays.asList(
+ new SearchQuestionsResponse(
+ 1,
+ "질문1",
+ LocalDateTime.now(),
+ "과목1",
+ new SearchSemesterResponse(1, "2024-1"),
+ new SearchCategoryResponse(1, "카테고리1"),
+ 0,
+ 0,
+ List.of(new SearchFieldResponse(1, "분야1")),
+ new SearchUserResponse(1, "작성자1", 1)
+ ),
+ new SearchQuestionsResponse(
+ 2,
+ "질문2",
+ LocalDateTime.now(),
+ "과목2",
+ new SearchSemesterResponse(1, "2024-1"),
+ new SearchCategoryResponse(2, "카테고리2"),
+ 1,
+ 2,
+ List.of(new SearchFieldResponse(2, "분야2")),
+ new SearchUserResponse(2, "작성자2", 1)
+ )
+ );
+ Page expectedPage = new PageImpl<>(questions);
+
+ given(pagingUtils.toPageIndex(page)).willReturn(0);
+ given(pagingUtils.toPageSize(size)).willReturn(9);
+ given(questionService.getQuestions(0, 9)).willReturn(expectedPage);
+
+ // when
+ BaseResponse> response = questionController.getQuestions(page, size);
+
+ // then
+ assertThat(response.getResult()).isEqualTo(expectedPage);
+ verify(pagingUtils).validatePage(page);
+ verify(pagingUtils).validateSize(size);
+ verify(questionService).getQuestions(0, 9);
+ }
+
+ @Test
+ @DisplayName("질문 조건 검색 성공")
+ void getCondQuestions_Success() {
+ // given
+ int page = 1;
+ int size = 10;
+ SearchQuestionCond searchQuestionCond = new SearchQuestionCond(
+ 1, 1, 1, 1, 1, "알고리즘", "정렬"
+ );
+
+ List questions = Arrays.asList(
+ new SearchQuestionsResponse(
+ 1,
+ "정렬 알고리즘 질문",
+ LocalDateTime.now(),
+ "알고리즘",
+ new SearchSemesterResponse(1, "2024-1"),
+ new SearchCategoryResponse(1, "CS"),
+ 0,
+ 0,
+ List.of(new SearchFieldResponse(1, "알고리즘")),
+ new SearchUserResponse(1, "작성자1", 1)
+ )
+ );
+ Page expectedPage = new PageImpl<>(questions);
+
+ given(pagingUtils.toPageIndex(page)).willReturn(0);
+ given(pagingUtils.toPageSize(size)).willReturn(9);
+ given(questionService.getCondQuestions(searchQuestionCond, 0, 9))
+ .willReturn(expectedPage);
+
+ // when
+ BaseResponse> response =
+ questionController.getCondQuestions(page, size, searchQuestionCond);
+
+ // then
+ assertThat(response.getResult()).isEqualTo(expectedPage);
+ verify(pagingUtils).validatePage(page);
+ verify(pagingUtils).validateSize(size);
+ verify(questionService).getCondQuestions(searchQuestionCond, 0, 9);
+ }
+
+ @Test
+ @DisplayName("잘못된 페이지 번호로 조회 시 예외 발생")
+ void getQuestions_WithInvalidPage_ThrowsException() {
+ // given
+ Integer invalidPage = 0;
+ Integer size = 10;
+
+ doThrow(new BaseException(INVALID_PAGE))
+ .when(pagingUtils).validatePage(invalidPage);
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class, () ->
+ questionController.getQuestions(invalidPage, size));
+
+ assertThat(exception.getErrorReason().getMessage())
+ .isEqualTo(INVALID_PAGE.getMessage());
+ }
+
+ @Test
+ @DisplayName("질문 조건 검색 - 잘못된 페이지 번호")
+ void getCondQuestions_WithInvalidPage_ThrowsException() {
+ // given
+ Integer invalidPage = 0;
+ Integer size = 10;
+ SearchQuestionCond searchQuestionCond = new SearchQuestionCond(
+ 1, 1, 1, 1, 1, "알고리즘", "정렬"
+ );
+
+ doThrow(new BaseException(INVALID_PAGE))
+ .when(pagingUtils).validatePage(invalidPage);
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class, () ->
+ questionController.getCondQuestions(invalidPage, size, searchQuestionCond));
+
+ assertThat(exception.getErrorReason().getMessage())
+ .isEqualTo(INVALID_PAGE.getMessage());
+ }
+
+ @Test
+ @DisplayName("질문 상세 조회 성공")
+ void getQuestion_Success() {
+ // given
+ User user = createTestUser(1, "테스트유저", Role.USER);
+ Integer questionIdx = 1;
+ SearchQuestionResponse expectedResponse = createSearchQuestionResponse();
+
+ when(questionService.getQuestion(user, questionIdx))
+ .thenReturn(expectedResponse);
+
+ // when
+ BaseResponse response = questionController.getQuestion(user, questionIdx);
+
+ // then
+ assertThat(response.getResult()).isEqualTo(expectedResponse);
+ verify(questionService).getQuestion(user, questionIdx);
+ }
+
+ @Test
+ @DisplayName("존재하지 않는 질문 조회시 예외 발생")
+ void getQuestion_NotFound_ThrowsException() {
+ // given
+ User user = createTestUser(1, "테스트유저", Role.USER);
+ Integer invalidQuestionIdx = 999;
+
+ when(questionService.getQuestion(user, invalidQuestionIdx))
+ .thenThrow(new BaseException(QUESTION_NOT_FOUND));
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class,
+ () -> questionController.getQuestion(user, invalidQuestionIdx));
+
+ assertThat(exception.getErrorReason().getMessage())
+ .isEqualTo(QUESTION_NOT_FOUND.getMessage());
+ }
+
+ @Test
+ @DisplayName("질문 생성 성공")
+ void createQuestion_Success() {
+ // given
+ User user = createTestUser(1, "테스트유저", Role.USER);
+ CreateQuestionRequest request = new CreateQuestionRequest(
+ "질문 제목",
+ "질문 내용",
+ "알고리즘",
+ List.of(1),
+ 1
+ );
+ QuestionResponse expectedResponse = new QuestionResponse(1);
+
+ when(questionService.createQuestion(user, request))
+ .thenReturn(expectedResponse);
+
+ // when
+ BaseResponse response = questionController.createQuestion(user, request);
+
+ // then
+ assertThat(response.getResult()).isEqualTo(expectedResponse);
+ verify(questionService).createQuestion(user, request);
+ }
+
+ @Test
+ @DisplayName("기업 회원의 질문 생성 시도시 예외 발생")
+ void createQuestion_CompanyUser_ThrowsException() {
+ // given
+ User companyUser = createTestUser(1, "기업회원", Role.COMPANY);
+ CreateQuestionRequest request = new CreateQuestionRequest(
+ "질문 제목",
+ "질문 내용",
+ "알고리즘",
+ List.of(1),
+ 1
+ );
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class,
+ () -> questionController.createQuestion(companyUser, request));
+
+ assertThat(exception.getErrorReason().getMessage())
+ .isEqualTo(COMPANY_CANNOT_CREATE_QUESTION.getMessage());
+ verify(questionService, never()).createQuestion(any(), any());
+ }
+
+ @Test
+ @DisplayName("질문 수정 성공")
+ void updateQuestion_Success() {
+ // given
+ User user = createTestUser(1, "테스트유저", Role.USER);
+ Integer questionIdx = 1;
+ UpdateQuestionRequest request = new UpdateQuestionRequest(
+ "수정된 제목",
+ "수정된 내용",
+ "수정된 주제",
+ List.of(1, 2),
+ 1
+ );
+ QuestionResponse expectedResponse = new QuestionResponse(1);
+
+ when(questionService.updateQuestion(user, questionIdx, request))
+ .thenReturn(expectedResponse);
+
+ // when
+ BaseResponse response =
+ questionController.updateQuestion(user, questionIdx, request);
+
+ // then
+ assertThat(response.getResult()).isEqualTo(expectedResponse);
+ verify(questionService).updateQuestion(user, questionIdx, request);
+ }
+
+ @Test
+ @DisplayName("질문 삭제 성공")
+ void deleteQuestion_Success() {
+ // given
+ User user = createTestUser(1, "테스트유저", Role.USER);
+ Integer questionIdx = 1;
+ QuestionResponse expectedResponse = new QuestionResponse(1);
+
+ when(questionService.deleteQuestion(user, questionIdx)).thenReturn(expectedResponse);
+
+ // when
+ BaseResponse response = questionController.deleteQuestion(user, questionIdx);
+
+ // then
+ assertThat(response.getResult()).isEqualTo(expectedResponse);
+ verify(questionService).deleteQuestion(user, questionIdx);
+ }
+
+ @Test
+ @DisplayName("존재하지 않는 질문 삭제 시도시 예외 발생")
+ void deleteQuestion_QuestionNotFound_ThrowsException() {
+ // given
+ User user = createTestUser(1, "테스트유저", Role.USER);
+ Integer invalidQuestionIdx = 999;
+
+ when(questionService.deleteQuestion(user, invalidQuestionIdx))
+ .thenThrow(new BaseException(QUESTION_NOT_FOUND));
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class,
+ () -> questionController.deleteQuestion(user, invalidQuestionIdx));
+
+ assertThat(exception.getErrorReason().getMessage()).isEqualTo(QUESTION_NOT_FOUND.getMessage());
+ verify(questionService).deleteQuestion(user, invalidQuestionIdx);
+ }
+
+ @Test
+ @DisplayName("권한 없는 사용자가 질문 삭제 시도시 예외 발생")
+ void deleteQuestion_UnauthorizedUser_ThrowsException() {
+ // given
+ User unauthorizedUser = createTestUser(2, "다른 사용자", Role.USER);
+ Integer questionIdx = 1;
+
+ when(questionService.deleteQuestion(unauthorizedUser, questionIdx))
+ .thenThrow(new BaseException(QUESTION_DELETE_NOT_AUTHORIZED));
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class,
+ () -> questionController.deleteQuestion(unauthorizedUser, questionIdx));
+
+ assertThat(exception.getErrorReason().getMessage()).isEqualTo(QUESTION_DELETE_NOT_AUTHORIZED.getMessage());
+ verify(questionService).deleteQuestion(unauthorizedUser, questionIdx);
+ }
+
+ @Test
+ @DisplayName("관리자가 다른 유저의 질문 삭제 성공")
+ void deleteQuestion_AsAdmin_Success() {
+ // given
+ User admin = createTestUser(1, "관리자", Role.ADMIN);
+ User otherUser = createTestUser(2, "다른유저", Role.USER);
+ createTestQuestion(1, "질문 제목", "질문 내용", otherUser);
+
+ when(questionService.deleteQuestion(admin, 1))
+ .thenReturn(new QuestionResponse(1));
+
+ // when
+ BaseResponse response = questionController.deleteQuestion(admin, 1);
+
+ // then
+ assertThat(response.getResult().idx()).isEqualTo(1);
+ verify(questionService).deleteQuestion(admin, 1);
+ }
+
+ @Test
+ @DisplayName("질문 좋아요 성공")
+ void questionLike_Success() {
+ // given
+ User user = createTestUser(1, "테스트유저", Role.USER);
+ LikeRequest likeRequest = new LikeRequest(100); // 질문 ID 100
+
+ given(questionService.createQuestionLike(user, likeRequest))
+ .willReturn("100번 질문 좋아요 완료");
+
+ // when
+ BaseResponse response = questionController.questionLike(user, likeRequest);
+
+ // then
+ assertThat(response.getResult()).isEqualTo("100번 질문 좋아요 완료");
+ verify(questionService).createQuestionLike(user, likeRequest);
+ }
+
+ @Test
+ @DisplayName("질문 좋아요 - 내 질문 좋아요 시도 시 예외 발생")
+ void questionLike_MyQuestion_ThrowsException() {
+ // given
+ User user = createTestUser(1, "내질문", Role.USER);
+ LikeRequest likeRequest = new LikeRequest(100);
+
+ willThrow(new BaseException(MY_QUESTION_LIKE))
+ .given(questionService).createQuestionLike(user, likeRequest);
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class,
+ () -> questionController.questionLike(user, likeRequest));
+
+ assertThat(exception.getErrorReason().getMessage())
+ .isEqualTo(MY_QUESTION_LIKE.getMessage());
+ verify(questionService).createQuestionLike(user, likeRequest);
+ }
+
+ @Test
+ @DisplayName("질문 좋아요 - 이미 좋아요 한 질문 예외 발생")
+ void questionLike_AlreadyLiked_ThrowsException() {
+ // given
+ User user = createTestUser(1, "테스트유저", Role.USER);
+ LikeRequest likeRequest = new LikeRequest(100);
+
+ willThrow(new BaseException(QUESTION_ALREADY_LIKE))
+ .given(questionService).createQuestionLike(user, likeRequest);
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class,
+ () -> questionController.questionLike(user, likeRequest));
+
+ assertThat(exception.getErrorReason().getMessage())
+ .isEqualTo(QUESTION_ALREADY_LIKE.getMessage());
+ verify(questionService).createQuestionLike(user, likeRequest);
+ }
+
+ @Test
+ @DisplayName("질문 좋아요 - 질문 없음 예외 발생")
+ void questionLike_QuestionNotFound_ThrowsException() {
+ // given
+ User user = createTestUser(1, "테스트유저", Role.USER);
+ LikeRequest likeRequest = new LikeRequest(999);
+
+ willThrow(new BaseException(QUESTION_NOT_FOUND))
+ .given(questionService).createQuestionLike(user, likeRequest);
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class,
+ () -> questionController.questionLike(user, likeRequest));
+
+ assertThat(exception.getErrorReason().getMessage())
+ .isEqualTo(QUESTION_NOT_FOUND.getMessage());
+ verify(questionService).createQuestionLike(user, likeRequest);
+ }
+
+
+ @Test
+ @DisplayName("질문 좋아요 취소 성공")
+ void questionLikeCancel_Success() {
+ // given
+ User user = createTestUser(1, "테스트유저", Role.USER);
+ LikeRequest likeRequest = new LikeRequest(200); // 질문 ID 200
+
+ given(questionService.questionLikeCancel(user, likeRequest))
+ .willReturn("200번 프로젝트 좋아요 취소 완료");
+
+ // when
+ BaseResponse response = questionController.questionLikeCancel(user, likeRequest);
+
+ // then
+ assertThat(response.getResult()).isEqualTo("200번 프로젝트 좋아요 취소 완료");
+ verify(questionService).questionLikeCancel(user, likeRequest);
+ }
+
+ @Test
+ @DisplayName("질문 좋아요 취소 - 내 질문 예외 발생")
+ void questionLikeCancel_MyQuestion_ThrowsException() {
+ // given
+ User user = createTestUser(1, "테스트유저", Role.USER);
+ LikeRequest likeRequest = new LikeRequest(200);
+
+ willThrow(new BaseException(MY_QUESTION_LIKE))
+ .given(questionService).questionLikeCancel(user, likeRequest);
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class,
+ () -> questionController.questionLikeCancel(user, likeRequest));
+
+ assertThat(exception.getErrorReason().getMessage())
+ .isEqualTo(MY_QUESTION_LIKE.getMessage());
+ verify(questionService).questionLikeCancel(user, likeRequest);
+ }
+
+ @Test
+ @DisplayName("질문 좋아요 취소 - 좋아요하지 않은 질문 예외 발생")
+ void questionLikeCancel_NotLiked_ThrowsException() {
+ // given
+ User user = createTestUser(1, "테스트유저", Role.USER);
+ LikeRequest likeRequest = new LikeRequest(200);
+
+ willThrow(new BaseException(QUESTION_NOT_LIKE))
+ .given(questionService).questionLikeCancel(user, likeRequest);
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class,
+ () -> questionController.questionLikeCancel(user, likeRequest));
+
+ assertThat(exception.getErrorReason().getMessage())
+ .isEqualTo(QUESTION_NOT_LIKE.getMessage());
+ verify(questionService).questionLikeCancel(user, likeRequest);
+ }
+
+ @Test
+ @DisplayName("질문 좋아요 취소 - 질문 없음 예외 발생")
+ void questionLikeCancel_QuestionNotFound_ThrowsException() {
+ // given
+ User user = createTestUser(1, "테스트유저", Role.USER);
+ LikeRequest likeRequest = new LikeRequest(999);
+
+ willThrow(new BaseException(QUESTION_NOT_FOUND))
+ .given(questionService).questionLikeCancel(user, likeRequest);
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class,
+ () -> questionController.questionLikeCancel(user, likeRequest));
+
+ assertThat(exception.getErrorReason().getMessage())
+ .isEqualTo(QUESTION_NOT_FOUND.getMessage());
+ verify(questionService).questionLikeCancel(user, likeRequest);
+ }
+
+ private Question createTestQuestion(Integer id , String title, String contents, User user) {
+ return Question.builder()
+ .id(id)
+ .title(title)
+ .contents(contents)
+ .user(user)
+ .build();
+ }
+
+ private User createTestUser(Integer id, String name, Role role) {
+ return User.builder()
+ .id(id)
+ .name(name)
+ .role(role)
+ .build();
+ }
+
+ private SearchQuestionResponse createSearchQuestionResponse() {
+ return new SearchQuestionResponse(
+ 1, // idx
+ "질문 제목", // title
+ "질문 내용", // contents
+ LocalDateTime.now(), // createdAt
+ new SearchLikeState(false), // likeState
+ 0, // likeCount
+ "알고리즘", // subject
+ List.of(new SearchFieldResponse(1, "알고리즘")), // fieldList
+ new SearchUserResponse(1, "테스트유저", 1), // author
+ new SearchSemesterResponse(1, "2024-1") // semester
+ );
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/inha/git/question/api/service/QuestionCommentServiceTest.java b/src/test/java/inha/git/question/api/service/QuestionCommentServiceTest.java
new file mode 100644
index 00000000..7be06226
--- /dev/null
+++ b/src/test/java/inha/git/question/api/service/QuestionCommentServiceTest.java
@@ -0,0 +1,809 @@
+package inha.git.question.api.service;
+
+import inha.git.common.exceptions.BaseException;
+import inha.git.mapping.domain.repository.QuestionCommentLikeJpaRepository;
+import inha.git.mapping.domain.repository.QuestionReplyCommentLikeJpaRepository;
+import inha.git.question.api.controller.dto.request.*;
+import inha.git.question.api.controller.dto.response.CommentResponse;
+import inha.git.question.api.controller.dto.response.ReplyCommentResponse;
+import inha.git.question.api.mapper.QuestionMapper;
+import inha.git.question.domain.Question;
+import inha.git.question.domain.QuestionComment;
+import inha.git.question.domain.QuestionReplyComment;
+import inha.git.question.domain.repository.QuestionCommentJpaRepository;
+import inha.git.question.domain.repository.QuestionJpaRepository;
+import inha.git.question.domain.repository.QuestionReplyCommentJpaRepository;
+import inha.git.user.domain.User;
+import inha.git.user.domain.enums.Role;
+import inha.git.utils.IdempotentProvider;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import static inha.git.common.BaseEntity.State.ACTIVE;
+import static inha.git.common.code.status.ErrorStatus.*;
+import static org.assertj.core.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.*;
+
+@DisplayName("질문 댓글 서비스 테스트")
+@ExtendWith(MockitoExtension.class)
+class QuestionCommentServiceTest {
+
+ @InjectMocks
+ private QuestionCommentServiceImpl questionCommentService;
+
+ @Mock
+ private QuestionJpaRepository questionJpaRepository;
+ @Mock
+ private QuestionCommentJpaRepository questionCommentJpaRepository;
+ @Mock
+ private QuestionCommentLikeJpaRepository questionCommentLikeJpaRepository;
+ @Mock
+ private QuestionReplyCommentLikeJpaRepository questionReplyCommentLikeJpaRepository;
+
+ @Mock
+ private QuestionReplyCommentJpaRepository questionReplyCommentJpaRepository;
+
+ @Mock
+ private QuestionMapper questionMapper;
+
+ @Mock
+ private IdempotentProvider idempotentProvider;
+
+ @Test
+ @DisplayName("특정 질문의 댓글+대댓글 조회 성공")
+ void getAllCommentsByQuestionIdx_Success() {
+ // given
+ User user = createTestUser(1, "테스트유저", Role.USER);
+ Question question = createTestQuestion(100, "질문 제목", "질문 내용", createTestUser(999, "작성자", Role.USER));
+
+ QuestionComment comment = createTestComment(10, createTestUser(2, "댓글작성자", Role.USER), question);
+ QuestionReplyComment reply = createTestReply(1001, createTestUser(3, "대댓글작성자", Role.USER), comment);
+
+ comment.setLikeCount(2);
+ comment.getReplies().add(reply);
+
+ // 리포지토리 mock
+ given(questionJpaRepository.findByIdAndState(100, ACTIVE))
+ .willReturn(Optional.of(question));
+ given(questionCommentJpaRepository.findAllByQuestionAndStateOrderByIdAsc(question, ACTIVE))
+ .willReturn(List.of(comment));
+
+ given(questionCommentLikeJpaRepository.existsByUserAndQuestionComment(user, comment))
+ .willReturn(true);
+ given(questionReplyCommentLikeJpaRepository.existsByUserAndQuestionReplyComment(user, reply))
+ .willReturn(false);
+
+ SearchReplyCommentResponse fakeReplyRes = new SearchReplyCommentResponse(
+ 1001,
+ "대댓글 내용",
+ null,
+ 1,
+ false,
+ LocalDateTime.now()
+ );
+ CommentWithRepliesResponse fakeCommentRes = new CommentWithRepliesResponse(
+ 10,
+ "댓글 내용",
+ null,
+ LocalDateTime.now(),
+ 2,
+ true,
+ List.of(fakeReplyRes)
+ );
+
+ given(questionMapper.toSearchReplyCommentResponse(reply, false))
+ .willReturn(fakeReplyRes);
+ given(questionMapper.toCommentWithRepliesResponse(eq(comment), eq(true), anyList()))
+ .willReturn(fakeCommentRes);
+
+ // when
+ List result =
+ questionCommentService.getAllCommentsByQuestionIdx(user, 100);
+
+ // then
+ assertThat(result).hasSize(1);
+ assertThat(result.get(0).idx()).isEqualTo(10);
+ assertThat(result.get(0).likeState()).isTrue();
+ assertThat(result.get(0).replies()).hasSize(1);
+ assertThat(result.get(0).replies().get(0).idx()).isEqualTo(1001);
+ assertThat(result.get(0).replies().get(0).likeState()).isFalse();
+
+ verify(questionJpaRepository).findByIdAndState(100, ACTIVE);
+ verify(questionCommentJpaRepository).findAllByQuestionAndStateOrderByIdAsc(question, ACTIVE);
+ verify(questionMapper).toSearchReplyCommentResponse(reply, false);
+ verify(questionMapper).toCommentWithRepliesResponse(eq(comment), eq(true), anyList());
+ }
+
+ @Test
+ @DisplayName("존재하지 않는 질문 조회 시 예외 발생")
+ void getAllCommentsByQuestionIdx_QuestionNotFound_ThrowsException() {
+ // given
+ User user = createTestUser(1, "테스트유저", Role.USER);
+ Integer invalidQuestionIdx = 999;
+
+ given(questionJpaRepository.findByIdAndState(invalidQuestionIdx, ACTIVE))
+ .willReturn(Optional.empty());
+
+ // when & then
+ BaseException ex = assertThrows(BaseException.class,
+ () -> questionCommentService.getAllCommentsByQuestionIdx(user, invalidQuestionIdx));
+
+ assertThat(ex.getErrorReason().getMessage()).isEqualTo(QUESTION_NOT_FOUND.getMessage());
+ verify(questionCommentJpaRepository, never())
+ .findAllByQuestionAndStateOrderByIdAsc(any(), any());
+ }
+
+ @Test
+ @DisplayName("질문 댓글 생성 성공")
+ void createComment_Success() {
+ // given
+ User user = createTestUser(1, "테스트유저", Role.USER);
+ Question question = createTestQuestion(1, "질문 제목", "질문 내용", user);
+ CreateCommentRequest request = new CreateCommentRequest(1, "댓글 내용");
+
+ QuestionComment questionComment = createTestComment(10, user, question);
+ CommentResponse expectedResponse = new CommentResponse(10);
+
+ when(questionJpaRepository.findByIdAndState(1, ACTIVE))
+ .thenReturn(Optional.of(question));
+ when(questionMapper.toQuestionComment(request, user, question))
+ .thenReturn(questionComment);
+ when(questionCommentJpaRepository.save(any(QuestionComment.class)))
+ .thenReturn(questionComment);
+ when(questionMapper.toCommentResponse(questionComment))
+ .thenReturn(expectedResponse);
+
+ // when
+ CommentResponse response = questionCommentService.createComment(user, request);
+
+ // then
+ assertThat(response).isEqualTo(expectedResponse);
+ verify(questionJpaRepository).findByIdAndState(1, ACTIVE);
+ verify(questionCommentJpaRepository).save(questionComment);
+ }
+
+ @Test
+ @DisplayName("존재하지 않는 질문에 댓글 생성 시 예외 발생")
+ void createComment_QuestionNotFound_ThrowsException() {
+ // given
+ User user = createTestUser(1, "테스트유저", Role.USER);
+ CreateCommentRequest request = new CreateCommentRequest(999, "댓글 내용");
+
+ when(questionJpaRepository.findByIdAndState(999, ACTIVE))
+ .thenReturn(Optional.empty());
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class,
+ () -> questionCommentService.createComment(user, request));
+
+ assertThat(exception.getErrorReason().getMessage()).isEqualTo(QUESTION_NOT_FOUND.getMessage());
+ verify(questionJpaRepository).findByIdAndState(999, ACTIVE);
+ verify(questionCommentJpaRepository, never()).save(any());
+ }
+
+ @Test
+ @DisplayName("질문 댓글 수정 성공")
+ void updateComment_Success() {
+ // given
+ User user = createTestUser(1, "테스트유저", Role.USER);
+ Integer commentIdx = 1;
+ UpdateCommentRequest request = new UpdateCommentRequest("수정된 댓글 내용");
+ QuestionComment originalComment = createTestQuestionComment(commentIdx, "원본 댓글 내용", user);
+
+ CommentResponse expectedResponse = new CommentResponse(commentIdx);
+
+ // mocking
+ when(questionCommentJpaRepository.findByIdAndState(commentIdx, ACTIVE))
+ .thenReturn(Optional.of(originalComment));
+ when(questionMapper.toCommentResponse(any(QuestionComment.class)))
+ .thenReturn(expectedResponse);
+
+ // when
+ CommentResponse response = questionCommentService.updateComment(user, commentIdx, request);
+
+ // then
+ assertThat(response).isNotNull();
+ assertThat(response).isEqualTo(expectedResponse);
+
+ verify(questionCommentJpaRepository).findByIdAndState(commentIdx, ACTIVE);
+ verify(questionCommentJpaRepository).save(any(QuestionComment.class));
+ verify(questionMapper).toCommentResponse(any(QuestionComment.class));
+ }
+
+ @Test
+ @DisplayName("존재하지 않는 댓글 수정 시도시 예외 발생")
+ void updateComment_CommentNotFound_ThrowsException() {
+ // given
+ User user = createTestUser(1, "테스트유저", Role.USER);
+ Integer commentIdx = 999;
+ UpdateCommentRequest request = new UpdateCommentRequest("수정된 댓글 내용");
+
+ // mocking
+ when(questionCommentJpaRepository.findByIdAndState(commentIdx, ACTIVE))
+ .thenReturn(Optional.empty());
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class, () ->
+ questionCommentService.updateComment(user, commentIdx, request));
+
+ assertThat(exception.getErrorReason().getMessage())
+ .isEqualTo(QUESTION_COMMENT_NOT_FOUND.getMessage());
+ }
+
+ @Test
+ @DisplayName("권한 없는 사용자의 댓글 수정 시도시 예외 발생")
+ void updateComment_Unauthorized_ThrowsException() {
+ // given
+ User originalAuthor = createTestUser(1, "작성자", Role.USER);
+ User unauthorizedUser = createTestUser(2, "다른사용자", Role.USER);
+ Integer commentIdx = 1;
+ UpdateCommentRequest request = new UpdateCommentRequest("수정된 댓글 내용");
+ QuestionComment originalComment = createTestQuestionComment(commentIdx, "원본 댓글 내용", originalAuthor);
+
+ // mocking
+ when(questionCommentJpaRepository.findByIdAndState(commentIdx, ACTIVE))
+ .thenReturn(Optional.of(originalComment));
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class, () ->
+ questionCommentService.updateComment(unauthorizedUser, commentIdx, request));
+
+ assertThat(exception.getErrorReason().getMessage())
+ .isEqualTo(QUESTION_COMMENT_UPDATE_NOT_AUTHORIZED.getMessage());
+ }
+
+ @Test
+ @DisplayName("질문 댓글 삭제 성공")
+ void deleteComment_Success() {
+ // given
+ User user = createTestUser(1, "테스트유저", Role.USER);
+ Integer commentIdx = 1;
+ Question question = createTestQuestion(1, "테스트 질문", "테스트 내용", user);
+ QuestionComment comment = createTestQuestionComment(commentIdx, "테스트 댓글", user);
+ comment.setQuestion(question);
+ CommentResponse expectedResponse = new CommentResponse(commentIdx);
+
+ // mocking
+ when(questionCommentJpaRepository.findByIdAndState(commentIdx, ACTIVE))
+ .thenReturn(Optional.of(comment));
+ when(questionReplyCommentJpaRepository.existsByQuestionCommentAndState(comment, ACTIVE))
+ .thenReturn(false);
+ when(questionMapper.toCommentResponse(any(QuestionComment.class)))
+ .thenReturn(expectedResponse);
+
+ // when
+ CommentResponse response = questionCommentService.deleteComment(user, commentIdx);
+
+ // then
+ assertThat(response).isNotNull();
+ assertThat(response).isEqualTo(expectedResponse);
+
+ verify(questionCommentJpaRepository).findByIdAndState(commentIdx, ACTIVE);
+ verify(questionCommentJpaRepository).save(any(QuestionComment.class));
+ verify(questionMapper).toCommentResponse(any(QuestionComment.class));
+ }
+
+ @Test
+ @DisplayName("존재하지 않는 댓글 삭제 시도시 예외 발생")
+ void deleteComment_CommentNotFound_ThrowsException() {
+ // given
+ User user = createTestUser(1, "테스트유저", Role.USER);
+ Integer commentIdx = 999;
+
+ // mocking
+ when(questionCommentJpaRepository.findByIdAndState(commentIdx, ACTIVE))
+ .thenReturn(Optional.empty());
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class, () ->
+ questionCommentService.deleteComment(user, commentIdx));
+
+ assertThat(exception.getErrorReason().getMessage())
+ .isEqualTo(QUESTION_COMMENT_NOT_FOUND.getMessage());
+ }
+
+ @Test
+ @DisplayName("권한 없는 사용자의 댓글 삭제 시도시 예외 발생")
+ void deleteComment_Unauthorized_ThrowsException() {
+ // given
+ User originalAuthor = createTestUser(1, "작성자", Role.USER);
+ User unauthorizedUser = createTestUser(2, "다른사용자", Role.USER);
+ Integer commentIdx = 1;
+ QuestionComment comment = createTestQuestionComment(commentIdx, "테스트 댓글", originalAuthor);
+
+ // mocking
+ when(questionCommentJpaRepository.findByIdAndState(commentIdx, ACTIVE))
+ .thenReturn(Optional.of(comment));
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class, () ->
+ questionCommentService.deleteComment(unauthorizedUser, commentIdx));
+
+ assertThat(exception.getErrorReason().getMessage())
+ .isEqualTo(QUESTION_COMMENT_DELETE_NOT_AUTHORIZED.getMessage());
+ }
+
+ @Test
+ @DisplayName("이미 삭제된 댓글을 삭제하려고 시도 시 예외 발생")
+ void deleteComment_AlreadyDeleted_ThrowsException() {
+ // given
+ User user = createTestUser(1, "테스트유저", Role.USER);
+ Integer commentIdx = 1;
+ QuestionComment comment = createTestQuestionComment(commentIdx, "삭제된 댓글", user);
+ comment.setDeletedAt();
+
+ // mocking
+ when(questionCommentJpaRepository.findByIdAndState(commentIdx, ACTIVE))
+ .thenReturn(Optional.of(comment));
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class, () ->
+ questionCommentService.deleteComment(user, commentIdx));
+
+ assertThat(exception.getErrorReason().getMessage())
+ .isEqualTo(QUESTION_COMMENT_ALREADY_DELETED.getMessage());
+ }
+
+ @Test
+ @DisplayName("질문 댓글 답글 생성 성공")
+ void createReplyComment_Success() {
+ // given
+ User user = createTestUser(1, "테스트유저", Role.USER);
+ Integer commentIdx = 1;
+ Question question = createTestQuestion(1, "테스트 질문", "테스트 내용", user);
+ QuestionComment comment = createTestQuestionComment(commentIdx, "테스트 댓글", user);
+ comment.setQuestion(question);
+ CreateReplyCommentRequest request = new CreateReplyCommentRequest(
+ commentIdx,
+ "테스트 답글 내용"
+ );
+ QuestionReplyComment replyComment = createTestQuestionReplyComment(1, "테스트 답글 내용", user, comment);
+ ReplyCommentResponse expectedResponse = new ReplyCommentResponse(1);
+
+ // mocking
+ when(questionCommentJpaRepository.findByIdAndState(commentIdx, ACTIVE))
+ .thenReturn(Optional.of(comment));
+ when(questionMapper.toQuestionReplyComment(request, user, comment))
+ .thenReturn(replyComment);
+ when(questionReplyCommentJpaRepository.save(any(QuestionReplyComment.class)))
+ .thenReturn(replyComment);
+ when(questionMapper.toReplyCommentResponse(replyComment))
+ .thenReturn(expectedResponse);
+
+ // when
+ ReplyCommentResponse response = questionCommentService.createReplyComment(user, request);
+
+ // then
+ assertThat(response).isNotNull();
+ assertThat(response).isEqualTo(expectedResponse);
+
+ verify(questionCommentJpaRepository).findByIdAndState(commentIdx, ACTIVE);
+ verify(questionReplyCommentJpaRepository).save(replyComment);
+ verify(questionMapper).toReplyCommentResponse(replyComment);
+ }
+
+ @Test
+ @DisplayName("존재하지 않는 댓글에 답글 생성 시 예외 발생")
+ void createReplyComment_CommentNotFound_ThrowsException() {
+ // given
+ User user = createTestUser(1, "테스트유저", Role.USER);
+ Integer invalidCommentIdx = 999;
+ CreateReplyCommentRequest request = new CreateReplyCommentRequest(
+ invalidCommentIdx,
+ "테스트 답글 내용"
+ );
+
+ // mocking
+ when(questionCommentJpaRepository.findByIdAndState(invalidCommentIdx, ACTIVE))
+ .thenReturn(Optional.empty());
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class,
+ () -> questionCommentService.createReplyComment(user, request));
+
+ assertThat(exception.getErrorReason().getMessage())
+ .isEqualTo(QUESTION_COMMENT_NOT_FOUND.getMessage());
+ }
+
+ @Test
+ @DisplayName("대댓글 수정 성공")
+ void updateReplyComment_Success() {
+ // given
+ Integer replyCommentIdx = 1;
+ UpdateCommentRequest request = new UpdateCommentRequest("수정된 대댓글 내용");
+ QuestionReplyComment replyComment = createTestReplyComment(replyCommentIdx, "원본 대댓글 내용");
+ ReplyCommentResponse expectedResponse = new ReplyCommentResponse(replyCommentIdx);
+
+ // Mocking
+ when(questionReplyCommentJpaRepository.findByIdAndState(replyCommentIdx, ACTIVE))
+ .thenReturn(Optional.of(replyComment));
+ when(questionMapper.toReplyCommentResponse(replyComment))
+ .thenReturn(expectedResponse);
+
+ User testUser = createTestUser(1, "테스트 사용자", Role.USER);
+
+ // when
+ ReplyCommentResponse response = questionCommentService.updateReplyComment(testUser, replyCommentIdx, request);
+
+ // then
+ assertThat(response).isEqualTo(expectedResponse);
+
+ // Verify interactions
+ verify(questionReplyCommentJpaRepository).findByIdAndState(replyCommentIdx, ACTIVE);
+ verify(questionReplyCommentJpaRepository).save(replyComment);
+ verify(questionMapper).toReplyCommentResponse(replyComment);
+ }
+
+ @Test
+ @DisplayName("존재하지 않는 대댓글 수정 시 예외 발생")
+ void updateReplyComment_NotFound_ThrowsException() {
+ // given
+ Integer replyCommentIdx = 999;
+ UpdateCommentRequest request = new UpdateCommentRequest("수정된 대댓글 내용");
+
+ when(questionReplyCommentJpaRepository.findByIdAndState(replyCommentIdx, ACTIVE))
+ .thenReturn(Optional.empty());
+
+ User testUser = createTestUser(1, "테스트 사용자", Role.USER);
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class, () ->
+ questionCommentService.updateReplyComment(testUser, replyCommentIdx, request));
+
+ assertThat(exception.getErrorReason().getMessage()).isEqualTo(QUESTION_COMMENT_REPLY_NOT_FOUND.getMessage());
+
+ // Verify no interactions with mapper or save
+ verify(questionReplyCommentJpaRepository).findByIdAndState(replyCommentIdx, ACTIVE);
+ verifyNoInteractions(questionMapper);
+ }
+
+ @Test
+ @DisplayName("수정 권한 없는 대댓글 수정 시 예외 발생")
+ void updateReplyComment_NotAuthorized_ThrowsException() {
+ // given
+ Integer replyCommentIdx = 1;
+ UpdateCommentRequest request = new UpdateCommentRequest("수정된 대댓글 내용");
+ User anotherUser = createTestUser(2, "다른 사용자", Role.USER);
+ QuestionReplyComment replyComment = createTestReplyComment(replyCommentIdx, "원본 대댓글 내용", anotherUser);
+
+ when(questionReplyCommentJpaRepository.findByIdAndState(replyCommentIdx, ACTIVE))
+ .thenReturn(Optional.of(replyComment));
+
+ User testUser = createTestUser(1, "테스트 사용자", Role.USER);
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class, () ->
+ questionCommentService.updateReplyComment(testUser, replyCommentIdx, request));
+
+ assertThat(exception.getErrorReason().getMessage()).isEqualTo(QUESTION_COMMENT_REPLY_UPDATE_NOT_AUTHORIZED.getMessage());
+
+ // Verify no save or mapping
+ verify(questionReplyCommentJpaRepository).findByIdAndState(replyCommentIdx, ACTIVE);
+ verifyNoInteractions(questionMapper);
+ }
+
+ @Test
+ @DisplayName("대댓글 삭제 성공")
+ void deleteReplyComment_Success() {
+ // given
+ Integer replyCommentIdx = 1;
+ User user = createTestUser(1, "테스트 사용자", Role.USER);
+ QuestionReplyComment replyComment = createTestReplyComment(replyCommentIdx, "대댓글 내용", user);
+ ReplyCommentResponse expectedResponse = new ReplyCommentResponse(replyCommentIdx);
+
+ // Mocking
+ when(questionReplyCommentJpaRepository.findByIdAndState(replyCommentIdx, ACTIVE))
+ .thenReturn(Optional.of(replyComment));
+ when(questionMapper.toReplyCommentResponse(replyComment))
+ .thenReturn(expectedResponse);
+
+ // when
+ ReplyCommentResponse response = questionCommentService.deleteReplyComment(user, replyCommentIdx);
+
+ // then
+ assertThat(response).isEqualTo(expectedResponse);
+
+ // Verify interactions
+ verify(questionReplyCommentJpaRepository).findByIdAndState(replyCommentIdx, ACTIVE);
+ verify(questionReplyCommentJpaRepository).save(replyComment);
+ verify(questionMapper).toReplyCommentResponse(replyComment);
+ }
+
+ @Test
+ @DisplayName("존재하지 않는 대댓글 삭제 시 예외 발생")
+ void deleteReplyComment_NotFound_ThrowsException() {
+ // given
+ Integer replyCommentIdx = 999;
+
+ when(questionReplyCommentJpaRepository.findByIdAndState(replyCommentIdx, ACTIVE))
+ .thenReturn(Optional.empty());
+
+ User testUser = createTestUser(1, "테스트 사용자", Role.USER);
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class, () ->
+ questionCommentService.deleteReplyComment(testUser, replyCommentIdx));
+
+ assertThat(exception.getErrorReason().getMessage())
+ .isEqualTo(QUESTION_COMMENT_REPLY_NOT_FOUND.getMessage());
+ }
+
+ @Test
+ @DisplayName("삭제 권한 없는 대댓글 삭제 시 예외 발생")
+ void deleteReplyComment_NotAuthorized_ThrowsException() {
+ // given
+ Integer replyCommentIdx = 1;
+ User anotherUser = createTestUser(2, "다른 사용자", Role.USER);
+ QuestionReplyComment replyComment = createTestReplyComment(replyCommentIdx, "대댓글 내용", anotherUser);
+
+ when(questionReplyCommentJpaRepository.findByIdAndState(replyCommentIdx, ACTIVE))
+ .thenReturn(Optional.of(replyComment));
+
+ User testUser = createTestUser(1, "테스트 사용자", Role.USER);
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class, () ->
+ questionCommentService.deleteReplyComment(testUser, replyCommentIdx));
+
+ assertThat(exception.getErrorReason().getMessage())
+ .isEqualTo(QUESTION_COMMENT_REPLY_DELETE_NOT_AUTHORIZED.getMessage());
+ }
+
+ @Test
+ @DisplayName("질문 댓글 좋아요 취소 성공")
+ void questionCommentLikeCancel_Success() {
+ // given
+ User user = createTestUser(1, "테스트 사용자", Role.USER);
+ CommentLikeRequest request = new CommentLikeRequest(1);
+
+ // 댓글 객체 생성 및 초기화
+ QuestionComment comment = createTestQuestionComment(1, "테스트 댓글", createTestUser(2, "작성자", Role.USER));
+ comment.setLikeCount(1); // 좋아요 1로 초기화
+
+ // Mock 설정
+ when(questionCommentJpaRepository.findByIdAndState(request.idx(), ACTIVE))
+ .thenReturn(Optional.of(comment));
+ when(questionCommentLikeJpaRepository.existsByUserAndQuestionComment(user, comment))
+ .thenReturn(true); // 이미 좋아요를 누른 상태로 설정
+
+ // when
+ String result = questionCommentService.questionCommentLikeCancel(user, request);
+
+ // then
+ assertThat(result).isEqualTo("1번 질문 댓글 좋아요 취소 완료");
+ assertThat(comment.getLikeCount()).isEqualTo(0); // 좋아요 수가 0이어야 함
+ verify(questionCommentLikeJpaRepository).deleteByUserAndQuestionComment(user, comment);
+ }
+
+
+
+ @Test
+ @DisplayName("이미 좋아요한 댓글 좋아요 시도 시 예외 발생")
+ void questionCommentLike_AlreadyLiked_ThrowsException() {
+ // given
+ User user = createTestUser(1, "테스트 사용자", Role.USER);
+ CommentLikeRequest request = new CommentLikeRequest(1);
+ QuestionComment comment = createTestQuestionComment(1, "테스트 댓글", createTestUser(2, "작성자", Role.USER));
+
+ when(questionCommentJpaRepository.findByIdAndState(request.idx(), ACTIVE))
+ .thenReturn(Optional.of(comment));
+ when(questionCommentLikeJpaRepository.existsByUserAndQuestionComment(user, comment))
+ .thenReturn(true);
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class,
+ () -> questionCommentService.questionCommentLike(user, request));
+ assertThat(exception.getErrorReason().getMessage()).isEqualTo(ALREADY_LIKE.getMessage());
+ }
+
+ @Test
+ @DisplayName("좋아요하지 않은 댓글 좋아요 취소 시도 시 예외 발생")
+ void questionCommentLikeCancel_NotLiked_ThrowsException() {
+ // given
+ User user = createTestUser(1, "테스트 사용자", Role.USER);
+ CommentLikeRequest request = new CommentLikeRequest(1);
+ QuestionComment comment = createTestQuestionComment(1, "테스트 댓글", createTestUser(2, "작성자", Role.USER));
+
+ when(questionCommentJpaRepository.findByIdAndState(request.idx(), ACTIVE))
+ .thenReturn(Optional.of(comment));
+ when(questionCommentLikeJpaRepository.existsByUserAndQuestionComment(user, comment))
+ .thenReturn(false);
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class,
+ () -> questionCommentService.questionCommentLikeCancel(user, request));
+ assertThat(exception.getErrorReason().getMessage()).isEqualTo(NOT_LIKE.getMessage());
+ }
+
+ @Test
+ @DisplayName("자신의 댓글 좋아요 시도 시 예외 발생")
+ void questionCommentLike_MyComment_ThrowsException() {
+ // given
+ User user = createTestUser(1, "테스트 사용자", Role.USER);
+ CommentLikeRequest request = new CommentLikeRequest(1);
+ QuestionComment comment = createTestQuestionComment(1, "테스트 댓글", user);
+
+ when(questionCommentJpaRepository.findByIdAndState(request.idx(), ACTIVE))
+ .thenReturn(Optional.of(comment));
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class,
+ () -> questionCommentService.questionCommentLike(user, request));
+ assertThat(exception.getErrorReason().getMessage()).isEqualTo(MY_COMMENT_LIKE.getMessage());
+ }
+
+ @Test
+ @DisplayName("질문 대댓글 좋아요 성공")
+ void questionReplyCommentLike_Success() {
+ // given
+ User user = createTestUser(1, "테스트 사용자", Role.USER);
+ CommentLikeRequest request = new CommentLikeRequest(1);
+ QuestionReplyComment replyComment = createTestReplyComment(1, "대댓글 내용", createTestUser(2, "작성자", Role.USER));
+
+ when(questionReplyCommentJpaRepository.findByIdAndState(request.idx(), ACTIVE))
+ .thenReturn(Optional.of(replyComment));
+ when(questionReplyCommentLikeJpaRepository.existsByUserAndQuestionReplyComment(user, replyComment))
+ .thenReturn(false);
+
+ // when
+ String result = questionCommentService.questionReplyCommentLike(user, request);
+
+ // then
+ assertThat(result).isEqualTo("1번 질문 대댓글 좋아요 완료");
+ assertThat(replyComment.getLikeCount()).isEqualTo(1);
+ verify(questionReplyCommentLikeJpaRepository).save(any());
+ }
+
+ @Test
+ @DisplayName("이미 좋아요한 대댓글에 다시 좋아요 시도 시 예외 발생")
+ void questionReplyCommentLike_AlreadyLiked_ThrowsException() {
+ // given
+ User user = createTestUser(1, "테스트 사용자", Role.USER);
+ CommentLikeRequest request = new CommentLikeRequest(1);
+ QuestionReplyComment replyComment = createTestReplyComment(1, "대댓글 내용", createTestUser(2, "작성자", Role.USER));
+
+ when(questionReplyCommentJpaRepository.findByIdAndState(request.idx(), ACTIVE))
+ .thenReturn(Optional.of(replyComment));
+ when(questionReplyCommentLikeJpaRepository.existsByUserAndQuestionReplyComment(user, replyComment))
+ .thenReturn(true);
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class,
+ () -> questionCommentService.questionReplyCommentLike(user, request));
+ assertThat(exception.getErrorReason().getMessage()).isEqualTo(ALREADY_LIKE.getMessage());
+ }
+
+ @Test
+ @DisplayName("질문 대댓글 좋아요 취소 성공")
+ void questionReplyCommentLikeCancel_Success() {
+ // given
+ User user = createTestUser(1, "테스트 사용자", Role.USER);
+ CommentLikeRequest request = new CommentLikeRequest(1);
+ QuestionReplyComment replyComment = createTestReplyComment(1, "대댓글 내용", createTestUser(2, "작성자", Role.USER));
+ replyComment.setLikeCount(1);
+
+ when(questionReplyCommentJpaRepository.findByIdAndState(request.idx(), ACTIVE))
+ .thenReturn(Optional.of(replyComment));
+ when(questionReplyCommentLikeJpaRepository.existsByUserAndQuestionReplyComment(user, replyComment))
+ .thenReturn(true);
+
+ // when
+ String result = questionCommentService.questionReplyCommentLikeCancel(user, request);
+
+ // then
+ assertThat(result).isEqualTo("1번 질문 대댓글 좋아요 취소 완료");
+ assertThat(replyComment.getLikeCount()).isEqualTo(0);
+ verify(questionReplyCommentLikeJpaRepository).deleteByUserAndQuestionReplyComment(user, replyComment);
+ }
+
+ @Test
+ @DisplayName("좋아요하지 않은 대댓글 좋아요 취소 시도 시 예외 발생")
+ void questionReplyCommentLikeCancel_NotLiked_ThrowsException() {
+ // given
+ User user = createTestUser(1, "테스트 사용자", Role.USER);
+ CommentLikeRequest request = new CommentLikeRequest(1);
+ QuestionReplyComment replyComment = createTestReplyComment(1, "대댓글 내용", createTestUser(2, "작성자", Role.USER));
+
+ when(questionReplyCommentJpaRepository.findByIdAndState(request.idx(), ACTIVE))
+ .thenReturn(Optional.of(replyComment));
+ when(questionReplyCommentLikeJpaRepository.existsByUserAndQuestionReplyComment(user, replyComment))
+ .thenReturn(false);
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class,
+ () -> questionCommentService.questionReplyCommentLikeCancel(user, request));
+ assertThat(exception.getErrorReason().getMessage()).isEqualTo(NOT_LIKE.getMessage());
+ }
+
+ private User createTestUser(Integer id, String name, Role role) {
+ return User.builder()
+ .id(id)
+ .name(name)
+ .role(role)
+ .build();
+ }
+
+ private Question createTestQuestion(Integer id, String title, String contents, User user) {
+ return Question.builder()
+ .id(id)
+ .title(title)
+ .contents(contents)
+ .user(user)
+ .commentCount(0)
+ .build();
+ }
+
+ private QuestionComment createTestComment(Integer commentId, User user, Question question) {
+ return QuestionComment.builder()
+ .id(commentId)
+ .user(user)
+ .question(question)
+ .contents("댓글 내용")
+ .likeCount(2)
+ .replies(new ArrayList<>())
+ .build();
+
+ }
+
+ private QuestionComment createTestQuestionComment(Integer id, String contents, User user) {
+ return QuestionComment.builder()
+ .id(id)
+ .contents(contents)
+ .user(user)
+ .likeCount(0)
+ .build();
+ }
+
+ private QuestionReplyComment createTestReply(Integer replyId, User user, QuestionComment parentComment) {
+ return QuestionReplyComment.builder()
+ .id(replyId)
+ .user(user)
+ .questionComment(parentComment)
+ .contents("대댓글 내용")
+ .likeCount(1)
+ .build();
+ }
+
+ private QuestionReplyComment createTestQuestionReplyComment(Integer id, String contents, User user, QuestionComment comment) {
+ return QuestionReplyComment.builder()
+ .id(id)
+ .contents(contents)
+ .likeCount(0)
+ .user(user)
+ .questionComment(comment)
+ .build();
+ }
+
+ private QuestionReplyComment createTestReplyComment(Integer id, String contents) {
+ return QuestionReplyComment.builder()
+ .id(id)
+ .contents(contents)
+ .user(createTestUser(1, "테스트 사용자", Role.USER))
+ .build();
+ }
+
+
+ private QuestionReplyComment createTestReplyComment(Integer id, String contents, User user) {
+ QuestionComment parentComment = createTestComment(1, user, createTestQuestion(1, "테스트 질문", "테스트 내용", user));
+ return QuestionReplyComment.builder()
+ .id(id)
+ .contents(contents)
+ .user(user)
+ .questionComment(parentComment) // 부모 댓글 설정
+ .likeCount(0)
+ .build();
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/inha/git/question/api/service/QuestionServiceTest.java b/src/test/java/inha/git/question/api/service/QuestionServiceTest.java
new file mode 100644
index 00000000..cfdd83cf
--- /dev/null
+++ b/src/test/java/inha/git/question/api/service/QuestionServiceTest.java
@@ -0,0 +1,942 @@
+package inha.git.question.api.service;
+
+import inha.git.category.controller.dto.response.SearchCategoryResponse;
+import inha.git.category.domain.Category;
+import inha.git.category.domain.repository.CategoryJpaRepository;
+import inha.git.common.exceptions.BaseException;
+import inha.git.field.domain.Field;
+import inha.git.question.api.controller.dto.request.LikeRequest;
+import inha.git.question.api.controller.dto.request.UpdateQuestionRequest;
+import inha.git.user.domain.enums.Role;
+import inha.git.field.domain.repository.FieldJpaRepository;
+import inha.git.mapping.domain.QuestionField;
+import inha.git.mapping.domain.id.QuestionFieldId;
+import inha.git.mapping.domain.repository.QuestionFieldJpaRepository;
+import inha.git.mapping.domain.repository.QuestionLikeJpaRepository;
+import inha.git.project.api.controller.dto.response.SearchFieldResponse;
+import inha.git.project.api.controller.dto.response.SearchUserResponse;
+import inha.git.question.api.controller.dto.request.CreateQuestionRequest;
+import inha.git.question.api.controller.dto.request.SearchQuestionCond;
+import inha.git.question.api.controller.dto.response.QuestionResponse;
+import inha.git.question.api.controller.dto.response.SearchLikeState;
+import inha.git.question.api.controller.dto.response.SearchQuestionResponse;
+import inha.git.question.api.controller.dto.response.SearchQuestionsResponse;
+import inha.git.question.api.mapper.QuestionMapper;
+import inha.git.question.domain.Question;
+import inha.git.question.domain.repository.QuestionJpaRepository;
+import inha.git.question.domain.repository.QuestionQueryRepository;
+import inha.git.semester.controller.dto.response.SearchSemesterResponse;
+import inha.git.semester.domain.Semester;
+import inha.git.semester.domain.repository.SemesterJpaRepository;
+import inha.git.semester.mapper.SemesterMapper;
+import inha.git.statistics.api.service.StatisticsServiceImpl;
+import inha.git.user.domain.User;
+import inha.git.utils.IdempotentProvider;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.data.domain.*;
+
+import java.time.LocalDateTime;
+import java.util.*;
+
+import static inha.git.common.BaseEntity.State.ACTIVE;
+import static inha.git.common.Constant.CREATE_AT;
+import static inha.git.common.Constant.CURRICULUM;
+import static inha.git.common.code.status.ErrorStatus.*;
+import static org.assertj.core.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.*;
+
+@DisplayName("질문 서비스 테스트")
+@ExtendWith(MockitoExtension.class)
+class QuestionServiceTest {
+
+ @InjectMocks
+ private QuestionServiceImpl questionService;
+
+ @Mock
+ private QuestionQueryRepository questionQueryRepository;
+
+ @Mock
+ private QuestionJpaRepository questionJpaRepository;
+
+ @Mock
+ private QuestionMapper questionMapper;
+
+ @Mock
+ private QuestionLikeJpaRepository questionLikeJpaRepository;
+
+ @Mock
+ private QuestionFieldJpaRepository questionFieldJpaRepository;
+
+ @Mock
+ private FieldJpaRepository fieldJpaRepository;
+
+ @Mock
+ private SemesterJpaRepository semesterJpaRepository;
+
+ @Mock
+ private CategoryJpaRepository categoryJpaRepository;
+
+ @Mock
+ private StatisticsServiceImpl statisticsService;
+
+ @Mock
+ private IdempotentProvider idempotentProvider;
+
+ @Mock
+ private SemesterMapper semesterMapper;
+
+ @Test
+ @DisplayName("질문 페이징 조회 성공")
+ void getQuestions_Success() {
+ // given
+ int page = 0;
+ int size = 10;
+ Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, CREATE_AT));
+
+ List questions = Arrays.asList(
+ new SearchQuestionsResponse(
+ 1,
+ "질문1",
+ LocalDateTime.now(),
+ "과목1",
+ new SearchSemesterResponse(1, "학기1"),
+ new SearchCategoryResponse(1, "카테고리1"),
+ 0,
+ 0,
+ List.of(new SearchFieldResponse(1, "분야1")),
+ new SearchUserResponse(1, "작성자1", 1)
+ ),
+ new SearchQuestionsResponse(
+ 2,
+ "질문2",
+ LocalDateTime.now(),
+ "과목2",
+ new SearchSemesterResponse(2, "학기2"),
+ new SearchCategoryResponse(2, "카테고리2"),
+ 1,
+ 2,
+ List.of(new SearchFieldResponse(2, "분야2")),
+ new SearchUserResponse(2, "작성자2", 1)
+ )
+ );
+
+ Page expectedPage = new PageImpl<>(questions);
+
+ given(questionQueryRepository.getQuestions(pageable))
+ .willReturn(expectedPage);
+
+ // when
+ Page result = questionService.getQuestions(page, size);
+
+ // then
+ assertThat(result).isEqualTo(expectedPage);
+ verify(questionQueryRepository).getQuestions(pageable);
+ }
+
+ @Test
+ @DisplayName("조건 검색 - 모든 조건이 있는 경우")
+ void getCondQuestions_WithAllConditions_Success() {
+ // given
+ int page = 0;
+ int size = 10;
+ SearchQuestionCond searchQuestionCond = new SearchQuestionCond(
+ 1, // collegeIdx
+ 1, // departmentIdx
+ 1, // semesterIdx
+ 1, // categoryIdx
+ 1, // fieldIdx
+ "알고리즘", // subject
+ "정렬" // title
+ );
+
+ Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, CREATE_AT));
+ List questions = Arrays.asList(
+ new SearchQuestionsResponse(
+ 1,
+ "정렬 알고리즘 질문",
+ LocalDateTime.now(),
+ "알고리즘",
+ new SearchSemesterResponse(1, "2024-1"),
+ new SearchCategoryResponse(1, "CS"),
+ 0,
+ 0,
+ List.of(new SearchFieldResponse(1, "알고리즘")),
+ new SearchUserResponse(1, "작성자1",1)
+ )
+ );
+ Page expectedPage = new PageImpl<>(questions);
+
+ given(questionQueryRepository.getCondQuestions(searchQuestionCond, pageable))
+ .willReturn(expectedPage);
+
+ // when
+ Page result = questionService.getCondQuestions(
+ searchQuestionCond, page, size);
+
+ // then
+ assertThat(result).isEqualTo(expectedPage);
+ verify(questionQueryRepository).getCondQuestions(searchQuestionCond, pageable);
+ }
+
+ @Test
+ @DisplayName("조건 검색 - 일부 조건만 있는 경우")
+ void getCondQuestions_WithPartialConditions_Success() {
+ // given
+ int page = 0;
+ int size = 10;
+ SearchQuestionCond searchQuestionCond = new SearchQuestionCond(
+ null, // collegeIdx
+ null, // departmentIdx
+ 1, // semesterIdx
+ null, // categoryIdx
+ null, // fieldIdx
+ "알고리즘", // subject
+ null // title
+ );
+
+ Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, CREATE_AT));
+ List questions = Arrays.asList(
+ new SearchQuestionsResponse(
+ 1,
+ "알고리즘 질문1",
+ LocalDateTime.now(),
+ "알고리즘",
+ new SearchSemesterResponse(1, "2024-1"),
+ new SearchCategoryResponse(1, "CS"),
+ 0,
+ 0,
+ List.of(new SearchFieldResponse(1, "알고리즘")),
+ new SearchUserResponse(1, "작성자1", 1)
+ ),
+ new SearchQuestionsResponse(
+ 2,
+ "알고리즘 질문2",
+ LocalDateTime.now(),
+ "알고리즘",
+ new SearchSemesterResponse(1, "2024-1"),
+ new SearchCategoryResponse(2, "AI"),
+ 0,
+ 0,
+ List.of(new SearchFieldResponse(2, "머신러닝")),
+ new SearchUserResponse(2, "작성자2", 1)
+ )
+ );
+ Page expectedPage = new PageImpl<>(questions);
+
+ given(questionQueryRepository.getCondQuestions(searchQuestionCond, pageable))
+ .willReturn(expectedPage);
+
+ // when
+ Page result = questionService.getCondQuestions(
+ searchQuestionCond, page, size);
+
+ // then
+ assertThat(result).isEqualTo(expectedPage);
+ verify(questionQueryRepository).getCondQuestions(searchQuestionCond, pageable);
+ }
+
+ @Test
+ @DisplayName("조건 검색 - 검색 결과가 없는 경우")
+ void getCondQuestions_NoResults_Success() {
+ // given
+ int page = 0;
+ int size = 10;
+ SearchQuestionCond searchQuestionCond = new SearchQuestionCond(
+ 1, 1, 1, 1, 1, "존재하지않는과목", "존재하지않는제목"
+ );
+
+ Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, CREATE_AT));
+ Page expectedPage = new PageImpl<>(Collections.emptyList());
+
+ given(questionQueryRepository.getCondQuestions(searchQuestionCond, pageable))
+ .willReturn(expectedPage);
+
+ // when
+ Page result = questionService.getCondQuestions(
+ searchQuestionCond, page, size);
+
+ // then
+ assertThat(result).isEqualTo(expectedPage);
+ assertThat(result.getContent()).isEmpty();
+ verify(questionQueryRepository).getCondQuestions(searchQuestionCond, pageable);
+ }
+
+ @Test
+ @DisplayName("질문 상세 조회 성공")
+ void getQuestion_Success() {
+ // given
+ User user = createTestUser(1, "테스트유저", Role.USER);
+ Question question = createTestQuestion(1, "질문 제목", "질문 내용", user);
+ Semester semester = createTestSemester(1, "2024-1");
+ question.setSemester(semester);
+
+ // 각 매퍼의 반환값 설정
+ SearchSemesterResponse semesterResponse = new SearchSemesterResponse(1, "2024-1");
+ SearchUserResponse userResponse = new SearchUserResponse(1, "테스트유저", 1);
+ SearchLikeState likeState = new SearchLikeState(false);
+
+ Field field1 = createTestField(1, "알고리즘");
+ Field field2 = createTestField(2, "자료구조");
+ List questionFields = List.of(
+ createTestQuestionField(1, question, field1),
+ createTestQuestionField(2, question, field2)
+ );
+
+ List fieldResponses = List.of(
+ new SearchFieldResponse(1, "알고리즘"),
+ new SearchFieldResponse(2, "자료구조")
+ );
+
+ SearchQuestionResponse expectedResponse = new SearchQuestionResponse(
+ 1,
+ "질문 제목",
+ "질문 내용",
+ question.getCreatedAt(),
+ likeState,
+ 0,
+ "알고리즘",
+ fieldResponses,
+ userResponse,
+ semesterResponse
+ );
+
+ // 각 메서드 호출에 대한 동작 설정
+ when(questionJpaRepository.findByIdAndState(1, ACTIVE))
+ .thenReturn(Optional.of(question));
+
+ when(semesterMapper.semesterToSearchSemesterResponse(question.getSemester()))
+ .thenReturn(semesterResponse);
+
+ when(questionMapper.userToSearchUserResponse(question.getUser()))
+ .thenReturn(userResponse);
+
+ when(questionLikeJpaRepository.existsByUserAndQuestion(user, question))
+ .thenReturn(false);
+
+ when(questionMapper.questionToSearchLikeState(false))
+ .thenReturn(likeState);
+
+ when(questionFieldJpaRepository.findByQuestion(question))
+ .thenReturn(questionFields);
+
+ when(questionMapper.projectFieldToSearchFieldResponse(field1))
+ .thenReturn(new SearchFieldResponse(1, "알고리즘"));
+ when(questionMapper.projectFieldToSearchFieldResponse(field2))
+ .thenReturn(new SearchFieldResponse(2, "자료구조"));
+
+ when(questionMapper.questionToSearchQuestionResponse(
+ eq(question),
+ eq(fieldResponses),
+ eq(userResponse),
+ eq(semesterResponse),
+ eq(likeState)))
+ .thenReturn(expectedResponse);
+
+ // when
+ SearchQuestionResponse response = questionService.getQuestion(user, 1);
+
+ // then
+ assertThat(response)
+ .isNotNull()
+ .isEqualTo(expectedResponse);
+
+ // 모든 메서드 호출 검증
+ verify(questionJpaRepository).findByIdAndState(1, ACTIVE);
+ verify(semesterMapper).semesterToSearchSemesterResponse(question.getSemester());
+ verify(questionMapper).userToSearchUserResponse(question.getUser());
+ verify(questionLikeJpaRepository).existsByUserAndQuestion(user, question);
+ verify(questionMapper).questionToSearchLikeState(false);
+ verify(questionFieldJpaRepository).findByQuestion(question);
+ verify(questionMapper).questionToSearchQuestionResponse(
+ eq(question),
+ eq(fieldResponses),
+ eq(userResponse),
+ eq(semesterResponse),
+ eq(likeState)
+ );
+ }
+
+ @Test
+ @DisplayName("존재하지 않는 질문 조회 시 예외 발생")
+ void getQuestion_NotFound_ThrowsException() {
+ // given
+ User user = createTestUser(1, "테스트유저", Role.USER);
+ Integer nonExistentQuestionId = 999;
+
+ given(questionJpaRepository.findByIdAndState(nonExistentQuestionId, ACTIVE))
+ .willReturn(Optional.empty());
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class, () ->
+ questionService.getQuestion(user, nonExistentQuestionId));
+
+ assertThat(exception.getErrorReason().getMessage())
+ .isEqualTo(QUESTION_NOT_FOUND.getMessage());
+ }
+
+ @Test
+ @DisplayName("질문 생성 성공")
+ void createQuestion_Success() {
+ // given
+ User user = createTestUser(1, "테스트유저", Role.USER);
+ CreateQuestionRequest request = new CreateQuestionRequest(
+ "질문 제목",
+ "질문 내용",
+ "알고리즘",
+ List.of(1),
+ 1
+ );
+
+ Semester semester = createTestSemester(1, "2024-1");
+ Category category = createTestCategory(1, "커리큘럼");
+ Question question = createTestQuestion(1, "질문 제목", "질문 내용", user);
+ Field field = createTestField(1, "알고리즘");
+ QuestionField questionField = createTestQuestionField(1, question, field);
+
+ // 학기, 카테고리 조회
+ when(semesterJpaRepository.findByIdAndState(1, ACTIVE))
+ .thenReturn(Optional.of(semester));
+ when(categoryJpaRepository.findByNameAndState(CURRICULUM, ACTIVE))
+ .thenReturn(Optional.of(category));
+
+ // 질문 생성
+ when(questionMapper.createQuestionRequestToQuestion(request, user, semester, category))
+ .thenReturn(question);
+ when(questionJpaRepository.save(question))
+ .thenReturn(question);
+
+ // 필드 관련 처리
+ when(fieldJpaRepository.findAllById(List.of(1)))
+ .thenReturn(List.of(field));
+ when(fieldJpaRepository.findByIdAndState(1, ACTIVE))
+ .thenReturn(Optional.of(field));
+ when(questionMapper.createQuestionField(any(Question.class), any(Field.class)))
+ .thenReturn(questionField);
+
+ // 응답 변환
+ when(questionMapper.questionToQuestionResponse(question))
+ .thenReturn(new QuestionResponse(1));
+
+ // when
+ QuestionResponse response = questionService.createQuestion(user, request);
+
+ // then
+ assertThat(response).isNotNull();
+ assertThat(response.idx()).isEqualTo(1);
+
+ verify(questionJpaRepository).save(any(Question.class));
+ verify(questionFieldJpaRepository).saveAll(anyList());
+ verify(statisticsService).increaseCount(eq(user), anyList(), eq(semester), eq(category), eq(2));
+ verify(fieldJpaRepository).findByIdAndState(eq(1), eq(ACTIVE));
+ }
+
+ @Test
+ @DisplayName("존재하지 않는 학기로 질문 생성 시 예외 발생")
+ void createQuestion_WithInvalidSemester_ThrowsException() {
+ // given
+ User user = createTestUser(1, "테스트유저", Role.USER);
+ CreateQuestionRequest request = new CreateQuestionRequest(
+ "질문 제목",
+ "질문 내용",
+ "알고리즘",
+ List.of(1),
+ 999 // 존재하지 않는 학기 ID
+ );
+
+ when(semesterJpaRepository.findByIdAndState(999, ACTIVE))
+ .thenReturn(Optional.empty());
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class,
+ () -> questionService.createQuestion(user, request));
+
+ assertThat(exception.getErrorReason().getMessage())
+ .isEqualTo(SEMESTER_NOT_FOUND.getMessage());
+ }
+
+ @Test
+ @DisplayName("카테고리를 찾을 수 없을 때 예외 발생")
+ void createQuestion_CategoryNotFound_ThrowsException() {
+ // given
+ User user = createTestUser(1, "테스트유저", Role.USER);
+ CreateQuestionRequest request = new CreateQuestionRequest(
+ "질문 제목",
+ "질문 내용",
+ "알고리즘",
+ List.of(1),
+ 1
+ );
+
+ Semester semester = createTestSemester(1, "2024-1");
+
+ when(semesterJpaRepository.findByIdAndState(1, ACTIVE))
+ .thenReturn(Optional.of(semester));
+ when(categoryJpaRepository.findByNameAndState(CURRICULUM, ACTIVE))
+ .thenReturn(Optional.empty());
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class,
+ () -> questionService.createQuestion(user, request));
+
+ assertThat(exception.getErrorReason().getMessage())
+ .isEqualTo(CATEGORY_NOT_FOUND.getMessage());
+ }
+
+ @Test
+ @DisplayName("질문 수정 성공")
+ void updateQuestion_Success() {
+ // given
+ User user = createTestUser(1, "테스트유저", Role.USER);
+ Question originalQuestion = createTestQuestion(1, "원본 제목", "원본 내용", user);
+
+ Semester originalSemester = createTestSemester(1, "2024-1");
+ Semester newSemester = createTestSemester(2, "2024-2");
+ Category category = createTestCategory(1, "커리큘럼");
+
+ Field originalField = createTestField(1, "알고리즘");
+ Field newField = createTestField(2, "자료구조");
+ QuestionField originalQuestionField = createTestQuestionField(1, originalQuestion, originalField);
+
+ originalQuestion.setSemester(originalSemester);
+ originalQuestion.setCategory(category);
+ originalQuestion.setQuestionFields(new ArrayList<>(List.of(originalQuestionField)));
+
+ UpdateQuestionRequest request = new UpdateQuestionRequest(
+ "수정된 제목",
+ "수정된 내용",
+ "수정된 주제",
+ List.of(2), // 새로운 필드 ID
+ 2 // 새로운 학기 ID
+ );
+
+ // mocking
+ when(questionJpaRepository.findByIdAndState(1, ACTIVE))
+ .thenReturn(Optional.of(originalQuestion));
+ when(semesterJpaRepository.findByIdAndState(2, ACTIVE))
+ .thenReturn(Optional.of(newSemester));
+ when(fieldJpaRepository.findAllById(List.of(2)))
+ .thenReturn(List.of(newField));
+ when(fieldJpaRepository.findById(2))
+ .thenReturn(Optional.of(newField));
+ when(questionJpaRepository.save(any(Question.class)))
+ .thenReturn(originalQuestion);
+ when(questionMapper.questionToQuestionResponse(any(Question.class)))
+ .thenReturn(new QuestionResponse(1));
+
+ // when
+ QuestionResponse response = questionService.updateQuestion(user, 1, request);
+
+ // then
+ assertThat(response).isNotNull();
+ assertThat(response.idx()).isEqualTo(1);
+
+ verify(questionJpaRepository).save(any(Question.class));
+ verify(statisticsService).decreaseCount(eq(user), anyList(), eq(originalSemester), eq(category), eq(2));
+ verify(statisticsService).increaseCount(eq(user), anyList(), eq(newSemester), eq(category), eq(2));
+ }
+
+ @Test
+ @DisplayName("존재하지 않는 질문 수정 시도시 예외 발생")
+ void updateQuestion_QuestionNotFound_ThrowsException() {
+ // given
+ User user = createTestUser(1, "테스트유저", Role.USER);
+ UpdateQuestionRequest request = new UpdateQuestionRequest(
+ "수정된 제목",
+ "수정된 내용",
+ "수정된 주제",
+ List.of(1),
+ 1
+ );
+
+ when(questionJpaRepository.findByIdAndState(999, ACTIVE))
+ .thenReturn(Optional.empty());
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class,
+ () -> questionService.updateQuestion(user, 999, request));
+
+ assertThat(exception.getErrorReason().getMessage())
+ .isEqualTo(QUESTION_NOT_FOUND.getMessage());
+ }
+
+ @Test
+ @DisplayName("권한 없는 사용자의 질문 수정 시도시 예외 발생")
+ void updateQuestion_Unauthorized_ThrowsException() {
+ // given
+ User originalAuthor = createTestUser(1, "작성자", Role.USER);
+ User unauthorizedUser = createTestUser(2, "다른사용자", Role.USER);
+ Question question = createTestQuestion(1, "원본 제목", "원본 내용", originalAuthor);
+
+ UpdateQuestionRequest request = new UpdateQuestionRequest(
+ "수정된 제목",
+ "수정된 내용",
+ "수정된 주제",
+ List.of(1),
+ 1
+ );
+
+ when(questionJpaRepository.findByIdAndState(1, ACTIVE))
+ .thenReturn(Optional.of(question));
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class,
+ () -> questionService.updateQuestion(unauthorizedUser, 1, request));
+
+ assertThat(exception.getErrorReason().getMessage())
+ .isEqualTo(QUESTION_NOT_AUTHORIZED.getMessage());
+ }
+
+ @Test
+ @DisplayName("존재하지 않는 학기로 수정 시도시 예외 발생")
+ void updateQuestion_SemesterNotFound_ThrowsException() {
+ // given
+ User user = createTestUser(1, "테스트유저", Role.USER);
+ Question question = createTestQuestion(1, "원본 제목", "원본 내용", user);
+
+ UpdateQuestionRequest request = new UpdateQuestionRequest(
+ "수정된 제목",
+ "수정된 내용",
+ "수정된 주제",
+ List.of(1),
+ 999 // 존재하지 않는 학기 ID
+ );
+
+ when(questionJpaRepository.findByIdAndState(1, ACTIVE))
+ .thenReturn(Optional.of(question));
+ when(semesterJpaRepository.findByIdAndState(999, ACTIVE))
+ .thenReturn(Optional.empty());
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class,
+ () -> questionService.updateQuestion(user, 1, request));
+
+ assertThat(exception.getErrorReason().getMessage())
+ .isEqualTo(SEMESTER_NOT_FOUND.getMessage());
+ }
+
+ @Test
+ @DisplayName("존재하지 않는 필드로 수정 시도시 예외 발생")
+ void updateQuestion_FieldNotFound_ThrowsException() {
+ // given
+ User user = createTestUser(1, "테스트유저", Role.USER);
+ Question question = createTestQuestion(1, "원본 제목", "원본 내용", user);
+ Semester semester = createTestSemester(1, "2024-1");
+
+ UpdateQuestionRequest request = new UpdateQuestionRequest(
+ "수정된 제목",
+ "수정된 내용",
+ "수정된 주제",
+ List.of(999), // 존재하지 않는 필드 ID
+ 1
+ );
+
+ when(questionJpaRepository.findByIdAndState(1, ACTIVE))
+ .thenReturn(Optional.of(question));
+ when(semesterJpaRepository.findByIdAndState(1, ACTIVE))
+ .thenReturn(Optional.of(semester));
+ when(fieldJpaRepository.findById(999))
+ .thenReturn(Optional.empty());
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class,
+ () -> questionService.updateQuestion(user, 1, request));
+
+ assertThat(exception.getErrorReason().getMessage())
+ .isEqualTo(FIELD_NOT_FOUND.getMessage());
+ }
+
+ @Test
+ @DisplayName("질문 삭제 성공")
+ void deleteQuestion_Success() {
+ // given
+ User user = createTestUser(1, "테스트유저", Role.USER);
+ Question question = createTestQuestion(1, "질문 제목", "질문 내용", user);
+
+ when(questionJpaRepository.findByIdAndState(1, ACTIVE))
+ .thenReturn(Optional.of(question));
+ when(questionMapper.questionToQuestionResponse(question))
+ .thenReturn(new QuestionResponse(1));
+
+ // when
+ QuestionResponse response = questionService.deleteQuestion(user, 1);
+
+ // then
+ assertThat(response).isNotNull();
+ assertThat(response.idx()).isEqualTo(1);
+
+ verify(questionJpaRepository).findByIdAndState(1, ACTIVE);
+ verify(statisticsService).decreaseCount(eq(user), anyList(), eq(question.getSemester()), eq(question.getCategory()), eq(2));
+ verify(questionJpaRepository).save(question);
+ }
+
+ @Test
+ @DisplayName("존재하지 않는 질문 삭제 시도시 예외 발생")
+ void deleteQuestion_QuestionNotFound_ThrowsException() {
+ // given
+ User user = createTestUser(1, "테스트유저", Role.USER);
+
+ when(questionJpaRepository.findByIdAndState(999, ACTIVE))
+ .thenReturn(Optional.empty());
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class,
+ () -> questionService.deleteQuestion(user, 999));
+
+ assertThat(exception.getErrorReason().getMessage()).isEqualTo(QUESTION_NOT_FOUND.getMessage());
+ }
+
+ @Test
+ @DisplayName("권한 없는 사용자가 질문 삭제 시도시 예외 발생")
+ void deleteQuestion_UnauthorizedUser_ThrowsException() {
+ // given
+ User originalAuthor = createTestUser(1, "작성자", Role.USER);
+ User unauthorizedUser = createTestUser(2, "다른 사용자", Role.USER);
+ Question question = createTestQuestion(1, "질문 제목", "질문 내용", originalAuthor);
+
+ when(questionJpaRepository.findByIdAndState(1, ACTIVE))
+ .thenReturn(Optional.of(question));
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class,
+ () -> questionService.deleteQuestion(unauthorizedUser, 1));
+
+ assertThat(exception.getErrorReason().getMessage()).isEqualTo(QUESTION_DELETE_NOT_AUTHORIZED.getMessage());
+ }
+
+
+
+ @Test
+ @DisplayName("질문 좋아요 성공")
+ void createQuestionLike_Success() {
+ // given
+ User user = createTestUser(1, "사용자", Role.USER);
+ LikeRequest likeRequest = new LikeRequest(100);
+ Question question = createTestQuestion(100, 999, 0);
+
+ when(questionJpaRepository.findByIdAndState(100, ACTIVE))
+ .thenReturn(Optional.of(question));
+ when(questionLikeJpaRepository.existsByUserAndQuestion(user, question))
+ .thenReturn(false);
+
+ when(questionMapper.createQuestionLike(user, question))
+ .thenReturn(null);
+
+ // when
+ String result = questionService.createQuestionLike(user, likeRequest);
+
+ // then
+ assertThat(result).isEqualTo("100번 질문 좋아요 완료");
+ assertThat(question.getLikeCount()).isEqualTo(1);
+ verify(questionLikeJpaRepository).save(any());
+ }
+
+ @Test
+ @DisplayName("질문 좋아요 - 내 질문이면 예외 발생")
+ void createQuestionLike_MyQuestion_ThrowsException() {
+ // given
+ User user = createTestUser(1, "내질문", Role.USER);
+ LikeRequest likeRequest = new LikeRequest(200);
+ Question myQuestion = createTestQuestion(200, 1, 0);
+
+ when(questionJpaRepository.findByIdAndState(200, ACTIVE))
+ .thenReturn(Optional.of(myQuestion));
+
+ // when & then
+ BaseException ex = assertThrows(BaseException.class,
+ () -> questionService.createQuestionLike(user, likeRequest));
+
+ assertThat(ex.getErrorReason().getMessage()).isEqualTo(MY_QUESTION_LIKE.getMessage());
+ verify(questionLikeJpaRepository, never()).save(any());
+ }
+
+ @Test
+ @DisplayName("질문 좋아요 - 이미 좋아요한 질문 예외 발생")
+ void createQuestionLike_AlreadyLiked_ThrowsException() {
+ // given
+ User user = createTestUser(1, "사용자", Role.USER);
+ LikeRequest likeRequest = new LikeRequest(300);
+ Question question = createTestQuestion(300, 999, 10);
+
+ when(questionJpaRepository.findByIdAndState(300, ACTIVE))
+ .thenReturn(Optional.of(question));
+ // 이미 좋아요 했다고 Mock
+ when(questionLikeJpaRepository.existsByUserAndQuestion(user, question))
+ .thenReturn(true);
+
+ // when & then
+ BaseException ex = assertThrows(BaseException.class,
+ () -> questionService.createQuestionLike(user, likeRequest));
+
+ assertThat(ex.getErrorReason().getMessage()).isEqualTo(QUESTION_ALREADY_LIKE.getMessage());
+ verify(questionLikeJpaRepository, never()).save(any());
+ }
+
+ @Test
+ @DisplayName("질문 좋아요 - 질문이 없음 예외 발생")
+ void createQuestionLike_QuestionNotFound_ThrowsException() {
+ // given
+ User user = createTestUser(1, "사용자", Role.USER);
+ LikeRequest likeRequest = new LikeRequest(999);
+
+ when(questionJpaRepository.findByIdAndState(999, ACTIVE))
+ .thenReturn(Optional.empty());
+
+ // when & then
+ BaseException ex = assertThrows(BaseException.class,
+ () -> questionService.createQuestionLike(user, likeRequest));
+
+ assertThat(ex.getErrorReason().getMessage()).isEqualTo(QUESTION_NOT_FOUND.getMessage());
+ verify(questionLikeJpaRepository, never()).save(any());
+ }
+
+ @Test
+ @DisplayName("질문 좋아요 취소 성공")
+ void questionLikeCancel_Success() {
+ // given
+ User user = createTestUser(1, "사용자", Role.USER);
+ LikeRequest likeRequest = new LikeRequest(555);
+ Question question = createTestQuestion(555, 999, 5);
+
+ doNothing().when(idempotentProvider).isValidIdempotent(anyList());
+
+ when(questionJpaRepository.findByIdAndState(555, ACTIVE))
+ .thenReturn(Optional.of(question));
+ // 이미 좋아요 했다고 가정
+ when(questionLikeJpaRepository.existsByUserAndQuestion(user, question))
+ .thenReturn(true);
+
+ // when
+ String result = questionService.questionLikeCancel(user, likeRequest);
+
+ // then
+ assertThat(result).isEqualTo("555번 프로젝트 좋아요 취소 완료");
+ assertThat(question.getLikeCount()).isEqualTo(4); // 5 -> 4
+ verify(questionLikeJpaRepository).deleteByUserAndQuestion(user, question);
+ }
+
+ @Test
+ @DisplayName("질문 좋아요 취소 - 내 질문이면 예외 발생")
+ void questionLikeCancel_MyQuestion_ThrowsException() {
+ // given
+ User user = createTestUser(1, "사용자", Role.USER);
+ Question myQuestion = createTestQuestion(777, 1, 10);
+ LikeRequest likeRequest = new LikeRequest(777);
+
+ when(questionJpaRepository.findByIdAndState(777, ACTIVE))
+ .thenReturn(Optional.of(myQuestion));
+
+ // when & then
+ BaseException ex = assertThrows(BaseException.class,
+ () -> questionService.questionLikeCancel(user, likeRequest));
+
+ assertThat(ex.getErrorReason().getMessage()).isEqualTo(MY_QUESTION_LIKE.getMessage());
+ verify(questionLikeJpaRepository, never()).deleteByUserAndQuestion(any(), any());
+ }
+
+ @Test
+ @DisplayName("질문 좋아요 취소 - 좋아요한 적 없는 경우 예외 발생")
+ void questionLikeCancel_NotLiked_ThrowsException() {
+ // given
+ User user = createTestUser(1, "사용자", Role.USER);
+ Question question = createTestQuestion(888, 999, 3);
+ LikeRequest likeRequest = new LikeRequest(888);
+
+ when(questionJpaRepository.findByIdAndState(888, ACTIVE))
+ .thenReturn(Optional.of(question));
+ // 좋아요하지 않은 상태
+ when(questionLikeJpaRepository.existsByUserAndQuestion(user, question))
+ .thenReturn(false);
+
+ // when & then
+ BaseException ex = assertThrows(BaseException.class,
+ () -> questionService.questionLikeCancel(user, likeRequest));
+
+ assertThat(ex.getErrorReason().getMessage()).isEqualTo(QUESTION_NOT_LIKE.getMessage());
+ verify(questionLikeJpaRepository, never()).deleteByUserAndQuestion(any(), any());
+ }
+
+ @Test
+ @DisplayName("질문 좋아요 취소 - 질문이 없음 예외 발생")
+ void questionLikeCancel_QuestionNotFound_ThrowsException() {
+ // given
+ User user = createTestUser(1, "사용자", Role.USER);
+ LikeRequest likeRequest = new LikeRequest(999);
+
+ when(questionJpaRepository.findByIdAndState(999, ACTIVE))
+ .thenReturn(Optional.empty());
+
+ // when & then
+ BaseException ex = assertThrows(BaseException.class,
+ () -> questionService.questionLikeCancel(user, likeRequest));
+
+ assertThat(ex.getErrorReason().getMessage()).isEqualTo(QUESTION_NOT_FOUND.getMessage());
+ verify(questionLikeJpaRepository, never()).deleteByUserAndQuestion(any(), any());
+ }
+
+
+ private Question createTestQuestion(Integer questionId, Integer authorId, int likeCount) {
+ return Question.builder()
+ .id(questionId)
+ .user(User.builder().id(authorId).build()) // question의 작성자
+ .likeCount(likeCount)
+ .build();
+ }
+ private User createTestUser(Integer userId, String name, Role role) {
+ return User.builder()
+ .id(userId)
+ .name(name)
+ .role(role)
+ .build();
+ }
+
+ private Question createTestQuestion(Integer id, String title, String contents, User user) {
+ Question question = Question.builder()
+ .id(id)
+ .title(title)
+ .contents(contents)
+ .user(user)
+ .subjectName("알고리즘")
+ .build();
+ question.setQuestionFields(new ArrayList<>());
+ return question;
+ }
+
+ private Semester createTestSemester(Integer id, String name) {
+ return Semester.builder()
+ .id(id)
+ .name(name)
+ .build();
+ }
+
+ private Field createTestField(Integer id, String name) {
+ return Field.builder()
+ .id(id)
+ .name(name)
+ .build();
+ }
+
+ private QuestionField createTestQuestionField(Integer id, Question question, Field field) {
+ return QuestionField.builder()
+ .id(new QuestionFieldId(question.getId(), field.getId()))
+ .question(question)
+ .field(field)
+ .build();
+ }
+
+ private Category createTestCategory(Integer id, String name) {
+ return Category.builder()
+ .id(id)
+ .name(name)
+ .build();
+ }
+
+
+}
\ No newline at end of file
diff --git a/src/test/java/inha/git/semester/api/controller/SemesterControllerTest.java b/src/test/java/inha/git/semester/api/controller/SemesterControllerTest.java
new file mode 100644
index 00000000..de3ed548
--- /dev/null
+++ b/src/test/java/inha/git/semester/api/controller/SemesterControllerTest.java
@@ -0,0 +1,121 @@
+package inha.git.semester.api.controller;
+
+import inha.git.common.BaseResponse;
+import inha.git.semester.controller.SemesterController;
+import inha.git.semester.controller.dto.request.CreateSemesterRequest;
+import inha.git.semester.controller.dto.request.UpdateSemesterRequest;
+import inha.git.semester.controller.dto.response.SearchSemesterResponse;
+import inha.git.semester.service.SemesterService;
+import inha.git.user.domain.User;
+import inha.git.user.domain.enums.Role;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.verify;
+
+@DisplayName("학기 컨트롤러 테스트")
+@ExtendWith(MockitoExtension.class)
+class SemesterControllerTest {
+
+ @InjectMocks
+ private SemesterController semesterController;
+
+ @Mock
+ private SemesterService semesterService;
+
+ @Test
+ @DisplayName("학기 전체 조회 성공")
+ void getSemesters_Success() {
+ // given
+ List expectedResponses = Arrays.asList(
+ new SearchSemesterResponse(1, "2023-1"),
+ new SearchSemesterResponse(2, "2023-2")
+ );
+
+ given(semesterService.getSemesters())
+ .willReturn(expectedResponses);
+
+ // when
+ BaseResponse> response = semesterController.getSemesters();
+
+ // then
+ assertThat(response.getResult()).isEqualTo(expectedResponses);
+ verify(semesterService).getSemesters();
+ }
+
+ @Test
+ @DisplayName("학기 생성 성공")
+ void createSemester_Success() {
+ // given
+ User admin = createAdminUser();
+ CreateSemesterRequest request = new CreateSemesterRequest("2024-1");
+ String expectedResponse = "2024-1 학기가 생성되었습니다.";
+
+ given(semesterService.createSemester(admin, request))
+ .willReturn(expectedResponse);
+
+ // when
+ BaseResponse response = semesterController.createSemester(admin, request);
+
+ // then
+ assertThat(response.getResult()).isEqualTo(expectedResponse);
+ verify(semesterService).createSemester(admin, request);
+ }
+
+ @Test
+ @DisplayName("학기명 수정 성공")
+ void updateSemester_Success() {
+ // given
+ User admin = createAdminUser();
+ Integer semesterIdx = 1;
+ UpdateSemesterRequest request = new UpdateSemesterRequest("2024-2");
+ String expectedResponse = "2024-2 학기 이름이 수정되었습니다.";
+
+ given(semesterService.updateSemesterName(admin, semesterIdx, request))
+ .willReturn(expectedResponse);
+
+ // when
+ BaseResponse response = semesterController.updateSemester(admin, semesterIdx, request);
+
+ // then
+ assertThat(response.getResult()).isEqualTo(expectedResponse);
+ verify(semesterService).updateSemesterName(admin, semesterIdx, request);
+ }
+
+ @Test
+ @DisplayName("학기 삭제 성공")
+ void deleteSemester_Success() {
+ // given
+ User admin = createAdminUser();
+ Integer semesterIdx = 1;
+ String expectedResponse = "2024-1 학기가 삭제되었습니다.";
+
+ given(semesterService.deleteSemester(admin, semesterIdx))
+ .willReturn(expectedResponse);
+
+ // when
+ BaseResponse response = semesterController.deleteSemester(admin, semesterIdx);
+
+ // then
+ assertThat(response.getResult()).isEqualTo(expectedResponse);
+ verify(semesterService).deleteSemester(admin, semesterIdx);
+ }
+
+ private User createAdminUser() {
+ return User.builder()
+ .id(1)
+ .email("admin@test.com")
+ .name("관리자")
+ .role(Role.ADMIN)
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/inha/git/semester/api/service/SemesterServiceTest.java b/src/test/java/inha/git/semester/api/service/SemesterServiceTest.java
new file mode 100644
index 00000000..df01b38d
--- /dev/null
+++ b/src/test/java/inha/git/semester/api/service/SemesterServiceTest.java
@@ -0,0 +1,186 @@
+package inha.git.semester.api.service;
+
+import inha.git.common.exceptions.BaseException;
+import inha.git.semester.controller.dto.request.CreateSemesterRequest;
+import inha.git.semester.controller.dto.request.UpdateSemesterRequest;
+import inha.git.semester.controller.dto.response.SearchSemesterResponse;
+import inha.git.semester.domain.Semester;
+import inha.git.semester.domain.repository.SemesterJpaRepository;
+import inha.git.semester.mapper.SemesterMapper;
+import inha.git.semester.service.SemesterServiceImpl;
+import inha.git.user.domain.User;
+import inha.git.user.domain.enums.Role;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.data.domain.Sort;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+import static inha.git.common.BaseEntity.State.ACTIVE;
+import static inha.git.common.BaseEntity.State.INACTIVE;
+import static inha.git.common.code.status.ErrorStatus.SEMESTER_NOT_FOUND;
+import static org.assertj.core.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.verify;
+
+@DisplayName("학기 서비스 테스트")
+@ExtendWith(MockitoExtension.class)
+class SemesterServiceTest {
+
+ @InjectMocks
+ private SemesterServiceImpl semesterService;
+
+ @Mock
+ private SemesterJpaRepository semesterJpaRepository;
+
+ @Mock
+ private SemesterMapper semesterMapper;
+
+ @Test
+ @DisplayName("학기 전체 조회 성공")
+ void getSemesters_Success() {
+ // given
+ List semesters = Arrays.asList(
+ createSemester(1, "2023-1"),
+ createSemester(2, "2023-2")
+ );
+ List expectedResponses = Arrays.asList(
+ new SearchSemesterResponse(1, "2023-1"),
+ new SearchSemesterResponse(2, "2023-2")
+ );
+
+ given(semesterJpaRepository.findAllByState(ACTIVE, Sort.by(Sort.Direction.ASC, "name")))
+ .willReturn(semesters);
+ given(semesterMapper.semestersToSearchSemesterResponses(semesters))
+ .willReturn(expectedResponses);
+
+ // when
+ List result = semesterService.getSemesters();
+
+ // then
+ assertThat(result).isEqualTo(expectedResponses);
+ verify(semesterJpaRepository).findAllByState(ACTIVE, Sort.by(Sort.Direction.ASC, "name"));
+ }
+
+ @Test
+ @DisplayName("학기 생성 성공")
+ void createSemester_Success() {
+ // given
+ User admin = createAdminUser();
+ CreateSemesterRequest request = new CreateSemesterRequest("2024-1");
+ Semester semester = createSemester(1, "2024-1");
+
+ given(semesterMapper.createSemesterRequestToSemester(request))
+ .willReturn(semester);
+ given(semesterJpaRepository.save(any(Semester.class)))
+ .willReturn(semester);
+
+ // when
+ String result = semesterService.createSemester(admin, request);
+
+ // then
+ assertThat(result).isEqualTo("2024-1 학기가 생성되었습니다.");
+ verify(semesterJpaRepository).save(any(Semester.class));
+ }
+
+ @Test
+ @DisplayName("학기명 수정 성공")
+ void updateSemesterName_Success() {
+ // given
+ User admin = createAdminUser();
+ Integer semesterIdx = 1;
+ UpdateSemesterRequest request = new UpdateSemesterRequest("2024-2");
+ Semester semester = createSemester(semesterIdx, "2024-1");
+
+ given(semesterJpaRepository.findByIdAndState(semesterIdx, ACTIVE))
+ .willReturn(Optional.of(semester));
+
+ // when
+ String result = semesterService.updateSemesterName(admin, semesterIdx, request);
+
+ // then
+ assertThat(result).isEqualTo("2024-2 학기 이름이 수정되었습니다.");
+ assertThat(semester.getName()).isEqualTo("2024-2");
+ }
+
+ @Test
+ @DisplayName("존재하지 않는 학기 수정 시 예외 발생")
+ void updateSemesterName_NotFound_ThrowsException() {
+ // given
+ User admin = createAdminUser();
+ Integer semesterIdx = 999;
+ UpdateSemesterRequest request = new UpdateSemesterRequest("2024-2");
+
+ given(semesterJpaRepository.findByIdAndState(semesterIdx, ACTIVE))
+ .willReturn(Optional.empty());
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class, () ->
+ semesterService.updateSemesterName(admin, semesterIdx, request));
+
+ assertThat(exception.getErrorReason().getMessage())
+ .isEqualTo(SEMESTER_NOT_FOUND.getMessage());
+ }
+
+ @Test
+ @DisplayName("학기 삭제 성공")
+ void deleteSemester_Success() {
+ // given
+ User admin = createAdminUser();
+ Integer semesterIdx = 1;
+ Semester semester = createSemester(semesterIdx, "2024-1");
+
+ given(semesterJpaRepository.findByIdAndState(semesterIdx, ACTIVE))
+ .willReturn(Optional.of(semester));
+
+ // when
+ String result = semesterService.deleteSemester(admin, semesterIdx);
+
+ // then
+ assertThat(result).isEqualTo("2024-1 학기가 삭제되었습니다.");
+ assertThat(semester.getState()).isEqualTo(INACTIVE);
+ assertThat(semester.getDeletedAt()).isNotNull();
+ }
+
+ @Test
+ @DisplayName("존재하지 않는 학기 삭제 시 예외 발생")
+ void deleteSemester_NotFound_ThrowsException() {
+ // given
+ User admin = createAdminUser();
+ Integer semesterIdx = 999;
+
+ given(semesterJpaRepository.findByIdAndState(semesterIdx, ACTIVE))
+ .willReturn(Optional.empty());
+
+ // when & then
+ BaseException exception = assertThrows(BaseException.class, () ->
+ semesterService.deleteSemester(admin, semesterIdx));
+
+ assertThat(exception.getErrorReason().getMessage())
+ .isEqualTo(SEMESTER_NOT_FOUND.getMessage());
+ }
+
+ private Semester createSemester(Integer id, String name) {
+ return Semester.builder()
+ .id(id)
+ .name(name)
+ .build();
+ }
+
+ private User createAdminUser() {
+ return User.builder()
+ .id(1)
+ .email("admin@test.com")
+ .name("관리자")
+ .role(Role.ADMIN)
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/inha/git/user/api/controller/UserControllerTest.java b/src/test/java/inha/git/user/api/controller/UserControllerTest.java
new file mode 100644
index 00000000..d1100928
--- /dev/null
+++ b/src/test/java/inha/git/user/api/controller/UserControllerTest.java
@@ -0,0 +1,151 @@
+package inha.git.user.api.controller;
+
+import inha.git.common.BaseResponse;
+import inha.git.user.api.controller.dto.request.CompanySignupRequest;
+import inha.git.user.api.controller.dto.request.ProfessorSignupRequest;
+import inha.git.user.api.controller.dto.request.StudentSignupRequest;
+import inha.git.user.api.controller.dto.response.CompanySignupResponse;
+import inha.git.user.api.controller.dto.response.ProfessorSignupResponse;
+import inha.git.user.api.controller.dto.response.StudentSignupResponse;
+import inha.git.user.api.service.CompanyService;
+import inha.git.user.api.service.ProfessorService;
+import inha.git.user.api.service.StudentService;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.http.MediaType;
+import org.springframework.mock.web.MockMultipartFile;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.verify;
+
+@DisplayName("사용자 컨트롤러 테스트")
+@ExtendWith(MockitoExtension.class)
+class UserControllerTest {
+
+ @InjectMocks
+ private UserController userController;
+
+ @Mock
+ private StudentService studentService;
+
+ @Mock
+ private ProfessorService professorService;
+
+ @Mock
+ private CompanyService companyService;
+
+ @Nested
+ @DisplayName("학생 회원가입 테스트")
+ class StudentSignupTest {
+
+ @Test
+ @DisplayName("학생 회원가입 성공")
+ void studentSignup_Success() {
+ // given
+ StudentSignupRequest request = createValidStudentSignupRequest();
+ StudentSignupResponse expectedResponse = new StudentSignupResponse(1);
+ given(studentService.studentSignup(request))
+ .willReturn(expectedResponse);
+
+ // when
+ BaseResponse response =
+ userController.studentSignup(request);
+
+ // then
+ assertThat(response.getResult()).isEqualTo(expectedResponse);
+ verify(studentService).studentSignup(request);
+ }
+
+ private StudentSignupRequest createValidStudentSignupRequest() {
+ return new StudentSignupRequest(
+ "test@inha.edu",
+ "홍길동",
+ "password2@",
+ "12241234",
+ List.of(1)
+ );
+ }
+ }
+
+ @Nested
+ @DisplayName("교수 회원가입 테스트")
+ class ProfessorSignupTest {
+ @Test
+ @DisplayName("교수 회원가입 성공")
+ void professorSignup_Success() {
+ // given
+ ProfessorSignupRequest request = createValidProfessorSignupRequest();
+ ProfessorSignupResponse expectedResponse = new ProfessorSignupResponse(1);
+ given(professorService.professorSignup(request))
+ .willReturn(expectedResponse);
+
+ // when
+ BaseResponse response = userController.professorSignup(request);
+
+ // then
+ assertThat(response.getResult()).isEqualTo(expectedResponse);
+ verify(professorService).professorSignup(request);
+ }
+
+ private ProfessorSignupRequest createValidProfessorSignupRequest() {
+ return new ProfessorSignupRequest(
+ "professor@inha.ac.kr",
+ "홍길동",
+ "password2@",
+ "221121",
+ List.of(1)
+ );
+ }
+ }
+
+ @Nested
+ @DisplayName("기업 회원가입 테스트")
+ class CompanySignupTest {
+ @Test
+ @DisplayName("기업 회원가입 성공")
+ void companySignup_Success() {
+ // given
+ CompanySignupRequest request = createValidCompanySignupRequest();
+ MultipartFile evidence = createMockMultipartFile();
+ CompanySignupResponse expectedResponse = new CompanySignupResponse(1);
+
+ given(companyService.companySignup(request, evidence))
+ .willReturn(expectedResponse);
+
+ // when
+ BaseResponse response =
+ userController.companySignup(request, evidence);
+
+ // then
+ assertThat(response.getResult()).isEqualTo(expectedResponse);
+ verify(companyService).companySignup(request, evidence);
+ }
+
+ private CompanySignupRequest createValidCompanySignupRequest() {
+ return new CompanySignupRequest(
+ "company@example.com",
+ "홍길동",
+ "password2@",
+ "인하대학교"
+ );
+ }
+
+ private MultipartFile createMockMultipartFile() {
+ return new MockMultipartFile(
+ "evidence",
+ "evidence.pdf",
+ MediaType.APPLICATION_PDF_VALUE,
+ "test".getBytes()
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/inha/git/user/api/service/CompanyServiceTest.java b/src/test/java/inha/git/user/api/service/CompanyServiceTest.java
new file mode 100644
index 00000000..d4ae4290
--- /dev/null
+++ b/src/test/java/inha/git/user/api/service/CompanyServiceTest.java
@@ -0,0 +1,146 @@
+package inha.git.user.api.service;
+
+import inha.git.auth.api.service.MailService;
+import inha.git.common.exceptions.BaseException;
+import inha.git.user.api.controller.dto.request.CompanySignupRequest;
+import inha.git.user.api.controller.dto.response.CompanySignupResponse;
+import inha.git.user.api.mapper.UserMapper;
+import inha.git.user.domain.Company;
+import inha.git.user.domain.User;
+import inha.git.user.domain.enums.Role;
+import inha.git.user.domain.repository.CompanyJpaRepository;
+import inha.git.user.domain.repository.UserJpaRepository;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.http.MediaType;
+import org.springframework.mock.web.MockMultipartFile;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.web.multipart.MultipartFile;
+
+import static inha.git.common.Constant.COMPANY_SIGN_UP_TYPE;
+import static inha.git.common.code.status.ErrorStatus.EMAIL_AUTH_NOT_FOUND;
+import static org.assertj.core.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.*;
+
+@DisplayName("기업 서비스 테스트")
+@ExtendWith(MockitoExtension.class)
+class CompanyServiceTest {
+
+ @InjectMocks
+ private CompanyServiceImpl companyService;
+
+ @Mock
+ private UserJpaRepository userJpaRepository;
+
+ @Mock
+ private CompanyJpaRepository companyJpaRepository;
+
+ @Mock
+ private UserMapper userMapper;
+
+ @Mock
+ private PasswordEncoder passwordEncoder;
+
+ @Mock
+ private MailService mailService;
+
+ @Nested
+ @DisplayName("기업 회원가입 테스트")
+ class CompanySignupTest {
+
+ @org.junit.Test
+ @DisplayName("기업 회원가입 성공")
+ void companySignup_Success() {
+ // given
+ CompanySignupRequest request = createValidCompanySignupRequest();
+ MultipartFile evidence = createMockMultipartFile();
+ User mockUser = createMockUser();
+ User savedMockUser = createMockUser();
+ Company mockCompany = createMockCompany(mockUser);
+ CompanySignupResponse expectedResponse = new CompanySignupResponse(1);
+
+ given(userMapper.companySignupRequestToUser(request))
+ .willReturn(mockUser);
+ given(passwordEncoder.encode(request.pw()))
+ .willReturn("encodedPassword");
+ given(userJpaRepository.save(any(User.class)))
+ .willReturn(savedMockUser);
+ given(userMapper.companySignupRequestToCompany(eq(request), anyString()))
+ .willReturn(mockCompany);
+ given(userMapper.userToCompanySignupResponse(savedMockUser))
+ .willReturn(expectedResponse);
+
+ // when
+ CompanySignupResponse response = companyService.companySignup(request, evidence);
+
+ // then
+ assertThat(response).isEqualTo(expectedResponse);
+ verify(mailService).emailAuth(request.email(), COMPANY_SIGN_UP_TYPE);
+ verify(userJpaRepository).save(any(User.class));
+ verify(companyJpaRepository).save(any(Company.class));
+ }
+
+ @Test
+ @DisplayName("이메일 인증 실패시 예외 발생")
+ void companySignup_EmailAuthFail_ThrowsException() {
+ // given
+ CompanySignupRequest request = createValidCompanySignupRequest();
+ MultipartFile evidence = createMockMultipartFile();
+
+ doThrow(new BaseException(EMAIL_AUTH_NOT_FOUND))
+ .when(mailService)
+ .emailAuth(request.email(), COMPANY_SIGN_UP_TYPE);
+
+ // when & then
+ assertThrows(BaseException.class, () ->
+ companyService.companySignup(request, evidence));
+ verify(userJpaRepository, never()).save(any());
+ verify(companyJpaRepository, never()).save(any());
+ }
+
+ private CompanySignupRequest createValidCompanySignupRequest() {
+ return new CompanySignupRequest(
+ "company@example.com",
+ "홍길동",
+ "password2@",
+ "인하대학교"
+ );
+ }
+
+ private MultipartFile createMockMultipartFile() {
+ return new MockMultipartFile(
+ "evidence",
+ "evidence.pdf",
+ MediaType.APPLICATION_PDF_VALUE,
+ "test".getBytes()
+ );
+ }
+
+ private User createMockUser() {
+ return User.builder()
+ .id(1)
+ .email("company@example.com")
+ .name("홍길동")
+ .pw("encodedPassword")
+ .role(Role.COMPANY)
+ .build();
+ }
+
+ private Company createMockCompany(User user) {
+ return Company.builder()
+ .id(1)
+ .user(user)
+ .affiliation("인하대학교")
+ .evidenceFilePath("/path/to/evidence.pdf")
+ .build();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/inha/git/user/api/service/ProfessorServiceTest.java b/src/test/java/inha/git/user/api/service/ProfessorServiceTest.java
new file mode 100644
index 00000000..ee91f7ce
--- /dev/null
+++ b/src/test/java/inha/git/user/api/service/ProfessorServiceTest.java
@@ -0,0 +1,155 @@
+package inha.git.user.api.service;
+
+import inha.git.auth.api.service.MailService;
+import inha.git.common.exceptions.BaseException;
+import inha.git.user.api.controller.dto.request.ProfessorSignupRequest;
+import inha.git.user.api.controller.dto.response.ProfessorSignupResponse;
+import inha.git.user.api.mapper.UserMapper;
+import inha.git.user.domain.Professor;
+import inha.git.user.domain.User;
+import inha.git.user.domain.enums.Role;
+import inha.git.user.domain.repository.ProfessorJpaRepository;
+import inha.git.user.domain.repository.UserJpaRepository;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+import java.util.List;
+
+import static inha.git.common.Constant.PROFESSOR_SIGN_UP_TYPE;
+import static inha.git.common.Constant.PROFESSOR_TYPE;
+import static inha.git.common.code.status.ErrorStatus.EMAIL_AUTH_NOT_FOUND;
+import static inha.git.common.code.status.ErrorStatus.INVALID_EMAIL_DOMAIN;
+import static org.assertj.core.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.*;
+
+@DisplayName("교수 서비스 테스트")
+@ExtendWith(MockitoExtension.class)
+class ProfessorServiceTest {
+
+ @InjectMocks
+ private ProfessorServiceImpl professorService;
+
+ @Mock
+ private UserJpaRepository userJpaRepository;
+
+ @Mock
+ private ProfessorJpaRepository professorJpaRepository;
+
+ @Mock
+ private UserMapper userMapper;
+
+ @Mock
+ private PasswordEncoder passwordEncoder;
+
+ @Mock
+ private EmailDomainService emailDomainService;
+
+ @Mock
+ private MailService mailService;
+
+ @Nested
+ @DisplayName("교수 회원가입 테스트")
+ class ProfessorSignupTest {
+
+ @Test
+ @DisplayName("교수 회원가입 성공")
+ void professorSignup_Success() {
+ // given
+ ProfessorSignupRequest request = createValidProfessorSignupRequest();
+ User mockUser = createMockUser();
+ Professor mockProfessor = createMockProfessor(mockUser);
+ User savedMockUser = createMockUser();
+ ProfessorSignupResponse expectedResponse = new ProfessorSignupResponse(1);
+
+ given(userMapper.professorSignupRequestToUser(request))
+ .willReturn(mockUser);
+ given(passwordEncoder.encode(request.pw()))
+ .willReturn("encodedPassword");
+ given(userMapper.professorSignupRequestToProfessor(request))
+ .willReturn(mockProfessor);
+ given(userJpaRepository.save(any(User.class)))
+ .willReturn(savedMockUser);
+ given(userMapper.userToProfessorSignupResponse(savedMockUser))
+ .willReturn(expectedResponse);
+
+ // when
+ ProfessorSignupResponse response = professorService.professorSignup(request);
+
+ // then
+ assertThat(response).isEqualTo(expectedResponse);
+ verify(emailDomainService).validateEmailDomain(request.email(), PROFESSOR_TYPE);
+ verify(mailService).emailAuth(request.email(), PROFESSOR_SIGN_UP_TYPE);
+ verify(professorJpaRepository).save(any(Professor.class));
+ verify(userJpaRepository).save(any(User.class));
+ }
+
+ @Test
+ @DisplayName("이메일 도메인 검증 실패시 예외 발생")
+ void professorSignup_InvalidEmailDomain_ThrowsException() {
+ // given
+ ProfessorSignupRequest request = createValidProfessorSignupRequest();
+ doThrow(new BaseException(INVALID_EMAIL_DOMAIN))
+ .when(emailDomainService)
+ .validateEmailDomain(request.email(), PROFESSOR_TYPE);
+
+ // when & then
+ assertThrows(BaseException.class, () ->
+ professorService.professorSignup(request));
+ verify(professorJpaRepository, never()).save(any());
+ verify(userJpaRepository, never()).save(any());
+ }
+
+ @Test
+ @DisplayName("이메일 인증 실패시 예외 발생")
+ void professorSignup_EmailAuthFail_ThrowsException() {
+ // given
+ ProfessorSignupRequest request = createValidProfessorSignupRequest();
+ doThrow(new BaseException(EMAIL_AUTH_NOT_FOUND))
+ .when(mailService)
+ .emailAuth(request.email(), PROFESSOR_SIGN_UP_TYPE);
+
+ // when & then
+ assertThrows(BaseException.class, () ->
+ professorService.professorSignup(request));
+ verify(professorJpaRepository, never()).save(any());
+ verify(userJpaRepository, never()).save(any());
+ }
+
+ private ProfessorSignupRequest createValidProfessorSignupRequest() {
+ return new ProfessorSignupRequest(
+ "professor@inha.ac.kr",
+ "홍길동",
+ "password2@",
+ "221121",
+ List.of(1)
+ );
+ }
+
+ private User createMockUser() {
+ return User.builder()
+ .id(1)
+ .email("professor@inha.ac.kr")
+ .name("홍길동")
+ .pw("encodedPassword")
+ .userNumber("221121")
+ .role(Role.PROFESSOR)
+ .build();
+ }
+
+ private Professor createMockProfessor(User user) {
+ return Professor.builder()
+ .id(1)
+ .user(user)
+ .build();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/inha/git/user/api/service/StudentServiceTest.java b/src/test/java/inha/git/user/api/service/StudentServiceTest.java
new file mode 100644
index 00000000..8e509c6f
--- /dev/null
+++ b/src/test/java/inha/git/user/api/service/StudentServiceTest.java
@@ -0,0 +1,170 @@
+package inha.git.user.api.service;
+
+import inha.git.auth.api.service.MailService;
+import inha.git.common.exceptions.BaseException;
+import inha.git.user.api.controller.dto.request.StudentSignupRequest;
+import inha.git.user.api.controller.dto.response.StudentSignupResponse;
+import inha.git.user.api.mapper.UserMapper;
+import inha.git.user.domain.User;
+import inha.git.user.domain.enums.Role;
+import inha.git.user.domain.repository.UserJpaRepository;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+import java.util.List;
+
+import static inha.git.common.Constant.STUDENT_SIGN_UP_TYPE;
+import static inha.git.common.Constant.STUDENT_TYPE;
+import static inha.git.common.code.status.ErrorStatus.*;
+import static org.assertj.core.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.*;
+
+@DisplayName("학생 서비스 테스트")
+@ExtendWith(MockitoExtension.class)
+class StudentServiceTest {
+
+ @InjectMocks
+ private StudentServiceImpl studentService;
+
+ @Mock
+ private UserJpaRepository userJpaRepository;
+
+ @Mock
+ private UserMapper userMapper;
+
+ @Mock
+ private PasswordEncoder passwordEncoder;
+
+ @Mock
+ private EmailDomainService emailDomainService;
+
+ @Mock
+ private MailService mailService;
+
+ @Nested
+ @DisplayName("학생 회원가입 테스트")
+ class StudentSignupTest {
+
+ @Test
+ @DisplayName("학생 회원가입 성공")
+ void studentSignup_Success() {
+ // given
+ StudentSignupRequest request = createValidStudentSignupRequest();
+ User mockUser = createMockUser();
+ User savedMockUser = createMockUser();
+ StudentSignupResponse expectedResponse = new StudentSignupResponse(1);
+
+ given(userMapper.studentSignupRequestToUser(request))
+ .willReturn(mockUser);
+ given(passwordEncoder.encode(request.pw()))
+ .willReturn("encodedPassword");
+ given(userJpaRepository.save(any(User.class)))
+ .willReturn(savedMockUser);
+ given(userMapper.userToStudentSignupResponse(savedMockUser))
+ .willReturn(expectedResponse);
+
+ // when
+ StudentSignupResponse response = studentService.studentSignup(request);
+
+ // then
+ assertThat(response).isEqualTo(expectedResponse);
+ verify(emailDomainService).validateEmailDomain(request.email(), STUDENT_TYPE);
+ verify(mailService).emailAuth(request.email(), STUDENT_SIGN_UP_TYPE);
+ verify(userJpaRepository).save(any(User.class));
+ }
+
+ @Test
+ @DisplayName("이메일 도메인 검증 실패시 예외 발생")
+ void studentSignup_InvalidEmailDomain_ThrowsException() {
+ // given
+ StudentSignupRequest request = createValidStudentSignupRequest();
+ doThrow(new BaseException(INVALID_EMAIL_DOMAIN))
+ .when(emailDomainService)
+ .validateEmailDomain(request.email(), STUDENT_TYPE);
+
+ // when & then
+ assertThrows(BaseException.class, () ->
+ studentService.studentSignup(request));
+ verify(userJpaRepository, never()).save(any());
+ }
+
+ @Test
+ @DisplayName("이메일 인증 실패시 예외 발생")
+ void studentSignup_EmailAuthFail_ThrowsException() {
+ // given
+ StudentSignupRequest request = createValidStudentSignupRequest();
+ doThrow(new BaseException(EMAIL_AUTH_NOT_FOUND))
+ .when(mailService)
+ .emailAuth(request.email(), STUDENT_SIGN_UP_TYPE);
+
+ // when & then
+ assertThrows(BaseException.class, () ->
+ studentService.studentSignup(request));
+ verify(userJpaRepository, never()).save(any());
+ }
+
+ @Test
+ @DisplayName("학과 정보 매핑 실패")
+ void studentSignup_DepartmentMappingFail_ThrowsException() {
+ // given
+ StudentSignupRequest request = createValidStudentSignupRequest();
+ User mockUser = createMockUser();
+
+ given(userMapper.studentSignupRequestToUser(request))
+ .willReturn(mockUser);
+ doThrow(new BaseException(DEPARTMENT_NOT_FOUND))
+ .when(userMapper)
+ .mapDepartmentsToUser(any(), any(), any());
+
+ // when & then
+ assertThrows(BaseException.class, () ->
+ studentService.studentSignup(request));
+ verify(userJpaRepository, never()).save(any());
+ }
+
+ @Test
+ @DisplayName("중복 이메일로 회원가입 시도")
+ void studentSignup_DuplicateEmail_ThrowsException() {
+ // given
+ StudentSignupRequest request = createValidStudentSignupRequest();
+ doThrow(new BaseException(DUPLICATE_EMAIL))
+ .when(emailDomainService)
+ .validateEmailDomain(request.email(), STUDENT_TYPE);
+
+ // when & then
+ assertThrows(BaseException.class, () ->
+ studentService.studentSignup(request));
+ verify(userJpaRepository, never()).save(any());
+ }
+
+ private StudentSignupRequest createValidStudentSignupRequest() {
+ return new StudentSignupRequest(
+ "test@inha.edu",
+ "홍길동",
+ "password2@",
+ "12241234",
+ List.of(1)
+ );
+ }
+
+ private User createMockUser() {
+ return User.builder()
+ .id(1)
+ .email("test@inha.edu")
+ .name("홍길동")
+ .pw("encodedPassword")
+ .userNumber("12241234")
+ .role(Role.USER)
+ .build();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/inha/git/utils/IdempotentProviderTest.java b/src/test/java/inha/git/utils/IdempotentProviderTest.java
new file mode 100644
index 00000000..fd4881f9
--- /dev/null
+++ b/src/test/java/inha/git/utils/IdempotentProviderTest.java
@@ -0,0 +1,107 @@
+package inha.git.utils;
+
+import inha.git.common.exceptions.BaseException;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static inha.git.common.Constant.IDEMPOTENT;
+import static inha.git.common.Constant.TIME_LIMIT;
+import static inha.git.common.code.status.ErrorStatus.DUPLICATION_REQUEST;
+import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+@DisplayName("IdempotentProvider 테스트")
+@ExtendWith(MockitoExtension.class)
+class IdempotentProviderTest {
+
+ @InjectMocks
+ private IdempotentProvider idempotentProvider;
+
+ @Mock
+ private RedisProvider redisProvider;
+
+
+ @Test
+ @DisplayName("유효한 Idempotency 키 검증 성공")
+ void isValidIdempotent_Success() {
+ // given
+ List keyElements = Arrays.asList("createRequest", "1", "사용자", "제목", "내용");
+ String expectedKey = "createRequest1사용자제목내용";
+
+ given(redisProvider.getValueOps(expectedKey))
+ .willReturn(null);
+
+ // when
+ idempotentProvider.isValidIdempotent(keyElements);
+
+ // then
+ verify(redisProvider).getValueOps(expectedKey);
+ verify(redisProvider).setDataExpire(expectedKey, IDEMPOTENT, TIME_LIMIT);
+ }
+
+ //@Test
+ @DisplayName("중복된 Idempotency 키 검증 실패")
+ void isValidIdempotent_Duplicated_ThrowsException() {
+ // given
+ List keyElements = Arrays.asList("createRequest", "1", "사용자", "제목", "내용");
+ String expectedKey = "createRequest1사용자제목내용";
+
+ given(redisProvider.getValueOps(expectedKey))
+ .willReturn(IDEMPOTENT);
+
+ // when & then
+ assertThatThrownBy(() -> idempotentProvider.isValidIdempotent(keyElements))
+ .isInstanceOf(BaseException.class)
+ .hasFieldOrPropertyWithValue("errorStatus", DUPLICATION_REQUEST);
+
+ verify(redisProvider).getValueOps(expectedKey);
+ verify(redisProvider, never()).setDataExpire(any(), any(), any());
+ }
+
+ @Test
+ @DisplayName("빈 키 요소 리스트로 검증 시도")
+ void isValidIdempotent_EmptyKeyElements() {
+ // given
+ List emptyKeyElements = Collections.emptyList();
+ String expectedKey = "";
+
+ given(redisProvider.getValueOps(expectedKey))
+ .willReturn(null);
+
+ // when
+ idempotentProvider.isValidIdempotent(emptyKeyElements);
+
+ // then
+ verify(redisProvider).getValueOps(expectedKey);
+ verify(redisProvider).setDataExpire(expectedKey, IDEMPOTENT, TIME_LIMIT);
+ }
+
+ //@Test
+ @DisplayName("null 값이 포함된 키 요소로 검증 시도")
+ void isValidIdempotent_WithNullElement() {
+ // given
+ List keyElements = Arrays.asList("createRequest", null, "사용자", "제목", "내용");
+ String expectedKey = "createRequest사용자제목내용"; // null은 빈 문자열로 처리됨
+
+ given(redisProvider.getValueOps(expectedKey))
+ .willReturn(null);
+
+ // when
+ idempotentProvider.isValidIdempotent(keyElements);
+
+ // then
+ verify(redisProvider).getValueOps(expectedKey);
+ verify(redisProvider).setDataExpire(expectedKey, IDEMPOTENT, TIME_LIMIT);
+ }
+}
\ No newline at end of file