feat: 추천 작업 5종 일괄 진행 (4 agents 병렬)

[A] 등록 모달 7페이지 (frontend)
- CompanyList / ProductList / Commission / Payout / Override / Chargeback / ExceptionCode
- ModalForm 580px, Switch Y/N 변환, 날짜 dayjs, PermissionButton
- api/company/product/rule.ts 에 SaveReq + create/update/remove 추가

[B] BatchRun + RoleList 보강 (frontend)
- BatchRun: 정산월/보험사 선택 → 실행, antd Steps 8단계 + 5초 폴링
  (refetchInterval 자동 정지), KPI 4 카드, 최근 이력
- RoleList: 좌(역할 목록) 우(메뉴×6권한 매트릭스 체크박스),
  DIRECTORY 들여쓰기, 전체 토글, Modal.confirm 후 저장

[C] 이체파일 + 은행 어댑터 (api)
- BankTransferFileGenerator 인터페이스 + GenericCsvTransferGenerator
  (UTF-8 BOM, 표준 CSV) + KbBankTransferGenerator (088, 고정폭 + 합계)
- BankTransferFactory (Spring Map 빈)
- TransferFileService: PENDING 조회 → 파일 생성 → file_storage 적재 →
  pay_file_ref 갱신 → status=SENT
- POST /api/payments/transfer-file (DataChangeLog + EXPORT 권한)
- account_no EncryptTypeHandler 자동 복호화 후 파일에 사용
- FileService.saveBytes(byte[]) 오버로드 추가
- PaymentList: 이체파일 생성 ModalForm + 다운로드 컬럼

[D] MockMvc Controller 테스트 (api)
- AbstractControllerTest (SpringBootTest + MockMvc, JwtFilter doAnswer
  로 chain 통과 처리), TestSecurityConfig (filter 빈 override)
- 5 클래스 36 테스트: Agent/Contract/Ledger/Settle/User Controller
- happy path + validation 400 + 권한 403 + 인증 401

검증:
- ./gradlew clean build 성공
- 백엔드 단위테스트 94건 (common 33 + batch 19 + api 36 + 기타 6) 통과
- 프론트 tsc -b / vite build 통과

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
GA Pro
2026-05-11 00:08:32 +09:00
parent a314a95ecf
commit 87e4e3d1da
35 changed files with 2962 additions and 110 deletions
@@ -65,6 +65,39 @@ public class FileService {
}
}
/**
* byte[] 콘텐츠를 파일로 저장하고 fileId를 반환한다.
* MultipartFile 없이 프로그래밍 방식으로 파일을 생성할 때 사용.
*/
@Transactional
public Long saveBytes(byte[] content, String fileName, String fileGroup, String contentType) {
String ext = extension(fileName);
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);
Files.write(dest, content);
FileVO vo = new FileVO();
vo.setFileGroup(fileGroup);
vo.setOriginalName(fileName);
vo.setStoredName(stored);
vo.setFilePath(dest.toAbsolutePath().toString());
vo.setFileSize((long) content.length);
vo.setContentType(contentType);
vo.setExtension(ext);
vo.setCreatedBy(SecurityUtil.getCurrentUserId());
vo.setCreatedAt(java.time.LocalDateTime.now());
mapper.insert(vo);
return vo.getFileId();
} catch (IOException e) {
log.error("File save 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);