diff --git a/src/main/java/com/bamboo/log/emotion/controller/EmotionController.java b/src/main/java/com/bamboo/log/emotion/controller/EmotionController.java new file mode 100644 index 0000000..b801e59 --- /dev/null +++ b/src/main/java/com/bamboo/log/emotion/controller/EmotionController.java @@ -0,0 +1,50 @@ +package com.bamboo.log.emotion.controller; + +import com.bamboo.log.emotion.dto.BoundingBox; +import com.bamboo.log.emotion.dto.EmotionAnalysisResponse; +import com.bamboo.log.emotion.dto.req.EmotionAnalysisRequest; +import com.bamboo.log.emotion.service.EmotionAnalysisService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +@RestController +@RequestMapping("/api/emotion") +@RequiredArgsConstructor +@Tag(name = "Emotion Analysis", description = "얼굴 감정(표정)분석 API") +public class EmotionController { + + private final EmotionAnalysisService emotionAnalysisService; + + @PostMapping(value = "/result", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @Operation( + summary = "감정 분석", + description = "반환받은 얼굴 영역 좌표와 이미지를 분석하여 감정을 반환합니다." + ) + public ResponseEntity analyzeEmotion( + + @RequestPart("image") + @Parameter(description = "감정 분석할 얼굴 이미지", required = true) + MultipartFile image, + + @RequestPart("faceBox") + @Parameter(description = "얼굴 영역 좌표 (JSON 형식)", required = true, example = "{\"x1\": 12.34, \"y1\": 56.78, \"x2\": 123.456, \"y2\": 789.87}") + BoundingBox faceBox) { + + EmotionAnalysisRequest request = new EmotionAnalysisRequest(image, faceBox); + EmotionAnalysisResponse response = emotionAnalysisService.analyzeEmotion(request); + + HttpStatus status = HttpStatus.resolve(response.status().value()); + if (status == null) { + status = HttpStatus.INTERNAL_SERVER_ERROR; + } + + return ResponseEntity.status(status).body(response); + } +} diff --git a/src/main/java/com/bamboo/log/emotion/controller/FaceDetectController.java b/src/main/java/com/bamboo/log/emotion/controller/FaceDetectController.java index fee5189..62fd28f 100644 --- a/src/main/java/com/bamboo/log/emotion/controller/FaceDetectController.java +++ b/src/main/java/com/bamboo/log/emotion/controller/FaceDetectController.java @@ -15,14 +15,14 @@ @RestController @RequiredArgsConstructor -@RequestMapping("/api/emotion") -@Tag(name = "Face Detection", description = "얼굴 인식 관련 API") +@RequestMapping("/api/face") +@Tag(name = "Face Detection", description = "얼굴 인식 API") public class FaceDetectController { private final FaceDetectionService faceDetectionService; @PostMapping("/detect") @Operation( - summary = "얼굴 인식 API", + summary = "얼굴 인식", description = "이미지를 업로드하면 얼굴 인식 여부를 반환합니다." ) public ResponseEntity detectFace(@RequestParam("image") MultipartFile image) { diff --git a/src/main/java/com/bamboo/log/emotion/domain/EmotionType.java b/src/main/java/com/bamboo/log/emotion/domain/EmotionType.java new file mode 100644 index 0000000..8fbbe0e --- /dev/null +++ b/src/main/java/com/bamboo/log/emotion/domain/EmotionType.java @@ -0,0 +1,10 @@ +package com.bamboo.log.emotion.domain; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum EmotionType { + ANGRY, HAPPY, NEUTRAL, SAD, NONE ; +} \ No newline at end of file diff --git a/src/main/java/com/bamboo/log/emotion/dto/BoundingBox.java b/src/main/java/com/bamboo/log/emotion/dto/BoundingBox.java new file mode 100644 index 0000000..7fcdb2a --- /dev/null +++ b/src/main/java/com/bamboo/log/emotion/dto/BoundingBox.java @@ -0,0 +1,3 @@ +package com.bamboo.log.emotion.dto; + +public record BoundingBox(double x1, double y1, double x2, double y2) {} diff --git a/src/main/java/com/bamboo/log/emotion/dto/EmotionAnalysisResponse.java b/src/main/java/com/bamboo/log/emotion/dto/EmotionAnalysisResponse.java new file mode 100644 index 0000000..cf925a5 --- /dev/null +++ b/src/main/java/com/bamboo/log/emotion/dto/EmotionAnalysisResponse.java @@ -0,0 +1,6 @@ +package com.bamboo.log.emotion.dto; + +import com.bamboo.log.emotion.domain.EmotionType; +import org.springframework.http.HttpStatus; + +public record EmotionAnalysisResponse (HttpStatus status, EmotionType emotion) {} diff --git a/src/main/java/com/bamboo/log/emotion/dto/FaceDetectionResponse.java b/src/main/java/com/bamboo/log/emotion/dto/FaceDetectionResponse.java index f9a2b09..16551a0 100644 --- a/src/main/java/com/bamboo/log/emotion/dto/FaceDetectionResponse.java +++ b/src/main/java/com/bamboo/log/emotion/dto/FaceDetectionResponse.java @@ -2,12 +2,14 @@ import org.springframework.http.HttpStatus; +import java.util.List; + public record FaceDetectionResponse( int statusCode, String statusMessage, - String message + List faceBox ) { - public FaceDetectionResponse(HttpStatus httpStatus, String message) { - this(httpStatus.value(), httpStatus.name(), message); + public FaceDetectionResponse(HttpStatus httpStatus, List faceBox) { + this(httpStatus.value(), httpStatus.name(), faceBox); } } \ No newline at end of file diff --git a/src/main/java/com/bamboo/log/emotion/dto/req/EmotionAnalysisRequest.java b/src/main/java/com/bamboo/log/emotion/dto/req/EmotionAnalysisRequest.java new file mode 100644 index 0000000..7961010 --- /dev/null +++ b/src/main/java/com/bamboo/log/emotion/dto/req/EmotionAnalysisRequest.java @@ -0,0 +1,6 @@ +package com.bamboo.log.emotion.dto.req; + +import com.bamboo.log.emotion.dto.BoundingBox; +import org.springframework.web.multipart.MultipartFile; + +public record EmotionAnalysisRequest(MultipartFile image, BoundingBox faceBox) {} diff --git a/src/main/java/com/bamboo/log/emotion/service/EmotionAnalysisService.java b/src/main/java/com/bamboo/log/emotion/service/EmotionAnalysisService.java new file mode 100644 index 0000000..7e4300d --- /dev/null +++ b/src/main/java/com/bamboo/log/emotion/service/EmotionAnalysisService.java @@ -0,0 +1,8 @@ +package com.bamboo.log.emotion.service; + +import com.bamboo.log.emotion.dto.EmotionAnalysisResponse; +import com.bamboo.log.emotion.dto.req.EmotionAnalysisRequest; + +public interface EmotionAnalysisService { + EmotionAnalysisResponse analyzeEmotion(EmotionAnalysisRequest request); +} \ No newline at end of file diff --git a/src/main/java/com/bamboo/log/emotion/service/impl/EmotionAnalysisServiceImpl.java b/src/main/java/com/bamboo/log/emotion/service/impl/EmotionAnalysisServiceImpl.java new file mode 100644 index 0000000..c707b8e --- /dev/null +++ b/src/main/java/com/bamboo/log/emotion/service/impl/EmotionAnalysisServiceImpl.java @@ -0,0 +1,84 @@ +package com.bamboo.log.emotion.service.impl; + +import com.bamboo.log.emotion.domain.EmotionType; +import com.bamboo.log.emotion.dto.BoundingBox; +import com.bamboo.log.emotion.dto.EmotionAnalysisResponse; +import com.bamboo.log.emotion.dto.req.EmotionAnalysisRequest; +import com.bamboo.log.emotion.service.EmotionAnalysisService; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import okhttp3.*; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; + +@Slf4j +@Service +@RequiredArgsConstructor +public class EmotionAnalysisServiceImpl implements EmotionAnalysisService { + @Value("${emotion.api.url}") + private String emotionApiUrl; + + private final OkHttpClient client = new OkHttpClient(); + + @Override + public EmotionAnalysisResponse analyzeEmotion(EmotionAnalysisRequest request) { + MultipartFile image = request.image(); + BoundingBox faceBox = request.faceBox(); + + log.info("Received EmotionAnalysisRequest: {}", request); + log.info("Received BoundingBox: {}", (faceBox != null) ? faceBox.toString() : "NULL"); + + if (faceBox == null) { + log.warn("얼굴 영역이 감지되지 않음. 감정 분석 불가능"); + return new EmotionAnalysisResponse(HttpStatus.OK, EmotionType.NONE); + } + + EmotionAnalysisResponse response = callFastAPI(image, request); + + log.info("Received response: {}", response); + return response; + } + + private EmotionAnalysisResponse callFastAPI(MultipartFile image, EmotionAnalysisRequest request) { + try { + RequestBody requestBody = new MultipartBody.Builder() + .setType(MultipartBody.FORM) + .addFormDataPart("image", image.getOriginalFilename(), + RequestBody.create(image.getBytes(), MediaType.parse(image.getContentType()))) + .addFormDataPart("x1", String.valueOf(request.faceBox().x1())) + .addFormDataPart("y1", String.valueOf(request.faceBox().y1())) + .addFormDataPart("x2", String.valueOf(request.faceBox().x2())) + .addFormDataPart("y2", String.valueOf(request.faceBox().y2())) + .build(); + + Request fastApiRequest = new Request.Builder() + .url(emotionApiUrl) + .post(requestBody) + .addHeader("accept", "application/json") + .build(); + + try (Response response = client.newCall(fastApiRequest).execute()) { + if (!response.isSuccessful()) { + return new EmotionAnalysisResponse(HttpStatus.valueOf(response.code()), EmotionType.NONE); + } + + String responseBody = response.body().string(); + JsonNode jsonNode = new ObjectMapper().readTree(responseBody); + String emotionString = jsonNode.get(0).get("emotion").asText(); + EmotionType emotionType = EmotionType.valueOf(emotionString.toUpperCase()); + + log.info("Received response about api: {}", response); + return new EmotionAnalysisResponse(HttpStatus.OK, emotionType); + } + } catch (IOException e) { + log.error("FASTAPI 호출 중 오류 발생", e); + return new EmotionAnalysisResponse(HttpStatus.INTERNAL_SERVER_ERROR, EmotionType.NONE); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/bamboo/log/emotion/service/impl/FaceDetectionServiceImpl.java b/src/main/java/com/bamboo/log/emotion/service/impl/FaceDetectionServiceImpl.java index 47e6d8a..0175ac0 100644 --- a/src/main/java/com/bamboo/log/emotion/service/impl/FaceDetectionServiceImpl.java +++ b/src/main/java/com/bamboo/log/emotion/service/impl/FaceDetectionServiceImpl.java @@ -1,5 +1,6 @@ package com.bamboo.log.emotion.service.impl; +import com.bamboo.log.emotion.dto.BoundingBox; import com.bamboo.log.emotion.dto.FaceDetectionResponse; import com.bamboo.log.emotion.service.FaceDetectionService; import com.fasterxml.jackson.databind.JsonNode; @@ -13,6 +14,8 @@ import org.springframework.web.multipart.MultipartFile; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; @Slf4j @Service @@ -31,7 +34,7 @@ public class FaceDetectionServiceImpl implements FaceDetectionService { public FaceDetectionResponse detectFace(MultipartFile image) { if (image == null || image.isEmpty()) { log.error("파일이 전달되지 않음."); - return new FaceDetectionResponse(HttpStatus.BAD_REQUEST, "파일이 전달되지 않았습니다."); + return new FaceDetectionResponse(HttpStatus.BAD_REQUEST, List.of()); } log.info("파일 이름: {}", image.getOriginalFilename()); log.info("파일 크기: {} bytes", image.getSize()); @@ -61,7 +64,7 @@ public FaceDetectionResponse detectFace(MultipartFile image) { log.info("API 응답 메시지: {}", response.message()); if (!response.isSuccessful()) { - return new FaceDetectionResponse(HttpStatus.valueOf(response.code()), "API 요청 실패: " + response.message()); + return new FaceDetectionResponse(HttpStatus.valueOf(response.code()), List.of()); } String responseBody = response.body().string(); @@ -72,18 +75,27 @@ public FaceDetectionResponse detectFace(MultipartFile image) { JsonNode results = jsonNode.get("results"); // 얼굴 인식 여부 확인 - if (results != null && results.isArray() && results.size() > 0) { + List boxList = new ArrayList<>(); + if (results != null && results.isArray()) { for (JsonNode result : results) { if ("face".equals(result.get("name").asText())) { - return new FaceDetectionResponse(HttpStatus.OK, responseBody); + JsonNode boxNode = result.get("box"); + if (boxNode != null) { + boxList.add(new BoundingBox( + boxNode.get("x1").asDouble(), + boxNode.get("y1").asDouble(), + boxNode.get("x2").asDouble(), + boxNode.get("y2").asDouble() + )); + } } } } - return new FaceDetectionResponse(HttpStatus.NOT_FOUND, "얼굴이 인식되지 않았습니다."); + return new FaceDetectionResponse(HttpStatus.OK, boxList); } } catch (IOException e) { log.error("API 요청 중 예외 발생", e); - return new FaceDetectionResponse(HttpStatus.INTERNAL_SERVER_ERROR, "API 요청 중 오류 발생: " + e.getMessage()); + return new FaceDetectionResponse(HttpStatus.INTERNAL_SERVER_ERROR, List.of()); } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 17527e6..3ab1365 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -39,4 +39,7 @@ elice: url: face: ${FACE_URL} img: ${IMG_URL} - chat: ${CHAT_URL} \ No newline at end of file + chat: ${CHAT_URL} +emotion: + api: + url: ${EMOTION_URL} \ No newline at end of file