diff --git a/.gitignore b/.gitignore index b3db3e2..b63174a 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,5 @@ out/ .vscode/ application.yml +comma_firebase_key.json + diff --git a/build.gradle b/build.gradle index b0c003d..42b6855 100644 --- a/build.gradle +++ b/build.gradle @@ -26,6 +26,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' testImplementation 'org.springframework.boot:spring-boot-starter-test' + // lombok compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' @@ -41,7 +42,6 @@ dependencies { implementation group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.2' implementation group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.2' - //oauth2 implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' @@ -55,7 +55,19 @@ dependencies { //implementation 'org.springframework.boot:spring-boot-starter-data-redis' // aws - //implementation 'io.awspring.cloud:spring-cloud-starter-aws:2.4.1' + implementation 'io.awspring.cloud:spring-cloud-starter-aws:2.4.1' + + //jsoup + implementation 'org.jsoup:jsoup:1.15.3' + + //chatgpt + implementation 'io.github.flashvayne:chatgpt-spring-boot-starter:1.0.4' + + //firebase + implementation 'com.google.firebase:firebase-admin:9.2.0' + implementation 'com.squareup.okhttp3:okhttp:4.11.0' + + implementation 'com.google.cloud:google-cloud-vertex-ai:1.2.0' } tasks.named('test') { diff --git a/src/main/java/com/example/comma/domain/card/controller/CardController.java b/src/main/java/com/example/comma/domain/card/controller/CardController.java index dea7b2e..5434cf4 100644 --- a/src/main/java/com/example/comma/domain/card/controller/CardController.java +++ b/src/main/java/com/example/comma/domain/card/controller/CardController.java @@ -1,14 +1,18 @@ package com.example.comma.domain.card.controller; +import com.example.comma.domain.card.dto.request.CardInfoRequest; import com.example.comma.domain.card.dto.response.*; import com.example.comma.domain.card.service.CardService; +import com.example.comma.domain.external.service.GeminiService; +import com.example.comma.domain.external.service.ImageCrawlerService; import com.example.comma.global.common.SuccessResponse; import com.example.comma.global.config.auth.UserId; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import java.util.ArrayList; +import java.io.IOException; +import java.util.Collections; import java.util.List; @RequiredArgsConstructor @@ -16,19 +20,58 @@ @RestController public class CardController { private final CardService cardService; + private final GeminiService geminiService; + private final ImageCrawlerService imageCrawlerService; - @GetMapping("/{name}") - public ResponseEntity> getWord(@PathVariable(name = "name") String name) { - CardImageResponseDto CardImage = cardService.getCardImage(name); - return SuccessResponse.ok(CardImage); + + //단어 리스트 검색 + @GetMapping("/search-word") + public ResponseEntity> getSearchList(@RequestParam(name = "searchWord") String searchWord) throws IOException { + List searchResults = imageCrawlerService.crawlSearchList(searchWord); + List descriptionResponse = geminiService.generateDescriptionList(searchResults); + return SuccessResponse.ok(descriptionResponse); } + //단어 상세 정보 조회 + @GetMapping("/search-details") + public ResponseEntity> generateResponse(@RequestParam(name = "searchWord") String searchWord) throws IOException { + + //수형, 단어 이미지 생성 + List signImageUrls = imageCrawlerService.crawlImageUrls(searchWord); + byte[] mergeImages = imageCrawlerService.mergeImages(signImageUrls); + String signImageUrl = imageCrawlerService.uploadFile(mergeImages, searchWord + ".jpg"); + String generatedImageUrl = imageCrawlerService.generateImage(searchWord);; + + //cardId 생성 + cardService.registerCard(searchWord, signImageUrl); + Long cardId = cardService.getCardId(searchWord); + + //단어 사전 정의 생성 + List descriptionResponse = geminiService.generateDescriptionList(Collections.singletonList(searchWord)); + + //수형 동작 설명 + String generatesignLanguageDescription= geminiService.generateSignDescription(searchWord); + + WordDatailsResponseDto wordDatailsResponse = new WordDatailsResponseDto(cardId, searchWord, descriptionResponse.get(0).description(),descriptionResponse.get(0).partsOfSeech(), generatedImageUrl, signImageUrl,generatesignLanguageDescription ); + + return SuccessResponse.ok(wordDatailsResponse); + } + + + //UserCard 단어 카드 저장 @PostMapping("/{cardId}") - public ResponseEntity> createCard(@UserId Long userId, @PathVariable(name = "cardId") Long cardId) { - cardService.createCard(userId, cardId); + public ResponseEntity> saveCard(@UserId Long userId, @PathVariable(name = "cardId") Long cardId, @RequestBody CardInfoRequest cardInfoRequest) { + cardService.saveCard(userId, cardId, cardInfoRequest); return SuccessResponse.created(null); } + //UserCard 개별 정보 조회 + @GetMapping("/my-card/{userCardId}") + public ResponseEntity> getMyCard(@PathVariable(name = "userCardId") Long userCardId) { + MyCardResponseDto myCard = cardService.getMyCard(userCardId); + return SuccessResponse.ok(myCard); + } + @GetMapping("/lastest") public ResponseEntity> getLastestCard(@UserId Long userId) { List CardImage = cardService.getLatestCard(userId); diff --git a/src/main/java/com/example/comma/domain/card/dto/request/CardInfoRequest.java b/src/main/java/com/example/comma/domain/card/dto/request/CardInfoRequest.java new file mode 100644 index 0000000..50ce7b9 --- /dev/null +++ b/src/main/java/com/example/comma/domain/card/dto/request/CardInfoRequest.java @@ -0,0 +1,7 @@ +package com.example.comma.domain.card.dto.request; + +public record CardInfoRequest( + String cardImageUrl, + String signLanguageDescription +) { +} diff --git a/src/main/java/com/example/comma/domain/card/dto/response/DescriptionResponseDto.java b/src/main/java/com/example/comma/domain/card/dto/response/DescriptionResponseDto.java new file mode 100644 index 0000000..4772f25 --- /dev/null +++ b/src/main/java/com/example/comma/domain/card/dto/response/DescriptionResponseDto.java @@ -0,0 +1,8 @@ +package com.example.comma.domain.card.dto.response; + +public record DescriptionResponseDto( + String word, + String description, + String partsOfSeech +) { +} diff --git a/src/main/java/com/example/comma/domain/card/dto/response/MyCardResponseDto.java b/src/main/java/com/example/comma/domain/card/dto/response/MyCardResponseDto.java new file mode 100644 index 0000000..8225bea --- /dev/null +++ b/src/main/java/com/example/comma/domain/card/dto/response/MyCardResponseDto.java @@ -0,0 +1,9 @@ +package com.example.comma.domain.card.dto.response; + +public record MyCardResponseDto( + String name, + String cardImageUrl, + String signImageUrl, + String signLanguageDescription +) { +} diff --git a/src/main/java/com/example/comma/domain/card/dto/response/SearchCardResponseDto.java b/src/main/java/com/example/comma/domain/card/dto/response/SearchCardResponseDto.java new file mode 100644 index 0000000..328ae9c --- /dev/null +++ b/src/main/java/com/example/comma/domain/card/dto/response/SearchCardResponseDto.java @@ -0,0 +1,7 @@ +package com.example.comma.domain.card.dto.response; + +public record SearchCardResponseDto( + String generatedImageUrl, + String signImageUrl +) { +} diff --git a/src/main/java/com/example/comma/domain/card/dto/response/WordDatailsResponseDto.java b/src/main/java/com/example/comma/domain/card/dto/response/WordDatailsResponseDto.java new file mode 100644 index 0000000..892cdc6 --- /dev/null +++ b/src/main/java/com/example/comma/domain/card/dto/response/WordDatailsResponseDto.java @@ -0,0 +1,13 @@ +package com.example.comma.domain.card.dto.response; + +public record WordDatailsResponseDto( + Long cardId, + String word, + String description, + String partsOfSeech, + String cardImageUrl, + String signImageUrl, + String signLanguageDescription + +) { +} diff --git a/src/main/java/com/example/comma/domain/card/entity/Card.java b/src/main/java/com/example/comma/domain/card/entity/Card.java index 6ca6377..eafd27b 100644 --- a/src/main/java/com/example/comma/domain/card/entity/Card.java +++ b/src/main/java/com/example/comma/domain/card/entity/Card.java @@ -20,12 +20,14 @@ public class Card { private String name; - private String CardImageUrl; - - private String SignImageUrl; - + private String signImageUrl; @OneToMany(mappedBy = "card") private List userCardList; + public Card(String name, String signImageUrl) { + this.name = name; + this.signImageUrl = signImageUrl; + } + } diff --git a/src/main/java/com/example/comma/domain/card/entity/UserCard.java b/src/main/java/com/example/comma/domain/card/entity/UserCard.java index 0035d57..90e1046 100644 --- a/src/main/java/com/example/comma/domain/card/entity/UserCard.java +++ b/src/main/java/com/example/comma/domain/card/entity/UserCard.java @@ -29,11 +29,16 @@ public class UserCard extends BaseTimeEntity { private Boolean cardRegistration; - public UserCard(User user, Card card, Boolean quizParticipation, Boolean cardRegistration) { + private String cardImageUrl; + private String signLanguageDescription; + + public UserCard(User user, Card card, Boolean quizParticipation, Boolean cardRegistration, String cardImageUrl, String signLanguageDescription) { this.user = user; this.card = card; this.quizParticipation = quizParticipation; this.cardRegistration = cardRegistration; + this.cardImageUrl = cardImageUrl; + this.signLanguageDescription = signLanguageDescription; } public void setQuizParticipation(Boolean quizParticipation) { diff --git a/src/main/java/com/example/comma/domain/card/service/CardService.java b/src/main/java/com/example/comma/domain/card/service/CardService.java index f9cae11..3e43f37 100644 --- a/src/main/java/com/example/comma/domain/card/service/CardService.java +++ b/src/main/java/com/example/comma/domain/card/service/CardService.java @@ -1,13 +1,15 @@ package com.example.comma.domain.card.service; -import com.example.comma.domain.card.dto.response.CardImageResponseDto; +import com.example.comma.domain.card.dto.request.CardInfoRequest; import com.example.comma.domain.card.dto.response.CardResponseDto; import com.example.comma.domain.card.dto.response.CorrectCardResponseDto; +import com.example.comma.domain.card.dto.response.MyCardResponseDto; import com.example.comma.domain.card.dto.response.WrongCardResponseDto; import com.example.comma.domain.card.entity.Card; import com.example.comma.domain.card.entity.UserCard; import com.example.comma.domain.card.repository.CardRepository; import com.example.comma.domain.card.repository.UserCardRepository; +import com.example.comma.domain.external.service.ImageCrawlerService; import com.example.comma.domain.user.entity.User; import com.example.comma.domain.user.repository.UserRepository; import com.example.comma.global.error.ErrorCode; @@ -20,6 +22,7 @@ import org.springframework.transaction.annotation.Transactional; import java.util.List; +import java.util.Optional; import java.util.Random; import java.util.stream.Collectors; @@ -32,17 +35,25 @@ public class CardService { private final CardRepository cardRepository; private final UserRepository userRepository; private final UserCardRepository userCardRepository; + private final ImageCrawlerService imageCrawlerService; - public CardImageResponseDto getCardImage(String name) { - System.out.println("name = " + name); + public Long getCardId(String name) { Card card = cardRepository.findByName(name) .orElseThrow(() -> new EntityNotFoundException(ErrorCode.CARD_NOT_FOUND)); - return new CardImageResponseDto(card.getId(), card.getCardImageUrl(), card.getSignImageUrl()); + return card.getId(); } + public void registerCard(String name, String signImageUrl) { + Optional existingCardOptional = cardRepository.findByName(name); + if (existingCardOptional.isPresent()) { + return; + } + Card newCard = new Card(name, signImageUrl); + cardRepository.save(newCard); + } - public void createCard(Long userId, Long cardId) { + public void saveCard(Long userId, Long cardId, CardInfoRequest cardInfoRequest) { User user = userRepository.findById(userId) .orElseThrow(() -> new EntityNotFoundException(ErrorCode.USER_NOT_FOUND)); @@ -53,11 +64,12 @@ public void createCard(Long userId, Long cardId) { throw new ConflictException(ErrorCode.USER_CARD_ALREADY_EXISTS); } - UserCard userCard = new UserCard(user, card, false, true); + UserCard userCard = new UserCard(user, card, false, true, cardInfoRequest.cardImageUrl(), cardInfoRequest.signLanguageDescription()); userCardRepository.save(userCard); } + @Transactional @Scheduled(cron = "0 0 0 * * ?") public void resetCardRegistration() { @@ -82,7 +94,7 @@ private List convertToCardResponseDtos(List userCards return userCards.stream() .map(userCard -> { Card card = userCard.getCard(); - return new CardResponseDto(userCard.getId(), card.getName(), card.getCardImageUrl(), card.getSignImageUrl()); + return new CardResponseDto(userCard.getId(), card.getName(), userCard.getCardImageUrl(), card.getSignImageUrl()); }) .collect(Collectors.toList()); } @@ -102,13 +114,12 @@ public CardResponseDto getCardDetail(Long userCardId) { .orElseThrow(() -> new EntityNotFoundException(ErrorCode.USER_CARD_NOT_FOUND)); Card card = userCard.getCard(); - return new CardResponseDto(card.getId(), card.getName(), card.getCardImageUrl(), card.getSignImageUrl()); + return new CardResponseDto(card.getId(), card.getName(), userCard.getCardImageUrl(), card.getSignImageUrl()); } public WrongCardResponseDto getRandomQuizCard(Long userCardId) { UserCard userCard = userCardRepository.findById(userCardId) .orElseThrow(() -> new EntityNotFoundException(ErrorCode.USER_CARD_NOT_FOUND)); - List remainUserCards = userCardRepository.findUserCardByUserIdAndCardIdNot(userCard.getUser().getId(), userCardId); if (remainUserCards.size() <= 1) { @@ -125,7 +136,7 @@ public WrongCardResponseDto getRandomQuizCard(Long userCardId) { randomCard = randomUserCard.getCard(); - return new WrongCardResponseDto(randomCard.getName(), randomCard.getCardImageUrl(), randomCard.getSignImageUrl()); + return new WrongCardResponseDto(randomCard.getName(), randomUserCard.getCardImageUrl(), randomCard.getSignImageUrl()); } @@ -134,7 +145,7 @@ public CorrectCardResponseDto getQuizCard(Long userCardId) { .orElseThrow(() -> new EntityNotFoundException(ErrorCode.USER_CARD_NOT_FOUND)); Card card = userCard.getCard(); - return new CorrectCardResponseDto(card.getName(), card.getCardImageUrl(), card.getSignImageUrl()); + return new CorrectCardResponseDto(card.getName(), userCard.getCardImageUrl(), card.getSignImageUrl()); } @Transactional @@ -149,4 +160,12 @@ public List getTop5Cards(Long userId) { List userCards = userCardRepository.findTop5ByUserIdOrderByCreateDateDesc(userId); return convertToCardResponseDtos(userCards); } + + public MyCardResponseDto getMyCard(Long userCardId) { + UserCard userCard = userCardRepository.findById(userCardId) + .orElseThrow(() -> new EntityNotFoundException(ErrorCode.USER_CARD_NOT_FOUND)); + + Card card = userCard.getCard(); + return new MyCardResponseDto(card.getName(), userCard.getCardImageUrl(),card.getSignImageUrl(), userCard.getSignLanguageDescription()); + } } diff --git a/src/main/java/com/example/comma/domain/external/controller/FirebaseMessagingController.java b/src/main/java/com/example/comma/domain/external/controller/FirebaseMessagingController.java new file mode 100644 index 0000000..e69de29 diff --git a/src/main/java/com/example/comma/domain/external/dto/request/PushNotificationRequest.java b/src/main/java/com/example/comma/domain/external/dto/request/PushNotificationRequest.java new file mode 100644 index 0000000..e69de29 diff --git a/src/main/java/com/example/comma/domain/external/dto/response/PushNotificationResponse.java b/src/main/java/com/example/comma/domain/external/dto/response/PushNotificationResponse.java new file mode 100644 index 0000000..e69de29 diff --git a/src/main/java/com/example/comma/domain/external/service/FirebaseMessagingService.java b/src/main/java/com/example/comma/domain/external/service/FirebaseMessagingService.java new file mode 100644 index 0000000..e69de29 diff --git a/src/main/java/com/example/comma/domain/external/service/GeminiService.java b/src/main/java/com/example/comma/domain/external/service/GeminiService.java new file mode 100644 index 0000000..c1eb8d5 --- /dev/null +++ b/src/main/java/com/example/comma/domain/external/service/GeminiService.java @@ -0,0 +1,99 @@ +package com.example.comma.domain.external.service; + +import com.example.comma.domain.card.dto.response.DescriptionResponseDto; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.*; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.util.ArrayList; +import java.util.List; + +@RequiredArgsConstructor +@Service +public class GeminiService { + private final RestTemplate restTemplate = new RestTemplate(); + + @Value("${gemini.api.key}") + private String apiKey; + + //수형 설명 검색 + + public String generateSignDescription(String text) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + text = "한국 수화 단어 중 " + text + "에 대한 수화 동작 방법만 작성"; + String requestBody = "{\"contents\": [{\"parts\":[{\"text\":\"" + text + "\"}]}]}"; + + return sendGeminiResponse(requestBody); + } + + + //단어 설명 검색 + public List generateDescriptionList(List words) { + List responses = new ArrayList<>(); + + for (String text : words) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + //단어 설명 + String requestBody1 = "{\"contents\": [{\"parts\":[{\"text\":\"1." + text + "단어에 대한 사전 정의 => 20자 이내로 답변은 하나씩만 작성\"}]}]}"; + // 품사 + String requestBody2 = "{\"contents\": [{\"parts\":[{\"text\":\"2." + text + "단어에 대한 품사 => 5자 이내로 답변은 하나씩만 작성\"}]}]}"; + + String response1 = sendGeminiResponse(requestBody1); + String response2 = sendGeminiResponse(requestBody2); + + responses.add(new DescriptionResponseDto(text, response1, response2)); + } + + return responses; + } + + //Gemini API 호출 + private String sendGeminiResponse(String requestBody) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + UriComponentsBuilder builder = UriComponentsBuilder.fromUriString("https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent") + .queryParam("key", apiKey); + + HttpEntity requestEntity = new HttpEntity<>(requestBody, headers); + + ResponseEntity responseEntity = restTemplate.exchange( + builder.toUriString(), + HttpMethod.POST, + requestEntity, + String.class + ); + + if (responseEntity.getStatusCode() == HttpStatus.OK) { + String responseBody = responseEntity.getBody(); + return extractTextFromResponse(responseBody); + } else { + return "Failed to generate content. Status code: " + responseEntity.getStatusCodeValue(); + } + } + + private String extractTextFromResponse(String responseBody) { + int startIndex = responseBody.indexOf("\"text\":") + "\"text\":".length(); + int endIndex = responseBody.indexOf("\"role\""); + String text = responseBody.substring(startIndex, endIndex); + String cleanedText = text + .replaceAll("[^가-힣a-zA-Z0-9.\\s]", "") + .replaceAll("nn", "\n\n") + .replaceAll("n", " ") + .replaceAll("\\s+", " "); + + + return cleanedText; + } + + + + +} \ No newline at end of file diff --git a/src/main/java/com/example/comma/domain/external/service/ImageCrawlerService.java b/src/main/java/com/example/comma/domain/external/service/ImageCrawlerService.java new file mode 100644 index 0000000..513afc6 --- /dev/null +++ b/src/main/java/com/example/comma/domain/external/service/ImageCrawlerService.java @@ -0,0 +1,210 @@ +package com.example.comma.domain.external.service; + +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PutObjectRequest; +import com.example.comma.global.error.exception.EntityNotFoundException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.RestTemplate; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.URL; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.List; + +import static com.example.comma.global.error.ErrorCode.SIGNLANGUAGE_NOT_FOUND; + +@RequiredArgsConstructor +@Service +@Transactional +public class ImageCrawlerService { + + private final AmazonS3 amazonS3; + + @Value("${cloud.aws.s3.bucket}") + private String bucketName; + + @Value("${dalle.api.key}") + private String openaiApiKey; + + private final RestTemplate restTemplate = new RestTemplate(); + + // 이미지 크롤링 + public List crawlImageUrls(String searchWord) { + try { + // 검색 페이지 URL + String searchUrl = "https://sldict.korean.go.kr/front/search/searchAllList.do?searchKeyword=" + searchWord; + + // 검색 페이지에서 첫 번째 결과 페이지로 이동 + Document doc = Jsoup.connect(searchUrl).get(); + Elements spanElements = doc.select("span[class=tit]"); + Element spanElement = spanElements.first(); + assert spanElement != null; + Element aElement = spanElement.selectFirst("a"); + assert aElement != null; + + String aElementText = aElement.text().trim(); + if (!aElementText.contains(searchWord)) { + throw new EntityNotFoundException(SIGNLANGUAGE_NOT_FOUND); + } + + String href = aElement.attr("href"); + + // fnSearchContentsView 인자 추출 + String[] argsArray = href.split("'"); + String originNo = argsArray[1]; + String topCategory = argsArray[2]; + + // URL 생성 + String url = "https://sldict.korean.go.kr/front/sign/signContentsView.do" + + "?origin_no=" + originNo + + "&top_category=" + topCategory + + "&category=" + + "&searchKeyword=" + URLEncoder.encode(searchWord, "UTF-8") + + "&searchCondition=" + + "&search_gubun=" + + "&museum_type=00" + + "¤t_pos_index=0"; + + // 상세 페이지로 이동하여 이미지 URL 추출 + Document detailDoc = Jsoup.connect(url).get(); + Elements imgElements = detailDoc.select("img[alt=수어동작 이미지]"); + + List imageUrls = new ArrayList<>(); + + for (Element imgElement : imgElements) { + String imageUrl = imgElement.attr("src"); + if (imageUrl.startsWith("http://")) { + imageUrl = imageUrl.replace("http://", "https://"); + } + imageUrls.add(imageUrl); + } + + return imageUrls; + + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + + + //이미지 S3 업로드 + public String uploadFile(byte[] fileData, String fileName) throws IOException { + String directory = "mergedImg/"; + String contentType = "image/jpg"; + + ByteArrayInputStream inputStream = new ByteArrayInputStream(fileData); + + ObjectMetadata metadata = new ObjectMetadata(); + metadata.setContentType(contentType); + metadata.setContentLength(fileData.length); + + amazonS3.putObject(new PutObjectRequest(bucketName, directory + fileName, inputStream, metadata)); + + return "https://" + bucketName + ".s3.amazonaws.com/" + directory + fileName; + } + + + //이미지 병합 + public static byte[] mergeImages(List imageUrls) throws IOException { + BufferedImage[] images = new BufferedImage[imageUrls.size()]; + int totalWidth = 0; + int maxHeight = 0; + + for (int i = 0; i < imageUrls.size(); i++) { + URL imageUrl = new URL(imageUrls.get(i)); + BufferedImage image = ImageIO.read(imageUrl); + images[i] = image; + totalWidth += image.getWidth(); + maxHeight = Math.max(maxHeight, image.getHeight()); + } + + BufferedImage mergedImage = new BufferedImage(totalWidth, maxHeight, BufferedImage.TYPE_INT_RGB); + Graphics2D g2d = mergedImage.createGraphics(); + + int x = 0; + for (BufferedImage image : images) { + g2d.drawImage(image, x, 0, null); + x += image.getWidth(); + } + g2d.dispose(); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ImageIO.write(mergedImage, "jpg", baos); + baos.flush(); + byte[] bytes = baos.toByteArray(); + baos.close(); + + return bytes; + } + + + public List crawlSearchList(String searchWord) { + List searchResults = new ArrayList<>(); + try { + String searchUrl = "https://sldict.korean.go.kr/front/search/searchAllList.do?searchKeyword=" + searchWord; + Document doc = Jsoup.connect(searchUrl).get(); + Elements aElements = doc.select("span[class=tit] a"); + for (Element aElement : aElements) { + String text = aElement.text(); + searchResults.add(text); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + return searchResults; + } + + //달리 이미지 생성 + public String generateImage(String searchWord) throws IOException { + String apiUrl = "https://api.openai.com/v1/images/generations"; + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.set("Authorization", "Bearer " + openaiApiKey); + + String requestBody = "{\"model\": \"dall-e-3\", \"prompt\": \"" + "[" +searchWord + "]"+ + " [](대괄호) 안에 들어가는 단어에 대해 사실적인 이미지를 그대로 일러스트 화한 느낌으로, 부드러운 그림체로 그려줘" + + "\", \"n\": 1, \"size\": \"1024x1024\"}"; + + HttpEntity requestEntity = new HttpEntity<>(requestBody, headers); + String imageURL = restTemplate.postForObject(apiUrl, requestEntity, String.class); + + return extractImageUrl(imageURL); + } + + //json 파싱 + public String extractImageUrl(String json) throws IOException { + ObjectMapper mapper = new ObjectMapper(); + JsonNode rootNode = mapper.readTree(json); + + JsonNode dataNode = rootNode.get("data").get(0); + String imageUrl = dataNode.get("url").asText(); + + return imageUrl; + } +} + + + + + diff --git a/src/main/java/com/example/comma/domain/user/controller/UserController.java b/src/main/java/com/example/comma/domain/user/controller/UserController.java index 5c6fe06..abd1528 100644 --- a/src/main/java/com/example/comma/domain/user/controller/UserController.java +++ b/src/main/java/com/example/comma/domain/user/controller/UserController.java @@ -1,10 +1,11 @@ package com.example.comma.domain.user.controller; -import ch.qos.logback.core.net.HardenedObjectInputStream; import com.example.comma.domain.card.dto.response.CardResponseDto; import com.example.comma.domain.card.service.CardService; import com.example.comma.domain.fairytale.dto.Top2FairytaleResponseDto; import com.example.comma.domain.fairytale.service.FairytaleService; +import com.example.comma.domain.user.dto.request.EmotionRequest; +import com.example.comma.domain.user.dto.response.EmotionResponse; import com.example.comma.domain.user.dto.response.HomepageResponseDto; import com.example.comma.domain.user.service.UserService; import com.example.comma.global.common.SuccessResponse; @@ -53,6 +54,18 @@ public ResponseEntity> getHome(@UserId Long userId) { return SuccessResponse.ok(responseData); } + @PostMapping("/emotion") + public ResponseEntity> registerEmotion(@UserId Long userId, @RequestBody EmotionRequest emotionRequest){ + userService.registerEmotion(userId, emotionRequest); + return SuccessResponse.created(null); + } + + @GetMapping("/emotion") + public ResponseEntity> getEmotion(@UserId Long userId){ + EmotionResponse emotionResponse = userService.getEmotion(userId); + return SuccessResponse.ok(emotionResponse); + } + } diff --git a/src/main/java/com/example/comma/domain/user/dto/request/EmotionRequest.java b/src/main/java/com/example/comma/domain/user/dto/request/EmotionRequest.java new file mode 100644 index 0000000..d0aa602 --- /dev/null +++ b/src/main/java/com/example/comma/domain/user/dto/request/EmotionRequest.java @@ -0,0 +1,9 @@ +package com.example.comma.domain.user.dto.request; + +import com.example.comma.domain.user.entity.Emotion; + +public record EmotionRequest( + Emotion parentEmotion, + Emotion childEmotion +) { +} diff --git a/src/main/java/com/example/comma/domain/user/dto/response/EmotionResponse.java b/src/main/java/com/example/comma/domain/user/dto/response/EmotionResponse.java new file mode 100644 index 0000000..b2ce27a --- /dev/null +++ b/src/main/java/com/example/comma/domain/user/dto/response/EmotionResponse.java @@ -0,0 +1,12 @@ +package com.example.comma.domain.user.dto.response; + +import com.example.comma.domain.user.entity.Emotion; +import lombok.Builder; + +@Builder +public record EmotionResponse( + String parentEmotion, + String childEmotion +) { + +} diff --git a/src/main/java/com/example/comma/domain/user/entity/Emotion.java b/src/main/java/com/example/comma/domain/user/entity/Emotion.java new file mode 100644 index 0000000..b780679 --- /dev/null +++ b/src/main/java/com/example/comma/domain/user/entity/Emotion.java @@ -0,0 +1,23 @@ +package com.example.comma.domain.user.entity; + +public enum Emotion { + SOSO("그저 그래요"), + ANGRY("화나요"), + SAD("슬퍼요"), + PEACEFUL("평온해요"), + DESPRESS("우울해요"), + HAPPY("행복해요"), + FULLFIILED("뿌듯해요"), + ANXIOUS("불안해요"), + NONE("모르겠어요"); + + private final String koreanEmotion; + + Emotion(String koreanEmotion) { + this.koreanEmotion = koreanEmotion; + } + + public String getKoreanEmotion() { + return koreanEmotion; + } +} diff --git a/src/main/java/com/example/comma/domain/user/entity/User.java b/src/main/java/com/example/comma/domain/user/entity/User.java index 2a6fc61..22f3957 100644 --- a/src/main/java/com/example/comma/domain/user/entity/User.java +++ b/src/main/java/com/example/comma/domain/user/entity/User.java @@ -30,6 +30,14 @@ public class User extends BaseTimeEntity { private String profileImage; + @Enumerated(EnumType.STRING) + @Column(name = "parent_emotion") + private Emotion parentEmotion; + + @Enumerated(EnumType.STRING) + @Column(name = "child_emotion") + private Emotion childEmotion; + @Column(nullable = false) private String socialId; @@ -50,5 +58,13 @@ public User(String socialId, String name, String email, String profileImage) { public void setNickname(String nickname) { this.nickname = nickname; } + + public void setParentEmotion(Emotion parentEmotion) { + this.parentEmotion = parentEmotion; + } + + public void setChildEmotion(Emotion childEmotion) { + this.childEmotion = childEmotion; + } } diff --git a/src/main/java/com/example/comma/domain/user/service/UserService.java b/src/main/java/com/example/comma/domain/user/service/UserService.java index d1b8f0e..9f6b161 100644 --- a/src/main/java/com/example/comma/domain/user/service/UserService.java +++ b/src/main/java/com/example/comma/domain/user/service/UserService.java @@ -1,19 +1,21 @@ package com.example.comma.domain.user.service; -import com.example.comma.domain.card.dto.response.CardResponseDto; import com.example.comma.domain.card.entity.UserCard; import com.example.comma.domain.card.repository.UserCardRepository; -import com.example.comma.domain.card.service.CardService; import com.example.comma.domain.fairytale.entity.UserFairytale; import com.example.comma.domain.fairytale.repository.UserFairytaleRepository; +import com.example.comma.domain.user.dto.request.EmotionRequest; +import com.example.comma.domain.user.dto.response.EmotionResponse; import com.example.comma.domain.user.dto.response.HomepageResponseDto; import com.example.comma.domain.user.dto.response.UserTokenResponseDto; +import com.example.comma.domain.user.entity.Emotion; import com.example.comma.domain.user.entity.User; import com.example.comma.domain.user.repository.UserRepository; import com.example.comma.global.config.auth.jwt.JwtProvider; import com.example.comma.global.error.ErrorCode; import com.example.comma.global.error.exception.EntityNotFoundException; import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -103,7 +105,36 @@ private static String getRandomElement(String[] array) { return array[randomIndex]; } + @Scheduled(cron = "0 0 0 * * ?") + public void initEmotion() { + userRepository.findAll().forEach(user -> { + user.setParentEmotion(null); + user.setChildEmotion(null); + userRepository.save(user); + }); + } + + + public void registerEmotion(Long userId, EmotionRequest emotionRequest) { + System.out.println(emotionRequest); + User user = userRepository.findById(userId) + .orElseThrow(() -> new EntityNotFoundException(ErrorCode.USER_NOT_FOUND)); + user.setParentEmotion(emotionRequest.parentEmotion()); + user.setChildEmotion(emotionRequest.childEmotion()); + } + public EmotionResponse getEmotion(Long userId) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new EntityNotFoundException(ErrorCode.USER_NOT_FOUND)); + + Emotion parentEmotion = user.getParentEmotion(); + Emotion childEmotion = user.getChildEmotion(); + + return EmotionResponse.builder() + .parentEmotion(parentEmotion.getKoreanEmotion()) + .childEmotion(childEmotion.getKoreanEmotion()) + .build(); + } } diff --git a/src/main/java/com/example/comma/global/config/S3Config.java b/src/main/java/com/example/comma/global/config/S3Config.java new file mode 100644 index 0000000..d4c6d67 --- /dev/null +++ b/src/main/java/com/example/comma/global/config/S3Config.java @@ -0,0 +1,33 @@ +package com.example.comma.global.config; + +import com.amazonaws.auth.AWSCredentials; +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class S3Config { + @Value("${cloud.aws.credentials.access-key}") + private String accessKey; + + @Value("${cloud.aws.credentials.secret-key}") + private String secretKey; + + @Value("${cloud.aws.region.static}") + private String region; + + @Bean + public AmazonS3 amazonS3Client() { + AWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey); + + return AmazonS3ClientBuilder + .standard() + .withCredentials(new AWSStaticCredentialsProvider(credentials)) + .withRegion(region) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/comma/global/config/auth/SecurityConfig.java b/src/main/java/com/example/comma/global/config/auth/SecurityConfig.java index 4e0a891..7798b26 100644 --- a/src/main/java/com/example/comma/global/config/auth/SecurityConfig.java +++ b/src/main/java/com/example/comma/global/config/auth/SecurityConfig.java @@ -27,6 +27,8 @@ public class SecurityConfig { private static final String[] whiteList = {"/", "api/user/token/**", "login/oauth2/code/google/**", + "/gemini",//임시 + "/push-notification" //임시 }; @Bean diff --git a/src/main/java/com/example/comma/global/error/ErrorCode.java b/src/main/java/com/example/comma/global/error/ErrorCode.java index 0114201..82904b7 100644 --- a/src/main/java/com/example/comma/global/error/ErrorCode.java +++ b/src/main/java/com/example/comma/global/error/ErrorCode.java @@ -37,6 +37,7 @@ public enum ErrorCode { USER_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 유저를 찾을 수 없습니다."), USER_CARD_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 유저가 등록한 카드를 찾을 수 없습니다."), FAIRYTALE_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 동화를 찾을 수 없습니다."), + SIGNLANGUAGE_NOT_FOUND(HttpStatus.NOT_FOUND, "일치하는 수화 카드를 찾을 수 없습니다."), /** * 405 Method Not Allowed */