feat: 세공 상세 테이블 생성 및 동기화 API 구현#97
Conversation
✅ 테스트 결과 for PRBuild: success 🧪 테스트 실행 with Gradle |
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Pull request overview
세공(세공 옵션) 데이터를 더 잘 구조화하기 위해 metalware_attribute_info 테이블을 추가하고, 경매 기록에서 세공 능력치 정보를 동기화/조회할 수 있는 API 및 metalware_info 업서트 동기화 API를 구현합니다. 또한 세공 옵션 문자열 파싱 로직을 공통 유틸로 분리하고, 경매 히스토리 중복 판정에서 KST 변환 기준을 보정합니다.
Changes:
metalware_attribute_info테이블/엔티티/리포지토리/서비스/컨트롤러를 추가하고 동기화·검색 API를 구현metalware_attribute_info→metalware_info업서트 동기화 API 및 응답 DTO 추가- 세공 옵션 파서(
SegongOptionParser) 도입 및 기존 Mapper들의 파싱 로직 통합, KST 기반 중복 판정 테스트/로직 보강
Reviewed changes
Copilot reviewed 25 out of 25 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| src/test/java/until/the/eternity/metalwareinfo/application/service/MetalwareInfoServiceTest.java | 세공 정보 동기화 서비스 로직에 대한 단위 테스트 추가 |
| src/test/java/until/the/eternity/common/util/SegongOptionParserTest.java | 세공 옵션 파서에 대한 패턴별 테스트 추가 |
| src/test/java/until/the/eternity/auctionhistory/domain/service/AuctionHistoryDuplicateCheckerTest.java | KST 변환 기준을 반영한 중복 판정 테스트 보강 |
| src/main/resources/db/migration/V19__create_metalware_attribute_info.sql | 세공 능력치 정보 테이블 신규 생성 |
| src/main/resources/db/migration/V18__migrate_unparsed_segong_option.sql | 기존 미파싱 세공 옵션 데이터 파싱을 위한 마이그레이션 추가 |
| src/main/java/until/the/eternity/metalwareinfo/interfaces/rest/dto/response/MetalwareInfoSyncResponse.java | 세공 정보 업서트 동기화 결과 응답 DTO 추가 |
| src/main/java/until/the/eternity/metalwareinfo/interfaces/rest/dto/response/MetalwareAttributeInfoResponse.java | 세공 능력치 정보 조회 응답 DTO 추가 |
| src/main/java/until/the/eternity/metalwareinfo/interfaces/rest/dto/request/MetalwareAttributeInfoSearchRequest.java | 세공 능력치 검색 요청 파라미터(+페이징/정렬) DTO 추가 |
| src/main/java/until/the/eternity/metalwareinfo/interfaces/rest/controller/MetalwareInfoController.java | metalware_info 동기화 API(/sync) 추가 |
| src/main/java/until/the/eternity/metalwareinfo/interfaces/rest/controller/MetalwareAttributeInfoController.java | 세공 능력치 동기화/검색 API 컨트롤러 추가 |
| src/main/java/until/the/eternity/metalwareinfo/infrastructure/persistence/MetalwareInfoRepositoryPortImpl.java | metalware_info 업서트 메서드 구현 추가 |
| src/main/java/until/the/eternity/metalwareinfo/infrastructure/persistence/MetalwareInfoJpaRepository.java | metalware_attribute_info 기반 업서트 네이티브 쿼리 추가 |
| src/main/java/until/the/eternity/metalwareinfo/infrastructure/persistence/MetalwareAttributeInfoRepositoryPortImpl.java | 세공 능력치 동기화/검색 리포지토리 어댑터 추가 |
| src/main/java/until/the/eternity/metalwareinfo/infrastructure/persistence/MetalwareAttributeInfoJpaRepository.java | 경매 히스토리 옵션에서 세공 능력치 정보 upsert 동기화 쿼리 추가 |
| src/main/java/until/the/eternity/metalwareinfo/infrastructure/persistence/MetalwareAttributeInfoEntity.java | metalware_attribute_info JPA 엔티티 추가 |
| src/main/java/until/the/eternity/metalwareinfo/domain/repository/MetalwareInfoRepositoryPort.java | metalware_info 업서트 포트 메서드 추가 |
| src/main/java/until/the/eternity/metalwareinfo/domain/repository/MetalwareAttributeInfoRepositoryPort.java | 세공 능력치 동기화/검색 포트 인터페이스 추가 |
| src/main/java/until/the/eternity/metalwareinfo/application/service/MetalwareInfoService.java | metalware_info 업서트 동기화 서비스 메서드 추가 |
| src/main/java/until/the/eternity/metalwareinfo/application/service/MetalwareAttributeInfoService.java | 세공 능력치 동기화/검색 서비스 추가 |
| src/main/java/until/the/eternity/common/util/SegongOptionParser.java | 세공 옵션 문자열 파싱 유틸리티 신규 추가 |
| src/main/java/until/the/eternity/auctionrealtime/domain/mapper/OpenApiRealtimeItemOptionMapper.java | 세공 옵션 파싱 로직을 공통 파서로 이관 |
| src/main/java/until/the/eternity/auctionhistory/domain/service/AuctionHistoryDuplicateChecker.java | 중복 판정 시 KST 변환 적용 |
| src/main/java/until/the/eternity/auctionhistory/domain/mapper/OpenApiItemOptionMapper.java | 세공 옵션 파싱 로직을 공통 파서로 이관 |
| src/main/java/until/the/eternity/auctionhistory/domain/mapper/OpenApiAuctionHistoryMapper.java | UTC→KST 변환 구현을 상수 기반으로 정리 |
| local_oab_auction_history_item_option.csv | 로컬 CSV 샘플(추가됨) |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| import until.the.eternity.metalwareinfo.infrastructure.persistence.MetalwareAttributeInfoEntity; | ||
|
|
||
| public interface MetalwareAttributeInfoRepositoryPort { | ||
|
|
||
| int syncFromAuctionHistory(); | ||
|
|
||
| Page<MetalwareAttributeInfoEntity> searchByMetalware(String metalware, Pageable pageable); |
There was a problem hiding this comment.
도메인 레이어의 RepositoryPort가 infrastructure.persistence의 JPA Entity를 반환 타입으로 노출하고 있습니다. 포트는 도메인 모델(또는 별도 DTO/Projection)만 의존하도록 분리하고, JPA Entity는 infrastructure 계층 내부로 숨기는 구조가 유지보수/의존성 방향 측면에서 안전합니다.
| import until.the.eternity.metalwareinfo.infrastructure.persistence.MetalwareAttributeInfoEntity; | |
| public interface MetalwareAttributeInfoRepositoryPort { | |
| int syncFromAuctionHistory(); | |
| Page<MetalwareAttributeInfoEntity> searchByMetalware(String metalware, Pageable pageable); | |
| /** | |
| * Domain-level view of metalware attribute information. | |
| * <p> | |
| * This type intentionally lives in the domain package and is decoupled from | |
| * any JPA or infrastructure concerns. Implementations of the repository port | |
| * are responsible for mapping from persistence entities to this type. | |
| */ | |
| class MetalwareAttributeInfoDomain { | |
| // Add domain fields and behavior here as needed. | |
| } | |
| public interface MetalwareAttributeInfoRepositoryPort { | |
| int syncFromAuctionHistory(); | |
| Page<MetalwareAttributeInfoDomain> searchByMetalware(String metalware, Pageable pageable); |
| import lombok.Builder; | ||
| import until.the.eternity.metalwareinfo.infrastructure.persistence.MetalwareAttributeInfoEntity; | ||
|
|
||
| @Builder |
There was a problem hiding this comment.
응답 DTO가 infrastructure.persistence의 JPA Entity 타입을 직접 import해서 사용하고 있습니다. 컨트롤러/DTO 레이어가 영속성 구현에 결합되지 않도록, 서비스에서 도메인 모델/Projection으로 변환한 값을 DTO로 매핑하거나 from(...)의 파라미터를 도메인 타입으로 바꾸는 방식을 권장합니다.
| ( | ||
| id BIGINT AUTO_INCREMENT PRIMARY KEY, | ||
| metalware VARCHAR(50) NOT NULL COMMENT '세공', | ||
| level TINYINT NULL COMMENT '레벨', |
There was a problem hiding this comment.
level 컬럼이 NULL 허용인데 (metalware, level)에 UNIQUE KEY가 걸려 있어 metalware가 같고 level이 NULL인 행이 여러 개 들어갈 수 있습니다(MySQL UNIQUE에서 NULL은 중복 허용). limit_break_level 집계(MAX(level))도 고려하면 level을 NOT NULL로 두는 것이 데이터 정합성에 더 안전합니다.
| level TINYINT NULL COMMENT '레벨', | |
| level TINYINT NOT NULL COMMENT '레벨', |
| int levelAttributeUpserted = | ||
| metalwareInfoRepository.upsertLevelAttributeFromAttributeInfo(); | ||
| int limitBreakLevelUpserted = | ||
| metalwareInfoRepository.upsertLimitBreakLevelFromAttributeInfo(); | ||
|
|
There was a problem hiding this comment.
MySQL의 INSERT ... ON DUPLICATE KEY UPDATE는 JDBC update count가 '업서트된 레코드 수'와 일치하지 않을 수 있습니다(업데이트 시 2로 집계, no-op은 0 등). 이 값을 응답의 ...UpsertedCount로 노출하면 오해 소지가 있으니, 의미를 affectedRows로 명확히 하거나 커넥터 useAffectedRows=true 등으로 카운트 의미를 통일하는 방안을 검토해 주세요.
| @Operation(summary = "세공 능력치 정보 동기화", description = "경매 기록에서 세공 능력치 정보를 추출하여 동기화합니다.") | ||
| @PostMapping("/sync") | ||
| public ResponseEntity<Integer> sync() { | ||
| int syncedCount = metalwareAttributeInfoService.sync(); | ||
| return ResponseEntity.ok(syncedCount); |
There was a problem hiding this comment.
이 API가 반환하는 syncedCount는 upsert 쿼리의 update count라서 '실제 동기화된 레코드 수'와 다르게 보일 수 있습니다(업데이트=2, no-op=0 등). 응답 타입/문서에서 의미를 affectedRows로 명확히 하거나, 별도 집계 방식으로 동기화 건수를 계산하는 쪽이 안전합니다.
| @Operation(summary = "세공 능력치 정보 동기화", description = "경매 기록에서 세공 능력치 정보를 추출하여 동기화합니다.") | |
| @PostMapping("/sync") | |
| public ResponseEntity<Integer> sync() { | |
| int syncedCount = metalwareAttributeInfoService.sync(); | |
| return ResponseEntity.ok(syncedCount); | |
| /** | |
| * 세공 능력치 정보 동기화 결과 응답 DTO. | |
| * affectedRows는 upsert 쿼리로 인해 영향 받은 레코드 수를 의미합니다. | |
| */ | |
| public static class SyncResultResponse { | |
| private final int affectedRows; | |
| public SyncResultResponse(int affectedRows) { | |
| this.affectedRows = affectedRows; | |
| } | |
| public int getAffectedRows() { | |
| return affectedRows; | |
| } | |
| } | |
| @Operation(summary = "세공 능력치 정보 동기화", description = "경매 기록에서 세공 능력치 정보를 추출하여 동기화합니다.") | |
| @PostMapping("/sync") | |
| public ResponseEntity<SyncResultResponse> sync() { | |
| int affectedRows = metalwareAttributeInfoService.sync(); | |
| return ResponseEntity.ok(new SyncResultResponse(affectedRows)); |
| SegongOptionParser.ParseResult result = SegongOptionParser.parse(SEGONG, input); | ||
|
|
||
| assertThat(result).isNotNull(); | ||
| assertThat(result.optionValue2()).isNotNull(); | ||
| assertThat(result.optionDesc()).isNotNull(); |
There was a problem hiding this comment.
현재 단언은 null 여부 위주라서 잘못된 파싱(예: optionValue에 레벨이 남거나 optionValue2가 오파싱)도 통과할 수 있습니다. 대표 케이스 몇 개는 optionValue/optionValue2/optionDesc의 '정확한 값'까지 검증하도록 보강하면 회귀 방지에 더 효과적입니다.
| ON DUPLICATE KEY UPDATE attribute = VALUES(attribute) | ||
| """, | ||
| nativeQuery = true) | ||
| int syncFromAuctionHistory(); |
There was a problem hiding this comment.
이 upsert 쿼리의 반환 int는 MySQL에서 '영향받은 행 수'(업데이트 시 2로 집계, no-op은 0 등)로 내려올 수 있어 '동기화된 레코드 수'와 다를 수 있습니다. 상위 레이어에서 이 값을 그대로 노출한다면 의미를 affectedRows로 명확히 하거나 별도 집계 방식을 고려해 주세요.
| int syncFromAuctionHistory(); | |
| int syncFromAuctionHistoryAffectedRows(); |
| option_value,option_value2,option_desc | ||
| 최대생명력(15레벨:37.50 증가),, | ||
| 최대마나(19레벨:95 증가),, | ||
| 보호(5레벨:5 증가),, | ||
| 돌진,,인간 및 엘프일 때 방패 없이 사용 가능 |
There was a problem hiding this comment.
현재 CSV는 코드/테스트에서 참조되는 위치가 아니고 파일명도 local_... 형태라 커밋된 로컬 실험 데이터로 보입니다. 저장이 필요하다면 src/test/resources 또는 docs/로 이동하고 사용 목적을 문서화하고, 아니라면 PR에서 제거하는 것이 저장소 관리에 유리합니다.
📋 상세 설명
📊 체크리스트
이슈 미등록