Skip to content
Open
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,110 @@
package com.bcsdlab.bcsdinternalapiv2.activity.controller;

import com.bcsdlab.bcsdinternalapiv2.activity.controller.dto.request.ActivityCreateRequest;
import com.bcsdlab.bcsdinternalapiv2.activity.controller.dto.request.ActivityImagesReplaceRequest;
import com.bcsdlab.bcsdinternalapiv2.activity.controller.dto.request.ActivityUpdateRequest;
import com.bcsdlab.bcsdinternalapiv2.activity.controller.dto.response.AdminActivityDetailResponse;
import com.bcsdlab.bcsdinternalapiv2.activity.controller.dto.response.AdminActivitySummaryResponse;
import com.bcsdlab.bcsdinternalapiv2.global.controller.dto.request.OrderRequest;
import com.bcsdlab.bcsdinternalapiv2.global.controller.dto.request.PublishRequest;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
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.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import java.util.List;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;

@Tag(name = "관리자 - 활동 API")
@SecurityRequirement(name = "JWT")
public interface AdminActivityApi {

@ApiResponses(value = {
@ApiResponse(responseCode = "200"),
@ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))),
})
@Operation(summary = "활동 목록", description = "숨김 포함. categoryId/year/published로 선택적으로 필터링한다.")
Page<AdminActivitySummaryResponse> getActivities(@RequestParam(required = false) Long categoryId,
@RequestParam(required = false) Integer year,
@RequestParam(required = false) Boolean published,
Pageable pageable);

@ApiResponses(value = {
@ApiResponse(responseCode = "200"),
@ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))),
})
@Operation(summary = "활동 상세 조회")
AdminActivityDetailResponse getActivity(@PathVariable Long id);

@ApiResponses(value = {
@ApiResponse(responseCode = "201"),
@ApiResponse(responseCode = "400", content = @Content(schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))),
})
@Operation(summary = "활동 생성", description = "본문은 저장 전 정제된다(ADR-008). 생성 직후 공개 상태다.")
ResponseEntity<AdminActivityDetailResponse> createActivity(@RequestBody @Valid ActivityCreateRequest request);

@ApiResponses(value = {
@ApiResponse(responseCode = "200"),
@ApiResponse(responseCode = "400", content = @Content(schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))),
})
@Operation(summary = "활동 수정")
AdminActivityDetailResponse updateActivity(@PathVariable Long id,
@RequestBody @Valid ActivityUpdateRequest request);

@ApiResponses(value = {
@ApiResponse(responseCode = "204"),
@ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))),
})
@Operation(summary = "활동 삭제", description = "soft delete — 데이터는 보존된다.")
ResponseEntity<Void> deleteActivity(@PathVariable Long id);

@ApiResponses(value = {
@ApiResponse(responseCode = "204"),
@ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))),
})
@Operation(summary = "공개/숨김")
ResponseEntity<Void> publish(@PathVariable Long id, @RequestBody @Valid PublishRequest request);

@ApiResponses(value = {
@ApiResponse(responseCode = "204"),
@ApiResponse(responseCode = "400", content = @Content(schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))),
})
@Operation(summary = "같은 (카테고리, 연, 월) 안에서 순서 변경")
ResponseEntity<Void> reorder(@Parameter(required = true) @RequestParam Long categoryId,
@Parameter(required = true) @RequestParam int year,
@Parameter(required = true) @RequestParam int month,
@RequestBody @Valid OrderRequest request);

@ApiResponses(value = {
@ApiResponse(responseCode = "200"),
@ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))),
})
@Operation(summary = "활동 사진 전체 교체", description = "첫 번째 항목이 목록 썸네일이다(INV-12).")
List<String> replaceImages(@PathVariable Long id, @RequestBody @Valid ActivityImagesReplaceRequest request);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package com.bcsdlab.bcsdinternalapiv2.activity.controller;

