Skip to content

Commit 929eeef

Browse files
committed
refactor/OPS-373 : 자료 수정 가능 칼럼 추가
1 parent 4a0a73e commit 929eeef

File tree

5 files changed

+239
-45
lines changed

5 files changed

+239
-45
lines changed

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

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -189,18 +189,42 @@ public ResponseEntity<?> updateDataSource(
189189
@AuthenticationPrincipal CustomUserDetails userDetails
190190
) {
191191
// 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() != null) ||
194+
(body.summary() != null) ||
195+
(body.sourceUrl() != null) ||
196+
(body.imageUrl() != null) ||
197+
(body.source() != null) ||
198+
(body.tags() != null) ||
199+
(body.category() != null);
200+
201+
if (!anyPresent) {
202+
throw new IllegalArgumentException(
203+
"변경할 값이 없습니다. title, summary, sourceUrl, imageUrl, source, tags, category 중 하나 이상을 전달하세요."
204+
);
205+
}
206+
207+
// 비즈니스 규칙: sourceUrl은 엔티티 nullable=false 이므로, 빈 문자열로 업데이트 요청 시 400
208+
if (body.sourceUrl() != null && body.sourceUrl().isBlank()) {
209+
throw new IllegalArgumentException("sourceUrl은 빈 값일 수 없습니다.");
196210
}
197211

198212
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))
213+
214+
Integer updatedId = dataSourceService.updateDataSource(
215+
member.getId(),
216+
dataSourceId,
217+
body.title(),
218+
body.summary(),
219+
body.sourceUrl(),
220+
body.imageUrl(),
221+
body.source(),
222+
body.tags(),
223+
body.category()
203224
);
225+
226+
String msg = updatedId + "번 자료가 수정됐습니다.";
227+
return ResponseEntity.ok(new ApiResponse<>(200, msg, new resBodyForUpdateDataSource(updatedId)));
204228
}
205229

