Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
}
Copy link
Member

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() 를 호출해서 객체 인스턴스를 생성/초기화 하도록 구성해주세요

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네 수정하겠습니다

}
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;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이너 클래스 대신 외부로 분리해주세요

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네 확인했습니다 수정하겠습니다


@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();
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이너 클래스 대신 외부로 분리해주세요

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네 확인했습니다 수정하겠습니다


// 응답 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());
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그룹핑을 수행하긴 하지만 어쨌든 팩토리 메소드의 역할을 하고 있기 때문에 메소드 이름을 toDto 또는 of 라는 네이밍으로 수정하면 좋을 것 같습니다

}
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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ActivityHistoryRequestDto에 필드 크기 제약조건을 설정했다면 그를 저장하는 속성의 크기도 그에 맞게 설정해주세요.
length 설정이 없으면 기본적으로 길이가 255로 설정되기 때문에 의도치 않게 길이가 긴 항목도 저장될 수 있습니다.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네 확인했습니다 수정하겠습니다


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);
}
Loading
Loading