diff --git a/src/main/java/dmu/dasom/api/domain/activity/controller/ActivityController.java b/src/main/java/dmu/dasom/api/domain/activity/controller/ActivityController.java new file mode 100644 index 0000000..c099323 --- /dev/null +++ b/src/main/java/dmu/dasom/api/domain/activity/controller/ActivityController.java @@ -0,0 +1,59 @@ +package dmu.dasom.api.domain.activity.controller; + +import dmu.dasom.api.domain.activity.dto.ActivityResponseDto; +import dmu.dasom.api.domain.activity.service.ActivityService; +import dmu.dasom.api.domain.common.exception.ErrorResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/api/activities") +@RequiredArgsConstructor +@Tag(name = "Activity API", description = "활동 연혁 조회 API") +public class ActivityController { + + private final ActivityService activityService; + + @Operation(summary = "활동 연혁 전체 조회", description = "모든 활동 연혁을 연도별, 섹션별로 그룹화하여 조회합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "활동 연혁 전체 조회 성공"), + @ApiResponse(responseCode = "405", description = "허용되지 않은 요청 방식", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ErrorResponse.class), + examples = { + @ExampleObject( + name = "허용되지 않은 메서드", + value = "{ \"code\": \"C007\", \"message\": \"허용되지 않은 요청 방식입니다.\" }" + ) + } + )), + @ApiResponse(responseCode = "500", description = "서버 내부 오류", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ErrorResponse.class), + examples = { + @ExampleObject( + name = "서버 문제 발생", + value = "{ \"code\": \"C009\", \"message\": \"서버에 문제가 발생하였습니다.\" }" + ) + } + )) + }) + @GetMapping + public ResponseEntity> getActivities() { + return ResponseEntity.ok(activityService.getActivities()); + } +} diff --git a/src/main/java/dmu/dasom/api/domain/activity/controller/AdminActivityController.java b/src/main/java/dmu/dasom/api/domain/activity/controller/AdminActivityController.java new file mode 100644 index 0000000..ce34be2 --- /dev/null +++ b/src/main/java/dmu/dasom/api/domain/activity/controller/AdminActivityController.java @@ -0,0 +1,72 @@ +package dmu.dasom.api.domain.activity.controller; + +import dmu.dasom.api.domain.activity.dto.ActivityRequestDto; +import dmu.dasom.api.domain.activity.service.ActivityService; +import dmu.dasom.api.domain.common.exception.ErrorResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Min; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/admin/activities") +@RequiredArgsConstructor +@Tag(name = "ADMIN - Activity API", description = "어드민 활동 연혁 관리 API") +public class AdminActivityController { + + private final ActivityService activityService; + + @Operation(summary = "활동 연혁 생성") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "활동 연혁 생성 성공"), + @ApiResponse(responseCode = "400", description = "요청 값 유효성 검사 실패", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class), examples = @ExampleObject(name = "유효성 검사 실패", value = "{ \"code\": \"C007\", \"message\": \"요청한 값이 올바르지 않습니다.\" }"))), + @ApiResponse(responseCode = "401", description = "인증되지 않은 사용자", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class), examples = @ExampleObject(name = "인증 실패", value = "{ \"code\": \"C001\", \"message\": \"인증되지 않은 사용자입니다.\" }"))), + @ApiResponse(responseCode = "403", description = "접근 권한 없음 (ADMIN이 아님)", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class), examples = @ExampleObject(name = "권한 없음", value = "{ \"code\": \"C002\", \"message\": \"접근 권한이 없습니다.\" }"))), + @ApiResponse(responseCode = "500", description = "서버 내부 오류", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class), examples = @ExampleObject(name = "서버 문제 발생", value = "{ \"code\": \"C009\", \"message\": \"서버에 문제가 발생하였습니다.\" }"))) + }) + @PostMapping + public ResponseEntity createActivity(@Valid @RequestBody ActivityRequestDto requestDto) { + activityService.createActivity(requestDto); + return ResponseEntity.status(201).build(); + } + + @Operation(summary = "활동 연혁 수정") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "활동 연혁 수정 성공"), + @ApiResponse(responseCode = "400", description = "요청 값 유효성 검사 실패", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class), examples = @ExampleObject(name = "유효성 검사 실패", value = "{ \"code\": \"C007\", \"message\": \"요청한 값이 올바르지 않습니다.\" }"))), + @ApiResponse(responseCode = "401", description = "인증되지 않은 사용자", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class), examples = @ExampleObject(name = "인증 실패", value = "{ \"code\": \"C001\", \"message\": \"인증되지 않은 사용자입니다.\" }"))), + @ApiResponse(responseCode = "403", description = "접근 권한 없음 (ADMIN이 아님)", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class), examples = @ExampleObject(name = "권한 없음", value = "{ \"code\": \"C002\", \"message\": \"접근 권한이 없습니다.\" }"))), + @ApiResponse(responseCode = "404", description = "수정할 리소스를 찾을 수 없음", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class), examples = @ExampleObject(name = "조회 결과 없음", value = "{ \"code\": \"C010\", \"message\": \"해당 리소스를 찾을 수 없습니다.\" }"))), + @ApiResponse(responseCode = "500", description = "서버 내부 오류", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class), examples = @ExampleObject(name = "서버 문제 발생", value = "{ \"code\": \"C009\", \"message\": \"서버에 문제가 발생하였습니다.\" }"))) + }) + @PutMapping("/{activityId}") + public ResponseEntity updateActivity( + @PathVariable @Min(1) Long activityId, + @Valid @RequestBody ActivityRequestDto requestDto + ) { + activityService.updateActivity(activityId, requestDto); + return ResponseEntity.ok().build(); + } + + @Operation(summary = "활동 연혁 삭제") + @ApiResponses(value = { + @ApiResponse(responseCode = "204", description = "활동 연혁 삭제 성공"), + @ApiResponse(responseCode = "401", description = "인증되지 않은 사용자", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class), examples = @ExampleObject(name = "인증 실패", value = "{ \"code\": \"C001\", \"message\": \"인증되지 않은 사용자입니다.\" }"))), + @ApiResponse(responseCode = "403", description = "접근 권한 없음 (ADMIN이 아님)", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class), examples = @ExampleObject(name = "권한 없음", value = "{ \"code\": \"C002\", \"message\": \"접근 권한이 없습니다.\" }"))), + @ApiResponse(responseCode = "404", description = "삭제할 리소스를 찾을 수 없음", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class), examples = @ExampleObject(name = "조회 결과 없음", value = "{ \"code\": \"C010\", \"message\": \"해당 리소스를 찾을 수 없습니다.\" }"))), + @ApiResponse(responseCode = "500", description = "서버 내부 오류", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class), examples = @ExampleObject(name = "서버 문제 발생", value = "{ \"code\": \"C009\", \"message\": \"서버에 문제가 발생하였습니다.\" }"))) + }) + @DeleteMapping("/{activityId}") + public ResponseEntity deleteActivity(@PathVariable Long activityId) { + activityService.deleteActivity(activityId); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/dmu/dasom/api/domain/activity/dto/ActivityItemDto.java b/src/main/java/dmu/dasom/api/domain/activity/dto/ActivityItemDto.java new file mode 100644 index 0000000..af39df2 --- /dev/null +++ b/src/main/java/dmu/dasom/api/domain/activity/dto/ActivityItemDto.java @@ -0,0 +1,37 @@ +package dmu.dasom.api.domain.activity.dto; + +import dmu.dasom.api.domain.activity.entity.Activity; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Getter; +import java.time.format.DateTimeFormatter; + +@Getter +@Builder +@Schema(name = "ActivityItemDto", description = "개별 활동 목록") +public class ActivityItemDto { + + @Schema(description = "활동 고유 ID", example = "1") + private final Long id; + + @Schema(description = "활동 날짜", example = "05.10") + private final String monthDay; // 날짜 필드 추가 + + @Schema(description = "활동 제목", example = "컴퓨터 공학부 경진대회") + private final String title; + + @Schema(description = "수상 내역", example = "최우수상") + private final String award; + + public static ActivityItemDto of(Activity activity) { + String formattedMonthDay = activity.getActivityDate() + .format(DateTimeFormatter.ofPattern("MM.dd")); + + return ActivityItemDto.builder() + .id(activity.getId()) + .monthDay(formattedMonthDay) + .title(activity.getTitle()) + .award(activity.getAward()) + .build(); + } +} diff --git a/src/main/java/dmu/dasom/api/domain/activity/dto/ActivityRequestDto.java b/src/main/java/dmu/dasom/api/domain/activity/dto/ActivityRequestDto.java new file mode 100644 index 0000000..75e8a69 --- /dev/null +++ b/src/main/java/dmu/dasom/api/domain/activity/dto/ActivityRequestDto.java @@ -0,0 +1,35 @@ +package dmu.dasom.api.domain.activity.dto; + +import dmu.dasom.api.domain.activity.entity.Activity; +import dmu.dasom.api.domain.activity.entity.Section; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDate; + +@Getter +@Builder +@Schema(name = "ActivityHistoryRequestDto", description = "활동 연혁 생성/수정 요청 DTO") +public class ActivityRequestDto { + + @NotNull(message = "활동 날짜는 필수입니다.") + @Schema(description = "활동 날짜", example = "2024-08-21") + private LocalDate activityDate; + + @Schema(description = "활동 섹션", example = "교내 경진대회", maxLength = 50) + private String section; + + @NotBlank(message = "활동 제목은 필수입니다.") + @Size(max = 50, message = "제목은 50자 이내로 입력해주세요.") + @Schema(description = "활동 제목", example = "컴퓨터 공학부 경진대회", maxLength = 50) + private String title; + + @Size(max = 50, message = "수상 내역은 50자 이내로 입력해주세요.") + @Schema(description = "수상 내역 (선택 사항)", example = "최우수상", maxLength = 50) + private String award; + +} diff --git a/src/main/java/dmu/dasom/api/domain/activity/dto/ActivityResponseDto.java b/src/main/java/dmu/dasom/api/domain/activity/dto/ActivityResponseDto.java new file mode 100644 index 0000000..8da36ef --- /dev/null +++ b/src/main/java/dmu/dasom/api/domain/activity/dto/ActivityResponseDto.java @@ -0,0 +1,60 @@ +package dmu.dasom.api.domain.activity.dto; + +import dmu.dasom.api.domain.activity.entity.Activity; +import dmu.dasom.api.domain.activity.entity.Section; +import lombok.Builder; +import lombok.Getter; + +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Getter +@Builder +public class ActivityResponseDto { + + private final int year; + private final List sections; + + public static List of(List activities) { + return activities.stream() + .collect(Collectors.groupingBy(activity -> activity.getActivityDate().getYear())) + .entrySet().stream() + .sorted(Map.Entry.comparingByKey()) // 1. 연도 오름차순 정렬 + .map(entryByYear -> { + List sectionDtos = groupAndSortSections(entryByYear.getValue()); + return ActivityResponseDto.builder() + .year(entryByYear.getKey()) + .sections(sectionDtos) + .build(); + }) + .collect(Collectors.toList()); + } + + private static List groupAndSortSections(List activitiesForYear) { + return activitiesForYear.stream() + .collect(Collectors.groupingBy(Activity::getSection)) + .entrySet().stream() + .map(entryBySection -> { + Section section = entryBySection.getKey(); + List activityDtos = mapAndSortActivities(entryBySection.getValue()); + return SectionItemDto.builder() + .id(section.getId()) + .section(section.getName()) + .activities(activityDtos) + .build(); + }) + // 2. 섹션 ID 오름차순 정렬 + .sorted(Comparator.comparing(SectionItemDto::getId)) + .collect(Collectors.toList()); + } + + private static List mapAndSortActivities(List activitiesForSection) { + return activitiesForSection.stream() + // 3. 활동 날짜 오름차순 정렬 + .sorted(Comparator.comparing(Activity::getActivityDate)) + .map(ActivityItemDto::of) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/dmu/dasom/api/domain/activity/dto/SectionItemDto.java b/src/main/java/dmu/dasom/api/domain/activity/dto/SectionItemDto.java new file mode 100644 index 0000000..80bd73f --- /dev/null +++ b/src/main/java/dmu/dasom/api/domain/activity/dto/SectionItemDto.java @@ -0,0 +1,31 @@ +package dmu.dasom.api.domain.activity.dto; + +import dmu.dasom.api.domain.activity.entity.Section; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Getter; + +import java.util.List; + +@Getter +@Builder +@Schema(name = "SectionItemDto", description = "섹션별 활동 목록") +public class SectionItemDto { + + @Schema(description = "활동 섹션 고유 ID", example = "1") + private final Long id; + + @Schema(description = "활동 섹션", example = "교내 경진대회") + private final String section; + + @Schema(description = "활동 목록") + private final List activities; + + public static SectionItemDto of(Section section, List activities) { + return SectionItemDto.builder() + .id(section.getId()) + .section(section.getName()) + .activities(activities) + .build(); + } +} diff --git a/src/main/java/dmu/dasom/api/domain/activity/entity/Activity.java b/src/main/java/dmu/dasom/api/domain/activity/entity/Activity.java new file mode 100644 index 0000000..0d9db06 --- /dev/null +++ b/src/main/java/dmu/dasom/api/domain/activity/entity/Activity.java @@ -0,0 +1,47 @@ +package dmu.dasom.api.domain.activity.entity; + +import jakarta.persistence.*; +import lombok.*; +import java.time.LocalDate; + +@Entity +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "activities") +public class Activity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "section_id", nullable = false) + private Section section; + + @Column(nullable = false) + private LocalDate activityDate; + + @Column(length = 50, nullable = false) + private String title; + + @Column(length = 50) + private String award; + + public static Activity create(Section section, LocalDate activityDate, String title, String award) { + return Activity.builder() + .section(section) + .activityDate(activityDate) + .title(title) + .award(award) + .build(); + } + + public void update(Section section, LocalDate activityDate, String title, String award) { + this.section = section; + this.activityDate = activityDate; + this.title = title; + this.award = award; + } +} diff --git a/src/main/java/dmu/dasom/api/domain/activity/entity/Section.java b/src/main/java/dmu/dasom/api/domain/activity/entity/Section.java new file mode 100644 index 0000000..e0cf84c --- /dev/null +++ b/src/main/java/dmu/dasom/api/domain/activity/entity/Section.java @@ -0,0 +1,33 @@ +package dmu.dasom.api.domain.activity.entity; + +import jakarta.persistence.*; +import lombok.*; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "sections") +public class Section { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(length = 50, nullable = false, unique = true) + private String name; + + @OneToMany(mappedBy = "section", cascade = CascadeType.ALL, orphanRemoval = true) + private List activities = new ArrayList<>(); + + public static Section create(String name) { + return Section.builder().name(name).build(); + } + + public void update(String name) { + this.name = name; + } +} diff --git a/src/main/java/dmu/dasom/api/domain/activity/repository/ActivityRepository.java b/src/main/java/dmu/dasom/api/domain/activity/repository/ActivityRepository.java new file mode 100644 index 0000000..7786245 --- /dev/null +++ b/src/main/java/dmu/dasom/api/domain/activity/repository/ActivityRepository.java @@ -0,0 +1,8 @@ +package dmu.dasom.api.domain.activity.repository; + +import dmu.dasom.api.domain.activity.entity.Activity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ActivityRepository extends JpaRepository { + +} \ No newline at end of file diff --git a/src/main/java/dmu/dasom/api/domain/activity/repository/SectionRepository.java b/src/main/java/dmu/dasom/api/domain/activity/repository/SectionRepository.java new file mode 100644 index 0000000..c47a4f1 --- /dev/null +++ b/src/main/java/dmu/dasom/api/domain/activity/repository/SectionRepository.java @@ -0,0 +1,12 @@ +package dmu.dasom.api.domain.activity.repository; + +import dmu.dasom.api.domain.activity.entity.Section; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface SectionRepository extends JpaRepository { + Optional
findByName(String name); +} diff --git a/src/main/java/dmu/dasom/api/domain/activity/service/ActivityService.java b/src/main/java/dmu/dasom/api/domain/activity/service/ActivityService.java new file mode 100644 index 0000000..9b06a6c --- /dev/null +++ b/src/main/java/dmu/dasom/api/domain/activity/service/ActivityService.java @@ -0,0 +1,13 @@ +package dmu.dasom.api.domain.activity.service; + +import dmu.dasom.api.domain.activity.dto.ActivityRequestDto; +import dmu.dasom.api.domain.activity.dto.ActivityResponseDto; + +import java.util.List; + +public interface ActivityService { + List getActivities(); + void createActivity(ActivityRequestDto requestDto); + void updateActivity(Long activityId, ActivityRequestDto requestDto); + void deleteActivity(Long activityId); +} diff --git a/src/main/java/dmu/dasom/api/domain/activity/service/ActivityServiceImpl.java b/src/main/java/dmu/dasom/api/domain/activity/service/ActivityServiceImpl.java new file mode 100644 index 0000000..821765e --- /dev/null +++ b/src/main/java/dmu/dasom/api/domain/activity/service/ActivityServiceImpl.java @@ -0,0 +1,73 @@ +package dmu.dasom.api.domain.activity.service; + +import dmu.dasom.api.domain.activity.dto.ActivityRequestDto; +import dmu.dasom.api.domain.activity.dto.ActivityResponseDto; +import dmu.dasom.api.domain.activity.entity.Activity; +import dmu.dasom.api.domain.activity.entity.Section; +import dmu.dasom.api.domain.activity.repository.ActivityRepository; +import dmu.dasom.api.domain.activity.repository.SectionRepository; +import dmu.dasom.api.domain.common.exception.CustomException; +import dmu.dasom.api.domain.common.exception.ErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +@Transactional +public class ActivityServiceImpl implements ActivityService { + + private final ActivityRepository activityRepository; + private final SectionRepository sectionRepository; + + @Override + @Transactional(readOnly = true) + public List getActivities() { + return ActivityResponseDto.of(activityRepository.findAll()); + } + + @Override + public void createActivity(ActivityRequestDto requestDto) { + // DTO에서 받은 String(섹션 이름)으로 Section 엔티티를 찾거나 생성 + Section section = findOrCreateSection(requestDto.getSection()); + + activityRepository.save(Activity.create( + section, + requestDto.getActivityDate(), + requestDto.getTitle(), + requestDto.getAward() + )); + } + + @Override + public void updateActivity(Long activityId, ActivityRequestDto requestDto) { + Activity activity = activityRepository.findById(activityId) + .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND)); + + // DTO에서 받은 String(섹션 이름)으로 Section 엔티티를 찾거나 생성 + Section section = findOrCreateSection(requestDto.getSection()); + + activity.update( + section, + requestDto.getActivityDate(), + requestDto.getTitle(), + requestDto.getAward() + ); + } + + @Override + public void deleteActivity(Long activityId) { + if (!activityRepository.existsById(activityId)) { + throw new CustomException(ErrorCode.NOT_FOUND); + } + activityRepository.deleteById(activityId); + } + + // 파라미터 타입을 Section에서 String으로 변경 + private Section findOrCreateSection(String sectionName) { + return sectionRepository.findByName(sectionName) + .orElseGet(() -> sectionRepository.save(Section.create(sectionName))); + } +} diff --git a/src/test/java/dmu/dasom/api/domain/activity/ActivityServiceTest.java b/src/test/java/dmu/dasom/api/domain/activity/ActivityServiceTest.java new file mode 100644 index 0000000..e0b6788 --- /dev/null +++ b/src/test/java/dmu/dasom/api/domain/activity/ActivityServiceTest.java @@ -0,0 +1,177 @@ +package dmu.dasom.api.domain.activity; + +import dmu.dasom.api.domain.activity.dto.ActivityRequestDto; +import dmu.dasom.api.domain.activity.dto.ActivityResponseDto; +import dmu.dasom.api.domain.activity.entity.Activity; +import dmu.dasom.api.domain.activity.entity.Section; +import dmu.dasom.api.domain.activity.repository.ActivityRepository; +import dmu.dasom.api.domain.activity.repository.SectionRepository; +import dmu.dasom.api.domain.activity.service.ActivityServiceImpl; +import dmu.dasom.api.domain.common.exception.CustomException; +import dmu.dasom.api.domain.common.exception.ErrorCode; +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.LocalDate; +import java.util.List; +import java.util.Optional; + +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.BDDMockito.given; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class ActivityServiceTest { + + @InjectMocks + private ActivityServiceImpl activityService; + + @Mock + private ActivityRepository activityRepository; + + @Mock + private SectionRepository sectionRepository; + + @Test + @DisplayName("활동 전체 조회 성공") + void getActivities() { + // given + Section section1 = createSection(1L, "교내 경진대회"); + Section section2 = createSection(2L, "외부 경진대회"); + Activity activity1 = createActivity(10L, "캡스톤 디자인", section1, LocalDate.of(2024, 5, 10)); + Activity activity2 = createActivity(11L, "구글 해커톤", section2, LocalDate.of(2024, 8, 20)); + + given(activityRepository.findAll()).willReturn(List.of(activity1, activity2)); + + // when + List response = activityService.getActivities(); + + // then + assertThat(response).hasSize(1); + assertThat(response.get(0).getYear()).isEqualTo(2024); + assertThat(response.get(0).getSections()).hasSize(2); + verify(activityRepository).findAll(); + } + + @Test + @DisplayName("활동 생성 성공") + void createActivity() { + // given + Section section = createSection(1L, "교내 경진대회"); + ActivityRequestDto request = createRequestDto(section); // Section 객체로 DTO 생성 + + // findByName 대신 findBySection 사용 가정 (혹은 그 반대) + given(sectionRepository.findByName("교내 경진대회")).willReturn(Optional.of(section)); + + // when + activityService.createActivity(request); + + // then + verify(activityRepository).save(any(Activity.class)); + verify(sectionRepository).findByName("교내 경진대회"); + } + + @Test + @DisplayName("활동 수정 성공") + void updateActivity() { + // given + long activityId = 1L; + Section oldSection = createSection(1L, "교내 경진대회"); + Section updatedSection = createSection(2L, "외부 경진대회"); + ActivityRequestDto request = createRequestDto(updatedSection); // Section 객체로 DTO 생성 + Activity existingActivity = createActivity(activityId, "기존 제목", oldSection, LocalDate.now()); + + given(activityRepository.findById(activityId)).willReturn(Optional.of(existingActivity)); + given(sectionRepository.findByName("외부 경진대회")).willReturn(Optional.of(updatedSection)); + + // when + activityService.updateActivity(activityId, request); + + // then + assertThat(existingActivity.getSection()).isEqualTo(updatedSection); + assertThat(existingActivity.getTitle()).isEqualTo(request.getTitle()); + verify(activityRepository).findById(activityId); + } + + @Test + @DisplayName("활동 수정 실패 - 존재하지 않는 ID") + void updateActivity_whenNotFound_thenThrowException() { + // given + long nonExistentId = 99L; + Section dummySection = createSection(99L, "아무 섹션"); + ActivityRequestDto request = createRequestDto(dummySection); // Section 객체로 DTO 생성 + given(activityRepository.findById(nonExistentId)).willReturn(Optional.empty()); + + // when & then + CustomException exception = assertThrows(CustomException.class, + () -> activityService.updateActivity(nonExistentId, request)); + assertThat(exception.getErrorCode()).isEqualTo(ErrorCode.NOT_FOUND); + verify(activityRepository).findById(nonExistentId); + } + + @Test + @DisplayName("활동 삭제 성공") + void deleteActivity() { + // given + long activityId = 1L; + given(activityRepository.existsById(activityId)).willReturn(true); + + // when + activityService.deleteActivity(activityId); + + // then + verify(activityRepository).existsById(activityId); + verify(activityRepository).deleteById(activityId); + } + + @Test + @DisplayName("활동 삭제 실패 - 존재하지 않는 ID") + void deleteActivity_whenNotFound_thenThrowException() { + // given + long nonExistentId = 99L; + given(activityRepository.existsById(nonExistentId)).willReturn(false); + + // when & then + CustomException exception = assertThrows(CustomException.class, + () -> activityService.deleteActivity(nonExistentId)); + assertThat(exception.getErrorCode()).isEqualTo(ErrorCode.NOT_FOUND); + verify(activityRepository).existsById(nonExistentId); + verify(activityRepository, never()).deleteById(nonExistentId); + } + + // --- Helper Methods --- + + // [수정] 파라미터 타입을 String에서 Section으로 변경 + private ActivityRequestDto createRequestDto(Section section) { + return ActivityRequestDto.builder() + .activityDate(LocalDate.of(2024, 5, 10)) + .section(section.getName()) // Section 객체를 직접 전달 + .title("새로운 활동 제목") + .award("최우수상") + .build(); + } + + private Section createSection(Long id, String sectionName) { + return Section.builder() + .id(id) + .name(sectionName) + .build(); + } + + private Activity createActivity(Long id, String title, Section section, LocalDate date) { + return Activity.builder() + .id(id) + .activityDate(date) + .section(section) + .title(title) + .award("테스트 상") + .build(); + } +}