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
Expand Up @@ -19,4 +19,6 @@ public enum AdminAuditAction {
FREEBOARD_POST_DELETED,
FREEBOARD_COMMENT_DELETED,
ADMIN_MESSAGE_SENT,
BIRD_CREATED,
BIRD_UPDATED,
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ public enum AdminAuditTargetType {
FREEBOARD_POST,
FREEBOARD_COMMENT,
ADMIN_MESSAGE,
BIRD,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package org.devkor.apu.saerok_server.domain.admin.dex.api;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.devkor.apu.saerok_server.domain.admin.dex.api.dto.request.AdminBirdImagePresignRequest;
import org.devkor.apu.saerok_server.domain.admin.dex.api.dto.request.AdminBirdUpsertRequest;
import org.devkor.apu.saerok_server.domain.admin.dex.api.dto.response.AdminBirdDetailResponse;
import org.devkor.apu.saerok_server.domain.admin.dex.api.dto.response.AdminBirdImagePresignResponse;
import org.devkor.apu.saerok_server.domain.admin.dex.api.dto.response.AdminBirdListResponse;
import org.devkor.apu.saerok_server.domain.admin.dex.application.AdminBirdService;
import org.devkor.apu.saerok_server.domain.admin.dex.application.dto.AdminBirdListCommand;
import org.devkor.apu.saerok_server.global.security.principal.UserPrincipal;
import org.devkor.apu.saerok_server.global.shared.exception.BadRequestException;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
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;

@Tag(name = "Admin Dex API", description = "도감 관리용 관리자 API")
@RestController
@RequiredArgsConstructor
@RequestMapping("${api_prefix}/admin/dex/birds")
public class AdminBirdController {

private final AdminBirdService adminBirdService;

@GetMapping
@PreAuthorize("@perm.has('ADMIN_LOGIN')")
@Operation(summary = "관리자 도감 목록 조회", security = @SecurityRequirement(name = "bearerAuth"))
public AdminBirdListResponse listBirds(
@RequestParam(required = false) String q,
@RequestParam(required = false) Integer page,
@RequestParam(required = false) Integer size
) {
AdminBirdListCommand command = new AdminBirdListCommand(page, size, q);
if (!command.hasValidPagination()) {
throw new BadRequestException("page와 size 값이 유효하지 않아요.");
}
return adminBirdService.listBirds(command);
}

@GetMapping("/{birdId}")
@PreAuthorize("@perm.has('ADMIN_LOGIN')")
@Operation(summary = "관리자 도감 상세 조회", security = @SecurityRequirement(name = "bearerAuth"))
public AdminBirdDetailResponse getBird(@PathVariable Long birdId) {
return adminBirdService.getBird(birdId);
}

@PostMapping
@PreAuthorize("@perm.has('ADMIN_LOGIN')")
@Operation(summary = "관리자 도감 등록", security = @SecurityRequirement(name = "bearerAuth"))
public AdminBirdDetailResponse createBird(
@Valid @RequestBody AdminBirdUpsertRequest request,
@AuthenticationPrincipal UserPrincipal admin
) {
return adminBirdService.createBird(admin.getId(), request);
}

@PutMapping("/{birdId}")
@PreAuthorize("@perm.has('ADMIN_LOGIN')")
@Operation(summary = "관리자 도감 수정", security = @SecurityRequirement(name = "bearerAuth"))
public AdminBirdDetailResponse updateBird(
@PathVariable Long birdId,
@Valid @RequestBody AdminBirdUpsertRequest request,
@AuthenticationPrincipal UserPrincipal admin
) {
return adminBirdService.updateBird(admin.getId(), birdId, request);
}

@PostMapping("/image/presign")
@PreAuthorize("@perm.has('ADMIN_LOGIN')")
@Operation(summary = "도감 이미지 Presigned URL 발급", security = @SecurityRequirement(name = "bearerAuth"))
public AdminBirdImagePresignResponse generateImagePresignUrl(
@Valid @RequestBody AdminBirdImagePresignRequest request
) {
return adminBirdService.generateImagePresignUrl(request.contentType());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.devkor.apu.saerok_server.domain.admin.dex.api.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;

public record AdminBirdImagePresignRequest(
@Schema(description = "업로드할 도감 이미지 Content-Type", example = "image/jpeg")
@NotBlank(message = "contentType을 입력해 주세요.")
String contentType
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package org.devkor.apu.saerok_server.domain.admin.dex.api.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import java.util.List;
import org.devkor.apu.saerok_server.domain.dex.bird.core.enums.ConservationGrade;
import org.devkor.apu.saerok_server.domain.dex.bird.core.enums.HabitatType;

public record AdminBirdUpsertRequest(
@Valid
@NotNull(message = "이름 정보를 입력해 주세요.")
Name name,

@Valid
@NotNull(message = "분류 정보를 입력해 주세요.")
Taxonomy taxonomy,

@Valid
Description description,

@Schema(description = "체장(cm)", example = "47.5")
@Positive(message = "체장은 0보다 커야 합니다.")
Double bodyLengthCm,

@Schema(description = "국립생물자원관 URL")
String nibrUrl,

@NotNull(message = "보호등급을 선택해 주세요.")
ConservationGrade conservationGrade,

@NotEmpty(message = "서식지를 하나 이상 선택해 주세요.")
List<@NotNull(message = "서식지를 선택해 주세요.") HabitatType> habitats,

@Valid
@NotEmpty(message = "체류/희귀도 정보를 하나 이상 입력해 주세요.")
List<@NotNull(message = "체류/희귀도 정보를 입력해 주세요.") @Valid Residency> residencies,

@Valid
@NotEmpty(message = "대표 이미지를 업로드해 주세요.")
List<@NotNull(message = "이미지 정보를 입력해 주세요.") @Valid Image> images
) {
public record Name(
@Schema(description = "국문명", example = "해오라기")
@NotBlank(message = "국문명을 입력해 주세요.")
String koreanName,

@Schema(description = "학명", example = "Butorides striata")
@NotBlank(message = "학명을 입력해 주세요.")
String scientificName,

@Schema(description = "학명 명명자", example = "Linnaeus")
String scientificAuthor,

@Schema(description = "학명 명명년도", example = "1758")
Integer scientificYear
) {
}

public record Taxonomy(
@Schema(description = "문 영문명", example = "Chordata")
@NotBlank(message = "문 영문명을 입력해 주세요.")
String phylumEng,

@Schema(description = "문 국문명", example = "척삭동물문")
@NotBlank(message = "문 국문명을 입력해 주세요.")
String phylumKor,

@Schema(description = "강 영문명", example = "Aves")
@NotBlank(message = "강 영문명을 입력해 주세요.")
String classEng,

@Schema(description = "강 국문명", example = "조강")
@NotBlank(message = "강 국문명을 입력해 주세요.")
String classKor,

@Schema(description = "목 영문명", example = "Pelecaniformes")
@NotBlank(message = "목 영문명을 입력해 주세요.")
String orderEng,

@Schema(description = "목 국문명", example = "사다새목")
@NotBlank(message = "목 국문명을 입력해 주세요.")
String orderKor,

@Schema(description = "과 영문명", example = "Ardeidae")
@NotBlank(message = "과 영문명을 입력해 주세요.")
String familyEng,

@Schema(description = "과 국문명", example = "왜가리과")
@NotBlank(message = "과 국문명을 입력해 주세요.")
String familyKor,

@Schema(description = "속 영문명", example = "Butorides")
@NotBlank(message = "속 영문명을 입력해 주세요.")
String genusEng,

@Schema(description = "속 국문명", example = "해오라기속")
@NotBlank(message = "속 국문명을 입력해 주세요.")
String genusKor,

@Schema(description = "종 영문명", example = "striata")
@NotBlank(message = "종 영문명을 입력해 주세요.")
String speciesEng,

@Schema(description = "종 국문명", example = "해오라기")
@NotBlank(message = "종 국문명을 입력해 주세요.")
String speciesKor
) {
}

public record Description(
@Schema(description = "도감 설명")
String description,

@Schema(description = "설명 출처")
String source,

@Schema(description = "AI 생성 설명 여부")
Boolean isAiGenerated
) {
}

public record Residency(
@Schema(description = "체류 형태", example = "RESIDENT")
@NotNull(message = "체류 형태를 선택해 주세요.")
org.devkor.apu.saerok_server.domain.dex.residency.entity.ResidencyType residencyType,

@Schema(description = "희귀도", example = "COMMON")
@NotNull(message = "희귀도를 선택해 주세요.")
org.devkor.apu.saerok_server.domain.dex.residency.entity.RarityType rarity,

@Schema(description = "월 비트마스크. 비우면 체류 형태 기본 월 범위를 사용합니다.", example = "4095")
@Min(value = 0, message = "월 비트마스크는 0 이상이어야 합니다.")
@Max(value = 4095, message = "월 비트마스크는 4095 이하여야 합니다.")
Integer monthBitmask
) {
}

public record Image(
@Schema(description = "업로드된 도감 이미지 S3 object key", example = "raw/uuid.jpg")
@NotBlank(message = "이미지를 업로드해 주세요.")
String objectKey,

@Schema(description = "이미지 원본 출처 URL", example = "https://commons.wikimedia.org/...")
@NotBlank(message = "이미지 원본 출처 URL을 입력해 주세요.")
String originalUrl
) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package org.devkor.apu.saerok_server.domain.admin.dex.api.dto.response;

import java.time.OffsetDateTime;
import java.util.List;
import org.devkor.apu.saerok_server.domain.dex.bird.core.enums.ConservationGrade;
import org.devkor.apu.saerok_server.domain.dex.bird.core.enums.HabitatType;
import org.devkor.apu.saerok_server.domain.dex.residency.entity.RarityType;
import org.devkor.apu.saerok_server.domain.dex.residency.entity.ResidencyType;

public record AdminBirdDetailResponse(
Long id,
BirdName name,
BirdTaxonomy taxonomy,
BirdDescription description,
Double bodyLengthCm,
String nibrUrl,
ConservationGrade conservationGrade,
List<HabitatType> habitats,
List<Residency> residencies,
List<SeasonWithRarity> seasonsWithRarity,
List<Image> images,
OffsetDateTime createdAt,
OffsetDateTime updatedAt
) {
public record BirdName(
String koreanName,
String scientificName,
String scientificAuthor,
Integer scientificYear
) {
}

public record BirdTaxonomy(
String phylumEng,
String phylumKor,
String classEng,
String classKor,
String orderEng,
String orderKor,
String familyEng,
String familyKor,
String genusEng,
String genusKor,
String speciesEng,
String speciesKor
) {
}

public record BirdDescription(
String description,
String source,
Boolean isAiGenerated
) {
}

public record SeasonWithRarity(
String season,
String rarity,
Integer priority
) {
}

public record Residency(
ResidencyType residencyType,
RarityType rarity,
Integer monthBitmask,
Integer effectiveMonthBitmask
) {
}

public record Image(
String objectKey,
String imageUrl,
String originalUrl,
Integer orderIndex,
Boolean isThumb
) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.devkor.apu.saerok_server.domain.admin.dex.api.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;

public record AdminBirdImagePresignResponse(
@Schema(description = "클라이언트가 이미지를 PUT 업로드할 Presigned URL")
String presignedUrl,

@Schema(description = "업로드된 도감 이미지 S3 object key", example = "raw/uuid.jpg")
String objectKey
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.devkor.apu.saerok_server.domain.admin.dex.api.dto.response;

import java.time.OffsetDateTime;
import java.util.List;
import org.devkor.apu.saerok_server.domain.dex.bird.core.enums.ConservationGrade;
import org.devkor.apu.saerok_server.domain.dex.bird.core.enums.HabitatType;

public record AdminBirdListResponse(
List<Item> birds,
int page,
int size,
long totalElements,
int totalPages
) {
public record Item(
Long id,
String koreanName,
String scientificName,
ConservationGrade conservationGrade,
Double bodyLengthCm,
List<HabitatType> habitats,
String thumbImageUrl,
OffsetDateTime updatedAt
) {
}
}
Loading
Loading