Skip to content

Commit e6b7df9

Browse files
authored
[feat/OPS-365 ] soft delete + 휴지통 조회 구현 (#105)
* feat/OPS-365 : soft delete + 휴지통 조회 구현 * refactor/OPS-246 : 요구사항 리팩토링
1 parent 709a2af commit e6b7df9

File tree

18 files changed

+459
-87
lines changed

18 files changed

+459
-87
lines changed

src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/controller/FolderController.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,9 @@ public ResponseEntity<Map<String, Object>> deleteFolder(
6060
) {
6161
if (folderId == 0) {
6262
var body = new java.util.HashMap<String, Object>();
63-
body.put("status", 400);
63+
body.put("status", 409);
6464
body.put("msg", "default 폴더는 삭제할 수 없습니다.");
65-
body.put("data", null); // HashMap은 null 허용
65+
body.put("data", null);
6666
return ResponseEntity.badRequest().body(body);
6767
}
6868

@@ -72,7 +72,7 @@ public ResponseEntity<Map<String, Object>> deleteFolder(
7272
var body = new java.util.HashMap<String, Object>();
7373
body.put("status", 200);
7474
body.put("msg", deletedFolderName + " 폴더가 삭제됐습니다.");
75-
body.put("data", null); // <- 여기도 Map.of 쓰면 NPE 납니다
75+
body.put("data", null);
7676
return ResponseEntity.ok(body);
7777
}
7878

src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/entity/Folder.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ public class Folder extends BaseEntity {
4242
@Column(nullable = false, name = "is_default")
4343
private boolean isDefault = false;
4444

45-
// 폴더 삭제 시 데이터 일괄 삭제
46-
@OneToMany(mappedBy = "folder", cascade = CascadeType.REMOVE, orphanRemoval = true)
45+
// 폴더 삭제 시 데이터 softdelete
46+
@OneToMany(mappedBy = "folder")
4747
private List<DataSource> dataSources = new ArrayList<>();
4848

4949

src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/repository/FolderRepository.java

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@ public interface FolderRepository extends JpaRepository<Folder, Integer>{
1717
* @param filenameEnd "파일명 + \ufffff"
1818
*/
1919
@Query("""
20-
select f.name
21-
from Folder f
22-
where f.archive.id = :archiveId
23-
and f.name >= :filename
24-
and f.name < :filenameEnd
25-
""")
20+
select f.name
21+
from Folder f
22+
where f.archive.id = :archiveId
23+
and f.name >= :filename
24+
and f.name < :filenameEnd
25+
""")
2626
List<String> findNamesForConflictCheck(@Param("archiveId") Integer archiveId,
2727
@Param("filename") String filename,
2828
@Param("filenameEnd") String filenameEnd);
@@ -40,28 +40,36 @@ List<String> findNamesForConflictCheck(@Param("archiveId") Integer archiveId,
4040
* @param memberId 조회할 회원 Id
4141
*/
4242
@Query("""
43-
select f
44-
from Folder f
45-
join f.archive a
46-
join PersonalArchive pa on pa.archive = a
47-
where pa.member.id = :memberId
48-
and f.isDefault = true
49-
""")
43+
select f
44+
from Folder f
45+
join f.archive a
46+
join PersonalArchive pa on pa.archive = a
47+
where pa.member.id = :memberId
48+
and f.isDefault = true
49+
""")
5050
Optional<Folder> findDefaultFolderByMemberId(@Param("memberId") Integer memberId);
5151

5252
// 한 번의 조인으로 존재 + 소유권(memberId) 검증
5353
@Query("""
54-
select f
55-
from Folder f
56-
join f.archive a
57-
join PersonalArchive pa on pa.archive = a
58-
where f.id = :folderId
59-
and pa.member.id = :memberId
60-
""")
54+
select f
55+
from Folder f
56+
join f.archive a
57+
join PersonalArchive pa on pa.archive = a
58+
where f.id = :folderId
59+
and pa.member.id = :memberId
60+
""")
6161
Optional<Folder> findByIdAndMemberId(@Param("folderId") Integer folderId,
6262
@Param("memberId") Integer memberId);
6363

6464
Optional<Folder> findByArchiveIdAndName(Integer archiveId, String name);
6565

6666
List<Folder> findAllByArchiveId(Integer archiveId);
67+
68+
@Query("""
69+
select f from Folder f
70+
join f.archive a
71+
join PersonalArchive pa on pa.archive.id = a.id
72+
where pa.member.id = :memberId and f.isDefault = true
73+
""")
74+
Optional<Folder> findDefaultByMemberId(@Param("memberId") Integer memberId);
6775
}

src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/service/FolderService.java

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@
1313
import org.tuna.zoopzoop.backend.domain.archive.folder.repository.FolderRepository;
1414
import org.tuna.zoopzoop.backend.domain.datasource.dto.FileSummary;
1515
import org.tuna.zoopzoop.backend.domain.datasource.dto.FolderFilesDto;
16+
import org.tuna.zoopzoop.backend.domain.datasource.entity.DataSource;
17+
import org.tuna.zoopzoop.backend.domain.datasource.entity.Tag;
1618
import org.tuna.zoopzoop.backend.domain.datasource.repository.DataSourceRepository;
1719
import org.tuna.zoopzoop.backend.domain.member.entity.Member;
1820
import org.tuna.zoopzoop.backend.domain.member.repository.MemberRepository;
19-
import org.tuna.zoopzoop.backend.domain.datasource.entity.Tag;
2021

22+
import java.time.LocalDate;
2123
import java.util.HashSet;
2224
import java.util.List;
2325
import java.util.Set;
@@ -111,20 +113,33 @@ private static String pickNextAvailable(String file, List<String> existing) {
111113
}
112114

113115
/**
114-
* folderId에 해당하는 폴더 삭제
115-
* soft delete 아직 구현 X
116+
* folderId에 해당하는 폴더 영구 삭제
116117
*/
117118
@Transactional
118119
public String deleteFolder(Integer currentId, Integer folderId) {
119-
// 공격자에게 리소스 존재 여부를 노출 X (존재하지 않음 / 남의 폴더)
120+
// 소유한 폴더인지 확인
120121
Folder folder = folderRepository.findByIdAndMemberId(folderId, currentId)
121122
.orElseThrow(() -> new NoResultException("존재하지 않는 폴더입니다."));
122123

123-
if (folder.isDefault())
124+
if (folder.isDefault()) {
124125
throw new IllegalArgumentException("default 폴더는 삭제할 수 없습니다.");
126+
}
127+
128+
Folder defaultFolder = folderRepository.findDefaultByMemberId(currentId)
129+
.orElseThrow(() -> new IllegalStateException("default 폴더가 존재하지 않습니다."));
130+
131+
// 폴더 내 자료들을 Default로 이관 + soft delete
132+
List<DataSource> dataSources = dataSourceRepository.findAllByFolderId(folderId);
133+
LocalDate now = LocalDate.now();
134+
for (DataSource ds : dataSources) {
135+
ds.setFolder(defaultFolder);
136+
ds.setActive(false);
137+
ds.setDeletedAt(now);
138+
}
125139

126140
String name = folder.getName();
127141
folderRepository.delete(folder);
142+
128143
return name;
129144
}
130145

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

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public ResponseEntity<?> createDataSource(
5151
}
5252

5353
/**
54-
* 자료 단건 삭제
54+
* 자료 단건 완전 삭제
5555
*/
5656
@Operation(summary = "자료 단건 삭제", description = "내 PersonalArchive 안에 자료를 단건 삭제합니다.")
5757
@DeleteMapping("/{dataSourceId}")
@@ -71,7 +71,7 @@ public ResponseEntity<Map<String, Object>> delete(
7171
}
7272

7373
/**
74-
* 자료 다건 삭제
74+
* 자료 다건 완전 삭제
7575
*/
7676
@Operation(summary = "자료 다건 삭제", description = "내 PersonalArchive 안에 자료를 다건 삭제합니다.")
7777
@PostMapping("/delete")
@@ -90,6 +90,38 @@ public ResponseEntity<Map<String, Object>> deleteMany(
9090
return ResponseEntity.ok(res);
9191
}
9292

93+
/**
94+
* 자료 다건 소프트 삭제
95+
*/
96+
@Operation(summary = "자료 다건 임시 삭제", description = "내 PersonalArchive 안에 자료들을 임시 삭제합니다.")
97+
@PatchMapping("/soft-delete")
98+
public ResponseEntity<?> softDelete(
99+
@RequestBody @Valid IdsRequest req,
100+
@AuthenticationPrincipal CustomUserDetails user) {
101+
102+
int cnt = dataSourceService.softDelete(user.getMember().getId(), req.ids());
103+
Map<String, Object> res = new LinkedHashMap<>();
104+
res.put("status", 200);
105+
res.put("msg", "자료들이 임시 삭제됐습니다.");
106+
res.put("data", null);
107+
return ResponseEntity.ok(res);
108+
}
109+
/**
110+
* 자료 다건 복원
111+
*/
112+
@Operation(summary = "자료 다건 복원", description = "내 PersonalArchive 안에 자료들을 복원합니다.")
113+
@PatchMapping("/restore")
114+
public ResponseEntity<?> restore(
115+
@RequestBody @Valid IdsRequest req,
116+
@AuthenticationPrincipal CustomUserDetails user) {
117+
118+
int cnt = dataSourceService.restore(user.getMember().getId(), req.ids());
119+
Map<String, Object> res = new LinkedHashMap<>();
120+
res.put("status", 200);
121+
res.put("msg", "자료들이 복구됐습니다.");
122+
res.put("data", null);
123+
return ResponseEntity.ok(res);
124+
}
93125
/**
94126
* 자료 단건 이동
95127
* folderId=null 이면 default 폴더
@@ -172,15 +204,17 @@ public ResponseEntity<?> updateDataSource(
172204
}
173205

174206
/**
175-
* 자료 검색
207+
* 자료 검색
176208
*/
177209
@Operation(summary = "자료 검색", description = "내 PersonalArchive 안에 자료들을 검색합니다.")
178210
@GetMapping("")
179211
public ResponseEntity<?> search(
180212
@RequestParam(required = false) String title,
181213
@RequestParam(required = false) String summary,
182214
@RequestParam(required = false) String category,
215+
@RequestParam(required = false) Integer folderId,
183216
@RequestParam(required = false) String folderName,
217+
@RequestParam(required = false, defaultValue = "true") Boolean isActive,
184218
@PageableDefault(size = 8, sort = "createdAt", direction = Sort.Direction.DESC)
185219
Pageable pageable,
186220
@AuthenticationPrincipal CustomUserDetails userDetails
@@ -190,8 +224,10 @@ public ResponseEntity<?> search(
190224
DataSourceSearchCondition cond = DataSourceSearchCondition.builder()
191225
.title(title)
192226
.summary(summary)
193-
.folderName(folderName)
194227
.category(category)
228+
.folderId(folderId)
229+
.folderName(folderName)
230+
.isActive(isActive)
195231
.build();
196232

197233
Page<DataSourceSearchItem> page = dataSourceService.search(memberId, cond, pageable);

src/main/java/org/tuna/zoopzoop/backend/domain/datasource/dto/DataSourceSearchCondition.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,7 @@ public class DataSourceSearchCondition {
99
private final String title;
1010
private final String summary;
1111
private final String category;
12+
private final Integer folderId;
1213
private final String folderName;
14+
private final Boolean isActive;
1315
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package org.tuna.zoopzoop.backend.domain.datasource.dto;
2+
3+
import jakarta.validation.constraints.NotEmpty;
4+
5+
import java.util.List;
6+
7+
8+
public record IdsRequest (
9+
@NotEmpty(message = "dataSourceId 배열은 비어있을 수 없습니다.")
10+
List<Integer> ids
11+
){}

src/main/java/org/tuna/zoopzoop/backend/domain/datasource/dto/reqBodyForCreateDataSource.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33

44
public record reqBodyForCreateDataSource(
55
@NotBlank String sourceUrl,
6-
Integer folderId // null 일 경우 default 폴더(최상위 폴더)
6+
Integer folderId // 0일 경우 default folder
77
) {}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,8 @@ public class DataSource extends BaseEntity {
6868
// 활성화 여부
6969
@Column(nullable = false)
7070
private boolean isActive = true;
71+
72+
// 삭제 일자
73+
@Column
74+
private LocalDate deletedAt;
7175
}

src/main/java/org/tuna/zoopzoop/backend/domain/datasource/repository/DataSourceQRepositoryImpl.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,13 @@ public Page<DataSourceSearchItem> search(Integer memberId, DataSourceSearchCondi
3636
QPersonalArchive pa = QPersonalArchive.personalArchive;
3737
QTag tag = QTag.tag;
3838

39-
// where
40-
BooleanBuilder where = new BooleanBuilder()
41-
.and(ds.isActive.isTrue());
39+
BooleanBuilder where = new BooleanBuilder();
4240

41+
if (cond.getIsActive() == null || Boolean.TRUE.equals(cond.getIsActive())) {
42+
where.and(ds.isActive.isTrue());
43+
} else {
44+
where.and(ds.isActive.isFalse());
45+
}
4346
if (cond.getTitle() != null && !cond.getTitle().isBlank()) {
4447
where.and(ds.title.containsIgnoreCase(cond.getTitle()));
4548
}
@@ -52,6 +55,9 @@ public Page<DataSourceSearchItem> search(Integer memberId, DataSourceSearchCondi
5255
if (cond.getFolderName() != null && !cond.getFolderName().isBlank()) {
5356
where.and(ds.folder.name.eq(cond.getFolderName()));
5457
}
58+
if (cond.getFolderId() != null) {
59+
where.and(ds.folder.id.eq(cond.getFolderId()));
60+
}
5561

5662
BooleanBuilder ownership = new BooleanBuilder()
5763
.and(pa.member.id.eq(memberId));
@@ -109,7 +115,7 @@ public Page<DataSourceSearchItem> search(Integer memberId, DataSourceSearchCondi
109115
.map(row -> new DataSourceSearchItem(
110116
row.get(ds.id),
111117
row.get(ds.title),
112-
row.get(ds.dataCreatedDate), // LocalDate 그대로 내려줌
118+
row.get(ds.dataCreatedDate),
113119
row.get(ds.summary),
114120
row.get(ds.sourceUrl),
115121
row.get(ds.imageUrl),

0 commit comments

Comments
 (0)