Skip to content

Commit 15bad58

Browse files
committed
Amazon S3 Upload 구현 완료, 프론트엔드 static 파일 주소 변경
1 parent f8d3848 commit 15bad58

File tree

10 files changed

+160
-54
lines changed

10 files changed

+160
-54
lines changed

backend/build.gradle

+9
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,11 @@ dependencies {
3030
implementation 'io.jsonwebtoken:jjwt:0.12.5'
3131
implementation 'io.jsonwebtoken:jjwt-api:0.12.5'
3232
implementation 'com.sun.activation:jakarta.activation:2.0.1'
33+
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
3334
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5'
3435
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.5'
3536
implementation 'org.springframework.session:spring-session-core'
37+
implementation 'org.springframework.boot:spring-boot-starter-actuator'
3638
implementation 'org.springframework.boot:spring-boot-starter-data-jpa:3.2.2'
3739
compileOnly 'org.projectlombok:lombok'
3840
developmentOnly 'org.springframework.boot:spring-boot-devtools'
@@ -46,3 +48,10 @@ dependencies {
4648
tasks.named('test') {
4749
useJUnitPlatform()
4850
}
51+
52+
tasks.withType(JavaCompile) {
53+
options.incremental = false
54+
inputs.property("buildAlways", System.currentTimeMillis())
55+
}
56+
57+
tasks.build.dependsOn tasks.clean
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.spooder.weshlist.config;
2+
3+
import org.springframework.beans.factory.annotation.Value;
4+
import org.springframework.context.annotation.Bean;
5+
import org.springframework.context.annotation.ComponentScan;
6+
import org.springframework.context.annotation.Configuration;
7+
import org.springframework.context.annotation.PropertySource;
8+
import org.springframework.stereotype.Component;
9+
10+
import com.amazonaws.auth.*;
11+
import com.amazonaws.services.s3.AmazonS3Client;
12+
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
13+
14+
@Configuration
15+
@Component
16+
public class S3Config {
17+
18+
@Value("${cloud.aws.credentials.accessKey}")
19+
private String accessKey;
20+
21+
@Value("${cloud.aws.credentials.secretKey}")
22+
private String secretKey;
23+
24+
@Value("${cloud.aws.region.static}")
25+
private String region;
26+
27+
@Bean
28+
public AmazonS3Client amazonS3Client() {
29+
BasicAWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);
30+
31+
return (AmazonS3Client) AmazonS3ClientBuilder
32+
.standard()
33+
.withCredentials(new AWSStaticCredentialsProvider(credentials))
34+
.withRegion(region)
35+
.build();
36+
}
37+
}

backend/src/main/java/com/spooder/weshlist/controller/FileController.java

+16-24
Original file line numberDiff line numberDiff line change
@@ -2,39 +2,31 @@
22

33
import java.io.File;
44
import java.io.IOException;
5-
import java.nio.file.Files;
65

7-
import org.springframework.http.MediaType;
6+
import org.springframework.beans.factory.annotation.Autowired;
7+
import org.springframework.beans.factory.annotation.Value;
88
import org.springframework.http.ResponseEntity;
9-
import org.springframework.util.ResourceUtils;
9+
import org.springframework.security.access.method.P;
10+
import org.springframework.stereotype.Component;
1011
import org.springframework.web.bind.annotation.*;
12+
import org.springframework.web.multipart.MultipartFile;
1113

12-
import jakarta.activation.MimetypesFileTypeMap;
13-
14+
import com.amazonaws.services.s3.AmazonS3;
15+
import com.amazonaws.services.s3.AmazonS3Client;
16+
import com.amazonaws.services.s3.model.ObjectMetadata;
17+
import com.spooder.weshlist.service.FileService;
1418

1519
@RestController
20+
@Component
1621
@CrossOrigin(origins = "*", allowedHeaders = "*")
17-
@RequestMapping("/static")
22+
@RequestMapping("/file")
1823
public class FileController {
19-
@GetMapping("/image/{filename}")
20-
public ResponseEntity<byte[]> getImage(@PathVariable String filename) {
21-
try {
22-
File file = ResourceUtils.getFile("file:backend/image/"+filename);
23-
if (file.exists()) {
24-
byte[] imageFile = Files.readAllBytes(file.toPath());
25-
String mimeType = new MimetypesFileTypeMap().getContentType(file);
26-
MediaType mediaType = MediaType.parseMediaType(mimeType);
24+
25+
@Autowired
26+
private FileService fileService;
2727

28-
return ResponseEntity.ok()
29-
.contentType(mediaType)
30-
.body(imageFile);
31-
} else {
32-
return ResponseEntity.notFound().build();
33-
}
34-
} catch (IOException e) {
35-
return ResponseEntity.internalServerError().build();
36-
}
37-
28+
public ResponseEntity<String> uploadImage(MultipartFile imageFile) {
29+
return fileService.uploadImage(imageFile);
3830
}
3931

4032
}

backend/src/main/java/com/spooder/weshlist/controller/ProductController.java

+26-3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import org.springframework.beans.factory.annotation.Autowired;
99
import org.springframework.http.HttpStatus;
1010
import org.springframework.http.ResponseEntity;
11+
import org.springframework.security.access.method.P;
1112
import org.springframework.web.bind.annotation.CrossOrigin;
1213
import org.springframework.web.bind.annotation.DeleteMapping;
1314
import org.springframework.web.bind.annotation.GetMapping;
@@ -17,9 +18,11 @@
1718
import org.springframework.web.bind.annotation.RestController;
1819
import org.springframework.web.multipart.MultipartFile;
1920

21+
import com.amazonaws.services.s3.AmazonS3Client;
2022
import com.spooder.weshlist.Model.Product;
2123
import com.spooder.weshlist.Model.ProductDetail;
2224
import com.spooder.weshlist.dto.RatingDto;
25+
import com.spooder.weshlist.service.FileService;
2326
import com.spooder.weshlist.service.ProductService;
2427

2528
import org.slf4j.Logger;
@@ -38,14 +41,19 @@ public class ProductController {
3841
@Autowired
3942
private ProductService productService;
4043

44+
@Autowired
45+
private FileService fileService;
46+
4147
private static final Logger logger = LoggerFactory.getLogger(ProductService.class);
4248

4349
@PostMapping
4450
public ResponseEntity<Product> addProduct(@ModelAttribute Product product, @RequestPart(name = "imageFile", required = false) MultipartFile imageFile) {
4551
if (product == null) {
4652
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
4753
}
48-
Product savedProduct = productService.addProduct(product, imageFile);
54+
Product savedProduct = replaceImage(product, imageFile); // Image 처리 후, Service 단에 등록
55+
savedProduct = productService.addProduct(savedProduct);
56+
4957
return new ResponseEntity<>(savedProduct, HttpStatus.CREATED);
5058
}
5159

@@ -68,6 +76,8 @@ public Product getProductById(@PathVariable Long product_id) {
6876

6977
@PutMapping("/{product_id}")
7078
public ResponseEntity<String> updateProduct(@PathVariable Long product_id, @ModelAttribute Product updatedProduct, @RequestPart(name = "imageFile", required = false) MultipartFile imageFile) {
79+
80+
// 기존의 Product 불러온 후 새로운 Product의 정보를 주입해줌.
7181
Product product = productService.getProductById(product_id);
7282
Calendar calendar = Calendar.getInstance();
7383
calendar.setTimeZone(TimeZone.getTimeZone("Asia/Seoul"));
@@ -80,8 +90,8 @@ public ResponseEntity<String> updateProduct(@PathVariable Long product_id, @Mode
8090
else
8191
product.setUploader(updatedProduct.getUploader());
8292

83-
if (updatedProduct.getImage_name() == null) productService.replaceImageFile(product, imageFile);
84-
System.out.println(updatedProduct.getBrand());
93+
replaceImage(product, imageFile); // 이미지 없으면 알아서 걸러주므로, 일단 보내기
94+
8595
product.setName(updatedProduct.getName());
8696
product.setPrice(updatedProduct.getPrice());
8797
product.setBrand(updatedProduct.getBrand());
@@ -118,5 +128,18 @@ public ResponseEntity<String> deleteProduct(@PathVariable Long product_id) {
118128
return new ResponseEntity<>("Error on deleting image", HttpStatus.INTERNAL_SERVER_ERROR);
119129
}
120130
}
131+
132+
private Product replaceImage(Product product, MultipartFile imageFile) { // Product와 imageFile을 보내면 기존 Product에 이미지 업로드 및 등록 처리
133+
if (imageFile == null || imageFile.isEmpty()) {
134+
return product; // imageFile이 없으면 기존 product 반환
135+
}
136+
try {
137+
fileService.uploadImage(imageFile);
138+
product.setImage_name(imageFile.getOriginalFilename());
139+
} catch (Exception e) {
140+
e.printStackTrace();
141+
}
142+
return product;
143+
}
121144
}
122145

backend/src/main/java/com/spooder/weshlist/security/SecurityConfig.java

-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import org.springframework.context.annotation.Configuration;
55
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
66
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
7-
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration;
87
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
98
import org.springframework.security.crypto.password.PasswordEncoder;
109
import org.springframework.security.web.SecurityFilterChain;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.spooder.weshlist.service;
2+
3+
import java.io.IOException;
4+
5+
import org.springframework.beans.factory.annotation.Autowired;
6+
import org.springframework.beans.factory.annotation.Value;
7+
import org.springframework.http.ResponseEntity;
8+
import org.springframework.stereotype.Service;
9+
import org.springframework.web.multipart.MultipartFile;
10+
11+
import com.amazonaws.services.s3.AmazonS3Client;
12+
import com.amazonaws.services.s3.model.ObjectMetadata;
13+
14+
@Service
15+
public class FileService {
16+
17+
@Autowired
18+
private AmazonS3Client amazonS3Client;
19+
20+
@Value("${cloud.aws.s3.bucket}")
21+
private String bucket;
22+
23+
public ResponseEntity<String> uploadImage(MultipartFile imageFile) {
24+
if (imageFile != null && !imageFile.isEmpty()) { // 파일이 있을 경우에만 시도
25+
try {
26+
String filename = imageFile.getOriginalFilename();
27+
ObjectMetadata metadata = new ObjectMetadata();
28+
metadata.setContentType(imageFile.getContentType());
29+
metadata.setContentLength(imageFile.getSize());
30+
amazonS3Client.putObject(bucket, filename, imageFile.getInputStream(), metadata);
31+
} catch (IOException e) {
32+
e.printStackTrace();
33+
}
34+
}
35+
return ResponseEntity.ok("image uploaded");
36+
}
37+
}

backend/src/main/java/com/spooder/weshlist/service/ProductService.java

+7-20
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.spooder.weshlist.service;
22

33
import org.springframework.beans.factory.annotation.Autowired;
4+
import org.springframework.beans.factory.annotation.Value;
45
import org.springframework.stereotype.Service;
56
import org.springframework.web.multipart.MultipartFile;
67

@@ -17,11 +18,16 @@
1718
import org.slf4j.Logger;
1819
import org.slf4j.LoggerFactory;
1920

21+
import com.amazonaws.services.s3.AmazonS3Client;
22+
import com.amazonaws.services.s3.model.ObjectMetadata;
2023
import com.spooder.weshlist.Model.Product;
2124
import com.spooder.weshlist.Model.ProductDetail;
25+
import com.spooder.weshlist.controller.FileController;
2226
import com.spooder.weshlist.repository.ProductDetailRepository;
2327
import com.spooder.weshlist.repository.ProductRepository;
2428

29+
import lombok.extern.slf4j.Slf4j;
30+
2531
@Service
2632
public class ProductService {
2733
@Autowired
@@ -33,7 +39,7 @@ public class ProductService {
3339
private final String imageDirectory = "backend/image/";
3440
private static final Logger logger = LoggerFactory.getLogger(ProductService.class);
3541

36-
public Product addProduct(Product product, MultipartFile imageFile) {
42+
public Product addProduct(Product product) {
3743
Calendar calendar = Calendar.getInstance();
3844
calendar.setTimeZone(TimeZone.getTimeZone("Asia/Seoul"));
3945
Date now = calendar.getTime();
@@ -46,7 +52,6 @@ public Product addProduct(Product product, MultipartFile imageFile) {
4652
else productDetail.setUnknown(false);
4753
productDetail.setProduct(product);
4854
}
49-
replaceImageFile(product, imageFile);
5055

5156
if (product.getUploader() == null) {
5257
product.setUploader("익명");
@@ -78,24 +83,6 @@ public void deleteProduct(Long id) throws IOException {
7883
productRepository.deleteById(id);
7984
}
8085

81-
private void saveImageFile(MultipartFile imageFile, String filename) throws IOException {
82-
Files.copy(imageFile.getInputStream(), Paths.get(imageDirectory, filename), StandardCopyOption.REPLACE_EXISTING);
83-
}
84-
85-
public void replaceImageFile(Product product, MultipartFile imageFile) {
86-
if (imageFile != null && !imageFile.isEmpty()) {
87-
try {
88-
String originalFilename = imageFile.getOriginalFilename();
89-
String fileExtension = originalFilename.substring(originalFilename.lastIndexOf("."));
90-
String imageName = product.getName().replaceAll("\\s+", "_") + fileExtension;
91-
saveImageFile(imageFile, imageName);
92-
product.setImage_name(imageName);
93-
} catch (Exception e) {
94-
e.printStackTrace();
95-
}
96-
}
97-
}
98-
9986
public void updateProduct(Product product) {
10087
productRepository.save(product);
10188
}

frontend/src/components/ItemCard.vue

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<template>
22
<div @click="$router.push({path: '/finditem/detail', query: { id: id } })" class="m-4 bg-white rounded-xl shadow-lg">
33
<div class="h-28">
4-
<img v-if="productData.image_name" class="w-24 h-24 absolute my-2 mx-1" :src="backend_address+'/static/image/'+productData.image_name">
4+
<img v-if="productData.image_name" class="w-24 h-24 absolute my-2 mx-1" :src="static_address+productData.image_name">
55
<img v-if="!productData.image_name" class="w-24 h-24 absolute my-2 mx-1" src="@/assets/unavailable_image.png">
66
<div class="inline-block mt-2">
77
<p class="inline ml-24 pl-2 mt-2 text-lg">{{ productData.name }}</p>
@@ -31,13 +31,13 @@ export default defineComponent({
3131
data(){
3232
return {
3333
preview_description: '',
34-
backend_address: '',
34+
static_address: '',
3535
uploaded_date: ''
3636
}
3737
},
3838
beforeMount() {
3939
this.uploaded_date = changeDateFormat(this.productData.uploaded_date);
40-
this.backend_address = process.env.VUE_APP_BACKEND_ADDRESS;
40+
this.static_address = process.env.VUE_APP_STATIC_ADDRESS;
4141
if (this.$props.productData.detail[0].before_value !== null) {
4242
this.preview_description = `${this.$props.productData.detail[0].changed_point}
4343
${this.$props.productData.detail[0].before_value}${this.$props.productData.detail[0].unit}

frontend/src/views/ItemDetail.vue

+3-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<div class="m-4 p-2 rounded-xl shadow-lg">
33
<div class="">
44
<p class="text-right text-gray-400"><a @click="modify()">수정하기</a> | <a @click="remove()">삭제하기</a></p>
5-
<img v-if="productData?.image_name" class="max-w-full mx-auto" :src="backend_address+'/static/image/'+productData?.image_name">
5+
<img v-if="productData?.image_name" class="max-w-full mx-auto" :src="static_address+productData?.image_name">
66
<img v-if="!productData?.image_name" class="w-full" src="@/assets/unavailable_image.png">
77
<div class="ml-4">
88
<div id="inline-block">
@@ -51,7 +51,7 @@ export default defineComponent({
5151
setup() {
5252
let productData = ref<product| null>(null);
5353
let description = ref<string[]>([]);
54-
let backend_address = process.env.VUE_APP_BACKEND_ADDRESS;
54+
let static_address = process.env.VUE_APP_STATIC_ADDRESS;
5555
let uploaded_date = ref('');
5656
5757
onMounted(async ()=> {
@@ -85,7 +85,7 @@ export default defineComponent({
8585
productData.value!.negative_point--;
8686
}
8787
88-
return { productData, description, backend_address, uploaded_date, addPosValue, subPosValue, addNegValue, subNegValue };
88+
return { productData, description, static_address, uploaded_date, addPosValue, subPosValue, addNegValue, subNegValue };
8989
},
9090
methods: {
9191
async remove() {

frontend/webpack.config.js

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
const path = require('path');
2+
3+
module.exports = {
4+
// 기타 설정...
5+
resolve: {
6+
fallback: {
7+
url: require.resolve('url/'),
8+
fs: false,
9+
"https": false,
10+
util: require.resolve("util/")
11+
},
12+
extensions: ['.ts', '.tsx', '.js', '.jsx', '...']
13+
},
14+
plugins: [
15+
new webpack.ProvidePlugin({
16+
fs: 'empty'
17+
})
18+
],
19+
"compilerOptions": {
20+
"allowJs": true
21+
}
22+
};

0 commit comments

Comments
 (0)