Repository: Study-AID/study-AID-backend
Pull request #50: feat: upload the pdf file and pub SQS message
Author: login: mingrammer
Created: 2025-05-15T10:29:00Z | Merged: 2025-05-17T11:19:06Z

# Ticket

task-3-2-6

# Slack

<!-- 작업과 관련된 슬랙 스레드 링크를 입력해주세요. -->
<!-- [Slack Thread](...) -->

# Description

Lecture 생성시 file upload 및 SQS 메시지 발행 로직을 구현해요.

# Checklist

<!-- 머지하기 전 확인해야 할 사항을 체크해주세요. -->


Diff:
diff --git a/Makefile b/Makefile
index 384336b0..aaa64b0d 100644
--- a/Makefile
+++ b/Makefile
@@ -38,8 +38,15 @@ build:
 # Start all services
 run:
 	docker compose up -d
+	@echo "Waiting for services to be ready..."
+	@sleep 5
+	@echo "Initializing LocalStack..."
+	@bash scripts/init-localstack.sh
 	@echo "API Server is running at http://localhost:8080/api"
 	@echo "Swagger UI is available at http://localhost:8080/api/swagger-ui.html"
+	@echo "MinIO Console is available at http://localhost:9001"
+	@echo "  - Username: minioadmin"
+	@echo "  - Password: minioadmin"
 
 # Follow logs
 logs:
diff --git a/build.gradle b/build.gradle
index b22ba6ba..24512706 100644
--- a/build.gradle
+++ b/build.gradle
@@ -62,6 +62,7 @@ dependencies {
 
     implementation platform('software.amazon.awssdk:bom:2.25.26') // AWS SDK BOM
     implementation 'software.amazon.awssdk:s3'
+    implementation 'software.amazon.awssdk:sqs'
 }
 
 tasks.named('test') {
diff --git a/docker-compose.yml b/docker-compose.yml
index d076e710..5e01dfcb 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -14,6 +14,8 @@ services:
       - postgres
       - redis
       - minio
+      - createbuckets
+      - localstack
     environment:
       DB_HOST: postgres
       DB_PORT: 5432
@@ -81,7 +83,7 @@ services:
     environment:
       MINIO_ROOT_USER: minioadmin
       MINIO_ROOT_PASSWORD: minioadmin
-    command: server /data
+    command: server /data --console-address ":9001"
     volumes:
       - ./minio/data:/data
     networks:
@@ -93,6 +95,37 @@ services:
       retries: 5
     restart: unless-stopped
 
+  createbuckets:
+    image: minio/mc:latest
+    depends_on:
+      minio:
+        condition: service_healthy
+    entrypoint: >
+      /bin/sh -c "
+      /usr/bin/mc alias set myminio http://minio:9000 minioadmin minioadmin;
+      /usr/bin/mc mb myminio/study-aid-lecture-materials --ignore-existing;
+      /usr/bin/mc policy set public myminio/study-aid-lecture-materials;
+      exit 0;
+      "
+    networks:
+      - app-network
+
+  localstack:
+    image: localstack/localstack:latest
+    container_name: localstack
+    ports:
+      - "4566:4566"
+    environment:
+      - SERVICES=sqs
+      - DEBUG=1
+      - DATA_DIR=/tmp/localstack/data
+      - AWS_ACCESS_KEY_ID=test
+      - AWS_SECRET_ACCESS_KEY=test
+      - AWS_REGION=ap-northeast-2
+    networks:
+      - app-network
+    restart: unless-stopped
+
 networks:
   app-network:
     driver: bridge
@@ -101,3 +134,4 @@ volumes:
   postgres-data:
   redis-data:
   gradle-cache:
+  localstack-data:
diff --git a/scripts/init-localstack.sh b/scripts/init-localstack.sh
new file mode 100644
index 00000000..58a7539f
--- /dev/null
+++ b/scripts/init-localstack.sh
@@ -0,0 +1,40 @@
+#!/bin/bash
+
+# Wait for LocalStack to be ready
+echo "Waiting for LocalStack to be ready..."
+max_retries=30
+retry_count=0
+
+while [ $retry_count -lt $max_retries ]; do
+  # Simply check if LocalStack is responding
+  if curl -sf http://localhost:4566/_localstack/health > /dev/null; then
+    echo "LocalStack is ready"
+    break
+  fi
+  echo "LocalStack is unavailable - sleeping"
+  sleep 2
+  retry_count=$((retry_count + 1))
+done
+
+if [ $retry_count -eq $max_retries ]; then
+  echo "Failed to connect to LocalStack after $max_retries attempts"
+  exit 1
+fi
+
+echo "LocalStack is up - creating SQS queue"
+
+# Create SQS queue using AWS CLI command via curl
+QUEUE_NAME="local-study-aid-summarize-lecture"
+
+# Create queue
+echo "Creating queue: $QUEUE_NAME"
+curl -X POST "http://localhost:4566/" \
+  -H "Content-Type: application/x-www-form-urlencoded" \
+  -d "Action=CreateQueue&QueueName=$QUEUE_NAME&Version=2012-11-05"
+
+echo -e "\n\nListing queues..."
+curl -X POST "http://localhost:4566/" \
+  -H "Content-Type: application/x-www-form-urlencoded" \
+  -d "Action=ListQueues&Version=2012-11-05"
+
+echo -e "\n\nSetup completed!"
\ No newline at end of file
diff --git a/src/main/java/com/example/api/adapters/sqs/GenerateSummaryMessage.java b/src/main/java/com/example/api/adapters/sqs/GenerateSummaryMessage.java
new file mode 100644
index 00000000..e89143ec
--- /dev/null
+++ b/src/main/java/com/example/api/adapters/sqs/GenerateSummaryMessage.java
@@ -0,0 +1,24 @@
+package com.example.api.adapters.sqs;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.OffsetDateTime;
+import java.util.UUID;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class GenerateSummaryMessage {
+    private String schemaVersion;
+    private UUID requestId;
+    private OffsetDateTime occurredAt;
+    private UUID userId;
+    private UUID courseId;
+    private UUID lectureId;
+    private String s3Bucket;
+    private String s3Key;
+}
diff --git a/src/main/java/com/example/api/adapters/sqs/SQSClient.java b/src/main/java/com/example/api/adapters/sqs/SQSClient.java
new file mode 100644
index 00000000..ca98edd9
--- /dev/null
+++ b/src/main/java/com/example/api/adapters/sqs/SQSClient.java
@@ -0,0 +1,5 @@
+package com.example.api.adapters.sqs;
+
+public interface SQSClient {
+    void sendGenerateSummaryMessage(GenerateSummaryMessage message);
+}
diff --git a/src/main/java/com/example/api/adapters/sqs/SQSClientImpl.java b/src/main/java/com/example/api/adapters/sqs/SQSClientImpl.java
new file mode 100644
index 00000000..e28e8499
--- /dev/null
+++ b/src/main/java/com/example/api/adapters/sqs/SQSClientImpl.java
@@ -0,0 +1,45 @@
+package com.example.api.adapters.sqs;
+
+import com.example.api.config.SQSMessageConfig;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import lombok.RequiredArgsConstructor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+import software.amazon.awssdk.services.sqs.SqsClient;
+import software.amazon.awssdk.services.sqs.model.SendMessageRequest;
+import software.amazon.awssdk.services.sqs.model.SendMessageResponse;
+
+@Component
+@RequiredArgsConstructor
+public class SQSClientImpl implements SQSClient {
+    private static final Logger logger = LoggerFactory.getLogger(SQSClientImpl.class);
+
+    private final SqsClient sqsClient;
+    private final SQSMessageConfig sqsMessageConfig;
+
+    @Override
+    public void sendGenerateSummaryMessage(GenerateSummaryMessage message) {
+        try {
+            ObjectMapper mapper = new ObjectMapper();
+            mapper.registerModule(new JavaTimeModule());
+
+            String messageBody = mapper.writeValueAsString(message);
+
+            SendMessageRequest sendMessageRequest = SendMessageRequest.builder()
+                    .queueUrl(sqsMessageConfig.getGenerateSummary().getQueueUrl())
+                    .messageBody(messageBody)
+                    .build();
+
+            SendMessageResponse response = sqsClient.sendMessage(sendMessageRequest);
+
+            logger.info("Successfully sent generate summary message with requestId: {} and messageId: {}",
+                    message.getRequestId(), response.messageId());
+
+        } catch (Exception e) {
+            logger.error("Failed to send generate summary message for lectureId: {}", message.getLectureId(), e);
+            throw new RuntimeException("Failed to send SQS message", e);
+        }
+    }
+}
diff --git a/src/main/java/com/example/api/config/LocalStackConfig.java b/src/main/java/com/example/api/config/LocalStackConfig.java
new file mode 100644
index 00000000..50acd49d
--- /dev/null
+++ b/src/main/java/com/example/api/config/LocalStackConfig.java
@@ -0,0 +1,14 @@
+package com.example.api.config;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+@ConfigurationProperties(prefix = "localstack")
+@Getter
+@Setter
+public class LocalStackConfig {
+    private String endpoint;
+}
diff --git a/src/main/java/com/example/api/config/MinioConfig.java b/src/main/java/com/example/api/config/MinioConfig.java
new file mode 100644
index 00000000..8e536ed3
--- /dev/null
+++ b/src/main/java/com/example/api/config/MinioConfig.java
@@ -0,0 +1,28 @@
+package com.example.api.config;
+
+import io.minio.MinioClient;
+import lombok.Getter;
+import lombok.Setter;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Profile;
+
+@Configuration
+@ConfigurationProperties(prefix = "minio")
+@Profile("local")
+@Getter
+@Setter
+public class MinioConfig {
+    private String endpoint;
+    private String accessKey;
+    private String secretKey;
+
+    @Bean
+    public MinioClient minioClient() {
+        return MinioClient.builder()
+                .endpoint(endpoint)
+                .credentials(accessKey, secretKey)
+                .build();
+    }
+}
diff --git a/src/main/java/com/example/api/config/OpenApiConfig.java b/src/main/java/com/example/api/config/OpenApiConfig.java
index 38a05407..99d3eb93 100644
--- a/src/main/java/com/example/api/config/OpenApiConfig.java
+++ b/src/main/java/com/example/api/config/OpenApiConfig.java
@@ -1,8 +1,10 @@
 package com.example.api.config;
 
+import io.swagger.v3.oas.models.Components;
 import io.swagger.v3.oas.models.OpenAPI;
-import io.swagger.v3.oas.models.info.Contact;
 import io.swagger.v3.oas.models.info.Info;
+import io.swagger.v3.oas.models.security.SecurityRequirement;
+import io.swagger.v3.oas.models.security.SecurityScheme;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 
@@ -12,11 +14,15 @@ public class OpenApiConfig {
     public OpenAPI customOpenAPI() {
         return new OpenAPI()
                 .info(new Info()
-                        .title("API Server")
-                        .description("Spring Boot REST API Server")
-                        .version("v1.0.0")
-                        .contact(new Contact()
-                                .name("API Team")
-                                .email("api-team@example.com")));
+                        .title("Study AID API")
+                        .version("v1")
+                        .description("Study AID Backend API"))
+                .components(new Components()
+                        .addSecuritySchemes("bearerAuth", new SecurityScheme()
+                                .type(SecurityScheme.Type.HTTP)
+                                .scheme("bearer")
+                                .bearerFormat("JWT")
+                                .description("JWT token from login")))
+                .addSecurityItem(new SecurityRequirement().addList("bearerAuth"));
     }
 }
