-
Notifications
You must be signed in to change notification settings - Fork 0
[feat] 활동 연혁 CRUD 기능 구현 및 테스트 #95
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 7 commits
070c798
2d827cd
6899774
8f497a7
38ab213
903de41
181ed4e
4fdba6b
1c73794
27c1687
fb365ca
77d1538
9145987
0b4d239
2a6a2d7
87a1187
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| package dmu.dasom.api.domain.activity.controller; | ||
|
|
||
| import dmu.dasom.api.domain.activity.dto.GroupedActivityHistoryDto; | ||
| import dmu.dasom.api.domain.activity.service.ActivityHistoryService; | ||
| 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/activity/histories") | ||
| @RequiredArgsConstructor | ||
| @Tag(name = "Activity History API", description = "활동 연혁 API") | ||
| public class ActivityHistoryController { | ||
|
|
||
| private final ActivityHistoryService historyService; | ||
|
|
||
| @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<List<GroupedActivityHistoryDto>> getAllHistories() { | ||
| return ResponseEntity.ok(historyService.getAllHistories()); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| package dmu.dasom.api.domain.activity.controller; | ||
|
|
||
| import dmu.dasom.api.domain.activity.dto.ActivityHistoryRequestDto; | ||
| import dmu.dasom.api.domain.activity.dto.ActivityHistoryResponseDto; | ||
| import dmu.dasom.api.domain.activity.service.ActivityHistoryService; | ||
| 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/activity/histories") | ||
| @RequiredArgsConstructor | ||
| @Tag(name = "ADMIN - Activity History API", description = "어드민 활동 연혁 관리 API") | ||
| public class AdminActivityHistoryController { | ||
|
|
||
| private final ActivityHistoryService historyService; | ||
|
|
||
| @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<ActivityHistoryResponseDto> createHistory(@Valid @RequestBody ActivityHistoryRequestDto requestDto) { | ||
| return ResponseEntity.status(201).body(historyService.createHistory(requestDto)); | ||
| } | ||
|
|
||
| @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("/{id}") | ||
| public ResponseEntity<ActivityHistoryResponseDto> updateHistory( | ||
| @PathVariable @Min(1) Long id, | ||
| @Valid @RequestBody ActivityHistoryRequestDto requestDto | ||
| ) { | ||
| return ResponseEntity.ok(historyService.updateHistory(id, requestDto)); | ||
| } | ||
|
|
||
| @Operation(summary = "활동 연혁 삭제") | ||
| @ApiResponses(value = { | ||
| @ApiResponse(responseCode = "200", 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("/{id}") | ||
| public ResponseEntity<Void> deleteHistory(@PathVariable Long id) { | ||
| historyService.deleteHistory(id); | ||
| return ResponseEntity.noContent().build(); // 삭제 성공 시에는 204 No Content가 더 명확합니다. | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| package dmu.dasom.api.domain.activity.dto; | ||
|
|
||
| import dmu.dasom.api.domain.activity.entity.ActivityHistory; | ||
| import io.swagger.v3.oas.annotations.media.Schema; | ||
| import jakarta.validation.constraints.Min; | ||
| import jakarta.validation.constraints.NotBlank; | ||
| import jakarta.validation.constraints.NotNull; | ||
| import jakarta.validation.constraints.Size; | ||
| import lombok.Getter; | ||
|
|
||
| @Getter | ||
| @Schema(name = "ActivityHistoryRequestDto", description = "활동 연혁 생성/수정 요청 DTO") | ||
| public class ActivityHistoryRequestDto { | ||
|
|
||
| @NotNull(message = "연도는 필수입니다.") | ||
| @Min(value = 1992, message = "연도는 1992년 이상이어야 합니다.") | ||
| @Schema(description = "활동 연도", example = "2024", minimum = "1992", maximum = "2050") | ||
| private int year; | ||
|
|
||
| @NotBlank(message = "섹션 제목은 필수입니다.") | ||
| @Size(max = 50, message = "섹션은 50자 이내로 입력해주세요.") | ||
| @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; | ||
|
|
||
| public ActivityHistory toEntity() { | ||
| return ActivityHistory.builder() | ||
| .year(this.year) | ||
| .section(this.section) | ||
| .title(this.title) | ||
| .award(this.award) | ||
| .build(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| package dmu.dasom.api.domain.activity.dto; | ||
|
|
||
| import dmu.dasom.api.domain.activity.entity.ActivityHistory; | ||
| import io.swagger.v3.oas.annotations.media.Schema; | ||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
|
|
||
| @Getter | ||
| @Builder | ||
| @Schema(name = "ActivityHistoryResponseDto", description = "활동 연혁 단일 조회 응답 DTO") | ||
| public class ActivityHistoryResponseDto { // 생성, 수정 응답 DTO | ||
|
|
||
| @Schema(description = "활동 연혁 고유 ID", example = "1") | ||
| private final Long id; | ||
|
|
||
| @Schema(description = "활동 연도", example = "2024") | ||
| private final int year; | ||
|
|
||
| @Schema(description = "활동 섹션", example = "교내 경진대회") | ||
| private final String section; | ||
|
|
||
| @Schema(description = "활동 제목", example = "컴퓨터 공학부 경진대회") | ||
| private final String title; | ||
|
|
||
| @Schema(description = "수상 내역", example = "최우수상") | ||
| private final String award; | ||
|
|
||
| public static ActivityHistoryResponseDto toDto(ActivityHistory history) { | ||
| return ActivityHistoryResponseDto.builder() | ||
| .id(history.getId()) | ||
| .year(history.getYear()) | ||
| .section(history.getSection()) | ||
| .title(history.getTitle()) | ||
| .award(history.getAward()) | ||
| .build(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| package dmu.dasom.api.domain.activity.dto; | ||
|
|
||
| import dmu.dasom.api.domain.activity.entity.ActivityHistory; | ||
| import io.swagger.v3.oas.annotations.media.Schema; | ||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
|
|
||
| import java.util.Comparator; | ||
| import java.util.List; | ||
| import java.util.stream.Collectors; | ||
|
|
||
| @Getter | ||
| @Builder | ||
| @Schema(name = "GroupedActivityHistoryDto", description = "활동 연혁 전체 조회 응답 DTO (연도별/섹션별 그룹화)") | ||
| public class GroupedActivityHistoryDto { // 전체 조회 응답 DTO | ||
|
|
||
| @Schema(description = "활동 연도", example = "2024") | ||
| private final int year; | ||
|
|
||
| @Schema(description = "해당 연도 섹션") | ||
| private final List<SectionItemDto> sections; | ||
|
|
||
| @Getter @Builder | ||
| @Schema(name = "SectionItemDto", description = "섹션별 활동 목록") | ||
| public static class SectionItemDto { | ||
|
|
||
| @Schema(description = "활동 섹션", example = "교내 경진대회") | ||
| private final String section; | ||
|
|
||
| @Schema(description = "활동 목록") | ||
| private final List<ActivityItemDto> activities; | ||
| } | ||
|
||
|
|
||
| @Getter @Builder | ||
| @Schema(name = "ActivityItemDto", description = "개별 활동 목록") | ||
| public static class ActivityItemDto { | ||
|
|
||
| @Schema(description = "활동 연혁 고유 ID", example = "1") | ||
| private final Long id; | ||
|
|
||
| @Schema(description = "활동 제목", example = "컴퓨터 공학부 경진대회") | ||
| private final String title; | ||
|
|
||
| @Schema(description = "수상 내역", example = "최우수상") | ||
| private final String award; | ||
|
|
||
| public static ActivityItemDto toDto(ActivityHistory history) { | ||
| return ActivityItemDto.builder() | ||
| .id(history.getId()).title(history.getTitle()) | ||
| .award(history.getAward()) | ||
| .build(); | ||
| } | ||
| } | ||
|
||
|
|
||
| // 응답 DTO를 계층적으로 그룹핑하는 함수 | ||
| public static List<GroupedActivityHistoryDto> groupedActivityHistoryDto(List<ActivityHistory> histories) { | ||
| return histories.stream() | ||
| .collect(Collectors.groupingBy(ActivityHistory::getYear)) | ||
| .entrySet().stream() | ||
| .sorted(Comparator.comparing(java.util.Map.Entry::getKey, Comparator.reverseOrder())) | ||
| .map(entryByYear -> { | ||
| List<SectionItemDto> sectionItems = entryByYear.getValue().stream() | ||
| .collect(Collectors.groupingBy(ActivityHistory::getSection)) | ||
| .entrySet().stream() | ||
| .map(entryBySection -> { | ||
| List<ActivityItemDto> activityItems = entryBySection.getValue().stream() | ||
| .map(ActivityItemDto::toDto) | ||
| .collect(Collectors.toList()); | ||
| return SectionItemDto.builder() | ||
| .section(entryBySection.getKey()) | ||
| .activities(activityItems) | ||
| .build(); | ||
| }) | ||
| .collect(Collectors.toList()); | ||
| return GroupedActivityHistoryDto.builder() | ||
| .year(entryByYear.getKey()) | ||
| .sections(sectionItems) | ||
| .build(); | ||
| }) | ||
| .collect(Collectors.toList()); | ||
| } | ||
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| package dmu.dasom.api.domain.activity.entity; | ||
|
|
||
| import jakarta.persistence.*; | ||
| import lombok.*; | ||
|
|
||
| @Entity | ||
| @Getter | ||
| @Builder | ||
| @AllArgsConstructor | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| public class ActivityHistory { | ||
|
|
||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| private Long id; | ||
|
|
||
| private int year; | ||
| private String section; | ||
| private String title; | ||
| private String award; | ||
|
||
|
|
||
| public void update(int year, String section, String title, String award) { | ||
| this.year = year; | ||
| this.section = section; | ||
| this.title = title; | ||
| this.award = award; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| package dmu.dasom.api.domain.activity.repository; | ||
|
|
||
| import dmu.dasom.api.domain.activity.entity.ActivityHistory; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
|
|
||
| public interface ActivityHistoryRepository extends JpaRepository<ActivityHistory, Long> { | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package dmu.dasom.api.domain.activity.service; | ||
|
|
||
| import dmu.dasom.api.domain.activity.dto.ActivityHistoryRequestDto; | ||
| import dmu.dasom.api.domain.activity.dto.ActivityHistoryResponseDto; | ||
| import dmu.dasom.api.domain.activity.dto.GroupedActivityHistoryDto; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| public interface ActivityHistoryService { | ||
|
|
||
| List<GroupedActivityHistoryDto> getAllHistories(); | ||
|
|
||
| ActivityHistoryResponseDto createHistory(ActivityHistoryRequestDto requestDto); | ||
|
|
||
| ActivityHistoryResponseDto updateHistory(Long id, ActivityHistoryRequestDto requestDto); | ||
|
|
||
| void deleteHistory(Long id); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ActivieyHistory의 팩토리 메소드를ActivieyHistory클래스 내부로 옮기고Service계층에서Dto->Entity로 변환할 때ActivieyHistory.create()를 호출해서 객체 인스턴스를 생성/초기화 하도록 구성해주세요There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
네 수정하겠습니다