Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 13 additions & 28 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.5.0'
id 'org.springframework.boot' version '3.5.3'
id 'io.spring.dependency-management' version '1.1.7'
}

group = 'com.dasom'
group = 'DASOM'
version = '0.0.1-SNAPSHOT'

java {
Expand All @@ -13,46 +13,31 @@ java {
}
}

configurations {
compileOnly {
extendsFrom annotationProcessor
}
}

repositories {
mavenCentral()
maven { url 'https://jitpack.io' } // IPFS는 JitPack을 통해 배포됨.
}

dependencies {
// Security
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'

//JWT
implementation 'io.jsonwebtoken:jjwt-api:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.5'


implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
// implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'mysql:mysql-connector-java:8.0.33'

// lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

// swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.9'

// IPFS
implementation 'com.github.ipfs:java-ipfs-http-client:1.4.4'
}

tasks.named('test') {
useJUnitPlatform()
}

bootJar {
enabled = true
}

jar {
enabled = false
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.dasom.MemoReal.domain.capsule.config;

import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SwaggerConfig {

@Bean
public OpenAPI openAPI() {
return new OpenAPI()
.components(new Components())
.info(new Info()
.title("API 문서")
.description("Swagger를 활용한 API 문서입니다.")
.version("1.0.0")
);
}

@Bean
public GroupedOpenApi publicApi() {
return GroupedOpenApi.builder()
.group("v1-definition")
.pathsToMatch("/**")
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.dasom.MemoReal.domain.capsule.controller;

import com.dasom.MemoReal.domain.capsule.dto.CapsuleCreateRequestDto;
import com.dasom.MemoReal.domain.capsule.dto.CapsuleUpdateRequestDto;
import com.dasom.MemoReal.domain.capsule.entity.Capsule;
import com.dasom.MemoReal.domain.capsule.service.CapsuleService;
import com.dasom.MemoReal.domain.ipfs.service.IpfsService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

@RestController
@RequestMapping("/capsules")
@RequiredArgsConstructor
public class CapsuleController {

private final CapsuleService capsuleService;
private final IpfsService ipfsService;
private final ObjectMapper objectMapper;

@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<Capsule> createCapsule(
@RequestPart("data") String data,
// 수동 데이터 입력
// {"capsuleType":"일반","title":"제목","content":"내용","openTime":"2025-07-20T14:03:42.989Z"}
@RequestPart(value = "media", required = false) MultipartFile mediaFile
) throws JsonProcessingException {
CapsuleCreateRequestDto dto = objectMapper.readValue(data, CapsuleCreateRequestDto.class);

String cid = (mediaFile != null && !mediaFile.isEmpty()) ? ipfsService.uploadFile(mediaFile) : null;

Capsule saved = capsuleService.createCapsule(dto, cid);
return ResponseEntity.status(HttpStatus.CREATED).body(saved);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@PostMapping
public ResponseEntity createCapsule(@ModelAttribute InputDto dto);

InputDto {
MultipartFile meiaFile;
String data;
}

@ModelAttribute annotion을 사용해 String과 MultipartFile 동시에 받을 수 있음

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ModelAttribute 라는 어노테이션 사용하여 컨트롤러 코드 구조 변경해봤습니다 감사합니당.

@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public ResponseEntity<CapsuleDto.Select> createCapsule(@ModelAttribute CapsuleDto.Create requestDto) {
        CapsuleDto.Select created = capsuleService.createCapsule(requestDto);
        return ResponseEntity.status(HttpStatus.CREATED).body(created);
    }

구조 변경하면서 Dto도 기존에는 기능별로 나뉘어져 있었는데 구조 합쳐서 다시 만들었습니다

public class CapsuleDto {

    // 캡슐 생성
    @Getter @Setter
    @NoArgsConstructor
    @AllArgsConstructor
    public static class Create {
        private String capsuleType;
        private String title;
        private String content;
        private Date openTime;

        // 이미지 & 동영상
        private MultipartFile mediaFile;
    }

    // 캡슐 조회
    @Getter @Setter
    @NoArgsConstructor
    @AllArgsConstructor
    public static class Select {
        private Long casuleId;
        private String capsuleType;
        private String title;
        private String content;
        private Date openTime;
    }

    // 캡슐 수정
    @Getter @Setter
    @NoArgsConstructor
    @AllArgsConstructor
    public static class Update {
        private String capsuleType;
        private String title;
        private String content;
        private Date openTime;
    }
}

이런식으로 변경했는데, 어노테이션이 겹치는 부분이 너무 많은데 괜찮은거 맞을까용;;;
리뷰 감사합니다. 배워갑니당 >0<


@GetMapping("/{id}")
public ResponseEntity<Capsule> getCapsuleById(@PathVariable Long id) {
Capsule capsule = capsuleService.selectCapsuleById(id);
return (capsule != null)
? ResponseEntity.ok(capsule)
: ResponseEntity.notFound().build();
}

@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteCapsule(@PathVariable Long id) {
capsuleService.deleteCapsule(id);
return ResponseEntity.noContent().build();
}

@PatchMapping(value = "/{id}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<Void> updateCapsule(
@PathVariable("id") Long id,
@RequestPart("data") String data,
// 수동 데이터 입력
// {"capsuleType":"일반","title":"제목","content":"내용","openTime":"2025-07-20T14:03:42.989Z"}
@RequestPart(value = "media", required = false) MultipartFile mediaFile
) throws JsonProcessingException {

CapsuleUpdateRequestDto dto = objectMapper.readValue(data, CapsuleUpdateRequestDto.class);

String newCid = null;
if (mediaFile != null && !mediaFile.isEmpty()) {
newCid = ipfsService.uploadFile(mediaFile);
}

capsuleService.updateCapsule(id, dto, newCid);
return ResponseEntity.ok().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.dasom.MemoReal.domain.capsule.dto;

import lombok.Getter;
import lombok.Setter;

import java.util.Date;

@Getter
@Setter
public class CapsuleCreateRequestDto {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dto class의 경우
spring boot에 의해 json parsing되는 일이 많음으로
@NoArgStructure을 꼭 붙여준다 (없을시 기본 생성되지만 명시하는게 좋음)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 부분 반영하여 코드 수정하겠습니다. 감사합니다 :)

private String capsuleType;
private String title;
private String content;
private Date openTime;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.dasom.MemoReal.domain.capsule.dto;

import lombok.Getter;

import java.util.Date;

public class CapsuleSelectResponseDto {

@Getter
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Getter는 dto column에 두지 말고
class위에 둘것

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

앗; 알겠습니다. 바로 반영하겠습니다. 감사합니당


private Long capsuleId;
private String capsuleType;
private String capsuleTitle;
private String capsuleContent;
private Date openTime;

public CapsuleSelectResponseDto(Long capsuleId) {
this.capsuleId = capsuleId;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.dasom.MemoReal.domain.capsule.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;

import java.util.Date;

@Getter
@Setter
@RequiredArgsConstructor
@AllArgsConstructor
public class CapsuleUpdateRequestDto {

// 캡슐 ID는 따로 요청하지 않습니다. * URL을 통해 어떤 ID를 수정할지 알 수 있기 떄문에
// private Long capsuleId;

private String capsuleType;
private String title;
private String content;
private Date openTime;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.dasom.MemoReal.domain.capsule.entity;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.util.Date;

@Entity
@Getter // 테스트 시 필요
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Capsule {

// 캡슐 아이디
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long capsuleId;

// 캡슐 종류
private String capsuleType;

// 제목
private String title;

// 내용
private String content;

// 캡슐 오픈일
private Date openTime;

// IPFS CID 필드 ( 이미지 / 동영상 )
private String mediaCid;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

column 마다
@column annotion붙이기

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


// 캡슐 아이디
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long capsuleId;

  // 캡슐 종류
  @Column(nullable = false)
  private String capsuleType;

  // 제목
  @Column(nullable = false)
  private String title;

  // 내용
  @Column
  private String content;

  // 캡슐 오픈일
  @Column
  private Date openTime;

  // IPFS CID 필드 ( 이미지 / 동영상 )
  @Column
  private String mediaCid;

엔티티 구조도 @column 사용하여 변경했습니당. 감사합니다 :):)

// 생성자 (id 제외)
public Capsule(String capsuleType, String title, String content, Date openTime) {
this.capsuleType = capsuleType;
this.title = title;
this.content = content;
this.openTime = openTime;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.dasom.MemoReal.domain.capsule.repository;

import com.dasom.MemoReal.domain.capsule.entity.Capsule;
import org.springframework.data.jpa.repository.JpaRepository;

public interface CapsuleRepository extends JpaRepository<Capsule, Long> {

// MySQL과 연동

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.dasom.MemoReal.domain.capsule.service;

import com.dasom.MemoReal.domain.capsule.dto.CapsuleCreateRequestDto;
import com.dasom.MemoReal.domain.capsule.dto.CapsuleUpdateRequestDto;
import com.dasom.MemoReal.domain.capsule.entity.Capsule;
import com.dasom.MemoReal.domain.capsule.repository.CapsuleRepository;
import com.dasom.MemoReal.global.exception.CustomException;
import com.dasom.MemoReal.global.exception.ErrorCode;
import jakarta.persistence.EntityNotFoundException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class CapsuleService {

private final CapsuleRepository capsuleRepository;

public Capsule createCapsule(CapsuleCreateRequestDto dto, String mediaCid) {
Capsule capsule = new Capsule();
capsule.setCapsuleType(dto.getCapsuleType());
capsule.setTitle(dto.getTitle());
capsule.setContent(dto.getContent());
capsule.setOpenTime(dto.getOpenTime());
capsule.setMediaCid(mediaCid);

return capsuleRepository.save(capsule);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createCapule 함수의 위치가 올바르지 않음

  1. 확장중 다른 service에서 CapuleService에 의존성을 가진다면 createCapules함수에도 의존성을 가지게 됨
  2. dto -> entity 함수는 대부분 dto에 의존성을 가짐으로
    dto로 옮기는게 좋음

@Getter
@Setter
@NoArgStructor
class Dto {
int id; String col;
public Entity toEntity() { return Entity.builder(). .. .build(); } // dto -> entity 함수
public static Dto of(Entity entity) { return new Dto(); } // entity -> dto 함수
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분은 아직 완벽하게 이해를 못해서 좀 더 알아보고 수정하여 다시 코멘트 올리겠습니다!


public Capsule selectCapsuleById(Long id) {
return capsuleRepository.findById(id).orElse(null);
}

@Transactional
public void deleteCapsule(Long capsuleId) {
Capsule capsule = capsuleRepository.findById(capsuleId)
.orElseThrow(() -> new CustomException(ErrorCode.CAPSULE_NOT_FOUND));

capsuleRepository.delete(capsule);
}

@Transactional
public void updateCapsule(Long id, CapsuleUpdateRequestDto dto, String newCid) {
Capsule capsule = capsuleRepository.findById(id)
.orElseThrow(() -> new EntityNotFoundException("캡슐 없음"));

capsule.setCapsuleType(dto.getCapsuleType());
capsule.setTitle(dto.getTitle());
capsule.setContent(dto.getContent());
capsule.setOpenTime(dto.getOpenTime());

if (newCid != null) {
capsule.setMediaCid(newCid); // 새로운 이미지로 대체
}

capsuleRepository.save(capsule);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.dasom.MemoReal.domain.ipfs.config;

import io.ipfs.api.IPFS;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class IpfsConfig {

@Bean
public IPFS ipfs() {
// 로컬 IPFS 노드 (기본 포트 5001)
return new IPFS("/ip4/127.0.0.1/tcp/5001");
}
}
Loading