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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ dependencies {
// OpenAPI / Swagger UI
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.9'

// OpenAPI - nullable support
implementation "org.openapitools:jackson-databind-nullable:0.2.6"

// JWT
implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,29 +178,48 @@ public ResponseEntity<?> moveMany(

/**
* 파일 수정
* @param dataSourceId 수정할 파일 Id
* @param body 수정할 내용
* - 전달된 필드만 반영 (present)
* - 명시적 null이면 DB에 null 저장
* - 미전달(not present)이면 변경 없음
*/
@Operation(summary = "자료 수정", description = "내 PersonalArchive 안에 자료를 수정합니다.")
@PatchMapping("/{dataSourceId}")
public ResponseEntity<?> updateDataSource(
@PathVariable Integer dataSourceId,
@Valid @RequestBody reqBodyForUpdateDataSource body,
@RequestBody reqBodyForUpdateDataSource body,
@AuthenticationPrincipal CustomUserDetails userDetails
) {
// title, summary 둘 다 비어있으면 의미 없는 요청 → 400
boolean noTitle = (body.title() == null || body.title().isBlank());
boolean noSummary = (body.summary() == null || body.summary().isBlank());
if (noTitle && noSummary) {
throw new IllegalArgumentException("변경할 값이 없습니다. title 또는 summary 중 하나 이상을 전달하세요.");
boolean anyPresent =
body.title().isPresent() ||
body.summary().isPresent() ||
body.sourceUrl().isPresent() ||
body.imageUrl().isPresent() ||
body.source().isPresent() ||
body.tags().isPresent() ||
body.category().isPresent();

if (!anyPresent) {
throw new IllegalArgumentException(
"변경할 값이 없습니다. title, summary, sourceUrl, imageUrl, source, tags, category 중 하나 이상을 전달하세요."
);
}

Member member = userDetails.getMember();
Integer updatedId = dataSourceService.updateDataSource(member.getId(), dataSourceId, body.title(), body.summary()); // CHANGED
String msg = updatedId + "번 자료가 수정됐습니다.";
return ResponseEntity.ok(
new ApiResponse<>(200, msg, new resBodyForUpdateDataSource(updatedId))
Integer updatedId = dataSourceService.updateDataSource(
userDetails.getMember().getId(),
dataSourceId,
DataSourceService.UpdateCommand.builder()
.title(body.title())
.summary(body.summary())
.sourceUrl(body.sourceUrl())
.imageUrl(body.imageUrl())
.source(body.source())
.tags(body.tags())
.category(body.category())
.build()
);

String msg = updatedId + "번 자료가 수정됐습니다.";
return ResponseEntity.ok(new ApiResponse<>(200, msg, new resBodyForUpdateDataSource(updatedId)));
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
package org.tuna.zoopzoop.backend.domain.datasource.dto;

import jakarta.validation.constraints.NotNull;
import org.openapitools.jackson.nullable.JsonNullable;

public record reqBodyForUpdateDataSource(
@NotNull String title,
@NotNull String summary
JsonNullable<String> title,
JsonNullable<String> summary,
JsonNullable<String> sourceUrl,
JsonNullable<String> imageUrl,
JsonNullable<String> source,
JsonNullable<java.util.List<String>> tags,
JsonNullable<String> category
) {}
Original file line number Diff line number Diff line change
Expand Up @@ -15,45 +15,35 @@
@Setter
@Entity
@NoArgsConstructor
@Table(
uniqueConstraints = {
// 복합 Unique 제약(folder_id, title)
// 같은 폴더 내에 자료 제목 중복 금지
@UniqueConstraint(columnNames = {"folder_id", "title"})
},
// 폴더 내 자료 목록 조회 최적화
indexes = {
@Index(name = "idx_datasource__folder_id", columnList = "folder_id")
}
)
public class DataSource extends BaseEntity {
//연결된 폴더 id
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "folder_id")
private Folder folder;

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

//요약
@Column(nullable = false)
@Column
private String summary;

//소스 데이터의 작성일자
//DB 저장용 createdDate와 다름.
@Column(nullable = false)
@Column
private LocalDate dataCreatedDate;

//소스 데이터 URL
@Column(nullable = false)
@Column
private String sourceUrl;

//썸네일 이미지 URL
@Column
private String imageUrl;

// 자료 출처 (동아일보, Tstory 등등)
@Column
private String source;

// 태그 목록
Expand All @@ -62,11 +52,11 @@ public class DataSource extends BaseEntity {

// 카테고리 목록
@Enumerated(EnumType.STRING) // IT, SCIENCE 등 ENUM 이름으로 저장
@Column(nullable = false)
@Column
private Category category;

// 활성화 여부
@Column(nullable = false)
@Column
private boolean isActive = true;

// 삭제 일자
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import jakarta.persistence.NoResultException;
import jakarta.transaction.Transactional;
import lombok.Builder;
import lombok.RequiredArgsConstructor;
import org.openapitools.jackson.nullable.JsonNullable;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
Expand All @@ -15,6 +17,7 @@
import org.tuna.zoopzoop.backend.domain.datasource.dto.DataSourceDto;
import org.tuna.zoopzoop.backend.domain.datasource.dto.DataSourceSearchCondition;
import org.tuna.zoopzoop.backend.domain.datasource.dto.DataSourceSearchItem;
import org.tuna.zoopzoop.backend.domain.datasource.entity.Category;
import org.tuna.zoopzoop.backend.domain.datasource.entity.DataSource;
import org.tuna.zoopzoop.backend.domain.datasource.entity.Tag;
import org.tuna.zoopzoop.backend.domain.datasource.repository.DataSourceQRepository;
Expand Down Expand Up @@ -236,19 +239,50 @@ private Folder resolveTargetFolder(Integer currentMemberId, Integer targetFolder
/**
* 자료 수정
*/
public Integer updateDataSource(Integer memberId, Integer dataSourceId, String newTitle, String newSummary) {
@Builder
public record UpdateCommand(
JsonNullable<String> title,
JsonNullable<String> summary,
JsonNullable<String> sourceUrl,
JsonNullable<String> imageUrl,
JsonNullable<String> source,
JsonNullable<List<String>> tags,
JsonNullable<String> category
) {}

@Transactional
public Integer updateDataSource(Integer memberId, Integer dataSourceId, UpdateCommand cmd) {
DataSource ds = dataSourceRepository.findByIdAndMemberId(dataSourceId, memberId)
.orElseThrow(() -> new NoResultException("존재하지 않는 자료입니다."));

if (newTitle != null && !newTitle.isBlank())
ds.setTitle(newTitle);

if (newSummary != null && !newSummary.isBlank())
ds.setSummary(newSummary);
// 문자열/enum 필드들
if (cmd.title().isPresent()) ds.setTitle(cmd.title().orElse(null));
if (cmd.summary().isPresent()) ds.setSummary(cmd.summary().orElse(null));
if (cmd.sourceUrl().isPresent()) ds.setSourceUrl(cmd.sourceUrl().orElse(null));
if (cmd.imageUrl().isPresent()) ds.setImageUrl(cmd.imageUrl().orElse(null));
if (cmd.source().isPresent()) ds.setSource(cmd.source().orElse(null));
if (cmd.category().isPresent()) ds.setCategory(parseCategoryNullable(cmd.category().orElse(null)));

// 태그
if (cmd.tags().isPresent()) {
List<String> names = cmd.tags().orElse(null);
if (names == null) {
ds.getTags().clear();
} else {
replaceTags(ds, names);
}
}

return ds.getId();
}

private Category parseCategoryNullable(String raw) {
if (raw == null) return null;
String k = raw.trim();
if (k.isEmpty()) return null; // 빈문자 들어오면 null로 저장(원하면 그대로 저장하도록 바꿔도 됨)
return Category.valueOf(k.toUpperCase(Locale.ROOT));
}

/**
* 자료 검색
*/
Expand All @@ -272,4 +306,17 @@ public Page<DataSourceSearchItem> search(Integer memberId, DataSourceSearchCondi
}

public record MoveResult(Integer datasourceId, Integer folderId) {}

private void replaceTags(DataSource ds, List<String> names) {
ds.getTags().clear();

for (String name : names) {
if (name == null) continue;
Tag tag = Tag.builder()
.tagName(name)
.dataSource(ds)
.build();
ds.getTags().add(tag);
}
}
}
Loading