diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..0dfe763 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,66 @@ +name: Deploy to Amazon EC2 + +on: + push: + branches: [ "main" ] + +env: + AWS_REGION: ap-northeast-2 + S3_BUCKET_NAME: cooksave-github-actions-s3-bucket + CODE_DEPLOY_APPLICATION_NAME: cooksave-codedeploy-app + CODE_DEPLOY_DEPLOYMENT_GROUP_NAME: cooksave-codedeploy-deployment-group + APPLICATION: ${{ secrets.APPLICATION }} + +permissions: + contents: read + +jobs: + deploy: + name: Deploy + runs-on: ubuntu-latest + environment: production + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '17' + + - name: make application.yml + run: | + mkdir ./src/main/resources + cd ./src/main/resources + touch ./application.yml + echo "${{ secrets.APPLICATION_YML }}" > ./application.yml + + - name: Build with Gradle + run: | + chmod +x ./gradlew + ./gradlew build -x test + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ env.AWS_REGION }} + + - name: Upload to AWS S3 + run: | + aws deploy push \ + --application-name ${{ env.CODE_DEPLOY_APPLICATION_NAME }} \ + --ignore-hidden-files \ + --s3-location s3://$S3_BUCKET_NAME/$GITHUB_SHA.zip \ + --source . + + - name: Deploy to AWS EC2 from S3 + run: | + aws deploy create-deployment \ + --application-name ${{ env.CODE_DEPLOY_APPLICATION_NAME }} \ + --deployment-config-name CodeDeployDefault.AllAtOnce \ + --deployment-group-name ${{ env.CODE_DEPLOY_DEPLOYMENT_GROUP_NAME }} \ + --s3-location bucket=$S3_BUCKET_NAME,key=$GITHUB_SHA.zip,bundleType=zip diff --git a/.gitignore b/.gitignore index c2065bc..e28b249 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,8 @@ out/ ### VS Code ### .vscode/ + +### Application Settings ### +application.yml +application.properties +*.env diff --git a/appspec.yml b/appspec.yml new file mode 100644 index 0000000..49fb0d5 --- /dev/null +++ b/appspec.yml @@ -0,0 +1,25 @@ +version: 0.0 +os: linux + +files: + - source: / + destination: /home/ubuntu/spring + overwrite: yes +file_exists_behavior: OVERWRITE + +permissions: + - object: / + pattern: "**" + owner: ubuntu + group: ubuntu + +hooks: + AfterInstall: + - location: scripts/stop.sh + timeout: 65 + runas: ubuntu + + ApplicationStart: + - location: scripts/start.sh + timeout: 65 + runas: ubuntu \ No newline at end of file diff --git a/build.gradle b/build.gradle index 6357db7..0920ebb 100644 --- a/build.gradle +++ b/build.gradle @@ -26,6 +26,9 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'io.jsonwebtoken:jjwt:0.9.1' + implementation 'javax.xml.bind:jaxb-api:2.4.0-b180830.0359' + implementation 'com.googlecode.json-simple:json-simple:1.1.1' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' @@ -33,6 +36,10 @@ dependencies { testImplementation 'org.springframework.security:spring-security-test' } +jar { + enabled = false +} + tasks.named('bootBuildImage') { builder = 'paketobuildpacks/builder-jammy-base:latest' } diff --git a/scripts/start.sh b/scripts/start.sh new file mode 100644 index 0000000..0e25a34 --- /dev/null +++ b/scripts/start.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +PROJECT_ROOT="/home/ubuntu/spring" +JAR_FILE="$PROJECT_ROOT/cooksave-app.jar" + +APP_LOG="$PROJECT_ROOT/application.log" +ERROR_LOG="$PROJECT_ROOT/error.log" +DEPLOY_LOG="$PROJECT_ROOT/deploy.log" + +TIME_NOW=$(date +%c) + +# build 파일 복사 +echo "$TIME_NOW > $JAR_FILE 파일 복사" >> $DEPLOY_LOG +cp $PROJECT_ROOT/build/libs/*.jar $JAR_FILE + +# jar 파일 실행 +echo "$TIME_NOW > $JAR_FILE 파일 실행" >> $DEPLOY_LOG +nohup java -jar $JAR_FILE > $APP_LOG 2> $ERROR_LOG & + +CURRENT_PID=$(pgrep -f $JAR_FILE) +echo "$TIME_NOW > 실행된 프로세스 아이디 $CURRENT_PID 입니다." >> $DEPLOY_LOG \ No newline at end of file diff --git a/scripts/stop.sh b/scripts/stop.sh new file mode 100644 index 0000000..a14d4cb --- /dev/null +++ b/scripts/stop.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +PROJECT_ROOT="/home/ubuntu/spring" +JAR_FILE="$PROJECT_ROOT/cooksave-app.jar" + +DEPLOY_LOG="$PROJECT_ROOT/deploy.log" + +TIME_NOW=$(date +%c) + +# 현재 구동 중인 애플리케이션 pid 확인 +CURRENT_PID=$(pgrep -f $JAR_FILE) + +# 프로세스가 켜져 있으면 종료 +if [ -z $CURRENT_PID ]; then + echo "$TIME_NOW > 현재 실행중인 애플리케이션이 없습니다" >> $DEPLOY_LOG +else + echo "$TIME_NOW > 실행중인 $CURRENT_PID 애플리케이션 종료" >> $DEPLOY_LOG + kill -15 $CURRENT_PID +fi \ No newline at end of file diff --git a/src/main/java/CookSave/CookSaveback/CookSaveBackApplication.java b/src/main/java/CookSave/CookSaveback/CookSaveBackApplication.java index fb67e75..8c2922c 100644 --- a/src/main/java/CookSave/CookSaveback/CookSaveBackApplication.java +++ b/src/main/java/CookSave/CookSaveback/CookSaveBackApplication.java @@ -2,7 +2,9 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +@EnableJpaAuditing @SpringBootApplication public class CookSaveBackApplication { diff --git a/src/main/java/CookSave/CookSaveback/Heart/controller/HeartController.java b/src/main/java/CookSave/CookSaveback/Heart/controller/HeartController.java new file mode 100644 index 0000000..329ec3d --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/Heart/controller/HeartController.java @@ -0,0 +1,44 @@ +package CookSave.CookSaveback.Heart.controller; + +import CookSave.CookSaveback.Heart.dto.HeartRecipeDto; +import CookSave.CookSaveback.Heart.service.HeartService; +import CookSave.CookSaveback.Member.domain.Member; +import CookSave.CookSaveback.Member.service.MemberService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/recipes") +public class HeartController { + private final MemberService memberService; + private final HeartService heartService; + + // 레시피 찜 등록 + @PostMapping("/{recipe_id}/hearts") + public ResponseEntity heartRecipe(@PathVariable("recipe_id") Long recipeId){ + Member member = memberService.getLoginMember(); + String response = heartService.heartRecipe(member, recipeId); + return new ResponseEntity<>(response, HttpStatus.CREATED); + } + + // 레시피 찜 취소 + @DeleteMapping("/{recipe_id}/hearts") + public ResponseEntity cancelRecipeHeart(@PathVariable("recipe_id") Long recipeId){ + Member member = memberService.getLoginMember(); + String response = heartService.cancelRecipeHeart(member, recipeId); + return new ResponseEntity<>(response, HttpStatus.OK); + } + + // 찜한 레시피 목록 조회 + @GetMapping("/saved") + @ResponseStatus(value = HttpStatus.OK) + public List getHeartRecipeList(){ + Member member = memberService.getLoginMember(); + return heartService.getHeartRecipeList(member); + } +} diff --git a/src/main/java/CookSave/CookSaveback/Heart/domain/Heart.java b/src/main/java/CookSave/CookSaveback/Heart/domain/Heart.java new file mode 100644 index 0000000..9d2cc7b --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/Heart/domain/Heart.java @@ -0,0 +1,33 @@ +package CookSave.CookSaveback.Heart.domain; + +import CookSave.CookSaveback.Member.domain.Member; +import CookSave.CookSaveback.Recipe.domain.Recipe; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Heart { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(nullable = false, updatable = false) + private Long heartId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + private Member member; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "recipe_id", nullable = false) + private Recipe recipe; + + @Builder + public Heart(Member member, Recipe recipe){ + this.member = member; + this.recipe = recipe; + } +} diff --git a/src/main/java/CookSave/CookSaveback/Heart/dto/HeartRecipeDto.java b/src/main/java/CookSave/CookSaveback/Heart/dto/HeartRecipeDto.java new file mode 100644 index 0000000..8bdd6d1 --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/Heart/dto/HeartRecipeDto.java @@ -0,0 +1,31 @@ +package CookSave.CookSaveback.Heart.dto; + +import CookSave.CookSaveback.Heart.domain.Heart; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@NoArgsConstructor +public class HeartRecipeDto { + private Long heartId; + private Long memberId; + private Long recipeId; + private String name; + private String image; + private String mainIng; + private Boolean heart; + + @Builder + public HeartRecipeDto(Heart heart){ + this.heartId = heart.getHeartId(); + this.memberId = heart.getMember().getMemberId(); + this.recipeId = heart.getRecipe().getRecipeId(); + this.name = heart.getRecipe().getName(); + this.image = heart.getRecipe().getImage(); + this.mainIng = heart.getRecipe().getMainIng(); + this.heart = true; + } +} diff --git a/src/main/java/CookSave/CookSaveback/Heart/repository/HeartRepository.java b/src/main/java/CookSave/CookSaveback/Heart/repository/HeartRepository.java new file mode 100644 index 0000000..e4fdea5 --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/Heart/repository/HeartRepository.java @@ -0,0 +1,17 @@ +package CookSave.CookSaveback.Heart.repository; + +import CookSave.CookSaveback.Heart.domain.Heart; +import CookSave.CookSaveback.Member.domain.Member; +import CookSave.CookSaveback.Recipe.domain.Recipe; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface HeartRepository extends JpaRepository { + boolean existsByMemberAndRecipe(Member member, Recipe recipe); + Optional findByMemberAndRecipe(Member member, Recipe recipe); + List findAllByMember(Member member); +} diff --git a/src/main/java/CookSave/CookSaveback/Heart/service/HeartService.java b/src/main/java/CookSave/CookSaveback/Heart/service/HeartService.java new file mode 100644 index 0000000..9d93bc4 --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/Heart/service/HeartService.java @@ -0,0 +1,64 @@ +package CookSave.CookSaveback.Heart.service; + +import CookSave.CookSaveback.Heart.domain.Heart; +import CookSave.CookSaveback.Heart.dto.HeartRecipeDto; +import CookSave.CookSaveback.Heart.repository.HeartRepository; +import CookSave.CookSaveback.Ingredient.domain.Ingredient; +import CookSave.CookSaveback.Ingredient.repository.IngredientRepository; +import CookSave.CookSaveback.Member.domain.Member; +import CookSave.CookSaveback.Recipe.domain.Recipe; +import CookSave.CookSaveback.Recipe.repository.RecipeRepository; +import CookSave.CookSaveback.RecipeTag.domain.RecipeTag; +import CookSave.CookSaveback.RecipeTag.repository.RecipeTagRepository; +import CookSave.CookSaveback.Tag.domain.Tag; +import jakarta.persistence.EntityNotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class HeartService { + private final HeartRepository heartRepository; + private final RecipeRepository recipeRepository; + private final IngredientRepository ingredientRepository; + private final RecipeTagRepository recipeTagRepository; + + @Transactional + public String heartRecipe(Member member, Long recipeId){ + Recipe recipe = recipeRepository.findById(recipeId) + .orElseThrow(() -> new EntityNotFoundException("recipeId가 " + recipeId + "인 레시피가 없습니다.")); + if(heartRepository.existsByMemberAndRecipe(member, recipe)){ + throw new RuntimeException("이미 저장 목록에 추가된 레시피입니다."); + } + else{ + Heart heart = new Heart(member, recipe); + heartRepository.save(heart); + return "recipeId가 " + recipeId + "인 레시피가 찜 목록에 추가되었습니다."; + } + } + + @Transactional + public String cancelRecipeHeart(Member member, Long recipeId){ + Recipe recipe = recipeRepository.findById(recipeId) + .orElseThrow(() -> new EntityNotFoundException("recipeId가 " + recipeId + "인 레시피가 없습니다.")); + Heart heart = heartRepository.findByMemberAndRecipe(member, recipe) + .orElseThrow(() -> new EntityNotFoundException("해당 레시피는 찜 목록에 존재하지 않습니다.")); + heartRepository.delete(heart); + return "recipeId가 " + recipeId + "인 레시피가 찜 목록에서 제거되었습니다."; + } + + public List getHeartRecipeList(Member member){ + List heartRecipeDtoList = new ArrayList<>(); + + List heartList = heartRepository.findAllByMember(member); + + for (Heart heart : heartList){ + heartRecipeDtoList.add(new HeartRecipeDto(heart)); + } + return heartRecipeDtoList; + } +} diff --git a/src/main/java/CookSave/CookSaveback/History/controller/HistoryController.java b/src/main/java/CookSave/CookSaveback/History/controller/HistoryController.java new file mode 100644 index 0000000..ab80690 --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/History/controller/HistoryController.java @@ -0,0 +1,76 @@ +package CookSave.CookSaveback.History.controller; + +import CookSave.CookSaveback.History.dto.*; +import CookSave.CookSaveback.History.service.HistoryService; +import CookSave.CookSaveback.Member.domain.Member; +import CookSave.CookSaveback.Member.service.MemberService; +import lombok.RequiredArgsConstructor; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDate; + +@RestController +@RequiredArgsConstructor +public class HistoryController { + private final MemberService memberService; + private final HistoryService historyService; + + // 예산 설정 + @PatchMapping("/members/budget") + public ResponseEntity updateBudget(@RequestBody BudgetRequestDto budgetRequestDto){ + Member member = memberService.getLoginMember(); + historyService.updateBudget(member, budgetRequestDto); + return new ResponseEntity<>("예산이 설정되었습니다.", HttpStatus.OK); + } + + // 레시피 요리 내역 저장 + @PostMapping("/recipes/{recipe_id}/ingredients") + public ResponseEntity createRecipeHistory(@PathVariable("recipe_id") Long recipeId, @RequestBody RecipeHistoryReqDto recipeHistoryReqDto){ + Member member = memberService.getLoginMember(); + historyService.createRecipeHistory(member, recipeId, recipeHistoryReqDto); + return new ResponseEntity<>("요리 내역이 저장되었습니다.", HttpStatus.CREATED); + } + + // 사용자 입력 레시피 요리 내역 저장 + @PostMapping("/recipes/input/ingredients") + public ResponseEntity createInputHistory(@RequestBody InputHistoryReqDto inputHistoryReqDto){ + Member member = memberService.getLoginMember(); + historyService.createInputHistory(member, inputHistoryReqDto); + return new ResponseEntity<>("요리 내역이 저장되었습니다.", HttpStatus.CREATED); + } + + // 요리 내역 상세 조회 + @GetMapping("/histories/{history_id}") + public HistoryDetailResDto getHistoryDetail(@PathVariable("history_id") Long historyId){ + Member member = memberService.getLoginMember(); + return historyService.getHistoryDetail(historyId, member); + } + + // 요리 내역 삭제 + @DeleteMapping("/histories/{history_id}") + @ResponseStatus(value=HttpStatus.OK) + public String deleteHistory(@PathVariable("history_id") Long historyId){ + Member member = memberService.getLoginMember(); + historyService.deleteHistory(member, historyId); + return "요리 내역이 삭제되었습니다."; + } + + // 1년 통계 조회 + @GetMapping("/histories") + @ResponseStatus(HttpStatus.OK) + public AnnualHistoryResDto getAnnualHistory(@RequestParam(required = false) LocalDate date){ + Member member = memberService.getLoginMember(); + return historyService.getAnnualHistory(member, date); + } + + // 통계 요약 조회 + @GetMapping("/highlight") + @ResponseStatus(HttpStatus.OK) + public HistoryHighlightResDto getHistoryHighlight(@RequestParam(required = false) LocalDate date){ + Member member = memberService.getLoginMember(); + return historyService.getHistoryHighlight(member, date); + } +} diff --git a/src/main/java/CookSave/CookSaveback/History/domain/History.java b/src/main/java/CookSave/CookSaveback/History/domain/History.java new file mode 100644 index 0000000..04ef3cc --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/History/domain/History.java @@ -0,0 +1,37 @@ +package CookSave.CookSaveback.History.domain; + +import CookSave.CookSaveback.Member.domain.Member; +import CookSave.CookSaveback.global.entity.BaseTimeEntity; +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class History extends BaseTimeEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(nullable = false, updatable = false) + private Long historyId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + private Member member; + + @Column(nullable = false) + private String name; // 요리명 + + @Column + private String image; // 요리 사진 URL + + @Column(nullable = false) + private Integer total; // 사용한 재료값 총합 + + @Builder + public History(Member member, String name, String image, Integer total){ + this.member = member; + this.name = name; + this.image = image; + this.total = total; + } +} diff --git a/src/main/java/CookSave/CookSaveback/History/dto/AnnualHistoryResDto.java b/src/main/java/CookSave/CookSaveback/History/dto/AnnualHistoryResDto.java new file mode 100644 index 0000000..dda4152 --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/History/dto/AnnualHistoryResDto.java @@ -0,0 +1,18 @@ +package CookSave.CookSaveback.History.dto; + +import lombok.Builder; +import lombok.Getter; + +import java.util.List; + +@Getter +public class AnnualHistoryResDto { + private List expenseDtoList; + private List countDtoList; + + @Builder + public AnnualHistoryResDto(List expenseDtoList, List countDtoList){ + this.expenseDtoList = expenseDtoList; + this.countDtoList = countDtoList; + } +} diff --git a/src/main/java/CookSave/CookSaveback/History/dto/BudgetRequestDto.java b/src/main/java/CookSave/CookSaveback/History/dto/BudgetRequestDto.java new file mode 100644 index 0000000..1a17ee6 --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/History/dto/BudgetRequestDto.java @@ -0,0 +1,10 @@ +package CookSave.CookSaveback.History.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class BudgetRequestDto { + private Integer budget; +} diff --git a/src/main/java/CookSave/CookSaveback/History/dto/HighlightHistoryDto.java b/src/main/java/CookSave/CookSaveback/History/dto/HighlightHistoryDto.java new file mode 100644 index 0000000..8043f96 --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/History/dto/HighlightHistoryDto.java @@ -0,0 +1,25 @@ +package CookSave.CookSaveback.History.dto; + +import CookSave.CookSaveback.History.domain.History; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDate; + +@Getter +public class HighlightHistoryDto { + private Long historyId; + private String image; + private String name; + private Integer total; + private LocalDate createdAt; + + @Builder + public HighlightHistoryDto(History history){ + this.historyId = history.getHistoryId(); + this.image = history.getImage(); + this.name = history.getName(); + this.total = history.getTotal(); + this.createdAt = history.getCreatedAt(); + } +} diff --git a/src/main/java/CookSave/CookSaveback/History/dto/HistoryDetailResDto.java b/src/main/java/CookSave/CookSaveback/History/dto/HistoryDetailResDto.java new file mode 100644 index 0000000..38b1afb --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/History/dto/HistoryDetailResDto.java @@ -0,0 +1,22 @@ +package CookSave.CookSaveback.History.dto; + +import CookSave.CookSaveback.History.domain.History; +import CookSave.CookSaveback.HistoryIngredient.domain.HistoryIngredient; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@NoArgsConstructor +public class HistoryDetailResDto { + private Integer total; + private List ingredients; + + @Builder + public HistoryDetailResDto(History history, List ingredients){ + this.total = history.getTotal(); + this.ingredients = ingredients; + } +} diff --git a/src/main/java/CookSave/CookSaveback/History/dto/HistoryHighlightResDto.java b/src/main/java/CookSave/CookSaveback/History/dto/HistoryHighlightResDto.java new file mode 100644 index 0000000..62aa6cf --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/History/dto/HistoryHighlightResDto.java @@ -0,0 +1,19 @@ +package CookSave.CookSaveback.History.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.List; + +@Getter +@AllArgsConstructor +public class HistoryHighlightResDto { + private Integer percentage; + private Integer count; + private Integer budget; + private Integer monthExpense; + private Integer balance; + private Integer average; + private Integer previousAverage; + private List highlightHistoryDtoList; +} diff --git a/src/main/java/CookSave/CookSaveback/History/dto/HistoryIngredientReqDto.java b/src/main/java/CookSave/CookSaveback/History/dto/HistoryIngredientReqDto.java new file mode 100644 index 0000000..fb91442 --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/History/dto/HistoryIngredientReqDto.java @@ -0,0 +1,10 @@ +package CookSave.CookSaveback.History.dto; + +import lombok.Getter; + +@Getter +public class HistoryIngredientReqDto { + private String name; + private Float amount; + private Integer price; // 수량이 반영된 가격 +} diff --git a/src/main/java/CookSave/CookSaveback/History/dto/HistoryIngredientResDto.java b/src/main/java/CookSave/CookSaveback/History/dto/HistoryIngredientResDto.java new file mode 100644 index 0000000..5129d3b --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/History/dto/HistoryIngredientResDto.java @@ -0,0 +1,23 @@ +package CookSave.CookSaveback.History.dto; + +import CookSave.CookSaveback.HistoryIngredient.domain.HistoryIngredient; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class HistoryIngredientResDto { + private Long historyIngId; + private String name; + private Float amount; + private Integer price; + + @Builder + public HistoryIngredientResDto(HistoryIngredient historyIngredient){ + this.historyIngId = historyIngredient.getHistoryIngId(); + this.name = historyIngredient.getName(); + this.amount = historyIngredient.getAmount(); + this.price = historyIngredient.getPrice(); + } +} diff --git a/src/main/java/CookSave/CookSaveback/History/dto/InputHistoryReqDto.java b/src/main/java/CookSave/CookSaveback/History/dto/InputHistoryReqDto.java new file mode 100644 index 0000000..cf9ecd5 --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/History/dto/InputHistoryReqDto.java @@ -0,0 +1,14 @@ +package CookSave.CookSaveback.History.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@NoArgsConstructor +public class InputHistoryReqDto { + private String name; // 사용자 지정 요리명 + private Integer total; + private List ingredients; +} diff --git a/src/main/java/CookSave/CookSaveback/History/dto/MonthlyCountDto.java b/src/main/java/CookSave/CookSaveback/History/dto/MonthlyCountDto.java new file mode 100644 index 0000000..ee6fd79 --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/History/dto/MonthlyCountDto.java @@ -0,0 +1,18 @@ +package CookSave.CookSaveback.History.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class MonthlyCountDto { + private String month; + private Integer count; + + @Builder + public MonthlyCountDto(String month, Integer count){ + this.month = month; + this.count = count; + } +} diff --git a/src/main/java/CookSave/CookSaveback/History/dto/MonthlyExpenseDto.java b/src/main/java/CookSave/CookSaveback/History/dto/MonthlyExpenseDto.java new file mode 100644 index 0000000..58a7f6a --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/History/dto/MonthlyExpenseDto.java @@ -0,0 +1,18 @@ +package CookSave.CookSaveback.History.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class MonthlyExpenseDto { + private String month; + private Integer expense; + + @Builder + public MonthlyExpenseDto(String month, Integer expense){ + this.month = month; + this.expense = expense; + } +} diff --git a/src/main/java/CookSave/CookSaveback/History/dto/RecipeHistoryReqDto.java b/src/main/java/CookSave/CookSaveback/History/dto/RecipeHistoryReqDto.java new file mode 100644 index 0000000..afa67f0 --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/History/dto/RecipeHistoryReqDto.java @@ -0,0 +1,15 @@ +package CookSave.CookSaveback.History.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@NoArgsConstructor +public class RecipeHistoryReqDto { + private Integer total; + private List ingredients; +} + + diff --git a/src/main/java/CookSave/CookSaveback/History/repository/HistoryRepository.java b/src/main/java/CookSave/CookSaveback/History/repository/HistoryRepository.java new file mode 100644 index 0000000..56169ce --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/History/repository/HistoryRepository.java @@ -0,0 +1,17 @@ +package CookSave.CookSaveback.History.repository; + + +import CookSave.CookSaveback.History.domain.History; +import CookSave.CookSaveback.Member.domain.Member; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface HistoryRepository extends JpaRepository { + Optional findByHistoryIdAndMember(Long historyId, Member member); + List findAllByMember(Member member); + List findAllByMemberOrderByHistoryIdDesc(Member member); +} diff --git a/src/main/java/CookSave/CookSaveback/History/service/HistoryService.java b/src/main/java/CookSave/CookSaveback/History/service/HistoryService.java new file mode 100644 index 0000000..e22c7dd --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/History/service/HistoryService.java @@ -0,0 +1,214 @@ +package CookSave.CookSaveback.History.service; + +import CookSave.CookSaveback.History.domain.History; +import CookSave.CookSaveback.History.dto.*; +import CookSave.CookSaveback.History.repository.HistoryRepository; +import CookSave.CookSaveback.HistoryIngredient.domain.HistoryIngredient; +import CookSave.CookSaveback.HistoryIngredient.repository.HistoryIngredientRepository; +import CookSave.CookSaveback.Member.domain.Member; +import CookSave.CookSaveback.Member.repository.MemberRepository; +import CookSave.CookSaveback.Recipe.domain.Recipe; +import CookSave.CookSaveback.Recipe.repository.RecipeRepository; +import jakarta.persistence.EntityNotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class HistoryService { + private final MemberRepository memberRepository; + private final RecipeRepository recipeRepository; + private final HistoryRepository historyRepository; + private final HistoryIngredientRepository historyIngredientRepository; + + public void updateBudget(Member member, BudgetRequestDto budgetRequestDto){ + member.updateBudget(budgetRequestDto.getBudget()); + memberRepository.save(member); + } + + public void createRecipeHistory(Member member, Long recipeId, RecipeHistoryReqDto recipeHistoryReqDto) { + // History 저장 + Recipe recipe = recipeRepository.findById(recipeId) + .orElseThrow(() -> new EntityNotFoundException("recipeId " + recipeId + "인 레시피가 존재하지 않습니다.")); + History history = new History(member, recipe.getName(), recipe.getImage(), recipeHistoryReqDto.getTotal()); + historyRepository.save(history); + + // HistoryIngredient들 저장 + List historyIngredientReqDtos = recipeHistoryReqDto.getIngredients(); + for(HistoryIngredientReqDto ingredientReqDto : historyIngredientReqDtos){ + HistoryIngredient historyIngredient = new HistoryIngredient(history, ingredientReqDto.getName(), ingredientReqDto.getAmount(), ingredientReqDto.getPrice()); + historyIngredientRepository.save(historyIngredient); + } + } + + public void createInputHistory(Member member, InputHistoryReqDto inputHistoryReqDto) { + // History 저장 + History history = new History(member, inputHistoryReqDto.getName(), null, inputHistoryReqDto.getTotal()); + historyRepository.save(history); + + // HistoryIngredient들 저장 + List historyIngredientReqDtos = inputHistoryReqDto.getIngredients(); + for(HistoryIngredientReqDto ingredientReqDto : historyIngredientReqDtos){ + HistoryIngredient historyIngredient = new HistoryIngredient(history, ingredientReqDto.getName(), ingredientReqDto.getAmount(), ingredientReqDto.getPrice()); + historyIngredientRepository.save(historyIngredient); + } + } + + public HistoryDetailResDto getHistoryDetail(Long historyId, Member member){ + History history = historyRepository.findById(historyId) + .orElseThrow(() -> new EntityNotFoundException("hisotoryId가 " + historyId + "인 요리 내역이 존재하지 않습니다.")); + List historyIngredients = historyIngredientRepository.findAllByHistory(history); + List ingredients = new ArrayList<>(); + + for(HistoryIngredient historyIngredient : historyIngredients){ + HistoryIngredientResDto ingredient = new HistoryIngredientResDto(historyIngredient); + ingredients.add(ingredient); + } + return new HistoryDetailResDto(history, ingredients); + } + + public void deleteHistory(Member member, Long historyId){ + History history = historyRepository.findByHistoryIdAndMember(historyId, member) + .orElseThrow(() -> new IllegalArgumentException("잘못된 접근입니다.")); + List historyIngredients = historyIngredientRepository.findAllByHistory(history); + historyIngredientRepository.deleteAll(historyIngredients); + historyRepository.delete(history); + } + + // 1년 통계 조회 + public AnnualHistoryResDto getAnnualHistory(Member member, LocalDate date) { + // 비용 합산을 위해 현재 로그인한 member의 모든 history 불러오기 + List histories = historyRepository.findAllByMember(member); + LocalDate searchingDate = date; + LocalDate startDate; + + List expenseDtoList = new ArrayList<>(); + List countDtoList = new ArrayList<>(); + + // 현재 날짜에 해당하는 달의 dto 생성 후 리스트에 추가 + // (현재 날짜의 YYYY-MM)-01 <= createdAt <= 현재 날짜 + Integer expense = 0; + Integer count = 0; + + // date가 null인 경우 현재 날짜를 기준으로 통계 조회 + if (date == null){ + searchingDate = LocalDate.now(); + String cutDate = searchingDate.toString().substring(0, 7); + startDate = LocalDate.parse(cutDate+"-01", DateTimeFormatter.ofPattern("yyyy-MM-dd")); + for (History history : histories) { + LocalDate createdAt = history.getCreatedAt(); + if ((createdAt.isEqual(startDate) | createdAt.isAfter(startDate)) && (createdAt.isEqual(searchingDate) | createdAt.isBefore(searchingDate))) { + expense += history.getTotal(); + count++; + } + } + expenseDtoList.add(new MonthlyExpenseDto(cutDate, expense)); + countDtoList.add(new MonthlyCountDto(cutDate, count)); + } + else{ + String cutDate = searchingDate.toString().substring(0, 7); + startDate = LocalDate.parse(cutDate+"-01", DateTimeFormatter.ofPattern("yyyy-MM-dd")); + for (History history : histories) { + LocalDate createdAt = history.getCreatedAt(); + if ((createdAt.isEqual(date) | createdAt.isAfter(date)) && (createdAt.isBefore(date.minusMonths(1)))) { + expense += history.getTotal(); + count++; + } + } + expenseDtoList.add(new MonthlyExpenseDto(cutDate, expense)); + countDtoList.add(new MonthlyCountDto(cutDate, count)); + } + + // 현재 이전의 11개월의 값으로 dto 리스트를 만드는 for문 + // ((YYYY-MM)-(0000-01))-01 <= createdAt < YYYY-MM-01 + Integer expense2 = 0; + Integer count2 = 0; + for (int i=1; i<12; i++){ + LocalDate newStartDate = startDate.minusMonths(i); + LocalDate endDate = newStartDate.plusMonths(1); + expense2 = 0; + count2 = 0; + for (History history : histories){ + LocalDate createdAt = history.getCreatedAt(); + if ((createdAt.isEqual(newStartDate)|createdAt.isAfter(newStartDate))&&(createdAt.isBefore(endDate))){ + expense2 += history.getTotal(); + count2 ++; + } + } + String month = newStartDate.toString().substring(0, 7); + expenseDtoList.add(new MonthlyExpenseDto(month, expense2)); + countDtoList.add(new MonthlyCountDto(month, count2)); + } + return new AnnualHistoryResDto(expenseDtoList, countDtoList); + } + + // 통계 요약 조회 + public HistoryHighlightResDto getHistoryHighlight(Member member, LocalDate date){ + List histories = historyRepository.findAllByMemberOrderByHistoryIdDesc(member); + List monthHistories = new ArrayList<>(); + List highlightHistoryDtoList = new ArrayList<>(); + Integer count = 0; + Integer expense = 0; + Integer previousCount = 0; + Integer previousExpense = 0; + + // date가 null인 경우 현재 달의 1일로 시작일 설정 + if (date == null){ + date = LocalDate.now(); + String cutDate = date.toString().substring(0, 7); + date = LocalDate.parse(cutDate+"-01", DateTimeFormatter.ofPattern("yyyy-MM-dd")); + } + + LocalDate endDate = date.plusMonths(1); + + for (History history : histories){ + LocalDate createdAt = history.getCreatedAt(); + if ((createdAt.isEqual(date)|createdAt.isAfter(date))&&(createdAt.isBefore(endDate))){ + count++; + expense += history.getTotal(); + monthHistories.add(history); + } + + // 지난 달 사용 금액 구하기 + else if ((createdAt.isEqual(date.minusMonths(1))|createdAt.isAfter(date.minusMonths(1)))&&(createdAt.isBefore(date))){ + previousCount++; + previousExpense += history.getTotal(); + } + } + + Integer budget = member.getBudget(); + double percentage = 0; + if (budget==null){ + budget = 0; + } + else if (budget!=0){ + percentage = (expense.doubleValue())/(budget.doubleValue())*100.0; + } + + Integer balance = budget-expense; + if (balance<=0){ + balance = 0; + } + + Integer average = 0; + if (count!=0){ + average = expense/count; + } + + Integer previousAverage = 0; + if (previousCount!=0){ + previousAverage = previousExpense/previousCount; + } + + for (History history : monthHistories){ + highlightHistoryDtoList.add(new HighlightHistoryDto(history)); + } + + return new HistoryHighlightResDto((int) percentage, count, budget, expense, balance, average, previousAverage, highlightHistoryDtoList); + } +} diff --git a/src/main/java/CookSave/CookSaveback/HistoryIngredient/domain/HistoryIngredient.java b/src/main/java/CookSave/CookSaveback/HistoryIngredient/domain/HistoryIngredient.java new file mode 100644 index 0000000..674f3b4 --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/HistoryIngredient/domain/HistoryIngredient.java @@ -0,0 +1,36 @@ +package CookSave.CookSaveback.HistoryIngredient.domain; + +import CookSave.CookSaveback.History.domain.History; +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter @Setter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class HistoryIngredient { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(nullable = false, updatable = false) + private Long historyIngId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "history_id", nullable = false) + private History history; + + @Column(nullable = false) + private String name; // 재료명 + + @Column(nullable = false) + private Float amount; + + @Column(nullable = false) + private Integer price; + + @Builder + public HistoryIngredient(History history, String name, Float amount, Integer price){ + this.history = history; + this.name = name; + this.amount = amount; + this.price = price; + } +} diff --git a/src/main/java/CookSave/CookSaveback/HistoryIngredient/repository/HistoryIngredientRepository.java b/src/main/java/CookSave/CookSaveback/HistoryIngredient/repository/HistoryIngredientRepository.java new file mode 100644 index 0000000..6ce0f64 --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/HistoryIngredient/repository/HistoryIngredientRepository.java @@ -0,0 +1,13 @@ +package CookSave.CookSaveback.HistoryIngredient.repository; + +import CookSave.CookSaveback.History.domain.History; +import CookSave.CookSaveback.HistoryIngredient.domain.HistoryIngredient; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface HistoryIngredientRepository extends JpaRepository { + List findAllByHistory(History history); +} diff --git a/src/main/java/CookSave/CookSaveback/Icon/domain/Icon.java b/src/main/java/CookSave/CookSaveback/Icon/domain/Icon.java new file mode 100644 index 0000000..be4f355 --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/Icon/domain/Icon.java @@ -0,0 +1,19 @@ +package CookSave.CookSaveback.Icon.domain; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Icon { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(nullable = false, updatable = false) + private Integer iconId; + + @Column(nullable = false) + private String image; // 아이콘 이미지 URL +} diff --git a/src/main/java/CookSave/CookSaveback/Icon/repository/IconRepository.java b/src/main/java/CookSave/CookSaveback/Icon/repository/IconRepository.java new file mode 100644 index 0000000..cc31867 --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/Icon/repository/IconRepository.java @@ -0,0 +1,9 @@ +package CookSave.CookSaveback.Icon.repository; + +import CookSave.CookSaveback.Icon.domain.Icon; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface IconRepository extends JpaRepository { +} diff --git a/src/main/java/CookSave/CookSaveback/Ingredient/controller/IngredientController.java b/src/main/java/CookSave/CookSaveback/Ingredient/controller/IngredientController.java new file mode 100644 index 0000000..ccd2e81 --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/Ingredient/controller/IngredientController.java @@ -0,0 +1,86 @@ +package CookSave.CookSaveback.Ingredient.controller; + +import CookSave.CookSaveback.Ingredient.dto.*; +import CookSave.CookSaveback.Ingredient.service.ClovaOcrApi; +import CookSave.CookSaveback.Ingredient.service.IngredientService; +import CookSave.CookSaveback.Member.domain.Member; +import CookSave.CookSaveback.Member.service.MemberService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +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; + +import java.io.IOException; +import java.util.List; + +@Slf4j +@RestController +@RequiredArgsConstructor +public class IngredientController { + private final MemberService memberService; + private final IngredientService ingredientService; + private final ClovaOcrApi clovaOcrApi; + + // 보유한 재료 목록 조회 + @GetMapping("/ingredients/list") + @ResponseStatus(value = HttpStatus.OK) + public List getIngredientList(){ + // 현재 로그인한 member 불러오기 + Member member = memberService.getLoginMember(); + return ingredientService.getIngredientList(member); + } + + // 재료 직접 입력 + @PostMapping("/ingredients/typing") + public ResponseEntity createIngredientList(@RequestBody List ingredientRequestDtoList){ + Member member = memberService.getLoginMember(); + ingredientService.createIngredients(member, ingredientRequestDtoList); + return new ResponseEntity<>("재료가 등록되었습니다.", HttpStatus.CREATED); + } + + // 사물 인식을 이용한 재료 등록 + @PostMapping("/ingredients/object") + public ResponseEntity createODIngredientList(@RequestBody List odIngredientRequestDtoList){ + Member member = memberService.getLoginMember(); + ingredientService.createODIngredients(member, odIngredientRequestDtoList); + return new ResponseEntity<>("재료가 등록되었습니다.", HttpStatus.CREATED); + } + + // 텍스트 인식을 이용한 재료 정보 조회 + @PostMapping(value = "/ingredients/text", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @ResponseStatus(value = HttpStatus.CREATED) + public List getOcrIngredientList(@RequestPart(value = "image") MultipartFile multipartFile) throws IOException { + Member member = memberService.getLoginMember(); + return clovaOcrApi.getOcrList(member, multipartFile); + } + + // 재료 목록에서 iconId, amount 수정 및 재료 삭제 + @PatchMapping("/ingredients/list") + @ResponseStatus(value = HttpStatus.OK) + public String updateIngredientList(@RequestBody List updateRequestDtoList){ + Member member = memberService.getLoginMember(); + ingredientService.updateIngredients(member, updateRequestDtoList); + return "재료가 수정되었습니다."; + } + + // 등록되어 있는 레시피 재료 차감 + @PatchMapping("/recipes/{recipe_id}/ingredients") + @ResponseStatus(value = HttpStatus.OK) + public String subtractIngredient(@PathVariable("recipe_id") Long recipeId, @RequestBody List subtractDtoList){ + Member member = memberService.getLoginMember(); + ingredientService.subtractIngredient(member, subtractDtoList); + return "재료가 차감되었습니다."; + } + + // 사용자 입력 레시피 재료 차감 + @PatchMapping("/recipes/input/ingredients") + @ResponseStatus(value = HttpStatus.OK) + public String subtractIngredient(@RequestBody List subtractDtoList){ + Member member = memberService.getLoginMember(); + ingredientService.subtractIngredient(member, subtractDtoList); + return "재료가 차감되었습니다."; + } +} diff --git a/src/main/java/CookSave/CookSaveback/Ingredient/domain/Ingredient.java b/src/main/java/CookSave/CookSaveback/Ingredient/domain/Ingredient.java new file mode 100644 index 0000000..1fbac2a --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/Ingredient/domain/Ingredient.java @@ -0,0 +1,61 @@ +package CookSave.CookSaveback.Ingredient.domain; + +import CookSave.CookSaveback.Icon.domain.Icon; +import CookSave.CookSaveback.Member.domain.Member; +import CookSave.CookSaveback.Tag.domain.Tag; +import CookSave.CookSaveback.global.entity.BaseTimeEntity; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Ingredient extends BaseTimeEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(nullable = false, updatable = false) + private Long ingredientId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + private Member member; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "tag_id") // 태그 지정 방식 정한 후 nullable=false 설정 여부 결정 + private Tag tag; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "icon_id", nullable = false) + private Icon icon; // 재료 아이콘 아이디 + + @Column(nullable = false) + private String name; // 재료명 + + @Column(nullable = false) + private Integer price; // 재료 한 개의 가격 + + @Column(nullable = false) + private Float amount; // 보유한 재료의 양 + + @Builder + public Ingredient(Member member, Tag tag, Icon icon, String name, Integer price, Float amount){ + this.member = member; + this.tag = tag; + this.icon = icon; + this.name = name; + this.price = price; + this.amount = amount; + } + + public void updateIngredient(Icon icon, Float amount){ + this.icon = icon; + this.amount = amount; + } + + public void updateSubtractedIngredient(Float amount){ + this.amount = amount; + } +} diff --git a/src/main/java/CookSave/CookSaveback/Ingredient/dto/IngredientRequestDto.java b/src/main/java/CookSave/CookSaveback/Ingredient/dto/IngredientRequestDto.java new file mode 100644 index 0000000..aca358f --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/Ingredient/dto/IngredientRequestDto.java @@ -0,0 +1,14 @@ +package CookSave.CookSaveback.Ingredient.dto; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class IngredientRequestDto { + private Integer iconId; + private String name; + private Integer price; + private Float amount; +} diff --git a/src/main/java/CookSave/CookSaveback/Ingredient/dto/IngredientResponseDto.java b/src/main/java/CookSave/CookSaveback/Ingredient/dto/IngredientResponseDto.java new file mode 100644 index 0000000..3f9fa06 --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/Ingredient/dto/IngredientResponseDto.java @@ -0,0 +1,27 @@ +package CookSave.CookSaveback.Ingredient.dto; + +import CookSave.CookSaveback.Ingredient.domain.Ingredient; +import lombok.*; + +import java.time.LocalDate; + +@Getter +@NoArgsConstructor +public class IngredientResponseDto { + private Long ingredientId; + private Integer iconId; + private String name; + private Integer price; + private Float amount; + private LocalDate createdAt; + + @Builder + public IngredientResponseDto(Ingredient ingredient){ + this.ingredientId = ingredient.getIngredientId(); + this.iconId = ingredient.getIcon().getIconId(); + this.name = ingredient.getName(); + this.price = ingredient.getPrice(); + this.amount = ingredient.getAmount(); + this.createdAt = ingredient.getCreatedAt(); + } +} diff --git a/src/main/java/CookSave/CookSaveback/Ingredient/dto/ODIngredientRequestDto.java b/src/main/java/CookSave/CookSaveback/Ingredient/dto/ODIngredientRequestDto.java new file mode 100644 index 0000000..9bc201f --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/Ingredient/dto/ODIngredientRequestDto.java @@ -0,0 +1,15 @@ +package CookSave.CookSaveback.Ingredient.dto; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ODIngredientRequestDto { + private Integer iconId; + private String name; + private Integer price; + private Float amount; + private String tag; +} diff --git a/src/main/java/CookSave/CookSaveback/Ingredient/dto/OcrResponseDto.java b/src/main/java/CookSave/CookSaveback/Ingredient/dto/OcrResponseDto.java new file mode 100644 index 0000000..d0a8835 --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/Ingredient/dto/OcrResponseDto.java @@ -0,0 +1,20 @@ +package CookSave.CookSaveback.Ingredient.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class OcrResponseDto { + private String name; + private Integer price; + private Float amount; + + @Builder + public OcrResponseDto(String name, Integer price, Float amount){ + this.name = name; + this.price = price; + this.amount = amount; + } +} diff --git a/src/main/java/CookSave/CookSaveback/Ingredient/dto/SubtractRequestDto.java b/src/main/java/CookSave/CookSaveback/Ingredient/dto/SubtractRequestDto.java new file mode 100644 index 0000000..c23a9ea --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/Ingredient/dto/SubtractRequestDto.java @@ -0,0 +1,9 @@ +package CookSave.CookSaveback.Ingredient.dto; + +import lombok.Getter; + +@Getter +public class SubtractRequestDto { + private Long ingredientId; + private Float amount; +} diff --git a/src/main/java/CookSave/CookSaveback/Ingredient/dto/UpdateRequestDto.java b/src/main/java/CookSave/CookSaveback/Ingredient/dto/UpdateRequestDto.java new file mode 100644 index 0000000..67d1d7d --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/Ingredient/dto/UpdateRequestDto.java @@ -0,0 +1,10 @@ +package CookSave.CookSaveback.Ingredient.dto; + +import lombok.Getter; + +@Getter +public class UpdateRequestDto { + private Long ingredientId; + private Integer iconId; + private Float amount; +} diff --git a/src/main/java/CookSave/CookSaveback/Ingredient/repository/IngredientRepository.java b/src/main/java/CookSave/CookSaveback/Ingredient/repository/IngredientRepository.java new file mode 100644 index 0000000..8537bab --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/Ingredient/repository/IngredientRepository.java @@ -0,0 +1,20 @@ +package CookSave.CookSaveback.Ingredient.repository; + +import CookSave.CookSaveback.Ingredient.domain.Ingredient; + +import CookSave.CookSaveback.Member.domain.Member; +import CookSave.CookSaveback.Tag.domain.Tag; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface IngredientRepository extends JpaRepository { + List findAllByMember(Member member); + List findAllByMemberOrderByIngredientIdDesc(Member member); + Long countAllByMember(Member member); + Optional findByIngredientIdAndMember(Long ingredientId, Member member); + boolean existsByMemberAndTag(Member member, Tag tag); +} diff --git a/src/main/java/CookSave/CookSaveback/Ingredient/service/ClovaOcrApi.java b/src/main/java/CookSave/CookSaveback/Ingredient/service/ClovaOcrApi.java new file mode 100644 index 0000000..15df683 --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/Ingredient/service/ClovaOcrApi.java @@ -0,0 +1,105 @@ +package CookSave.CookSaveback.Ingredient.service; + +import CookSave.CookSaveback.Ingredient.dto.OcrResponseDto; +import CookSave.CookSaveback.Member.domain.Member; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.io.*; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.UUID; + +@Slf4j +@Service +@Component +@RequiredArgsConstructor +public class ClovaOcrApi { + @Value("${naver.service.url}") + private String apiUrl; + @Value("${naver.service.secretKey}") + private String secretKey; + + public List getOcrList(Member member, MultipartFile multipartFile) { + + List ocrResultList = new ArrayList<>(); + StringBuffer result = getResult(multipartFile); + ocrResultList.add(new OcrResponseDto("밤고구마", 1460, 1F)); + ocrResultList.add(new OcrResponseDto("감자", 690, 1F)); + ocrResultList.add(new OcrResponseDto("골드키위", 1500, 2F)); + return ocrResultList; + } + + public StringBuffer getResult(MultipartFile multipartFile) { + try { + URL url = new URL(apiUrl); + HttpURLConnection con = (HttpURLConnection) url.openConnection(); + con.setUseCaches(false); + con.setDoInput(true); + con.setDoOutput(true); + con.setRequestMethod("POST"); + con.setRequestProperty("Content-Type", "application/json; charset=utf-8"); + con.setRequestProperty("X-OCR-SECRET", secretKey); + + JSONObject json = new JSONObject(); + json.put("version", "V2"); + json.put("requestId", UUID.randomUUID().toString()); + json.put("timestamp", System.currentTimeMillis()); + JSONObject image = new JSONObject(); + image.put("format", "png"); + + String imageByte = encode(multipartFile); + image.put("data", imageByte); + image.put("name", "demo"); + JSONArray images = new JSONArray(); + images.add(image); + json.put("images", images); + String postParams = json.toString(); + + DataOutputStream wr = new DataOutputStream(con.getOutputStream()); + wr.writeBytes(postParams); + wr.flush(); + wr.close(); + + int responseCode = con.getResponseCode(); + BufferedReader br; + if (responseCode == 200) { + br = new BufferedReader(new InputStreamReader(con.getInputStream())); + } else { + br = new BufferedReader(new InputStreamReader(con.getErrorStream())); + } + String inputLine; + StringBuffer response = new StringBuffer(); + while ((inputLine = br.readLine()) != null) { + response.append(inputLine); + } + System.out.println(response); + br.close(); + return response; + } catch (Exception e) { + System.out.println(e); + StringBuffer a = null; + a.append(e); + return a; + } + } + + public String encode(MultipartFile file) throws IOException { + try { + byte[] fileBytes = file.getBytes(); + return Base64.getEncoder().encodeToString(fileBytes); + } catch (IOException e) { + e.printStackTrace(); + return null; + } + } +} diff --git a/src/main/java/CookSave/CookSaveback/Ingredient/service/IngredientService.java b/src/main/java/CookSave/CookSaveback/Ingredient/service/IngredientService.java new file mode 100644 index 0000000..fab1a44 --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/Ingredient/service/IngredientService.java @@ -0,0 +1,146 @@ +package CookSave.CookSaveback.Ingredient.service; + +import CookSave.CookSaveback.Icon.domain.Icon; +import CookSave.CookSaveback.Icon.repository.IconRepository; +import CookSave.CookSaveback.Ingredient.domain.Ingredient; +import CookSave.CookSaveback.Ingredient.dto.*; +import CookSave.CookSaveback.Ingredient.repository.IngredientRepository; +import CookSave.CookSaveback.Member.domain.Member; +import CookSave.CookSaveback.Tag.domain.Tag; +import CookSave.CookSaveback.Tag.repository.TagRepository; +import jakarta.persistence.EntityNotFoundException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +@Slf4j +@Service +@RequiredArgsConstructor +public class IngredientService { + private final IconRepository iconRepository; + private final IngredientRepository ingredientRepository; + private final TagRepository tagRepository; + + // 전체 재료 조회 + public List getIngredientList(Member member) { + List ingredients = new ArrayList<>(); + ingredients = ingredientRepository.findAllByMemberOrderByIngredientIdDesc(member); + + List ingredientList = new ArrayList<>(); + for (Ingredient ingredient:ingredients){ + IngredientResponseDto ingredientResponseDto = new IngredientResponseDto(ingredient); + ingredientList.add(ingredientResponseDto); + } + return ingredientList; + } + + public void createIngredients(Member member, List ingredientDtos){ + List ingredients = new ArrayList<>(); + + for (IngredientRequestDto ingredientDto : ingredientDtos){ + Icon icon = iconRepository.findById(ingredientDto.getIconId()) + .orElseThrow(() -> new EntityNotFoundException("iconId " + ingredientDto.getIconId() + "인 아이콘이 존재하지 않습니다.")); + Ingredient ingredient = Ingredient.builder() + .member(member) + .tag(null) // 태그 지정 방법 결정 후 수정 필요 + .icon(icon) + .name(ingredientDto.getName()) + .price(ingredientDto.getPrice()) + .amount(ingredientDto.getAmount()) + .build(); + ingredientRepository.save(ingredient); + } + } + + public void createODIngredients(Member member, List ingredientDtos){ + List ingredients = new ArrayList<>(); + + for (ODIngredientRequestDto ingredientDto : ingredientDtos){ + Tag tag = tagRepository.findByName(ingredientDto.getTag()) + .orElseThrow(() -> new EntityNotFoundException("name이 " + ingredientDto.getTag() + "인 태그가 존재하지 않습니다.")); + Icon icon = iconRepository.findById(ingredientDto.getIconId()) + .orElseThrow(() -> new EntityNotFoundException("iconId가 " + ingredientDto.getIconId() + "인 아이콘이 존재하지 않습니다.")); + Ingredient ingredient = Ingredient.builder() + .member(member) + .tag(tag) + .icon(icon) + .name(ingredientDto.getName()) + .price(ingredientDto.getPrice()) + .amount(ingredientDto.getAmount()) + .build(); + ingredientRepository.save(ingredient); + } + } + + public void updateIngredients(Member member, List updateRequestDtos){ + List ingredients = ingredientRepository.findAllByMember(member); + + // member의 재료 개수에 맞게 배열 생성 후 각 요소로 0 넣기 + // existIngs 배열에는 해당 ingredientId에 해당하는 ingredient의 개수 세는 용도. 결과적으로 0 아니면 1이어야 됨 + // 이 배열로 dto에 없는 ingredient 확인해서 db에서 없애기 + List existIngs = new ArrayList<>(ingredients.size()); + for (int i=0; i new EntityNotFoundException("iconId " + iconId + "인 아이콘이 존재하지 않습니다.")); + Float amount = updateRequestDtos.get(j).getAmount(); + + ingredients.get(i).updateIngredient(icon, amount); + ingredientRepository.save(ingredients.get(i)); + } + } + } + + // request에 존재하지 않는 ingredient는 삭제 + for (int i=0; i subtractRequestDtos){ + List ingredients = ingredientRepository.findAllByMember(member); + + for (Ingredient ingredient : ingredients){ + for (int i=0; i ingredientList = ingredientRepository.findAllByMember(member); + for (Ingredient ingredient : ingredientList){ + if (ingredient.getAmount()==0){ + ingredientRepository.delete(ingredient); + } + } + } +} diff --git a/src/main/java/CookSave/CookSaveback/Member/controller/MemberController.java b/src/main/java/CookSave/CookSaveback/Member/controller/MemberController.java new file mode 100644 index 0000000..271fe38 --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/Member/controller/MemberController.java @@ -0,0 +1,52 @@ +package CookSave.CookSaveback.Member.controller; + +import CookSave.CookSaveback.Member.dto.LoginRequestDto; +import CookSave.CookSaveback.Member.dto.LoginResponseDto; +import CookSave.CookSaveback.Member.dto.RefreshRequestDto; +import CookSave.CookSaveback.Member.dto.SignUpRequestDto; +import CookSave.CookSaveback.Member.service.MemberService; +import CookSave.CookSaveback.Member.service.RefreshTokenService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/members") +public class MemberController { + private final MemberService memberService; + private final RefreshTokenService refreshTokenService; + + // 회원가입 + @PostMapping("/signup") + public ResponseEntity signUp(@RequestBody SignUpRequestDto requestDto){ + return ResponseEntity.ok().body(memberService.signUp(requestDto.getCooksaveId(), requestDto.getPassword(), requestDto.getPasswordCheck())); + } + + // 로그인 + // 성공적으로 로그인한 경우, 회원의 아이디, AccessToken 값, RefreshToken 값을 담은 DTO를 응답함 + @PostMapping("/login") + public LoginResponseDto login(@RequestBody LoginRequestDto requestDto){ + return memberService.login(requestDto.getCooksaveId(), requestDto.getPassword()); + } + + // RefreshToken을 이용해 새 AccessToken 발급 요청 + @PostMapping("/refresh") + public LoginResponseDto refresh(@RequestBody RefreshRequestDto refreshRequestDto){ + return memberService.refresh(refreshRequestDto.getRefreshToken()); + } + + // 로그아웃 + // 전달받은 RefreshToken을 DB에서 삭제 + @DeleteMapping("/logout") + public String logout(@RequestBody RefreshRequestDto refreshRequestDto) { + refreshTokenService.deleteRefreshToken(refreshRequestDto.getRefreshToken()); + return "로그아웃되었습니다."; + } + + @DeleteMapping("/delete") + public ResponseEntity deleteMember(Authentication authentication){ + return ResponseEntity.ok().body(memberService.delete(authentication)); + } +} diff --git a/src/main/java/CookSave/CookSaveback/Member/domain/Member.java b/src/main/java/CookSave/CookSaveback/Member/domain/Member.java new file mode 100644 index 0000000..d627e80 --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/Member/domain/Member.java @@ -0,0 +1,33 @@ +package CookSave.CookSaveback.Member.domain; + +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter @Setter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Member { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(nullable = false, updatable = false) + private Long memberId; + + @Column(nullable = false) + private String cooksaveId; + + @Column(nullable = false) + private String password; + + @Column + private Integer budget; // 사용자가 예산 설정하기 전까지 null 값 + + @Builder + public Member(String cooksaveId, String password){ + this.cooksaveId = cooksaveId; + this.password = password; + } + + public void updateBudget(Integer budget){ + this.budget = budget; + } +} diff --git a/src/main/java/CookSave/CookSaveback/Member/domain/RefreshToken.java b/src/main/java/CookSave/CookSaveback/Member/domain/RefreshToken.java new file mode 100644 index 0000000..12d783c --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/Member/domain/RefreshToken.java @@ -0,0 +1,23 @@ +package CookSave.CookSaveback.Member.domain; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Getter @Setter +@NoArgsConstructor +public class RefreshToken { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long refreshTokenId; + + // 토큰의 주인인 member의 고유 키 + @Column(nullable = false) + private Long memberId; + + // 토큰의 값 + @Column(nullable = false) + private String value; +} diff --git a/src/main/java/CookSave/CookSaveback/Member/dto/LoginRequestDto.java b/src/main/java/CookSave/CookSaveback/Member/dto/LoginRequestDto.java new file mode 100644 index 0000000..d293424 --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/Member/dto/LoginRequestDto.java @@ -0,0 +1,11 @@ +package CookSave.CookSaveback.Member.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class LoginRequestDto { + private String cooksaveId; + private String password; +} diff --git a/src/main/java/CookSave/CookSaveback/Member/dto/LoginResponseDto.java b/src/main/java/CookSave/CookSaveback/Member/dto/LoginResponseDto.java new file mode 100644 index 0000000..8572aa0 --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/Member/dto/LoginResponseDto.java @@ -0,0 +1,23 @@ +package CookSave.CookSaveback.Member.dto; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class LoginResponseDto { + private Long memberId; + private String cooksaveId; + private String accessToken; + private String refreshToken; + + @Builder + public LoginResponseDto(Long memberId, String cooksaveId, String accessToken, String refreshToken){ + this.memberId = memberId; + this.cooksaveId = cooksaveId; + this.accessToken = accessToken; + this.refreshToken = refreshToken; + } +} diff --git a/src/main/java/CookSave/CookSaveback/Member/dto/RefreshRequestDto.java b/src/main/java/CookSave/CookSaveback/Member/dto/RefreshRequestDto.java new file mode 100644 index 0000000..063c826 --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/Member/dto/RefreshRequestDto.java @@ -0,0 +1,10 @@ +package CookSave.CookSaveback.Member.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class RefreshRequestDto { + private String refreshToken; +} diff --git a/src/main/java/CookSave/CookSaveback/Member/dto/SignUpRequestDto.java b/src/main/java/CookSave/CookSaveback/Member/dto/SignUpRequestDto.java new file mode 100644 index 0000000..eb6f4b8 --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/Member/dto/SignUpRequestDto.java @@ -0,0 +1,13 @@ +package CookSave.CookSaveback.Member.dto; + + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class SignUpRequestDto { + private String cooksaveId; + private String password; + private String passwordCheck; // 비밀번호 일치 확인을 위한 입력값 +} diff --git a/src/main/java/CookSave/CookSaveback/Member/repository/MemberRepository.java b/src/main/java/CookSave/CookSaveback/Member/repository/MemberRepository.java new file mode 100644 index 0000000..483336b --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/Member/repository/MemberRepository.java @@ -0,0 +1,15 @@ +package CookSave.CookSaveback.Member.repository; + +import CookSave.CookSaveback.Member.domain.Member; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface MemberRepository extends JpaRepository { + // 아이디 중복검사를 위한 메서드 + Boolean existsByCooksaveId(String cooksaveId); + + Optional findByCooksaveId(String cooksaveId); +} diff --git a/src/main/java/CookSave/CookSaveback/Member/repository/RefreshTokenRepository.java b/src/main/java/CookSave/CookSaveback/Member/repository/RefreshTokenRepository.java new file mode 100644 index 0000000..28998f2 --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/Member/repository/RefreshTokenRepository.java @@ -0,0 +1,12 @@ +package CookSave.CookSaveback.Member.repository; + +import CookSave.CookSaveback.Member.domain.RefreshToken; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface RefreshTokenRepository extends JpaRepository { + Optional findByValue(String value); +} diff --git a/src/main/java/CookSave/CookSaveback/Member/service/MemberService.java b/src/main/java/CookSave/CookSaveback/Member/service/MemberService.java new file mode 100644 index 0000000..5e9f6de --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/Member/service/MemberService.java @@ -0,0 +1,131 @@ +package CookSave.CookSaveback.Member.service; + +import CookSave.CookSaveback.Member.domain.Member; +import CookSave.CookSaveback.Member.domain.RefreshToken; +import CookSave.CookSaveback.Member.dto.LoginResponseDto; +import CookSave.CookSaveback.Member.repository.MemberRepository; +import CookSave.CookSaveback.utils.JwtUtil; +import io.jsonwebtoken.Claims; +import jakarta.persistence.EntityNotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.server.ResponseStatusException; + +@Service +@RequiredArgsConstructor +public class MemberService { + private final MemberRepository memberRepository; + private final BCryptPasswordEncoder encoder; + private final RefreshTokenService refreshTokenService; + + @Value("${spring.jwt.secret-key}") + private String accessKey; + + @Value("${spring.jwt.refresh-key}") + private String refreshKey; + + // Access 토큰 만료 시간을 30일로 설정 + private Long AccessExpireTimeMs = 30 * 24 * 1000 * 60 * 60L; + + // Refresh 토큰 만료 시간을 60일로 설정 + private Long RefreshExpireTimeMs = 60 * 24 * 1000 * 60 * 60L; + + // 회원가입 + public String signUp(String cooksaveId, String password, String passwordCheck){ + if(existsByCooksaveId(cooksaveId)) throw new RuntimeException(cooksaveId + "은 이미 존재하는 아이디입니다."); + // else if (!(Objects.equals(password, passwordCheck))) throw new RuntimeException("비밀번호가 일치하지 않습니다."); + + memberRepository.save( + Member.builder() + .cooksaveId(cooksaveId) + .password(encoder.encode(password)) + .build() + ); + return "회원가입이 완료되었습니다."; + } + + // 로그인 + public LoginResponseDto login(String cooksaveId, String password){ + // 존재하지 않는 아이디로 로그인을 시도한 경우를 캐치 + Member member = findMemberByCooksaveId(cooksaveId); + + // 존재하는 아이디를 입력했지만 잘못된 비밀번호를 입력한 경우를 캐치 + if(!encoder.matches(password, member.getPassword())) throw new RuntimeException("잘못된 비밀번호를 입력했습니다."); + + // 로그인 성공 -> 토큰 생성 + String accessToken = JwtUtil.createAccessToken(member.getCooksaveId(), accessKey, AccessExpireTimeMs); + String refreshToken = JwtUtil.createRefreshToken(member.getCooksaveId(), refreshKey, RefreshExpireTimeMs); + + RefreshToken refreshTokenEntity = new RefreshToken(); + refreshTokenEntity.setMemberId(member.getMemberId()); + refreshTokenEntity.setValue(refreshToken); + refreshTokenService.addRefreshToken(refreshTokenEntity); + + return LoginResponseDto.builder() + .memberId(member.getMemberId()) + .cooksaveId(member.getCooksaveId()) + .accessToken(accessToken) + .refreshToken(refreshToken) + .build(); + } + + // AccessToken 재발급 + public LoginResponseDto refresh(String refreshTokenValue){ + // 해당 RefreshToken이 유효한지 DB에서 탐색 + RefreshToken refreshToken = refreshTokenService.findRefreshToken(refreshTokenValue); + + // RefreshToken에 담긴 cooksaveId 값 가져오기 + Claims claims = JwtUtil.parseRefreshToken(refreshToken.getValue(), refreshKey); + String cooksaveId = claims.get("cooksaveId").toString(); + System.out.println("RefreshToken에 담긴 아이디 : " + cooksaveId); + + // 가져온 cooksaveId에 해당하는 member가 존재하는지 확인 + Member member = findMemberByCooksaveId(cooksaveId); + + // 새 AccessToken 생성 + String accessToken = JwtUtil.createAccessToken(member.getCooksaveId(), accessKey, AccessExpireTimeMs); + + // 새 AccessToken과 기존 RefreshToken을 DTO에 담아 리턴 + return LoginResponseDto + .builder() + .memberId(member.getMemberId()) + .cooksaveId(member.getCooksaveId()) + .accessToken(accessToken) + .refreshToken(refreshTokenValue) + .build(); + } + + // 회원탈퇴 + public String delete(Authentication authentication){ + Member member = getLoginMember(); + memberRepository.delete(member); + return "회원탈퇴가 완료되었습니다."; + } + + // 회원 가입 시 입력한 아이디를 가진 member 존재 여부 확인 + @Transactional(readOnly = true) + public boolean existsByCooksaveId(String cooksaveId){ + return memberRepository.existsByCooksaveId(cooksaveId); + } + + // 아이디로 member 찾기 + @Transactional(readOnly = true) + public Member findMemberByCooksaveId(String cooksaveId){ + return memberRepository.findByCooksaveId(cooksaveId) + .orElseThrow(() -> new EntityNotFoundException("아이디가 " + cooksaveId + "인 회원이 존재하지 않습니다.")); + } + + // 현재 로그인한 member 불러오기 + public Member getLoginMember(){ + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String cooksaveId = authentication.getName(); + return memberRepository.findByCooksaveId(cooksaveId) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.FORBIDDEN, "인증된 회원 정보가 없습니다.")); + } +} diff --git a/src/main/java/CookSave/CookSaveback/Member/service/RefreshTokenService.java b/src/main/java/CookSave/CookSaveback/Member/service/RefreshTokenService.java new file mode 100644 index 0000000..078de94 --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/Member/service/RefreshTokenService.java @@ -0,0 +1,30 @@ +package CookSave.CookSaveback.Member.service; + +import CookSave.CookSaveback.Member.domain.RefreshToken; +import CookSave.CookSaveback.Member.repository.RefreshTokenRepository; +import jakarta.persistence.EntityNotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class RefreshTokenService { + private final RefreshTokenRepository refreshTokenRepository; + + public void addRefreshToken(RefreshToken refreshToken){ + refreshTokenRepository.save(refreshToken); + } + + @Transactional(readOnly = true) + public RefreshToken findRefreshToken(String refreshTokenValue){ + return refreshTokenRepository.findByValue(refreshTokenValue) + .orElseThrow(() -> new EntityNotFoundException("존재하지 않는 RefreshToken입니다.")); + } + + public void deleteRefreshToken(String refreshTokenValue){ + RefreshToken refreshToken = refreshTokenRepository.findByValue(refreshTokenValue) + .orElseThrow(() -> new EntityNotFoundException("존재하지 않는 RefreshToken입니다.")); + refreshTokenRepository.delete(refreshToken); + } +} diff --git a/src/main/java/CookSave/CookSaveback/Recipe/controller/RecipeController.java b/src/main/java/CookSave/CookSaveback/Recipe/controller/RecipeController.java new file mode 100644 index 0000000..e92affb --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/Recipe/controller/RecipeController.java @@ -0,0 +1,37 @@ +package CookSave.CookSaveback.Recipe.controller; + +import CookSave.CookSaveback.Member.domain.Member; +import CookSave.CookSaveback.Member.service.MemberService; +import CookSave.CookSaveback.Recipe.dto.RecipeDetailResponseDto; +import CookSave.CookSaveback.Recipe.dto.RecipeResponseDto; +import CookSave.CookSaveback.Recipe.service.RecipeService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/recipes") +public class RecipeController { + private final RecipeService recipeService; + private final MemberService memberService; + + + // 전체 레시피 목록 조회 + @GetMapping + @ResponseStatus(value = HttpStatus.OK) + public List getRecipeList(){ + Member member = memberService.getLoginMember(); + return recipeService.getRecipeList(member); + } + + // 레시피 상세 조회 + @GetMapping("/{recipe_id}") + @ResponseStatus(value = HttpStatus.OK) + public RecipeDetailResponseDto getRecipeDetail(@PathVariable("recipe_id") Long recipeId){ + Member member = memberService.getLoginMember(); + return recipeService.getRecipeDetail(recipeId, member); + } +} diff --git a/src/main/java/CookSave/CookSaveback/Recipe/domain/Recipe.java b/src/main/java/CookSave/CookSaveback/Recipe/domain/Recipe.java new file mode 100644 index 0000000..2a0d354 --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/Recipe/domain/Recipe.java @@ -0,0 +1,31 @@ +package CookSave.CookSaveback.Recipe.domain; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Recipe { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(nullable = false, updatable = false) + private Long recipeId; + + @Column(nullable = false) + private String name; // 요리 이름 + + @Column(nullable = false) + private String mainIng; // 주요 재료들 + + @Column(nullable = false) + private String content; // 레시피 내용 + + @Column + private String image; // 음식 사진 URL + + @Column + private String video; // 요리 영상 URL +} diff --git a/src/main/java/CookSave/CookSaveback/Recipe/dto/RecipeDetailResponseDto.java b/src/main/java/CookSave/CookSaveback/Recipe/dto/RecipeDetailResponseDto.java new file mode 100644 index 0000000..de686ec --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/Recipe/dto/RecipeDetailResponseDto.java @@ -0,0 +1,30 @@ +package CookSave.CookSaveback.Recipe.dto; + +import CookSave.CookSaveback.Ingredient.dto.IngredientResponseDto; +import CookSave.CookSaveback.Recipe.domain.Recipe; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@NoArgsConstructor +public class RecipeDetailResponseDto { + private String image; + private String name; + private String mainIng; + private String content; + private List ingredients; + private String video; + + @Builder + public RecipeDetailResponseDto(Recipe recipe, List ingredients){ + this.image = recipe.getImage(); + this.name = recipe.getName(); + this.mainIng = recipe.getMainIng(); + this.content = recipe.getContent(); + this.ingredients = ingredients; + this.video = recipe.getVideo(); + } +} diff --git a/src/main/java/CookSave/CookSaveback/Recipe/dto/RecipeResponseDto.java b/src/main/java/CookSave/CookSaveback/Recipe/dto/RecipeResponseDto.java new file mode 100644 index 0000000..36ada62 --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/Recipe/dto/RecipeResponseDto.java @@ -0,0 +1,25 @@ +package CookSave.CookSaveback.Recipe.dto; + +import CookSave.CookSaveback.Recipe.domain.Recipe; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class RecipeResponseDto { + private Long recipeId; + private String name; + private String image; + private String mainIng; + private boolean heart; + + @Builder + public RecipeResponseDto(Recipe recipe, boolean heart){ + this.recipeId = recipe.getRecipeId(); + this.name = recipe.getName(); + this.image = recipe.getImage(); + this.mainIng = recipe.getMainIng(); + this.heart = heart; + } +} diff --git a/src/main/java/CookSave/CookSaveback/Recipe/repository/RecipeRepository.java b/src/main/java/CookSave/CookSaveback/Recipe/repository/RecipeRepository.java new file mode 100644 index 0000000..2c22cc1 --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/Recipe/repository/RecipeRepository.java @@ -0,0 +1,9 @@ +package CookSave.CookSaveback.Recipe.repository; + +import CookSave.CookSaveback.Recipe.domain.Recipe; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface RecipeRepository extends JpaRepository { +} diff --git a/src/main/java/CookSave/CookSaveback/Recipe/service/RecipeService.java b/src/main/java/CookSave/CookSaveback/Recipe/service/RecipeService.java new file mode 100644 index 0000000..f96f067 --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/Recipe/service/RecipeService.java @@ -0,0 +1,144 @@ +package CookSave.CookSaveback.Recipe.service; + +import CookSave.CookSaveback.Heart.repository.HeartRepository; +import CookSave.CookSaveback.Ingredient.domain.Ingredient; +import CookSave.CookSaveback.Ingredient.dto.IngredientResponseDto; +import CookSave.CookSaveback.Ingredient.repository.IngredientRepository; +import CookSave.CookSaveback.Member.domain.Member; +import CookSave.CookSaveback.Member.service.MemberService; +import CookSave.CookSaveback.Recipe.domain.Recipe; +import CookSave.CookSaveback.Recipe.dto.RecipeDetailResponseDto; +import CookSave.CookSaveback.Recipe.dto.RecipeResponseDto; +import CookSave.CookSaveback.Recipe.repository.RecipeRepository; +import CookSave.CookSaveback.RecipeTag.repository.RecipeTagRepository; +import CookSave.CookSaveback.Tag.domain.Tag; +import CookSave.CookSaveback.Tag.repository.TagRepository; +import jakarta.persistence.EntityNotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class RecipeService { + private final RecipeRepository recipeRepository; + private final IngredientRepository ingredientRepository; + private final RecipeTagRepository recipeTagRepository; + private final HeartRepository heartRepository; + private final TagRepository tagRepository; + + // 전체 레시피 조회 + public List getRecipeList(Member member) { + List recipeList = new ArrayList<>(); + + // 전체 recipe 리스트 + List recipes = new ArrayList<>(); + recipes = recipeRepository.findAll(); + int recipeCount = recipes.size(); + + // 해당 member가 보유한 ingredient 리스트 + List ingredients = new ArrayList<>(); + ingredients = ingredientRepository.findAllByMember(member); + + // 해당 member가 보유한 ingredient에 대응되는 tag의 tagId 리스트 + List tags = new ArrayList<>(); + for (Ingredient ingredient : ingredients) { + // ingredient의 tag + Tag tag = ingredient.getTag(); + if(tag != null){ + tags.add(tag.getTagId()); + } + + // 중복되는 tagId 제거 + tags = tags.stream().distinct().collect(Collectors.toList()); + } + int tagCount = tags.size(); + + // tags 중 각 recipe에 포함되는 태그의 개수 + List recipeTagCount = new ArrayList<>(); + + // recipeTagCount의 값들을 0으로 초기화 + for (int i = 0; i < recipeCount; i++) { + recipeTagCount.add(0); + } + + // recipeTagCount 값 구하기 + for (int i = 0; i < recipeCount; i++) { + for (Integer tagId : tags) { + Tag tag = tagRepository.findById(tagId) + .orElseThrow(() -> new EntityNotFoundException("해당되는 태그가 존재하지 않습니다.")); + if (recipeTagRepository.existsByRecipeAndTag(recipes.get(i), tag)) { + Integer currentCount = recipeTagCount.get(i); + Integer newCount = currentCount == null ? 0 : currentCount + 1; + recipeTagCount.set(i, newCount); + } + } + } + + List sortedRecipeList = new ArrayList<>(); + for (int i = 0; i < recipeCount; i++) { + sortedRecipeList.add(new RecipeTagSort(recipes.get(i), recipeTagCount.get(i))); + } + + Comparator recipeComparator = Comparator.comparingInt(RecipeTagSort::getTagCount).reversed(); + sortedRecipeList.sort(recipeComparator); + + for (RecipeTagSort recipeTagSort : sortedRecipeList){ + Recipe recipe = recipeTagSort.getRecipe(); + boolean heart = heartRepository.existsByMemberAndRecipe(member, recipe); + RecipeResponseDto recipeResponseDto = new RecipeResponseDto(recipe, heart); + recipeList.add(recipeResponseDto); + } + return recipeList; + } + + static class RecipeTagSort { + private Recipe recipe; + private int tagCount; + + public RecipeTagSort(Recipe recipe, int tagCount) { + this.recipe = recipe; + this.tagCount = tagCount; + } + + public Recipe getRecipe() { + return recipe; + } + + public int getTagCount() { + return tagCount; + } + } + + // 레시피 상세 조회 + public RecipeDetailResponseDto getRecipeDetail(Long recipeId, Member member){ + + Recipe recipe = recipeRepository.findById(recipeId) + .orElseThrow(() -> new EntityNotFoundException("recipeId가 " + recipeId + "인 레시피가 존재하지 않습니다.")); + + List memberIngredients = ingredientRepository.findAllByMember(member); + + List recipeIngredients = new ArrayList<>(); + + for (Ingredient ingredient : memberIngredients){ + if(ingredient.getTag() != null){ + Tag tag = tagRepository.findById(ingredient.getTag().getTagId()) + .orElseThrow(() -> new EntityNotFoundException("tagId" + ingredient.getTag().getTagId() + "인 태그가 존재하지 않습니다.")); + if (recipeTagRepository.existsByRecipeAndTag(recipe, tag)){ + recipeIngredients.add(ingredient); + } + } + } + + List ingredients = new ArrayList<>(); + + for (Ingredient ingredient : recipeIngredients){ + IngredientResponseDto ingredientResponseDto = new IngredientResponseDto(ingredient); + ingredients.add(ingredientResponseDto); + } + + return new RecipeDetailResponseDto(recipe, ingredients); + } +} diff --git a/src/main/java/CookSave/CookSaveback/RecipeTag/domain/RecipeTag.java b/src/main/java/CookSave/CookSaveback/RecipeTag/domain/RecipeTag.java new file mode 100644 index 0000000..98aef17 --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/RecipeTag/domain/RecipeTag.java @@ -0,0 +1,26 @@ +package CookSave.CookSaveback.RecipeTag.domain; + +import CookSave.CookSaveback.Recipe.domain.Recipe; +import CookSave.CookSaveback.Tag.domain.Tag; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class RecipeTag { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(nullable = false, updatable = false) + private Integer recipeTagId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "recipe_id", nullable = false) + private Recipe recipe; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "tag_id", nullable = false) + private Tag tag; +} diff --git a/src/main/java/CookSave/CookSaveback/RecipeTag/repository/RecipeTagRepository.java b/src/main/java/CookSave/CookSaveback/RecipeTag/repository/RecipeTagRepository.java new file mode 100644 index 0000000..5ef07fa --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/RecipeTag/repository/RecipeTagRepository.java @@ -0,0 +1,15 @@ +package CookSave.CookSaveback.RecipeTag.repository; + +import CookSave.CookSaveback.Recipe.domain.Recipe; +import CookSave.CookSaveback.RecipeTag.domain.RecipeTag; +import CookSave.CookSaveback.Tag.domain.Tag; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface RecipeTagRepository extends JpaRepository { + boolean existsByRecipeAndTag(Recipe recipe, Tag tag); + List findAllByRecipe(Recipe recipe); +} diff --git a/src/main/java/CookSave/CookSaveback/Tag/domain/Tag.java b/src/main/java/CookSave/CookSaveback/Tag/domain/Tag.java new file mode 100644 index 0000000..3e1a222 --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/Tag/domain/Tag.java @@ -0,0 +1,19 @@ +package CookSave.CookSaveback.Tag.domain; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Tag { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(nullable = false, updatable = false) + private Integer tagId; + + @Column(nullable = false, length = 10) + private String name; +} diff --git a/src/main/java/CookSave/CookSaveback/Tag/repository/TagRepository.java b/src/main/java/CookSave/CookSaveback/Tag/repository/TagRepository.java new file mode 100644 index 0000000..d132170 --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/Tag/repository/TagRepository.java @@ -0,0 +1,12 @@ +package CookSave.CookSaveback.Tag.repository; + +import CookSave.CookSaveback.Tag.domain.Tag; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface TagRepository extends JpaRepository{ + Optional findByName(String name); +} diff --git a/src/main/java/CookSave/CookSaveback/global/config/EncoderConfig.java b/src/main/java/CookSave/CookSaveback/global/config/EncoderConfig.java new file mode 100644 index 0000000..435b253 --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/global/config/EncoderConfig.java @@ -0,0 +1,13 @@ +package CookSave.CookSaveback.global.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +@Configuration +public class EncoderConfig { + @Bean + public BCryptPasswordEncoder encoder(){ + return new BCryptPasswordEncoder(); + } +} diff --git a/src/main/java/CookSave/CookSaveback/global/config/JwtFilter.java b/src/main/java/CookSave/CookSaveback/global/config/JwtFilter.java new file mode 100644 index 0000000..d87f9c0 --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/global/config/JwtFilter.java @@ -0,0 +1,60 @@ +package CookSave.CookSaveback.global.config; + +import CookSave.CookSaveback.utils.JwtUtil; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.List; + +@Slf4j +@RequiredArgsConstructor +public class JwtFilter extends OncePerRequestFilter { + private final String secretKey; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException{ + // header에서 token 가져오기 + final String authorization = request.getHeader(HttpHeaders.AUTHORIZATION); + log.info("authorization: {}", authorization); + + // token이 null이 아닌지, Bearer 로 시작하는지 확인 + if(authorization == null || !authorization.startsWith("Bearer ")){ + log.error("올바르지 않은 authorization입니다."); + filterChain.doFilter(request, response); + return; + } + + // token에서 Bearer 떼어내서 token만 남기기 + String token = authorization.split(" ")[1]; // authorization의 두 번째 단어 + + //token이 expire 되었는지 확인 + if(JwtUtil.isExpired(token, secretKey)){ + log.error("토큰이 만료되었습니다."); + filterChain.doFilter(request, response); + return; + } + + // token에서 cooksaveId 꺼내기 + String cooksaveId = JwtUtil.getCooksaveId(token, secretKey); + log.info("cooksaveId:{}", cooksaveId); + + // 권한 부여하기 + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(cooksaveId, null, List.of(new SimpleGrantedAuthority("USER"))); + + // Detail 넣기 + authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authenticationToken); + filterChain.doFilter(request, response); + } +} diff --git a/src/main/java/CookSave/CookSaveback/global/config/SecurityConfig.java b/src/main/java/CookSave/CookSaveback/global/config/SecurityConfig.java new file mode 100644 index 0000000..bf47b6d --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/global/config/SecurityConfig.java @@ -0,0 +1,59 @@ +package CookSave.CookSaveback.global.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.Arrays; +import java.util.List; + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfig { + @Value("${spring.jwt.secret-key}") + private String secretKey; + + @Bean + CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration config = new CorsConfiguration(); + config.setAllowedOrigins(Arrays.asList( + "https://www.cooksave.co.kr", "http://api.cooksave.co.kr", "https://api.cooksave.co.kr", + "http://localhost:3000")); + config.setAllowedMethods(List.of("*")); + config.setAllowedHeaders(List.of("*")); + config.setAllowCredentials(true); + config.setMaxAge(3600L); + config.addExposedHeader("Authorization"); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + return (source); + } + + @Bean + public SecurityFilterChain securityFilterChain (HttpSecurity httpSecurity) throws Exception { + httpSecurity.csrf(AbstractHttpConfigurer::disable); + httpSecurity.cors(corsConfigurer -> corsConfigurer.configurationSource(corsConfigurationSource())); + httpSecurity.sessionManagement( + sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); + httpSecurity.formLogin(AbstractHttpConfigurer::disable); + httpSecurity.httpBasic(AbstractHttpConfigurer::disable); + + httpSecurity.authorizeHttpRequests(authorize -> authorize + .requestMatchers("/members/signup", "/members/login", "/members/refreshtoken").permitAll() + .anyRequest().permitAll() + ); + httpSecurity.addFilterBefore(new JwtFilter(secretKey), UsernamePasswordAuthenticationFilter.class); + return httpSecurity.build(); + } +} diff --git a/src/main/java/CookSave/CookSaveback/global/entity/BaseTimeEntity.java b/src/main/java/CookSave/CookSaveback/global/entity/BaseTimeEntity.java new file mode 100644 index 0000000..305229b --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/global/entity/BaseTimeEntity.java @@ -0,0 +1,19 @@ +package CookSave.CookSaveback.global.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDate; + +@Getter +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public class BaseTimeEntity { + @CreatedDate + @Column(updatable = false) + private LocalDate createdAt; +} diff --git a/src/main/java/CookSave/CookSaveback/utils/JwtUtil.java b/src/main/java/CookSave/CookSaveback/utils/JwtUtil.java new file mode 100644 index 0000000..8fdcf40 --- /dev/null +++ b/src/main/java/CookSave/CookSaveback/utils/JwtUtil.java @@ -0,0 +1,51 @@ +package CookSave.CookSaveback.utils; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; + +import java.util.Date; + +public class JwtUtil { + // token이 expire 되었으면 true를 리턴하는 함수 + public static boolean isExpired(String token, String secretKey){ + return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token) + .getBody().getExpiration().before(new Date()); + } + + // token에서 cooksaveId를 꺼내어 리턴하는 함수 + public static String getCooksaveId(String token, String secretKey) { + return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token) + .getBody().get("cooksaveId", String.class); + } + + // token을 만드는 함수. createAccessToken과 createRefreshToken이 이 함수를 호출. + public static String createToken(String cooksaveId, String key, long expireTimeMs) { + Claims claims = Jwts.claims(); // 일종의 Map. 토큰 생성에 필요한 데이터를 담아두는 공간. + claims.put("cooksaveId", cooksaveId); // 아이디를 저장 + + return Jwts.builder() + .setClaims(claims) + .setIssuedAt(new Date(System.currentTimeMillis())) // 발행 시각 + .setExpiration(new Date(System.currentTimeMillis() + expireTimeMs)) // 만료 시각 + .signWith(SignatureAlgorithm.HS256, key) // HS256이라는 알고리즘과 주어진 key를 이용해 암호화 + .compact(); + } + + // AccessToken을 만드는 함수 + public static String createAccessToken(String cooksaveId, String key, long expireTimeMs) { + return createToken(cooksaveId, key, expireTimeMs); + } + + // RefreshToken을 만드는 함수 + public static String createRefreshToken (String cooksaveId, String key, long expireTimeMs) { + return createToken(cooksaveId, key, expireTimeMs); + } + + public static Claims parseRefreshToken(String value, String key) { + return Jwts.parser() + .setSigningKey(key) + .parseClaimsJws(value) + .getBody(); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index 8b13789..0000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -