Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,4 @@ out/


application.properties
moadong.json
moadong.json
5 changes: 5 additions & 0 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

// google drive
implementation 'com.google.api-client:google-api-client:2.0.0'
implementation 'com.google.oauth-client:google-oauth-client-jetty:1.34.1'
implementation 'com.google.apis:google-api-services-drive:v3-rev20220815-2.0.0'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public enum ErrorCode {
TOO_MANY_FILES(HttpStatus.PAYLOAD_TOO_LARGE, "601-3", "이미지 파일이 최대치보다 많습니다."),
IMAGE_DELETE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "601-4", "이미지 삭제에 실패하였습니다"),
KOREAN_FILE_NAME(HttpStatus.INTERNAL_SERVER_ERROR, "601-5", "파일명의 한국어를 인코딩할 수 없습니다."),
FILE_TRANSFER_ERROR(HttpStatus.BAD_REQUEST, "601-6", "파일을 올바른 형식으로 변경할 수 없습니다."),
USER_ALREADY_EXIST(HttpStatus.BAD_REQUEST, "700-1","이미 존재하는 계정입니다."),
USER_NOT_EXIST(HttpStatus.BAD_REQUEST, "700-2","존재하지 않는 계정입니다."),
USER_INVALID_FORMAT(HttpStatus.BAD_REQUEST, "700-3","올바르지 않은 유저 형식입니다."),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
package moadong.gcs.controller;
package moadong.media.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import moadong.gcs.domain.FileType;
import moadong.gcs.dto.FeedUpdateRequest;
import moadong.gcs.service.ClubImageService;
import moadong.media.dto.FeedUpdateRequest;
import moadong.global.payload.Response;
import moadong.media.service.ClubImageService;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
Expand Down Expand Up @@ -46,7 +45,6 @@ public ResponseEntity<?> deleteLogo(@PathVariable String clubId) {
return Response.ok("success delete logo");
}

