Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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.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<List<ActivityResponseDto>> getActivities() {
return ResponseEntity.ok(activityService.getActivities());
}
}
Original file line number Diff line number Diff line change
@@ -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<Void> 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<Void> 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<Void> deleteActivity(@PathVariable Long activityId) {
activityService.deleteActivity(activityId);
return ResponseEntity.noContent().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.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();
}
}
Original file line number Diff line number Diff line change
@@ -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;

}
Original file line number Diff line number Diff line change
@@ -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<SectionItemDto> sections;

public static List<ActivityResponseDto> of(List<Activity> activities) {
return activities.stream()
.collect(Collectors.groupingBy(activity -> activity.getActivityDate().getYear()))
.entrySet().stream()
.sorted(Map.Entry.comparingByKey()) // 1. 연도 오름차순 정렬
.map(entryByYear -> {
List<SectionItemDto> sectionDtos = groupAndSortSections(entryByYear.getValue());
return ActivityResponseDto.builder()
.year(entryByYear.getKey())
.sections(sectionDtos)
.build();
})
.collect(Collectors.toList());
}

private static List<SectionItemDto> groupAndSortSections(List<Activity> activitiesForYear) {
return activitiesForYear.stream()
.collect(Collectors.groupingBy(Activity::getSection))
.entrySet().stream()
.map(entryBySection -> {
Section section = entryBySection.getKey();
List<ActivityItemDto> 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<ActivityItemDto> mapAndSortActivities(List<Activity> activitiesForSection) {
return activitiesForSection.stream()
// 3. 활동 날짜 오름차순 정렬
.sorted(Comparator.comparing(Activity::getActivityDate))
.map(ActivityItemDto::of)
.collect(Collectors.toList());
}
}
Original file line number Diff line number Diff line change
@@ -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<ActivityItemDto> activities;

public static SectionItemDto of(Section section, List<ActivityItemDto> activities) {
return SectionItemDto.builder()
.id(section.getId())
.section(section.getName())
.activities(activities)
.build();
}
}
47 changes: 47 additions & 0 deletions src/main/java/dmu/dasom/api/domain/activity/entity/Activity.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
33 changes: 33 additions & 0 deletions src/main/java/dmu/dasom/api/domain/activity/entity/Section.java
Original file line number Diff line number Diff line change
@@ -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<Activity> activities = new ArrayList<>();

public static Section create(String name) {
return Section.builder().name(name).build();
}

public void update(String name) {
this.name = name;
}
}
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.Activity;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ActivityRepository extends JpaRepository<Activity, Long> {

}
Loading
Loading