상세 컨텐츠

본문 제목

250217 TIL - AI 활용 비즈니스 프로젝트 - P1_ai_description error 수정

Java 심화 3기 Spring boot camp

by Laika25 2025. 2. 17. 23:31

본문

일단 기록용으로 Ai 파트 짠 코드들 나열부터 하겠습니다

AiController.java

package com.p1.nomnom.ai.controller;

import com.p1.nomnom.ai.dto.request.AiRequestDto;
import com.p1.nomnom.ai.dto.response.AiResponseDto;
import com.p1.nomnom.ai.entity.Ai;
import com.p1.nomnom.ai.service.AiService;
import com.p1.nomnom.store.service.StoreService;
import com.p1.nomnom.ai.GeminiService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.UUID;

@RestController
@RequestMapping("/api/nom/ai")
public class AiController {

    private final AiService aiService;
    private final GeminiService geminiService;
    private final StoreService storeService;  // StoreService를 추가합니다.

    @Autowired
    public AiController(AiService aiService, GeminiService geminiService, StoreService storeService) {
        this.aiService = aiService;
        this.geminiService = geminiService;
        this.storeService = storeService;  // StoreService 주입
    }

    // AI 상품 설명 자동 생성
    @PostMapping("/foods/description")
    public AiResponseDto generateFoodDescription(@RequestBody AiRequestDto requestDto) {
        try {
            // Gemini로 텍스트 생성 요청 (question, descriptionHint, keyword를 포함)
            String generatedDescription = geminiService.generateContent(
                    requestDto.getQuestion(),
                    requestDto.getDescriptionHint(),
                    requestDto.getKeyword()
            );

            // storeId로 storeName을 찾기
            String storeName = storeService.getStoreNameById(requestDto.getStoreId());

            // AI 응답을 DB에 저장
            Ai aiEntity = new Ai();
            aiEntity.setQuestion(requestDto.getQuestion());  // 받아온 질문을 저장
            aiEntity.setAnswer(generatedDescription);
            aiEntity.setFoodName(requestDto.getFoodName());
            aiEntity.setDescriptionHint(requestDto.getDescriptionHint());
            aiEntity.setKeyword(requestDto.getKeyword());
            aiEntity.setStoreId(requestDto.getStoreId());  // store_id는 UUID로 변환

            aiService.save(aiEntity);  // DB에 저장 aiService - save 메서드

            // 응답 객체 반환
            return new AiResponseDto(
                    aiEntity.getQuestion(),
                    aiEntity.getFoodName(),
                    aiEntity.getStoreId(),
                    storeName,
                    aiEntity.getDescriptionHint(),
                    aiEntity.getKeyword(),
                    generatedDescription
            );
        } catch (Exception e) {
            e.printStackTrace();
            return new AiResponseDto("Error generating description", "", null, "", "", "", "Error generating description");
        }
    }
}

AiRequestDto.java

package com.p1.nomnom.ai.dto.request;

import lombok.Getter;
import lombok.Setter;

import java.util.UUID;

@Getter
@Setter
public class AiRequestDto {
    private String question;
    private String foodName;
    private String descriptionHint;
    private String keyword;
    private UUID storeId;  // storeId 추가
}

AiResponseDto.java

package com.p1.nomnom.ai.dto.response;

import lombok.Getter;
import lombok.Setter;

import java.util.UUID;

@Getter
@Setter
public class AiResponseDto {
    private String question;            // AI가 받은 질문
    private String foodName;            // 음식 이름
    private UUID storeId;               // storeId (UUID 형식)
    private String storeName;           // storeName - 입력받은 storeId로 이름 찾아오기
    private String descriptionHint;     // 설명 힌트
    private String keyword;             // 설명 키워드
    private String generatedDescription; // AI가 생성한 설명

    // 기본 생성자 추가
    public AiResponseDto() {
    }

    // 생성자 추가
    public AiResponseDto(String question, String foodName, UUID storeId, String storeName,
                         String descriptionHint, String keyword, String generatedDescription) {
        this.question = question;
        this.foodName = foodName;
        this.storeId = storeId;
        this.storeName = storeName;
        this.descriptionHint = descriptionHint;
        this.keyword = keyword;
        this.generatedDescription = generatedDescription;
    }
}

Ai.java

package com.p1.nomnom.ai.entity;

import com.p1.nomnom.common.entity.BaseEntity;
import jakarta.persistence.*;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.Setter;

import java.util.UUID;

