Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
13 commits
Select commit Hold shift + click to select a range
2fd34f3
feat : 컨트롤러, 서비스, dto, 엔티티, 레포지토리 생성 및 에러코드 추가된 기능에 대응하게 추가 && 로컬 ipf…
junjinyun Jul 18, 2025
f30d4ca
feat : 메타데이터 속성의 값을 수정하는 기능 추가, IPFS의 파일 접근시도 하고 다운받는 기능 추가
junjinyun Jul 18, 2025
d05ede3
feat : 메타데이터와 컨텐츠를 삭제하는 기능 구현
junjinyun Jul 18, 2025
bdd7bfb
feat : UserService 에 username을 이용해서 userid를 추출하는 기능 추가
junjinyun Jul 19, 2025
8ff5580
feat : 컨트롤러에서 SecurityUtil 와 UserService 이용하여 userid 추출하여 서비스에 넘겨주고 이…
junjinyun Jul 19, 2025
f644983
chore: ipfs 주소 application.yml 에 추가
junjinyun Jul 19, 2025
dff0970
refactor: 기존코드 및 IPFS 관련 코드 전체적인 리팩토링 및 테스트 코드에 맞게 수정
junjinyun Jul 19, 2025
a7fc848
test: 캡슐 관련 기능(ex: 컨텐츠 IPFS 업로드, 메타데이터 업로드 및 조작 등)에 대한 테스트 코드 작성
junjinyun Jul 19, 2025
c7a490a
refactor: 기존 mfs방식 대신 해시 기반으로 작동하게 수정
junjinyun Jul 20, 2025
e3f1f7a
refactor: 테스트 코드에서 유저 id 가져오는 것을 로그인 기능을 이용해 jwt생성 및 해당 jwt 가공해서 가져오는…
junjinyun Jul 20, 2025
fd84693
Fix: 메타데이터 수정 기능에서 수정되면 안되는 필드(filename, contentType) 가 수정 가능한 필드로 지정…
junjinyun Jul 20, 2025
dd350f2
Fix: 깃허브 테스트 오류 방지를 위해 임시로 yml 파일의 IPFS 주소값을 환경변수가 아닌 하드코딩 방식으로 변경
junjinyun Jul 20, 2025
d9ba8c2
test: ContentFullIntegrationTest 에 이미지 파일 업로드 및 다운로드 하는 기능 추가
junjinyun Jul 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<String> 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<MetadataDto> 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<List<MetadataDto>> getUserContent() {
Long userId = userService.getUserIdByUsername(SecurityUtil.getCurrentUsername());
List<MetadataDto> list = contentService.findAllByUserId(userId);
return ResponseEntity.ok(list);
}

@PatchMapping("/{id}")
public ResponseEntity<String> updateMetadataFields(
@PathVariable Long id,
@RequestBody Map<String, Object> updates
) {
Long userId = userService.getUserIdByUsername(SecurityUtil.getCurrentUsername());
String message = contentService.updateMetadataFields(id, updates, userId);
return ResponseEntity.ok(message);
}

@DeleteMapping("/delete/{metadataId}")
public ResponseEntity<String> deleteContent(@PathVariable Long metadataId) {
Long userId = userService.getUserIdByUsername(SecurityUtil.getCurrentUsername());
contentService.deleteMetadataAndContent(metadataId, userId);
return ResponseEntity.ok("메타데이터 및 IPFS 컨텐츠가 성공적으로 삭제되었습니다.");
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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()
);
}

}
Original file line number Diff line number Diff line change
@@ -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 테이블과 외래키 연결
}
Original file line number Diff line number Diff line change
@@ -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<Metadata, Long> {
List<Metadata> findAllByUserId(Long userId); // 유저별 메타데이터 조회

}
Original file line number Diff line number Diff line change
@@ -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<MetadataDto> findAllByUserId(Long userId) {
if (userId == null) {
throw new CustomException(ErrorCode.USER_ID_NOT_FOUND);
}
List<Metadata> 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<String, Object> 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<String> allowedFields = List.of("title", "description", "category", "tags");

List<String> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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";
Expand Down
Loading
Loading