mirror of
https://github.com/won-ktds/smarketing-backend.git
synced 2025-12-06 15:16:23 +00:00
Merge branch 'marketing-contents' of https://github.com/won-ktds/smarketing-backend into poster-content
This commit is contained in:
commit
f7b0ad41d7
@ -84,6 +84,7 @@ public class PosterContentService implements PosterContentUseCase {
|
|||||||
// 콘텐츠 엔티티 생성
|
// 콘텐츠 엔티티 생성
|
||||||
Content content = Content.builder()
|
Content content = Content.builder()
|
||||||
.contentType(ContentType.POSTER)
|
.contentType(ContentType.POSTER)
|
||||||
|
.platform(Platform.GENERAL)
|
||||||
.title(request.getTitle())
|
.title(request.getTitle())
|
||||||
.content(request.getContent())
|
.content(request.getContent())
|
||||||
.images(request.getImages())
|
.images(request.getImages())
|
||||||
|
|||||||
@ -0,0 +1,37 @@
|
|||||||
|
// store/src/main/java/com/won/smarketing/store/service/BlobStorageService.java
|
||||||
|
package com.won.smarketing.content.domain.service;
|
||||||
|
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Azure Blob Storage 서비스 인터페이스
|
||||||
|
* 파일 업로드, 다운로드, 삭제 기능 정의
|
||||||
|
*/
|
||||||
|
public interface BlobStorageService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이미지 파일 업로드
|
||||||
|
*
|
||||||
|
* @param file 업로드할 파일
|
||||||
|
* @return 업로드된 파일의 URL
|
||||||
|
*/
|
||||||
|
List<String> uploadImage(List<MultipartFile> file, String containerName);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 파일 삭제
|
||||||
|
*
|
||||||
|
* @param fileUrl 삭제할 파일의 URL
|
||||||
|
* @return 삭제 성공 여부
|
||||||
|
*/
|
||||||
|
//boolean deleteFile(String fileUrl);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 컨테이너 존재 여부 확인 및 생성
|
||||||
|
*
|
||||||
|
* @param containerName 컨테이너 이름
|
||||||
|
*/
|
||||||
|
void ensureContainerExists(String containerName);
|
||||||
|
}
|
||||||
@ -0,0 +1,252 @@
|
|||||||
|
// store/src/main/java/com/won/smarketing/store/service/BlobStorageServiceImpl.java
|
||||||
|
package com.won.smarketing.content.domain.service;
|
||||||
|
|
||||||
|
import com.azure.core.util.BinaryData;
|
||||||
|
import com.azure.storage.blob.BlobClient;
|
||||||
|
import com.azure.storage.blob.BlobContainerClient;
|
||||||
|
import com.azure.storage.blob.BlobServiceClient;
|
||||||
|
import com.azure.storage.blob.models.BlobHttpHeaders;
|
||||||
|
import com.azure.storage.blob.models.PublicAccessType;
|
||||||
|
import com.won.smarketing.common.exception.BusinessException;
|
||||||
|
import com.won.smarketing.common.exception.ErrorCode;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Azure Blob Storage 서비스 구현체
|
||||||
|
* 이미지 파일 업로드, 삭제 기능 구현
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class BlobStorageServiceImpl implements BlobStorageService {
|
||||||
|
|
||||||
|
private final BlobServiceClient blobServiceClient;
|
||||||
|
|
||||||
|
@Value("${azure.storage.container.poster-images:poster-images}")
|
||||||
|
private String posterImageContainer;
|
||||||
|
|
||||||
|
@Value("${azure.storage.container.content-images:content-images}")
|
||||||
|
private String contentImageContainer;
|
||||||
|
|
||||||
|
@Value("${azure.storage.max-file-size:10485760}") // 10MB
|
||||||
|
private long maxFileSize;
|
||||||
|
|
||||||
|
// 허용되는 이미지 확장자
|
||||||
|
private static final List<String> ALLOWED_EXTENSIONS = Arrays.asList(
|
||||||
|
"jpg", "jpeg", "png", "gif", "bmp", "webp"
|
||||||
|
);
|
||||||
|
|
||||||
|
// 허용되는 MIME 타입
|
||||||
|
private static final List<String> ALLOWED_MIME_TYPES = Arrays.asList(
|
||||||
|
"image/jpeg", "image/png", "image/gif", "image/bmp", "image/webp"
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이미지 파일 업로드
|
||||||
|
*
|
||||||
|
* @param files 업로드할 파일들
|
||||||
|
* @param containerName 컨테이너 이름
|
||||||
|
* @return 업로드된 파일의 URL
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<String> uploadImage(List<MultipartFile> files, String containerName) {
|
||||||
|
// 파일 유효성 검증
|
||||||
|
validateImageFile(files);
|
||||||
|
List<String> urls = new ArrayList<>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 컨테이너 존재 확인 및 생성
|
||||||
|
for(MultipartFile file : files) {
|
||||||
|
String fileName = generateMenuImageFileName(file.getOriginalFilename());
|
||||||
|
|
||||||
|
ensureContainerExists(containerName);
|
||||||
|
|
||||||
|
// Blob 클라이언트 생성
|
||||||
|
BlobContainerClient containerClient = blobServiceClient.getBlobContainerClient(containerName);
|
||||||
|
BlobClient blobClient = containerClient.getBlobClient(fileName);
|
||||||
|
|
||||||
|
// 파일 업로드 (간단한 방식)
|
||||||
|
BinaryData binaryData = BinaryData.fromBytes(file.getBytes());
|
||||||
|
|
||||||
|
// 파일 업로드 실행 (덮어쓰기 허용)
|
||||||
|
blobClient.upload(binaryData, true);
|
||||||
|
|
||||||
|
// Content-Type 설정
|
||||||
|
BlobHttpHeaders headers = new BlobHttpHeaders().setContentType(file.getContentType());
|
||||||
|
blobClient.setHttpHeaders(headers);
|
||||||
|
|
||||||
|
String fileUrl = blobClient.getBlobUrl();
|
||||||
|
log.info("이미지 업로드 성공: {}", fileUrl);
|
||||||
|
|
||||||
|
urls.add(fileUrl);
|
||||||
|
}
|
||||||
|
return urls;
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("이미지 업로드 실패 - 파일 읽기 오류: {}", e.getMessage());
|
||||||
|
throw new BusinessException(ErrorCode.FILE_UPLOAD_FAILED);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("이미지 업로드 실패: {}", e.getMessage());
|
||||||
|
throw new BusinessException(ErrorCode.FILE_UPLOAD_FAILED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 파일 삭제
|
||||||
|
*
|
||||||
|
* @param fileUrl 삭제할 파일의 URL
|
||||||
|
*/
|
||||||
|
// @Override
|
||||||
|
public void deleteFile(String fileUrl) {
|
||||||
|
try {
|
||||||
|
// URL에서 컨테이너명과 파일명 추출
|
||||||
|
String[] urlParts = extractContainerAndFileName(fileUrl);
|
||||||
|
String containerName = urlParts[0];
|
||||||
|
String fileName = urlParts[1];
|
||||||
|
|
||||||
|
BlobContainerClient containerClient = blobServiceClient.getBlobContainerClient(containerName);
|
||||||
|
BlobClient blobClient = containerClient.getBlobClient(fileName);
|
||||||
|
|
||||||
|
boolean deleted = blobClient.deleteIfExists();
|
||||||
|
|
||||||
|
if (deleted) {
|
||||||
|
log.info("파일 삭제 성공: {}", fileUrl);
|
||||||
|
} else {
|
||||||
|
log.warn("파일이 존재하지 않음: {}", fileUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("파일 삭제 실패: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 컨테이너 존재 여부 확인 및 생성
|
||||||
|
*
|
||||||
|
* @param containerName 컨테이너 이름
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void ensureContainerExists(String containerName) {
|
||||||
|
try {
|
||||||
|
BlobContainerClient containerClient = blobServiceClient.getBlobContainerClient(containerName);
|
||||||
|
|
||||||
|
if (!containerClient.exists()) {
|
||||||
|
containerClient.createWithResponse(null, PublicAccessType.BLOB, null, null);
|
||||||
|
log.info("컨테이너 생성 완료: {}", containerName);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("컨테이너 생성 실패: {}", e.getMessage());
|
||||||
|
throw new BusinessException(ErrorCode.STORAGE_CONTAINER_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이미지 파일 유효성 검증
|
||||||
|
*
|
||||||
|
* @param files 검증할 파일
|
||||||
|
*/
|
||||||
|
private void validateImageFile(List<MultipartFile> files) {
|
||||||
|
for (MultipartFile file : files) {
|
||||||
|
// 파일 존재 여부 확인
|
||||||
|
if (file == null || file.isEmpty()) {
|
||||||
|
throw new BusinessException(ErrorCode.FILE_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 파일 크기 확인
|
||||||
|
if (file.getSize() > maxFileSize) {
|
||||||
|
throw new BusinessException(ErrorCode.FILE_SIZE_EXCEEDED);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 파일 확장자 확인
|
||||||
|
String originalFilename = file.getOriginalFilename();
|
||||||
|
if (originalFilename == null) {
|
||||||
|
throw new BusinessException(ErrorCode.INVALID_FILE_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
String extension = getFileExtension(originalFilename).toLowerCase();
|
||||||
|
if (!ALLOWED_EXTENSIONS.contains(extension)) {
|
||||||
|
throw new BusinessException(ErrorCode.INVALID_FILE_EXTENSION);
|
||||||
|
}
|
||||||
|
|
||||||
|
// MIME 타입 확인
|
||||||
|
String contentType = file.getContentType();
|
||||||
|
if (contentType == null || !ALLOWED_MIME_TYPES.contains(contentType)) {
|
||||||
|
throw new BusinessException(ErrorCode.INVALID_FILE_TYPE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메뉴 이미지 파일명 생성
|
||||||
|
*
|
||||||
|
* @param originalFilename 원본 파일명
|
||||||
|
* @return 생성된 파일명
|
||||||
|
*/
|
||||||
|
private String generateMenuImageFileName(String originalFilename) {
|
||||||
|
String extension = getFileExtension(originalFilename);
|
||||||
|
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"));
|
||||||
|
String uuid = UUID.randomUUID().toString().substring(0, 8);
|
||||||
|
|
||||||
|
return String.format("menu_%s_%s.%s", timestamp, uuid, extension);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장 이미지 파일명 생성
|
||||||
|
*
|
||||||
|
* @param storeId 매장 ID
|
||||||
|
* @param originalFilename 원본 파일명
|
||||||
|
* @return 생성된 파일명
|
||||||
|
*/
|
||||||
|
private String generateStoreImageFileName(Long storeId, String originalFilename) {
|
||||||
|
String extension = getFileExtension(originalFilename);
|
||||||
|
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"));
|
||||||
|
String uuid = UUID.randomUUID().toString().substring(0, 8);
|
||||||
|
|
||||||
|
return String.format("store_%d_%s_%s.%s", storeId, timestamp, uuid, extension);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 파일 확장자 추출
|
||||||
|
*
|
||||||
|
* @param filename 파일명
|
||||||
|
* @return 확장자
|
||||||
|
*/
|
||||||
|
private String getFileExtension(String filename) {
|
||||||
|
int lastDotIndex = filename.lastIndexOf('.');
|
||||||
|
if (lastDotIndex == -1) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return filename.substring(lastDotIndex + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URL에서 컨테이너명과 파일명 추출
|
||||||
|
*
|
||||||
|
* @param fileUrl 파일 URL
|
||||||
|
* @return [컨테이너명, 파일명] 배열
|
||||||
|
*/
|
||||||
|
private String[] extractContainerAndFileName(String fileUrl) {
|
||||||
|
// URL 형식: https://accountname.blob.core.windows.net/container/filename
|
||||||
|
try {
|
||||||
|
String[] parts = fileUrl.split("/");
|
||||||
|
String containerName = parts[parts.length - 2];
|
||||||
|
String fileName = parts[parts.length - 1];
|
||||||
|
return new String[]{containerName, fileName};
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new BusinessException(ErrorCode.INVALID_FILE_URL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -19,9 +19,9 @@ import java.util.List;
|
|||||||
@Schema(description = "포스터 콘텐츠 저장 요청")
|
@Schema(description = "포스터 콘텐츠 저장 요청")
|
||||||
public class PosterContentSaveRequest {
|
public class PosterContentSaveRequest {
|
||||||
|
|
||||||
@Schema(description = "콘텐츠 ID", example = "1", required = true)
|
// @Schema(description = "콘텐츠 ID", example = "1", required = true)
|
||||||
@NotNull(message = "콘텐츠 ID는 필수입니다")
|
// @NotNull(message = "콘텐츠 ID는 필수입니다")
|
||||||
private Long contentId;
|
// private Long contentId;
|
||||||
|
|
||||||
@Schema(description = "매장 ID", example = "1", required = true)
|
@Schema(description = "매장 ID", example = "1", required = true)
|
||||||
@NotNull(message = "매장 ID는 필수입니다")
|
@NotNull(message = "매장 ID는 필수입니다")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user