diff --git a/build.gradle b/build.gradle index 7395099d..a94f1e42 100644 --- a/build.gradle +++ b/build.gradle @@ -76,9 +76,13 @@ dependencies { implementation 'com.vladmihalcea:hibernate-types-60:2.21.1' implementation 'org.hibernate.orm:hibernate-core:6.5.2.Final' -//FCM + //FCM implementation 'com.google.firebase:firebase-admin:9.2.0' + // Mocking Test + testImplementation 'org.springframework.security:spring-security-test' + + } diff --git a/src/main/java/com/mumuk/domain/ocr/service/OcrServiceImpl.java b/src/main/java/com/mumuk/domain/ocr/service/OcrServiceImpl.java index 4e1232fa..563c822c 100644 --- a/src/main/java/com/mumuk/domain/ocr/service/OcrServiceImpl.java +++ b/src/main/java/com/mumuk/domain/ocr/service/OcrServiceImpl.java @@ -10,9 +10,7 @@ import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -24,7 +22,80 @@ public class OcrServiceImpl implements OcrService { private final ClovaOcrClient clovaOcrClient; private final ObjectMapper objectMapper; - public OcrServiceImpl(UserHealthDataRepository userHealthDataRepository, ClovaOcrClient clovaOcrClient, ObjectMapper objectMapper) { + // ๐Ÿ”ฅ ํ•ต์‹ฌ ๊ฐœ์„ : ๋ฒ”์šฉ์ ์ธ ๊ฑด๊ฐ• ์ง€ํ‘œ ํŒจํ„ด๋“ค + private static final Map HEALTH_PATTERNS = new LinkedHashMap<>(); + + static { + // ์ฒด์ค‘ ๊ด€๋ จ ํŒจํ„ด๋“ค (๋‹ค์–‘ํ•œ ํ˜•ํƒœ ์ง€์›) + HEALTH_PATTERNS.put(Pattern.compile("(?i)(์ฒด์ค‘|weight|๋ชธ๋ฌด๊ฒŒ|Weight)\\s*(?:\\([^)]*\\))?\\s*(\\d{2,3}(?:\\.\\d{1,2})?)\\s*(?:kg|ํ‚ฌ๋กœ|ํ‚ค๋กœ)?"), "์ฒด์ค‘"); + HEALTH_PATTERNS.put(Pattern.compile("(?i)(๊ณจ๊ฒฉ๊ทผ๋Ÿ‰|๊ทผ์œก๋Ÿ‰|skeletal muscle|muscle mass|Skeletal Muscle Mass)\\s*(?:\\([^)]*\\))?\\s*(\\d{1,3}(?:\\.\\d{1,2})?)\\s*(?:kg|ํ‚ฌ๋กœ)?"), "๊ณจ๊ฒฉ๊ทผ๋Ÿ‰"); + HEALTH_PATTERNS.put(Pattern.compile("(?i)(์ฒด์ง€๋ฐฉ๋Ÿ‰|์ง€๋ฐฉ๋Ÿ‰|body fat mass|fat mass|Body FatMass)\\s*(?:\\([^)]*\\))?\\s*(\\d{1,3}(?:\\.\\d{1,2})?)\\s*(?:kg|ํ‚ฌ๋กœ)?"), "์ฒด์ง€๋ฐฉ๋Ÿ‰"); + + // ์ฒด์„ฑ๋ถ„ ๊ด€๋ จ + HEALTH_PATTERNS.put(Pattern.compile("(?i)(์ฒด์ˆ˜๋ถ„|์ˆ˜๋ถ„๋Ÿ‰|body water|total.*water|Total Body Water)\\s*(?:\\([^)]*\\))?\\s*(\\d{1,3}(?:\\.\\d{1,2})?)\\s*(?:L|๋ฆฌํ„ฐ|โ„“)?"), "์ฒด์ˆ˜๋ถ„"); + HEALTH_PATTERNS.put(Pattern.compile("(?i)(๋‹จ๋ฐฑ์งˆ|protein|Protein)\\s*(?:\\([^)]*\\))?\\s*(\\d{1,2}(?:\\.\\d{1,2})?)\\s*(?:kg|ํ‚ฌ๋กœ)?"), "๋‹จ๋ฐฑ์งˆ"); + HEALTH_PATTERNS.put(Pattern.compile("(?i)(๋ฌด๊ธฐ์งˆ|๋ฏธ๋„ค๋ž„|mineral|minerals|Minerals)\\s*(?:\\([^)]*\\))?\\s*(\\d{1,2}(?:\\.\\d{1,2})?)\\s*(?:kg|ํ‚ฌ๋กœ)?"), "๋ฌด๊ธฐ์งˆ"); + HEALTH_PATTERNS.put(Pattern.compile("(?i)(์ฒด์ง€๋ฐฉ|body fat|์ง€๋ฐฉ|fat)\\s*(?:\\([^)]*\\))?\\s*(\\d{1,3}(?:\\.\\d{1,2})?)\\s*(?:%|ํผ์„ผํŠธ|percent)?"), "์ฒด์ง€๋ฐฉ๋ฅ "); + + // BMI ๋ฐ ๊ธฐํƒ€ ์ง€์ˆ˜ + HEALTH_PATTERNS.put(Pattern.compile("(?i)(BMI|body mass index|๋น„๋งŒ์ง€์ˆ˜)\\s*(?:\\([^)]*\\))?\\s*(\\d{1,2}(?:\\.\\d{1,2})?)"), "BMI"); + HEALTH_PATTERNS.put(Pattern.compile("(?i)(๊ธฐ์ดˆ๋Œ€์‚ฌ์œจ|bmr|basal metabolic|๊ธฐ์ดˆ๋Œ€์‚ฌ|BMR)\\s*(?:\\([^)]*\\))?\\s*(\\d{3,4})\\s*(?:kcal|์นผ๋กœ๋ฆฌ)?"), "๊ธฐ์ดˆ๋Œ€์‚ฌ์œจ"); + + // ํ˜ˆ์•• ๊ด€๋ จ + HEALTH_PATTERNS.put(Pattern.compile("(?i)(์ˆ˜์ถ•๊ธฐ|์ตœ๊ณ ํ˜ˆ์••|systolic)\\s*(?:\\([^)]*\\))?\\s*(\\d{2,3})\\s*(?:mmHg)?"), "์ˆ˜์ถ•๊ธฐํ˜ˆ์••"); + HEALTH_PATTERNS.put(Pattern.compile("(?i)(์ด์™„๊ธฐ|์ตœ์ €ํ˜ˆ์••|diastolic)\\s*(?:\\([^)]*\\))?\\s*(\\d{2,3})\\s*(?:mmHg)?"), "์ด์™„๊ธฐํ˜ˆ์••"); + + // ํ˜ˆ๋‹น ๊ด€๋ จ + HEALTH_PATTERNS.put(Pattern.compile("(?i)(ํ˜ˆ๋‹น|glucose|blood sugar|๊ธ€๋ฃจ์ฝ”์Šค)\\s*(?:\\([^)]*\\))?\\s*(\\d{2,3})\\s*(?:mg/dL|mg/dl)?"), "ํ˜ˆ๋‹น"); + HEALTH_PATTERNS.put(Pattern.compile("(?i)(๋‹นํ™”ํ˜ˆ์ƒ‰์†Œ|HbA1c|hba1c)\\s*(?:\\([^)]*\\))?\\s*(\\d{1,2}(?:\\.\\d{1,2})?)\\s*(?:%)?"), "๋‹นํ™”ํ˜ˆ์ƒ‰์†Œ"); + + // ์ฝœ๋ ˆ์Šคํ…Œ๋กค ๊ด€๋ จ + HEALTH_PATTERNS.put(Pattern.compile("(?i)(์ด์ฝœ๋ ˆ์Šคํ…Œ๋กค|total cholesterol|์ฝœ๋ ˆ์Šคํ…Œ๋กค)\\s*(?:\\([^)]*\\))?\\s*(\\d{2,3})\\s*(?:mg/dL|mg/dl)?"), "์ด์ฝœ๋ ˆ์Šคํ…Œ๋กค"); + HEALTH_PATTERNS.put(Pattern.compile("(?i)(HDL|good cholesterol|hdl)\\s*(?:\\([^)]*\\))?\\s*(\\d{2,3})\\s*(?:mg/dL|mg/dl)?"), "HDL์ฝœ๋ ˆ์Šคํ…Œ๋กค"); + HEALTH_PATTERNS.put(Pattern.compile("(?i)(LDL|bad cholesterol|ldl)\\s*(?:\\([^)]*\\))?\\s*(\\d{2,3})\\s*(?:mg/dL|mg/dl)?"), "LDL์ฝœ๋ ˆ์Šคํ…Œ๋กค"); + + // ๊ฐ„๊ธฐ๋Šฅ ๊ด€๋ จ + HEALTH_PATTERNS.put(Pattern.compile("(?i)(ALT|SGPT|alt)\\s*(?:\\([^)]*\\))?\\s*(\\d{1,3})\\s*(?:U/L|IU/L)?"), "ALT"); + HEALTH_PATTERNS.put(Pattern.compile("(?i)(AST|SGOT|ast)\\s*(?:\\([^)]*\\))?\\s*(\\d{1,3})\\s*(?:U/L|IU/L)?"), "AST"); + + // ์‹ ์žฅ๊ธฐ๋Šฅ ๊ด€๋ จ + HEALTH_PATTERNS.put(Pattern.compile("(?i)(ํฌ๋ ˆ์•„ํ‹ฐ๋‹Œ|creatinine)\\s*(?:\\([^)]*\\))?\\s*(\\d{1,2}(?:\\.\\d{1,2})?)\\s*(?:mg/dL|mg/dl)?"), "ํฌ๋ ˆ์•„ํ‹ฐ๋‹Œ"); + HEALTH_PATTERNS.put(Pattern.compile("(?i)(์š”์†Œ์งˆ์†Œ|BUN|urea nitrogen)\\s*(?:\\([^)]*\\))?\\s*(\\d{1,3})\\s*(?:mg/dL|mg/dl)?"), "์š”์†Œ์งˆ์†Œ"); + + // ์—ผ์ฆ ์ง€ํ‘œ + HEALTH_PATTERNS.put(Pattern.compile("(?i)(CRP|c-reactive protein|์—ผ์ฆ์ˆ˜์น˜)\\s*(?:\\([^)]*\\))?\\s*(\\d{1,2}(?:\\.\\d{1,2})?)\\s*(?:mg/L|mg/dl)?"), "CRP"); + + // ๊ฐ‘์ƒ์„  ๊ด€๋ จ + HEALTH_PATTERNS.put(Pattern.compile("(?i)(TSH|๊ฐ‘์ƒ์„ ์ž๊ทนํ˜ธ๋ฅด๋ชฌ)\\s*(?:\\([^)]*\\))?\\s*(\\d{1,2}(?:\\.\\d{1,2})?)\\s*(?:mIU/L)?"), "TSH"); + + // ๋น„ํƒ€๋ฏผ ๊ด€๋ จ + HEALTH_PATTERNS.put(Pattern.compile("(?i)(๋น„ํƒ€๋ฏผD|vitamin d|25-OH)\\s*(?:\\([^)]*\\))?\\s*(\\d{1,3}(?:\\.\\d{1,2})?)\\s*(?:ng/mL|ng/ml)?"), "๋น„ํƒ€๋ฏผD"); + + // ๐Ÿ’ก ๋ฒ”์šฉ ์ˆซ์ž-ํ…์ŠคํŠธ ํŒจํ„ด (์œ„์˜ ํŠน์ • ํŒจํ„ด์— ๋งค์นญ๋˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ ์‚ฌ์šฉ) + HEALTH_PATTERNS.put(Pattern.compile("([๊ฐ€-ํžฃA-Za-z][๊ฐ€-ํžฃA-Za-z\\s]{1,15})\\s*(?:\\([^)]*\\))?\\s*(\\d{1,4}(?:\\.\\d{1,3})?)\\s*(?:[๊ฐ€-ํžฃA-Za-z/%]{0,10})?"), "๊ธฐํƒ€์ง€ํ‘œ"); + } + + // ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๋ฅผ ์œ„ํ•œ ๋ฒ”์œ„ ์ •์˜ (๋” ์œ ์—ฐํ•˜๊ฒŒ) + private static final Map VALUE_RANGES = new HashMap() {{ + put("์ฒด์ค‘", new double[]{20.0, 300.0}); + put("๊ณจ๊ฒฉ๊ทผ๋Ÿ‰", new double[]{5.0, 100.0}); + put("์ฒด์ง€๋ฐฉ๋Ÿ‰", new double[]{0.0, 150.0}); + put("์ฒด์ˆ˜๋ถ„", new double[]{15.0, 100.0}); + put("๋‹จ๋ฐฑ์งˆ", new double[]{3.0, 50.0}); + put("๋ฌด๊ธฐ์งˆ", new double[]{1.0, 15.0}); + put("์ฒด์ง€๋ฐฉ๋ฅ ", new double[]{0.0, 70.0}); + put("BMI", new double[]{10.0, 60.0}); + put("๊ธฐ์ดˆ๋Œ€์‚ฌ์œจ", new double[]{800.0, 4000.0}); + put("์ˆ˜์ถ•๊ธฐํ˜ˆ์••", new double[]{60.0, 300.0}); + put("์ด์™„๊ธฐํ˜ˆ์••", new double[]{30.0, 200.0}); + put("ํ˜ˆ๋‹น", new double[]{30.0, 600.0}); + put("์ด์ฝœ๋ ˆ์Šคํ…Œ๋กค", new double[]{50.0, 500.0}); + put("HDL์ฝœ๋ ˆ์Šคํ…Œ๋กค", new double[]{10.0, 150.0}); + put("LDL์ฝœ๋ ˆ์Šคํ…Œ๋กค", new double[]{20.0, 400.0}); + }}; + + public OcrServiceImpl(UserHealthDataRepository userHealthDataRepository, + ClovaOcrClient clovaOcrClient, ObjectMapper objectMapper) { this.userHealthDataRepository = userHealthDataRepository; this.clovaOcrClient = clovaOcrClient; this.objectMapper = objectMapper; @@ -32,34 +103,9 @@ public OcrServiceImpl(UserHealthDataRepository userHealthDataRepository, ClovaOc @Override public Map extractText(MultipartFile imageFile) { - String ocrJson = clovaOcrClient.callClovaOcr(imageFile); Map result = new LinkedHashMap<>(); - Map keyNameMap = Map.of( - "์ฒด์ˆ˜๋ถ„", "์ฒด์ˆ˜๋ถ„ (L)", - "๋‹จ๋ฐฑ์งˆ", "๋‹จ๋ฐฑ์งˆ (kg)", - "๋ฌด๊ธฐ์งˆ", "๋ฌด๊ธฐ์งˆ", - "์ฒด์ง€๋ฐฉ", "์ฒด์ง€๋ฐฉ (kg)", - "์ฒด์ค‘", "์ฒด์ค‘ (kg)", - "๊ณจ๊ฒฉ๊ทผ๋Ÿ‰", "๊ณจ๊ฒฉ๊ทผ๋Ÿ‰", - "์ฒด์ง€๋ฐฉ๋Ÿ‰", "์ฒด์ง€๋ฐฉ๋Ÿ‰ (kg)", - "BMI", "BMI", - "์ฒด์ง€๋ฐฉ๋ฅ ", "์ฒด์ง€๋ฐฉ๋ฅ " - ); - - Map overrideKeys = Map.of( - "Weight", "์ฒด์ค‘ (kg)", - "Skeletal Muscle Mass", "๊ณจ๊ฒฉ๊ทผ๋Ÿ‰", - "Body FatMass", "์ฒด์ง€๋ฐฉ๋Ÿ‰ (kg)", - "Protein", "๋‹จ๋ฐฑ์งˆ (kg)", - "Body Fat", "์ฒด์ง€๋ฐฉ (kg)", - "Total Body Water", "์ฒด์ˆ˜๋ถ„ (L)", - "Minerals", "๋ฌด๊ธฐ์งˆ" - ); - - Set allowedKeys = keyNameMap.keySet(); - try { JsonNode fields = objectMapper .readTree(ocrJson) @@ -69,93 +115,166 @@ public Map extractText(MultipartFile imageFile) { if (fields.size() == 0) return result; String rawText = fields.get(0).path("inferText").asText(); - String[] lines = rawText.split("\n"); - - String pendingKey = null; + log.info("๐Ÿ” ์›๋ณธ OCR ํ…์ŠคํŠธ:\n{}", rawText); + + // ๐Ÿ”ฅ ๊ฐœ์„ ๋œ ๋ฒ”์šฉ ํŒŒ์‹ฑ ๋กœ์ง + result = parseHealthDataUniversally(rawText); + + // ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ ๋ฐ ์ •์ œ + result = validateAndCleanResults(result); - for (String line : lines) { - line = line.trim(); - log.info("๐Ÿ” Line: {}", line); + } catch (JsonProcessingException e) { + log.error("โŒ OCR JSON ํŒŒ์‹ฑ ์‹คํŒจ", e); + throw new RuntimeException("OCR JSON ํŒŒ์‹ฑ ์‹คํŒจ", e); + } - if (line.matches(".*ํ‘œ์ค€[์ดํ•˜|์ด์ƒ|์ •๋„]?.*")) continue; + log.info("โœ… ์ตœ์ข… ์ถ”์ถœ ๊ฒฐ๊ณผ: {}", result); + return result; + } - // 1. pendingKey ์šฐ์„  ์ฒ˜๋ฆฌ - if (pendingKey != null && line.matches(".*\\d+.*")) { - String value = extractFirstDecimalOutsideParentheses(line); - if (value != null) { - result.putIfAbsent(keyNameMap.get(pendingKey), value); - pendingKey = null; - continue; - } + // ๐Ÿ”ฅ ํ•ต์‹ฌ ๊ฐœ์„ : ๋ฒ”์šฉ์ ์ธ ๊ฑด๊ฐ• ๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ + private Map parseHealthDataUniversally(String rawText) { + Map result = new LinkedHashMap<>(); + Set usedValues = new HashSet<>(); // ์ค‘๋ณต ๊ฐ’ ๋ฐฉ์ง€ + + log.info("๐Ÿ” ๋ฒ”์šฉ ๊ฑด๊ฐ• ๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ ์‹œ์ž‘"); + + // ํ…์ŠคํŠธ ์ •์ œ + String cleanedText = preprocessText(rawText); + + // ๊ฐ ํŒจํ„ด์— ๋Œ€ํ•ด ๋งค์นญ ์‹œ๋„ + for (Map.Entry entry : HEALTH_PATTERNS.entrySet()) { + Pattern pattern = entry.getKey(); + String categoryPrefix = entry.getValue(); + + Matcher matcher = pattern.matcher(cleanedText); + + while (matcher.find()) { + String keyMatch = matcher.group(1); // ํ‚ค์›Œ๋“œ ๋ถ€๋ถ„ + String valueMatch = matcher.group(2); // ์ˆซ์ž ๋ถ€๋ถ„ + + // ์ค‘๋ณต ๊ฐ’ ์ฒดํฌ + if (usedValues.contains(valueMatch)) { + continue; } - - // 2. override ํ‚ค ๋Œ€์‘ (์˜๋ฌธ) - for (Map.Entry entry : overrideKeys.entrySet()) { - if (line.contains(entry.getKey()) && line.matches(".*\\d+.*")) { - String value = extractFirstDecimalOutsideParentheses(line); - if (value != null && !result.containsKey(entry.getValue())) { - result.put(entry.getValue(), value); - } + + // ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ + if (isValidHealthValue(categoryPrefix, keyMatch, valueMatch)) { + String finalKey; + + if ("๊ธฐํƒ€์ง€ํ‘œ".equals(categoryPrefix)) { + // ๊ธฐํƒ€ ์ง€ํ‘œ์˜ ๊ฒฝ์šฐ ์›๋ณธ ํ‚ค์›Œ๋“œ ์‚ฌ์šฉ + finalKey = normalizeKeyName(keyMatch); + } else { + // ์ •์˜๋œ ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๊ฒฝ์šฐ ํ‘œ์ค€ํ™”๋œ ์ด๋ฆ„ ์‚ฌ์šฉ + finalKey = categoryPrefix; } + + result.putIfAbsent(finalKey, valueMatch); + usedValues.add(valueMatch); + + log.info("โœ… ํŒจํ„ด ๋งค์นญ ์„ฑ๊ณต: {} = {} (ํŒจํ„ด: {})", finalKey, valueMatch, categoryPrefix); + + break; // ๊ฐ™์€ ํŒจํ„ด์—์„œ ์ฒซ ๋ฒˆ์งธ ๋งค์นญ๋งŒ ์‚ฌ์šฉ } + } + } + + log.info("๐Ÿ” ํŒŒ์‹ฑ ์™„๋ฃŒ, ์ถ”์ถœ๋œ ํ•ญ๋ชฉ ์ˆ˜: {}", result.size()); + return result; + } - // 3. ํ•œ๊ธ€ ํ‚ค + ์ˆซ์ž ํฌํ•จ๋œ ๋ผ์ธ - if (line.matches(".*[๊ฐ€-ํžฃ]+.*\\d+.*")) { - String rawKey = extractKey(line); - String normKey = normalizeKey(rawKey); - - if (allowedKeys.contains(normKey)) { - String stdKey = keyNameMap.get(normKey); - String value = extractFirstDecimalOutsideParentheses(line); - if (value != null && !result.containsKey(stdKey)) { - result.put(stdKey, value); - continue; - } - } - } + // ํ…์ŠคํŠธ ์ „์ฒ˜๋ฆฌ + private String preprocessText(String rawText) { + return rawText + .replaceAll("(?i)inbody|๊ฒ€์‚ฌ|๊ฒฐ๊ณผ|report", "") // ๋ธŒ๋žœ๋“œ๋ช…/๋ถˆํ•„์š”ํ•œ ๋‹จ์–ด ์ œ๊ฑฐ + .replaceAll("ํ‘œ์ค€\\s*[์ดํ•˜|์ด์ƒ|์ •๋„|๋ฒ”์œ„]", "") // ํ‘œ์ค€ ๊ด€๋ จ ํ…์ŠคํŠธ ์ œ๊ฑฐ + .replaceAll("๊ถŒ์žฅ\\s*[๋ฒ”์œ„|์ˆ˜์น˜]", "") // ๊ถŒ์žฅ ๊ด€๋ จ ํ…์ŠคํŠธ ์ œ๊ฑฐ + .replaceAll("์ •์ƒ\\s*[๋ฒ”์œ„|์ˆ˜์น˜]", "") // ์ •์ƒ ๊ด€๋ จ ํ…์ŠคํŠธ ์ œ๊ฑฐ + .replaceAll("\\s+", " ") // ๋‹ค์ค‘ ๊ณต๋ฐฑ ์ •๋ฆฌ + .trim(); + } - // 4. ํ‚ค๋งŒ ์žˆ๋Š” ๊ฒฝ์šฐ โ†’ ๋‹ค์Œ ์ค„์—์„œ ๊ฐ’ ๋ฐ›๊ธฐ - if (line.matches(".*[๊ฐ€-ํžฃ]+.*") && !line.matches(".*\\d+.*")) { - String rawKey = extractKey(line); - String normKey = normalizeKey(rawKey); + // ํ‚ค ์ด๋ฆ„ ์ •๊ทœํ™” + private String normalizeKeyName(String rawKey) { + return rawKey.trim() + .replaceAll("\\s+", "_") // ๊ณต๋ฐฑ์„ ์–ธ๋”์Šค์ฝ”์–ด๋กœ + .replaceAll("[^๊ฐ€-ํžฃA-Za-z0-9_]", "") // ํŠน์ˆ˜๋ฌธ์ž ์ œ๊ฑฐ + .toLowerCase(); + } - if (allowedKeys.contains(normKey)) { - pendingKey = normKey; - } + // ๊ฑด๊ฐ• ์ˆ˜์น˜ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ (๋” ์œ ์—ฐํ•˜๊ฒŒ) + private boolean isValidHealthValue(String category, String key, String value) { + try { + double val = Double.parseDouble(value); + + // ๋ฒ”์œ„๊ฐ€ ์ •์˜๋œ ๊ฒฝ์šฐ ๋ฒ”์œ„ ์ฒดํฌ + double[] range = VALUE_RANGES.get(category); + if (range != null) { + boolean valid = val >= range[0] && val <= range[1]; + if (!valid) { + log.debug("โš ๏ธ ๋ฒ”์œ„ ๋ฒ—์–ด๋‚จ: {} = {} (๋ฒ”์œ„: {}-{})", key, val, range[0], range[1]); + return false; + } + } else { + // ๋ฒ”์œ„๊ฐ€ ์ •์˜๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ ๊ธฐ๋ณธ ๊ฒ€์‚ฌ + if (val <= 0 || val > 10000) { + log.debug("โš ๏ธ ๊ธฐ๋ณธ ๋ฒ”์œ„ ๋ฒ—์–ด๋‚จ: {} = {}", key, val); + return false; } } - - } catch (JsonProcessingException e) { - throw new RuntimeException("OCR JSON ํŒŒ์‹ฑ ์‹คํŒจ", e); + + // ์†Œ์ˆ˜์  ์ž๋ฆฟ์ˆ˜ ์ฒดํฌ (๋„ˆ๋ฌด ๋งŽ์€ ์†Œ์ˆ˜์ ์€ ์˜ค์ธ์‹์ผ ๊ฐ€๋Šฅ์„ฑ) + String[] parts = value.split("\\."); + if (parts.length > 1 && parts[1].length() > 3) { + log.debug("โš ๏ธ ์†Œ์ˆ˜์  ์ž๋ฆฟ์ˆ˜ ์ดˆ๊ณผ: {} = {}", key, value); + return false; + } + + return true; + + } catch (NumberFormatException e) { + log.debug("โš ๏ธ ์ˆซ์ž ํ˜•์‹ ์˜ค๋ฅ˜: {} = {}", key, value); + return false; } + } - log.info("์ถ”์ถœ๋œ ํ‚ค-๊ฐ’ : {}", result); - return result; + // ๊ฒฐ๊ณผ ๊ฒ€์ฆ ๋ฐ ์ •์ œ + private Map validateAndCleanResults(Map result) { + Map cleaned = new LinkedHashMap<>(); + + for (Map.Entry entry : result.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue().trim().replaceAll("[^\\d\\.]", ""); + + // ๋นˆ ๊ฐ’ ์ฒดํฌ + if (value.isEmpty()) { + log.warn("โš ๏ธ ๋นˆ ๊ฐ’ ์ œ์™ธ: {}", key); + continue; + } + + // ์ตœ์ข… ์œ ํšจ์„ฑ ์žฌ๊ฒ€์‚ฌ + try { + double val = Double.parseDouble(value); + if (val > 0 && val <= 10000) { // ๊ธฐ๋ณธ ๋ฒ”์œ„ + cleaned.put(key, value); + log.debug("โœ… ์ตœ์ข… ๊ฒ€์ฆ ํ†ต๊ณผ: {} = {}", key, value); + } else { + log.warn("โš ๏ธ ์ตœ์ข… ๊ฒ€์ฆ ์‹คํŒจ: {} = {}", key, value); + } + } catch (NumberFormatException e) { + log.warn("โš ๏ธ ์ตœ์ข… ์ˆซ์ž ๋ณ€ํ™˜ ์‹คํŒจ: {} = {}", key, value); + } + } + + return cleaned; } @Override public void saveOcrResult(Long userId, Map ocrResult) { UserHealthData entity = new UserHealthData(userId, ocrResult); userHealthDataRepository.save(entity); - } - - - private String extractKey(String line) { - Matcher matcher = Pattern.compile("([๊ฐ€-ํžฃA-Za-zยท\\s\\(\\)]+)").matcher(line); - return matcher.find() ? matcher.group(1).trim() : null; - } - - private String normalizeKey(String key) { - if (key == null) return null; - return key.replaceAll("\\s*\\(.*?\\)", "") // ๊ด„ํ˜ธ ์ œ๊ฑฐ - .trim() - .replace(" ", ""); - } - - - private String extractFirstDecimalOutsideParentheses(String line) { - String cleaned = line.replaceAll("\\([^\\)]*\\)", " "); - Matcher matcher = Pattern.compile("\\d+\\.\\d+").matcher(cleaned); - return matcher.find() ? matcher.group() : null; + + log.info("๐Ÿ’พ ์‚ฌ์šฉ์ž {}์˜ ๊ฑด๊ฐ• ๋ฐ์ดํ„ฐ ์ €์žฅ ์™„๋ฃŒ: {} ํ•ญ๋ชฉ", userId, ocrResult.size()); } } diff --git a/src/main/java/com/mumuk/global/client/ClovaOcrClient.java b/src/main/java/com/mumuk/global/client/ClovaOcrClient.java index e1ea0282..ad31bec1 100644 --- a/src/main/java/com/mumuk/global/client/ClovaOcrClient.java +++ b/src/main/java/com/mumuk/global/client/ClovaOcrClient.java @@ -1,6 +1,10 @@ package com.mumuk.global.client; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.mumuk.global.util.FileResourceUtil; +import com.mumuk.global.util.ImagePreprocessingUtil; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpEntity; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; @@ -14,8 +18,8 @@ import java.io.IOException; import java.util.UUID; - @Component +@Slf4j public class ClovaOcrClient { @Value("${naver.clova.ocr.invoke-url}") @@ -25,34 +29,185 @@ public class ClovaOcrClient { private String secretKey; private final RestTemplate restTemplate; + private final ImagePreprocessingUtil imagePreprocessingUtil; + private final ObjectMapper objectMapper; - public ClovaOcrClient(RestTemplate restTemplate) { + public ClovaOcrClient(RestTemplate restTemplate, + ImagePreprocessingUtil imagePreprocessingUtil) { this.restTemplate = restTemplate; + this.imagePreprocessingUtil = imagePreprocessingUtil; + this.objectMapper = new ObjectMapper(); } + /** + * ๊ธฐ๋ณธ OCR ํ˜ธ์ถœ + */ public String callClovaOcr(MultipartFile imageFile) { + return callClovaOcrWithOptimization(imageFile, false); + } + + /** + * ์ตœ์ ํ™”๋œ OCR ํ˜ธ์ถœ (์ด๋ฏธ์ง€ ์ „์ฒ˜๋ฆฌ ํฌํ•จ) + */ + public String callClovaOcrWithOptimization(MultipartFile imageFile, boolean enablePreprocessing) { try { - HttpHeaders headers = new HttpHeaders(); - headers.set("X-OCR-SECRET", secretKey); + log.info("๐Ÿ”„ Clova OCR ์š”์ฒญ ์‹œ์ž‘: {}", imageFile.getOriginalFilename()); + + MultipartFile processedImage = imageFile; + + // ์ด๋ฏธ์ง€ ์ „์ฒ˜๋ฆฌ ์ˆ˜ํ–‰ (์˜ต์…˜) + if (enablePreprocessing && imagePreprocessingUtil.needsPreprocessing(imageFile)) { + log.info("์ด๋ฏธ์ง€ ์ „์ฒ˜๋ฆฌ ์ˆ˜ํ–‰"); + byte[] preprocessedBytes = imagePreprocessingUtil.preprocessForOcr(imageFile); + processedImage = createMultipartFileFromBytes(preprocessedBytes, imageFile.getOriginalFilename()); + } + + // OCR ์š”์ฒญ ์ˆ˜ํ–‰ + String result = performOcrRequest(processedImage); + + // ๊ฒฐ๊ณผ ํ’ˆ์งˆ ๊ฒ€์ฆ + if (!isValidOcrResult(result) && !enablePreprocessing) { + log.warn("โš ๏ธ OCR ๊ฒฐ๊ณผ ํ’ˆ์งˆ ๋ถˆ๋Ÿ‰, ์ „์ฒ˜๋ฆฌ ํ›„ ์žฌ์‹œ๋„"); + return callClovaOcrWithOptimization(imageFile, true); + } + + log.info("โœ… Clova OCR ์‘๋‹ต ์ˆ˜์‹  ์™„๋ฃŒ"); + return result; + + } catch (IOException e) { + log.error("โŒ CLOVA OCR ์š”์ฒญ ์‹คํŒจ: {}", e.getMessage()); + throw new RuntimeException("CLOVA OCR ์š”์ฒญ ์‹คํŒจ", e); + } + } - String messageJson = buildClovaRequestMessage(imageFile); + /** + * ๋‹ค์ค‘ ํ…œํ”Œ๋ฆฟ์„ ์‚ฌ์šฉํ•œ OCR (๋” ๋†’์€ ์ •ํ™•๋„) + */ + public String callClovaOcrWithMultipleTemplates(MultipartFile imageFile) { + try { + log.info("๋‹ค์ค‘ ํ…œํ”Œ๋ฆฟ OCR ์‹œ์ž‘"); + + // ์ธ๋ฐ”๋”” ๊ด€๋ จ ์—ฌ๋Ÿฌ ํ…œํ”Œ๋ฆฟ ์‹œ๋„ + int[] templateIds = {38491, 0}; // 38491: ๊ธฐ๋ณธ ํ…œํ”Œ๋ฆฟ, 0: ๋ฒ”์šฉ ํ…œํ”Œ๋ฆฟ + + String bestResult = null; + int maxFieldCount = 0; + + for (int templateId : templateIds) { + log.info("ํ…œํ”Œ๋ฆฟ {} ์‹œ๋„", templateId); + String result = callClovaOcrWithTemplate(imageFile, templateId); + int fieldCount = countExtractedFields(result); + + log.info("ํ…œํ”Œ๋ฆฟ {} ๊ฒฐ๊ณผ: {} ํ•„๋“œ ์ถ”์ถœ", templateId, fieldCount); + + if (fieldCount > maxFieldCount) { + maxFieldCount = fieldCount; + bestResult = result; + } + } + + log.info("์ตœ์  ๊ฒฐ๊ณผ ์„ ํƒ: {} ํ•„๋“œ", maxFieldCount); + return bestResult; + + } catch (Exception e) { + log.warn("๋‹ค์ค‘ ํ…œํ”Œ๋ฆฟ ์‹คํŒจ, ๊ธฐ๋ณธ ํ…œํ”Œ๋ฆฟ ์‚ฌ์šฉ"); + return callClovaOcr(imageFile); + } + } + /** + * ์žฌ์‹œ๋„ ๋กœ์ง์ด ํฌํ•จ๋œ OCR ํ˜ธ์ถœ + */ + public String callClovaOcrWithRetry(MultipartFile imageFile, int maxRetries) { + Exception lastException = null; + + for (int attempt = 1; attempt <= maxRetries; attempt++) { + try { + log.info("OCR ์‹œ๋„ {}/{}", attempt, maxRetries); + + // ์ฒซ ๋ฒˆ์งธ ์‹œ๋„๋Š” ๊ธฐ๋ณธ, ์ดํ›„๋Š” ์ „์ฒ˜๋ฆฌ ์ ์šฉ + boolean enablePreprocessing = attempt > 1; + String result = callClovaOcrWithOptimization(imageFile, enablePreprocessing); + + // ๊ฒฐ๊ณผ ํ’ˆ์งˆ ๊ฒ€์ฆ + if (isValidOcrResult(result)) { + log.info("OCR ์„ฑ๊ณต (์‹œ๋„ {})", attempt); + return result; + } else { + log.warn("OCR ๊ฒฐ๊ณผ ํ’ˆ์งˆ ๋ถˆ๋Ÿ‰ (์‹œ๋„ {})", attempt); + if (attempt < maxRetries) { + Thread.sleep(1000 * attempt); // ์ ์ง„์  ๋Œ€๊ธฐ + } + } + + } catch (Exception e) { + lastException = e; + log.warn("OCR ์‹คํŒจ (์‹œ๋„ {}): {}", attempt, e.getMessage()); + + if (attempt < maxRetries) { + try { + Thread.sleep(2000 * attempt); // ์ ์ง„์  ๋Œ€๊ธฐ + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + break; + } + } + } + } + + throw new RuntimeException("OCR ์ตœ๋Œ€ ์žฌ์‹œ๋„ ํšŸ์ˆ˜ ์ดˆ๊ณผ", lastException); + } - MultiValueMap body = new LinkedMultiValueMap<>(); - body.add("file", FileResourceUtil.toResource(imageFile)); - body.add("message", messageJson); + /** + * ์‹ค์ œ OCR ์š”์ฒญ ์ˆ˜ํ–‰ + */ + private String performOcrRequest(MultipartFile imageFile) throws IOException { + HttpHeaders headers = createOptimizedHeaders(); + String messageJson = buildOptimizedClovaRequestMessage(imageFile); + + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("file", FileResourceUtil.toResource(imageFile)); + body.add("message", messageJson); - HttpEntity> requestEntity = new HttpEntity<>(body, headers); + HttpEntity> requestEntity = new HttpEntity<>(body, headers); + ResponseEntity response = restTemplate.postForEntity(invokeUrl, requestEntity, String.class); + + return response.getBody(); + } - ResponseEntity response = restTemplate.postForEntity(invokeUrl, requestEntity, String.class); + /** + * ํŠน์ • ํ…œํ”Œ๋ฆฟ์œผ๋กœ OCR ์š”์ฒญ + */ + private String callClovaOcrWithTemplate(MultipartFile imageFile, int templateId) throws IOException { + HttpHeaders headers = createOptimizedHeaders(); + String messageJson = buildTemplateSpecificMessage(imageFile, templateId); + + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("file", FileResourceUtil.toResource(imageFile)); + body.add("message", messageJson); - return response.getBody(); - } catch (IOException e) { - throw new RuntimeException("CLOVA OCR ์š”์ฒญ ์‹คํŒจ", e); - } + HttpEntity> requestEntity = new HttpEntity<>(body, headers); + ResponseEntity response = restTemplate.postForEntity(invokeUrl, requestEntity, String.class); + + return response.getBody(); } - private String buildClovaRequestMessage(MultipartFile imageFile) { + /** + * ์ตœ์ ํ™”๋œ ํ—ค๋” ์ƒ์„ฑ + */ + private HttpHeaders createOptimizedHeaders() { + HttpHeaders headers = new HttpHeaders(); + headers.set("X-OCR-SECRET", secretKey); + headers.set("Content-Type", "multipart/form-data"); + headers.set("Connection", "keep-alive"); + headers.set("Accept", "application/json"); + return headers; + } + + /** + * ์ตœ์ ํ™”๋œ ์š”์ฒญ ๋ฉ”์‹œ์ง€ ์ƒ์„ฑ + */ + private String buildOptimizedClovaRequestMessage(MultipartFile imageFile) { return """ { "version": "V2", @@ -64,19 +219,152 @@ private String buildClovaRequestMessage(MultipartFile imageFile) { "name": "%s", "templateIds": [38491] } - ] + ], + "lang": "ko", + "resultType": "string", + "enableTableDetection": false } """.formatted( UUID.randomUUID(), System.currentTimeMillis(), - getFileExtension(imageFile.getOriginalFilename()), // "jpg", "png" ๋“ฑ + getFileExtension(imageFile.getOriginalFilename()), imageFile.getOriginalFilename() ); } + /** + * ํ…œํ”Œ๋ฆฟ๋ณ„ ์š”์ฒญ ๋ฉ”์‹œ์ง€ ์ƒ์„ฑ + */ + private String buildTemplateSpecificMessage(MultipartFile imageFile, int templateId) { + return """ + { + "version": "V2", + "requestId": "%s", + "timestamp": %d, + "images": [ + { + "format": "%s", + "name": "%s", + "templateIds": [%d] + } + ], + "lang": "ko", + "resultType": "string" + } + """.formatted( + UUID.randomUUID(), + System.currentTimeMillis(), + getFileExtension(imageFile.getOriginalFilename()), + imageFile.getOriginalFilename(), + templateId + ); + } + + /** + * OCR ๊ฒฐ๊ณผ์—์„œ ์ถ”์ถœ๋œ ํ•„๋“œ ๊ฐœ์ˆ˜ ๊ณ„์‚ฐ + */ + private int countExtractedFields(String ocrResult) { + try { + JsonNode root = objectMapper.readTree(ocrResult); + JsonNode fields = root.path("images").get(0).path("fields"); + + if (fields.size() > 0) { + String text = fields.get(0).path("inferText").asText(); + // ์ธ๋ฐ”๋”” ๊ด€๋ จ ํ‚ค์›Œ๋“œ ๊ฐœ์ˆ˜ ๊ณ„์‚ฐ + String[] keywords = {"์ฒด์ค‘", "์ฒด์ง€๋ฐฉ", "๊ณจ๊ฒฉ๊ทผ", "BMI", "๋‹จ๋ฐฑ์งˆ", "์ฒด์ˆ˜๋ถ„", "๋ฌด๊ธฐ์งˆ"}; + int count = 0; + for (String keyword : keywords) { + if (text.contains(keyword)) count++; + } + return count; + } + return 0; + } catch (Exception e) { + log.warn("ํ•„๋“œ ๊ฐœ์ˆ˜ ๊ณ„์‚ฐ ์‹คํŒจ: {}", e.getMessage()); + return 0; + } + } + + /** + * OCR ๊ฒฐ๊ณผ ํ’ˆ์งˆ ๊ฒ€์ฆ + */ + private boolean isValidOcrResult(String result) { + try { + JsonNode root = objectMapper.readTree(result); + JsonNode fields = root.path("images").get(0).path("fields"); + + if (fields.size() > 0) { + String text = fields.get(0).path("inferText").asText(); + // ์ตœ์†Œํ•œ์˜ ํ…์ŠคํŠธ๊ฐ€ ์žˆ๊ณ , ์ˆซ์ž๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธ + boolean hasMinLength = text.length() > 10; + boolean hasNumbers = text.matches(".*\\d+.*"); + boolean hasInBodyKeywords = text.contains("์ฒด์ค‘") || text.contains("BMI") || + text.contains("Weight") || text.contains("์ฒด์ง€๋ฐฉ"); + + return hasMinLength && hasNumbers && hasInBodyKeywords; + } + return false; + } catch (Exception e) { + log.warn("OCR ๊ฒฐ๊ณผ ๊ฒ€์ฆ ์‹คํŒจ: {}", e.getMessage()); + return false; + } + } + + /** + * ํŒŒ์ผ ํ™•์žฅ์ž ์ถ”์ถœ + */ private String getFileExtension(String filename) { if (filename == null) return "jpg"; int dotIndex = filename.lastIndexOf('.'); return (dotIndex != -1) ? filename.substring(dotIndex + 1).toLowerCase() : "jpg"; } + + /** + * byte[]๋ฅผ MultipartFile๋กœ ๋ณ€ํ™˜ + */ + private MultipartFile createMultipartFileFromBytes(byte[] bytes, String originalFilename) { + return new MultipartFile() { + @Override + public String getName() { + return "file"; + } + + @Override + public String getOriginalFilename() { + return originalFilename; + } + + @Override + public String getContentType() { + return "image/jpeg"; + } + + @Override + public boolean isEmpty() { + return bytes == null || bytes.length == 0; + } + + @Override + public long getSize() { + return bytes.length; + } + + @Override + public byte[] getBytes() throws IOException { + return bytes; + } + + @Override + public java.io.InputStream getInputStream() throws IOException { + return new java.io.ByteArrayInputStream(bytes); + } + + @Override + public void transferTo(java.io.File dest) throws IOException, IllegalStateException { + try (java.io.FileOutputStream fos = new java.io.FileOutputStream(dest)) { + fos.write(bytes); + } + } + }; + } } diff --git a/src/main/java/com/mumuk/global/util/ImagePreprocessingUtil.java b/src/main/java/com/mumuk/global/util/ImagePreprocessingUtil.java new file mode 100644 index 00000000..dfa17646 --- /dev/null +++ b/src/main/java/com/mumuk/global/util/ImagePreprocessingUtil.java @@ -0,0 +1,222 @@ +package com.mumuk.global.util; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.IOException; +import javax.imageio.ImageIO; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; + +@Slf4j +@Component +public class ImagePreprocessingUtil { + + /** + * ์ „์ฒ˜๋ฆฌ๊ฐ€ ํ•„์š”ํ•œ์ง€ ํŒ๋‹จ + */ + public boolean needsPreprocessing(MultipartFile imageFile) { + try { + byte[] imageBytes = imageFile.getBytes(); + ImageQualityResult qualityResult = analyzeImageQuality(imageBytes); + return qualityResult.hasAnyIssue(); + } catch (IOException e) { + log.error("โŒ ์ „์ฒ˜๋ฆฌ ํ•„์š” ์—ฌ๋ถ€ ํŒ๋‹จ ์‹คํŒจ", e); + return true; // ์•ˆ์ „ํ•˜๊ฒŒ ์ „์ฒ˜๋ฆฌ ํ•„์š”ํ•˜๋‹ค๊ณ  ์ฒ˜๋ฆฌ + } + } + + /** + * OCR์„ ์œ„ํ•œ ์ด๋ฏธ์ง€ ์ „์ฒ˜๋ฆฌ + */ + public byte[] preprocessForOcr(MultipartFile imageFile) { + try { + byte[] originalBytes = imageFile.getBytes(); + return preprocessImage(originalBytes); + } catch (IOException e) { + log.error("โŒ OCR ์ „์ฒ˜๋ฆฌ ์‹คํŒจ", e); + try { + return imageFile.getBytes(); // ์‹คํŒจ์‹œ ์›๋ณธ ๋ฐ˜ํ™˜ + } catch (IOException ex) { + throw new RuntimeException("์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ ์‹คํŒจ", ex); + } + } + } + + /** + * ์ด๋ฏธ์ง€ ํ’ˆ์งˆ ๋ถ„์„ + */ + public ImageQualityResult analyzeImageQuality(byte[] imageBytes) { + try { + BufferedImage image = ImageIO.read(new ByteArrayInputStream(imageBytes)); + + boolean sizeIssue = image.getWidth() < 300 || image.getHeight() < 300; + boolean contrastIssue = hasLowContrast(image); + + log.info("๐Ÿ“Š ์ด๋ฏธ์ง€ ํ’ˆ์งˆ ๋ถ„์„: ํฌ๊ธฐ๋ฌธ์ œ={}, ๋Œ€๋น„๋ฌธ์ œ={}", sizeIssue, contrastIssue); + + return new ImageQualityResult(sizeIssue, contrastIssue); + + } catch (IOException e) { + log.error("โŒ ์ด๋ฏธ์ง€ ํ’ˆ์งˆ ๋ถ„์„ ์‹คํŒจ", e); + return new ImageQualityResult(true, true); // ์•ˆ์ „ํ•˜๊ฒŒ ๋ฌธ์ œ ์žˆ๋‹ค๊ณ  ์ฒ˜๋ฆฌ + } + } + + /** + * ์ด๋ฏธ์ง€ ์ „์ฒ˜๋ฆฌ (ํ’ˆ์งˆ ๊ฐœ์„ ) + */ + public byte[] preprocessImage(byte[] originalImageBytes) { + try { + BufferedImage image = ImageIO.read(new ByteArrayInputStream(originalImageBytes)); + + // ํ•ด์ƒ๋„ ๊ฐœ์„  + BufferedImage enhanced = enhanceResolution(image); + + // ๋Œ€๋น„ ๊ฐœ์„  + enhanced = enhanceContrast(enhanced); + + // ๋…ธ์ด์ฆˆ ์ œ๊ฑฐ + enhanced = removeNoise(enhanced); + + // ๊ฒฐ๊ณผ๋ฅผ byte array๋กœ ๋ณ€ํ™˜ + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ImageIO.write(enhanced, "jpg", baos); + + log.info("โœ… ์ด๋ฏธ์ง€ ์ „์ฒ˜๋ฆฌ ์™„๋ฃŒ"); + return baos.toByteArray(); + + } catch (IOException e) { + log.error("โŒ ์ด๋ฏธ์ง€ ์ „์ฒ˜๋ฆฌ ์‹คํŒจ, ์›๋ณธ ๋ฐ˜ํ™˜", e); + return originalImageBytes; // ์‹คํŒจ์‹œ ์›๋ณธ ๋ฐ˜ํ™˜ + } + } + + private boolean hasLowContrast(BufferedImage image) { + // ๊ฐ„๋‹จํ•œ ๋Œ€๋น„ ๊ฒ€์‚ฌ ๋กœ์ง + int[] histogram = new int[256]; + + for (int y = 0; y < image.getHeight(); y++) { + for (int x = 0; x < image.getWidth(); x++) { + int rgb = image.getRGB(x, y); + int gray = (int) (0.299 * ((rgb >> 16) & 0xFF) + + 0.587 * ((rgb >> 8) & 0xFF) + + 0.114 * (rgb & 0xFF)); + histogram[gray]++; + } + } + + // ํžˆ์Šคํ† ๊ทธ๋žจ ๋ถ„์‚ฐ์„ ํ†ตํ•œ ๋Œ€๋น„ ํŒ๋‹จ + double mean = 127.5; + double variance = 0; + int totalPixels = image.getWidth() * image.getHeight(); + + for (int i = 0; i < 256; i++) { + double probability = (double) histogram[i] / totalPixels; + variance += probability * Math.pow(i - mean, 2); + } + + return variance < 1000; // ์ž„๊ณ„๊ฐ’์€ ์กฐ์ • ๊ฐ€๋Šฅ + } + + private BufferedImage enhanceResolution(BufferedImage image) { + // ์ตœ์†Œ ํ•ด์ƒ๋„ ๋ณด์žฅ + int minWidth = 800; + int minHeight = 600; + + if (image.getWidth() < minWidth || image.getHeight() < minHeight) { + double scaleX = (double) minWidth / image.getWidth(); + double scaleY = (double) minHeight / image.getHeight(); + double scale = Math.max(scaleX, scaleY); + + int newWidth = (int) (image.getWidth() * scale); + int newHeight = (int) (image.getHeight() * scale); + + BufferedImage scaledImage = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_RGB); + Graphics2D g2d = scaledImage.createGraphics(); + g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); + g2d.drawImage(image, 0, 0, newWidth, newHeight, null); + g2d.dispose(); + + return scaledImage; + } + + return image; + } + + private BufferedImage enhanceContrast(BufferedImage image) { + BufferedImage enhanced = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_RGB); + + for (int y = 0; y < image.getHeight(); y++) { + for (int x = 0; x < image.getWidth(); x++) { + int rgb = image.getRGB(x, y); + + int r = (rgb >> 16) & 0xFF; + int g = (rgb >> 8) & 0xFF; + int b = rgb & 0xFF; + + // ๊ฐ„๋‹จํ•œ ๋Œ€๋น„ ํ–ฅ์ƒ (gamma correction) + r = (int) Math.min(255, Math.pow(r / 255.0, 0.8) * 255); + g = (int) Math.min(255, Math.pow(g / 255.0, 0.8) * 255); + b = (int) Math.min(255, Math.pow(b / 255.0, 0.8) * 255); + + enhanced.setRGB(x, y, (r << 16) | (g << 8) | b); + } + } + + return enhanced; + } + + private BufferedImage removeNoise(BufferedImage image) { + // ๊ฐ„๋‹จํ•œ ๋ธ”๋Ÿฌ ํ•„ํ„ฐ๋กœ ๋…ธ์ด์ฆˆ ์ œ๊ฑฐ + BufferedImage denoised = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_RGB); + + for (int y = 1; y < image.getHeight() - 1; y++) { + for (int x = 1; x < image.getWidth() - 1; x++) { + int totalR = 0, totalG = 0, totalB = 0; + + for (int dy = -1; dy <= 1; dy++) { + for (int dx = -1; dx <= 1; dx++) { + int rgb = image.getRGB(x + dx, y + dy); + totalR += (rgb >> 16) & 0xFF; + totalG += (rgb >> 8) & 0xFF; + totalB += rgb & 0xFF; + } + } + + int avgR = totalR / 9; + int avgG = totalG / 9; + int avgB = totalB / 9; + + denoised.setRGB(x, y, (avgR << 16) | (avgG << 8) | avgB); + } + } + + return denoised; + } + + public static class ImageQualityResult { + private final boolean sizeIssue; + private final boolean contrastIssue; + + public ImageQualityResult(boolean sizeIssue, boolean contrastIssue) { + this.sizeIssue = sizeIssue; + this.contrastIssue = contrastIssue; + } + + public boolean hasSizeIssue() { + return sizeIssue; + } + + public boolean hasContrastIssue() { + return contrastIssue; + } + + public boolean hasAnyIssue() { + return sizeIssue || contrastIssue; + } + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index b997038b..c876a5e6 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -59,6 +59,12 @@ jwt: app: env: local + ocr: + max-retries: 3 + retry-delay-ms: 2000 + enable-image-preprocessing: true + enable-multi-template: true + confidence-threshold: 0.7 kakao: redirect-uri: http://localhost:8080/login/oauth2/code/kakao # ์ธ๊ฐ€ ์ฝ”๋“œ