Skip to content

Commit f1cf6b9

Browse files
authored
Merge pull request #324 from DevKor-github/feat/dex-register
feat: 도감 등록 기능 구현
2 parents 527bab0 + f3580e6 commit f1cf6b9

32 files changed

Lines changed: 1900 additions & 17 deletions

src/main/java/org/devkor/apu/saerok_server/domain/admin/audit/core/entity/AdminAuditAction.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,6 @@ public enum AdminAuditAction {
1919
FREEBOARD_POST_DELETED,
2020
FREEBOARD_COMMENT_DELETED,
2121
ADMIN_MESSAGE_SENT,
22+
BIRD_CREATED,
23+
BIRD_UPDATED,
2224
}

src/main/java/org/devkor/apu/saerok_server/domain/admin/audit/core/entity/AdminAuditTargetType.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@ public enum AdminAuditTargetType {
1414
FREEBOARD_POST,
1515
FREEBOARD_COMMENT,
1616
ADMIN_MESSAGE,
17+
BIRD,
1718
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package org.devkor.apu.saerok_server.domain.admin.dex.api;
2+
3+
import io.swagger.v3.oas.annotations.Operation;
4+
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
5+
import io.swagger.v3.oas.annotations.tags.Tag;
6+
import jakarta.validation.Valid;
7+
import lombok.RequiredArgsConstructor;
8+
import org.devkor.apu.saerok_server.domain.admin.dex.api.dto.request.AdminBirdImagePresignRequest;
9+
import org.devkor.apu.saerok_server.domain.admin.dex.api.dto.request.AdminBirdUpsertRequest;
10+
import org.devkor.apu.saerok_server.domain.admin.dex.api.dto.response.AdminBirdDetailResponse;
11+
import org.devkor.apu.saerok_server.domain.admin.dex.api.dto.response.AdminBirdImagePresignResponse;
12+
import org.devkor.apu.saerok_server.domain.admin.dex.api.dto.response.AdminBirdListResponse;
13+
import org.devkor.apu.saerok_server.domain.admin.dex.application.AdminBirdService;
14+
import org.devkor.apu.saerok_server.domain.admin.dex.application.dto.AdminBirdListCommand;
15+
import org.devkor.apu.saerok_server.global.security.principal.UserPrincipal;
16+
import org.devkor.apu.saerok_server.global.shared.exception.BadRequestException;
17+
import org.springframework.security.access.prepost.PreAuthorize;
18+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
19+
import org.springframework.web.bind.annotation.GetMapping;
20+
import org.springframework.web.bind.annotation.PathVariable;
21+
import org.springframework.web.bind.annotation.PostMapping;
22+
import org.springframework.web.bind.annotation.PutMapping;
23+
import org.springframework.web.bind.annotation.RequestBody;
24+
import org.springframework.web.bind.annotation.RequestMapping;
25+
import org.springframework.web.bind.annotation.RequestParam;
26+
import org.springframework.web.bind.annotation.RestController;
27+
28+
@Tag(name = "Admin Dex API", description = "도감 관리용 관리자 API")
29+
@RestController
30+
@RequiredArgsConstructor
31+
@RequestMapping("${api_prefix}/admin/dex/birds")
32+
public class AdminBirdController {
33+
34+
private final AdminBirdService adminBirdService;
35+
36+
@GetMapping
37+
@PreAuthorize("@perm.has('ADMIN_LOGIN')")
38+
@Operation(summary = "관리자 도감 목록 조회", security = @SecurityRequirement(name = "bearerAuth"))
39+
public AdminBirdListResponse listBirds(
40+
@RequestParam(required = false) String q,
41+
@RequestParam(required = false) Integer page,
42+
@RequestParam(required = false) Integer size
43+
) {
44+
AdminBirdListCommand command = new AdminBirdListCommand(page, size, q);
45+
if (!command.hasValidPagination()) {
46+
throw new BadRequestException("page와 size 값이 유효하지 않아요.");
47+
}
48+
return adminBirdService.listBirds(command);
49+
}
50+
51+
@GetMapping("/{birdId}")
52+
@PreAuthorize("@perm.has('ADMIN_LOGIN')")
53+
@Operation(summary = "관리자 도감 상세 조회", security = @SecurityRequirement(name = "bearerAuth"))
54+
public AdminBirdDetailResponse getBird(@PathVariable Long birdId) {
55+
return adminBirdService.getBird(birdId);
56+
}
57+
58+
@PostMapping
59+
@PreAuthorize("@perm.has('ADMIN_LOGIN')")
60+
@Operation(summary = "관리자 도감 등록", security = @SecurityRequirement(name = "bearerAuth"))
61+
public AdminBirdDetailResponse createBird(
62+
@Valid @RequestBody AdminBirdUpsertRequest request,
63+
@AuthenticationPrincipal UserPrincipal admin
64+
) {
65+
return adminBirdService.createBird(admin.getId(), request);
66+
}
67+
68+
@PutMapping("/{birdId}")
69+
@PreAuthorize("@perm.has('ADMIN_LOGIN')")
70+
@Operation(summary = "관리자 도감 수정", security = @SecurityRequirement(name = "bearerAuth"))
71+
public AdminBirdDetailResponse updateBird(
72+
@PathVariable Long birdId,
73+
@Valid @RequestBody AdminBirdUpsertRequest request,
74+
@AuthenticationPrincipal UserPrincipal admin
75+
) {
76+
return adminBirdService.updateBird(admin.getId(), birdId, request);
77+
}
78+
79+
@PostMapping("/image/presign")
80+
@PreAuthorize("@perm.has('ADMIN_LOGIN')")
81+
@Operation(summary = "도감 이미지 Presigned URL 발급", security = @SecurityRequirement(name = "bearerAuth"))
82+
public AdminBirdImagePresignResponse generateImagePresignUrl(
83+
@Valid @RequestBody AdminBirdImagePresignRequest request
84+
) {
85+
return adminBirdService.generateImagePresignUrl(request.contentType());
86+
}
87+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package org.devkor.apu.saerok_server.domain.admin.dex.api.dto.request;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import jakarta.validation.constraints.NotBlank;
5+
6+
public record AdminBirdImagePresignRequest(
7+
@Schema(description = "업로드할 도감 이미지 Content-Type", example = "image/jpeg")
8+
@NotBlank(message = "contentType을 입력해 주세요.")
9+
String contentType
10+
) {
11+
}
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package org.devkor.apu.saerok_server.domain.admin.dex.api.dto.request;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import jakarta.validation.Valid;
5+
import jakarta.validation.constraints.Max;
6+
import jakarta.validation.constraints.Min;
7+
import jakarta.validation.constraints.NotBlank;
8+
import jakarta.validation.constraints.NotEmpty;
9+
import jakarta.validation.constraints.NotNull;
10+
import jakarta.validation.constraints.Positive;
11+
import java.util.List;
12+
import org.devkor.apu.saerok_server.domain.dex.bird.core.enums.ConservationGrade;
13+
import org.devkor.apu.saerok_server.domain.dex.bird.core.enums.HabitatType;
14+
15+
public record AdminBirdUpsertRequest(
16+
@Valid
17+
@NotNull(message = "이름 정보를 입력해 주세요.")
18+
Name name,
19+
20+
@Valid
21+
@NotNull(message = "분류 정보를 입력해 주세요.")
22+
Taxonomy taxonomy,
23+
24+
@Valid
25+
Description description,
26+
27+
@Schema(description = "체장(cm)", example = "47.5")
28+
@Positive(message = "체장은 0보다 커야 합니다.")
29+
Double bodyLengthCm,
30+
31+
@Schema(description = "국립생물자원관 URL")
32+
String nibrUrl,
33+
34+
@NotNull(message = "보호등급을 선택해 주세요.")
35+
ConservationGrade conservationGrade,
36+
37+
@NotEmpty(message = "서식지를 하나 이상 선택해 주세요.")
38+
List<@NotNull(message = "서식지를 선택해 주세요.") HabitatType> habitats,
39+
40+
@Valid
41+
@NotEmpty(message = "체류/희귀도 정보를 하나 이상 입력해 주세요.")
42+
List<@NotNull(message = "체류/희귀도 정보를 입력해 주세요.") @Valid Residency> residencies,
43+
44+
@Valid
45+
@NotEmpty(message = "대표 이미지를 업로드해 주세요.")
46+
List<@NotNull(message = "이미지 정보를 입력해 주세요.") @Valid Image> images
47+
) {
48+
public record Name(
49+
@Schema(description = "국문명", example = "해오라기")
50+
@NotBlank(message = "국문명을 입력해 주세요.")
51+
String koreanName,
52+
53+
@Schema(description = "학명", example = "Butorides striata")
54+
@NotBlank(message = "학명을 입력해 주세요.")
55+
String scientificName,
56+
57+
@Schema(description = "학명 명명자", example = "Linnaeus")
58+
String scientificAuthor,
59+
60+
@Schema(description = "학명 명명년도", example = "1758")
61+
Integer scientificYear
62+
) {
63+
}
64+
65+
public record Taxonomy(
66+
@Schema(description = "문 영문명", example = "Chordata")
67+
@NotBlank(message = "문 영문명을 입력해 주세요.")
68+
String phylumEng,
69+
70+
@Schema(description = "문 국문명", example = "척삭동물문")
71+
@NotBlank(message = "문 국문명을 입력해 주세요.")
72+
String phylumKor,
73+
74+
@Schema(description = "강 영문명", example = "Aves")
75+
@NotBlank(message = "강 영문명을 입력해 주세요.")
76+
String classEng,
77+
78+
@Schema(description = "강 국문명", example = "조강")
79+
@NotBlank(message = "강 국문명을 입력해 주세요.")
80+
String classKor,
81+
82+
@Schema(description = "목 영문명", example = "Pelecaniformes")
83+
@NotBlank(message = "목 영문명을 입력해 주세요.")
84+
String orderEng,
85+
86+
@Schema(description = "목 국문명", example = "사다새목")
87+
@NotBlank(message = "목 국문명을 입력해 주세요.")
88+
String orderKor,
89+
90+
@Schema(description = "과 영문명", example = "Ardeidae")
91+
@NotBlank(message = "과 영문명을 입력해 주세요.")
92+
String familyEng,
93+
94+
@Schema(description = "과 국문명", example = "왜가리과")
95+
@NotBlank(message = "과 국문명을 입력해 주세요.")
96+
String familyKor,
97+
98+
@Schema(description = "속 영문명", example = "Butorides")
99+
@NotBlank(message = "속 영문명을 입력해 주세요.")
100+
String genusEng,
101+
102+
@Schema(description = "속 국문명", example = "해오라기속")
103+
@NotBlank(message = "속 국문명을 입력해 주세요.")
104+
String genusKor,
105+
106+
@Schema(description = "종 영문명", example = "striata")
107+
@NotBlank(message = "종 영문명을 입력해 주세요.")
108+
String speciesEng,
109+
110+
@Schema(description = "종 국문명", example = "해오라기")
111+
@NotBlank(message = "종 국문명을 입력해 주세요.")
112+
String speciesKor
113+
) {
114+
}
115+
116+
public record Description(
117+
@Schema(description = "도감 설명")
118+
String description,
119+
120+
@Schema(description = "설명 출처")
121+
String source,
122+
123+
@Schema(description = "AI 생성 설명 여부")
124+
Boolean isAiGenerated
125+
) {
126+
}
127+
128+
public record Residency(
129+
@Schema(description = "체류 형태", example = "RESIDENT")
130+
@NotNull(message = "체류 형태를 선택해 주세요.")
131+
org.devkor.apu.saerok_server.domain.dex.residency.entity.ResidencyType residencyType,
132+
133+
@Schema(description = "희귀도", example = "COMMON")
134+
@NotNull(message = "희귀도를 선택해 주세요.")
135+
org.devkor.apu.saerok_server.domain.dex.residency.entity.RarityType rarity,
136+
137+
@Schema(description = "월 비트마스크. 비우면 체류 형태 기본 월 범위를 사용합니다.", example = "4095")
138+
@Min(value = 0, message = "월 비트마스크는 0 이상이어야 합니다.")
139+
@Max(value = 4095, message = "월 비트마스크는 4095 이하여야 합니다.")
140+
Integer monthBitmask
141+
) {
142+
}
143+
144+
public record Image(
145+
@Schema(description = "업로드된 도감 이미지 S3 object key", example = "raw/uuid.jpg")
146+
@NotBlank(message = "이미지를 업로드해 주세요.")
147+
String objectKey,
148+
149+
@Schema(description = "이미지 원본 출처 URL", example = "https://commons.wikimedia.org/...")
150+
@NotBlank(message = "이미지 원본 출처 URL을 입력해 주세요.")
151+
String originalUrl
152+
) {
153+
}
154+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package org.devkor.apu.saerok_server.domain.admin.dex.api.dto.response;
2+
3+
import java.time.OffsetDateTime;
4+
import java.util.List;
5+
import org.devkor.apu.saerok_server.domain.dex.bird.core.enums.ConservationGrade;
6+
import org.devkor.apu.saerok_server.domain.dex.bird.core.enums.HabitatType;
7+
import org.devkor.apu.saerok_server.domain.dex.residency.entity.RarityType;
8+
import org.devkor.apu.saerok_server.domain.dex.residency.entity.ResidencyType;
9+
10+
public record AdminBirdDetailResponse(
11+
Long id,
12+
BirdName name,
13+
BirdTaxonomy taxonomy,
14+
BirdDescription description,
15+
Double bodyLengthCm,
16+
String nibrUrl,
17+
ConservationGrade conservationGrade,
18+
List<HabitatType> habitats,
19+
List<Residency> residencies,
20+
List<SeasonWithRarity> seasonsWithRarity,
21+
List<Image> images,
22+
OffsetDateTime createdAt,
23+
OffsetDateTime updatedAt
24+
) {
25+
public record BirdName(
26+
String koreanName,
27+
String scientificName,
28+
String scientificAuthor,
29+
Integer scientificYear
30+
) {
31+
}
32+
33+
public record BirdTaxonomy(
34+
String phylumEng,
35+
String phylumKor,
36+
String classEng,
37+
String classKor,
38+
String orderEng,
39+
String orderKor,
40+
String familyEng,
41+
String familyKor,
42+
String genusEng,
43+
String genusKor,
44+
String speciesEng,
45+
String speciesKor
46+
) {
47+
}
48+
49+
public record BirdDescription(
50+
String description,
51+
String source,
52+
Boolean isAiGenerated
53+
) {
54+
}
55+
56+
public record SeasonWithRarity(
57+
String season,
58+
String rarity,
59+
Integer priority
60+
) {
61+
}
62+
63+
public record Residency(
64+
ResidencyType residencyType,
65+
RarityType rarity,
66+
Integer monthBitmask,
67+
Integer effectiveMonthBitmask
68+
) {
69+
}
70+
71+
public record Image(
72+
String objectKey,
73+
String imageUrl,
74+
String originalUrl,
75+
Integer orderIndex,
76+
Boolean isThumb
77+
) {
78+
}
79+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package org.devkor.apu.saerok_server.domain.admin.dex.api.dto.response;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
5+
public record AdminBirdImagePresignResponse(
6+
@Schema(description = "클라이언트가 이미지를 PUT 업로드할 Presigned URL")
7+
String presignedUrl,
8+
9+
@Schema(description = "업로드된 도감 이미지 S3 object key", example = "raw/uuid.jpg")
10+
String objectKey
11+
) {
12+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package org.devkor.apu.saerok_server.domain.admin.dex.api.dto.response;
2+
3+
import java.time.OffsetDateTime;
4+
import java.util.List;
5+
import org.devkor.apu.saerok_server.domain.dex.bird.core.enums.ConservationGrade;
6+
import org.devkor.apu.saerok_server.domain.dex.bird.core.enums.HabitatType;
7+
8+
public record AdminBirdListResponse(
9+
List<Item> birds,
10+
int page,
11+
int size,
12+
long totalElements,
13+
int totalPages
14+
) {
15+
public record Item(
16+
Long id,
17+
String koreanName,
18+
String scientificName,
19+
ConservationGrade conservationGrade,
20+
Double bodyLengthCm,
21+
List<HabitatType> habitats,
22+
String thumbImageUrl,
23+
OffsetDateTime updatedAt
24+
) {
25+
}
26+
}

0 commit comments

Comments
 (0)