@Getter
@Setter
@Entity
@Table(name = "p_ai")
public class Ai extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private UUID id;

    @Column(name = "question", nullable = false, columnDefinition = "TEXT")
    private String question;

    @Column(name = "answer", nullable = false, columnDefinition = "TEXT")
    private String answer;

    @Column(name = "food_name", nullable = false, columnDefinition = "TEXT")
    private String foodName;

    @Column(name = "store_id")
    private UUID storeId;

    @Column(name = "description_hint", columnDefinition = "TEXT")
    private String descriptionHint;

    @Column(name = "keyword", columnDefinition = "TEXT")
    private String keyword;
}

 

AiRepository.java

package com.p1.nomnom.ai.repository;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import com.p1.nomnom.ai.entity.Ai;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.UUID;

@Repository
public interface AiRepository extends JpaRepository<Ai, UUID> {
    // 질문을 포함하고 storeId로 검색하는 메소드
    Page<Ai> findByQuestionContainingAndStoreId(String question, UUID storeId, Pageable pageable);

    // storeId로만 검색하는 메소드
    Page<Ai> findAllByStoreId(UUID storeId, Pageable pageable);
}

AiService.java -> Interface로 분리해서 Service 파일을 두개로 쓰는게 훨씬 깔끔한듯 해요 어떤게 있나 확인할때도 AiService 인터페이스랑 레포지토리랑 컨트롤러만 보면 구성을 알수있으니

package com.p1.nomnom.ai.service;

import com.p1.nomnom.ai.dto.request.AiRequestDto;
import com.p1.nomnom.ai.dto.response.AiResponseDto;
import com.p1.nomnom.ai.entity.Ai;
import org.springframework.data.domain.Sort;

import java.util.List;
import java.util.UUID;

public interface AiService {
    AiResponseDto getAiAnswer(AiRequestDto requestDto);

    void save(Ai aiEntity);

    List<AiResponseDto> searchAi(UUID storeId, String question, int pageSize, Sort sort);
}

AiServiceImpl.java

package com.p1.nomnom.ai.service;

import com.p1.nomnom.ai.dto.request.AiRequestDto;
import com.p1.nomnom.ai.dto.response.AiResponseDto;
import com.p1.nomnom.ai.entity.Ai;
import com.p1.nomnom.ai.repository.AiRepository;
import com.p1.nomnom.store.service.StoreService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.UUID;

@Service
@RequiredArgsConstructor
public class AiServiceImpl implements AiService {

    private final AiRepository aiRepository;
    private final StoreService storeService;

    //@Autowired  대신  @RequiredArgsConstructor
//    public AiServiceImpl(AiRepository aiRepository, StoreService storeService) {
//        this.aiRepository = aiRepository;
//        this.storeService = storeService;
//    }

    @Transactional
    @Override
    public AiResponseDto getAiAnswer(AiRequestDto requestDto) {
        String answer = "AI로부터 받은 답변";  // AI API 호출 결과

        Ai aiEntity = new Ai();
        aiEntity.setQuestion(requestDto.getQuestion());
        aiEntity.setAnswer(answer);
        aiEntity.setFoodName(requestDto.getFoodName());
        aiEntity.setDescriptionHint(requestDto.getDescriptionHint());
        aiEntity.setKeyword(requestDto.getKeyword());

        aiRepository.save(aiEntity);

        // storeId로 storeName을 조회
        UUID storeId = requestDto.getStoreId();
        String storeName = storeService.getStoreNameById(storeId);

        return new AiResponseDto(
                aiEntity.getQuestion(),
                aiEntity.getFoodName(),
                storeId,
                storeName,
                aiEntity.getDescriptionHint(),
                aiEntity.getKeyword(),
                answer
        );
    }

    @Override
    public void save(Ai aiEntity) {
        aiRepository.save(aiEntity);
    }

    @Override
    @Transactional
    public List<AiResponseDto> searchAi(UUID storeId, String question, int pageSize, Sort sort) {
        Pageable pageable = PageRequest.of(0, pageSize, sort);
        Page<Ai> pageResult;

        if (question != null && !question.isEmpty()) {
            pageResult = aiRepository.findByQuestionContainingAndStoreId(question, storeId, pageable);
        } else {
            pageResult = aiRepository.findAllByStoreId(storeId, pageable);
        }

        return pageResult.stream()
                .map(ai -> new AiResponseDto(ai.getQuestion(), ai.getFoodName(), ai.getStoreId(),
                        storeService.getStoreNameById(ai.getStoreId()), ai.getDescriptionHint(), ai.getKeyword(),
                        ai.getAnswer()))
                .toList();
    }
}