diff --git a/src/main/java/com/example/api/config/S3Config.java b/src/main/java/com/example/api/config/S3Config.java
new file mode 100644
index 00000000..51b21860
--- /dev/null
+++ b/src/main/java/com/example/api/config/S3Config.java
@@ -0,0 +1,19 @@
+package com.example.api.config;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Profile;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.services.s3.S3Client;
+
+@Configuration
+@Profile({"dev", "prod"})
+public class S3Config {
+    @Bean
+    public S3Client s3Client(@Value("${aws.region:ap-northeast-2}") String region) {
+        return S3Client.builder()
+                .region(Region.of(region))
+                .build();
+    }
+}
diff --git a/src/main/java/com/example/api/config/SQSConfig.java b/src/main/java/com/example/api/config/SQSConfig.java
new file mode 100644
index 00000000..f4f7fd9b
--- /dev/null
+++ b/src/main/java/com/example/api/config/SQSConfig.java
@@ -0,0 +1,41 @@
+package com.example.api.config;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Profile;
+import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
+import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
+import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.services.sqs.SqsClient;
+
+import java.net.URI;
+
+@Configuration
+@RequiredArgsConstructor
+public class SQSConfig {
+    private final LocalStackConfig localStackConfig;
+
+    @Bean
+    @Profile("local")
+    public SqsClient localStackSqsClient() {
+        AwsCredentialsProvider credentialsProvider = StaticCredentialsProvider.create(
+                AwsBasicCredentials.create("test", "test")
+        );
+
+        return SqsClient.builder()
+                .endpointOverride(URI.create(localStackConfig.getEndpoint()))
+                .region(Region.AP_NORTHEAST_2)
+                .credentialsProvider(credentialsProvider)
+                .build();
+    }
+
+    @Bean
+    @Profile({"dev", "prod"})
+    public SqsClient awsSqsClient() {
+        return SqsClient.builder()
+                .region(Region.AP_NORTHEAST_2)
+                .build();
+    }
+}
diff --git a/src/main/java/com/example/api/config/SQSMessageConfig.java b/src/main/java/com/example/api/config/SQSMessageConfig.java
new file mode 100644
index 00000000..ba61a855
--- /dev/null
+++ b/src/main/java/com/example/api/config/SQSMessageConfig.java
@@ -0,0 +1,20 @@
+package com.example.api.config;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+@ConfigurationProperties(prefix = "sqs")
+@Getter
+@Setter
+public class SQSMessageConfig {
+    private GenerateSummary generateSummary = new GenerateSummary();
+
+    @Getter
+    @Setter
+    public static class GenerateSummary {
+        private String queueUrl;
+    }
+}
diff --git a/src/main/java/com/example/api/config/StorageConfig.java b/src/main/java/com/example/api/config/StorageConfig.java
index 952f06f3..d0eb2c87 100644
--- a/src/main/java/com/example/api/config/StorageConfig.java
+++ b/src/main/java/com/example/api/config/StorageConfig.java
@@ -1,33 +1,14 @@
 package com.example.api.config;
 
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.context.annotation.Profile;
-
-import io.minio.MinioClient;
-import software.amazon.awssdk.regions.Region;
-import software.amazon.awssdk.services.s3.S3Client;
-
-@Configuration
+import lombok.Getter;
+import lombok.Setter;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+@Component
+@ConfigurationProperties(prefix = "storage")
+@Getter
+@Setter
 public class StorageConfig {
-    @Bean
-    @Profile({"dev", "prod"})
-    public S3Client s3Client(
-        @Value("${aws.region}") String region) {
-        return S3Client.builder()
-                .region(Region.of(region))
-                .build();
-    }
-
-    @Bean
-    @Profile("local")
-    public MinioClient minioClient(@Value("${minio.endpoint}") String endpoint,
-                                  @Value("${minio.access-key}") String accessKey,
-                                  @Value("${minio.secret-key}") String secretKey) {
-        return MinioClient.builder()
-                .endpoint(endpoint)
-                .credentials(accessKey, secretKey)
-                .build();
-    }
+    private String bucket;
 }
diff --git a/src/main/java/com/example/api/config/StorageProperties.java b/src/main/java/com/example/api/config/StorageProperties.java
deleted file mode 100644
index a5c0f391..00000000
--- a/src/main/java/com/example/api/config/StorageProperties.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.example.api.config;
-
-import org.springframework.boot.context.properties.ConfigurationProperties;
-import org.springframework.stereotype.Component;
-
-import lombok.Getter;
-import lombok.Setter;
-
-@Component
-@ConfigurationProperties(prefix = "storage")
-@Getter
-@Setter
-public class StorageProperties {
-    private String bucket;
-}
diff --git a/src/main/java/com/example/api/controller/LectureController.java b/src/main/java/com/example/api/controller/LectureController.java
index dea46c67..eba9a642 100644
--- a/src/main/java/com/example/api/controller/LectureController.java
+++ b/src/main/java/com/example/api/controller/LectureController.java
@@ -1,22 +1,12 @@
 package com.example.api.controller;
 
-import java.util.List;
-import java.util.Optional;
-import java.util.UUID;
-
-import com.example.api.controller.dto.lecture.CreateLectureRequest;
-import com.example.api.controller.dto.lecture.LectureListResponse;
-import com.example.api.controller.dto.lecture.LectureResponse;
-import com.example.api.controller.dto.lecture.UpdateLectureDisplayOrderLexRequest;
-import com.example.api.controller.dto.lecture.UpdateLectureRequest;
+import com.example.api.adapters.sqs.GenerateSummaryMessage;
+import com.example.api.adapters.sqs.SQSClient;
+import com.example.api.controller.dto.lecture.*;
 import com.example.api.service.CourseService;
 import com.example.api.service.LectureService;
-import com.example.api.service.dto.lecture.CreateLectureInput;
-import com.example.api.service.dto.lecture.LectureListOutput;
-import com.example.api.service.dto.lecture.LectureOutput;
-import com.example.api.service.dto.lecture.UpdateLectureDisplayOrderLexInput;
-import com.example.api.service.dto.lecture.UpdateLectureInput;
-
+import com.example.api.service.StorageService;
+import com.example.api.service.dto.lecture.*;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.media.Content;
@@ -24,54 +14,67 @@
 import io.swagger.v3.oas.annotations.responses.ApiResponse;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import jakarta.persistence.EntityNotFoundException;
-
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
 
+import java.time.OffsetDateTime;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
 
 @RestController
 @RequestMapping("/v1/lectures")
 @Tag(name = "lecture", description = "Lecture API")