206230
/**
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 java.util.List;
44

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

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

Lines changed: 73 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import org.springframework.data.domain.Page;
77
import org.springframework.data.domain.Pageable;
88
import org.springframework.stereotype.Service;
9+
import org.springframework.util.StringUtils;
910
import org.tuna.zoopzoop.backend.domain.archive.archive.entity.PersonalArchive;
1011
import org.tuna.zoopzoop.backend.domain.archive.archive.repository.PersonalArchiveRepository;
1112
import org.tuna.zoopzoop.backend.domain.archive.folder.entity.Folder;
@@ -15,6 +16,7 @@
1516
import org.tuna.zoopzoop.backend.domain.datasource.dto.DataSourceDto;
1617
import org.tuna.zoopzoop.backend.domain.datasource.dto.DataSourceSearchCondition;
1718
import org.tuna.zoopzoop.backend.domain.datasource.dto.DataSourceSearchItem;
19+
import org.tuna.zoopzoop.backend.domain.datasource.entity.Category;
1820
import org.tuna.zoopzoop.backend.domain.datasource.entity.DataSource;
1921
import org.tuna.zoopzoop.backend.domain.datasource.entity.Tag;
2022
import org.tuna.zoopzoop.backend.domain.datasource.repository.DataSourceQRepository;
@@ -236,19 +238,85 @@ private Folder resolveTargetFolder(Integer currentMemberId, Integer targetFolder
236238
/**
237239
* 자료 수정
238240
*/
239-
public Integer updateDataSource(Integer memberId, Integer dataSourceId, String newTitle, String newSummary) {
241+
@Transactional
242+
public Integer updateDataSource(
243+
Integer memberId,
244+
Integer dataSourceId,
245+
String newTitle,
246+
String newSummary,
247+
String newSourceUrl,
248+
String newImageUrl,
249+
String newSource,
250+
List<String> newTags,
251+
String newCategory // enum 이름 string
252+
) {
240253
DataSource ds = dataSourceRepository.findByIdAndMemberId(dataSourceId, memberId)
241254
.orElseThrow(() -> new NoResultException("존재하지 않는 자료입니다."));
242255

243-
if (newTitle != null && !newTitle.isBlank())
244-
ds.setTitle(newTitle);
256+
if (StringUtils.hasText(newTitle))
257+
ds.setTitle(newTitle.trim());
258+
259+
if (StringUtils.hasText(newSummary))
260+
ds.setSummary(newSummary.trim());
245261

246-
if (newSummary != null && !newSummary.isBlank())
247-
ds.setSummary(newSummary);
262+
if (newSourceUrl != null) {
263+
if (!StringUtils.hasText(newSourceUrl))
264+
throw new IllegalArgumentException("sourceUrl은 빈 값일 수 없습니다.");
265+
266+
ds.setSourceUrl(newSourceUrl.trim());
267+
}
268+
269+
if (newImageUrl != null) {
270+
String v = newImageUrl.trim();
271+
ds.setImageUrl(v.isEmpty() ? null : v);
272+
}
273+
274+
if (newSource != null) {
275+
String v = newSource.trim();
276+
ds.setSource(v.isEmpty() ? null : v);
277+
}
278+
279+
if (newCategory != null) {
280+
ds.setCategory(parseCategoryOrThrow(newCategory));
281+
}
282+
283+
if (newTags != null) {
284+
replaceTags(ds, newTags);
285+
}
248286

249287
return ds.getId();
250288
}
251289

290+
private Category parseCategoryOrThrow(String raw) {
291+
String key = raw.trim();
292+
if (key.isEmpty()) {
293+
throw new IllegalArgumentException("category는 빈 값일 수 없습니다.");
294+
}
295+
// 대소문자 관대 처리 (IT, Science, science 등)
296+
try {
297+
return Category.valueOf(key.toUpperCase(Locale.ROOT));
298+
} catch (IllegalArgumentException e) {
299+
throw new IllegalArgumentException("지원하지 않는 category 값입니다: " + raw);
300+
}
301+
}
302+
303+
private void replaceTags(DataSource ds, List<String> names) {
304+
ds.getTags().clear();
305+
306+
if (names == null) return;
307+
308+
for (String name : names) {
309+
if (name == null || name.isBlank()) continue;
310+
311+
Tag tag = Tag.builder()
312+
.tagName(name.trim())
313+
.dataSource(ds)
314+
.build();
315+
316+
ds.getTags().add(tag);
317+
}
318+
}
319+
252320
/**
253321
* 자료 검색
254322
*/

src/test/java/org/tuna/zoopzoop/backend/domain/datasource/controller/DatasourceControllerTest.java

Lines changed: 56 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -446,48 +446,89 @@ void moveMany_default_ok() throws Exception {
446446
}
447447

448448
// 자료 수정
449+
450+
private String updateJson(
451+
String title, String summary, String sourceUrl,
452+
String imageUrl, String source, List<String> tags, String category
453+
) throws Exception {
454+
var map = new java.util.LinkedHashMap<String, Object>();
455+
if (title != null) map.put("title", title);
456+
if (summary != null) map.put("summary", summary);
457+
if (sourceUrl != null) map.put("sourceUrl", sourceUrl);
458+
if (imageUrl != null) map.put("imageUrl", imageUrl);
459+
if (source != null) map.put("source", source);
460+
if (tags != null) map.put("tags", tags);
461+
if (category != null) map.put("category", category);
462+
return objectMapper.writeValueAsString(map);
463+
}
464+
449465
@Test
450-
@DisplayName("자료 수정 성공 -> 200")
466+
@DisplayName("자료 수정 성공: title+summary만 부분 수정 → 200")
451467
@WithUserDetails(value = "KAKAO:testUser_sc1111", setupBefore = TestExecutionEvent.TEST_METHOD)
452-
void update_ok() throws Exception {
453-
var body = new reqBodyForUpdateDataSource("새 제목", "짧은 요약");
468+
void update_ok_title_summary_only() throws Exception {
469+
String body = updateJson("새 제목", "짧은 요약", null, null, null, null, null);
454470

455471
mockMvc.perform(patch("/api/v1/archive/{dataSourceId}", dataSourceId1)
456472
.contentType(MediaType.APPLICATION_JSON)
457-
.content(objectMapper.writeValueAsString(body)))
473+
.content(body))
458474
.andExpect(status().isOk())
459475
.andExpect(jsonPath("$.status").value(200))
460476
.andExpect(jsonPath("$.msg").exists())
461477
.andExpect(jsonPath("$.data.dataSourceId").value(dataSourceId1));
462478
}
463479

464480
@Test
465-
@DisplayName("자료 수정 실패: 요청 바디가 모두 공백 -> 400")
481+
@DisplayName("자료 수정 성공: 확장 필드 전부(대소문자 category 허용, imageUrl='', source='') → 200")
466482
@WithUserDetails(value = "KAKAO:testUser_sc1111", setupBefore = TestExecutionEvent.TEST_METHOD)
467-
void update_badRequest_whenEmpty() throws Exception {
468-
var body = new reqBodyForUpdateDataSource(" ", null);
483+
void update_ok_all_fields() throws Exception {
484+
String body = updateJson(
485+
"T2", // title
486+
"S2", // summary
487+
"https://new.src", // sourceUrl
488+
"", // imageUrl
489+
"", // source
490+
List.of("A","B"), // tags 리스트
491+
"science" // category
492+
);
469493

470494
mockMvc.perform(patch("/api/v1/archive/{dataSourceId}", dataSourceId1)
471495
.contentType(MediaType.APPLICATION_JSON)
472-
.content(objectMapper.writeValueAsString(body)))
496+
.content(body))
497+
.andExpect(status().isOk())
498+
.andExpect(jsonPath("$.status").value(200))
499+
.andExpect(jsonPath("$.data.dataSourceId").value(dataSourceId1));
500+
}
501+
502+
503+
@Test
504+
@DisplayName("자료 수정 실패: sourceUrl가 빈 문자열 → 400")
505+
@WithUserDetails(value = "KAKAO:testUser_sc1111", setupBefore = TestExecutionEvent.TEST_METHOD)
506+
void update_badRequest_sourceUrl_blank() throws Exception {
507+
String body = updateJson(null, null, " ", null, null, null, null);
508+
509+
mockMvc.perform(patch("/api/v1/archive/{dataSourceId}", dataSourceId1)
510+
.contentType(MediaType.APPLICATION_JSON)
511+
.content(body))
473512
.andExpect(status().isBadRequest())
474513
.andExpect(jsonPath("$.status").value(400))
475514
.andExpect(jsonPath("$.msg").exists());
476515
}
477516

478517
@Test
479-
@DisplayName("자료 수정 실패: 존재하지 않는 자료 -> 404")
518+
@DisplayName("자료 수정 실패: 모든 필드 미전달(null) → 400")
480519
@WithUserDetails(value = "KAKAO:testUser_sc1111", setupBefore = TestExecutionEvent.TEST_METHOD)
481-
void update_notFound() throws Exception {
482-
var body = new reqBodyForUpdateDataSource("제목", "요약");
520+
void update_badRequest_all_null() throws Exception {
521+
// 빈 JSON 객체 {}
522+
String body = "{}";
483523

484-
mockMvc.perform(patch("/api/v1/archive/{dataSourceId}", 999999)
524+
mockMvc.perform(patch("/api/v1/archive/{dataSourceId}", dataSourceId1)
485525
.contentType(MediaType.APPLICATION_JSON)
486-
.content(objectMapper.writeValueAsString(body)))
487-
.andExpect(status().isNotFound())
488-
.andExpect(jsonPath("$.status").value(404));
526+
.content(body))
527+
.andExpect(status().isBadRequest())
528+
.andExpect(jsonPath("$.status").value(400));
489529
}
490530

531+
491532
// 검색
492533
@Test
493534
@DisplayName("검색 성공: page, size, dataCreatedDate DESC 기본정렬")

src/test/java/org/tuna/zoopzoop/backend/domain/datasource/service/DataSourceServiceTest.java

Lines changed: 70 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,6 @@ void collectDistinctTagsOfFolder_success() {
241241
when(tagRepository.findDistinctTagNamesByFolderId(eq(folderId)))
242242
.thenReturn(List.of("AI", "Spring", "JPA"));
243243

244-
@SuppressWarnings("unchecked")
245244
List<Tag> ctxTags = ReflectionTestUtils.invokeMethod(
246245
dataSourceService, "collectDistinctTagsOfFolder", folderId
247246
);
@@ -681,33 +680,90 @@ void moveMany_default_withDuplicatedIds_illegalArgument() {
681680
}
682681

683682
// 자료 수정
684-
@Test
685-
@DisplayName("수정 성공: 제목과 요약 일부/전체 변경")
686-
void update_ok() {
687-
Integer memberId = 3;
683+
private DataSource baseDs(Folder folder) {
688684
DataSource ds = new DataSource();
689-
ReflectionTestUtils.setField(ds, "id", 7);
690-
ds.setTitle("old");
691-
ds.setSummary("old sum");
685+
ds.setFolder(folder);
686+
ds.setTitle("old-title");
687+
ds.setSummary("old-summary");
688+
ds.setSourceUrl("http://old.src");
689+
ds.setImageUrl("http://old.img");
690+
ds.setSource("old-source");
691+
ds.setCategory(Category.IT);
692+
ds.setActive(true);
693+
ds.setTags(new java.util.ArrayList<>(List.of(new Tag("x"), new Tag("y"))));
694+
// 태그의 dataSource 역방향 세팅
695+
ds.getTags().forEach(t -> t.setDataSource(ds));
696+
ReflectionTestUtils.setField(ds, "id", 77);
697+
return ds;
698+
}
699+
700+
701+
@Test
702+
@DisplayName("수정 성공: 확장 필드 정상 반영(imageUrl '', source '' 처리, category case-insensitive, tags 전량 교체)")
703+
void update_expand_all_fields_ok() {
704+
Integer memberId = 1;
705+
Folder f = new Folder("f"); ReflectionTestUtils.setField(f, "id", 10);
706+
DataSource ds = baseDs(f);
707+
708+
when(dataSourceRepository.findByIdAndMemberId(eq(77), eq(memberId)))
709+
.thenReturn(Optional.of(ds));
710+
711+
Integer id = dataSourceService.updateDataSource(
712+
memberId,
713+
77,
714+
"new-title", // title
715+
"new-summary", // summary
716+
"https://new.src", // sourceUrl
717+
"", // imageUrl -> null 처리
718+
"", // source -> null 처리
719+
List.of("A","B"), // tags 교체
720+
"science" // category (대소문자 허용)
721+
);
692722

693-
when(dataSourceRepository.findByIdAndMemberId(eq(7), eq(memberId)))
723+
assertThat(id).isEqualTo(77);
724+
assertThat(ds.getTitle()).isEqualTo("new-title");
725+
assertThat(ds.getSummary()).isEqualTo("new-summary");
726+
assertThat(ds.getSourceUrl()).isEqualTo("https://new.src");
727+
assertThat(ds.getImageUrl()).isNull(); // '' → null
728+
assertThat(ds.getSource()).isNull(); // '' → null
729+
assertThat(ds.getCategory()).isEqualTo(Category.SCIENCE);
730+
731+
assertThat(ds.getTags()).extracting(Tag::getTagName)
732+
.containsExactlyInAnyOrder("A","B");
733+
assertThat(ds.getTags().stream().allMatch(t -> t.getDataSource() == ds)).isTrue();
734+
}
735+
736+
@Test
737+
@DisplayName("수정: tags=[] → 모든 태그 삭제")
738+
void update_tags_empty_clear() {
739+
Integer memberId = 1;
740+
Folder f = new Folder("f"); ReflectionTestUtils.setField(f, "id", 10);
741+
DataSource ds = baseDs(f);
742+
743+
when(dataSourceRepository.findByIdAndMemberId(eq(77), eq(memberId)))
694744
.thenReturn(Optional.of(ds));
695745

696-
Integer id = dataSourceService.updateDataSource(memberId, 7, "new", null);
746+
dataSourceService.updateDataSource(
747+
memberId, 77,
748+
null, null, null, null, null,
749+
List.of(), // 빈 리스트
750+
null
751+
);
697752

698-
assertThat(id).isEqualTo(7);
699-
assertThat(ds.getTitle()).isEqualTo("new");
700-
assertThat(ds.getSummary()).isEqualTo("old sum"); // summary 미전달 → 유지
753+
assertThat(ds.getTags()).isEmpty();
701754
}
702755

756+
757+
758+
703759
@Test
704760
@DisplayName("수정 실패: 존재하지 않는 자료")
705761
void update_notFound() {
706762
Integer memberId = 3;
707763
when(dataSourceRepository.findByIdAndMemberId(anyInt(), eq(memberId)))
708764
.thenReturn(Optional.empty());
709765

710-
assertThatThrownBy(() -> dataSourceService.updateDataSource(memberId, 1, "t", "s"))
766+
assertThatThrownBy(() -> dataSourceService.updateDataSource(memberId, 123, null, null, null, null, null, null, null))
711767
.isInstanceOf(NoResultException.class)
712768
.hasMessageContaining("존재하지 않는 자료");
713769
}

0 commit comments

Comments
 (0)