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..0c25778 --- /dev/null +++ b/src/main/java/com/dasom/MemoReal/domain/Capsule/controller/ContentController.java @@ -0,0 +1,72 @@ +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 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.*; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; +import java.util.Map; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/content") +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 = userService.getUserIdByUsername(SecurityUtil.getCurrentUsername()); + MetadataDto savedMetadata = contentService.upload(file, metadata, userId); + return ResponseEntity.ok("정상적으로 업로드 되었습니다. 파일 해시: " + savedMetadata.getFileHash()); + } + + @GetMapping("/{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) { + 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") + public ResponseEntity> getUserContent() { + Long userId = userService.getUserIdByUsername(SecurityUtil.getCurrentUsername()); + List list = contentService.findAllByUserId(userId); + return ResponseEntity.ok(list); + } + + @PatchMapping("/{id}") + public ResponseEntity updateMetadataFields( + @PathVariable Long id, + @RequestBody Map updates + ) { + Long userId = userService.getUserIdByUsername(SecurityUtil.getCurrentUsername()); + String message = contentService.updateMetadataFields(id, updates, userId); + return ResponseEntity.ok(message); + } + + @DeleteMapping("/delete/{metadataId}") + public ResponseEntity deleteContent(@PathVariable Long metadataId) { + 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/dto/ContentUploadRequest.java b/src/main/java/com/dasom/MemoReal/domain/Capsule/dto/ContentUploadRequest.java new file mode 100644 index 0000000..ba3d5b1 --- /dev/null +++ b/src/main/java/com/dasom/MemoReal/domain/Capsule/dto/ContentUploadRequest.java @@ -0,0 +1,35 @@ +package com.dasom.MemoReal.domain.Capsule.dto; + +import com.dasom.MemoReal.domain.Capsule.entity.Metadata; +import lombok.*; + +import java.time.LocalDate; + +@Data +@NoArgsConstructor +@Builder +@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..19ee30e --- /dev/null +++ b/src/main/java/com/dasom/MemoReal/domain/Capsule/dto/MetadataDto.java @@ -0,0 +1,38 @@ +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 Long id;// 테스트용 필드 + 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.getId(), + 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..987bf3a --- /dev/null +++ b/src/main/java/com/dasom/MemoReal/domain/Capsule/service/ContentService.java @@ -0,0 +1,155 @@ +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.IpfsClient; +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.IOException; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class ContentService { + + private final MetadataRepository repository; + private final IpfsClient ipfsClient; + + public MetadataDto upload(MultipartFile file, ContentUploadRequest request, Long userId) { + if (userId == null) { + throw new CustomException(ErrorCode.USER_ID_NOT_FOUND); + } + + try { + byte[] contentBytes = file.getBytes(); + + //(pin=false 옵션은 클라이언트 내부 처리) + IpfsUploadResult ipfsResult = ipfsClient.upload(contentBytes, file.getOriginalFilename()); + + Metadata metadata = request.toEntity( + ipfsResult.getFileName(), + file.getContentType(), + ipfsResult.getHash(), + LocalDate.now(), + userId + ); + repository.save(metadata); + + return MetadataDto.fromEntity(metadata); + + } catch (IOException e) { + throw new CustomException(ErrorCode.UPLOAD_FAILED, "파일 처리 중 오류 발생: " + e.getMessage()); + } + } + + public MetadataDto retrieveMetadata(Long id) { + Metadata metadata = repository.findById(id) + .orElseThrow(() -> new CustomException(ErrorCode.METADATA_NOT_FOUND, "메타데이터를 찾을 수 없습니다. ID: " + id)); + + return MetadataDto.fromEntity(metadata); + } + + 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,"해당 메타데이터의 소유자가 아님"); + } + + LocalDate accessDate = LocalDate.parse(metadata.getAccessCondition()); + if (LocalDate.now().isBefore(accessDate)) { + throw new CustomException(ErrorCode.ACCESS_DENIED); + } + + // 해시 기반 다운로드 + return ipfsClient.downloadByHash(metadata.getIpfsContentHash()); + } + + + public List findAllByUserId(Long userId) { + if (userId == null) { + throw new CustomException(ErrorCode.USER_ID_NOT_FOUND); + } + List metadataList = repository.findAllByUserId(userId); + + if (metadataList == null || metadataList.isEmpty()) { + throw new CustomException(ErrorCode.METADATA_NOT_FOUND, + "해당 유저의 메타데이터를 조회할 수 없습니다. userId: " + userId); + } + return metadataList.stream() + .map(MetadataDto::fromEntity) + .collect(Collectors.toList()); + } + + 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("title", "description", "category", "tags"); + + List ignoredFields = new ArrayList<>(); + + updates.forEach((key, value) -> { + if (allowedFields.contains(key)) { + switch (key) { + 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); + } + } + + 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.deleteByHash(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/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"; 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..1a6560f 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,8 +19,16 @@ 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, "유효하지 않은 입력 값입니다."), + 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; @@ -29,4 +38,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/IpfsClient.java b/src/main/java/com/dasom/MemoReal/global/ipfs/IpfsClient.java new file mode 100644 index 0000000..0ab1a31 --- /dev/null +++ b/src/main/java/com/dasom/MemoReal/global/ipfs/IpfsClient.java @@ -0,0 +1,112 @@ +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 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.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; + +@Component +public class IpfsClient { + + @Value("${ipfs.api-base-url}") + private String ipfsApiBaseUrl; + + private final RestTemplate restTemplate = new RestTemplate(); + + public IpfsUploadResult upload(byte[] content, String filename) { + try { + String url = ipfsApiBaseUrl + "/add?pin=false"; // 테스트 단계에서는 비활성화(추후 운용단계 에서는 핀설정하게 변경) + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.MULTIPART_FORM_DATA); + + ByteArrayResource fileAsResource = new ByteArrayResource(content) { + @Override + public String getFilename() { + return filename; + } + }; + + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("file", fileAsResource); + + HttpEntity> requestEntity = new HttpEntity<>(body, headers); + + ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class); + + 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()); + } + + } catch (Exception e) { + throw new CustomException(ErrorCode.UPLOAD_FAILED, "IPFS 업로드 중 예외 발생: " + e.getMessage()); + } + } + + private String extractHashFromResponse(String response) { + try { + 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, "응답에서 해시를 추출할 수 없습니다."); + } + + public byte[] downloadByHash(String hash) { + try { + String url = ipfsApiBaseUrl + "/cat?arg=" + hash; + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + HttpEntity request = new HttpEntity<>(headers); + + ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, request, byte[].class); + + if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) { + return response.getBody(); + } else { + throw new CustomException(ErrorCode.CONTENT_DOWNLOAD_FAILED, "IPFS 다운로드 실패: " + response.getStatusCode()); + } + } catch (Exception e) { + throw new CustomException(ErrorCode.CONTENT_DOWNLOAD_FAILED, "IPFS 해시 다운로드 예외: " + e.getMessage()); + } + } + + public void deleteByHash(String hash) { + try { + String url = ipfsApiBaseUrl + "/pin/rm?arg=" + hash; + + restTemplate.postForEntity(url, null, String.class); + + } 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, "IPFS 해시 삭제 예외: " + e.getMessage()); + } + } +} 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; +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 93cfb32..e0f9d10 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -18,4 +18,8 @@ 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: http://127.0.0.1:5001/api/v0 + # ${IPFS_API_BASE_URL} \ No newline at end of file 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..e8398bb --- /dev/null +++ b/src/test/java/com/dasom/MemoReal/ContentFullIntegrationTest.java @@ -0,0 +1,214 @@ +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.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; +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 UserService userService; + + @Autowired + private JwtTokenProvider jwtTokenProvider; + + private Long testUserId; + private MockMultipartFile testFile; + + @BeforeEach + void setUp() { + // 1. 회원가입 + JoinDTO joinDTO = JoinDTO.builder() + .username("testuser") + .password("1234") + .email("testuser@example.com") + .roles(new ArrayList<>() {{ + add("USER"); + }}) + .build(); + 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") + .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); + + // title 대신 IPFS 해시값을 출력 및 비교 + String expectedHash = metadata.getFileHash(); + assertNotNull(expectedHash, "해시 값이 null이면 안됩니다."); + logger.info("testUpload 성공 - IPFS 해시: {}", expectedHash); + } + + @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()); + } + + @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()); + } + + @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); + + } + + @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()); + } + + @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 성공 - 삭제 후 메타데이터 조회 시 예외 발생 확인"); + } + @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); + } +}