diff --git a/.github/workflows/deploy-lambda-dev.yml b/.github/workflows/deploy-lambda-dev.yml new file mode 100644 index 00000000..d9dbea61 --- /dev/null +++ b/.github/workflows/deploy-lambda-dev.yml @@ -0,0 +1,77 @@ +name: Deploy Lambda to Dev + +on: + workflow_dispatch: # 수동 실행 + push: + branches: + - develop # develop 브랜치 푸시 시 자동 실행 + +jobs: + deploy: + name: Deploy Lambda Dev + runs-on: ubuntu-latest + + env: + S3_BUCKET: sopt-makers-app + STACK_NAME: app-dev + AWS_REGION: ap-northeast-2 + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up JDK 21 + uses: actions/setup-java@v3 + with: + distribution: 'corretto' + java-version: '21' + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v2 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }} + aws-region: ${{ env.AWS_REGION }} + + - name: copy application.yml files + run: | + mkdir -p ./src/main/resources + mkdir -p ./src/test/resources + echo "${{ secrets.APPLICATION_DEV_YML }}" > ./src/main/resources/application-dev.yml + echo "${{ secrets.APPLICATION_LAMBDA_YML }}" > ./src/main/resources/application-lambda.yml + echo "${{ secrets.APPLICATION_TEST_YML }}" > ./src/test/resources/application-test.yml + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Build Lambda JAR + run: ./gradlew clean lambdaJar -x test + + - name: Upload JAR to S3 + run: | + # 빌드된 ZIP 파일 찾기 + JAR_FILE=$(ls build/distributions/*-lambda.zip | head -1) + + # 타임스탬프 생성 + TIMESTAMP=$(date +"%Y%m%d-%H%M%S") + S3_KEY="lambda/${{ env.STACK_NAME }}-${TIMESTAMP}.zip" + + # S3 업로드 + aws s3 cp "$JAR_FILE" "s3://${{ env.S3_BUCKET }}/$S3_KEY" + + echo "S3_KEY=$S3_KEY" >> $GITHUB_ENV + + - name: Install SAM CLI + uses: aws-actions/setup-sam@v2 + + - name: Deploy with SAM + working-directory: ./lambda + run: | + sam deploy \ + --config-env dev \ + --stack-name ${{ env.STACK_NAME }} \ + --no-fail-on-empty-changeset \ + --parameter-overrides \ + S3Bucket=${{ env.S3_BUCKET }} \ + S3Key=${{ env.S3_KEY }} \ + Profile="dev,lambda" diff --git a/.gitignore b/.gitignore index beb05097..b995d2c2 100755 --- a/.gitignore +++ b/.gitignore @@ -44,6 +44,7 @@ application-local.yml application-prod.yml application-dev.yml application-test.yml +application-lambda.yml docker-compose.yml ### HTTP 관련 diff --git a/build.gradle b/build.gradle index 40625703..cb831e1f 100755 --- a/build.gradle +++ b/build.gradle @@ -90,7 +90,6 @@ dependencies { // jwt implementation 'com.nimbusds:nimbus-jose-jwt:9.37.3' implementation 'com.github.ben-manes.caffeine:caffeine:3.1.8' - implementation 'org.springframework.boot:spring-boot-starter-webflux' // test testImplementation 'org.springframework.boot:spring-boot-starter-test' @@ -104,6 +103,11 @@ dependencies { // slack implementation 'com.slack.api:slack-api-client:1.30.0' + // AWS Lambda Dependencies for JAR deployment + implementation 'com.amazonaws.serverless:aws-serverless-java-container-springboot3:2.1.5' + implementation 'com.amazonaws:aws-lambda-java-core:1.4.0' + implementation 'com.amazonaws:aws-lambda-java-events:3.16.1' + } tasks.named('test') { useJUnitPlatform() @@ -118,4 +122,39 @@ dependencyManagement { mavenBom "org.springframework.cloud:spring-cloud-dependencies:$springCloudVersion" mavenBom "software.amazon.awssdk:bom:2.20.0" } +} + +// Lambda ZIP 빌드 설정 +task lambdaJar(type: Zip) { + dependsOn bootJar + archiveClassifier = 'lambda' + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + zip64 = true // 대용량 ZIP 파일 지원 + + // lib 디렉토리 구조로 패키징 + into('lib') { + from(jar) + from(configurations.runtimeClasspath) { + // Lambda에서 불필요한 파일 제외 + exclude "org/apache/tomcat/embed/**" + exclude "META-INF/*.SF" + exclude "META-INF/*.DSA" + exclude "META-INF/*.RSA" + exclude "META-INF/MANIFEST.MF" + exclude "**/module-info.class" + } + } +} + +// Lambda 빌드를 기본 빌드에 포함 +build.dependsOn lambdaJar + +// JAR 설정 +jar { + enabled = true + archiveClassifier = '' +} + +bootJar { + enabled = true } \ No newline at end of file diff --git a/lambda/lambda-deploy-test.sh b/lambda/lambda-deploy-test.sh new file mode 100644 index 00000000..8d023609 --- /dev/null +++ b/lambda/lambda-deploy-test.sh @@ -0,0 +1,59 @@ +#!/bin/bash + +# 간단한 Lambda JAR 배포 스크립트 +set -e # 에러 발생시 중단 + +# 설정 +ENV=${1:-dev} +S3_BUCKET="sopt-makers-app" +STACK_NAME="app-${ENV}" +AWS_REGION="ap-northeast-2" +TARGET_PROFILE="${ENV},lambda" + +# 색상 정의 +GREEN='\033[0;32m' +NC='\033[0m' # No Color + +echo "🚀 Lambda JAR 배포 시작 (환경: $ENV)" + +# 0. S3에서 yml 파일 가져오기 +#echo "📥 S3에서 설정 파일 다운로드 중..." +#aws s3 cp s3://${S3_BUCKET}/dev/deploy/application-lambda-dev.yml src/main/resources/application-lambda-dev.yml + +# 1. JAR 빌드 +echo "📦 JAR 빌드 중..." +./gradlew clean lambdaJar -x test + +# 2. S3 업로드 +JAR_FILE=$(ls build/distributions/*-lambda.zip | head -1) +TIMESTAMP=$(date +"%Y%m%d-%H%M%S") +S3_KEY="lambda/${STACK_NAME}-${TIMESTAMP}-lambda.zip" + +echo "☁️ S3 업로드 중..." +echo " 파일: $JAR_FILE" +echo " S3 경로: s3://${S3_BUCKET}/${S3_KEY}" +aws s3 cp "$JAR_FILE" "s3://${S3_BUCKET}/${S3_KEY}" + +# 3. SAM으로 배포 +echo "🔄 SAM 배포 중..." +cd lambda + +sam deploy \ + --config-env ${ENV} \ + --stack-name ${STACK_NAME} \ + --no-fail-on-empty-changeset \ + --parameter-overrides \ + "S3Bucket=${S3_BUCKET} S3Key=${S3_KEY} Profile=${TARGET_PROFILE}" + +cd .. + +echo -e "${GREEN}✅ 배포 완료!${NC}" + +# API 엔드포인트 출력 +API_ENDPOINT=$(aws cloudformation describe-stacks \ + --stack-name ${STACK_NAME} \ + --query "Stacks[0].Outputs[?OutputKey=='ApiEndpoint'].OutputValue" \ + --output text \ + --region ${AWS_REGION}) + +echo -e "${GREEN}🌐 API Endpoint: ${API_ENDPOINT}${NC}" \ No newline at end of file diff --git a/lambda/samconfig.toml b/lambda/samconfig.toml new file mode 100644 index 00000000..bca17501 --- /dev/null +++ b/lambda/samconfig.toml @@ -0,0 +1,21 @@ +version = 0.1 + +[default.build.parameters] +cached = true +parallel = true + +[dev.deploy.parameters] +stack_name = "app-dev" +region = "ap-northeast-2" +capabilities = "CAPABILITY_IAM" +confirm_changeset = false +template = "template-dev.yaml" +resolve_s3 = true + +[prod.deploy.parameters] +stack_name = "app-prod" +region = "ap-northeast-2" +capabilities = "CAPABILITY_IAM" +confirm_changeset = true # 운영은 변경사항 확인 +template = "template-prod.yaml" +resolve_s3 = true \ No newline at end of file diff --git a/lambda/template-dev.yaml b/lambda/template-dev.yaml new file mode 100644 index 00000000..8de29bd5 --- /dev/null +++ b/lambda/template-dev.yaml @@ -0,0 +1,79 @@ +AWSTemplateFormatVersion: "2010-09-09" +Transform: AWS::Serverless-2016-10-31 +Description: > + Sopt makers App Backend DEV - Spring Boot JAR on AWS Lambda with SnapStart + +Globals: + Function: + Timeout: 300 # 최대 실행 시간 (초) + MemorySize: 3072 # 메모리 (MB) - 프로젝트 크기에 따라 조정 + Runtime: java21 # Java 버전 + +Parameters: + S3Bucket: + Type: String + Default: "" + Description: S3 bucket containing the Lambda JAR + S3Key: + Type: String + Default: "" + Description: S3 key (path) to the Lambda JAR file + Profile: + Type: String + Default: "lambda,dev" + Description: Spring profile to use + + +Resources: + ApiFunction: + Type: AWS::Serverless::Function + Properties: + FunctionName: !Sub "${AWS::StackName}-api" + CodeUri: + Bucket: !Ref S3Bucket + Key: !Ref S3Key + Handler: org.sopt.app.LambdaHandler + SnapStart: + ApplyOn: PublishedVersions # SnapStart 활성화 + AutoPublishAlias: live + Environment: + Variables: + SPRING_PROFILES_ACTIVE: !Ref Profile + TZ: Asia/Seoul + JAVA_TOOL_OPTIONS: "-XX:+TieredCompilation -XX:TieredStopAtLevel=1" + Policies: + - AWSLambdaBasicExecutionRole + - Statement: + - Effect: Allow + Action: + - s3:GetObject + Resource: !Sub "arn:aws:s3:::${S3Bucket}/*" + Events: + ApiProxy: + Type: Api + Properties: + RestApiId: !Ref ApiGateway + Path: /{proxy+} + Method: ANY + ApiRoot: + Type: Api + Properties: + RestApiId: !Ref ApiGateway + Path: / + Method: ANY + + ApiGateway: + Type: AWS::Serverless::Api + Properties: + StageName: dev + EndpointConfiguration: REGIONAL + Cors: + AllowOrigin: "'*'" + AllowHeaders: "'*'" + AllowMethods: "'*'" + AllowCredentials: "'false'" + +Outputs: + ApiEndpoint: + Description: "API Gateway endpoint URL" + Value: !Sub "[https://${ApiGateway}.execute-api.${AWS::Region}.amazonaws.com/dev](https://${ApiGateway}.execute-api.${AWS::Region}.amazonaws.com/dev)" \ No newline at end of file diff --git a/src/main/java/org/sopt/app/AppApplication.java b/src/main/java/org/sopt/app/AppApplication.java index 18f5ded8..dcbd282b 100755 --- a/src/main/java/org/sopt/app/AppApplication.java +++ b/src/main/java/org/sopt/app/AppApplication.java @@ -1,6 +1,7 @@ package org.sopt.app; import jakarta.annotation.PostConstruct; +import java.util.TimeZone; import org.sopt.app.common.external.auth.AuthClientProperty; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @@ -8,12 +9,8 @@ import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.scheduling.annotation.EnableAsync; -import java.util.TimeZone; -import org.springframework.scheduling.annotation.EnableScheduling; - @EnableJpaAuditing // JPA Auditing(감시, 감사) 기능을 활성화 하는 어노테이션 createdDate, modifiedDate 저장 활성화 @EnableAsync -@EnableScheduling @SpringBootApplication @EnableConfigurationProperties(AuthClientProperty.class) public class AppApplication { diff --git a/src/main/java/org/sopt/app/LambdaHandler.java b/src/main/java/org/sopt/app/LambdaHandler.java new file mode 100644 index 00000000..e249beec --- /dev/null +++ b/src/main/java/org/sopt/app/LambdaHandler.java @@ -0,0 +1,40 @@ +package org.sopt.app; + +import com.amazonaws.serverless.exceptions.ContainerInitializationException; +import com.amazonaws.serverless.proxy.model.AwsProxyRequest; +import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.spring.SpringBootLambdaContainerHandler; +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestStreamHandler; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class LambdaHandler implements RequestStreamHandler { + + private static SpringBootLambdaContainerHandler handler; + + static { + try { + handler = SpringBootLambdaContainerHandler.getAwsProxyHandler(AppApplication.class); + + // Swagger + SpringBootLambdaContainerHandler.getContainerConfig().addBinaryContentTypes( + "image/png", + "image/jpeg", + "image/gif", + "application/octet-stream" + ); + } catch (ContainerInitializationException e) { + throw new RuntimeException("Could not initialize Spring Boot application", e); + } catch (Exception e) { + throw new RuntimeException("Could not initialize Lambda handler", e); + } + } + + @Override + public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) + throws IOException { + handler.proxyStream(inputStream, outputStream, context); + } +} diff --git a/src/main/java/org/sopt/app/application/rank/RankScheduler.java b/src/main/java/org/sopt/app/application/rank/RankScheduler.java index 21acf79a..a1dec65f 100644 --- a/src/main/java/org/sopt/app/application/rank/RankScheduler.java +++ b/src/main/java/org/sopt/app/application/rank/RankScheduler.java @@ -1,14 +1,13 @@ package org.sopt.app.application.rank; import java.util.List; +import lombok.RequiredArgsConstructor; import org.sopt.app.application.soptamp.SoptampUserFinder; import org.sopt.app.application.soptamp.SoptampUserInfo; import org.springframework.beans.factory.annotation.Value; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; -import lombok.RequiredArgsConstructor; - @Service @RequiredArgsConstructor public class RankScheduler { @@ -21,6 +20,10 @@ public class RankScheduler { // 매일 오전 4시에 정합성을 맞추기 위해 스케쥴링 @Scheduled(cron = "0 0 4 * * *") public void initialSoptampRank(){ + executeSoptampRank(); + } + + public void executeSoptampRank(){ if (appjamMode) { return; } diff --git a/src/main/java/org/sopt/app/common/config/SchedulingConfig.java b/src/main/java/org/sopt/app/common/config/SchedulingConfig.java new file mode 100644 index 00000000..a34f8b40 --- /dev/null +++ b/src/main/java/org/sopt/app/common/config/SchedulingConfig.java @@ -0,0 +1,11 @@ +package org.sopt.app.common.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.scheduling.annotation.EnableScheduling; + +@Configuration +@EnableScheduling +@Profile("!lambda") +public class SchedulingConfig { +} diff --git a/src/main/java/org/sopt/app/common/external/auth/AuthClient.java b/src/main/java/org/sopt/app/common/external/auth/AuthClient.java index 8a04bbf2..1728e21c 100644 --- a/src/main/java/org/sopt/app/common/external/auth/AuthClient.java +++ b/src/main/java/org/sopt/app/common/external/auth/AuthClient.java @@ -4,32 +4,37 @@ import lombok.extern.slf4j.Slf4j; import org.sopt.app.common.exception.ClientException; import org.sopt.app.common.response.ErrorCode; +import org.springframework.http.HttpStatusCode; import org.springframework.stereotype.Component; -import org.springframework.web.reactive.function.client.WebClient; -import org.springframework.web.reactive.function.client.WebClientResponseException; +import org.springframework.web.client.RestClient; @Slf4j @Component @RequiredArgsConstructor public class AuthClient { - private final WebClient authWebClient; + private final RestClient authRestClient; // WebClient -> RestClient 변경 private final AuthClientProperty authProperty; public String getJwk() { try { - return authWebClient.get() - .uri(authProperty.endpoints().jwk()) - .retrieve() - .bodyToMono(String.class) - .onErrorMap(WebClientResponseException.class, ex -> { - log.error("Failed to receive response from Auth server: {}", ex.getResponseBodyAsString(), ex); - return new ClientException(ErrorCode.RESPONSE_ERROR); - }) - .block(); - } catch (RuntimeException e) { - log.error("Unexpected exception occurred during Auth server communication: {}", e.getMessage(), e); + return authRestClient.get() + .uri(authProperty.endpoints().jwk()) + .retrieve() + .onStatus(HttpStatusCode::isError, (request, response) -> { + String errorBody = new String(response.getBody().readAllBytes()); + log.error("Failed to receive response from Auth server: {}", errorBody); + throw new ClientException(ErrorCode.RESPONSE_ERROR); + }) + .body(String.class); + + } catch (ClientException e) { // onStatus에서 던진 예외만 그대로 다시 던짐 + throw e; + } catch (Exception e) { + log.error("Unexpected exception occurred during Auth server communication: {}", + e.getMessage(), e); throw new ClientException(ErrorCode.COMMUNICATION_ERROR); } } -} \ No newline at end of file + +} diff --git a/src/main/java/org/sopt/app/common/external/auth/AuthRestClientConfig.java b/src/main/java/org/sopt/app/common/external/auth/AuthRestClientConfig.java new file mode 100644 index 00000000..c399c6ee --- /dev/null +++ b/src/main/java/org/sopt/app/common/external/auth/AuthRestClientConfig.java @@ -0,0 +1,33 @@ +package org.sopt.app.common.external.auth; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.web.client.RestClient; + +@Configuration +public class AuthRestClientConfig { + + public static final String HEADER_API_KEY = "X-Api-Key"; + public static final String HEADER_SERVICE_NAME = "X-Service-Name"; + private static final int TIMEOUT_MILLIS = 5000; + + @Bean + public RestClient authWebClient(AuthClientProperty property) { + + SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); + requestFactory.setConnectTimeout(TIMEOUT_MILLIS); + requestFactory.setReadTimeout(TIMEOUT_MILLIS); + + return RestClient.builder() + .baseUrl(property.url()) + .requestFactory(requestFactory) // 타임아웃 설정 적용 + .defaultHeader(HEADER_API_KEY, property.apiKey()) + .defaultHeader(HEADER_SERVICE_NAME, property.serviceName()) + .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .build(); + } + +} diff --git a/src/main/java/org/sopt/app/common/external/auth/AuthWebClientConfig.java b/src/main/java/org/sopt/app/common/external/auth/AuthWebClientConfig.java deleted file mode 100644 index 3a6f0611..00000000 --- a/src/main/java/org/sopt/app/common/external/auth/AuthWebClientConfig.java +++ /dev/null @@ -1,44 +0,0 @@ -package org.sopt.app.common.external.auth; - -import io.netty.channel.ChannelOption; -import io.netty.handler.timeout.ReadTimeoutHandler; -import io.netty.handler.timeout.WriteTimeoutHandler; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; -import org.springframework.http.client.reactive.ReactorClientHttpConnector; -import org.springframework.web.reactive.function.client.WebClient; -import reactor.netty.http.client.HttpClient; - -import java.time.Duration; - -@Configuration -public class AuthWebClientConfig { - - public static final String HEADER_API_KEY = "X-Api-Key"; - public static final String HEADER_SERVICE_NAME = "X-Service-Name"; - private static final int TIMEOUT_MILLIS = 5000; - private static final int TIMEOUT_SECONDS = 5; - - @Bean - public WebClient authWebClient(AuthClientProperty property) { - return WebClient.builder() - .baseUrl(property.url()) - .clientConnector(new ReactorClientHttpConnector(createDefaultHttpClient())) - .defaultHeader(HEADER_API_KEY, property.apiKey()) - .defaultHeader(HEADER_SERVICE_NAME, property.serviceName()) - .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) - .build(); - } - - private HttpClient createDefaultHttpClient() { - return HttpClient.create() - .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, TIMEOUT_MILLIS) - .responseTimeout(Duration.ofSeconds(TIMEOUT_SECONDS)) - .doOnConnected(conn -> conn - .addHandlerLast(new ReadTimeoutHandler(TIMEOUT_SECONDS)) - .addHandlerLast(new WriteTimeoutHandler(TIMEOUT_SECONDS)) - ); - } -} diff --git a/src/main/java/org/sopt/app/facade/HomeFacade.java b/src/main/java/org/sopt/app/facade/HomeFacade.java index 4a698681..ff16fcbe 100755 --- a/src/main/java/org/sopt/app/facade/HomeFacade.java +++ b/src/main/java/org/sopt/app/facade/HomeFacade.java @@ -1,11 +1,11 @@ package org.sopt.app.facade; -import static org.sopt.app.common.utils.HtmlTagWrapper.*; +import static org.sopt.app.common.utils.HtmlTagWrapper.wrapWithTag; import java.util.List; import java.util.Map; import java.util.stream.Collectors; - +import lombok.RequiredArgsConstructor; import org.sopt.app.application.appservice.AppServiceBadgeService; import org.sopt.app.application.appservice.AppServiceName; import org.sopt.app.application.appservice.AppServiceService; @@ -32,8 +32,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import lombok.RequiredArgsConstructor; - @Service @RequiredArgsConstructor public class HomeFacade { @@ -74,14 +72,15 @@ public List checkAppServiceEntryStatus(Long userI // TODO : 추후 유저 생성 api response 변경해 생성 api 쪽에서 soptamp user upsert 하도록 변경 PlatformUserInfoResponse platformUserInfo = platformService.getPlatformUserInfoResponse(userId); - soptampUserService.upsertSoptampUser(platformUserInfo, userId); - return appServiceService.getAllAppService().stream() - .filter(appServiceInfo -> isServiceVisibleToUser(appServiceInfo, status)) - .map(appServiceInfo -> appServiceBadgeService.getAppServiceEntryStatusResponse( - appServiceInfo, userId - )) - .toList(); + List appServiceEntryStatusResponses = appServiceService.getAllAppService().stream() + .filter(appServiceInfo -> isServiceVisibleToUser(appServiceInfo, status)) + .map(appServiceInfo -> appServiceBadgeService.getAppServiceEntryStatusResponse( + appServiceInfo, userId + )) + .toList(); + soptampUserService.upsertSoptampUser(platformUserInfo, userId); + return appServiceEntryStatusResponses; } private List getOnlyAppServiceInfo() { diff --git a/src/main/java/org/sopt/app/presentation/admin/schedule/AdminScheduleController.java b/src/main/java/org/sopt/app/presentation/admin/schedule/AdminScheduleController.java new file mode 100644 index 00000000..b1033cba --- /dev/null +++ b/src/main/java/org/sopt/app/presentation/admin/schedule/AdminScheduleController.java @@ -0,0 +1,43 @@ +package org.sopt.app.presentation.admin.schedule; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import lombok.RequiredArgsConstructor; +import org.sopt.app.application.rank.RankScheduler; +import org.sopt.app.common.exception.BadRequestException; +import org.sopt.app.common.response.ErrorCode; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Profile; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v2/admin/schedule") +@SecurityRequirement(name = "Authorization") +@Profile("lambda") +public class AdminScheduleController { + private final RankScheduler rankScheduler; + + @Value("${makers.app.admin.password}") + private String adminPassword; + + @Operation(summary = "스탬프/포인트 랭킹 데이터 정합성 맞추기") + @PostMapping("/soptamp/sync-rank-cache") + public ResponseEntity syncSoptampRankCache( + @RequestHeader("x-admin-password") String password + ){ + validateAdmin(password); + rankScheduler.executeSoptampRank(); + return ResponseEntity.ok().build(); + } + + private void validateAdmin(String password) { + if (!adminPassword.equals(password)) { + throw new BadRequestException(ErrorCode.INVALID_APP_ADMIN_PASSWORD); + } + } +}