Skip to content
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

✨ Feature/#27 - 4.4 가게 방문 인증용 영수증 OCR 조회하기 API 구현 #36

Merged
merged 1 commit into from
Nov 19, 2024

Conversation

dongkyeomjang
Copy link
Collaborator

@dongkyeomjang dongkyeomjang commented Nov 19, 2024

Related issue 🛠

closed #27

어떤 변경사항이 있었나요?

  • 🐞 BugFix Something isn't working
  • 💻 CrossBrowsing Browser compatibility
  • 🌏 Deploy Deploy
  • 🎨 Design Markup & styling
  • 📃 Docs Documentation writing and editing (README.md, etc.)
  • ✨ Feature Feature
  • 🔨 Refactor Code refactoring
  • ⚙️ Setting Development environment setup
  • ✅ Test Test related (storybook, jest, etc.)

CheckPoint ✅

PR이 다음 요구 사항을 충족하는지 확인하세요.

  • PR 컨벤션에 맞게 작성했습니다. (필수)
  • Docker Container 환경으로 변경 사항에 대한 테스트를 했습니다.(버그 수정/기능에 대한 테스트). (필수)
  • Feature의 경우, API 문서 업데이트를 완료했습니다. (선택)
  • BugFix의 경우, 버그의 원인을 파악하였습니다. (선택)

Work Description ✏️

  • ✨ Feature - 4.4 가게 방문 인증용 영수증 OCR 조회하기 API 구현

Uncompleted Tasks 😅

N/A

To Reviewers 📢

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced a new API endpoint for receipt OCR processing, allowing users to upload receipt images for recognition.
    • Added a new service and controller for handling receipt OCR requests.
    • Implemented a structured response DTO for receipt OCR results.
  • Bug Fixes

    • Enhanced error handling for OCR recognition failures.
  • Documentation

    • Updated API documentation to include details for the new receipt OCR endpoint.
  • Chores

    • Improved date formatting capabilities within the application.

@dongkyeomjang dongkyeomjang added ✨ Feature 기능 개발 dongkyeomjang assigned to dongkyeomjang labels Nov 19, 2024
@dongkyeomjang dongkyeomjang self-assigned this Nov 19, 2024
Copy link

coderabbitai bot commented Nov 19, 2024

Walkthrough

The changes introduce a new API endpoint for receipt Optical Character Recognition (OCR) functionality and enhance existing components related to this feature. A new controller, service, and DTOs are added to manage the OCR process, including handling requests and responses. The error handling capabilities are expanded with a new error code for OCR recognition failures. Additionally, utility classes are modified to support the new functionality, and the API documentation is updated to reflect these changes.

Changes

File Change Summary
http/onjung/OnjungControllerHttpRequest.http Added new POST endpoint for receipt OCR and updated existing endpoint formatting.
src/main/java/com/daon/onjung/core/dto/ReceiptOCRDto.java Introduced ReceiptOCRDto and related records for receipt data structure.
src/main/java/com/daon/onjung/core/exception/error/ErrorCode.java Added new error code OCR_RECOGNITION_ERROR for handling OCR failures.
src/main/java/com/daon/onjung/core/utility/DateTimeUtil.java Added method for flexible date string formatting.
src/main/java/com/daon/onjung/core/utility/OCRUtil.java Created utility class for handling OCR requests, including methods for URL, headers, and request body.
src/main/java/com/daon/onjung/core/utility/RestClientUtil.java Added method for sending multipart form data POST requests and updated GET method for authorization.
src/main/java/com/daon/onjung/onjung/application/controller/command/OnjungCommandV1Controller.java Introduced controller for handling OCR requests.
src/main/java/com/daon/onjung/onjung/application/dto/response/ReceiptOCRResponseDto.java Added DTO for OCR response handling with validation logic.
src/main/java/com/daon/onjung/onjung/application/service/ReceiptOCRService.java Created service class for processing OCR requests.
src/main/java/com/daon/onjung/onjung/application/usecase/ReceiptOCRUseCase.java Introduced interface for OCR use case.

Assessment against linked issues

