feat: project skeleton + DB migrations V1-V12 + ga-common framework
- Gradle multi-module: ga-common, ga-core, ga-api, ga-batch, ga-admin - Flyway V1-V12: org/product/rule/receive/ledger/settle/batch/code/menu/system + seed - ga-common (complete): - model: ApiResponse, PageResponse, SearchParam, TreeNode - exception: ErrorCode, BizException, GlobalExceptionHandler - annotation + aop: @RequirePermission, @DataChangeLog, ApiLog - auth + security: JWT, SecurityConfig - mybatis: BaseMapper, EncryptTypeHandler, JsonTypeHandler, AuditInterceptor - code (Redis cached) / menu (RBAC tree) / file / system / notification - excel: SXSSF+Cursor export, SAX+batch import (1M/700K rows) - util: Date/Money/Mask/Encrypt/Security
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
package com.ga.common.file;
|
||||
|
||||
import com.ga.common.exception.BizException;
|
||||
import com.ga.common.exception.ErrorCode;
|
||||
import com.ga.common.model.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
|
||||
@Tag(name = "파일")
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/files")
|
||||
@RequiredArgsConstructor
|
||||
public class FileController {
|
||||
|
||||
private final FileService service;
|
||||
|
||||
@PostMapping
|
||||
public ApiResponse<FileVO> upload(@RequestParam MultipartFile file,
|
||||
@RequestParam(required = false, defaultValue = "default") String fileGroup) {
|
||||
return ApiResponse.ok(service.upload(file, fileGroup));
|
||||
}
|
||||
|
||||
@GetMapping("/{fileId}")
|
||||
public void download(@PathVariable Long fileId, HttpServletResponse response) {
|
||||
FileVO vo = service.download(fileId);
|
||||
Path path = Paths.get(vo.getFilePath());
|
||||
if (!Files.exists(path)) throw new BizException(ErrorCode.FILE_NOT_FOUND);
|
||||
try {
|
||||
response.setContentType(vo.getContentType() != null ? vo.getContentType() : "application/octet-stream");
|
||||
String encoded = URLEncoder.encode(vo.getOriginalName(), StandardCharsets.UTF_8).replace("+", "%20");
|
||||
response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + encoded);
|
||||
Files.copy(path, response.getOutputStream());
|
||||
} catch (IOException e) {
|
||||
log.error("File download error", e);
|
||||
throw new BizException(ErrorCode.INTERNAL_ERROR, "파일 다운로드 실패");
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/groups/{fileGroup}")
|
||||
public ApiResponse<List<FileVO>> listByGroup(@PathVariable String fileGroup) {
|
||||
return ApiResponse.ok(service.listByGroup(fileGroup));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{fileId}")
|
||||
public ApiResponse<Void> delete(@PathVariable Long fileId) {
|
||||
service.delete(fileId);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.ga.common.file;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface FileMapper {
|
||||
int insert(FileVO vo);
|
||||
FileVO selectById(@Param("fileId") Long fileId);
|
||||
List<FileVO> selectByGroup(@Param("fileGroup") String fileGroup);
|
||||
int markDeleted(@Param("fileId") Long fileId);
|
||||
int incrementDownloadCount(@Param("fileId") Long fileId);
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.ga.common.file;
|
||||
|
||||
import com.ga.common.exception.BizException;
|
||||
import com.ga.common.exception.ErrorCode;
|
||||
import com.ga.common.util.SecurityUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 파일 업로드/다운로드. 로컬 파일시스템 저장 (S3 등으로 교체 가능한 구조).
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class FileService {
|
||||
|
||||
private final FileMapper mapper;
|
||||
|
||||
@Value("${ga.file.upload-dir:./uploads}")
|
||||
private String uploadDir;
|
||||
|
||||
@Transactional
|
||||
public FileVO upload(MultipartFile file, String fileGroup) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new BizException(ErrorCode.FILE_UPLOAD_FAIL, "빈 파일입니다");
|
||||
}
|
||||
String original = file.getOriginalFilename();
|
||||
if (original == null) original = "unknown";
|
||||
String ext = extension(original);
|
||||
String stored = UUID.randomUUID() + (ext.isEmpty() ? "" : "." + ext);
|
||||
String relPath = LocalDate.now().toString().replace("-", "/");
|
||||
Path dir = Paths.get(uploadDir, relPath);
|
||||
try {
|
||||
Files.createDirectories(dir);
|
||||
Path dest = dir.resolve(stored);
|
||||
file.transferTo(dest.toFile());
|
||||
|
||||
FileVO vo = new FileVO();
|
||||
vo.setFileGroup(fileGroup);
|
||||
vo.setOriginalName(original);
|
||||
vo.setStoredName(stored);
|
||||
vo.setFilePath(dest.toAbsolutePath().toString());
|
||||
vo.setFileSize(file.getSize());
|
||||
vo.setContentType(file.getContentType());
|
||||
vo.setExtension(ext);
|
||||
vo.setCreatedBy(SecurityUtil.getCurrentUserId());
|
||||
vo.setCreatedAt(java.time.LocalDateTime.now());
|
||||
mapper.insert(vo);
|
||||
return vo;
|
||||
} catch (IOException e) {
|
||||
log.error("File upload error", e);
|
||||
throw new BizException(ErrorCode.FILE_UPLOAD_FAIL, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public FileVO download(Long fileId) {
|
||||
FileVO vo = mapper.selectById(fileId);
|
||||
if (vo == null) throw new BizException(ErrorCode.FILE_NOT_FOUND);
|
||||
mapper.incrementDownloadCount(fileId);
|
||||
return vo;
|
||||
}
|
||||
|
||||
public List<FileVO> listByGroup(String fileGroup) {
|
||||
return mapper.selectByGroup(fileGroup);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void delete(Long fileId) {
|
||||
FileVO vo = mapper.selectById(fileId);
|
||||
if (vo == null) throw new BizException(ErrorCode.FILE_NOT_FOUND);
|
||||
mapper.markDeleted(fileId);
|
||||
// 물리 삭제는 배치로 처리
|
||||
}
|
||||
|
||||
private String extension(String name) {
|
||||
int idx = name.lastIndexOf('.');
|
||||
return idx >= 0 ? name.substring(idx + 1).toLowerCase() : "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.ga.common.file;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
public class FileVO {
|
||||
private Long fileId;
|
||||
private String fileGroup;
|
||||
private String originalName;
|
||||
private String storedName;
|
||||
private String filePath;
|
||||
private Long fileSize;
|
||||
private String contentType;
|
||||
private String extension;
|
||||
private Integer downloadCount;
|
||||
private String isDeleted;
|
||||
private LocalDateTime createdAt;
|
||||
private Long createdBy;
|
||||
}
|
||||
Reference in New Issue
Block a user