+@Slf4j
 public class LectureController extends BaseController {
-    private LectureService lectureService;
-    private CourseService courseService;
+    private final LectureService lectureService;
+    private final CourseService courseService;
+    private final StorageService storageService;
+    private final SQSClient sqsClient;
 
-    public LectureController(LectureService lectureService, CourseService courseService) {
+    public LectureController(
+            LectureService lectureService, CourseService courseService, StorageService storageService, SQSClient sqsClient
+    ) {
         this.lectureService = lectureService;
         this.courseService = courseService;
+        this.storageService = storageService;
+        this.sqsClient = sqsClient;
     }
 
     @GetMapping("/course/{courseId}")
     @Operation(
-        summary = "Get all lectures for a specific course",
-        description = "Retrieves a list of all lectures for a specific course",
-        parameters = {
-            @Parameter(
-                name = "courseId",
-                description = "ID of the lecture",
-                required = true
-            )
-        },
-        responses = {
-            @ApiResponse(
-                responseCode = "200",
-                description = "Successfully retrieved lectures",
-                content = @Content(schema = @Schema(implementation = LectureListResponse.class))
-            ),
-            @ApiResponse(
-                responseCode = "403",
-                description = "User does not have access to this lecture"
-            ),
-            @ApiResponse(
-                responseCode = "404",
-                description = "Lecture not found"
-            ),
-            @ApiResponse(
-                responseCode = "500",
-                description = "Internal server error"
-            )
-        }
+            summary = "Get all lectures for a specific course",
+            description = "Retrieves a list of all lectures for a specific course",
+            parameters = {
+                    @Parameter(
+                            name = "courseId",
+                            description = "ID of the course",
+                            required = true
+                    )
+            },
+            responses = {
+                    @ApiResponse(
+                            responseCode = "200",
+                            description = "Successfully retrieved lectures",
+                            content = @Content(schema = @Schema(implementation = LectureListResponse.class))
+                    ),
+                    @ApiResponse(
+                            responseCode = "403",
+                            description = "User does not have access to this lecture"
+                    ),
+                    @ApiResponse(
+                            responseCode = "404",
+                            description = "Lecture not found"
+                    ),
+                    @ApiResponse(
+                            responseCode = "500",
+                            description = "Internal server error"
+                    )
+            }
     )
     public ResponseEntity<LectureListResponse> getLecturesByCourse(@PathVariable UUID courseId) {
         UUID userId = getAuthenticatedUserId();
@@ -93,37 +96,38 @@ public ResponseEntity<LectureListResponse> getLecturesByCourse(@PathVariable UUI
 
     @GetMapping("/{id}")
     @Operation(
-        summary = "Get a specific lecture by ID",
-        description = "Retrieves a specific lecture by its ID",
-        parameters = {
-            @Parameter(
-                name = "id",
-                description = "ID of the lecture to retrieve",
-                required = true
-            )
-        },
-        responses = {
-            @ApiResponse(
-                responseCode = "200",
-                description = "Successfully retrieved lecture",
-                content = @Content(schema = @Schema(implementation = LectureResponse.class))
-            ),
-            @ApiResponse(
-                responseCode = "403",
-                description = "User does not have access to this lecture"
-            ),
-            @ApiResponse(
-                responseCode = "404",
-                description = "Lecture not found"
-            ),
-            @ApiResponse(
-                responseCode = "500",
-                description = "Internal server error"
-            )
-        }
+            summary = "Get a specific lecture by ID",
+            description = "Retrieves a specific lecture by its ID",
+            parameters = {
+                    @Parameter(
+                            name = "id",
+                            description = "ID of the lecture to retrieve",
+                            required = true
+                    )
+            },
+            responses = {
+                    @ApiResponse(
+                            responseCode = "200",
+                            description = "Successfully retrieved lecture",
+                            content = @Content(schema = @Schema(implementation = LectureResponse.class))
+                    ),
+                    @ApiResponse(
+                            responseCode = "403",
+                            description = "User does not have access to this lecture"
+                    ),
+                    @ApiResponse(
+                            responseCode = "404",
+                            description = "Lecture not found"
+                    ),
+                    @ApiResponse(
+                            responseCode = "500",
+                            description = "Internal server error"
+                    )
+            }
     )
     public ResponseEntity<LectureResponse> getLectureById(@PathVariable UUID id) {
         UUID userId = getAuthenticatedUserId();
+
         try {
             // Check if the lecture exists
             Optional<LectureOutput> lectureOutput = lectureService.findLectureById(id);
@@ -139,40 +143,79 @@ public ResponseEntity<LectureResponse> getLectureById(@PathVariable UUID id) {
         }
     }
 
-    @PostMapping
+    private String determineMaterialType(MultipartFile file) {
+        String contentType = file.getContentType();
+        String filename = file.getOriginalFilename();
+
+        // Currently only PDF is supported
+        if (contentType != null && contentType.equals("application/pdf")) {
+            return "pdf";
+        }
+
+        // Fallback to filename extension
+        if (filename != null && filename.toLowerCase().endsWith(".pdf")) {
+            return "pdf";
+        }
+
+        // TODO: Add support for audio (mp3, wav) and presentation (pptx) formats in the future
+        throw new IllegalArgumentException("Only PDF files are supported. Uploaded file type: " + contentType);
+    }
+
+    private String generateDisplayOrderLex() {
+        // TODO: Implement proper lexicographic ordering generation
+        // This is a placeholder implementation
+        return String.valueOf(System.currentTimeMillis());
+    }
+
+    private void sendGenerateSummaryMessage(LectureOutput lectureOutput, String fileKey, UUID userId) {
+        GenerateSummaryMessage message = GenerateSummaryMessage.builder()
+                .schemaVersion("1.0.0")
+                .requestId(UUID.randomUUID())
+                .occurredAt(OffsetDateTime.now())
+                .userId(userId)
+                .courseId(lectureOutput.getCourseId())
+                .lectureId(lectureOutput.getId())
+                .s3Bucket(storageService.getBucket())
+                .s3Key(fileKey)
+                .build();
+
+        try {
+            sqsClient.sendGenerateSummaryMessage(message);
+            log.info("Successfully sent generate summary message for lecture: {}", lectureOutput.getId());
+        } catch (Exception e) {
+            log.error("Failed to send generate summary message for lecture: {}", lectureOutput.getId(), e);
+            // Don't fail the whole request if SQS message fails
+        }
+    }
+
+    @PostMapping(consumes = "multipart/form-data")
     @Operation(
-        summary = "Create a new lecture",
-        description = "Creates a new lecture",
-        requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(
-            description = "Lecture details (courseId, title)",
-            required = true,
-            content = @Content(schema = @Schema(implementation = CreateLectureRequest.class))
-        ),
-        responses = {
-            @ApiResponse(
-                responseCode = "201",
-                description = "Successfully created lecture",
-                content = @Content(schema = @Schema(implementation = LectureResponse.class))
-            ),
-            @ApiResponse(
-                responseCode = "400",
-                description = "Invalid input data"
+            description = "Creates a new lecture",
+            requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(
+                    content = @Content(
+                            mediaType = MediaType.MULTIPART_FORM_DATA_VALUE,
+                            schema = @Schema(implementation = CreateLectureRequest.class)
+                    )
             ),
-            @ApiResponse(
-                responseCode = "500",
-                description = "Internal server error"
-            )
-        }
+            responses = {
+                    @ApiResponse(
+                            responseCode = "201",
+                            description = "Successfully created lecture",
+                            content = @Content(schema = @Schema(implementation = LectureResponse.class))
+                    ),
+                    @ApiResponse(
+                            responseCode = "400",
+                            description = "Invalid input data"
+                    ),
+                    @ApiResponse(
+                            responseCode = "500",
+                            description = "Internal server error"
+                    ),
+            }
     )
-    public ResponseEntity<LectureResponse> createLecture(@RequestBody CreateLectureRequest request) {
+    public ResponseEntity<LectureResponse> createLecture(@ModelAttribute CreateLectureRequest request) {
         UUID userId = getAuthenticatedUserId();
 
-        // TODO(yoon): Remove this placeholder data and use actual data from server data
-        String testTitle = "Test Title";
-        String testMaterialPath = "test/path";
-        String testMaterialType = "pdf";
-        String testDisplayOrderLex = "1";
-
         try {
             // Check if the course exists
             var courseOutput = courseService.findCourseById(request.getCourseId());
@@ -184,72 +227,95 @@ public ResponseEntity<LectureResponse> createLecture(@RequestBody CreateLectureR
                 return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
             }
 
-            // TODO(yoon): Validate input data
-            // Validate title
-            // Validate materialPath
-            // ...
+            // Validate input data
+            if (request.getTitle() == null || request.getTitle().trim().isEmpty()) {
+                return ResponseEntity.badRequest().build();
+            }
+            if (request.getFile() == null || request.getFile().isEmpty()) {
+                return ResponseEntity.badRequest().build();
+            }
+
+            // Upload file to storage
+            MultipartFile file = request.getFile();
+
+            // Validate file type
+            String materialType;
+            try {
+                materialType = determineMaterialType(file);
+            } catch (IllegalArgumentException e) {
+                log.error("Invalid file type uploaded", e);
+                return ResponseEntity.badRequest().build();
+            }
+
+            String fileKey = storageService.upload(file);
 
             CreateLectureInput createLectureInput = new CreateLectureInput();
             createLectureInput.setCourseId(request.getCourseId());
             createLectureInput.setUserId(userId);
-            createLectureInput.setTitle(testTitle);
-            createLectureInput.setMaterialPath(testMaterialPath);
-            createLectureInput.setMaterialType(testMaterialType);
-            createLectureInput.setDisplayOrderLex(testDisplayOrderLex);
+            // TODO(mj): extract title from material.
+            createLectureInput.setTitle(request.getTitle());
+            createLectureInput.setMaterialPath(fileKey);
+            createLectureInput.setMaterialType(materialType);
+            createLectureInput.setDisplayOrderLex(generateDisplayOrderLex());
+
+            LectureOutput createdLectureOutput = lectureService.createLecture(createLectureInput);
+
+            // Send SQS message for summary generation
+            sendGenerateSummaryMessage(createdLectureOutput, fileKey, userId);
 
-            LectureOutput createdlectureOutput = lectureService.createLecture(createLectureInput);
-            return ResponseEntity.status(HttpStatus.CREATED).body(LectureResponse.fromServiceDto(createdlectureOutput));
+            return ResponseEntity.status(HttpStatus.CREATED).body(LectureResponse.fromServiceDto(createdLectureOutput));
         } catch (IllegalArgumentException e) {
+            log.error("Invalid input data", e);
             return ResponseEntity.badRequest().build();
         } catch (Exception e) {
+            log.error("Error creating lecture", e);
             return ResponseEntity.internalServerError().build();
         }
     }
 
     @PutMapping("/{id}")
     @Operation(
-        summary = "Update a lecture",
-        description = "Updates an existing lecture",
-        parameters = {
-            @Parameter(
-                name = "id",
-                description = "ID of the lecture to update",
-                required = true
-            )
-        },
-        requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(
-            description = "Lecture details (title, materialPath, materialType)",
-            required = true,
-            content = @Content(schema = @Schema(implementation = UpdateLectureRequest.class))
-        ),
-        responses = {
-            @ApiResponse(
-                responseCode = "200",
-                description = "Successfully updated lecture",
-                content = @Content(schema = @Schema(implementation = LectureResponse.class))
+            summary = "Update a lecture",
+            description = "Updates an existing lecture",
+            parameters = {
+                    @Parameter(
+                            name = "id",
+                            description = "ID of the lecture to update",
+                            required = true
+                    )
+            },
+            requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(
+                    description = "Lecture details (title, materialPath, materialType)",
+                    required = true,
+                    content = @Content(schema = @Schema(implementation = UpdateLectureRequest.class))
             ),
-            @ApiResponse(
-                responseCode = "400",
-                description = "Invalid input data"
-            ),
-            @ApiResponse(
-                responseCode = "403",
-                description = "User does not have access to this lecture"
-            ),
-            @ApiResponse(
-                responseCode = "404",
-                description = "Lecture not found"
-            ),
-            @ApiResponse(
-                responseCode = "500",
-                description = "Internal server error"
-            )
-        }
+            responses = {
+                    @ApiResponse(
+                            responseCode = "200",
+                            description = "Successfully updated lecture",
+                            content = @Content(schema = @Schema(implementation = LectureResponse.class))
+                    ),
+                    @ApiResponse(
+                            responseCode = "400",
+                            description = "Invalid input data"
+                    ),
+                    @ApiResponse(
+                            responseCode = "403",
+                            description = "User does not have access to this lecture"
+                    ),
+                    @ApiResponse(
+                            responseCode = "404",
+                            description = "Lecture not found"
+                    ),
+                    @ApiResponse(
+                            responseCode = "500",
+                            description = "Internal server error"
+                    )
+            }
     )
     public ResponseEntity<LectureResponse> updateLecture(
-        @PathVariable UUID id,
-        @RequestBody UpdateLectureRequest request
-    ) {
+            @PathVariable @Parameter(name = "id", description = "ID of the lecture to update", required = true) UUID id,
+            @RequestBody UpdateLectureRequest request) {
         UUID userId = getAuthenticatedUserId();
 
         try {
@@ -295,48 +361,47 @@ public ResponseEntity<LectureResponse> updateLecture(
 
     @PutMapping("/{id}/display-order-lex")
     @Operation(
-        summary = "Update lecture display order lex",
-        description = "Updates the display order lex of a lecture",
-        parameters = {
-            @Parameter(
-                name = "id",
-                description = "ID of the lecture to update",
-                required = true
-            )
-        },
-        requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(
-            description = "Updated lecture display order lex (displayOrderLex)",
-            required = true,
-            content = @Content(schema = @Schema(implementation = UpdateLectureDisplayOrderLexRequest.class))
-        ),
-        responses = {
-            @ApiResponse(
-                responseCode = "204",
-                description = "Successfully updated lecture display order lex",
-                content = @Content(schema = @Schema(implementation = LectureResponse.class))
-            ),
-            @ApiResponse(
-                responseCode = "400",
-                description = "Invalid input data"
-            ),
-            @ApiResponse(
-                responseCode = "403",
-                description = "User does not have access to this lecture"
-            ),
-            @ApiResponse(
-                responseCode = "404",
-                description = "Lecture not found"
+            summary = "Update lecture display order lex",
+            description = "Updates the display order lex of a lecture",
+            parameters = {
+                    @Parameter(
+                            name = "id",
+                            description = "ID of the lecture to update",
+                            required = true
+                    )
+            },
+            requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(
+                    description = "Updated lecture display order lex (displayOrderLex)",
+                    required = true,
+                    content = @Content(schema = @Schema(implementation = UpdateLectureDisplayOrderLexRequest.class))
             ),
-            @ApiResponse(
-                responseCode = "500",
-                description = "Internal server error"
-            )
-        }
+            responses = {
+                    @ApiResponse(
+                            responseCode = "204",
+                            description = "Successfully updated lecture display order lex",
+                            content = @Content(schema = @Schema(implementation = LectureResponse.class))
+                    ),
+                    @ApiResponse(
+                            responseCode = "400",
+                            description = "Invalid input data"
+                    ),
+                    @ApiResponse(
+                            responseCode = "403",
+                            description = "User does not have access to this lecture"
+                    ),
+                    @ApiResponse(
+                            responseCode = "404",
+                            description = "Lecture not found"
+                    ),
+                    @ApiResponse(
+                            responseCode = "500",
+                            description = "Internal server error"
+                    )
+            }
     )
     public ResponseEntity<Void> updateLectureDisplayOrderLex(
-        @PathVariable UUID id,
-        @RequestBody UpdateLectureDisplayOrderLexRequest request
-    ) {
+            @PathVariable @Parameter(name = "id", description = "ID of the lecture to update", required = true) UUID id,
+            @RequestBody UpdateLectureDisplayOrderLexRequest request) {
         UUID userId = getAuthenticatedUserId();
 
         try {
@@ -371,33 +436,33 @@ public ResponseEntity<Void> updateLectureDisplayOrderLex(
 
     @DeleteMapping("/{id}")
     @Operation(
-        summary = "Delete a lecture",
-        description = "Deletes a specific lecture by its ID",
-        parameters = {
-            @Parameter(
-                name = "id",
-                description = "ID of the lecture to delete",
-                required = true
-            )
-        },
-        responses = {
-            @ApiResponse(
-                responseCode = "204",
-                description = "Successfully deleted lecture"
-            ),
-            @ApiResponse(
-                responseCode = "403",
-                description = "User does not have access to this lecture"
-            ),
-            @ApiResponse(
-                responseCode = "404",
-                description = "Lecture not found"
-            ),
-            @ApiResponse(
-                responseCode = "500",
-                description = "Internal server error"
-            )
-        }
+            summary = "Delete a lecture",
+            description = "Deletes a specific lecture by its ID",
+            parameters = {
+                    @Parameter(
+                            name = "id",
+                            description = "ID of the lecture to delete",
+                            required = true
+                    )
+            },
+            responses = {
+                    @ApiResponse(
+                            responseCode = "204",
+                            description = "Successfully deleted lecture"
+                    ),
+                    @ApiResponse(
+                            responseCode = "403",
+                            description = "User does not have access to this lecture"
+                    ),
+                    @ApiResponse(
+                            responseCode = "404",
+                            description = "Lecture not found"
+                    ),
+                    @ApiResponse(
+                            responseCode = "500",
+                            description = "Internal server error"
+                    )
+            }
     )
     public ResponseEntity<Void> deleteLecture(@PathVariable UUID id) {
         UUID userId = getAuthenticatedUserId();
diff --git a/src/main/java/com/example/api/controller/StorageController.java b/src/main/java/com/example/api/controller/StorageController.java
deleted file mode 100644
index b85fc9a3..00000000
--- a/src/main/java/com/example/api/controller/StorageController.java
+++ /dev/null
@@ -1,41 +0,0 @@
-package com.example.api.controller;
-
-import io.swagger.v3.oas.annotations.tags.Tag;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RequestParam;
-import org.springframework.web.bind.annotation.RestController;
-import org.springframework.web.multipart.MultipartFile;
-import org.springframework.web.server.ResponseStatusException;
-
-import com.example.api.service.StorageService;
-
-import lombok.RequiredArgsConstructor;
-
-@RestController
-@RequestMapping("/v1/storage")
-@Tag(name = "Storage", description = "Storage API")
-@RequiredArgsConstructor
-public class StorageController extends BaseController {
-    private final StorageService storageService;
-
-    @PostMapping("/upload")
-    public ResponseEntity<Void> upload(@RequestParam MultipartFile file) {
-        try {
-            storageService.upload(file);
-            return ResponseEntity.ok().build();
-        } catch (IllegalArgumentException e) {
-            // 파일 크기 초과 등 클라이언트 예외
-            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage(), e);
-        } catch (Exception e) {
-            throw new ResponseStatusException(
-                HttpStatus.INTERNAL_SERVER_ERROR,
-                "파일 업로드 처리 중 오류가 발생했습니다.",
-                e
-            );
-        }
-    }
-}
-
diff --git a/src/main/java/com/example/api/controller/dto/lecture/CreateLectureRequest.java b/src/main/java/com/example/api/controller/dto/lecture/CreateLectureRequest.java
index e58e1c1b..e9de537b 100644
--- a/src/main/java/com/example/api/controller/dto/lecture/CreateLectureRequest.java
+++ b/src/main/java/com/example/api/controller/dto/lecture/CreateLectureRequest.java
@@ -5,6 +5,7 @@
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.NoArgsConstructor;
+import org.springframework.web.multipart.MultipartFile;
 
 import java.util.UUID;
 
@@ -16,4 +17,12 @@ public class CreateLectureRequest {
     @NotNull
     @Schema(description = "ID of the course")
     private UUID courseId;
+
+    @NotNull
+    @Schema(description = "Title of the lecture")
+    private String title;
+
+    @NotNull
+    @Schema(description = "Lecture material file (Supported formats: PDF)")
+    private MultipartFile file;
 }
diff --git a/src/main/java/com/example/api/service/MinioStorageService.java b/src/main/java/com/example/api/service/MinioStorageService.java
index dd6e3aea..55f89bd9 100644
--- a/src/main/java/com/example/api/service/MinioStorageService.java
+++ b/src/main/java/com/example/api/service/MinioStorageService.java
@@ -1,54 +1,66 @@
 package com.example.api.service;
 
-import java.io.InputStream;
-import java.util.UUID;
-
-import org.springframework.beans.factory.annotation.Value;
+import com.example.api.config.StorageConfig;
+import io.minio.MinioClient;
+import io.minio.PutObjectArgs;
 import org.springframework.boot.autoconfigure.web.servlet.MultipartProperties;
 import org.springframework.context.annotation.Profile;
 import org.springframework.stereotype.Service;
 import org.springframework.web.multipart.MultipartFile;
 
-import com.example.api.config.StorageProperties;
-
-import io.minio.MinioClient;
-import io.minio.PutObjectArgs;
+import java.io.InputStream;
+import java.util.UUID;
 
 @Service
 @Profile("local")
 public class MinioStorageService implements StorageService {
     private final MinioClient minioClient;
     private final MultipartProperties multipartProperties;
-    private final StorageProperties storageProperties;
+    private final StorageConfig storageConfig;
 
     public MinioStorageService(MinioClient minioClient,
-                              MultipartProperties multipartProperties,
-                              StorageProperties storageProperties) {
+                               MultipartProperties multipartProperties,
+                               StorageConfig storageConfig) {
         this.minioClient = minioClient;
         this.multipartProperties = multipartProperties;
-        this.storageProperties = storageProperties;
+        this.storageConfig = storageConfig;
     }
 
     @Override
-    public void upload(MultipartFile pdf) throws Exception {
+    public String upload(MultipartFile pdf) throws Exception {
         long size = pdf.getSize();
         long maxFileSize = multipartProperties.getMaxFileSize().toBytes();
 
         if (size > maxFileSize) {
             throw new IllegalArgumentException(
-                String.format("파일 크기는 %dMB를 초과할 수 없습니다.", maxFileSize / 1024 / 1024)
+                    String.format("파일 크기는 %dMB를 초과할 수 없습니다.", maxFileSize / 1024 / 1024)
             );
         }
 
+        // Validate PDF file
+        String contentType = pdf.getContentType();
+        String filename = pdf.getOriginalFilename();
+
+        if (!"application/pdf".equals(contentType) &&
+                (filename == null || !filename.toLowerCase().endsWith(".pdf"))) {
+            throw new IllegalArgumentException("Only PDF files are supported");
+        }
+
         String key = UUID.randomUUID() + ".pdf";
         try (InputStream in = pdf.getInputStream()) {
             minioClient.putObject(
-                PutObjectArgs.builder()
-                    .bucket(storageProperties.getBucket())
-                    .object(key)
-                    .stream(in, size, -1)
-                    .contentType("application/pdf")
-                    .build());
+                    PutObjectArgs.builder()
+                            .bucket(storageConfig.getBucket())
+                            .object(key)
+                            .stream(in, size, -1)
+                            .contentType("application/pdf")
+                            .build());
+            return key;
         }
     }
+
+    @Override
+    public String getBucket() {
+        return storageConfig.getBucket();
+    }
 }
diff --git a/src/main/java/com/example/api/service/S3StorageService.java b/src/main/java/com/example/api/service/S3StorageService.java
index 776f1c0b..b5179b49 100644
--- a/src/main/java/com/example/api/service/S3StorageService.java
+++ b/src/main/java/com/example/api/service/S3StorageService.java
@@ -1,56 +1,68 @@
 package com.example.api.service;
 
-import java.io.InputStream;
-import java.util.UUID;
-
-import org.springframework.beans.factory.annotation.Value;
+import com.example.api.config.StorageConfig;
 import org.springframework.boot.autoconfigure.web.servlet.MultipartProperties;
 import org.springframework.context.annotation.Profile;
 import org.springframework.stereotype.Service;
 import org.springframework.web.multipart.MultipartFile;
-
-import com.example.api.config.StorageProperties;
-
 import software.amazon.awssdk.core.sync.RequestBody;
 import software.amazon.awssdk.services.s3.S3Client;
 import software.amazon.awssdk.services.s3.model.PutObjectRequest;
 
+import java.io.InputStream;
+import java.util.UUID;
+
 @Service
 @Profile({"dev", "prod"})
 public class S3StorageService implements StorageService {
     private final S3Client s3Client;
     private final MultipartProperties multipartProperties;
-    private final StorageProperties storageProperties;
+    private final StorageConfig storageConfig;
 
     public S3StorageService(S3Client s3Client,
-                           MultipartProperties multipartProperties,
-                           StorageProperties storageProperties) {
+                            MultipartProperties multipartProperties,
+                            StorageConfig storageConfig) {
         this.s3Client = s3Client;
         this.multipartProperties = multipartProperties;
-        this.storageProperties = storageProperties;
+        this.storageConfig = storageConfig;
     }
 
     @Override
-    public void upload(MultipartFile pdf) throws Exception {
+    public String upload(MultipartFile pdf) throws Exception {
         long size = pdf.getSize();
         long maxFileSize = multipartProperties.getMaxFileSize().toBytes();
 
         if (size > maxFileSize) {
             throw new IllegalArgumentException(
-                String.format("파일 크기는 %dMB를 초과할 수 없습니다.", maxFileSize / 1024 / 1024)
+                    String.format("파일 크기는 %dMB를 초과할 수 없습니다.", maxFileSize / 1024 / 1024)
             );
         }
 
+        // Validate PDF file
+        String contentType = pdf.getContentType();
+        String filename = pdf.getOriginalFilename();
+
+        if (!"application/pdf".equals(contentType) &&
+                (filename == null || !filename.toLowerCase().endsWith(".pdf"))) {
+            throw new IllegalArgumentException("Only PDF files are supported");
+        }
+
         String key = UUID.randomUUID() + ".pdf";
         try (InputStream in = pdf.getInputStream()) {
             s3Client.putObject(
-                PutObjectRequest.builder()
-                    .bucket(storageProperties.getBucket())
-                    .key(key)
-                    .contentType("application/pdf")
-                    .contentLength(size)
-                    .build(),
-                RequestBody.fromInputStream(in, size));
+                    PutObjectRequest.builder()
+                            .bucket(storageConfig.getBucket())
+                            .key(key)
+                            .contentType("application/pdf")
+                            .contentLength(size)
+                            .build(),
+                    RequestBody.fromInputStream(in, size));
+            return key;
         }
     }
+
+    @Override
+    public String getBucket() {
+        return storageConfig.getBucket();
+    }
 }
diff --git a/src/main/java/com/example/api/service/StorageService.java b/src/main/java/com/example/api/service/StorageService.java
index c8a8248d..1530fd3d 100644
--- a/src/main/java/com/example/api/service/StorageService.java
+++ b/src/main/java/com/example/api/service/StorageService.java
@@ -3,5 +3,7 @@
 import org.springframework.web.multipart.MultipartFile;
 
 public interface StorageService {
-    public void upload(MultipartFile pdf) throws Exception;
+    public String upload(MultipartFile pdf) throws Exception;
+
+    public String getBucket();
 }
diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml
index cbc3dec5..2a713b7a 100644
--- a/src/main/resources/application-local.yml
+++ b/src/main/resources/application-local.yml
@@ -52,4 +52,13 @@ minio:
   secret-key: minioadmin
 
 aws:  
-  region: ap-northeast-2
\ No newline at end of file
+  region: ap-northeast-2
+
+# LocalStack Configuration
+localstack:
+  endpoint: http://localstack:4566
+
+# SQS Configuration for Local Testing
+sqs:
+  generate-summary:
+    queue-url: http://sqs.ap-northeast-2.localstack.localstack.cloud:4566/000000000000/local-study-aid-summarize-lecture
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 39988709..34210c3d 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -56,4 +56,12 @@ springdoc:
 jwt:
   issuer: studyaid
   access-token-ttl-ms: 900000  # 15분
-  refresh-token-ttl-ms: 1209600000  # 2주
\ No newline at end of file
+  refresh-token-ttl-ms: 1209600000  # 2주
+
+storage:
+  bucket: study-aid-lecture-materials
+
+# SQS Configuration
+sqs:
+  generate-summary:
+    queue-url: ${SQS_GENERATE_SUMMARY_QUEUE_URL}
diff --git a/src/main/resources/schemas/sqs/generate_summary.json b/src/main/resources/schemas/sqs/generate_summary.json
index 1a7abe23..cd4e50bb 100644
--- a/src/main/resources/schemas/sqs/generate_summary.json
+++ b/src/main/resources/schemas/sqs/generate_summary.json
@@ -48,4 +48,4 @@
     }
   },
   "additionalProperties": false
-}
\ No newline at end of file
+}
diff --git a/src/test/java/com/example/api/adapters/sqs/SQSClientImplTest.java b/src/test/java/com/example/api/adapters/sqs/SQSClientImplTest.java
new file mode 100644
index 00000000..bb9c3fbd
--- /dev/null
+++ b/src/test/java/com/example/api/adapters/sqs/SQSClientImplTest.java
@@ -0,0 +1,100 @@
+package com.example.api.adapters.sqs;
+
+import com.example.api.config.SQSMessageConfig;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import software.amazon.awssdk.services.sqs.SqsClient;
+import software.amazon.awssdk.services.sqs.model.SendMessageRequest;
+import software.amazon.awssdk.services.sqs.model.SendMessageResponse;
+
+import java.time.OffsetDateTime;
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+@ExtendWith(MockitoExtension.class)
+@DisplayName("SQSClientImpl 테스트")
+class SQSClientImplTest {
+
+    @Mock
+    private SqsClient sqsClient;
+
+    @Mock
+    private SQSMessageConfig sqsMessageConfig;
+
+    private SQSClientImpl sqsClientImpl;
+
+    @BeforeEach
+    void setUp() {
+        SQSMessageConfig.GenerateSummary generateSummary = new SQSMessageConfig.GenerateSummary();
+        generateSummary.setQueueUrl("http://localhost:4566/000000000000/test-queue");
+        when(sqsMessageConfig.getGenerateSummary()).thenReturn(generateSummary);
+
+        sqsClientImpl = new SQSClientImpl(sqsClient, sqsMessageConfig);
+    }
+
+    @Test
+    @DisplayName("GenerateSummaryMessage 전송 성공")
+    void sendGenerateSummaryMessage_Success() throws Exception {
+        // Given
+        GenerateSummaryMessage message = GenerateSummaryMessage.builder()
+                .schemaVersion("1.0.0")
+                .requestId(UUID.randomUUID())
+                .occurredAt(OffsetDateTime.now())
+                .userId(UUID.randomUUID())
+                .courseId(UUID.randomUUID())
+                .lectureId(UUID.randomUUID())
+                .s3Bucket("test-bucket")
+                .s3Key("test-key.pdf")
+                .build();
+
+        SendMessageResponse mockResponse = SendMessageResponse.builder()
+                .messageId("test-message-id")
+                .build();
+
+        when(sqsClient.sendMessage(any(SendMessageRequest.class))).thenReturn(mockResponse);
+
+        // When
+        assertDoesNotThrow(() -> sqsClientImpl.sendGenerateSummaryMessage(message));
+
+        // Then
+        ArgumentCaptor<SendMessageRequest> requestCaptor = ArgumentCaptor.forClass(SendMessageRequest.class);
+        verify(sqsClient, times(1)).sendMessage(requestCaptor.capture());
+
+        SendMessageRequest capturedRequest = requestCaptor.getValue();
+        assertEquals("http://localhost:4566/000000000000/test-queue", capturedRequest.queueUrl());
+        assertNotNull(capturedRequest.messageBody());
+    }
+
+    @Test
+    @DisplayName("GenerateSummaryMessage 전송 실패")
+    void sendGenerateSummaryMessage_Failure() throws Exception {
+        // Given
+        GenerateSummaryMessage message = GenerateSummaryMessage.builder()
+                .schemaVersion("1.0.0")
+                .requestId(UUID.randomUUID())
+                .occurredAt(OffsetDateTime.now())
+                .userId(UUID.randomUUID())
+                .courseId(UUID.randomUUID())
+                .lectureId(UUID.randomUUID())
+                .s3Bucket("test-bucket")
+                .s3Key("test-key.pdf")
+                .build();
+
+        when(sqsClient.sendMessage(any(SendMessageRequest.class)))
+                .thenThrow(new RuntimeException("SQS error"));
+
+        // When & Then
+        assertThrows(RuntimeException.class,
+                () -> sqsClientImpl.sendGenerateSummaryMessage(message));
+
+        verify(sqsClient, times(1)).sendMessage(any(SendMessageRequest.class));
+    }
+}
diff --git a/src/test/java/com/example/api/controller/LectureControllerTest.java b/src/test/java/com/example/api/controller/LectureControllerTest.java
index 1020b58e..5379fa77 100644
--- a/src/test/java/com/example/api/controller/LectureControllerTest.java
+++ b/src/test/java/com/example/api/controller/LectureControllerTest.java
@@ -1,7 +1,7 @@
 package com.example.api.controller;
 
+import com.example.api.adapters.sqs.SQSClient;
 import com.example.api.config.TestSecurityConfig;
-import com.example.api.controller.dto.lecture.CreateLectureRequest;
 import com.example.api.controller.dto.lecture.UpdateLectureRequest;
 import com.example.api.repository.UserRepository;
 import com.example.api.security.jwt.JwtAuthenticationFilter;
@@ -9,6 +9,7 @@
 import com.example.api.service.CourseService;
 import com.example.api.service.LectureService;
 import com.example.api.service.SemesterService;
+import com.example.api.service.StorageService;
 import com.example.api.service.dto.course.CourseOutput;
 import com.example.api.service.dto.lecture.CreateLectureInput;
 import com.example.api.service.dto.lecture.LectureListOutput;
@@ -26,6 +27,7 @@
 import org.springframework.boot.test.mock.mockito.MockBean;
 import org.springframework.context.annotation.Import;
 import org.springframework.http.MediaType;
+import org.springframework.mock.web.MockMultipartFile;
 import org.springframework.test.context.ActiveProfiles;
 import org.springframework.test.web.servlet.MockMvc;
 
@@ -71,6 +73,12 @@ class LectureControllerTest {
     @MockBean
     private JwtAuthenticationFilter jwtAuthenticationFilter;
 
+    @MockBean
+    private StorageService storageService;
+
+    @MockBean
+    private SQSClient sqsClient;
+
     private UUID userId;
     private UUID semesterId;
     private UUID courseId;
@@ -246,18 +254,25 @@ void getLectureByIdForbiddenTest() throws Exception {
     @WithMockUser
     void createLectureTest() throws Exception {
         // Given
-        CreateLectureRequest createLectureRequest = new CreateLectureRequest();
-        createLectureRequest.setCourseId(courseId);
+        MockMultipartFile pdfFile = new MockMultipartFile(
+                "file",
+                "test-lecture.pdf",
+                "application/pdf",
+                "PDF content".getBytes()
+        );
 
         when(courseService.findCourseById(courseId))
                 .thenReturn(Optional.of(testCourseOutput));
+        when(storageService.upload(any()))
+                .thenReturn("test-key.pdf");
         when(lectureService.createLecture(any(CreateLectureInput.class)))
                 .thenReturn(testLectureOutput);
 
         // When & Then
-        mockMvc.perform(post("/v1/lectures")
-                        .contentType(MediaType.APPLICATION_JSON)
-                        .content(objectMapper.writeValueAsString(createLectureRequest)))
+        mockMvc.perform(multipart("/v1/lectures")
+                        .file(pdfFile)
+                        .param("courseId", courseId.toString())
+                        .param("title", "Test Lecture"))
                 .andExpect(status().isCreated())
                 .andExpect(jsonPath("$.id").value(testLectureOutput.getId().toString()))
                 .andExpect(jsonPath("$.title").value("Introduction to Operating Systems"))
@@ -265,17 +280,45 @@ void createLectureTest() throws Exception {
                 .andExpect(jsonPath("$.courseId").value(courseId.toString()));
 
         verify(courseService).findCourseById(courseId);
-        // LectureController에서 사용하는 sample prefix 변수들과 비교
+        verify(storageService).upload(any());
+        verify(sqsClient).sendGenerateSummaryMessage(any());
         verify(lectureService).createLecture(argThat(input ->
                 input.getCourseId().equals(courseId) &&
                         input.getUserId().equals(userId) &&
-                        input.getTitle().equals("Test Title") &&
-                        input.getMaterialPath().equals("test/path") &&
-                        input.getMaterialType().equals("pdf") &&
-                        input.getDisplayOrderLex().equals("1")
+                        input.getTitle().equals("Test Lecture") &&
+                        input.getMaterialPath().equals("test-key.pdf") &&
+                        input.getMaterialType().equals("pdf")
         ));
     }
 
+    @Test
+    @DisplayName("강의 생성 실패 테스트 - PDF가 아닌 파일")
+    @WithMockUser
+    void createLectureNonPdfFileTest() throws Exception {
+        // Given
+        MockMultipartFile textFile = new MockMultipartFile(
+                "file",
+                "test-lecture.txt",
+                "text/plain",
+                "Text content".getBytes()
+        );
+
+        when(courseService.findCourseById(courseId))
+                .thenReturn(Optional.of(testCourseOutput));
+
+        // When & Then
+        mockMvc.perform(multipart("/v1/lectures")
+                        .file(textFile)
+                        .param("courseId", courseId.toString())
+                        .param("title", "Test Lecture"))
+                .andExpect(status().isBadRequest());
+
+        verify(courseService).findCourseById(courseId);
+        verify(storageService, never()).upload(any());
+        verify(sqsClient, never()).sendGenerateSummaryMessage(any());
+        verify(lectureService, never()).createLecture(any());
+    }
+
     @Test
     @DisplayName("강의 정보 업데이트")
     @WithMockUser
diff --git a/src/test/java/com/example/api/controller/StorageControllerTest.java b/src/test/java/com/example/api/controller/StorageControllerTest.java
deleted file mode 100644
index 4c8a33b5..00000000
--- a/src/test/java/com/example/api/controller/StorageControllerTest.java
+++ /dev/null
@@ -1,58 +0,0 @@
-package com.example.api.controller;
-
-import com.example.api.config.TestSecurityConfig;
-import com.example.api.security.jwt.JwtAuthenticationFilter;
-import com.example.api.security.jwt.JwtProvider;
-import com.example.api.service.StorageService;
-import com.example.api.util.WithMockUser;
-import org.junit.jupiter.api.DisplayName;
-import org.junit.jupiter.api.Test;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
-import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
-import org.springframework.boot.test.mock.mockito.MockBean;
-import org.springframework.context.annotation.Import;
-import org.springframework.http.MediaType;
-import org.springframework.mock.web.MockMultipartFile;
-import org.springframework.test.context.ActiveProfiles;
-import org.springframework.test.web.servlet.MockMvc;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.doNothing;
-import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
-import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
-
-@WebMvcTest(StorageController.class)
-@Import({TestSecurityConfig.class})
-@AutoConfigureMockMvc(addFilters = false)
-@ActiveProfiles("test")
-class StorageControllerTest {
-
-    @Autowired
-    private MockMvc mockMvc;
-
-    @MockBean
-    private JwtProvider jwtProvider;
-
-    @MockBean
-    private JwtAuthenticationFilter jwtAuthenticationFilter;
-
-    @MockBean
-    private StorageService storageService;
-
-    @Test
-    @DisplayName("파일 업로드 성공 시 200 OK 반환")
-    @WithMockUser
-    void uploadSuccess() throws Exception {
-        // Given
-        MockMultipartFile file = new MockMultipartFile(
-                "file", "test.pdf", MediaType.APPLICATION_PDF_VALUE, "dummy content".getBytes()
-        );
-        doNothing().when(storageService).upload(any());
-
-        // When & Then
-        mockMvc.perform(multipart("/v1/storage/upload")
-                        .file(file))
-                .andExpect(status().isOk());
-    }
-}
diff --git a/src/test/java/com/example/api/service/MinioStorageServiceTest.java b/src/test/java/com/example/api/service/MinioStorageServiceTest.java
index 3a19ffa4..4e2e7bad 100644
--- a/src/test/java/com/example/api/service/MinioStorageServiceTest.java
+++ b/src/test/java/com/example/api/service/MinioStorageServiceTest.java
@@ -1,90 +1,90 @@
 package com.example.api.service;
 
-import static org.junit.jupiter.api.Assertions.*;
-import static org.mockito.Mockito.*;
-
+import com.example.api.config.StorageConfig;
+import io.minio.MinioClient;
+import io.minio.PutObjectArgs;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.ArgumentCaptor;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.autoconfigure.web.servlet.MultipartProperties;
+import org.springframework.boot.test.context.TestConfiguration;
 import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Import;
 import org.springframework.mock.web.MockMultipartFile;
-import org.springframework.boot.test.context.TestConfiguration;
-import org.springframework.context.annotation.Bean;
 import org.springframework.test.context.TestPropertySource;
 import org.springframework.test.context.junit.jupiter.SpringExtension;
 
-import com.example.api.config.StorageProperties;
-
-import io.minio.MinioClient;
-import io.minio.PutObjectArgs;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.*;
 
 @ExtendWith(SpringExtension.class)
 @Import({MinioStorageServiceTest.TestConfig.class})
 @TestPropertySource(properties = {
-    "storage.bucket=test-bucket",
-    "spring.servlet.multipart.max-file-size=50MB"
+        "storage.bucket=test-bucket",
+        "spring.servlet.multipart.max-file-size=50MB"
 })
 @DisplayName("MinioStorageService 테스트")
 class MinioStorageServiceTest {
-    
+
     @TestConfiguration
     static class TestConfig {
         @Bean
-        public StorageProperties storageProperties() {
-            StorageProperties properties = new StorageProperties();
-            properties.setBucket("test-bucket");
-            return properties;
+        public StorageConfig storageConfig() {
+            StorageConfig config = new StorageConfig();
+            config.setBucket("test-bucket");
+            return config;
         }
-        
+
         @Bean
         public MinioStorageService minioStorageService(MinioClient minioClient,
-                                                      MultipartProperties multipartProperties,
-                                                      StorageProperties storageProperties) {
-            return new MinioStorageService(minioClient, multipartProperties, storageProperties);
+                                                       MultipartProperties multipartProperties,
+                                                       StorageConfig storageConfig) {
+            return new MinioStorageService(minioClient, multipartProperties, storageConfig);
         }
     }
-    
+
     @MockBean
     private MinioClient minioClient;
-    
+
     @MockBean
     private MultipartProperties multipartProperties;
-    
+
     @Autowired
     private MinioStorageService minioStorageService;
-    
+
     @Test
     @DisplayName("PDF 파일 업로드 성공")
     void uploadPdfSuccess() throws Exception {
         // Given
         byte[] content = "PDF content".getBytes();
         MockMultipartFile pdfFile = new MockMultipartFile(
-            "file", 
-            "test.pdf", 
-            "application/pdf", 
-            content
+                "file",
+                "test.pdf",
+                "application/pdf",
+                content
         );
-        
+
         // When
         when(multipartProperties.getMaxFileSize())
-        .thenReturn(org.springframework.util.unit.DataSize.ofMegabytes(50));
+                .thenReturn(org.springframework.util.unit.DataSize.ofMegabytes(50));
+
+        String result = minioStorageService.upload(pdfFile);
 
-        minioStorageService.upload(pdfFile);
-        
         // Then
         ArgumentCaptor<PutObjectArgs> putObjectArgsCaptor = ArgumentCaptor.forClass(PutObjectArgs.class);
-        
+
         verify(minioClient, times(1)).putObject(putObjectArgsCaptor.capture());
-        
+
         PutObjectArgs capturedArgs = putObjectArgsCaptor.getValue();
         assertEquals("test-bucket", capturedArgs.bucket());
         assertEquals("application/pdf", capturedArgs.contentType());
+        assertEquals(result.endsWith(".pdf"), true);
     }
-    
+
     @Test
     @DisplayName("50MB 초과 파일 업로드 실패")
     void uploadLargeFileFail() {
@@ -92,15 +92,38 @@ void uploadLargeFileFail() {
         MockMultipartFile largePdfFile = mock(MockMultipartFile.class);
         when(largePdfFile.getSize()).thenReturn(51L * 1024 * 1024); // 51MB
         when(multipartProperties.getMaxFileSize())
-        .thenReturn(org.springframework.util.unit.DataSize.ofMegabytes(50));
-        
+                .thenReturn(org.springframework.util.unit.DataSize.ofMegabytes(50));
+
         // When & Then
         IllegalArgumentException exception = assertThrows(
-            IllegalArgumentException.class, 
-            () -> minioStorageService.upload(largePdfFile)
+                IllegalArgumentException.class,
+                () -> minioStorageService.upload(largePdfFile)
         );
 
         String expectedMessage = String.format("파일 크기는 %dMB를 초과할 수 없습니다.", multipartProperties.getMaxFileSize().toBytes() / 1024 / 1024);
         assertEquals(expectedMessage, exception.getMessage());
     }
+
+    @Test
+    @DisplayName("PDF가 아닌 파일 업로드 실패")
+    void uploadNonPdfFileFail() {
+        // Given
+        byte[] content = "Text content".getBytes();
+        MockMultipartFile textFile = new MockMultipartFile(
+                "file",
+                "test.txt",
+                "text/plain",
+                content
+        );
+        when(multipartProperties.getMaxFileSize())
+                .thenReturn(org.springframework.util.unit.DataSize.ofMegabytes(50));
+
+        // When & Then
+        IllegalArgumentException exception = assertThrows(
+                IllegalArgumentException.class,
+                () -> minioStorageService.upload(textFile)
+        );
+
+        assertEquals("Only PDF files are supported", exception.getMessage());
+    }
 }
\ No newline at end of file
diff --git a/src/test/java/com/example/api/service/S3StorageServiceTest.java b/src/test/java/com/example/api/service/S3StorageServiceTest.java
index 3796a490..32213cb2 100644
--- a/src/test/java/com/example/api/service/S3StorageServiceTest.java
+++ b/src/test/java/com/example/api/service/S3StorageServiceTest.java
@@ -1,105 +1,103 @@
 
 package com.example.api.service;
 
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import java.io.ByteArrayInputStream;
-import java.io.InputStream;
-
+import com.example.api.config.StorageConfig;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.ArgumentCaptor;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.autoconfigure.web.servlet.MultipartProperties;
+import org.springframework.boot.test.context.TestConfiguration;
 import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Import;
 import org.springframework.mock.web.MockMultipartFile;
-import org.springframework.boot.test.context.TestConfiguration;
-import org.springframework.context.annotation.Bean;
 import org.springframework.test.context.TestPropertySource;
 import org.springframework.test.context.junit.jupiter.SpringExtension;
 import org.springframework.web.multipart.MultipartFile;
-
-import com.example.api.config.StorageProperties;
-
 import software.amazon.awssdk.core.sync.RequestBody;
 import software.amazon.awssdk.services.s3.S3Client;
 import software.amazon.awssdk.services.s3.model.PutObjectRequest;
 
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
 
 @ExtendWith(SpringExtension.class)
 @Import({S3StorageServiceTest.TestConfig.class})
 @TestPropertySource(properties = {
-    "storage.bucket=test-bucket",
-    "spring.servlet.multipart.max-file-size=50MB"
+        "storage.bucket=test-bucket",
+        "spring.servlet.multipart.max-file-size=50MB"
 })
 @DisplayName("S3StorageService 테스트")
 class S3StorageServiceTest {
-    
+
     @TestConfiguration
     static class TestConfig {
         @Bean
-        public StorageProperties storageProperties() {
-            StorageProperties properties = new StorageProperties();
-            properties.setBucket("test-bucket");
-            return properties;
+        public StorageConfig storageConfig() {
+            StorageConfig config = new StorageConfig();
+            config.setBucket("test-bucket");
+            return config;
         }
-        
+
         @Bean
         public S3StorageService s3StorageService(S3Client s3Client,
-                                                MultipartProperties multipartProperties,
-                                                StorageProperties storageProperties) {
-            return new S3StorageService(s3Client, multipartProperties, storageProperties);
+                                                 MultipartProperties multipartProperties,
+                                                 StorageConfig storageConfig) {
+            return new S3StorageService(s3Client, multipartProperties, storageConfig);
         }
     }
-    
+
     @MockBean
     private S3Client s3Client;
-    
+
     @MockBean
     private MultipartProperties multipartProperties;
-    
+
     @Autowired
     private S3StorageService s3storageService;
-    
+
     @Test
     @DisplayName("PDF 파일 업로드 성공")
     void uploadPdfSuccess() throws Exception {
         // Given
         byte[] content = "PDF content".getBytes();
-        
+
         // Mock InputStream to avoid null pointer exception
         InputStream inputStream = new ByteArrayInputStream(content);
         MultipartFile pdfFile = mock(MultipartFile.class);
         when(pdfFile.getInputStream()).thenReturn(inputStream);
         when(pdfFile.getSize()).thenReturn((long) content.length);
-        
+        when(pdfFile.getContentType()).thenReturn("application/pdf");
+        when(pdfFile.getOriginalFilename()).thenReturn("test.pdf");
+
         when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class))).thenReturn(null);
         when(multipartProperties.getMaxFileSize())
-        .thenReturn(org.springframework.util.unit.DataSize.ofMegabytes(50));
-        
+                .thenReturn(org.springframework.util.unit.DataSize.ofMegabytes(50));
+
         // When
-        s3storageService.upload(pdfFile);
-        
+        String result = s3storageService.upload(pdfFile);
+
         // Then
         ArgumentCaptor<PutObjectRequest> putObjectRequestCaptor = ArgumentCaptor.forClass(PutObjectRequest.class);
         ArgumentCaptor<RequestBody> requestBodyCaptor = ArgumentCaptor.forClass(RequestBody.class);
-        
+
         verify(s3Client, times(1)).putObject(putObjectRequestCaptor.capture(), requestBodyCaptor.capture());
-        
+
         PutObjectRequest capturedRequest = putObjectRequestCaptor.getValue();
         assertEquals("test-bucket", capturedRequest.bucket());
         assertEquals("application/pdf", capturedRequest.contentType());
         assertEquals(content.length, capturedRequest.contentLength());
+        assertEquals(result.endsWith(".pdf"), true);
     }
-    
+
     @Test
     @DisplayName("용량 초과 파일 업로드 실패")
     void uploadLargeFileFail() {
@@ -107,15 +105,36 @@ void uploadLargeFileFail() {
         MockMultipartFile largePdfFile = mock(MockMultipartFile.class);
         when(largePdfFile.getSize()).thenReturn(51L * 1024 * 1024); // 51MB
         when(multipartProperties.getMaxFileSize())
-        .thenReturn(org.springframework.util.unit.DataSize.ofMegabytes(50));
-        
+                .thenReturn(org.springframework.util.unit.DataSize.ofMegabytes(50));
+
         // When & Then
         IllegalArgumentException exception = assertThrows(
-            IllegalArgumentException.class, 
-            () -> s3storageService.upload(largePdfFile)
+                IllegalArgumentException.class,
+                () -> s3storageService.upload(largePdfFile)
         );
-        
+
         String expectedMessage = String.format("파일 크기는 %dMB를 초과할 수 없습니다.", multipartProperties.getMaxFileSize().toBytes() / 1024 / 1024);
         assertEquals(expectedMessage, exception.getMessage());
     }
+
+    @Test
+    @DisplayName("PDF가 아닌 파일 업로드 실패")
+    void uploadNonPdfFileFail() {
+        // Given
+        byte[] content = "Text content".getBytes();
+        MultipartFile textFile = mock(MultipartFile.class);
+        when(textFile.getSize()).thenReturn((long) content.length);
+        when(textFile.getContentType()).thenReturn("text/plain");
+        when(textFile.getOriginalFilename()).thenReturn("test.txt");
+        when(multipartProperties.getMaxFileSize())
+                .thenReturn(org.springframework.util.unit.DataSize.ofMegabytes(50));
+
+        // When & Then
+        IllegalArgumentException exception = assertThrows(
+                IllegalArgumentException.class,
+                () -> s3storageService.upload(textFile)
+        );
+
+        assertEquals("Only PDF files are supported", exception.getMessage());
+    }
 }
\ No newline at end of file


Reviews:
user: login: HyunjinNoh
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-17T11:10:08Z
user: login: mingrammer
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-17T11:11:45Z
user: login: HyunjinNoh
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-17T11:17:53Z

Review comments:
diff_hunk: @@ -0,0 +1,19 @@
+package com.example.api.config;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Profile;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.services.s3.S3Client;
+
+@Configuration
+@Profile("prod")
path: src/main/java/com/example/api/config/S3Config.java
user: login: HyunjinNoh
body: 혹시 여기도 @Profile({"dev", "prod"}) 이렇게 수정해주실 수 있을까요?
created_at: 2025-05-17T11:10:03Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,19 @@
+package com.example.api.config;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Profile;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.services.s3.S3Client;
+
+@Configuration
+@Profile("prod")
path: src/main/java/com/example/api/config/S3Config.java
user: login: mingrammer
body: 앗 빠뜨렸네요, 추가했어요: 0893155
created_at: 2025-05-17T11:11:45Z
author_association: COLLABORATOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-15T10:33:22Z
body: ## Test Results (17)
148 tests  +4   148 ✅ +4   8s ⏱️ +2s
 33 suites ±0     0 💤 ±0 
 33 files   ±0     0 ❌ ±0 

Results for commit 08931551. ± Comparison against base commit a9557b4f.

<details>
  <summary>This pull request <b>removes</b> 1 and <b>adds</b> 5 tests. <i>Note that renamed tests count towards both.</i></summary>

```
com.example.api.controller.StorageControllerTest ‑ 파일 업로드 성공 시 200 OK 반환
```

```
com.example.api.adapters.sqs.SQSClientImplTest ‑ GenerateSummaryMessage 전송 성공
com.example.api.adapters.sqs.SQSClientImplTest ‑ GenerateSummaryMessage 전송 실패
com.example.api.controller.LectureControllerTest ‑ 강의 생성 실패 테스트 - PDF가 아닌 파일
com.example.api.service.MinioStorageServiceTest ‑ PDF가 아닌 파일 업로드 실패
com.example.api.service.S3StorageServiceTest ‑ PDF가 아닌 파일 업로드 실패
```
</details>

[test-results]:data:application/gzip;base64,H4sIADhvKGgC/12MSw6CMBQAr0K6dkEL7QMvY2o/yYtATT8r4919oKHgcmaSeTGPk0vs2nTdpWGpYK5kS9QZw0I8EFLJa+P9TrdUjPlXD3ySanfhNU4n4WIM8WdiWepzhfPya+px48Nw4+PPhHnGTMDaYey4lNwqJQFGKzgoocELYZ3k0pie6h2AvT+jO+KRBQEAAA==

:recycle: This comment has been updated with latest results.
author_association: NONE
user: login: mingrammer
created_at: 2025-05-15T16:54:14Z
body: 유저 인증 추가 PR: https://github.com/Study-AID/study-AID-backend/pull/52
author_association: COLLABORATOR