Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
65226d3
refactor: 에러 메세지 변경
leebs0521 Nov 28, 2024
7585a2e
refactor(recruit-board): 조회 기능 메서드 변경
leebs0521 Nov 28, 2024
4167c57
test(recruit-board): 조회 기능 메서드 변경에 따른 테스트
leebs0521 Nov 28, 2024
0799ec0
reactor(recruit-board): RecruitBoard 레포지토리 기능 분리
leebs0521 Nov 28, 2024
81303be
test(fixture): 테스트용 엔티티 생성을 위한 Fixture 클래스
leebs0521 Nov 28, 2024
de3a4fa
feat(recruit-board): 조회를 위한 엔티티 매핑 클래스
leebs0521 Nov 28, 2024
594c200
feat(recruit-board): 모집글 조회 응답 Dto 작성
leebs0521 Nov 28, 2024
4a31b23
feat(recruit-board): 동적 쿼리를 위한 조건 Condition Dto
leebs0521 Nov 28, 2024
85fa126
feat(utils): Haversine 유틸 클래스
leebs0521 Nov 28, 2024
9990908
feat(recruit-board): 모집글 조회 기능
leebs0521 Nov 28, 2024
b42fb0f
test(recruit-board): 모집글 조회 기능 테스트
leebs0521 Nov 28, 2024
5157d67
feat(recruit-board): 모집글 조회 컨트롤러
leebs0521 Nov 28, 2024
fc4e571
test(recruit-board): 컨트롤러 테스트 작성
leebs0521 Nov 28, 2024
51ffdff
chore: Fixture 클래스 커버리지 제외
leebs0521 Nov 28, 2024
da9dfd7
chore: Fixture 클래스 커버리지 제외
leebs0521 Nov 28, 2024
8315364
chore: fixture 디렉토리 커버리지 제외
leebs0521 Nov 28, 2024
56019c1
fix: 기본 생성자 생성
leebs0521 Nov 28, 2024
5dd55e3
refactor: isEqualTo(0) -> isZero() 변경
leebs0521 Nov 28, 2024
5d1d480
refactor: duplicated code 제거
leebs0521 Nov 28, 2024
624e132
fix: 파라미터가 아닌 고정된 값이 들어간 오류 해결
leebs0521 Nov 28, 2024
420c61f
refactor: Center Dto 이름 좀 더 명확하게 변경
leebs0521 Nov 29, 2024
a93b332
refactor: GeoUtils 디렉토리 위치 변경
leebs0521 Nov 29, 2024
c27b961
refactor: PageableDefault size, page 제거
leebs0521 Nov 29, 2024
b737683
refactor: 메서드 추출
leebs0521 Nov 29, 2024
a8afe60
feat: 기관 아이디로 모집글 조회 기능 및 검증 추가
leebs0521 Nov 29, 2024
e2bfae0
test: 기관 아이디로 모집글 조회 기능 및 검증 테스트
leebs0521 Nov 29, 2024
2974fc8
test: TestControllerSupport 으로 변경
leebs0521 Nov 29, 2024
9ba4cb6
chore: 불필요한 import 제거
leebs0521 Nov 29, 2024
3ee132e
chore: mapper 클래스 디렉토리 변경
leebs0521 Nov 29, 2024
a04afbd
test(recruit-board): 테스트 코드 수정
leebs0521 Nov 29, 2024
8609f23
refactor(location): Location 조회 수정 기능 리팩토링
leebs0521 Nov 29, 2024
38795fb
test(location): Location 조회 수정 기능 리팩토링 테스트
leebs0521 Nov 29, 2024
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
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ def jacocoExcludePatterns = [
'**/auth/**',
'**/domain/*',
'**/domains/*',
'**/fixture/*'
]

