diff --git a/src/main/java/com/seeat/server/domain/review/application/dto/response/OcrResponse.java b/src/main/java/com/seeat/server/domain/review_ai/application/dto/response/OcrResponse.java similarity index 97% rename from src/main/java/com/seeat/server/domain/review/application/dto/response/OcrResponse.java rename to src/main/java/com/seeat/server/domain/review_ai/application/dto/response/OcrResponse.java index 0d0f18ca..dde1a599 100644 --- a/src/main/java/com/seeat/server/domain/review/application/dto/response/OcrResponse.java +++ b/src/main/java/com/seeat/server/domain/review_ai/application/dto/response/OcrResponse.java @@ -1,4 +1,4 @@ -package com.seeat.server.domain.review.application.dto.response; +package com.seeat.server.domain.review_ai.application.dto.response; import com.seeat.server.domain.theater.domain.entity.Seat; import io.swagger.v3.oas.annotations.media.Schema; diff --git a/src/main/java/com/seeat/server/domain/review/application/dto/response/ReviewSummaryResponse.java b/src/main/java/com/seeat/server/domain/review_ai/application/dto/response/ReviewSummaryResponse.java similarity index 95% rename from src/main/java/com/seeat/server/domain/review/application/dto/response/ReviewSummaryResponse.java rename to src/main/java/com/seeat/server/domain/review_ai/application/dto/response/ReviewSummaryResponse.java index e34461ae..1b9c42e5 100644 --- a/src/main/java/com/seeat/server/domain/review/application/dto/response/ReviewSummaryResponse.java +++ b/src/main/java/com/seeat/server/domain/review_ai/application/dto/response/ReviewSummaryResponse.java @@ -1,4 +1,4 @@ -package com.seeat.server.domain.review.application.dto.response; +package com.seeat.server.domain.review_ai.application.dto.response; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; diff --git a/src/main/java/com/seeat/server/domain/review/application/service/ReviewSummaryService.java b/src/main/java/com/seeat/server/domain/review_ai/application/service/ReviewSummaryService.java similarity index 77% rename from src/main/java/com/seeat/server/domain/review/application/service/ReviewSummaryService.java rename to src/main/java/com/seeat/server/domain/review_ai/application/service/ReviewSummaryService.java index 4a17c826..56d369b7 100644 --- a/src/main/java/com/seeat/server/domain/review/application/service/ReviewSummaryService.java +++ b/src/main/java/com/seeat/server/domain/review_ai/application/service/ReviewSummaryService.java @@ -1,22 +1,25 @@ -package com.seeat.server.domain.review.application.service; +package com.seeat.server.domain.review_ai.application.service; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.seeat.server.domain.review.application.dto.response.ReviewSummaryResponse; -import com.seeat.server.domain.review.application.usecase.ReviewSummaryUseCase; +import com.seeat.server.domain.review_ai.application.dto.response.ReviewSummaryResponse; +import com.seeat.server.domain.review_ai.application.usecase.ReviewSummaryUseCase; import com.seeat.server.domain.review.application.usecase.ReviewUseCase; -import com.seeat.server.domain.review.external.LangchainApi; -import com.seeat.server.domain.review.external.LangchainApiException; +import com.seeat.server.domain.review_ai.external.LangchainApi; +import com.seeat.server.domain.review_ai.external.LangchainApiException; import com.seeat.server.domain.theater.application.usecase.TheaterUseCase; import com.seeat.server.domain.theater.domain.entity.Auditorium; import com.seeat.server.global.response.ErrorCode; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.cache.annotation.Cacheable; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.concurrent.TimeUnit; + import static com.seeat.server.global.util.RedisKeyUtil.REVIEW_SUMMARY_KEY; /** @@ -33,19 +36,24 @@ public class ReviewSummaryService implements ReviewSummaryUseCase { private final LangchainApi langchainApi; + private final RedisTemplate redisTemplate; /// 외부 의존성 private final TheaterUseCase theaterService; private final ReviewUseCase reviewService; private final ObjectMapper objectMapper; + /** * 상영관 ID를 바탕으로 랭체인 요청 보내는 함수 * 레디스에서 값 가져오기, 없으면 동기화로 AI에게 요청 - * @param auditoriumId 상영관 ID + * + * @param auditoriumId 상영관 ID */ @Override - @Cacheable(cacheNames = REVIEW_SUMMARY_KEY, key = "#auditoriumId") + @Cacheable(cacheNames = REVIEW_SUMMARY_KEY, + key = "#auditoriumId", + unless = "#result.summary.contains('요약이 가능') == false") public ReviewSummaryResponse loadSummaryByAuditoriumId(String auditoriumId) { /// 상영관 예외처리 @@ -63,8 +71,15 @@ public ReviewSummaryResponse loadSummaryByAuditoriumId(String auditoriumId) { } /// 상영관 아이디 전송으로 AI에게 요약 정보 요청하기 - String summary; + String summary = getSummary(auditorium); + + /// 결과 응답하기 + return ReviewSummaryResponse.from(auditorium.getId(), auditoriumName, summary); + } + /// 랭체인을 바탕으로 요약하는 함수 + private String getSummary(Auditorium auditorium) { + String summary; try { String rawSummary = langchainApi.postSummaryByLangchain(auditorium.getId()) .block(); @@ -84,7 +99,6 @@ public ReviewSummaryResponse loadSummaryByAuditoriumId(String auditoriumId) { throw new IllegalStateException(ErrorCode.INTERNAL_LANGCHAIN_ERROR.getMessage()); } - /// 결과 응답하기 - return ReviewSummaryResponse.from(auditorium.getId(), auditoriumName, summary); + return summary; } } diff --git a/src/main/java/com/seeat/server/domain/review/application/service/TicketOcrService.java b/src/main/java/com/seeat/server/domain/review_ai/application/service/TicketOcrService.java similarity index 59% rename from src/main/java/com/seeat/server/domain/review/application/service/TicketOcrService.java rename to src/main/java/com/seeat/server/domain/review_ai/application/service/TicketOcrService.java index 7dcf619d..0c8aa1b1 100644 --- a/src/main/java/com/seeat/server/domain/review/application/service/TicketOcrService.java +++ b/src/main/java/com/seeat/server/domain/review_ai/application/service/TicketOcrService.java @@ -1,8 +1,8 @@ -package com.seeat.server.domain.review.application.service; +package com.seeat.server.domain.review_ai.application.service; -import com.seeat.server.domain.review.application.usecase.TicketOcrUseCase; -import com.seeat.server.domain.review.external.NaverOcrApi; -import com.seeat.server.domain.review.application.dto.response.OcrResponse; +import com.seeat.server.domain.review_ai.application.usecase.TicketOcrUseCase; +import com.seeat.server.domain.review_ai.external.NaverOcrApi; +import com.seeat.server.domain.review_ai.application.dto.response.OcrResponse; import com.seeat.server.domain.theater.application.usecase.TheaterUseCase; import com.seeat.server.domain.theater.domain.entity.Auditorium; import com.seeat.server.domain.theater.domain.entity.Seat; @@ -60,11 +60,22 @@ public OcrResponse extractText(MultipartFile file) throws Exception { /// OCR API 호출 (텍스트 추출 결과 리스트) List result = naverOcrApi.callApi(HttpMethod.POST.name(), tempFile.getAbsolutePath(), ext); - /// OCR 결과를 분기 - OcrCGVResponse cgvResponse = cgvImageOCR(result); + String type = result.get(3); + + /// 공통 응답 + OcrTheaterResponse theaterResponse; + + if (type.equals("메가박스 영화 예시")) { + + /// 메가박스 적용 + theaterResponse = megaboxImageOCR(result); + } else { + /// CGV 적용 + theaterResponse = cgvImageOCR(result); + } /// 분기한 것을 DTO로 변환 - return toResponse(cgvResponse); + return toResponse(theaterResponse); } finally { // 임시 파일 삭제 (예외 발생 여부와 관계없이 항상 삭제) @@ -74,8 +85,8 @@ public OcrResponse extractText(MultipartFile file) throws Exception { } } - /// - private OcrCGVResponse cgvImageOCR(List strings) { + /// CGV 파싱 + private OcrTheaterResponse cgvImageOCR(List strings) { /// 예외 처리 if (strings == null || strings.size() < 3) { @@ -124,24 +135,86 @@ private OcrCGVResponse cgvImageOCR(List strings) { } - return OcrCGVResponse.from(theater, title, hall, seat); + return OcrTheaterResponse.from(theater, title, hall, seat); + } + + /// 메가박스 파싱 + private OcrTheaterResponse megaboxImageOCR(List strings) { + + /// 예외 처리 + if (strings == null || strings.size() < 3) { + return null; + } + + String titleInfo = strings.get(0); + String hallInfo = strings.get(1); + String seatInfo = strings.get(2); + + /// 영화제목 정보 추출 + String[] titleParts = titleInfo.split("\n"); + String title = titleParts.length > 0 ? titleParts[0] : null; + System.out.println("title: " + title); + + /// 상영관 정보 추출 + String[] hallLines = hallInfo.split("\n"); + String rawTheater = hallLines.length > 0 ? hallLines[0].trim() : null; + String theater = null; + + /// 메가박스가 없다면 추가 + if (rawTheater != null) { + theater = rawTheater.startsWith("메가박스") ? rawTheater : "메가박스 " + rawTheater; + System.out.println("theater: " + theater); + } + + + /// "IMAX관 7층" 같은 줄에서 "IMAX관"만 추출 + String hallLine = hallLines.length > 1 ? hallLines[1] : null; + String hall = null; + if (hallLine != null) { + hall = hallLine; + System.out.println("hall: " + hall); + + } + + /// 좌석 정보 추출 + String[] seatLines = seatInfo.split("\n"); + List seat = new ArrayList<>(); + + /// 콤마로 되어있는 것 나누기 + for (String line : seatLines) { + + // "좌석"이라는 단어가 포함된 라인은 건너뜀 + if (line.contains("좌석")) { + continue; + } + + String[] parts = line.split(","); + for (String part : parts) { + String trimmed = part.trim(); + if (!trimmed.isEmpty()) { + seat.add(trimmed); + } + } + } + + return OcrTheaterResponse.from(theater, title, hall, seat); } /// DB에서 결과 가져오기 - private OcrResponse toResponse(OcrCGVResponse cgvResponse) { + private OcrResponse toResponse(OcrTheaterResponse theaterResponse) { /// 예외처리 - if (cgvResponse == null) { + if (theaterResponse == null) { return null; } /// DB에서 해당 값들 조회 - String theater = cgvResponse.theater; - String title = cgvResponse.title; - String auditorium = cgvResponse.auditorium; - List seats = cgvResponse.seats; + String theater = theaterResponse.theater; + String title = theaterResponse.title; + String auditorium = theaterResponse.auditorium; + List seats = theaterResponse.seats; /// 체크 List checkSeats = theaterService.getSeatByName(theater, auditorium, seats); @@ -159,14 +232,14 @@ private OcrResponse toResponse(OcrCGVResponse cgvResponse) { /// CGV OCR 응답을 내부에서 사용하는 DTO @Builder - record OcrCGVResponse( + record OcrTheaterResponse( String theater, String title, String auditorium, List seats){ /// 정적 팩토리 메서드 - public static OcrCGVResponse from(String theater, String title, String auditorium, List seats) { - return OcrCGVResponse.builder() + public static OcrTheaterResponse from(String theater, String title, String auditorium, List seats) { + return OcrTheaterResponse.builder() .theater(theater) .title(title) .auditorium(auditorium) diff --git a/src/main/java/com/seeat/server/domain/review/application/usecase/ReviewSummaryUseCase.java b/src/main/java/com/seeat/server/domain/review_ai/application/usecase/ReviewSummaryUseCase.java similarity index 51% rename from src/main/java/com/seeat/server/domain/review/application/usecase/ReviewSummaryUseCase.java rename to src/main/java/com/seeat/server/domain/review_ai/application/usecase/ReviewSummaryUseCase.java index 65f4d28b..2bdeb105 100644 --- a/src/main/java/com/seeat/server/domain/review/application/usecase/ReviewSummaryUseCase.java +++ b/src/main/java/com/seeat/server/domain/review_ai/application/usecase/ReviewSummaryUseCase.java @@ -1,6 +1,6 @@ -package com.seeat.server.domain.review.application.usecase; +package com.seeat.server.domain.review_ai.application.usecase; -import com.seeat.server.domain.review.application.dto.response.ReviewSummaryResponse; +import com.seeat.server.domain.review_ai.application.dto.response.ReviewSummaryResponse; public interface ReviewSummaryUseCase { diff --git a/src/main/java/com/seeat/server/domain/review/application/usecase/TicketOcrUseCase.java b/src/main/java/com/seeat/server/domain/review_ai/application/usecase/TicketOcrUseCase.java similarity index 78% rename from src/main/java/com/seeat/server/domain/review/application/usecase/TicketOcrUseCase.java rename to src/main/java/com/seeat/server/domain/review_ai/application/usecase/TicketOcrUseCase.java index f87e0a81..7a6c0399 100644 --- a/src/main/java/com/seeat/server/domain/review/application/usecase/TicketOcrUseCase.java +++ b/src/main/java/com/seeat/server/domain/review_ai/application/usecase/TicketOcrUseCase.java @@ -1,6 +1,6 @@ -package com.seeat.server.domain.review.application.usecase; +package com.seeat.server.domain.review_ai.application.usecase; -import com.seeat.server.domain.review.application.dto.response.OcrResponse; +import com.seeat.server.domain.review_ai.application.dto.response.OcrResponse; import org.springframework.web.multipart.MultipartFile; /** diff --git a/src/main/java/com/seeat/server/domain/review/external/LangchainApi.java b/src/main/java/com/seeat/server/domain/review_ai/external/LangchainApi.java similarity index 98% rename from src/main/java/com/seeat/server/domain/review/external/LangchainApi.java rename to src/main/java/com/seeat/server/domain/review_ai/external/LangchainApi.java index 7b34b150..76f5bce0 100644 --- a/src/main/java/com/seeat/server/domain/review/external/LangchainApi.java +++ b/src/main/java/com/seeat/server/domain/review_ai/external/LangchainApi.java @@ -1,4 +1,4 @@ -package com.seeat.server.domain.review.external; +package com.seeat.server.domain.review_ai.external; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/com/seeat/server/domain/review/external/LangchainApiException.java b/src/main/java/com/seeat/server/domain/review_ai/external/LangchainApiException.java similarity index 82% rename from src/main/java/com/seeat/server/domain/review/external/LangchainApiException.java rename to src/main/java/com/seeat/server/domain/review_ai/external/LangchainApiException.java index 4aa6febc..a74848c1 100644 --- a/src/main/java/com/seeat/server/domain/review/external/LangchainApiException.java +++ b/src/main/java/com/seeat/server/domain/review_ai/external/LangchainApiException.java @@ -1,4 +1,4 @@ -package com.seeat.server.domain.review.external; +package com.seeat.server.domain.review_ai.external; public class LangchainApiException extends RuntimeException { public LangchainApiException(String message, Throwable throwable) { diff --git a/src/main/java/com/seeat/server/domain/review/external/NaverOcrApi.java b/src/main/java/com/seeat/server/domain/review_ai/external/NaverOcrApi.java similarity index 95% rename from src/main/java/com/seeat/server/domain/review/external/NaverOcrApi.java rename to src/main/java/com/seeat/server/domain/review_ai/external/NaverOcrApi.java index 4f4214cd..f171d076 100644 --- a/src/main/java/com/seeat/server/domain/review/external/NaverOcrApi.java +++ b/src/main/java/com/seeat/server/domain/review_ai/external/NaverOcrApi.java @@ -1,4 +1,4 @@ -package com.seeat.server.domain.review.external; +package com.seeat.server.domain.review_ai.external; import lombok.extern.slf4j.Slf4j; import org.json.simple.JSONArray; @@ -66,6 +66,7 @@ public List callApi(String httpMethod, String filePath, String ext) { images.add(image); json.put("images", images); + String postParams = json.toString(); // 멀티파트 데이터 전송 @@ -179,6 +180,14 @@ private List jsonParse(String response) throws ParseException, org.json. } } + // matchedTemplate 추출 + if (imageObj.containsKey("matchedTemplate")) { + JSONObject matchedTemplate = (JSONObject) imageObj.get("matchedTemplate"); + + result.add(matchedTemplate.get("name").toString()); + + } + return result; } } diff --git a/src/main/java/com/seeat/server/domain/review/presentation/ReviewSummaryController.java b/src/main/java/com/seeat/server/domain/review_ai/presentation/ReviewSummaryController.java similarity index 77% rename from src/main/java/com/seeat/server/domain/review/presentation/ReviewSummaryController.java rename to src/main/java/com/seeat/server/domain/review_ai/presentation/ReviewSummaryController.java index 5d982c6f..7dcd1b0e 100644 --- a/src/main/java/com/seeat/server/domain/review/presentation/ReviewSummaryController.java +++ b/src/main/java/com/seeat/server/domain/review_ai/presentation/ReviewSummaryController.java @@ -1,8 +1,8 @@ -package com.seeat.server.domain.review.presentation; +package com.seeat.server.domain.review_ai.presentation; -import com.seeat.server.domain.review.application.dto.response.ReviewSummaryResponse; -import com.seeat.server.domain.review.application.usecase.ReviewSummaryUseCase; -import com.seeat.server.domain.review.presentation.swagger.ReviewSummaryControllerSpec; +import com.seeat.server.domain.review_ai.application.dto.response.ReviewSummaryResponse; +import com.seeat.server.domain.review_ai.application.usecase.ReviewSummaryUseCase; +import com.seeat.server.domain.review_ai.presentation.swagger.ReviewSummaryControllerSpec; import com.seeat.server.global.response.ApiResponse; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; diff --git a/src/main/java/com/seeat/server/domain/review/presentation/TicketOcrController.java b/src/main/java/com/seeat/server/domain/review_ai/presentation/TicketOcrController.java similarity index 72% rename from src/main/java/com/seeat/server/domain/review/presentation/TicketOcrController.java rename to src/main/java/com/seeat/server/domain/review_ai/presentation/TicketOcrController.java index e5949a19..887d6d5e 100644 --- a/src/main/java/com/seeat/server/domain/review/presentation/TicketOcrController.java +++ b/src/main/java/com/seeat/server/domain/review_ai/presentation/TicketOcrController.java @@ -1,8 +1,8 @@ -package com.seeat.server.domain.review.presentation; +package com.seeat.server.domain.review_ai.presentation; -import com.seeat.server.domain.review.application.usecase.TicketOcrUseCase; -import com.seeat.server.domain.review.application.dto.response.OcrResponse; -import com.seeat.server.domain.review.presentation.swagger.TicketOcrControllerSpec; +import com.seeat.server.domain.review_ai.application.usecase.TicketOcrUseCase; +import com.seeat.server.domain.review_ai.application.dto.response.OcrResponse; +import com.seeat.server.domain.review_ai.presentation.swagger.TicketOcrControllerSpec; import com.seeat.server.global.response.ApiResponse; import lombok.RequiredArgsConstructor; import org.springframework.http.MediaType; diff --git a/src/main/java/com/seeat/server/domain/review/presentation/swagger/ReviewSummaryControllerSpec.java b/src/main/java/com/seeat/server/domain/review_ai/presentation/swagger/ReviewSummaryControllerSpec.java similarity index 86% rename from src/main/java/com/seeat/server/domain/review/presentation/swagger/ReviewSummaryControllerSpec.java rename to src/main/java/com/seeat/server/domain/review_ai/presentation/swagger/ReviewSummaryControllerSpec.java index 02ce9e11..797b80a4 100644 --- a/src/main/java/com/seeat/server/domain/review/presentation/swagger/ReviewSummaryControllerSpec.java +++ b/src/main/java/com/seeat/server/domain/review_ai/presentation/swagger/ReviewSummaryControllerSpec.java @@ -1,6 +1,6 @@ -package com.seeat.server.domain.review.presentation.swagger; +package com.seeat.server.domain.review_ai.presentation.swagger; -import com.seeat.server.domain.review.application.dto.response.ReviewSummaryResponse; +import com.seeat.server.domain.review_ai.application.dto.response.ReviewSummaryResponse; import com.seeat.server.global.response.ApiResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; diff --git a/src/main/java/com/seeat/server/domain/review/presentation/swagger/TicketOcrControllerSpec.java b/src/main/java/com/seeat/server/domain/review_ai/presentation/swagger/TicketOcrControllerSpec.java similarity index 82% rename from src/main/java/com/seeat/server/domain/review/presentation/swagger/TicketOcrControllerSpec.java rename to src/main/java/com/seeat/server/domain/review_ai/presentation/swagger/TicketOcrControllerSpec.java index 7413c7b9..3b90f2a2 100644 --- a/src/main/java/com/seeat/server/domain/review/presentation/swagger/TicketOcrControllerSpec.java +++ b/src/main/java/com/seeat/server/domain/review_ai/presentation/swagger/TicketOcrControllerSpec.java @@ -1,6 +1,6 @@ -package com.seeat.server.domain.review.presentation.swagger; +package com.seeat.server.domain.review_ai.presentation.swagger; -import com.seeat.server.domain.review.application.dto.response.OcrResponse; +import com.seeat.server.domain.review_ai.application.dto.response.OcrResponse; import com.seeat.server.global.response.ApiResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; diff --git a/src/main/java/com/seeat/server/domain/theater/application/service/TheaterService.java b/src/main/java/com/seeat/server/domain/theater/application/service/TheaterService.java index b5b63a9f..8ba5d82c 100644 --- a/src/main/java/com/seeat/server/domain/theater/application/service/TheaterService.java +++ b/src/main/java/com/seeat/server/domain/theater/application/service/TheaterService.java @@ -136,11 +136,13 @@ public List getSeat(List seatIds) { @Override public List getSeatByName(String theaterName, String auditoriumName, List seats) { + /// 이름 하드 코딩 + String name = getName(theaterName); + /// 영화관 예외 처리 - Theater theater = theaterRepository.findByName(theaterName) + Theater theater = theaterRepository.findByName(name) .orElseThrow(() -> new NoSuchElementException(ErrorCode.NOT_THEATER.getMessage())); - /// 상영관 예외 처리 Auditorium auditorium = auditoriumRepository.findByNameContainingIgnoreCaseAndTheater_Id(auditoriumName, theater.getId()) .orElseThrow(() -> new NoSuchElementException(ErrorCode.NOT_AUDITORIUM.getMessage())); @@ -164,6 +166,17 @@ public List getSeatByName(String theaterName, String auditoriumName, List< } + /// 인식 안되는 오류 상영관들 하드코딩 + private String getName(String theaterName) { + + /// 하드 코딩 + if (theaterName.equals("메가박스 남양주현대아울렛스퍼이스원")){ + theaterName = "메가박스 남양주현대아울렛스페이스원"; + } + + return theaterName; + } + /** * 외부 참조 함수