import com.bcsdlab.bcsdinternalapiv2.activity.controller.dto.request.ActivityCreateRequest;
import com.bcsdlab.bcsdinternalapiv2.activity.controller.dto.request.ActivityImagesReplaceRequest;
import com.bcsdlab.bcsdinternalapiv2.activity.controller.dto.request.ActivityUpdateRequest;
import com.bcsdlab.bcsdinternalapiv2.activity.controller.dto.response.AdminActivityDetailResponse;
import com.bcsdlab.bcsdinternalapiv2.activity.controller.dto.response.AdminActivitySummaryResponse;
import com.bcsdlab.bcsdinternalapiv2.activity.service.AdminActivityService;
import com.bcsdlab.bcsdinternalapiv2.global.controller.dto.request.OrderRequest;
import com.bcsdlab.bcsdinternalapiv2.global.controller.dto.request.PublishRequest;
import jakarta.validation.Valid;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/v1/admin/activities")
@RequiredArgsConstructor
public class AdminActivityController implements AdminActivityApi {

private final AdminActivityService adminActivityService;

@Override
@GetMapping
public Page<AdminActivitySummaryResponse> getActivities(@RequestParam(required = false) Long categoryId,
@RequestParam(required = false) Integer year,
@RequestParam(required = false) Boolean published,
Pageable pageable) {
return adminActivityService.getActivities(categoryId, year, published, pageable);
}

@Override
@GetMapping("/{id}")
public AdminActivityDetailResponse getActivity(@PathVariable Long id) {
return adminActivityService.getActivity(id);
}

@Override
@PostMapping
public ResponseEntity<AdminActivityDetailResponse> createActivity(
@Valid @RequestBody ActivityCreateRequest request) {
return ResponseEntity.status(HttpStatus.CREATED).body(adminActivityService.createActivity(request));
}

@Override
@PutMapping("/{id}")
public AdminActivityDetailResponse updateActivity(@PathVariable Long id,
@Valid @RequestBody ActivityUpdateRequest request) {
return adminActivityService.updateActivity(id, request);
}

@Override
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteActivity(@PathVariable Long id) {
adminActivityService.deleteActivity(id);
return ResponseEntity.noContent().build();
}

@Override
@PatchMapping("/{id}/publish")
public ResponseEntity<Void> publish(@PathVariable Long id, @Valid @RequestBody PublishRequest request) {
adminActivityService.publish(id, request);
return ResponseEntity.noContent().build();
}

@Override
@PatchMapping("/order")
public ResponseEntity<Void> reorder(@RequestParam Long categoryId, @RequestParam int year,
@RequestParam int month, @Valid @RequestBody OrderRequest request) {
adminActivityService.reorder(categoryId, year, month, request);
return ResponseEntity.noContent().build();
}

@Override
@PutMapping("/{id}/images")
public List<String> replaceImages(@PathVariable Long id,
@Valid @RequestBody ActivityImagesReplaceRequest request) {
return adminActivityService.replaceImages(id, request);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.bcsdlab.bcsdinternalapiv2.activity.controller.dto.request;

import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

/**
* 공개/숨김은 별도 PATCH .../publish로만 다룬다(트랙 페이지·커리큘럼과 동일한 규약).
* 사진은 PUT .../images 전체 교체로만 다룬다 — 항목이 여러 개고 "드래그 후 저장" 화면이라
* study-points/tech-stacks와 같은 패턴을 쓴다.
*/
public record ActivityCreateRequest(
@NotNull Long categoryId,

@NotNull
@Min(2000)
@Max(2100)
Integer year,

@NotNull
@Min(1)
@Max(12)
Integer month,

@NotBlank
@Size(max = 80)
String title,

@NotBlank
@Size(max = 200)
String summary,

String content,

String externalUrl
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.bcsdlab.bcsdinternalapiv2.activity.controller.dto.request;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.util.List;

/**
* 활동 사진 전체 교체. 배열의 첫 번째가 목록 썸네일이다(INV-12).
*/
public record ActivityImagesReplaceRequest(
@NotNull
List<@NotBlank String> imageUrls
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.bcsdlab.bcsdinternalapiv2.activity.controller.dto.request;

import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

/**
* categoryId는 수정 대상이 아니다 — 카테고리를 옮기는 요구가 없고, 잘못 만들었으면
* 삭제 후 다시 만드는 편이 트리(사진 포함)를 옮기는 것보다 단순하다.
*/
public record ActivityUpdateRequest(
@NotNull
@Min(2000)
@Max(2100)
Integer year,

@NotNull
@Min(1)
@Max(12)
Integer month,

@NotBlank
@Size(max = 80)
String title,

@NotBlank
@Size(max = 200)
String summary,

String content,

String externalUrl
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.bcsdlab.bcsdinternalapiv2.activity.controller.dto.response;

import com.bcsdlab.bcsdinternalapiv2.activity.model.Activity;
import java.util.List;

public record AdminActivityDetailResponse(
Long id,
Long categoryId,
Integer year,
Integer month,
String title,
String summary,
String content,
String externalUrl,
boolean isPublished,
int displayOrder,
List<String> imageUrls
) {
public static AdminActivityDetailResponse of(Activity activity, List<String> imageUrls) {
return new AdminActivityDetailResponse(
activity.getId(),
activity.getCategory().getId(),
activity.getYear(),
activity.getMonth(),
activity.getTitle(),
activity.getSummary(),
activity.getContent(),
activity.getExternalUrl(),
activity.isPublished(),
activity.getDisplayOrder(),
imageUrls
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.bcsdlab.bcsdinternalapiv2.activity.controller.dto.response;

import com.bcsdlab.bcsdinternalapiv2.activity.model.Activity;

public record AdminActivitySummaryResponse(
Long id,
Integer year,
Integer month,
String title,
String summary,
boolean isPublished,
int displayOrder
) {
public static AdminActivitySummaryResponse from(Activity activity) {
return new AdminActivitySummaryResponse(
activity.getId(), activity.getYear(), activity.getMonth(), activity.getTitle(),
activity.getSummary(), activity.isPublished(), activity.getDisplayOrder());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public enum ActivityExceptionType implements BcsdExceptionType {
ACTIVITY_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 활동입니다."),
CATEGORY_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 활동 카테고리입니다."),
CATEGORY_SLUG_DUPLICATED(HttpStatus.CONFLICT, "이미 사용 중인 카테고리 주소입니다."),
CATEGORY_HAS_ACTIVITIES(HttpStatus.CONFLICT, "활동이 남아 있는 카테고리는 삭제할 수 없습니다."),
;

private final HttpStatus status;
Expand Down
Loading