-
Notifications
You must be signed in to change notification settings - Fork 1
feat: 아이템 메타 데이터 조회 API에 item_top_category 파라미터 추가 #79
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
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds a required topCategory parameter to item metadata retrieval APIs and introduces new endpoints for paginated/filtered item queries and synchronization with auction history data. The changes enhance query performance through database indexing and implement authentication infrastructure via a gateway filter, though the API endpoints themselves remain publicly accessible.
Key Changes
- Added
topCategoryas a required parameter for item search APIs with validation - Introduced new
/detailand/summaryendpoints with pagination and filtering capabilities - Implemented item synchronization from auction history with duplicate detection
- Created database indexes for improved query performance on item_info table
- Added gateway authentication filter infrastructure (but APIs remain public)
Reviewed changes
Copilot reviewed 21 out of 21 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
| ItemInfoService.java | Added search methods with topCategory validation and auction history sync logic |
| ItemInfoController.java | Replaced category-based search endpoints with new detail/summary/sync endpoints |
| ItemInfoSearchRequest.java | New DTO with required topCategory field validation |
| ItemInfoQueryDslRepository.java | QueryDSL implementation for dynamic search with pagination |
| V12__create_item_info_indexes.sql | Database indexes for top_category, sub_category, and name combinations |
| ItemInfoServiceTest.java | Comprehensive tests for new search and sync functionality |
| SecurityConfig.java | Enhanced security configuration with gateway auth filter integration |
| GatewayAuthFilter.java | New filter to process authentication headers from gateway |
| AuctionHistoryJpaRepository.java | Added query to fetch distinct item information |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| .description(null) | ||
| .inventoryWidth(null) | ||
| .inventoryHeight(null) | ||
| .inventoryMaxBundleCount(null) | ||
| .history(null) | ||
| .acquisitionMethod(null) | ||
| .storeSalesPrice(null) | ||
| .weaponType(null) | ||
| .repair(null) | ||
| .maxAlterationCount(null) |
Copilot
AI
Nov 26, 2025
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.
[nitpick] Code maintainability issue: Explicitly setting all fields to null in the builder is verbose and adds no value since unset builder fields default to null anyway. This makes the code harder to maintain when the entity structure changes.
Consider simplifying to:
ItemInfo itemInfo = ItemInfo.builder()
.id(itemInfoId)
.build();This achieves the same result with less code and is easier to maintain.
| .description(null) | |
| .inventoryWidth(null) | |
| .inventoryHeight(null) | |
| .inventoryMaxBundleCount(null) | |
| .history(null) | |
| .acquisitionMethod(null) | |
| .storeSalesPrice(null) | |
| .weaponType(null) | |
| .repair(null) | |
| .maxAlterationCount(null) |
src/test/java/until/the/eternity/iteminfo/application/service/ItemInfoServiceTest.java
Show resolved
Hide resolved
src/test/java/until/the/eternity/iteminfo/application/service/ItemInfoServiceTest.java
Show resolved
Hide resolved
| String topCategory = (String) item[1]; | ||
| String subCategory = (String) item[2]; |
Copilot
AI
Nov 26, 2025
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.
The order of array destructuring does not match the query result order. The query returns a.itemName, a.itemTopCategory, a.itemSubCategory (line 42 of AuctionHistoryJpaRepository), but the code expects {name, topCategory, subCategory}. This should be {name, subCategory, topCategory} to match ItemInfoId constructor at line 96.
Lines 92-94 should be:
String itemName = (String) item[0];
String subCategory = (String) item[1];
String topCategory = (String) item[2];| String topCategory = (String) item[1]; | |
| String subCategory = (String) item[2]; | |
| String subCategory = (String) item[1]; | |
| String topCategory = (String) item[2]; |
| for (Object[] item : distinctItems) { | ||
| String itemName = (String) item[0]; | ||
| String topCategory = (String) item[1]; | ||
| String subCategory = (String) item[2]; | ||
|
|
||
| ItemInfoId itemInfoId = new ItemInfoId(itemName, subCategory, topCategory); | ||
|
|
||
| // 이미 존재하는 아이템인지 확인 | ||
| if (!itemInfoRepository.existsById(itemInfoId)) { | ||
| ItemInfo itemInfo = | ||
| ItemInfo.builder() | ||
| .id(itemInfoId) | ||
| .description(null) | ||
| .inventoryWidth(null) | ||
| .inventoryHeight(null) | ||
| .inventoryMaxBundleCount(null) | ||
| .history(null) | ||
| .acquisitionMethod(null) | ||
| .storeSalesPrice(null) | ||
| .weaponType(null) | ||
| .repair(null) | ||
| .maxAlterationCount(null) | ||
| .build(); | ||
|
|
||
| newItemInfos.add(itemInfo); | ||
| syncedItemNames.add(itemName); | ||
| log.debug("Adding new item to sync: {}", itemName); | ||
| } | ||
| } |
Copilot
AI
Nov 26, 2025
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.
Potential N+1 query problem. The code checks existsById() for each item in a loop (line 99), which generates one database query per item. For large datasets, this could result in hundreds or thousands of individual queries.
Consider using a batch approach:
- Fetch all existing ItemInfoIds in a single query
- Filter in-memory to identify new items
- Bulk insert new items
Example:
Set<ItemInfoId> existingIds = new HashSet<>(itemInfoRepository.findAllIds());
for (Object[] item : distinctItems) {
ItemInfoId itemInfoId = new ItemInfoId(itemName, subCategory, topCategory);
if (!existingIds.contains(itemInfoId)) {
// add to new items
}
}
src/main/java/until/the/eternity/iteminfo/domain/exception/ItemInfoExceptionCode.java
Show resolved
Hide resolved
src/main/java/until/the/eternity/iteminfo/domain/exception/ItemInfoExceptionCode.java
Show resolved
Hide resolved
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.
Pull request overview
Copilot reviewed 22 out of 22 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| .where(whereClause) | ||
| .orderBy(itemInfo.id.name.asc()); | ||
|
|
Copilot
AI
Dec 5, 2025
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.
The searchWithPagination method hardcodes sorting to name ascending (line 32) and ignores the Pageable's sort configuration. This is inconsistent with the search method (lines 51-66) which properly applies the Pageable's sort information. Consider applying the Pageable's sort here as well for consistency, or document why this endpoint always sorts by name ascending.
| .where(whereClause) | |
| .orderBy(itemInfo.id.name.asc()); | |
| .where(whereClause); | |
| // Apply Pageable's sort information, mirroring the logic from search() | |
| if (pageable.getSort().isSorted()) { | |
| pageable.getSort() | |
| .forEach( | |
| order -> { | |
| if (order.getProperty().equals("name") | |
| || order.getProperty().equals("id.name")) { | |
| query.orderBy( | |
| order.isAscending() | |
| ? itemInfo.id.name.asc() | |
| : itemInfo.id.name.desc()); | |
| } | |
| }); | |
| } else { | |
| // Default sort: name ascending | |
| query.orderBy(itemInfo.id.name.asc()); | |
| } |
| org.springframework.data.domain.Sort.Direction direction) { | ||
| validateTopCategory(searchRequest); | ||
| // direction을 Pageable로 변환 | ||
| Pageable pageable = | ||
| org.springframework.data.domain.PageRequest.of( | ||
| 0, | ||
| Integer.MAX_VALUE, |
Copilot
AI
Dec 5, 2025
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.
The findAllSummary method uses Integer.MAX_VALUE as the page size to fetch all records. This could cause memory and performance issues if the number of items grows large. Consider adding a maximum limit or implementing proper pagination for this endpoint as well.
| org.springframework.data.domain.Sort.Direction direction) { | |
| validateTopCategory(searchRequest); | |
| // direction을 Pageable로 변환 | |
| Pageable pageable = | |
| org.springframework.data.domain.PageRequest.of( | |
| 0, | |
| Integer.MAX_VALUE, | |
| org.springframework.data.domain.Sort.Direction direction, | |
| int page, | |
| int size) { | |
| validateTopCategory(searchRequest); | |
| // Enforce a maximum page size to prevent memory issues | |
| final int MAX_PAGE_SIZE = 1000; | |
| int safeSize = Math.min(size, MAX_PAGE_SIZE); | |
| Pageable pageable = | |
| org.springframework.data.domain.PageRequest.of( | |
| page, | |
| safeSize, |
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| @AllArgsConstructor | ||
| @Builder | ||
| @ToString |
Copilot
AI
Dec 5, 2025
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.
Default toString(): ItemInfoId inherits toString() from Object, and so is not suitable for printing.
| @ToString | |
| @ToString(exclude = "id") |
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| @AllArgsConstructor | ||
| @Builder | ||
| @ToString |
Copilot
AI
Dec 5, 2025
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.
Default toString(): ItemInfoId inherits toString() from Object, and so is not suitable for printing.
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.
Pull request overview
Copilot reviewed 22 out of 23 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| private void validateTopCategory(ItemInfoSearchRequest searchRequest) { | ||
| if (searchRequest.topCategory() == null || searchRequest.topCategory().isBlank()) { | ||
| throw new CustomException(ItemInfoExceptionCode.TOP_CATEGORY_REQUIRED); | ||
| } | ||
| } |
Copilot
AI
Dec 5, 2025
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.
[nitpick] The validateTopCategory method performs manual validation that duplicates the @NotBlank validation already present on the topCategory field in ItemInfoSearchRequest. Since the controller uses @Valid on the request parameter, Spring's validation framework will already enforce the @NotBlank constraint before the service method is called.
Consider removing this manual validation method and relying solely on the @Valid annotation at the controller level to avoid redundancy. If you want to keep defensive validation in the service layer, consider adding a comment explaining why (e.g., "defensive check for direct service calls").
| // Roles가 없으면 익명 사용자로 처리 | ||
| if (rolesHeader == null || rolesHeader.isEmpty()) { | ||
| log.debug("No roles found, creating anonymous authentication"); | ||
| return new UsernamePasswordAuthenticationToken(userId, null); | ||
| } |
Copilot
AI
Dec 5, 2025
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.
When rolesHeader is null or empty, the method creates an authentication with no authorities (line 88). This creates an authenticated user without any roles, which might not be the intended behavior.
Consider whether this should actually be treated as an anonymous/unauthenticated request instead, or if you want to require roles for all authenticated requests. If the current behavior is intentional, add a comment explaining that authenticated users without roles are valid in this system.
| String role = rolesHeader.trim(); | ||
| if (!role.startsWith("ROLE_")) { | ||
| role = "ROLE_" + role; | ||
| } | ||
| authorities.add(new SimpleGrantedAuthority(role)); | ||
|
|
||
| log.debug( | ||
| "Created authentication for userId: {}, username: {}, role: {}", | ||
| userId, | ||
| usernameHeader, | ||
| role); |
Copilot
AI
Dec 5, 2025
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.
The role parsing logic only handles a single role (line 96), but the header name X-Auth-Roles (plural) and the variable name rolesHeader suggest it should support multiple roles. If multiple roles are passed (e.g., "ROLE_USER,ROLE_ADMIN"), only one SimpleGrantedAuthority will be created for the entire string "ROLE_USER,ROLE_ADMIN".
If multiple roles are supported, the logic should split the header by a delimiter (e.g., comma) and create a SimpleGrantedAuthority for each role:
String[] roles = rolesHeader.split(",");
for (String role : roles) {
role = role.trim();
if (!role.startsWith("ROLE_")) {
role = "ROLE_" + role;
}
authorities.add(new SimpleGrantedAuthority(role));
}If only single role is supported, consider renaming the header to X-Auth-Role (singular) for clarity.
| String role = rolesHeader.trim(); | |
| if (!role.startsWith("ROLE_")) { | |
| role = "ROLE_" + role; | |
| } | |
| authorities.add(new SimpleGrantedAuthority(role)); | |
| log.debug( | |
| "Created authentication for userId: {}, username: {}, role: {}", | |
| userId, | |
| usernameHeader, | |
| role); | |
| String[] roles = rolesHeader.split(","); | |
| List<String> parsedRoles = new ArrayList<>(); | |
| for (String r : roles) { | |
| String role = r.trim(); | |
| if (!role.startsWith("ROLE_")) { | |
| role = "ROLE_" + role; | |
| } | |
| authorities.add(new SimpleGrantedAuthority(role)); | |
| parsedRoles.add(role); | |
| } | |
| log.debug( | |
| "Created authentication for userId: {}, username: {}, roles: {}", | |
| userId, | |
| usernameHeader, | |
| parsedRoles); |
| * <p>Gateway에서 전달하는 헤더: - X-Auth-User-Id: 사용자 ID (Long) - X-Auth-Username: 사용자 이메일/username | ||
| * (String) - X-Auth-Roles: 사용자 역할 (예: ROLE_USER, ROLE_ADMIN) |
Copilot
AI
Dec 5, 2025
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.
[nitpick] The Javadoc comment states "Gateway에서 전달한 인증 헤더(X-Auth-*)를 기반으로..." but the actual description format has a line break issue. The description text appears on line 25 without proper spacing from the <p> tag on line 25.
Consider reformatting for better readability:
/**
* Gateway에서 전달한 인증 헤더(X-Auth-*)를 기반으로 Spring Security의 Authentication을 생성하는 필터
*
* <p>Gateway에서 전달하는 헤더:
* <ul>
* <li>X-Auth-User-Id: 사용자 ID (Long)</li>
* <li>X-Auth-Username: 사용자 이메일/username (String)</li>
* <li>X-Auth-Roles: 사용자 역할 (예: ROLE_USER, ROLE_ADMIN)</li>
* </ul>
*/| * <p>Gateway에서 전달하는 헤더: - X-Auth-User-Id: 사용자 ID (Long) - X-Auth-Username: 사용자 이메일/username | |
| * (String) - X-Auth-Roles: 사용자 역할 (예: ROLE_USER, ROLE_ADMIN) | |
| * <p>Gateway에서 전달하는 헤더: | |
| * <ul> | |
| * <li>X-Auth-User-Id: 사용자 ID (Long)</li> | |
| * <li>X-Auth-Username: 사용자 이메일/username (String)</li> | |
| * <li>X-Auth-Roles: 사용자 역할 (예: ROLE_USER, ROLE_ADMIN)</li> | |
| * </ul> |
📋 상세 설명
📊 체크리스트
📆 마감일