diff --git a/be/issue_tracker/build.gradle b/be/issue_tracker/build.gradle index 884198ebd..b373b81af 100644 --- a/be/issue_tracker/build.gradle +++ b/be/issue_tracker/build.gradle @@ -40,6 +40,9 @@ dependencies { runtimeOnly 'com.mysql:mysql-connector-j' implementation 'org.springframework.boot:spring-boot-starter-data-jdbc' + // S3 + implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' + // Test testRuntimeOnly 'com.h2database:h2' testImplementation 'org.springframework.boot:spring-boot-starter-test' diff --git a/be/issue_tracker/src/main/java/team1/issuetracker/HomeController.java b/be/issue_tracker/src/main/java/team1/issuetracker/HomeController.java index ad0dcfd51..e67948926 100644 --- a/be/issue_tracker/src/main/java/team1/issuetracker/HomeController.java +++ b/be/issue_tracker/src/main/java/team1/issuetracker/HomeController.java @@ -1,18 +1,27 @@ package team1.issuetracker; import jakarta.servlet.http.HttpServletResponse; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import team1.issuetracker.util.S3Util; import java.io.IOException; @RestController @RequestMapping("/") +@RequiredArgsConstructor public class HomeController { + private final S3Util s3Util; + @GetMapping("/github") public void redirectToGithub(HttpServletResponse httpServletResponse) throws IOException { httpServletResponse.sendRedirect("https://github.com/codesquad-masters2024-team01/issue-tracker"); } + + @PostMapping("/upload") + public String uploadImage(@RequestPart(value = "image", required = false) MultipartFile image){ + return s3Util.upload(image); + } } diff --git a/be/issue_tracker/src/main/java/team1/issuetracker/config/S3Config.java b/be/issue_tracker/src/main/java/team1/issuetracker/config/S3Config.java new file mode 100644 index 000000000..dbe9841f0 --- /dev/null +++ b/be/issue_tracker/src/main/java/team1/issuetracker/config/S3Config.java @@ -0,0 +1,29 @@ +package team1.issuetracker.config; + +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.regions.Regions; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class S3Config { + + @Value("${aws.s3.accessKey}") + private String accessKey; + @Value("${aws.s3.secretKey}") + private String secretKey; + + @Bean + public AmazonS3 amazonS3Client(){ + return AmazonS3ClientBuilder.standard() + .withCredentials( + new AWSStaticCredentialsProvider(new BasicAWSCredentials(accessKey, secretKey)) + ) + .withRegion(Regions.AP_NORTHEAST_2) + .build(); + } +} diff --git a/be/issue_tracker/src/main/java/team1/issuetracker/domain/user/UserController.java b/be/issue_tracker/src/main/java/team1/issuetracker/domain/user/UserController.java index f55a070a8..c89c1a6b3 100644 --- a/be/issue_tracker/src/main/java/team1/issuetracker/domain/user/UserController.java +++ b/be/issue_tracker/src/main/java/team1/issuetracker/domain/user/UserController.java @@ -25,7 +25,7 @@ public void register(@RequestBody RegisterInfo registerInfo) throws IllegalArgum userService.createUser(registerInfo); } - @GetMapping("/duplicate") + @PostMapping("/duplicate") public boolean isDuplicate(@RequestBody CheckDuplicateRequest request) { if (request.id() != null) return userService.isDuplicateId(request.id()); if (request.nickname() != null) return userService.isDuplicateNickName(request.nickname()); @@ -42,7 +42,7 @@ public String login(@RequestBody LoginInfo loginInfo) { @GetMapping("/login/github") public String githubLogin(@RequestParam String code){ String id = githubLoginUtil.validateCode(code); - return id; + return jwtUtil.generateToken(id); } @GetMapping("/{id}") diff --git a/be/issue_tracker/src/main/java/team1/issuetracker/util/S3Util.java b/be/issue_tracker/src/main/java/team1/issuetracker/util/S3Util.java new file mode 100644 index 000000000..de3e57598 --- /dev/null +++ b/be/issue_tracker/src/main/java/team1/issuetracker/util/S3Util.java @@ -0,0 +1,95 @@ +package team1.issuetracker.util; + +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.CannedAccessControlList; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PutObjectRequest; +import com.amazonaws.util.IOUtils; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +@Component +@RequiredArgsConstructor +public class S3Util { + private final AmazonS3 amazonS3; + + @Value("${aws.s3.bucketName}") + private String bucketName; + + public String upload(MultipartFile image) { + //입력받은 이미지 파일이 빈 파일인지 검증 + if (image.isEmpty() || Objects.isNull(image.getOriginalFilename())) { + throw new IllegalArgumentException(); + } + //uploadImage를 호출하여 S3에 저장된 이미지의 public url을 반환한다. + return this.uploadImage(image); + } + + private String uploadImage(MultipartFile image) { + this.validateImageFileExtention(image.getOriginalFilename()); + try { + return this.uploadImageToS3(image); + } catch (IOException e) { + throw new IllegalArgumentException(e); + } + } + + private void validateImageFileExtention(String filename) { + int lastDotIndex = filename.lastIndexOf("."); + if (lastDotIndex == -1) { + throw new IllegalArgumentException("-1"); + } + + String extention = filename.substring(lastDotIndex + 1).toLowerCase(); + List allowedExtentionList = Arrays.asList("jpg", "jpeg", "png", "gif"); + + if (!allowedExtentionList.contains(extention)) { + throw new IllegalArgumentException("extention"); + } + } + + private String uploadImageToS3(MultipartFile image) throws IOException { + String originalFilename = image.getOriginalFilename(); //원본 파일 명 + String extention = originalFilename.substring(originalFilename.lastIndexOf(".")); //확장자 명 + + String s3FileName = UUID.randomUUID().toString().substring(0, 10) + originalFilename; //변경된 파일 명 + + InputStream is = image.getInputStream(); + byte[] bytes = IOUtils.toByteArray(is); //image를 byte[]로 변환 + + ObjectMetadata metadata = new ObjectMetadata(); //metadata 생성 + metadata.setContentType("image/" + extention); + metadata.setContentLength(bytes.length); + + //S3에 요청할 때 사용할 byteInputStream 생성 + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes); + + try { + //S3로 putObject 할 때 사용할 요청 객체 + //생성자 : bucket 이름, 파일 명, byteInputStream, metadata + PutObjectRequest putObjectRequest = + new PutObjectRequest(bucketName, s3FileName, byteArrayInputStream, metadata) + .withCannedAcl(CannedAccessControlList.PublicRead); + + //실제로 S3에 이미지 데이터를 넣는 부분이다. + amazonS3.putObject(putObjectRequest); // put image to S3 + } catch (Exception e) { + throw new IllegalArgumentException(e); + } finally { + byteArrayInputStream.close(); + is.close(); + } + + return amazonS3.getUrl(bucketName, s3FileName).toString(); + } +} diff --git a/be/issue_tracker/src/main/resources/static/index.html b/be/issue_tracker/src/main/resources/static/index.html index f47a7318a..497c6ca25 100644 --- a/be/issue_tracker/src/main/resources/static/index.html +++ b/be/issue_tracker/src/main/resources/static/index.html @@ -33,6 +33,46 @@ object-fit: cover; border: 2px solid #000; } + + .upload-btn { + display: inline-block; + padding: 10px 20px; + font-size: 16px; + cursor: pointer; + background-color: #4CAF50; + color: white; + border: none; + border-radius: 5px; + transition: background-color 0.3s; + text-align: center; + text-decoration: none; + margin-top: 10px; + } + + .upload-btn:hover { + background-color: #45a049; + } + + .upload-image-container { + margin-top: 20px; + width: 300px; + height: 300px; + border: 2px solid #000; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + } + + .upload-image-container img { + width: 100%; + height: 100%; + object-fit: cover; + } + + input[type="file"] { + display: none; + } @@ -48,6 +88,7 @@ > 로그인 + Image 1 @@ -59,5 +100,42 @@ Image 3 + +
+
+ +
+

No image chosen

+
+ + +
+
+ +