Objective Addressed Explanation
Implement receipt OCR retrieval API (Issue #27)

Possibly related issues

🐰 "In fields of green, with receipts in hand,
We scan and we read, oh isn't it grand!
With OCR magic, our work's now a breeze,
No more lost details, just joy and ease!
Let's hop with delight, for features anew,
A world of receipts, all clear and true!" 🌼


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 15

🧹 Outside diff range and nitpick comments (17)
src/main/java/com/daon/onjung/onjung/application/usecase/ReceiptOCRUseCase.java (1)

7-10: LGTM! Consider adding interface documentation.

The interface follows clean architecture principles and has a clear single responsibility. Consider adding Javadoc to document:

  • Expected file formats and size limits
  • Possible exceptions that might be thrown
  • Example usage
 @UseCase
+/**
+ * Use case for processing receipt images through OCR.
+ *
+ * @throws IllegalArgumentException if the file is empty or invalid
+ * @throws OCRProcessingException if OCR processing fails
+ */
 public interface ReceiptOCRUseCase {
+    /**
+     * Processes the receipt image using OCR.
+     *
+     * @param file the receipt image file (supported formats: JPEG, PNG)
+     * @return DTO containing the extracted receipt data
+     */
     ReceiptOCRResponseDto execute(MultipartFile file);
 }
src/main/java/com/daon/onjung/onjung/application/controller/command/OnjungCommandV1Controller.java (2)

12-16: Consider improving class naming and API path structure

While the implementation is correct, consider these improvements:

  1. The class name OnjungCommandV1Controller is too generic. Consider renaming to ReceiptOCRCommandController to better reflect its specific responsibility.
  2. Add a class-level @RequestMapping("/api/v1") to avoid path repetition and make versioning easier to maintain.
 @RestController
 @RequiredArgsConstructor
-public class OnjungCommandV1Controller {
+@RequestMapping("/api/v1")
+public class ReceiptOCRCommandController {

1-27: Consider Docker configuration for OCR service

Since this controller is part of a store visit verification system and Docker testing is pending, ensure that:

  1. The OCR service dependencies are properly configured in the Docker environment
  2. Resource limits are set appropriately for OCR processing
  3. Temporary storage for uploaded files is configured correctly

Consider adding the following to your Docker configuration:

  • Memory limits suitable for OCR processing
  • Temporary storage volume for file uploads
  • Health check endpoint for the OCR service
src/main/java/com/daon/onjung/onjung/application/service/ReceiptOCRService.java (1)

1-36: Consider async processing and resilience patterns

Since OCR processing can be time-consuming and relies on external services, consider these architectural improvements:

  1. Implement async processing using @Async or reactive programming
  2. Add circuit breaker pattern for external service calls
  3. Configure timeouts and rate limiting
  4. Consider implementing a retry mechanism

Would you like me to provide implementation examples for any of these suggestions?

src/main/java/com/daon/onjung/onjung/application/dto/response/ReceiptOCRResponseDto.java (2)

25-33: Improve constructor robustness.

The constructor should validate parameters before object construction to prevent partially initialized objects.

     @Builder
     public ReceiptOCRResponseDto(String storeName, String storeAddress, String paymentDate, String paymentAmount) {
+        validateParameters(storeName, storeAddress, paymentDate, paymentAmount);
         this.storeName = storeName;
         this.storeAddress = storeAddress;
         this.paymentDate = paymentDate;
-        this.paymentAmount = paymentAmount;
+        this.paymentAmount = new BigDecimal(paymentAmount);
         this.validateSelf();
     }
+
+    private void validateParameters(String storeName, String storeAddress, String paymentDate, String paymentAmount) {
+        if (paymentAmount != null && !paymentAmount.matches("^\\d+(\\.\\d{1,2})?$")) {
+            throw new IllegalArgumentException("Invalid payment amount format");
+        }
+        // Add other parameter validations as needed
+    }

1-59: Consider architectural improvements.

  1. Consider implementing caching for OCR results to improve performance and reduce API calls:

    • Use a distributed cache (e.g., Redis) to store OCR results
    • Cache key could be based on receipt image hash or metadata
    • Set appropriate TTL based on business requirements
  2. Add logging for better observability:

    • Log OCR processing steps
    • Include correlation IDs for request tracing
    • Log performance metrics

Would you like me to provide implementation details for these suggestions?

src/main/java/com/daon/onjung/core/exception/error/ErrorCode.java (1)

28-29: LGTM! Consider adding error code range documentation.

The new OCR error category and code follow the established patterns correctly. The error code (40010) aligns with the HTTP status (400) and maintains consistency with the existing structure.

Consider adding a comment at the top of the file documenting the error code ranges for each category to aid maintainability:

/**
 * Error Code Ranges:
 * 400xx - Invalid Argument Errors
 * 401xx - Unauthorized Errors
 * 403xx - Access Denied Errors
 * 404xx - Not Found Errors
 * 405xx - Method Not Allowed Errors
 * 500xx - Internal Server Errors
 * 502xx - External Server Errors
 */
src/main/java/com/daon/onjung/core/utility/OCRUtil.java (3)

19-30: Add class-level documentation and validate configuration values.

Consider adding:

  1. JavaDoc documentation explaining the class's purpose and responsibilities
  2. Validation for configuration values to ensure they're not null/empty at startup
 @Component
+@Validated
 public class OCRUtil {
+    /**
+     * Utility class for handling OCR (Optical Character Recognition) operations.
+     * Manages communication with Naver Cloud OCR service for receipt processing.
+     */
 
     @Value("${naver-cloud.ocr.url}")
+    @NotBlank(message = "OCR URL must not be blank")
     private String ocrUrl;
 
     @Value("${naver-cloud.ocr.path}")
+    @NotBlank(message = "OCR path must not be blank")
     private String ocrPath;
 
     @Value("${naver-cloud.ocr.key}")
+    @NotBlank(message = "OCR key must not be blank")
     private String ocrKey;

33-37: Add URL validation and logging.

The URL construction should be validated and logged for debugging purposes.

+    private static final Logger log = LoggerFactory.getLogger(OCRUtil.class);
+
     public String createOCRRequestUrl() {
-        return UriComponentsBuilder.fromHttpUrl(ocrUrl)
+        String url = UriComponentsBuilder.fromHttpUrl(ocrUrl)
                 .path(ocrPath)
                 .toUriString();
+        try {
+            new URL(url); // Validate URL format
+            log.debug("Created OCR request URL: {}", url);
+            return url;
+        } catch (MalformedURLException e) {
+            throw new IllegalStateException("Invalid OCR URL configuration", e);
+        }
     }

39-44: Enhance headers configuration.

The headers setup could be more comprehensive for multipart requests.

     public HttpHeaders createOCRRequestHeaders() {
         HttpHeaders headers = new HttpHeaders();
         headers.add("X-OCR-SECRET", ocrKey);
-        headers.setContentType(org.springframework.http.MediaType.MULTIPART_FORM_DATA);
+        headers.setContentType(MediaType.MULTIPART_FORM_DATA);
+        headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
         return headers;
     }
src/main/java/com/daon/onjung/core/utility/RestClientUtil.java (2)

36-36: Address TODO: Consider splitting responsibilities

The TODO comment highlights a valid concern about dependency separation. The class currently handles multiple responsibilities (HTTP operations, error handling, response mapping).

Consider:

  1. Extract error handling into a separate HttpErrorHandler class
  2. Create a dedicated ResponseMapper for JSON conversions
  3. Keep only HTTP client operations in this class

This would improve maintainability and make the code more testable.


Line range hint 22-22: Add timeout configuration and retry mechanism

The RestClient is created without timeout configuration, which could lead to hanging requests in production.

Consider configuring the RestClient with appropriate timeouts and retry mechanism:

- private final RestClient restClient = RestClient.create();
+ private final RestClient restClient = RestClient.builder()
+     .requestFactory(clientHttpRequestFactory())
+     .build();
+
+ private ClientHttpRequestFactory clientHttpRequestFactory() {
+     HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
+     factory.setConnectTimeout(Duration.ofSeconds(5));
+     factory.setReadTimeout(Duration.ofSeconds(10));
+     return factory;
+ }
src/main/java/com/daon/onjung/core/utility/DateTimeUtil.java (1)

24-49: Consider optimizing the date format patterns.

While the list is comprehensive, consider the following improvements:

  1. Some patterns might be redundant as DateTimeFormatter can handle single-digit months/days by default
  2. Consider adding common receipt formats like:
    • "dd/MM/yyyy"
    • "MM/dd/yyyy"
    • Time-included formats (for receipts with timestamps)
 private static final List<DateTimeFormatter> DATE_FORMATTERS = List.of(
     DateTimeFormatter.ofPattern("yyyyMMdd"),
     DateTimeFormatter.ofPattern("yyyyMMdd(E)"),
     DateTimeFormatter.ofPattern("yyyyMMdd (E)"),
     DateTimeFormatter.ofPattern("yyyyMMdd E"),
     DateTimeFormatter.ofPattern("yyyy-MM-dd"),
-    DateTimeFormatter.ofPattern("yyyy-M-d"),  // Redundant
     DateTimeFormatter.ofPattern("yyyy-MM-dd(E)"),
     DateTimeFormatter.ofPattern("yyyy-MM-dd (E)"),
     DateTimeFormatter.ofPattern("yyyy-MM-dd E"),
     DateTimeFormatter.ofPattern("yyyy/MM/dd"),
-    DateTimeFormatter.ofPattern("yyyy/M/d"),  // Redundant
     DateTimeFormatter.ofPattern("yyyy/MM/dd(E)"),
     DateTimeFormatter.ofPattern("yyyy/MM/dd (E)"),
     DateTimeFormatter.ofPattern("yyyy/MM/dd E"),
+    DateTimeFormatter.ofPattern("dd/MM/yyyy"), // Common in receipts
+    DateTimeFormatter.ofPattern("MM/dd/yyyy"), // Common in receipts
+    DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"), // For timestamps
     // ... rest of the patterns
 );
src/main/java/com/daon/onjung/core/dto/ReceiptOCRDto.java (4)

6-253: Consider extracting nested records into separate files for better organization

The ReceiptOCRDto class contains multiple nested records, which can make the class lengthy and harder to navigate. For better code organization and maintainability, consider moving some of the nested records into separate class files. This approach enhances readability and adheres to best practices for class design.


77-89: Clarify the naming of the tel field in StoreInfo

In the StoreInfo record, the field tel (lines 87-88) is a list of TextBlock, suggesting multiple telephone numbers. If only one telephone number is expected, consider changing the type to a single TextBlock. If multiple numbers are possible, consider renaming the field to tels or telephoneNumbers for clarity.


109-148: Combine DateInfo and TimeInfo into a unified DateTimeInfo record

The DateInfo (lines 109-127) and TimeInfo (lines 130-148) records share similar structures, both representing temporal information with identical fields except for their formatted property. Consider merging them into a single DateTimeInfo record to reduce code duplication and improve maintainability.

Here's a conceptual example of how you might refactor the code:

- public record DateInfo(
+ public record DateTimeInfo(
    @JsonProperty("text")
    String text,

-   @JsonProperty("formatted")
-   FormattedDate formatted,
+   @JsonProperty("formatted")
+   FormattedDateTime formatted,

    @JsonProperty("keyText")
    String keyText,

    @JsonProperty("confidenceScore")
    Double confidenceScore,

    @JsonProperty("boundingPolys")
    List<Polygon> boundingPolys,

    @JsonProperty("maskingPolys")
    List<Polygon> maskingPolys
) {}

You would also need to adjust the formatted property to accommodate both date and time formats.


197-203: Avoid naming conflicts by renaming the Price record or its fields

Having a Price record with a field named price (lines 198-199) can lead to confusion. To improve clarity, consider renaming the record to PriceInfo and the fields to more descriptive names, such as totalPrice and unitPrice.

Apply this diff to implement the suggestion:

- public record Price(
+ public record PriceInfo(
    @JsonProperty("price")
-   TextBlock price,
+   TextBlock totalPrice,

    @JsonProperty("unitPrice")
    TextBlock unitPrice
) {}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between d41b674 and b4d14da.

📒 Files selected for processing (10)
  • http/onjung/OnjungControllerHttpRequest.http (1 hunks)
  • src/main/java/com/daon/onjung/core/dto/ReceiptOCRDto.java (1 hunks)
  • src/main/java/com/daon/onjung/core/exception/error/ErrorCode.java (1 hunks)
  • src/main/java/com/daon/onjung/core/utility/DateTimeUtil.java (3 hunks)
  • src/main/java/com/daon/onjung/core/utility/OCRUtil.java (1 hunks)
  • src/main/java/com/daon/onjung/core/utility/RestClientUtil.java (3 hunks)
  • src/main/java/com/daon/onjung/onjung/application/controller/command/OnjungCommandV1Controller.java (1 hunks)
  • src/main/java/com/daon/onjung/onjung/application/dto/response/ReceiptOCRResponseDto.java (1 hunks)
  • src/main/java/com/daon/onjung/onjung/application/service/ReceiptOCRService.java (1 hunks)
  • src/main/java/com/daon/onjung/onjung/application/usecase/ReceiptOCRUseCase.java (1 hunks)
🔇 Additional comments (9)
http/onjung/OnjungControllerHttpRequest.http (2)

14-15: LGTM! Improved formatting

The added newline after the authorization header improves readability and follows HTTP request formatting conventions.


19-20: Verify multipart configuration

Ensure that the server is properly configured to handle multipart file uploads with appropriate size limits.

✅ Verification successful

Multipart configuration is properly implemented

The server is correctly configured to handle multipart file uploads:

  • The controller OnjungCommandV1Controller properly declares multipart handling with @PostMapping(consumes = "multipart/form-data")
  • The endpoint correctly uses @RequestPart annotation with MultipartFile for file upload handling
  • Spring Boot's default multipart configuration is sufficient for basic file uploads, as no custom size limits or configurations are overridden
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for multipart configuration in application properties and configuration files
echo "Searching for multipart configuration..."
rg -l "multipart" --type properties --type yaml
rg -l "MultipartConfig|@MultipartConfig" --type java

Length of output: 228


Script:

#!/bin/bash
# Search for multipart configuration with broader file patterns
echo "Searching for multipart configuration in properties and yaml files..."
fd -e properties -e yml -e yaml | xargs rg -l "multipart"

echo -e "\nSearching for multipart configuration in Java files..."
fd -e java | xargs rg -l "multipart|MultipartConfig|@MultipartConfig"

echo -e "\nSearching for Spring Boot configuration classes..."
ast-grep --pattern 'class $_ {
  $$$
  @Configuration
  $$$
}'

Length of output: 1221


Script:

#!/bin/bash
# Check the actual multipart configuration implementation
echo "Checking OnjungCommandV1Controller implementation..."
rg -A 10 "multipart" src/main/java/com/daon/onjung/onjung/application/controller/command/OnjungCommandV1Controller.java

echo -e "\nChecking Spring configuration for multipart..."
fd -e java -e properties -e yml -e yaml | xargs rg "spring\..*multipart|@EnableConfigurationProperties"

Length of output: 962

src/main/java/com/daon/onjung/onjung/application/controller/command/OnjungCommandV1Controller.java (2)

1-11: LGTM! Clean and well-organized imports

The package structure and imports are appropriate for a REST controller implementing the command pattern.


18-26: ⚠️ Potential issue

Add input validation and API documentation

The endpoint implementation has several security considerations that should be addressed:

  1. Missing input validation for the file parameter
  2. No file size limits specified
  3. No file type restrictions
  4. Missing API documentation annotations

Apply these security improvements:

+    @Operation(summary = "Process receipt OCR", description = "Upload receipt image for OCR processing")
+    @ApiResponse(responseCode = "200", description = "Successfully processed OCR")
+    @ApiResponse(responseCode = "400", description = "Invalid file format or size")
     @PostMapping(value = "/api/v1/receipts/ocr", consumes = "multipart/form-data")
     public ResponseDto<ReceiptOCRResponseDto> OCRReceipt(
-            @RequestPart(value = "file") MultipartFile file
+            @RequestPart(value = "file") 
+            @Parameter(description = "Receipt image file (JPEG/PNG only, max 5MB)")
+            @ValidImage(maxSize = 5_242_880, types = {"image/jpeg", "image/png"}) 
+            MultipartFile file
     ) {
+        if (file == null || file.isEmpty()) {
+            throw new InvalidInputException("Receipt image file is required");
+        }
         return ResponseDto.ok(receiptOCRUseCase.execute(file));
     }

Let's verify if similar validation exists in other controllers:

src/main/java/com/daon/onjung/onjung/application/service/ReceiptOCRService.java (1)

1-19: Verify OCR utility configurations and dependencies

The service structure looks good, following Spring best practices. However, we should verify the configuration of the utility classes.

src/main/java/com/daon/onjung/core/exception/error/ErrorCode.java (1)

29-29: Verify OCR error handling implementation.

Let's ensure this error code is properly implemented and there are no duplicate OCR-related error codes.

✅ Verification successful

Let me check the implementation details of the OCR error handling to ensure it's properly used.


Error code implementation is appropriate and non-redundant

The OCR_RECOGNITION_ERROR code is:

  • Properly implemented as the only OCR-related error code in ErrorCode.java
  • Correctly used in ReceiptOCRResponseDto to handle OCR processing failures
  • Well-integrated with the OCR-related components (ReceiptOCRService, ReceiptOCRUseCase) without any duplicate error handling patterns

The error code serves its intended purpose without redundancy in the codebase.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for OCR-related error handling implementation and potential duplicates

# Check for OCR error handling usage
echo "Checking OCR error handling implementation..."
rg -l "OCR_RECOGNITION_ERROR" 

# Check for other potential OCR-related error codes
echo "Checking for other OCR-related error codes..."
rg -i "ocr.*error|error.*ocr" --type java

Length of output: 818


Script:

#!/bin/bash
# Get the context of OCR error usage in ReceiptOCRResponseDto
echo "Checking OCR error usage context..."
rg -B 5 -A 5 "OCR_RECOGNITION_ERROR" src/main/java/com/daon/onjung/onjung/application/dto/response/ReceiptOCRResponseDto.java

# Check if there are any other OCR-related classes or methods
echo "Checking OCR-related implementations..."
rg -l -i "ocr|receipt.*recognition"

Length of output: 1698

src/main/java/com/daon/onjung/core/utility/RestClientUtil.java (1)

55-73: Verify handling of large OCR image uploads

Since this method will be used for OCR image processing, we need to ensure proper handling of large files.

src/main/java/com/daon/onjung/core/utility/DateTimeUtil.java (1)

8-9: LGTM!

The new imports are necessary and properly used for the new date parsing functionality.

src/main/java/com/daon/onjung/core/dto/ReceiptOCRDto.java (1)

55-58: Validate the estimatedLanguage field in Meta

The estimatedLanguage field (lines 56-57) holds a language code. Consider validating this field against a list of supported language codes to ensure correctness, and handle any unexpected values appropriately.

You can verify the usage of estimatedLanguage in the codebase:


@UseCase
public interface ReceiptOCRUseCase {
ReceiptOCRResponseDto execute(MultipartFile file);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

Add input validation for file uploads before OCR processing

The implementation lacks necessary file validation checks. The service directly processes the MultipartFile without any validation:

  • No file size limits are enforced
  • No file type validation (currently hardcoded as "jpg" in OCRUtil)
  • No null or empty file checks
  • No malware scanning

Add validation in ReceiptOCRService.execute() before processing:

public ReceiptOCRResponseDto execute(MultipartFile file) {
    if (file == null || file.isEmpty()) {
        throw new IllegalArgumentException("File cannot be empty");
    }
    if (!file.getContentType().startsWith("image/")) {
        throw new IllegalArgumentException("Only image files are allowed");
    }
    // ... existing code
}

Also consider:

  1. Adding multipart configuration in application.properties/yml for max file size
  2. Implementing virus scanning if handling sensitive documents
  3. Adding proper image format validation beyond just MIME type
🔗 Analysis chain

Add input validation for file uploads.

The execute method accepts a MultipartFile without any apparent validation. Consider:

  1. File size limits
  2. Allowed file types (JPEG, PNG)
  3. Basic image validation
  4. Malware scanning

Let's check if there's any validation in the implementation:


Let me search for more specific information about the implementation and file handling.


Let me check for any application configuration files that might contain file upload settings and the implementation details of the OCR service.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Look for file validation logic in the implementation
# Expected: Find MultipartFile validation in the service layer

# Search for the implementation class
ast-grep --pattern 'class $_ implements ReceiptOCRUseCase {
  $$$
}'

# Search for validation utilities
rg -l "MultipartFile.*validate|checkFile|isValid"

Length of output: 457


Script:

#!/bin/bash
# Search for the implementation of ReceiptOCRUseCase
rg -l "class.*implements.*ReceiptOCRUseCase"

# Look for MultipartFile handling in the codebase
rg "MultipartFile" -A 5

# Check for any existing file size or type configurations
rg -l "multipart|file\.(size|type|extension)"

Length of output: 10886


Script:

#!/bin/bash
# Check application properties/yml files for multipart configuration
rg -l "multipart" --glob "*.properties" --glob "*.yml"

# Look at the implementation of ReceiptOCRService
rg -A 20 "class ReceiptOCRService implements ReceiptOCRUseCase" 

# Check OCRUtil implementation for any validation
rg -A 10 "createOCRRequestBody.*MultipartFile"

Length of output: 3744

Comment on lines +16 to +26
// @no-log
POST {{host_url}}/api/v1/receipts/ocr
Content-Type: multipart/form-data; boundary=boundary
Authorization: Bearer {{access_token}}

--boundary
Content-Disposition: form-data; name="file"; filename="receipt.jpg"
Content-Type: image/jpeg

< /Users/kyeom/Desktop/receipt3.jpeg
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve OCR endpoint documentation

While the basic structure is good, there are several improvements needed:

  1. Replace the hardcoded local file path with a placeholder or relative path
  2. Add example response structure
  3. Document file size limits and supported formats

Apply this diff:

 ### 4.4 가게 방문 인증용 영수증 OCR 조회하기
 // @no-log
 POST {{host_url}}/api/v1/receipts/ocr
 Content-Type: multipart/form-data; boundary=boundary
 Authorization: Bearer {{access_token}}
 
 --boundary
 Content-Disposition: form-data; name="file"; filename="receipt.jpg"
 Content-Type: image/jpeg
 
-< /Users/kyeom/Desktop/receipt3.jpeg
+< ./test-data/sample-receipt.jpg
+
+### Example Response
+{
+  "success": true,
+  "data": {
+    // Add expected OCR response structure here
+  }
+}
+
+### Notes
+// Maximum file size: [specify limit]
+// Supported formats: JPEG, PNG, etc.

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +26 to +34
try {
return ReceiptOCRResponseDto.fromEntity(
ocrUtil.mapToReceiptOCRDto(
restClientUtil.sendMultipartFormDataPostMethod(url, headers, body)
)
);
} catch (Exception e) {
throw new RuntimeException("OCR 요청 중 오류가 발생했습니다. {}", e);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Improve error handling and message formatting

The current error handling has several issues:

  1. Using generic RuntimeException instead of specific exceptions
  2. Incorrect string formatting syntax
  3. Loss of original error context

Apply these improvements:

-        try {
-            return ReceiptOCRResponseDto.fromEntity(
-                    ocrUtil.mapToReceiptOCRDto(
-                            restClientUtil.sendMultipartFormDataPostMethod(url, headers, body)
-                    )
-            );
-        } catch (Exception e) {
-            throw new RuntimeException("OCR 요청 중 오류가 발생했습니다. {}", e);
-        }
+        try {
+            String response = restClientUtil.sendMultipartFormDataPostMethod(url, headers, body);
+            log.debug("Received OCR response: {}", response);
+            
+            var ocrDto = ocrUtil.mapToReceiptOCRDto(response);
+            return ReceiptOCRResponseDto.fromEntity(ocrDto);
+        } catch (RestClientException e) {
+            log.error("Failed to communicate with OCR service: {}", e.getMessage(), e);
+            throw new OCRServiceException("Failed to process OCR request: " + e.getMessage(), e);
+        } catch (Exception e) {
+            log.error("Unexpected error during OCR processing: {}", e.getMessage(), e);
+            throw new OCRProcessingException("OCR 요청 중 오류가 발생했습니다: " + e.getMessage(), e);
+        }

Also, create specific exception classes:

public class OCRServiceException extends RuntimeException {
    public OCRServiceException(String message, Throwable cause) {
        super(message, cause);
    }
}

public class OCRProcessingException extends RuntimeException {
    public OCRProcessingException(String message, Throwable cause) {
        super(message, cause);
    }
}

Comment on lines +20 to +25
@Override
public ReceiptOCRResponseDto execute(MultipartFile file) {
String url = ocrUtil.createOCRRequestUrl();
HttpHeaders headers = ocrUtil.createOCRRequestHeaders();
MultiValueMap<String, Object> body = ocrUtil.createOCRRequestBody(file);

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add input validation and logging for better observability

The current implementation could benefit from:

  1. Input validation for the MultipartFile
  2. Logging for debugging and monitoring
  3. Breaking down the chained calls for better error tracking

Consider applying these improvements:

 @Override
 public ReceiptOCRResponseDto execute(MultipartFile file) {
+    if (file == null || file.isEmpty()) {
+        throw new IllegalArgumentException("Receipt file is required");
+    }
+    log.debug("Processing receipt OCR for file: {}", file.getOriginalFilename());
+
     String url = ocrUtil.createOCRRequestUrl();
     HttpHeaders headers = ocrUtil.createOCRRequestHeaders();
     MultiValueMap<String, Object> body = ocrUtil.createOCRRequestBody(file);
+    log.debug("Prepared OCR request for URL: {}", url);

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +11 to +24
public class ReceiptOCRResponseDto extends SelfValidating<ReceiptOCRDto> {

@JsonProperty("store_name")
private final String storeName;

@JsonProperty("store_address")
private final String storeAddress;

@JsonProperty("payment_date")
private final String paymentDate;

@JsonProperty("payment_amount")
private final String paymentAmount;

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider improving type safety and documentation.

  1. The paymentAmount field should be a numeric type (e.g., BigDecimal) instead of String to ensure precise monetary calculations and prevent potential parsing issues.
  2. Add validation annotations (e.g., @NotNull, @Pattern) to enforce data integrity since the class extends SelfValidating.
  3. Include Javadoc documentation for the class and fields to improve maintainability.
+/**
+ * Response DTO for OCR receipt data containing store and payment information.
+ */
 public class ReceiptOCRResponseDto extends SelfValidating<ReceiptOCRDto> {
 
+    /** The name of the store from the receipt. */
+    @NotNull(message = "Store name cannot be null")
     @JsonProperty("store_name")
     private final String storeName;
 
+    /** The address of the store from the receipt. */
+    @NotNull(message = "Store address cannot be null")
     @JsonProperty("store_address")
     private final String storeAddress;
 
+    /** The payment date in formatted string (e.g., YYYY-MM-DD). */
+    @NotNull(message = "Payment date cannot be null")
+    @Pattern(regexp = "\\d{4}-\\d{2}-\\d{2}", message = "Invalid date format")
     @JsonProperty("payment_date")
     private final String paymentDate;
 
+    /** The payment amount from the receipt. */
+    @NotNull(message = "Payment amount cannot be null")
     @JsonProperty("payment_amount")
-    private final String paymentAmount;
+    private final BigDecimal paymentAmount;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public class ReceiptOCRResponseDto extends SelfValidating<ReceiptOCRDto> {
@JsonProperty("store_name")
private final String storeName;
@JsonProperty("store_address")
private final String storeAddress;
@JsonProperty("payment_date")
private final String paymentDate;
@JsonProperty("payment_amount")
private final String paymentAmount;
/**
* Response DTO for OCR receipt data containing store and payment information.
*/
public class ReceiptOCRResponseDto extends SelfValidating<ReceiptOCRDto> {
/** The name of the store from the receipt. */
@NotNull(message = "Store name cannot be null")
@JsonProperty("store_name")
private final String storeName;
/** The address of the store from the receipt. */
@NotNull(message = "Store address cannot be null")
@JsonProperty("store_address")
private final String storeAddress;
/** The payment date in formatted string (e.g., YYYY-MM-DD). */
@NotNull(message = "Payment date cannot be null")
@Pattern(regexp = "\\d{4}-\\d{2}-\\d{2}", message = "Invalid date format")
@JsonProperty("payment_date")
private final String paymentDate;
/** The payment amount from the receipt. */
@NotNull(message = "Payment amount cannot be null")
@JsonProperty("payment_amount")
private final BigDecimal paymentAmount;

Comment on lines +55 to +73
public JSONObject sendMultipartFormDataPostMethod(String url, HttpHeaders headers, MultiValueMap<String, Object> body) {
try {
return new JSONObject(Objects.requireNonNull(restClient.post()
.uri(url)
.headers(httpHeaders -> httpHeaders.addAll(headers))
.contentType(MediaType.MULTIPART_FORM_DATA)
.body(body)
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError, (request, response) -> {
throw new CommonException(ErrorCode.INVALID_ARGUMENT);
})
.onStatus(HttpStatusCode::is5xxServerError, (request, response) -> {
throw new CommonException(ErrorCode.INTERNAL_SERVER_ERROR);
})
.toEntity(JSONObject.class).getBody()));
} catch (Exception e) {
throw new CommonException(ErrorCode.INTERNAL_SERVER_ERROR);
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider refactoring common error handling logic

The error handling code is duplicated across all methods in this class.

Extract the common error handling into a separate method:

private <T> T handleResponse(ResponseSpec responseSpec) {
    return responseSpec
        .onStatus(HttpStatusCode::is4xxClientError, 
            (request, response) -> {
                throw new CommonException(ErrorCode.INVALID_ARGUMENT);
            })
        .onStatus(HttpStatusCode::is5xxServerError, 
            (request, response) -> {
                throw new CommonException(ErrorCode.INTERNAL_SERVER_ERROR);
            })
        .toEntity(JSONObject.class)
        .getBody();
}

Then simplify the methods to:

public JSONObject sendMultipartFormDataPostMethod(...) {
    try {
        return new JSONObject(Objects.requireNonNull(
            handleResponse(restClient.post()
                .uri(url)
                .headers(httpHeaders -> httpHeaders.addAll(headers))
                .contentType(MediaType.MULTIPART_FORM_DATA)
                .body(body)
                .retrieve())
        ));
    } catch (Exception e) {
        log.error("Failed to process multipart request", e);
        throw new CommonException(ErrorCode.INTERNAL_SERVER_ERROR, e);
    }
}

Comment on lines +70 to +72
} catch (Exception e) {
throw new CommonException(ErrorCode.INTERNAL_SERVER_ERROR);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Improve error handling to preserve context

The generic catch block swallows the original exception details and always throws INTERNAL_SERVER_ERROR. This makes debugging harder.

Consider:

- } catch (Exception e) {
-     throw new CommonException(ErrorCode.INTERNAL_SERVER_ERROR);
+ } catch (Exception e) {
+     log.error("Failed to process multipart request", e);
+     throw new CommonException(ErrorCode.INTERNAL_SERVER_ERROR, e);

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +173 to +189
/**
* 날짜 문자열을 특정 포맷으로 변환
*
* @param dateString 날짜 문자열
* @return String
*/
public static String formatDateString(String dateString) {
for (DateTimeFormatter formatter : DATE_FORMATTERS) {
try {
LocalDate date = LocalDate.parse(dateString, formatter);
return date.format(ISODateFormatter);
} catch (DateTimeParseException e) {
// Ignore and try the next format
}
}
throw new IllegalArgumentException("Unrecognized date format: " + dateString);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance error handling and documentation.

The method implementation is solid, but consider these improvements:

  1. Add input validation
  2. Enhance documentation with examples
  3. Consider using a custom exception for better error handling
 /**
  * 날짜 문자열을 특정 포맷으로 변환
  *
  * @param dateString 날짜 문자열
+ * @return ISO formatted date string (yyyy-MM-dd)
+ * @throws IllegalArgumentException if the date string is null, empty, or in an unrecognized format
+ * @example
+ *   formatDateString("20231101") returns "2023-11-01"
+ *   formatDateString("2023년 11월 01일") returns "2023-11-01"
  */
 public static String formatDateString(String dateString) {
+    if (dateString == null || dateString.trim().isEmpty()) {
+        throw new IllegalArgumentException("Date string cannot be null or empty");
+    }
+    
+    dateString = dateString.trim();
     
     for (DateTimeFormatter formatter : DATE_FORMATTERS) {
         try {
             LocalDate date = LocalDate.parse(dateString, formatter);
             return date.format(ISODateFormatter);
         } catch (DateTimeParseException e) {
-            // Ignore and try the next format
+            // Continue to the next format
         }
     }
-    throw new IllegalArgumentException("Unrecognized date format: " + dateString);
+    throw new IllegalArgumentException(String.format(
+        "Unrecognized date format: '%s'. Expected formats: yyyy-MM-dd, yyyyMMdd, etc.",
+        dateString
+    ));
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* 날짜 문자열을 특정 포맷으로 변환
*
* @param dateString 날짜 문자열
* @return String
*/
public static String formatDateString(String dateString) {
for (DateTimeFormatter formatter : DATE_FORMATTERS) {
try {
LocalDate date = LocalDate.parse(dateString, formatter);
return date.format(ISODateFormatter);
} catch (DateTimeParseException e) {
// Ignore and try the next format
}
}
throw new IllegalArgumentException("Unrecognized date format: " + dateString);
}
/**
* 날짜 문자열을 특정 포맷으로 변환
*
* @param dateString 날짜 문자열
* @return ISO formatted date string (yyyy-MM-dd)
* @throws IllegalArgumentException if the date string is null, empty, or in an unrecognized format
* @example
* formatDateString("20231101") returns "2023-11-01"
* formatDateString("2023년 11월 01일") returns "2023-11-01"
*/
public static String formatDateString(String dateString) {
if (dateString == null || dateString.trim().isEmpty()) {
throw new IllegalArgumentException("Date string cannot be null or empty");
}
dateString = dateString.trim();
for (DateTimeFormatter formatter : DATE_FORMATTERS) {
try {
LocalDate date = LocalDate.parse(dateString, formatter);
return date.format(ISODateFormatter);
} catch (DateTimeParseException e) {
// Continue to the next format
}
}
throw new IllegalArgumentException(String.format(
"Unrecognized date format: '%s'. Expected formats: yyyy-MM-dd, yyyyMMdd, etc.",
dateString
));
}

💡 Codebase verification

Missing test coverage for DateTimeUtil.formatDateString

The verification reveals:

  1. No test class exists for DateTimeUtil
  2. The method is only used in ReceiptOCRResponseDto for parsing dates from OCR results
  3. No test cases cover the date formatting functionality

Action items:

  • Add unit tests for DateTimeUtil.formatDateString to verify:
    • Successful parsing of various date formats
    • Error handling for invalid date strings
    • Correct ISO format output
  • Add integration tests for ReceiptOCRResponseDto to verify OCR date parsing
🔗 Analysis chain

Verify test coverage and usage in OCR feature.

Since this method is crucial for the OCR receipt feature, let's verify its integration and test coverage.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for test coverage and usage in OCR feature

# Check for test cases
echo "Checking for test cases..."
fd -e java "DateTimeUtilTest" src/test

# Check for usage in OCR related files
echo "Checking usage in OCR feature..."
rg "formatDateString" -A 3 -B 3

# Check for potential date patterns in existing receipt tests
echo "Checking date patterns in receipt tests..."
rg -g "**/*Test.java" -g "**/*Receipt*.java" "(19|20)\d{2}[-/.년]\s*\d{1,2}[-/.월]\s*\d{1,2}"

Length of output: 1977


Script:

#!/bin/bash
# Let's check for test coverage more thoroughly

# Check for any test files related to DateTimeUtil
echo "Checking for DateTimeUtil tests..."
rg -g "**/*Test*.java" "class.*DateTimeUtil.*Test" -A 5

# Check for test methods that might test date formatting
echo "Checking for date format test methods..."
rg -g "**/*Test*.java" "@Test.*format.*Date|test.*format.*Date" -A 10

# Check for any test data with dates in OCR related tests
echo "Checking OCR test data..."
rg -g "**/*Test*.java" -g "**/*OCR*.java" "\"date\".*:.*\".*\"|date.*=.*\".*\"" -A 2

# Check for more usage contexts of formatDateString
echo "Checking all formatDateString usages with wider context..."
rg "formatDateString" -A 5 -B 5 --no-filename

Length of output: 1580

Comment on lines +216 to +234
public record TextBlock(
@JsonProperty("text")
String text,

@JsonProperty("formatted")
FormattedText formatted,

@JsonProperty("keyText")
String keyText,

@JsonProperty("confidenceScore")
Double confidenceScore,

@JsonProperty("boundingPolys")
List<Polygon> boundingPolys,

@JsonProperty("maskingPolys")
List<Polygon> maskingPolys
) {}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Use unmodifiable lists to preserve record immutability

Fields like List<Polygon> boundingPolys (line 229) and List<Polygon> maskingPolys (line 232) in the TextBlock record are mutable collections. To ensure the immutability of records, consider wrapping these lists with Collections.unmodifiableList() or using immutable collections like those provided by Guava or Java's own List.copyOf() method.

Apply this modification within the constructor or factory method:

public TextBlock(
    String text,
    FormattedText formatted,
    String keyText,
    Double confidenceScore,
    List<Polygon> boundingPolys,
    List<Polygon> maskingPolys
) {
    this.text = text;
    this.formatted = formatted;
    this.keyText = keyText;
    this.confidenceScore = confidenceScore;
    this.boundingPolys = List.copyOf(boundingPolys);
    this.maskingPolys = List.copyOf(maskingPolys);
}

Comment on lines +174 to +179
@JsonProperty("company")
TextBlock company,

@JsonProperty("number")
TextBlock number
) {}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Handle potential null values in CardInfo fields

In the CardInfo record (lines 174-179), fields company and number may not always be present in OCR results. Ensure that your application logic can handle null values gracefully to prevent NullPointerException.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
dongkyeomjang assigned to dongkyeomjang ✨ Feature 기능 개발
Projects
None yet
Development

Successfully merging this pull request may close these issues.

✨ Feature - 4.4 가게 방문 인증용 영수증 OCR 조회하기 API 구현
1 participant