검증게시판 기능 추가

This commit is contained in:
Macbook
2026-05-27 15:41:19 +09:00
parent 7a0af36b9b
commit 63693d47c0
57 changed files with 4480 additions and 14 deletions
@@ -0,0 +1,60 @@
package com.goldenchart.controller;
import com.goldenchart.dto.VerificationIssueCommentDto;
import com.goldenchart.service.VerificationIssueCommentService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/verification-issues/{issueId}/comments")
@RequiredArgsConstructor
public class VerificationIssueCommentController {
private final VerificationIssueCommentService service;
@GetMapping
public ResponseEntity<List<VerificationIssueCommentDto>> list(@PathVariable Long issueId) {
return ResponseEntity.ok(service.listByIssue(issueId));
}
@PostMapping
public ResponseEntity<VerificationIssueCommentDto> create(
@PathVariable Long issueId,
@RequestBody VerificationIssueCommentDto dto,
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader,
@RequestHeader(value = "X-Device-Id", required = false) String deviceId) {
try {
return ResponseEntity.ok(service.create(issueId, dto, parseUserId(userIdHeader), deviceId));
} catch (IllegalArgumentException e) {
return ResponseEntity.notFound().build();
}
}
@PutMapping("/{commentId}")
public ResponseEntity<VerificationIssueCommentDto> update(
@PathVariable Long issueId,
@PathVariable Long commentId,
@RequestBody VerificationIssueCommentDto dto) {
return service.update(issueId, commentId, dto)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@DeleteMapping("/{commentId}")
public ResponseEntity<Void> delete(
@PathVariable Long issueId,
@PathVariable Long commentId) {
if (!service.delete(issueId, commentId)) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.noContent().build();
}
private Long parseUserId(String header) {
if (header == null || header.isBlank()) return null;
try { return Long.parseLong(header); } catch (NumberFormatException e) { return null; }
}
}
@@ -0,0 +1,60 @@
package com.goldenchart.controller;
import com.goldenchart.dto.VerificationIssueDto;
import com.goldenchart.entity.VerificationIssueStage;
import com.goldenchart.service.VerificationIssueService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/verification-issues")
@RequiredArgsConstructor
public class VerificationIssueController {
private final VerificationIssueService service;
@GetMapping
public ResponseEntity<List<VerificationIssueDto>> list(
@RequestParam(value = "stage", required = false) String stage) {
VerificationIssueStage parsed = parseStage(stage);
return ResponseEntity.ok(service.list(parsed));
}
@GetMapping("/{id}")
public ResponseEntity<VerificationIssueDto> get(@PathVariable Long id) {
return service.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<VerificationIssueDto> save(
@RequestBody VerificationIssueDto dto,
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader,
@RequestHeader(value = "X-Device-Id", required = false) String deviceId) {
return ResponseEntity.ok(service.save(dto, parseUserId(userIdHeader), deviceId));
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
service.delete(id);
return ResponseEntity.noContent().build();
}
private Long parseUserId(String header) {
if (header == null || header.isBlank()) return null;
try { return Long.parseLong(header); } catch (NumberFormatException e) { return null; }
}
private VerificationIssueStage parseStage(String stage) {
if (stage == null || stage.isBlank()) return null;
try {
return VerificationIssueStage.valueOf(stage);
} catch (IllegalArgumentException e) {
return null;
}
}
}
@@ -0,0 +1,87 @@
package com.goldenchart.controller;
import com.goldenchart.dto.VerificationIssueImageDto;
import com.goldenchart.service.VerificationIssueImageService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/verification-issues/{issueId}/images")
@RequiredArgsConstructor
@Slf4j
public class VerificationIssueImageController {
private final VerificationIssueImageService service;
@GetMapping
public ResponseEntity<List<VerificationIssueImageDto>> list(@PathVariable Long issueId) {
return ResponseEntity.ok(service.listByIssue(issueId));
}
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<?> upload(
@PathVariable Long issueId,
@RequestParam("files") List<MultipartFile> files) {
try {
return ResponseEntity.ok(service.upload(issueId, files));
} catch (IllegalArgumentException e) {
log.warn("[VerificationImage] upload rejected issueId={}: {}", issueId, e.getMessage());
return ResponseEntity.badRequest().body(Map.of("message", toUserMessage(e.getMessage())));
} catch (IOException e) {
log.error("[VerificationImage] upload io error issueId={}", issueId, e);
return ResponseEntity.internalServerError()
.body(Map.of("message", "이미지 저장에 실패했습니다. 잠시 후 다시 시도하세요."));
}
}
private static String toUserMessage(String code) {
if (code == null) return "업로드할 수 없습니다.";
return switch (code) {
case "unsupported image type" -> "지원하지 않는 이미지 형식입니다. (jpeg, png, gif, webp, bmp)";
case "file too large" -> "이미지 크기는 파일당 5MB 이하여야 합니다.";
case "no files", "no valid image files" -> "업로드할 이미지가 없습니다.";
default -> code.startsWith("max images exceeded")
? "이슈당 첨부 이미지는 최대 30장까지입니다."
: code.startsWith("issue not found") ? "검증 이슈를 찾을 수 없습니다." : code;
};
}
@GetMapping("/{imageId}/file")
public ResponseEntity<Resource> file(
@PathVariable Long issueId,
@PathVariable Long imageId) {
return service.loadFile(issueId, imageId)
.map(resource -> {
String type = service.contentType(issueId, imageId).orElse(MediaType.APPLICATION_OCTET_STREAM_VALUE);
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(type))
.header(HttpHeaders.CACHE_CONTROL, "private, max-age=3600")
.body(resource);
})
.orElse(ResponseEntity.notFound().build());
}
@DeleteMapping("/{imageId}")
public ResponseEntity<Void> delete(
@PathVariable Long issueId,
@PathVariable Long imageId) {
try {
if (!service.delete(issueId, imageId)) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.noContent().build();
} catch (IOException e) {
return ResponseEntity.internalServerError().build();
}
}
}
@@ -0,0 +1,30 @@
package com.goldenchart.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import org.springframework.web.multipart.MultipartException;
import java.util.Map;
@RestControllerAdvice
@Slf4j
public class VerificationUploadExceptionHandler {
@ExceptionHandler(MaxUploadSizeExceededException.class)
public ResponseEntity<Map<String, String>> handleMaxUpload(MaxUploadSizeExceededException e) {
log.warn("[VerificationUpload] size exceeded: {}", e.getMessage());
return ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE)
.body(Map.of("message", "업로드 용량이 제한(30MB)을 초과했습니다."));
}
@ExceptionHandler(MultipartException.class)
public ResponseEntity<Map<String, String>> handleMultipart(MultipartException e) {
log.warn("[VerificationUpload] multipart error: {}", e.getMessage());
return ResponseEntity.badRequest()
.body(Map.of("message", "이미지 파일 형식을 확인할 수 없습니다."));
}
}