diff --git a/.gitignore b/.gitignore index 26572e9..f97013b 100644 --- a/.gitignore +++ b/.gitignore @@ -111,6 +111,7 @@ desktop.ini # Environment files application.properties credentials.json +service_account_key.json #프론트 templeates/ diff --git a/build.gradle b/build.gradle index 2b43b25..1215d8d 100644 --- a/build.gradle +++ b/build.gradle @@ -38,6 +38,9 @@ dependencies { implementation 'com.google.api-client:google-api-client:2.0.0' implementation 'com.google.auth:google-auth-library-oauth2-http:1.11.0' implementation 'com.google.code.gson:gson:2.11.0' + implementation 'com.google.api-client:google-api-client:2.2.0' + implementation 'com.google.auth:google-auth-library-credentials:1.16.1' + // JWT 관련 의존성 implementation 'io.jsonwebtoken:jjwt-api:0.11.5' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' diff --git a/src/main/java/com/boot/swlugweb/v1/DriveQuickstart.java b/src/main/java/com/boot/swlugweb/v1/DriveQuickstart.java new file mode 100644 index 0000000..90cfbb8 --- /dev/null +++ b/src/main/java/com/boot/swlugweb/v1/DriveQuickstart.java @@ -0,0 +1,121 @@ +package com.boot.swlugweb.v1; + +import com.google.api.client.auth.oauth2.Credential; +import com.google.api.client.extensions.java6.auth.oauth2.AuthorizationCodeInstalledApp; +import com.google.api.client.extensions.jetty.auth.oauth2.LocalServerReceiver; +import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow; +import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets; +import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport; +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.gson.GsonFactory; +import com.google.api.client.util.store.FileDataStoreFactory; +import com.google.api.services.drive.Drive; +import com.google.api.services.drive.DriveScopes; +import com.google.api.services.drive.model.File; +import com.google.api.services.drive.model.FileList; +import com.google.auth.http.HttpCredentialsAdapter; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.auth.oauth2.ServiceAccountCredentials; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.security.GeneralSecurityException; +import java.util.Collections; +import java.util.List; + +/* class to demonstrate use of Drive files list API */ +public class DriveQuickstart { + /** + * Application name. + */ + private static final String APPLICATION_NAME = "Google Drive API Java Quickstart"; + /** + * Global instance of the JSON factory. + */ + private static final JsonFactory JSON_FACTORY = GsonFactory.getDefaultInstance(); + /** + * Directory to store authorization tokens for this application. + */ + private static final String TOKENS_DIRECTORY_PATH = "tokens"; + + /** + * Global instance of the scopes required by this quickstart. + * If modifying these scopes, delete your previously saved tokens/ folder. + */ + private static final List SCOPES = + Collections.singletonList(DriveScopes.DRIVE); +// private static final String CREDENTIALS_FILE_PATH = "/credentials1.json"; + private static final String CREDENTIALS_FILE_PATH = "/service_account_key.json"; + + /** + * Creates an authorized Credential object. + * +// * @param HTTP_TRANSPORT The network HTTP Transport. +// * @return An authorized Credential object. + * @throws IOException If the credentials.json file cannot be found. + */ +// public static Credential getCredentials(final NetHttpTransport HTTP_TRANSPORT) +// throws IOException { +// // Load client secrets. +// InputStream in = DriveQuickstart.class.getResourceAsStream(CREDENTIALS_FILE_PATH); +// if (in == null) { +// throw new FileNotFoundException("Resource not found: " + CREDENTIALS_FILE_PATH); +// } +// GoogleClientSecrets clientSecrets = +// GoogleClientSecrets.load(JSON_FACTORY, new InputStreamReader(in)); +// +// // Build flow and trigger user authorization request. +// GoogleAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow.Builder( +// HTTP_TRANSPORT, JSON_FACTORY, clientSecrets, SCOPES) +// .setDataStoreFactory(new FileDataStoreFactory(new java.io.File(TOKENS_DIRECTORY_PATH))) +// .setAccessType("offline") +// .build(); +// LocalServerReceiver receiver = new LocalServerReceiver.Builder().setPort(8888).build(); +// Credential credential = new AuthorizationCodeInstalledApp(flow, receiver).authorize("user"); +// //returns an authorized Credential object. +// return credential; +// } + + public static Drive getDriveService() throws Exception { + InputStream in = DriveQuickstart.class.getResourceAsStream(CREDENTIALS_FILE_PATH); + if (in == null) { + throw new RuntimeException("service_account_key.json not found"); + } + + GoogleCredentials credentials = ServiceAccountCredentials.fromStream(in) + .createScoped(SCOPES); + + return new Drive.Builder( + GoogleNetHttpTransport.newTrustedTransport(), + JSON_FACTORY, + new HttpCredentialsAdapter(credentials) + ).setApplicationName(APPLICATION_NAME).build(); + } + public static void main(String... args) throws Exception { + // Build a new authorized API client service. + final NetHttpTransport HTTP_TRANSPORT = GoogleNetHttpTransport.newTrustedTransport(); + Drive service = new Drive.Builder(HTTP_TRANSPORT, JSON_FACTORY, (com.google.api.client.http.HttpRequestInitializer) getDriveService()) + .setApplicationName(APPLICATION_NAME) + .build(); + + // Print the names and IDs for up to 10 files. + FileList result = service.files().list() + .setPageSize(10) + .setFields("nextPageToken, files(id, name)") + .execute(); + List files = result.getFiles(); + if (files == null || files.isEmpty()) { + System.out.println("No files found."); + } else { + System.out.println("Files:"); + for (File file : files) { + System.out.printf("%s (%s)\n", file.getName(), file.getId()); + } + } + } + + +} \ No newline at end of file diff --git a/src/main/java/com/boot/swlugweb/v1/blog/BlogController.java b/src/main/java/com/boot/swlugweb/v1/blog/BlogController.java index 7941c2e..1a03471 100644 --- a/src/main/java/com/boot/swlugweb/v1/blog/BlogController.java +++ b/src/main/java/com/boot/swlugweb/v1/blog/BlogController.java @@ -1,33 +1,30 @@ package com.boot.swlugweb.v1.blog; import jakarta.servlet.http.HttpSession; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.core.io.Resource; -import org.springframework.core.io.UrlResource; import org.springframework.http.*; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import lombok.extern.slf4j.Slf4j; + +@Slf4j @RestController @RequestMapping("/api/blog") public class BlogController { - @Value("${file.upload-dir}") - private String uploadDir; - private final BlogService blogService; + private final GoogleDriveService googleDriveService; - public BlogController(BlogService blogService) { + public BlogController(BlogService blogService,GoogleDriveService googleDriveService) + { this.blogService = blogService; + this.googleDriveService = googleDriveService; } @GetMapping @@ -49,78 +46,130 @@ public ResponseEntity getBlogDetail(@RequestBody Map saveBlog( - @RequestPart(name = "blogCreateDto") BlogCreateDto blogCreateDto, + @RequestPart("blogCreateDto") BlogCreateDto blogCreateDto, @RequestPart(name = "imageFiles", required = false) List imageFiles, HttpSession session) { String userId = (String) session.getAttribute("USER"); - if (userId == null) { - return ResponseEntity.status(401).build(); - } + if (userId == null) return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); try { - // HTML 컨텐츠에서 이미지 처리를 위한 코드 추가 - if (blogCreateDto.getBoardContent() != null) { - blogCreateDto.setImageFiles(imageFiles); - } + // 👉 imageFiles만 DTO에 세팅 (업로드는 서비스에서 수행) + blogCreateDto.setImageFiles(imageFiles); + blogService.createBlog(blogCreateDto, userId); - return ResponseEntity.status(302) + + return ResponseEntity.status(HttpStatus.FOUND) .header(HttpHeaders.LOCATION, "/api/blog") .build(); + } catch (Exception e) { e.printStackTrace(); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } } + + + + @PostMapping(value = "/update", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public ResponseEntity updateBlogPost( - @RequestPart(name = "blogUpdateRequestDto") BlogUpdateRequestDto blogUpdateRequestDto, + public ResponseEntity updateBlogPost( + @RequestPart("blogUpdateRequestDto") BlogUpdateRequestDto blogUpdateRequestDto, @RequestPart(name = "imageFiles", required = false) List imageFiles, - HttpSession session - ) { + HttpSession session) { + String userId = (String) session.getAttribute("USER"); if (userId == null) { - return ResponseEntity.status(401).build(); + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } try { - if (imageFiles != null && !imageFiles.isEmpty()) { - blogUpdateRequestDto.setImageFiles(imageFiles); - } + // 새 이미지 파일들을 DTO에 주입 + blogUpdateRequestDto.setImageFiles(imageFiles); blogService.updateBlog(blogUpdateRequestDto, userId); - return ResponseEntity.status(302) + return ResponseEntity.status(HttpStatus.FOUND) .header(HttpHeaders.LOCATION, "/api/blog") .build(); + } catch (Exception e) { + e.printStackTrace(); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } } + + + @PostMapping("/delete") public ResponseEntity deleteBlog( @RequestBody BlogDeleteRequestDto blogDeleteRequestDto, - HttpSession session - ) { + HttpSession session) { + String userId = (String) session.getAttribute("USER"); - if (userId == null) { - return ResponseEntity.status(401).build(); - } + if (userId == null) return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); try { + List imageUrls = blogService.getImageUrlsByBlogId(blogDeleteRequestDto.getId()); + + + for (String url : imageUrls) { + String fileId = extractFileIdFromUrl(url); + if (fileId == null) { + log.warn("유효하지 않은 Google Drive 이미지 URL입니다: {}", url); + continue; + } + + googleDriveService.deleteFile(fileId); + } + + blogService.deleteBlog(blogDeleteRequestDto, userId); - return ResponseEntity.status(302) + + return ResponseEntity.status(HttpStatus.FOUND) .header(HttpHeaders.LOCATION, "/api/blog") .build(); + } catch (Exception e) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } } + public String extractFileIdFromUrl(String url) { + if (url == null) return null; + + // 1. drive.google.com 링크 + if (url.contains("drive.google.com/file/d/")) { + int start = url.indexOf("/d/") + 3; + int end = url.indexOf("/", start); + if (start > 2 && end > start) { + return url.substring(start, end); + } + } + + // 2. lh3.googleusercontent.com 링크 + if (url.contains("lh3.googleusercontent.com/d/")) { + int start = url.indexOf("/d/") + 3; + int end = url.indexOf("?", start); // 쿼리 파라미터 제거 (optional) + if (end == -1) end = url.length(); // ?가 없는 경우 + return url.substring(start, end); + } + + // 3. https://drive.google.com/uc?id=FILE_ID + Pattern pattern = Pattern.compile("id=([^&]+)"); + Matcher matcher = pattern.matcher(url); + if (matcher.find()) { + return matcher.group(1); + } + return null; + } + + + @GetMapping("/tags") public ResponseEntity> getTags() { List tags = blogService.getAllTags(); @@ -152,33 +201,4 @@ public ResponseEntity> uploadImage(@RequestParam("upload") M } } - // 이미지 조회 엔드포인트 - @GetMapping("/images/{filename:.+}") - @ResponseBody - public ResponseEntity serveImage(@PathVariable String filename) { - try { - Path imagePath = Paths.get(uploadDir).resolve(filename); - Resource resource = new UrlResource(imagePath.toUri()); - - if (resource.exists() && resource.isReadable()) { - String contentType = Files.probeContentType(imagePath); - if (contentType == null) { - contentType = "application/octet-stream"; - } - - // 캐시 설정 추가 - CacheControl cacheControl = CacheControl.maxAge(365, TimeUnit.DAYS); - - return ResponseEntity.ok() - .cacheControl(cacheControl) - .contentType(MediaType.parseMediaType(contentType)) - .header("Content-Disposition", "inline; filename=\"" + filename + "\"") - .body(resource); - } else { - return ResponseEntity.notFound().build(); - } - } catch (IOException e) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); - } - } } \ No newline at end of file diff --git a/src/main/java/com/boot/swlugweb/v1/blog/BlogCreateDto.java b/src/main/java/com/boot/swlugweb/v1/blog/BlogCreateDto.java index f359c38..c5ebd32 100644 --- a/src/main/java/com/boot/swlugweb/v1/blog/BlogCreateDto.java +++ b/src/main/java/com/boot/swlugweb/v1/blog/BlogCreateDto.java @@ -3,13 +3,13 @@ import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; import org.springframework.web.multipart.MultipartFile; import java.time.LocalDateTime; import java.util.List; + @Setter @Getter @Builder @@ -22,6 +22,8 @@ public class BlogCreateDto { @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss") private LocalDateTime createAt; + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss") + private LocalDateTime updateAt; private List imageUrl; private List imageFiles; } diff --git a/src/main/java/com/boot/swlugweb/v1/blog/BlogDetailResponseDto.java b/src/main/java/com/boot/swlugweb/v1/blog/BlogDetailResponseDto.java index 6916aaf..a01dfde 100644 --- a/src/main/java/com/boot/swlugweb/v1/blog/BlogDetailResponseDto.java +++ b/src/main/java/com/boot/swlugweb/v1/blog/BlogDetailResponseDto.java @@ -14,6 +14,7 @@ public class BlogDetailResponseDto { private String boardTitle; private String boardContents; private LocalDateTime createAt; + private LocalDateTime updateAt; private String userId; private String nickname; private List tag; diff --git a/src/main/java/com/boot/swlugweb/v1/blog/BlogDomain.java b/src/main/java/com/boot/swlugweb/v1/blog/BlogDomain.java index 6ea6c93..7b72e7c 100644 --- a/src/main/java/com/boot/swlugweb/v1/blog/BlogDomain.java +++ b/src/main/java/com/boot/swlugweb/v1/blog/BlogDomain.java @@ -38,6 +38,10 @@ public class BlogDomain { @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") // JSON 직렬화 시 포맷 지정 private LocalDateTime createAt; + @Field("updated_at") + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") + private LocalDateTime updateAt; + @Field("tag") private List tag; diff --git a/src/main/java/com/boot/swlugweb/v1/blog/BlogDto.java b/src/main/java/com/boot/swlugweb/v1/blog/BlogDto.java index 8988852..87eb88a 100644 --- a/src/main/java/com/boot/swlugweb/v1/blog/BlogDto.java +++ b/src/main/java/com/boot/swlugweb/v1/blog/BlogDto.java @@ -1,6 +1,7 @@ package com.boot.swlugweb.v1.blog; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Getter; import lombok.Setter; import java.time.LocalDateTime; @@ -14,6 +15,7 @@ public class BlogDto { private Integer boardCategory; private String boardTitle; private LocalDateTime createAt; + private LocalDateTime updateAt; @JsonIgnore private String userId; private String nickname; @@ -29,7 +31,7 @@ public class BlogDto { private String thumbnailImage; // 필드는 유지 public String getThumbnailUrl() { - if (image != null && !image.isEmpty()) { + if (image != null && !image.isEmpty() && image.get(0) != null) { String firstImage = image.get(0); return firstImage.startsWith("/api/blog/images/") ? firstImage @@ -37,4 +39,5 @@ public String getThumbnailUrl() { } return "/img/apply_swlug.png"; } + } \ No newline at end of file diff --git a/src/main/java/com/boot/swlugweb/v1/blog/BlogService.java b/src/main/java/com/boot/swlugweb/v1/blog/BlogService.java index 910ae2e..1888d5e 100644 --- a/src/main/java/com/boot/swlugweb/v1/blog/BlogService.java +++ b/src/main/java/com/boot/swlugweb/v1/blog/BlogService.java @@ -9,101 +9,51 @@ import org.springframework.util.StringUtils; import org.springframework.web.multipart.MultipartFile; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardCopyOption; + import java.time.LocalDateTime; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; +import static com.google.common.io.Files.getFileExtension; + @Service public class BlogService { - @Value("${file.upload-dir}") - private String uploadDir; +// @Value("${file.upload-dir}") +// private String uploadDir; private final BlogRepository blogRepository; private final MyPageRepository myPageRepository; + private final GoogleDriveService googleDriveService; - public BlogService(BlogRepository blogRepository, MyPageRepository myPageRepository) { + public BlogService(BlogRepository blogRepository, MyPageRepository myPageRepository,GoogleDriveService googleDriveService) { this.blogRepository = blogRepository; this.myPageRepository = myPageRepository; + this.googleDriveService = googleDriveService; } + public String saveImage(MultipartFile file) throws Exception { + if (file == null || file.isEmpty()) throw new IllegalArgumentException("Empty file"); - // BlogService.java - public String saveImage(MultipartFile file) throws IOException { - try { - // 파일 유효성 검사 - if (file.isEmpty()) { - throw new IllegalArgumentException("Empty file"); - } - - // 파일 크기 검사 (10MB) - if (file.getSize() > 20 * 1024 * 1024) { - throw new IllegalArgumentException("File size exceeds maximum limit"); - } - - // 파일 확장자 검사 - String originalFilename = StringUtils.cleanPath(file.getOriginalFilename()); - String extension = getFileExtension(originalFilename).toLowerCase(); - Set allowedExtensions = new HashSet<>(Arrays.asList( - "jpg", "jpeg", "png", "gif", "bmp", "webp", "heic", "heif", "tiff", "tif", "svg" - )); + if (file.getSize() > 20 * 1024 * 1024) // 20MB 제한 + throw new IllegalArgumentException("File size exceeds maximum limit"); - if (!allowedExtensions.contains(extension)) { - throw new IllegalArgumentException("Invalid file extension"); - } + String originalFilename = StringUtils.cleanPath(file.getOriginalFilename()); + String extension = getFileExtension(originalFilename).toLowerCase(); - // 고유한 파일명 생성 - String newFilename = UUID.randomUUID().toString() + "." + extension; - Path uploadPath = Paths.get(uploadDir); - - // 업로드 디렉토리가 없으면 생성 - if (!Files.exists(uploadPath)) { - Files.createDirectories(uploadPath); - System.out.println("Created upload directory: " + uploadPath.toAbsolutePath()); - } - - // 파일 저장 - Path destinationFile = uploadPath.resolve(newFilename); - Files.copy(file.getInputStream(), destinationFile, StandardCopyOption.REPLACE_EXISTING); - System.out.println("File saved at: " + destinationFile.toAbsolutePath()); + Set allowedExtensions = Set.of( + "jpg", "jpeg", "png", "gif", "bmp", "webp", "heic", "heif", "tiff", "tif", "svg" + ); - // 접근 가능한 URL 반환 - return "/api/blog/images/" + newFilename; - } catch (IOException e) { - System.err.println("Error saving file: " + e.getMessage()); - e.printStackTrace(); - throw e; + if (!allowedExtensions.contains(extension)) { + throw new IllegalArgumentException("Invalid file extension: " + extension); } - } - // 파일 확장자 추출 메서드 - private String getFileExtension(String filename) { - int lastDotIndex = filename.lastIndexOf('.'); - if (lastDotIndex > 0) { - return filename.substring(lastDotIndex + 1); - } - return ""; - } - // 이미지 삭제 메소드 - public void deleteImage(String imageUrl) { - if (imageUrl != null && imageUrl.startsWith("/api/blog/images/")) { - String filename = imageUrl.substring("/api/blog/images/".length()); - try { - Path imagePath = Paths.get(uploadDir).resolve(filename); - Files.deleteIfExists(imagePath); - } catch (IOException e) { - e.printStackTrace(); - } - } + return googleDriveService.uploadFileToDrive(file); } - public BlogDomain createBlog(BlogCreateDto blogCreateDto, String userId) throws IOException { + public BlogDomain createBlog(BlogCreateDto blogCreateDto, String userId) throws Exception { BlogDomain blogDomain = new BlogDomain(); blogDomain.setUserId(userId); @@ -111,84 +61,128 @@ public BlogDomain createBlog(BlogCreateDto blogCreateDto, String userId) throws blogDomain.setBoardTitle(blogCreateDto.getBoardTitle()); blogDomain.setBoardContents(blogCreateDto.getBoardContent()); blogDomain.setCreateAt(LocalDateTime.now()); + blogDomain.setUpdateAt(LocalDateTime.now()); blogDomain.setTag(blogCreateDto.getTag()); blogDomain.setIsPin(false); blogDomain.setIsSecure(0); blogDomain.setIsDelete(0); List uploadedImageUrls = new ArrayList<>(); - if (blogCreateDto.getImageFiles() != null && !blogCreateDto.getImageFiles().isEmpty()) { - for (MultipartFile file : blogCreateDto.getImageFiles()) { - try { - String imageUrl = saveImage(file); - uploadedImageUrls.add(imageUrl); - } catch (Exception e) { - uploadedImageUrls.forEach(this::deleteImage); - throw e; + + try { + // 1️⃣ 업로드된 이미지 파일 처리 (saveImage 사용) + if (blogCreateDto.getImageFiles() != null) { + for (MultipartFile file : blogCreateDto.getImageFiles()) { + uploadedImageUrls.add(saveImage(file)); } } - } - // HTML 컨텐츠에서 이미지 URL 추출 - Pattern pattern = Pattern.compile("src=\"(/api/blog/images/[^\"]+)\""); - Matcher matcher = pattern.matcher(blogCreateDto.getBoardContent()); - while (matcher.find()) { - String imageUrl = matcher.group(1); - if (!uploadedImageUrls.contains(imageUrl)) { - uploadedImageUrls.add(imageUrl); + // 2️⃣ 본문 HTML에 포함된 이미지 URL도 추출해서 저장 + Pattern pattern = Pattern.compile("src=[\"']([^\"']+)[\"']"); + Matcher matcher = pattern.matcher(blogCreateDto.getBoardContent()); + + while (matcher.find()) { + String imageUrl = matcher.group(1); + if (!uploadedImageUrls.contains(imageUrl)) { + uploadedImageUrls.add(imageUrl); + } } + + } catch (Exception e) { + // 업로드한 이미지 모두 삭제 + uploadedImageUrls.forEach(this::deleteImage); + throw e; } blogDomain.setImage(uploadedImageUrls); + + return blogRepository.save(blogDomain); + } + + private void deleteImage(String imageUrl) { + try { + String fileId = extractFileIdFromUrl(imageUrl); // 🔍 fileId 추출 + googleDriveService.deleteFile(fileId); + System.out.println("🗑️ Deleted fileId: " + fileId); + } catch (Exception e) { + System.err.println("❌ Failed to delete image: " + imageUrl); + e.printStackTrace(); + } } - public void updateBlog(BlogUpdateRequestDto blogUpdateRequestDto, String userId) throws IOException { - BlogDomain blog = blogRepository.findById(blogUpdateRequestDto.getId()) - .orElseThrow(() -> new IllegalArgumentException("Blog not found")); - if (!blog.getUserId().equals(userId)) { - throw new SecurityException("Not authorized"); + // ✅ URL에서 fileId 추출 + public String extractFileIdFromUrl(String url) { + if (url == null) return null; + + // 1. drive.google.com 링크 + if (url.contains("drive.google.com/file/d/")) { + int start = url.indexOf("/d/") + 3; + int end = url.indexOf("/", start); + if (start > 2 && end > start) { + return url.substring(start, end); + } } - if (blogUpdateRequestDto.getBoardTitle() != null) { - blog.setBoardTitle(blogUpdateRequestDto.getBoardTitle()); + // 2. lh3.googleusercontent.com 링크 + if (url.contains("lh3.googleusercontent.com/d/")) { + int start = url.indexOf("/d/") + 3; + int end = url.indexOf("?", start); // 쿼리 파라미터 제거 (optional) + if (end == -1) end = url.length(); // ?가 없는 경우 + return url.substring(start, end); } - if (blogUpdateRequestDto.getBoardContent() != null) { - blog.setBoardContents(blogUpdateRequestDto.getBoardContent()); + + // 3. https://drive.google.com/uc?id=FILE_ID + Pattern pattern = Pattern.compile("id=([^&]+)"); + Matcher matcher = pattern.matcher(url); + if (matcher.find()) { + return matcher.group(1); } - if (blogUpdateRequestDto.getTag() != null) { - blog.setTag(blogUpdateRequestDto.getTag()); + return null; + } + + + + public void updateBlog(BlogUpdateRequestDto dto, String userId) throws Exception { + BlogDomain blog = blogRepository.findById(dto.getId()) + .orElseThrow(() -> new IllegalArgumentException("Blog not found")); + + if (!blog.getUserId().equals(userId)) { + throw new SecurityException("Not authorized"); } + // 제목, 내용, 태그 수정 + if (dto.getBoardTitle() != null) blog.setBoardTitle(dto.getBoardTitle()); + if (dto.getBoardContent() != null) blog.setBoardContents(dto.getBoardContent()); + if (dto.getTag() != null) blog.setTag(dto.getTag()); + // 기존 이미지 목록 List currentImageUrls = blog.getImage() != null ? new ArrayList<>(blog.getImage()) : new ArrayList<>(); - List updatedImageUrls = blogUpdateRequestDto.getImageUrls() != null ? - blogUpdateRequestDto.getImageUrls() : new ArrayList<>(); + List updatedImageUrls = dto.getImageUrls() != null ? new ArrayList<>(dto.getImageUrls()) : new ArrayList<>(); + // 삭제 대상 이미지 계산 List imagesToDelete = new ArrayList<>(currentImageUrls); imagesToDelete.removeAll(updatedImageUrls); + for (String imageUrl : imagesToDelete) { - deleteImage(imageUrl); + deleteImage(imageUrl); // Google Drive에서 삭제 } - if (blogUpdateRequestDto.getImageFiles() != null) { - for (MultipartFile file : blogUpdateRequestDto.getImageFiles()) { - try { - String imageUrl = saveImage(file); - updatedImageUrls.add(imageUrl); - } catch (Exception e) { - e.printStackTrace(); - throw e; - } + // 새로 추가할 이미지 업로드 + if (dto.getImageFiles() != null && !dto.getImageFiles().isEmpty()) { + for (MultipartFile file : dto.getImageFiles()) { + String imageUrl = saveImage(file); // 업로드 후 URL 반환 + updatedImageUrls.add(imageUrl); } } blog.setImage(updatedImageUrls); - blog.setCreateAt(LocalDateTime.now()); + blog.setUpdateAt(LocalDateTime.now()); blogRepository.save(blog); } + public void deleteBlog(BlogDeleteRequestDto blogDeleteRequestDto, String userId) { BlogDomain blog = blogRepository.findById(blogDeleteRequestDto.getId()) .orElseThrow(() -> new IllegalArgumentException("Blog not found")); @@ -288,6 +282,7 @@ public BlogDetailResponseDto getBlogDetail(String id) { blogDetailResponseDto.setBoardContents(blog.getBoardContents()); blogDetailResponseDto.setNickname(nickname); blogDetailResponseDto.setCreateAt(blog.getCreateAt()); + blogDetailResponseDto.setUpdateAt(blog.getUpdateAt()); blogDetailResponseDto.setTag(blog.getTag()); blogDetailResponseDto.setImage(blog.getImage()); blogDetailResponseDto.setThumbnailImage(blog.getThumbnailImage()); @@ -335,4 +330,13 @@ public Map getAdjacentBlogs(String id) { public List getAllTags() { return blogRepository.findAllTags(); } + + //구글 코드 + public List getImageUrlsByBlogId(String blogId) { + return blogRepository.findById(blogId) + .map(BlogDomain::getImage) + .orElse(Collections.emptyList()); + } + + } \ No newline at end of file diff --git a/src/main/java/com/boot/swlugweb/v1/blog/BlogUpdateRequestDto.java b/src/main/java/com/boot/swlugweb/v1/blog/BlogUpdateRequestDto.java index 8d93d2c..4a13835 100644 --- a/src/main/java/com/boot/swlugweb/v1/blog/BlogUpdateRequestDto.java +++ b/src/main/java/com/boot/swlugweb/v1/blog/BlogUpdateRequestDto.java @@ -19,4 +19,5 @@ public class BlogUpdateRequestDto { private String thumbnailImage; private List imageUrls; private List imageFiles; + private LocalDateTime updateAt; } diff --git a/src/main/java/com/boot/swlugweb/v1/blog/GoogleDriveService.java b/src/main/java/com/boot/swlugweb/v1/blog/GoogleDriveService.java new file mode 100644 index 0000000..77da034 --- /dev/null +++ b/src/main/java/com/boot/swlugweb/v1/blog/GoogleDriveService.java @@ -0,0 +1,188 @@ +package com.boot.swlugweb.v1.blog; + +import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport; +import com.google.api.client.http.FileContent; +import com.google.api.client.http.InputStreamContent; +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.client.json.gson.GsonFactory; +import org.springframework.beans.factory.annotation.Value; +import com.google.api.services.drive.Drive; +import com.google.api.services.drive.DriveScopes; +import com.google.api.services.drive.model.Permission; +import com.google.auth.http.HttpCredentialsAdapter; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.auth.oauth2.ServiceAccountCredentials; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.io.*; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.GeneralSecurityException; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +@Component +@Service +public class GoogleDriveService { + private static final String APPLICATION_NAME = "Google Drive API Java with Service Account"; + private static final GsonFactory JSON_FACTORY = GsonFactory.getDefaultInstance(); + private static final List SCOPES = Collections.singletonList(DriveScopes.DRIVE_FILE); + private static final String SERVICE_ACCOUNT_KEY_PATH = "/service_account_key.json"; + + + private final Drive driveService; + + @Value("${google.drive.folder-id}") + private String folderId; + + + //서비스 코드 + public GoogleDriveService() throws Exception { + final NetHttpTransport HTTP_TRANSPORT = GoogleNetHttpTransport.newTrustedTransport(); + GoogleCredentials credentials = getCredentials(); + this.driveService = new Drive.Builder( + HTTP_TRANSPORT, + JSON_FACTORY, + new HttpCredentialsAdapter(credentials) + ).setApplicationName(APPLICATION_NAME).build(); + } + private GoogleCredentials getCredentials() throws IOException { + InputStream in = GoogleDriveService.class.getResourceAsStream(SERVICE_ACCOUNT_KEY_PATH); + if (in == null) { + throw new FileNotFoundException("Service account key not found at " + SERVICE_ACCOUNT_KEY_PATH); + } + return ServiceAccountCredentials.fromStream(in) + .createScoped(Collections.singletonList("https://www.googleapis.com/auth/drive")); + } + + public String uploadFileToDrive(MultipartFile file) { + try { + // 1. 파일 메타데이터 설정 + com.google.api.services.drive.model.File fileMetadata = new com.google.api.services.drive.model.File(); + fileMetadata.setName(file.getOriginalFilename()); + fileMetadata.setParents(Collections.singletonList(folderId)); // 폴더 ID + + // 2. 파일 내용 설정 (application/octet-stream은 범용이지만 필요시 "image/jpeg" 등으로 교체 가능) + InputStreamContent mediaContent = new InputStreamContent( + "application/octet-stream", file.getInputStream() + ); + + // 3. 업로드 요청 생성 + Drive.Files.Create createRequest = driveService.files() + .create(fileMetadata, mediaContent) + .setFields("id, webViewLink"); + + // 4. 업로드 상태 로깅 리스너 등록 + createRequest.getMediaHttpUploader().setProgressListener( + uploader -> { + switch (uploader.getUploadState()) { + case INITIATION_STARTED: + System.out.println("✅ Upload Initiation Started"); + break; + case INITIATION_COMPLETE: + System.out.println("✅ Upload Initiation Complete"); + break; + case MEDIA_IN_PROGRESS: + System.out.printf("🔄 Upload Progress: %.2f%%\n", uploader.getProgress() * 100); + break; + case MEDIA_COMPLETE: + System.out.println("✅ Upload Complete"); + break; + default: + System.out.println("⚠️ Upload State: " + uploader.getUploadState()); + break; + } + } + ); + + // 5. 실제 업로드 수행 + com.google.api.services.drive.model.File uploadedFile = createRequest.execute(); + String fileId = uploadedFile.getId(); + + System.out.println("✅ File Uploaded: " + fileId); + + // 6. 업로드된 파일 공개 권한 설정 + setFilePublic(fileId); + + // 7. 공유 가능한 웹 뷰 링크 대신 직접 렌더링 URL 반환 + return "https://lh3.googleusercontent.com/d/" + uploadedFile.getId(); + + } catch (Exception e) { + System.err.println("❌ 파일 업로드 실패: " + file.getOriginalFilename()); + e.printStackTrace(); + throw new RuntimeException("Google Drive upload failed", e); + } + } + + private void setFilePublic(String fileId) throws IOException { + Permission permission = new Permission() + .setType("anyone") // 누구나 접근 가능 + .setRole("reader") // 읽기 권한만 + .setAllowFileDiscovery(false); // 검색엔진에서 노출 방지 + + driveService.permissions().create(fileId, permission) + .setFields("id") + .execute(); + + System.out.println("🌍 File is now public: " + fileId); + } + + + + + + + + + + public String uploadFile(MultipartFile file) throws IOException { + java.io.File convFile = new java.io.File(System.getProperty("java.io.tmpdir") + "/" + file.getOriginalFilename()); + file.transferTo(convFile); + + com.google.api.services.drive.model.File fileMetaData = new com.google.api.services.drive.model.File(); + String randomFileName = UUID.randomUUID().toString() + ".jpg"; + fileMetaData.setName(randomFileName); + + FileContent fileContent = new FileContent("image/jpeg", convFile); + com.google.api.services.drive.model.File uploadedFile = driveService.files().create(fileMetaData, fileContent).execute(); + + return "https://drive.google.com/uc?id=" + uploadedFile.getId(); + } + + + + + + + + private Path saveTempFile(MultipartFile multipartFile) throws IOException { + Path tempDir = Paths.get(System.getProperty("java.io.tmpdir")); + Path tempFile = tempDir.resolve(multipartFile.getOriginalFilename()); + try (FileOutputStream fos = new FileOutputStream(tempFile.toFile())) { + fos.write(multipartFile.getBytes()); + } + return tempFile; + } + + + public void deleteFile(String fileId) throws IOException, GeneralSecurityException { + Drive service = driveService; + service.files().delete(fileId).execute(); + } + + + + + private java.io.File convertMultipartFileToFile(MultipartFile file) throws IOException { + java.io.File convFile = new java.io.File(file.getOriginalFilename()); + try (FileOutputStream fos = new FileOutputStream(convFile)) { + fos.write(file.getBytes()); + } + return convFile; + } + +} +