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
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public static UploadConfirmResponse from(Asset asset) {
.mimeType(asset.getMimeType())
.source(asset.getSource().name())
.ocrStatus(asset.getOcrStatus() != null ? asset.getOcrStatus().name() : null)
.thumbnailS3Key(asset.getThumbnailS3Key())
.thumbnailS3Key(asset.getThumbnailObjectKey())
.createdAt(asset.getCreatedAt())
.build();
}
Expand Down
20 changes: 10 additions & 10 deletions src/main/java/com/proovy/domain/asset/entity/Asset.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@ public class Asset {
@Column(nullable = false, length = 100)
private String mimeType;

@Column(nullable = false, length = 500)
private String s3Key; // S3 저장 경로
@Column(name = "object_key", nullable = false, length = 500)
private String objectKey; // GCS 저장 경로

@Column(length = 500)
private String thumbnailS3Key; // 썸네일 S3 경로
@Column(name = "thumbnail_object_key", length = 500)
private String thumbnailObjectKey; // 썸네일 GCS 경로

@Column(nullable = false, length = 20)
@Enumerated(EnumType.STRING)
Expand Down Expand Up @@ -77,15 +77,15 @@ public class Asset {

@Builder
public Asset(Long userId, Long noteId, String fileName, Long fileSize,
String mimeType, String s3Key, String thumbnailS3Key, AssetSource source,
String mimeType, String objectKey, String thumbnailObjectKey, AssetSource source,
AssetStatus status, LocalDateTime uploadExpiresAt) {
this.userId = userId;
this.noteId = noteId;
this.fileName = fileName;
this.fileSize = fileSize;
this.mimeType = mimeType;
this.s3Key = s3Key;
this.thumbnailS3Key = thumbnailS3Key;
this.objectKey = objectKey;
this.thumbnailObjectKey = thumbnailObjectKey;
this.source = source;
this.status = status;
this.uploadExpiresAt = uploadExpiresAt;
Expand Down Expand Up @@ -129,10 +129,10 @@ public void failOcr() {
}

/**
* 썸네일 S3 키 업데이트
* 썸네일 Object 키 업데이트
*/
public void updateThumbnail(String thumbnailS3Key) {
this.thumbnailS3Key = thumbnailS3Key;
public void updateThumbnail(String thumbnailObjectKey) {
this.thumbnailObjectKey = thumbnailObjectKey;
// 이미지 파일은 썸네일만 있으면 완료 (OCR 불필요)
if (this.ocrStatus == OcrStatus.processing) {
this.ocrStatus = OcrStatus.completed;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ public UploadUrlResponse generateUploadUrl(Long userId, UploadUrlRequest request
.fileName(request.getFileName())
.fileSize(request.getFileSize())
.mimeType(request.getMimeType())
.s3Key(s3Key)
.objectKey(s3Key)
.source(Asset.AssetSource.upload)
.status(AssetStatus.PENDING)
.uploadExpiresAt(expiresAt)
Expand Down Expand Up @@ -172,7 +172,7 @@ public DownloadUrlResponse generateDownloadUrl(Long userId, Long assetId) {
// 3. Presigned URL 생성
LocalDateTime expiresAt = LocalDateTime.now().plusMinutes(PRESIGNED_URL_DURATION_MINUTES);
String downloadUrl = s3Service.generatePresignedDownloadUrl(
asset.getS3Key(),
asset.getObjectKey(),
asset.getFileName(),
PRESIGNED_URL_DURATION_MINUTES
);
Expand Down Expand Up @@ -200,7 +200,7 @@ public AssetDetailResponse confirmUpload(Long userId, Long assetId) {
}

// 4. S3 파일 존재 여부 확인
if (!s3Service.doesFileExist(asset.getS3Key())) {
if (!s3Service.doesFileExist(asset.getObjectKey())) {
throw new BusinessException(ErrorCode.ASSET4007);
}

Expand All @@ -217,7 +217,7 @@ public AssetDetailResponse confirmUpload(Long userId, Long assetId) {

// 6. 썸네일 생성
final Long savedAssetId = asset.getId();
final String s3Key = asset.getS3Key();
final String s3Key = asset.getObjectKey();
final String mimeType = asset.getMimeType();

// 이미지 파일인 경우: 동기적으로 썸네일 생성 (빠른 응답)
Expand Down Expand Up @@ -252,8 +252,8 @@ public void afterCommit() {
assetId, userId, asset.getOcrStatus());

// 썸네일 URL 생성 (있는 경우)
String thumbnailUrl = asset.getThumbnailS3Key() != null
? s3Service.getThumbnailUrl(asset.getThumbnailS3Key())
String thumbnailUrl = asset.getThumbnailObjectKey() != null
? s3Service.getThumbnailUrl(asset.getThumbnailObjectKey())
: null;

return AssetDetailResponse.from(asset, thumbnailUrl);
Expand All @@ -273,8 +273,8 @@ public AssetDetailResponse getAssetDetail(Long userId, Long assetId) {
log.debug("[Asset] 자산 상세 조회 - assetId: {}, ocrStatus: {}", assetId, asset.getOcrStatus());

// 썸네일 URL 생성 (있는 경우)
String thumbnailUrl = asset.getThumbnailS3Key() != null
? s3Service.getThumbnailUrl(asset.getThumbnailS3Key())
String thumbnailUrl = asset.getThumbnailObjectKey() != null
? s3Service.getThumbnailUrl(asset.getThumbnailObjectKey())
: null;

return AssetDetailResponse.from(asset, thumbnailUrl);
Expand All @@ -292,32 +292,32 @@ public void deleteAsset(Long userId, Long assetId) {
throw new BusinessException(ErrorCode.ASSET4031);
}

// S3 키 저장 (트랜잭션 커밋 후 삭제를 위해)
final String s3Key = asset.getS3Key();
final String thumbnailS3Key = asset.getThumbnailS3Key();
// GCS 키 저장 (트랜잭션 커밋 후 삭제를 위해)
final String objectKey = asset.getObjectKey();
final String thumbnailObjectKey = asset.getThumbnailObjectKey();

// 3. DB Asset 레코드 삭제 (먼저 수행)
assetRepository.delete(asset);

// 4. 트랜잭션 커밋 후 S3 파일 삭제 (afterCommit 콜백)
// 4. 트랜잭션 커밋 후 GCS 파일 삭제 (afterCommit 콜백)
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
// S3 원본 파일 삭제
// GCS 원본 파일 삭제
try {
s3Service.deleteFile(s3Key);
log.info("[Asset] S3 원본 파일 삭제 완료 - s3Key: {}", s3Key);
s3Service.deleteFile(objectKey);
log.info("[Asset] GCS 원본 파일 삭제 완료 - objectKey: {}", objectKey);
} catch (Exception e) {
log.error("[Asset] S3 원본 파일 삭제 실패 - s3Key: {}, error: {}", s3Key, e.getMessage());
log.error("[Asset] GCS 원본 파일 삭제 실패 - objectKey: {}, error: {}", objectKey, e.getMessage());
}

// S3 썸네일 삭제 (있는 경우)
if (thumbnailS3Key != null && !thumbnailS3Key.equals(s3Key)) {
// GCS 썸네일 삭제 (원본과 다른 경우에만)
if (thumbnailObjectKey != null && !thumbnailObjectKey.equals(objectKey)) {
try {
s3Service.deleteFile(thumbnailS3Key);
log.info("[Asset] S3 썸네일 삭제 완료 - s3Key: {}", thumbnailS3Key);
s3Service.deleteFile(thumbnailObjectKey);
log.info("[Asset] GCS 썸네일 삭제 완료 - thumbnailObjectKey: {}", thumbnailObjectKey);
} catch (Exception e) {
log.error("[Asset] S3 썸네일 삭제 실패 - s3Key: {}, error: {}", thumbnailS3Key, e.getMessage());
log.error("[Asset] GCS 썸네일 삭제 실패 - thumbnailObjectKey: {}, error: {}", thumbnailObjectKey, e.getMessage());
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public static CanvasImageUploadResponse of(Asset asset, String uploadUrl) {
.fileName(asset.getFileName())
.fileSize(asset.getFileSize())
.mimeType(asset.getMimeType())
.storageKey(asset.getS3Key())
.storageKey(asset.getObjectKey())
.uploadUrl(uploadUrl)
.createdAt(asset.getCreatedAt())
.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -506,9 +506,9 @@ private List<String> convertAssetIdsToUrls(List<Long> assetIds, Long userId) {
.map(asset -> {
// Presigned URL 생성 (15분 유효)
String url = s3Service.generatePresignedDownloadUrl(
asset.getS3Key(), asset.getFileName(), 15);
log.debug("[Chat] Presigned URL 생성 - assetId: {}, s3Key: {}",
asset.getId(), asset.getS3Key());
asset.getObjectKey(), asset.getFileName(), 15);
log.debug("[Chat] Presigned URL 생성 - assetId: {}, objectKey: {}",
asset.getId(), asset.getObjectKey());
return url;
})
.collect(Collectors.toList());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ public CanvasImageUploadResponse uploadCanvasImage(Long userId, CanvasImageUploa
.fileName(request.getFileName())
.fileSize(request.getFileSize())
.mimeType(request.getMimeType())
.s3Key(s3Key)
.objectKey(s3Key)
.source(Asset.AssetSource.upload)
.status(AssetStatus.PENDING)
.uploadExpiresAt(expiresAt)
Expand Down
30 changes: 15 additions & 15 deletions src/main/java/com/proovy/domain/note/service/NoteServiceImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -357,20 +357,20 @@ public DeleteNoteResponse deleteNote(Long userId, Long noteId) {
long messageCount = chatMessageRepository.countByNoteId(noteId);
long conversationCount = messageCount / 2;

// 3. S3 삭제를 위한 Asset 정보만 조회 (영속성 컨텍스트 오염 방지를 위해 별도 처리)
// 3. GCS 삭제를 위한 Asset 정보만 조회 (영속성 컨텍스트 오염 방지를 위해 별도 처리)
List<Asset> assets = assetRepository.findAllByNoteId(noteId);
int assetCount = assets.size();

List<String> s3KeysToDelete = new ArrayList<>();
Set<String> objectKeysToDelete = new HashSet<>();
long freedStorageBytes = 0L;

for (Asset asset : assets) {
if (asset.getS3Key() != null) {
s3KeysToDelete.add(asset.getS3Key());
if (asset.getObjectKey() != null) {
objectKeysToDelete.add(asset.getObjectKey());
freedStorageBytes += asset.getFileSize();
}
if (asset.getThumbnailS3Key() != null) {
s3KeysToDelete.add(asset.getThumbnailS3Key());
if (asset.getThumbnailObjectKey() != null && !asset.getThumbnailObjectKey().equals(asset.getObjectKey())) {
objectKeysToDelete.add(asset.getThumbnailObjectKey());
}
}

Expand All @@ -379,10 +379,10 @@ public DeleteNoteResponse deleteNote(Long userId, Long noteId) {
.map(Asset::getId)
.collect(Collectors.toList());

// 5. S3 파일 삭제
if (!s3KeysToDelete.isEmpty()) {
s3Service.deleteFiles(s3KeysToDelete);
log.info("S3 파일 삭제 완료 - {} 개 파일", s3KeysToDelete.size());
// 5. GCS 파일 삭제
if (!objectKeysToDelete.isEmpty()) {
s3Service.deleteFiles(new ArrayList<>(objectKeysToDelete));
log.info("GCS 파일 삭제 완료 - {} 개 파일", objectKeysToDelete.size());
}

// 6. ChatMessage ID 목록 조회
Expand Down Expand Up @@ -503,8 +503,8 @@ public NoteDetailResponse getNoteDetail(Long userId, Long noteId, int conversati
// 12. 자산 정보 DTO 생성
List<NoteDetailResponse.AssetInfo> assetInfos = noteAssets.stream()
.map(asset -> {
String thumbnailUrl = asset.getThumbnailS3Key() != null
? s3Service.getThumbnailUrl(asset.getThumbnailS3Key())
String thumbnailUrl = asset.getThumbnailObjectKey() != null
? s3Service.getThumbnailUrl(asset.getThumbnailObjectKey())
: null;
FileCategory category = FileCategory.fromMimeType(asset.getMimeType());

Expand Down Expand Up @@ -672,7 +672,7 @@ private NoteDetailResponse.MessageInfo buildChatMessageInfo(
.fileId(asset.getId())
.fileName(asset.getFileName())
.fileType("SOLUTION")
.downloadUrl(s3Service.getFileUrl(asset.getS3Key()))
.downloadUrl(s3Service.getFileUrl(asset.getObjectKey()))
.build())
.collect(Collectors.toList());
if (generatedFiles.isEmpty()) {
Expand Down Expand Up @@ -713,8 +713,8 @@ public AssetListResponse getAssetList(Long userId, Long noteId, String query) {
// 3. 자산 정보 DTO 생성
List<AssetListResponse.AssetInfo> assetInfos = assets.stream()
.map(asset -> {
String thumbnailUrl = asset.getThumbnailS3Key() != null
? s3Service.getThumbnailUrl(asset.getThumbnailS3Key())
String thumbnailUrl = asset.getThumbnailObjectKey() != null
? s3Service.getThumbnailUrl(asset.getThumbnailObjectKey())
: null;
FileCategory category = FileCategory.fromMimeType(asset.getMimeType());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Map;
import java.util.stream.Collectors;

Expand Down Expand Up @@ -62,26 +64,26 @@ public BulkDeleteResponse bulkDeleteAssets(Long userId, BulkDeleteRequest reques
throw new BusinessException(ErrorCode.STORAGE4031);
}

// S3 키 수집 (원본 + 썸네일)
List<String> s3KeysToDelete = new ArrayList<>();
// GCS 키 수집 (원본 + 썸네일, 중복 제거)
Set<String> objectKeysToDelete = new HashSet<>();
long totalFileSize = 0L;

for (Asset asset : assets) {
// 원본 파일
s3KeysToDelete.add(asset.getS3Key());
objectKeysToDelete.add(asset.getObjectKey());
totalFileSize += asset.getFileSize();

// 썸네일 파일
if (asset.getThumbnailS3Key() != null) {
s3KeysToDelete.add(asset.getThumbnailS3Key());
// 썸네일 파일 (원본과 다른 경우에만)
if (asset.getThumbnailObjectKey() != null && !asset.getThumbnailObjectKey().equals(asset.getObjectKey())) {
objectKeysToDelete.add(asset.getThumbnailObjectKey());
}
}

// DB에서 자산 삭제
assetRepository.deleteAllInBatch(assets);

// S3에서 파일 삭제
s3Service.deleteFiles(s3KeysToDelete);
s3Service.deleteFiles(new ArrayList<>(objectKeysToDelete));

// 스토리지 용량 반환 로깅
log.info("[Storage] 사용자 {} - {} 개 파일 삭제, 용량 반환: {} bytes",
Expand Down Expand Up @@ -151,7 +153,7 @@ public StorageResponse getStorageUsage(Long userId, String keyword) {
.filter(asset -> lowerKeyword == null || titleMatches ||
asset.getFileName().toLowerCase().contains(lowerKeyword))
.map(asset -> {
String thumbnailUrl = s3Service.getThumbnailUrl(asset.getThumbnailS3Key());
String thumbnailUrl = s3Service.getThumbnailUrl(asset.getThumbnailObjectKey());
return AssetSummaryDto.of(asset, thumbnailUrl);
})
.toList();
Expand Down
17 changes: 9 additions & 8 deletions src/main/java/com/proovy/domain/user/service/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

@Slf4j
Expand Down Expand Up @@ -205,14 +206,14 @@ public void afterCommit() {
}

private void deleteUserData(Long userId) {
// S3 키를 먼저 수집 (원본 + 썸네일)
List<String> s3Keys = new java.util.ArrayList<>();
// GCS 키를 먼저 수집 (원본 + 썸네일, 중복 제거)
Set<String> objectKeys = new java.util.HashSet<>();
assetRepository.findAllByUserId(userId).forEach(asset -> {
if (asset.getS3Key() != null) {
s3Keys.add(asset.getS3Key());
if (asset.getObjectKey() != null) {
objectKeys.add(asset.getObjectKey());
}
if (asset.getThumbnailS3Key() != null) {
s3Keys.add(asset.getThumbnailS3Key());
if (asset.getThumbnailObjectKey() != null && !asset.getThumbnailObjectKey().equals(asset.getObjectKey())) {
objectKeys.add(asset.getThumbnailObjectKey());
}
});

Expand All @@ -230,11 +231,11 @@ private void deleteUserData(Long userId) {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
s3Keys.forEach(key -> {
objectKeys.forEach(key -> {
try {
s3Service.deleteFile(key);
} catch (Exception e) {
log.warn("S3 파일 삭제 실패: s3Key={}", key, e);
log.warn("GCS 파일 삭제 실패: objectKey={}", key, e);
}
});
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- V32: Rename S3 column names to generic object key names for GCS compatibility
ALTER TABLE assets RENAME COLUMN s3_key TO object_key;
ALTER TABLE assets RENAME COLUMN thumbnail_s3_key TO thumbnail_object_key;
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ void setUp() {
.fileName("test.pdf")
.fileSize(1024L * 1024L * 100) // 100MB
.mimeType("application/pdf")
.s3Key("users/1/assets/test.pdf")
.objectKey("users/1/assets/test.pdf")
.source(Asset.AssetSource.upload)
.build();
ReflectionTestUtils.setField(testAsset, "id", 1L);
Expand Down Expand Up @@ -217,7 +217,7 @@ void successStorageCalculation() {
.fileName("file1.pdf")
.fileSize(1024L * 1024L * 200) // 200MB
.mimeType("application/pdf")
.s3Key("key1")
.objectKey("key1")
.source(Asset.AssetSource.upload)
.build();
ReflectionTestUtils.setField(asset1, "id", 2L);
Expand All @@ -228,7 +228,7 @@ void successStorageCalculation() {
.fileName("file2.png")
.fileSize(1024L * 1024L * 50) // 50MB
.mimeType("image/png")
.s3Key("key2")
.objectKey("key2")
.source(Asset.AssetSource.upload)
.build();
ReflectionTestUtils.setField(asset2, "id", 3L);
Expand Down
Loading