|
| 1 | +package org.tuna.zoopzoop.backend.domain.datasource.controller; |
| 2 | + |
| 3 | +import io.swagger.v3.oas.annotations.Operation; |
| 4 | +import io.swagger.v3.oas.annotations.tags.Tag; |
| 5 | +import jakarta.validation.Valid; |
| 6 | +import lombok.RequiredArgsConstructor; |
| 7 | +import org.openapitools.jackson.nullable.JsonNullable; |
| 8 | +import org.springframework.data.domain.Page; |
| 9 | +import org.springframework.data.domain.Pageable; |
| 10 | +import org.springframework.data.domain.Sort; |
| 11 | +import org.springframework.data.web.PageableDefault; |
| 12 | +import org.springframework.http.ResponseEntity; |
| 13 | +import org.springframework.security.core.annotation.AuthenticationPrincipal; |
| 14 | +import org.springframework.web.bind.annotation.*; |
| 15 | +import org.tuna.zoopzoop.backend.domain.datasource.dto.*; |
| 16 | +import org.tuna.zoopzoop.backend.domain.datasource.entity.Category; |
| 17 | +import org.tuna.zoopzoop.backend.domain.datasource.service.DataSourceService; |
| 18 | +import org.tuna.zoopzoop.backend.domain.datasource.service.PersonalDataSourceService; |
| 19 | +import org.tuna.zoopzoop.backend.global.rsData.RsData; |
| 20 | +import org.tuna.zoopzoop.backend.global.security.jwt.CustomUserDetails; |
| 21 | + |
| 22 | +import java.io.IOException; |
| 23 | +import java.util.Map; |
| 24 | + |
| 25 | +@RestController |
| 26 | +@RequestMapping("/api/v1/archive") |
| 27 | +@RequiredArgsConstructor |
| 28 | +@Tag(name = "ApiV1DataSource(Personal)", description = "개인 아카이브 자료 API") |
| 29 | +public class DataSourceController { |
| 30 | + |
| 31 | + private final PersonalDataSourceService personalApp; |
| 32 | + |
| 33 | + // ===== 등록 (개인만) ===== |
| 34 | + // DataSourceController |
| 35 | + |
| 36 | + @Operation(summary = "자료 등록", description = "내 PersonalArchive 안에 자료를 등록합니다.") |
| 37 | + @PostMapping("") |
| 38 | + public ResponseEntity<RsData<Map<String, Integer>>> createDataSource( |
| 39 | + @Valid @RequestBody reqBodyForCreateDataSource rq, |
| 40 | + @AuthenticationPrincipal CustomUserDetails user |
| 41 | + ) throws IOException { |
| 42 | + int id = personalApp.create( |
| 43 | + user.getMember().getId(), |
| 44 | + rq.sourceUrl(), |
| 45 | + rq.folderId(), |
| 46 | + DataSourceService.CreateCmd.builder().build() |
| 47 | + ); |
| 48 | + return ResponseEntity.ok( |
| 49 | + new RsData<>("200", "새로운 자료가 등록됐습니다.", Map.of("dataSourceId", id)) |
| 50 | + ); |
| 51 | + } |
| 52 | + |
| 53 | + |
| 54 | + // ===== 단건 삭제 ===== |
| 55 | + @Operation(summary = "자료 단건 삭제", description = "내 PersonalArchive 안에 자료를 단건 삭제합니다.") |
| 56 | + @DeleteMapping("/{dataSourceId}") |
| 57 | + public ResponseEntity<RsData<Map<String, Integer>>> delete( |
| 58 | + @PathVariable Integer dataSourceId, |
| 59 | + @AuthenticationPrincipal CustomUserDetails user |
| 60 | + ) { |
| 61 | + int deletedId = personalApp.deleteOne(user.getMember().getId(), dataSourceId); |
| 62 | + return ResponseEntity.ok( |
| 63 | + new RsData<>("200", deletedId + "번 자료가 삭제됐습니다.", Map.of("dataSourceId", deletedId)) |
| 64 | + ); |
| 65 | + } |
| 66 | + |
| 67 | + // ===== 다건 삭제 ===== |
| 68 | + @Operation(summary = "자료 다건 삭제", description = "내 PersonalArchive 안에 자료를 다건 삭제합니다.") |
| 69 | + @PostMapping("/delete") |
| 70 | + public ResponseEntity<RsData<Void>> deleteMany( |
| 71 | + @Valid @RequestBody reqBodyForDeleteMany rq, |
| 72 | + @AuthenticationPrincipal CustomUserDetails user |
| 73 | + ) { |
| 74 | + personalApp.deleteMany(user.getMember().getId(), rq.dataSourceId()); |
| 75 | + return ResponseEntity.ok(new RsData<>("200", "복수개의 자료가 삭제됐습니다.", null)); |
| 76 | + } |
| 77 | + |
| 78 | + // ===== 소프트 삭제/복원 ===== |
| 79 | + @Operation(summary = "자료 다건 임시 삭제", description = "내 PersonalArchive 안에 자료들을 임시 삭제합니다.") |
| 80 | + @PatchMapping("/soft-delete") |
| 81 | + public ResponseEntity<RsData<Void>> softDelete(@RequestBody @Valid IdsRequest rq, |
| 82 | + @AuthenticationPrincipal CustomUserDetails user) { |
| 83 | + personalApp.softDelete(user.getMember().getId(), rq.ids()); |
| 84 | + return ResponseEntity.ok(new RsData<>("200", "자료들이 임시 삭제됐습니다.", null)); |
| 85 | + } |
| 86 | + |
| 87 | + @Operation(summary = "자료 다건 복원", description = "내 PersonalArchive 안에 자료들을 복원합니다.") |
| 88 | + @PatchMapping("/restore") |
| 89 | + public ResponseEntity<RsData<Void>> restore(@RequestBody @Valid IdsRequest rq, |
| 90 | + @AuthenticationPrincipal CustomUserDetails user) { |
| 91 | + personalApp.restore(user.getMember().getId(), rq.ids()); |
| 92 | + return ResponseEntity.ok(new RsData<>("200", "자료들이 복구됐습니다.", null)); |
| 93 | + } |
| 94 | + |
| 95 | + // ===== 이동 ===== |
| 96 | + @Operation(summary = "자료 단건 이동", description = "내 PersonalArchive 안에 자료를 단건 이동합니다.") |
| 97 | + @PatchMapping("/{dataSourceId}/move") |
| 98 | + public ResponseEntity<RsData<Map<String, Integer>>> moveDataSource( |
| 99 | + @PathVariable Integer dataSourceId, |
| 100 | + @Valid @RequestBody reqBodyForMoveDataSource rq, |
| 101 | + @AuthenticationPrincipal CustomUserDetails user |
| 102 | + ) { |
| 103 | + var result = personalApp.moveOne(user.getMember().getId(), dataSourceId, rq.folderId()); |
| 104 | + String msg = result.dataSourceId() + "번 자료가 " + result.folderId() + "번 폴더로 이동했습니다."; |
| 105 | + return ResponseEntity.ok( |
| 106 | + new RsData<>("200", msg, |
| 107 | + Map.of("folderId", result.folderId(), "dataSourceId", result.dataSourceId())) |
| 108 | + ); |
| 109 | + } |
| 110 | + |
| 111 | + @Operation(summary = "자료 다건 이동", description = "내 PersonalArchive 안에 자료들을 다건 이동합니다.") |
| 112 | + @PatchMapping("/move") |
| 113 | + public ResponseEntity<RsData<Void>> moveMany( |
| 114 | + @Valid @RequestBody reqBodyForMoveMany rq, |
| 115 | + @AuthenticationPrincipal CustomUserDetails user |
| 116 | + ) { |
| 117 | + personalApp.moveMany(user.getMember().getId(), rq.folderId(), rq.dataSourceId()); |
| 118 | + return ResponseEntity.ok(new RsData<>("200", "복수 개의 자료를 이동했습니다.", null)); |
| 119 | + } |
| 120 | + |
| 121 | + // ===== 수정 ===== |
| 122 | + @Operation(summary = "자료 수정", description = "내 PersonalArchive 안에 자료를 수정합니다.") |
| 123 | + @PatchMapping("/{dataSourceId}") |
| 124 | + public ResponseEntity<RsData<Map<String, Integer>>> updateDataSource( |
| 125 | + @PathVariable Integer dataSourceId, |
| 126 | + @RequestBody reqBodyForUpdateDataSource body, |
| 127 | + @AuthenticationPrincipal CustomUserDetails user |
| 128 | + ) { |
| 129 | + boolean anyPresent = |
| 130 | + (body.title() != null && body.title().isPresent()) || |
| 131 | + (body.summary() != null && body.summary().isPresent()) || |
| 132 | + (body.sourceUrl() != null && body.sourceUrl().isPresent()) || |
| 133 | + (body.imageUrl() != null && body.imageUrl().isPresent()) || |
| 134 | + (body.source() != null && body.source().isPresent()) || |
| 135 | + (body.tags() != null && body.tags().isPresent()) || |
| 136 | + (body.category() != null && body.category().isPresent()); |
| 137 | + if (!anyPresent) throw new IllegalArgumentException("변경할 값이 없습니다."); |
| 138 | + |
| 139 | + |
| 140 | + var catNullable = body.category(); |
| 141 | + |
| 142 | + // category enum 변환 시도 |
| 143 | + JsonNullable<Category> enumCat = null; |
| 144 | + if (catNullable != null && catNullable.isPresent()) { |
| 145 | + String raw = catNullable.get(); |
| 146 | + try { |
| 147 | + // 필요하면 대소문자 허용 로직 추가 |
| 148 | + enumCat = JsonNullable.of(Category.valueOf(raw.toUpperCase())); |
| 149 | + } catch (IllegalArgumentException ex) { |
| 150 | + throw new IllegalArgumentException("유효하지 않은 카테고리입니다: " + raw); |
| 151 | + } |
| 152 | + } |
| 153 | + |
| 154 | + int updatedId = personalApp.update( |
| 155 | + user.getMember().getId(), |
| 156 | + dataSourceId, |
| 157 | + DataSourceService.UpdateCmd.builder() |
| 158 | + .title(body.title()).summary(body.summary()).sourceUrl(body.sourceUrl()) |
| 159 | + .imageUrl(body.imageUrl()).source(body.source()) |
| 160 | + .tags(body.tags()).category(enumCat) |
| 161 | + .build() |
| 162 | + ); |
| 163 | + |
| 164 | + return ResponseEntity.ok( |
| 165 | + new RsData<>("200", updatedId + "번 자료가 수정됐습니다.", Map.of("dataSourceId", updatedId)) |
| 166 | + ); |
| 167 | + } |
| 168 | + |
| 169 | + // ===== 검색 ===== |
| 170 | + @Operation(summary = "자료 검색", description = "내 PersonalArchive 안에 자료들을 검색합니다.") |
| 171 | + @GetMapping("") |
| 172 | + public ResponseEntity<RsData<SearchResponse<DataSourceSearchItem>>> search( |
| 173 | + @RequestParam(required = false) String title, |
| 174 | + @RequestParam(required = false) String summary, |
| 175 | + @RequestParam(required = false) String category, |
| 176 | + @RequestParam(required = false) String keyword, |
| 177 | + @RequestParam(required = false) Integer folderId, |
| 178 | + @RequestParam(required = false) String folderName, |
| 179 | + @RequestParam(required = false, defaultValue = "true") Boolean isActive, |
| 180 | + @PageableDefault(size = 8, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable, |
| 181 | + @AuthenticationPrincipal CustomUserDetails user |
| 182 | + ) { |
| 183 | + var cond = DataSourceSearchCondition.builder() |
| 184 | + .title(title).summary(summary).category(category).folderId(folderId) |
| 185 | + .folderName(folderName).isActive(isActive).keyword(keyword).build(); |
| 186 | + |
| 187 | + Page<DataSourceSearchItem> page = personalApp.search(user.getMember().getId(), cond, pageable); |
| 188 | + String sorted = pageable.getSort().toString().replace(": ", ","); |
| 189 | + |
| 190 | + var pageInfo = new PageInfo( |
| 191 | + page.getNumber(), page.getSize(), page.getTotalElements(), page.getTotalPages(), |
| 192 | + page.isFirst(), page.isLast(), sorted |
| 193 | + ); |
| 194 | + var body = new SearchResponse<>(page.getContent(), pageInfo); |
| 195 | + |
| 196 | + return ResponseEntity.ok(new RsData<>("200", "복수개의 자료가 조회됐습니다.", body)); |
| 197 | + } |
| 198 | +} |
0 commit comments