Skip to content

Commit

Permalink
Merge branch 'develop' into feat/#30
Browse files Browse the repository at this point in the history
  • Loading branch information
ckkim817 authored Jan 20, 2025
2 parents c52d25c + c0e3d6d commit 5ee0407
Show file tree
Hide file tree
Showing 63 changed files with 1,012 additions and 160 deletions.
25 changes: 2 additions & 23 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,29 +13,8 @@ out/
!**/src/main/**/out/
!**/src/test/**/out/

### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/

### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/

### VS Code ###
.vscode/

### Mac OS ###
.DS_Store

*.yml
*.yml
application.properties
16 changes: 13 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,22 @@ dependencies {
// Database
implementation 'org.postgresql:postgresql:42.7.4'

// MapStruct
implementation 'org.mapstruct:mapstruct:1.6.3'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.6.3'
testAnnotationProcessor 'org.mapstruct:mapstruct-processor:1.6.3'

// FeignClient
implementation "org.springframework.cloud:spring-cloud-starter-openfeign:4.2.0"

// TODO: 버전 확인하기
// Google
implementation 'com.google.api-client:google-api-client:2.7.0'

// JWT
implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
implementation group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
implementation group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'

// OpenFeign
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign:4.2.0'
}

tasks.named('test') {
Expand Down
2 changes: 1 addition & 1 deletion scripts/prepare-commit-msg
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ CURRENT_BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD)

ISSUE_NUMBER=$(echo "$CURRENT_BRANCH_NAME" | grep -o '/#.*' | sed 's/\///')