GeminiService.java -> 이부분이 젤 중요한데 생각보다 여기서 에러는 적었고 테이버베이스 타입에서 varchar로 값을 주는 바람에 계속 입력받은 값이 너무 크다고 긴 에러를 띄운거였어요 

package com.p1.nomnom.ai;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.http.*;
import java.io.IOException;

@Service
public class GeminiService {

    @Value("${google.api.key}")
    private String apiKey;  // API 키를 application.properties에서 주입받음

    // question과 descriptionHint, keyword를 바탕으로 설명을 생성하는 메서드
    public String generateContent(String question, String descriptionHint, String keyword) {
        // 요청 텍스트 생성
        String fullRequestText = "질문: " + question + " " + descriptionHint + " " + keyword + "에 대해 설명해주세요.";

        // Gemini로 요청 보내기
        return sendRequestToGemini(fullRequestText);
    }

    // Gemini API로 요청을 보내는 메서드
    private String sendRequestToGemini(String prompt) {
        // REST API 호출을 위한 설정
        String url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent?key=" + apiKey;
        RestTemplate restTemplate = new RestTemplate();

        // 요청 데이터 준비
        String requestBody = "{\"contents\": [{\"parts\": [{\"text\": \"" + prompt + "\"}]}]}";

        // 헤더 설정
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);

        // HTTP 요청 설정
        HttpEntity<String> entity = new HttpEntity<>(requestBody, headers);

        // API 호출 및 응답 처리
        ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, entity, String.class);

        // 응답 본문 반환
        return response.getBody();
    }
}



오늘 에러를 고친 방법은 어이없게도 
DB의 타입값 변경입니다

원래제가 테이터 명세서에 작성한 것을 보면 

이렇게 작성이 되어있습니다 값이 들어가야하는 부분은 전부 varchar로 해두고 제한까지 놨죠 근데 제한둔걸 잊어버리고 왜 안들어가지...  하고 씨름중이었으니...어이가없네요

근데 이거도 하나 하나 변경했는데 어떤거에서 공간이 부족한지 몰라서 그랬습니다 그리고 왜 인지는 모르겠지만

기록을 보면 분명히 alter column으로 type을 TEXT로 바꿨는데 적용이 안돼서 몇번 더 한 기록이 있습니다 
이건 왜 이런지 모르겠어요 새로고침 적용이 안된건지

아무든 결과적으로는 type을 TEXT로 변경하니 정상 작동했고 postman으로 테스트 했을 때 
결과도 잘 나왔습니다

 

이제 기능 작동하는거 확인했으니 프로젝트 요구사항에 있는 
<입력 텍스트의 글자수를 제한합니다. 또한 실제 요청 텍스트 마지막에 “답변을 최대한 간결하게 50자 이하로” 라는 텍스트를 요청시에 삽입하여 사용량을 줄이는 처리를 추가합니다. >

요청 텍스트 마지막에 글 삽입해서 Gemini 답변 글을 줄이는걸 해야겠네요
question의 마지막에 넣으면 되는거겠죠

근데

  1. 모든 도메인을 기준으로 아래의 기준이 충족 되어야 합니다.
    • [ ] 모든 도메인은 CRUD 와 Search 가 구현 되어야 합니다.
    • [ ] 서치에는 검색조건 및 정렬기능이 추가되어 있어야 합니다.
      • [ ] 정렬기능은 기본적으로 생성일순, 수정일순을 기준으로 합니다.
      • [ ] 서치 기능에는 10건, 30건, 50건 기준으로 페이지에 노출 될 수 있습니다. 이외의 건수는 제한하여 기본 10건씩으로 고정합니다.
    • [ ] 모든 도메인의 컨트롤러 끝단은 접근 권한 및 로그인 체크가 진행되어야 합니다.
      • [ ] 접근 권한이 관리자, 가게주인, 구매자에 맞춰 동작해야 함을 주의 합니다.
      • 각 도메인 수정에서 수정할 수 있는 필드 또한 관리자, 가게주인, 구매자에 맞춰 동작해야 함을 주의 합니다. 예를들어 주문에서 주문 상태는 구매자가 수정 할 수 없어야 할것입니다.


이런 요구사항도 있는데 그럼 Ai도 검색기능을 만들어야겠네요
p_ai로 저장되는 question이랑 answer값들이 있으니

그럼 anwser 값들을 검색해야하는건가


관련글 더보기