Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,5 @@ out/
### VS Code ###
.vscode/

### api key ###
src/main/resources/application.properties
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
gemini 호출과 응답 코드

gitignore에 application.properties 포함되어 있음
application.properties에 api 키 작성되어 있음

3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,16 @@ dependencies {
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
implementation 'com.google.cloud:google-cloud-speech:4.40.0' // google stt
runtimeOnly 'com.mysql:mysql-connector-j'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
implementation 'com.google.genai:google-genai:1.0.0' // gemini api
implementation 'org.springframework.boot:spring-boot-starter-webflux' // gemini 검색기능
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.webservice.algorithmchef.config;

import com.google.genai.Client;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class GeminiConfig {

@Value("${gemini.api.key}")
private String apiKey;

@Bean
public Client geminiClient() {

return Client.builder()
.apiKey(apiKey)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package com.webservice.algorithmchef.controller;

import com.webservice.algorithmchef.dto.board.BoardCommentRequest;
import com.webservice.algorithmchef.dto.board.BoardPostResponse;
import com.webservice.algorithmchef.dto.board.BoardPostListResponse;
import com.webservice.algorithmchef.dto.board.BoardPostRequest;
import com.webservice.algorithmchef.dto.board.CommentReplyListResponse;
import com.webservice.algorithmchef.service.BoardService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

@Slf4j
@RestController
@RequestMapping("/board")
@RequiredArgsConstructor
public class BoardController {

private final BoardService boardService;

// 게시글 목록 조회(게시판)
@GetMapping("/posts")
public ResponseEntity<BoardPostListResponse> getPostList(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "createdAt,desc") String sort,
@RequestParam(required = false) String filter
) {
BoardPostListResponse response = boardService.getPostList(page, size, sort, filter);
return ResponseEntity.ok(response);
}

// 게시글 작성
@PostMapping("/post")
public ResponseEntity<Map<String, String>> createPost(
@RequestBody BoardPostRequest requestDto,
@AuthenticationPrincipal UserDetails userDetails
) {
if (userDetails == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}

boardService.createPost(requestDto, userDetails.getUsername());

return ResponseEntity.status(HttpStatus.CREATED)
.body(Map.of("message", "게시글이 작성되었습니다."));
}

// 게시글 조회
@GetMapping("/post/{postId}")
public ResponseEntity<BoardPostResponse> getPostDetail(
@PathVariable Long postId,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "createdAt,asc") String sort
) {
try {
BoardPostResponse response = boardService.getPostDetail(postId, page, size, sort);
return ResponseEntity.ok(response);
} catch (IllegalArgumentException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
}
}

// 댓글 작성
@PostMapping("/post/{postId}/comment")
public ResponseEntity<Map<String, String>> createComment(
@PathVariable Long postId,
@RequestBody BoardCommentRequest requestDto,
@AuthenticationPrincipal UserDetails userDetails
) {
if (userDetails == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}

try {
boardService.createComment(postId, requestDto, userDetails.getUsername());
return ResponseEntity.status(HttpStatus.CREATED)
.body(Map.of("message", "댓글 작성완료"));
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
}
}

// 대댓글 조회
@GetMapping("/comments/{commentId}/replies")
public ResponseEntity<CommentReplyListResponse> getReplies(
@PathVariable Long commentId,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "createdAt,asc") String sort
) {
try {
CommentReplyListResponse response = boardService.getReplies(commentId, page, size, sort);
return ResponseEntity.ok(response);
} catch (IllegalArgumentException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
}
}

// 게시글 수정
@PutMapping("/post/{postId}")
public ResponseEntity<Map<String, String>> updatePost(
@PathVariable Long postId,
@RequestBody BoardPostRequest requestDto,
@AuthenticationPrincipal UserDetails userDetails
) {
if (userDetails == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
// 401 Unauthorized
}

try {
// userDetails.getUsername()은 JWT에서 추출한 userId(String)
boardService.updatePost(postId, requestDto, userDetails.getUsername());

return ResponseEntity.ok(Map.of("message", "게시글이 수정되었습니다."));
} catch (IllegalArgumentException e) {
// 게시글이 없거나, 권한이 없는 경우
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
} catch (Exception e) {
log.error("게시글 수정 오류", e);
return ResponseEntity.internalServerError().body(Map.of("error", "서버 오류가 발생했습니다."));
}
}

// 게시글 삭제
@DeleteMapping("/post/{postId}")
public ResponseEntity<Map<String, String>> deletePost(
@PathVariable Long postId,
@AuthenticationPrincipal UserDetails userDetails
) {
if (userDetails == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}

try {
boardService.deletePost(postId, userDetails.getUsername());

return ResponseEntity.ok(Map.of("message", "게시글이 삭제되었습니다."));
} catch (IllegalArgumentException e) {
// 게시글이 없거나, 권한이 없는 경우
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
} catch (Exception e) {
log.error("게시글 삭제 오류", e);
return ResponseEntity.internalServerError().body(Map.of("error", "서버 오류가 발생했습니다."));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.webservice.algorithmchef.controller;

import com.webservice.algorithmchef.dto.gemini.GeminiRecipeRequest.*;
import com.webservice.algorithmchef.dto.gemini.GeminiRecipeResponse;
import com.webservice.algorithmchef.service.GeminiRecipeService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
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.List;

@Slf4j
@RestController
@RequestMapping("/recipes/recommend")
@RequiredArgsConstructor
public class GeminiRecipeController {

private final GeminiRecipeService recipeService;

// 1. 유저 성향 및 식습관 기반 추천 (요청주소 /recipes/recommend/prefer)
@PostMapping("/prefer")
public ResponseEntity<List<GeminiRecipeResponse>> recommendPrefer(@RequestBody PreferRequest request) {
log.info("성향 추천 요청 - UserPkId: {}", request.userPkId());
return ResponseEntity.ok(
recipeService.recommendPrefer(request.userPkId(), request.excludedTitles())
);
}

// 2. speech to text 조건 기반 추천 (요청주소 /recipes/recommend/condition)
@PostMapping("/condition")
public ResponseEntity<List<GeminiRecipeResponse>> recommendCondition(@RequestBody ConditionRequest request) {
log.info("조건 추천 요청 - UserPkId: {}, Condition: {}", request.userPkId(), request.condition());
return ResponseEntity.ok(
recipeService.recommendCondition(request.userPkId(), request.condition(), request.excludedTitles())
);
}

// 3. 임박재료 기반 추천 (요청주소 /recipes/recommend/expir)
@PostMapping("/expir")
public ResponseEntity<List<GeminiRecipeResponse>> recommendExpir(@RequestBody ExpirRequest request) {
log.info("재료 추천 요청 - UserPkId: {}", request.userPkId());
return ResponseEntity.ok(
recipeService.recommendExpir(request.userPkId(), request.ingredients(), request.excludedTitles())
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.webservice.algorithmchef.controller;

import com.webservice.algorithmchef.dto.gemini.GeminiRecipeResponse;
import com.webservice.algorithmchef.service.GeminiRecipeService;
import com.webservice.algorithmchef.service.SpeechService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.util.Collections;
import java.util.List;

@Slf4j
@RestController
@RequestMapping("/api")
@CrossOrigin(origins = "http://localhost:3000")
@RequiredArgsConstructor
public class SttController {

private final SpeechService speechService;
private final GeminiRecipeService geminiRecipeService; // 레시피 서비스 객체

@PostMapping("/stt")
public ResponseEntity<?> stt(
@RequestParam("audio") MultipartFile audioFile,
@RequestParam("userId") String userIdStr
) {
log.info("음성 기반 레시피 추천 요청 - UserId: {}, 파일크기: {} bytes", userIdStr, audioFile.getSize());

try {
// 1. 음성 -> 텍스트 변환 (STT)
String convertedText = speechService.stt(audioFile);

if (convertedText == null || convertedText.isEmpty()) {
log.warn("STT 변환 실패: 음성 인식 결과 없음");
return ResponseEntity.badRequest().body("음성을 인식하지 못했습니다. 다시 말씀해 주세요.");
}

log.info("STT 변환 결과: \"{}\"", convertedText);

// userId String -> Long 변환
Long userId = Long.parseLong(userIdStr);

// 음성 검색은 보통 '새로운 검색'이므로 excludedTitles는 빈 리스트로 전달
List<String> excludedTitles = Collections.emptyList();

// recipeservice 호출
List<GeminiRecipeResponse> recipes = geminiRecipeService.recommendCondition(
userId,
convertedText,
excludedTitles
);

log.info("음성 추천 완료 - {}개의 레시피 반환", recipes.size());

// 3. 레시피 리스트 반환
return ResponseEntity.ok(recipes);

} catch (NumberFormatException e) {
log.error("UserId 형식 오류: {}", userIdStr, e);
return ResponseEntity.badRequest().body("유효하지 않은 사용자 ID입니다.");
} catch (Exception e) {
log.error("음성 레시피 추천 중 오류 발생", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("처리 중 서버 오류가 발생했습니다: " + e.getMessage());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.webservice.algorithmchef.dto.board;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class BoardCommentRequest {

// 댓글이 달릴 게시글 ID
private Long postId;

// 부모 댓글 ID (대댓글일 경우 필수, 최상위 댓글이면 null)
private Long parentCommentId;

// 댓글 내용
private String content;
}
Loading