// TODO : Signed URL 을 통한 업로드로 추후 변경
@PostMapping(value = "/{clubId}/feed", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@Operation(summary = "피드 이미지 업로드", description = "피드에 사용할 이미지를 업로드하고 주소를 반환받습니다.")
public ResponseEntity<?> uploadFeed(@PathVariable String clubId, @RequestPart("feed") MultipartFile file) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package moadong.gcs.domain;
package moadong.media.domain;

import lombok.Getter;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package moadong.gcs.dto;
package moadong.media.dto;

import java.util.List;

Expand Down
19 changes: 19 additions & 0 deletions backend/src/main/java/moadong/media/service/ClubImageService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package moadong.media.service;

import java.util.List;
import moadong.club.entity.Club;
import org.springframework.web.multipart.MultipartFile;

public interface ClubImageService {

String uploadLogo(String clubId, MultipartFile file);

void deleteLogo(String clubId);

String uploadFeed(String clubId, MultipartFile file);

void updateFeeds(String clubId, List<String> newFeedImageList);

void deleteFile(Club club, String filePath);

}
Original file line number Diff line number Diff line change
@@ -1,37 +1,39 @@
package moadong.gcs.service;
package moadong.media.service;

import static moadong.media.util.ClubImageUtil.containsKorean;

import com.google.cloud.storage.BlobId;
import com.google.cloud.storage.BlobInfo;
import com.google.cloud.storage.Storage;
import java.text.Normalizer;
import java.util.List;
import java.util.regex.Pattern;
import lombok.RequiredArgsConstructor;
import moadong.club.entity.Club;
import moadong.club.repository.ClubRepository;
import moadong.gcs.domain.FileType;
import moadong.global.exception.ErrorCode;
import moadong.global.exception.RestApiException;
import moadong.global.util.ObjectIdConverter;
import moadong.global.util.RandomStringUtil;
import moadong.media.domain.FileType;
import org.bson.types.ObjectId;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

@Service
//@Service
@RequiredArgsConstructor
Comment on lines +22 to 23
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

@service 어노테이션이 주석 처리된 이유 확인 필요

@Service 어노테이션이 주석 처리되어 있습니다. 이렇게 되면 Spring이 이 클래스를 서비스 빈으로 자동 등록하지 않아 의존성 주입에 문제가 발생할 수 있습니다.

가능한 이유:

  1. 구글 드라이브 서비스와 GCS 서비스 중 하나만 사용하기 위한 의도일 수 있습니다.
  2. 다른 설정 방식(예: Java 설정 클래스)으로 빈을 등록하려는 의도일 수 있습니다.

의도적인 경우가 아니라면 주석을 제거하거나, 조건부 빈 등록(@ConditionalOnProperty)을 사용하는 것이 좋습니다.

다음 스크립트로 ClubImageService를 주입받는 곳이 있는지 확인해보세요:


🏁 Script executed:

#!/bin/bash
# 의존성 주입 방식 확인
rg "ClubImageService" --type java

Length of output: 659


GcsClubImageService에 @service 주석 해제 및 조건부 빈 등록 검토

현재 GcsClubImageService 클래스 상단의 @Service 어노테이션이 주석 처리되어 있어 Spring 컨텍스트에 빈으로 등록되지 않습니다. 이로 인해 ClubImageController에서 ClubImageService를 주입받을 때 GCS 구현체가 등록되지 않아 의도와 다른 구현체가 선택되거나 주입 오류가 발생할 수 있습니다.

검증 결과:

  • ClubImageController(backend/src/main/java/moadong/media/controller/ClubImageController.java)에서 private final ClubImageService clubImageService;를 통해 주입받고 있습니다.
  • GoogleDriveClubImageService는 빈으로 등록되나, GcsClubImageService는 등록되지 않습니다.

수정 제안:

  • 의도대로 GCS 구현체를 사용하려면:
    • GcsClubImageService(backend/src/main/java/moadong/media/service/GcsClubImageService.java:22–23)에서 @Service 주석을 제거
    • 환경별 선택적 빈 등록이 필요하다면 @ConditionalOnProperty("club.image.storage","gcs") 등을 추가
  • GCS 구현체를 현재 사용하지 않으려면:
    • 해당 클래스를 제거하거나, 주석 처리 상태를 유지하되 불필요한 코드는 삭제

public class ClubImageService {
public class GcsClubImageService implements ClubImageService {

private final ClubRepository clubRepository;

@Value("${google.cloud.storage.bucket.name}")
private String bucketName;

private final Storage storage;
private final int MAX_FEED_COUNT = 5;
@Value("${server.feed.max-count}")
private int MAX_FEED_COUNT;

private final Storage storage;

@Override
public String uploadLogo(String clubId, MultipartFile file) {
ObjectId objectId = ObjectIdConverter.convertString(clubId);
Club club = clubRepository.findClubById(objectId)
Expand All @@ -47,6 +49,7 @@ public String uploadLogo(String clubId, MultipartFile file) {
return filePath;
}

@Override
public void deleteLogo(String clubId) {
ObjectId objectId = ObjectIdConverter.convertString(clubId);
Club club = clubRepository.findClubById(objectId)
Expand All @@ -57,6 +60,7 @@ public void deleteLogo(String clubId) {
}
}

@Override
public String uploadFeed(String clubId, MultipartFile file) {
ObjectId objectId = ObjectIdConverter.convertString(clubId);
int feedImagesCount = clubRepository.findClubById(objectId)
Expand All @@ -69,6 +73,7 @@ public String uploadFeed(String clubId, MultipartFile file) {
return uploadFile(clubId, file, FileType.FEED);
}

@Override
public void updateFeeds(String clubId, List<String> newFeedImageList) {
ObjectId objectId = ObjectIdConverter.convertString(clubId);
Club club = clubRepository.findClubById(objectId)
Expand All @@ -94,7 +99,6 @@ private void deleteFeedImages(Club club, List<String> feedImages, List<String> n
}
}

// TODO : Signed URL 을 통한 업로드 URL 반환으로 추후 변경
private String uploadFile(String clubId, MultipartFile file, FileType fileType) {
if (file == null) {
throw new RestApiException(ErrorCode.FILE_NOT_FOUND);
Expand All @@ -110,6 +114,7 @@ private String uploadFile(String clubId, MultipartFile file, FileType fileType)
return "https://storage.googleapis.com/" + bucketName + "/" + blobInfo.getName();
}

@Override
public void deleteFile(Club club, String filePath) {
// 삭제할 파일의 BlobId를 생성
BlobId blobId = BlobId.of(bucketName,splitPath(filePath));
Expand Down Expand Up @@ -152,9 +157,4 @@ private String splitPath(String path) {
return path.split("/",5)[4];
}

private boolean containsKorean(String text) {
text = Normalizer.normalize(text, Normalizer.Form.NFC);
return Pattern.matches(".*[ㄱ-ㅎㅏ-ㅣ가-힣]+.*", text);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package moadong.media.service;

import static moadong.media.util.ClubImageUtil.containsKorean;

import com.google.api.client.http.FileContent;
import com.google.api.services.drive.Drive;
import com.google.api.services.drive.model.File;
import com.google.api.services.drive.model.Permission;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import lombok.RequiredArgsConstructor;
import moadong.club.entity.Club;
import moadong.club.repository.ClubRepository;
import moadong.global.exception.ErrorCode;
import moadong.global.exception.RestApiException;
import moadong.global.util.ObjectIdConverter;
import moadong.global.util.RandomStringUtil;
import moadong.media.domain.FileType;
import org.bson.types.ObjectId;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

@Service
@RequiredArgsConstructor
public class GoogleDriveClubImageService implements ClubImageService {

@Value("${google.drive.share-file-id}")
String shareFileId;
@Value("${server.feed.max-count}")
private int MAX_FEED_COUNT;

private final Drive googleDrive;
private final ClubRepository clubRepository;

@Override
public String uploadLogo(String clubId, MultipartFile file) {
ObjectId objectId = ObjectIdConverter.convertString(clubId);
Club club = clubRepository.findClubById(objectId)
.orElseThrow(() -> new RestApiException(ErrorCode.CLUB_NOT_FOUND));

if (club.getClubRecruitmentInformation().getLogo() != null) {
deleteFile(club, club.getClubRecruitmentInformation().getLogo());
}

String filePath = uploadFile(clubId, file, FileType.LOGO);
club.updateLogo(filePath);
clubRepository.save(club);
return filePath;
}
Comment on lines +37 to +51
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

로고 업로드 메서드 구현 검토

로고 업로드 로직은 잘 구현되어 있으나, 트랜잭션 관리가 필요할 수 있습니다. 현재 파일 업로드와 클럽 정보 업데이트가 별도 작업으로 진행되고 있어, 한 작업이 실패할 경우 일관성 문제가 발생할 수 있습니다.

메서드에 @Transactional 어노테이션을 추가하여 트랜잭션 관리를 개선하세요:

+    @Transactional
     @Override
     public String uploadLogo(String clubId, MultipartFile file) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Override
public String uploadLogo(String clubId, MultipartFile file) {
ObjectId objectId = ObjectIdConverter.convertString(clubId);
Club club = clubRepository.findClubById(objectId)
.orElseThrow(() -> new RestApiException(ErrorCode.CLUB_NOT_FOUND));
if (club.getClubRecruitmentInformation().getLogo() != null) {
deleteFile(club, club.getClubRecruitmentInformation().getLogo());
}
String filePath = uploadFile(clubId, file, FileType.LOGO);
club.updateLogo(filePath);
clubRepository.save(club);
return filePath;
}
@Transactional
@Override
public String uploadLogo(String clubId, MultipartFile file) {
ObjectId objectId = ObjectIdConverter.convertString(clubId);
Club club = clubRepository.findClubById(objectId)
.orElseThrow(() -> new RestApiException(ErrorCode.CLUB_NOT_FOUND));
if (club.getClubRecruitmentInformation().getLogo() != null) {
deleteFile(club, club.getClubRecruitmentInformation().getLogo());
}
String filePath = uploadFile(clubId, file, FileType.LOGO);
club.updateLogo(filePath);
clubRepository.save(club);
return filePath;
}


@Override
public void deleteLogo(String clubId) {
ObjectId objectId = ObjectIdConverter.convertString(clubId);
Club club = clubRepository.findClubById(objectId)
.orElseThrow(() -> new RestApiException(ErrorCode.CLUB_NOT_FOUND));

if (club.getClubRecruitmentInformation().getLogo() != null) {
deleteFile(club, club.getClubRecruitmentInformation().getLogo());
}
club.updateLogo(null);
clubRepository.save(club);
}
Comment on lines +53 to +64
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

로고 삭제 메서드 구현 검토

deleteLogo 메서드 역시 클럽 정보 조회와 업데이트 작업을 하므로 트랜잭션 관리가 필요합니다.

+    @Transactional
     @Override
     public void deleteLogo(String clubId) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Override
public void deleteLogo(String clubId) {
ObjectId objectId = ObjectIdConverter.convertString(clubId);
Club club = clubRepository.findClubById(objectId)
.orElseThrow(() -> new RestApiException(ErrorCode.CLUB_NOT_FOUND));
if (club.getClubRecruitmentInformation().getLogo() != null) {
deleteFile(club, club.getClubRecruitmentInformation().getLogo());
}
club.updateLogo(null);
clubRepository.save(club);
}
@Transactional
@Override
public void deleteLogo(String clubId) {
ObjectId objectId = ObjectIdConverter.convertString(clubId);
Club club = clubRepository.findClubById(objectId)
.orElseThrow(() -> new RestApiException(ErrorCode.CLUB_NOT_FOUND));
if (club.getClubRecruitmentInformation().getLogo() != null) {
deleteFile(club, club.getClubRecruitmentInformation().getLogo());
}
club.updateLogo(null);
clubRepository.save(club);
}


@Override
public String uploadFeed(String clubId, MultipartFile file) {
ObjectId objectId = ObjectIdConverter.convertString(clubId);
int feedImagesCount = clubRepository.findClubById(objectId)
.orElseThrow(() -> new RestApiException(ErrorCode.CLUB_NOT_FOUND))
.getClubRecruitmentInformation().getFeedImages().size();

if (feedImagesCount + 1 > MAX_FEED_COUNT) {
throw new RestApiException(ErrorCode.TOO_MANY_FILES);
}
return uploadFile(clubId, file, FileType.FEED);
}
Comment on lines +66 to +77
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

피드 업로드 메서드 검토

피드 이미지 업로드 로직은 잘 구현되었으나, 클럽 엔티티 저장 과정이 누락되어 있습니다. 현재 코드는 파일을 업로드하지만 클럽 엔티티의 피드 이미지 리스트를 업데이트하지 않고 있습니다.

피드 이미지 URL을 클럽 엔티티에 추가하고 저장하는 로직을 추가해야 합니다:

     @Override
+    @Transactional
     public String uploadFeed(String clubId, MultipartFile file) {
         ObjectId objectId = ObjectIdConverter.convertString(clubId);
-        int feedImagesCount = clubRepository.findClubById(objectId)
+        Club club = clubRepository.findClubById(objectId)
                 .orElseThrow(() -> new RestApiException(ErrorCode.CLUB_NOT_FOUND));
-                .getClubRecruitmentInformation().getFeedImages().size();
+        int feedImagesCount = club.getClubRecruitmentInformation().getFeedImages().size();

         if (feedImagesCount + 1 > MAX_FEED_COUNT) {
             throw new RestApiException(ErrorCode.TOO_MANY_FILES);
         }
-        return uploadFile(clubId, file, FileType.FEED);
+        String filePath = uploadFile(clubId, file, FileType.FEED);
+        List<String> feedImages = new ArrayList<>(club.getClubRecruitmentInformation().getFeedImages());
+        feedImages.add(filePath);
+        club.updateFeedImages(feedImages);
+        clubRepository.save(club);
+        return filePath;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Override
public String uploadFeed(String clubId, MultipartFile file) {
ObjectId objectId = ObjectIdConverter.convertString(clubId);
int feedImagesCount = clubRepository.findClubById(objectId)
.orElseThrow(() -> new RestApiException(ErrorCode.CLUB_NOT_FOUND))
.getClubRecruitmentInformation().getFeedImages().size();
if (feedImagesCount + 1 > MAX_FEED_COUNT) {
throw new RestApiException(ErrorCode.TOO_MANY_FILES);
}
return uploadFile(clubId, file, FileType.FEED);
}
@Override
@Transactional
public String uploadFeed(String clubId, MultipartFile file) {
ObjectId objectId = ObjectIdConverter.convertString(clubId);
Club club = clubRepository.findClubById(objectId)
.orElseThrow(() -> new RestApiException(ErrorCode.CLUB_NOT_FOUND));
int feedImagesCount = club.getClubRecruitmentInformation().getFeedImages().size();
if (feedImagesCount + 1 > MAX_FEED_COUNT) {
throw new RestApiException(ErrorCode.TOO_MANY_FILES);
}
String filePath = uploadFile(clubId, file, FileType.FEED);
List<String> feedImages = new ArrayList<>(club.getClubRecruitmentInformation().getFeedImages());
feedImages.add(filePath);
club.updateFeedImages(feedImages);
clubRepository.save(club);
return filePath;
}


@Override
public void updateFeeds(String clubId, List<String> newFeedImageList) {
ObjectId objectId = ObjectIdConverter.convertString(clubId);
Club club = clubRepository.findClubById(objectId)
.orElseThrow(() -> new RestApiException(ErrorCode.CLUB_NOT_FOUND));

if (newFeedImageList.size() > MAX_FEED_COUNT) {
throw new RestApiException(ErrorCode.TOO_MANY_FILES);
}

List<String> feedImages = club.getClubRecruitmentInformation().getFeedImages();
if (feedImages != null && !feedImages.isEmpty()) {
deleteFeedImages(club, feedImages, newFeedImageList);
}
club.updateFeedImages(newFeedImageList);
clubRepository.save(club);
}
Comment on lines +79 to +95
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

피드 이미지 업데이트 메서드 검토

피드 이미지 업데이트 로직은 전반적으로 잘 구현되어 있습니다. 트랜잭션 관리가 필요합니다.

+    @Transactional
     @Override
     public void updateFeeds(String clubId, List<String> newFeedImageList) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Override
public void updateFeeds(String clubId, List<String> newFeedImageList) {
ObjectId objectId = ObjectIdConverter.convertString(clubId);
Club club = clubRepository.findClubById(objectId)
.orElseThrow(() -> new RestApiException(ErrorCode.CLUB_NOT_FOUND));
if (newFeedImageList.size() > MAX_FEED_COUNT) {
throw new RestApiException(ErrorCode.TOO_MANY_FILES);
}
List<String> feedImages = club.getClubRecruitmentInformation().getFeedImages();
if (feedImages != null && !feedImages.isEmpty()) {
deleteFeedImages(club, feedImages, newFeedImageList);
}
club.updateFeedImages(newFeedImageList);
clubRepository.save(club);
}
@Transactional
@Override
public void updateFeeds(String clubId, List<String> newFeedImageList) {
ObjectId objectId = ObjectIdConverter.convertString(clubId);
Club club = clubRepository.findClubById(objectId)
.orElseThrow(() -> new RestApiException(ErrorCode.CLUB_NOT_FOUND));
if (newFeedImageList.size() > MAX_FEED_COUNT) {
throw new RestApiException(ErrorCode.TOO_MANY_FILES);
}
List<String> feedImages = club.getClubRecruitmentInformation().getFeedImages();
if (feedImages != null && !feedImages.isEmpty()) {
deleteFeedImages(club, feedImages, newFeedImageList);
}
club.updateFeedImages(newFeedImageList);
clubRepository.save(club);
}


private void deleteFeedImages(Club club, List<String> feedImages, List<String> newFeedImages) {
for (String feedsImage : feedImages) {
if (!newFeedImages.contains(feedsImage)) {
deleteFile(club, feedsImage);
}
}
}

@Override
public void deleteFile(Club club, String filePath) {
//"https://drive.google.com/file/d/{fileId}/view" -> {fileId}
String fileId = filePath.split("/")[5];
try {
googleDrive.files()
.delete(fileId)
.setSupportsAllDrives(true) // 공유 드라이브(Shared Drive)도 지원할 경우
.execute();
} catch (IOException e) {
throw new RestApiException(ErrorCode.IMAGE_DELETE_FAILED);
}
}
Comment on lines +105 to +117
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

파일 삭제 메서드 구현 관련 중요 이슈

현재 파일 ID 추출 방식은 Google Drive URL 형식에 의존하고 있어 Google Drive가 URL 형식을 변경하면 문제가 발생할 수 있습니다. 또한 split 인덱스를 하드코딩하는 것은 안전하지 않습니다.

정규식 또는 URL 파싱 라이브러리를 사용하여 더 안정적으로 fileId를 추출하도록 개선하세요:

-        //"https://drive.google.com/file/d/{fileId}/view" -> {fileId}
-        String fileId = filePath.split("/")[5];
+        // "https://drive.google.com/file/d/{fileId}/view" -> {fileId}
+        String fileId;
+        try {
+            java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("https://drive\\.google\\.com/file/d/([^/]+)/view");
+            java.util.regex.Matcher matcher = pattern.matcher(filePath);
+            if (matcher.find()) {
+                fileId = matcher.group(1);
+            } else {
+                throw new RestApiException(ErrorCode.INVALID_FILE_URL);
+            }
+        } catch (Exception e) {
+            throw new RestApiException(ErrorCode.INVALID_FILE_URL);
+        }

추가로 ErrorCode.INVALID_FILE_URL 오류 코드를 정의해야 합니다.


private String uploadFile(String clubId, MultipartFile file, FileType fileType) {
if (file == null) {
throw new RestApiException(ErrorCode.FILE_NOT_FOUND);
}
// MultipartFile → java.io.File 변환
java.io.File tempFile;
try {
tempFile = java.io.File.createTempFile("upload-", file.getOriginalFilename());
file.transferTo(tempFile);
} catch (IOException e) {
throw new RestApiException(ErrorCode.FILE_TRANSFER_ERROR);
}

// 메타데이터 생성
File fileMetadata = new File();
String fileName = file.getOriginalFilename();
if (containsKorean(fileName)) {
fileName = RandomStringUtil.generateRandomString(10);
}

fileMetadata.setName(clubId + "/" + fileType + "/" + fileName);
fileMetadata.setMimeType(file.getContentType());
// 공유 ID 설정
fileMetadata.setParents(Collections.singletonList(shareFileId));

// 파일 업로드
FileContent mediaContent = new FileContent(file.getContentType(), tempFile);
// 전체 공개 권한 설정
Permission publicPermission = new Permission()
.setType("anyone") // 누구나
.setRole("reader"); // 읽기 권한

File uploadedFile;
try {
uploadedFile= googleDrive.files().create(fileMetadata, mediaContent)
.setFields("id")
.execute();

googleDrive.permissions().create(uploadedFile.getId(), publicPermission)
.setFields("id")
.execute();
} catch (Exception e) {
throw new RestApiException(ErrorCode.IMAGE_UPLOAD_FAILED);
}finally {
// 임시 파일 삭제
tempFile.delete();
}
// 공유 링크 반환
String publicUrl = "https://drive.google.com/file/d/" + uploadedFile.getId() + "/view";
return publicUrl;
}
Comment on lines +119 to +169
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

파일 업로드 메서드 구현 검토

파일 업로드 메서드는 전반적으로 잘 구현되어 있지만, 몇 가지 개선 사항이 있습니다:

  1. 파일 크기나 형식에 대한 검증이 없습니다.
  2. 임시 파일 생성 시 try-with-resources 패턴을 사용하지 않았습니다.
  3. 파일 이름에서 한글만 확인하지만 다른 비ASCII 문자에 대한 처리도 필요할 수 있습니다.
  4. 모든 파일에 공개 권한을 부여하는 것은 보안 이슈가 될 수 있습니다.

파일 크기 및 형식 검증을 추가하고, 임시 파일 관리를 개선하세요:

     private String uploadFile(String clubId, MultipartFile file, FileType fileType) {
         if (file == null) {
             throw new RestApiException(ErrorCode.FILE_NOT_FOUND);
         }
+        
+        // 파일 크기 및 형식 검증
+        if (file.getSize() > 10 * 1024 * 1024) { // 예: 10MB 제한
+            throw new RestApiException(ErrorCode.FILE_SIZE_EXCEEDED);
+        }
+        
+        // 지원되는 형식 검증 (이미지 파일만 허용하는 예시)
+        String contentType = file.getContentType();
+        if (contentType == null || !contentType.startsWith("image/")) {
+            throw new RestApiException(ErrorCode.UNSUPPORTED_FILE_TYPE);
+        }
+        
         // MultipartFile → java.io.File 변환
-        java.io.File tempFile;
+        java.io.File tempFile = null;
         try {
             tempFile = java.io.File.createTempFile("upload-", file.getOriginalFilename());
             file.transferTo(tempFile);

또한 파일명 처리 로직을 개선하세요:

         // 메타데이터 생성
         File fileMetadata = new File();
         String fileName = file.getOriginalFilename();
-        if (containsKorean(fileName)) {
+        // 한글뿐만 아니라 모든 특수문자에 대비하여 파일명 정리
+        if (fileName == null || !fileName.matches("[a-zA-Z0-9._-]+")) {
             fileName = RandomStringUtil.generateRandomString(10);
         }


}
12 changes: 12 additions & 0 deletions backend/src/main/java/moadong/media/util/ClubImageUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package moadong.media.util;

import java.text.Normalizer;
import java.util.regex.Pattern;

public class ClubImageUtil {

public static boolean containsKorean(String text) {
text = Normalizer.normalize(text, Normalizer.Form.NFC);
return Pattern.matches(".*[ㄱ-ㅎㅏ-ㅣ가-힣]+.*", text);
}
}
Loading
Loading