Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
5594d2c
feat: 로그아웃 엔드포인트 추가
pizzazoa Jun 20, 2026
3b72685
feat: 디바이스 토큰 정리 로직 추가
pizzazoa Jun 20, 2026
c79a46e
feat: fcm 토큰을 nullable로 변경, 디바이스 id 행 전체 삭제 -> 토큰만 null로 비활성화로 로직 변경
pizzazoa Jun 20, 2026
527bab0
Merge pull request #323 from DevKor-github/feat/delete-deviceId-logout
pizzazoa Jun 20, 2026
c337234
feat: 도감 등록 기능 추가
pizzazoa Jul 3, 2026
f3580e6
test: 테스트 추가
pizzazoa Jul 3, 2026
f1cf6b9
Merge pull request #324 from DevKor-github/feat/dex-register
pizzazoa Jul 5, 2026
20d5562
feat: 동정 돕기 안 받기 추가
pizzazoa Jul 11, 2026
95b733d
Merge pull request #325 from DevKor-github/feat/bird_id_suggestion
pizzazoa Jul 11, 2026
97624af
feat: 새록이 반환되는 모든 응답에 canSuggestBirdId 추가
pizzazoa Jul 22, 2026
6025f2c
refactor: 이미 birdIdSuggestionEnabled를 받는 응답에선 canSuggestBirdId 응답 필드 제거
pizzazoa Jul 22, 2026
bab2d04
Merge pull request #326 from DevKor-github/feature/canSuggestionBirdId
pizzazoa Jul 22, 2026
3e75be8
feat: 지도에서 새 이름으로 컬렉션 검색 추가
pizzazoa Jul 25, 2026
234e481
feat: geogaphy에 인덱스 추가
pizzazoa Jul 25, 2026
e8ab276
test: 테스트 추가
pizzazoa Jul 25, 2026
db9755c
feat: 위도/경도에 제한 추가, 반경 확장 로직 개선
pizzazoa Jul 25, 2026
c311629
Merge pull request #327 from DevKor-github/feature/search-bird-in-map
pizzazoa Jul 25, 2026
c22a947
chore: 스웨거 문서 편의성 향상
pizzazoa Jul 25, 2026
42cf82f
Merge pull request #328 from DevKor-github/chore/swagger-refactor
pizzazoa Jul 25, 2026
211654b
feat: 도감 상세 응답에 관련 컬렉션 포함
pizzazoa Jul 25, 2026
6f2f3d5
Merge pull request #329 from DevKor-github/feature/collection-in-dex
pizzazoa Jul 25, 2026
b484855
feat: 누적 가입자 수 정적 집계 추가, 플랫폼 누적 가입자 수 로직 변경
pizzazoa Aug 1, 2026
d6c6e26
Merge pull request #330 from DevKor-github/feat/user-data
pizzazoa Aug 5, 2026
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
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ repositories {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'org.postgresql:postgresql'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Tag(name = "Ad API", description = "클라이언트 광고 노출 및 이벤트 API")
@Tag(name = "Ads API", description = "클라이언트 광고 노출 및 이벤트 API")
@RestController
@RequiredArgsConstructor
@RequestMapping("${api_prefix}/ad")
Expand Down
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
) {
}
Loading
Loading