From d02aeb8a84e8fc9edbe6a069c903eb55a487856e Mon Sep 17 00:00:00 2001 From: 5win <94297900+5win@users.noreply.github.com> Date: Mon, 23 Sep 2024 18:05:07 +0900 Subject: [PATCH 01/17] =?UTF-8?q?feat:=20=ED=99=9C=EB=8F=99=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EC=99=84=EC=84=B1=20=EB=B0=8F?= =?UTF-8?q?=20=EB=8B=A8=EC=9C=84=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#15)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [#6] feat: JPA 레포지토리 연결 - save, findAll, findById 구현 * [#6] feat: 활동 리스트, 상세정보 응답 DTO 구현 * [#6] feat: JPA 엔티티 애노테이션 수정 - GeneratedValue 삭제 : 받아온 데이터의 id를 그대로 사용하기 위함. - String 컬럼에 length 제한 적용 - LocalDateTime 컬럼에 @Temporal 적용 * [#6] feat: 예외 메시지 enum 생성 및 적용 - 기존 string 하드코딩에서 enum으로 변경 * [#6] feat: Spring security 우회 설정 - 컨트롤러 테스트를 위해 모든 경로 permitAll로 지정하여 우회함. * [#6] fix: 컨트롤러 URI 변경 - program -> activity로 명명 변경에서 놓친 부분 수정 * [#6] fix: 활동 저장 오류 수정 - 서비스의 잘못된 로직 수정 - JpaEntity에 기본 생성자 추가 * [#6] refact: DTO 변수를 final로 변경 - 응답 dto의 변수를 final로 변경 - 저장 요청 dto에서 @Data 에서 @Getter로 변경 * [#6] feat: Activity 예외 수정 및 핸들러 추가 - common.constant.ErrorCode 추가 - common.exception.GlobalExceptionHandler 추가 - activity.exception.ActivityCustomException 수정 * [#6] feat: Activity 관련 클래스 변수 추가 및 삭제 - onlinePossible 변수 삭제 - actStartTime, actEndTime 변수 추가 * [#6] feat: 활동 제목 변수 추가 - actTitle 추가 * [#6] test: Activity 단위 테스트 추가 - 아래 클래스에 대한 단위 테스트를 추가함. - JpaEntity - Service - JpaRepository --- .../controller/ActivityController.java | 2 +- .../com/gamsa/activity/domain/Activity.java | 15 ++- .../activity/dto/ActivityDetailResponse.java | 47 +++++++- .../activity/dto/ActivityFindAllResponse.java | 31 +++++- .../activity/dto/ActivitySaveRequest.java | 34 +++++- .../activity/entity/ActivityJpaEntity.java | 41 +++++-- .../ActivityAlreadyExistsException.java | 8 -- .../exception/ActivityCustomException.java | 12 +++ .../repository/ActivityJpaRepository.java | 8 ++ .../repository/ActivityRepositoryImpl.java | 33 ++++++ .../activity/service/ActivityService.java | 20 ++-- .../java/com/gamsa/auth/SecurityConfig.java | 24 +++++ .../com/gamsa/common/constant/ErrorCode.java | 18 ++++ .../exception/GlobalExceptionHandler.java | 20 ++++ .../entity/ActivityJpaEntityTest.java | 72 +++++++++++++ .../repository/ActivityJpaRepositoryTest.java | 60 +++++++++++ .../activity/service/ActivityServiceTest.java | 100 ++++++++++++++++++ .../stub/StubEmptyActivityRepository.java | 24 +++++ .../stub/StubExistsActivityRepository.java | 46 ++++++++ 19 files changed, 577 insertions(+), 38 deletions(-) delete mode 100644 src/main/java/com/gamsa/activity/exception/ActivityAlreadyExistsException.java create mode 100644 src/main/java/com/gamsa/activity/exception/ActivityCustomException.java create mode 100644 src/main/java/com/gamsa/activity/repository/ActivityJpaRepository.java create mode 100644 src/main/java/com/gamsa/activity/repository/ActivityRepositoryImpl.java create mode 100644 src/main/java/com/gamsa/auth/SecurityConfig.java create mode 100644 src/main/java/com/gamsa/common/constant/ErrorCode.java create mode 100644 src/main/java/com/gamsa/common/exception/GlobalExceptionHandler.java create mode 100644 src/test/java/com/gamsa/activity/entity/ActivityJpaEntityTest.java create mode 100644 src/test/java/com/gamsa/activity/repository/ActivityJpaRepositoryTest.java create mode 100644 src/test/java/com/gamsa/activity/service/ActivityServiceTest.java create mode 100644 src/test/java/com/gamsa/activity/stub/StubEmptyActivityRepository.java create mode 100644 src/test/java/com/gamsa/activity/stub/StubExistsActivityRepository.java diff --git a/src/main/java/com/gamsa/activity/controller/ActivityController.java b/src/main/java/com/gamsa/activity/controller/ActivityController.java index 405ccd0..e494cfa 100644 --- a/src/main/java/com/gamsa/activity/controller/ActivityController.java +++ b/src/main/java/com/gamsa/activity/controller/ActivityController.java @@ -17,7 +17,7 @@ @RequiredArgsConstructor @RestController -@RequestMapping("/api/v1/programs") +@RequestMapping("/api/v1/activities") public class ActivityController { private final ActivityService activityService; diff --git a/src/main/java/com/gamsa/activity/domain/Activity.java b/src/main/java/com/gamsa/activity/domain/Activity.java index 4a60eae..4d4a7ce 100644 --- a/src/main/java/com/gamsa/activity/domain/Activity.java +++ b/src/main/java/com/gamsa/activity/domain/Activity.java @@ -8,40 +8,45 @@ public class Activity { private Long actId; + private String actTitle; private String actLocation; private String description; private LocalDateTime noticeStartDate; private LocalDateTime noticeEndDate; private LocalDateTime actStartDate; private LocalDateTime actEndDate; + private int actStartTime; + private int actEndTime; private int recruitTotalNum; private boolean adultPossible; private boolean teenPossible; private boolean groupPossible; - private boolean onlinePossible; private int actWeek; private String actManager; private String actPhone; private String url; @Builder - public Activity(Long actId, String actLocation, String description, + public Activity(Long actId, String actTitle, String actLocation, String description, LocalDateTime noticeStartDate, LocalDateTime noticeEndDate, LocalDateTime actStartDate, LocalDateTime actEndDate, - int recruitTotalNum, boolean adultPossible, boolean teenPossible, boolean groupPossible, - boolean onlinePossible, int actWeek, String actManager, String actPhone, String url) { + int actStartTime, int actEndTime, int recruitTotalNum, boolean adultPossible, + boolean teenPossible, boolean groupPossible, + int actWeek, String actManager, String actPhone, String url) { this.actId = actId; + this.actTitle = actTitle; this.actLocation = actLocation; this.description = description; this.noticeStartDate = noticeStartDate; this.noticeEndDate = noticeEndDate; this.actStartDate = actStartDate; this.actEndDate = actEndDate; + this.actStartTime = actStartTime; + this.actEndTime = actEndTime; this.recruitTotalNum = recruitTotalNum; this.adultPossible = adultPossible; this.teenPossible = teenPossible; this.groupPossible = groupPossible; - this.onlinePossible = onlinePossible; this.actWeek = actWeek; this.actManager = actManager; this.actPhone = actPhone; diff --git a/src/main/java/com/gamsa/activity/dto/ActivityDetailResponse.java b/src/main/java/com/gamsa/activity/dto/ActivityDetailResponse.java index 5e0015b..4754fc3 100644 --- a/src/main/java/com/gamsa/activity/dto/ActivityDetailResponse.java +++ b/src/main/java/com/gamsa/activity/dto/ActivityDetailResponse.java @@ -2,10 +2,55 @@ import com.gamsa.activity.domain.Activity; +import java.time.LocalDateTime; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +@Getter +@Builder +@RequiredArgsConstructor public class ActivityDetailResponse { + private final Long actId; + private final String actTitle; + private final String actLocation; + private final String description; + private final LocalDateTime noticeStartDate; + private final LocalDateTime noticeEndDate; + private final LocalDateTime actStartDate; + private final LocalDateTime actEndDate; + private final int actStartTime; + private final int actEndTime; + private final int recruitTotalNum; + private final boolean adultPossible; + private final boolean teenPossible; + private final boolean groupPossible; + private final int actWeek; + private final String actManager; + private final String actPhone; + private final String url; + public static ActivityDetailResponse from(Activity activity) { - return null; + return ActivityDetailResponse.builder() + .actId(activity.getActId()) + .actTitle(activity.getActTitle()) + .actLocation(activity.getActLocation()) + .description(activity.getDescription()) + .noticeStartDate(activity.getNoticeStartDate()) + .noticeEndDate(activity.getNoticeEndDate()) + .actStartDate(activity.getActStartDate()) + .actEndDate(activity.getActEndDate()) + .actStartTime(activity.getActStartTime()) + .actEndTime(activity.getActEndTime()) + .recruitTotalNum(activity.getRecruitTotalNum()) + .adultPossible(activity.isAdultPossible()) + .teenPossible(activity.isTeenPossible()) + .groupPossible(activity.isGroupPossible()) + .actWeek(activity.getActWeek()) + .actManager(activity.getActManager()) + .actPhone(activity.getActPhone()) + .url(activity.getUrl()) + .build(); } } diff --git a/src/main/java/com/gamsa/activity/dto/ActivityFindAllResponse.java b/src/main/java/com/gamsa/activity/dto/ActivityFindAllResponse.java index c8bed58..e869836 100644 --- a/src/main/java/com/gamsa/activity/dto/ActivityFindAllResponse.java +++ b/src/main/java/com/gamsa/activity/dto/ActivityFindAllResponse.java @@ -1,10 +1,39 @@ package com.gamsa.activity.dto; import com.gamsa.activity.domain.Activity; +import java.time.LocalDateTime; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +@Getter +@Builder +@RequiredArgsConstructor public class ActivityFindAllResponse { + private final Long actId; + private final String actTitle; + private final String actLocation; + private final LocalDateTime noticeStartDate; + private final LocalDateTime noticeEndDate; + private final LocalDateTime actStartDate; + private final LocalDateTime actEndDate; + private final int actStartTime; + private final int actEndTime; + private final int recruitTotalNum; + public static ActivityFindAllResponse from(Activity activity) { - return null; + return ActivityFindAllResponse.builder() + .actId(activity.getActId()) + .actTitle(activity.getActTitle()) + .actLocation(activity.getActLocation()) + .noticeStartDate(activity.getNoticeStartDate()) + .noticeEndDate(activity.getNoticeEndDate()) + .actStartDate(activity.getActStartDate()) + .actEndDate(activity.getActEndDate()) + .actStartTime(activity.getActStartTime()) + .actEndTime(activity.getActEndTime()) + .recruitTotalNum(activity.getRecruitTotalNum()) + .build(); } } diff --git a/src/main/java/com/gamsa/activity/dto/ActivitySaveRequest.java b/src/main/java/com/gamsa/activity/dto/ActivitySaveRequest.java index ded20a1..29165f7 100644 --- a/src/main/java/com/gamsa/activity/dto/ActivitySaveRequest.java +++ b/src/main/java/com/gamsa/activity/dto/ActivitySaveRequest.java @@ -1,27 +1,55 @@ package com.gamsa.activity.dto; +import com.gamsa.activity.domain.Activity; import java.time.LocalDateTime; -import lombok.Data; +import lombok.Builder; +import lombok.Getter; import lombok.RequiredArgsConstructor; -@Data +@Getter +@Builder @RequiredArgsConstructor public class ActivitySaveRequest { private final Long actId; + private final String actTitle; private final String actLocation; private final String description; private final LocalDateTime noticeStartDate; private final LocalDateTime noticeEndDate; private final LocalDateTime actStartDate; private final LocalDateTime actEndDate; + private final int actStartTime; + private final int actEndTime; private final int recruitTotalNum; private final boolean adultPossible; private final boolean teenPossible; private final boolean groupPossible; - private final boolean onlinePossible; private final int actWeek; private final String actManager; private final String actPhone; private final String url; + + public Activity toModel() { + return Activity.builder() + .actId(actId) + .actTitle(actTitle) + .actLocation(actLocation) + .description(description) + .noticeStartDate(noticeStartDate) + .noticeEndDate(noticeEndDate) + .actStartDate(actStartDate) + .actEndDate(actEndDate) + .actStartTime(actStartTime) + .actEndTime(actEndTime) + .recruitTotalNum(recruitTotalNum) + .adultPossible(adultPossible) + .teenPossible(teenPossible) + .groupPossible(groupPossible) + .actWeek(actWeek) + .actManager(actManager) + .actPhone(actPhone) + .url(url) + .build(); + } } diff --git a/src/main/java/com/gamsa/activity/entity/ActivityJpaEntity.java b/src/main/java/com/gamsa/activity/entity/ActivityJpaEntity.java index ee70b39..a45a36a 100644 --- a/src/main/java/com/gamsa/activity/entity/ActivityJpaEntity.java +++ b/src/main/java/com/gamsa/activity/entity/ActivityJpaEntity.java @@ -4,43 +4,60 @@ import com.gamsa.common.entity.BaseEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; +import jakarta.persistence.Temporal; +import jakarta.persistence.TemporalType; import java.time.LocalDateTime; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; @Getter @Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor @Entity @Table(name = "Activity") public class ActivityJpaEntity extends BaseEntity { @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") private Long actId; - @Column(name = "act_location") + @Column(name = "act_title", length = 255) + private String actTitle; + + @Column(name = "act_location", length = 255) private String actLocation; - @Column(name = "description") + @Column(name = "description", length = 1024) private String description; @Column(name = "notice_start_date") + @Temporal(TemporalType.TIMESTAMP) private LocalDateTime noticeStartDate; @Column(name = "notice_end_date") + @Temporal(TemporalType.TIMESTAMP) private LocalDateTime noticeEndDate; @Column(name = "act_start_date") + @Temporal(TemporalType.TIMESTAMP) private LocalDateTime actStartDate; @Column(name = "act_end_date") + @Temporal(TemporalType.TIMESTAMP) private LocalDateTime actEndDate; + @Column(name = "act_start_time") + private int actStartTime; + + @Column(name = "act_end_time") + private int actEndTime; + @Column(name = "recruit_total_num") private int recruitTotalNum; @@ -59,29 +76,31 @@ public class ActivityJpaEntity extends BaseEntity { @Column(name = "act_week") private int actWeek; - @Column(name = "act_manager") + @Column(name = "act_manager", length = 255) private String actManager; - @Column(name = "act_phone") + @Column(name = "act_phone", length = 12) private String actPhone; - @Column(name = "url") + @Column(name = "url", length = 255) private String url; public static ActivityJpaEntity from(Activity activity) { return ActivityJpaEntity.builder() .actId(activity.getActId()) + .actTitle(activity.getActTitle()) .actLocation(activity.getActLocation()) .description(activity.getDescription()) .noticeStartDate(activity.getNoticeStartDate()) .noticeEndDate(activity.getNoticeEndDate()) .actStartDate(activity.getActStartDate()) .actEndDate(activity.getActEndDate()) + .actStartTime(activity.getActStartTime()) + .actEndTime(activity.getActEndTime()) .recruitTotalNum(activity.getRecruitTotalNum()) .adultPossible(activity.isAdultPossible()) .teenPossible(activity.isTeenPossible()) .groupPossible(activity.isGroupPossible()) - .onlinePossible(activity.isOnlinePossible()) .actWeek(activity.getActWeek()) .actManager(activity.getActManager()) .actPhone(activity.getActPhone()) @@ -92,17 +111,19 @@ public static ActivityJpaEntity from(Activity activity) { public Activity toModel() { return Activity.builder() .actId(actId) + .actTitle(actTitle) .actLocation(actLocation) .description(description) .noticeStartDate(noticeStartDate) .noticeEndDate(noticeEndDate) .actStartDate(actStartDate) .actEndDate(actEndDate) + .actStartTime(actStartTime) + .actEndTime(actEndTime) .recruitTotalNum(recruitTotalNum) .adultPossible(adultPossible) .teenPossible(teenPossible) .groupPossible(groupPossible) - .onlinePossible(onlinePossible) .actWeek(actWeek) .actManager(actManager) .actPhone(actPhone) diff --git a/src/main/java/com/gamsa/activity/exception/ActivityAlreadyExistsException.java b/src/main/java/com/gamsa/activity/exception/ActivityAlreadyExistsException.java deleted file mode 100644 index f3ce3af..0000000 --- a/src/main/java/com/gamsa/activity/exception/ActivityAlreadyExistsException.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.gamsa.activity.exception; - -public class ActivityAlreadyExistsException extends RuntimeException { - - public ActivityAlreadyExistsException(String message) { - super(message); - } -} diff --git a/src/main/java/com/gamsa/activity/exception/ActivityCustomException.java b/src/main/java/com/gamsa/activity/exception/ActivityCustomException.java new file mode 100644 index 0000000..276f692 --- /dev/null +++ b/src/main/java/com/gamsa/activity/exception/ActivityCustomException.java @@ -0,0 +1,12 @@ +package com.gamsa.activity.exception; + +import com.gamsa.common.constant.ErrorCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class ActivityCustomException extends RuntimeException { + + private final ErrorCode errorCode; +} diff --git a/src/main/java/com/gamsa/activity/repository/ActivityJpaRepository.java b/src/main/java/com/gamsa/activity/repository/ActivityJpaRepository.java new file mode 100644 index 0000000..c20b7b2 --- /dev/null +++ b/src/main/java/com/gamsa/activity/repository/ActivityJpaRepository.java @@ -0,0 +1,8 @@ +package com.gamsa.activity.repository; + +import com.gamsa.activity.entity.ActivityJpaEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ActivityJpaRepository extends JpaRepository { + +} diff --git a/src/main/java/com/gamsa/activity/repository/ActivityRepositoryImpl.java b/src/main/java/com/gamsa/activity/repository/ActivityRepositoryImpl.java new file mode 100644 index 0000000..7d28dd6 --- /dev/null +++ b/src/main/java/com/gamsa/activity/repository/ActivityRepositoryImpl.java @@ -0,0 +1,33 @@ +package com.gamsa.activity.repository; + +import com.gamsa.activity.domain.Activity; +import com.gamsa.activity.entity.ActivityJpaEntity; +import java.util.List; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@RequiredArgsConstructor +@Repository +public class ActivityRepositoryImpl implements ActivityRepository { + + private final ActivityJpaRepository activityJpaRepository; + + @Override + public void save(Activity activity) { + activityJpaRepository.save(ActivityJpaEntity.from(activity)); + } + + @Override + public List findAll() { + return activityJpaRepository.findAll() + .stream().map(ActivityJpaEntity::toModel) + .toList(); + } + + @Override + public Optional findById(Long activityId) { + return activityJpaRepository.findById(activityId) + .map(ActivityJpaEntity::toModel); + } +} diff --git a/src/main/java/com/gamsa/activity/service/ActivityService.java b/src/main/java/com/gamsa/activity/service/ActivityService.java index 54e0822..8a460fd 100644 --- a/src/main/java/com/gamsa/activity/service/ActivityService.java +++ b/src/main/java/com/gamsa/activity/service/ActivityService.java @@ -4,10 +4,10 @@ import com.gamsa.activity.dto.ActivityDetailResponse; import com.gamsa.activity.dto.ActivityFindAllResponse; import com.gamsa.activity.dto.ActivitySaveRequest; -import com.gamsa.activity.exception.ActivityAlreadyExistsException; +import com.gamsa.activity.exception.ActivityCustomException; import com.gamsa.activity.repository.ActivityRepository; +import com.gamsa.common.constant.ErrorCode; import java.util.List; -import java.util.NoSuchElementException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -17,6 +17,14 @@ public class ActivityService { private final ActivityRepository activityRepository; + public void save(ActivitySaveRequest saveRequest) { + activityRepository.findById(saveRequest.getActId()) + .ifPresent(activity -> { + throw new ActivityCustomException(ErrorCode.ACTIVITY_ALREADY_EXISTS); + }); + activityRepository.save(saveRequest.toModel()); + } + public List findAll() { List activities = activityRepository.findAll(); return activities.stream() @@ -26,13 +34,7 @@ public List findAll() { public ActivityDetailResponse findById(Long activityId) { Activity activity = activityRepository.findById(activityId) - .orElseThrow(() -> new NoSuchElementException("존재하지 않는 활동입니다.")); + .orElseThrow(() -> new ActivityCustomException(ErrorCode.ACTIVITY_NOT_EXISTS)); return ActivityDetailResponse.from(activity); } - - public void save(ActivitySaveRequest saveRequest) { - Activity activity = activityRepository.findById(saveRequest.getActId()) - .orElseThrow(() -> new ActivityAlreadyExistsException("이미 활동이 존재합니다.")); - activityRepository.save(activity); - } } diff --git a/src/main/java/com/gamsa/auth/SecurityConfig.java b/src/main/java/com/gamsa/auth/SecurityConfig.java new file mode 100644 index 0000000..571302d --- /dev/null +++ b/src/main/java/com/gamsa/auth/SecurityConfig.java @@ -0,0 +1,24 @@ +package com.gamsa.auth; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(authorizeRequest -> { + authorizeRequest + .anyRequest().permitAll(); + }); + return http.build(); + } +} diff --git a/src/main/java/com/gamsa/common/constant/ErrorCode.java b/src/main/java/com/gamsa/common/constant/ErrorCode.java new file mode 100644 index 0000000..99a2251 --- /dev/null +++ b/src/main/java/com/gamsa/common/constant/ErrorCode.java @@ -0,0 +1,18 @@ +package com.gamsa.common.constant; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum ErrorCode { + + // 404 Not Found : 존재하지 않는 리소스 접근 + ACTIVITY_NOT_EXISTS(404, "존재하지 않는 활동입니다."), + + // 409 Conflict : 요청이 현재 서버의 상태와 충돌 + ACTIVITY_ALREADY_EXISTS(409, "이미 존재하는 활동입니다."); + + private final int status; + private final String msg; +} diff --git a/src/main/java/com/gamsa/common/exception/GlobalExceptionHandler.java b/src/main/java/com/gamsa/common/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..f6aa449 --- /dev/null +++ b/src/main/java/com/gamsa/common/exception/GlobalExceptionHandler.java @@ -0,0 +1,20 @@ +package com.gamsa.common.exception; + +import com.gamsa.activity.exception.ActivityCustomException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(ActivityCustomException.class) + private ResponseEntity ActivityCustomExceptionHandler(ActivityCustomException e) { + log.error(String.valueOf(e.getStackTrace()[0])); + return ResponseEntity + .status(e.getErrorCode().getStatus()) + .body(e.getErrorCode().getMsg()); + } +} diff --git a/src/test/java/com/gamsa/activity/entity/ActivityJpaEntityTest.java b/src/test/java/com/gamsa/activity/entity/ActivityJpaEntityTest.java new file mode 100644 index 0000000..b83861f --- /dev/null +++ b/src/test/java/com/gamsa/activity/entity/ActivityJpaEntityTest.java @@ -0,0 +1,72 @@ +package com.gamsa.activity.entity; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.gamsa.activity.domain.Activity; +import java.time.LocalDateTime; +import org.junit.jupiter.api.Test; + +class ActivityJpaEntityTest { + + @Test + void 도메인모델에서_JPA엔티티로_변환() { + // given + Activity activity = Activity.builder() + .actId(1L) + .actTitle("어린이놀이안전관리 및 놀잇감 청결유지 및 정리") + .actLocation("아이사랑꿈터 서구 5호점") + .description("봉사 내용") + .noticeStartDate(LocalDateTime.of(2024, 9, 10, 0, 0)) + .noticeEndDate(LocalDateTime.of(2024, 12, 7, 0, 0)) + .actStartDate(LocalDateTime.of(2024, 9, 10, 0, 0)) + .actEndDate(LocalDateTime.of(2024, 12, 7, 0, 0)) + .actStartTime(13) + .actEndTime(18) + .recruitTotalNum(1) + .adultPossible(true) + .teenPossible(false) + .groupPossible(false) + .actWeek(0111110) + .actManager("윤순영") + .actPhone("032-577-3026") + .url("https://...") + .build(); + + // when + ActivityJpaEntity jpaEntity = ActivityJpaEntity.from(activity); + + // then + assertThat(jpaEntity.getActId()).isEqualTo(activity.getActId()); + } + + @Test + void JPA엔티에서_도메인모델로_변환() { + // given + ActivityJpaEntity jpaEntity = ActivityJpaEntity.builder() + .actId(1L) + .actTitle("어린이놀이안전관리 및 놀잇감 청결유지 및 정리") + .actLocation("아이사랑꿈터 서구 5호점") + .description("봉사 내용") + .noticeStartDate(LocalDateTime.of(2024, 9, 10, 0, 0)) + .noticeEndDate(LocalDateTime.of(2024, 12, 7, 0, 0)) + .actStartDate(LocalDateTime.of(2024, 9, 10, 0, 0)) + .actEndDate(LocalDateTime.of(2024, 12, 7, 0, 0)) + .actStartTime(13) + .actEndTime(18) + .recruitTotalNum(1) + .adultPossible(true) + .teenPossible(false) + .groupPossible(false) + .actWeek(0111110) + .actManager("윤순영") + .actPhone("032-577-3026") + .url("https://...") + .build(); + + // when + Activity activity = jpaEntity.toModel(); + + // then + assertThat(activity.getActId()).isEqualTo(jpaEntity.getActId()); + } +} \ No newline at end of file diff --git a/src/test/java/com/gamsa/activity/repository/ActivityJpaRepositoryTest.java b/src/test/java/com/gamsa/activity/repository/ActivityJpaRepositoryTest.java new file mode 100644 index 0000000..7d115b9 --- /dev/null +++ b/src/test/java/com/gamsa/activity/repository/ActivityJpaRepositoryTest.java @@ -0,0 +1,60 @@ +package com.gamsa.activity.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.gamsa.activity.entity.ActivityJpaEntity; +import java.time.LocalDateTime; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; + +@DataJpaTest +class ActivityJpaRepositoryTest { + + @Autowired + private ActivityJpaRepository activityJpaRepository; + private final ActivityJpaEntity jpaEntity = ActivityJpaEntity.builder() + .actId(1L) + .actTitle("어린이놀이안전관리 및 놀잇감 청결유지 및 정리") + .actLocation("아이사랑꿈터 서구 5호점") + .description("봉사 내용") + .noticeStartDate(LocalDateTime.of(2024, 9, 10, 0, 0)) + .noticeEndDate(LocalDateTime.of(2024, 12, 7, 0, 0)) + .actStartDate(LocalDateTime.of(2024, 9, 10, 0, 0)) + .actEndDate(LocalDateTime.of(2024, 12, 7, 0, 0)) + .actStartTime(13) + .actEndTime(18) + .recruitTotalNum(1) + .adultPossible(true) + .teenPossible(false) + .groupPossible(false) + .actWeek(0111110) + .actManager("윤순영") + .actPhone("032-577-3026") + .url("https://...") + .build(); + + + @Test + void 새_활동_저장() { + // when + activityJpaRepository.save(jpaEntity); + + // then + assertThat(activityJpaRepository.findById(1L).get().getActTitle()) + .isEqualTo(jpaEntity.getActTitle()); + } + + @Test + void 모든_활동_리스트_반환() { + // given + activityJpaRepository.save(jpaEntity); + + // when + List result = activityJpaRepository.findAll(); + + // then + assertThat(result.size()).isEqualTo(1); + } +} \ No newline at end of file diff --git a/src/test/java/com/gamsa/activity/service/ActivityServiceTest.java b/src/test/java/com/gamsa/activity/service/ActivityServiceTest.java new file mode 100644 index 0000000..01c32a0 --- /dev/null +++ b/src/test/java/com/gamsa/activity/service/ActivityServiceTest.java @@ -0,0 +1,100 @@ +package com.gamsa.activity.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.gamsa.activity.dto.ActivityDetailResponse; +import com.gamsa.activity.dto.ActivityFindAllResponse; +import com.gamsa.activity.dto.ActivitySaveRequest; +import com.gamsa.activity.exception.ActivityCustomException; +import com.gamsa.activity.stub.StubEmptyActivityRepository; +import com.gamsa.activity.stub.StubExistsActivityRepository; +import com.gamsa.common.constant.ErrorCode; +import java.time.LocalDateTime; +import java.util.List; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class ActivityServiceTest { + + ActivitySaveRequest saveRequest = ActivitySaveRequest.builder() + .actId(1L) + .actTitle("어린이놀이안전관리 및 놀잇감 청결유지 및 정리") + .actLocation("아이사랑꿈터 서구 5호점") + .description("봉사 내용") + .noticeStartDate(LocalDateTime.of(2024, 9, 10, 0, 0)) + .noticeEndDate(LocalDateTime.of(2024, 12, 7, 0, 0)) + .actStartDate(LocalDateTime.of(2024, 9, 10, 0, 0)) + .actEndDate(LocalDateTime.of(2024, 12, 7, 0, 0)) + .actStartTime(13) + .actEndTime(18) + .recruitTotalNum(1) + .adultPossible(true) + .teenPossible(false) + .groupPossible(false) + .actWeek(0111110) + .actManager("윤순영") + .actPhone("032-577-3026") + .url("https://...") + .build(); + + @Test + void 활동객체를_저장하고_성공한다() { + // given + ActivityService activityService = new ActivityService(new StubEmptyActivityRepository()); + + // then + Assertions.assertDoesNotThrow(() -> { + // when + activityService.save(saveRequest); + }); + + } + + @Test + void 이미_존재하는_ID의_활동객체를_생성하고_실패한다() { + // given + ActivityService activityService = new ActivityService(new StubExistsActivityRepository()); + + // then + Assertions.assertThrows(ActivityCustomException.class, () -> { + // when + activityService.save(saveRequest); + }, ErrorCode.ACTIVITY_ALREADY_EXISTS.getMsg()); + } + + @Test + void 활동객체_리스트를_반환한다() { + // given + ActivityService activityService = new ActivityService(new StubEmptyActivityRepository()); + + // when + List result = activityService.findAll(); + + // then + assertThat(result.size()).isZero(); + } + + @Test + void ID로_활동조회에_성공한다() { + // given + ActivityService activityService = new ActivityService(new StubExistsActivityRepository()); + + // when + ActivityDetailResponse result = activityService.findById(1L); + + // then + assertThat(result.getActId()).isEqualTo(1L); + } + + @Test + void ID로_활동조회에_실패한다() { + // given + ActivityService activityService = new ActivityService(new StubEmptyActivityRepository()); + + // then + Assertions.assertThrows(ActivityCustomException.class, () -> { + // when + activityService.findById(1L); + }, ErrorCode.ACTIVITY_NOT_EXISTS.getMsg()); + } +} \ No newline at end of file diff --git a/src/test/java/com/gamsa/activity/stub/StubEmptyActivityRepository.java b/src/test/java/com/gamsa/activity/stub/StubEmptyActivityRepository.java new file mode 100644 index 0000000..0a59ff9 --- /dev/null +++ b/src/test/java/com/gamsa/activity/stub/StubEmptyActivityRepository.java @@ -0,0 +1,24 @@ +package com.gamsa.activity.stub; + +import com.gamsa.activity.domain.Activity; +import com.gamsa.activity.repository.ActivityRepository; +import java.util.List; +import java.util.Optional; + +public class StubEmptyActivityRepository implements ActivityRepository { + + @Override + public void save(Activity activity) { + // do nothing + } + + @Override + public List findAll() { + return List.of(); + } + + @Override + public Optional findById(Long activityId) { + return Optional.empty(); + } +} diff --git a/src/test/java/com/gamsa/activity/stub/StubExistsActivityRepository.java b/src/test/java/com/gamsa/activity/stub/StubExistsActivityRepository.java new file mode 100644 index 0000000..c2c845b --- /dev/null +++ b/src/test/java/com/gamsa/activity/stub/StubExistsActivityRepository.java @@ -0,0 +1,46 @@ +package com.gamsa.activity.stub; + +import com.gamsa.activity.domain.Activity; +import com.gamsa.activity.repository.ActivityRepository; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +public class StubExistsActivityRepository implements ActivityRepository { + + private final Activity activity = Activity.builder() + .actId(1L) + .actTitle("어린이놀이안전관리 및 놀잇감 청결유지 및 정리") + .actLocation("아이사랑꿈터 서구 5호점") + .description("봉사 내용") + .noticeStartDate(LocalDateTime.of(2024, 9, 10, 0, 0)) + .noticeEndDate(LocalDateTime.of(2024, 12, 7, 0, 0)) + .actStartDate(LocalDateTime.of(2024, 9, 10, 0, 0)) + .actEndDate(LocalDateTime.of(2024, 12, 7, 0, 0)) + .actStartTime(13) + .actEndTime(18) + .recruitTotalNum(1) + .adultPossible(true) + .teenPossible(false) + .groupPossible(false) + .actWeek(0111110) + .actManager("윤순영") + .actPhone("032-577-3026") + .url("https://...") + .build(); + + @Override + public void save(Activity activity) { + // do nothing + } + + @Override + public List findAll() { + return List.of(); + } + + @Override + public Optional findById(Long activityId) { + return Optional.of(activity); + } +} From 3b98b12334ea4eff50ad64d622a4097dd9112801 Mon Sep 17 00:00:00 2001 From: 5win <94297900+5win@users.noreply.github.com> Date: Sat, 28 Sep 2024 22:51:16 +0900 Subject: [PATCH 02/17] =?UTF-8?q?weekly:=204=EC=A3=BC=EC=B0=A8=20=EC=9E=91?= =?UTF-8?q?=EC=97=85=20=EB=B3=91=ED=95=A9=20(#24)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: Update issue templates 이슈 템플릿 생성 * docs: Create pull_request_template.md - PR 템플릿 작성 * init: 프로젝트 생성 [#2] init: 프로젝트 생성 및 의존성 추가 (#3) (#4) * weekly: 3주차 작업 내용 머지 (#8) * [#2] init: 프로젝트 생성 및 의존성 추가 (#3) - 프로젝트 생성 - 의존성 추가 - H2 연결 및 테스트 * [#6] feat: 봉사활동 조회 기능 추가 (#7) - 아직 모두 구현하지 못했음 * fix: PR 템플릿 위치 수정 (#14) * [#5] init: PR 템플릿 위치 변경 - 템플릿 파일의 위치를 변경함 * rename: 기본 테스트 클래스 이름 변경 * feat: 활동 조회 기능 완성 및 단위테스트 추가 (#15) * [#6] feat: JPA 레포지토리 연결 - save, findAll, findById 구현 * [#6] feat: 활동 리스트, 상세정보 응답 DTO 구현 * [#6] feat: JPA 엔티티 애노테이션 수정 - GeneratedValue 삭제 : 받아온 데이터의 id를 그대로 사용하기 위함. - String 컬럼에 length 제한 적용 - LocalDateTime 컬럼에 @Temporal 적용 * [#6] feat: 예외 메시지 enum 생성 및 적용 - 기존 string 하드코딩에서 enum으로 변경 * [#6] feat: Spring security 우회 설정 - 컨트롤러 테스트를 위해 모든 경로 permitAll로 지정하여 우회함. * [#6] fix: 컨트롤러 URI 변경 - program -> activity로 명명 변경에서 놓친 부분 수정 * [#6] fix: 활동 저장 오류 수정 - 서비스의 잘못된 로직 수정 - JpaEntity에 기본 생성자 추가 * [#6] refact: DTO 변수를 final로 변경 - 응답 dto의 변수를 final로 변경 - 저장 요청 dto에서 @Data 에서 @Getter로 변경 * [#6] feat: Activity 예외 수정 및 핸들러 추가 - common.constant.ErrorCode 추가 - common.exception.GlobalExceptionHandler 추가 - activity.exception.ActivityCustomException 수정 * [#6] feat: Activity 관련 클래스 변수 추가 및 삭제 - onlinePossible 변수 삭제 - actStartTime, actEndTime 변수 추가 * [#6] feat: 활동 제목 변수 추가 - actTitle 추가 * [#6] test: Activity 단위 테스트 추가 - 아래 클래스에 대한 단위 테스트를 추가함. - JpaEntity - Service - JpaRepository * feat: 봉사활동 조회 무한스크롤 기능 및 단위테스트 구현 (#20) * [#18] feat: 활동 조회 id 기준 QueryDSL 무한스크롤 구현 - QueryDSL 의존성 추가 및 common.config.QueryDslConfig 파일 추가 - Slice 객체 반환하도록 변경 - CustomRepository 만드는 패턴을 통해 QueryDSL과 Spring data JPA 결합 * [#18] rename: 활동 테이블 PK명 id에서 actId로 변경 * [#18] test: 무한스크롤 단위 테스트 추가 및 수정 * [#18] feat: 활동 정렬 기준 상수클래스 생성 - 정렬 기준 상수클래스 생성하여 하드코딩 제거 - common.constant를 activity 패키지로 이동 * [#18] fix: ActivityCustomRepositoryImpl에서의 도메인 엔티티 종속성 제거 - Activity로의 종속성을 제거하고 ActivityJpaEntity만 종속하도록 수정함. * [#18] rename: 바꾸지 않은 findAll 명명을 모두 findSlice로 변경 * [#18] rename: ActivityCustomException을 ActivityException으로 변경 * [#18] refact: OrderSpecifier 정렬 기준 생성 메서드 리팩토링 불필요한 클래스 삭제 - common.utils.QueryDslUtil - activity.constant.ActivitySortType * feat: 봉사 분야 기능, 봉사 조회 필터링 기능 추가 (#23) * [#22] refact: lombok 사용하여 생성자 제거 * [#22] feat: H2콘솔 위한 X-Frame-Options 비활성화 * [#22] feat: 봉사 분야 컬럼 추가 - enum 형태로 관리 - CategoryConverter를 통해 DB에는 한글로 저장. * [#22] feat: 봉사활동 조회 필터링 기능 추가 - filter DTO를 추가하여 필터링 정보 전달. - 파라미터 변경에 따른 클래스, 테스트코드 수정 - ActivityFilterBuilder: 필터링 정보 조합 BooleanBuilder 반환 * [#22] fix: 마감 전 활동만 필터링 하도록 조건 수정 - 마감된 활동만 필터링에서 마감 전 활동 필터링으로 변경 * [#22] test: 활동 조회 정렬, 필터링 기능 단위테스트 - 마감 날짜 오름차순 정렬 조회 - 마감된 공고 중, 마감 날짜 가까운 순 정렬 - 카테고리 필터링 - 청소년 가능 활동만 필터링 - 마감되지 않은 활동만 필터링 * [#22] refact: 기존 활동 조회 단위 테스트 리팩토링 * feat: 아바타 관련 기능 구현 * feat: 아바타 관련 기능 구현 신규 아바타 저장 기존 아바타 호출 기존 아바타 수정 * test: 아바타 관련 단위 테스트 * fix: 아바타 기능 수정 --------- Co-authored-by: Awhn <69659322+Awhn@users.noreply.github.com> --- build.gradle | 47 +++-- .../activity/constant/ActivityErrorCode.java | 18 ++ .../com/gamsa/activity/constant/Category.java | 18 ++ .../activity/constant/CategoryConverter.java | 28 +++ .../controller/ActivityController.java | 19 +- .../com/gamsa/activity/domain/Activity.java | 32 +-- .../activity/dto/ActivityDetailResponse.java | 3 + .../activity/dto/ActivityFilterRequest.java | 21 ++ .../dto/ActivityFindSliceResponse.java | 42 ++++ .../activity/dto/ActivitySaveRequest.java | 3 + .../activity/entity/ActivityJpaEntity.java | 11 +- .../activity/exception/ActivityException.java | 12 ++ .../repository/ActivityCustomRepository.java | 12 ++ .../ActivityCustomRepositoryImpl.java | 67 ++++++ .../repository/ActivityFilterBuilder.java | 39 ++++ .../repository/ActivityJpaRepository.java | 3 +- .../repository/ActivityRepository.java | 6 +- .../repository/ActivityRepositoryImpl.java | 11 +- .../activity/service/ActivityService.java | 23 +- .../java/com/gamsa/auth/SecurityConfig.java | 3 + .../com/gamsa/avatar/constant/AgeRange.java | 14 ++ .../avatar/constant/AgeRangeConverter.java | 17 ++ .../gamsa/avatar/constant/Experienced.java | 14 ++ .../avatar/constant/ExperiencedConverter.java | 17 ++ .../avatar/controller/AvatarController.java | 41 ++++ .../java/com/gamsa/avatar/domain/Avatar.java | 42 ++++ .../gamsa/avatar/dto/AvatarFindResponse.java | 33 +++ .../gamsa/avatar/dto/AvatarSaveRequest.java | 35 ++++ .../gamsa/avatar/entity/AvatarJpaEntity.java | 63 ++++++ .../repository/AvatarJpaRepository.java | 12 ++ .../avatar/repository/AvatarRepository.java | 13 ++ .../repository/AvatarRepositoryImpl.java | 29 +++ .../gamsa/avatar/service/AvatarService.java | 48 +++++ .../gamsa/common/config/QueryDslConfig.java | 19 ++ .../exception/GlobalExceptionHandler.java | 10 +- .../entity/ActivityJpaEntityTest.java | 3 + .../repository/ActivityJpaRepositoryTest.java | 196 +++++++++++++++++- .../activity/service/ActivityServiceTest.java | 24 ++- .../stub/StubEmptyActivityRepository.java | 8 +- .../stub/StubExistsActivityRepository.java | 8 +- .../avatar/entity/AvatarJpaEntityTest.java | 48 +++++ .../repository/AvatarJpaRepositoryTest.java | 50 +++++ .../avatar/service/AvatarServiceTest.java | 56 +++++ .../avatar/stub/StubAvatarRepository.java | 31 +++ .../com/gamsa/common/config/TestConfig.java | 19 ++ 45 files changed, 1174 insertions(+), 94 deletions(-) create mode 100644 src/main/java/com/gamsa/activity/constant/ActivityErrorCode.java create mode 100644 src/main/java/com/gamsa/activity/constant/Category.java create mode 100644 src/main/java/com/gamsa/activity/constant/CategoryConverter.java create mode 100644 src/main/java/com/gamsa/activity/dto/ActivityFilterRequest.java create mode 100644 src/main/java/com/gamsa/activity/dto/ActivityFindSliceResponse.java create mode 100644 src/main/java/com/gamsa/activity/exception/ActivityException.java create mode 100644 src/main/java/com/gamsa/activity/repository/ActivityCustomRepository.java create mode 100644 src/main/java/com/gamsa/activity/repository/ActivityCustomRepositoryImpl.java create mode 100644 src/main/java/com/gamsa/activity/repository/ActivityFilterBuilder.java create mode 100644 src/main/java/com/gamsa/avatar/constant/AgeRange.java create mode 100644 src/main/java/com/gamsa/avatar/constant/AgeRangeConverter.java create mode 100644 src/main/java/com/gamsa/avatar/constant/Experienced.java create mode 100644 src/main/java/com/gamsa/avatar/constant/ExperiencedConverter.java create mode 100644 src/main/java/com/gamsa/avatar/controller/AvatarController.java create mode 100644 src/main/java/com/gamsa/avatar/domain/Avatar.java create mode 100644 src/main/java/com/gamsa/avatar/dto/AvatarFindResponse.java create mode 100644 src/main/java/com/gamsa/avatar/dto/AvatarSaveRequest.java create mode 100644 src/main/java/com/gamsa/avatar/entity/AvatarJpaEntity.java create mode 100644 src/main/java/com/gamsa/avatar/repository/AvatarJpaRepository.java create mode 100644 src/main/java/com/gamsa/avatar/repository/AvatarRepository.java create mode 100644 src/main/java/com/gamsa/avatar/repository/AvatarRepositoryImpl.java create mode 100644 src/main/java/com/gamsa/avatar/service/AvatarService.java create mode 100644 src/main/java/com/gamsa/common/config/QueryDslConfig.java create mode 100644 src/test/java/com/gamsa/avatar/entity/AvatarJpaEntityTest.java create mode 100644 src/test/java/com/gamsa/avatar/repository/AvatarJpaRepositoryTest.java create mode 100644 src/test/java/com/gamsa/avatar/service/AvatarServiceTest.java create mode 100644 src/test/java/com/gamsa/avatar/stub/StubAvatarRepository.java create mode 100644 src/test/java/com/gamsa/common/config/TestConfig.java diff --git a/build.gradle b/build.gradle index c1e3180..7d091ce 100644 --- a/build.gradle +++ b/build.gradle @@ -1,40 +1,47 @@ plugins { - id 'java' - id 'org.springframework.boot' version '3.3.3' - id 'io.spring.dependency-management' version '1.1.6' + id 'java' + id 'org.springframework.boot' version '3.3.3' + id 'io.spring.dependency-management' version '1.1.6' } group = 'com' version = '0.0.1-SNAPSHOT' java { - toolchain { - languageVersion = JavaLanguageVersion.of(21) - } + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } } configurations { - compileOnly { - extendsFrom annotationProcessor - } + compileOnly { + extendsFrom annotationProcessor + } } repositories { - mavenCentral() + mavenCentral() } dependencies { - implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' - implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - compileOnly 'org.projectlombok:lombok' - runtimeOnly 'com.h2database:h2' - annotationProcessor 'org.projectlombok:lombok' - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'org.springframework.security:spring-security-test' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + + implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' + annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jakarta' + annotationProcessor 'jakarta.annotation:jakarta.annotation-api' + annotationProcessor 'jakarta.persistence:jakarta.persistence-api' + implementation 'org.hibernate.validator:hibernate-validator' + + compileOnly 'org.projectlombok:lombok' + runtimeOnly 'com.h2database:h2' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.security:spring-security-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } tasks.named('test') { - useJUnitPlatform() + useJUnitPlatform() } diff --git a/src/main/java/com/gamsa/activity/constant/ActivityErrorCode.java b/src/main/java/com/gamsa/activity/constant/ActivityErrorCode.java new file mode 100644 index 0000000..163c36f --- /dev/null +++ b/src/main/java/com/gamsa/activity/constant/ActivityErrorCode.java @@ -0,0 +1,18 @@ +package com.gamsa.activity.constant; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum ActivityErrorCode { + + // 404 Not Found : 존재하지 않는 리소스 접근 + ACTIVITY_NOT_EXISTS(404, "존재하지 않는 활동입니다."), + + // 409 Conflict : 요청이 현재 서버의 상태와 충돌 + ACTIVITY_ALREADY_EXISTS(409, "이미 존재하는 활동입니다."); + + private final int status; + private final String msg; +} diff --git a/src/main/java/com/gamsa/activity/constant/Category.java b/src/main/java/com/gamsa/activity/constant/Category.java new file mode 100644 index 0000000..561983f --- /dev/null +++ b/src/main/java/com/gamsa/activity/constant/Category.java @@ -0,0 +1,18 @@ +package com.gamsa.activity.constant; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum Category { + LIFE_SUPPORT_AND_HOUSING_IMPROVEMENT("생활지원 및 주거환경 개선"), + EDUCATION_AND_MENTORING("교육 및 멘토링"), + ADMINISTRATIVE_AND_OFFICE_SUPPORT("행정 및 사무지원"), + CULTURE_ENVIRONMENT_AND_INTERNATIONAL_COOPERATION("문화, 환경 및 국제협력 활동"), + HEALTHCARE_AND_PUBLIC_WELFARE("보건의료 및 공익활동"), + COUNSELING_AND_VOLUNTEER_TRAINING("상담 및 자원봉사 교육"), + OTHER_ACTIVITIES("기타 활동"); + + private final String name; +} diff --git a/src/main/java/com/gamsa/activity/constant/CategoryConverter.java b/src/main/java/com/gamsa/activity/constant/CategoryConverter.java new file mode 100644 index 0000000..10fee2a --- /dev/null +++ b/src/main/java/com/gamsa/activity/constant/CategoryConverter.java @@ -0,0 +1,28 @@ +package com.gamsa.activity.constant; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; +import java.util.stream.Stream; + +@Converter +public class CategoryConverter implements AttributeConverter { + + @Override + public String convertToDatabaseColumn(Category category) { + if (category == null) { + return null; + } + return category.getName(); + } + + @Override + public Category convertToEntityAttribute(String name) { + if (name == null) { + return null; + } + return Stream.of(Category.values()) + .filter(category -> category.getName().equals(name)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 카테고리 접근.")); + } +} diff --git a/src/main/java/com/gamsa/activity/controller/ActivityController.java b/src/main/java/com/gamsa/activity/controller/ActivityController.java index e494cfa..a231180 100644 --- a/src/main/java/com/gamsa/activity/controller/ActivityController.java +++ b/src/main/java/com/gamsa/activity/controller/ActivityController.java @@ -1,11 +1,14 @@ package com.gamsa.activity.controller; +import com.gamsa.activity.constant.Category; import com.gamsa.activity.dto.ActivityDetailResponse; -import com.gamsa.activity.dto.ActivityFindAllResponse; +import com.gamsa.activity.dto.ActivityFilterRequest; +import com.gamsa.activity.dto.ActivityFindSliceResponse; import com.gamsa.activity.dto.ActivitySaveRequest; import com.gamsa.activity.service.ActivityService; -import java.util.List; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -13,6 +16,7 @@ import org.springframework.web.bind.annotation.PostMapping; 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; @RequiredArgsConstructor @@ -23,8 +27,15 @@ public class ActivityController { private final ActivityService activityService; @GetMapping - public List findAll() { - return activityService.findAll(); + public Slice findSlice( + @RequestParam(required = false) Category category, + @RequestParam(defaultValue = "false") boolean teenPossibleOnly, + @RequestParam(defaultValue = "false") boolean beforeDeadlineOnly, + Pageable pageable) { + + ActivityFilterRequest request = new ActivityFilterRequest(category, teenPossibleOnly, + beforeDeadlineOnly); + return activityService.findSlice(request, pageable); } @GetMapping("{activity-id}") diff --git a/src/main/java/com/gamsa/activity/domain/Activity.java b/src/main/java/com/gamsa/activity/domain/Activity.java index 4d4a7ce..662cd17 100644 --- a/src/main/java/com/gamsa/activity/domain/Activity.java +++ b/src/main/java/com/gamsa/activity/domain/Activity.java @@ -1,10 +1,14 @@ package com.gamsa.activity.domain; +import com.gamsa.activity.constant.Category; import java.time.LocalDateTime; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @Getter +@Builder +@AllArgsConstructor public class Activity { private Long actId; @@ -25,31 +29,5 @@ public class Activity { private String actManager; private String actPhone; private String url; - - @Builder - public Activity(Long actId, String actTitle, String actLocation, String description, - LocalDateTime noticeStartDate, LocalDateTime noticeEndDate, - LocalDateTime actStartDate, LocalDateTime actEndDate, - int actStartTime, int actEndTime, int recruitTotalNum, boolean adultPossible, - boolean teenPossible, boolean groupPossible, - int actWeek, String actManager, String actPhone, String url) { - this.actId = actId; - this.actTitle = actTitle; - this.actLocation = actLocation; - this.description = description; - this.noticeStartDate = noticeStartDate; - this.noticeEndDate = noticeEndDate; - this.actStartDate = actStartDate; - this.actEndDate = actEndDate; - this.actStartTime = actStartTime; - this.actEndTime = actEndTime; - this.recruitTotalNum = recruitTotalNum; - this.adultPossible = adultPossible; - this.teenPossible = teenPossible; - this.groupPossible = groupPossible; - this.actWeek = actWeek; - this.actManager = actManager; - this.actPhone = actPhone; - this.url = url; - } + private Category category; } diff --git a/src/main/java/com/gamsa/activity/dto/ActivityDetailResponse.java b/src/main/java/com/gamsa/activity/dto/ActivityDetailResponse.java index 4754fc3..d45f6df 100644 --- a/src/main/java/com/gamsa/activity/dto/ActivityDetailResponse.java +++ b/src/main/java/com/gamsa/activity/dto/ActivityDetailResponse.java @@ -1,6 +1,7 @@ package com.gamsa.activity.dto; +import com.gamsa.activity.constant.Category; import com.gamsa.activity.domain.Activity; import java.time.LocalDateTime; import lombok.Builder; @@ -30,6 +31,7 @@ public class ActivityDetailResponse { private final String actManager; private final String actPhone; private final String url; + private final Category category; public static ActivityDetailResponse from(Activity activity) { return ActivityDetailResponse.builder() @@ -51,6 +53,7 @@ public static ActivityDetailResponse from(Activity activity) { .actManager(activity.getActManager()) .actPhone(activity.getActPhone()) .url(activity.getUrl()) + .category(activity.getCategory()) .build(); } } diff --git a/src/main/java/com/gamsa/activity/dto/ActivityFilterRequest.java b/src/main/java/com/gamsa/activity/dto/ActivityFilterRequest.java new file mode 100644 index 0000000..e0aef5a --- /dev/null +++ b/src/main/java/com/gamsa/activity/dto/ActivityFilterRequest.java @@ -0,0 +1,21 @@ +package com.gamsa.activity.dto; + +import com.gamsa.activity.constant.Category; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class ActivityFilterRequest { + + // 카테고리 + private final Category category; + + // Todo 지역 + + // 청소년 가능한 것만 + private final boolean teenPossibleOnly; + + // 마감되지 않은 활동만 + private final boolean beforeDeadlineOnly; +} diff --git a/src/main/java/com/gamsa/activity/dto/ActivityFindSliceResponse.java b/src/main/java/com/gamsa/activity/dto/ActivityFindSliceResponse.java new file mode 100644 index 0000000..f1661a9 --- /dev/null +++ b/src/main/java/com/gamsa/activity/dto/ActivityFindSliceResponse.java @@ -0,0 +1,42 @@ +package com.gamsa.activity.dto; + +import com.gamsa.activity.constant.Category; +import com.gamsa.activity.domain.Activity; +import java.time.LocalDateTime; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@Builder +@RequiredArgsConstructor +public class ActivityFindSliceResponse { + + private final Long actId; + private final String actTitle; + private final String actLocation; + private final LocalDateTime noticeStartDate; + private final LocalDateTime noticeEndDate; + private final LocalDateTime actStartDate; + private final LocalDateTime actEndDate; + private final int actStartTime; + private final int actEndTime; + private final int recruitTotalNum; + private final Category category; + + public static ActivityFindSliceResponse from(Activity activity) { + return ActivityFindSliceResponse.builder() + .actId(activity.getActId()) + .actTitle(activity.getActTitle()) + .actLocation(activity.getActLocation()) + .noticeStartDate(activity.getNoticeStartDate()) + .noticeEndDate(activity.getNoticeEndDate()) + .actStartDate(activity.getActStartDate()) + .actEndDate(activity.getActEndDate()) + .actStartTime(activity.getActStartTime()) + .actEndTime(activity.getActEndTime()) + .recruitTotalNum(activity.getRecruitTotalNum()) + .category(activity.getCategory()) + .build(); + } +} diff --git a/src/main/java/com/gamsa/activity/dto/ActivitySaveRequest.java b/src/main/java/com/gamsa/activity/dto/ActivitySaveRequest.java index 29165f7..4dff7d0 100644 --- a/src/main/java/com/gamsa/activity/dto/ActivitySaveRequest.java +++ b/src/main/java/com/gamsa/activity/dto/ActivitySaveRequest.java @@ -1,5 +1,6 @@ package com.gamsa.activity.dto; +import com.gamsa.activity.constant.Category; import com.gamsa.activity.domain.Activity; import java.time.LocalDateTime; import lombok.Builder; @@ -29,6 +30,7 @@ public class ActivitySaveRequest { private final String actManager; private final String actPhone; private final String url; + private final Category category; public Activity toModel() { return Activity.builder() @@ -50,6 +52,7 @@ public Activity toModel() { .actManager(actManager) .actPhone(actPhone) .url(url) + .category(category) .build(); } } diff --git a/src/main/java/com/gamsa/activity/entity/ActivityJpaEntity.java b/src/main/java/com/gamsa/activity/entity/ActivityJpaEntity.java index a45a36a..7106db4 100644 --- a/src/main/java/com/gamsa/activity/entity/ActivityJpaEntity.java +++ b/src/main/java/com/gamsa/activity/entity/ActivityJpaEntity.java @@ -1,8 +1,11 @@ package com.gamsa.activity.entity; +import com.gamsa.activity.constant.Category; +import com.gamsa.activity.constant.CategoryConverter; import com.gamsa.activity.domain.Activity; import com.gamsa.common.entity.BaseEntity; import jakarta.persistence.Column; +import jakarta.persistence.Convert; import jakarta.persistence.Entity; import jakarta.persistence.Id; import jakarta.persistence.Table; @@ -24,7 +27,7 @@ public class ActivityJpaEntity extends BaseEntity { @Id - @Column(name = "id") + @Column(name = "act_id") private Long actId; @Column(name = "act_title", length = 255) @@ -85,6 +88,10 @@ public class ActivityJpaEntity extends BaseEntity { @Column(name = "url", length = 255) private String url; + @Convert(converter = CategoryConverter.class) + @Column(name = "category", length = 255) + private Category category; + public static ActivityJpaEntity from(Activity activity) { return ActivityJpaEntity.builder() .actId(activity.getActId()) @@ -105,6 +112,7 @@ public static ActivityJpaEntity from(Activity activity) { .actManager(activity.getActManager()) .actPhone(activity.getActPhone()) .url(activity.getUrl()) + .category(activity.getCategory()) .build(); } @@ -128,6 +136,7 @@ public Activity toModel() { .actManager(actManager) .actPhone(actPhone) .url(url) + .category(category) .build(); } diff --git a/src/main/java/com/gamsa/activity/exception/ActivityException.java b/src/main/java/com/gamsa/activity/exception/ActivityException.java new file mode 100644 index 0000000..e9ee075 --- /dev/null +++ b/src/main/java/com/gamsa/activity/exception/ActivityException.java @@ -0,0 +1,12 @@ +package com.gamsa.activity.exception; + +import com.gamsa.activity.constant.ActivityErrorCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class ActivityException extends RuntimeException { + + private final ActivityErrorCode activityErrorCode; +} diff --git a/src/main/java/com/gamsa/activity/repository/ActivityCustomRepository.java b/src/main/java/com/gamsa/activity/repository/ActivityCustomRepository.java new file mode 100644 index 0000000..862b484 --- /dev/null +++ b/src/main/java/com/gamsa/activity/repository/ActivityCustomRepository.java @@ -0,0 +1,12 @@ +package com.gamsa.activity.repository; + +import com.gamsa.activity.dto.ActivityFilterRequest; +import com.gamsa.activity.entity.ActivityJpaEntity; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; + +public interface ActivityCustomRepository { + + Slice findSlice(ActivityFilterRequest request, Pageable pageable); + +} diff --git a/src/main/java/com/gamsa/activity/repository/ActivityCustomRepositoryImpl.java b/src/main/java/com/gamsa/activity/repository/ActivityCustomRepositoryImpl.java new file mode 100644 index 0000000..003d7fd --- /dev/null +++ b/src/main/java/com/gamsa/activity/repository/ActivityCustomRepositoryImpl.java @@ -0,0 +1,67 @@ +package com.gamsa.activity.repository; + +import static com.gamsa.activity.entity.QActivityJpaEntity.activityJpaEntity; + +import com.gamsa.activity.dto.ActivityFilterRequest; +import com.gamsa.activity.entity.ActivityJpaEntity; +import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.types.Order; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.dsl.PathBuilder; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.ArrayList; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; + +@RequiredArgsConstructor +public class ActivityCustomRepositoryImpl implements ActivityCustomRepository { + + private final JPAQueryFactory jpaQueryFactory; + + @Override + public Slice findSlice(ActivityFilterRequest request, Pageable pageable) { + List orders = getAllOrderSpecifiers(pageable); + BooleanBuilder filterBuilder = ActivityFilterBuilder.createFilter(request); + + List results = jpaQueryFactory + .selectFrom(activityJpaEntity) + .where(filterBuilder) + .orderBy(orders.toArray(OrderSpecifier[]::new)) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize() + 1) + .fetch(); + + return checkLastPage(pageable, results); + } + + // 무한스크롤 처리 + private Slice checkLastPage(Pageable pageable, + List results) { + + boolean hasNext = false; + if (results.size() > pageable.getPageSize()) { + hasNext = true; + results.remove(pageable.getPageSize()); + } + return new SliceImpl<>(results, pageable, hasNext); + } + + // 정렬 기준에 맞는 OrderSpecifier 생성 + private List getAllOrderSpecifiers(Pageable pageable) { + + List orders = new ArrayList<>(); + + pageable.getSort().stream() + .forEach(order -> { + Order direction = order.getDirection().isAscending() ? Order.ASC : Order.DESC; + String property = order.getProperty(); + PathBuilder orderPath = new PathBuilder(ActivityJpaEntity.class, + "activityJpaEntity"); + orders.add(new OrderSpecifier(direction, orderPath.get(property))); + }); + return orders; + } +} diff --git a/src/main/java/com/gamsa/activity/repository/ActivityFilterBuilder.java b/src/main/java/com/gamsa/activity/repository/ActivityFilterBuilder.java new file mode 100644 index 0000000..4da810a --- /dev/null +++ b/src/main/java/com/gamsa/activity/repository/ActivityFilterBuilder.java @@ -0,0 +1,39 @@ +package com.gamsa.activity.repository; + +import static com.gamsa.activity.entity.QActivityJpaEntity.activityJpaEntity; + +import com.gamsa.activity.constant.Category; +import com.gamsa.activity.dto.ActivityFilterRequest; +import com.querydsl.core.BooleanBuilder; +import java.time.LocalDateTime; + +public class ActivityFilterBuilder { + + public static BooleanBuilder createFilter(ActivityFilterRequest request) { + BooleanBuilder filterBuilder = new BooleanBuilder(); + + eqCategory(filterBuilder, request.getCategory()); + isTeenPossibleOnly(filterBuilder, request.isTeenPossibleOnly()); + isDeadlineEndOnly(filterBuilder, request.isBeforeDeadlineOnly()); + + return filterBuilder; + } + + public static void eqCategory(BooleanBuilder filterBuilder, Category category) { + if (category != null) { + filterBuilder.and(activityJpaEntity.category.eq(category)); + } + } + + public static void isTeenPossibleOnly(BooleanBuilder filterBuilder, boolean teenPossibleOnly) { + if (teenPossibleOnly) { + filterBuilder.and(activityJpaEntity.teenPossible.isTrue()); + } + } + + public static void isDeadlineEndOnly(BooleanBuilder filterBuilder, boolean beforeDeadlineOnly) { + if (beforeDeadlineOnly) { + filterBuilder.and(activityJpaEntity.noticeEndDate.after(LocalDateTime.now())); + } + } +} diff --git a/src/main/java/com/gamsa/activity/repository/ActivityJpaRepository.java b/src/main/java/com/gamsa/activity/repository/ActivityJpaRepository.java index c20b7b2..969f10e 100644 --- a/src/main/java/com/gamsa/activity/repository/ActivityJpaRepository.java +++ b/src/main/java/com/gamsa/activity/repository/ActivityJpaRepository.java @@ -3,6 +3,7 @@ import com.gamsa.activity.entity.ActivityJpaEntity; import org.springframework.data.jpa.repository.JpaRepository; -public interface ActivityJpaRepository extends JpaRepository { +public interface ActivityJpaRepository extends JpaRepository, + ActivityCustomRepository { } diff --git a/src/main/java/com/gamsa/activity/repository/ActivityRepository.java b/src/main/java/com/gamsa/activity/repository/ActivityRepository.java index 57ce1d8..ad9e60e 100644 --- a/src/main/java/com/gamsa/activity/repository/ActivityRepository.java +++ b/src/main/java/com/gamsa/activity/repository/ActivityRepository.java @@ -1,14 +1,16 @@ package com.gamsa.activity.repository; import com.gamsa.activity.domain.Activity; -import java.util.List; +import com.gamsa.activity.dto.ActivityFilterRequest; import java.util.Optional; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; public interface ActivityRepository { void save(Activity activity); - List findAll(); + Slice findSlice(ActivityFilterRequest request, Pageable pageable); Optional findById(Long activityId); } diff --git a/src/main/java/com/gamsa/activity/repository/ActivityRepositoryImpl.java b/src/main/java/com/gamsa/activity/repository/ActivityRepositoryImpl.java index 7d28dd6..b2ec5f1 100644 --- a/src/main/java/com/gamsa/activity/repository/ActivityRepositoryImpl.java +++ b/src/main/java/com/gamsa/activity/repository/ActivityRepositoryImpl.java @@ -1,10 +1,12 @@ package com.gamsa.activity.repository; import com.gamsa.activity.domain.Activity; +import com.gamsa.activity.dto.ActivityFilterRequest; import com.gamsa.activity.entity.ActivityJpaEntity; -import java.util.List; import java.util.Optional; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import org.springframework.stereotype.Repository; @RequiredArgsConstructor @@ -19,10 +21,9 @@ public void save(Activity activity) { } @Override - public List findAll() { - return activityJpaRepository.findAll() - .stream().map(ActivityJpaEntity::toModel) - .toList(); + public Slice findSlice(ActivityFilterRequest request, Pageable pageable) { + return activityJpaRepository.findSlice(request, pageable) + .map(ActivityJpaEntity::toModel); } @Override diff --git a/src/main/java/com/gamsa/activity/service/ActivityService.java b/src/main/java/com/gamsa/activity/service/ActivityService.java index 8a460fd..2c8df8c 100644 --- a/src/main/java/com/gamsa/activity/service/ActivityService.java +++ b/src/main/java/com/gamsa/activity/service/ActivityService.java @@ -1,14 +1,16 @@ package com.gamsa.activity.service; +import com.gamsa.activity.constant.ActivityErrorCode; import com.gamsa.activity.domain.Activity; import com.gamsa.activity.dto.ActivityDetailResponse; -import com.gamsa.activity.dto.ActivityFindAllResponse; +import com.gamsa.activity.dto.ActivityFilterRequest; +import com.gamsa.activity.dto.ActivityFindSliceResponse; import com.gamsa.activity.dto.ActivitySaveRequest; -import com.gamsa.activity.exception.ActivityCustomException; +import com.gamsa.activity.exception.ActivityException; import com.gamsa.activity.repository.ActivityRepository; -import com.gamsa.common.constant.ErrorCode; -import java.util.List; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import org.springframework.stereotype.Service; @RequiredArgsConstructor @@ -20,21 +22,20 @@ public class ActivityService { public void save(ActivitySaveRequest saveRequest) { activityRepository.findById(saveRequest.getActId()) .ifPresent(activity -> { - throw new ActivityCustomException(ErrorCode.ACTIVITY_ALREADY_EXISTS); + throw new ActivityException(ActivityErrorCode.ACTIVITY_ALREADY_EXISTS); }); activityRepository.save(saveRequest.toModel()); } - public List findAll() { - List activities = activityRepository.findAll(); - return activities.stream() - .map(ActivityFindAllResponse::from) - .toList(); + public Slice findSlice(ActivityFilterRequest request, + Pageable pageable) { + Slice activities = activityRepository.findSlice(request, pageable); + return activities.map(ActivityFindSliceResponse::from); } public ActivityDetailResponse findById(Long activityId) { Activity activity = activityRepository.findById(activityId) - .orElseThrow(() -> new ActivityCustomException(ErrorCode.ACTIVITY_NOT_EXISTS)); + .orElseThrow(() -> new ActivityException(ActivityErrorCode.ACTIVITY_NOT_EXISTS)); return ActivityDetailResponse.from(activity); } } diff --git a/src/main/java/com/gamsa/auth/SecurityConfig.java b/src/main/java/com/gamsa/auth/SecurityConfig.java index 571302d..0898128 100644 --- a/src/main/java/com/gamsa/auth/SecurityConfig.java +++ b/src/main/java/com/gamsa/auth/SecurityConfig.java @@ -5,6 +5,7 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; import org.springframework.security.web.SecurityFilterChain; @Configuration @@ -15,6 +16,8 @@ public class SecurityConfig { public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(AbstractHttpConfigurer::disable) + .headers(headerConfig -> headerConfig.frameOptions( + HeadersConfigurer.FrameOptionsConfig::sameOrigin)) .authorizeHttpRequests(authorizeRequest -> { authorizeRequest .anyRequest().permitAll(); diff --git a/src/main/java/com/gamsa/avatar/constant/AgeRange.java b/src/main/java/com/gamsa/avatar/constant/AgeRange.java new file mode 100644 index 0000000..9da6952 --- /dev/null +++ b/src/main/java/com/gamsa/avatar/constant/AgeRange.java @@ -0,0 +1,14 @@ +package com.gamsa.avatar.constant; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum AgeRange { + TEENAGE("청소년"), + UNDERGRADUATE("대학생"), + ADULT("성인"); + + private final String name; +} diff --git a/src/main/java/com/gamsa/avatar/constant/AgeRangeConverter.java b/src/main/java/com/gamsa/avatar/constant/AgeRangeConverter.java new file mode 100644 index 0000000..f93c2d8 --- /dev/null +++ b/src/main/java/com/gamsa/avatar/constant/AgeRangeConverter.java @@ -0,0 +1,17 @@ +package com.gamsa.avatar.constant; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +@Converter(autoApply = true) +public class AgeRangeConverter implements AttributeConverter { + @Override + public String convertToDatabaseColumn(AgeRange attribute) { + return attribute.toString(); + } + + @Override + public AgeRange convertToEntityAttribute(String dbData) { + return AgeRange.valueOf(dbData); + } +} diff --git a/src/main/java/com/gamsa/avatar/constant/Experienced.java b/src/main/java/com/gamsa/avatar/constant/Experienced.java new file mode 100644 index 0000000..c54cddc --- /dev/null +++ b/src/main/java/com/gamsa/avatar/constant/Experienced.java @@ -0,0 +1,14 @@ +package com.gamsa.avatar.constant; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum Experienced { + NOVICE("초심자"), + INTERMIDIATE("중급자"), + EXPERT("상급자"); + + private final String name; +} diff --git a/src/main/java/com/gamsa/avatar/constant/ExperiencedConverter.java b/src/main/java/com/gamsa/avatar/constant/ExperiencedConverter.java new file mode 100644 index 0000000..a25c395 --- /dev/null +++ b/src/main/java/com/gamsa/avatar/constant/ExperiencedConverter.java @@ -0,0 +1,17 @@ +package com.gamsa.avatar.constant; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +@Converter(autoApply = true) +public class ExperiencedConverter implements AttributeConverter { + @Override + public String convertToDatabaseColumn(Experienced attribute) { + return attribute.toString(); + } + + @Override + public Experienced convertToEntityAttribute(String dbData) { + return Experienced.valueOf(dbData); + } +} diff --git a/src/main/java/com/gamsa/avatar/controller/AvatarController.java b/src/main/java/com/gamsa/avatar/controller/AvatarController.java new file mode 100644 index 0000000..9008cbd --- /dev/null +++ b/src/main/java/com/gamsa/avatar/controller/AvatarController.java @@ -0,0 +1,41 @@ +package com.gamsa.avatar.controller; + + +import com.gamsa.avatar.dto.AvatarFindResponse; +import com.gamsa.avatar.dto.AvatarSaveRequest; +import com.gamsa.avatar.service.AvatarService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/v1/avatar") +public class AvatarController { + + private final AvatarService avatarService; + + @PostMapping + public ResponseEntity saveAvatar(@RequestBody AvatarSaveRequest saveRequest) { + avatarService.save(saveRequest); + return new ResponseEntity<>(HttpStatus.CREATED); + } + + @GetMapping("{avatar-id}") + public AvatarFindResponse getAvatar(@PathVariable Long avatarId) { + return avatarService.findById(avatarId); + } + + @PutMapping("{avatar-id}") + public ResponseEntity updateAvatar(@PathVariable Long avatarId, @RequestBody AvatarSaveRequest saveRequest) { + avatarService.update(avatarId, saveRequest); + return new ResponseEntity<>(HttpStatus.OK); + } + + @DeleteMapping("{avatar-Id}") + public ResponseEntity deleteAvatar(@PathVariable Long avatarId) { + avatarService.delete(avatarId); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } +} diff --git a/src/main/java/com/gamsa/avatar/domain/Avatar.java b/src/main/java/com/gamsa/avatar/domain/Avatar.java new file mode 100644 index 0000000..9242853 --- /dev/null +++ b/src/main/java/com/gamsa/avatar/domain/Avatar.java @@ -0,0 +1,42 @@ +package com.gamsa.avatar.domain; + +import com.gamsa.avatar.constant.AgeRange; +import com.gamsa.avatar.constant.Experienced; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +@AllArgsConstructor +public class Avatar { + private Long avatarId; + private Long avatarExp; + private Long avatarLevel; + private String nickname; + private AgeRange ageRange; + private Experienced experienced; + + public void expUp(int amount) { + this.avatarExp += amount; + checkLevel(); + } + + public void checkLevel() { + if(this.avatarExp >= this.avatarLevel * this.avatarLevel * 100) { + avatarLevel += 1; + } + } + + public void changeExperience(Experienced experienced) { + this.experienced = experienced; + } + + public void changeAgeRange(AgeRange ageRange) { + this.ageRange = ageRange; + } + + public void changeNickname(String nickname) { + this.nickname = nickname; + } +} diff --git a/src/main/java/com/gamsa/avatar/dto/AvatarFindResponse.java b/src/main/java/com/gamsa/avatar/dto/AvatarFindResponse.java new file mode 100644 index 0000000..f016524 --- /dev/null +++ b/src/main/java/com/gamsa/avatar/dto/AvatarFindResponse.java @@ -0,0 +1,33 @@ +package com.gamsa.avatar.dto; + +import com.gamsa.avatar.constant.AgeRange; +import com.gamsa.avatar.constant.Experienced; +import com.gamsa.avatar.domain.Avatar; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.time.LocalDateTime; + +@Getter +@Builder +@RequiredArgsConstructor +public class AvatarFindResponse { + private final Long avatarId; + private final Long avatarExp; + private final Long avatarLevel; + private final String nickName; + private final AgeRange ageRange; + private final Experienced exprienced; + + public static AvatarFindResponse from(Avatar avatar) { + return AvatarFindResponse.builder() + .avatarId(avatar.getAvatarId()) + .avatarExp(avatar.getAvatarExp()) + .avatarLevel(avatar.getAvatarLevel()) + .nickName(avatar.getNickname()) + .ageRange(avatar.getAgeRange()) + .exprienced(avatar.getExperienced()) + .build(); + } +} diff --git a/src/main/java/com/gamsa/avatar/dto/AvatarSaveRequest.java b/src/main/java/com/gamsa/avatar/dto/AvatarSaveRequest.java new file mode 100644 index 0000000..3efd859 --- /dev/null +++ b/src/main/java/com/gamsa/avatar/dto/AvatarSaveRequest.java @@ -0,0 +1,35 @@ +package com.gamsa.avatar.dto; + +import com.gamsa.avatar.constant.AgeRange; +import com.gamsa.avatar.constant.Experienced; +import com.gamsa.avatar.domain.Avatar; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.NotNull; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.time.LocalDateTime; + +@Getter +@Builder +@RequiredArgsConstructor +public class AvatarSaveRequest { + @NotNull(message = "닉네임은 비어있으면 안됩니다.") + @Max(value=10, message = "닉네임은 최대 10자입니다.") + private final String nickname; + @NotNull(message = "연령대를 선택해야 합니다.") + private final AgeRange ageRange; + @NotNull(message = "봉사 활동 경험을 선택해야 합니다.") + private final Experienced experienced; + + public Avatar toModel() { + return Avatar.builder() + .nickname(nickname) + .avatarExp(0L) + .avatarLevel(0L) + .ageRange(ageRange) + .experienced(experienced) + .build(); + } +} diff --git a/src/main/java/com/gamsa/avatar/entity/AvatarJpaEntity.java b/src/main/java/com/gamsa/avatar/entity/AvatarJpaEntity.java new file mode 100644 index 0000000..ca9d4ec --- /dev/null +++ b/src/main/java/com/gamsa/avatar/entity/AvatarJpaEntity.java @@ -0,0 +1,63 @@ +package com.gamsa.avatar.entity; + +import com.gamsa.avatar.constant.AgeRange; +import com.gamsa.avatar.constant.AgeRangeConverter; +import com.gamsa.avatar.constant.Experienced; +import com.gamsa.avatar.constant.ExperiencedConverter; +import com.gamsa.avatar.domain.Avatar; +import com.gamsa.common.entity.BaseEntity; +import jakarta.persistence.*; +import lombok.*; + +@Getter +@Setter +@Builder +@Entity +@Table(name = "Avatar") +@AllArgsConstructor +@NoArgsConstructor +public class AvatarJpaEntity extends BaseEntity { + @Id + @GeneratedValue + @Column(name = "id") + private Long avatarId; + + @Column(name = "avatar_exp") + private Long avatarExp; + + @Column(name = "avatar_level") + private Long avatarLevel; + + @Column(name = "nickname") + private String nickname; + + @Convert(converter = AgeRangeConverter.class) + @Column(name = "age_range") + private AgeRange ageRange; + + @Convert(converter = ExperiencedConverter.class) + @Column(name = "experienced") + private Experienced experienced; + + public static AvatarJpaEntity from(Avatar avatar) { + return AvatarJpaEntity.builder() + .avatarId(avatar.getAvatarId()) + .avatarExp(avatar.getAvatarExp()) + .avatarLevel(avatar.getAvatarLevel()) + .nickname(avatar.getNickname()) + .ageRange(avatar.getAgeRange()) + .experienced(avatar.getExperienced()) + .build(); + } + + public Avatar toModel() { + return Avatar.builder() + .avatarId(avatarId) + .avatarExp(avatarExp) + .avatarLevel(avatarLevel) + .nickname(nickname) + .ageRange(ageRange) + .experienced(experienced) + .build(); + } +} diff --git a/src/main/java/com/gamsa/avatar/repository/AvatarJpaRepository.java b/src/main/java/com/gamsa/avatar/repository/AvatarJpaRepository.java new file mode 100644 index 0000000..08a8155 --- /dev/null +++ b/src/main/java/com/gamsa/avatar/repository/AvatarJpaRepository.java @@ -0,0 +1,12 @@ +package com.gamsa.avatar.repository; + +import com.gamsa.avatar.domain.Avatar; +import com.gamsa.avatar.entity.AvatarJpaEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface AvatarJpaRepository extends JpaRepository { +} diff --git a/src/main/java/com/gamsa/avatar/repository/AvatarRepository.java b/src/main/java/com/gamsa/avatar/repository/AvatarRepository.java new file mode 100644 index 0000000..fc6a8eb --- /dev/null +++ b/src/main/java/com/gamsa/avatar/repository/AvatarRepository.java @@ -0,0 +1,13 @@ +package com.gamsa.avatar.repository; + +import com.gamsa.avatar.domain.Avatar; + +import java.util.Optional; + +public interface AvatarRepository { + void save(Avatar avatar); + + Optional findById(Long id); + + void deleteById(Long id); +} diff --git a/src/main/java/com/gamsa/avatar/repository/AvatarRepositoryImpl.java b/src/main/java/com/gamsa/avatar/repository/AvatarRepositoryImpl.java new file mode 100644 index 0000000..bb75b85 --- /dev/null +++ b/src/main/java/com/gamsa/avatar/repository/AvatarRepositoryImpl.java @@ -0,0 +1,29 @@ +package com.gamsa.avatar.repository; + +import com.gamsa.avatar.domain.Avatar; +import com.gamsa.avatar.entity.AvatarJpaEntity; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@RequiredArgsConstructor +@Repository +public class AvatarRepositoryImpl implements AvatarRepository { + private final AvatarJpaRepository avatarJpaRepository; + + @Override + public void save(Avatar avatar) { + avatarJpaRepository.save(AvatarJpaEntity.from(avatar)); + } + + @Override + public Optional findById(Long id) { + return avatarJpaRepository.findById(id).map(AvatarJpaEntity::toModel); + } + + @Override + public void deleteById(Long id) { + avatarJpaRepository.deleteById(id); + } +} diff --git a/src/main/java/com/gamsa/avatar/service/AvatarService.java b/src/main/java/com/gamsa/avatar/service/AvatarService.java new file mode 100644 index 0000000..b55c41a --- /dev/null +++ b/src/main/java/com/gamsa/avatar/service/AvatarService.java @@ -0,0 +1,48 @@ +package com.gamsa.avatar.service; + +import com.gamsa.avatar.domain.Avatar; +import com.gamsa.avatar.dto.AvatarFindResponse; +import com.gamsa.avatar.dto.AvatarSaveRequest; +import com.gamsa.avatar.repository.AvatarRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.NoSuchElementException; + +@RequiredArgsConstructor +@Service +public class AvatarService { + private final AvatarRepository avatarRepository; + + public AvatarFindResponse findById(Long id) { + Avatar avatar = avatarRepository.findById(id).orElseThrow(NoSuchElementException::new); + return AvatarFindResponse.from(avatar); + } + + public void save(AvatarSaveRequest saveRequest) { + Avatar avatar = saveRequest.toModel(); + avatarRepository.findById(avatar.getAvatarId()).orElseThrow(NoSuchElementException::new); + avatarRepository.save(avatar); + } + + public void delete(Long id) { + avatarRepository.findById(id).orElseThrow(NoSuchElementException::new); + avatarRepository.deleteById(id); + } + + public AvatarFindResponse expUp(Long avatarId, int amount) { + Avatar avatar = avatarRepository.findById(avatarId).orElseThrow(NoSuchElementException::new); + avatar.expUp(amount); + avatarRepository.save(avatar); + return AvatarFindResponse.from(avatar); + } + + public AvatarFindResponse update(Long avatarId, AvatarSaveRequest saveRequest) { + Avatar avatar = avatarRepository.findById(avatarId).orElseThrow(NoSuchElementException::new); + avatar.changeAgeRange(saveRequest.getAgeRange()); + avatar.changeExperience(saveRequest.getExperienced()); + avatar.changeNickname(saveRequest.getNickname()); + avatarRepository.save(avatar); + return AvatarFindResponse.from(avatar); + } +} diff --git a/src/main/java/com/gamsa/common/config/QueryDslConfig.java b/src/main/java/com/gamsa/common/config/QueryDslConfig.java new file mode 100644 index 0000000..98912e7 --- /dev/null +++ b/src/main/java/com/gamsa/common/config/QueryDslConfig.java @@ -0,0 +1,19 @@ +package com.gamsa.common.config; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class QueryDslConfig { + + @PersistenceContext + private EntityManager entityManager; + + @Bean + public JPAQueryFactory jpaQueryFactory() { + return new JPAQueryFactory(entityManager); + } +} diff --git a/src/main/java/com/gamsa/common/exception/GlobalExceptionHandler.java b/src/main/java/com/gamsa/common/exception/GlobalExceptionHandler.java index f6aa449..8c71a3d 100644 --- a/src/main/java/com/gamsa/common/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/gamsa/common/exception/GlobalExceptionHandler.java @@ -1,6 +1,6 @@ package com.gamsa.common.exception; -import com.gamsa.activity.exception.ActivityCustomException; +import com.gamsa.activity.exception.ActivityException; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -10,11 +10,11 @@ @RestControllerAdvice public class GlobalExceptionHandler { - @ExceptionHandler(ActivityCustomException.class) - private ResponseEntity ActivityCustomExceptionHandler(ActivityCustomException e) { + @ExceptionHandler(ActivityException.class) + private ResponseEntity ActivityCustomExceptionHandler(ActivityException e) { log.error(String.valueOf(e.getStackTrace()[0])); return ResponseEntity - .status(e.getErrorCode().getStatus()) - .body(e.getErrorCode().getMsg()); + .status(e.getActivityErrorCode().getStatus()) + .body(e.getActivityErrorCode().getMsg()); } } diff --git a/src/test/java/com/gamsa/activity/entity/ActivityJpaEntityTest.java b/src/test/java/com/gamsa/activity/entity/ActivityJpaEntityTest.java index b83861f..c8db623 100644 --- a/src/test/java/com/gamsa/activity/entity/ActivityJpaEntityTest.java +++ b/src/test/java/com/gamsa/activity/entity/ActivityJpaEntityTest.java @@ -2,6 +2,7 @@ import static org.assertj.core.api.Assertions.assertThat; +import com.gamsa.activity.constant.Category; import com.gamsa.activity.domain.Activity; import java.time.LocalDateTime; import org.junit.jupiter.api.Test; @@ -29,6 +30,7 @@ class ActivityJpaEntityTest { .actWeek(0111110) .actManager("윤순영") .actPhone("032-577-3026") + .category(Category.OTHER_ACTIVITIES) .url("https://...") .build(); @@ -61,6 +63,7 @@ class ActivityJpaEntityTest { .actManager("윤순영") .actPhone("032-577-3026") .url("https://...") + .category(Category.OTHER_ACTIVITIES) .build(); // when diff --git a/src/test/java/com/gamsa/activity/repository/ActivityJpaRepositoryTest.java b/src/test/java/com/gamsa/activity/repository/ActivityJpaRepositoryTest.java index 7d115b9..37b457d 100644 --- a/src/test/java/com/gamsa/activity/repository/ActivityJpaRepositoryTest.java +++ b/src/test/java/com/gamsa/activity/repository/ActivityJpaRepositoryTest.java @@ -2,27 +2,37 @@ import static org.assertj.core.api.Assertions.assertThat; +import com.gamsa.activity.constant.Category; +import com.gamsa.activity.dto.ActivityFilterRequest; import com.gamsa.activity.entity.ActivityJpaEntity; +import com.gamsa.common.config.TestConfig; import java.time.LocalDateTime; import java.util.List; +import java.util.Optional; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort.Direction; @DataJpaTest +@Import(TestConfig.class) class ActivityJpaRepositoryTest { @Autowired private ActivityJpaRepository activityJpaRepository; + private final ActivityJpaEntity jpaEntity = ActivityJpaEntity.builder() .actId(1L) .actTitle("어린이놀이안전관리 및 놀잇감 청결유지 및 정리") .actLocation("아이사랑꿈터 서구 5호점") .description("봉사 내용") .noticeStartDate(LocalDateTime.of(2024, 9, 10, 0, 0)) - .noticeEndDate(LocalDateTime.of(2024, 12, 7, 0, 0)) + .noticeEndDate(LocalDateTime.of(2024, 9, 20, 0, 0)) .actStartDate(LocalDateTime.of(2024, 9, 10, 0, 0)) - .actEndDate(LocalDateTime.of(2024, 12, 7, 0, 0)) + .actEndDate(LocalDateTime.of(2024, 9, 20, 0, 0)) .actStartTime(13) .actEndTime(18) .recruitTotalNum(1) @@ -33,8 +43,65 @@ class ActivityJpaRepositoryTest { .actManager("윤순영") .actPhone("032-577-3026") .url("https://...") + .category(Category.OTHER_ACTIVITIES) .build(); + private final ActivityJpaEntity jpaEntity2 = ActivityJpaEntity.builder() + .actId(2L) + .actTitle("어린이놀이안전관리 청소") + .actLocation("아이사랑꿈터 서구 7호점") + .description("봉사 내용2") + .noticeStartDate(LocalDateTime.of(2024, 11, 1, 0, 0)) + .noticeEndDate(LocalDateTime.of(2024, 12, 8, 0, 0)) + .actStartDate(LocalDateTime.of(2024, 11, 1, 0, 0)) + .actEndDate(LocalDateTime.of(2024, 12, 8, 0, 0)) + .actStartTime(10) + .actEndTime(20) + .recruitTotalNum(2) + .adultPossible(true) + .teenPossible(true) + .groupPossible(false) + .actWeek(0111110) + .actManager("홀란드") + .actPhone("032-111-2222") + .url("https://...") + .category(Category.EDUCATION_AND_MENTORING) + .build(); + + private final ActivityJpaEntity jpaEntity3 = ActivityJpaEntity.builder() + .actId(3L) + .actTitle("학교") + .actLocation("도서관") + .description("책 정리") + .noticeStartDate(LocalDateTime.of(2025, 1, 1, 0, 0)) + .noticeEndDate(LocalDateTime.of(2025, 1, 8, 0, 0)) + .actStartDate(LocalDateTime.of(2025, 1, 1, 0, 0)) + .actEndDate(LocalDateTime.of(2025, 1, 8, 0, 0)) + .actStartTime(10) + .actEndTime(20) + .recruitTotalNum(5) + .adultPossible(true) + .teenPossible(true) + .groupPossible(false) + .actWeek(0111110) + .actManager("사서쌤") + .actPhone("032-111-2222") + .url("https://...") + .category(Category.ADMINISTRATIVE_AND_OFFICE_SUPPORT) + .build(); + + // 필터링 + private final ActivityFilterRequest noFilterReq = new ActivityFilterRequest( + null, false, false); + + private final ActivityFilterRequest otherCategoryFilterReq = new ActivityFilterRequest( + Category.OTHER_ACTIVITIES, false, false); + + private final ActivityFilterRequest teenPossibleFilterReq = new ActivityFilterRequest( + null, true, false); + + private final ActivityFilterRequest beforeDeadlineFilterReq = new ActivityFilterRequest( + null, false, true); @Test void 새_활동_저장() { @@ -50,11 +117,132 @@ class ActivityJpaRepositoryTest { void 모든_활동_리스트_반환() { // given activityJpaRepository.save(jpaEntity); + // when + List content = activityJpaRepository + .findSlice(noFilterReq, PageRequest.of(0, 10)) + .getContent(); + // then + assertThat(content.size()).isEqualTo(1); + } + + @Test + void 활동_상세정보_조회() { + // given + activityJpaRepository.save(jpaEntity); + // when + Optional result = activityJpaRepository.findById(1L); + // then + assertThat(result.get().getActTitle()).isEqualTo(jpaEntity.getActTitle()); + } + + @Test + void id로_정렬된_조회() { + // given + activityJpaRepository.save(jpaEntity); // id = 1L + activityJpaRepository.save(jpaEntity2); // id = 2L + Pageable pageable = PageRequest.of(0, 2, Direction.DESC, "actId"); + + // when + List content = activityJpaRepository.findSlice(noFilterReq, pageable) + .getContent(); + + // then + assertThat(content.size()).isEqualTo(2); + assertThat(content.getFirst().getActId()).isEqualTo(2L); + assertThat(content.get(1).getActId()).isEqualTo(1L); + } + + @Test + void 마감_날짜_오름차순_정렬_조회() { + // given + activityJpaRepository.save(jpaEntity); + activityJpaRepository.save(jpaEntity2); + activityJpaRepository.save(jpaEntity3); + Pageable pageable = PageRequest + .of(0, 3, Direction.ASC, "noticeEndDate"); + + // when + List content = activityJpaRepository.findSlice(noFilterReq, pageable) + .getContent(); + + // then + assertThat(content.size()).isEqualTo(3); + assertThat(content.get(0).getNoticeEndDate().isBefore(content.get(1).getNoticeEndDate())) + .isTrue(); + assertThat(content.get(1).getNoticeEndDate().isBefore(content.get(2).getNoticeEndDate())) + .isTrue(); + } + + @Test + void 마감된_공고중_마감_날짜_가까운순_정렬_조회() { + // given + LocalDateTime date = LocalDateTime.of(2024, 10, 1, 0, 0); + activityJpaRepository.save(jpaEntity); + activityJpaRepository.save(jpaEntity2); + activityJpaRepository.save(jpaEntity3); + Pageable pageable = PageRequest + .of(0, 3, Direction.ASC, "noticeEndDate"); + + // when + List content1 = activityJpaRepository + .findSlice(noFilterReq, pageable).getContent(); + List content2 = activityJpaRepository + .findSlice(beforeDeadlineFilterReq, pageable).getContent(); + + // then + assertThat(content1.size()).isEqualTo(3); // 필터링 X + assertThat(content2.size()).isEqualTo(2); // 필터링 O + assertThat(content2.getFirst().getNoticeEndDate() + .isBefore(content2.getLast().getNoticeEndDate())) + .isTrue(); + } + + @Test + void 카테고리로_필터링_조회() { + // given + activityJpaRepository.save(jpaEntity); + activityJpaRepository.save(jpaEntity2); + Pageable pageable = PageRequest.of(0, 2, Direction.ASC, "actId"); + + // when + List content = activityJpaRepository.findSlice(otherCategoryFilterReq, + pageable).getContent(); + + // then + assertThat(content.size()).isEqualTo(1); + assertThat(content.getFirst().getCategory()).isEqualTo(Category.OTHER_ACTIVITIES); + } + + @Test + void 청소년_가능여부_필터링_조회() { + // given + activityJpaRepository.save(jpaEntity); + activityJpaRepository.save(jpaEntity2); + Pageable pageable = PageRequest.of(0, 2, Direction.ASC, "actId"); + + // when + List content = activityJpaRepository.findSlice(teenPossibleFilterReq, + pageable).getContent(); + + // then + assertThat(content.size()).isEqualTo(1); + assertThat(content.getFirst().isTeenPossible()).isTrue(); + } + + @Test + void 마감되지_않은_활동만_필터링_조회() { + // given + LocalDateTime date = LocalDateTime.of(2024, 10, 1, 0, 0); + activityJpaRepository.save(jpaEntity); + activityJpaRepository.save(jpaEntity2); + Pageable pageable = PageRequest.of(0, 2, Direction.ASC, "actId"); // when - List result = activityJpaRepository.findAll(); + List content = activityJpaRepository.findSlice(beforeDeadlineFilterReq, + pageable).getContent(); // then - assertThat(result.size()).isEqualTo(1); + assertThat(content.size()).isEqualTo(1); + assertThat(content.getFirst().getNoticeEndDate().isAfter(date)).isTrue(); } } \ No newline at end of file diff --git a/src/test/java/com/gamsa/activity/service/ActivityServiceTest.java b/src/test/java/com/gamsa/activity/service/ActivityServiceTest.java index 01c32a0..c3a8356 100644 --- a/src/test/java/com/gamsa/activity/service/ActivityServiceTest.java +++ b/src/test/java/com/gamsa/activity/service/ActivityServiceTest.java @@ -2,17 +2,19 @@ import static org.assertj.core.api.Assertions.assertThat; +import com.gamsa.activity.constant.ActivityErrorCode; +import com.gamsa.activity.constant.Category; import com.gamsa.activity.dto.ActivityDetailResponse; -import com.gamsa.activity.dto.ActivityFindAllResponse; +import com.gamsa.activity.dto.ActivityFindSliceResponse; import com.gamsa.activity.dto.ActivitySaveRequest; -import com.gamsa.activity.exception.ActivityCustomException; +import com.gamsa.activity.exception.ActivityException; import com.gamsa.activity.stub.StubEmptyActivityRepository; import com.gamsa.activity.stub.StubExistsActivityRepository; -import com.gamsa.common.constant.ErrorCode; import java.time.LocalDateTime; -import java.util.List; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; class ActivityServiceTest { @@ -35,6 +37,7 @@ class ActivityServiceTest { .actManager("윤순영") .actPhone("032-577-3026") .url("https://...") + .category(Category.OTHER_ACTIVITIES) .build(); @Test @@ -56,10 +59,10 @@ class ActivityServiceTest { ActivityService activityService = new ActivityService(new StubExistsActivityRepository()); // then - Assertions.assertThrows(ActivityCustomException.class, () -> { + Assertions.assertThrows(ActivityException.class, () -> { // when activityService.save(saveRequest); - }, ErrorCode.ACTIVITY_ALREADY_EXISTS.getMsg()); + }, ActivityErrorCode.ACTIVITY_ALREADY_EXISTS.getMsg()); } @Test @@ -68,10 +71,11 @@ class ActivityServiceTest { ActivityService activityService = new ActivityService(new StubEmptyActivityRepository()); // when - List result = activityService.findAll(); + Slice result = activityService.findSlice(null, + PageRequest.of(0, 10)); // then - assertThat(result.size()).isZero(); + assertThat(result.getContent().size()).isZero(); } @Test @@ -92,9 +96,9 @@ class ActivityServiceTest { ActivityService activityService = new ActivityService(new StubEmptyActivityRepository()); // then - Assertions.assertThrows(ActivityCustomException.class, () -> { + Assertions.assertThrows(ActivityException.class, () -> { // when activityService.findById(1L); - }, ErrorCode.ACTIVITY_NOT_EXISTS.getMsg()); + }, ActivityErrorCode.ACTIVITY_NOT_EXISTS.getMsg()); } } \ No newline at end of file diff --git a/src/test/java/com/gamsa/activity/stub/StubEmptyActivityRepository.java b/src/test/java/com/gamsa/activity/stub/StubEmptyActivityRepository.java index 0a59ff9..4b58491 100644 --- a/src/test/java/com/gamsa/activity/stub/StubEmptyActivityRepository.java +++ b/src/test/java/com/gamsa/activity/stub/StubEmptyActivityRepository.java @@ -1,9 +1,13 @@ package com.gamsa.activity.stub; import com.gamsa.activity.domain.Activity; +import com.gamsa.activity.dto.ActivityFilterRequest; import com.gamsa.activity.repository.ActivityRepository; import java.util.List; import java.util.Optional; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; public class StubEmptyActivityRepository implements ActivityRepository { @@ -13,8 +17,8 @@ public void save(Activity activity) { } @Override - public List findAll() { - return List.of(); + public Slice findSlice(ActivityFilterRequest request, Pageable pageable) { + return new SliceImpl<>(List.of()); } @Override diff --git a/src/test/java/com/gamsa/activity/stub/StubExistsActivityRepository.java b/src/test/java/com/gamsa/activity/stub/StubExistsActivityRepository.java index c2c845b..73bea05 100644 --- a/src/test/java/com/gamsa/activity/stub/StubExistsActivityRepository.java +++ b/src/test/java/com/gamsa/activity/stub/StubExistsActivityRepository.java @@ -1,10 +1,14 @@ package com.gamsa.activity.stub; import com.gamsa.activity.domain.Activity; +import com.gamsa.activity.dto.ActivityFilterRequest; import com.gamsa.activity.repository.ActivityRepository; import java.time.LocalDateTime; import java.util.List; import java.util.Optional; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; public class StubExistsActivityRepository implements ActivityRepository { @@ -35,8 +39,8 @@ public void save(Activity activity) { } @Override - public List findAll() { - return List.of(); + public Slice findSlice(ActivityFilterRequest request, Pageable pageable) { + return new SliceImpl<>(List.of(activity)); } @Override diff --git a/src/test/java/com/gamsa/avatar/entity/AvatarJpaEntityTest.java b/src/test/java/com/gamsa/avatar/entity/AvatarJpaEntityTest.java new file mode 100644 index 0000000..f3e14fc --- /dev/null +++ b/src/test/java/com/gamsa/avatar/entity/AvatarJpaEntityTest.java @@ -0,0 +1,48 @@ +package com.gamsa.avatar.entity; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.gamsa.avatar.constant.AgeRange; +import com.gamsa.avatar.constant.Experienced; +import com.gamsa.avatar.domain.Avatar; +import org.junit.jupiter.api.Test; + +public class AvatarJpaEntityTest { + @Test + void 도메인에서_JPA엔티티로() { + //given + Avatar avatar = Avatar.builder() + .avatarId(1L) + .avatarLevel(1L) + .avatarExp(1L) + .nickname("닉네임") + .ageRange(AgeRange.ADULT) + .experienced(Experienced.NOVICE) + .build(); + + //when + AvatarJpaEntity jpaEntity = AvatarJpaEntity.from(avatar); + + //then + assertThat(jpaEntity.getAvatarId()).isEqualTo(avatar.getAvatarId()); + } + + @Test + void JPA엔티티에서_도메인으로() { + //given + AvatarJpaEntity avatarJpaEntity = AvatarJpaEntity.builder() + .avatarId(1L) + .avatarLevel(1L) + .avatarExp(1L) + .nickname("닉네임") + .ageRange(AgeRange.ADULT) + .experienced(Experienced.NOVICE) + .build(); + + //when + Avatar avatar = avatarJpaEntity.toModel(); + + //then + assertThat(avatar.getAvatarId()).isEqualTo(avatarJpaEntity.getAvatarId()); + } +} diff --git a/src/test/java/com/gamsa/avatar/repository/AvatarJpaRepositoryTest.java b/src/test/java/com/gamsa/avatar/repository/AvatarJpaRepositoryTest.java new file mode 100644 index 0000000..8572c66 --- /dev/null +++ b/src/test/java/com/gamsa/avatar/repository/AvatarJpaRepositoryTest.java @@ -0,0 +1,50 @@ +package com.gamsa.avatar.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.gamsa.avatar.constant.AgeRange; +import com.gamsa.avatar.constant.Experienced; +import com.gamsa.avatar.entity.AvatarJpaEntity; +import com.gamsa.common.config.TestConfig; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + +import java.time.LocalDateTime; + +@DataJpaTest +public class AvatarJpaRepositoryTest { + @Autowired + private AvatarJpaRepository avatarJpaRepository; + + private final AvatarJpaEntity jpaEntity = AvatarJpaEntity.builder() + .avatarId(1L) + .avatarLevel(1L) + .avatarExp(1L) + .nickname("닉네임") + .ageRange(AgeRange.ADULT) + .experienced(Experienced.NOVICE) + .build(); + + @Test + void 새로운_유저_저장() { + //when + avatarJpaRepository.save(jpaEntity); + + //then + assertThat(avatarJpaRepository.findById(1L).get().getNickname()).isEqualTo(jpaEntity.getNickname()); + } + + @Test + void 저장된_유저_삭제() { + //given + avatarJpaRepository.save(jpaEntity); + + //when + avatarJpaRepository.deleteById(1L); + + //then + assertThat(avatarJpaRepository.findById(1L)).isEmpty(); + } +} diff --git a/src/test/java/com/gamsa/avatar/service/AvatarServiceTest.java b/src/test/java/com/gamsa/avatar/service/AvatarServiceTest.java new file mode 100644 index 0000000..3a16636 --- /dev/null +++ b/src/test/java/com/gamsa/avatar/service/AvatarServiceTest.java @@ -0,0 +1,56 @@ +package com.gamsa.avatar.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import com.gamsa.avatar.constant.AgeRange; +import com.gamsa.avatar.constant.Experienced; +import com.gamsa.avatar.dto.AvatarSaveRequest; +import com.gamsa.avatar.stub.StubAvatarRepository; +import org.junit.jupiter.api.Test; + +public class AvatarServiceTest { + AvatarSaveRequest saveRequest = AvatarSaveRequest.builder() + .nickname("닉네임") + .ageRange(AgeRange.ADULT) + .experienced(Experienced.EXPERT) + .build(); + + @Test + void 새로운_유저_저장() { + //given + AvatarService avatarService = new AvatarService(new StubAvatarRepository()); + + //then + assertDoesNotThrow(() -> avatarService.save(saveRequest)); + } + + @Test + void 기존_유저_검색() { + //given + AvatarService avatarService = new AvatarService(new StubAvatarRepository()); + + //then + assertThat(avatarService.findById(1L)).isNotNull(); + } + + @Test + void 기존_유저_삭제() { + //given + AvatarService avatarService = new AvatarService(new StubAvatarRepository()); + + //then + assertDoesNotThrow(() -> avatarService.delete(1L)); + } + + + + @Test + void 기존_유저_업데이트() { + //given + AvatarService avatarService = new AvatarService(new StubAvatarRepository()); + + //then + assertDoesNotThrow(() -> avatarService.save(saveRequest)); + } +} diff --git a/src/test/java/com/gamsa/avatar/stub/StubAvatarRepository.java b/src/test/java/com/gamsa/avatar/stub/StubAvatarRepository.java new file mode 100644 index 0000000..a24b653 --- /dev/null +++ b/src/test/java/com/gamsa/avatar/stub/StubAvatarRepository.java @@ -0,0 +1,31 @@ +package com.gamsa.avatar.stub; + +import com.gamsa.avatar.constant.AgeRange; +import com.gamsa.avatar.constant.Experienced; +import com.gamsa.avatar.domain.Avatar; +import com.gamsa.avatar.repository.AvatarRepository; + +import java.time.LocalDateTime; +import java.util.Optional; + +public class StubAvatarRepository implements AvatarRepository { + private final Avatar avatar = Avatar.builder() + .avatarId(1L) + .avatarLevel(1L) + .avatarExp(1L) + .nickname("닉네임") + .ageRange(AgeRange.ADULT) + .experienced(Experienced.NOVICE) + .build(); + + @Override + public void save(Avatar avatar) {} + + @Override + public Optional findById(Long id) { + return Optional.of(avatar); + } + + @Override + public void deleteById(Long id) {} +} diff --git a/src/test/java/com/gamsa/common/config/TestConfig.java b/src/test/java/com/gamsa/common/config/TestConfig.java new file mode 100644 index 0000000..996e012 --- /dev/null +++ b/src/test/java/com/gamsa/common/config/TestConfig.java @@ -0,0 +1,19 @@ +package com.gamsa.common.config; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +@TestConfiguration +public class TestConfig { + + @PersistenceContext + private EntityManager entityManager; + + @Bean + public JPAQueryFactory jpaQueryFactory() { + return new JPAQueryFactory(entityManager); + } +} From 6fd199712afde0c81dd0e7ab667894583774bb6f Mon Sep 17 00:00:00 2001 From: 5win <94297900+5win@users.noreply.github.com> Date: Wed, 9 Oct 2024 00:00:25 +0900 Subject: [PATCH 03/17] =?UTF-8?q?feat:=20=EB=B4=89=EC=82=AC=20=EA=B8=B0?= =?UTF-8?q?=EA=B4=80=20=EA=B4=80=EB=A0=A8=20=EA=B8=B0=EB=8A=A5=20=EB=B0=8F?= =?UTF-8?q?=20=EB=8B=A8=EC=9C=84=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#29)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [#21] fix: AvatarJpaRepositoryTest에 @Import 추가 * [#21] refact: Activity 도메인 lombok 수정 - AllArgsConstructor 제거 * [#21] feat: 봉사기관 테이블 생성 및 조회 - 봉사활동:봉사기관 = N:1 (ManyToOne) - ActivityDetail 응답에 기관 정보 포함 * [#21] test: 봉사기관 관련 단위테스트 * [#21] fix: 리뷰 후 1차 수정 사항 - InstituteDetailResponse로 명명 수정 - 위도, 경도 BigDecimal로 변경 --- .../activity/constant/ActivityErrorCode.java | 4 +- .../controller/InstituteController.java | 25 +++++++ .../com/gamsa/activity/domain/Activity.java | 3 +- .../com/gamsa/activity/domain/Institute.java | 18 +++++ .../activity/dto/ActivityDetailResponse.java | 2 + .../activity/dto/ActivitySaveRequest.java | 5 +- .../activity/dto/InstituteDetailResponse.java | 32 +++++++++ .../activity/dto/InstituteSaveRequest.java | 28 ++++++++ .../activity/entity/ActivityJpaEntity.java | 9 +++ .../activity/entity/InstituteJpaEntity.java | 69 +++++++++++++++++++ .../exception/ActivityCustomException.java | 12 ---- .../repository/InstituteJpaRepository.java | 10 +++ .../repository/InstituteRepository.java | 13 ++++ .../repository/InstituteRepositoryImpl.java | 31 +++++++++ .../activity/service/ActivityService.java | 10 ++- .../activity/service/InstituteService.java | 23 +++++++ .../entity/ActivityJpaEntityTest.java | 24 ++++++- .../entity/InstituteJpaEntityTest.java | 48 +++++++++++++ .../InstituteJpaRepositoryTest.java | 39 +++++++++++ .../activity/service/ActivityServiceTest.java | 34 +++++++-- .../service/InstituteServiceTest.java | 49 +++++++++++++ .../stub/StubEmptyInstituteRepository.java | 23 +++++++ .../stub/StubExistsActivityRepository.java | 12 ++++ .../stub/StubExistsInstituteRepository.java | 33 +++++++++ .../repository/AvatarJpaRepositoryTest.java | 3 +- 25 files changed, 534 insertions(+), 25 deletions(-) create mode 100644 src/main/java/com/gamsa/activity/controller/InstituteController.java create mode 100644 src/main/java/com/gamsa/activity/domain/Institute.java create mode 100644 src/main/java/com/gamsa/activity/dto/InstituteDetailResponse.java create mode 100644 src/main/java/com/gamsa/activity/dto/InstituteSaveRequest.java create mode 100644 src/main/java/com/gamsa/activity/entity/InstituteJpaEntity.java delete mode 100644 src/main/java/com/gamsa/activity/exception/ActivityCustomException.java create mode 100644 src/main/java/com/gamsa/activity/repository/InstituteJpaRepository.java create mode 100644 src/main/java/com/gamsa/activity/repository/InstituteRepository.java create mode 100644 src/main/java/com/gamsa/activity/repository/InstituteRepositoryImpl.java create mode 100644 src/main/java/com/gamsa/activity/service/InstituteService.java create mode 100644 src/test/java/com/gamsa/activity/entity/InstituteJpaEntityTest.java create mode 100644 src/test/java/com/gamsa/activity/repository/InstituteJpaRepositoryTest.java create mode 100644 src/test/java/com/gamsa/activity/service/InstituteServiceTest.java create mode 100644 src/test/java/com/gamsa/activity/stub/StubEmptyInstituteRepository.java create mode 100644 src/test/java/com/gamsa/activity/stub/StubExistsInstituteRepository.java diff --git a/src/main/java/com/gamsa/activity/constant/ActivityErrorCode.java b/src/main/java/com/gamsa/activity/constant/ActivityErrorCode.java index 163c36f..4e8770f 100644 --- a/src/main/java/com/gamsa/activity/constant/ActivityErrorCode.java +++ b/src/main/java/com/gamsa/activity/constant/ActivityErrorCode.java @@ -9,9 +9,11 @@ public enum ActivityErrorCode { // 404 Not Found : 존재하지 않는 리소스 접근 ACTIVITY_NOT_EXISTS(404, "존재하지 않는 활동입니다."), + INSTITUTE_NOT_EXISTS(404, "존재하지 않는 기관입니다."), // 409 Conflict : 요청이 현재 서버의 상태와 충돌 - ACTIVITY_ALREADY_EXISTS(409, "이미 존재하는 활동입니다."); + ACTIVITY_ALREADY_EXISTS(409, "이미 존재하는 활동입니다."), + INSTITUTE_ALREADY_EXISTS(409, "이미 존재하는 기관입니다."); private final int status; private final String msg; diff --git a/src/main/java/com/gamsa/activity/controller/InstituteController.java b/src/main/java/com/gamsa/activity/controller/InstituteController.java new file mode 100644 index 0000000..e25a285 --- /dev/null +++ b/src/main/java/com/gamsa/activity/controller/InstituteController.java @@ -0,0 +1,25 @@ +package com.gamsa.activity.controller; + +import com.gamsa.activity.dto.InstituteSaveRequest; +import com.gamsa.activity.service.InstituteService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/v1/institutes") +public class InstituteController { + + private final InstituteService instituteService; + + @PostMapping + public ResponseEntity save(@RequestBody InstituteSaveRequest saveRequest) { + instituteService.save(saveRequest); + return new ResponseEntity<>(HttpStatus.CREATED); + } +} diff --git a/src/main/java/com/gamsa/activity/domain/Activity.java b/src/main/java/com/gamsa/activity/domain/Activity.java index 662cd17..b88fe96 100644 --- a/src/main/java/com/gamsa/activity/domain/Activity.java +++ b/src/main/java/com/gamsa/activity/domain/Activity.java @@ -2,13 +2,11 @@ import com.gamsa.activity.constant.Category; import java.time.LocalDateTime; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @Getter @Builder -@AllArgsConstructor public class Activity { private Long actId; @@ -30,4 +28,5 @@ public class Activity { private String actPhone; private String url; private Category category; + private Institute institute; } diff --git a/src/main/java/com/gamsa/activity/domain/Institute.java b/src/main/java/com/gamsa/activity/domain/Institute.java new file mode 100644 index 0000000..e086ec3 --- /dev/null +++ b/src/main/java/com/gamsa/activity/domain/Institute.java @@ -0,0 +1,18 @@ +package com.gamsa.activity.domain; + +import java.math.BigDecimal; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class Institute { + + private Long instituteId; + private String name; + private String location; + private BigDecimal latitude; + private BigDecimal longitude; + // Todo 시군구 코드 + private String phone; +} diff --git a/src/main/java/com/gamsa/activity/dto/ActivityDetailResponse.java b/src/main/java/com/gamsa/activity/dto/ActivityDetailResponse.java index d45f6df..7743e52 100644 --- a/src/main/java/com/gamsa/activity/dto/ActivityDetailResponse.java +++ b/src/main/java/com/gamsa/activity/dto/ActivityDetailResponse.java @@ -32,6 +32,7 @@ public class ActivityDetailResponse { private final String actPhone; private final String url; private final Category category; + private final InstituteDetailResponse institute; public static ActivityDetailResponse from(Activity activity) { return ActivityDetailResponse.builder() @@ -54,6 +55,7 @@ public static ActivityDetailResponse from(Activity activity) { .actPhone(activity.getActPhone()) .url(activity.getUrl()) .category(activity.getCategory()) + .institute(InstituteDetailResponse.from(activity.getInstitute())) .build(); } } diff --git a/src/main/java/com/gamsa/activity/dto/ActivitySaveRequest.java b/src/main/java/com/gamsa/activity/dto/ActivitySaveRequest.java index 4dff7d0..9da43ac 100644 --- a/src/main/java/com/gamsa/activity/dto/ActivitySaveRequest.java +++ b/src/main/java/com/gamsa/activity/dto/ActivitySaveRequest.java @@ -2,6 +2,7 @@ import com.gamsa.activity.constant.Category; import com.gamsa.activity.domain.Activity; +import com.gamsa.activity.domain.Institute; import java.time.LocalDateTime; import lombok.Builder; import lombok.Getter; @@ -31,8 +32,9 @@ public class ActivitySaveRequest { private final String actPhone; private final String url; private final Category category; + private final Long instituteId; - public Activity toModel() { + public Activity toModel(Institute institute) { return Activity.builder() .actId(actId) .actTitle(actTitle) @@ -53,6 +55,7 @@ public Activity toModel() { .actPhone(actPhone) .url(url) .category(category) + .institute(institute) .build(); } } diff --git a/src/main/java/com/gamsa/activity/dto/InstituteDetailResponse.java b/src/main/java/com/gamsa/activity/dto/InstituteDetailResponse.java new file mode 100644 index 0000000..54f3d92 --- /dev/null +++ b/src/main/java/com/gamsa/activity/dto/InstituteDetailResponse.java @@ -0,0 +1,32 @@ +package com.gamsa.activity.dto; + +import com.gamsa.activity.domain.Institute; +import java.math.BigDecimal; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@Builder +@RequiredArgsConstructor +public class InstituteDetailResponse { + + private final Long instituteId; + private final String name; + private final String location; + private final BigDecimal latitude; + private final BigDecimal longitude; + // Todo 시군구 코드 + private final String phone; + + public static InstituteDetailResponse from(Institute institute) { + return InstituteDetailResponse.builder() + .instituteId(institute.getInstituteId()) + .name(institute.getName()) + .location(institute.getLocation()) + .latitude(institute.getLatitude()) + .longitude(institute.getLongitude()) + .phone(institute.getPhone()) + .build(); + } +} diff --git a/src/main/java/com/gamsa/activity/dto/InstituteSaveRequest.java b/src/main/java/com/gamsa/activity/dto/InstituteSaveRequest.java new file mode 100644 index 0000000..5c5bb2d --- /dev/null +++ b/src/main/java/com/gamsa/activity/dto/InstituteSaveRequest.java @@ -0,0 +1,28 @@ +package com.gamsa.activity.dto; + +import com.gamsa.activity.domain.Institute; +import java.math.BigDecimal; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class InstituteSaveRequest { + + private String name; + private String location; + private BigDecimal latitude; + private BigDecimal longitude; + // Todo 시군구 코드 + private String phone; + + public Institute toModel() { + return Institute.builder() + .name(name) + .location(location) + .latitude(latitude) + .longitude(longitude) + .phone(phone) + .build(); + } +} diff --git a/src/main/java/com/gamsa/activity/entity/ActivityJpaEntity.java b/src/main/java/com/gamsa/activity/entity/ActivityJpaEntity.java index 7106db4..799c9db 100644 --- a/src/main/java/com/gamsa/activity/entity/ActivityJpaEntity.java +++ b/src/main/java/com/gamsa/activity/entity/ActivityJpaEntity.java @@ -7,7 +7,10 @@ import jakarta.persistence.Column; import jakarta.persistence.Convert; import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; import jakarta.persistence.Temporal; import jakarta.persistence.TemporalType; @@ -92,6 +95,10 @@ public class ActivityJpaEntity extends BaseEntity { @Column(name = "category", length = 255) private Category category; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "institute_id") + private InstituteJpaEntity institute; + public static ActivityJpaEntity from(Activity activity) { return ActivityJpaEntity.builder() .actId(activity.getActId()) @@ -113,6 +120,7 @@ public static ActivityJpaEntity from(Activity activity) { .actPhone(activity.getActPhone()) .url(activity.getUrl()) .category(activity.getCategory()) + .institute(InstituteJpaEntity.from(activity.getInstitute())) .build(); } @@ -137,6 +145,7 @@ public Activity toModel() { .actPhone(actPhone) .url(url) .category(category) + .institute(institute.toModel()) .build(); } diff --git a/src/main/java/com/gamsa/activity/entity/InstituteJpaEntity.java b/src/main/java/com/gamsa/activity/entity/InstituteJpaEntity.java new file mode 100644 index 0000000..057ea04 --- /dev/null +++ b/src/main/java/com/gamsa/activity/entity/InstituteJpaEntity.java @@ -0,0 +1,69 @@ +package com.gamsa.activity.entity; + +import com.gamsa.activity.domain.Institute; +import com.gamsa.common.entity.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.math.BigDecimal; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Entity +@Table(name = "Institute") +public class InstituteJpaEntity extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "institute_id") + private Long instituteId; + + @Column(name = "name", unique = true) + private String name; + + @Column(name = "location", length = 255) + private String location; + + @Column(name = "latitude") + private BigDecimal latitude; + + @Column(name = "longitude") + private BigDecimal longitude; + + // Todo 시군구 코드 + + @Column(name = "phone", length = 12) + private String phone; + + public static InstituteJpaEntity from(Institute institute) { + return InstituteJpaEntity.builder() + .instituteId(institute.getInstituteId()) + .name(institute.getName()) + .location(institute.getLocation()) + .latitude(institute.getLatitude()) + .longitude(institute.getLongitude()) + .phone(institute.getPhone()) + .build(); + } + + public Institute toModel() { + return Institute.builder() + .instituteId(instituteId) + .name(name) + .location(location) + .latitude(latitude) + .longitude(longitude) + .phone(phone) + .build(); + } +} diff --git a/src/main/java/com/gamsa/activity/exception/ActivityCustomException.java b/src/main/java/com/gamsa/activity/exception/ActivityCustomException.java deleted file mode 100644 index 276f692..0000000 --- a/src/main/java/com/gamsa/activity/exception/ActivityCustomException.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.gamsa.activity.exception; - -import com.gamsa.common.constant.ErrorCode; -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -@Getter -@RequiredArgsConstructor -public class ActivityCustomException extends RuntimeException { - - private final ErrorCode errorCode; -} diff --git a/src/main/java/com/gamsa/activity/repository/InstituteJpaRepository.java b/src/main/java/com/gamsa/activity/repository/InstituteJpaRepository.java new file mode 100644 index 0000000..dfe9d52 --- /dev/null +++ b/src/main/java/com/gamsa/activity/repository/InstituteJpaRepository.java @@ -0,0 +1,10 @@ +package com.gamsa.activity.repository; + +import com.gamsa.activity.entity.InstituteJpaEntity; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface InstituteJpaRepository extends JpaRepository { + + Optional findByName(String name); +} diff --git a/src/main/java/com/gamsa/activity/repository/InstituteRepository.java b/src/main/java/com/gamsa/activity/repository/InstituteRepository.java new file mode 100644 index 0000000..2e5f6c4 --- /dev/null +++ b/src/main/java/com/gamsa/activity/repository/InstituteRepository.java @@ -0,0 +1,13 @@ +package com.gamsa.activity.repository; + +import com.gamsa.activity.domain.Institute; +import java.util.Optional; + +public interface InstituteRepository { + + void save(Institute institute); + + Optional findById(Long id); + + Optional findByName(String name); +} diff --git a/src/main/java/com/gamsa/activity/repository/InstituteRepositoryImpl.java b/src/main/java/com/gamsa/activity/repository/InstituteRepositoryImpl.java new file mode 100644 index 0000000..a54e8bb --- /dev/null +++ b/src/main/java/com/gamsa/activity/repository/InstituteRepositoryImpl.java @@ -0,0 +1,31 @@ +package com.gamsa.activity.repository; + +import com.gamsa.activity.domain.Institute; +import com.gamsa.activity.entity.InstituteJpaEntity; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@RequiredArgsConstructor +@Repository +public class InstituteRepositoryImpl implements InstituteRepository { + + private final InstituteJpaRepository instituteJpaRepository; + + @Override + public void save(Institute institute) { + instituteJpaRepository.save(InstituteJpaEntity.from(institute)); + } + + @Override + public Optional findById(Long id) { + return instituteJpaRepository.findById(id) + .map(InstituteJpaEntity::toModel); + } + + @Override + public Optional findByName(String name) { + return instituteJpaRepository.findByName(name) + .map(InstituteJpaEntity::toModel); + } +} diff --git a/src/main/java/com/gamsa/activity/service/ActivityService.java b/src/main/java/com/gamsa/activity/service/ActivityService.java index 2c8df8c..6ed4a38 100644 --- a/src/main/java/com/gamsa/activity/service/ActivityService.java +++ b/src/main/java/com/gamsa/activity/service/ActivityService.java @@ -2,12 +2,14 @@ import com.gamsa.activity.constant.ActivityErrorCode; import com.gamsa.activity.domain.Activity; +import com.gamsa.activity.domain.Institute; import com.gamsa.activity.dto.ActivityDetailResponse; import com.gamsa.activity.dto.ActivityFilterRequest; import com.gamsa.activity.dto.ActivityFindSliceResponse; import com.gamsa.activity.dto.ActivitySaveRequest; import com.gamsa.activity.exception.ActivityException; import com.gamsa.activity.repository.ActivityRepository; +import com.gamsa.activity.repository.InstituteRepository; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; @@ -18,13 +20,19 @@ public class ActivityService { private final ActivityRepository activityRepository; + private final InstituteRepository instituteRepository; public void save(ActivitySaveRequest saveRequest) { + // 중복 여부 확인 activityRepository.findById(saveRequest.getActId()) .ifPresent(activity -> { throw new ActivityException(ActivityErrorCode.ACTIVITY_ALREADY_EXISTS); }); - activityRepository.save(saveRequest.toModel()); + // 기관 존재 확인 + Institute institute = instituteRepository.findById(saveRequest.getInstituteId()) + .orElseThrow(() -> new ActivityException(ActivityErrorCode.INSTITUTE_NOT_EXISTS)); + + activityRepository.save(saveRequest.toModel(institute)); } public Slice findSlice(ActivityFilterRequest request, diff --git a/src/main/java/com/gamsa/activity/service/InstituteService.java b/src/main/java/com/gamsa/activity/service/InstituteService.java new file mode 100644 index 0000000..05d4a47 --- /dev/null +++ b/src/main/java/com/gamsa/activity/service/InstituteService.java @@ -0,0 +1,23 @@ +package com.gamsa.activity.service; + +import com.gamsa.activity.constant.ActivityErrorCode; +import com.gamsa.activity.dto.InstituteSaveRequest; +import com.gamsa.activity.exception.ActivityException; +import com.gamsa.activity.repository.InstituteRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class InstituteService { + + private final InstituteRepository instituteRepository; + + public void save(InstituteSaveRequest saveRequest) { + instituteRepository.findByName(saveRequest.getName()) + .ifPresent(institute -> { + throw new ActivityException(ActivityErrorCode.INSTITUTE_ALREADY_EXISTS); + }); + instituteRepository.save(saveRequest.toModel()); + } +} diff --git a/src/test/java/com/gamsa/activity/entity/ActivityJpaEntityTest.java b/src/test/java/com/gamsa/activity/entity/ActivityJpaEntityTest.java index c8db623..ce7dea7 100644 --- a/src/test/java/com/gamsa/activity/entity/ActivityJpaEntityTest.java +++ b/src/test/java/com/gamsa/activity/entity/ActivityJpaEntityTest.java @@ -4,6 +4,8 @@ import com.gamsa.activity.constant.Category; import com.gamsa.activity.domain.Activity; +import com.gamsa.activity.domain.Institute; +import java.math.BigDecimal; import java.time.LocalDateTime; import org.junit.jupiter.api.Test; @@ -12,6 +14,15 @@ class ActivityJpaEntityTest { @Test void 도메인모델에서_JPA엔티티로_변환() { // given + Institute institute = Institute.builder() + .instituteId(1L) + .name("도서관") + .location("서울시") + .latitude(new BigDecimal("123456789.12341234")) + .longitude(new BigDecimal("987654321.43214321")) + .phone("010xxxxxxxx") + .build(); + Activity activity = Activity.builder() .actId(1L) .actTitle("어린이놀이안전관리 및 놀잇감 청결유지 및 정리") @@ -30,8 +41,9 @@ class ActivityJpaEntityTest { .actWeek(0111110) .actManager("윤순영") .actPhone("032-577-3026") - .category(Category.OTHER_ACTIVITIES) .url("https://...") + .category(Category.OTHER_ACTIVITIES) + .institute(institute) .build(); // when @@ -44,6 +56,15 @@ class ActivityJpaEntityTest { @Test void JPA엔티에서_도메인모델로_변환() { // given + InstituteJpaEntity institute = InstituteJpaEntity.builder() + .instituteId(1L) + .name("도서관") + .location("서울시") + .latitude(new BigDecimal("123456789.12341234")) + .longitude(new BigDecimal("987654321.43214321")) + .phone("010xxxxxxxx") + .build(); + ActivityJpaEntity jpaEntity = ActivityJpaEntity.builder() .actId(1L) .actTitle("어린이놀이안전관리 및 놀잇감 청결유지 및 정리") @@ -64,6 +85,7 @@ class ActivityJpaEntityTest { .actPhone("032-577-3026") .url("https://...") .category(Category.OTHER_ACTIVITIES) + .institute(institute) .build(); // when diff --git a/src/test/java/com/gamsa/activity/entity/InstituteJpaEntityTest.java b/src/test/java/com/gamsa/activity/entity/InstituteJpaEntityTest.java new file mode 100644 index 0000000..9b71ac1 --- /dev/null +++ b/src/test/java/com/gamsa/activity/entity/InstituteJpaEntityTest.java @@ -0,0 +1,48 @@ +package com.gamsa.activity.entity; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.gamsa.activity.domain.Institute; +import java.math.BigDecimal; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class InstituteJpaEntityTest { + + @Test + @DisplayName("도메인 모델에서 JPA엔티티로 변환") + void changeToJpaEntity() { + // given + Institute model = Institute.builder() + .instituteId(1L) + .name("도서관") + .location("서울시") + .latitude(new BigDecimal("123456789.12341234")) + .longitude(new BigDecimal("987654321.43214321")) + .phone("010xxxxxxxx") + .build(); + // when + InstituteJpaEntity jpaEntity = InstituteJpaEntity.from(model); + // then + assertThat(jpaEntity.getInstituteId()).isEqualTo(model.getInstituteId()); + } + + @Test + @DisplayName("JPA엔티티에서 도메인 모델로 변환") + void changeToDomainModel() { + // given + InstituteJpaEntity jpaEntity = InstituteJpaEntity.builder() + .instituteId(1L) + .name("도서관") + .location("서울시") + .latitude(new BigDecimal("123456789.12341234")) + .longitude(new BigDecimal("987654321.43214321")) + .phone("010xxxxxxxx") + .build(); + // when + Institute model = jpaEntity.toModel(); + // then + assertThat(model.getInstituteId()).isEqualTo(jpaEntity.getInstituteId()); + } + +} \ No newline at end of file diff --git a/src/test/java/com/gamsa/activity/repository/InstituteJpaRepositoryTest.java b/src/test/java/com/gamsa/activity/repository/InstituteJpaRepositoryTest.java new file mode 100644 index 0000000..45f82c2 --- /dev/null +++ b/src/test/java/com/gamsa/activity/repository/InstituteJpaRepositoryTest.java @@ -0,0 +1,39 @@ +package com.gamsa.activity.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.gamsa.activity.entity.InstituteJpaEntity; +import com.gamsa.common.config.TestConfig; +import java.math.BigDecimal; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + +@DataJpaTest +@Import(TestConfig.class) +class InstituteJpaRepositoryTest { + + @Autowired + private InstituteJpaRepository instituteJpaRepository; + + private final InstituteJpaEntity jpaEntity = InstituteJpaEntity.builder() + .instituteId(1L) + .name("도서관") + .location("서울시") + .latitude(new BigDecimal("123456789.12341234")) + .longitude(new BigDecimal("987654321.43214321")) + .phone("010xxxxxxxx") + .build(); + + @Test + @DisplayName("새 봉사기관 저장") + void save() { + // when + instituteJpaRepository.save(jpaEntity); + // then + assertThat(instituteJpaRepository.findById(1L).get().getName()) + .isEqualTo(jpaEntity.getName()); + } +} \ No newline at end of file diff --git a/src/test/java/com/gamsa/activity/service/ActivityServiceTest.java b/src/test/java/com/gamsa/activity/service/ActivityServiceTest.java index c3a8356..27a88b4 100644 --- a/src/test/java/com/gamsa/activity/service/ActivityServiceTest.java +++ b/src/test/java/com/gamsa/activity/service/ActivityServiceTest.java @@ -10,8 +10,10 @@ import com.gamsa.activity.exception.ActivityException; import com.gamsa.activity.stub.StubEmptyActivityRepository; import com.gamsa.activity.stub.StubExistsActivityRepository; +import com.gamsa.activity.stub.StubExistsInstituteRepository; import java.time.LocalDateTime; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Slice; @@ -38,12 +40,16 @@ class ActivityServiceTest { .actPhone("032-577-3026") .url("https://...") .category(Category.OTHER_ACTIVITIES) + .instituteId(1L) .build(); @Test void 활동객체를_저장하고_성공한다() { // given - ActivityService activityService = new ActivityService(new StubEmptyActivityRepository()); + ActivityService activityService = new ActivityService( + new StubEmptyActivityRepository(), + new StubExistsInstituteRepository() + ); // then Assertions.assertDoesNotThrow(() -> { @@ -56,7 +62,10 @@ class ActivityServiceTest { @Test void 이미_존재하는_ID의_활동객체를_생성하고_실패한다() { // given - ActivityService activityService = new ActivityService(new StubExistsActivityRepository()); + ActivityService activityService = new ActivityService( + new StubExistsActivityRepository(), + new StubExistsInstituteRepository() + ); // then Assertions.assertThrows(ActivityException.class, () -> { @@ -68,7 +77,10 @@ class ActivityServiceTest { @Test void 활동객체_리스트를_반환한다() { // given - ActivityService activityService = new ActivityService(new StubEmptyActivityRepository()); + ActivityService activityService = new ActivityService( + new StubEmptyActivityRepository(), + new StubExistsInstituteRepository() + ); // when Slice result = activityService.findSlice(null, @@ -81,7 +93,10 @@ class ActivityServiceTest { @Test void ID로_활동조회에_성공한다() { // given - ActivityService activityService = new ActivityService(new StubExistsActivityRepository()); + ActivityService activityService = new ActivityService( + new StubExistsActivityRepository(), + new StubExistsInstituteRepository() + ); // when ActivityDetailResponse result = activityService.findById(1L); @@ -93,7 +108,10 @@ class ActivityServiceTest { @Test void ID로_활동조회에_실패한다() { // given - ActivityService activityService = new ActivityService(new StubEmptyActivityRepository()); + ActivityService activityService = new ActivityService( + new StubEmptyActivityRepository(), + new StubExistsInstituteRepository() + ); // then Assertions.assertThrows(ActivityException.class, () -> { @@ -101,4 +119,10 @@ class ActivityServiceTest { activityService.findById(1L); }, ActivityErrorCode.ACTIVITY_NOT_EXISTS.getMsg()); } + + @Test + @DisplayName("활동 ID로 봉사 기관 정보를 조회한다.") + void findInstituteByActivityId() { + + } } \ No newline at end of file diff --git a/src/test/java/com/gamsa/activity/service/InstituteServiceTest.java b/src/test/java/com/gamsa/activity/service/InstituteServiceTest.java new file mode 100644 index 0000000..c69d4c7 --- /dev/null +++ b/src/test/java/com/gamsa/activity/service/InstituteServiceTest.java @@ -0,0 +1,49 @@ +package com.gamsa.activity.service; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.gamsa.activity.constant.ActivityErrorCode; +import com.gamsa.activity.dto.InstituteSaveRequest; +import com.gamsa.activity.exception.ActivityException; +import com.gamsa.activity.stub.StubEmptyInstituteRepository; +import com.gamsa.activity.stub.StubExistsInstituteRepository; +import java.math.BigDecimal; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class InstituteServiceTest { + + private final InstituteSaveRequest saveRequest = InstituteSaveRequest.builder() + .name("도서관") + .location("서울시") + .latitude(new BigDecimal("123456789.12341234")) + .longitude(new BigDecimal("987654321.43214321")) + .phone("010xxxxxxxx") + .build(); + + + @Test + @DisplayName("봉사기관을 생성한다.") + void save() { + // given + InstituteService service = new InstituteService(new StubEmptyInstituteRepository()); + // then + assertDoesNotThrow(() -> { + // when + service.save(saveRequest); + }); + } + + @Test + @DisplayName("봉시기관 동일 이름 충돌로 실패한다.") + void saveFail() { + // given + InstituteService service = new InstituteService(new StubExistsInstituteRepository()); + // then + assertThrows(ActivityException.class, () -> { + // when + service.save(saveRequest); + }, ActivityErrorCode.INSTITUTE_ALREADY_EXISTS.getMsg()); + } +} \ No newline at end of file diff --git a/src/test/java/com/gamsa/activity/stub/StubEmptyInstituteRepository.java b/src/test/java/com/gamsa/activity/stub/StubEmptyInstituteRepository.java new file mode 100644 index 0000000..cd85f5f --- /dev/null +++ b/src/test/java/com/gamsa/activity/stub/StubEmptyInstituteRepository.java @@ -0,0 +1,23 @@ +package com.gamsa.activity.stub; + +import com.gamsa.activity.domain.Institute; +import com.gamsa.activity.repository.InstituteRepository; +import java.util.Optional; + +public class StubEmptyInstituteRepository implements InstituteRepository { + + @Override + public void save(Institute institute) { + // do nothing + } + + @Override + public Optional findById(Long id) { + return Optional.empty(); + } + + @Override + public Optional findByName(String name) { + return Optional.empty(); + } +} diff --git a/src/test/java/com/gamsa/activity/stub/StubExistsActivityRepository.java b/src/test/java/com/gamsa/activity/stub/StubExistsActivityRepository.java index 73bea05..d5a6c15 100644 --- a/src/test/java/com/gamsa/activity/stub/StubExistsActivityRepository.java +++ b/src/test/java/com/gamsa/activity/stub/StubExistsActivityRepository.java @@ -1,8 +1,10 @@ package com.gamsa.activity.stub; import com.gamsa.activity.domain.Activity; +import com.gamsa.activity.domain.Institute; import com.gamsa.activity.dto.ActivityFilterRequest; import com.gamsa.activity.repository.ActivityRepository; +import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.List; import java.util.Optional; @@ -12,6 +14,15 @@ public class StubExistsActivityRepository implements ActivityRepository { + private final Institute institute = Institute.builder() + .instituteId(1L) + .name("도서관") + .location("서울시") + .latitude(new BigDecimal("123456789.12341234")) + .longitude(new BigDecimal("987654321.43214321")) + .phone("010xxxxxxxx") + .build(); + private final Activity activity = Activity.builder() .actId(1L) .actTitle("어린이놀이안전관리 및 놀잇감 청결유지 및 정리") @@ -31,6 +42,7 @@ public class StubExistsActivityRepository implements ActivityRepository { .actManager("윤순영") .actPhone("032-577-3026") .url("https://...") + .institute(institute) .build(); @Override diff --git a/src/test/java/com/gamsa/activity/stub/StubExistsInstituteRepository.java b/src/test/java/com/gamsa/activity/stub/StubExistsInstituteRepository.java new file mode 100644 index 0000000..09a6f77 --- /dev/null +++ b/src/test/java/com/gamsa/activity/stub/StubExistsInstituteRepository.java @@ -0,0 +1,33 @@ +package com.gamsa.activity.stub; + +import com.gamsa.activity.domain.Institute; +import com.gamsa.activity.repository.InstituteRepository; +import java.math.BigDecimal; +import java.util.Optional; + +public class StubExistsInstituteRepository implements InstituteRepository { + + private final Institute institute = Institute.builder() + .instituteId(1L) + .name("도서관") + .location("서울시") + .latitude(new BigDecimal("123456789.12341234")) + .longitude(new BigDecimal("987654321.43214321")) + .phone("010xxxxxxxx") + .build(); + + @Override + public void save(Institute institute) { + // do nothing + } + + @Override + public Optional findById(Long id) { + return Optional.of(institute); + } + + @Override + public Optional findByName(String name) { + return Optional.of(institute); + } +} diff --git a/src/test/java/com/gamsa/avatar/repository/AvatarJpaRepositoryTest.java b/src/test/java/com/gamsa/avatar/repository/AvatarJpaRepositoryTest.java index 8572c66..37f5252 100644 --- a/src/test/java/com/gamsa/avatar/repository/AvatarJpaRepositoryTest.java +++ b/src/test/java/com/gamsa/avatar/repository/AvatarJpaRepositoryTest.java @@ -11,9 +11,8 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.Import; -import java.time.LocalDateTime; - @DataJpaTest +@Import(TestConfig.class) public class AvatarJpaRepositoryTest { @Autowired private AvatarJpaRepository avatarJpaRepository; From 4d4de84ad2975a35cf91854d6825cee61b68f0cf Mon Sep 17 00:00:00 2001 From: 5win <94297900+5win@users.noreply.github.com> Date: Fri, 11 Oct 2024 23:06:11 +0900 Subject: [PATCH 04/17] =?UTF-8?q?feat:=20=EC=8B=9C=EB=8F=84,=20=EC=8B=9C?= =?UTF-8?q?=EA=B5=B0=EA=B5=AC=20API=20=EB=B0=8F=20=EC=8B=9C=EB=8F=84,=20?= =?UTF-8?q?=EC=8B=9C=EA=B5=B0=EA=B5=AC=20=ED=95=84=ED=84=B0=EB=A7=81=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20(#31)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [#30] feat: 시도, 군구 CR API 구현 - 시도, 군구 영속성 엔티티 생성 - create, read API 구현 - 시도 조회, 군구 조회 별도로 생성 * [#30] fix: 변수명, 제약조건 수정 - gunguCode 에서 sidoGunguCode로 변경 - nullable 제약조건 추가 * [#30] feat: 봉사활동과 시도군구 연관관계 매핑 * [#30] feat: 시도, 시군구 필터링 조회 기능 추가 - 봉사활동 조회 시, 시도, 시군구 필터링 기능 추가 - sidoCode, sidoGunguCode로 queryDsl BooleanBuilder 추가. --- .../activity/constant/ActivityErrorCode.java | 4 +- .../controller/ActivityController.java | 12 +++- .../controller/DistrictController.java | 38 ++++++++++++ .../com/gamsa/activity/domain/Activity.java | 1 + .../com/gamsa/activity/domain/District.java | 15 +++++ .../com/gamsa/activity/domain/Institute.java | 2 +- .../activity/dto/ActivityDetailResponse.java | 2 + .../activity/dto/ActivityFilterRequest.java | 8 ++- .../activity/dto/ActivitySaveRequest.java | 5 +- .../activity/dto/DistrictDetailResponse.java | 28 +++++++++ .../activity/dto/DistrictFindAllResponse.java | 28 +++++++++ .../activity/dto/DistrictSaveRequest.java | 28 +++++++++ .../activity/dto/InstituteDetailResponse.java | 1 - .../activity/dto/InstituteSaveRequest.java | 7 ++- .../activity/entity/ActivityJpaEntity.java | 6 ++ .../activity/entity/DistrictJpaEntity.java | 58 +++++++++++++++++++ .../activity/entity/InstituteJpaEntity.java | 9 ++- .../repository/ActivityFilterBuilder.java | 14 +++++ .../repository/DistrictJpaRepository.java | 13 +++++ .../repository/DistrictRepository.java | 14 +++++ .../repository/DistrictRepositoryImpl.java | 33 +++++++++++ .../activity/service/ActivityService.java | 9 ++- .../activity/service/DistrictService.java | 47 +++++++++++++++ .../activity/service/InstituteService.java | 9 ++- .../entity/ActivityJpaEntityTest.java | 21 +++++++ .../entity/InstituteJpaEntityTest.java | 20 +++++++ .../repository/ActivityJpaRepositoryTest.java | 8 +-- .../activity/service/ActivityServiceTest.java | 17 ++++-- .../service/InstituteServiceTest.java | 11 +++- .../stub/StubExistsActivityRepository.java | 10 ++++ .../stub/StubExistsDistrictRepository.java | 32 ++++++++++ 31 files changed, 487 insertions(+), 23 deletions(-) create mode 100644 src/main/java/com/gamsa/activity/controller/DistrictController.java create mode 100644 src/main/java/com/gamsa/activity/domain/District.java create mode 100644 src/main/java/com/gamsa/activity/dto/DistrictDetailResponse.java create mode 100644 src/main/java/com/gamsa/activity/dto/DistrictFindAllResponse.java create mode 100644 src/main/java/com/gamsa/activity/dto/DistrictSaveRequest.java create mode 100644 src/main/java/com/gamsa/activity/entity/DistrictJpaEntity.java create mode 100644 src/main/java/com/gamsa/activity/repository/DistrictJpaRepository.java create mode 100644 src/main/java/com/gamsa/activity/repository/DistrictRepository.java create mode 100644 src/main/java/com/gamsa/activity/repository/DistrictRepositoryImpl.java create mode 100644 src/main/java/com/gamsa/activity/service/DistrictService.java create mode 100644 src/test/java/com/gamsa/activity/stub/StubExistsDistrictRepository.java diff --git a/src/main/java/com/gamsa/activity/constant/ActivityErrorCode.java b/src/main/java/com/gamsa/activity/constant/ActivityErrorCode.java index 4e8770f..9930e68 100644 --- a/src/main/java/com/gamsa/activity/constant/ActivityErrorCode.java +++ b/src/main/java/com/gamsa/activity/constant/ActivityErrorCode.java @@ -10,10 +10,12 @@ public enum ActivityErrorCode { // 404 Not Found : 존재하지 않는 리소스 접근 ACTIVITY_NOT_EXISTS(404, "존재하지 않는 활동입니다."), INSTITUTE_NOT_EXISTS(404, "존재하지 않는 기관입니다."), + DISTRICT_NOT_EXISTS(404, "존재하지 않는 지역입니다."), // 409 Conflict : 요청이 현재 서버의 상태와 충돌 ACTIVITY_ALREADY_EXISTS(409, "이미 존재하는 활동입니다."), - INSTITUTE_ALREADY_EXISTS(409, "이미 존재하는 기관입니다."); + INSTITUTE_ALREADY_EXISTS(409, "이미 존재하는 기관입니다."), + DISTRICT_ALREADY_EXISTS(409, "이미 존재하는 지역입니다."); private final int status; private final String msg; diff --git a/src/main/java/com/gamsa/activity/controller/ActivityController.java b/src/main/java/com/gamsa/activity/controller/ActivityController.java index a231180..6da07fc 100644 --- a/src/main/java/com/gamsa/activity/controller/ActivityController.java +++ b/src/main/java/com/gamsa/activity/controller/ActivityController.java @@ -29,12 +29,20 @@ public class ActivityController { @GetMapping public Slice findSlice( @RequestParam(required = false) Category category, + @RequestParam(required = false) Integer sidoGunguCode, + @RequestParam(required = false) Integer sidoCode, @RequestParam(defaultValue = "false") boolean teenPossibleOnly, @RequestParam(defaultValue = "false") boolean beforeDeadlineOnly, Pageable pageable) { - ActivityFilterRequest request = new ActivityFilterRequest(category, teenPossibleOnly, - beforeDeadlineOnly); + ActivityFilterRequest request = ActivityFilterRequest.builder() + .category(category) + .sidoGunguCode(sidoGunguCode) + .sidoCode(sidoCode) + .teenPossibleOnly(teenPossibleOnly) + .beforeDeadlineOnly(beforeDeadlineOnly) + .build(); + return activityService.findSlice(request, pageable); } diff --git a/src/main/java/com/gamsa/activity/controller/DistrictController.java b/src/main/java/com/gamsa/activity/controller/DistrictController.java new file mode 100644 index 0000000..0be5c79 --- /dev/null +++ b/src/main/java/com/gamsa/activity/controller/DistrictController.java @@ -0,0 +1,38 @@ +package com.gamsa.activity.controller; + +import com.gamsa.activity.dto.DistrictFindAllResponse; +import com.gamsa.activity.dto.DistrictSaveRequest; +import com.gamsa.activity.service.DistrictService; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/v1/districts") +public class DistrictController { + + private final DistrictService districtService; + + @PostMapping + public ResponseEntity save(@RequestBody DistrictSaveRequest saveRequest) { + districtService.save(saveRequest); + return new ResponseEntity<>(HttpStatus.CREATED); + } + + @GetMapping("/sido") + public List findAllSido() { + return districtService.findAllSido(); + } + + @GetMapping("/gungu") + public List findAllGungu() { + return districtService.findAllGungu(); + } +} diff --git a/src/main/java/com/gamsa/activity/domain/Activity.java b/src/main/java/com/gamsa/activity/domain/Activity.java index b88fe96..96afa9f 100644 --- a/src/main/java/com/gamsa/activity/domain/Activity.java +++ b/src/main/java/com/gamsa/activity/domain/Activity.java @@ -29,4 +29,5 @@ public class Activity { private String url; private Category category; private Institute institute; + private District sidoGungu; } diff --git a/src/main/java/com/gamsa/activity/domain/District.java b/src/main/java/com/gamsa/activity/domain/District.java new file mode 100644 index 0000000..9379f12 --- /dev/null +++ b/src/main/java/com/gamsa/activity/domain/District.java @@ -0,0 +1,15 @@ +package com.gamsa.activity.domain; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class District { + + private int sidoGunguCode; + private int sidoCode; + private String sidoName; + private String gunguName; + private boolean sido; +} diff --git a/src/main/java/com/gamsa/activity/domain/Institute.java b/src/main/java/com/gamsa/activity/domain/Institute.java index e086ec3..b3153e8 100644 --- a/src/main/java/com/gamsa/activity/domain/Institute.java +++ b/src/main/java/com/gamsa/activity/domain/Institute.java @@ -13,6 +13,6 @@ public class Institute { private String location; private BigDecimal latitude; private BigDecimal longitude; - // Todo 시군구 코드 + private District sidoGungu; private String phone; } diff --git a/src/main/java/com/gamsa/activity/dto/ActivityDetailResponse.java b/src/main/java/com/gamsa/activity/dto/ActivityDetailResponse.java index 7743e52..9e50ce7 100644 --- a/src/main/java/com/gamsa/activity/dto/ActivityDetailResponse.java +++ b/src/main/java/com/gamsa/activity/dto/ActivityDetailResponse.java @@ -33,6 +33,7 @@ public class ActivityDetailResponse { private final String url; private final Category category; private final InstituteDetailResponse institute; + private final DistrictDetailResponse sidoGungu; public static ActivityDetailResponse from(Activity activity) { return ActivityDetailResponse.builder() @@ -56,6 +57,7 @@ public static ActivityDetailResponse from(Activity activity) { .url(activity.getUrl()) .category(activity.getCategory()) .institute(InstituteDetailResponse.from(activity.getInstitute())) + .sidoGungu(DistrictDetailResponse.from(activity.getSidoGungu())) .build(); } } diff --git a/src/main/java/com/gamsa/activity/dto/ActivityFilterRequest.java b/src/main/java/com/gamsa/activity/dto/ActivityFilterRequest.java index e0aef5a..9be4f9d 100644 --- a/src/main/java/com/gamsa/activity/dto/ActivityFilterRequest.java +++ b/src/main/java/com/gamsa/activity/dto/ActivityFilterRequest.java @@ -1,17 +1,23 @@ package com.gamsa.activity.dto; import com.gamsa.activity.constant.Category; +import lombok.Builder; import lombok.Getter; import lombok.RequiredArgsConstructor; @Getter +@Builder @RequiredArgsConstructor public class ActivityFilterRequest { // 카테고리 private final Category category; - // Todo 지역 + // 시도군구 코드 + private final Integer sidoGunguCode; + + // 시도 코드 + private final Integer sidoCode; // 청소년 가능한 것만 private final boolean teenPossibleOnly; diff --git a/src/main/java/com/gamsa/activity/dto/ActivitySaveRequest.java b/src/main/java/com/gamsa/activity/dto/ActivitySaveRequest.java index 9da43ac..cc0bc8d 100644 --- a/src/main/java/com/gamsa/activity/dto/ActivitySaveRequest.java +++ b/src/main/java/com/gamsa/activity/dto/ActivitySaveRequest.java @@ -2,6 +2,7 @@ import com.gamsa.activity.constant.Category; import com.gamsa.activity.domain.Activity; +import com.gamsa.activity.domain.District; import com.gamsa.activity.domain.Institute; import java.time.LocalDateTime; import lombok.Builder; @@ -33,8 +34,9 @@ public class ActivitySaveRequest { private final String url; private final Category category; private final Long instituteId; + private final Integer sidoGunguCode; - public Activity toModel(Institute institute) { + public Activity toModel(Institute institute, District sidoGungu) { return Activity.builder() .actId(actId) .actTitle(actTitle) @@ -56,6 +58,7 @@ public Activity toModel(Institute institute) { .url(url) .category(category) .institute(institute) + .sidoGungu(sidoGungu) .build(); } } diff --git a/src/main/java/com/gamsa/activity/dto/DistrictDetailResponse.java b/src/main/java/com/gamsa/activity/dto/DistrictDetailResponse.java new file mode 100644 index 0000000..3db46c0 --- /dev/null +++ b/src/main/java/com/gamsa/activity/dto/DistrictDetailResponse.java @@ -0,0 +1,28 @@ +package com.gamsa.activity.dto; + +import com.gamsa.activity.domain.District; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@Builder +@RequiredArgsConstructor +public class DistrictDetailResponse { + + private final int sidoGunguCode; + private final int sidoCode; + private final String sidoName; + private final String gunguName; + private final boolean sido; + + public static DistrictDetailResponse from(District district) { + return DistrictDetailResponse.builder() + .sidoGunguCode(district.getSidoGunguCode()) + .sidoCode(district.getSidoCode()) + .sidoName(district.getSidoName()) + .gunguName(district.getGunguName()) + .sido(district.isSido()) + .build(); + } +} diff --git a/src/main/java/com/gamsa/activity/dto/DistrictFindAllResponse.java b/src/main/java/com/gamsa/activity/dto/DistrictFindAllResponse.java new file mode 100644 index 0000000..d045c62 --- /dev/null +++ b/src/main/java/com/gamsa/activity/dto/DistrictFindAllResponse.java @@ -0,0 +1,28 @@ +package com.gamsa.activity.dto; + +import com.gamsa.activity.domain.District; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@Builder +@RequiredArgsConstructor +public class DistrictFindAllResponse { + + private final int sidoGunguCode; + private final int sidoCode; + private final String sidoName; + private final String gunguName; + private final boolean sido; + + public static DistrictFindAllResponse from(District district) { + return DistrictFindAllResponse.builder() + .sidoGunguCode(district.getSidoGunguCode()) + .sidoCode(district.getSidoCode()) + .sidoName(district.getSidoName()) + .gunguName(district.getGunguName()) + .sido(district.isSido()) + .build(); + } +} diff --git a/src/main/java/com/gamsa/activity/dto/DistrictSaveRequest.java b/src/main/java/com/gamsa/activity/dto/DistrictSaveRequest.java new file mode 100644 index 0000000..6217fdd --- /dev/null +++ b/src/main/java/com/gamsa/activity/dto/DistrictSaveRequest.java @@ -0,0 +1,28 @@ +package com.gamsa.activity.dto; + +import com.gamsa.activity.domain.District; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@Builder +@RequiredArgsConstructor +public class DistrictSaveRequest { + + private final int sidoGunguCode; + private final int sidoCode; + private final String sidoName; + private final String gunguName; + private final boolean sido; + + public District toModel() { + return District.builder() + .sidoGunguCode(sidoGunguCode) + .sidoCode(sidoCode) + .sidoName(sidoName) + .gunguName(gunguName) + .sido(sido) + .build(); + } +} diff --git a/src/main/java/com/gamsa/activity/dto/InstituteDetailResponse.java b/src/main/java/com/gamsa/activity/dto/InstituteDetailResponse.java index 54f3d92..effb689 100644 --- a/src/main/java/com/gamsa/activity/dto/InstituteDetailResponse.java +++ b/src/main/java/com/gamsa/activity/dto/InstituteDetailResponse.java @@ -16,7 +16,6 @@ public class InstituteDetailResponse { private final String location; private final BigDecimal latitude; private final BigDecimal longitude; - // Todo 시군구 코드 private final String phone; public static InstituteDetailResponse from(Institute institute) { diff --git a/src/main/java/com/gamsa/activity/dto/InstituteSaveRequest.java b/src/main/java/com/gamsa/activity/dto/InstituteSaveRequest.java index 5c5bb2d..630cc48 100644 --- a/src/main/java/com/gamsa/activity/dto/InstituteSaveRequest.java +++ b/src/main/java/com/gamsa/activity/dto/InstituteSaveRequest.java @@ -1,5 +1,6 @@ package com.gamsa.activity.dto; +import com.gamsa.activity.domain.District; import com.gamsa.activity.domain.Institute; import java.math.BigDecimal; import lombok.Builder; @@ -13,15 +14,17 @@ public class InstituteSaveRequest { private String location; private BigDecimal latitude; private BigDecimal longitude; - // Todo 시군구 코드 + private int sidoCode; + private int sidoGunguCode; private String phone; - public Institute toModel() { + public Institute toModel(District district) { return Institute.builder() .name(name) .location(location) .latitude(latitude) .longitude(longitude) + .sidoGungu(district) .phone(phone) .build(); } diff --git a/src/main/java/com/gamsa/activity/entity/ActivityJpaEntity.java b/src/main/java/com/gamsa/activity/entity/ActivityJpaEntity.java index 799c9db..7dc92b5 100644 --- a/src/main/java/com/gamsa/activity/entity/ActivityJpaEntity.java +++ b/src/main/java/com/gamsa/activity/entity/ActivityJpaEntity.java @@ -99,6 +99,10 @@ public class ActivityJpaEntity extends BaseEntity { @JoinColumn(name = "institute_id") private InstituteJpaEntity institute; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "sido_gungu_code", referencedColumnName = "sido_gungu_code") + private DistrictJpaEntity sidoGungu; + public static ActivityJpaEntity from(Activity activity) { return ActivityJpaEntity.builder() .actId(activity.getActId()) @@ -121,6 +125,7 @@ public static ActivityJpaEntity from(Activity activity) { .url(activity.getUrl()) .category(activity.getCategory()) .institute(InstituteJpaEntity.from(activity.getInstitute())) + .sidoGungu(DistrictJpaEntity.from(activity.getSidoGungu())) .build(); } @@ -146,6 +151,7 @@ public Activity toModel() { .url(url) .category(category) .institute(institute.toModel()) + .sidoGungu(sidoGungu.toModel()) .build(); } diff --git a/src/main/java/com/gamsa/activity/entity/DistrictJpaEntity.java b/src/main/java/com/gamsa/activity/entity/DistrictJpaEntity.java new file mode 100644 index 0000000..d66c64e --- /dev/null +++ b/src/main/java/com/gamsa/activity/entity/DistrictJpaEntity.java @@ -0,0 +1,58 @@ +package com.gamsa.activity.entity; + +import com.gamsa.activity.domain.District; +import com.gamsa.common.entity.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Entity +@Table(name = "District") +public class DistrictJpaEntity extends BaseEntity { + + @Id + @Column(name = "sido_gungu_code", nullable = false, unique = true) + private int sidoGunguCode; + + @Column(name = "sido_code", nullable = false) + private int sidoCode; + + @Column(name = "sido_name", length = 15, nullable = false) + private String sidoName; + + @Column(name = "gungu_name", length = 15) + private String gunguName; + + @Column(name = "sido", nullable = false) + private boolean sido; + + public static DistrictJpaEntity from(District district) { + return DistrictJpaEntity.builder() + .sidoGunguCode(district.getSidoGunguCode()) + .sidoCode(district.getSidoCode()) + .sidoName(district.getSidoName()) + .gunguName(district.getGunguName()) + .sido(district.isSido()) + .build(); + } + + public District toModel() { + return District.builder() + .sidoGunguCode(getSidoGunguCode()) + .sidoCode(getSidoCode()) + .sidoName(getSidoName()) + .gunguName(getGunguName()) + .sido(isSido()) + .build(); + } +} diff --git a/src/main/java/com/gamsa/activity/entity/InstituteJpaEntity.java b/src/main/java/com/gamsa/activity/entity/InstituteJpaEntity.java index 057ea04..3dcb812 100644 --- a/src/main/java/com/gamsa/activity/entity/InstituteJpaEntity.java +++ b/src/main/java/com/gamsa/activity/entity/InstituteJpaEntity.java @@ -4,9 +4,12 @@ import com.gamsa.common.entity.BaseEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; import java.math.BigDecimal; import lombok.AccessLevel; @@ -40,7 +43,9 @@ public class InstituteJpaEntity extends BaseEntity { @Column(name = "longitude") private BigDecimal longitude; - // Todo 시군구 코드 + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "sido_gungu_code", referencedColumnName = "sido_gungu_code") + private DistrictJpaEntity sidoGungu; @Column(name = "phone", length = 12) private String phone; @@ -52,6 +57,7 @@ public static InstituteJpaEntity from(Institute institute) { .location(institute.getLocation()) .latitude(institute.getLatitude()) .longitude(institute.getLongitude()) + .sidoGungu(DistrictJpaEntity.from(institute.getSidoGungu())) .phone(institute.getPhone()) .build(); } @@ -63,6 +69,7 @@ public Institute toModel() { .location(location) .latitude(latitude) .longitude(longitude) + .sidoGungu(sidoGungu.toModel()) .phone(phone) .build(); } diff --git a/src/main/java/com/gamsa/activity/repository/ActivityFilterBuilder.java b/src/main/java/com/gamsa/activity/repository/ActivityFilterBuilder.java index 4da810a..c9b83e9 100644 --- a/src/main/java/com/gamsa/activity/repository/ActivityFilterBuilder.java +++ b/src/main/java/com/gamsa/activity/repository/ActivityFilterBuilder.java @@ -13,6 +13,8 @@ public static BooleanBuilder createFilter(ActivityFilterRequest request) { BooleanBuilder filterBuilder = new BooleanBuilder(); eqCategory(filterBuilder, request.getCategory()); + eqSidoGunguCode(filterBuilder, request.getSidoGunguCode()); + eqSidoCode(filterBuilder, request.getSidoCode()); isTeenPossibleOnly(filterBuilder, request.isTeenPossibleOnly()); isDeadlineEndOnly(filterBuilder, request.isBeforeDeadlineOnly()); @@ -25,6 +27,18 @@ public static void eqCategory(BooleanBuilder filterBuilder, Category category) { } } + public static void eqSidoGunguCode(BooleanBuilder filterBuilder, Integer sidoGunguCode) { + if (sidoGunguCode != null) { + filterBuilder.and(activityJpaEntity.sidoGungu.sidoGunguCode.eq(sidoGunguCode)); + } + } + + public static void eqSidoCode(BooleanBuilder filterBuilder, Integer sidoCode) { + if (sidoCode != null) { + filterBuilder.and(activityJpaEntity.sidoGungu.sidoCode.eq(sidoCode)); + } + } + public static void isTeenPossibleOnly(BooleanBuilder filterBuilder, boolean teenPossibleOnly) { if (teenPossibleOnly) { filterBuilder.and(activityJpaEntity.teenPossible.isTrue()); diff --git a/src/main/java/com/gamsa/activity/repository/DistrictJpaRepository.java b/src/main/java/com/gamsa/activity/repository/DistrictJpaRepository.java new file mode 100644 index 0000000..8120bae --- /dev/null +++ b/src/main/java/com/gamsa/activity/repository/DistrictJpaRepository.java @@ -0,0 +1,13 @@ +package com.gamsa.activity.repository; + +import com.gamsa.activity.entity.DistrictJpaEntity; +import java.util.List; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface DistrictJpaRepository extends JpaRepository { + + Optional findBySidoGunguCode(int gunguCode); + + List findAllBySido(boolean sido); +} diff --git a/src/main/java/com/gamsa/activity/repository/DistrictRepository.java b/src/main/java/com/gamsa/activity/repository/DistrictRepository.java new file mode 100644 index 0000000..111326d --- /dev/null +++ b/src/main/java/com/gamsa/activity/repository/DistrictRepository.java @@ -0,0 +1,14 @@ +package com.gamsa.activity.repository; + +import com.gamsa.activity.domain.District; +import java.util.List; +import java.util.Optional; + +public interface DistrictRepository { + + void save(District district); + + Optional findBySidoGunguCode(int gunguCode); + + List findAllBysido(boolean sido); +} diff --git a/src/main/java/com/gamsa/activity/repository/DistrictRepositoryImpl.java b/src/main/java/com/gamsa/activity/repository/DistrictRepositoryImpl.java new file mode 100644 index 0000000..c4a0b65 --- /dev/null +++ b/src/main/java/com/gamsa/activity/repository/DistrictRepositoryImpl.java @@ -0,0 +1,33 @@ +package com.gamsa.activity.repository; + +import com.gamsa.activity.domain.District; +import com.gamsa.activity.entity.DistrictJpaEntity; +import java.util.List; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@RequiredArgsConstructor +@Repository +public class DistrictRepositoryImpl implements DistrictRepository { + + private final DistrictJpaRepository districtJpaRepository; + + @Override + public void save(District district) { + districtJpaRepository.save(DistrictJpaEntity.from(district)); + } + + @Override + public Optional findBySidoGunguCode(int gunguCode) { + return districtJpaRepository.findBySidoGunguCode(gunguCode) + .map(DistrictJpaEntity::toModel); + } + + @Override + public List findAllBysido(boolean sido) { + return districtJpaRepository.findAllBySido(sido).stream() + .map(DistrictJpaEntity::toModel) + .toList(); + } +} diff --git a/src/main/java/com/gamsa/activity/service/ActivityService.java b/src/main/java/com/gamsa/activity/service/ActivityService.java index 6ed4a38..88b1687 100644 --- a/src/main/java/com/gamsa/activity/service/ActivityService.java +++ b/src/main/java/com/gamsa/activity/service/ActivityService.java @@ -2,6 +2,7 @@ import com.gamsa.activity.constant.ActivityErrorCode; import com.gamsa.activity.domain.Activity; +import com.gamsa.activity.domain.District; import com.gamsa.activity.domain.Institute; import com.gamsa.activity.dto.ActivityDetailResponse; import com.gamsa.activity.dto.ActivityFilterRequest; @@ -9,6 +10,7 @@ import com.gamsa.activity.dto.ActivitySaveRequest; import com.gamsa.activity.exception.ActivityException; import com.gamsa.activity.repository.ActivityRepository; +import com.gamsa.activity.repository.DistrictRepository; import com.gamsa.activity.repository.InstituteRepository; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; @@ -21,6 +23,7 @@ public class ActivityService { private final ActivityRepository activityRepository; private final InstituteRepository instituteRepository; + private final DistrictRepository districtRepository; public void save(ActivitySaveRequest saveRequest) { // 중복 여부 확인 @@ -32,7 +35,11 @@ public void save(ActivitySaveRequest saveRequest) { Institute institute = instituteRepository.findById(saveRequest.getInstituteId()) .orElseThrow(() -> new ActivityException(ActivityErrorCode.INSTITUTE_NOT_EXISTS)); - activityRepository.save(saveRequest.toModel(institute)); + // 시도, 군구 존재 확인 + District sidoGungu = districtRepository.findBySidoGunguCode(saveRequest.getSidoGunguCode()) + .orElseThrow(() -> new ActivityException(ActivityErrorCode.DISTRICT_NOT_EXISTS)); + + activityRepository.save(saveRequest.toModel(institute, sidoGungu)); } public Slice findSlice(ActivityFilterRequest request, diff --git a/src/main/java/com/gamsa/activity/service/DistrictService.java b/src/main/java/com/gamsa/activity/service/DistrictService.java new file mode 100644 index 0000000..bcc853e --- /dev/null +++ b/src/main/java/com/gamsa/activity/service/DistrictService.java @@ -0,0 +1,47 @@ +package com.gamsa.activity.service; + +import com.gamsa.activity.constant.ActivityErrorCode; +import com.gamsa.activity.dto.DistrictFindAllResponse; +import com.gamsa.activity.dto.DistrictSaveRequest; +import com.gamsa.activity.exception.ActivityException; +import com.gamsa.activity.repository.DistrictRepository; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class DistrictService { + + private final DistrictRepository districtRepository; + + public void save(DistrictSaveRequest saveRequest) { + districtRepository.findBySidoGunguCode(saveRequest.getSidoGunguCode()) + .ifPresent(district -> { + throw new ActivityException(ActivityErrorCode.DISTRICT_ALREADY_EXISTS); + }); + districtRepository.save(saveRequest.toModel()); + } + + /** + * 시도 코드 데이터만 반환 + * + * @return 시도 코드 리스트 + */ + public List findAllSido() { + return districtRepository.findAllBysido(true).stream() + .map(DistrictFindAllResponse::from) + .toList(); + } + + /** + * 군구 코드 데이터만 반환 + * + * @return 군구 코드 리스트 + */ + public List findAllGungu() { + return districtRepository.findAllBysido(false).stream() + .map(DistrictFindAllResponse::from) + .toList(); + } +} diff --git a/src/main/java/com/gamsa/activity/service/InstituteService.java b/src/main/java/com/gamsa/activity/service/InstituteService.java index 05d4a47..7849c43 100644 --- a/src/main/java/com/gamsa/activity/service/InstituteService.java +++ b/src/main/java/com/gamsa/activity/service/InstituteService.java @@ -1,8 +1,10 @@ package com.gamsa.activity.service; import com.gamsa.activity.constant.ActivityErrorCode; +import com.gamsa.activity.domain.District; import com.gamsa.activity.dto.InstituteSaveRequest; import com.gamsa.activity.exception.ActivityException; +import com.gamsa.activity.repository.DistrictRepository; import com.gamsa.activity.repository.InstituteRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -12,12 +14,17 @@ public class InstituteService { private final InstituteRepository instituteRepository; + private final DistrictRepository districtRepository; public void save(InstituteSaveRequest saveRequest) { instituteRepository.findByName(saveRequest.getName()) .ifPresent(institute -> { throw new ActivityException(ActivityErrorCode.INSTITUTE_ALREADY_EXISTS); }); - instituteRepository.save(saveRequest.toModel()); + + District district = districtRepository.findBySidoGunguCode(saveRequest.getSidoGunguCode()) + .orElseThrow(() -> new ActivityException(ActivityErrorCode.DISTRICT_NOT_EXISTS)); + + instituteRepository.save(saveRequest.toModel(district)); } } diff --git a/src/test/java/com/gamsa/activity/entity/ActivityJpaEntityTest.java b/src/test/java/com/gamsa/activity/entity/ActivityJpaEntityTest.java index ce7dea7..3dfe74a 100644 --- a/src/test/java/com/gamsa/activity/entity/ActivityJpaEntityTest.java +++ b/src/test/java/com/gamsa/activity/entity/ActivityJpaEntityTest.java @@ -4,6 +4,7 @@ import com.gamsa.activity.constant.Category; import com.gamsa.activity.domain.Activity; +import com.gamsa.activity.domain.District; import com.gamsa.activity.domain.Institute; import java.math.BigDecimal; import java.time.LocalDateTime; @@ -14,12 +15,21 @@ class ActivityJpaEntityTest { @Test void 도메인모델에서_JPA엔티티로_변환() { // given + District district = District.builder() + .sidoCode(1234) + .sidoGunguCode(8888) + .sidoName("서울특별시") + .gunguName("강남구") + .sido(false) + .build(); + Institute institute = Institute.builder() .instituteId(1L) .name("도서관") .location("서울시") .latitude(new BigDecimal("123456789.12341234")) .longitude(new BigDecimal("987654321.43214321")) + .sidoGungu(district) .phone("010xxxxxxxx") .build(); @@ -44,6 +54,7 @@ class ActivityJpaEntityTest { .url("https://...") .category(Category.OTHER_ACTIVITIES) .institute(institute) + .sidoGungu(district) .build(); // when @@ -56,12 +67,21 @@ class ActivityJpaEntityTest { @Test void JPA엔티에서_도메인모델로_변환() { // given + DistrictJpaEntity districtJpaEntity = DistrictJpaEntity.builder() + .sidoCode(1234) + .sidoGunguCode(8888) + .sidoName("서울특별시") + .gunguName("강남구") + .sido(false) + .build(); + InstituteJpaEntity institute = InstituteJpaEntity.builder() .instituteId(1L) .name("도서관") .location("서울시") .latitude(new BigDecimal("123456789.12341234")) .longitude(new BigDecimal("987654321.43214321")) + .sidoGungu(districtJpaEntity) .phone("010xxxxxxxx") .build(); @@ -86,6 +106,7 @@ class ActivityJpaEntityTest { .url("https://...") .category(Category.OTHER_ACTIVITIES) .institute(institute) + .sidoGungu(districtJpaEntity) .build(); // when diff --git a/src/test/java/com/gamsa/activity/entity/InstituteJpaEntityTest.java b/src/test/java/com/gamsa/activity/entity/InstituteJpaEntityTest.java index 9b71ac1..8e10d69 100644 --- a/src/test/java/com/gamsa/activity/entity/InstituteJpaEntityTest.java +++ b/src/test/java/com/gamsa/activity/entity/InstituteJpaEntityTest.java @@ -2,6 +2,7 @@ import static org.assertj.core.api.Assertions.assertThat; +import com.gamsa.activity.domain.District; import com.gamsa.activity.domain.Institute; import java.math.BigDecimal; import org.junit.jupiter.api.DisplayName; @@ -9,16 +10,26 @@ class InstituteJpaEntityTest { + @Test @DisplayName("도메인 모델에서 JPA엔티티로 변환") void changeToJpaEntity() { // given + District district = District.builder() + .sidoCode(1234) + .sidoGunguCode(8888) + .sidoName("서울특별시") + .gunguName("강남구") + .sido(false) + .build(); + Institute model = Institute.builder() .instituteId(1L) .name("도서관") .location("서울시") .latitude(new BigDecimal("123456789.12341234")) .longitude(new BigDecimal("987654321.43214321")) + .sidoGungu(district) .phone("010xxxxxxxx") .build(); // when @@ -31,12 +42,21 @@ void changeToJpaEntity() { @DisplayName("JPA엔티티에서 도메인 모델로 변환") void changeToDomainModel() { // given + DistrictJpaEntity districtJpaEntity = DistrictJpaEntity.builder() + .sidoCode(1234) + .sidoGunguCode(8888) + .sidoName("서울특별시") + .gunguName("강남구") + .sido(false) + .build(); + InstituteJpaEntity jpaEntity = InstituteJpaEntity.builder() .instituteId(1L) .name("도서관") .location("서울시") .latitude(new BigDecimal("123456789.12341234")) .longitude(new BigDecimal("987654321.43214321")) + .sidoGungu(districtJpaEntity) .phone("010xxxxxxxx") .build(); // when diff --git a/src/test/java/com/gamsa/activity/repository/ActivityJpaRepositoryTest.java b/src/test/java/com/gamsa/activity/repository/ActivityJpaRepositoryTest.java index 37b457d..c9ad167 100644 --- a/src/test/java/com/gamsa/activity/repository/ActivityJpaRepositoryTest.java +++ b/src/test/java/com/gamsa/activity/repository/ActivityJpaRepositoryTest.java @@ -92,16 +92,16 @@ class ActivityJpaRepositoryTest { // 필터링 private final ActivityFilterRequest noFilterReq = new ActivityFilterRequest( - null, false, false); + null, null, null, false, false); private final ActivityFilterRequest otherCategoryFilterReq = new ActivityFilterRequest( - Category.OTHER_ACTIVITIES, false, false); + Category.OTHER_ACTIVITIES, null, null, false, false); private final ActivityFilterRequest teenPossibleFilterReq = new ActivityFilterRequest( - null, true, false); + null, null, null, true, false); private final ActivityFilterRequest beforeDeadlineFilterReq = new ActivityFilterRequest( - null, false, true); + null, null, null, false, true); @Test void 새_활동_저장() { diff --git a/src/test/java/com/gamsa/activity/service/ActivityServiceTest.java b/src/test/java/com/gamsa/activity/service/ActivityServiceTest.java index 27a88b4..9a1e164 100644 --- a/src/test/java/com/gamsa/activity/service/ActivityServiceTest.java +++ b/src/test/java/com/gamsa/activity/service/ActivityServiceTest.java @@ -10,6 +10,7 @@ import com.gamsa.activity.exception.ActivityException; import com.gamsa.activity.stub.StubEmptyActivityRepository; import com.gamsa.activity.stub.StubExistsActivityRepository; +import com.gamsa.activity.stub.StubExistsDistrictRepository; import com.gamsa.activity.stub.StubExistsInstituteRepository; import java.time.LocalDateTime; import org.junit.jupiter.api.Assertions; @@ -41,6 +42,7 @@ class ActivityServiceTest { .url("https://...") .category(Category.OTHER_ACTIVITIES) .instituteId(1L) + .sidoGunguCode(1) .build(); @Test @@ -48,7 +50,8 @@ class ActivityServiceTest { // given ActivityService activityService = new ActivityService( new StubEmptyActivityRepository(), - new StubExistsInstituteRepository() + new StubExistsInstituteRepository(), + new StubExistsDistrictRepository() ); // then @@ -64,7 +67,8 @@ class ActivityServiceTest { // given ActivityService activityService = new ActivityService( new StubExistsActivityRepository(), - new StubExistsInstituteRepository() + new StubExistsInstituteRepository(), + new StubExistsDistrictRepository() ); // then @@ -79,7 +83,8 @@ class ActivityServiceTest { // given ActivityService activityService = new ActivityService( new StubEmptyActivityRepository(), - new StubExistsInstituteRepository() + new StubExistsInstituteRepository(), + new StubExistsDistrictRepository() ); // when @@ -95,7 +100,8 @@ class ActivityServiceTest { // given ActivityService activityService = new ActivityService( new StubExistsActivityRepository(), - new StubExistsInstituteRepository() + new StubExistsInstituteRepository(), + new StubExistsDistrictRepository() ); // when @@ -110,7 +116,8 @@ class ActivityServiceTest { // given ActivityService activityService = new ActivityService( new StubEmptyActivityRepository(), - new StubExistsInstituteRepository() + new StubExistsInstituteRepository(), + new StubExistsDistrictRepository() ); // then diff --git a/src/test/java/com/gamsa/activity/service/InstituteServiceTest.java b/src/test/java/com/gamsa/activity/service/InstituteServiceTest.java index c69d4c7..16814aa 100644 --- a/src/test/java/com/gamsa/activity/service/InstituteServiceTest.java +++ b/src/test/java/com/gamsa/activity/service/InstituteServiceTest.java @@ -7,6 +7,7 @@ import com.gamsa.activity.dto.InstituteSaveRequest; import com.gamsa.activity.exception.ActivityException; import com.gamsa.activity.stub.StubEmptyInstituteRepository; +import com.gamsa.activity.stub.StubExistsDistrictRepository; import com.gamsa.activity.stub.StubExistsInstituteRepository; import java.math.BigDecimal; import org.junit.jupiter.api.DisplayName; @@ -27,7 +28,10 @@ class InstituteServiceTest { @DisplayName("봉사기관을 생성한다.") void save() { // given - InstituteService service = new InstituteService(new StubEmptyInstituteRepository()); + InstituteService service = new InstituteService( + new StubEmptyInstituteRepository(), + new StubExistsDistrictRepository() + ); // then assertDoesNotThrow(() -> { // when @@ -39,7 +43,10 @@ void save() { @DisplayName("봉시기관 동일 이름 충돌로 실패한다.") void saveFail() { // given - InstituteService service = new InstituteService(new StubExistsInstituteRepository()); + InstituteService service = new InstituteService( + new StubExistsInstituteRepository(), + new StubExistsDistrictRepository() + ); // then assertThrows(ActivityException.class, () -> { // when diff --git a/src/test/java/com/gamsa/activity/stub/StubExistsActivityRepository.java b/src/test/java/com/gamsa/activity/stub/StubExistsActivityRepository.java index d5a6c15..d4161d2 100644 --- a/src/test/java/com/gamsa/activity/stub/StubExistsActivityRepository.java +++ b/src/test/java/com/gamsa/activity/stub/StubExistsActivityRepository.java @@ -1,6 +1,7 @@ package com.gamsa.activity.stub; import com.gamsa.activity.domain.Activity; +import com.gamsa.activity.domain.District; import com.gamsa.activity.domain.Institute; import com.gamsa.activity.dto.ActivityFilterRequest; import com.gamsa.activity.repository.ActivityRepository; @@ -14,6 +15,14 @@ public class StubExistsActivityRepository implements ActivityRepository { + private final District district = District.builder() + .sidoCode(1234) + .sidoGunguCode(8888) + .sidoName("서울특별시") + .gunguName("강남구") + .sido(false) + .build(); + private final Institute institute = Institute.builder() .instituteId(1L) .name("도서관") @@ -43,6 +52,7 @@ public class StubExistsActivityRepository implements ActivityRepository { .actPhone("032-577-3026") .url("https://...") .institute(institute) + .sidoGungu(district) .build(); @Override diff --git a/src/test/java/com/gamsa/activity/stub/StubExistsDistrictRepository.java b/src/test/java/com/gamsa/activity/stub/StubExistsDistrictRepository.java new file mode 100644 index 0000000..247b7d1 --- /dev/null +++ b/src/test/java/com/gamsa/activity/stub/StubExistsDistrictRepository.java @@ -0,0 +1,32 @@ +package com.gamsa.activity.stub; + +import com.gamsa.activity.domain.District; +import com.gamsa.activity.repository.DistrictRepository; +import java.util.List; +import java.util.Optional; + +public class StubExistsDistrictRepository implements DistrictRepository { + + private final District district = District.builder() + .sidoCode(1234) + .sidoGunguCode(8888) + .sidoName("서울특별시") + .gunguName("강남구") + .sido(false) + .build(); + + @Override + public void save(District district) { + // do nothing + } + + @Override + public Optional findBySidoGunguCode(int gunguCode) { + return Optional.of(district); + } + + @Override + public List findAllBysido(boolean sido) { + return List.of(district); + } +} From 98f0921ea505282cbcc05fbc10e613f8ad9792e2 Mon Sep 17 00:00:00 2001 From: 5win <94297900+5win@users.noreply.github.com> Date: Wed, 16 Oct 2024 21:13:03 +0900 Subject: [PATCH 05/17] =?UTF-8?q?infra:=20Github=20actions=EB=A5=BC=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=9C=20CI=20(#36)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/dev-ci.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .github/workflows/dev-ci.yml diff --git a/.github/workflows/dev-ci.yml b/.github/workflows/dev-ci.yml new file mode 100644 index 0000000..7d56331 --- /dev/null +++ b/.github/workflows/dev-ci.yml @@ -0,0 +1,26 @@ +name: Backend CI + +on: + pull_request: + branches: [ "develop", "week**" ] + +jobs: + build: + + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582 # v4.0.0 + + - name: Build with Gradle Wrapper + run: ./gradlew build From 149d6fca7c80159faca2715de7737dc46ab50556 Mon Sep 17 00:00:00 2001 From: Awhn <69659322+Awhn@users.noreply.github.com> Date: Fri, 25 Oct 2024 23:22:17 +0900 Subject: [PATCH 06/17] =?UTF-8?q?feat:=20=EB=B4=89=EC=82=AC=20=ED=99=9C?= =?UTF-8?q?=EB=8F=99=20=EA=B8=B0=EB=A1=9D=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#39)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 유저 기록 기능 구현 * test: 유저 기록 기능 테스트 * fix: activity 관련 의존성 수정 --- .../history/constant/ActivityStatus.java | 16 +++ .../constant/ActivityStatusConverter.java | 29 +++++ .../history/controller/HistoryController.java | 35 ++++++ .../com/gamsa/history/domain/History.java | 37 ++++++ .../history/dto/HistoryFindSliceResponse.java | 30 +++++ .../gamsa/history/dto/HistorySaveRequest.java | 26 ++++ .../history/entity/HistoryJpaEntity.java | 59 +++++++++ .../repository/HistoryCustomRepository.java | 9 ++ .../HistoryCustomRepositoryImpl.java | 59 +++++++++ .../repository/HistoryJpaRepository.java | 8 ++ .../history/repository/HistoryRepository.java | 18 +++ .../repository/HistoryRepositoryImpl.java | 38 ++++++ .../gamsa/history/service/HistoryService.java | 56 +++++++++ .../history/entity/HistoryJpaEntityTest.java | 113 ++++++++++++++++++ .../repository/HistoryJpaRepositoryTest.java | 102 ++++++++++++++++ .../history/service/HitstoryServiceTest.java | 57 +++++++++ .../history/stub/StubHistoryRepository.java | 99 +++++++++++++++ 17 files changed, 791 insertions(+) create mode 100644 src/main/java/com/gamsa/history/constant/ActivityStatus.java create mode 100644 src/main/java/com/gamsa/history/constant/ActivityStatusConverter.java create mode 100644 src/main/java/com/gamsa/history/controller/HistoryController.java create mode 100644 src/main/java/com/gamsa/history/domain/History.java create mode 100644 src/main/java/com/gamsa/history/dto/HistoryFindSliceResponse.java create mode 100644 src/main/java/com/gamsa/history/dto/HistorySaveRequest.java create mode 100644 src/main/java/com/gamsa/history/entity/HistoryJpaEntity.java create mode 100644 src/main/java/com/gamsa/history/repository/HistoryCustomRepository.java create mode 100644 src/main/java/com/gamsa/history/repository/HistoryCustomRepositoryImpl.java create mode 100644 src/main/java/com/gamsa/history/repository/HistoryJpaRepository.java create mode 100644 src/main/java/com/gamsa/history/repository/HistoryRepository.java create mode 100644 src/main/java/com/gamsa/history/repository/HistoryRepositoryImpl.java create mode 100644 src/main/java/com/gamsa/history/service/HistoryService.java create mode 100644 src/test/java/com/gamsa/history/entity/HistoryJpaEntityTest.java create mode 100644 src/test/java/com/gamsa/history/repository/HistoryJpaRepositoryTest.java create mode 100644 src/test/java/com/gamsa/history/service/HitstoryServiceTest.java create mode 100644 src/test/java/com/gamsa/history/stub/StubHistoryRepository.java diff --git a/src/main/java/com/gamsa/history/constant/ActivityStatus.java b/src/main/java/com/gamsa/history/constant/ActivityStatus.java new file mode 100644 index 0000000..ad88bbd --- /dev/null +++ b/src/main/java/com/gamsa/history/constant/ActivityStatus.java @@ -0,0 +1,16 @@ +package com.gamsa.history.constant; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum ActivityStatus { + APPLIED("접수"), + WAITING("활동 대기"), + ACT("활동"), + FINISHED("활동 완료"), + REVIEWED("리뷰 완료"); + + private final String name; +} diff --git a/src/main/java/com/gamsa/history/constant/ActivityStatusConverter.java b/src/main/java/com/gamsa/history/constant/ActivityStatusConverter.java new file mode 100644 index 0000000..0f9a451 --- /dev/null +++ b/src/main/java/com/gamsa/history/constant/ActivityStatusConverter.java @@ -0,0 +1,29 @@ +package com.gamsa.history.constant; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +import java.util.stream.Stream; + +@Converter +public class ActivityStatusConverter implements AttributeConverter { + + @Override + public String convertToDatabaseColumn(ActivityStatus activityStatus) { + if (activityStatus == null) { + return null; + } + return activityStatus.getName(); + } + + @Override + public ActivityStatus convertToEntityAttribute(String name) { + if (name == null) { + return null; + } + return Stream.of(ActivityStatus.values()) + .filter(category -> category.getName().equals(name)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 활동 상태 접근.")); + } +} diff --git a/src/main/java/com/gamsa/history/controller/HistoryController.java b/src/main/java/com/gamsa/history/controller/HistoryController.java new file mode 100644 index 0000000..1dac62f --- /dev/null +++ b/src/main/java/com/gamsa/history/controller/HistoryController.java @@ -0,0 +1,35 @@ +package com.gamsa.history.controller; + +import com.gamsa.history.dto.HistoryFindSliceResponse; +import com.gamsa.history.dto.HistorySaveRequest; +import com.gamsa.history.service.HistoryService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/v1/history") +public class HistoryController { + private HistoryService historyService; + + @PostMapping + public ResponseEntity addHistory(@RequestBody HistorySaveRequest saveRequest) { + historyService.save(saveRequest); + return new ResponseEntity<>(HttpStatus.CREATED); + } + + @GetMapping("{avatar-id}") + public Slice findSliceByUserId(@PathVariable("avatar-id") long avatarId, Pageable pageable) { + return historyService.findSliceByAvatarId(avatarId, pageable); + } + + @DeleteMapping("{history-id}") + public ResponseEntity deleteHistory(@PathVariable("history-id") long historyId) { + historyService.delete(historyId); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } +} diff --git a/src/main/java/com/gamsa/history/domain/History.java b/src/main/java/com/gamsa/history/domain/History.java new file mode 100644 index 0000000..661816e --- /dev/null +++ b/src/main/java/com/gamsa/history/domain/History.java @@ -0,0 +1,37 @@ +package com.gamsa.history.domain; + +import com.gamsa.activity.domain.Activity; +import com.gamsa.avatar.domain.Avatar; +import com.gamsa.history.constant.ActivityStatus; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +@Builder +@AllArgsConstructor +public class History { + private long historyId; + private Avatar avatar; + private Activity activity; + private ActivityStatus activityStatus; + private boolean reviewed; + + public void changeActivityStatusOnDate(LocalDateTime now) { + if ((this.activityStatus == ActivityStatus.APPLIED) + && (now.isAfter(activity.getNoticeEndDate()))) { + this.activityStatus = ActivityStatus.WAITING; + } else if ((this.activityStatus == ActivityStatus.WAITING) + && (now.isAfter(activity.getActStartDate()))) { + this.activityStatus = ActivityStatus.ACT; + } else if (now.isAfter(activity.getActEndDate())) { + this.activityStatus = ActivityStatus.FINISHED; + } + } + + public void changeReviewed(boolean reviewed) { + this.reviewed = reviewed; + } +} diff --git a/src/main/java/com/gamsa/history/dto/HistoryFindSliceResponse.java b/src/main/java/com/gamsa/history/dto/HistoryFindSliceResponse.java new file mode 100644 index 0000000..c01fb39 --- /dev/null +++ b/src/main/java/com/gamsa/history/dto/HistoryFindSliceResponse.java @@ -0,0 +1,30 @@ +package com.gamsa.history.dto; + +import com.gamsa.activity.dto.ActivityDetailResponse; +import com.gamsa.history.constant.ActivityStatus; +import com.gamsa.history.domain.History; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@Builder +@RequiredArgsConstructor +public class HistoryFindSliceResponse { + + private final long historyId; + private final long avatarId; + private final ActivityDetailResponse activity; + private final ActivityStatus activityStatus; + private final boolean reviewed; + + public static HistoryFindSliceResponse from(final History history) { + return HistoryFindSliceResponse.builder() + .historyId(history.getHistoryId()) + .avatarId(history.getAvatar().getAvatarId()) + .activityStatus(history.getActivityStatus()) + .reviewed(history.isReviewed()) + .activity(ActivityDetailResponse.from(history.getActivity())) + .build(); + } +} diff --git a/src/main/java/com/gamsa/history/dto/HistorySaveRequest.java b/src/main/java/com/gamsa/history/dto/HistorySaveRequest.java new file mode 100644 index 0000000..b1f5964 --- /dev/null +++ b/src/main/java/com/gamsa/history/dto/HistorySaveRequest.java @@ -0,0 +1,26 @@ +package com.gamsa.history.dto; + +import com.gamsa.activity.domain.Activity; +import com.gamsa.avatar.domain.Avatar; +import com.gamsa.history.constant.ActivityStatus; +import com.gamsa.history.domain.History; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@Builder +@RequiredArgsConstructor +public class HistorySaveRequest { + private final long avatarId; + private final long actId; + + public History toModel(Avatar avatar, Activity activity) { + return History.builder() + .activity(activity) + .avatar(avatar) + .activityStatus(ActivityStatus.APPLIED) + .reviewed(false) + .build(); + } +} diff --git a/src/main/java/com/gamsa/history/entity/HistoryJpaEntity.java b/src/main/java/com/gamsa/history/entity/HistoryJpaEntity.java new file mode 100644 index 0000000..b9f7e07 --- /dev/null +++ b/src/main/java/com/gamsa/history/entity/HistoryJpaEntity.java @@ -0,0 +1,59 @@ +package com.gamsa.history.entity; + + +import com.gamsa.activity.entity.ActivityJpaEntity; +import com.gamsa.avatar.entity.AvatarJpaEntity; +import com.gamsa.common.entity.BaseEntity; +import com.gamsa.history.constant.ActivityStatus; +import com.gamsa.history.constant.ActivityStatusConverter; +import com.gamsa.history.domain.History; +import jakarta.persistence.*; +import lombok.*; + +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Entity +@Table(name = "History") +public class HistoryJpaEntity extends BaseEntity { + @Id + @GeneratedValue() + @Column(name = "history_id") + private long historyId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "avatar_id") + private AvatarJpaEntity avatar; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "activity_id") + private ActivityJpaEntity activity; + + @Convert(converter = ActivityStatusConverter.class) + @Column(name = "activity_status") + private ActivityStatus activityStatus; + + @Column(name = "reviewed") + private boolean reviewed; + + public static HistoryJpaEntity from(History history) { + return HistoryJpaEntity.builder() + .historyId(history.getHistoryId()) + .avatar(AvatarJpaEntity.from(history.getAvatar())) + .activity(ActivityJpaEntity.from(history.getActivity())) + .activityStatus(history.getActivityStatus()) + .reviewed(history.isReviewed()) + .build(); + } + + public History toModel() { + return History.builder() + .historyId(historyId) + .avatar(avatar.toModel()) + .activity(activity.toModel()) + .activityStatus(activityStatus) + .reviewed(reviewed) + .build(); + } +} diff --git a/src/main/java/com/gamsa/history/repository/HistoryCustomRepository.java b/src/main/java/com/gamsa/history/repository/HistoryCustomRepository.java new file mode 100644 index 0000000..d04ab24 --- /dev/null +++ b/src/main/java/com/gamsa/history/repository/HistoryCustomRepository.java @@ -0,0 +1,9 @@ +package com.gamsa.history.repository; + +import com.gamsa.history.entity.HistoryJpaEntity; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; + +public interface HistoryCustomRepository { + Slice findSliceByAvatarId(long avatarId, Pageable pageable); +} diff --git a/src/main/java/com/gamsa/history/repository/HistoryCustomRepositoryImpl.java b/src/main/java/com/gamsa/history/repository/HistoryCustomRepositoryImpl.java new file mode 100644 index 0000000..012feed --- /dev/null +++ b/src/main/java/com/gamsa/history/repository/HistoryCustomRepositoryImpl.java @@ -0,0 +1,59 @@ +package com.gamsa.history.repository; + +import com.gamsa.history.entity.HistoryJpaEntity; +import com.querydsl.core.types.Order; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.dsl.PathBuilder; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; + +import java.util.ArrayList; +import java.util.List; + +import static com.gamsa.history.entity.QHistoryJpaEntity.historyJpaEntity; + +@RequiredArgsConstructor +public class HistoryCustomRepositoryImpl implements HistoryCustomRepository { + private final JPAQueryFactory jpaQueryFactory; + + @Override + public Slice findSliceByAvatarId(long avatarId, Pageable pageable) { + List orders = getAllOrderSpecifiers(pageable); + + List results = jpaQueryFactory + .selectFrom(historyJpaEntity) + .where(historyJpaEntity.avatar.avatarId.eq(avatarId)) + .orderBy(orders.toArray(OrderSpecifier[]::new)) + .offset(pageable.getOffset()) + .limit(pageable.getOffset() + 1) + .fetch(); + + return checkLastPage(pageable, results); + } + + private Slice checkLastPage(Pageable pageable, List results) { + boolean hasNest = false; + if (results.size() > pageable.getPageSize()) { + hasNest = true; + results.remove(pageable.getPageSize()); + } + return new SliceImpl<>(results, pageable, hasNest); + } + + private List getAllOrderSpecifiers(Pageable pageable) { + List orders = new ArrayList<>(); + + pageable.getSort().stream() + .forEach(order -> { + Order direction = order.getDirection().isAscending() ? Order.ASC : Order.DESC; + String property = order.getProperty(); + PathBuilder orderPath = new PathBuilder(HistoryJpaEntity.class, "activityJpaEntity"); + orders.add(new OrderSpecifier(direction, orderPath.get(property))); + } + ); + return orders; + } +} diff --git a/src/main/java/com/gamsa/history/repository/HistoryJpaRepository.java b/src/main/java/com/gamsa/history/repository/HistoryJpaRepository.java new file mode 100644 index 0000000..c3dc2ca --- /dev/null +++ b/src/main/java/com/gamsa/history/repository/HistoryJpaRepository.java @@ -0,0 +1,8 @@ +package com.gamsa.history.repository; + +import com.gamsa.history.entity.HistoryJpaEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface HistoryJpaRepository extends JpaRepository, + HistoryCustomRepository { +} diff --git a/src/main/java/com/gamsa/history/repository/HistoryRepository.java b/src/main/java/com/gamsa/history/repository/HistoryRepository.java new file mode 100644 index 0000000..f2f0683 --- /dev/null +++ b/src/main/java/com/gamsa/history/repository/HistoryRepository.java @@ -0,0 +1,18 @@ +package com.gamsa.history.repository; + +import com.gamsa.history.domain.History; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; + +import java.util.Optional; + +public interface HistoryRepository { + + void save(History history); + + Optional findById(long id); + + void delete(History history); + + Slice findSliceByAvatarId(long avatarId, Pageable pageable); +} diff --git a/src/main/java/com/gamsa/history/repository/HistoryRepositoryImpl.java b/src/main/java/com/gamsa/history/repository/HistoryRepositoryImpl.java new file mode 100644 index 0000000..7832a47 --- /dev/null +++ b/src/main/java/com/gamsa/history/repository/HistoryRepositoryImpl.java @@ -0,0 +1,38 @@ +package com.gamsa.history.repository; + +import com.gamsa.history.domain.History; +import com.gamsa.history.entity.HistoryJpaEntity; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@RequiredArgsConstructor +@Repository +public class HistoryRepositoryImpl implements HistoryRepository { + private final HistoryJpaRepository historyJpaRepository; + + @Override + public void save(History history) { + historyJpaRepository.save(HistoryJpaEntity.from(history)); + } + + @Override + public Slice findSliceByAvatarId(long avatarId, Pageable pageable) { + return historyJpaRepository.findSliceByAvatarId(avatarId, pageable) + .map(entity -> entity.toModel()); + } + + @Override + public void delete(History history) { + historyJpaRepository.delete(HistoryJpaEntity.from(history)); + } + + @Override + public Optional findById(long id) { + return historyJpaRepository.findById(id) + .map(entity -> entity.toModel()); + } +} diff --git a/src/main/java/com/gamsa/history/service/HistoryService.java b/src/main/java/com/gamsa/history/service/HistoryService.java new file mode 100644 index 0000000..8bb326c --- /dev/null +++ b/src/main/java/com/gamsa/history/service/HistoryService.java @@ -0,0 +1,56 @@ +package com.gamsa.history.service; + +import com.gamsa.activity.domain.Activity; +import com.gamsa.activity.repository.ActivityRepository; +import com.gamsa.avatar.domain.Avatar; +import com.gamsa.avatar.repository.AvatarRepository; +import com.gamsa.history.domain.History; +import com.gamsa.history.dto.HistoryFindSliceResponse; +import com.gamsa.history.dto.HistorySaveRequest; +import com.gamsa.history.repository.HistoryRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.NoSuchElementException; + +@RequiredArgsConstructor +@Service +public class HistoryService { + private final HistoryRepository historyRepository; + private final AvatarRepository avatarRepository; + private final ActivityRepository activityRepository; + + public void save(HistorySaveRequest saveRequest) { + Avatar avatar = avatarRepository.findById(saveRequest.getAvatarId()).orElseThrow(NoSuchElementException::new); + Activity activity = activityRepository.findById(saveRequest.getActId()).orElseThrow(NoSuchElementException::new); + History history = saveRequest.toModel(avatar, activity); + historyRepository.save(history); + } + + public Slice findSliceByAvatarId(long avatarId, Pageable pageable) { + Slice histories = historyRepository.findSliceByAvatarId(avatarId, pageable); + histories.forEach(this::checkDate); + return histories.map(HistoryFindSliceResponse::from); + } + + public void delete(long historyId) { + History history = historyRepository.findById(historyId).orElseThrow(NoSuchElementException::new); + historyRepository.delete(history); + } + + public void updateReviewed(long historyId, boolean isReviewed) { + History history = historyRepository.findById(historyId).orElseThrow(NoSuchElementException::new); + history.changeReviewed(isReviewed); + historyRepository.save(history); + } + + public History checkDate(History history) { + LocalDateTime now = LocalDateTime.now(); + history.changeActivityStatusOnDate(now); + historyRepository.save(history); + return history; + } +} diff --git a/src/test/java/com/gamsa/history/entity/HistoryJpaEntityTest.java b/src/test/java/com/gamsa/history/entity/HistoryJpaEntityTest.java new file mode 100644 index 0000000..26f0594 --- /dev/null +++ b/src/test/java/com/gamsa/history/entity/HistoryJpaEntityTest.java @@ -0,0 +1,113 @@ +package com.gamsa.history.entity; + +import com.gamsa.activity.constant.Category; +import com.gamsa.activity.domain.Activity; +import com.gamsa.activity.domain.District; +import com.gamsa.activity.domain.Institute; +import com.gamsa.activity.entity.ActivityJpaEntity; +import com.gamsa.avatar.constant.AgeRange; +import com.gamsa.avatar.constant.Experienced; +import com.gamsa.avatar.domain.Avatar; +import com.gamsa.avatar.entity.AvatarJpaEntity; +import com.gamsa.common.config.TestConfig; +import com.gamsa.history.constant.ActivityStatus; +import com.gamsa.history.domain.History; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; + +@Import(TestConfig.class) +public class HistoryJpaEntityTest { + // given + District district = District.builder() + .sidoCode(1234) + .sidoGunguCode(8888) + .sidoName("서울특별시") + .gunguName("강남구") + .sido(false) + .build(); + + Institute institute = Institute.builder() + .instituteId(1L) + .name("도서관") + .location("서울시") + .latitude(new BigDecimal("123456789.12341234")) + .longitude(new BigDecimal("987654321.43214321")) + .sidoGungu(district) + .phone("010xxxxxxxx") + .build(); + + Activity activity = Activity.builder() + .actId(1L) + .actTitle("어린이놀이안전관리 및 놀잇감 청결유지 및 정리") + .actLocation("아이사랑꿈터 서구 5호점") + .description("봉사 내용") + .noticeStartDate(LocalDateTime.of(2024, 9, 10, 0, 0)) + .noticeEndDate(LocalDateTime.of(2024, 12, 7, 0, 0)) + .actStartDate(LocalDateTime.of(2024, 9, 10, 0, 0)) + .actEndDate(LocalDateTime.of(2024, 12, 7, 0, 0)) + .actStartTime(13) + .actEndTime(18) + .recruitTotalNum(1) + .adultPossible(true) + .teenPossible(false) + .groupPossible(false) + .actWeek(0111110) + .actManager("윤순영") + .actPhone("032-577-3026") + .url("https://...") + .category(Category.OTHER_ACTIVITIES) + .institute(institute) + .sidoGungu(district) + .build(); + + Avatar avatar = Avatar.builder() + .avatarId(1L) + .avatarLevel(1L) + .avatarExp(1L) + .nickname("닉네임") + .ageRange(AgeRange.ADULT) + .experienced(Experienced.NOVICE) + .build(); + + @Test + void 도메인에서_엔티티로() { + // given + History history = History.builder() + .historyId(1L) + .activity(activity) + .avatar(avatar) + .activityStatus(ActivityStatus.APPLIED) + .reviewed(false) + .build(); + + //when + HistoryJpaEntity historyJpaEntity = HistoryJpaEntity.from(history); + + //then + assertThat(historyJpaEntity.getHistoryId()).isEqualTo(history.getHistoryId()); + + } + + @Test + void 엔티티에서_도메인으로() { + //given + HistoryJpaEntity historyJpaEntity = HistoryJpaEntity.builder() + .historyId(1L) + .activity(ActivityJpaEntity.from(activity)) + .avatar(AvatarJpaEntity.from(avatar)) + .activityStatus(ActivityStatus.APPLIED) + .reviewed(false) + .build(); + + //when + History history = historyJpaEntity.toModel(); + + //then + assertThat(history.getHistoryId()).isEqualTo(historyJpaEntity.getHistoryId()); + } +} diff --git a/src/test/java/com/gamsa/history/repository/HistoryJpaRepositoryTest.java b/src/test/java/com/gamsa/history/repository/HistoryJpaRepositoryTest.java new file mode 100644 index 0000000..9e3c050 --- /dev/null +++ b/src/test/java/com/gamsa/history/repository/HistoryJpaRepositoryTest.java @@ -0,0 +1,102 @@ +package com.gamsa.history.repository; + +import com.gamsa.activity.constant.Category; +import com.gamsa.activity.domain.Activity; +import com.gamsa.activity.domain.District; +import com.gamsa.activity.domain.Institute; +import com.gamsa.activity.entity.ActivityJpaEntity; +import com.gamsa.avatar.constant.AgeRange; +import com.gamsa.avatar.constant.Experienced; +import com.gamsa.avatar.domain.Avatar; +import com.gamsa.avatar.entity.AvatarJpaEntity; +import com.gamsa.common.config.TestConfig; +import com.gamsa.history.constant.ActivityStatus; +import com.gamsa.history.entity.HistoryJpaEntity; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; + +@DataJpaTest +@Import(TestConfig.class) +public class HistoryJpaRepositoryTest { + @Autowired + private HistoryJpaRepository historyJpaRepository; + + District district = District.builder() + .sidoCode(1234) + .sidoGunguCode(8888) + .sidoName("서울특별시") + .gunguName("강남구") + .sido(false) + .build(); + + Institute institute = Institute.builder() + .instituteId(1L) + .name("도서관") + .location("서울시") + .latitude(new BigDecimal("123456789.12341234")) + .longitude(new BigDecimal("987654321.43214321")) + .sidoGungu(district) + .phone("010xxxxxxxx") + .build(); + + Activity activity = Activity.builder() + .actId(1L) + .actTitle("어린이놀이안전관리 및 놀잇감 청결유지 및 정리") + .actLocation("아이사랑꿈터 서구 5호점") + .description("봉사 내용") + .noticeStartDate(LocalDateTime.of(2024, 9, 10, 0, 0)) + .noticeEndDate(LocalDateTime.of(2024, 12, 7, 0, 0)) + .actStartDate(LocalDateTime.of(2024, 9, 10, 0, 0)) + .actEndDate(LocalDateTime.of(2024, 12, 7, 0, 0)) + .actStartTime(13) + .actEndTime(18) + .recruitTotalNum(1) + .adultPossible(true) + .teenPossible(false) + .groupPossible(false) + .actWeek(0111110) + .actManager("윤순영") + .actPhone("032-577-3026") + .url("https://...") + .category(Category.OTHER_ACTIVITIES) + .institute(institute) + .sidoGungu(district) + .build(); + + private final Avatar avatar = Avatar.builder() + .avatarId(1L) + .avatarLevel(1L) + .avatarExp(1L) + .nickname("닉네임") + .ageRange(AgeRange.ADULT) + .experienced(Experienced.NOVICE) + .build(); + + private final HistoryJpaEntity historyJpaEntity = HistoryJpaEntity.builder() + .historyId(1L) + .activity(ActivityJpaEntity.from(activity)) + .avatar(AvatarJpaEntity.from(avatar)) + .activityStatus(ActivityStatus.APPLIED) + .reviewed(false) + .build(); + + + @Test + void 새_기록_저장() { + // when + historyJpaRepository.save(historyJpaEntity); + + //then + assertThat(historyJpaRepository.findById(1L).get().getHistoryId()) + .isEqualTo(historyJpaEntity.getHistoryId()); + } +} + + diff --git a/src/test/java/com/gamsa/history/service/HitstoryServiceTest.java b/src/test/java/com/gamsa/history/service/HitstoryServiceTest.java new file mode 100644 index 0000000..9c63fb3 --- /dev/null +++ b/src/test/java/com/gamsa/history/service/HitstoryServiceTest.java @@ -0,0 +1,57 @@ +package com.gamsa.history.service; + +import com.gamsa.activity.stub.StubExistsActivityRepository; +import com.gamsa.avatar.stub.StubAvatarRepository; +import com.gamsa.history.dto.HistorySaveRequest; +import com.gamsa.history.stub.StubHistoryRepository; +import org.junit.jupiter.api.Test; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +public class HitstoryServiceTest { + + HistorySaveRequest historySaveRequest = HistorySaveRequest.builder() + .actId(1L) + .avatarId(1L) + .build(); + + @Test + void 새로운_기록_저장() { + //given + HistoryService historyService = new HistoryService(new StubHistoryRepository(), new StubAvatarRepository(), new StubExistsActivityRepository()); + + //when & then + assertDoesNotThrow(() -> historyService.save(historySaveRequest)); + } + + @Test + void 유저_기록_찾기() { + //given + HistoryService historyService = new HistoryService(new StubHistoryRepository(), new StubAvatarRepository(), new StubExistsActivityRepository()); + + //when & then + Pageable pageable = PageRequest.of(0, 10); + assertThat(historyService.findSliceByAvatarId(1L, pageable)).isNotNull(); + } + + @Test + void 기록_삭제() { + //given + HistoryService historyService = new HistoryService(new StubHistoryRepository(), new StubAvatarRepository(), new StubExistsActivityRepository()); + + //when & then + assertDoesNotThrow(() -> historyService.delete(1L)); + } + + @Test + void 리뷰_상태_업데이트() { + //given + HistoryService historyService = new HistoryService(new StubHistoryRepository(), new StubAvatarRepository(), new StubExistsActivityRepository()); + + //when & then + assertDoesNotThrow(() -> historyService.updateReviewed(1L, true)); + } +} diff --git a/src/test/java/com/gamsa/history/stub/StubHistoryRepository.java b/src/test/java/com/gamsa/history/stub/StubHistoryRepository.java new file mode 100644 index 0000000..d98a270 --- /dev/null +++ b/src/test/java/com/gamsa/history/stub/StubHistoryRepository.java @@ -0,0 +1,99 @@ +package com.gamsa.history.stub; + +import com.gamsa.activity.constant.Category; +import com.gamsa.activity.domain.Activity; +import com.gamsa.activity.domain.District; +import com.gamsa.activity.domain.Institute; +import com.gamsa.avatar.constant.AgeRange; +import com.gamsa.avatar.constant.Experienced; +import com.gamsa.avatar.domain.Avatar; +import com.gamsa.history.constant.ActivityStatus; +import com.gamsa.history.domain.History; +import com.gamsa.history.repository.HistoryRepository; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +public class StubHistoryRepository implements HistoryRepository { + District district = District.builder() + .sidoCode(1234) + .sidoGunguCode(8888) + .sidoName("서울특별시") + .gunguName("강남구") + .sido(false) + .build(); + + Institute institute = Institute.builder() + .instituteId(1L) + .name("도서관") + .location("서울시") + .latitude(new BigDecimal("123456789.12341234")) + .longitude(new BigDecimal("987654321.43214321")) + .sidoGungu(district) + .phone("010xxxxxxxx") + .build(); + + Activity activity = Activity.builder() + .actId(1L) + .actTitle("어린이놀이안전관리 및 놀잇감 청결유지 및 정리") + .actLocation("아이사랑꿈터 서구 5호점") + .description("봉사 내용") + .noticeStartDate(LocalDateTime.of(2024, 9, 10, 0, 0)) + .noticeEndDate(LocalDateTime.of(2024, 12, 7, 0, 0)) + .actStartDate(LocalDateTime.of(2024, 9, 10, 0, 0)) + .actEndDate(LocalDateTime.of(2024, 12, 7, 0, 0)) + .actStartTime(13) + .actEndTime(18) + .recruitTotalNum(1) + .adultPossible(true) + .teenPossible(false) + .groupPossible(false) + .actWeek(0111110) + .actManager("윤순영") + .actPhone("032-577-3026") + .url("https://...") + .category(Category.OTHER_ACTIVITIES) + .institute(institute) + .sidoGungu(district) + .build(); + + Avatar avatar = Avatar.builder() + .avatarId(1L) + .avatarLevel(1L) + .avatarExp(1L) + .nickname("닉네임") + .ageRange(AgeRange.ADULT) + .experienced(Experienced.NOVICE) + .build(); + + private final History history = History.builder() + .historyId(1L) + .activity(activity) + .avatar(avatar) + .activityStatus(ActivityStatus.APPLIED) + .reviewed(false) + .build(); + + @Override + public void save(History history) { + } + + @Override + public void delete(History history) { + } + + @Override + public Optional findById(long id) { + return Optional.of(history); + } + + @Override + public Slice findSliceByAvatarId(long avatarId, Pageable pageable) { + return new SliceImpl<>(List.of(history)); + } +} From 6f5e976a73f2e97927b0ed6b6bd3dd15949ffce9 Mon Sep 17 00:00:00 2001 From: Awhn <69659322+Awhn@users.noreply.github.com> Date: Tue, 29 Oct 2024 13:32:48 +0900 Subject: [PATCH 07/17] =?UTF-8?q?fix:=20=EC=95=84=EB=B0=94=ED=83=80=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=EC=8B=9C=20=EC=98=A4=EB=A5=98=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0=20(#44)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 유저 기록 기능 구현 * test: 유저 기록 기능 테스트 * fix: activity 관련 의존성 수정 * fix: 요구사항 변경에 따른 선택적 무한 스크롤 * fix: 아바타 역직렬화 과정에서의 오류 수정 --- .../activity/dto/ActivitySaveRequest.java | 49 ++++++++++--------- .../com/gamsa/avatar/constant/AgeRange.java | 17 +++++++ .../gamsa/avatar/constant/Experienced.java | 17 +++++++ .../avatar/controller/AvatarController.java | 6 +-- .../gamsa/avatar/dto/AvatarSaveRequest.java | 6 +-- .../gamsa/avatar/service/AvatarService.java | 1 - .../history/controller/HistoryController.java | 15 +++++- 7 files changed, 79 insertions(+), 32 deletions(-) diff --git a/src/main/java/com/gamsa/activity/dto/ActivitySaveRequest.java b/src/main/java/com/gamsa/activity/dto/ActivitySaveRequest.java index cc0bc8d..8f4bc59 100644 --- a/src/main/java/com/gamsa/activity/dto/ActivitySaveRequest.java +++ b/src/main/java/com/gamsa/activity/dto/ActivitySaveRequest.java @@ -4,11 +4,12 @@ import com.gamsa.activity.domain.Activity; import com.gamsa.activity.domain.District; import com.gamsa.activity.domain.Institute; -import java.time.LocalDateTime; import lombok.Builder; import lombok.Getter; import lombok.RequiredArgsConstructor; +import java.time.LocalDateTime; + @Getter @Builder @RequiredArgsConstructor @@ -38,27 +39,27 @@ public class ActivitySaveRequest { public Activity toModel(Institute institute, District sidoGungu) { return Activity.builder() - .actId(actId) - .actTitle(actTitle) - .actLocation(actLocation) - .description(description) - .noticeStartDate(noticeStartDate) - .noticeEndDate(noticeEndDate) - .actStartDate(actStartDate) - .actEndDate(actEndDate) - .actStartTime(actStartTime) - .actEndTime(actEndTime) - .recruitTotalNum(recruitTotalNum) - .adultPossible(adultPossible) - .teenPossible(teenPossible) - .groupPossible(groupPossible) - .actWeek(actWeek) - .actManager(actManager) - .actPhone(actPhone) - .url(url) - .category(category) - .institute(institute) - .sidoGungu(sidoGungu) - .build(); + .actId(actId) + .actTitle(actTitle) + .actLocation(actLocation) + .description(description) + .noticeStartDate(noticeStartDate) + .noticeEndDate(noticeEndDate) + .actStartDate(actStartDate) + .actEndDate(actEndDate) + .actStartTime(actStartTime) + .actEndTime(actEndTime) + .recruitTotalNum(recruitTotalNum) + .adultPossible(adultPossible) + .teenPossible(teenPossible) + .groupPossible(groupPossible) + .actWeek(actWeek) + .actManager(actManager) + .actPhone(actPhone) + .url(url) + .category(category) + .institute(institute) + .sidoGungu(sidoGungu) + .build(); } -} +} \ No newline at end of file diff --git a/src/main/java/com/gamsa/avatar/constant/AgeRange.java b/src/main/java/com/gamsa/avatar/constant/AgeRange.java index 9da6952..fd857c3 100644 --- a/src/main/java/com/gamsa/avatar/constant/AgeRange.java +++ b/src/main/java/com/gamsa/avatar/constant/AgeRange.java @@ -1,5 +1,7 @@ package com.gamsa.avatar.constant; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -11,4 +13,19 @@ public enum AgeRange { ADULT("성인"); private final String name; + + @JsonCreator + public static AgeRange fromValue(String value) { + for (AgeRange ageRange : AgeRange.values()) { + if (ageRange.name.equals(value)) { + return ageRange; + } + } + throw new IllegalArgumentException("Unknown value: " + value); + } + + @JsonValue + public String toValue() { + return this.name; + } } diff --git a/src/main/java/com/gamsa/avatar/constant/Experienced.java b/src/main/java/com/gamsa/avatar/constant/Experienced.java index c54cddc..392c006 100644 --- a/src/main/java/com/gamsa/avatar/constant/Experienced.java +++ b/src/main/java/com/gamsa/avatar/constant/Experienced.java @@ -1,5 +1,7 @@ package com.gamsa.avatar.constant; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -11,4 +13,19 @@ public enum Experienced { EXPERT("상급자"); private final String name; + + @JsonCreator + public static Experienced fromValue(String value) { + for (Experienced level : Experienced.values()) { + if (level.name.equals(value)) { + return level; + } + } + throw new IllegalArgumentException("Unknown value: " + value); + } + + @JsonValue + public String toValue() { + return this.name; + } } diff --git a/src/main/java/com/gamsa/avatar/controller/AvatarController.java b/src/main/java/com/gamsa/avatar/controller/AvatarController.java index 9008cbd..ee6c805 100644 --- a/src/main/java/com/gamsa/avatar/controller/AvatarController.java +++ b/src/main/java/com/gamsa/avatar/controller/AvatarController.java @@ -23,18 +23,18 @@ public ResponseEntity saveAvatar(@RequestBody AvatarSaveRequest saveRequ } @GetMapping("{avatar-id}") - public AvatarFindResponse getAvatar(@PathVariable Long avatarId) { + public AvatarFindResponse getAvatar(@PathVariable("avatar-id") Long avatarId) { return avatarService.findById(avatarId); } @PutMapping("{avatar-id}") - public ResponseEntity updateAvatar(@PathVariable Long avatarId, @RequestBody AvatarSaveRequest saveRequest) { + public ResponseEntity updateAvatar(@PathVariable("avatar-id") Long avatarId, @RequestBody AvatarSaveRequest saveRequest) { avatarService.update(avatarId, saveRequest); return new ResponseEntity<>(HttpStatus.OK); } @DeleteMapping("{avatar-Id}") - public ResponseEntity deleteAvatar(@PathVariable Long avatarId) { + public ResponseEntity deleteAvatar(@PathVariable("avatar-id") Long avatarId) { avatarService.delete(avatarId); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } diff --git a/src/main/java/com/gamsa/avatar/dto/AvatarSaveRequest.java b/src/main/java/com/gamsa/avatar/dto/AvatarSaveRequest.java index 3efd859..28af1b5 100644 --- a/src/main/java/com/gamsa/avatar/dto/AvatarSaveRequest.java +++ b/src/main/java/com/gamsa/avatar/dto/AvatarSaveRequest.java @@ -7,16 +7,16 @@ import jakarta.validation.constraints.NotNull; import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.RequiredArgsConstructor; -import java.time.LocalDateTime; - @Getter @Builder @RequiredArgsConstructor +@NoArgsConstructor(force = true) public class AvatarSaveRequest { @NotNull(message = "닉네임은 비어있으면 안됩니다.") - @Max(value=10, message = "닉네임은 최대 10자입니다.") + @Max(value = 10, message = "닉네임은 최대 10자입니다.") private final String nickname; @NotNull(message = "연령대를 선택해야 합니다.") private final AgeRange ageRange; diff --git a/src/main/java/com/gamsa/avatar/service/AvatarService.java b/src/main/java/com/gamsa/avatar/service/AvatarService.java index b55c41a..3eb0193 100644 --- a/src/main/java/com/gamsa/avatar/service/AvatarService.java +++ b/src/main/java/com/gamsa/avatar/service/AvatarService.java @@ -21,7 +21,6 @@ public AvatarFindResponse findById(Long id) { public void save(AvatarSaveRequest saveRequest) { Avatar avatar = saveRequest.toModel(); - avatarRepository.findById(avatar.getAvatarId()).orElseThrow(NoSuchElementException::new); avatarRepository.save(avatar); } diff --git a/src/main/java/com/gamsa/history/controller/HistoryController.java b/src/main/java/com/gamsa/history/controller/HistoryController.java index 1dac62f..3e57205 100644 --- a/src/main/java/com/gamsa/history/controller/HistoryController.java +++ b/src/main/java/com/gamsa/history/controller/HistoryController.java @@ -4,8 +4,10 @@ import com.gamsa.history.dto.HistorySaveRequest; import com.gamsa.history.service.HistoryService; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; +import org.springframework.data.domain.Sort; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -16,6 +18,8 @@ public class HistoryController { private HistoryService historyService; + private static final int MAX_SIZE = Integer.MAX_VALUE; + @PostMapping public ResponseEntity addHistory(@RequestBody HistorySaveRequest saveRequest) { historyService.save(saveRequest); @@ -23,7 +27,16 @@ public ResponseEntity addHistory(@RequestBody HistorySaveRequest saveReq } @GetMapping("{avatar-id}") - public Slice findSliceByUserId(@PathVariable("avatar-id") long avatarId, Pageable pageable) { + public Slice findSliceByUserId(@PathVariable("avatar-id") long avatarId, + @RequestParam(value = "page", required = false) Integer page, + @RequestParam(value = "size", required = false) Integer size) { + Pageable pageable; + + if (page == null || size == null) { + pageable = PageRequest.of(0, MAX_SIZE, Sort.unsorted()); + } else { + pageable = PageRequest.of(page, size, Sort.unsorted()); + } return historyService.findSliceByAvatarId(avatarId, pageable); } From e9b3e9aba224af59f8e07ab3c6f79c1708563091 Mon Sep 17 00:00:00 2001 From: Awhn <69659322+Awhn@users.noreply.github.com> Date: Tue, 29 Oct 2024 13:34:44 +0900 Subject: [PATCH 08/17] =?UTF-8?q?=20fix:=20=EC=9A=94=EA=B5=AC=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EB=B3=80=EA=B2=BD=EC=97=90=20=EB=94=B0=EB=A5=B8=20?= =?UTF-8?q?=EC=84=A0=ED=83=9D=EC=A0=81=20=EB=AC=B4=ED=95=9C=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A1=A4=20=20(#43)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 유저 기록 기능 구현 * test: 유저 기록 기능 테스트 * fix: activity 관련 의존성 수정 * fix: 요구사항 변경에 따른 선택적 무한 스크롤 --- .../java/com/gamsa/history/controller/HistoryController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/gamsa/history/controller/HistoryController.java b/src/main/java/com/gamsa/history/controller/HistoryController.java index 3e57205..3d3ace9 100644 --- a/src/main/java/com/gamsa/history/controller/HistoryController.java +++ b/src/main/java/com/gamsa/history/controller/HistoryController.java @@ -19,7 +19,7 @@ public class HistoryController { private HistoryService historyService; private static final int MAX_SIZE = Integer.MAX_VALUE; - + @PostMapping public ResponseEntity addHistory(@RequestBody HistorySaveRequest saveRequest) { historyService.save(saveRequest); From 6a4687fe8e152e4d178af46e17a5b6f9a2a941fe Mon Sep 17 00:00:00 2001 From: 5win <94297900+5win@users.noreply.github.com> Date: Tue, 29 Oct 2024 23:46:45 +0900 Subject: [PATCH 09/17] =?UTF-8?q?infra:=20=EB=B0=B0=ED=8F=AC=20CI=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=B4=ED=94=84=EB=9D=BC=EC=9D=B8=20(#48)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [#37] infra: 배포 CI 파이프라인 * [#37] fix: 브랜치 및 오류 수정 --- .github/workflows/prod-ci.yml | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .github/workflows/prod-ci.yml diff --git a/.github/workflows/prod-ci.yml b/.github/workflows/prod-ci.yml new file mode 100644 index 0000000..5d4dfc3 --- /dev/null +++ b/.github/workflows/prod-ci.yml @@ -0,0 +1,34 @@ +name: Prod-CI + +on: + pull_request: + branches: [ "master" ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582 # v4.0.0 + + - name: Build with Gradle Wrapper + run: ./gradlew build + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ap-northeast-2 + + run: | + aws s3 sync build s3://gamja-bongsa \ No newline at end of file From f0045bf98f6cefb4c2da72d1a917826851015eb6 Mon Sep 17 00:00:00 2001 From: 5win <94297900+5win@users.noreply.github.com> Date: Wed, 30 Oct 2024 19:25:42 +0900 Subject: [PATCH 10/17] =?UTF-8?q?feat:=20=EC=B9=B4=EC=B9=B4=EC=98=A4=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EA=B5=AC=ED=98=84=20=EB=B0=8F=20?= =?UTF-8?q?=EC=95=84=EB=B0=94=ED=83=80,=20=ED=9E=88=EC=8A=A4=ED=86=A0?= =?UTF-8?q?=EB=A6=AC=EC=99=80=20=EC=97=B0=EB=8F=99=20(#45)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [#19] feat: 유저 카카오 회원가입/로그인 구현 - JWT + Interceptor를 사용하여 구현 * [#19] feat: 유저, 아바타 연동 - userId로 아바타 생성 기능 추가. - user와 avatar 1:1 연관관계 매핑. * [#19] fix: 테스트용 로깅 제거 * [#19] feat: 로그인 시, 아바타 유무 응답 반환 - 헤더의 token : JWT - 바디의 avatarExists: true/false * [#19] refact: 아바타 URI 변경 - /api/v1/avatar -> /api/v1/avatars * [#19] refact: kakao-token 바디에서 헤더로 변경 - @RequesetBody -> @RequestHeader 로 수정 * [#19] refact: 유저 도메인과 영속성 엔티티 분리 - User 클래스 생성 * [#19] test: 유저 테스트 추가 및 다른 테스트코드 수정 - 유저 단위테스트 추가 - 다른 테스트 충돌 부분 해결 * [#19] fix: history URI 수정 및 불필요한 test 삭제 - /api/v1/history -> /api/v1/histories - ApplicationTest.java 삭제 * [#19] feat: User, Avatar, History 연동 및 수정 - INTERMEDIATE 오타 수정 - 인터셉터 인증 경로에 /api/v1/histories 추가 - NoSuchElement, IllegalArgument 예외 핸들러 추가 - History 관련 QueryDsl 수정 - JWT 파싱 후, 아바타 조회한 뒤 해당 아바타ID로 history 조회하도록 변경 * [#19] fix: History 생성 API의 avatarId 제거 - JWT 인증 토큰을 통해 userId를 받으므로 제거함 * [#19] refact: JWT userId 추출 로직 공통화 - common.utils.ExtractUserIdFromJwt.java * [#19] feat: 아바타 존재시 아바타 정보 반환 - body에 아바타 객체 정보 담아서 반환. - 없으면 null * [#19] test: 테스트 코드 수정 --- build.gradle | 6 +- src/main/java/com/gamsa/Application.java | 2 + .../java/com/gamsa/auth/SecurityConfig.java | 27 ---- .../gamsa/avatar/constant/Experienced.java | 2 +- .../avatar/controller/AvatarController.java | 42 ++++-- .../java/com/gamsa/avatar/domain/Avatar.java | 2 + .../gamsa/avatar/dto/AvatarFindResponse.java | 18 ++- .../gamsa/avatar/dto/AvatarSaveRequest.java | 16 ++- .../gamsa/avatar/entity/AvatarJpaEntity.java | 55 +++++--- .../repository/AvatarJpaRepository.java | 8 +- .../avatar/repository/AvatarRepository.java | 5 +- .../repository/AvatarRepositoryImpl.java | 15 ++- .../gamsa/avatar/service/AvatarService.java | 41 ++++-- .../com/gamsa/common/config/WebConfig.java | 27 ++++ .../exception/GlobalExceptionHandler.java | 18 +++ .../exception/RestClientErrorHandler.java | 27 ++++ .../common/interceptor/JwtInterceptor.java | 51 +++++++ .../java/com/gamsa/common/jwt/JwtUtil.java | 63 +++++++++ .../common/utils/ExtractUserIdFromJwt.java | 11 ++ .../history/controller/HistoryController.java | 39 ++++-- .../gamsa/history/dto/HistorySaveRequest.java | 1 - .../HistoryCustomRepositoryImpl.java | 14 +- .../gamsa/history/service/HistoryService.java | 23 ++-- .../gamsa/user/controller/UserController.java | 30 +++++ .../com/gamsa/user/domain/KakaoLogin.java | 29 ++++ src/main/java/com/gamsa/user/domain/User.java | 12 ++ .../gamsa/user/dto/KakaoLoginResponse.java | 12 ++ .../com/gamsa/user/dto/KakaoProperties.java | 13 ++ .../gamsa/user/dto/KakaoUserInfoResponse.java | 34 +++++ .../com/gamsa/user/entity/UserJpaEntity.java | 42 ++++++ .../user/exception/KakaoApiErrorCode.java | 20 +++ .../user/exception/KakaoApiException.java | 11 ++ .../KakaoAccessTokenRepository.java | 20 +++ .../user/repository/UserJpaRepository.java | 8 ++ .../gamsa/user/repository/UserRepository.java | 12 ++ .../user/repository/UserRepositoryImpl.java | 25 ++++ .../com/gamsa/user/service/UserService.java | 57 ++++++++ src/main/resources/application-dev.yml | 3 + src/main/resources/application.yml | 3 + src/test/java/com/gamsa/ApplicationTests.java | 13 -- .../avatar/entity/AvatarJpaEntityTest.java | 40 ++++-- .../avatar/service/AvatarServiceTest.java | 45 +++++-- .../avatar/stub/StubAvatarRepository.java | 31 ----- .../stub/StubEmptyAvatarRepository.java | 33 +++++ .../stub/StubExistsAvatarRepository.java | 47 +++++++ .../history/entity/HistoryJpaEntityTest.java | 124 +++++++++--------- .../repository/HistoryJpaRepositoryTest.java | 114 ++++++++-------- .../history/service/HitstoryServiceTest.java | 23 ++-- .../gamsa/user/service/UserServiceTest.java | 58 ++++++++ .../stub/DummyKakaoAccessTokenRepository.java | 17 +++ .../com/gamsa/user/stub/DummyKakaoLogin.java | 17 +++ .../user/stub/StubExistsUserRepository.java | 23 ++++ 52 files changed, 1113 insertions(+), 316 deletions(-) delete mode 100644 src/main/java/com/gamsa/auth/SecurityConfig.java create mode 100644 src/main/java/com/gamsa/common/config/WebConfig.java create mode 100644 src/main/java/com/gamsa/common/exception/RestClientErrorHandler.java create mode 100644 src/main/java/com/gamsa/common/interceptor/JwtInterceptor.java create mode 100644 src/main/java/com/gamsa/common/jwt/JwtUtil.java create mode 100644 src/main/java/com/gamsa/common/utils/ExtractUserIdFromJwt.java create mode 100644 src/main/java/com/gamsa/user/controller/UserController.java create mode 100644 src/main/java/com/gamsa/user/domain/KakaoLogin.java create mode 100644 src/main/java/com/gamsa/user/domain/User.java create mode 100644 src/main/java/com/gamsa/user/dto/KakaoLoginResponse.java create mode 100644 src/main/java/com/gamsa/user/dto/KakaoProperties.java create mode 100644 src/main/java/com/gamsa/user/dto/KakaoUserInfoResponse.java create mode 100644 src/main/java/com/gamsa/user/entity/UserJpaEntity.java create mode 100644 src/main/java/com/gamsa/user/exception/KakaoApiErrorCode.java create mode 100644 src/main/java/com/gamsa/user/exception/KakaoApiException.java create mode 100644 src/main/java/com/gamsa/user/repository/KakaoAccessTokenRepository.java create mode 100644 src/main/java/com/gamsa/user/repository/UserJpaRepository.java create mode 100644 src/main/java/com/gamsa/user/repository/UserRepository.java create mode 100644 src/main/java/com/gamsa/user/repository/UserRepositoryImpl.java create mode 100644 src/main/java/com/gamsa/user/service/UserService.java delete mode 100644 src/test/java/com/gamsa/ApplicationTests.java delete mode 100644 src/test/java/com/gamsa/avatar/stub/StubAvatarRepository.java create mode 100644 src/test/java/com/gamsa/avatar/stub/StubEmptyAvatarRepository.java create mode 100644 src/test/java/com/gamsa/avatar/stub/StubExistsAvatarRepository.java create mode 100644 src/test/java/com/gamsa/user/service/UserServiceTest.java create mode 100644 src/test/java/com/gamsa/user/stub/DummyKakaoAccessTokenRepository.java create mode 100644 src/test/java/com/gamsa/user/stub/DummyKakaoLogin.java create mode 100644 src/test/java/com/gamsa/user/stub/StubExistsUserRepository.java diff --git a/build.gradle b/build.gradle index 7d091ce..18efdfa 100644 --- a/build.gradle +++ b/build.gradle @@ -24,7 +24,6 @@ repositories { } dependencies { - implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' @@ -34,6 +33,11 @@ dependencies { annotationProcessor 'jakarta.persistence:jakarta.persistence-api' implementation 'org.hibernate.validator:hibernate-validator' + // jwt + implementation 'io.jsonwebtoken:jjwt-api:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' + compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' annotationProcessor 'org.projectlombok:lombok' diff --git a/src/main/java/com/gamsa/Application.java b/src/main/java/com/gamsa/Application.java index df58f1c..1ac8a46 100644 --- a/src/main/java/com/gamsa/Application.java +++ b/src/main/java/com/gamsa/Application.java @@ -2,8 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.ConfigurationPropertiesScan; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +@ConfigurationPropertiesScan @EnableJpaAuditing @SpringBootApplication public class Application { diff --git a/src/main/java/com/gamsa/auth/SecurityConfig.java b/src/main/java/com/gamsa/auth/SecurityConfig.java deleted file mode 100644 index 0898128..0000000 --- a/src/main/java/com/gamsa/auth/SecurityConfig.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.gamsa.auth; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; -import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; -import org.springframework.security.web.SecurityFilterChain; - -@Configuration -@EnableWebSecurity -public class SecurityConfig { - - @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - http - .csrf(AbstractHttpConfigurer::disable) - .headers(headerConfig -> headerConfig.frameOptions( - HeadersConfigurer.FrameOptionsConfig::sameOrigin)) - .authorizeHttpRequests(authorizeRequest -> { - authorizeRequest - .anyRequest().permitAll(); - }); - return http.build(); - } -} diff --git a/src/main/java/com/gamsa/avatar/constant/Experienced.java b/src/main/java/com/gamsa/avatar/constant/Experienced.java index 392c006..3e6133d 100644 --- a/src/main/java/com/gamsa/avatar/constant/Experienced.java +++ b/src/main/java/com/gamsa/avatar/constant/Experienced.java @@ -9,7 +9,7 @@ @RequiredArgsConstructor public enum Experienced { NOVICE("초심자"), - INTERMIDIATE("중급자"), + INTERMEDIATE("중급자"), EXPERT("상급자"); private final String name; diff --git a/src/main/java/com/gamsa/avatar/controller/AvatarController.java b/src/main/java/com/gamsa/avatar/controller/AvatarController.java index ee6c805..d95105c 100644 --- a/src/main/java/com/gamsa/avatar/controller/AvatarController.java +++ b/src/main/java/com/gamsa/avatar/controller/AvatarController.java @@ -4,38 +4,54 @@ import com.gamsa.avatar.dto.AvatarFindResponse; import com.gamsa.avatar.dto.AvatarSaveRequest; import com.gamsa.avatar.service.AvatarService; +import com.gamsa.common.utils.ExtractUserIdFromJwt; +import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +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.RestController; @RequiredArgsConstructor @RestController -@RequestMapping("/api/v1/avatar") +@RequestMapping("/api/v1/avatars") public class AvatarController { private final AvatarService avatarService; @PostMapping - public ResponseEntity saveAvatar(@RequestBody AvatarSaveRequest saveRequest) { - avatarService.save(saveRequest); + public ResponseEntity saveAvatar(@RequestBody AvatarSaveRequest saveRequest, + HttpServletRequest request) { + + Long userId = ExtractUserIdFromJwt.extract(request); + avatarService.save(saveRequest, userId); return new ResponseEntity<>(HttpStatus.CREATED); } - @GetMapping("{avatar-id}") - public AvatarFindResponse getAvatar(@PathVariable("avatar-id") Long avatarId) { - return avatarService.findById(avatarId); + @GetMapping + public AvatarFindResponse getAvatar(HttpServletRequest request) { + + Long userId = ExtractUserIdFromJwt.extract(request); + return avatarService.findByUserId(userId); } - @PutMapping("{avatar-id}") - public ResponseEntity updateAvatar(@PathVariable("avatar-id") Long avatarId, @RequestBody AvatarSaveRequest saveRequest) { - avatarService.update(avatarId, saveRequest); + @PutMapping + public ResponseEntity updateAvatar(@RequestBody AvatarSaveRequest saveRequest, + HttpServletRequest request) { + Long userId = ExtractUserIdFromJwt.extract(request); + avatarService.update(saveRequest, userId); return new ResponseEntity<>(HttpStatus.OK); } - @DeleteMapping("{avatar-Id}") - public ResponseEntity deleteAvatar(@PathVariable("avatar-id") Long avatarId) { - avatarService.delete(avatarId); + @DeleteMapping + public ResponseEntity deleteAvatar(HttpServletRequest request) { + Long userId = ExtractUserIdFromJwt.extract(request); + avatarService.delete(userId); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } } diff --git a/src/main/java/com/gamsa/avatar/domain/Avatar.java b/src/main/java/com/gamsa/avatar/domain/Avatar.java index 9242853..faebb7b 100644 --- a/src/main/java/com/gamsa/avatar/domain/Avatar.java +++ b/src/main/java/com/gamsa/avatar/domain/Avatar.java @@ -2,6 +2,7 @@ import com.gamsa.avatar.constant.AgeRange; import com.gamsa.avatar.constant.Experienced; +import com.gamsa.user.domain.User; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -11,6 +12,7 @@ @AllArgsConstructor public class Avatar { private Long avatarId; + private User user; private Long avatarExp; private Long avatarLevel; private String nickname; diff --git a/src/main/java/com/gamsa/avatar/dto/AvatarFindResponse.java b/src/main/java/com/gamsa/avatar/dto/AvatarFindResponse.java index f016524..5717ad1 100644 --- a/src/main/java/com/gamsa/avatar/dto/AvatarFindResponse.java +++ b/src/main/java/com/gamsa/avatar/dto/AvatarFindResponse.java @@ -7,8 +7,6 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; -import java.time.LocalDateTime; - @Getter @Builder @RequiredArgsConstructor @@ -18,16 +16,16 @@ public class AvatarFindResponse { private final Long avatarLevel; private final String nickName; private final AgeRange ageRange; - private final Experienced exprienced; + private final Experienced experienced; public static AvatarFindResponse from(Avatar avatar) { return AvatarFindResponse.builder() - .avatarId(avatar.getAvatarId()) - .avatarExp(avatar.getAvatarExp()) - .avatarLevel(avatar.getAvatarLevel()) - .nickName(avatar.getNickname()) - .ageRange(avatar.getAgeRange()) - .exprienced(avatar.getExperienced()) - .build(); + .avatarId(avatar.getAvatarId()) + .avatarExp(avatar.getAvatarExp()) + .avatarLevel(avatar.getAvatarLevel()) + .nickName(avatar.getNickname()) + .ageRange(avatar.getAgeRange()) + .experienced(avatar.getExperienced()) + .build(); } } diff --git a/src/main/java/com/gamsa/avatar/dto/AvatarSaveRequest.java b/src/main/java/com/gamsa/avatar/dto/AvatarSaveRequest.java index 28af1b5..c5e95ef 100644 --- a/src/main/java/com/gamsa/avatar/dto/AvatarSaveRequest.java +++ b/src/main/java/com/gamsa/avatar/dto/AvatarSaveRequest.java @@ -3,6 +3,7 @@ import com.gamsa.avatar.constant.AgeRange; import com.gamsa.avatar.constant.Experienced; import com.gamsa.avatar.domain.Avatar; +import com.gamsa.user.domain.User; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.NotNull; import lombok.Builder; @@ -23,13 +24,14 @@ public class AvatarSaveRequest { @NotNull(message = "봉사 활동 경험을 선택해야 합니다.") private final Experienced experienced; - public Avatar toModel() { + public Avatar toModel(User user) { return Avatar.builder() - .nickname(nickname) - .avatarExp(0L) - .avatarLevel(0L) - .ageRange(ageRange) - .experienced(experienced) - .build(); + .user(user) + .nickname(nickname) + .avatarExp(0L) + .avatarLevel(0L) + .ageRange(ageRange) + .experienced(experienced) + .build(); } } diff --git a/src/main/java/com/gamsa/avatar/entity/AvatarJpaEntity.java b/src/main/java/com/gamsa/avatar/entity/AvatarJpaEntity.java index ca9d4ec..209840f 100644 --- a/src/main/java/com/gamsa/avatar/entity/AvatarJpaEntity.java +++ b/src/main/java/com/gamsa/avatar/entity/AvatarJpaEntity.java @@ -6,8 +6,21 @@ import com.gamsa.avatar.constant.ExperiencedConverter; import com.gamsa.avatar.domain.Avatar; import com.gamsa.common.entity.BaseEntity; -import jakarta.persistence.*; -import lombok.*; +import com.gamsa.user.entity.UserJpaEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; @Getter @Setter @@ -18,10 +31,14 @@ @NoArgsConstructor public class AvatarJpaEntity extends BaseEntity { @Id - @GeneratedValue - @Column(name = "id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "avatar_id") private Long avatarId; + @OneToOne + @JoinColumn(name = "user_id", unique = true) + private UserJpaEntity user; + @Column(name = "avatar_exp") private Long avatarExp; @@ -41,23 +58,25 @@ public class AvatarJpaEntity extends BaseEntity { public static AvatarJpaEntity from(Avatar avatar) { return AvatarJpaEntity.builder() - .avatarId(avatar.getAvatarId()) - .avatarExp(avatar.getAvatarExp()) - .avatarLevel(avatar.getAvatarLevel()) - .nickname(avatar.getNickname()) - .ageRange(avatar.getAgeRange()) - .experienced(avatar.getExperienced()) - .build(); + .avatarId(avatar.getAvatarId()) + .user(UserJpaEntity.from(avatar.getUser())) + .avatarExp(avatar.getAvatarExp()) + .avatarLevel(avatar.getAvatarLevel()) + .nickname(avatar.getNickname()) + .ageRange(avatar.getAgeRange()) + .experienced(avatar.getExperienced()) + .build(); } public Avatar toModel() { return Avatar.builder() - .avatarId(avatarId) - .avatarExp(avatarExp) - .avatarLevel(avatarLevel) - .nickname(nickname) - .ageRange(ageRange) - .experienced(experienced) - .build(); + .avatarId(avatarId) + .user(user.toModel()) + .avatarExp(avatarExp) + .avatarLevel(avatarLevel) + .nickname(nickname) + .ageRange(ageRange) + .experienced(experienced) + .build(); } } diff --git a/src/main/java/com/gamsa/avatar/repository/AvatarJpaRepository.java b/src/main/java/com/gamsa/avatar/repository/AvatarJpaRepository.java index 08a8155..365a29c 100644 --- a/src/main/java/com/gamsa/avatar/repository/AvatarJpaRepository.java +++ b/src/main/java/com/gamsa/avatar/repository/AvatarJpaRepository.java @@ -1,12 +1,14 @@ package com.gamsa.avatar.repository; -import com.gamsa.avatar.domain.Avatar; import com.gamsa.avatar.entity.AvatarJpaEntity; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; -import java.util.Optional; - @Repository public interface AvatarJpaRepository extends JpaRepository { + + Optional findByUserId(Long userId); + + Optional findByNickname(String nickname); } diff --git a/src/main/java/com/gamsa/avatar/repository/AvatarRepository.java b/src/main/java/com/gamsa/avatar/repository/AvatarRepository.java index fc6a8eb..aa8cf73 100644 --- a/src/main/java/com/gamsa/avatar/repository/AvatarRepository.java +++ b/src/main/java/com/gamsa/avatar/repository/AvatarRepository.java @@ -1,7 +1,6 @@ package com.gamsa.avatar.repository; import com.gamsa.avatar.domain.Avatar; - import java.util.Optional; public interface AvatarRepository { @@ -9,5 +8,9 @@ public interface AvatarRepository { Optional findById(Long id); + Optional findByUserId(Long userId); + + Optional findByNickname(String nickname); + void deleteById(Long id); } diff --git a/src/main/java/com/gamsa/avatar/repository/AvatarRepositoryImpl.java b/src/main/java/com/gamsa/avatar/repository/AvatarRepositoryImpl.java index bb75b85..4031d03 100644 --- a/src/main/java/com/gamsa/avatar/repository/AvatarRepositoryImpl.java +++ b/src/main/java/com/gamsa/avatar/repository/AvatarRepositoryImpl.java @@ -2,11 +2,10 @@ import com.gamsa.avatar.domain.Avatar; import com.gamsa.avatar.entity.AvatarJpaEntity; +import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; -import java.util.Optional; - @RequiredArgsConstructor @Repository public class AvatarRepositoryImpl implements AvatarRepository { @@ -22,6 +21,18 @@ public Optional findById(Long id) { return avatarJpaRepository.findById(id).map(AvatarJpaEntity::toModel); } + @Override + public Optional findByUserId(Long userId) { + return avatarJpaRepository.findByUserId(userId) + .map(AvatarJpaEntity::toModel); + } + + @Override + public Optional findByNickname(String nickname) { + return avatarJpaRepository.findByNickname(nickname) + .map(AvatarJpaEntity::toModel); + } + @Override public void deleteById(Long id) { avatarJpaRepository.deleteById(id); diff --git a/src/main/java/com/gamsa/avatar/service/AvatarService.java b/src/main/java/com/gamsa/avatar/service/AvatarService.java index 3eb0193..503ca7c 100644 --- a/src/main/java/com/gamsa/avatar/service/AvatarService.java +++ b/src/main/java/com/gamsa/avatar/service/AvatarService.java @@ -4,40 +4,53 @@ import com.gamsa.avatar.dto.AvatarFindResponse; import com.gamsa.avatar.dto.AvatarSaveRequest; import com.gamsa.avatar.repository.AvatarRepository; +import com.gamsa.user.domain.User; +import com.gamsa.user.repository.UserRepository; +import java.util.NoSuchElementException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import java.util.NoSuchElementException; - @RequiredArgsConstructor @Service public class AvatarService { private final AvatarRepository avatarRepository; + private final UserRepository userRepository; - public AvatarFindResponse findById(Long id) { - Avatar avatar = avatarRepository.findById(id).orElseThrow(NoSuchElementException::new); + public AvatarFindResponse findByUserId(Long userId) { + Avatar avatar = avatarRepository.findByUserId(userId) + .orElseThrow(NoSuchElementException::new); return AvatarFindResponse.from(avatar); } - public void save(AvatarSaveRequest saveRequest) { - Avatar avatar = saveRequest.toModel(); - avatarRepository.save(avatar); + public void save(AvatarSaveRequest saveRequest, Long userId) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new NoSuchElementException("존재하지 않는 유저.")); + + Avatar newAvatar = saveRequest.toModel(user); + avatarRepository.findByNickname(newAvatar.getNickname()) + .ifPresent(avatar -> { + throw new IllegalArgumentException("이미 존재하는 닉네임"); + }); + avatarRepository.save(newAvatar); } - public void delete(Long id) { - avatarRepository.findById(id).orElseThrow(NoSuchElementException::new); - avatarRepository.deleteById(id); + public void delete(Long userId) { + Avatar avatar = avatarRepository.findByUserId(userId) + .orElseThrow(NoSuchElementException::new); + avatarRepository.deleteById(avatar.getAvatarId()); } - public AvatarFindResponse expUp(Long avatarId, int amount) { - Avatar avatar = avatarRepository.findById(avatarId).orElseThrow(NoSuchElementException::new); + public AvatarFindResponse expUp(Long userId, int amount) { + Avatar avatar = avatarRepository.findByUserId(userId) + .orElseThrow(NoSuchElementException::new); avatar.expUp(amount); avatarRepository.save(avatar); return AvatarFindResponse.from(avatar); } - public AvatarFindResponse update(Long avatarId, AvatarSaveRequest saveRequest) { - Avatar avatar = avatarRepository.findById(avatarId).orElseThrow(NoSuchElementException::new); + public AvatarFindResponse update(AvatarSaveRequest saveRequest, Long userId) { + Avatar avatar = avatarRepository.findByUserId(userId) + .orElseThrow(NoSuchElementException::new); avatar.changeAgeRange(saveRequest.getAgeRange()); avatar.changeExperience(saveRequest.getExperienced()); avatar.changeNickname(saveRequest.getNickname()); diff --git a/src/main/java/com/gamsa/common/config/WebConfig.java b/src/main/java/com/gamsa/common/config/WebConfig.java new file mode 100644 index 0000000..5b87dee --- /dev/null +++ b/src/main/java/com/gamsa/common/config/WebConfig.java @@ -0,0 +1,27 @@ +package com.gamsa.common.config; + +import com.gamsa.common.interceptor.JwtInterceptor; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@RequiredArgsConstructor +@Configuration +public class WebConfig implements WebMvcConfigurer { + + private final JwtInterceptor jwtInterceptor; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(jwtInterceptor) + .addPathPatterns("/api/v1/avatars/**") + .addPathPatterns("/api/v1/histories/**"); + } + + @Override + public void addCorsMappings(CorsRegistry registry) { + WebMvcConfigurer.super.addCorsMappings(registry); + } +} diff --git a/src/main/java/com/gamsa/common/exception/GlobalExceptionHandler.java b/src/main/java/com/gamsa/common/exception/GlobalExceptionHandler.java index 8c71a3d..845f381 100644 --- a/src/main/java/com/gamsa/common/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/gamsa/common/exception/GlobalExceptionHandler.java @@ -1,6 +1,8 @@ package com.gamsa.common.exception; import com.gamsa.activity.exception.ActivityException; +import com.gamsa.user.exception.KakaoApiException; +import java.util.NoSuchElementException; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -17,4 +19,20 @@ private ResponseEntity ActivityCustomExceptionHandler(ActivityException e) { .status(e.getActivityErrorCode().getStatus()) .body(e.getActivityErrorCode().getMsg()); } + + @ExceptionHandler(KakaoApiException.class) + private ResponseEntity kakaoApiExceptionHandler(KakaoApiException e) { + log.error(String.valueOf(e.getStackTrace()[0])); + return ResponseEntity + .status(e.getKakaoAPIErrorCode().getStatus()) + .body(e.getKakaoAPIErrorCode().getMsg()); + } + + @ExceptionHandler(value = {NoSuchElementException.class, IllegalArgumentException.class}) + private ResponseEntity noSuchElementExceptionHandler(Exception e) { + log.error(String.valueOf(e.getStackTrace()[0])); + return ResponseEntity + .badRequest() + .body(e.getMessage()); + } } diff --git a/src/main/java/com/gamsa/common/exception/RestClientErrorHandler.java b/src/main/java/com/gamsa/common/exception/RestClientErrorHandler.java new file mode 100644 index 0000000..00675e2 --- /dev/null +++ b/src/main/java/com/gamsa/common/exception/RestClientErrorHandler.java @@ -0,0 +1,27 @@ +package com.gamsa.common.exception; + +import com.gamsa.user.exception.KakaoApiErrorCode; +import com.gamsa.user.exception.KakaoApiException; +import org.springframework.web.client.RestClient.ResponseSpec.ErrorHandler; + +public class RestClientErrorHandler { + + public static ErrorHandler http4xxErrorHandler = (request, response) -> { + switch (response.getStatusCode().value()) { + case 400: + throw new KakaoApiException(KakaoApiErrorCode.KAKAO_API_BAD_REQUEST); + case 401: + throw new KakaoApiException(KakaoApiErrorCode.KAKAO_API_UNAUTHORIZED); + case 403: + throw new KakaoApiException(KakaoApiErrorCode.KAKAO_API_FORBIDDEN); + } + }; + + public static ErrorHandler http5xxErrorHandler = (request, response) -> { + switch (response.getStatusCode().value()) { + case 500: + throw new KakaoApiException(KakaoApiErrorCode.KAKAO_API_INTERNAL_SERVER_ERROR); + } + }; + +} diff --git a/src/main/java/com/gamsa/common/interceptor/JwtInterceptor.java b/src/main/java/com/gamsa/common/interceptor/JwtInterceptor.java new file mode 100644 index 0000000..f0e6489 --- /dev/null +++ b/src/main/java/com/gamsa/common/interceptor/JwtInterceptor.java @@ -0,0 +1,51 @@ +package com.gamsa.common.interceptor; + +import com.gamsa.common.jwt.JwtUtil; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +@Slf4j +@RequiredArgsConstructor +@Component +public class JwtInterceptor implements HandlerInterceptor { + + private final JwtUtil jwtUtil; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, + Object handler) throws Exception { + + String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION); + String token = null; + + if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { + token = authorizationHeader.substring(7); + } else { + unauthorizedResponse(response); + return false; + } + + try { + Long userId = jwtUtil.getUserId(token); + request.setAttribute("userId", userId); + } catch (Exception e) { + log.warn(e.getMessage()); + unauthorizedResponse(response); + return false; + } + + return true; + } + + private void unauthorizedResponse(HttpServletResponse response) throws IOException { + response.setCharacterEncoding("UTF-8"); + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + } +} diff --git a/src/main/java/com/gamsa/common/jwt/JwtUtil.java b/src/main/java/com/gamsa/common/jwt/JwtUtil.java new file mode 100644 index 0000000..7f1646b --- /dev/null +++ b/src/main/java/com/gamsa/common/jwt/JwtUtil.java @@ -0,0 +1,63 @@ +package com.gamsa.common.jwt; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.Jwts.SIG; +import java.nio.charset.StandardCharsets; +import java.util.Date; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class JwtUtil { + + private final SecretKey secretKey; + + /** + * secret과 알고리즘을 저장하여 Secretkey에 저장 + */ + public JwtUtil(@Value("${spring.jwt.secret}") String secret) { + this.secretKey = new SecretKeySpec( + secret.getBytes(StandardCharsets.UTF_8), + SIG.HS256.key().build().getAlgorithm() + ); + } + + /** + * 토큰을 통해 payload 정보 반환 + */ + public Long getUserId(String token) { + return validateJwt(token) + .getPayload() + .get("userId", Long.class); + } + + /** + * 토큰을 통해 만료가 되었는지 확인 + */ + public boolean isExpired(String token) { + return validateJwt(token) + .getPayload() + .getExpiration() + .before(new Date()); + } + + public String createJwt(Long userId, long expirationMs) { + return Jwts.builder() + .claim("userId", userId) + .issuedAt(new Date()) + .expiration(new Date(System.currentTimeMillis() + expirationMs)) + .signWith(secretKey) + .compact(); + } + + private Jws validateJwt(String token) { + return Jwts.parser() + .verifyWith(secretKey) + .build() + .parseSignedClaims(token); + } +} diff --git a/src/main/java/com/gamsa/common/utils/ExtractUserIdFromJwt.java b/src/main/java/com/gamsa/common/utils/ExtractUserIdFromJwt.java new file mode 100644 index 0000000..5d470e5 --- /dev/null +++ b/src/main/java/com/gamsa/common/utils/ExtractUserIdFromJwt.java @@ -0,0 +1,11 @@ +package com.gamsa.common.utils; + +import jakarta.servlet.http.HttpServletRequest; + +public class ExtractUserIdFromJwt { + + public static Long extract(HttpServletRequest request) { + return (Long) request.getAttribute("userId"); + } + +} diff --git a/src/main/java/com/gamsa/history/controller/HistoryController.java b/src/main/java/com/gamsa/history/controller/HistoryController.java index 3d3ace9..f5ba140 100644 --- a/src/main/java/com/gamsa/history/controller/HistoryController.java +++ b/src/main/java/com/gamsa/history/controller/HistoryController.java @@ -1,8 +1,10 @@ package com.gamsa.history.controller; +import com.gamsa.common.utils.ExtractUserIdFromJwt; import com.gamsa.history.dto.HistoryFindSliceResponse; import com.gamsa.history.dto.HistorySaveRequest; import com.gamsa.history.service.HistoryService; +import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -10,26 +12,41 @@ import org.springframework.data.domain.Sort; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.DeleteMapping; +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.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; @RequiredArgsConstructor @RestController -@RequestMapping("/api/v1/history") +@RequestMapping("/api/v1/histories") public class HistoryController { - private HistoryService historyService; - private static final int MAX_SIZE = Integer.MAX_VALUE; + private final HistoryService historyService; + + private static final int MAX_SIZE = Integer.MAX_VALUE - 1; @PostMapping - public ResponseEntity addHistory(@RequestBody HistorySaveRequest saveRequest) { - historyService.save(saveRequest); + public ResponseEntity addHistory(@RequestBody HistorySaveRequest saveRequest, + HttpServletRequest request) { + + Long userId = ExtractUserIdFromJwt.extract(request); + historyService.save(saveRequest, userId); return new ResponseEntity<>(HttpStatus.CREATED); } - @GetMapping("{avatar-id}") - public Slice findSliceByUserId(@PathVariable("avatar-id") long avatarId, - @RequestParam(value = "page", required = false) Integer page, - @RequestParam(value = "size", required = false) Integer size) { + @GetMapping + public Slice findSliceByUserId( + @RequestParam(value = "page", required = false) Integer page, + @RequestParam(value = "size", required = false) Integer size, + HttpServletRequest request) { + + Long userId = ExtractUserIdFromJwt.extract(request); + Pageable pageable; if (page == null || size == null) { @@ -37,7 +54,7 @@ public Slice findSliceByUserId(@PathVariable("avatar-i } else { pageable = PageRequest.of(page, size, Sort.unsorted()); } - return historyService.findSliceByAvatarId(avatarId, pageable); + return historyService.findSliceByAvatarId(userId, pageable); } @DeleteMapping("{history-id}") diff --git a/src/main/java/com/gamsa/history/dto/HistorySaveRequest.java b/src/main/java/com/gamsa/history/dto/HistorySaveRequest.java index b1f5964..5c83bd2 100644 --- a/src/main/java/com/gamsa/history/dto/HistorySaveRequest.java +++ b/src/main/java/com/gamsa/history/dto/HistorySaveRequest.java @@ -12,7 +12,6 @@ @Builder @RequiredArgsConstructor public class HistorySaveRequest { - private final long avatarId; private final long actId; public History toModel(Avatar avatar, Activity activity) { diff --git a/src/main/java/com/gamsa/history/repository/HistoryCustomRepositoryImpl.java b/src/main/java/com/gamsa/history/repository/HistoryCustomRepositoryImpl.java index 012feed..9ee3a9e 100644 --- a/src/main/java/com/gamsa/history/repository/HistoryCustomRepositoryImpl.java +++ b/src/main/java/com/gamsa/history/repository/HistoryCustomRepositoryImpl.java @@ -1,20 +1,19 @@ package com.gamsa.history.repository; +import static com.gamsa.history.entity.QHistoryJpaEntity.historyJpaEntity; + import com.gamsa.history.entity.HistoryJpaEntity; import com.querydsl.core.types.Order; import com.querydsl.core.types.OrderSpecifier; import com.querydsl.core.types.dsl.PathBuilder; import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.ArrayList; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.domain.SliceImpl; -import java.util.ArrayList; -import java.util.List; - -import static com.gamsa.history.entity.QHistoryJpaEntity.historyJpaEntity; - @RequiredArgsConstructor public class HistoryCustomRepositoryImpl implements HistoryCustomRepository { private final JPAQueryFactory jpaQueryFactory; @@ -28,7 +27,7 @@ public Slice findSliceByAvatarId(long avatarId, Pageable pagea .where(historyJpaEntity.avatar.avatarId.eq(avatarId)) .orderBy(orders.toArray(OrderSpecifier[]::new)) .offset(pageable.getOffset()) - .limit(pageable.getOffset() + 1) + .limit(pageable.getPageSize() + 1) .fetch(); return checkLastPage(pageable, results); @@ -50,7 +49,8 @@ private List getAllOrderSpecifiers(Pageable pageable) { .forEach(order -> { Order direction = order.getDirection().isAscending() ? Order.ASC : Order.DESC; String property = order.getProperty(); - PathBuilder orderPath = new PathBuilder(HistoryJpaEntity.class, "activityJpaEntity"); + PathBuilder orderPath = new PathBuilder(HistoryJpaEntity.class, + "historyJpaEntity"); orders.add(new OrderSpecifier(direction, orderPath.get(property))); } ); diff --git a/src/main/java/com/gamsa/history/service/HistoryService.java b/src/main/java/com/gamsa/history/service/HistoryService.java index 8bb326c..6807550 100644 --- a/src/main/java/com/gamsa/history/service/HistoryService.java +++ b/src/main/java/com/gamsa/history/service/HistoryService.java @@ -8,14 +8,13 @@ import com.gamsa.history.dto.HistoryFindSliceResponse; import com.gamsa.history.dto.HistorySaveRequest; import com.gamsa.history.repository.HistoryRepository; +import java.time.LocalDateTime; +import java.util.NoSuchElementException; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.stereotype.Service; -import java.time.LocalDateTime; -import java.util.NoSuchElementException; - @RequiredArgsConstructor @Service public class HistoryService { @@ -23,16 +22,24 @@ public class HistoryService { private final AvatarRepository avatarRepository; private final ActivityRepository activityRepository; - public void save(HistorySaveRequest saveRequest) { - Avatar avatar = avatarRepository.findById(saveRequest.getAvatarId()).orElseThrow(NoSuchElementException::new); - Activity activity = activityRepository.findById(saveRequest.getActId()).orElseThrow(NoSuchElementException::new); + public void save(HistorySaveRequest saveRequest, Long userId) { + + Avatar avatar = avatarRepository.findByUserId(userId) + .orElseThrow(NoSuchElementException::new); + Activity activity = activityRepository.findById(saveRequest.getActId()) + .orElseThrow(NoSuchElementException::new); History history = saveRequest.toModel(avatar, activity); historyRepository.save(history); } - public Slice findSliceByAvatarId(long avatarId, Pageable pageable) { - Slice histories = historyRepository.findSliceByAvatarId(avatarId, pageable); + public Slice findSliceByAvatarId(Long userId, Pageable pageable) { + Avatar avatar = avatarRepository.findByUserId(userId) + .orElseThrow(NoSuchElementException::new); + + Slice histories = historyRepository + .findSliceByAvatarId(avatar.getAvatarId(), pageable); histories.forEach(this::checkDate); + return histories.map(HistoryFindSliceResponse::from); } diff --git a/src/main/java/com/gamsa/user/controller/UserController.java b/src/main/java/com/gamsa/user/controller/UserController.java new file mode 100644 index 0000000..e82c2d5 --- /dev/null +++ b/src/main/java/com/gamsa/user/controller/UserController.java @@ -0,0 +1,30 @@ +package com.gamsa.user.controller; + +import com.gamsa.user.dto.KakaoLoginResponse; +import com.gamsa.user.service.UserService; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/v1/users") +public class UserController { + + private final UserService userService; + + @GetMapping("/login/kakao") + public ResponseEntity kakaoLogin( + @RequestHeader Map headers) { + + Map response = userService.userKakaoLogin(headers.get("token")); + + return ResponseEntity.ok() + .header("token", (String) response.get("token")) + .body((KakaoLoginResponse) response.get("body")); + } +} diff --git a/src/main/java/com/gamsa/user/domain/KakaoLogin.java b/src/main/java/com/gamsa/user/domain/KakaoLogin.java new file mode 100644 index 0000000..0fa42de --- /dev/null +++ b/src/main/java/com/gamsa/user/domain/KakaoLogin.java @@ -0,0 +1,29 @@ +package com.gamsa.user.domain; + +import com.gamsa.common.exception.RestClientErrorHandler; +import com.gamsa.user.dto.KakaoProperties; +import com.gamsa.user.dto.KakaoUserInfoResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClient; + +@RequiredArgsConstructor +@Component +public class KakaoLogin { + + private final KakaoProperties kakaoProperties; + private final RestClient restClient = RestClient.create(); + + public KakaoUserInfoResponse getUserInfo(String token) { + return restClient.post() + .uri(kakaoProperties.getUserInfoUrl()) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .header("Authorization", "Bearer " + token) + .retrieve() + .onStatus(HttpStatusCode::is4xxClientError, RestClientErrorHandler.http4xxErrorHandler) + .onStatus(HttpStatusCode::is5xxServerError, RestClientErrorHandler.http5xxErrorHandler) + .body(KakaoUserInfoResponse.class); + } +} diff --git a/src/main/java/com/gamsa/user/domain/User.java b/src/main/java/com/gamsa/user/domain/User.java new file mode 100644 index 0000000..4efeae4 --- /dev/null +++ b/src/main/java/com/gamsa/user/domain/User.java @@ -0,0 +1,12 @@ +package com.gamsa.user.domain; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class User { + + private Long id; + private String nickname; +} diff --git a/src/main/java/com/gamsa/user/dto/KakaoLoginResponse.java b/src/main/java/com/gamsa/user/dto/KakaoLoginResponse.java new file mode 100644 index 0000000..7148934 --- /dev/null +++ b/src/main/java/com/gamsa/user/dto/KakaoLoginResponse.java @@ -0,0 +1,12 @@ +package com.gamsa.user.dto; + +import com.gamsa.avatar.dto.AvatarFindResponse; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class KakaoLoginResponse { + + private final AvatarFindResponse avatar; +} diff --git a/src/main/java/com/gamsa/user/dto/KakaoProperties.java b/src/main/java/com/gamsa/user/dto/KakaoProperties.java new file mode 100644 index 0000000..ab85284 --- /dev/null +++ b/src/main/java/com/gamsa/user/dto/KakaoProperties.java @@ -0,0 +1,13 @@ +package com.gamsa.user.dto; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Getter +@RequiredArgsConstructor +@ConfigurationProperties(prefix = "kakao") +public class KakaoProperties { + + private final String userInfoUrl; +} diff --git a/src/main/java/com/gamsa/user/dto/KakaoUserInfoResponse.java b/src/main/java/com/gamsa/user/dto/KakaoUserInfoResponse.java new file mode 100644 index 0000000..e51650e --- /dev/null +++ b/src/main/java/com/gamsa/user/dto/KakaoUserInfoResponse.java @@ -0,0 +1,34 @@ +package com.gamsa.user.dto; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class) +public class KakaoUserInfoResponse { + + private Long id; + private KakaoAccount kakaoAccount; + + public String getNickname() { + return kakaoAccount.getProfile().getNickname(); + } + + @Getter + @NoArgsConstructor + private static class KakaoAccount { + + private Profile profile; + } + + @Getter + @NoArgsConstructor + private static class Profile { + + private String nickname; + } +} + diff --git a/src/main/java/com/gamsa/user/entity/UserJpaEntity.java b/src/main/java/com/gamsa/user/entity/UserJpaEntity.java new file mode 100644 index 0000000..9db4086 --- /dev/null +++ b/src/main/java/com/gamsa/user/entity/UserJpaEntity.java @@ -0,0 +1,42 @@ +package com.gamsa.user.entity; + +import com.gamsa.user.domain.User; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "users") +@Entity +public class UserJpaEntity { + + @Id + @Column(name = "user_id") + private Long id; + + @Column(name = "nickname", nullable = false) + private String nickname; + + public static UserJpaEntity from(User user) { + return UserJpaEntity.builder() + .id(user.getId()) + .nickname(user.getNickname()) + .build(); + } + + public User toModel() { + return User.builder() + .id(id) + .nickname(nickname) + .build(); + } +} diff --git a/src/main/java/com/gamsa/user/exception/KakaoApiErrorCode.java b/src/main/java/com/gamsa/user/exception/KakaoApiErrorCode.java new file mode 100644 index 0000000..c6cd8ec --- /dev/null +++ b/src/main/java/com/gamsa/user/exception/KakaoApiErrorCode.java @@ -0,0 +1,20 @@ +package com.gamsa.user.exception; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum KakaoApiErrorCode { + + // 4xx error + KAKAO_API_BAD_REQUEST(400, "잘못된 요청"), + KAKAO_API_UNAUTHORIZED(401, "인증 오류"), + KAKAO_API_FORBIDDEN(403, "허가되지 않은 접근"), + + // 5xx error + KAKAO_API_INTERNAL_SERVER_ERROR(500, "서버 내부 오류"); + + private final int status; + private final String msg; +} diff --git a/src/main/java/com/gamsa/user/exception/KakaoApiException.java b/src/main/java/com/gamsa/user/exception/KakaoApiException.java new file mode 100644 index 0000000..5f21f7b --- /dev/null +++ b/src/main/java/com/gamsa/user/exception/KakaoApiException.java @@ -0,0 +1,11 @@ +package com.gamsa.user.exception; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class KakaoApiException extends RuntimeException { + + private final KakaoApiErrorCode kakaoAPIErrorCode; +} diff --git a/src/main/java/com/gamsa/user/repository/KakaoAccessTokenRepository.java b/src/main/java/com/gamsa/user/repository/KakaoAccessTokenRepository.java new file mode 100644 index 0000000..5cf6dab --- /dev/null +++ b/src/main/java/com/gamsa/user/repository/KakaoAccessTokenRepository.java @@ -0,0 +1,20 @@ +package com.gamsa.user.repository; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import org.springframework.stereotype.Repository; + +@Repository +public class KakaoAccessTokenRepository { + + private final Map tokenRepository = new HashMap<>(); + + public Optional findById(Long id) { + return Optional.of(tokenRepository.getOrDefault(id, null)); + } + + public void save(Long id, String token) { + tokenRepository.put(id, token); + } +} diff --git a/src/main/java/com/gamsa/user/repository/UserJpaRepository.java b/src/main/java/com/gamsa/user/repository/UserJpaRepository.java new file mode 100644 index 0000000..0f2b495 --- /dev/null +++ b/src/main/java/com/gamsa/user/repository/UserJpaRepository.java @@ -0,0 +1,8 @@ +package com.gamsa.user.repository; + +import com.gamsa.user.entity.UserJpaEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserJpaRepository extends JpaRepository { + +} diff --git a/src/main/java/com/gamsa/user/repository/UserRepository.java b/src/main/java/com/gamsa/user/repository/UserRepository.java new file mode 100644 index 0000000..86d949a --- /dev/null +++ b/src/main/java/com/gamsa/user/repository/UserRepository.java @@ -0,0 +1,12 @@ +package com.gamsa.user.repository; + +import com.gamsa.user.domain.User; +import java.util.Optional; + +public interface UserRepository { + + void save(User user); + + Optional findById(Long userId); + +} diff --git a/src/main/java/com/gamsa/user/repository/UserRepositoryImpl.java b/src/main/java/com/gamsa/user/repository/UserRepositoryImpl.java new file mode 100644 index 0000000..4182b57 --- /dev/null +++ b/src/main/java/com/gamsa/user/repository/UserRepositoryImpl.java @@ -0,0 +1,25 @@ +package com.gamsa.user.repository; + +import com.gamsa.user.domain.User; +import com.gamsa.user.entity.UserJpaEntity; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@RequiredArgsConstructor +@Repository +public class UserRepositoryImpl implements UserRepository { + + private final UserJpaRepository userJpaRepository; + + @Override + public void save(User user) { + userJpaRepository.save(UserJpaEntity.from(user)); + } + + @Override + public Optional findById(Long userId) { + return userJpaRepository.findById(userId) + .map(UserJpaEntity::toModel); + } +} diff --git a/src/main/java/com/gamsa/user/service/UserService.java b/src/main/java/com/gamsa/user/service/UserService.java new file mode 100644 index 0000000..b823d20 --- /dev/null +++ b/src/main/java/com/gamsa/user/service/UserService.java @@ -0,0 +1,57 @@ +package com.gamsa.user.service; + +import com.gamsa.avatar.domain.Avatar; +import com.gamsa.avatar.dto.AvatarFindResponse; +import com.gamsa.avatar.repository.AvatarRepository; +import com.gamsa.common.jwt.JwtUtil; +import com.gamsa.user.domain.KakaoLogin; +import com.gamsa.user.domain.User; +import com.gamsa.user.dto.KakaoLoginResponse; +import com.gamsa.user.dto.KakaoUserInfoResponse; +import com.gamsa.user.repository.KakaoAccessTokenRepository; +import com.gamsa.user.repository.UserRepository; +import java.util.Map; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class UserService { + + @Value("${spring.jwt.expiration-time}") + private long TOKEN_EXPIRED_TIME; + private final JwtUtil jwtUtil; + private final KakaoLogin kakaoLogin; + private final UserRepository userRepository; + private final AvatarRepository avatarRepository; + private final KakaoAccessTokenRepository kakaoAccessTokenRepository; + + public Map userKakaoLogin(String kakaoToken) { + KakaoUserInfoResponse userInfo = kakaoLogin.getUserInfo(kakaoToken); + Optional user = userRepository.findById(userInfo.getId()); + + if (user.isEmpty()) { + userRepository.save(generateNewUser(userInfo)); + } + kakaoAccessTokenRepository.save(userInfo.getId(), kakaoToken); + + Optional avatar = avatarRepository.findByUserId(userInfo.getId()); + KakaoLoginResponse body = KakaoLoginResponse.builder() + .avatar(avatar.map(AvatarFindResponse::from).orElse(null)) + .build(); + + return Map.of( + "token", (Object) jwtUtil.createJwt(userInfo.getId(), TOKEN_EXPIRED_TIME), + "body", (Object) body + ); + } + + private User generateNewUser(KakaoUserInfoResponse userInfo) { + return User.builder() + .id(userInfo.getId()) + .nickname(userInfo.getNickname()) + .build(); + } +} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index f46c632..4b7f737 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -13,6 +13,9 @@ spring: hibernate.ddl-auto: create properties.hibernate.format_sql: true show-sql: true + jwt: + secret: kghobnakghwoigabsdlkbghaoigqhegowebnhlkwehgaoiwehtoaweqnbzoiwnyzbvwow + expiration-time: 86400000 logging: level: diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 202f78a..e75c3d5 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -3,3 +3,6 @@ spring: name: gamja-bongsa profiles: active: dev + +kakao: + user-info-url: https://kapi.kakao.com/v2/user/me diff --git a/src/test/java/com/gamsa/ApplicationTests.java b/src/test/java/com/gamsa/ApplicationTests.java deleted file mode 100644 index 411875a..0000000 --- a/src/test/java/com/gamsa/ApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.gamsa; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class ApplicationTests { - - @Test - void contextLoads() { - } - -} diff --git a/src/test/java/com/gamsa/avatar/entity/AvatarJpaEntityTest.java b/src/test/java/com/gamsa/avatar/entity/AvatarJpaEntityTest.java index f3e14fc..38f5e78 100644 --- a/src/test/java/com/gamsa/avatar/entity/AvatarJpaEntityTest.java +++ b/src/test/java/com/gamsa/avatar/entity/AvatarJpaEntityTest.java @@ -5,20 +5,27 @@ import com.gamsa.avatar.constant.AgeRange; import com.gamsa.avatar.constant.Experienced; import com.gamsa.avatar.domain.Avatar; +import com.gamsa.user.domain.User; +import com.gamsa.user.entity.UserJpaEntity; import org.junit.jupiter.api.Test; public class AvatarJpaEntityTest { @Test void 도메인에서_JPA엔티티로() { //given + User user = User.builder() + .id(1L) + .nickname("nickname") + .build(); Avatar avatar = Avatar.builder() - .avatarId(1L) - .avatarLevel(1L) - .avatarExp(1L) - .nickname("닉네임") - .ageRange(AgeRange.ADULT) - .experienced(Experienced.NOVICE) - .build(); + .avatarId(1L) + .user(user) + .avatarLevel(1L) + .avatarExp(1L) + .nickname("닉네임") + .ageRange(AgeRange.ADULT) + .experienced(Experienced.NOVICE) + .build(); //when AvatarJpaEntity jpaEntity = AvatarJpaEntity.from(avatar); @@ -30,14 +37,19 @@ public class AvatarJpaEntityTest { @Test void JPA엔티티에서_도메인으로() { //given + UserJpaEntity userJpaEntity = UserJpaEntity.builder() + .id(1L) + .nickname("nickname") + .build(); AvatarJpaEntity avatarJpaEntity = AvatarJpaEntity.builder() - .avatarId(1L) - .avatarLevel(1L) - .avatarExp(1L) - .nickname("닉네임") - .ageRange(AgeRange.ADULT) - .experienced(Experienced.NOVICE) - .build(); + .avatarId(1L) + .user(userJpaEntity) + .avatarLevel(1L) + .avatarExp(1L) + .nickname("닉네임") + .ageRange(AgeRange.ADULT) + .experienced(Experienced.NOVICE) + .build(); //when Avatar avatar = avatarJpaEntity.toModel(); diff --git a/src/test/java/com/gamsa/avatar/service/AvatarServiceTest.java b/src/test/java/com/gamsa/avatar/service/AvatarServiceTest.java index 3a16636..80fd197 100644 --- a/src/test/java/com/gamsa/avatar/service/AvatarServiceTest.java +++ b/src/test/java/com/gamsa/avatar/service/AvatarServiceTest.java @@ -6,38 +6,53 @@ import com.gamsa.avatar.constant.AgeRange; import com.gamsa.avatar.constant.Experienced; import com.gamsa.avatar.dto.AvatarSaveRequest; -import com.gamsa.avatar.stub.StubAvatarRepository; +import com.gamsa.avatar.stub.StubEmptyAvatarRepository; +import com.gamsa.avatar.stub.StubExistsAvatarRepository; +import com.gamsa.user.domain.User; +import com.gamsa.user.stub.StubExistsUserRepository; import org.junit.jupiter.api.Test; public class AvatarServiceTest { - AvatarSaveRequest saveRequest = AvatarSaveRequest.builder() + + private final AvatarSaveRequest saveRequest = AvatarSaveRequest.builder() .nickname("닉네임") .ageRange(AgeRange.ADULT) .experienced(Experienced.EXPERT) .build(); + private final User user = User.builder() + .id(1L) + .nickname("nickname") + .build(); + @Test - void 새로운_유저_저장() { + void 새로운_아바타_저장() { //given - AvatarService avatarService = new AvatarService(new StubAvatarRepository()); + AvatarService avatarService = new AvatarService( + new StubEmptyAvatarRepository(), + new StubExistsUserRepository()); //then - assertDoesNotThrow(() -> avatarService.save(saveRequest)); + assertDoesNotThrow(() -> avatarService.save(saveRequest, user.getId())); } @Test - void 기존_유저_검색() { + void 기존_아바타_검색_성공() { //given - AvatarService avatarService = new AvatarService(new StubAvatarRepository()); + AvatarService avatarService = new AvatarService( + new StubExistsAvatarRepository(), + new StubExistsUserRepository()); //then - assertThat(avatarService.findById(1L)).isNotNull(); + assertThat(avatarService.findByUserId(1L)).isNotNull(); } @Test void 기존_유저_삭제() { //given - AvatarService avatarService = new AvatarService(new StubAvatarRepository()); + AvatarService avatarService = new AvatarService( + new StubExistsAvatarRepository(), + new StubExistsUserRepository()); //then assertDoesNotThrow(() -> avatarService.delete(1L)); @@ -48,9 +63,17 @@ public class AvatarServiceTest { @Test void 기존_유저_업데이트() { //given - AvatarService avatarService = new AvatarService(new StubAvatarRepository()); + AvatarService avatarService = new AvatarService( + new StubEmptyAvatarRepository(), + new StubExistsUserRepository()); + + AvatarSaveRequest updateRequest = AvatarSaveRequest.builder() + .nickname("새 닉네임") + .ageRange(AgeRange.ADULT) + .experienced(Experienced.EXPERT) + .build(); //then - assertDoesNotThrow(() -> avatarService.save(saveRequest)); + assertDoesNotThrow(() -> avatarService.save(updateRequest, user.getId())); } } diff --git a/src/test/java/com/gamsa/avatar/stub/StubAvatarRepository.java b/src/test/java/com/gamsa/avatar/stub/StubAvatarRepository.java deleted file mode 100644 index a24b653..0000000 --- a/src/test/java/com/gamsa/avatar/stub/StubAvatarRepository.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.gamsa.avatar.stub; - -import com.gamsa.avatar.constant.AgeRange; -import com.gamsa.avatar.constant.Experienced; -import com.gamsa.avatar.domain.Avatar; -import com.gamsa.avatar.repository.AvatarRepository; - -import java.time.LocalDateTime; -import java.util.Optional; - -public class StubAvatarRepository implements AvatarRepository { - private final Avatar avatar = Avatar.builder() - .avatarId(1L) - .avatarLevel(1L) - .avatarExp(1L) - .nickname("닉네임") - .ageRange(AgeRange.ADULT) - .experienced(Experienced.NOVICE) - .build(); - - @Override - public void save(Avatar avatar) {} - - @Override - public Optional findById(Long id) { - return Optional.of(avatar); - } - - @Override - public void deleteById(Long id) {} -} diff --git a/src/test/java/com/gamsa/avatar/stub/StubEmptyAvatarRepository.java b/src/test/java/com/gamsa/avatar/stub/StubEmptyAvatarRepository.java new file mode 100644 index 0000000..c60fc46 --- /dev/null +++ b/src/test/java/com/gamsa/avatar/stub/StubEmptyAvatarRepository.java @@ -0,0 +1,33 @@ +package com.gamsa.avatar.stub; + +import com.gamsa.avatar.domain.Avatar; +import com.gamsa.avatar.repository.AvatarRepository; +import java.util.Optional; + +public class StubEmptyAvatarRepository implements AvatarRepository { + + @Override + public void save(Avatar avatar) { + // do nothing + } + + @Override + public Optional findById(Long id) { + return Optional.empty(); + } + + @Override + public Optional findByUserId(Long userId) { + return Optional.empty(); + } + + @Override + public Optional findByNickname(String nickname) { + return Optional.empty(); + } + + @Override + public void deleteById(Long id) { + // do nothing + } +} diff --git a/src/test/java/com/gamsa/avatar/stub/StubExistsAvatarRepository.java b/src/test/java/com/gamsa/avatar/stub/StubExistsAvatarRepository.java new file mode 100644 index 0000000..1a0051e --- /dev/null +++ b/src/test/java/com/gamsa/avatar/stub/StubExistsAvatarRepository.java @@ -0,0 +1,47 @@ +package com.gamsa.avatar.stub; + +import com.gamsa.avatar.constant.AgeRange; +import com.gamsa.avatar.constant.Experienced; +import com.gamsa.avatar.domain.Avatar; +import com.gamsa.avatar.repository.AvatarRepository; +import com.gamsa.user.entity.UserJpaEntity; +import java.util.Optional; + +public class StubExistsAvatarRepository implements AvatarRepository { + + private final UserJpaEntity user = UserJpaEntity.builder() + .id(1L) + .nickname("nickname") + .build(); + + private final Avatar avatar = Avatar.builder() + .avatarId(1L) + .user(user.toModel()) + .avatarLevel(1L) + .avatarExp(1L) + .nickname("닉네임") + .ageRange(AgeRange.ADULT) + .experienced(Experienced.NOVICE) + .build(); + + @Override + public void save(Avatar avatar) {} + + @Override + public Optional findById(Long id) { + return Optional.of(avatar); + } + + @Override + public Optional findByUserId(Long userId) { + return Optional.of(avatar); + } + + @Override + public Optional findByNickname(String nickname) { + return Optional.of(avatar); + } + + @Override + public void deleteById(Long id) {} +} diff --git a/src/test/java/com/gamsa/history/entity/HistoryJpaEntityTest.java b/src/test/java/com/gamsa/history/entity/HistoryJpaEntityTest.java index 26f0594..6b8b95b 100644 --- a/src/test/java/com/gamsa/history/entity/HistoryJpaEntityTest.java +++ b/src/test/java/com/gamsa/history/entity/HistoryJpaEntityTest.java @@ -1,5 +1,7 @@ package com.gamsa.history.entity; +import static org.assertj.core.api.Assertions.assertThat; + import com.gamsa.activity.constant.Category; import com.gamsa.activity.domain.Activity; import com.gamsa.activity.domain.District; @@ -12,78 +14,82 @@ import com.gamsa.common.config.TestConfig; import com.gamsa.history.constant.ActivityStatus; import com.gamsa.history.domain.History; -import org.junit.jupiter.api.Test; -import org.springframework.context.annotation.Import; - +import com.gamsa.user.domain.User; import java.math.BigDecimal; import java.time.LocalDateTime; - -import static org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; @Import(TestConfig.class) public class HistoryJpaEntityTest { // given - District district = District.builder() - .sidoCode(1234) - .sidoGunguCode(8888) - .sidoName("서울특별시") - .gunguName("강남구") - .sido(false) - .build(); + private final District district = District.builder() + .sidoCode(1234) + .sidoGunguCode(8888) + .sidoName("서울특별시") + .gunguName("강남구") + .sido(false) + .build(); - Institute institute = Institute.builder() - .instituteId(1L) - .name("도서관") - .location("서울시") - .latitude(new BigDecimal("123456789.12341234")) - .longitude(new BigDecimal("987654321.43214321")) - .sidoGungu(district) - .phone("010xxxxxxxx") - .build(); - - Activity activity = Activity.builder() - .actId(1L) - .actTitle("어린이놀이안전관리 및 놀잇감 청결유지 및 정리") - .actLocation("아이사랑꿈터 서구 5호점") - .description("봉사 내용") - .noticeStartDate(LocalDateTime.of(2024, 9, 10, 0, 0)) - .noticeEndDate(LocalDateTime.of(2024, 12, 7, 0, 0)) - .actStartDate(LocalDateTime.of(2024, 9, 10, 0, 0)) - .actEndDate(LocalDateTime.of(2024, 12, 7, 0, 0)) - .actStartTime(13) - .actEndTime(18) - .recruitTotalNum(1) - .adultPossible(true) - .teenPossible(false) - .groupPossible(false) - .actWeek(0111110) - .actManager("윤순영") - .actPhone("032-577-3026") - .url("https://...") - .category(Category.OTHER_ACTIVITIES) - .institute(institute) - .sidoGungu(district) - .build(); + private final Institute institute = Institute.builder() + .instituteId(1L) + .name("도서관") + .location("서울시") + .latitude(new BigDecimal("123456789.12341234")) + .longitude(new BigDecimal("987654321.43214321")) + .sidoGungu(district) + .phone("010xxxxxxxx") + .build(); - Avatar avatar = Avatar.builder() - .avatarId(1L) - .avatarLevel(1L) - .avatarExp(1L) - .nickname("닉네임") - .ageRange(AgeRange.ADULT) - .experienced(Experienced.NOVICE) - .build(); + private final Activity activity = Activity.builder() + .actId(1L) + .actTitle("어린이놀이안전관리 및 놀잇감 청결유지 및 정리") + .actLocation("아이사랑꿈터 서구 5호점") + .description("봉사 내용") + .noticeStartDate(LocalDateTime.of(2024, 9, 10, 0, 0)) + .noticeEndDate(LocalDateTime.of(2024, 12, 7, 0, 0)) + .actStartDate(LocalDateTime.of(2024, 9, 10, 0, 0)) + .actEndDate(LocalDateTime.of(2024, 12, 7, 0, 0)) + .actStartTime(13) + .actEndTime(18) + .recruitTotalNum(1) + .adultPossible(true) + .teenPossible(false) + .groupPossible(false) + .actWeek(0111110) + .actManager("윤순영") + .actPhone("032-577-3026") + .url("https://...") + .category(Category.OTHER_ACTIVITIES) + .institute(institute) + .sidoGungu(district) + .build(); + + private final User user = User.builder() + .id(1L) + .nickname("nickname") + .build(); + + private final Avatar avatar = Avatar.builder() + .avatarId(1L) + .user(user) + .avatarLevel(1L) + .avatarExp(1L) + .nickname("닉네임") + .ageRange(AgeRange.ADULT) + .experienced(Experienced.NOVICE) + .build(); @Test void 도메인에서_엔티티로() { // given History history = History.builder() - .historyId(1L) - .activity(activity) - .avatar(avatar) - .activityStatus(ActivityStatus.APPLIED) - .reviewed(false) - .build(); + .historyId(1L) + .activity(activity) + .avatar(avatar) + .activityStatus(ActivityStatus.APPLIED) + .reviewed(false) + .build(); //when HistoryJpaEntity historyJpaEntity = HistoryJpaEntity.from(history); diff --git a/src/test/java/com/gamsa/history/repository/HistoryJpaRepositoryTest.java b/src/test/java/com/gamsa/history/repository/HistoryJpaRepositoryTest.java index 9e3c050..23a5655 100644 --- a/src/test/java/com/gamsa/history/repository/HistoryJpaRepositoryTest.java +++ b/src/test/java/com/gamsa/history/repository/HistoryJpaRepositoryTest.java @@ -1,5 +1,7 @@ package com.gamsa.history.repository; +import static org.assertj.core.api.Assertions.assertThat; + import com.gamsa.activity.constant.Category; import com.gamsa.activity.domain.Activity; import com.gamsa.activity.domain.District; @@ -12,16 +14,14 @@ import com.gamsa.common.config.TestConfig; import com.gamsa.history.constant.ActivityStatus; import com.gamsa.history.entity.HistoryJpaEntity; +import com.gamsa.user.domain.User; +import java.math.BigDecimal; +import java.time.LocalDateTime; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.Import; -import java.math.BigDecimal; -import java.time.LocalDateTime; - -import static org.assertj.core.api.Assertions.assertThat; - @DataJpaTest @Import(TestConfig.class) public class HistoryJpaRepositoryTest { @@ -29,63 +29,69 @@ public class HistoryJpaRepositoryTest { private HistoryJpaRepository historyJpaRepository; District district = District.builder() - .sidoCode(1234) - .sidoGunguCode(8888) - .sidoName("서울특별시") - .gunguName("강남구") - .sido(false) - .build(); + .sidoCode(1234) + .sidoGunguCode(8888) + .sidoName("서울특별시") + .gunguName("강남구") + .sido(false) + .build(); Institute institute = Institute.builder() - .instituteId(1L) - .name("도서관") - .location("서울시") - .latitude(new BigDecimal("123456789.12341234")) - .longitude(new BigDecimal("987654321.43214321")) - .sidoGungu(district) - .phone("010xxxxxxxx") - .build(); + .instituteId(1L) + .name("도서관") + .location("서울시") + .latitude(new BigDecimal("123456789.12341234")) + .longitude(new BigDecimal("987654321.43214321")) + .sidoGungu(district) + .phone("010xxxxxxxx") + .build(); Activity activity = Activity.builder() - .actId(1L) - .actTitle("어린이놀이안전관리 및 놀잇감 청결유지 및 정리") - .actLocation("아이사랑꿈터 서구 5호점") - .description("봉사 내용") - .noticeStartDate(LocalDateTime.of(2024, 9, 10, 0, 0)) - .noticeEndDate(LocalDateTime.of(2024, 12, 7, 0, 0)) - .actStartDate(LocalDateTime.of(2024, 9, 10, 0, 0)) - .actEndDate(LocalDateTime.of(2024, 12, 7, 0, 0)) - .actStartTime(13) - .actEndTime(18) - .recruitTotalNum(1) - .adultPossible(true) - .teenPossible(false) - .groupPossible(false) - .actWeek(0111110) - .actManager("윤순영") - .actPhone("032-577-3026") - .url("https://...") - .category(Category.OTHER_ACTIVITIES) - .institute(institute) - .sidoGungu(district) - .build(); + .actId(1L) + .actTitle("어린이놀이안전관리 및 놀잇감 청결유지 및 정리") + .actLocation("아이사랑꿈터 서구 5호점") + .description("봉사 내용") + .noticeStartDate(LocalDateTime.of(2024, 9, 10, 0, 0)) + .noticeEndDate(LocalDateTime.of(2024, 12, 7, 0, 0)) + .actStartDate(LocalDateTime.of(2024, 9, 10, 0, 0)) + .actEndDate(LocalDateTime.of(2024, 12, 7, 0, 0)) + .actStartTime(13) + .actEndTime(18) + .recruitTotalNum(1) + .adultPossible(true) + .teenPossible(false) + .groupPossible(false) + .actWeek(0111110) + .actManager("윤순영") + .actPhone("032-577-3026") + .url("https://...") + .category(Category.OTHER_ACTIVITIES) + .institute(institute) + .sidoGungu(district) + .build(); + + private final User user = User.builder() + .id(1L) + .nickname("nickname") + .build(); private final Avatar avatar = Avatar.builder() - .avatarId(1L) - .avatarLevel(1L) - .avatarExp(1L) - .nickname("닉네임") - .ageRange(AgeRange.ADULT) - .experienced(Experienced.NOVICE) - .build(); + .avatarId(1L) + .user(user) + .avatarLevel(1L) + .avatarExp(1L) + .nickname("닉네임") + .ageRange(AgeRange.ADULT) + .experienced(Experienced.NOVICE) + .build(); private final HistoryJpaEntity historyJpaEntity = HistoryJpaEntity.builder() - .historyId(1L) - .activity(ActivityJpaEntity.from(activity)) - .avatar(AvatarJpaEntity.from(avatar)) - .activityStatus(ActivityStatus.APPLIED) - .reviewed(false) - .build(); + .historyId(1L) + .activity(ActivityJpaEntity.from(activity)) + .avatar(AvatarJpaEntity.from(avatar)) + .activityStatus(ActivityStatus.APPLIED) + .reviewed(false) + .build(); @Test diff --git a/src/test/java/com/gamsa/history/service/HitstoryServiceTest.java b/src/test/java/com/gamsa/history/service/HitstoryServiceTest.java index 9c63fb3..9344843 100644 --- a/src/test/java/com/gamsa/history/service/HitstoryServiceTest.java +++ b/src/test/java/com/gamsa/history/service/HitstoryServiceTest.java @@ -1,36 +1,37 @@ package com.gamsa.history.service; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + import com.gamsa.activity.stub.StubExistsActivityRepository; -import com.gamsa.avatar.stub.StubAvatarRepository; +import com.gamsa.avatar.stub.StubExistsAvatarRepository; import com.gamsa.history.dto.HistorySaveRequest; import com.gamsa.history.stub.StubHistoryRepository; import org.junit.jupiter.api.Test; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; - public class HitstoryServiceTest { HistorySaveRequest historySaveRequest = HistorySaveRequest.builder() .actId(1L) - .avatarId(1L) .build(); @Test void 새로운_기록_저장() { //given - HistoryService historyService = new HistoryService(new StubHistoryRepository(), new StubAvatarRepository(), new StubExistsActivityRepository()); + HistoryService historyService = new HistoryService(new StubHistoryRepository(), + new StubExistsAvatarRepository(), new StubExistsActivityRepository()); //when & then - assertDoesNotThrow(() -> historyService.save(historySaveRequest)); + assertDoesNotThrow(() -> historyService.save(historySaveRequest, 1L)); } @Test void 유저_기록_찾기() { //given - HistoryService historyService = new HistoryService(new StubHistoryRepository(), new StubAvatarRepository(), new StubExistsActivityRepository()); + HistoryService historyService = new HistoryService(new StubHistoryRepository(), + new StubExistsAvatarRepository(), new StubExistsActivityRepository()); //when & then Pageable pageable = PageRequest.of(0, 10); @@ -40,7 +41,8 @@ public class HitstoryServiceTest { @Test void 기록_삭제() { //given - HistoryService historyService = new HistoryService(new StubHistoryRepository(), new StubAvatarRepository(), new StubExistsActivityRepository()); + HistoryService historyService = new HistoryService(new StubHistoryRepository(), + new StubExistsAvatarRepository(), new StubExistsActivityRepository()); //when & then assertDoesNotThrow(() -> historyService.delete(1L)); @@ -49,7 +51,8 @@ public class HitstoryServiceTest { @Test void 리뷰_상태_업데이트() { //given - HistoryService historyService = new HistoryService(new StubHistoryRepository(), new StubAvatarRepository(), new StubExistsActivityRepository()); + HistoryService historyService = new HistoryService(new StubHistoryRepository(), + new StubExistsAvatarRepository(), new StubExistsActivityRepository()); //when & then assertDoesNotThrow(() -> historyService.updateReviewed(1L, true)); diff --git a/src/test/java/com/gamsa/user/service/UserServiceTest.java b/src/test/java/com/gamsa/user/service/UserServiceTest.java new file mode 100644 index 0000000..7ca6cae --- /dev/null +++ b/src/test/java/com/gamsa/user/service/UserServiceTest.java @@ -0,0 +1,58 @@ +package com.gamsa.user.service; + +import com.gamsa.avatar.stub.StubEmptyAvatarRepository; +import com.gamsa.avatar.stub.StubExistsAvatarRepository; +import com.gamsa.common.jwt.JwtUtil; +import com.gamsa.user.dto.KakaoLoginResponse; +import com.gamsa.user.dto.KakaoProperties; +import com.gamsa.user.stub.DummyKakaoAccessTokenRepository; +import com.gamsa.user.stub.DummyKakaoLogin; +import com.gamsa.user.stub.StubExistsUserRepository; +import java.util.Map; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class UserServiceTest { + + private final String dummySecretKey = "sghdfdfsfskwpqdnblkdjofknvboiwrbnowagibsdhgalkshgowaweqnbzoiwnyzbvwow"; + + @Test + @DisplayName("아직 아바타가 없는 유저 카카오 로그인 성공") + void avatarEmptyUserKakaoLogin() { + // given + final UserService userService = new UserService( + new JwtUtil(dummySecretKey), + new DummyKakaoLogin(new KakaoProperties("dummyUrl")), + new StubExistsUserRepository(), + new StubEmptyAvatarRepository(), // 아바타 X + new DummyKakaoAccessTokenRepository()); + // when + Map result = userService.userKakaoLogin("dummyToken"); + + String token = (String) result.get("token"); + KakaoLoginResponse response = (KakaoLoginResponse) result.get("body"); + // then + Assertions.assertThat(token).isNotNull(); + Assertions.assertThat(response.getAvatar()).isNull(); + } + + @Test + @DisplayName("아바타가 존재하는 유저 카카오 로그인 성공") + void avatarExistsUserKakaoLogin() { + // given + final UserService userService = new UserService( + new JwtUtil(dummySecretKey), + new DummyKakaoLogin(new KakaoProperties("dummyUrl")), + new StubExistsUserRepository(), + new StubExistsAvatarRepository(), // 아바타 O + new DummyKakaoAccessTokenRepository()); + // when + Map result = userService.userKakaoLogin("dummyToken"); + String token = (String) result.get("token"); + KakaoLoginResponse response = (KakaoLoginResponse) result.get("body"); + // then + Assertions.assertThat(token).isNotNull(); + Assertions.assertThat(response.getAvatar()).isNotNull(); + } +} \ No newline at end of file diff --git a/src/test/java/com/gamsa/user/stub/DummyKakaoAccessTokenRepository.java b/src/test/java/com/gamsa/user/stub/DummyKakaoAccessTokenRepository.java new file mode 100644 index 0000000..a521ebc --- /dev/null +++ b/src/test/java/com/gamsa/user/stub/DummyKakaoAccessTokenRepository.java @@ -0,0 +1,17 @@ +package com.gamsa.user.stub; + +import com.gamsa.user.repository.KakaoAccessTokenRepository; +import java.util.Optional; + +public class DummyKakaoAccessTokenRepository extends KakaoAccessTokenRepository { + + @Override + public Optional findById(Long id) { + return Optional.empty(); + } + + @Override + public void save(Long id, String token) { + // do nothing + } +} diff --git a/src/test/java/com/gamsa/user/stub/DummyKakaoLogin.java b/src/test/java/com/gamsa/user/stub/DummyKakaoLogin.java new file mode 100644 index 0000000..8d5b1c9 --- /dev/null +++ b/src/test/java/com/gamsa/user/stub/DummyKakaoLogin.java @@ -0,0 +1,17 @@ +package com.gamsa.user.stub; + +import com.gamsa.user.domain.KakaoLogin; +import com.gamsa.user.dto.KakaoProperties; +import com.gamsa.user.dto.KakaoUserInfoResponse; + +public class DummyKakaoLogin extends KakaoLogin { + + public DummyKakaoLogin(KakaoProperties kakaoProperties) { + super(kakaoProperties); + } + + @Override + public KakaoUserInfoResponse getUserInfo(String token) { + return new KakaoUserInfoResponse(); + } +} diff --git a/src/test/java/com/gamsa/user/stub/StubExistsUserRepository.java b/src/test/java/com/gamsa/user/stub/StubExistsUserRepository.java new file mode 100644 index 0000000..2745ade --- /dev/null +++ b/src/test/java/com/gamsa/user/stub/StubExistsUserRepository.java @@ -0,0 +1,23 @@ +package com.gamsa.user.stub; + +import com.gamsa.user.domain.User; +import com.gamsa.user.repository.UserRepository; +import java.util.Optional; + +public class StubExistsUserRepository implements UserRepository { + + private final User user = User.builder() + .id(1L) + .nickname("nickname") + .build(); + + @Override + public void save(User user) { + // do nothing + } + + @Override + public Optional findById(Long userId) { + return Optional.of(user); + } +} From 674ff15d8ed3f6f3986f106743cc6c348317e5c1 Mon Sep 17 00:00:00 2001 From: 5win <94297900+5win@users.noreply.github.com> Date: Wed, 30 Oct 2024 20:09:59 +0900 Subject: [PATCH 11/17] =?UTF-8?q?feat:=20=EC=B9=B4=ED=85=8C=EA=B3=A0?= =?UTF-8?q?=EB=A6=AC=20=EB=AA=A9=EB=A1=9D=20=EB=B0=98=ED=99=98=20API=20(#4?= =?UTF-8?q?9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 카테고리 목록 반환 API 생성 - 클라이언트와 주고받는 카테고리명 한글로 수정 --- .../com/gamsa/activity/constant/Category.java | 26 +++++++++++++++++++ .../controller/ActivityController.java | 4 +-- .../controller/CategoryController.java | 17 ++++++++++++ 3 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/gamsa/activity/controller/CategoryController.java diff --git a/src/main/java/com/gamsa/activity/constant/Category.java b/src/main/java/com/gamsa/activity/constant/Category.java index 561983f..848c0cf 100644 --- a/src/main/java/com/gamsa/activity/constant/Category.java +++ b/src/main/java/com/gamsa/activity/constant/Category.java @@ -1,5 +1,7 @@ package com.gamsa.activity.constant; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -15,4 +17,28 @@ public enum Category { OTHER_ACTIVITIES("기타 활동"); private final String name; + + @JsonCreator + public static Category fromValues(String value) { + for (Category category : Category.values()) { + if (category.getName().equals(value)) { + return category; + } + } + throw new IllegalArgumentException("Unknown value: " + value); + } + + public static Category fromValuesForSlice(String value) { + for (Category category : Category.values()) { + if (category.getName().equals(value)) { + return category; + } + } + return null; // QueryDSL 에서는 null일 경우 필터링에서 제외하므로 null 반환 허용 + } + + @JsonValue + public String toValue() { + return this.name; + } } diff --git a/src/main/java/com/gamsa/activity/controller/ActivityController.java b/src/main/java/com/gamsa/activity/controller/ActivityController.java index 6da07fc..13e3fa5 100644 --- a/src/main/java/com/gamsa/activity/controller/ActivityController.java +++ b/src/main/java/com/gamsa/activity/controller/ActivityController.java @@ -28,7 +28,7 @@ public class ActivityController { @GetMapping public Slice findSlice( - @RequestParam(required = false) Category category, + @RequestParam(required = false) String category, @RequestParam(required = false) Integer sidoGunguCode, @RequestParam(required = false) Integer sidoCode, @RequestParam(defaultValue = "false") boolean teenPossibleOnly, @@ -36,7 +36,7 @@ public Slice findSlice( Pageable pageable) { ActivityFilterRequest request = ActivityFilterRequest.builder() - .category(category) + .category(Category.fromValuesForSlice(category)) .sidoGunguCode(sidoGunguCode) .sidoCode(sidoCode) .teenPossibleOnly(teenPossibleOnly) diff --git a/src/main/java/com/gamsa/activity/controller/CategoryController.java b/src/main/java/com/gamsa/activity/controller/CategoryController.java new file mode 100644 index 0000000..0a36aa5 --- /dev/null +++ b/src/main/java/com/gamsa/activity/controller/CategoryController.java @@ -0,0 +1,17 @@ +package com.gamsa.activity.controller; + +import com.gamsa.activity.constant.Category; +import java.util.List; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/activities/categories") +public class CategoryController { + + @GetMapping + public List findAllCategories() { + return List.of(Category.values()); + } +} From 8782d3b2f075fc5872f3e7a0c4d3dce2e0c2d1d8 Mon Sep 17 00:00:00 2001 From: 5win <94297900+5win@users.noreply.github.com> Date: Wed, 30 Oct 2024 20:32:31 +0900 Subject: [PATCH 12/17] =?UTF-8?q?fix:=20CI=20=ED=8C=8C=EC=9D=B4=ED=94=84?= =?UTF-8?q?=EB=9D=BC=EC=9D=B8=20yml=ED=8C=8C=EC=9D=BC=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=20(#52)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/prod-ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/prod-ci.yml b/.github/workflows/prod-ci.yml index 5d4dfc3..9ba8989 100644 --- a/.github/workflows/prod-ci.yml +++ b/.github/workflows/prod-ci.yml @@ -29,6 +29,7 @@ jobs: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: ap-northeast-2 - + + - name: deploy jar run: | aws s3 sync build s3://gamja-bongsa \ No newline at end of file From 9ab0c9457f1c24cc4dd2671dd6d4d061655757c9 Mon Sep 17 00:00:00 2001 From: 5win <94297900+5win@users.noreply.github.com> Date: Thu, 31 Oct 2024 14:27:20 +0900 Subject: [PATCH 13/17] =?UTF-8?q?infra:=20CD=20=ED=8C=8C=EC=9D=B4=ED=94=84?= =?UTF-8?q?=EB=9D=BC=EC=9D=B8=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20CI=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#56)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [#37] infra: 압축파일 S3 전송하도록 수정 - tar -cvfz ./build.tar.gz 명령어 추가 * [#37] infra: CD 파이프라인 추가, CI수정 - prod-cd.yml 추가 - codedeploy 위한 appspec.yml 추가 - 배포 script 추가 - prod-ci.yml 수정 --- .github/workflows/prod-cd.yml | 26 ++++++++++++++++++++++++++ .github/workflows/prod-ci.yml | 12 ++++++++++-- appspec.yml | 18 ++++++++++++++++++ scripts/deploy.sh | 29 +++++++++++++++++++++++++++++ 4 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/prod-cd.yml create mode 100644 appspec.yml create mode 100644 scripts/deploy.sh diff --git a/.github/workflows/prod-cd.yml b/.github/workflows/prod-cd.yml new file mode 100644 index 0000000..b67890e --- /dev/null +++ b/.github/workflows/prod-cd.yml @@ -0,0 +1,26 @@ +name: Prod-CD + +on: + push: + branches: [ "master" ] + +jobs: + deploy: + + runs-on: ubuntu-latest + + steps: + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ap-northeast-2 + + - name: Code Deploy + run: | + aws deploy create-deployment \ + --application-name gamsa-codedeploy \ + --deployment-config-name CodeDeployDefault.AllAtOnce \ + --deployment-group-name gamsa-deploy-group \ + --s3-location bucket=gamja-bongsa,bundleType=tgz,key=deploy.tar.gz \ No newline at end of file diff --git a/.github/workflows/prod-ci.yml b/.github/workflows/prod-ci.yml index 9ba8989..36f90ca 100644 --- a/.github/workflows/prod-ci.yml +++ b/.github/workflows/prod-ci.yml @@ -23,6 +23,14 @@ jobs: - name: Build with Gradle Wrapper run: ./gradlew build + - name: archive build directory + run: | + mkdir deploy + cp scripts/*.sh deploy + cp appspec.yml deploy + cp build/libs/*.jar deploy + tar cvfz deploy.tar.gz deploy + - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v1 with: @@ -30,6 +38,6 @@ jobs: aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: ap-northeast-2 - - name: deploy jar + - name: upload to S3 run: | - aws s3 sync build s3://gamja-bongsa \ No newline at end of file + aws s3 sync deploy.tar.gz s3://gamja-bongsa \ No newline at end of file diff --git a/appspec.yml b/appspec.yml new file mode 100644 index 0000000..bd86107 --- /dev/null +++ b/appspec.yml @@ -0,0 +1,18 @@ +version: 0.0 +os: linux +files: + - source: / + destination: /home/ubuntu/deploy + overwrite: yes + +permissions: + - object: / + pattern: "**" + owner: ubuntu + group: ubuntu + +hooks: + ApplicationStart: + - location: deploy.sh + timeout: 60 + runas: ubuntu \ No newline at end of file diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100644 index 0000000..cc6a3f2 --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +REPOSITORY=/home/ubuntu/deploy +APP_NAME=gamsa + +cd $REPOSITORY + +echo "> 현재 구동 중인 애플리케이션 pid 확인" +CURRENT_PID=$(pgrep -f ${APP_NAME}-*.jar) + +echo "> 현재 구동 중인 애플리케이션 pid: $CURRENT_PID" +if [ -z "$CURRENT_PID" ]; then + echo "> 구동 중인 애플리케이션이 없습니다." +else + echo "> kill -15 $CURRENT_PID" + kill -15 $CURRENT_PID + sleep 5 +fi + +echo "> 새 애플리케이션 배포" +JAR_NAME=$(ls -tr $REPOSITORY/ | grep jar | tail -n 1) + +echo "> JAR Name: $JAR_NAME" + +echo "> $JAR_NAME에 실행권한 추가" +chmod +x $JAR_NAME + +echo "> $JAR_NAME 실행" +nohup java -jar $REPOSITORY/$JAR_NAME --spring.profiles.active=prod 2>&1 & \ No newline at end of file From 5e24962d343e86f31301bec348eceb2161ddfca0 Mon Sep 17 00:00:00 2001 From: 5win <94297900+5win@users.noreply.github.com> Date: Thu, 31 Oct 2024 15:04:35 +0900 Subject: [PATCH 14/17] =?UTF-8?q?infra:=20S3=20=EC=97=85=EB=A1=9C=EB=93=9C?= =?UTF-8?q?=20=EB=B0=A9=EC=8B=9D=20cp=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - sync -> cp 로 수정 --- .github/workflows/prod-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/prod-ci.yml b/.github/workflows/prod-ci.yml index 36f90ca..c6a5f88 100644 --- a/.github/workflows/prod-ci.yml +++ b/.github/workflows/prod-ci.yml @@ -40,4 +40,4 @@ jobs: - name: upload to S3 run: | - aws s3 sync deploy.tar.gz s3://gamja-bongsa \ No newline at end of file + aws s3 cp deploy.tar.gz s3://gamja-bongsa From 3192bbe71620fdea6c04c1c9f9138e9f6e6e8adf Mon Sep 17 00:00:00 2001 From: 5win <94297900+5win@users.noreply.github.com> Date: Thu, 31 Oct 2024 21:11:48 +0900 Subject: [PATCH 15/17] =?UTF-8?q?infra:=20=ED=94=84=EB=A1=9C=EB=8D=95?= =?UTF-8?q?=EC=85=98=20=ED=99=98=EA=B2=BD=20=ED=8C=8C=EC=9D=BC=20=EB=B0=8F?= =?UTF-8?q?=20=EB=B0=B0=ED=8F=AC=20=EC=8A=A4=ED=81=AC=EB=A6=BD=ED=8A=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#60)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [#59] docs: gitignore에 민감 파일 추가 * [#59] infra: 설정 파일 변경 및 배포 스크립트 수정 * [#59] fix: 기본 실행 환경 dev로 수정 --- .gitignore | 6 ++++++ scripts/deploy.sh | 9 ++++++--- src/main/resources/application-prod.yml | 10 ++++++++++ 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 9de0e37..e17772e 100644 --- a/.gitignore +++ b/.gitignore @@ -88,3 +88,9 @@ nb-configuration.xml ## Miscellaneous ############################## *.log + +############################## +## Production +############################## +src/main/resources/application-prod-db.yml +src/main/resources/application-jwt.yml diff --git a/scripts/deploy.sh b/scripts/deploy.sh index cc6a3f2..8e195ce 100644 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -6,7 +6,7 @@ APP_NAME=gamsa cd $REPOSITORY echo "> 현재 구동 중인 애플리케이션 pid 확인" -CURRENT_PID=$(pgrep -f ${APP_NAME}-*.jar) +CURRENT_PID=$(lsof -i :8080 -t) echo "> 현재 구동 중인 애플리케이션 pid: $CURRENT_PID" if [ -z "$CURRENT_PID" ]; then @@ -18,7 +18,7 @@ else fi echo "> 새 애플리케이션 배포" -JAR_NAME=$(ls -tr $REPOSITORY/ | grep jar | tail -n 1) +JAR_NAME=$(ls -tr $REPOSITORY/ | grep jar | head -n 1) echo "> JAR Name: $JAR_NAME" @@ -26,4 +26,7 @@ echo "> $JAR_NAME에 실행권한 추가" chmod +x $JAR_NAME echo "> $JAR_NAME 실행" -nohup java -jar $REPOSITORY/$JAR_NAME --spring.profiles.active=prod 2>&1 & \ No newline at end of file +nohup java -jar \ + -Dspring.config.location=/home/ubuntu/prod/application-prod-db.yml,/home/ubuntu/prod/application-jwt.yml \ + -Dspring.profiles.active=prod \ + $REPOSITORY/$JAR_NAME 2>&1 & \ No newline at end of file diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index e69de29..6f7cd83 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -0,0 +1,10 @@ +spring: + config: + import: + - application-jwt.yml + - application-prod-db.yml + +logging: + level: + org: + hibernate: info From fd594238c993054bf24171c34c5ce5d58019c3ec Mon Sep 17 00:00:00 2001 From: 5win <94297900+5win@users.noreply.github.com> Date: Fri, 1 Nov 2024 15:27:37 +0900 Subject: [PATCH 16/17] =?UTF-8?q?infra:=20MySQL=20=EC=97=B0=EB=8F=99=20?= =?UTF-8?q?=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20(#61)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 프로덕션 환경에서 테스트 - AWS RDS 연동 테스트 --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index 18efdfa..513638e 100644 --- a/build.gradle +++ b/build.gradle @@ -38,6 +38,7 @@ dependencies { runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' + implementation 'com.mysql:mysql-connector-j:9.1.0' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' annotationProcessor 'org.projectlombok:lombok' From 67d6bd1a05fa593710c9ad7aa585492554e7007d Mon Sep 17 00:00:00 2001 From: Awhn <69659322+Awhn@users.noreply.github.com> Date: Fri, 1 Nov 2024 23:33:10 +0900 Subject: [PATCH 17/17] =?UTF-8?q?feat:=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#62)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 활동 참여 정보 동기화를 위한 기초 코드 * feat: 데이터 업데이트 서비스 및 유틸리티 구현 * fix: json으 로수 * fix: 테스트 코드를 위한 수정 * test: 주소와 활동 데이터 업데이트 테스트 코드(외부 API 사용) * test: 주소와 활동 데이터 업데이트 테스트 코드(외부 API 사용) --- build.gradle | 3 +- src/main/java/com/gamsa/Application.java | 4 +- .../com/gamsa/activity/domain/District.java | 4 + .../activity/dto/ActivityApiResponse.java | 62 +++ .../activity/dto/DistrictSaveRequest.java | 21 +- .../activity/dto/InstituteApiResponse.java | 35 ++ .../activity/dto/InstituteSaveRequest.java | 1 + .../activity/entity/DistrictJpaEntity.java | 38 +- .../activity/service/DistrictService.java | 15 +- .../activity/service/InstituteService.java | 4 + .../gamsa/dataupdate/DataUpdateErrorCode.java | 25 + .../gamsa/dataupdate/DataUpdateException.java | 10 + .../gamsa/dataupdate/DataUpdateScheduler.java | 26 ++ .../service/ActivityDataUpdateService.java | 48 ++ .../service/DistrictDataUpdateService.java | 70 +++ .../dataupdate/utils/ActivityDataUtils.java | 205 ++++++++ .../dataupdate/utils/KakaoLocalUtils.java | 59 +++ src/main/resources/application.yml | 11 + .../resources/district_and_coordinate_kr.csv | 441 ++++++++++++++++++ .../dataupdate/ActivityDataUtilsTest.java | 55 +++ .../dataupdate/DataUpdateSchedulerTest.java | 43 ++ .../gamsa/dataupdate/KakaoLocalUtilsTest.java | 28 ++ 22 files changed, 1180 insertions(+), 28 deletions(-) create mode 100644 src/main/java/com/gamsa/activity/dto/ActivityApiResponse.java create mode 100644 src/main/java/com/gamsa/activity/dto/InstituteApiResponse.java create mode 100644 src/main/java/com/gamsa/dataupdate/DataUpdateErrorCode.java create mode 100644 src/main/java/com/gamsa/dataupdate/DataUpdateException.java create mode 100644 src/main/java/com/gamsa/dataupdate/DataUpdateScheduler.java create mode 100644 src/main/java/com/gamsa/dataupdate/service/ActivityDataUpdateService.java create mode 100644 src/main/java/com/gamsa/dataupdate/service/DistrictDataUpdateService.java create mode 100644 src/main/java/com/gamsa/dataupdate/utils/ActivityDataUtils.java create mode 100644 src/main/java/com/gamsa/dataupdate/utils/KakaoLocalUtils.java create mode 100644 src/main/resources/district_and_coordinate_kr.csv create mode 100644 src/test/java/com/gamsa/dataupdate/ActivityDataUtilsTest.java create mode 100644 src/test/java/com/gamsa/dataupdate/DataUpdateSchedulerTest.java create mode 100644 src/test/java/com/gamsa/dataupdate/KakaoLocalUtilsTest.java diff --git a/build.gradle b/build.gradle index 513638e..4416840 100644 --- a/build.gradle +++ b/build.gradle @@ -24,6 +24,7 @@ repositories { } dependencies { + implementation 'com.opencsv:opencsv:5.5.2' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' @@ -49,4 +50,4 @@ dependencies { tasks.named('test') { useJUnitPlatform() -} +} \ No newline at end of file diff --git a/src/main/java/com/gamsa/Application.java b/src/main/java/com/gamsa/Application.java index 1ac8a46..a5a8742 100644 --- a/src/main/java/com/gamsa/Application.java +++ b/src/main/java/com/gamsa/Application.java @@ -4,14 +4,14 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.ConfigurationPropertiesScan; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.scheduling.annotation.EnableScheduling; @ConfigurationPropertiesScan @EnableJpaAuditing +@EnableScheduling @SpringBootApplication public class Application { - public static void main(String[] args) { SpringApplication.run(Application.class, args); } - } diff --git a/src/main/java/com/gamsa/activity/domain/District.java b/src/main/java/com/gamsa/activity/domain/District.java index 9379f12..d8982fb 100644 --- a/src/main/java/com/gamsa/activity/domain/District.java +++ b/src/main/java/com/gamsa/activity/domain/District.java @@ -3,6 +3,8 @@ import lombok.Builder; import lombok.Getter; +import java.math.BigDecimal; + @Getter @Builder public class District { @@ -11,5 +13,7 @@ public class District { private int sidoCode; private String sidoName; private String gunguName; + private BigDecimal latitude; + private BigDecimal longitude; private boolean sido; } diff --git a/src/main/java/com/gamsa/activity/dto/ActivityApiResponse.java b/src/main/java/com/gamsa/activity/dto/ActivityApiResponse.java new file mode 100644 index 0000000..0d86386 --- /dev/null +++ b/src/main/java/com/gamsa/activity/dto/ActivityApiResponse.java @@ -0,0 +1,62 @@ +package com.gamsa.activity.dto; + +import com.gamsa.activity.constant.Category; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.time.LocalDateTime; + +@Getter +@Builder +@RequiredArgsConstructor +public class ActivityApiResponse { + + private final Long actId; + private final String actTitle; + private final String actLocation; + private final String description; + private final LocalDateTime noticeStartDate; + private final LocalDateTime noticeEndDate; + private final LocalDateTime actStartDate; + private final LocalDateTime actEndDate; + private final int actStartTime; + private final int actEndTime; + private final int recruitTotalNum; + private final boolean adultPossible; + private final boolean teenPossible; + private final boolean groupPossible; + private final int actWeek; + private final String actManager; + private final String actPhone; + private final String url; + private final Category category; + private final String instituteName; + private final Integer sidoGunguCode; + + public ActivitySaveRequest toSaveRequest(long instituteId) { + return ActivitySaveRequest.builder() + .actId(actId) + .actTitle(actTitle) + .actLocation(actLocation) + .description(description) + .noticeStartDate(noticeStartDate) + .noticeEndDate(noticeEndDate) + .actStartDate(actStartDate) + .actEndDate(actEndDate) + .actStartTime(actStartTime) + .actEndTime(actEndTime) + .recruitTotalNum(recruitTotalNum) + .adultPossible(adultPossible) + .teenPossible(teenPossible) + .groupPossible(groupPossible) + .actWeek(actWeek) + .actManager(actManager) + .actPhone(actPhone) + .url(url) + .category(category) + .instituteId(instituteId) + .sidoGunguCode(sidoGunguCode) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/gamsa/activity/dto/DistrictSaveRequest.java b/src/main/java/com/gamsa/activity/dto/DistrictSaveRequest.java index 6217fdd..d85f732 100644 --- a/src/main/java/com/gamsa/activity/dto/DistrictSaveRequest.java +++ b/src/main/java/com/gamsa/activity/dto/DistrictSaveRequest.java @@ -1,28 +1,35 @@ package com.gamsa.activity.dto; import com.gamsa.activity.domain.District; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.RequiredArgsConstructor; +import java.math.BigDecimal; + @Getter @Builder -@RequiredArgsConstructor +@AllArgsConstructor public class DistrictSaveRequest { private final int sidoGunguCode; private final int sidoCode; private final String sidoName; private final String gunguName; + private BigDecimal latitude; + private BigDecimal longitude; private final boolean sido; public District toModel() { return District.builder() - .sidoGunguCode(sidoGunguCode) - .sidoCode(sidoCode) - .sidoName(sidoName) - .gunguName(gunguName) - .sido(sido) - .build(); + .sidoGunguCode(sidoGunguCode) + .sidoCode(sidoCode) + .sidoName(sidoName) + .gunguName(gunguName) + .latitude(latitude) + .longitude(longitude) + .sido(sido) + .build(); } } diff --git a/src/main/java/com/gamsa/activity/dto/InstituteApiResponse.java b/src/main/java/com/gamsa/activity/dto/InstituteApiResponse.java new file mode 100644 index 0000000..72a5179 --- /dev/null +++ b/src/main/java/com/gamsa/activity/dto/InstituteApiResponse.java @@ -0,0 +1,35 @@ +package com.gamsa.activity.dto; + +import com.gamsa.activity.domain.District; +import com.gamsa.activity.domain.Institute; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.math.BigDecimal; +import java.util.Map; + +@Getter +@Builder +@AllArgsConstructor +public class InstituteApiResponse { + + private String name; + private String location; + private int sidoCode; + private int sidoGunguCode; + private String phone; + + public InstituteSaveRequest toSaveRequest(Map coordinates) { + return InstituteSaveRequest.builder() + .name(name) + .location(location) + .sidoCode(sidoCode) + .sidoGunguCode(sidoGunguCode) + .longitude(coordinates.get("longitude")) + .latitude(coordinates.get("latitude")) + .phone(phone) + .build(); + } +} diff --git a/src/main/java/com/gamsa/activity/dto/InstituteSaveRequest.java b/src/main/java/com/gamsa/activity/dto/InstituteSaveRequest.java index 630cc48..8577dae 100644 --- a/src/main/java/com/gamsa/activity/dto/InstituteSaveRequest.java +++ b/src/main/java/com/gamsa/activity/dto/InstituteSaveRequest.java @@ -5,6 +5,7 @@ import java.math.BigDecimal; import lombok.Builder; import lombok.Getter; +import lombok.RequiredArgsConstructor; @Getter @Builder diff --git a/src/main/java/com/gamsa/activity/entity/DistrictJpaEntity.java b/src/main/java/com/gamsa/activity/entity/DistrictJpaEntity.java index d66c64e..8d72266 100644 --- a/src/main/java/com/gamsa/activity/entity/DistrictJpaEntity.java +++ b/src/main/java/com/gamsa/activity/entity/DistrictJpaEntity.java @@ -6,11 +6,9 @@ import jakarta.persistence.Entity; import jakarta.persistence.Id; import jakarta.persistence.Table; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.*; + +import java.math.BigDecimal; @Getter @Builder @@ -27,6 +25,12 @@ public class DistrictJpaEntity extends BaseEntity { @Column(name = "sido_code", nullable = false) private int sidoCode; + @Column(name = "latitude") + private BigDecimal latitude; + + @Column(name = "longitude") + private BigDecimal longitude; + @Column(name = "sido_name", length = 15, nullable = false) private String sidoName; @@ -38,21 +42,21 @@ public class DistrictJpaEntity extends BaseEntity { public static DistrictJpaEntity from(District district) { return DistrictJpaEntity.builder() - .sidoGunguCode(district.getSidoGunguCode()) - .sidoCode(district.getSidoCode()) - .sidoName(district.getSidoName()) - .gunguName(district.getGunguName()) - .sido(district.isSido()) - .build(); + .sidoGunguCode(district.getSidoGunguCode()) + .sidoCode(district.getSidoCode()) + .sidoName(district.getSidoName()) + .gunguName(district.getGunguName()) + .sido(district.isSido()) + .build(); } public District toModel() { return District.builder() - .sidoGunguCode(getSidoGunguCode()) - .sidoCode(getSidoCode()) - .sidoName(getSidoName()) - .gunguName(getGunguName()) - .sido(isSido()) - .build(); + .sidoGunguCode(getSidoGunguCode()) + .sidoCode(getSidoCode()) + .sidoName(getSidoName()) + .gunguName(getGunguName()) + .sido(isSido()) + .build(); } } diff --git a/src/main/java/com/gamsa/activity/service/DistrictService.java b/src/main/java/com/gamsa/activity/service/DistrictService.java index bcc853e..11ce1f3 100644 --- a/src/main/java/com/gamsa/activity/service/DistrictService.java +++ b/src/main/java/com/gamsa/activity/service/DistrictService.java @@ -1,11 +1,15 @@ package com.gamsa.activity.service; import com.gamsa.activity.constant.ActivityErrorCode; +import com.gamsa.activity.domain.District; import com.gamsa.activity.dto.DistrictFindAllResponse; import com.gamsa.activity.dto.DistrictSaveRequest; import com.gamsa.activity.exception.ActivityException; import com.gamsa.activity.repository.DistrictRepository; -import java.util.List; + +import java.math.BigDecimal; +import java.util.*; + import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -44,4 +48,13 @@ public List findAllGungu() { .map(DistrictFindAllResponse::from) .toList(); } + + public Map findCoordinates(int gunguCode) { + District find = districtRepository.findBySidoGunguCode(gunguCode) + .orElseThrow(NoSuchElementException::new); + Map coordinates = new HashMap<>(); + coordinates.put("longitude", find.getLongitude()); + coordinates.put("latitude", find.getLatitude()); + return coordinates; + } } diff --git a/src/main/java/com/gamsa/activity/service/InstituteService.java b/src/main/java/com/gamsa/activity/service/InstituteService.java index 7849c43..e0f1779 100644 --- a/src/main/java/com/gamsa/activity/service/InstituteService.java +++ b/src/main/java/com/gamsa/activity/service/InstituteService.java @@ -27,4 +27,8 @@ public void save(InstituteSaveRequest saveRequest) { instituteRepository.save(saveRequest.toModel(district)); } + + public Long findByName(String name) { + return instituteRepository.findByName(name).orElseThrow(() -> new ActivityException(ActivityErrorCode.INSTITUTE_NOT_EXISTS)).getInstituteId(); + } } diff --git a/src/main/java/com/gamsa/dataupdate/DataUpdateErrorCode.java b/src/main/java/com/gamsa/dataupdate/DataUpdateErrorCode.java new file mode 100644 index 0000000..f24851f --- /dev/null +++ b/src/main/java/com/gamsa/dataupdate/DataUpdateErrorCode.java @@ -0,0 +1,25 @@ +package com.gamsa.dataupdate; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum DataUpdateErrorCode { + // 1365 API 오류 + OPENAPI_NOT_RESPOND(504, "Open API가 응답하지 않습니다."), + OPENAPI_ERROR(504, "Open API의 반환 값을 처리할 수 없습니다."), + + + // 카카오 API 오류 + KAKAOLOCALAPI_NOT_RESPOND(504, "카카오 API가 정상적으로 응답하지 않습니다."), + KAKAOLOCALAPT_ERROR(504, "카카오 API의 반환 값을 처리할 수 없습니다."), + + // 내부 처리 오류 + INVALID_CSV(500, "주어진 CSV 파일을 처리할 수 없습니다"), + INVALID_FILE_SOURCE(500, "주어진 파일 경로가 올바르지 않습니다."); + + + private final int ststus; + private final String msg; +} diff --git a/src/main/java/com/gamsa/dataupdate/DataUpdateException.java b/src/main/java/com/gamsa/dataupdate/DataUpdateException.java new file mode 100644 index 0000000..a1ee8cd --- /dev/null +++ b/src/main/java/com/gamsa/dataupdate/DataUpdateException.java @@ -0,0 +1,10 @@ +package com.gamsa.dataupdate; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class DataUpdateException extends RuntimeException { + private final DataUpdateErrorCode errorCode; +} diff --git a/src/main/java/com/gamsa/dataupdate/DataUpdateScheduler.java b/src/main/java/com/gamsa/dataupdate/DataUpdateScheduler.java new file mode 100644 index 0000000..2ae1229 --- /dev/null +++ b/src/main/java/com/gamsa/dataupdate/DataUpdateScheduler.java @@ -0,0 +1,26 @@ +package com.gamsa.dataupdate; + +import com.gamsa.dataupdate.service.ActivityDataUpdateService; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.time.LocalDate; + +@Component +@RequiredArgsConstructor +public class DataUpdateScheduler { + private final ActivityDataUpdateService activityDataUpdateService; + + @Value("${spring.openapi.days}") + private int days; + + @Scheduled(cron = "0 1 0 * * *") + public void runActivityDataUpdate() { + LocalDate today = LocalDate.now(); + LocalDate endDate = today.plusDays(days); + + activityDataUpdateService.update(today, endDate); + } +} diff --git a/src/main/java/com/gamsa/dataupdate/service/ActivityDataUpdateService.java b/src/main/java/com/gamsa/dataupdate/service/ActivityDataUpdateService.java new file mode 100644 index 0000000..27568d8 --- /dev/null +++ b/src/main/java/com/gamsa/dataupdate/service/ActivityDataUpdateService.java @@ -0,0 +1,48 @@ +package com.gamsa.dataupdate.service; + +import com.gamsa.activity.dto.ActivitySaveRequest; +import com.gamsa.activity.dto.InstituteSaveRequest; +import com.gamsa.activity.service.ActivityService; +import com.gamsa.activity.service.DistrictService; +import com.gamsa.activity.service.InstituteService; +import com.gamsa.dataupdate.utils.ActivityDataUtils; +import com.gamsa.dataupdate.utils.KakaoLocalUtils; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.util.List; + +@RequiredArgsConstructor +@Service +public class ActivityDataUpdateService { + private final ActivityService activityService; + private final ActivityDataUtils activityDataUtils; + private final KakaoLocalUtils kakaoLocalUtils; + + DistrictService districtService; + InstituteService instituteService; + + public void update(LocalDate startDate, LocalDate endDate) { + List programList = activityDataUtils.getVolunteerParticipationList(startDate, endDate); + + List saveRequests = programList.stream() + .map(activityDataUtils::getInstituteApiResponse) + .map(instituteApiResponse -> { + return instituteApiResponse.toSaveRequest(kakaoLocalUtils.getCoordinateByAddress(instituteApiResponse.getLocation()) + .orElse(districtService.findCoordinates(instituteApiResponse.getSidoGunguCode()))); + }) + .toList(); + + saveRequests.forEach(instituteService::save); + + List activitySaveRequests = programList.stream() + .map(activityDataUtils::getVolunteerDetail) + .map(activityApiResponse -> { + return activityApiResponse.toSaveRequest(instituteService.findByName(activityApiResponse.getInstituteName())); + }) + .toList(); + + activitySaveRequests.forEach(activityService::save); + } +} \ No newline at end of file diff --git a/src/main/java/com/gamsa/dataupdate/service/DistrictDataUpdateService.java b/src/main/java/com/gamsa/dataupdate/service/DistrictDataUpdateService.java new file mode 100644 index 0000000..f0b6261 --- /dev/null +++ b/src/main/java/com/gamsa/dataupdate/service/DistrictDataUpdateService.java @@ -0,0 +1,70 @@ +package com.gamsa.dataupdate.service; + +import com.gamsa.activity.dto.DistrictSaveRequest; +import com.gamsa.activity.service.DistrictService; +import com.gamsa.dataupdate.DataUpdateErrorCode; +import com.gamsa.dataupdate.DataUpdateException; +import com.opencsv.CSVReader; +import com.opencsv.exceptions.CsvValidationException; +import jakarta.annotation.PostConstruct; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; + +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.math.BigDecimal; + +@Service +@Component +@RequiredArgsConstructor +public class DistrictDataUpdateService { + private final DistrictService districtService; + + @Value("{data.csvpath}") + private String csvPath; + + @PostConstruct + public void DistrictInit() { + if (!isDataChanged()) loadDataFromCSV(csvPath); + } + + private boolean isDataChanged() { + // 나중에 file 변경에 따른 로직으로 수정 + return (districtService.findAllGungu().isEmpty()) + && (districtService.findAllSido().isEmpty()); + } + + @Transactional + public void loadDataFromCSV(String csvPath) { + try { + ClassLoader classLoader = getClass().getClassLoader(); + File file = new File(classLoader.getResource(csvPath).getFile()); + FileReader fileReader = new FileReader(file); + CSVReader csvReader = new CSVReader(fileReader); + + csvReader.readNext(); + String[] nextRecord; + while ((nextRecord = csvReader.readNext()) != null) { + DistrictSaveRequest districtSaveRequest = DistrictSaveRequest.builder() + .sidoGunguCode(Integer.getInteger(nextRecord[0])) + .sidoCode(Integer.getInteger(nextRecord[1])) + .sidoName(nextRecord[2]) + .gunguName(nextRecord[3]) + .latitude(new BigDecimal(nextRecord[4])) + .longitude(new BigDecimal(nextRecord[5])) + .sido(Boolean.parseBoolean(nextRecord[6])) + .build(); + + districtService.save(districtSaveRequest); + } + } catch (IOException e) { + throw new DataUpdateException(DataUpdateErrorCode.INVALID_FILE_SOURCE); + } catch (CsvValidationException e) { + throw new DataUpdateException(DataUpdateErrorCode.INVALID_CSV); + } + } +} diff --git a/src/main/java/com/gamsa/dataupdate/utils/ActivityDataUtils.java b/src/main/java/com/gamsa/dataupdate/utils/ActivityDataUtils.java new file mode 100644 index 0000000..8ff1018 --- /dev/null +++ b/src/main/java/com/gamsa/dataupdate/utils/ActivityDataUtils.java @@ -0,0 +1,205 @@ +package com.gamsa.dataupdate.utils; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.gamsa.activity.constant.Category; +import com.gamsa.activity.dto.ActivityApiResponse; +import com.gamsa.activity.dto.InstituteApiResponse; +import com.gamsa.dataupdate.DataUpdateErrorCode; +import com.gamsa.dataupdate.DataUpdateException; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; + +@Component +@RequiredArgsConstructor +public class ActivityDataUtils { + @Value(value = "${openapi.key}") + private String openapiKey; + + @Value(value = "${openapi.url}") + private String openapiUrl; + + @Value(value = "${openapi.volurl}") + private String volUrl; + + private final RestTemplate restTemplate = new RestTemplate(); + + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd"); + + public List getVolunteerParticipationList(LocalDate startDate, LocalDate endDate) { + + String url = openapiUrl + "/getVltrPeriodSrvcList"; + + List volunteerList = new ArrayList<>(); + + int numOfItem = 20; + int pageNo = 1; + + while (numOfItem != 0) { + UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromHttpUrl(url) + .queryParam("ServiceKey", openapiKey) + .queryParam("progrmBgnde", startDate.format(formatter)) + .queryParam("progrmEndde", endDate.format(formatter)) + .queryParam("numOfRows", 20) + .queryParam("pageNo", pageNo); + + ResponseEntity response = restTemplate.getForEntity(uriBuilder.toUriString(), String.class); + + if (response.getStatusCode().is2xxSuccessful()) { + try { + String jsonContent = response.getBody(); + + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode rootNode = objectMapper.readTree(jsonContent); + + JsonNode itemsNode = rootNode.path("response").path("body").path("items"); + + numOfItem = itemsNode.size(); + + for (JsonNode item : itemsNode) { + String programNo = item.path("progrmRegistNo").asText(); + volunteerList.add(programNo); + } + + pageNo++; + + } catch (Exception e) { + System.out.println(e.getMessage()); + throw new DataUpdateException(DataUpdateErrorCode.OPENAPI_ERROR); + } + } else { + throw new DataUpdateException(DataUpdateErrorCode.OPENAPI_NOT_RESPOND); + } + } + + return volunteerList; + } + + public InstituteApiResponse getInstituteApiResponse(String programNo) { + String url = openapiUrl + "/getVltrPartcptnItem"; + + UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromHttpUrl(url) + .queryParam("ServiceKey", openapiKey) + .queryParam("progrmRegistNo", programNo); + + ResponseEntity response = restTemplate.getForEntity(uriBuilder.toUriString(), String.class); + + if (response.getStatusCode().is2xxSuccessful()) { + try { + String jsonContent = response.getBody(); + + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode rootNode = objectMapper.readTree(jsonContent); + + JsonNode item = rootNode.path("response").path("body").path("items").path("item"); + + System.out.println(item); + InstituteApiResponse instituteApiResponse = InstituteApiResponse.builder() + .name(item.path("mnnstNm").asText()) + .location(item.path("postAdres").asText()) + .sidoCode(item.path("sidoCd").asInt()) + .sidoGunguCode(item.path("gugunCd").asInt()) + .phone(item.path("telno").asText()) + .build(); + + return instituteApiResponse; + + } catch (Exception e) { + System.out.println(e.getMessage()); + throw new DataUpdateException(DataUpdateErrorCode.OPENAPI_ERROR); + } + } else { + throw new DataUpdateException(DataUpdateErrorCode.OPENAPI_NOT_RESPOND); + } + } + + public ActivityApiResponse getVolunteerDetail(String programNo) { + String url = openapiUrl + "/getVltrPartcptnItem"; + + UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromHttpUrl(url) + .queryParam("ServiceKey", openapiKey) + .queryParam("progrmRegistNo", programNo); + + ResponseEntity response = restTemplate.getForEntity(uriBuilder.toUriString(), String.class); + + if (response.getStatusCode().is2xxSuccessful()) { + try { + String jsonContent = response.getBody(); + + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode rootNode = objectMapper.readTree(jsonContent); + + JsonNode item = rootNode.path("response").path("body").path("items").path("item"); + + ActivityApiResponse activityApiResponse = ActivityApiResponse.builder() + .actId((long) item.path("progrmRegistNo").asInt()) + .actTitle(item.path("progrmSj").asText()) + .actLocation(item.path("actPlace").asText()) + .description(item.path("progrmCn").asText()) + .noticeStartDate(LocalDate.parse(String.valueOf(item.path("noticeBgnde").asInt()), formatter).atStartOfDay()) + .noticeEndDate(LocalDate.parse(String.valueOf(item.path("noticeEndde").asInt()), formatter).atStartOfDay()) + .actStartDate(LocalDate.parse(String.valueOf(item.path("progrmBgnde").asInt()), formatter).atStartOfDay()) + .actEndDate(LocalDate.parse(String.valueOf(item.path("progrmBgnde").asInt()), formatter).atStartOfDay()) + .actStartTime(item.path("actBeginTm").asInt()) + .actEndTime(item.path("actEndTm").asInt()) + .recruitTotalNum(item.path("rcritNmpr").asInt()) + .adultPossible(item.path("adultPosblAt").asText() == "Y" ? true : false) + .teenPossible(item.path("yngbgsPosblAt").asText() == "Y" ? true : false) + .groupPossible(item.path("grpPosblAt").asText() == "Y" ? true : false) + .actWeek(item.path("actWkdy").asInt()) + .actManager(item.path("nanmmbyNmAdmn").asText()) + .actPhone(item.path("telno").asText()) + .url(volUrl + item.path("progrmRegistNo").asText()) + .instituteName(item.path("mnnstNm").asText()) + .category(getCategory(item.path("srvcClCode").asText())) + .sidoGunguCode(item.path("gugunCd").asInt()) + + .build(); + + return activityApiResponse; + + } catch (Exception e) { + System.out.println(e.getMessage()); + throw new DataUpdateException(DataUpdateErrorCode.OPENAPI_ERROR); + } + } else { + System.out.println("Error: " + response.getStatusCode()); + throw new DataUpdateException(DataUpdateErrorCode.OPENAPI_NOT_RESPOND); + } + } + + private Category getCategory(String text) { + if ((text.contains("생활편의지원")) + || text.contains("주거환경") + || text.contains("농어촌 봉사")) { + return Category.LIFE_SUPPORT_AND_HOUSING_IMPROVEMENT; + } else if ((text.contains("교육")) + || text.contains("멘토링")) { + return Category.EDUCATION_AND_MENTORING; + } else if (text.contains("행정보조")) { + return Category.ADMINISTRATIVE_AND_OFFICE_SUPPORT; + } else if ((text.contains("문화행사")) + || text.contains("환경보호") + || text.contains("국제협력") + || text.contains("안전.예방")) { + return Category.CULTURE_ENVIRONMENT_AND_INTERNATIONAL_COOPERATION; + } else if ((text.contains("보건의료")) + || text.contains("공익.인권")) { + return Category.HEALTHCARE_AND_PUBLIC_WELFARE; + } else if ((text.contains("상담")) + || text.contains("자원봉사교육")) { + return Category.COUNSELING_AND_VOLUNTEER_TRAINING; + } else { + return Category.OTHER_ACTIVITIES; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/gamsa/dataupdate/utils/KakaoLocalUtils.java b/src/main/java/com/gamsa/dataupdate/utils/KakaoLocalUtils.java new file mode 100644 index 0000000..ca42e83 --- /dev/null +++ b/src/main/java/com/gamsa/dataupdate/utils/KakaoLocalUtils.java @@ -0,0 +1,59 @@ +package com.gamsa.dataupdate.utils; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +@Component +public class KakaoLocalUtils { + + @Value("${kakao.localkey}") + private String kakaoKey; + + private final RestTemplate restTemplate = new RestTemplate(); + + public Optional> getCoordinateByAddress(String address) { + // 요청 URL 생성 + String url = "https://dapi.kakao.com/v2/local/search/address.json?query=" + address; + // 헤더 설정 + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", "KakaoAK " + kakaoKey); + + // HTTP 요청 + HttpEntity entity = new HttpEntity<>(headers); + ResponseEntity response = restTemplate.exchange( + url, HttpMethod.GET, entity, Map.class); + + // 응답 처리 + if (response.getStatusCode().is2xxSuccessful()) { + Map result = response.getBody(); + if (result != null && result.containsKey("documents")) { + // 첫 번째 결과의 x, y 좌표 반환 + var documents = (List>) result.get("documents"); + if (!documents.isEmpty()) { + Map firstDoc = documents.getFirst(); + BigDecimal x = new BigDecimal(firstDoc.get("x").toString()); + BigDecimal y = new BigDecimal(firstDoc.get("y").toString()); + + Map coordinates = new HashMap<>(); + coordinates.put("longitude", x); + coordinates.put("latitude", y); + return Optional.of(coordinates); + } + } + } + + System.out.println("API 요청 실패: " + response.getStatusCode()); + return Optional.empty(); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index e75c3d5..e2c4134 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -6,3 +6,14 @@ spring: kakao: user-info-url: https://kapi.kakao.com/v2/user/me + localkey: + +data: + csvpath: "district_and_coordinate_kr.csv" + +openapi: + key: + url: "http://openapi.1365.go.kr/openapi/service/rest/VolunteerPartcptnService/" + volurl: "https://www.1365.go.kr/vols/1572247904127/partcptn/timeCptn.do?type=show&progrmRegistNo=" + days: 7 + diff --git a/src/main/resources/district_and_coordinate_kr.csv b/src/main/resources/district_and_coordinate_kr.csv new file mode 100644 index 0000000..e00fee1 --- /dev/null +++ b/src/main/resources/district_and_coordinate_kr.csv @@ -0,0 +1,441 @@ +sidoGunguCode,sidoCode,sidoName,gunguName,latitude,longitude,sido +3000000,6110000,서울특별시,종로구,37.59491732,126.9773213,FALSE +3410000,6270000,대구광역시,중구,37.56014356,126.9959681,FALSE +3650000,6300000,대전광역시,중구,37.56014356,126.9959681,FALSE +3250000,6260000,부산광역시,중구,37.56014356,126.9959681,FALSE +3010000,6110000,서울특별시,중구,37.56014356,126.9959681,FALSE +3690000,6310000,울산광역시,중구,37.56014356,126.9959681,FALSE +3490000,6280000,인천광역시,중구,37.56014356,126.9959681,FALSE +3410000,6270000,대구광역시,중구,35.10547776,129.0315402,FALSE +3650000,6300000,대전광역시,중구,35.10547776,129.0315402,FALSE +3250000,6260000,부산광역시,중구,35.10547776,129.0315402,FALSE +3010000,6110000,서울특별시,중구,35.10547776,129.0315402,FALSE +3690000,6310000,울산광역시,중구,35.10547776,129.0315402,FALSE +3490000,6280000,인천광역시,중구,35.10547776,129.0315402,FALSE +3410000,6270000,대구광역시,중구,35.86653529,128.5936058,FALSE +3650000,6300000,대전광역시,중구,35.86653529,128.5936058,FALSE +3250000,6260000,부산광역시,중구,35.86653529,128.5936058,FALSE +3010000,6110000,서울특별시,중구,35.86653529,128.5936058,FALSE +3690000,6310000,울산광역시,중구,35.86653529,128.5936058,FALSE +3490000,6280000,인천광역시,중구,35.86653529,128.5936058,FALSE +3410000,6270000,대구광역시,중구,37.46753391,126.4805605,FALSE +3650000,6300000,대전광역시,중구,37.46753391,126.4805605,FALSE +3250000,6260000,부산광역시,중구,37.46753391,126.4805605,FALSE +3010000,6110000,서울특별시,중구,37.46753391,126.4805605,FALSE +3690000,6310000,울산광역시,중구,37.46753391,126.4805605,FALSE +3490000,6280000,인천광역시,중구,37.46753391,126.4805605,FALSE +3410000,6270000,대구광역시,중구,36.28086015,127.4110573,FALSE +3650000,6300000,대전광역시,중구,36.28086015,127.4110573,FALSE +3250000,6260000,부산광역시,중구,36.28086015,127.4110573,FALSE +3010000,6110000,서울특별시,중구,36.28086015,127.4110573,FALSE +3690000,6310000,울산광역시,중구,36.28086015,127.4110573,FALSE +3490000,6280000,인천광역시,중구,36.28086015,127.4110573,FALSE +3410000,6270000,대구광역시,중구,35.57104934,129.3082429,FALSE +3650000,6300000,대전광역시,중구,35.57104934,129.3082429,FALSE +3250000,6260000,부산광역시,중구,35.57104934,129.3082429,FALSE +3010000,6110000,서울특별시,중구,35.57104934,129.3082429,FALSE +3690000,6310000,울산광역시,중구,35.57104934,129.3082429,FALSE +3490000,6280000,인천광역시,중구,35.57104934,129.3082429,FALSE +3020000,6110000,서울특별시,용산구,37.53138497,126.979907,FALSE +3030000,6110000,서울특별시,성동구,37.55102969,127.0410585,FALSE +3040000,6110000,서울특별시,광진구,37.54670608,127.0857435,FALSE +3050000,6110000,서울특별시,동대문구,37.58195655,127.0548481,FALSE +3060000,6110000,서울특별시,중랑구,37.59780259,127.0928803,FALSE +3070000,6110000,서울특별시,성북구,37.6057019,127.0175795,FALSE +3080000,6110000,서울특별시,강북구,37.64347391,127.011189,FALSE +3090000,6110000,서울특별시,도봉구,37.66910208,127.0323688,FALSE +3100000,6110000,서울특별시,노원구,37.65251105,127.0750347,FALSE +3110000,6110000,서울특별시,은평구,37.61921128,126.9270229,FALSE +3120000,6110000,서울특별시,서대문구,37.57778531,126.9390631,FALSE +3130000,6110000,서울특별시,마포구,37.55931349,126.90827,FALSE +3140000,6110000,서울특별시,양천구,37.52478941,126.8554777,FALSE +3360000,6260000,부산광역시,강서구,37.56123543,126.822807,FALSE +3150000,6110000,서울특별시,강서구,37.56123543,126.822807,FALSE +3360000,6260000,부산광역시,강서구,35.13834787,128.8924006,FALSE +3150000,6110000,서울특별시,강서구,35.13834787,128.8924006,FALSE +3160000,6110000,서울특별시,구로구,37.49440543,126.8563006,FALSE +3170000,6110000,서울특별시,금천구,37.46056756,126.9008202,FALSE +3180000,6110000,서울특별시,영등포구,37.52230829,126.9101695,FALSE +3190000,6110000,서울특별시,동작구,37.49887688,126.9516415,FALSE +3200000,6110000,서울특별시,관악구,37.46737569,126.9453372,FALSE +3210000,6110000,서울특별시,서초구,37.47329547,127.0312203,FALSE +3220000,6110000,서울특별시,강남구,37.49664389,127.0629852,FALSE +3230000,6110000,서울특별시,송파구,37.50561924,127.115295,FALSE +3240000,6110000,서울특별시,강동구,37.55045024,127.1470118,FALSE +3600000,6290000,광주광역시,서구,35.10383663,129.0149537,FALSE +3430000,6270000,대구광역시,서구,35.10383663,129.0149537,FALSE +3660000,6300000,대전광역시,서구,35.10383663,129.0149537,FALSE +3260000,6260000,부산광역시,서구,35.10383663,129.0149537,FALSE +3560000,6280000,인천광역시,서구,35.10383663,129.0149537,FALSE +3600000,6290000,광주광역시,서구,35.87500167,128.5496976,FALSE +3430000,6270000,대구광역시,서구,35.87500167,128.5496976,FALSE +3660000,6300000,대전광역시,서구,35.87500167,128.5496976,FALSE +3260000,6260000,부산광역시,서구,35.87500167,128.5496976,FALSE +3560000,6280000,인천광역시,서구,35.87500167,128.5496976,FALSE +3600000,6290000,광주광역시,서구,37.55780443,126.6563778,FALSE +3430000,6270000,대구광역시,서구,37.55780443,126.6563778,FALSE +3660000,6300000,대전광역시,서구,37.55780443,126.6563778,FALSE +3260000,6260000,부산광역시,서구,37.55780443,126.6563778,FALSE +3560000,6280000,인천광역시,서구,37.55780443,126.6563778,FALSE +3600000,6290000,광주광역시,서구,35.13569311,126.8507191,FALSE +3430000,6270000,대구광역시,서구,35.13569311,126.8507191,FALSE +3660000,6300000,대전광역시,서구,35.13569311,126.8507191,FALSE +3260000,6260000,부산광역시,서구,35.13569311,126.8507191,FALSE +3560000,6280000,인천광역시,서구,35.13569311,126.8507191,FALSE +3600000,6290000,광주광역시,서구,36.28023963,127.3451041,FALSE +3430000,6270000,대구광역시,서구,36.28023963,127.3451041,FALSE +3660000,6300000,대전광역시,서구,36.28023963,127.3451041,FALSE +3260000,6260000,부산광역시,서구,36.28023963,127.3451041,FALSE +3560000,6280000,인천광역시,서구,36.28023963,127.3451041,FALSE +3590000,6290000,광주광역시,동구,35.12918632,129.0445856,FALSE +3420000,6270000,대구광역시,동구,35.12918632,129.0445856,FALSE +3640000,6300000,대전광역시,동구,35.12918632,129.0445856,FALSE +3270000,6260000,부산광역시,동구,35.12918632,129.0445856,FALSE +3710000,6310000,울산광역시,동구,35.12918632,129.0445856,FALSE +3500000,6280000,인천광역시,동구,35.12918632,129.0445856,FALSE +3590000,6290000,광주광역시,동구,35.93442633,128.6856599,FALSE +3420000,6270000,대구광역시,동구,35.93442633,128.6856599,FALSE +3640000,6300000,대전광역시,동구,35.93442633,128.6856599,FALSE +3270000,6260000,부산광역시,동구,35.93442633,128.6856599,FALSE +3710000,6310000,울산광역시,동구,35.93442633,128.6856599,FALSE +3500000,6280000,인천광역시,동구,35.93442633,128.6856599,FALSE +3590000,6290000,광주광역시,동구,37.48298658,126.6397466,FALSE +3420000,6270000,대구광역시,동구,37.48298658,126.6397466,FALSE +3640000,6300000,대전광역시,동구,37.48298658,126.6397466,FALSE +3270000,6260000,부산광역시,동구,37.48298658,126.6397466,FALSE +3710000,6310000,울산광역시,동구,37.48298658,126.6397466,FALSE +3500000,6280000,인천광역시,동구,37.48298658,126.6397466,FALSE +3590000,6290000,광주광역시,동구,35.11738023,126.9494643,FALSE +3420000,6270000,대구광역시,동구,35.11738023,126.9494643,FALSE +3640000,6300000,대전광역시,동구,35.11738023,126.9494643,FALSE +3270000,6260000,부산광역시,동구,35.11738023,126.9494643,FALSE +3710000,6310000,울산광역시,동구,35.11738023,126.9494643,FALSE +3500000,6280000,인천광역시,동구,35.11738023,126.9494643,FALSE +3590000,6290000,광주광역시,동구,36.32392023,127.4750374,FALSE +3420000,6270000,대구광역시,동구,36.32392023,127.4750374,FALSE +3640000,6300000,대전광역시,동구,36.32392023,127.4750374,FALSE +3270000,6260000,부산광역시,동구,36.32392023,127.4750374,FALSE +3710000,6310000,울산광역시,동구,36.32392023,127.4750374,FALSE +3500000,6280000,인천광역시,동구,36.32392023,127.4750374,FALSE +3590000,6290000,광주광역시,동구,35.52557365,129.4260655,FALSE +3420000,6270000,대구광역시,동구,35.52557365,129.4260655,FALSE +3640000,6300000,대전광역시,동구,35.52557365,129.4260655,FALSE +3270000,6260000,부산광역시,동구,35.52557365,129.4260655,FALSE +3710000,6310000,울산광역시,동구,35.52557365,129.4260655,FALSE +3500000,6280000,인천광역시,동구,35.52557365,129.4260655,FALSE +3280000,6260000,부산광역시,영도구,35.07868795,129.0648096,FALSE +3290000,6260000,부산광역시,부산진구,35.16524411,129.0430603,FALSE +3300000,6260000,부산광역시,동래구,35.20621244,129.0792201,FALSE +3610000,6290000,광주광역시,남구,35.12613679,129.0940064,FALSE +3440000,6270000,대구광역시,남구,35.12613679,129.0940064,FALSE +3310000,6260000,부산광역시,남구,35.12613679,129.0940064,FALSE +3700000,6310000,울산광역시,남구,35.12613679,129.0940064,FALSE +3510000,6280000,인천광역시,남구,35.12613679,129.0940064,FALSE +3610000,6290000,광주광역시,남구,35.83517716,128.5853296,FALSE +3440000,6270000,대구광역시,남구,35.83517716,128.5853296,FALSE +3310000,6260000,부산광역시,남구,35.83517716,128.5853296,FALSE +3700000,6310000,울산광역시,남구,35.83517716,128.5853296,FALSE +3510000,6280000,인천광역시,남구,35.83517716,128.5853296,FALSE +3610000,6290000,광주광역시,남구,37.45259578,126.6646585,FALSE +3440000,6270000,대구광역시,남구,37.45259578,126.6646585,FALSE +3310000,6260000,부산광역시,남구,37.45259578,126.6646585,FALSE +3700000,6310000,울산광역시,남구,37.45259578,126.6646585,FALSE +3510000,6280000,인천광역시,미추홀구,37.45259578,126.6646585,FALSE +3610000,6290000,광주광역시,남구,35.09405825,126.8567181,FALSE +3440000,6270000,대구광역시,남구,35.09405825,126.8567181,FALSE +3310000,6260000,부산광역시,남구,35.09405825,126.8567181,FALSE +3700000,6310000,울산광역시,남구,35.09405825,126.8567181,FALSE +3510000,6280000,인천광역시,남구,35.09405825,126.8567181,FALSE +3610000,6290000,광주광역시,남구,35.51604996,129.3282023,FALSE +3440000,6270000,대구광역시,남구,35.51604996,129.3282023,FALSE +3310000,6260000,부산광역시,남구,35.51604996,129.3282023,FALSE +3700000,6310000,울산광역시,남구,35.51604996,129.3282023,FALSE +3510000,6280000,인천광역시,남구,35.51604996,129.3282023,FALSE +3620000,6290000,광주광역시,북구,35.22922961,129.0234398,FALSE +3450000,6270000,대구광역시,북구,35.22922961,129.0234398,FALSE +3320000,6260000,부산광역시,북구,35.22922961,129.0234398,FALSE +3720000,6310000,울산광역시,북구,35.22922961,129.0234398,FALSE +3620000,6290000,광주광역시,북구,35.92892237,128.5772044,FALSE +3450000,6270000,대구광역시,북구,35.92892237,128.5772044,FALSE +3320000,6260000,부산광역시,북구,35.92892237,128.5772044,FALSE +3720000,6310000,울산광역시,북구,35.92892237,128.5772044,FALSE +3620000,6290000,광주광역시,북구,35.19324611,126.9254865,FALSE +3450000,6270000,대구광역시,북구,35.19324611,126.9254865,FALSE +3320000,6260000,부산광역시,북구,35.19324611,126.9254865,FALSE +3720000,6310000,울산광역시,북구,35.19324611,126.9254865,FALSE +3620000,6290000,광주광역시,북구,35.61005392,129.3798105,FALSE +3450000,6270000,대구광역시,북구,35.61005392,129.3798105,FALSE +3320000,6260000,부산광역시,북구,35.61005392,129.3798105,FALSE +3720000,6310000,울산광역시,북구,35.61005392,129.3798105,FALSE +3330000,6260000,부산광역시,해운대구,35.19385339,129.1535934,FALSE +3340000,6260000,부산광역시,사하구,35.0893484,128.974349,FALSE +3350000,6260000,부산광역시,금정구,35.25889074,129.0915307,FALSE +3370000,6260000,부산광역시,연제구,35.18241798,129.0829353,FALSE +3380000,6260000,부산광역시,수영구,35.16133146,129.1111627,FALSE +3390000,6260000,부산광역시,사상구,35.15803037,128.9865922,FALSE +3400000,6260000,부산광역시,기장군,35.29801166,129.200956,FALSE +3460000,6270000,대구광역시,수성구,35.83384886,128.6612744,FALSE +3470000,6270000,대구광역시,달서구,35.8274831,128.5292121,FALSE +3480000,6270000,대구광역시,달성군,35.75957305,128.4982981,FALSE +3520000,6280000,인천광역시,연수구,37.39856402,126.6473033,FALSE +3530000,6280000,인천광역시,남동구,37.43134886,126.726461,FALSE +3540000,6280000,인천광역시,부평구,37.49665874,126.7212094,FALSE +3550000,6280000,인천광역시,계양구,37.55729696,126.7346999,FALSE +3570000,6280000,인천광역시,강화군,37.71244395,126.4020881,FALSE +3580000,6280000,인천광역시,옹진군,37.54401126,125.6360222,FALSE +3630000,6290000,광주광역시,광산구,35.16503502,126.7528952,FALSE +3670000,6300000,대전광역시,유성구,36.37679049,127.3332564,FALSE +3680000,6300000,대전광역시,대덕구,36.41217868,127.4401518,FALSE +3730000,6310000,울산광역시,울주군,35.54661673,129.1869255,FALSE +3750000,3740000,경기도,수원시 장안구,37.31396569,127.0034517,FALSE +3760000,3740000,경기도,수원시 권선구,37.26053016,126.9797438,FALSE +3770000,3740000,경기도,수원시 팔달구,37.27746363,127.0162387,FALSE +5610000,3740000,경기도,수원시 영통구,37.27499039,127.0566989,FALSE +3790000,3780000,경기도,성남시 수정구,37.43516624,127.1041452,FALSE +3800000,3780000,경기도,성남시 중원구,37.43343323,127.1639099,FALSE +3810000,3780000,경기도,성남시 분당구,37.37930157,127.106064,FALSE +3820000,6410000,경기도,의정부시,37.73619211,127.0684231,FALSE +3830000,6410000,경기도,안양시,37.40413345,126.9113856,FALSE +3840000,3830000,경기도,안양시 만안구,37.40413345,126.9113856,FALSE +3850000,3830000,경기도,안양시 동안구,37.40039183,126.9555027,FALSE +3860000,6410000,경기도,부천시,37.50425833,126.7887109,FALSE +3900000,6410000,경기도,광명시,37.44515907,126.8647013,FALSE +3910000,6410000,경기도,평택시,37.01183071,126.9877007,FALSE +3920000,6410000,경기도,동두천시,37.91653761,127.0779127,FALSE +5550000,3930000,경기도,안산시 상록구,37.31600383,126.870815,FALSE +5560000,3930000,경기도,안산시 단원구,37.28192491,126.6940412,FALSE +3960000,3940000,경기도,고양시 일산구,37.67986514,126.7975582,FALSE +3950000,3940000,경기도,고양시 덕양구,37.65580987,126.8786321,FALSE +3960100,3940000,경기도,고양시 일산동구,37.67986514,126.7975582,FALSE +4100100,3940000,경기도,고양시 일산서구,37.68020512,126.7279835,FALSE +3970000,6410000,경기도,과천시,37.43384529,127.0026795,FALSE +3980000,6410000,경기도,구리시,37.59922149,127.1312299,FALSE +3990000,6410000,경기도,남양주시,37.66252981,127.2436606,FALSE +4000000,6410000,경기도,오산시,37.16329068,127.0513297,FALSE +4010000,6410000,경기도,시흥시,37.38939741,126.7883928,FALSE +4020000,6410000,경기도,군포시,37.34348269,126.9211347,FALSE +4030000,6410000,경기도,의왕시,37.36238774,126.9896255,FALSE +4040000,6410000,경기도,하남시,37.52281418,127.2059438,FALSE +4050000,6410000,경기도,용인시,37.2033319,127.2529331,FALSE +5620000,4050000,경기도,용인시 처인구,37.2033319,127.2529331,FALSE +5630000,4050000,경기도,용인시 기흥구,37.26742665,127.1213215,FALSE +5640000,4050000,경기도,용인시 수지구,37.33344743,127.0715511,FALSE +4060000,6410000,경기도,파주시,37.85619198,126.8107502,FALSE +4070000,6410000,경기도,이천시,37.20977588,127.4810141,FALSE +4080000,6410000,경기도,안성시,37.03502705,127.3027223,FALSE +4090000,6410000,경기도,김포시,37.68177237,126.6264306,FALSE +5530000,6410000,경기도,화성시,37.16523661,126.8748585,FALSE +5540000,6410000,경기도,광주시,37.40309074,127.301176,FALSE +5590000,6410000,경기도,양주시,37.80865756,127.0011349,FALSE +5600000,6410000,경기도,포천시,37.96977801,127.2503373,FALSE +5700000,6410000,경기도,여주시,37.30245842,127.6157373,FALSE +4140000,6410000,경기도,연천군,38.09272995,127.0244564,FALSE +4160000,6410000,경기도,가평군,37.81843317,127.4501921,FALSE +4170000,6410000,경기도,양평군,37.51805585,127.5792645,FALSE +4181000,6530000,강원특별자치도,춘천시,37.88979679,127.7398684,FALSE +4191000,6530000,강원특별자치도,원주시,37.30822288,127.9295253,FALSE +4201000,6530000,강원특별자치도,강릉시,37.70910197,128.8323789,FALSE +4211000,6530000,강원특별자치도,동해시,37.5066814,129.0555688,FALSE +4221000,6530000,강원특별자치도,태백시,37.17231241,128.9800726,FALSE +4231000,6530000,강원특별자치도,속초시,38.17603138,128.519541,FALSE +4241000,6530000,강원특별자치도,삼척시,37.27748089,129.12171,FALSE +4251000,6530000,강원특별자치도,홍천군,37.74504907,128.0742609,FALSE +4261000,6530000,강원특별자치도,횡성군,37.50914207,128.0770674,FALSE +4271000,6530000,강원특별자치도,영월군,37.20411361,128.500296,FALSE +4281000,6530000,강원특별자치도,평창군,37.55683921,128.48259,FALSE +4291000,6530000,강원특별자치도,정선군,37.37868629,128.7390632,FALSE +4301000,6530000,강원특별자치도,철원군,38.23908523,127.3989244,FALSE +4311000,6530000,강원특별자치도,화천군,38.13842628,127.6851657,FALSE +4321000,6530000,강원특별자치도,양구군,38.17560879,128.0002064,FALSE +4331000,6530000,강원특별자치도,인제군,38.06460026,128.2647272,FALSE +4341000,6530000,강원특별자치도,고성군,38.36275363,128.4111555,FALSE +5420000,6480000,경상남도,고성군,38.36275363,128.4111555,FALSE +4341000,6530000,강원특별자치도,고성군,35.01630447,128.2906632,FALSE +5420000,6480000,경상남도,고성군,35.01630447,128.2906632,FALSE +4351000,6530000,강원특별자치도,양양군,38.00448634,128.5950086,FALSE +5720000,6430000,충청북도,청주시 상당구,36.59211242,127.5848802,FALSE +4370000,4360000,충청북도,청주시 상당구,36.59211242,127.5848802,FALSE +5725000,6430000,충청북도,청주시 서원구,36.54726305,127.4384007,FALSE +5730000,6430000,충청북도,청주시 흥덕구,36.64696076,127.3692749,FALSE +4380000,4360000,충청북도,청주시 흥덕구,36.64696076,127.3692749,FALSE +5735000,6430000,충청북도,청주시 청원구,36.72057891,127.4913176,FALSE +4390000,6430000,충청북도,충주시,37.01519694,127.8956623,FALSE +4400000,6430000,충청북도,제천시,37.05991183,128.1409593,FALSE +4420000,6430000,충청북도,보은군,36.48994959,127.7293357,FALSE +4430000,6430000,충청북도,옥천군,36.32045611,127.6565589,FALSE +4440000,6430000,충청북도,영동군,36.15965896,127.8142281,FALSE +5570000,6430000,충청북도,증평군,36.78647301,127.6046181,FALSE +4450000,6430000,충청북도,진천군,36.87099952,127.4404636,FALSE +4460000,6430000,충청북도,괴산군,36.76965903,127.8295881,FALSE +4470000,6430000,충청북도,음성군,36.97622261,127.6142068,FALSE +4480000,6430000,충청북도,단양군,36.99445276,128.3878416,FALSE +4490000,6440000,충청남도,천안시,36.76411722,127.2208946,FALSE +5650000,4490000,충청남도,천안시 동남구,36.76411722,127.2208946,FALSE +5660000,4490000,충청남도,천안시 서북구,36.89271067,127.1618094,FALSE +4500000,6440000,충청남도,공주시,36.47981976,127.0752191,FALSE +4510000,6440000,충청남도,보령시,36.34024523,126.594247,FALSE +4520000,6440000,충청남도,아산시,36.80731633,126.9800756,FALSE +4530000,6440000,충청남도,서산시,36.78399346,126.4636016,FALSE +4540000,6440000,충청남도,논산시,36.19088691,127.1577164,FALSE +5580000,6440000,충청남도,계룡시,36.2915937,127.2344266,FALSE +5680000,6440000,충청남도,당진시,36.90325778,126.6527445,FALSE +4550000,6440000,충청남도,금산군,36.11900081,127.4783119,FALSE +4570000,6440000,충청남도,부여군,36.2463839,126.8569676,FALSE +4580000,6440000,충청남도,서천군,36.10517212,126.7079805,FALSE +4590000,6440000,충청남도,청양군,36.43058296,126.8531131,FALSE +4600000,6440000,충청남도,홍성군,36.57009431,126.6258536,FALSE +4610000,6440000,충청남도,예산군,36.67062867,126.7843109,FALSE +4620000,6440000,충청남도,태안군,36.7036613,126.2809571,FALSE +4641000,6540000,전북특별자치도,전주시,35.79209428,127.1195036,FALSE +4651000,4641000,전북특별자치도,전주시 완산구,35.79209428,127.1195036,FALSE +4661000,4641000,전북특별자치도,전주시 덕진구,35.85870118,127.1129157,FALSE +4671000,6540000,전북특별자치도,군산시,35.95043894,126.7260152,FALSE +4681000,6540000,전북특별자치도,익산시,36.02310555,126.9895102,FALSE +4691000,6540000,전북특별자치도,정읍시,35.60262466,126.9058575,FALSE +4701000,6540000,전북특별자치도,남원시,35.4225448,127.4418897,FALSE +4711000,6540000,전북특별자치도,김제시,35.80671644,126.8948857,FALSE +4721000,6540000,전북특별자치도,완주군,35.91861444,127.2151146,FALSE +4731000,6540000,전북특별자치도,진안군,35.82880745,127.4300354,FALSE +4741000,6540000,전북특별자치도,무주군,35.93936736,127.7129531,FALSE +4751000,6540000,전북특별자치도,장수군,35.65746098,127.5442641,FALSE +4761000,6540000,전북특별자치도,임실군,35.59820204,127.2366472,FALSE +4771000,6540000,전북특별자치도,순창군,35.4336343,127.090087,FALSE +4781000,6540000,전북특별자치도,고창군,35.44816757,126.6160462,FALSE +4791000,6540000,전북특별자치도,부안군,35.6779229,126.6443774,FALSE +4800000,6460000,전라남도,목포시,34.80376416,126.3918353,FALSE +4810000,6460000,전라남도,여수시,34.69617175,127.6532031,FALSE +4820000,6460000,전라남도,순천시,34.99474506,127.3891627,FALSE +4830000,6460000,전라남도,나주시,34.9885894,126.7204103,FALSE +4840000,6460000,전라남도,광양시,35.0219785,127.6550735,FALSE +4850000,6460000,전라남도,담양군,35.2914951,126.9952909,FALSE +4860000,6460000,전라남도,곡성군,35.21661355,127.2635825,FALSE +4870000,6460000,전라남도,구례군,35.23676622,127.5031193,FALSE +4880000,6460000,전라남도,고흥군,34.59848495,127.3146205,FALSE +4890000,6460000,전라남도,보성군,34.8143749,127.1621214,FALSE +4900000,6460000,전라남도,화순군,35.00818573,127.0334335,FALSE +4910000,6460000,전라남도,장흥군,34.67653701,126.9215323,FALSE +4920000,6460000,전라남도,강진군,34.62046495,126.7721517,FALSE +4930000,6460000,전라남도,해남군,34.5458247,126.5217898,FALSE +4940000,6460000,전라남도,영암군,34.79956672,126.6306945,FALSE +4950000,6460000,전라남도,무안군,34.95320449,126.4259079,FALSE +4960000,6460000,전라남도,함평군,35.11267054,126.5356041,FALSE +4970000,6460000,전라남도,영광군,35.27848603,126.4531293,FALSE +4980000,6460000,전라남도,장성군,35.32956437,126.7684949,FALSE +4990000,6460000,전라남도,완도군,34.29557933,126.7768069,FALSE +5000000,6460000,전라남도,진도군,34.43939638,126.2150578,FALSE +5010000,6460000,전라남도,신안군,34.81240654,126.048907,FALSE +5030000,5020000,경상북도,포항시 남구,35.95813526,129.4376545,FALSE +5040000,5020000,경상북도,포항시 북구,36.16507327,129.234009,FALSE +5050000,6470000,경상북도,경주시,35.8266428,129.2359296,FALSE +5060000,6470000,경상북도,김천시,36.06042386,128.0777075,FALSE +5070000,6470000,경상북도,안동시,36.58024221,128.7800427,FALSE +5080000,6470000,경상북도,구미시,36.20730964,128.3555555,FALSE +5090000,6470000,경상북도,영주시,36.8704754,128.5976826,FALSE +5100000,6470000,경상북도,영천시,36.01577844,128.942624,FALSE +5110000,6470000,경상북도,상주시,36.42950234,128.067007,FALSE +5120000,6470000,경상북도,문경시,36.69079151,128.1486082,FALSE +5130000,6470000,경상북도,경산시,35.83406007,128.8090552,FALSE +5140000,6470000,경상북도,군위군,36.17011208,128.6482246,FALSE +5141000,6270000,대구광역시,군위군,36.17011208,128.6482246,FALSE +5150000,6470000,경상북도,의성군,36.36204451,128.6150673,FALSE +5160000,6470000,경상북도,청송군,36.35697643,129.0573852,FALSE +5170000,6470000,경상북도,영양군,36.69638883,129.1450305,FALSE +5180000,6470000,경상북도,영덕군,36.48238769,129.3173762,FALSE +5190000,6470000,경상북도,청도군,35.67297478,128.786527,FALSE +5200000,6470000,경상북도,고령군,35.73719712,128.3067337,FALSE +5210000,6470000,경상북도,성주군,35.90722342,128.2333931,FALSE +5220000,6470000,경상북도,칠곡군,36.01551131,128.4625838,FALSE +5230000,6470000,경상북도,예천군,36.65384527,128.422384,FALSE +5240000,6470000,경상북도,봉화군,36.93414016,128.9129004,FALSE +5250000,6470000,경상북도,울진군,36.90391313,129.3123186,FALSE +5260000,6470000,경상북도,울릉군,37.50194192,130.864243,FALSE +5670123,5670000,경상남도,창원시 의창구,35.30894896,128.6495884,FALSE +5670140,5670000,경상남도,창원시 성산구,35.19618568,128.6721044,FALSE +5670156,5670000,경상남도,창원시 마산합포구,35.13545806,128.4852558,FALSE +5670184,5670000,경상남도,창원시 마산회원구,35.23222659,128.5364332,FALSE +5670206,5670000,경상남도,창원시 진해구,35.12994945,128.7363008,FALSE +5310000,6480000,경상남도,진주시,35.20515765,128.1298011,FALSE +5330000,6480000,경상남도,통영시,34.82932843,128.3740775,FALSE +5340000,6480000,경상남도,사천시,35.04970341,128.0376963,FALSE +5350000,6480000,경상남도,김해시,35.27215642,128.8452158,FALSE +5360000,6480000,경상남도,밀양시,35.4984997,128.7896015,FALSE +5370000,6480000,경상남도,거제시,34.8704352,128.6231395,FALSE +5380000,6480000,경상남도,양산시,35.40188821,129.0410414,FALSE +5390000,6480000,경상남도,의령군,35.39243102,128.2770578,FALSE +5400000,6480000,경상남도,함안군,35.29100281,128.4308769,FALSE +5410000,6480000,경상남도,창녕군,35.50822773,128.4930519,FALSE +5430000,6480000,경상남도,남해군,34.81829098,127.9411405,FALSE +5440000,6480000,경상남도,하동군,35.13830463,127.779049,FALSE +5450000,6480000,경상남도,산청군,35.36859736,127.8843379,FALSE +5460000,6480000,경상남도,함양군,35.55160171,127.7220411,FALSE +5470000,6480000,경상남도,거창군,35.73255219,127.9041696,FALSE +5480000,6480000,경상남도,합천군,35.57657745,128.1415437,FALSE +5490000,6490000,제주도,제주시,33.44220188,126.5292476,FALSE +6510000,6500000,제주특별자치도,제주시,33.44220188,126.5292476,FALSE +5500000,6490000,제주도,서귀포시,33.32504027,126.5810857,FALSE +6520000,6500000,제주특별자치도,서귀포시,33.32504027,126.5810857,FALSE +6530000,6530000,강원특별자치도,강원특별자치도,37.71904264,128.3008969,TRUE +6410000,6410000,경기도,경기도,37.53434923,127.1810501,TRUE +6480000,6480000,경상남도,경상남도,35.32449891,128.2611748,TRUE +6470000,6470000,경상북도,경상북도,36.34862579,128.748716,TRUE +6290000,6290000,광주광역시,광주광역시,35.15572822,126.8354348,TRUE +6270000,6270000,대구광역시,대구광역시,35.8298146,128.5653587,TRUE +6300000,6300000,대전광역시,대전광역시,36.3397636,127.3940388,TRUE +6260000,6260000,부산광역시,부산광역시,35.21033858,129.0691138,TRUE +6110000,6110000,서울특별시,서울특별시,37.55191813,126.9918238,TRUE +5690000,5690000,세종특별자치시,세종특별자치시,36.56072897,127.258722,TRUE +6310000,6310000,울산광역시,울산광역시,35.55422196,129.237579,TRUE +6280000,6280000,인천광역시,인천광역시,37.58457102,126.3755151,TRUE +6460000,6460000,전라남도,전라남도,34.87817002,126.9052332,TRUE +6540000,6540000,전북특별자치도,전북특별자치도,35.71581062,127.1427384,TRUE +6490000,6490000,제주도,제주도,33.38699923,126.5538395,TRUE +6500000,6500000,제주특별자치도,제주특별자치도,33.38699923,126.5538395,TRUE +6440000,6440000,충청남도,충청남도,36.52940199,126.8497393,TRUE +6430000,6430000,충청북도,충청북도,36.73877678,127.8313457,TRUE +,,,,,, +,,,,,, +,,,,,, +,,,,,, +,,,,,, +,,,,,, +,,,,,, +,,,,,, +,,,,,, +,,,,,, +,,,,,, +,,,,,, +,,,,,, +,,,,,, +,,,,,, +,,,,,, +,,,,,, +,,,,,, +,,,,,, +,,,,,, +,,,,,, +,,,,,, +,,,,,, +,,,,,, +,,,,,, +,,,,,, +,,,,,, +,,,,,, +,,,,,, +,,,,,, +,,,,,, +,,,,,, +,,,,,, +,,,,,, +,,,,,, +,,,,,, +,,,,,, +,,,,,, +,,,,,, +,,,,,, +,,,,,, +,,,,,, +,,,,,, +,,,,,, +5710000,6430000,충청북도,청주시,,, +6430140,6430000,충청북도,증평출장소,,, +4410000,6430000,충청북도,청원군,,, diff --git a/src/test/java/com/gamsa/dataupdate/ActivityDataUtilsTest.java b/src/test/java/com/gamsa/dataupdate/ActivityDataUtilsTest.java new file mode 100644 index 0000000..fcf92b7 --- /dev/null +++ b/src/test/java/com/gamsa/dataupdate/ActivityDataUtilsTest.java @@ -0,0 +1,55 @@ +package com.gamsa.dataupdate; + +import com.gamsa.activity.constant.Category; +import com.gamsa.dataupdate.utils.ActivityDataUtils; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.time.LocalDate; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +public class ActivityDataUtilsTest { + + @Autowired + private ActivityDataUtils activityDataUtils; + + @Test + void 기간별_활동_리스트_조회() { + //given + LocalDate today = LocalDate.now(); + LocalDate endDate = today.plusDays(7); + + //when + var list = activityDataUtils.getVolunteerParticipationList(today, endDate); + + //then + assertThat(list.size()).isNotZero(); + } + + @Test + void 기관_상세_조회() { + //given + String programNo = "3168803"; + + //when + var result = activityDataUtils.getInstituteApiResponse(programNo); + + //then + assertThat(result.getName()).isEqualTo("음성효심주간보호센터"); + } + + @Test + void 활동_상세_조회() { + //given + String programNo = "3168803"; + + //when + var result = activityDataUtils.getVolunteerDetail(programNo); + + //then + assertThat(result.getCategory()).isEqualTo(Category.CULTURE_ENVIRONMENT_AND_INTERNATIONAL_COOPERATION); + } +} diff --git a/src/test/java/com/gamsa/dataupdate/DataUpdateSchedulerTest.java b/src/test/java/com/gamsa/dataupdate/DataUpdateSchedulerTest.java new file mode 100644 index 0000000..b7d25b4 --- /dev/null +++ b/src/test/java/com/gamsa/dataupdate/DataUpdateSchedulerTest.java @@ -0,0 +1,43 @@ +package com.gamsa.dataupdate; + +import com.gamsa.dataupdate.service.ActivityDataUpdateService; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import java.time.LocalDate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; + +@SpringBootTest +public class DataUpdateSchedulerTest { + + @Autowired + private DataUpdateScheduler dataUpdateScheduler; + + @MockBean + private ActivityDataUpdateService activityDataUpdateService; + + @Test + public void 업데이트_메서드_동작() { + //given + int days = 7; + + //when + dataUpdateScheduler.runActivityDataUpdate(); + + //then + ArgumentCaptor captor = ArgumentCaptor.forClass(LocalDate.class); + verify(activityDataUpdateService).update(captor.capture(), captor.capture()); + + LocalDate today = LocalDate.now(); + LocalDate expectedEndDate = today.plusDays(days); + + assertThat(captor.getAllValues()) + .hasSize(2) + .containsExactly(today, expectedEndDate); + } +} \ No newline at end of file diff --git a/src/test/java/com/gamsa/dataupdate/KakaoLocalUtilsTest.java b/src/test/java/com/gamsa/dataupdate/KakaoLocalUtilsTest.java new file mode 100644 index 0000000..46ced13 --- /dev/null +++ b/src/test/java/com/gamsa/dataupdate/KakaoLocalUtilsTest.java @@ -0,0 +1,28 @@ +package com.gamsa.dataupdate; + +import com.gamsa.dataupdate.utils.KakaoLocalUtils; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +public class KakaoLocalUtilsTest { + + @Autowired + private KakaoLocalUtils kakaoLocalUtils; + + @Test + void 주소_검색_테스트() { + //given + String address = "경기 광주시 중앙로175번길 14 송정문화센터 2층"; + + //when + var result = kakaoLocalUtils.getCoordinateByAddress(address).orElseThrow(); + + //then + System.out.println(result); + assertThat(result).isNotNull(); + } +}