if [[ "$DEFAULT_COMMIT_MSG" =~ ^[Mm]erge ]]; then
if [[ "$DEFAULT_COMMIT_MSG" =~ ^[Mm]erge || "$DEFAULT_COMMIT_MSG" =~ ^[Hh]otfix ]]; then
SUFFIX=""
else
SUFFIX="($ISSUE_NUMBER)"
Expand Down
2 changes: 0 additions & 2 deletions src/main/java/com/acon/server/ServerApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableFeignClients
public class ServerApplication {

public static void main(String[] args) {
Expand Down
43 changes: 43 additions & 0 deletions src/main/java/com/acon/server/common/auth/jwt/JwtUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.acon.server.common.auth.jwt;

import com.acon.server.common.auth.jwt.config.JwtConfig;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import java.util.Date;
import java.util.List;
import javax.crypto.SecretKey;
import org.springframework.stereotype.Component;

@Component
public class JwtUtils {

private final SecretKey secretKey;
private static final long ACCESS_TOKEN_EXPIRE_TIME = 1000 * 60 * 30; // 30분
private static final long REFRESH_TOKEN_EXPIRE_TIME = 1000 * 60 * 60 * 24 * 30L; // 30일
private static final String JWT_KEY = "memberId";

public JwtUtils(JwtConfig jwtConfig) {
byte[] keyBytes = jwtConfig.getSecretKey().getBytes();
this.secretKey = Keys.hmacShaKeyFor(keyBytes);
}

public List<String> createToken(Long memberId) {
long now = (new Date()).getTime();
Date accessTokenExpiryTime = new Date(now + ACCESS_TOKEN_EXPIRE_TIME);
Date refreshTokenExpiryTime = new Date(now + REFRESH_TOKEN_EXPIRE_TIME);

String accessToken = Jwts.builder()
.claim(JWT_KEY, memberId)
.setExpiration(accessTokenExpiryTime)
.signWith(secretKey, SignatureAlgorithm.HS512)
.compact();

String refreshToken = Jwts.builder()
.setExpiration(refreshTokenExpiryTime)
.signWith(secretKey, SignatureAlgorithm.HS512)
.compact();

return List.of(accessToken, refreshToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.acon.server.common.auth.jwt.config;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "jwt")
@RequiredArgsConstructor
@Getter
@Setter
public class JwtConfig {

private final String secretKey;
}
25 changes: 25 additions & 0 deletions src/main/java/com/acon/server/global/config/AppStartupConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.acon.server.global.config;

import com.acon.server.spot.application.service.SpotService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@RequiredArgsConstructor
@Slf4j
public class AppStartupConfig {

private final SpotService spotService;

@Bean
public ApplicationRunner applicationRunner() {
return args -> {
log.info("애플리케이션 시작 시 Spot 데이터를 업데이트하는 작업을 시작합니다.");
spotService.updateNullCoordinatesForSpots();
log.info("Spot 데이터 업데이트 작업이 완료되었습니다.");
};
}
}
12 changes: 12 additions & 0 deletions src/main/java/com/acon/server/global/config/OpenFeignConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.acon.server.global.config;

import com.acon.server.ServerApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableFeignClients(basePackageClasses = ServerApplication.class)
public class OpenFeignConfig {

// TODO: 타임아웃 설정 추가, 서킷 브레이커 적용하기
}
10 changes: 10 additions & 0 deletions src/main/java/com/acon/server/global/config/SchedulingConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.acon.server.global.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;

@Configuration
@EnableScheduling
public class SchedulingConfig {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.acon.server.global.config.properties;

import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@Getter
@Setter
@ConfigurationProperties(prefix = "naver.maps")
public class NaverMapsProperties {

private String clientId;
private String clientSecret;
}
20 changes: 16 additions & 4 deletions src/main/java/com/acon/server/global/exception/ErrorType.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public enum ErrorType {

// TODO: ErrorType code 한 번 싹 정리

/* Common Error */
/* 400 Bad Request */
INVALID_PATH_ERROR(HttpStatus.BAD_REQUEST, 40001, "요청 경로의 변수 값이 허용된 형식과 다릅니다."),
Expand All @@ -19,6 +21,7 @@ public enum ErrorType {
INVALID_REQUEST_BODY_ERROR(HttpStatus.BAD_REQUEST, 40006, "유효하지 않은 Request Body입니다. 요청 형식 또는 필드를 확인하세요."),
DATA_INTEGRITY_VIOLATION_ERROR(HttpStatus.BAD_REQUEST, 40007, "데이터 무결성 제약 조건을 위반했습니다."),
INVALID_ACCESS_TOKEN_ERROR(HttpStatus.BAD_REQUEST, 40008, "유효하지 않은 accessToken입니다."),
// TODO: NonNull 필드에 null 값이 입력되었을 때 발생하는 예외 처리 추가

/* 401 Unauthorized */
EXPIRED_ACCESS_TOKEN_ERROR(HttpStatus.UNAUTHORIZED, 40101, "만료된 accessToken입니다."),
Expand All @@ -33,18 +36,27 @@ public enum ErrorType {
/* Member Error */
/* 400 Bad Request */
INVALID_SOCIAL_TYPE_ERROR(HttpStatus.BAD_REQUEST, 40009, "유효하지 않은 socialType입니다."),
INVALID_ID_TOKEN_ERROR(HttpStatus.BAD_REQUEST, 40010, "ID 토큰의 서명이 올바르지 않습니다."),
INVALID_DISLIKE_FOOD_ERROR(HttpStatus.BAD_REQUEST, 40013, "유효하지 않은 dislikeFood입니다."),
INVALID_CUISINE_ERROR(HttpStatus.BAD_REQUEST, 40014, "유효하지 않은 cuisine입니다."),
INVALID_SPOT_TYPE_ERROR(HttpStatus.BAD_REQUEST, 40015, "유효하지 않은 spotType입니다."),
INVALID_FAVORITE_SPOT_ERROR(HttpStatus.BAD_REQUEST, 40016, "유효하지 않은 spotStyle입니다."),
INVALID_SPOT_STYLE_ERROR(HttpStatus.BAD_REQUEST, 40017, "유효하지 않은 favoriteSpot입니다."),
INVALID_SPOT_STYLE_ERROR(HttpStatus.BAD_REQUEST, 40016, "유효하지 않은 spotStyle입니다."),
INVALID_FAVORITE_SPOT_ERROR(HttpStatus.BAD_REQUEST, 40017, "유효하지 않은 favoriteSpot입니다."),
INVALID_FAVORITE_SPOT_RANK_SIZE_ERROR(HttpStatus.BAD_REQUEST, 40030, "favoriteSpotRank의 사이즈가 잘못되었습니다."),
INVALID_FAVORITE_CUISINE_RANK_SIZE_ERROR(HttpStatus.BAD_REQUEST, 40031, "favoriteCuisineRank의 사이즈가 잘못되었습니다."),

/* 500 Internal Server Error */
FAILED_DOWNLOAD_GOOGLE_PUBLIC_KEY_ERROR(HttpStatus.BAD_REQUEST, 50002, "구글 공개키 다운로드에 실패하였습니다."),

/* Spot Error */
/* 400 Bad Request */
INVALID_DAY_ERROR(HttpStatus.BAD_REQUEST, 40099, "유효하지 않은 day입니다."),

/* 404 Not Found */
NOT_FOUND_SPOT_ERROR(HttpStatus.NOT_FOUND, 40402, "유효한 장소가 없습니다"),
NOT_FOUND_SPOT_ERROR(HttpStatus.NOT_FOUND, 40402, "존재하지 않는 장소입니다."),

/* 500 Internal Server Error */
NAVER_MAPS_GEOCODING_API_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 50003, "Naver Maps GeoCoding API 호출에 실패했습니다."),
;

private final HttpStatus httpStatus;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.acon.server.global.external;

public record GeoCodingResponse(
String latitude,
String longitude
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.acon.server.global.external;

import com.acon.server.global.exception.BusinessException;
import com.acon.server.global.exception.ErrorType;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class NaverMapsAdapter {

private final NaverMapsClient naverMapsClient;

public GeoCodingResponse getGeoCodingResult(String address) {
// TODO: try-catch로 감싸기
return Optional.ofNullable(naverMapsClient.getGeoCode(address))
.map(response -> (List<Map<String, Object>>) response.get("addresses"))
.filter(addresses -> !addresses.isEmpty())
.map(addresses -> addresses.get(0))
.map(firstAddress -> new GeoCodingResponse(
(String) firstAddress.get("x"),
(String) firstAddress.get("y")
))
.orElseThrow(
() -> new BusinessException(ErrorType.NAVER_MAPS_GEOCODING_API_ERROR));
}
}
16 changes: 11 additions & 5 deletions src/main/java/com/acon/server/global/external/NaverMapsClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,21 @@
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(name = "naverMapsClient", url = "https://naveropenapi.apigw.ntruss.com")
@FeignClient(
name = "naverMapsClient",
url = "https://naveropenapi.apigw.ntruss.com",
configuration = NaverMapsFeignConfig.class
)
public interface NaverMapsClient {

@GetMapping(value = "/map-reversegeocode/v2/gc", headers = "Accept=application/json")
@GetMapping(value = "/map-geocode/v2/geocode")
Map<String, Object> getGeoCode(
@RequestParam("query") String query
);

@GetMapping(value = "/map-reversegeocode/v2/gc")
Map<String, Object> getReverseGeocode(
@RequestHeader("X-NCP-APIGW-API-KEY-ID") String apiKeyId,
@RequestHeader("X-NCP-APIGW-API-KEY") String apiKey,
@RequestParam("coords") String coords,
@RequestParam("orders") String orders,
@RequestParam("output") String output
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.acon.server.global.external;

import com.acon.server.global.config.properties.NaverMapsProperties;
import feign.RequestInterceptor;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;

@RequiredArgsConstructor
public class NaverMapsFeignConfig {

private final NaverMapsProperties naverMapsProperties;

@Bean
public RequestInterceptor requestInterceptor() {
return requestTemplate -> {
requestTemplate.header("X-NCP-APIGW-API-KEY-ID", naverMapsProperties.getClientId());
requestTemplate.header("X-NCP-APIGW-API-KEY", naverMapsProperties.getClientSecret());
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ public ResponseEntity<ErrorResponse> handleNoHandlerFoundException(NoHandlerFoun
.body(ErrorResponse.fail(ErrorType.NOT_FOUND_PATH_ERROR, e.getRequestURL()));
}

// TODO: NonNull 필드에 null 값이 입력되었을 때 발생하는 예외 처리 추가
// TODO: 존재하지 않는 경로로 요청이 들어왔을 시 예외 처리 추가

// 비즈니스 로직에서 발생하는 예외 처리
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleCustomException(BusinessException e) {
Expand Down
23 changes: 23 additions & 0 deletions src/main/java/com/acon/server/global/scheduler/SpotScheduler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.acon.server.global.scheduler;

import com.acon.server.spot.application.service.SpotService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
@Slf4j
public class SpotScheduler {

private final SpotService spotService;

// 매 시 정각에 실행
@Scheduled(cron = "0 0 * * * *")
public void scheduleUpdateCoordinates() {
log.info("스케줄링 작업: 위도 또는 경도 정보가 비어 있는 Spot 데이터 업데이트 시작");
spotService.updateNullCoordinatesForSpots();
log.info("스케줄링 작업: Spot 데이터 업데이트 완료");
}
}
Loading

0 comments on commit 5ee0407

Please sign in to comment.