Skip to content

Commit 7e4207c

Browse files
authored
Feat: [Photo] 이미지 업로드 및 조회 (#64)
1 parent 83ae2be commit 7e4207c

18 files changed

Lines changed: 444 additions & 2 deletions

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ jobs:
5050
Firebase_ID: ${{ secrets.FIREBASE_PROJECT_ID }}
5151
GOOGLE_MAP_API_KEY: ${{ secrets.GOOGLE_MAP_API_KEY }}
5252
SPRING_DOMAIN: ${{ secrets.SPRING_DOMAIN }}
53+
FILE_UPLOAD_DIR: ./uploads
5354

5455
steps:
5556
- name: Checkout source code

.github/workflows/deploy.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ jobs:
4242
echo "KAKAO_CLIENT_ID=${{ secrets.KAKAO_CLIENT_ID }}" >> .env
4343
echo "KAKAO_REDIRECT_URI=${{ secrets.KAKAO_REDIRECT_URI }}" >> .env
4444
echo "SPRING_DOMAIN=${{ secrets.SPRING_DOMAIN }}" >> .env
45+
echo "FILE_UPLOAD_DIR=/app/uploads" >> .env
4546
4647
mkdir -p ./src/main/resources/firebase
4748
echo '${{ secrets.FCM_JSON }}' > ./src/main/resources/${{ secrets.FIREBASE_SERVICE_ACCOUNT_KEY }}

runtracker/build.gradle

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ dependencies {
5252
// Logstash
5353
implementation 'net.logstash.logback:logstash-logback-encoder:7.4'
5454

55+
// Image Processing
56+
implementation 'com.sksamuel.scrimage:scrimage-core:4.1.1'
57+
implementation 'com.sksamuel.scrimage:scrimage-webp:4.1.1'
58+
5559
compileOnly 'org.projectlombok:lombok'
5660
developmentOnly 'org.springframework.boot:spring-boot-devtools'
5761
runtimeOnly 'com.mysql:mysql-connector-j'

runtracker/docker-compose.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ services:
1313
- .env # DB, Redis 접속 정보 등을 담을 파일
1414
volumes:
1515
- ./firebase:/app/secrets
16+
- uploads:/app/uploads
1617
depends_on:
1718
mysql:
1819
condition: service_healthy
@@ -65,3 +66,4 @@ services:
6566
volumes:
6667
mysql-data:
6768
redis-data:
69+
uploads:
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.runtracker.domain.upload.controller;
2+
3+
import com.runtracker.domain.upload.service.FileStorageService;
4+
import com.runtracker.global.response.ApiResponse;
5+
import lombok.RequiredArgsConstructor;
6+
import org.springframework.core.io.Resource;
7+
import org.springframework.http.HttpHeaders;
8+
import org.springframework.http.MediaType;
9+
import org.springframework.http.ResponseEntity;
10+
import org.springframework.web.bind.annotation.*;
11+
import org.springframework.web.multipart.MultipartFile;
12+
13+
import java.util.HashMap;
14+
import java.util.Map;
15+
16+
@RestController
17+
@RequestMapping("/api/upload")
18+
@RequiredArgsConstructor
19+
public class UploadController {
20+
21+
private final FileStorageService fileStorageService;
22+
23+
@PostMapping("/image")
24+
public ApiResponse<Map<String, String>> uploadImage(
25+
@RequestParam("file") MultipartFile file) {
26+
27+
String fileUrl = fileStorageService.uploadImage(file);
28+
29+
Map<String, String> response = new HashMap<>();
30+
response.put("url", fileUrl);
31+
32+
return ApiResponse.ok(response);
33+
}
34+
35+
@GetMapping("/image/{filename:.+}")
36+
public ResponseEntity<Resource> getImage(@PathVariable String filename) {
37+
38+
Resource resource = fileStorageService.loadFileAsResource(filename);
39+
String contentType = fileStorageService.determineContentType(resource);
40+
41+
return ResponseEntity.ok()
42+
.contentType(MediaType.parseMediaType(contentType))
43+
.header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + resource.getFilename() + "\"")
44+
.body(resource);
45+
}
46+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.runtracker.domain.upload.enums;
2+
3+
import com.runtracker.global.code.ResponseCode;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Getter;
6+
7+
@Getter
8+
@AllArgsConstructor
9+
public enum UploadErrorCode implements ResponseCode {
10+
11+
FILE_IS_EMPTY("UP001", "File is empty"),
12+
INVALID_FILE_TYPE("UP002", "Invalid file type. Only image files are allowed"),
13+
INVALID_FILE_NAME("UP003", "Invalid file name"),
14+
FILE_STORAGE_FAILED("UP004", "Failed to store file"),
15+
FILE_NOT_FOUND("UP005", "File not found"),
16+
IMAGE_CONVERSION_FAILED("UP006", "Failed to convert image to WebP format"),
17+
IMAGE_RESIZE_FAILED("UP007", "Failed to resize image");
18+
19+
private final String statusCode;
20+
private final String message;
21+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.runtracker.domain.upload.exception;
2+
3+
import com.runtracker.domain.upload.enums.UploadErrorCode;
4+
import com.runtracker.global.exception.CustomException;
5+
6+
public class FileIsEmptyException extends CustomException {
7+
public FileIsEmptyException() {
8+
super(UploadErrorCode.FILE_IS_EMPTY);
9+
}
10+
11+
public FileIsEmptyException(String message) {
12+
super(UploadErrorCode.FILE_IS_EMPTY, message);
13+
}
14+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.runtracker.domain.upload.exception;
2+
3+
import com.runtracker.domain.upload.enums.UploadErrorCode;
4+
import com.runtracker.global.exception.CustomException;
5+
6+
public class FileNotFoundException extends CustomException {
7+
public FileNotFoundException() {
8+
super(UploadErrorCode.FILE_NOT_FOUND);
9+
}
10+
11+
public FileNotFoundException(String message) {
12+
super(UploadErrorCode.FILE_NOT_FOUND, message);
13+
}
14+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.runtracker.domain.upload.exception;
2+
3+
import com.runtracker.domain.upload.enums.UploadErrorCode;
4+
import com.runtracker.global.exception.CustomException;
5+
6+
public class FileStorageFailedException extends CustomException {
7+
public FileStorageFailedException() {
8+
super(UploadErrorCode.FILE_STORAGE_FAILED);
9+
}
10+
11+
public FileStorageFailedException(String message) {
12+
super(UploadErrorCode.FILE_STORAGE_FAILED, message);
13+
}
14+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.runtracker.domain.upload.exception;
2+
3+
import com.runtracker.domain.upload.enums.UploadErrorCode;
4+
import com.runtracker.global.exception.CustomException;
5+
6+
public class ImageConversionFailedException extends CustomException {
7+
public ImageConversionFailedException() {
8+
super(UploadErrorCode.IMAGE_CONVERSION_FAILED);
9+
}
10+
11+
public ImageConversionFailedException(String message) {
12+
super(UploadErrorCode.IMAGE_CONVERSION_FAILED, message);
13+
}
14+
}

0 commit comments

Comments
 (0)