-
Notifications
You must be signed in to change notification settings - Fork 1
feat: 세공 상세 테이블 생성 및 동기화 API 구현 #97
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
6d2e2c7
532e8b4
79c4038
171f84d
d556ab2
3750335
1d06a94
52ee44d
3e0ded0
793b64b
991834d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| option_value,option_value2,option_desc | ||
| 최대생명력(15레벨:37.50 증가),, | ||
| 최대마나(19레벨:95 증가),, | ||
| 보호(5레벨:5 증가),, | ||
| 돌진,,인간 및 엘프일 때 방패 없이 사용 가능 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| package until.the.eternity.common.util; | ||
|
|
||
| import lombok.extern.slf4j.Slf4j; | ||
|
|
||
| import java.util.regex.Matcher; | ||
| import java.util.regex.Pattern; | ||
|
|
||
| /** | ||
| * 세공 옵션의 option_value를 파싱하여 스킬명(option_value), 레벨(option_value2), 설명(option_desc)을 분리하는 유틸리티. | ||
| * | ||
| * <p>지원 패턴: | ||
| * | ||
| * <ul> | ||
| * <li>패턴 1: "스킬명 숫자 레벨" (예: "지력 2레벨", "행운 6 레벨") | ||
| * <li>패턴 2: "스킬명(숫자레벨:효과)" (예: "매그넘 샷 대미지(20레벨:200 % 증가)") | ||
| * <li>패턴 2 확장: "스킬명(설명)(숫자레벨:효과)" (예: "교역 중 이동 속도(교역 강화 의상)(19레벨:57 % 증가)") | ||
| * <li>패턴 3: "스킬명 설명텍스트" (예: "돌진 인간 및 엘프일 때 방패 없이 사용 가능") | ||
| * </ul> | ||
| */ | ||
| @Slf4j | ||
| public final class SegongOptionParser { | ||
|
|
||
| public static final String SEGONG_OPTION_TYPE = "세공 옵션"; | ||
|
|
||
| // 패턴 1: "스킬명 숫자 레벨" 또는 "스킬명 숫자레벨" 형식 | ||
| // 그룹1: 스킬명, 그룹2: "숫자 레벨" 또는 "숫자레벨" (option_desc), 그룹3: 숫자 (option_value2) | ||
| private static final Pattern PATTERN_LEVEL_SUFFIX = Pattern.compile("^(.+?) ((\\d+) ?레벨)$"); | ||
|
|
||
| // 패턴 2: "스킬명(숫자레벨:효과)" 또는 "스킬명(설명)(숫자레벨:효과)" 형식 | ||
| // lazy(.+?)가 이중 괄호 케이스에서 첫 번째 괄호까지 확장하여 매칭 | ||
| // 그룹1: 스킬명(또는 스킬명+설명괄호), 그룹2: 괄호 전체 (option_desc), 그룹3: 숫자 (option_value2) | ||
| private static final Pattern PATTERN_LEVEL_PARENTHESIS = | ||
| Pattern.compile("^(.+?)(\\((\\d+)레벨:.+\\))$"); | ||
|
|
||
| // 패턴 3: "스킬명 설명텍스트" 형식 (레벨 정보 없이 텍스트 설명만 존재) | ||
| // 그룹1: 스킬명, 그룹2: 설명 텍스트 (option_desc) | ||
| private static final Pattern PATTERN_DESCRIPTION_ONLY = Pattern.compile("^(.+?) (.+)$"); | ||
|
|
||
| private SegongOptionParser() {} | ||
|
|
||
| public record ParseResult(String optionValue, String optionValue2, String optionDesc) {} | ||
|
|
||
| /** | ||
| * 세공 옵션의 option_value를 파싱하여 스킬명, 레벨, 설명을 분리한다. | ||
| * | ||
| * @param optionType 옵션 타입 | ||
| * @param optionValue 원본 옵션 값 | ||
| * @return 파싱 결과. 세공 옵션이 아니거나 파싱 불가능한 경우 null | ||
| */ | ||
| public static ParseResult parse(String optionType, String optionValue) { | ||
| if (!SEGONG_OPTION_TYPE.equals(optionType) || optionValue == null) { | ||
| if (log.isDebugEnabled()) { | ||
| log.debug( | ||
| "[SegongOptionParser] Skip parse: optionType='{}', optionValue='{}'", | ||
| optionType, | ||
| optionValue); | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| // 패턴 1: "스킬명 숫자 레벨" 또는 "스킬명 숫자레벨" | ||
| Matcher matcher1 = PATTERN_LEVEL_SUFFIX.matcher(optionValue); | ||
| if (matcher1.matches()) { | ||
| if (log.isDebugEnabled()) { | ||
| log.debug("[SegongOptionParser] Matched LEVEL_SUFFIX: '{}'", optionValue); | ||
| } | ||
| return new ParseResult(matcher1.group(1), matcher1.group(3), matcher1.group(2)); | ||
| } | ||
|
|
||
| // 패턴 2: "스킬명(숫자레벨:효과)" 또는 "스킬명(설명)(숫자레벨:효과)" | ||
| Matcher matcher2 = PATTERN_LEVEL_PARENTHESIS.matcher(optionValue); | ||
| if (matcher2.matches()) { | ||
| if (log.isDebugEnabled()) { | ||
| log.debug("[SegongOptionParser] Matched LEVEL_PARENTHESIS: '{}'", optionValue); | ||
| } | ||
| return new ParseResult(matcher2.group(1), matcher2.group(3), matcher2.group(2)); | ||
| } | ||
|
|
||
| // 패턴 3: "스킬명 설명텍스트" (레벨 정보 없는 텍스트 설명) | ||
| Matcher matcher3 = PATTERN_DESCRIPTION_ONLY.matcher(optionValue); | ||
| if (matcher3.matches()) { | ||
| if (log.isDebugEnabled()) { | ||
| log.debug("[SegongOptionParser] Matched DESCRIPTION_ONLY: '{}'", optionValue); | ||
| } | ||
| return new ParseResult(matcher3.group(1), null, matcher3.group(2)); | ||
| } | ||
|
|
||
| if (log.isDebugEnabled()) { | ||
| log.debug("[SegongOptionParser] No pattern matched for value: '{}'", optionValue); | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| package until.the.eternity.metalwareinfo.application.service; | ||
|
|
||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.data.domain.Page; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
| import until.the.eternity.metalwareinfo.domain.repository.MetalwareAttributeInfoRepositoryPort; | ||
| import until.the.eternity.metalwareinfo.interfaces.rest.dto.request.MetalwareAttributeInfoSearchRequest; | ||
| import until.the.eternity.metalwareinfo.interfaces.rest.dto.response.MetalwareAttributeInfoResponse; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| public class MetalwareAttributeInfoService { | ||
|
|
||
| private final MetalwareAttributeInfoRepositoryPort metalwareAttributeInfoRepository; | ||
|
|
||
| @Transactional | ||
| public int sync() { | ||
| return metalwareAttributeInfoRepository.syncFromAuctionHistory(); | ||
| } | ||
|
|
||
| @Transactional(readOnly = true) | ||
| public Page<MetalwareAttributeInfoResponse> search( | ||
| MetalwareAttributeInfoSearchRequest request) { | ||
| return metalwareAttributeInfoRepository | ||
| .searchByMetalware(request.metalware(), request.toPageable()) | ||
| .map(MetalwareAttributeInfoResponse::from); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,7 @@ | |
| import org.springframework.transaction.annotation.Transactional; | ||
| import until.the.eternity.metalwareinfo.domain.repository.MetalwareInfoRepositoryPort; | ||
| import until.the.eternity.metalwareinfo.interfaces.rest.dto.response.MetalwareInfoResponse; | ||
| import until.the.eternity.metalwareinfo.interfaces.rest.dto.response.MetalwareInfoSyncResponse; | ||
|
|
||
| import java.util.List; | ||
|
|
||
|
|
@@ -19,4 +20,18 @@ public List<MetalwareInfoResponse> findAll() { | |
| List<String> metalwares = metalwareInfoRepository.findAllMetalwares(); | ||
| return MetalwareInfoResponse.from(metalwares); | ||
| } | ||
|
|
||
| @Transactional | ||
| public MetalwareInfoSyncResponse syncFromAttributeInfo() { | ||
| int levelAttributeUpserted = | ||
| metalwareInfoRepository.upsertLevelAttributeFromAttributeInfo(); | ||
| int limitBreakLevelUpserted = | ||
| metalwareInfoRepository.upsertLimitBreakLevelFromAttributeInfo(); | ||
|
|
||
|
Comment on lines
+26
to
+30
|
||
| return MetalwareInfoSyncResponse.builder() | ||
| .levelAttributeUpsertedCount(levelAttributeUpserted) | ||
| .limitBreakLevelUpsertedCount(limitBreakLevelUpserted) | ||
| .totalUpsertedCount(levelAttributeUpserted + limitBreakLevelUpserted) | ||
| .build(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,12 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| package until.the.eternity.metalwareinfo.domain.repository; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.data.domain.Page; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.data.domain.Pageable; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import until.the.eternity.metalwareinfo.infrastructure.persistence.MetalwareAttributeInfoEntity; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| public interface MetalwareAttributeInfoRepositoryPort { | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| int syncFromAuctionHistory(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| Page<MetalwareAttributeInfoEntity> searchByMetalware(String metalware, Pageable pageable); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+5
to
+11
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| 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); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| package until.the.eternity.metalwareinfo.infrastructure.persistence; | ||
|
|
||
| import jakarta.persistence.*; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| @Entity | ||
| @Table( | ||
| name = "metalware_attribute_info", | ||
| uniqueConstraints = @UniqueConstraint(columnNames = {"metalware", "level"})) | ||
| @Getter | ||
| @NoArgsConstructor | ||
| public class MetalwareAttributeInfoEntity { | ||
|
|
||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| private Long id; | ||
|
|
||
| @Column(name = "metalware", nullable = false, length = 50) | ||
| private String metalware; | ||
|
|
||
| @Column(name = "level") | ||
| private Byte level; | ||
|
|
||
| @Column(name = "attribute", length = 100) | ||
| private String attribute; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
현재 CSV는 코드/테스트에서 참조되는 위치가 아니고 파일명도
local_...형태라 커밋된 로컬 실험 데이터로 보입니다. 저장이 필요하다면src/test/resources또는docs/로 이동하고 사용 목적을 문서화하고, 아니라면 PR에서 제거하는 것이 저장소 관리에 유리합니다.