Skip to content

Commit 7fb05c1

Browse files
authored
[Refactor/OPS-373] 자료 조회 검색 요구사항 수정 (#108)
* refactor/OPS-373 : 자료 이름 unique 속성 삭제 * refactor/OPS-373 : 자료 수정 가능 칼럼 추가 * refactor/OPS-373 : 명시적 null과 미전달 구분
1 parent 9daa947 commit 7fb05c1

File tree

7 files changed

+333
-76
lines changed

7 files changed

+333
-76
lines changed

build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ dependencies {
8282
// OpenAPI / Swagger UI
8383
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.9'
8484

85+
// OpenAPI - nullable support
86+
implementation "org.openapitools:jackson-databind-nullable:0.2.6"
87+
8588
// JWT
8689
implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
8790
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'

src/main/java/org/tuna/zoopzoop/backend/domain/datasource/controller/DatasourceController.java

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -178,29 +178,48 @@ public ResponseEntity<?> moveMany(
178178

179179
/**
180180
* 파일 수정
181-
* @param dataSourceId 수정할 파일 Id
182-
* @param body 수정할 내용
181+
* - 전달된 필드만 반영 (present)
182+
* - 명시적 null이면 DB에 null 저장
183+
* - 미전달(not present)이면 변경 없음
183184
*/
184185
@Operation(summary = "자료 수정", description = "내 PersonalArchive 안에 자료를 수정합니다.")
185186
@PatchMapping("/{dataSourceId}")
186187
public ResponseEntity<?> updateDataSource(
187188
@PathVariable Integer dataSourceId,
188-
@Valid @RequestBody reqBodyForUpdateDataSource body,
189+
@RequestBody reqBodyForUpdateDataSource body,
189190
@AuthenticationPrincipal CustomUserDetails userDetails
190191
) {
191-
// title, summary 둘 다 비어있으면 의미 없는 요청 → 400
192-
boolean noTitle = (body.title() == null || body.title().isBlank());
193-
boolean noSummary = (body.summary() == null || body.summary().isBlank());
194-
if (noTitle && noSummary) {
195-
throw new IllegalArgumentException("변경할 값이 없습니다. title 또는 summary 중 하나 이상을 전달하세요.");
192+
boolean anyPresent =
193+
body.title().isPresent() ||
194+
body.summary().isPresent() ||
195+
body.sourceUrl().isPresent() ||
196+
body.imageUrl().isPresent() ||
197+
body.source().isPresent() ||
198+
body.tags().isPresent() ||
199+
body.category().isPresent();
200+
201+
if (!anyPresent) {
202+
throw new IllegalArgumentException(
203+
"변경할 값이 없습니다. title, summary, sourceUrl, imageUrl, source, tags, category 중 하나 이상을 전달하세요."
204+
);
196205
}
197206

198-
Member member = userDetails.getMember();
199-
Integer updatedId = dataSourceService.updateDataSource(member.getId(), dataSourceId, body.title(), body.summary()); // CHANGED
200-
String msg = updatedId + "번 자료가 수정됐습니다.";
201-
return ResponseEntity.ok(
202-
new ApiResponse<>(200, msg, new resBodyForUpdateDataSource(updatedId))
207+
Integer updatedId = dataSourceService.updateDataSource(
208+
userDetails.getMember().getId(),
209+
dataSourceId,
210+
DataSourceService.UpdateCommand.builder()
211+
.title(body.title())
212+
.summary(body.summary())
213+
.sourceUrl(body.sourceUrl())
214+
.imageUrl(body.imageUrl())
215+
.source(body.source())
216+
.tags(body.tags())
217+
.category(body.category())
218+
.build()
203219
);
220+
221+
String msg = updatedId + "번 자료가 수정됐습니다.";
222+
return ResponseEntity.ok(new ApiResponse<>(200, msg, new resBodyForUpdateDataSource(updatedId)));
204223
}
205224

206225
/**
Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
package org.tuna.zoopzoop.backend.domain.datasource.dto;
22

3-
import jakarta.validation.constraints.NotNull;
3+
import org.openapitools.jackson.nullable.JsonNullable;
44

55
public record reqBodyForUpdateDataSource(
6-
@NotNull String title,
7-
@NotNull String summary
6+
JsonNullable<String> title,
7+
JsonNullable<String> summary,
8+
JsonNullable<String> sourceUrl,
9+
JsonNullable<String> imageUrl,
10+
JsonNullable<String> source,
11+
JsonNullable<java.util.List<String>> tags,
12+
JsonNullable<String> category
813
) {}

src/main/java/org/tuna/zoopzoop/backend/domain/datasource/entity/DataSource.java

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,45 +15,35 @@
1515
@Setter
1616
@Entity
1717
@NoArgsConstructor
18-
@Table(
19-
uniqueConstraints = {
20-
// 복합 Unique 제약(folder_id, title)
21-
// 같은 폴더 내에 자료 제목 중복 금지
22-
@UniqueConstraint(columnNames = {"folder_id", "title"})
23-
},
24-
// 폴더 내 자료 목록 조회 최적화
25-
indexes = {
26-
@Index(name = "idx_datasource__folder_id", columnList = "folder_id")
27-
}
28-
)
2918
public class DataSource extends BaseEntity {
3019
//연결된 폴더 id
3120
@ManyToOne(fetch = FetchType.LAZY, optional = false)
3221
@JoinColumn(name = "folder_id")
3322
private Folder folder;
3423

3524
//제목
36-
@Column(nullable = false)
25+
@Column
3726
private String title;
3827

3928
//요약
40-
@Column(nullable = false)
29+
@Column
4130
private String summary;
4231

4332
//소스 데이터의 작성일자
4433
//DB 저장용 createdDate와 다름.
45-
@Column(nullable = false)
34+
@Column
4635
private LocalDate dataCreatedDate;
4736

4837
//소스 데이터 URL
49-
@Column(nullable = false)
38+
@Column
5039
private String sourceUrl;
5140

5241
//썸네일 이미지 URL
5342
@Column
5443
private String imageUrl;
5544

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

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

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

6858
// 활성화 여부
69-
@Column(nullable = false)
59+
@Column
7060
private boolean isActive = true;
7161

7262
// 삭제 일자

src/main/java/org/tuna/zoopzoop/backend/domain/datasource/service/DataSourceService.java

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
import jakarta.persistence.NoResultException;
44
import jakarta.transaction.Transactional;
5+
import lombok.Builder;
56
import lombok.RequiredArgsConstructor;
7+
import org.openapitools.jackson.nullable.JsonNullable;
68
import org.springframework.data.domain.Page;
79
import org.springframework.data.domain.Pageable;
810
import org.springframework.stereotype.Service;
@@ -15,6 +17,7 @@
1517
import org.tuna.zoopzoop.backend.domain.datasource.dto.DataSourceDto;
1618
import org.tuna.zoopzoop.backend.domain.datasource.dto.DataSourceSearchCondition;
1719
import org.tuna.zoopzoop.backend.domain.datasource.dto.DataSourceSearchItem;
20+
import org.tuna.zoopzoop.backend.domain.datasource.entity.Category;
1821
import org.tuna.zoopzoop.backend.domain.datasource.entity.DataSource;
1922
import org.tuna.zoopzoop.backend.domain.datasource.entity.Tag;
2023
import org.tuna.zoopzoop.backend.domain.datasource.repository.DataSourceQRepository;
@@ -236,19 +239,50 @@ private Folder resolveTargetFolder(Integer currentMemberId, Integer targetFolder
236239
/**
237240
* 자료 수정
238241
*/
239-
public Integer updateDataSource(Integer memberId, Integer dataSourceId, String newTitle, String newSummary) {
242+
@Builder
243+
public record UpdateCommand(
244+
JsonNullable<String> title,
245+
JsonNullable<String> summary,
246+
JsonNullable<String> sourceUrl,
247+
JsonNullable<String> imageUrl,
248+
JsonNullable<String> source,
249+
JsonNullable<List<String>> tags,
250+
JsonNullable<String> category
251+
) {}
252+
253+
@Transactional
254+
public Integer updateDataSource(Integer memberId, Integer dataSourceId, UpdateCommand cmd) {
240255
DataSource ds = dataSourceRepository.findByIdAndMemberId(dataSourceId, memberId)
241256
.orElseThrow(() -> new NoResultException("존재하지 않는 자료입니다."));
242257

243-
if (newTitle != null && !newTitle.isBlank())
244-
ds.setTitle(newTitle);
245-
246-
if (newSummary != null && !newSummary.isBlank())
247-
ds.setSummary(newSummary);
258+
// 문자열/enum 필드들
259+
if (cmd.title().isPresent()) ds.setTitle(cmd.title().orElse(null));
260+
if (cmd.summary().isPresent()) ds.setSummary(cmd.summary().orElse(null));
261+
if (cmd.sourceUrl().isPresent()) ds.setSourceUrl(cmd.sourceUrl().orElse(null));
262+
if (cmd.imageUrl().isPresent()) ds.setImageUrl(cmd.imageUrl().orElse(null));
263+
if (cmd.source().isPresent()) ds.setSource(cmd.source().orElse(null));
264+
if (cmd.category().isPresent()) ds.setCategory(parseCategoryNullable(cmd.category().orElse(null)));
265+
266+
// 태그
267+
if (cmd.tags().isPresent()) {
268+
List<String> names = cmd.tags().orElse(null);
269+
if (names == null) {
270+
ds.getTags().clear();
271+
} else {
272+
replaceTags(ds, names);
273+
}
274+
}
248275

249276
return ds.getId();
250277
}
251278

279+
private Category parseCategoryNullable(String raw) {
280+
if (raw == null) return null;
281+
String k = raw.trim();
282+
if (k.isEmpty()) return null; // 빈문자 들어오면 null로 저장(원하면 그대로 저장하도록 바꿔도 됨)
283+
return Category.valueOf(k.toUpperCase(Locale.ROOT));
284+
}
285+
252286
/**
253287
* 자료 검색
254288
*/
@@ -272,4 +306,17 @@ public Page<DataSourceSearchItem> search(Integer memberId, DataSourceSearchCondi
272306
}
273307

274308
public record MoveResult(Integer datasourceId, Integer folderId) {}
309+
310+
private void replaceTags(DataSource ds, List<String> names) {
311+
ds.getTags().clear();
312+
313+
for (String name : names) {
314+
if (name == null) continue;
315+
Tag tag = Tag.builder()
316+
.tagName(name)
317+
.dataSource(ds)
318+
.build();
319+
ds.getTags().add(tag);
320+
}
321+
}
275322
}

0 commit comments

Comments
 (0)