def jacocoExcludePatternsForVerify = [
Expand All @@ -129,6 +130,7 @@ def jacocoExcludePatternsForVerify = [
'*.auth.*',
'*.domain.*',
'*.domains.*',
'*.fixture.*'
]

jacocoTestReport {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.somemore.center.dto.response;

import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import com.somemore.center.domain.Center;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.UUID;
import lombok.Builder;

@Builder
@JsonNaming(SnakeCaseStrategy.class)
@Schema(description = "기관 정보 응답 DTO")
public record CenterSimpleInfoResponseDto(
@Schema(description = "기관 아이디", example = "123e4567-e89b-12d3-a456-426614174000")
UUID id,
@Schema(description = "기관 이름", example = "환경 봉사 센터")
String name
) {

public static CenterSimpleInfoResponseDto from(Center center) {
return CenterSimpleInfoResponseDto.builder()
.id(center.getId())
.name(center.getName())
.build();
}

public static CenterSimpleInfoResponseDto of(UUID centerId, String name) {
return CenterSimpleInfoResponseDto.builder()
.id(centerId)
.name(name)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ public enum ExceptionMessage {
UNAUTHORIZED_COMMUNITY_COMMENT("해당 댓글에 권한이 없습니다."),
NOT_EXISTS_LOCATION("존재하지 않는 위치 ID 입니다."),
NOT_EXISTS_RECRUIT_BOARD("존재하지 않는 봉사 모집글 ID 입니다."),
UNAUTHORIZED_RECRUIT_BOARD("자신이 작성한 봉사 모집글이 아닙니다."),
UNAUTHORIZED_RECRUIT_BOARD("해당 봉사 모집글에 권한이 없습니다."),
UPLOAD_FAILED("파일 업로드에 실패했습니다."),
INVALID_FILE_TYPE("지원하지 않는 파일 형식입니다."),
FILE_SIZE_EXCEEDED("파일 크기가 허용된 한도를 초과했습니다."),
EMPTY_FILE("파일이 존재하지 않습니다."),
INSTANTIATION_NOT_ALLOWED("인스턴스화 할 수 없는 클래스 입니다.")
INSTANTIATION_NOT_ALLOWED("인스턴스화 할 수 없는 클래스 입니다."),
;

private final String message;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.somemore.location.dto.response;

import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import com.somemore.location.domain.Location;
import io.swagger.v3.oas.annotations.media.Schema;
import java.math.BigDecimal;
import lombok.Builder;

@Builder
@JsonNaming(SnakeCaseStrategy.class)
@Schema(description = "위치 조회 응답 DTO")
public record LocationResponseDto(
@Schema(description = "주소", example = "서울특별시 강남구 테헤란로 123")
String address,
@Schema(description = "위도", example = "37.5665")
BigDecimal latitude,
@Schema(description = "경도", example = "126.9780")
BigDecimal longitude
) {

public static LocationResponseDto from(Location location) {
return LocationResponseDto.builder()
.address(location.getAddress())
.latitude(location.getLatitude())
.longitude(location.getLongitude())
.build();
}

public static LocationResponseDto of(String address, BigDecimal latitude,
BigDecimal longitude) {
return LocationResponseDto.builder()
.address(address)
.latitude(latitude)
.longitude(longitude)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import com.somemore.location.dto.request.LocationUpdateRequestDto;
import com.somemore.location.repository.LocationRepository;
import com.somemore.location.usecase.command.UpdateLocationUseCase;
import com.somemore.location.usecase.query.LocationQueryUseCase;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -17,14 +16,17 @@
@Service
public class UpdateLocationService implements UpdateLocationUseCase {

private final LocationQueryUseCase locationQueryUseCase;
private final LocationRepository locationRepository;

@Override
public void updateLocation(LocationUpdateRequestDto requestDto, Long locationId) {
Location location = locationQueryUseCase.findById(locationId)
.orElseThrow(() -> new BadRequestException(NOT_EXISTS_LOCATION.getMessage()));
Location location = getLocation(locationId);
location.updateWith(requestDto);
locationRepository.save(location);
}

private Location getLocation(Long locationId) {
return locationRepository.findById(locationId)
.orElseThrow(() -> new BadRequestException(NOT_EXISTS_LOCATION.getMessage()));
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package com.somemore.location.service.query;

import static com.somemore.global.exception.ExceptionMessage.NOT_EXISTS_LOCATION;

import com.somemore.global.exception.BadRequestException;
import com.somemore.location.domain.Location;
import com.somemore.location.repository.LocationRepository;
import com.somemore.location.usecase.query.LocationQueryUseCase;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -16,8 +18,14 @@ public class LocationQueryService implements LocationQueryUseCase {
private final LocationRepository locationRepository;

@Override
public Optional<Location> findById(Long id) {
return locationRepository.findById(id);
public Location getById(Long id) {
return getLocation(id);
}

private Location getLocation(Long id) {
return locationRepository.findById(id).orElseThrow(
() -> new BadRequestException(NOT_EXISTS_LOCATION.getMessage())
);
}

}
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
package com.somemore.location.usecase.query;

import com.somemore.location.domain.Location;
import java.util.Optional;

public interface LocationQueryUseCase {

Optional<Location> findById(Long id);
Location getById(Long id);

}
29 changes: 29 additions & 0 deletions src/main/java/com/somemore/location/utils/GeoUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.somemore.location.utils;

import static lombok.AccessLevel.PRIVATE;

import lombok.NoArgsConstructor;

@NoArgsConstructor(access = PRIVATE)
public class GeoUtils {

private static final double EARTH_RADIUS = 6371.0;

public static double[] calculateMaxMinCoordinates(double latitude, double longitude,
double radius) {
double latRad = Math.toRadians(latitude);
double latDiff = radius / EARTH_RADIUS;
double maxLatRad = latRad + latDiff;
double minLatRad = latRad - latDiff;

double maxLat = Math.toDegrees(maxLatRad);
double minLat = Math.toDegrees(minLatRad);

double lonDiff = radius / (EARTH_RADIUS * Math.cos(latRad));
double maxLon = longitude + Math.toDegrees(lonDiff);
double minLon = longitude - Math.toDegrees(lonDiff);

return new double[]{minLat, minLon, maxLat, maxLon};
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package com.somemore.recruitboard.controller;

import static org.springframework.data.domain.Sort.Direction.DESC;

import com.somemore.global.common.response.ApiResponse;
import com.somemore.recruitboard.domain.RecruitStatus;
import com.somemore.recruitboard.domain.VolunteerType;
import com.somemore.recruitboard.dto.condition.RecruitBoardNearByCondition;
import com.somemore.recruitboard.dto.condition.RecruitBoardSearchCondition;
import com.somemore.recruitboard.dto.response.RecruitBoardDetailResponseDto;
import com.somemore.recruitboard.dto.response.RecruitBoardResponseDto;
import com.somemore.recruitboard.dto.response.RecruitBoardWithCenterResponseDto;
import com.somemore.recruitboard.dto.response.RecruitBoardWithLocationResponseDto;
import com.somemore.recruitboard.usecase.query.RecruitBoardQueryUseCase;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@Tag(name = "Recruit Board Query API", description = "봉사 활동 모집 조회 관련 API")
@RequiredArgsConstructor
@RequestMapping("/api")
@RestController
public class RecruitBoardQueryController {

private final RecruitBoardQueryUseCase recruitBoardQueryUseCase;

@GetMapping("/recruit-board/{id}")
@Operation(summary = "봉사 모집글 상세 조회", description = "특정 모집글의 상세 정보를 조회합니다.")
public ApiResponse<RecruitBoardWithLocationResponseDto> getById(
@PathVariable Long id
) {
return ApiResponse.ok(
200,
recruitBoardQueryUseCase.getWithLocationById(id),
"봉사 활동 모집 상세 조회 성공"
);
}

@GetMapping("/recruit-boards")
@Operation(summary = "전체 모집글 조회", description = "모든 봉사 모집글 목록을 조회합니다.")
public ApiResponse<Page<RecruitBoardWithCenterResponseDto>> getAll(
@PageableDefault(sort = "created_at", direction = DESC)
Pageable pageable
) {
RecruitBoardSearchCondition condition = RecruitBoardSearchCondition.builder()
.pageable(pageable)
.build();

return ApiResponse.ok(
200,
recruitBoardQueryUseCase.getAllWithCenter(condition),
"봉사 활동 모집글 리스트 조회 성공"
);
}

@GetMapping("/recruit-boards/search")
@Operation(summary = "모집글 검색 조회", description = "검색 조건을 기반으로 모집글을 조회합니다.")
public ApiResponse<Page<RecruitBoardWithCenterResponseDto>> getAllBySearch(
@PageableDefault(sort = "created_at", direction = DESC) Pageable pageable,
@RequestParam(required = false) String keyword,
@RequestParam(required = false) VolunteerType type,
@RequestParam(required = false) String region,
@RequestParam(required = false) Boolean admitted,
@RequestParam(required = false) RecruitStatus status
) {
RecruitBoardSearchCondition condition = RecruitBoardSearchCondition.builder()
.keyword(keyword)
.type(type)
.region(region)
.admitted(admitted)
.status(status)
.pageable(pageable)
.build();

return ApiResponse.ok(
200,
recruitBoardQueryUseCase.getAllWithCenter(condition),
"봉사 활동 모집글 검색 조회 성공"
);
}

@GetMapping("/recruit-boards/nearby")
@Operation(summary = "근처 모집글 조회", description = "주변 반경 내의 봉사 모집글을 조회합니다.")
public ApiResponse<Page<RecruitBoardDetailResponseDto>> getNearby(
@RequestParam double latitude,
@RequestParam double longitude,
@RequestParam(required = false, defaultValue = "5") double radius,
@RequestParam(required = false) String keyword,
@PageableDefault(sort = "created_at", direction = DESC) Pageable pageable
) {
RecruitBoardNearByCondition condition = RecruitBoardNearByCondition.builder()
.latitude(latitude)
.longitude(longitude)
.radius(radius)
.keyword(keyword)
.pageable(pageable)
.build();

return ApiResponse.ok(
200,
recruitBoardQueryUseCase.getRecruitBoardsNearby(condition),
"근처 봉사 활동 모집글 조회 성공"
);
}

@GetMapping("/recruit-boards/center/{centerId}")
@Operation(summary = "특정 기관 모집글 조회", description = "특정 기관의 봉사 모집글을 조회합니다.")
public ApiResponse<Page<RecruitBoardResponseDto>> getRecruitBoardsByCenterId(
@PathVariable UUID centerId,
@PageableDefault(sort = "created_at", direction = DESC) Pageable pageable,
@RequestParam(required = false) String keyword,
@RequestParam(required = false) VolunteerType type,
@RequestParam(required = false) String region,
@RequestParam(required = false) Boolean admitted,
@RequestParam(required = false) RecruitStatus status
) {
RecruitBoardSearchCondition condition = RecruitBoardSearchCondition.builder()
.keyword(keyword)
.type(type)
.region(region)
.admitted(admitted)
.status(status)
.pageable(pageable)
.build();

return ApiResponse.ok(
200,
recruitBoardQueryUseCase.getRecruitBoardsByCenterId(centerId, condition),
"기관 봉사 활동 모집글 조회 성공"
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.somemore.recruitboard.dto.condition;

import lombok.Builder;
import org.springframework.data.domain.Pageable;

@Builder
public record RecruitBoardNearByCondition(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

컨디션 클래스는 가져올 필드들을 명시하는 클래스인건가요?
컨디션 클래스 사용이 처음이라 여쭤봐요

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

동적 쿼리 파라미터를 위한 레코드 클래스입니다.
위치 기반 조회 때, keyword = 제목 검색이 있을 수도 없을 수도 있어서 만들어 놨어요.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RecruitBoardSearchCondition 레코드 클래스 보시는게 더 이해하시기 좋을거에요

Double latitude,
Double longitude,
Double radius,
String keyword,
Pageable pageable
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.somemore.recruitboard.dto.condition;

import com.somemore.recruitboard.domain.RecruitStatus;
import com.somemore.recruitboard.domain.VolunteerType;
import lombok.Builder;
import org.springframework.data.domain.Pageable;

@Builder
public record RecruitBoardSearchCondition(
String keyword,
VolunteerType type,
String region,
Boolean admitted,
RecruitStatus status,
Pageable pageable
) {

}
Loading