From 2fd34f3959be32e35a5fe6d89c01234ab96a26cd Mon Sep 17 00:00:00 2001 From: yjj Date: Fri, 18 Jul 2025 23:35:23 +0900 Subject: [PATCH 01/13] =?UTF-8?q?feat=20:=20=EC=BB=A8=ED=8A=B8=EB=A1=A4?= =?UTF-8?q?=EB=9F=AC,=20=EC=84=9C=EB=B9=84=EC=8A=A4,=20dto,=20=EC=97=94?= =?UTF-8?q?=ED=8B=B0=ED=8B=B0,=20=EB=A0=88=ED=8F=AC=EC=A7=80=ED=86=A0?= =?UTF-8?q?=EB=A6=AC=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=EC=97=90=EB=9F=AC?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80=EB=90=9C=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=EC=97=90=20=EB=8C=80=EC=9D=91=ED=95=98=EA=B2=8C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20&&=20=EB=A1=9C=EC=BB=AC=20ipfs=EC=97=90=20?= =?UTF-8?q?=EC=97=85=EB=A1=9C=EB=93=9C=20=EB=B0=8F=20=EB=B0=98=ED=99=98?= =?UTF-8?q?=EA=B0=92=20dto=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Capsule/controller/ContentController.java | 50 ++++++++ .../Capsule/dto/ContentUploadRequest.java | 34 ++++++ .../domain/Capsule/dto/MetadataDto.java | 35 ++++++ .../domain/Capsule/entity/Metadata.java | 33 ++++++ .../repository/MetadataRepository.java | 11 ++ .../Capsule/service/ContentService.java | 112 ++++++++++++++++++ .../MemoReal/global/exception/ErrorCode.java | 12 +- .../MemoReal/global/ipfs/IpfsUploader.java | 56 +++++++++ .../global/ipfs/dto/IpfsUploadResult.java | 12 ++ 9 files changed, 353 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/dasom/MemoReal/domain/Capsule/controller/ContentController.java create mode 100644 src/main/java/com/dasom/MemoReal/domain/Capsule/dto/ContentUploadRequest.java create mode 100644 src/main/java/com/dasom/MemoReal/domain/Capsule/dto/MetadataDto.java create mode 100644 src/main/java/com/dasom/MemoReal/domain/Capsule/entity/Metadata.java create mode 100644 src/main/java/com/dasom/MemoReal/domain/Capsule/repository/MetadataRepository.java create mode 100644 src/main/java/com/dasom/MemoReal/domain/Capsule/service/ContentService.java create mode 100644 src/main/java/com/dasom/MemoReal/global/ipfs/IpfsUploader.java create mode 100644 src/main/java/com/dasom/MemoReal/global/ipfs/dto/IpfsUploadResult.java diff --git a/src/main/java/com/dasom/MemoReal/domain/Capsule/controller/ContentController.java b/src/main/java/com/dasom/MemoReal/domain/Capsule/controller/ContentController.java new file mode 100644 index 0000000..82cafb6 --- /dev/null +++ b/src/main/java/com/dasom/MemoReal/domain/Capsule/controller/ContentController.java @@ -0,0 +1,50 @@ +package com.dasom.MemoReal.domain.Capsule.controller; + +import com.dasom.MemoReal.domain.Capsule.dto.ContentUploadRequest; +import com.dasom.MemoReal.domain.Capsule.dto.MetadataDto; +import com.dasom.MemoReal.domain.Capsule.service.ContentService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/content") +public class ContentController { + + private final ContentService contentService; + + @PostMapping("/upload") + public ResponseEntity uploadContent( + @RequestPart("file") MultipartFile file, + @RequestPart("metadata") ContentUploadRequest metadata + ) { + Long userId = 1L; // TODO: JWT에서 추출 예정 + MetadataDto savedMetadata = contentService.upload(file, metadata, userId); + return ResponseEntity.ok("정상적으로 업로드 되었습니다. 파일 해시: " + savedMetadata.getFileHash()); + } + + @GetMapping("/{id}") // 메타데이터 id를 받아서 상세정보 조회 + public ResponseEntity getMetadata(@PathVariable Long id) { + MetadataDto dto = contentService.retrieveMetadata(id); + return ResponseEntity.ok(dto); + } + + @GetMapping("/download/{id}") // 컨텐츠에 접근 + public ResponseEntity downloadFile(@PathVariable Long id) throws Exception { + byte[] fileData = contentService.downloadFile(id); + return ResponseEntity.ok() + .header("Content-Disposition", "attachment; filename=\"downloaded_file\"") + .body(fileData); + } + + @GetMapping("/user/{userId}") // userid를 기반으로 해당 유저가 등록한 모든 메타데이터 조회 + // Todo: JWT에서 userId 추출 예정 + public ResponseEntity> getUserContent(@PathVariable Long userId) { + List list = contentService.findAllByUserId(userId); + return ResponseEntity.ok(list); + } +} diff --git a/src/main/java/com/dasom/MemoReal/domain/Capsule/dto/ContentUploadRequest.java b/src/main/java/com/dasom/MemoReal/domain/Capsule/dto/ContentUploadRequest.java new file mode 100644 index 0000000..b6f3c7f --- /dev/null +++ b/src/main/java/com/dasom/MemoReal/domain/Capsule/dto/ContentUploadRequest.java @@ -0,0 +1,34 @@ +package com.dasom.MemoReal.domain.Capsule.dto; + +import com.dasom.MemoReal.domain.Capsule.entity.Metadata; +import lombok.*; + +import java.time.LocalDate; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ContentUploadRequest { + + private String title; + private String description; + private String accessCondition; + private String category; + private String tags; + + + public Metadata toEntity(String filename, String contentType, String ipfsContentHash, LocalDate uploadedDate, Long userId) { + return Metadata.builder() + .filename(filename) + .contentType(contentType) + .title(this.title) + .description(this.description) + .accessCondition(this.accessCondition) + .category(this.category) + .tags(this.tags) + .uploadedDate(uploadedDate) + .ipfsContentHash(ipfsContentHash) + .userId(userId) + .build(); + } +} diff --git a/src/main/java/com/dasom/MemoReal/domain/Capsule/dto/MetadataDto.java b/src/main/java/com/dasom/MemoReal/domain/Capsule/dto/MetadataDto.java new file mode 100644 index 0000000..8edf70a --- /dev/null +++ b/src/main/java/com/dasom/MemoReal/domain/Capsule/dto/MetadataDto.java @@ -0,0 +1,35 @@ +package com.dasom.MemoReal.domain.Capsule.dto; + +import com.dasom.MemoReal.domain.Capsule.entity.Metadata; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class MetadataDto { + private String filename; + private String contentType; //확장자 + private String title; + private String description; + private String uploadedDate; // yyyy-MM-dd + private String fileHash; // 실제 콘텐츠 파일의 IPFS 해시 + private String accessCondition; // 열람 조건 + private String category; //컨텐츠 분류 용 카테고리 + private String tags; + + public static MetadataDto fromEntity(Metadata metadata) { + return new MetadataDto( + metadata.getFilename(), + metadata.getContentType(), + metadata.getTitle(), + metadata.getDescription(), + metadata.getUploadedDate() != null ? metadata.getUploadedDate().toString() : null, + metadata.getIpfsContentHash(), + metadata.getAccessCondition(), + metadata.getCategory(), + metadata.getTags() + ); + } +} diff --git a/src/main/java/com/dasom/MemoReal/domain/Capsule/entity/Metadata.java b/src/main/java/com/dasom/MemoReal/domain/Capsule/entity/Metadata.java new file mode 100644 index 0000000..e56c702 --- /dev/null +++ b/src/main/java/com/dasom/MemoReal/domain/Capsule/entity/Metadata.java @@ -0,0 +1,33 @@ +package com.dasom.MemoReal.domain.Capsule.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; + +@Table(name = "metadata") +@Entity +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Metadata { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String ipfsContentHash; + private String filename; + private String contentType; + private String title; + private String description; + private LocalDate uploadedDate; + private String accessCondition; + private String category; + private String tags; + private Long userId;// 추후에 user 테이블과 외래키 연결 +} \ No newline at end of file diff --git a/src/main/java/com/dasom/MemoReal/domain/Capsule/repository/MetadataRepository.java b/src/main/java/com/dasom/MemoReal/domain/Capsule/repository/MetadataRepository.java new file mode 100644 index 0000000..dd6a500 --- /dev/null +++ b/src/main/java/com/dasom/MemoReal/domain/Capsule/repository/MetadataRepository.java @@ -0,0 +1,11 @@ +package com.dasom.MemoReal.domain.Capsule.repository; + +import com.dasom.MemoReal.domain.Capsule.entity.Metadata; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface MetadataRepository extends JpaRepository { + List findAllByUserId(Long userId); // 유저별 메타데이터 조회 + +} diff --git a/src/main/java/com/dasom/MemoReal/domain/Capsule/service/ContentService.java b/src/main/java/com/dasom/MemoReal/domain/Capsule/service/ContentService.java new file mode 100644 index 0000000..9882f36 --- /dev/null +++ b/src/main/java/com/dasom/MemoReal/domain/Capsule/service/ContentService.java @@ -0,0 +1,112 @@ +package com.dasom.MemoReal.domain.Capsule.service; + +import com.dasom.MemoReal.domain.Capsule.dto.ContentUploadRequest; +import com.dasom.MemoReal.domain.Capsule.dto.MetadataDto; +import com.dasom.MemoReal.domain.Capsule.entity.Metadata; +import com.dasom.MemoReal.domain.Capsule.repository.MetadataRepository; +import com.dasom.MemoReal.global.exception.CustomException; +import com.dasom.MemoReal.global.exception.ErrorCode; +import com.dasom.MemoReal.global.ipfs.IpfsUploader; +import com.dasom.MemoReal.global.ipfs.dto.IpfsUploadResult; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.LocalDate; +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class ContentService { + + private final MetadataRepository repository; + private final IpfsUploader ipfsUploader; + + public MetadataDto upload(MultipartFile file, ContentUploadRequest request, Long userId) { + if (userId == null) { + throw new CustomException(ErrorCode.USER_ID_NOT_FOUND); + } + File tempFile = null; + try { + // MultipartFile을 임시 파일로 저장 + tempFile = File.createTempFile("upload-", file.getOriginalFilename()); + file.transferTo(tempFile.toPath()); + + // IPFS에 업로드 + IpfsUploadResult ipfsResult = ipfsUploader.upload(tempFile); + + // Metadata 엔티티 생성 및 저장 + Metadata metadata = request.toEntity( + ipfsResult.getFileName(), + file.getContentType(), + ipfsResult.getHash(), + LocalDate.now(), + userId + ); + repository.save(metadata); + + // 저장된 메타데이터를 DTO로 변환 후 반환 + return MetadataDto.fromEntity(metadata); + + } catch (IOException e) { + throw new CustomException(ErrorCode.UPLOAD_FAILED, "파일 처리 중 오류 발생: " + e.getMessage()); + + } finally { + // 업로드 후 임시 파일 삭제 + if (tempFile != null && tempFile.exists()) { + boolean deleted = tempFile.delete(); + if (!deleted) { + System.err.println("임시 파일 삭제 실패: " + tempFile.getAbsolutePath()); + } + } + } + } + + // 메타데이터 조회 + public MetadataDto retrieveMetadata(Long id) { + Metadata metadata = repository.findById(id) + .orElseThrow(() -> new CustomException(ErrorCode.METADATA_NOT_FOUND, "메타데이터를 찾을 수 없습니다. ID: " + id)); + + return MetadataDto.fromEntity(metadata); + } + + // 파일 다운로드 (mocked) + public byte[] downloadFile(Long id) throws Exception { + Metadata metadata = repository.findById(id) + .orElseThrow(() -> new CustomException(ErrorCode.METADATA_NOT_FOUND, "메타데이터를 찾을 수 없습니다. ID: " + id)); + + MetadataDto dto = MetadataDto.fromEntity(metadata); + + LocalDate accessDate = LocalDate.parse(dto.getAccessCondition()); + if (LocalDate.now().isBefore(accessDate)) { + throw new CustomException(ErrorCode.ACCESS_DENIED); + } + + Path mockPath = Path.of("/tmp/mock-decrypted/" + metadata.getIpfsContentHash()); + if (!Files.exists(mockPath)) { + throw new CustomException(ErrorCode.FILE_NOT_FOUND); + } + + return Files.readAllBytes(mockPath); + } + + public List findAllByUserId(Long userId) { + if (userId == null) { + throw new CustomException(ErrorCode.USER_ID_NOT_FOUND); + } + List metadataList = repository.findAllByUserId(userId); + + if (metadataList == null) { + throw new CustomException(ErrorCode.METADATA_NOT_FOUND, + "해당 유저의 메타데이터를 조회할 수 없습니다. userId: " + userId); + } + return metadataList.stream() + .map(MetadataDto::fromEntity) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/com/dasom/MemoReal/global/exception/ErrorCode.java b/src/main/java/com/dasom/MemoReal/global/exception/ErrorCode.java index cca25cc..511fea1 100644 --- a/src/main/java/com/dasom/MemoReal/global/exception/ErrorCode.java +++ b/src/main/java/com/dasom/MemoReal/global/exception/ErrorCode.java @@ -10,6 +10,7 @@ public enum ErrorCode { DUPLICATE_EMAIL("USER_002", HttpStatus.CONFLICT, "이미 존재하는 이메일입니다."), USER_UPDATE_NOT_FOUND("USER_003", HttpStatus.NOT_FOUND, "사용자를 찾을 수 없습니다."), INVALID_UPDATE_FIELD("USER_004", HttpStatus.BAD_REQUEST, "수정할 수 없는 필드입니다."), + USER_ID_NOT_FOUND("USER_005", HttpStatus.NOT_FOUND, "유저 아이디를 찾을 수 없습니다."), // 사용자 로그인 관련 에러 USER_NOT_FOUND("AUTH_001", HttpStatus.NOT_FOUND, "이메일이 존재하지 않습니다."), @@ -18,7 +19,14 @@ public enum ErrorCode { // 일반적인 에러(유효성 검사 등) INVALID_INPUT_VALUE("COMMON_001", HttpStatus.BAD_REQUEST, "유효하지 않은 입력 값입니다."), UNAUTHORIZED("COMMON_002", HttpStatus.UNAUTHORIZED, "인증되지 않은 접근입니다."), - INTERNAL_SERVER_ERROR("COMMON_999", HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부 오류가 발생했습니다."); + INTERNAL_SERVER_ERROR("COMMON_999", HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부 오류가 발생했습니다."), + + // 콘텐츠 관련 에러 + METADATA_NOT_FOUND("CONTENT_001", HttpStatus.NOT_FOUND, "메타데이터를 찾을 수 없습니다."), + FILE_NOT_FOUND("CONTENT_002", HttpStatus.NOT_FOUND, "요청한 파일을 찾을 수 없습니다."), + ACCESS_DENIED("CONTENT_003", HttpStatus.FORBIDDEN, "열람 조건을 만족하지 못했습니다."), + UPLOAD_FAILED("CONTENT_004", HttpStatus.INTERNAL_SERVER_ERROR, "컨텐츠 업로드에 실패했습니다."), + INVALID_INPUT("CONTENT_005", HttpStatus.BAD_REQUEST, "유효하지 않은 입력 값입니다."); private final String code; private final HttpStatus httpStatus; @@ -29,4 +37,4 @@ public enum ErrorCode { this.httpStatus = httpStatus; this.message = message; } -} \ No newline at end of file +} diff --git a/src/main/java/com/dasom/MemoReal/global/ipfs/IpfsUploader.java b/src/main/java/com/dasom/MemoReal/global/ipfs/IpfsUploader.java new file mode 100644 index 0000000..d4cb81f --- /dev/null +++ b/src/main/java/com/dasom/MemoReal/global/ipfs/IpfsUploader.java @@ -0,0 +1,56 @@ +package com.dasom.MemoReal.global.ipfs; + +import com.dasom.MemoReal.global.exception.CustomException; +import com.dasom.MemoReal.global.exception.ErrorCode; +import com.dasom.MemoReal.global.ipfs.dto.IpfsUploadResult; +import org.springframework.core.io.FileSystemResource; +import org.springframework.http.*; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import java.io.File; + +@Component +public class IpfsUploader { + + public IpfsUploadResult upload(File file) { + try { + RestTemplate restTemplate = new RestTemplate(); + + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("file", new FileSystemResource(file)); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.MULTIPART_FORM_DATA); + + HttpEntity> requestEntity = new HttpEntity<>(body, headers); + + ResponseEntity response = restTemplate.postForEntity( + "http://localhost:5001/api/v0/add", + requestEntity, + String.class + ); + + if (response.getStatusCode().is2xxSuccessful()) { + String bodyStr = response.getBody(); + if (bodyStr != null && bodyStr.contains("Hash")) { + String hash = extractValue(bodyStr, "Hash"); + String name = extractValue(bodyStr, "Name"); + String size = extractValue(bodyStr, "Size"); + return new IpfsUploadResult(hash, name, Long.parseLong(size)); + } + } + throw new CustomException(ErrorCode.UPLOAD_FAILED, "IPFS 응답 실패: " + response.getStatusCode()); + } catch (Exception e) { + throw new CustomException(ErrorCode.UPLOAD_FAILED, "IPFS 요청 실패: " + e.getMessage()); + } + } + + private String extractValue(String json, String key) { + int start = json.indexOf("\"" + key + "\":\"") + key.length() + 4; + int end = json.indexOf("\"", start); + return json.substring(start, end); + } +} diff --git a/src/main/java/com/dasom/MemoReal/global/ipfs/dto/IpfsUploadResult.java b/src/main/java/com/dasom/MemoReal/global/ipfs/dto/IpfsUploadResult.java new file mode 100644 index 0000000..2c9c785 --- /dev/null +++ b/src/main/java/com/dasom/MemoReal/global/ipfs/dto/IpfsUploadResult.java @@ -0,0 +1,12 @@ +package com.dasom.MemoReal.global.ipfs.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class IpfsUploadResult { + private final String hash; + private final String fileName; + private final long fileSize; +} From f30d4caf7bc6e7e772c76945f3e55e99a2157393 Mon Sep 17 00:00:00 2001 From: yjj Date: Sat, 19 Jul 2025 00:51:44 +0900 Subject: [PATCH 02/13] =?UTF-8?q?feat=20:=20=EB=A9=94=ED=83=80=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=86=8D=EC=84=B1=EC=9D=98=20=EA=B0=92?= =?UTF-8?q?=EC=9D=84=20=EC=88=98=EC=A0=95=ED=95=98=EB=8A=94=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80,=20IPFS=EC=9D=98=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=A0=91=EA=B7=BC=EC=8B=9C=EB=8F=84=20=ED=95=98?= =?UTF-8?q?=EA=B3=A0=20=EB=8B=A4=EC=9A=B4=EB=B0=9B=EB=8A=94=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 값 수정 기능은 추후 JWT 이용해서 접근하는 유저의 id를 가져와 메타데이터의 id와 동일한 경우에만 수정 가능하게 변경 할 예정 --- .../Capsule/controller/ContentController.java | 16 ++++- .../Capsule/service/ContentService.java | 68 +++++++++++++++---- .../{IpfsUploader.java => IpfsClient.java} | 34 +++++++++- 3 files changed, 100 insertions(+), 18 deletions(-) rename src/main/java/com/dasom/MemoReal/global/ipfs/{IpfsUploader.java => IpfsClient.java} (64%) diff --git a/src/main/java/com/dasom/MemoReal/domain/Capsule/controller/ContentController.java b/src/main/java/com/dasom/MemoReal/domain/Capsule/controller/ContentController.java index 82cafb6..5e6a996 100644 --- a/src/main/java/com/dasom/MemoReal/domain/Capsule/controller/ContentController.java +++ b/src/main/java/com/dasom/MemoReal/domain/Capsule/controller/ContentController.java @@ -9,6 +9,7 @@ import org.springframework.web.multipart.MultipartFile; import java.util.List; +import java.util.Map; @RestController @RequiredArgsConstructor @@ -33,8 +34,8 @@ public ResponseEntity getMetadata(@PathVariable Long id) { return ResponseEntity.ok(dto); } - @GetMapping("/download/{id}") // 컨텐츠에 접근 - public ResponseEntity downloadFile(@PathVariable Long id) throws Exception { + @GetMapping("/download/{id}") + public ResponseEntity downloadFile(@PathVariable Long id) { byte[] fileData = contentService.downloadFile(id); return ResponseEntity.ok() .header("Content-Disposition", "attachment; filename=\"downloaded_file\"") @@ -42,9 +43,18 @@ public ResponseEntity downloadFile(@PathVariable Long id) throws Exception { } @GetMapping("/user/{userId}") // userid를 기반으로 해당 유저가 등록한 모든 메타데이터 조회 - // Todo: JWT에서 userId 추출 예정 + // Todo: JWT에서 userId 추출해서 사용하게 수정 할 예정 public ResponseEntity> getUserContent(@PathVariable Long userId) { List list = contentService.findAllByUserId(userId); return ResponseEntity.ok(list); } + @PatchMapping("/{id}") + // Todo: JWT에서 userId 추출하여 메타데이터 상 등록된 대상과 같을 경우에만 실행 되게 수정 + public ResponseEntity updateMetadataFields( + @PathVariable Long id, + @RequestBody Map updates + ) { + String message = contentService.updateMetadataFields(id, updates); + return ResponseEntity.ok(message); + } } diff --git a/src/main/java/com/dasom/MemoReal/domain/Capsule/service/ContentService.java b/src/main/java/com/dasom/MemoReal/domain/Capsule/service/ContentService.java index 9882f36..3ced8da 100644 --- a/src/main/java/com/dasom/MemoReal/domain/Capsule/service/ContentService.java +++ b/src/main/java/com/dasom/MemoReal/domain/Capsule/service/ContentService.java @@ -6,7 +6,7 @@ import com.dasom.MemoReal.domain.Capsule.repository.MetadataRepository; import com.dasom.MemoReal.global.exception.CustomException; import com.dasom.MemoReal.global.exception.ErrorCode; -import com.dasom.MemoReal.global.ipfs.IpfsUploader; +import com.dasom.MemoReal.global.ipfs.IpfsClient; import com.dasom.MemoReal.global.ipfs.dto.IpfsUploadResult; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -14,10 +14,10 @@ import java.io.File; import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; import java.time.LocalDate; +import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; @Service @@ -25,7 +25,7 @@ public class ContentService { private final MetadataRepository repository; - private final IpfsUploader ipfsUploader; + private final IpfsClient IpfsClient; public MetadataDto upload(MultipartFile file, ContentUploadRequest request, Long userId) { if (userId == null) { @@ -38,7 +38,7 @@ public MetadataDto upload(MultipartFile file, ContentUploadRequest request, Long file.transferTo(tempFile.toPath()); // IPFS에 업로드 - IpfsUploadResult ipfsResult = ipfsUploader.upload(tempFile); + IpfsUploadResult ipfsResult = IpfsClient.upload(tempFile); // Metadata 엔티티 생성 및 저장 Metadata metadata = request.toEntity( @@ -75,8 +75,8 @@ public MetadataDto retrieveMetadata(Long id) { return MetadataDto.fromEntity(metadata); } - // 파일 다운로드 (mocked) - public byte[] downloadFile(Long id) throws Exception { + // IPFS에서 실제 파일 다운로드 + public byte[] downloadFile(Long id) { Metadata metadata = repository.findById(id) .orElseThrow(() -> new CustomException(ErrorCode.METADATA_NOT_FOUND, "메타데이터를 찾을 수 없습니다. ID: " + id)); @@ -87,12 +87,8 @@ public byte[] downloadFile(Long id) throws Exception { throw new CustomException(ErrorCode.ACCESS_DENIED); } - Path mockPath = Path.of("/tmp/mock-decrypted/" + metadata.getIpfsContentHash()); - if (!Files.exists(mockPath)) { - throw new CustomException(ErrorCode.FILE_NOT_FOUND); - } - - return Files.readAllBytes(mockPath); + // IPFS 해시를 이용해 실제 파일 다운로드 + return IpfsClient.download(metadata.getIpfsContentHash()); } public List findAllByUserId(Long userId) { @@ -109,4 +105,50 @@ public List findAllByUserId(Long userId) { .map(MetadataDto::fromEntity) .collect(Collectors.toList()); } + public String updateMetadataFields(Long id, Map updates) { + Metadata metadata = repository.findById(id) + .orElseThrow(() -> new CustomException(ErrorCode.METADATA_NOT_FOUND, "메타데이터를 찾을 수 없습니다. ID: " + id)); + + // 수정 가능한 필드 목록 + List allowedFields = List.of("filename", "contentType", "title", "description", "category", "tags"); + + // 수정 불가능한 필드를 담을 리스트 + List ignoredFields = new ArrayList<>(); + + // 실제 수정할 필드들 처리 + updates.forEach((key, value) -> { + if (allowedFields.contains(key)) { + switch (key) { + case "filename": + metadata.setFilename((String) value); + break; + case "contentType": + metadata.setContentType((String) value); + break; + case "title": + metadata.setTitle((String) value); + break; + case "description": + metadata.setDescription((String) value); + break; + case "category": + metadata.setCategory((String) value); + break; + case "tags": + metadata.setTags((String) value); + break; + } + } else { + ignoredFields.add(key); + } + }); + + repository.save(metadata); + + if (ignoredFields.isEmpty()) { + return "수정 완료."; + } else { + return "수정 완료. 무시된 필드: " + String.join(", ", ignoredFields); + } + } } diff --git a/src/main/java/com/dasom/MemoReal/global/ipfs/IpfsUploader.java b/src/main/java/com/dasom/MemoReal/global/ipfs/IpfsClient.java similarity index 64% rename from src/main/java/com/dasom/MemoReal/global/ipfs/IpfsUploader.java rename to src/main/java/com/dasom/MemoReal/global/ipfs/IpfsClient.java index d4cb81f..7b04a99 100644 --- a/src/main/java/com/dasom/MemoReal/global/ipfs/IpfsUploader.java +++ b/src/main/java/com/dasom/MemoReal/global/ipfs/IpfsClient.java @@ -13,8 +13,15 @@ import java.io.File; @Component -public class IpfsUploader { +public class IpfsClient { + private static final String IPFS_API_BASE_URL = "http://localhost:5001/api/v0"; + + /** + * IPFS에 파일 업로드 + * @param file 업로드할 파일 + * @return 업로드 결과 객체 (해시, 파일명, 크기) + */ public IpfsUploadResult upload(File file) { try { RestTemplate restTemplate = new RestTemplate(); @@ -28,7 +35,7 @@ public IpfsUploadResult upload(File file) { HttpEntity> requestEntity = new HttpEntity<>(body, headers); ResponseEntity response = restTemplate.postForEntity( - "http://localhost:5001/api/v0/add", + IPFS_API_BASE_URL + "/add", requestEntity, String.class ); @@ -48,6 +55,29 @@ public IpfsUploadResult upload(File file) { } } + /** + * IPFS 해시로부터 파일 데이터 다운로드 + * @param ipfsHash IPFS 파일 해시 + * @return 파일 데이터 바이트 배열 + */ + public byte[] download(String ipfsHash) { + try { + RestTemplate restTemplate = new RestTemplate(); + String url = IPFS_API_BASE_URL + "/cat?arg=" + ipfsHash; + + ResponseEntity response = restTemplate.getForEntity(url, byte[].class); + + if (response.getStatusCode() == HttpStatus.OK) { + return response.getBody(); + } else { + throw new CustomException(ErrorCode.FILE_NOT_FOUND, "IPFS 파일 다운로드 실패: " + response.getStatusCode()); + } + } catch (Exception e) { + throw new CustomException(ErrorCode.FILE_NOT_FOUND, "IPFS 요청 실패: " + e.getMessage()); + } + } + + // 간단한 JSON 값 파싱 private String extractValue(String json, String key) { int start = json.indexOf("\"" + key + "\":\"") + key.length() + 4; int end = json.indexOf("\"", start); From d05ede3ff3b243b685028b34bc79b5d6664c3d69 Mon Sep 17 00:00:00 2001 From: yjj Date: Sat, 19 Jul 2025 02:10:53 +0900 Subject: [PATCH 03/13] =?UTF-8?q?feat=20:=20=EB=A9=94=ED=83=80=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=EC=99=80=20=EC=BB=A8=ED=85=90=EC=B8=A0?= =?UTF-8?q?=EB=A5=BC=20=EC=82=AD=EC=A0=9C=ED=95=98=EB=8A=94=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Capsule/controller/ContentController.java | 7 +++++++ .../Capsule/service/ContentService.java | 17 +++++++++++++++ .../MemoReal/global/exception/ErrorCode.java | 4 ++-- .../MemoReal/global/ipfs/IpfsClient.java | 21 +++++++++++++++++++ 4 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/dasom/MemoReal/domain/Capsule/controller/ContentController.java b/src/main/java/com/dasom/MemoReal/domain/Capsule/controller/ContentController.java index 5e6a996..341b40f 100644 --- a/src/main/java/com/dasom/MemoReal/domain/Capsule/controller/ContentController.java +++ b/src/main/java/com/dasom/MemoReal/domain/Capsule/controller/ContentController.java @@ -57,4 +57,11 @@ public ResponseEntity updateMetadataFields( String message = contentService.updateMetadataFields(id, updates); return ResponseEntity.ok(message); } + @DeleteMapping("/delete/{metadataId}") + // Todo: JWT 연동 후 실제 유저 아이디로 변경 + public ResponseEntity deleteContent(@PathVariable Long metadataId) { + Long userId = 1L; // TODO: JWT 연동 후 실제 유저 아이디로 변경 + contentService.deleteMetadataAndContent(metadataId, userId); + return ResponseEntity.ok("메타데이터 및 IPFS 컨텐츠가 성공적으로 삭제되었습니다."); + } } diff --git a/src/main/java/com/dasom/MemoReal/domain/Capsule/service/ContentService.java b/src/main/java/com/dasom/MemoReal/domain/Capsule/service/ContentService.java index 3ced8da..4fdc4dc 100644 --- a/src/main/java/com/dasom/MemoReal/domain/Capsule/service/ContentService.java +++ b/src/main/java/com/dasom/MemoReal/domain/Capsule/service/ContentService.java @@ -18,6 +18,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.stream.Collectors; @Service @@ -151,4 +152,20 @@ public String updateMetadataFields(Long id, Map updates) { return "수정 완료. 무시된 필드: " + String.join(", ", ignoredFields); } } + public void deleteMetadataAndContent(Long metadataId, Long userId) { + Metadata metadata = repository.findById(metadataId) + .orElseThrow(() -> new CustomException(ErrorCode.METADATA_NOT_FOUND)); + + if (!Objects.equals(metadata.getUserId(), userId)) { + throw new CustomException(ErrorCode.ACCESS_DENIED); + } + + try { + IpfsClient.unpinAndGc(metadata.getIpfsContentHash()); + } catch (Exception e) { + throw new CustomException(ErrorCode.CONTENT_DELETE_FAILED, "IPFS에서 컨텐츠 삭제 실패: " + e.getMessage()); + } + + repository.delete(metadata); + } } diff --git a/src/main/java/com/dasom/MemoReal/global/exception/ErrorCode.java b/src/main/java/com/dasom/MemoReal/global/exception/ErrorCode.java index 511fea1..d6d1067 100644 --- a/src/main/java/com/dasom/MemoReal/global/exception/ErrorCode.java +++ b/src/main/java/com/dasom/MemoReal/global/exception/ErrorCode.java @@ -26,8 +26,8 @@ public enum ErrorCode { FILE_NOT_FOUND("CONTENT_002", HttpStatus.NOT_FOUND, "요청한 파일을 찾을 수 없습니다."), ACCESS_DENIED("CONTENT_003", HttpStatus.FORBIDDEN, "열람 조건을 만족하지 못했습니다."), UPLOAD_FAILED("CONTENT_004", HttpStatus.INTERNAL_SERVER_ERROR, "컨텐츠 업로드에 실패했습니다."), - INVALID_INPUT("CONTENT_005", HttpStatus.BAD_REQUEST, "유효하지 않은 입력 값입니다."); - + INVALID_INPUT("CONTENT_005", HttpStatus.BAD_REQUEST, "유효하지 않은 입력 값입니다."), + CONTENT_DELETE_FAILED("CONTENT_006",HttpStatus.INTERNAL_SERVER_ERROR ,"컨텐츠 삭제 실패"); private final String code; private final HttpStatus httpStatus; private final String message; diff --git a/src/main/java/com/dasom/MemoReal/global/ipfs/IpfsClient.java b/src/main/java/com/dasom/MemoReal/global/ipfs/IpfsClient.java index 7b04a99..e87805f 100644 --- a/src/main/java/com/dasom/MemoReal/global/ipfs/IpfsClient.java +++ b/src/main/java/com/dasom/MemoReal/global/ipfs/IpfsClient.java @@ -76,7 +76,28 @@ public byte[] download(String ipfsHash) { throw new CustomException(ErrorCode.FILE_NOT_FOUND, "IPFS 요청 실패: " + e.getMessage()); } } + public void unpinAndGc(String ipfsHash) { + try { + ProcessBuilder unpinBuilder = new ProcessBuilder("ipfs", "pin", "rm", ipfsHash); + Process unpin = unpinBuilder.start(); + int unpinExit = unpin.waitFor(); + + if (unpinExit != 0) { + throw new RuntimeException("IPFS pin 제거 실패"); + } + ProcessBuilder gcBuilder = new ProcessBuilder("ipfs", "repo", "gc"); + Process gc = gcBuilder.start(); + int gcExit = gc.waitFor(); + + if (gcExit != 0) { + throw new RuntimeException("IPFS repo gc 실패"); + } + + } catch (Exception e) { + throw new RuntimeException("IPFS 삭제 과정 중 오류 발생: " + e.getMessage(), e); + } + } // 간단한 JSON 값 파싱 private String extractValue(String json, String key) { int start = json.indexOf("\"" + key + "\":\"") + key.length() + 4; From bdd7bfb743d646a2d2383c5c12d946b62d074d37 Mon Sep 17 00:00:00 2001 From: yjj Date: Sat, 19 Jul 2025 20:10:57 +0900 Subject: [PATCH 04/13] =?UTF-8?q?feat=20:=20UserService=20=EC=97=90=20user?= =?UTF-8?q?name=EC=9D=84=20=EC=9D=B4=EC=9A=A9=ED=95=B4=EC=84=9C=20userid?= =?UTF-8?q?=EB=A5=BC=20=EC=B6=94=EC=B6=9C=ED=95=98=EB=8A=94=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dasom/MemoReal/domain/user/service/UserService.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/dasom/MemoReal/domain/user/service/UserService.java b/src/main/java/com/dasom/MemoReal/domain/user/service/UserService.java index 545b93d..b088418 100644 --- a/src/main/java/com/dasom/MemoReal/domain/user/service/UserService.java +++ b/src/main/java/com/dasom/MemoReal/domain/user/service/UserService.java @@ -2,8 +2,9 @@ import com.dasom.MemoReal.domain.user.dto.JoinDTO; import com.dasom.MemoReal.domain.user.dto.UserDTO; -import com.dasom.MemoReal.domain.user.entity.User; import com.dasom.MemoReal.domain.user.repository.UserRepository; +import com.dasom.MemoReal.global.exception.CustomException; +import com.dasom.MemoReal.global.exception.ErrorCode; import com.dasom.MemoReal.global.jwt.dto.JwtTokenDTO; import com.dasom.MemoReal.global.jwt.provider.JwtTokenProvider; import lombok.RequiredArgsConstructor; @@ -52,7 +53,11 @@ public UserDTO join(JoinDTO signUpDto) { roles.add("USER"); // USER 권한 부여 return UserDTO.toDto(userRepository.save(signUpDto.toEntity(encodedPassword, roles))); } - + public Long getUserIdByUsername(String username) { + return userRepository.findByUsername(username) + .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)) + .getId(); + } @PostMapping("/test") public String test() { return "success"; From 8ff55800b4e01adbae9c28a03cb9bd7d166e72dc Mon Sep 17 00:00:00 2001 From: yjj Date: Sat, 19 Jul 2025 20:15:34 +0900 Subject: [PATCH 05/13] =?UTF-8?q?feat=20:=20=EC=BB=A8=ED=8A=B8=EB=A1=A4?= =?UTF-8?q?=EB=9F=AC=EC=97=90=EC=84=9C=20SecurityUtil=20=EC=99=80=20UserSe?= =?UTF-8?q?rvice=20=EC=9D=B4=EC=9A=A9=ED=95=98=EC=97=AC=20userid=20?= =?UTF-8?q?=EC=B6=94=EC=B6=9C=ED=95=98=EC=97=AC=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=EC=97=90=20=EB=84=98=EA=B2=A8=EC=A3=BC=EA=B3=A0=20?= =?UTF-8?q?=EC=9D=B4=EB=A5=BC=20=EA=B8=B0=EB=B0=98=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=B4=EC=95=88=EC=B2=98=EB=A6=AC=20=EB=B0=8F=20=EC=9C=A0?= =?UTF-8?q?=EC=A0=80=20=EC=8B=9D=EB=B3=84=ED=95=98=EC=97=AC=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=8B=A4=ED=96=89=ED=95=98=EA=B2=8C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Capsule/controller/ContentController.java | 25 +++++++------ .../Capsule/service/ContentService.java | 35 +++++++++---------- 2 files changed, 32 insertions(+), 28 deletions(-) diff --git a/src/main/java/com/dasom/MemoReal/domain/Capsule/controller/ContentController.java b/src/main/java/com/dasom/MemoReal/domain/Capsule/controller/ContentController.java index 341b40f..0c25778 100644 --- a/src/main/java/com/dasom/MemoReal/domain/Capsule/controller/ContentController.java +++ b/src/main/java/com/dasom/MemoReal/domain/Capsule/controller/ContentController.java @@ -3,6 +3,8 @@ import com.dasom.MemoReal.domain.Capsule.dto.ContentUploadRequest; import com.dasom.MemoReal.domain.Capsule.dto.MetadataDto; import com.dasom.MemoReal.domain.Capsule.service.ContentService; +import com.dasom.MemoReal.domain.user.service.UserService; +import com.dasom.MemoReal.global.security.util.SecurityUtil; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -17,18 +19,19 @@ public class ContentController { private final ContentService contentService; + private final UserService userService; @PostMapping("/upload") public ResponseEntity uploadContent( @RequestPart("file") MultipartFile file, @RequestPart("metadata") ContentUploadRequest metadata ) { - Long userId = 1L; // TODO: JWT에서 추출 예정 + Long userId = userService.getUserIdByUsername(SecurityUtil.getCurrentUsername()); MetadataDto savedMetadata = contentService.upload(file, metadata, userId); return ResponseEntity.ok("정상적으로 업로드 되었습니다. 파일 해시: " + savedMetadata.getFileHash()); } - @GetMapping("/{id}") // 메타데이터 id를 받아서 상세정보 조회 + @GetMapping("/{id}") public ResponseEntity getMetadata(@PathVariable Long id) { MetadataDto dto = contentService.retrieveMetadata(id); return ResponseEntity.ok(dto); @@ -36,31 +39,33 @@ public ResponseEntity getMetadata(@PathVariable Long id) { @GetMapping("/download/{id}") public ResponseEntity downloadFile(@PathVariable Long id) { - byte[] fileData = contentService.downloadFile(id); + Long userId = userService.getUserIdByUsername(SecurityUtil.getCurrentUsername()); + byte[] fileData = contentService.downloadFile(id, userId); return ResponseEntity.ok() .header("Content-Disposition", "attachment; filename=\"downloaded_file\"") .body(fileData); } - @GetMapping("/user/{userId}") // userid를 기반으로 해당 유저가 등록한 모든 메타데이터 조회 - // Todo: JWT에서 userId 추출해서 사용하게 수정 할 예정 - public ResponseEntity> getUserContent(@PathVariable Long userId) { + @GetMapping("/user") + public ResponseEntity> getUserContent() { + Long userId = userService.getUserIdByUsername(SecurityUtil.getCurrentUsername()); List list = contentService.findAllByUserId(userId); return ResponseEntity.ok(list); } + @PatchMapping("/{id}") - // Todo: JWT에서 userId 추출하여 메타데이터 상 등록된 대상과 같을 경우에만 실행 되게 수정 public ResponseEntity updateMetadataFields( @PathVariable Long id, @RequestBody Map updates ) { - String message = contentService.updateMetadataFields(id, updates); + Long userId = userService.getUserIdByUsername(SecurityUtil.getCurrentUsername()); + String message = contentService.updateMetadataFields(id, updates, userId); return ResponseEntity.ok(message); } + @DeleteMapping("/delete/{metadataId}") - // Todo: JWT 연동 후 실제 유저 아이디로 변경 public ResponseEntity deleteContent(@PathVariable Long metadataId) { - Long userId = 1L; // TODO: JWT 연동 후 실제 유저 아이디로 변경 + Long userId = userService.getUserIdByUsername(SecurityUtil.getCurrentUsername()); contentService.deleteMetadataAndContent(metadataId, userId); return ResponseEntity.ok("메타데이터 및 IPFS 컨텐츠가 성공적으로 삭제되었습니다."); } diff --git a/src/main/java/com/dasom/MemoReal/domain/Capsule/service/ContentService.java b/src/main/java/com/dasom/MemoReal/domain/Capsule/service/ContentService.java index 4fdc4dc..8acad03 100644 --- a/src/main/java/com/dasom/MemoReal/domain/Capsule/service/ContentService.java +++ b/src/main/java/com/dasom/MemoReal/domain/Capsule/service/ContentService.java @@ -26,7 +26,7 @@ public class ContentService { private final MetadataRepository repository; - private final IpfsClient IpfsClient; + private final IpfsClient ipfsClient; public MetadataDto upload(MultipartFile file, ContentUploadRequest request, Long userId) { if (userId == null) { @@ -34,14 +34,11 @@ public MetadataDto upload(MultipartFile file, ContentUploadRequest request, Long } File tempFile = null; try { - // MultipartFile을 임시 파일로 저장 tempFile = File.createTempFile("upload-", file.getOriginalFilename()); file.transferTo(tempFile.toPath()); - // IPFS에 업로드 - IpfsUploadResult ipfsResult = IpfsClient.upload(tempFile); + IpfsUploadResult ipfsResult = ipfsClient.upload(tempFile); - // Metadata 엔티티 생성 및 저장 Metadata metadata = request.toEntity( ipfsResult.getFileName(), file.getContentType(), @@ -51,14 +48,12 @@ public MetadataDto upload(MultipartFile file, ContentUploadRequest request, Long ); repository.save(metadata); - // 저장된 메타데이터를 DTO로 변환 후 반환 return MetadataDto.fromEntity(metadata); } catch (IOException e) { throw new CustomException(ErrorCode.UPLOAD_FAILED, "파일 처리 중 오류 발생: " + e.getMessage()); } finally { - // 업로드 후 임시 파일 삭제 if (tempFile != null && tempFile.exists()) { boolean deleted = tempFile.delete(); if (!deleted) { @@ -68,7 +63,6 @@ public MetadataDto upload(MultipartFile file, ContentUploadRequest request, Long } } - // 메타데이터 조회 public MetadataDto retrieveMetadata(Long id) { Metadata metadata = repository.findById(id) .orElseThrow(() -> new CustomException(ErrorCode.METADATA_NOT_FOUND, "메타데이터를 찾을 수 없습니다. ID: " + id)); @@ -76,11 +70,14 @@ public MetadataDto retrieveMetadata(Long id) { return MetadataDto.fromEntity(metadata); } - // IPFS에서 실제 파일 다운로드 - public byte[] downloadFile(Long id) { + public byte[] downloadFile(Long id, Long userId) { Metadata metadata = repository.findById(id) .orElseThrow(() -> new CustomException(ErrorCode.METADATA_NOT_FOUND, "메타데이터를 찾을 수 없습니다. ID: " + id)); + if (!Objects.equals(metadata.getUserId(), userId)) { + throw new CustomException(ErrorCode.ACCESS_DENIED,"해당 메타데이터의 소유자가 아님"); + } + MetadataDto dto = MetadataDto.fromEntity(metadata); LocalDate accessDate = LocalDate.parse(dto.getAccessCondition()); @@ -88,8 +85,7 @@ public byte[] downloadFile(Long id) { throw new CustomException(ErrorCode.ACCESS_DENIED); } - // IPFS 해시를 이용해 실제 파일 다운로드 - return IpfsClient.download(metadata.getIpfsContentHash()); + return ipfsClient.download(metadata.getIpfsContentHash()); } public List findAllByUserId(Long userId) { @@ -98,7 +94,7 @@ public List findAllByUserId(Long userId) { } List metadataList = repository.findAllByUserId(userId); - if (metadataList == null) { + if (metadataList == null || metadataList.isEmpty()) { throw new CustomException(ErrorCode.METADATA_NOT_FOUND, "해당 유저의 메타데이터를 조회할 수 없습니다. userId: " + userId); } @@ -106,17 +102,19 @@ public List findAllByUserId(Long userId) { .map(MetadataDto::fromEntity) .collect(Collectors.toList()); } - public String updateMetadataFields(Long id, Map updates) { + + public String updateMetadataFields(Long id, Map updates, Long userId) { Metadata metadata = repository.findById(id) .orElseThrow(() -> new CustomException(ErrorCode.METADATA_NOT_FOUND, "메타데이터를 찾을 수 없습니다. ID: " + id)); - // 수정 가능한 필드 목록 + if (!Objects.equals(metadata.getUserId(), userId)) { + throw new CustomException(ErrorCode.ACCESS_DENIED,"메타데이터의 소유자가 아님"); + } + List allowedFields = List.of("filename", "contentType", "title", "description", "category", "tags"); - // 수정 불가능한 필드를 담을 리스트 List ignoredFields = new ArrayList<>(); - // 실제 수정할 필드들 처리 updates.forEach((key, value) -> { if (allowedFields.contains(key)) { switch (key) { @@ -152,6 +150,7 @@ public String updateMetadataFields(Long id, Map updates) { return "수정 완료. 무시된 필드: " + String.join(", ", ignoredFields); } } + public void deleteMetadataAndContent(Long metadataId, Long userId) { Metadata metadata = repository.findById(metadataId) .orElseThrow(() -> new CustomException(ErrorCode.METADATA_NOT_FOUND)); @@ -161,7 +160,7 @@ public void deleteMetadataAndContent(Long metadataId, Long userId) { } try { - IpfsClient.unpinAndGc(metadata.getIpfsContentHash()); + ipfsClient.unpinAndGc(metadata.getIpfsContentHash()); } catch (Exception e) { throw new CustomException(ErrorCode.CONTENT_DELETE_FAILED, "IPFS에서 컨텐츠 삭제 실패: " + e.getMessage()); } From f644983b28e23cef7652b0cccd1ed932f74895c2 Mon Sep 17 00:00:00 2001 From: yjj Date: Sun, 20 Jul 2025 00:05:34 +0900 Subject: [PATCH 06/13] =?UTF-8?q?chore:=20ipfs=20=EC=A3=BC=EC=86=8C=20appl?= =?UTF-8?q?ication.yml=20=EC=97=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 93cfb32..707e9e0 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -18,4 +18,7 @@ spring: ddl-auto: update properties: hibernate: - dialect: org.hibernate.dialect.MySQL8Dialect \ No newline at end of file + dialect: org.hibernate.dialect.MySQL8Dialect + +ipfs: + api-base-url: ${IPFS_API_BASE_URL} \ No newline at end of file From dff09708149cedfd721130907dfb2cdb5c6ba670 Mon Sep 17 00:00:00 2001 From: yjj Date: Sun, 20 Jul 2025 00:06:43 +0900 Subject: [PATCH 07/13] =?UTF-8?q?refactor:=20=EA=B8=B0=EC=A1=B4=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EB=B0=8F=20IPFS=20=EA=B4=80=EB=A0=A8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=A0=84=EC=B2=B4=EC=A0=81=EC=9D=B8=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=EC=97=90=20=EB=A7=9E=EA=B2=8C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Capsule/dto/ContentUploadRequest.java | 1 + .../domain/Capsule/dto/MetadataDto.java | 3 + .../Capsule/service/ContentService.java | 12 +- .../MemoReal/global/ipfs/IpfsClient.java | 142 +++++++++++------- 4 files changed, 99 insertions(+), 59 deletions(-) diff --git a/src/main/java/com/dasom/MemoReal/domain/Capsule/dto/ContentUploadRequest.java b/src/main/java/com/dasom/MemoReal/domain/Capsule/dto/ContentUploadRequest.java index b6f3c7f..ba3d5b1 100644 --- a/src/main/java/com/dasom/MemoReal/domain/Capsule/dto/ContentUploadRequest.java +++ b/src/main/java/com/dasom/MemoReal/domain/Capsule/dto/ContentUploadRequest.java @@ -7,6 +7,7 @@ @Data @NoArgsConstructor +@Builder @AllArgsConstructor public class ContentUploadRequest { diff --git a/src/main/java/com/dasom/MemoReal/domain/Capsule/dto/MetadataDto.java b/src/main/java/com/dasom/MemoReal/domain/Capsule/dto/MetadataDto.java index 8edf70a..19ee30e 100644 --- a/src/main/java/com/dasom/MemoReal/domain/Capsule/dto/MetadataDto.java +++ b/src/main/java/com/dasom/MemoReal/domain/Capsule/dto/MetadataDto.java @@ -9,6 +9,7 @@ @NoArgsConstructor @AllArgsConstructor public class MetadataDto { + private Long id;// 테스트용 필드 private String filename; private String contentType; //확장자 private String title; @@ -21,6 +22,7 @@ public class MetadataDto { public static MetadataDto fromEntity(Metadata metadata) { return new MetadataDto( + metadata.getId(), metadata.getFilename(), metadata.getContentType(), metadata.getTitle(), @@ -32,4 +34,5 @@ public static MetadataDto fromEntity(Metadata metadata) { metadata.getTags() ); } + } diff --git a/src/main/java/com/dasom/MemoReal/domain/Capsule/service/ContentService.java b/src/main/java/com/dasom/MemoReal/domain/Capsule/service/ContentService.java index 8acad03..957e130 100644 --- a/src/main/java/com/dasom/MemoReal/domain/Capsule/service/ContentService.java +++ b/src/main/java/com/dasom/MemoReal/domain/Capsule/service/ContentService.java @@ -37,7 +37,7 @@ public MetadataDto upload(MultipartFile file, ContentUploadRequest request, Long tempFile = File.createTempFile("upload-", file.getOriginalFilename()); file.transferTo(tempFile.toPath()); - IpfsUploadResult ipfsResult = ipfsClient.upload(tempFile); + IpfsUploadResult ipfsResult = ipfsClient.uploadToMfs(tempFile); Metadata metadata = request.toEntity( ipfsResult.getFileName(), @@ -85,9 +85,15 @@ public byte[] downloadFile(Long id, Long userId) { throw new CustomException(ErrorCode.ACCESS_DENIED); } - return ipfsClient.download(metadata.getIpfsContentHash()); + // 현재는 MFS 내 저장된 파일명을 기반으로 다운로드 + // 추후 해시(IPFS content hash) 기반으로 다운로드 기능 개선 예정 + return ipfsClient.downloadFromMfs(metadata.getFilename()); + + // 아래는 해시 기반 다운로드 시 사용 예시 (미구현) + // return ipfsClient.downloadByHash(metadata.getIpfsContentHash()); } + public List findAllByUserId(Long userId) { if (userId == null) { throw new CustomException(ErrorCode.USER_ID_NOT_FOUND); @@ -160,7 +166,7 @@ public void deleteMetadataAndContent(Long metadataId, Long userId) { } try { - ipfsClient.unpinAndGc(metadata.getIpfsContentHash()); + ipfsClient.deleteFromMfs(metadata.getIpfsContentHash()); } catch (Exception e) { throw new CustomException(ErrorCode.CONTENT_DELETE_FAILED, "IPFS에서 컨텐츠 삭제 실패: " + e.getMessage()); } diff --git a/src/main/java/com/dasom/MemoReal/global/ipfs/IpfsClient.java b/src/main/java/com/dasom/MemoReal/global/ipfs/IpfsClient.java index e87805f..3be45a4 100644 --- a/src/main/java/com/dasom/MemoReal/global/ipfs/IpfsClient.java +++ b/src/main/java/com/dasom/MemoReal/global/ipfs/IpfsClient.java @@ -3,6 +3,7 @@ import com.dasom.MemoReal.global.exception.CustomException; import com.dasom.MemoReal.global.exception.ErrorCode; import com.dasom.MemoReal.global.ipfs.dto.IpfsUploadResult; +import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.FileSystemResource; import org.springframework.http.*; import org.springframework.stereotype.Component; @@ -15,93 +16,122 @@ @Component public class IpfsClient { - private static final String IPFS_API_BASE_URL = "http://localhost:5001/api/v0"; + @Value("${ipfs.api-base-url}") + private String ipfsApiBaseUrl; + + private final String MFS_BASE_PATH = "/test"; // MFS 폴더 경로 + + // RestTemplate 재사용용 필드 + private final RestTemplate restTemplate = new RestTemplate(); /** - * IPFS에 파일 업로드 - * @param file 업로드할 파일 - * @return 업로드 결과 객체 (해시, 파일명, 크기) + * MFS의 /test 폴더 아래에 파일 업로드 */ - public IpfsUploadResult upload(File file) { - try { - RestTemplate restTemplate = new RestTemplate(); + public IpfsUploadResult uploadToMfs(File file) { + String mfsPath = MFS_BASE_PATH + "/" + file.getName(); + String url = ipfsApiBaseUrl + "/files/write?arg=" + mfsPath + "&create=true&truncate=true"; - MultiValueMap body = new LinkedMultiValueMap<>(); - body.add("file", new FileSystemResource(file)); + // multipart/form-data 구성 + MultiValueMap body = new LinkedMultiValueMap<>(); + // Resource로 감싸서 전송 + body.add("file", new FileSystemResource(file)); - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.MULTIPART_FORM_DATA); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.MULTIPART_FORM_DATA); - HttpEntity> requestEntity = new HttpEntity<>(body, headers); + HttpEntity> requestEntity = new HttpEntity<>(body, headers); - ResponseEntity response = restTemplate.postForEntity( - IPFS_API_BASE_URL + "/add", - requestEntity, - String.class - ); + ResponseEntity writeResponse = restTemplate.postForEntity(url, requestEntity, String.class); - if (response.getStatusCode().is2xxSuccessful()) { - String bodyStr = response.getBody(); - if (bodyStr != null && bodyStr.contains("Hash")) { - String hash = extractValue(bodyStr, "Hash"); - String name = extractValue(bodyStr, "Name"); - String size = extractValue(bodyStr, "Size"); - return new IpfsUploadResult(hash, name, Long.parseLong(size)); - } - } - throw new CustomException(ErrorCode.UPLOAD_FAILED, "IPFS 응답 실패: " + response.getStatusCode()); - } catch (Exception e) { - throw new CustomException(ErrorCode.UPLOAD_FAILED, "IPFS 요청 실패: " + e.getMessage()); + if (!writeResponse.getStatusCode().is2xxSuccessful()) { + throw new CustomException(ErrorCode.UPLOAD_FAILED, "MFS 파일 쓰기 실패: " + writeResponse.getStatusCode()); + } + + // 파일 상태 조회는 기존 코드 유지 + String statUrl = ipfsApiBaseUrl + "/files/stat?arg=" + mfsPath; + ResponseEntity statResponse = restTemplate.postForEntity(statUrl, null, String.class); + if (!statResponse.getStatusCode().is2xxSuccessful()) { + throw new CustomException(ErrorCode.UPLOAD_FAILED, "MFS 파일 상태 조회 실패: " + statResponse.getStatusCode()); } + + String bodyStr = statResponse.getBody(); + if (bodyStr == null) { + throw new CustomException(ErrorCode.UPLOAD_FAILED, "MFS 파일 상태 응답 없음"); + } + + String hash = extractHashFromJson(bodyStr); + + return new IpfsUploadResult(hash, file.getName(), file.length()); + } /** - * IPFS 해시로부터 파일 데이터 다운로드 - * @param ipfsHash IPFS 파일 해시 - * @return 파일 데이터 바이트 배열 + * MFS 내 /test 폴더에 저장된 파일 다운로드 */ - public byte[] download(String ipfsHash) { + public byte[] downloadFromMfs(String filename) { try { - RestTemplate restTemplate = new RestTemplate(); - String url = IPFS_API_BASE_URL + "/cat?arg=" + ipfsHash; + if (filename == null || filename.trim().isEmpty() || filename.endsWith("/")) { + throw new CustomException(ErrorCode.FILE_NOT_FOUND, "유효하지 않은 파일명입니다: " + filename); + } + + if (!filename.startsWith("/")) { + filename = "/" + filename; + } - ResponseEntity response = restTemplate.getForEntity(url, byte[].class); + String targetPath = MFS_BASE_PATH + filename; // "/test/filename" + + String url = ipfsApiBaseUrl + "/files/read?arg=" + targetPath; + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + HttpEntity entity = new HttpEntity<>("", headers); + + ResponseEntity response = restTemplate.exchange( + url, + HttpMethod.POST, // GET이 아닌 POST로 변경해야 함 + entity, + byte[].class + ); if (response.getStatusCode() == HttpStatus.OK) { return response.getBody(); } else { - throw new CustomException(ErrorCode.FILE_NOT_FOUND, "IPFS 파일 다운로드 실패: " + response.getStatusCode()); + throw new CustomException(ErrorCode.FILE_NOT_FOUND, "MFS 파일 다운로드 실패: " + response.getStatusCode()); } } catch (Exception e) { - throw new CustomException(ErrorCode.FILE_NOT_FOUND, "IPFS 요청 실패: " + e.getMessage()); + throw new CustomException(ErrorCode.FILE_NOT_FOUND, "MFS 요청 실패: " + e.getMessage()); } } - public void unpinAndGc(String ipfsHash) { - try { - ProcessBuilder unpinBuilder = new ProcessBuilder("ipfs", "pin", "rm", ipfsHash); - Process unpin = unpinBuilder.start(); - int unpinExit = unpin.waitFor(); - if (unpinExit != 0) { - throw new RuntimeException("IPFS pin 제거 실패"); - } - ProcessBuilder gcBuilder = new ProcessBuilder("ipfs", "repo", "gc"); - Process gc = gcBuilder.start(); - int gcExit = gc.waitFor(); - if (gcExit != 0) { - throw new RuntimeException("IPFS repo gc 실패"); - } + /** + * MFS 내 /test 폴더에 저장된 파일 삭제 + */ + public void deleteFromMfs(String filename) { + try { + String targetPath = MFS_BASE_PATH + "/" + filename; + String url = ipfsApiBaseUrl + "/files/rm?arg=" + targetPath; + + ResponseEntity response = restTemplate.postForEntity(url, null, String.class); + if (!response.getStatusCode().is2xxSuccessful()) { + throw new CustomException(ErrorCode.CONTENT_DELETE_FAILED, "MFS 파일 삭제 실패: " + response.getStatusCode()); + } } catch (Exception e) { - throw new RuntimeException("IPFS 삭제 과정 중 오류 발생: " + e.getMessage(), e); + throw new CustomException(ErrorCode.CONTENT_DELETE_FAILED, "MFS 삭제 실패: " + e.getMessage()); } } - // 간단한 JSON 값 파싱 - private String extractValue(String json, String key) { - int start = json.indexOf("\"" + key + "\":\"") + key.length() + 4; + + /** + * 간단한 JSON 문자열에서 "Hash" 키의 값을 추출하는 메서드 + */ + private String extractHashFromJson(String json) { + // "Hash":"Qm..." 형태에서 값만 추출 + int start = json.indexOf("\"Hash\":\"") + 7; // "Hash":" 길이 = 7 int end = json.indexOf("\"", start); + if (start < 7 || end < 0) return ""; return json.substring(start, end); } } From a7fc8483af67c698d2e6cb3fdf4af193919c9258 Mon Sep 17 00:00:00 2001 From: yjj Date: Sun, 20 Jul 2025 00:07:44 +0900 Subject: [PATCH 08/13] =?UTF-8?q?test:=20=EC=BA=A1=EC=8A=90=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EA=B8=B0=EB=8A=A5(ex:=20=EC=BB=A8=ED=85=90?= =?UTF-8?q?=EC=B8=A0=20IPFS=20=EC=97=85=EB=A1=9C=EB=93=9C,=20=EB=A9=94?= =?UTF-8?q?=ED=83=80=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=97=85=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=20=EB=B0=8F=20=EC=A1=B0=EC=9E=91=20=EB=93=B1)?= =?UTF-8?q?=EC=97=90=20=EB=8C=80=ED=95=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MemoReal/ContentFullIntegrationTest.java | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 src/test/java/com/dasom/MemoReal/ContentFullIntegrationTest.java diff --git a/src/test/java/com/dasom/MemoReal/ContentFullIntegrationTest.java b/src/test/java/com/dasom/MemoReal/ContentFullIntegrationTest.java new file mode 100644 index 0000000..bd596c5 --- /dev/null +++ b/src/test/java/com/dasom/MemoReal/ContentFullIntegrationTest.java @@ -0,0 +1,147 @@ +package com.dasom.MemoReal; + +import com.dasom.MemoReal.domain.Capsule.dto.ContentUploadRequest; +import com.dasom.MemoReal.domain.Capsule.dto.MetadataDto; +import com.dasom.MemoReal.domain.Capsule.service.ContentService; +import com.dasom.MemoReal.domain.user.entity.User; +import com.dasom.MemoReal.domain.user.repository.UserRepository; +import com.dasom.MemoReal.global.exception.CustomException; +import org.junit.jupiter.api.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +@Transactional +public class ContentFullIntegrationTest { + + private static final Logger logger = LoggerFactory.getLogger(ContentFullIntegrationTest.class); + + @Autowired + private ContentService contentService; + + @Autowired + private UserRepository userRepository; + + private Long testUserId; + private MockMultipartFile testFile; + + @BeforeEach + void setUp() { + User testUser = User.builder() + .username("testuser") + .password("encodedpassword") + .email("testuser@example.com") + .roles(new ArrayList<>() {{ + add("USER"); + }}) + .build(); + userRepository.save(testUser); + testUserId = testUser.getId(); + + testFile = new MockMultipartFile( + "file", + "testfile.txt", + "text/plain", + "This is a test file content.".getBytes() + ); + logger.info("\n"); // 한 줄 띄우기 (테스트 시작 전) + } + + private MetadataDto uploadTestFile() { + ContentUploadRequest request = ContentUploadRequest.builder() + .title("Title") + .description("Desc") + .category("Category") + .tags("tag1,tag2") + .accessCondition(LocalDate.now().toString()) + .build(); + return contentService.upload(testFile, request, testUserId); + } + + @Test + void testUpload() { + logger.info("========== testUpload 시작 =========="); + MetadataDto metadata = uploadTestFile(); + + assertNotNull(metadata); + assertEquals("Title", metadata.getTitle()); + logger.info("testUpload 성공 - 제목: {}", metadata.getTitle()); + logger.info("========== testUpload 종료 ==========\n"); + } + + @Test + void testRetrieveMetadata() { + logger.info("========== testRetrieveMetadata 시작 =========="); + MetadataDto uploaded = uploadTestFile(); + + MetadataDto retrieved = contentService.retrieveMetadata(uploaded.getId()); + assertEquals(uploaded.getTitle(), retrieved.getTitle()); + logger.info("testRetrieveMetadata 성공 - 제목: {}", retrieved.getTitle()); + logger.info("========== testRetrieveMetadata 종료 ==========\n"); + } + + @Test + void testFindAllByUserId() { + logger.info("========== testFindAllByUserId 시작 =========="); + uploadTestFile(); + + List list = contentService.findAllByUserId(testUserId); + assertFalse(list.isEmpty()); + logger.info("testFindAllByUserId 성공 - 메타데이터 수: {}", list.size()); + logger.info("========== testFindAllByUserId 종료 ==========\n"); + } + + @Test + void testDownloadFile() { + logger.info("========== testDownloadFile 시작 =========="); + MetadataDto uploaded = uploadTestFile(); + + byte[] fileData = contentService.downloadFile(uploaded.getId(), testUserId); + assertNotNull(fileData); + assertTrue(fileData.length > 0); + logger.info("testDownloadFile 성공 - 파일 크기: {} bytes", fileData.length); + + // 파일 본문 출력 (텍스트 파일일 경우) + String content = new String(fileData); + logger.info("다운로드된 파일 내용: {}", content); + + logger.info("========== testDownloadFile 종료 ==========\n"); + } + + @Test + void testUpdateMetadataFields() { + logger.info("========== testUpdateMetadataFields 시작 =========="); + MetadataDto uploaded = uploadTestFile(); + + Map updates = Map.of("title", "Updated Title"); + contentService.updateMetadataFields(uploaded.getId(), updates, testUserId); + + MetadataDto updated = contentService.retrieveMetadata(uploaded.getId()); + assertEquals("Updated Title", updated.getTitle()); + logger.info("testUpdateMetadataFields 성공 - 수정된 제목: {}", updated.getTitle()); + logger.info("========== testUpdateMetadataFields 종료 ==========\n"); + } + + @Test + void testDeleteMetadataAndContent() { + logger.info("========== testDeleteMetadataAndContent 시작 =========="); + MetadataDto uploaded = uploadTestFile(); + Long id = uploaded.getId(); + + contentService.deleteMetadataAndContent(id, testUserId); + logger.info("testDeleteMetadataAndContent - 메타데이터 및 컨텐츠 삭제 요청 완료"); + + assertThrows(CustomException.class, () -> contentService.retrieveMetadata(id)); + logger.info("testDeleteMetadataAndContent 성공 - 삭제 후 메타데이터 조회 시 예외 발생 확인"); + logger.info("========== testDeleteMetadataAndContent 종료 ==========\n"); + } +} From c7a490a7f1a34a2e36d88d7daf36cc9a2ed4f351 Mon Sep 17 00:00:00 2001 From: yjj Date: Sun, 20 Jul 2025 21:18:52 +0900 Subject: [PATCH 09/13] =?UTF-8?q?refactor:=20=EA=B8=B0=EC=A1=B4=20mfs?= =?UTF-8?q?=EB=B0=A9=EC=8B=9D=20=EB=8C=80=EC=8B=A0=20=ED=95=B4=EC=8B=9C=20?= =?UTF-8?q?=EA=B8=B0=EB=B0=98=EC=9C=BC=EB=A1=9C=20=EC=9E=91=EB=8F=99?= =?UTF-8?q?=ED=95=98=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DB에 메타데이터로 해시값을 저장함 에 따라 과정, 코드 간결화 하기 위함 + 혹시 모를 오류 방지를 위해 업로드 시 핀 설정은 해제 상태로 업로드(추후 운영단계에서 true로 수정) --- .../Capsule/service/ContentService.java | 31 +--- .../MemoReal/global/exception/ErrorCode.java | 3 +- .../MemoReal/global/ipfs/IpfsClient.java | 141 +++++++----------- .../MemoReal/ContentFullIntegrationTest.java | 16 +- 4 files changed, 76 insertions(+), 115 deletions(-) diff --git a/src/main/java/com/dasom/MemoReal/domain/Capsule/service/ContentService.java b/src/main/java/com/dasom/MemoReal/domain/Capsule/service/ContentService.java index 957e130..31381b6 100644 --- a/src/main/java/com/dasom/MemoReal/domain/Capsule/service/ContentService.java +++ b/src/main/java/com/dasom/MemoReal/domain/Capsule/service/ContentService.java @@ -12,7 +12,6 @@ import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; -import java.io.File; import java.io.IOException; import java.time.LocalDate; import java.util.ArrayList; @@ -32,12 +31,12 @@ public MetadataDto upload(MultipartFile file, ContentUploadRequest request, Long if (userId == null) { throw new CustomException(ErrorCode.USER_ID_NOT_FOUND); } - File tempFile = null; + try { - tempFile = File.createTempFile("upload-", file.getOriginalFilename()); - file.transferTo(tempFile.toPath()); + byte[] contentBytes = file.getBytes(); - IpfsUploadResult ipfsResult = ipfsClient.uploadToMfs(tempFile); + //(pin=false 옵션은 클라이언트 내부 처리) + IpfsUploadResult ipfsResult = ipfsClient.upload(contentBytes, file.getOriginalFilename()); Metadata metadata = request.toEntity( ipfsResult.getFileName(), @@ -52,14 +51,6 @@ public MetadataDto upload(MultipartFile file, ContentUploadRequest request, Long } catch (IOException e) { throw new CustomException(ErrorCode.UPLOAD_FAILED, "파일 처리 중 오류 발생: " + e.getMessage()); - - } finally { - if (tempFile != null && tempFile.exists()) { - boolean deleted = tempFile.delete(); - if (!deleted) { - System.err.println("임시 파일 삭제 실패: " + tempFile.getAbsolutePath()); - } - } } } @@ -78,19 +69,13 @@ public byte[] downloadFile(Long id, Long userId) { throw new CustomException(ErrorCode.ACCESS_DENIED,"해당 메타데이터의 소유자가 아님"); } - MetadataDto dto = MetadataDto.fromEntity(metadata); - - LocalDate accessDate = LocalDate.parse(dto.getAccessCondition()); + LocalDate accessDate = LocalDate.parse(metadata.getAccessCondition()); if (LocalDate.now().isBefore(accessDate)) { throw new CustomException(ErrorCode.ACCESS_DENIED); } - // 현재는 MFS 내 저장된 파일명을 기반으로 다운로드 - // 추후 해시(IPFS content hash) 기반으로 다운로드 기능 개선 예정 - return ipfsClient.downloadFromMfs(metadata.getFilename()); - - // 아래는 해시 기반 다운로드 시 사용 예시 (미구현) - // return ipfsClient.downloadByHash(metadata.getIpfsContentHash()); + // 해시 기반 다운로드 + return ipfsClient.downloadByHash(metadata.getIpfsContentHash()); } @@ -166,7 +151,7 @@ public void deleteMetadataAndContent(Long metadataId, Long userId) { } try { - ipfsClient.deleteFromMfs(metadata.getIpfsContentHash()); + ipfsClient.deleteByHash(metadata.getIpfsContentHash()); } catch (Exception e) { throw new CustomException(ErrorCode.CONTENT_DELETE_FAILED, "IPFS에서 컨텐츠 삭제 실패: " + e.getMessage()); } diff --git a/src/main/java/com/dasom/MemoReal/global/exception/ErrorCode.java b/src/main/java/com/dasom/MemoReal/global/exception/ErrorCode.java index d6d1067..1a6560f 100644 --- a/src/main/java/com/dasom/MemoReal/global/exception/ErrorCode.java +++ b/src/main/java/com/dasom/MemoReal/global/exception/ErrorCode.java @@ -27,7 +27,8 @@ public enum ErrorCode { ACCESS_DENIED("CONTENT_003", HttpStatus.FORBIDDEN, "열람 조건을 만족하지 못했습니다."), UPLOAD_FAILED("CONTENT_004", HttpStatus.INTERNAL_SERVER_ERROR, "컨텐츠 업로드에 실패했습니다."), INVALID_INPUT("CONTENT_005", HttpStatus.BAD_REQUEST, "유효하지 않은 입력 값입니다."), - CONTENT_DELETE_FAILED("CONTENT_006",HttpStatus.INTERNAL_SERVER_ERROR ,"컨텐츠 삭제 실패"); + CONTENT_DELETE_FAILED("CONTENT_006",HttpStatus.INTERNAL_SERVER_ERROR ,"컨텐츠 삭제 실패"), + CONTENT_DOWNLOAD_FAILED("CONTENT_007",HttpStatus.INTERNAL_SERVER_ERROR ,"컨텐츠 다운로드 실패"); private final String code; private final HttpStatus httpStatus; private final String message; diff --git a/src/main/java/com/dasom/MemoReal/global/ipfs/IpfsClient.java b/src/main/java/com/dasom/MemoReal/global/ipfs/IpfsClient.java index 3be45a4..0ab1a31 100644 --- a/src/main/java/com/dasom/MemoReal/global/ipfs/IpfsClient.java +++ b/src/main/java/com/dasom/MemoReal/global/ipfs/IpfsClient.java @@ -3,135 +3,110 @@ import com.dasom.MemoReal.global.exception.CustomException; import com.dasom.MemoReal.global.exception.ErrorCode; import com.dasom.MemoReal.global.ipfs.dto.IpfsUploadResult; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.beans.factory.annotation.Value; -import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.ByteArrayResource; import org.springframework.http.*; import org.springframework.stereotype.Component; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.HttpServerErrorException; import org.springframework.web.client.RestTemplate; -import java.io.File; - @Component public class IpfsClient { @Value("${ipfs.api-base-url}") private String ipfsApiBaseUrl; - private final String MFS_BASE_PATH = "/test"; // MFS 폴더 경로 - - // RestTemplate 재사용용 필드 private final RestTemplate restTemplate = new RestTemplate(); - /** - * MFS의 /test 폴더 아래에 파일 업로드 - */ - public IpfsUploadResult uploadToMfs(File file) { - String mfsPath = MFS_BASE_PATH + "/" + file.getName(); - String url = ipfsApiBaseUrl + "/files/write?arg=" + mfsPath + "&create=true&truncate=true"; + public IpfsUploadResult upload(byte[] content, String filename) { + try { + String url = ipfsApiBaseUrl + "/add?pin=false"; // 테스트 단계에서는 비활성화(추후 운용단계 에서는 핀설정하게 변경) - // multipart/form-data 구성 - MultiValueMap body = new LinkedMultiValueMap<>(); - // Resource로 감싸서 전송 - body.add("file", new FileSystemResource(file)); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.MULTIPART_FORM_DATA); - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.MULTIPART_FORM_DATA); + ByteArrayResource fileAsResource = new ByteArrayResource(content) { + @Override + public String getFilename() { + return filename; + } + }; - HttpEntity> requestEntity = new HttpEntity<>(body, headers); + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("file", fileAsResource); - ResponseEntity writeResponse = restTemplate.postForEntity(url, requestEntity, String.class); + HttpEntity> requestEntity = new HttpEntity<>(body, headers); - if (!writeResponse.getStatusCode().is2xxSuccessful()) { - throw new CustomException(ErrorCode.UPLOAD_FAILED, "MFS 파일 쓰기 실패: " + writeResponse.getStatusCode()); - } + ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class); - // 파일 상태 조회는 기존 코드 유지 - String statUrl = ipfsApiBaseUrl + "/files/stat?arg=" + mfsPath; - ResponseEntity statResponse = restTemplate.postForEntity(statUrl, null, String.class); - if (!statResponse.getStatusCode().is2xxSuccessful()) { - throw new CustomException(ErrorCode.UPLOAD_FAILED, "MFS 파일 상태 조회 실패: " + statResponse.getStatusCode()); - } + if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) { + String responseBody = response.getBody(); + String hash = extractHashFromResponse(responseBody); + return new IpfsUploadResult(hash, filename, content.length); + } else { + throw new CustomException(ErrorCode.UPLOAD_FAILED, "IPFS 업로드 실패: " + response.getStatusCode()); + } - String bodyStr = statResponse.getBody(); - if (bodyStr == null) { - throw new CustomException(ErrorCode.UPLOAD_FAILED, "MFS 파일 상태 응답 없음"); + } catch (Exception e) { + throw new CustomException(ErrorCode.UPLOAD_FAILED, "IPFS 업로드 중 예외 발생: " + e.getMessage()); } - - String hash = extractHashFromJson(bodyStr); - - return new IpfsUploadResult(hash, file.getName(), file.length()); - } - /** - * MFS 내 /test 폴더에 저장된 파일 다운로드 - */ - public byte[] downloadFromMfs(String filename) { + private String extractHashFromResponse(String response) { try { - if (filename == null || filename.trim().isEmpty() || filename.endsWith("/")) { - throw new CustomException(ErrorCode.FILE_NOT_FOUND, "유효하지 않은 파일명입니다: " + filename); - } - - if (!filename.startsWith("/")) { - filename = "/" + filename; + ObjectMapper mapper = new ObjectMapper(); + JsonNode root = mapper.readTree(response); + if (root.has("Hash")) { + return root.get("Hash").asText(); } + } catch (JsonProcessingException e) { + throw new CustomException(ErrorCode.UPLOAD_FAILED, "응답 JSON 파싱 실패: " + e.getMessage()); + } + throw new CustomException(ErrorCode.UPLOAD_FAILED, "응답에서 해시를 추출할 수 없습니다."); + } - String targetPath = MFS_BASE_PATH + filename; // "/test/filename" - - String url = ipfsApiBaseUrl + "/files/read?arg=" + targetPath; + public byte[] downloadByHash(String hash) { + try { + String url = ipfsApiBaseUrl + "/cat?arg=" + hash; HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); - HttpEntity entity = new HttpEntity<>("", headers); + HttpEntity request = new HttpEntity<>(headers); - ResponseEntity response = restTemplate.exchange( - url, - HttpMethod.POST, // GET이 아닌 POST로 변경해야 함 - entity, - byte[].class - ); + ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, request, byte[].class); - if (response.getStatusCode() == HttpStatus.OK) { + if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) { return response.getBody(); } else { - throw new CustomException(ErrorCode.FILE_NOT_FOUND, "MFS 파일 다운로드 실패: " + response.getStatusCode()); + throw new CustomException(ErrorCode.CONTENT_DOWNLOAD_FAILED, "IPFS 다운로드 실패: " + response.getStatusCode()); } } catch (Exception e) { - throw new CustomException(ErrorCode.FILE_NOT_FOUND, "MFS 요청 실패: " + e.getMessage()); + throw new CustomException(ErrorCode.CONTENT_DOWNLOAD_FAILED, "IPFS 해시 다운로드 예외: " + e.getMessage()); } } - - - /** - * MFS 내 /test 폴더에 저장된 파일 삭제 - */ - public void deleteFromMfs(String filename) { + public void deleteByHash(String hash) { try { - String targetPath = MFS_BASE_PATH + "/" + filename; - String url = ipfsApiBaseUrl + "/files/rm?arg=" + targetPath; + String url = ipfsApiBaseUrl + "/pin/rm?arg=" + hash; - ResponseEntity response = restTemplate.postForEntity(url, null, String.class); + restTemplate.postForEntity(url, null, String.class); - if (!response.getStatusCode().is2xxSuccessful()) { - throw new CustomException(ErrorCode.CONTENT_DELETE_FAILED, "MFS 파일 삭제 실패: " + response.getStatusCode()); + } catch (HttpClientErrorException | HttpServerErrorException e) { + String responseBody = e.getResponseBodyAsString(); + if (responseBody.contains("not pinned")) { + // 이미 핀되어 있지 않으면 무시 + return; } + throw new CustomException(ErrorCode.CONTENT_DELETE_FAILED, "IPFS 해시 삭제 예외: " + e.getMessage()); } catch (Exception e) { - throw new CustomException(ErrorCode.CONTENT_DELETE_FAILED, "MFS 삭제 실패: " + e.getMessage()); + throw new CustomException(ErrorCode.CONTENT_DELETE_FAILED, "IPFS 해시 삭제 예외: " + e.getMessage()); } } - - /** - * 간단한 JSON 문자열에서 "Hash" 키의 값을 추출하는 메서드 - */ - private String extractHashFromJson(String json) { - // "Hash":"Qm..." 형태에서 값만 추출 - int start = json.indexOf("\"Hash\":\"") + 7; // "Hash":" 길이 = 7 - int end = json.indexOf("\"", start); - if (start < 7 || end < 0) return ""; - return json.substring(start, end); - } } diff --git a/src/test/java/com/dasom/MemoReal/ContentFullIntegrationTest.java b/src/test/java/com/dasom/MemoReal/ContentFullIntegrationTest.java index bd596c5..95bfd2d 100644 --- a/src/test/java/com/dasom/MemoReal/ContentFullIntegrationTest.java +++ b/src/test/java/com/dasom/MemoReal/ContentFullIntegrationTest.java @@ -73,9 +73,11 @@ void testUpload() { MetadataDto metadata = uploadTestFile(); assertNotNull(metadata); - assertEquals("Title", metadata.getTitle()); - logger.info("testUpload 성공 - 제목: {}", metadata.getTitle()); - logger.info("========== testUpload 종료 ==========\n"); + + // title 대신 IPFS 해시값을 출력 및 비교 + String expectedHash = metadata.getFileHash(); + assertNotNull(expectedHash, "해시 값이 null이면 안됩니다."); + logger.info("testUpload 성공 - IPFS 해시: {}", expectedHash); } @Test @@ -86,18 +88,19 @@ void testRetrieveMetadata() { MetadataDto retrieved = contentService.retrieveMetadata(uploaded.getId()); assertEquals(uploaded.getTitle(), retrieved.getTitle()); logger.info("testRetrieveMetadata 성공 - 제목: {}", retrieved.getTitle()); - logger.info("========== testRetrieveMetadata 종료 ==========\n"); } @Test void testFindAllByUserId() { logger.info("========== testFindAllByUserId 시작 =========="); + // 2개 이상의 콘텐츠 업로드 + uploadTestFile(); uploadTestFile(); List list = contentService.findAllByUserId(testUserId); assertFalse(list.isEmpty()); + assertTrue(list.size() >= 2, "2개 이상의 메타데이터가 조회되어야 합니다."); logger.info("testFindAllByUserId 성공 - 메타데이터 수: {}", list.size()); - logger.info("========== testFindAllByUserId 종료 ==========\n"); } @Test @@ -114,7 +117,6 @@ void testDownloadFile() { String content = new String(fileData); logger.info("다운로드된 파일 내용: {}", content); - logger.info("========== testDownloadFile 종료 ==========\n"); } @Test @@ -128,7 +130,6 @@ void testUpdateMetadataFields() { MetadataDto updated = contentService.retrieveMetadata(uploaded.getId()); assertEquals("Updated Title", updated.getTitle()); logger.info("testUpdateMetadataFields 성공 - 수정된 제목: {}", updated.getTitle()); - logger.info("========== testUpdateMetadataFields 종료 ==========\n"); } @Test @@ -142,6 +143,5 @@ void testDeleteMetadataAndContent() { assertThrows(CustomException.class, () -> contentService.retrieveMetadata(id)); logger.info("testDeleteMetadataAndContent 성공 - 삭제 후 메타데이터 조회 시 예외 발생 확인"); - logger.info("========== testDeleteMetadataAndContent 종료 ==========\n"); } } From e3f1f7accad7422079b0a50fe43f2c5f635ab7dc Mon Sep 17 00:00:00 2001 From: yjj Date: Sun, 20 Jul 2025 21:42:45 +0900 Subject: [PATCH 10/13] =?UTF-8?q?refactor:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=EC=97=90=EC=84=9C=20=EC=9C=A0=EC=A0=80=20?= =?UTF-8?q?id=20=EA=B0=80=EC=A0=B8=EC=98=A4=EB=8A=94=20=EA=B2=83=EC=9D=84?= =?UTF-8?q?=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EA=B8=B0=EB=8A=A5=EC=9D=84=20?= =?UTF-8?q?=EC=9D=B4=EC=9A=A9=ED=95=B4=20jwt=EC=83=9D=EC=84=B1=20=EB=B0=8F?= =?UTF-8?q?=20=ED=95=B4=EB=8B=B9=20jwt=20=EA=B0=80=EA=B3=B5=ED=95=B4?= =?UTF-8?q?=EC=84=9C=20=EA=B0=80=EC=A0=B8=EC=98=A4=EB=8A=94=20=EA=B2=83?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MemoReal/ContentFullIntegrationTest.java | 38 +++++++++++++++---- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/src/test/java/com/dasom/MemoReal/ContentFullIntegrationTest.java b/src/test/java/com/dasom/MemoReal/ContentFullIntegrationTest.java index 95bfd2d..61f1490 100644 --- a/src/test/java/com/dasom/MemoReal/ContentFullIntegrationTest.java +++ b/src/test/java/com/dasom/MemoReal/ContentFullIntegrationTest.java @@ -3,15 +3,20 @@ import com.dasom.MemoReal.domain.Capsule.dto.ContentUploadRequest; import com.dasom.MemoReal.domain.Capsule.dto.MetadataDto; import com.dasom.MemoReal.domain.Capsule.service.ContentService; -import com.dasom.MemoReal.domain.user.entity.User; -import com.dasom.MemoReal.domain.user.repository.UserRepository; +import com.dasom.MemoReal.domain.user.dto.JoinDTO; +import com.dasom.MemoReal.domain.user.service.UserService; import com.dasom.MemoReal.global.exception.CustomException; +import com.dasom.MemoReal.global.jwt.dto.JwtTokenDTO; +import com.dasom.MemoReal.global.jwt.provider.JwtTokenProvider; +import com.dasom.MemoReal.global.security.util.SecurityUtil; import org.junit.jupiter.api.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; @@ -29,33 +34,50 @@ public class ContentFullIntegrationTest { private ContentService contentService; @Autowired - private UserRepository userRepository; + private UserService userService; + + @Autowired + private JwtTokenProvider jwtTokenProvider; private Long testUserId; private MockMultipartFile testFile; @BeforeEach void setUp() { - User testUser = User.builder() + // 1. 회원가입 + JoinDTO joinDTO = JoinDTO.builder() .username("testuser") - .password("encodedpassword") + .password("1234") .email("testuser@example.com") .roles(new ArrayList<>() {{ add("USER"); }}) .build(); - userRepository.save(testUser); - testUserId = testUser.getId(); + userService.join(joinDTO); + + // 2. 로그인하여 JWT 토큰 생성 + JwtTokenDTO tokenDTO = userService.login("testuser@example.com", "1234"); + + // 3. JWT 토큰에서 사용자 인증 설정 + Authentication authentication = jwtTokenProvider.getAuthentication(tokenDTO.getAccessToken()); + SecurityContextHolder.getContext().setAuthentication(authentication); + // 4. SecurityContextHolder에서 유저 이름 추출 + String username = SecurityUtil.getCurrentUsername(); + + // 5. 유저 이름으로부터 ID 조회 + testUserId = userService.getUserIdByUsername(username); + + // 6. 테스트용 파일 생성 testFile = new MockMultipartFile( "file", "testfile.txt", "text/plain", "This is a test file content.".getBytes() ); + logger.info("\n"); // 한 줄 띄우기 (테스트 시작 전) } - private MetadataDto uploadTestFile() { ContentUploadRequest request = ContentUploadRequest.builder() .title("Title") From fd8469386ca87b62b3338fa211af8ca6a06006ae Mon Sep 17 00:00:00 2001 From: yjj Date: Sun, 20 Jul 2025 22:29:50 +0900 Subject: [PATCH 11/13] =?UTF-8?q?Fix:=20=EB=A9=94=ED=83=80=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=88=98=EC=A0=95=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=88=98=EC=A0=95=EB=90=98=EB=A9=B4=20?= =?UTF-8?q?=EC=95=88=EB=90=98=EB=8A=94=20=ED=95=84=EB=93=9C(filename,=20co?= =?UTF-8?q?ntentType)=20=EA=B0=80=20=EC=88=98=EC=A0=95=20=EA=B0=80?= =?UTF-8?q?=EB=8A=A5=ED=95=9C=20=ED=95=84=EB=93=9C=EB=A1=9C=20=EC=A7=80?= =?UTF-8?q?=EC=A0=95=EB=90=9C=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MemoReal/domain/Capsule/service/ContentService.java | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/main/java/com/dasom/MemoReal/domain/Capsule/service/ContentService.java b/src/main/java/com/dasom/MemoReal/domain/Capsule/service/ContentService.java index 31381b6..987bf3a 100644 --- a/src/main/java/com/dasom/MemoReal/domain/Capsule/service/ContentService.java +++ b/src/main/java/com/dasom/MemoReal/domain/Capsule/service/ContentService.java @@ -102,19 +102,13 @@ public String updateMetadataFields(Long id, Map updates, Long us throw new CustomException(ErrorCode.ACCESS_DENIED,"메타데이터의 소유자가 아님"); } - List allowedFields = List.of("filename", "contentType", "title", "description", "category", "tags"); + List allowedFields = List.of("title", "description", "category", "tags"); List ignoredFields = new ArrayList<>(); updates.forEach((key, value) -> { if (allowedFields.contains(key)) { switch (key) { - case "filename": - metadata.setFilename((String) value); - break; - case "contentType": - metadata.setContentType((String) value); - break; case "title": metadata.setTitle((String) value); break; From dd350f292c63463ef48243a7d99e4f3b5a8dbfb4 Mon Sep 17 00:00:00 2001 From: yjj Date: Mon, 21 Jul 2025 00:07:10 +0900 Subject: [PATCH 12/13] =?UTF-8?q?Fix:=20=EA=B9=83=ED=97=88=EB=B8=8C=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=98=A4=EB=A5=98=20=EB=B0=A9?= =?UTF-8?q?=EC=A7=80=EB=A5=BC=20=EC=9C=84=ED=95=B4=20=EC=9E=84=EC=8B=9C?= =?UTF-8?q?=EB=A1=9C=20yml=20=ED=8C=8C=EC=9D=BC=EC=9D=98=20IPFS=20?= =?UTF-8?q?=EC=A3=BC=EC=86=8C=EA=B0=92=EC=9D=84=20=ED=99=98=EA=B2=BD?= =?UTF-8?q?=EB=B3=80=EC=88=98=EA=B0=80=20=EC=95=84=EB=8B=8C=20=ED=95=98?= =?UTF-8?q?=EB=93=9C=EC=BD=94=EB=94=A9=20=EB=B0=A9=EC=8B=9D=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 707e9e0..e0f9d10 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -21,4 +21,5 @@ spring: dialect: org.hibernate.dialect.MySQL8Dialect ipfs: - api-base-url: ${IPFS_API_BASE_URL} \ No newline at end of file + api-base-url: http://127.0.0.1:5001/api/v0 + # ${IPFS_API_BASE_URL} \ No newline at end of file From d9ba8c27301923d0ec2cd83f9468b5f1f4fd83b6 Mon Sep 17 00:00:00 2001 From: yjj Date: Mon, 21 Jul 2025 00:08:05 +0900 Subject: [PATCH 13/13] =?UTF-8?q?test:=20ContentFullIntegrationTest=20?= =?UTF-8?q?=EC=97=90=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=97=85=EB=A1=9C=EB=93=9C=20=EB=B0=8F=20=EB=8B=A4=EC=9A=B4?= =?UTF-8?q?=EB=A1=9C=EB=93=9C=20=ED=95=98=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MemoReal/ContentFullIntegrationTest.java | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/test/java/com/dasom/MemoReal/ContentFullIntegrationTest.java b/src/test/java/com/dasom/MemoReal/ContentFullIntegrationTest.java index 61f1490..e8398bb 100644 --- a/src/test/java/com/dasom/MemoReal/ContentFullIntegrationTest.java +++ b/src/test/java/com/dasom/MemoReal/ContentFullIntegrationTest.java @@ -166,4 +166,49 @@ void testDeleteMetadataAndContent() { assertThrows(CustomException.class, () -> contentService.retrieveMetadata(id)); logger.info("testDeleteMetadataAndContent 성공 - 삭제 후 메타데이터 조회 시 예외 발생 확인"); } + @Test + void testUploadImageFile() { + logger.info("========== testUploadImageFile 시작 =========="); + + // 샘플 이미지 바이너리 (간단한 1픽셀 PNG 이미지) + byte[] imageBytes = new byte[]{ + (byte) 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, + 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, + 0x08, 0x06, 0x00, 0x00, 0x00, 0x1F, 0x15, (byte) 0xC4, (byte) 0x89, + 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, 0x41, 0x54, + 0x78, (byte) 0x9C, 0x63, 0x60, 0x00, 0x00, 0x00, 0x02, + 0x00, 0x01, (byte) 0xE2, 0x21, (byte) 0xBC, 0x33, + 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, + (byte) 0xAE, 0x42, 0x60, (byte) 0x82 + }; + + // 이미지 파일 생성 + MockMultipartFile imageFile = new MockMultipartFile( + "file", + "test-image.png", + "image/png", + imageBytes + ); + + // 요청 정보 생성 + ContentUploadRequest request = ContentUploadRequest.builder() + .title("Test Image") + .description("Image description") + .category("Image") + .tags("image,test") + .accessCondition(LocalDate.now().toString()) + .build(); + + // 업로드 + MetadataDto metadata = contentService.upload(imageFile, request, testUserId); + assertNotNull(metadata); + logger.info("testUploadImageFile 업로드 성공 - IPFS 해시: {}", metadata.getFileHash()); + + // 다운로드 + byte[] downloaded = contentService.downloadFile(metadata.getId(), testUserId); + assertNotNull(downloaded); + assertEquals(imageBytes.length, downloaded.length, "업로드한 이미지와 다운로드한 이미지 크기가 달라야 하지 않습니다."); + logger.info("testUploadImageFile 다운로드 성공 - 바이트 수: {}", downloaded.length); + } }