diff --git a/.gitignore b/.gitignore index 0021b38..9467dee 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,7 @@ out/ ### dev ### application-dev.yml +# local/dev only +.dev/ +docker-compose.yml +init-user.sql \ No newline at end of file diff --git a/src/main/java/dmu/dasom/api/domain/executive/dto/ExecutiveRequestDto.java b/src/main/java/dmu/dasom/api/domain/executive/dto/ExecutiveRequestDto.java index 8d87f00..62b9e8a 100644 --- a/src/main/java/dmu/dasom/api/domain/executive/dto/ExecutiveRequestDto.java +++ b/src/main/java/dmu/dasom/api/domain/executive/dto/ExecutiveRequestDto.java @@ -7,6 +7,7 @@ import jakarta.validation.constraints.Size; import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.Setter; import lombok.NoArgsConstructor; @Getter @@ -15,8 +16,6 @@ @Schema(name = "ExecutiveRequestDto", description = "임원진 요청 DTO") public class ExecutiveRequestDto { - private Long id; - @NotBlank(message = "임원진 이름은 필수 입력 사항입니다.") @Size(max = 50, message = "임원진 이름은 최대 50자입니다.") @Schema(description = "임원진 이름", example = "김다솜") @@ -45,7 +44,7 @@ public ExecutiveEntity toEntity() { .role(this.role) .githubUsername(this.github_username) .team(this.team) - .sortOrder(this.sortOrder) + .sortOrder(sortOrder != null ? sortOrder : 9999) .build(); } } diff --git a/src/main/java/dmu/dasom/api/domain/executive/entity/ExecutiveEntity.java b/src/main/java/dmu/dasom/api/domain/executive/entity/ExecutiveEntity.java index 45b1a22..8df0368 100644 --- a/src/main/java/dmu/dasom/api/domain/executive/entity/ExecutiveEntity.java +++ b/src/main/java/dmu/dasom/api/domain/executive/entity/ExecutiveEntity.java @@ -58,6 +58,15 @@ public void update(ExecutiveUpdateRequestDto dto) { if (dto.getSortOrder() != null) this.sortOrder = dto.getSortOrder(); } + @PrePersist + public void prePersist() { + if (sortOrder == null) sortOrder = 9999; + } + @PreUpdate + public void preUpdate() { + if (sortOrder == null) sortOrder = 9999; + } + // 엔티티 -> DTO 변환 책임 public ExecutiveResponseDto toResponseDto() { return ExecutiveResponseDto.builder() @@ -67,6 +76,7 @@ public ExecutiveResponseDto toResponseDto() { .role(this.role) .github_username(this.githubUsername) .team(this.team) + .sortOrder(this.sortOrder) .build(); } @@ -79,6 +89,7 @@ public ExecutiveListResponseDto toListResponseDto() { .role(this.role) .github_username(this.githubUsername) .team(this.team) + .sortOrder(this.sortOrder) .build(); } } diff --git a/src/main/java/dmu/dasom/api/domain/executive/service/ExecutiveServiceImpl.java b/src/main/java/dmu/dasom/api/domain/executive/service/ExecutiveServiceImpl.java index 42563d6..7070886 100644 --- a/src/main/java/dmu/dasom/api/domain/executive/service/ExecutiveServiceImpl.java +++ b/src/main/java/dmu/dasom/api/domain/executive/service/ExecutiveServiceImpl.java @@ -47,7 +47,8 @@ public List getAllExecutives() { // 임원진 멤버 생성 public ExecutiveCreationResponseDto createExecutive(ExecutiveRequestDto requestDto) { - return new ExecutiveCreationResponseDto(executiveRepository.save(requestDto.toEntity()).getId()); + ExecutiveEntity saved = executiveRepository.save(requestDto.toEntity()); + return new ExecutiveCreationResponseDto(saved.getId()); } // 임원진 멤버 삭제 diff --git a/src/main/java/dmu/dasom/api/domain/somkathon/dto/SomParticipantRequestDto.java b/src/main/java/dmu/dasom/api/domain/somkathon/dto/SomParticipantRequestDto.java index 261c4ae..177c8a9 100644 --- a/src/main/java/dmu/dasom/api/domain/somkathon/dto/SomParticipantRequestDto.java +++ b/src/main/java/dmu/dasom/api/domain/somkathon/dto/SomParticipantRequestDto.java @@ -5,40 +5,53 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; -import lombok.Setter; +import org.hibernate.validator.constraints.URL; @Getter -@Setter +@Builder +@AllArgsConstructor @Schema(description = "솜커톤 참가자 요청 DTO") public class SomParticipantRequestDto { + @NotBlank(message = "참가자 이름은 필수 입력 값입니다.") @Size(max = 50, message = "참가자 이름은 최대 50자까지 입력 가능합니다.") @Schema(description = "참가자 이름", example = "유승완", required = true) - private String participantName; // 참가자 이름 + private final String participantName; // 참가자 이름 @NotBlank(message = "학번은 필수 입력 값입니다.") @Pattern(regexp = "^[0-9]{8}$", message = "학번은 8자리 숫자여야 합니다.") @Schema(description = "학번 (8자리 숫자)", example = "20250001", required = true) - private String studentId; + private final String studentId; @NotBlank(message = "학과는 필수 입력 값입니다.") @Size(max = 100, message = "학과는 최대 100자까지 입력 가능합니다.") @Schema(description = "학과", example = "컴퓨터소프트웨어공학과", required = true) - private String department; // 학과 + private final String department; // 학과 @NotBlank(message = "학년은 필수 입력 값입니다.") @Pattern(regexp = "^[1-4]$", message = "학년은 1~4 사이의 숫자여야 합니다.") @Schema(description = "학년 (1~4)", example = "3", required = true) - private String grade; // 학년 + private final String grade; // 학년 @NotBlank(message = "연락처는 필수 입력 값입니다.") @Pattern(regexp = "^010-[0-9]{4}-[0-9]{4}$", message = "연락처는 '010-XXXX-XXXX' 형식이어야 합니다.") @Schema(description = "연락처 (010-XXXX-XXXX 형식)", example = "010-1234-5678", required = true) - private String contact; // 연락처 + private final String contact; // 연락처 @NotBlank(message = "이메일은 필수 입력 값입니다.") @Email(message = "올바른 이메일 형식이 아닙니다.") @Schema(description = "이메일 주소", example = "hong@example.com", required = true) - private String email; // 이메일 + private final String email; // 이메일 + + @URL(protocol = "https", host = "github.com", message = "GitHub URL이 올바르지 않습니다.") + @Schema(description = "GitHub 주소", example = "https://github.com/username") + private final String githubLink; // 깃허브 주소 + + @NotBlank(message = "포트폴리오 주소는 필수 입력 값입니다.") + @URL(protocol = "https", message = "포트폴리오 URL이 올바르지 않습니다.") + @Schema(description = "포트폴리오 주소", example = "https://portfolio.com/username", required = true) + private final String portfolioLink; // 포트폴리오 주소 } diff --git a/src/main/java/dmu/dasom/api/domain/somkathon/dto/SomParticipantResponseDto.java b/src/main/java/dmu/dasom/api/domain/somkathon/dto/SomParticipantResponseDto.java index 6e20c99..fe0befe 100644 --- a/src/main/java/dmu/dasom/api/domain/somkathon/dto/SomParticipantResponseDto.java +++ b/src/main/java/dmu/dasom/api/domain/somkathon/dto/SomParticipantResponseDto.java @@ -31,4 +31,10 @@ public class SomParticipantResponseDto { @Schema(description = "이메일 주소", example = "hong@example.com", required = true) private String email; // 이메일 -} + + @Schema(description = "깃허브 주소", example = "https://github.com/username", required = true) + private String githubLink; // 깃허브 주소 + + @Schema(description = "포트폴리오 주소", example = "https://portfolio.com/username", required = true) + private String portfolioLink; // 포트폴리오 주소 +} \ No newline at end of file diff --git a/src/main/java/dmu/dasom/api/domain/somkathon/entity/SomParticipant.java b/src/main/java/dmu/dasom/api/domain/somkathon/entity/SomParticipant.java index 7ab055c..23eba69 100644 --- a/src/main/java/dmu/dasom/api/domain/somkathon/entity/SomParticipant.java +++ b/src/main/java/dmu/dasom/api/domain/somkathon/entity/SomParticipant.java @@ -2,6 +2,7 @@ import dmu.dasom.api.domain.common.BaseEntity; import dmu.dasom.api.domain.somkathon.dto.SomParticipantRequestDto; +import dmu.dasom.api.domain.somkathon.dto.SomParticipantResponseDto; import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Builder; @@ -39,6 +40,12 @@ public class SomParticipant extends BaseEntity { @Column(nullable = false) private String email; // 이메일 + @Column + private String githubLink; // 깃허브 링크 + + @Column(nullable = false) + private String portfolioLink; // 포트폴리오 링크 + public void update(SomParticipantRequestDto requestDto) { this.participantName = requestDto.getParticipantName(); this.studentId = requestDto.getStudentId(); @@ -46,5 +53,24 @@ public void update(SomParticipantRequestDto requestDto) { this.grade = requestDto.getGrade(); this.contact = requestDto.getContact(); this.email = requestDto.getEmail(); + this.githubLink = requestDto.getGithubLink(); + this.portfolioLink = requestDto.getPortfolioLink(); + } + + /** + * Entity → Response DTO 변환 메서드 + */ + public SomParticipantResponseDto toResponseDto(SomParticipant participant) { + return SomParticipantResponseDto.builder() + .id(participant.getId()) + .participantName(participant.getParticipantName()) + .studentId(participant.getStudentId()) + .department(participant.getDepartment()) + .grade(participant.getGrade()) + .contact(participant.getContact()) + .email(participant.getEmail()) + .githubLink(participant.getGithubLink()) + .portfolioLink(participant.getPortfolioLink()) + .build(); } } diff --git a/src/main/java/dmu/dasom/api/domain/somkathon/service/SomParticipantService.java b/src/main/java/dmu/dasom/api/domain/somkathon/service/SomParticipantService.java index 0169d8e..d67edcc 100644 --- a/src/main/java/dmu/dasom/api/domain/somkathon/service/SomParticipantService.java +++ b/src/main/java/dmu/dasom/api/domain/somkathon/service/SomParticipantService.java @@ -29,11 +29,13 @@ public SomParticipantResponseDto createParticipant(SomParticipantRequestDto requ .grade(requestDto.getGrade()) .contact(requestDto.getContact()) .email(requestDto.getEmail()) + .githubLink(requestDto.getGithubLink()) + .portfolioLink(requestDto.getPortfolioLink()) .build(); SomParticipant saved = somParticipantRepository.save(participant); - return toResponseDto(saved); + return saved.toResponseDto(saved); } /** @@ -41,7 +43,7 @@ public SomParticipantResponseDto createParticipant(SomParticipantRequestDto requ */ public List getAllParticipants() { return somParticipantRepository.findAll().stream() - .map(this::toResponseDto) + .map(p -> p.toResponseDto(p)) .collect(Collectors.toList()); } @@ -51,15 +53,18 @@ public List getAllParticipants() { public SomParticipantResponseDto getParticipant(Long id){ SomParticipant participant = findParticipantById(id); - return toResponseDto(participant); + return participant.toResponseDto(participant); } + /** + * 참가자 수정 (Put) + */ public SomParticipantResponseDto updateParticipant(Long id, SomParticipantRequestDto requestDto){ SomParticipant participant = findParticipantById(id); participant.update(requestDto); - return toResponseDto(participant); + return participant.toResponseDto(participant); } /** @@ -70,22 +75,6 @@ public void deleteParticipant(Long id) { somParticipantRepository.deleteById(id); } - - /** - * Entity → Response DTO 변환 메서드 - */ - private SomParticipantResponseDto toResponseDto(SomParticipant participant) { - return SomParticipantResponseDto.builder() - .id(participant.getId()) - .participantName(participant.getParticipantName()) - .studentId(participant.getStudentId()) - .department(participant.getDepartment()) - .grade(participant.getGrade()) - .contact(participant.getContact()) - .email(participant.getEmail()) - .build(); - } - /** * ID로 참가자 조회 (공통 처리) */ diff --git a/src/test/java/dmu/dasom/api/domain/executive/service/ExecutiveServiceTest.java b/src/test/java/dmu/dasom/api/domain/executive/service/ExecutiveServiceTest.java index 93825df..884ef1b 100644 --- a/src/test/java/dmu/dasom/api/domain/executive/service/ExecutiveServiceTest.java +++ b/src/test/java/dmu/dasom/api/domain/executive/service/ExecutiveServiceTest.java @@ -88,7 +88,6 @@ void createExecutive_success() { // given Long id = 1L; ExecutiveRequestDto dto = new ExecutiveRequestDto( - id, "김다솜", "회장", "동아리 운영 총괄", diff --git a/src/test/java/dmu/dasom/api/domain/somkathon/SomParticipantServiceTest.java b/src/test/java/dmu/dasom/api/domain/somkathon/SomParticipantServiceTest.java new file mode 100644 index 0000000..47c4b63 --- /dev/null +++ b/src/test/java/dmu/dasom/api/domain/somkathon/SomParticipantServiceTest.java @@ -0,0 +1,241 @@ +package dmu.dasom.api.domain.somkathon; + +import dmu.dasom.api.domain.common.exception.CustomException; +import dmu.dasom.api.domain.common.exception.ErrorCode; +import dmu.dasom.api.domain.somkathon.dto.SomParticipantRequestDto; +import dmu.dasom.api.domain.somkathon.dto.SomParticipantResponseDto; +import dmu.dasom.api.domain.somkathon.entity.SomParticipant; +import dmu.dasom.api.domain.somkathon.repository.SomParticipantRepository; +import dmu.dasom.api.domain.somkathon.service.SomParticipantService; +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.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class SomParticipantServiceTest { + + @Mock + private SomParticipantRepository repository; + + @InjectMocks + private SomParticipantService service; + + // ======================= + // Create Tests + // ======================= + @Test + @DisplayName("참가자 생성 - 성공") + void createParticipant_success() { + SomParticipantRequestDto request = SomParticipantRequestDto.builder() + .participantName("홍길동") + .studentId("20250001") + .department("컴퓨터공학과") + .grade("3") + .contact("010-1234-5678") + .email("hong@example.com") + .githubLink("https://github.com/username") + .portfolioLink("https://drive.google.com/file") + .build(); + + when(repository.findByStudentId("20250001")).thenReturn(Optional.empty()); + when(repository.save(any(SomParticipant.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + SomParticipantResponseDto response = service.createParticipant(request); + + assertNotNull(response); + assertEquals("홍길동", response.getParticipantName()); + assertEquals("20250001", response.getStudentId()); + assertEquals("컴퓨터공학과", response.getDepartment()); + assertEquals("3", response.getGrade()); + assertEquals("010-1234-5678", response.getContact()); + assertEquals("hong@example.com", response.getEmail()); + assertEquals("https://github.com/username", response.getGithubLink()); + assertEquals("https://drive.google.com/file", response.getPortfolioLink()); + + verify(repository, times(1)).findByStudentId("20250001"); + verify(repository, times(1)).save(any(SomParticipant.class)); + } + + @Test + @DisplayName("참가자 생성 - 실패 (학생ID 중복)") + void createParticipant_fail_duplicateStudentId() { + SomParticipantRequestDto request = SomParticipantRequestDto.builder() + .studentId("20250001") + .build(); + + when(repository.findByStudentId("20250001")).thenReturn(Optional.of(mock(SomParticipant.class))); + + CustomException exception = assertThrows(CustomException.class, () -> service.createParticipant(request)); + assertEquals(ErrorCode.DUPLICATED_STUDENT_NO, exception.getErrorCode()); + + verify(repository, times(1)).findByStudentId("20250001"); + verify(repository, never()).save(any()); + } + + // ======================= + // Read Tests + // ======================= + @Test + @DisplayName("모든 참가자 조회") + void getAllParticipants_success() { + SomParticipant p1 = SomParticipant.builder() + .participantName("홍길동") + .studentId("20250001") + .department("컴퓨터공학과") + .grade("3") + .contact("010-1234-5678") + .email("hong@example.com") + .githubLink("https://github.com/hong") + .portfolioLink("https://drive.google.com/file") + .build(); + SomParticipant p2 = SomParticipant.builder() + .participantName("김철수") + .studentId("20250002") + .department("소프트웨어공학과") + .grade("2") + .contact("010-9876-5432") + .email("kim@example.com") + .githubLink("https://github.com/kim") + .portfolioLink("https://notion.site") + .build(); + + when(repository.findAll()).thenReturn(List.of(p1, p2)); + + List list = service.getAllParticipants(); + + assertEquals(2, list.size()); + assertEquals("홍길동", list.get(0).getParticipantName()); + assertEquals("김철수", list.get(1).getParticipantName()); + assertEquals("https://github.com/hong", list.get(0).getGithubLink()); + assertEquals("https://notion.site", list.get(1).getPortfolioLink()); + + verify(repository, times(1)).findAll(); + } + + @Test + @DisplayName("특정 참가자 조회 - 성공") + void getParticipant_success() { + SomParticipant participant = SomParticipant.builder() + .participantName("홍길동") + .studentId("20250001") + .department("컴퓨터공학과") + .grade("3") + .contact("010-1234-5678") + .email("hong@example.com") + .githubLink("https://github.com/username") + .portfolioLink("https://drive.google.com/file") + .build(); + + when(repository.findById(1L)).thenReturn(Optional.of(participant)); + + SomParticipantResponseDto response = service.getParticipant(1L); + + assertEquals("홍길동", response.getParticipantName()); + assertEquals("20250001", response.getStudentId()); + + verify(repository, times(1)).findById(1L); + } + + @Test + @DisplayName("특정 참가자 조회 - 실패 (없음)") + void getParticipant_fail_notFound() { + when(repository.findById(1L)).thenReturn(Optional.empty()); + + CustomException exception = assertThrows(CustomException.class, () -> service.getParticipant(1L)); + assertEquals(ErrorCode.NOT_FOUND_PARTICIPANT, exception.getErrorCode()); + + verify(repository, times(1)).findById(1L); + } + + // ======================= + // Put Tests + // ======================= + @Test + @DisplayName("참가자 수정 - 성공") + void updateParticipant_success() { + SomParticipant existing = SomParticipant.builder() + .participantName("홍길동") + .studentId("20250001") + .department("컴퓨터공학과") + .grade("3") + .contact("010-1234-5678") + .email("hong@example.com") + .githubLink("https://github.com/username") + .portfolioLink("https://drive.google.com/file") + .build(); + + SomParticipantRequestDto updateRequest = SomParticipantRequestDto.builder() + .participantName("홍길동2") + .studentId("20250001") + .department("소프트웨어공학과") + .grade("4") + .contact("010-1111-2222") + .email("hong2@example.com") + .githubLink("https://github.com/username2") + .portfolioLink("https://drive.google.com/file2") + .build(); + + when(repository.findById(1L)).thenReturn(Optional.of(existing)); + + SomParticipantResponseDto response = service.updateParticipant(1L, updateRequest); + + assertEquals("홍길동2", response.getParticipantName()); + assertEquals("20250001", response.getStudentId()); + assertEquals("소프트웨어공학과", response.getDepartment()); + assertEquals("4", response.getGrade()); + assertEquals("010-1111-2222", response.getContact()); + assertEquals("hong2@example.com", response.getEmail()); + assertEquals("https://github.com/username2", response.getGithubLink()); + assertEquals("https://drive.google.com/file2", response.getPortfolioLink()); + + verify(repository, times(1)).findById(1L); + } + + @Test + @DisplayName("참가자 수정 - 실패 (없음)") + void updateParticipant_fail_notFound() { + SomParticipantRequestDto updateRequest = SomParticipantRequestDto.builder() + .participantName("홍길동2") + .build(); + + when(repository.findById(1L)).thenReturn(Optional.empty()); + + CustomException exception = assertThrows(CustomException.class, () -> service.updateParticipant(1L, updateRequest)); + assertEquals(ErrorCode.NOT_FOUND_PARTICIPANT, exception.getErrorCode()); + + verify(repository, times(1)).findById(1L); + } + + // ======================= + // Delete Tests + // ======================= + @Test + @DisplayName("참가자 삭제 - 성공") + void deleteParticipant_success() { + SomParticipant participant = SomParticipant.builder().build(); + when(repository.findById(1L)).thenReturn(Optional.of(participant)); + + assertDoesNotThrow(() -> service.deleteParticipant(1L)); + verify(repository, times(1)).deleteById(1L); + } + + @Test + @DisplayName("참가자 삭제 - 실패 (없음)") + void deleteParticipant_fail_notFound() { + when(repository.findById(1L)).thenReturn(Optional.empty()); + + CustomException exception = assertThrows(CustomException.class, () -> service.deleteParticipant(1L)); + assertEquals(ErrorCode.NOT_FOUND_PARTICIPANT, exception.getErrorCode()); + + verify(repository, never()).deleteById(any()); + } +} \ No newline at end of file