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 new file mode 100644 index 0000000..c6a5f88 --- /dev/null +++ b/.github/workflows/prod-ci.yml @@ -0,0 +1,43 @@ +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: 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: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ap-northeast-2 + + - name: upload to S3 + run: | + aws s3 cp deploy.tar.gz s3://gamja-bongsa 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/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/build.gradle b/build.gradle index 7d091ce..4416840 100644 --- a/build.gradle +++ b/build.gradle @@ -24,7 +24,7 @@ repositories { } dependencies { - implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + implementation 'com.opencsv:opencsv:5.5.2' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' @@ -34,6 +34,12 @@ 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' + + implementation 'com.mysql:mysql-connector-j:9.1.0' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' annotationProcessor 'org.projectlombok:lombok' @@ -44,4 +50,4 @@ dependencies { tasks.named('test') { useJUnitPlatform() -} +} \ No newline at end of file diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100644 index 0000000..8e195ce --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +REPOSITORY=/home/ubuntu/deploy +APP_NAME=gamsa + +cd $REPOSITORY + +echo "> 현재 구동 중인 애플리케이션 pid 확인" +CURRENT_PID=$(lsof -i :8080 -t) + +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 | head -n 1) + +echo "> JAR Name: $JAR_NAME" + +echo "> $JAR_NAME에 실행권한 추가" +chmod +x $JAR_NAME + +echo "> $JAR_NAME 실행" +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/java/com/gamsa/Application.java b/src/main/java/com/gamsa/Application.java index df58f1c..a5a8742 100644 --- a/src/main/java/com/gamsa/Application.java +++ b/src/main/java/com/gamsa/Application.java @@ -2,14 +2,16 @@ 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; +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/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()); + } +} 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/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/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/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/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..3e6133d 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; @@ -7,8 +9,23 @@ @RequiredArgsConstructor public enum Experienced { NOVICE("초심자"), - INTERMIDIATE("중급자"), + INTERMEDIATE("중급자"), 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..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 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 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 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 3efd859..c5e95ef 100644 --- a/src/main/java/com/gamsa/avatar/dto/AvatarSaveRequest.java +++ b/src/main/java/com/gamsa/avatar/dto/AvatarSaveRequest.java @@ -3,33 +3,35 @@ 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; 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; @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 b55c41a..503ca7c 100644 --- a/src/main/java/com/gamsa/avatar/service/AvatarService.java +++ b/src/main/java/com/gamsa/avatar/service/AvatarService.java @@ -4,41 +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.findById(avatar.getAvatarId()).orElseThrow(NoSuchElementException::new); - 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/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/java/com/gamsa/history/controller/HistoryController.java b/src/main/java/com/gamsa/history/controller/HistoryController.java index 1dac62f..f5ba140 100644 --- a/src/main/java/com/gamsa/history/controller/HistoryController.java +++ b/src/main/java/com/gamsa/history/controller/HistoryController.java @@ -1,30 +1,60 @@ 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; 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.*; +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 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, Pageable pageable) { - return historyService.findSliceByAvatarId(avatarId, pageable); + @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) { + pageable = PageRequest.of(0, MAX_SIZE, Sort.unsorted()); + } else { + pageable = PageRequest.of(page, size, Sort.unsorted()); + } + 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-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 diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 202f78a..e2c4134 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -3,3 +3,17 @@ spring: name: gamja-bongsa profiles: active: dev + +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/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/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(); + } +} 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); + } +}