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
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@ public ResponseEntity<Map<String, Object>> deleteFolder(
) {
if (folderId == 0) {
var body = new java.util.HashMap<String, Object>();
body.put("status", 400);
body.put("status", 409);
body.put("msg", "default 폴더는 삭제할 수 없습니다.");
body.put("data", null); // HashMap은 null 허용
body.put("data", null);
return ResponseEntity.badRequest().body(body);
}

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ public class Folder extends BaseEntity {
@Column(nullable = false, name = "is_default")
private boolean isDefault = false;

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


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ public interface FolderRepository extends JpaRepository<Folder, Integer>{
* @param filenameEnd "파일명 + \ufffff"
*/
@Query("""
select f.name
from Folder f
where f.archive.id = :archiveId
and f.name >= :filename
and f.name < :filenameEnd
""")
select f.name
from Folder f
where f.archive.id = :archiveId
and f.name >= :filename
and f.name < :filenameEnd
""")
List<String> findNamesForConflictCheck(@Param("archiveId") Integer archiveId,
@Param("filename") String filename,
@Param("filenameEnd") String filenameEnd);
Expand All @@ -40,28 +40,36 @@ List<String> findNamesForConflictCheck(@Param("archiveId") Integer archiveId,
* @param memberId 조회할 회원 Id
*/
@Query("""
select f
from Folder f
join f.archive a
join PersonalArchive pa on pa.archive = a
where pa.member.id = :memberId
and f.isDefault = true
""")
select f
from Folder f
join f.archive a
join PersonalArchive pa on pa.archive = a
where pa.member.id = :memberId
and f.isDefault = true
""")
Optional<Folder> findDefaultFolderByMemberId(@Param("memberId") Integer memberId);

// 한 번의 조인으로 존재 + 소유권(memberId) 검증
@Query("""
select f
from Folder f
join f.archive a
join PersonalArchive pa on pa.archive = a
where f.id = :folderId
and pa.member.id = :memberId
""")
select f
from Folder f
join f.archive a
join PersonalArchive pa on pa.archive = a
where f.id = :folderId
and pa.member.id = :memberId
""")
Optional<Folder> findByIdAndMemberId(@Param("folderId") Integer folderId,
@Param("memberId") Integer memberId);

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

List<Folder> findAllByArchiveId(Integer archiveId);

@Query("""
select f from Folder f
join f.archive a
join PersonalArchive pa on pa.archive.id = a.id
where pa.member.id = :memberId and f.isDefault = true
""")
Optional<Folder> findDefaultByMemberId(@Param("memberId") Integer memberId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@
import org.tuna.zoopzoop.backend.domain.archive.folder.repository.FolderRepository;
import org.tuna.zoopzoop.backend.domain.datasource.dto.FileSummary;
import org.tuna.zoopzoop.backend.domain.datasource.dto.FolderFilesDto;
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.DataSourceRepository;
import org.tuna.zoopzoop.backend.domain.member.entity.Member;
import org.tuna.zoopzoop.backend.domain.member.repository.MemberRepository;
import org.tuna.zoopzoop.backend.domain.datasource.entity.Tag;

import java.time.LocalDate;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
Expand Down Expand Up @@ -111,20 +113,33 @@ private static String pickNextAvailable(String file, List<String> existing) {
}

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

if (folder.isDefault())
if (folder.isDefault()) {
throw new IllegalArgumentException("default 폴더는 삭제할 수 없습니다.");
}

Folder defaultFolder = folderRepository.findDefaultByMemberId(currentId)
.orElseThrow(() -> new IllegalStateException("default 폴더가 존재하지 않습니다."));

// 폴더 내 자료들을 Default로 이관 + soft delete
List<DataSource> dataSources = dataSourceRepository.findAllByFolderId(folderId);
LocalDate now = LocalDate.now();
for (DataSource ds : dataSources) {
ds.setFolder(defaultFolder);
ds.setActive(false);
ds.setDeletedAt(now);
}

String name = folder.getName();
folderRepository.delete(folder);

return name;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public ResponseEntity<?> createDataSource(
}

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

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

/**
* 자료 다건 소프트 삭제
*/
@Operation(summary = "자료 다건 임시 삭제", description = "내 PersonalArchive 안에 자료들을 임시 삭제합니다.")
@PatchMapping("/soft-delete")
public ResponseEntity<?> softDelete(
@RequestBody @Valid IdsRequest req,
@AuthenticationPrincipal CustomUserDetails user) {

int cnt = dataSourceService.softDelete(user.getMember().getId(), req.ids());
Map<String, Object> res = new LinkedHashMap<>();
res.put("status", 200);
res.put("msg", "자료들이 임시 삭제됐습니다.");
res.put("data", null);
return ResponseEntity.ok(res);
}
/**
* 자료 다건 복원
*/
@Operation(summary = "자료 다건 복원", description = "내 PersonalArchive 안에 자료들을 복원합니다.")
@PatchMapping("/restore")
public ResponseEntity<?> restore(
@RequestBody @Valid IdsRequest req,
@AuthenticationPrincipal CustomUserDetails user) {

int cnt = dataSourceService.restore(user.getMember().getId(), req.ids());
Map<String, Object> res = new LinkedHashMap<>();
res.put("status", 200);
res.put("msg", "자료들이 복구됐습니다.");
res.put("data", null);
return ResponseEntity.ok(res);
}
/**
* 자료 단건 이동
* folderId=null 이면 default 폴더
Expand Down Expand Up @@ -172,15 +204,17 @@ public ResponseEntity<?> updateDataSource(
}

/**
* 자료 검색
* 자료 검색
*/
@Operation(summary = "자료 검색", description = "내 PersonalArchive 안에 자료들을 검색합니다.")
@GetMapping("")
public ResponseEntity<?> search(
@RequestParam(required = false) String title,
@RequestParam(required = false) String summary,
@RequestParam(required = false) String category,
@RequestParam(required = false) Integer folderId,
@RequestParam(required = false) String folderName,
@RequestParam(required = false, defaultValue = "true") Boolean isActive,
@PageableDefault(size = 8, sort = "createdAt", direction = Sort.Direction.DESC)
Pageable pageable,
@AuthenticationPrincipal CustomUserDetails userDetails
Expand All @@ -190,8 +224,10 @@ public ResponseEntity<?> search(
DataSourceSearchCondition cond = DataSourceSearchCondition.builder()
.title(title)
.summary(summary)
.folderName(folderName)
.category(category)
.folderId(folderId)
.folderName(folderName)
.isActive(isActive)
.build();

Page<DataSourceSearchItem> page = dataSourceService.search(memberId, cond, pageable);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,7 @@ public class DataSourceSearchCondition {
private final String title;
private final String summary;
private final String category;
private final Integer folderId;
private final String folderName;
private final Boolean isActive;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.tuna.zoopzoop.backend.domain.datasource.dto;

import jakarta.validation.constraints.NotEmpty;

import java.util.List;


public record IdsRequest (
@NotEmpty(message = "dataSourceId 배열은 비어있을 수 없습니다.")
List<Integer> ids
){}
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@

public record reqBodyForCreateDataSource(
@NotBlank String sourceUrl,
Integer folderId // null 일 경우 default 폴더(최상위 폴더)
Integer folderId // 0일 경우 default folder
) {}
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,8 @@ public class DataSource extends BaseEntity {
// 활성화 여부
@Column(nullable = false)
private boolean isActive = true;

// 삭제 일자
@Column
private LocalDate deletedAt;
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,13 @@ public Page<DataSourceSearchItem> search(Integer memberId, DataSourceSearchCondi
QPersonalArchive pa = QPersonalArchive.personalArchive;
QTag tag = QTag.tag;

// where
BooleanBuilder where = new BooleanBuilder()
.and(ds.isActive.isTrue());
BooleanBuilder where = new BooleanBuilder();

if (cond.getIsActive() == null || Boolean.TRUE.equals(cond.getIsActive())) {
where.and(ds.isActive.isTrue());
} else {
where.and(ds.isActive.isFalse());
}
if (cond.getTitle() != null && !cond.getTitle().isBlank()) {
where.and(ds.title.containsIgnoreCase(cond.getTitle()));
}
Expand All @@ -52,6 +55,9 @@ public Page<DataSourceSearchItem> search(Integer memberId, DataSourceSearchCondi
if (cond.getFolderName() != null && !cond.getFolderName().isBlank()) {
where.and(ds.folder.name.eq(cond.getFolderName()));
}
if (cond.getFolderId() != null) {
where.and(ds.folder.id.eq(cond.getFolderId()));
}

BooleanBuilder ownership = new BooleanBuilder()
.and(pa.member.id.eq(memberId));
Expand Down Expand Up @@ -109,7 +115,7 @@ public Page<DataSourceSearchItem> search(Integer memberId, DataSourceSearchCondi
.map(row -> new DataSourceSearchItem(
row.get(ds.id),
row.get(ds.title),
row.get(ds.dataCreatedDate), // LocalDate 그대로 내려줌
row.get(ds.dataCreatedDate),
row.get(ds.summary),
row.get(ds.sourceUrl),
row.get(ds.imageUrl),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package org.tuna.zoopzoop.backend.domain.datasource.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import org.tuna.zoopzoop.backend.domain.archive.folder.entity.Folder;
import org.tuna.zoopzoop.backend.domain.datasource.entity.DataSource;

import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
Expand All @@ -17,29 +19,38 @@ public interface DataSourceRepository extends JpaRepository<DataSource, Integer>

List<DataSource> findAllByIdIn(Collection<Integer> ids);

// CHANGED: 특정 멤버(개인 아카이브 소유자) 범위에서 id로 조회 (ownership check)
// 개인 아카이브 범위에서 id로 조회 (ownership check)
@Query("""
select d from DataSource d
join d.folder f
join f.archive a
join PersonalArchive pa on pa.archive = a
where d.id = :id
and pa.member.id = :memberId
""")
""")
Optional<DataSource> findByIdAndMemberId(@Param("id") Integer id, @Param("memberId") Integer memberId);

// CHANGED: 여러 id 중에서 해당 member 소유인 id만 반환 (다건 삭제/검증용)
// 여러 id 중에서 해당 member 소유인 id만 반환 (다건 삭제/검증용)
@Query("""
select d.id from DataSource d
join d.folder f
join f.archive a
join PersonalArchive pa on pa.archive = a
where pa.member.id = :memberId
and d.id in :ids
""")
""")
List<Integer> findExistingIdsInMember(@Param("memberId") Integer memberId, @Param("ids") Collection<Integer> ids);

Optional<DataSource> findByFolderIdAndTitle(Integer folderId, String title);

List<DataSource> findAllByFolderId(Integer folderId);

@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("update DataSource d set d.isActive=false, d.deletedAt=:ts where d.id in :ids")
int softDeleteAllByIds(@Param("ids") List<Integer> ids, @Param("ts") LocalDateTime ts);

@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("update DataSource d set d.isActive=true, d.deletedAt=null where d.id in :ids")
int restoreAllByIds(@Param("ids") List<Integer> ids);
}

Loading