diff --git a/.github/ISSUE_TEMPLATE/issue--user-side-qa.md b/.github/ISSUE_TEMPLATE/issue--user-side-qa.md deleted file mode 100644 index 790a637e..00000000 --- a/.github/ISSUE_TEMPLATE/issue--user-side-qa.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -name: 'Issue: User-side QA' -about: UI/UX에 대한 QA 이슈 템플릿 -title: "[UI/UX]" -labels: '' -assignees: '' - ---- - -# 🤖 QA 개요 - - -### ✅ Resolve TODO - -- [ ] - -### 📚 Remarks - diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 323bb4d2..7b9b4f1f 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,15 +1,10 @@ -## 📝 PR Summary +# 💡 PR Summary - + +## 개요 +- close #issueNumber - +## 작업사항 +- 내용을 적어주세요. -#### 🌲 Working Branch - - - -#### 🌲 TODOs - - - -### Related Issues - - +## 변경로직 +- 내용을 적어주세요. diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 00000000..f9228549 --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,35 @@ +name: CI + +on: + pull_request: + branch: 'dev' + types: [ opened, synchronize, reopened ] + +jobs: + build: + name: CI + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Set up Java + uses: actions/setup-java@v3 + with: + java-version: 17 + distribution: 'corretto' + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Build with Gradle + run: ./gradlew :Api:build + + - name: Cache Gradle packages + uses: actions/cache@v1 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + restore-keys: ${{ runner.os }}-gradle \ No newline at end of file diff --git a/.github/workflows/DevCICD.yml b/.github/workflows/DevCICD.yml new file mode 100644 index 00000000..67c7d771 --- /dev/null +++ b/.github/workflows/DevCICD.yml @@ -0,0 +1,79 @@ +name: DEV CI/CD + +on: + push: + branches: dev + +env: + PROFILE_DEV: dev + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: checkout + uses: actions/checkout@v2 + + - name: Set up Java + uses: actions/setup-java@v3 + with: + java-version: 17 + distribution: 'corretto' + + - name: Grant execute permission for gradlew + run: chmod +x ./gradlew + + - name: Build with Gradle + run: ./gradlew :Api:build + + - name: Docker build + run: | + docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} + docker build -t ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_DEV_REPO }} . + docker push ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_DEV_REPO }}:latest + + - name: Get Public IP + id: publicip + run: | + response=$(curl -s canhazip.com) + echo "ip='$response'" >> "$GITHUB_OUTPUT" + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_DEV_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_DEV_SECRET_ACCESS_KEY }} + aws-region: ${{ secrets.AWS_REGION }} + + - name: Add GitHub IP to AWS + run: | + aws ec2 authorize-security-group-ingress --group-id ${{ secrets.AWS_DEV_SG_ID }} --protocol tcp --port ${{ secrets.EC2_SSH_PORT }} --cidr ${{ steps.publicip.outputs.ip }}/32 + + - name: Deploy + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.EC2_DEV_HOST }} + username: ${{ secrets.EC2_DEV_USERNAME }} + key: ${{ secrets.EC2_DEV_KEY }} + port: ${{ secrets.EC2_SSH_PORT }} + timeout: 60s + script: | + cd allchive-dev + + sudo touch .env + echo "${{ secrets.ENV_DEV_VARS }}" | sudo tee .env > /dev/null + + sudo docker stop $(sudo docker ps -a -q) + sudo docker rm $(sudo docker ps -a -q) + sudo docker rmi $(sudo docker images -q) + sudo docker pull ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_DEV_REPO }}:latest + sudo docker-compose -f ~/allchive-dev/docker-compose.yml --env-file ~/allchive-dev/.env up --build -d + sudo docker system prune --all -f + + - name: Remove IP FROM security group + run: | + aws ec2 revoke-security-group-ingress --group-id ${{ secrets.AWS_DEV_SG_ID }} --protocol tcp --port ${{ secrets.EC2_SSH_PORT }} --cidr ${{ steps.publicip.outputs.ip }}/32 diff --git a/.github/workflows/ProdCICD.yml b/.github/workflows/ProdCICD.yml new file mode 100644 index 00000000..e8173130 --- /dev/null +++ b/.github/workflows/ProdCICD.yml @@ -0,0 +1,79 @@ +name: PROD CI/CD + +on: + push: + branches: prod + +env: + PROFILE_PROD: prod + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: checkout + uses: actions/checkout@v2 + + - name: Set up Java + uses: actions/setup-java@v3 + with: + java-version: 17 + distribution: 'corretto' + + - name: Grant execute permission for gradlew + run: chmod +x ./gradlew + + - name: Build with Gradle + run: ./gradlew :Api:build + + - name: Docker build + run: | + docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} + docker build -t ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_PROD_REPO }} . + docker push ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_PROD_REPO }}:latest + + - name: Get Public IP + id: publicip + run: | + response=$(curl -s canhazip.com) + echo "ip='$response'" >> "$GITHUB_OUTPUT" + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_PROD_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_PROD_SECRET_ACCESS_KEY }} + aws-region: ${{ secrets.AWS_REGION }} + + - name: Add GitHub IP to AWS + run: | + aws ec2 authorize-security-group-ingress --group-id ${{ secrets.AWS_PROD_SG_ID }} --protocol tcp --port ${{ secrets.EC2_SSH_PORT }} --cidr ${{ steps.publicip.outputs.ip }}/32 + + - name: Deploy + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.EC2_PROD_HOST }} + username: ${{ secrets.EC2_PROD_USERNAME }} + key: ${{ secrets.EC2_PROD_KEY }} + port: ${{ secrets.EC2_SSH_PORT }} + timeout: 60s + script: | + cd allchive-prod + + sudo touch .env + echo "${{ secrets.ENV_PROD_VARS }}" | sudo tee .env > /dev/null + + sudo docker stop $(sudo docker ps -a -q) + sudo docker rm $(sudo docker ps -a -q) + sudo docker rmi $(sudo docker images -q) + sudo docker pull ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_PROD_REPO }}:latest + sudo docker-compose -f ~/allchive-prod/docker-compose.yml --env-file ~/allchive-prod/.env up --build -d + sudo docker system prune --all -f + + - name: Remove IP FROM security group + run: | + aws ec2 revoke-security-group-ingress --group-id ${{ secrets.AWS_PROD_SG_ID }} --protocol tcp --port ${{ secrets.EC2_SSH_PORT }} --cidr ${{ steps.publicip.outputs.ip }}/32 diff --git a/.gitignore b/.gitignore index 7bca7f65..9d21c5b9 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,7 @@ out/ .DS_Store .env -.env.* \ No newline at end of file +.env.* + + +Domain/src/main/generated/**/*.java \ No newline at end of file diff --git a/Api/build.gradle b/Api/build.gradle index 46ddfe76..93615c1b 100644 --- a/Api/build.gradle +++ b/Api/build.gradle @@ -11,11 +11,11 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-security' + + // swagger implementation 'org.springdoc:springdoc-openapi-ui:1.6.12' - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.2' - testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.2' + implementation project(':Domain') implementation project(':Core') implementation project(':Infrastructure') diff --git a/Api/src/main/java/allchive/server/ApiApplication.java b/Api/src/main/java/allchive/server/ApiApplication.java index cfd8dc67..cc95498b 100644 --- a/Api/src/main/java/allchive/server/ApiApplication.java +++ b/Api/src/main/java/allchive/server/ApiApplication.java @@ -7,7 +7,6 @@ @SpringBootApplication public class ApiApplication { public static void main(String[] args) { - System.out.println("Hello world!"); SpringApplication.run(ApiApplication.class, args); } } diff --git a/Api/src/main/java/allchive/server/api/SchedulerPackageLocation.java b/Api/src/main/java/allchive/server/api/SchedulerPackageLocation.java new file mode 100644 index 00000000..e57515ef --- /dev/null +++ b/Api/src/main/java/allchive/server/api/SchedulerPackageLocation.java @@ -0,0 +1,3 @@ +package allchive.server.api; + +public interface SchedulerPackageLocation {} diff --git a/Api/src/main/java/allchive/server/api/archiving/controller/ArchivingController.java b/Api/src/main/java/allchive/server/api/archiving/controller/ArchivingController.java new file mode 100644 index 00000000..ef85f68d --- /dev/null +++ b/Api/src/main/java/allchive/server/api/archiving/controller/ArchivingController.java @@ -0,0 +1,115 @@ +package allchive.server.api.archiving.controller; + + +import allchive.server.api.archiving.model.dto.request.CreateArchivingRequest; +import allchive.server.api.archiving.model.dto.request.UpdateArchivingRequest; +import allchive.server.api.archiving.model.dto.response.ArchivingContentsResponse; +import allchive.server.api.archiving.model.dto.response.ArchivingResponse; +import allchive.server.api.archiving.model.dto.response.ArchivingTitleResponse; +import allchive.server.api.archiving.service.*; +import allchive.server.api.common.slice.SliceResponse; +import allchive.server.domain.domains.archiving.domain.enums.Category; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.api.annotations.ParameterObject; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/archivings") +@RequiredArgsConstructor +@Slf4j +@Tag(name = "3. [archiving]") +public class ArchivingController { + private final CreateArchivingUseCase createArchivingUseCase; + private final UpdateArchivingUseCase updateArchivingUseCase; + private final DeleteArchivingUseCase deleteArchivingUseCase; + private final GetArchivingUseCase getArchivingUseCase; + private final GetArchivedArchivingUseCase getArchivedArchivingUseCase; + private final GetScrapArchivingUseCase getScrapArchivingUseCase; + private final GetArchivingTitleUseCase getArchivingTitleUseCase; + private final GetArchivingContentsUseCase getArchivingContentsUseCase; + private final UpdateArchivingScrapUseCase updateArchivingScrapUseCase; + private final UpdateArchivingPinUseCase updateArchivingPinUseCase; + + @Operation(summary = "아카이빙을 생성합니다.") + @PostMapping() + public void createArchiving(@RequestBody CreateArchivingRequest createArchivingRequest) { + createArchivingUseCase.execute(createArchivingRequest); + } + + @Operation(summary = "아카이빙을 수정합니다.") + @PatchMapping(value = "/{archivingId}") + public void updateArchiving( + @PathVariable("archivingId") Long archivingId, + @RequestBody UpdateArchivingRequest updateArchivingRequest) { + updateArchivingUseCase.execute(archivingId, updateArchivingRequest); + } + + @Operation(summary = "아카이빙을 삭제합니다.") + @DeleteMapping(value = "/{archivingId}") + public void deleteArchiving(@PathVariable("archivingId") Long archivingId) { + deleteArchivingUseCase.execute(archivingId); + } + + @Operation( + summary = "주제별 아카이빙 리스트를 가져옵니다.", + description = "sort parameter는 입력하지 말아주세요! sorting : 스크랩 여부 -> 스크랩 수 -> 생성일자") + @GetMapping() + public SliceResponse getArchiving( + @RequestParam("category") Category category, + @ParameterObject @PageableDefault(size = 10) Pageable pageable) { + return getArchivingUseCase.execute(category, pageable); + } + + @Operation( + summary = "내 아카이빙 주제별 아카이빙 리스트를 가져옵니다.", + description = "sort parameter는 입력하지 말아주세요! sorting : 고정 -> 스크랩 수 -> 생성일자") + @GetMapping(value = "/me/archiving") + public SliceResponse getArchivedArchiving( + @RequestParam("category") Category category, + @ParameterObject @PageableDefault(size = 10) Pageable pageable) { + return getArchivedArchivingUseCase.execute(category, pageable); + } + + @Operation( + summary = "스크랩 주제별 아카이빙 리스트를 가져옵니다.", + description = "sort parameter는 입력하지 말아주세요! sorting : 스크랩 수 -> 생성일자") + @GetMapping(value = "/me/scrap") + public SliceResponse getScrapArchiving( + @RequestParam("category") Category category, + @ParameterObject @PageableDefault(size = 10) Pageable pageable) { + return getScrapArchivingUseCase.execute(category, pageable); + } + + @Operation(summary = "사용 중인 주제 & 아카이빙 리스트를 가져옵니다. (컨텐츠 추가 시 사용)") + @GetMapping(value = "/lists") + public ArchivingTitleResponse getScrapArchiving() { + return getArchivingTitleUseCase.execute(); + } + + @Operation(summary = "아카이빙별 컨텐츠 리스트를 가져옵니다.") + @GetMapping(value = "/{archivingId}/contents") + public ArchivingContentsResponse getArchivingContents( + @PathVariable("archivingId") Long archivingId, + @ParameterObject @PageableDefault(size = 10) Pageable pageable) { + return getArchivingContentsUseCase.execute(archivingId, pageable); + } + + @Operation(summary = "아카이빙을 스크랩합니다.", description = "스크랩 취소면 cancel에 true 값 보내주세요") + @PatchMapping(value = "/{archivingId}/scrap") + public void updateArchivingScrap( + @RequestParam("cancel") Boolean cancel, @PathVariable("archivingId") Long archivingId) { + updateArchivingScrapUseCase.execute(archivingId, cancel); + } + + @Operation(summary = "아카이빙을 고정합니다.", description = "고정 취소면 cancel에 true 값 보내주세요") + @PatchMapping(value = "/{archivingId}/pin") + public void updateArchivingPin( + @RequestParam("cancel") Boolean cancel, @PathVariable("archivingId") Long archivingId) { + updateArchivingPinUseCase.execute(archivingId, cancel); + } +} diff --git a/Api/src/main/java/allchive/server/api/archiving/model/dto/request/CreateArchivingRequest.java b/Api/src/main/java/allchive/server/api/archiving/model/dto/request/CreateArchivingRequest.java new file mode 100644 index 00000000..f317dc85 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/archiving/model/dto/request/CreateArchivingRequest.java @@ -0,0 +1,23 @@ +package allchive.server.api.archiving.model.dto.request; + + +import allchive.server.core.annotation.ValidEnum; +import allchive.server.domain.domains.archiving.domain.enums.Category; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; + +@Getter +public class CreateArchivingRequest { + @Schema(defaultValue = "아카이빙 제목", description = "아카이빙 제목") + private String title; + + @Schema(defaultValue = "아카이빙 이미지 url", description = "아카이빙 이미지 url") + private String imageUrl; + + @Schema(defaultValue = "DESIGN", description = "주제") + @ValidEnum(target = Category.class) + private Category category; + + @Schema(defaultValue = "false", description = "공개 여부") + private boolean publicStatus; +} diff --git a/Api/src/main/java/allchive/server/api/archiving/model/dto/request/UpdateArchivingRequest.java b/Api/src/main/java/allchive/server/api/archiving/model/dto/request/UpdateArchivingRequest.java new file mode 100644 index 00000000..afa449ac --- /dev/null +++ b/Api/src/main/java/allchive/server/api/archiving/model/dto/request/UpdateArchivingRequest.java @@ -0,0 +1,23 @@ +package allchive.server.api.archiving.model.dto.request; + + +import allchive.server.core.annotation.ValidEnum; +import allchive.server.domain.domains.archiving.domain.enums.Category; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; + +@Getter +public class UpdateArchivingRequest { + @Schema(defaultValue = "아카이빙 제목", description = "아카이빙 제목") + private String title; + + @Schema(defaultValue = "아카이빙 이미지 url", description = "아카이빙 이미지 url") + private String imageUrl; + + @Schema(defaultValue = "DESIGN", description = "주제") + @ValidEnum(target = Category.class) + private Category category; + + @Schema(defaultValue = "false", description = "공개 여부") + private boolean publicStatus; +} diff --git a/Api/src/main/java/allchive/server/api/archiving/model/dto/response/ArchivingContentsResponse.java b/Api/src/main/java/allchive/server/api/archiving/model/dto/response/ArchivingContentsResponse.java new file mode 100644 index 00000000..d4e14344 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/archiving/model/dto/response/ArchivingContentsResponse.java @@ -0,0 +1,53 @@ +package allchive.server.api.archiving.model.dto.response; + + +import allchive.server.api.common.slice.SliceResponse; +import allchive.server.api.content.model.dto.response.ContentResponse; +import allchive.server.domain.domains.archiving.domain.Archiving; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Getter; + +@Getter +public class ArchivingContentsResponse { + private SliceResponse contents; + + @Schema(description = "아카이빙 제목") + private String archivingTitle; + + @Schema(description = "아카이빙 고유번호") + private Long archivingId; + + @Schema(description = "아카이빙의 총 컨텐츠 개수") + private Long totalContentsCount; + + @Schema(description = "유저 소유 여부") + private Boolean isMine; + + @Builder + private ArchivingContentsResponse( + SliceResponse contents, + String archivingTitle, + Long archivingId, + Long totalContentsCount, + Boolean isMine) { + this.contents = contents; + this.archivingTitle = archivingTitle; + this.archivingId = archivingId; + this.totalContentsCount = totalContentsCount; + this.isMine = isMine; + } + + public static ArchivingContentsResponse of( + SliceResponse contentResponseSlice, + Archiving archiving, + Boolean isMine) { + return ArchivingContentsResponse.builder() + .archivingId(archiving.getId()) + .archivingTitle(archiving.getTitle()) + .totalContentsCount(archiving.getScrapCnt() + archiving.getImgCnt()) + .contents(contentResponseSlice) + .isMine(isMine) + .build(); + } +} diff --git a/Api/src/main/java/allchive/server/api/archiving/model/dto/response/ArchivingResponse.java b/Api/src/main/java/allchive/server/api/archiving/model/dto/response/ArchivingResponse.java new file mode 100644 index 00000000..c63ac0d1 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/archiving/model/dto/response/ArchivingResponse.java @@ -0,0 +1,82 @@ +package allchive.server.api.archiving.model.dto.response; + + +import allchive.server.api.common.util.UrlUtil; +import allchive.server.core.annotation.DateFormat; +import allchive.server.domain.domains.archiving.domain.Archiving; +import allchive.server.domain.domains.archiving.domain.enums.Category; +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDateTime; +import lombok.Builder; +import lombok.Getter; + +@Getter +public class ArchivingResponse { + @Schema(description = "아카이빙 고유 아이디") + private Long archivingId; + + @Schema(description = "아카이빙 제목") + private String title; + + @Schema(defaultValue = "아카이빙 이미지 url", description = "아카이빙 이미지 url") + private String imageUrl; + + @Schema( + type = "string", + pattern = "yyyy.MM.dd", + defaultValue = "2023.07.02", + description = "아카이빙 생성일자") + @DateFormat + private LocalDateTime createdAt; + + @Schema(defaultValue = "아카이빙 주제", description = "아카이빙 주제") + private Category category; + + @Schema(description = "아카이빙 컨텐츠 중 이미지 수") + private Long imgCnt; + + @Schema(description = "아카이빙 컨텐츠 중 링크 수") + private Long linkCnt; + + @Schema(description = "아카이빙 스크랩 수") + private Long scrapCnt; + + @Schema(description = "아카이빙 스크랩/고정 여부, true == 스크랩/고정됨") + private boolean markStatus; + + @Builder + private ArchivingResponse( + Long archivingId, + String title, + String imageUrl, + LocalDateTime createdAt, + Category category, + Long imgCnt, + Long linkCnt, + Long scrapCnt, + boolean markStatus) { + this.archivingId = archivingId; + this.title = title; + this.imageUrl = imageUrl; + this.createdAt = createdAt; + this.category = category; + this.imgCnt = imgCnt; + this.linkCnt = linkCnt; + this.scrapCnt = scrapCnt; + this.markStatus = markStatus; + } + + public static ArchivingResponse of(Archiving archiving, boolean markStatus) { + return ArchivingResponse.builder() + .archivingId(archiving.getId()) + .imageUrl(UrlUtil.toAssetUrl(archiving.getImageUrl())) + .title(archiving.getTitle()) + .createdAt(archiving.getCreatedAt()) + .category(archiving.getCategory()) + .imgCnt(archiving.getImgCnt()) + .linkCnt(archiving.getLinkCnt()) + .scrapCnt(archiving.getScrapCnt()) + .markStatus(markStatus) + .build(); + } +} diff --git a/Api/src/main/java/allchive/server/api/archiving/model/dto/response/ArchivingTitleResponse.java b/Api/src/main/java/allchive/server/api/archiving/model/dto/response/ArchivingTitleResponse.java new file mode 100644 index 00000000..038f4893 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/archiving/model/dto/response/ArchivingTitleResponse.java @@ -0,0 +1,93 @@ +package allchive.server.api.archiving.model.dto.response; + + +import allchive.server.api.archiving.model.vo.TitleContentCntVo; +import java.util.ArrayList; +import java.util.List; +import lombok.Builder; +import lombok.Getter; + +@Getter +public class ArchivingTitleResponse { + private List food; + private List life; + private List homeLiving; + private List shopping; + private List sport; + private List selfImprovement; + private List tech; + private List design; + private List trend; + + @Builder + private ArchivingTitleResponse( + List food, + List life, + List homeLiving, + List shopping, + List sport, + List selfImprovement, + List tech, + List design, + List trend) { + this.food = food; + this.life = life; + this.homeLiving = homeLiving; + this.shopping = shopping; + this.sport = sport; + this.selfImprovement = selfImprovement; + this.tech = tech; + this.design = design; + this.trend = trend; + } + + public static ArchivingTitleResponse init() { + return ArchivingTitleResponse.builder() + .food(new ArrayList<>()) + .life(new ArrayList<>()) + .homeLiving(new ArrayList<>()) + .shopping(new ArrayList<>()) + .sport(new ArrayList<>()) + .selfImprovement(new ArrayList<>()) + .tech(new ArrayList<>()) + .design(new ArrayList<>()) + .trend(new ArrayList<>()) + .build(); + } + + public void addFood(TitleContentCntVo vo) { + this.food.add(vo); + } + + public void addLife(TitleContentCntVo vo) { + this.life.add(vo); + } + + public void addHomeLiving(TitleContentCntVo vo) { + this.homeLiving.add(vo); + } + + public void addShopping(TitleContentCntVo vo) { + this.shopping.add(vo); + } + + public void addSport(TitleContentCntVo vo) { + this.sport.add(vo); + } + + public void addSelfImprovement(TitleContentCntVo vo) { + this.selfImprovement.add(vo); + } + + public void addTech(TitleContentCntVo vo) { + this.tech.add(vo); + } + + public void addDesign(TitleContentCntVo vo) { + this.design.add(vo); + } + + public void addTrend(TitleContentCntVo vo) { + this.trend.add(vo); + } +} diff --git a/Api/src/main/java/allchive/server/api/archiving/model/mapper/ArchivingMapper.java b/Api/src/main/java/allchive/server/api/archiving/model/mapper/ArchivingMapper.java new file mode 100644 index 00000000..678cb2ee --- /dev/null +++ b/Api/src/main/java/allchive/server/api/archiving/model/mapper/ArchivingMapper.java @@ -0,0 +1,43 @@ +package allchive.server.api.archiving.model.mapper; + + +import allchive.server.api.archiving.model.dto.request.CreateArchivingRequest; +import allchive.server.api.archiving.model.dto.response.ArchivingTitleResponse; +import allchive.server.api.archiving.model.vo.TitleContentCntVo; +import allchive.server.api.common.util.UrlUtil; +import allchive.server.core.annotation.Mapper; +import allchive.server.domain.domains.archiving.domain.Archiving; +import java.util.List; + +@Mapper +public class ArchivingMapper { + public Archiving toEntity(CreateArchivingRequest request, Long userId) { + return Archiving.of( + userId, + request.getTitle(), + UrlUtil.convertUrlToKey(request.getImageUrl()), + request.isPublicStatus(), + request.getCategory()); + } + + public ArchivingTitleResponse toArchivingTitleResponse(List archivings) { + ArchivingTitleResponse response = ArchivingTitleResponse.init(); + archivings.forEach( + archiving -> { + switch (archiving.getCategory()) { + case FOOD -> response.addFood(TitleContentCntVo.from(archiving)); + case LIFE -> response.addLife(TitleContentCntVo.from(archiving)); + case HOME_LIVING -> response.addHomeLiving( + TitleContentCntVo.from(archiving)); + case SHOPPING -> response.addShopping(TitleContentCntVo.from(archiving)); + case SPORT -> response.addSport(TitleContentCntVo.from(archiving)); + case SELF_IMPROVEMENT -> response.addSelfImprovement( + TitleContentCntVo.from(archiving)); + case TECH -> response.addTech(TitleContentCntVo.from(archiving)); + case DESIGN -> response.addDesign(TitleContentCntVo.from(archiving)); + case TREND -> response.addTrend(TitleContentCntVo.from(archiving)); + } + }); + return response; + } +} diff --git a/Api/src/main/java/allchive/server/api/archiving/model/vo/TitleContentCntVo.java b/Api/src/main/java/allchive/server/api/archiving/model/vo/TitleContentCntVo.java new file mode 100644 index 00000000..60085516 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/archiving/model/vo/TitleContentCntVo.java @@ -0,0 +1,29 @@ +package allchive.server.api.archiving.model.vo; + + +import allchive.server.domain.domains.archiving.domain.Archiving; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Getter; + +@Getter +public class TitleContentCntVo { + @Schema(defaultValue = "아카이빙 제목", description = "아카이빙 제목") + private String title; + + @Schema(defaultValue = "1", description = "아카이빙에 속한 컨텐츠 총 개수") + private Long contentCnt; + + @Builder + private TitleContentCntVo(String title, Long contentCnt) { + this.title = title; + this.contentCnt = contentCnt; + } + + public static TitleContentCntVo from(Archiving archiving) { + return TitleContentCntVo.builder() + .contentCnt(archiving.getImgCnt() + archiving.getScrapCnt()) + .title(archiving.getTitle()) + .build(); + } +} diff --git a/Api/src/main/java/allchive/server/api/archiving/service/CreateArchivingUseCase.java b/Api/src/main/java/allchive/server/api/archiving/service/CreateArchivingUseCase.java new file mode 100644 index 00000000..b52ec1c9 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/archiving/service/CreateArchivingUseCase.java @@ -0,0 +1,25 @@ +package allchive.server.api.archiving.service; + + +import allchive.server.api.archiving.model.dto.request.CreateArchivingRequest; +import allchive.server.api.archiving.model.mapper.ArchivingMapper; +import allchive.server.api.config.security.SecurityUtil; +import allchive.server.core.annotation.UseCase; +import allchive.server.domain.domains.archiving.domain.Archiving; +import allchive.server.domain.domains.archiving.service.ArchivingDomainService; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +@UseCase +@RequiredArgsConstructor +public class CreateArchivingUseCase { + private final ArchivingMapper archivingMapper; + private final ArchivingDomainService archivingDomainService; + + @Transactional + public void execute(CreateArchivingRequest request) { + Long userId = SecurityUtil.getCurrentUserId(); + final Archiving archiving = archivingMapper.toEntity(request, userId); + archivingDomainService.save(archiving); + } +} diff --git a/Api/src/main/java/allchive/server/api/archiving/service/DeleteArchivingUseCase.java b/Api/src/main/java/allchive/server/api/archiving/service/DeleteArchivingUseCase.java new file mode 100644 index 00000000..d54ddd61 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/archiving/service/DeleteArchivingUseCase.java @@ -0,0 +1,34 @@ +package allchive.server.api.archiving.service; + + +import allchive.server.api.config.security.SecurityUtil; +import allchive.server.api.recycle.model.mapper.RecycleMapper; +import allchive.server.core.annotation.UseCase; +import allchive.server.domain.domains.archiving.service.ArchivingDomainService; +import allchive.server.domain.domains.archiving.validator.ArchivingValidator; +import allchive.server.domain.domains.recycle.domain.Recycle; +import allchive.server.domain.domains.recycle.service.RecycleDomainService; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +@UseCase +@RequiredArgsConstructor +public class DeleteArchivingUseCase { + private final ArchivingDomainService archivingDomainService; + private final ArchivingValidator archivingValidator; + private final RecycleMapper recycleMapper; + private final RecycleDomainService recycleDomainService; + + @Transactional + public void execute(Long archivingId) { + Long userId = SecurityUtil.getCurrentUserId(); + validateExecution(archivingId, userId); + archivingDomainService.softDeleteById(archivingId); + Recycle recycle = recycleMapper.toArchivingRecycleEntity(userId, archivingId); + recycleDomainService.save(recycle); + } + + private void validateExecution(Long archivingId, Long userId) { + archivingValidator.verifyUser(userId, archivingId); + } +} diff --git a/Api/src/main/java/allchive/server/api/archiving/service/GetArchivedArchivingUseCase.java b/Api/src/main/java/allchive/server/api/archiving/service/GetArchivedArchivingUseCase.java new file mode 100644 index 00000000..edaaf4f6 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/archiving/service/GetArchivedArchivingUseCase.java @@ -0,0 +1,33 @@ +package allchive.server.api.archiving.service; + + +import allchive.server.api.archiving.model.dto.response.ArchivingResponse; +import allchive.server.api.common.slice.SliceResponse; +import allchive.server.api.config.security.SecurityUtil; +import allchive.server.core.annotation.UseCase; +import allchive.server.domain.domains.archiving.adaptor.ArchivingAdaptor; +import allchive.server.domain.domains.archiving.domain.enums.Category; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.transaction.annotation.Transactional; + +@UseCase +@RequiredArgsConstructor +public class GetArchivedArchivingUseCase { + private final ArchivingAdaptor archivingAdaptor; + + @Transactional(readOnly = true) + public SliceResponse execute(Category category, Pageable pageable) { + Long userId = SecurityUtil.getCurrentUserId(); + Slice archivingSlices = + archivingAdaptor + .querySliceArchivingByUserId(userId, category, pageable) + .map( + archiving -> + ArchivingResponse.of( + archiving, + archiving.getPinUserId().contains(userId))); + return SliceResponse.of(archivingSlices); + } +} diff --git a/Api/src/main/java/allchive/server/api/archiving/service/GetArchivingContentsUseCase.java b/Api/src/main/java/allchive/server/api/archiving/service/GetArchivingContentsUseCase.java new file mode 100644 index 00000000..cc628be7 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/archiving/service/GetArchivingContentsUseCase.java @@ -0,0 +1,65 @@ +package allchive.server.api.archiving.service; + + +import allchive.server.api.archiving.model.dto.response.ArchivingContentsResponse; +import allchive.server.api.common.slice.SliceResponse; +import allchive.server.api.config.security.SecurityUtil; +import allchive.server.api.content.model.dto.response.ContentResponse; +import allchive.server.api.content.model.mapper.ContentMapper; +import allchive.server.core.annotation.UseCase; +import allchive.server.domain.domains.archiving.adaptor.ArchivingAdaptor; +import allchive.server.domain.domains.archiving.domain.Archiving; +import allchive.server.domain.domains.archiving.validator.ArchivingValidator; +import allchive.server.domain.domains.content.adaptor.ContentAdaptor; +import allchive.server.domain.domains.content.adaptor.ContentTagGroupAdaptor; +import allchive.server.domain.domains.content.domain.Content; +import allchive.server.domain.domains.content.domain.ContentTagGroup; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.transaction.annotation.Transactional; + +@UseCase +@RequiredArgsConstructor +public class GetArchivingContentsUseCase { + private final ArchivingAdaptor archivingAdaptor; + private final ArchivingValidator archivingValidator; + private final ContentAdaptor contentAdaptor; + private final ContentTagGroupAdaptor contentTagGroupAdaptor; + private final ContentMapper contentMapper; + + @Transactional(readOnly = true) + public ArchivingContentsResponse execute(Long archivingId, Pageable pageable) { + Long userId = SecurityUtil.getCurrentUserId(); + validateExecution(archivingId, userId); + Archiving archiving = archivingAdaptor.findById(archivingId); + Slice contentResponseSlice = + getContentResponseSlice(archivingId, pageable); + return ArchivingContentsResponse.of( + SliceResponse.of(contentResponseSlice), + archiving, + calculateIsMine(archiving, userId)); + } + + private void validateExecution(Long archivingId, Long userId) { + archivingValidator.validatePublicStatus(archivingId, userId); + archivingValidator.validateDeleteStatus(archivingId, userId); + } + + private Slice getContentResponseSlice(Long archivingId, Pageable pageable) { + Slice contentList = + contentAdaptor.querySliceContentByArchivingId(archivingId, pageable); + List contentTagGroupList = + contentTagGroupAdaptor.queryContentTagGroupByContentIn(contentList.getContent()); + return contentList.map( + content -> contentMapper.toContentResponse(content, contentTagGroupList)); + } + + private Boolean calculateIsMine(Archiving archiving, Long userId) { + if (archiving.getUserId().equals(userId)) { + return Boolean.TRUE; + } + return Boolean.FALSE; + } +} diff --git a/Api/src/main/java/allchive/server/api/archiving/service/GetArchivingTitleUseCase.java b/Api/src/main/java/allchive/server/api/archiving/service/GetArchivingTitleUseCase.java new file mode 100644 index 00000000..0b594cb6 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/archiving/service/GetArchivingTitleUseCase.java @@ -0,0 +1,24 @@ +package allchive.server.api.archiving.service; + + +import allchive.server.api.archiving.model.dto.response.ArchivingTitleResponse; +import allchive.server.api.archiving.model.mapper.ArchivingMapper; +import allchive.server.api.config.security.SecurityUtil; +import allchive.server.core.annotation.UseCase; +import allchive.server.domain.domains.archiving.adaptor.ArchivingAdaptor; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +@UseCase +@RequiredArgsConstructor +public class GetArchivingTitleUseCase { + private final ArchivingAdaptor archivingAdaptor; + private final ArchivingMapper archivingMapper; + + @Transactional(readOnly = true) + public ArchivingTitleResponse execute() { + Long userId = SecurityUtil.getCurrentUserId(); + return archivingMapper.toArchivingTitleResponse( + archivingAdaptor.queryArchivingByUserId(userId)); + } +} diff --git a/Api/src/main/java/allchive/server/api/archiving/service/GetArchivingUseCase.java b/Api/src/main/java/allchive/server/api/archiving/service/GetArchivingUseCase.java new file mode 100644 index 00000000..d0757297 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/archiving/service/GetArchivingUseCase.java @@ -0,0 +1,45 @@ +package allchive.server.api.archiving.service; + + +import allchive.server.api.archiving.model.dto.response.ArchivingResponse; +import allchive.server.api.common.slice.SliceResponse; +import allchive.server.api.config.security.SecurityUtil; +import allchive.server.core.annotation.UseCase; +import allchive.server.domain.domains.archiving.adaptor.ArchivingAdaptor; +import allchive.server.domain.domains.archiving.domain.enums.Category; +import allchive.server.domain.domains.block.adaptor.BlockAdaptor; +import allchive.server.domain.domains.block.domain.Block; +import allchive.server.domain.domains.user.adaptor.ScrapAdaptor; +import allchive.server.domain.domains.user.domain.Scrap; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.transaction.annotation.Transactional; + +@UseCase +@RequiredArgsConstructor +public class GetArchivingUseCase { + private final ScrapAdaptor scrapAdaptor; + private final BlockAdaptor blockAdaptor; + private final ArchivingAdaptor archivingAdaptor; + + @Transactional(readOnly = true) + public SliceResponse execute(Category category, Pageable pageable) { + Long userId = SecurityUtil.getCurrentUserId(); + List archivingIdList = + scrapAdaptor.findAllByUserId(userId).stream().map(Scrap::getArchivingId).toList(); + List blockList = + blockAdaptor.findByBlockFrom(userId).stream().map(Block::getBlockUser).toList(); + Slice archivingSlices = + archivingAdaptor + .querySliceArchivingExceptBlock( + archivingIdList, blockList, category, pageable) + .map( + archiving -> + ArchivingResponse.of( + archiving, + archivingIdList.contains(archiving.getId()))); + return SliceResponse.of(archivingSlices); + } +} diff --git a/Api/src/main/java/allchive/server/api/archiving/service/GetScrapArchivingUseCase.java b/Api/src/main/java/allchive/server/api/archiving/service/GetScrapArchivingUseCase.java new file mode 100644 index 00000000..9ec09011 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/archiving/service/GetScrapArchivingUseCase.java @@ -0,0 +1,39 @@ +package allchive.server.api.archiving.service; + + +import allchive.server.api.archiving.model.dto.response.ArchivingResponse; +import allchive.server.api.common.slice.SliceResponse; +import allchive.server.api.config.security.SecurityUtil; +import allchive.server.core.annotation.UseCase; +import allchive.server.domain.domains.archiving.adaptor.ArchivingAdaptor; +import allchive.server.domain.domains.archiving.domain.enums.Category; +import allchive.server.domain.domains.user.adaptor.ScrapAdaptor; +import allchive.server.domain.domains.user.domain.Scrap; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.transaction.annotation.Transactional; + +@UseCase +@RequiredArgsConstructor +public class GetScrapArchivingUseCase { + private final ScrapAdaptor scrapAdaptor; + private final ArchivingAdaptor archivingAdaptor; + + @Transactional(readOnly = true) + public SliceResponse execute(Category category, Pageable pageable) { + Long userId = SecurityUtil.getCurrentUserId(); + List archivingIdList = + scrapAdaptor.findAllByUserId(userId).stream().map(Scrap::getArchivingId).toList(); + Slice archivingSlices = + archivingAdaptor + .querySliceArchivingByIdIn(archivingIdList, category, pageable) + .map( + archiving -> + ArchivingResponse.of( + archiving, + archivingIdList.contains(archiving.getId()))); + return SliceResponse.of(archivingSlices); + } +} diff --git a/Api/src/main/java/allchive/server/api/archiving/service/UpdateArchivingPinUseCase.java b/Api/src/main/java/allchive/server/api/archiving/service/UpdateArchivingPinUseCase.java new file mode 100644 index 00000000..5108bc34 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/archiving/service/UpdateArchivingPinUseCase.java @@ -0,0 +1,37 @@ +package allchive.server.api.archiving.service; + + +import allchive.server.api.config.security.SecurityUtil; +import allchive.server.core.annotation.UseCase; +import allchive.server.domain.domains.archiving.service.ArchivingDomainService; +import allchive.server.domain.domains.archiving.validator.ArchivingValidator; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +@UseCase +@RequiredArgsConstructor +public class UpdateArchivingPinUseCase { + private final ArchivingValidator archivingValidator; + private final ArchivingDomainService archivingDomainService; + + @Transactional + public void execute(Long archivingId, Boolean cancel) { + Long userId = SecurityUtil.getCurrentUserId(); + validateExecution(archivingId, userId, cancel); + if (cancel) { + archivingDomainService.updatePin(archivingId, userId, false); + } else { + archivingDomainService.updatePin(archivingId, userId, true); + } + } + + private void validateExecution(Long archivingId, Long userId, Boolean cancel) { + archivingValidator.validateExistById(archivingId); + archivingValidator.validateDeleteStatus(archivingId, userId); + if (cancel) { + archivingValidator.validateNotPinStatus(archivingId, userId); + } else { + archivingValidator.validateAlreadyPinStatus(archivingId, userId); + } + } +} diff --git a/Api/src/main/java/allchive/server/api/archiving/service/UpdateArchivingScrapUseCase.java b/Api/src/main/java/allchive/server/api/archiving/service/UpdateArchivingScrapUseCase.java new file mode 100644 index 00000000..9ff7468b --- /dev/null +++ b/Api/src/main/java/allchive/server/api/archiving/service/UpdateArchivingScrapUseCase.java @@ -0,0 +1,47 @@ +package allchive.server.api.archiving.service; + + +import allchive.server.api.config.security.SecurityUtil; +import allchive.server.core.annotation.UseCase; +import allchive.server.domain.domains.archiving.service.ArchivingDomainService; +import allchive.server.domain.domains.archiving.validator.ArchivingValidator; +import allchive.server.domain.domains.user.adaptor.UserAdaptor; +import allchive.server.domain.domains.user.domain.Scrap; +import allchive.server.domain.domains.user.domain.User; +import allchive.server.domain.domains.user.service.ScrapDomainService; +import allchive.server.domain.domains.user.validator.ScrapValidator; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +@UseCase +@RequiredArgsConstructor +public class UpdateArchivingScrapUseCase { + private final ArchivingValidator archivingValidator; + private final UserAdaptor userAdaptor; + private final ScrapDomainService scrapDomainService; + private final ArchivingDomainService archivingDomainService; + private final ScrapValidator scrapValidator; + + @Transactional + public void execute(Long archivingId, Boolean cancel) { + Long userId = SecurityUtil.getCurrentUserId(); + validateExecution(archivingId, userId, cancel); + User user = userAdaptor.findById(userId); + if (cancel) { + scrapDomainService.deleteScrapByUserAndArchivingId(user, archivingId); + archivingDomainService.updateScrapCount(archivingId, -1); + } else { + Scrap scrap = Scrap.of(user, archivingId); + scrapDomainService.save(scrap); + archivingDomainService.updateScrapCount(archivingId, 1); + } + } + + private void validateExecution(Long archivingId, Long userId, Boolean cancel) { + archivingValidator.validateExistById(archivingId); + archivingValidator.validateDeleteStatus(archivingId, userId); + if (!cancel) { + scrapValidator.validateExistScrap(userId, archivingId); + } + } +} diff --git a/Api/src/main/java/allchive/server/api/archiving/service/UpdateArchivingUseCase.java b/Api/src/main/java/allchive/server/api/archiving/service/UpdateArchivingUseCase.java new file mode 100644 index 00000000..15fe872f --- /dev/null +++ b/Api/src/main/java/allchive/server/api/archiving/service/UpdateArchivingUseCase.java @@ -0,0 +1,38 @@ +package allchive.server.api.archiving.service; + + +import allchive.server.api.archiving.model.dto.request.UpdateArchivingRequest; +import allchive.server.api.common.util.UrlUtil; +import allchive.server.api.config.security.SecurityUtil; +import allchive.server.core.annotation.UseCase; +import allchive.server.domain.domains.archiving.adaptor.ArchivingAdaptor; +import allchive.server.domain.domains.archiving.domain.Archiving; +import allchive.server.domain.domains.archiving.service.ArchivingDomainService; +import allchive.server.domain.domains.archiving.validator.ArchivingValidator; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +@UseCase +@RequiredArgsConstructor +public class UpdateArchivingUseCase { + private final ArchivingDomainService archivingDomainService; + private final ArchivingAdaptor archivingAdaptor; + private final ArchivingValidator archivingValidator; + + @Transactional + public void execute(Long archivingId, UpdateArchivingRequest request) { + validateExecution(archivingId); + Archiving archiving = archivingAdaptor.findById(archivingId); + archivingDomainService.updateArchiving( + archiving, + request.getTitle(), + UrlUtil.convertUrlToKey(request.getImageUrl()), + request.isPublicStatus(), + request.getCategory()); + } + + private void validateExecution(Long archivingId) { + Long userId = SecurityUtil.getCurrentUserId(); + archivingValidator.verifyUser(userId, archivingId); + } +} diff --git a/Api/src/main/java/allchive/server/api/auth/controller/AuthController.java b/Api/src/main/java/allchive/server/api/auth/controller/AuthController.java new file mode 100644 index 00000000..6cb01deb --- /dev/null +++ b/Api/src/main/java/allchive/server/api/auth/controller/AuthController.java @@ -0,0 +1,46 @@ +package allchive.server.api.auth.controller; + + +import allchive.server.api.auth.model.dto.response.OauthRegisterResponse; +import allchive.server.api.auth.service.LogOutUserUseCase; +import allchive.server.api.auth.service.TokenRefreshUseCase; +import allchive.server.api.auth.service.WithdrawUserUseCase; +import allchive.server.domain.domains.user.domain.enums.OauthProvider; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/auth") +@RequiredArgsConstructor +@Slf4j +@Tag(name = "1-1. [auth]") +public class AuthController { + private final WithdrawUserUseCase withdrawUserUseCase; + private final LogOutUserUseCase logOutUserUseCase; + private final TokenRefreshUseCase tokenRefreshUseCase; + + @Operation(summary = "회원탈퇴를 합니다.") + @DeleteMapping("/withdrawal/{provider}") + public void withDrawUser( + @PathVariable OauthProvider provider, + @RequestParam(required = false, name = "appleAccessToken", value = "") + String appleAccessToken) { + withdrawUserUseCase.execute(provider, appleAccessToken); + } + + @Operation(summary = "로그아웃을 합니다.") + @PostMapping("/logout") + public void logOutUser() { + logOutUserUseCase.execute(); + } + + @Operation(summary = "토큰 재발급을 합니다.") + @PostMapping("/token/refresh") + public OauthRegisterResponse refreshToken( + @RequestParam(value = "refreshToken") String refreshToken) { + return tokenRefreshUseCase.execute(refreshToken); + } +} diff --git a/Api/src/main/java/allchive/server/api/auth/controller/OauthController.java b/Api/src/main/java/allchive/server/api/auth/controller/OauthController.java new file mode 100644 index 00000000..dfc246a5 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/auth/controller/OauthController.java @@ -0,0 +1,88 @@ +package allchive.server.api.auth.controller; + + +import allchive.server.api.auth.model.dto.request.RegisterRequest; +import allchive.server.api.auth.model.dto.response.OauthLoginLinkResponse; +import allchive.server.api.auth.model.dto.response.OauthRegisterResponse; +import allchive.server.api.auth.model.dto.response.OauthSignInResponse; +import allchive.server.api.auth.service.*; +import allchive.server.domain.domains.user.domain.enums.OauthProvider; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/auth/oauth") +@RequiredArgsConstructor +@Slf4j +@Tag(name = "1-2. [oauth]") +public class OauthController { + private final OauthLinkUseCase oauthLinkUseCase; + private final OauthLoginUseCase oauthLoginUseCase; + private final OauthRegisterUseCase oauthRegisterUseCase; + + @Operation( + summary = "oauth 링크발급", + description = "oauth 링크를 받아볼수 있습니다. referer, host 입력 안하셔도 됩니다!", + deprecated = true) + @GetMapping("/link/{provider}") + public OauthLoginLinkResponse getOauthLink( + @PathVariable("provider") OauthProvider provider, + @RequestHeader(value = "referer", required = false) String referer, + @RequestHeader(value = "host", required = false) String host) { + if (referer.contains(host)) { + String format = String.format("https://%s/", host); + return oauthLinkUseCase.getOauthLink(provider, format); + } + return oauthLinkUseCase.getOauthLink(provider, referer); + } + + @Operation(summary = "개발용 oauth 링크발급", deprecated = true) + @GetMapping("/link/{provider}/dev") + public OauthLoginLinkResponse getOauthLinkTest( + @PathVariable("provider") OauthProvider provider) { + return oauthLinkUseCase.getOauthLinkDev(provider); + } + + @Operation( + summary = "로그인 (code 용)", + description = "referer, host 입력 안하셔도 됩니다! 회원가입 안된 유저일 경우, canLogin=false 값을 보냅니다!") + @PostMapping("/login/{provider}/code") + public OauthSignInResponse oauthUserCodeLogin( + @PathVariable("provider") OauthProvider provider, + @RequestParam("code") String code, + @RequestHeader(value = "referer", required = false) String referer, + @RequestHeader(value = "host", required = false) String host) { + if (referer.contains(host)) { + String format = String.format("https://%s/", host); + return oauthLoginUseCase.loginWithCode(provider, code, format); + } + return oauthLoginUseCase.loginWithCode(provider, code, referer); + } + + @Operation(summary = "개발용 로그인", deprecated = true) + @GetMapping("/login/{provider}/dev") + public OauthSignInResponse oauthUserLoginDev( + @PathVariable("provider") OauthProvider provider, @RequestParam("code") String code) { + return oauthLoginUseCase.devLogin(provider, code); + } + + @Operation(summary = "로그인 (idtoken 용)", description = "회원가입 안된 유저일 경우, canLogin=false 값을 보냅니다!") + @PostMapping("/login/{provider}/idtoken") + public OauthSignInResponse oauthUserIdTokenLogin( + @PathVariable("provider") OauthProvider provider, + @RequestParam("idToken") String idToken) { + return oauthLoginUseCase.loginWithIdToken(provider, idToken); + } + + @Operation(summary = "회원가입") + @PostMapping("/register/{provider}") + public OauthRegisterResponse oauthUserRegister( + @PathVariable("provider") OauthProvider provider, + @RequestParam("idToken") String idToken, + @RequestBody RegisterRequest registerRequest) { + return oauthRegisterUseCase.execute(provider, idToken, registerRequest); + } +} diff --git a/Api/src/main/java/allchive/server/api/auth/model/dto/KakaoUserInfoDto.java b/Api/src/main/java/allchive/server/api/auth/model/dto/KakaoUserInfoDto.java new file mode 100644 index 00000000..a6be5cc0 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/auth/model/dto/KakaoUserInfoDto.java @@ -0,0 +1,26 @@ +package allchive.server.api.auth.model.dto; + + +import allchive.server.domain.domains.user.domain.enums.OauthInfo; +import allchive.server.domain.domains.user.domain.enums.OauthProvider; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class KakaoUserInfoDto { + + // oauth인증한 사용자 고유 아이디 + private final String oauthId; + + private final String email; + private final String phoneNumber; + private final String profileImage; + private final String name; + private final OauthProvider oauthProvider; + + public OauthInfo toOauthInfo() { + return OauthInfo.of(oauthProvider, oauthId); + } +} +; diff --git a/Api/src/main/java/allchive/server/api/auth/model/dto/request/RegisterRequest.java b/Api/src/main/java/allchive/server/api/auth/model/dto/request/RegisterRequest.java new file mode 100644 index 00000000..fe3d10f2 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/auth/model/dto/request/RegisterRequest.java @@ -0,0 +1,29 @@ +package allchive.server.api.auth.model.dto.request; + + +import allchive.server.core.annotation.ValidEnum; +import allchive.server.domain.domains.archiving.domain.enums.Category; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; +import javax.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class RegisterRequest { + @Schema( + defaultValue = + "https://asset.allchive.co.kr/staging/archiving/1/d241218a-a64c-4443-8aa4-ce98017a3d12", + description = "프로필 이미지 url") + @NotBlank(message = "프로필 이미지 url을 입력하세요") + private String profileImgUrl; + + @Schema(defaultValue = "닉네임", description = "닉네임") + @NotBlank(message = "닉네임을 입력하세요") + private String nickname; + + @ArraySchema(schema = @Schema(description = "관심 주제", defaultValue = "FOOD")) + private List<@ValidEnum(target = Category.class) Category> categories; +} diff --git a/Api/src/main/java/allchive/server/api/auth/model/dto/response/OauthLoginLinkResponse.java b/Api/src/main/java/allchive/server/api/auth/model/dto/response/OauthLoginLinkResponse.java new file mode 100644 index 00000000..3351f25e --- /dev/null +++ b/Api/src/main/java/allchive/server/api/auth/model/dto/response/OauthLoginLinkResponse.java @@ -0,0 +1,11 @@ +package allchive.server.api.auth.model.dto.response; + + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class OauthLoginLinkResponse { + private String link; +} diff --git a/Api/src/main/java/allchive/server/api/auth/model/dto/response/OauthRegisterResponse.java b/Api/src/main/java/allchive/server/api/auth/model/dto/response/OauthRegisterResponse.java new file mode 100644 index 00000000..21480013 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/auth/model/dto/response/OauthRegisterResponse.java @@ -0,0 +1,28 @@ +package allchive.server.api.auth.model.dto.response; + + +import allchive.server.api.auth.model.vo.TokenVo; +import com.fasterxml.jackson.annotation.JsonUnwrapped; +import lombok.Builder; +import lombok.Getter; + +@Getter +public class OauthRegisterResponse { + @JsonUnwrapped private TokenVo tokenVo; + + @Builder + private OauthRegisterResponse(TokenVo tokenVo) { + this.tokenVo = tokenVo; + } + + public static OauthRegisterResponse of( + String accessToken, Long accessTokenAge, String refreshToken, Long refreshTokenAge) { + return OauthRegisterResponse.builder() + .tokenVo(TokenVo.of(accessToken, accessTokenAge, refreshToken, refreshTokenAge)) + .build(); + } + + public static OauthRegisterResponse from(OauthSignInResponse oauthSignInResponse) { + return OauthRegisterResponse.builder().tokenVo(oauthSignInResponse.getTokenVo()).build(); + } +} diff --git a/Api/src/main/java/allchive/server/api/auth/model/dto/response/OauthSignInResponse.java b/Api/src/main/java/allchive/server/api/auth/model/dto/response/OauthSignInResponse.java new file mode 100644 index 00000000..68d7b4b8 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/auth/model/dto/response/OauthSignInResponse.java @@ -0,0 +1,48 @@ +package allchive.server.api.auth.model.dto.response; + + +import allchive.server.api.auth.model.vo.TokenVo; +import com.fasterxml.jackson.annotation.JsonUnwrapped; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Getter; + +@Getter +public class OauthSignInResponse { + @Schema(description = "로그인 가능 여부") + private boolean canLogin; + + @Schema(description = "idToken, 로그인 되었을 경우 null") + private String idToken; + + @JsonUnwrapped private TokenVo tokenVo; + + @Builder + private OauthSignInResponse(boolean canLogin, String idToken, TokenVo tokenVo) { + this.canLogin = canLogin; + this.idToken = idToken; + this.tokenVo = tokenVo; + } + + public static OauthSignInResponse of( + boolean canLogin, + String idToken, + String accessToken, + Long accessTokenAge, + String refreshToken, + Long refreshTokenAge) { + return OauthSignInResponse.builder() + .canLogin(canLogin) + .idToken(idToken) + .tokenVo(TokenVo.of(accessToken, accessTokenAge, refreshToken, refreshTokenAge)) + .build(); + } + + public static OauthSignInResponse cannotLogin(String idToken) { + return OauthSignInResponse.builder() + .canLogin(false) + .idToken(idToken) + .tokenVo(TokenVo.of(null, null, null, null)) + .build(); + } +} diff --git a/Api/src/main/java/allchive/server/api/auth/model/dto/response/OauthTokenResponse.java b/Api/src/main/java/allchive/server/api/auth/model/dto/response/OauthTokenResponse.java new file mode 100644 index 00000000..e9d6fd73 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/auth/model/dto/response/OauthTokenResponse.java @@ -0,0 +1,37 @@ +package allchive.server.api.auth.model.dto.response; + + +import allchive.server.infrastructure.oauth.apple.response.AppleTokenResponse; +import allchive.server.infrastructure.oauth.kakao.dto.KakaoTokenResponse; +import lombok.Builder; +import lombok.Getter; + +@Getter +public class OauthTokenResponse { + private String accessToken; + private String refreshToken; + private String idToken; + + @Builder + private OauthTokenResponse(String accessToken, String refreshToken, String idToken) { + this.accessToken = accessToken; + this.refreshToken = refreshToken; + this.idToken = idToken; + } + + public static OauthTokenResponse from(KakaoTokenResponse kakaoTokenResponse) { + return OauthTokenResponse.builder() + .idToken(kakaoTokenResponse.getIdToken()) + .refreshToken(kakaoTokenResponse.getRefreshToken()) + .accessToken(kakaoTokenResponse.getAccessToken()) + .build(); + } + + public static OauthTokenResponse from(AppleTokenResponse appleTokenResponse) { + return OauthTokenResponse.builder() + .idToken(appleTokenResponse.getIdToken()) + .refreshToken(appleTokenResponse.getRefreshToken()) + .accessToken(appleTokenResponse.getAccessToken()) + .build(); + } +} diff --git a/Api/src/main/java/allchive/server/api/auth/model/dto/response/OauthUserInfoResponse.java b/Api/src/main/java/allchive/server/api/auth/model/dto/response/OauthUserInfoResponse.java new file mode 100644 index 00000000..e0c97af3 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/auth/model/dto/response/OauthUserInfoResponse.java @@ -0,0 +1,32 @@ +package allchive.server.api.auth.model.dto.response; + + +import allchive.server.api.auth.model.dto.KakaoUserInfoDto; +import lombok.Builder; +import lombok.Getter; + +@Getter +public class OauthUserInfoResponse { + private final String email; + private final String phoneNumber; + private final String profileImage; + private final String name; + + @Builder + public OauthUserInfoResponse( + String email, String phoneNumber, String profileImage, String name) { + this.email = email; + this.phoneNumber = phoneNumber; + this.profileImage = profileImage; + this.name = name; + } + + public static OauthUserInfoResponse from(KakaoUserInfoDto kakaoUserInfoDto) { + return OauthUserInfoResponse.builder() + .email(kakaoUserInfoDto.getEmail()) + .phoneNumber(kakaoUserInfoDto.getPhoneNumber()) + .profileImage(kakaoUserInfoDto.getProfileImage()) + .name(kakaoUserInfoDto.getName()) + .build(); + } +} diff --git a/Api/src/main/java/allchive/server/api/auth/model/vo/TokenVo.java b/Api/src/main/java/allchive/server/api/auth/model/vo/TokenVo.java new file mode 100644 index 00000000..090856e1 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/auth/model/vo/TokenVo.java @@ -0,0 +1,38 @@ +package allchive.server.api.auth.model.vo; + + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Getter; + +@Getter +public class TokenVo { + @Schema(description = "어세스 토큰") + private final String accessToken; + + private final Long accessTokenAge; + + @Schema(description = "리프레쉬 토큰") + private final String refreshToken; + + private final Long refreshTokenAge; + + @Builder + private TokenVo( + String accessToken, Long accessTokenAge, String refreshToken, Long refreshTokenAge) { + this.accessToken = accessToken; + this.accessTokenAge = accessTokenAge; + this.refreshToken = refreshToken; + this.refreshTokenAge = refreshTokenAge; + } + + public static TokenVo of( + String accessToken, Long accessTokenAge, String refreshToken, Long refreshTokenAge) { + return TokenVo.builder() + .accessToken(accessToken) + .accessTokenAge(accessTokenAge) + .refreshToken(refreshToken) + .refreshTokenAge(refreshTokenAge) + .build(); + } +} diff --git a/Api/src/main/java/allchive/server/api/auth/service/LogOutUserUseCase.java b/Api/src/main/java/allchive/server/api/auth/service/LogOutUserUseCase.java new file mode 100644 index 00000000..ec842de9 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/auth/service/LogOutUserUseCase.java @@ -0,0 +1,18 @@ +package allchive.server.api.auth.service; + + +import allchive.server.api.config.security.SecurityUtil; +import allchive.server.core.annotation.UseCase; +import allchive.server.domain.domains.user.adaptor.RefreshTokenAdaptor; +import lombok.RequiredArgsConstructor; + +@UseCase +@RequiredArgsConstructor +public class LogOutUserUseCase { + private final RefreshTokenAdaptor refreshTokenAdaptor; + + public void execute() { + Long userId = SecurityUtil.getCurrentUserId(); + refreshTokenAdaptor.deleteTokenByUserId(userId); + } +} diff --git a/Api/src/main/java/allchive/server/api/auth/service/OauthLinkUseCase.java b/Api/src/main/java/allchive/server/api/auth/service/OauthLinkUseCase.java new file mode 100644 index 00000000..2892db3a --- /dev/null +++ b/Api/src/main/java/allchive/server/api/auth/service/OauthLinkUseCase.java @@ -0,0 +1,22 @@ +package allchive.server.api.auth.service; + + +import allchive.server.api.auth.model.dto.response.OauthLoginLinkResponse; +import allchive.server.api.auth.service.helper.OauthHelper; +import allchive.server.core.annotation.UseCase; +import allchive.server.domain.domains.user.domain.enums.OauthProvider; +import lombok.RequiredArgsConstructor; + +@UseCase +@RequiredArgsConstructor +public class OauthLinkUseCase { + private final OauthHelper oauthHelper; + + public OauthLoginLinkResponse getOauthLinkDev(OauthProvider provider) { + return oauthHelper.getOauthLinkDev(provider); + } + + public OauthLoginLinkResponse getOauthLink(OauthProvider provider, String referer) { + return oauthHelper.getOauthLink(provider, referer); + } +} diff --git a/Api/src/main/java/allchive/server/api/auth/service/OauthLoginUseCase.java b/Api/src/main/java/allchive/server/api/auth/service/OauthLoginUseCase.java new file mode 100644 index 00000000..2886b644 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/auth/service/OauthLoginUseCase.java @@ -0,0 +1,46 @@ +package allchive.server.api.auth.service; + + +import allchive.server.api.auth.model.dto.response.OauthSignInResponse; +import allchive.server.api.auth.model.dto.response.OauthTokenResponse; +import allchive.server.api.auth.service.helper.OauthHelper; +import allchive.server.api.auth.service.helper.TokenGenerateHelper; +import allchive.server.core.annotation.UseCase; +import allchive.server.domain.domains.user.domain.User; +import allchive.server.domain.domains.user.domain.enums.OauthInfo; +import allchive.server.domain.domains.user.domain.enums.OauthProvider; +import allchive.server.domain.domains.user.service.UserDomainService; +import lombok.RequiredArgsConstructor; + +@UseCase +@RequiredArgsConstructor +public class OauthLoginUseCase { + private final OauthHelper oauthHelper; + private final UserDomainService userDomainService; + private final TokenGenerateHelper tokenGenerateHelper; + + public OauthSignInResponse loginWithCode(OauthProvider provider, String code, String referer) { + final OauthTokenResponse oauthTokenResponse = + oauthHelper.getCredential(provider, code, referer); + return processLoginWithIdToken(provider, oauthTokenResponse.getIdToken()); + } + + public OauthSignInResponse loginWithIdToken(OauthProvider provider, String idToken) { + return processLoginWithIdToken(provider, idToken); + } + + public OauthSignInResponse devLogin(OauthProvider provider, String code) { + final OauthTokenResponse oauthTokenResponse = oauthHelper.getCredentialDev(provider, code); + return processLoginWithIdToken(provider, oauthTokenResponse.getIdToken()); + } + + private OauthSignInResponse processLoginWithIdToken(OauthProvider provider, String idToken) { + final OauthInfo oauthInfo = oauthHelper.getOauthInfo(provider, idToken); + if (userDomainService.checkUserCanLogin(oauthInfo)) { + User user = userDomainService.loginUser(oauthInfo); + return tokenGenerateHelper.execute(user); + } else { + return OauthSignInResponse.cannotLogin(idToken); + } + } +} diff --git a/Api/src/main/java/allchive/server/api/auth/service/OauthRegisterUseCase.java b/Api/src/main/java/allchive/server/api/auth/service/OauthRegisterUseCase.java new file mode 100644 index 00000000..377dc16d --- /dev/null +++ b/Api/src/main/java/allchive/server/api/auth/service/OauthRegisterUseCase.java @@ -0,0 +1,34 @@ +package allchive.server.api.auth.service; + + +import allchive.server.api.auth.model.dto.request.RegisterRequest; +import allchive.server.api.auth.model.dto.response.OauthRegisterResponse; +import allchive.server.api.auth.service.helper.OauthHelper; +import allchive.server.api.auth.service.helper.TokenGenerateHelper; +import allchive.server.api.common.util.UrlUtil; +import allchive.server.core.annotation.UseCase; +import allchive.server.domain.domains.user.domain.User; +import allchive.server.domain.domains.user.domain.enums.OauthInfo; +import allchive.server.domain.domains.user.domain.enums.OauthProvider; +import allchive.server.domain.domains.user.service.UserDomainService; +import lombok.RequiredArgsConstructor; + +@UseCase +@RequiredArgsConstructor +public class OauthRegisterUseCase { + private final OauthHelper oauthHelper; + private final UserDomainService userDomainService; + private final TokenGenerateHelper tokenGenerateHelper; + + public OauthRegisterResponse execute( + OauthProvider provider, String idToken, RegisterRequest registerRequest) { + final OauthInfo oauthInfo = oauthHelper.getOauthInfo(provider, idToken); + final User user = + userDomainService.registerUser( + registerRequest.getNickname(), + UrlUtil.convertUrlToKey(registerRequest.getProfileImgUrl()), + registerRequest.getCategories(), + oauthInfo); + return OauthRegisterResponse.from(tokenGenerateHelper.execute(user)); + } +} diff --git a/Api/src/main/java/allchive/server/api/auth/service/TokenRefreshUseCase.java b/Api/src/main/java/allchive/server/api/auth/service/TokenRefreshUseCase.java new file mode 100644 index 00000000..740c8a5c --- /dev/null +++ b/Api/src/main/java/allchive/server/api/auth/service/TokenRefreshUseCase.java @@ -0,0 +1,28 @@ +package allchive.server.api.auth.service; + + +import allchive.server.api.auth.model.dto.response.OauthRegisterResponse; +import allchive.server.api.auth.service.helper.TokenGenerateHelper; +import allchive.server.core.annotation.UseCase; +import allchive.server.core.jwt.JwtTokenProvider; +import allchive.server.domain.domains.user.adaptor.RefreshTokenAdaptor; +import allchive.server.domain.domains.user.adaptor.UserAdaptor; +import allchive.server.domain.domains.user.domain.RefreshTokenEntity; +import allchive.server.domain.domains.user.domain.User; +import lombok.RequiredArgsConstructor; + +@UseCase +@RequiredArgsConstructor +public class TokenRefreshUseCase { + private final RefreshTokenAdaptor refreshTokenAdaptor; + private final JwtTokenProvider jwtTokenProvider; + private final UserAdaptor userAdaptor; + private final TokenGenerateHelper tokenGenerateHelper; + + public OauthRegisterResponse execute(String refreshToken) { + RefreshTokenEntity oldToken = refreshTokenAdaptor.findTokenByRefreshToken(refreshToken); + Long userId = jwtTokenProvider.parseRefreshToken(oldToken.getRefreshToken()); + User user = userAdaptor.findById(userId); + return OauthRegisterResponse.from(tokenGenerateHelper.execute(user)); + } +} diff --git a/Api/src/main/java/allchive/server/api/auth/service/WithdrawUserUseCase.java b/Api/src/main/java/allchive/server/api/auth/service/WithdrawUserUseCase.java new file mode 100644 index 00000000..9e10da37 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/auth/service/WithdrawUserUseCase.java @@ -0,0 +1,78 @@ +package allchive.server.api.auth.service; + + +import allchive.server.api.auth.service.helper.OauthHelper; +import allchive.server.api.config.security.SecurityUtil; +import allchive.server.core.annotation.UseCase; +import allchive.server.core.error.exception.InvalidOauthProviderException; +import allchive.server.domain.domains.archiving.adaptor.ArchivingAdaptor; +import allchive.server.domain.domains.archiving.domain.Archiving; +import allchive.server.domain.domains.block.service.BlockDomainService; +import allchive.server.domain.domains.content.adaptor.TagAdaptor; +import allchive.server.domain.domains.content.domain.Tag; +import allchive.server.domain.domains.content.service.ContentDomainService; +import allchive.server.domain.domains.content.service.ContentTagGroupDomainService; +import allchive.server.domain.domains.content.service.TagDomainService; +import allchive.server.domain.domains.recycle.service.RecycleDomainService; +import allchive.server.domain.domains.report.service.ReportDomainService; +import allchive.server.domain.domains.search.service.LatestSearchDomainService; +import allchive.server.domain.domains.user.adaptor.RefreshTokenAdaptor; +import allchive.server.domain.domains.user.adaptor.UserAdaptor; +import allchive.server.domain.domains.user.domain.User; +import allchive.server.domain.domains.user.domain.enums.OauthProvider; +import allchive.server.domain.domains.user.service.ScrapDomainService; +import allchive.server.domain.domains.user.service.UserDomainService; +import java.util.List; +import lombok.RequiredArgsConstructor; + +@UseCase +@RequiredArgsConstructor +public class WithdrawUserUseCase { + private final UserAdaptor userAdaptor; + private final OauthHelper oauthHelper; + private final RefreshTokenAdaptor refreshTokenAdaptor; + private final LatestSearchDomainService latestSearchDomainService; + private final ScrapDomainService scrapDomainService; + private final BlockDomainService blockDomainService; + private final ArchivingAdaptor archivingAdaptor; + private final TagAdaptor tagAdaptor; + private final ContentTagGroupDomainService contentTagGroupDomainService; + private final ContentDomainService contentDomainService; + private final TagDomainService tagDomainService; + private final RecycleDomainService recycleDomainService; + private final ReportDomainService reportDomainService; + private final UserDomainService userDomainService; + + public void execute(OauthProvider provider, String appleAccessToken) { + Long userId = SecurityUtil.getCurrentUserId(); + User user = userAdaptor.findById(userId); + // oauth쪽 탈퇴 + withdrawOauth(provider, appleAccessToken, user); + // 우리쪽 탈퇴 + withdrawService(userId, user); + } + + private void withdrawOauth(OauthProvider provider, String appleAccessToken, User user) { + switch (provider) { + case KAKAO -> oauthHelper.withdraw(provider, user.getOauthInfo().getOid(), null); + case APPLE -> oauthHelper.withdraw(provider, null, appleAccessToken); + default -> throw InvalidOauthProviderException.EXCEPTION; + } + } + + private void withdrawService(Long userId, User user) { + refreshTokenAdaptor.deleteTokenByUserId(userId); + latestSearchDomainService.deleteAllByUserId(userId); + scrapDomainService.deleteAllByUser(user); + blockDomainService.queryDeleteBlockByBlockFromOrBlockUser(userId); + List archivingList = archivingAdaptor.findAllByUserId(userId); + List archivingId = archivingList.stream().map(Archiving::getId).toList(); + List tagList = tagAdaptor.findAllByUserId(userId); + contentTagGroupDomainService.deleteAllByTagIn(tagList); + tagDomainService.deleteAll(tagList); + contentDomainService.deleteAllByArchivingIdIn(archivingId); + recycleDomainService.deleteAllByUserId(userId); + reportDomainService.deleteAllByReportedUserId(userId); + userDomainService.deleteUserById(userId); + } +} diff --git a/Api/src/main/java/allchive/server/api/auth/service/helper/AppleOauthHelper.java b/Api/src/main/java/allchive/server/api/auth/service/helper/AppleOauthHelper.java new file mode 100644 index 00000000..5bdb06e0 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/auth/service/helper/AppleOauthHelper.java @@ -0,0 +1,93 @@ +package allchive.server.api.auth.service.helper; + +import static allchive.server.core.consts.AllchiveConst.APPLE_OAUTH_QUERY_STRING; + +import allchive.server.core.annotation.Helper; +import allchive.server.core.dto.OIDCDecodePayload; +import allchive.server.core.properties.AppleOAuthProperties; +import allchive.server.domain.domains.user.domain.enums.OauthInfo; +import allchive.server.domain.domains.user.domain.enums.OauthProvider; +import allchive.server.infrastructure.oauth.apple.client.AppleOAuthClient; +import allchive.server.infrastructure.oauth.apple.client.AppleOIDCClient; +import allchive.server.infrastructure.oauth.apple.helper.AppleLoginUtil; +import allchive.server.infrastructure.oauth.apple.response.AppleTokenResponse; +import allchive.server.infrastructure.oauth.kakao.dto.OIDCPublicKeysResponse; +import lombok.RequiredArgsConstructor; + +@Helper +@RequiredArgsConstructor +public class AppleOauthHelper { + private final AppleOAuthProperties appleOAuthProperties; + private final AppleOAuthClient appleOAuthClient; + private final AppleOIDCClient appleOIDCClient; + private final OauthOIDCHelper oAuthOIDCHelper; + + /** Link * */ + public String getAppleOAuthLink(String referer) { + return appleOAuthProperties.getBaseUrl() + + String.format( + APPLE_OAUTH_QUERY_STRING, + appleOAuthProperties.getClientId(), + referer + appleOAuthProperties.getWebCallbackUrl()); + } + + public String getAppleOauthLinkDev() { + return appleOAuthProperties.getBaseUrl() + + String.format( + APPLE_OAUTH_QUERY_STRING, + appleOAuthProperties.getClientId(), + appleOAuthProperties.getRedirectUrl()); + } + + /** token * */ + public AppleTokenResponse getAppleOAuthToken(String code, String referer) { + return appleOAuthClient.appleAuth( + appleOAuthProperties.getClientId(), + referer + appleOAuthProperties.getWebCallbackUrl(), + code, + this.getClientSecret()); + } + + public AppleTokenResponse getAppleOAuthTokenDev(String code) { + return appleOAuthClient.appleAuth( + appleOAuthProperties.getClientId(), + appleOAuthProperties.getRedirectUrl(), + code, + this.getClientSecret()); + } + + /** idtoken 분석 * */ + public OauthInfo getAppleOAuthInfoByIdToken(String idToken) { + OIDCDecodePayload oidcDecodePayload = this.getOIDCDecodePayload(idToken); + return OauthInfo.builder() + .provider(OauthProvider.APPLE) + .oid(oidcDecodePayload.getSub()) + .build(); + } + + /** oidc decode * */ + public OIDCDecodePayload getOIDCDecodePayload(String token) { + OIDCPublicKeysResponse oidcPublicKeysResponse = appleOIDCClient.getAppleOIDCOpenKeys(); + return oAuthOIDCHelper.getPayloadFromIdToken( + token, + appleOAuthProperties.getBaseUrl(), + appleOAuthProperties.getClientId(), + oidcPublicKeysResponse); + } + + /** apple측 회원 탈퇴 * */ + public void withdrawAppleOauthUser(String appleOAuthAccessToken) { + appleOAuthClient.revoke( + appleOAuthProperties.getClientId(), appleOAuthAccessToken, this.getClientSecret()); + } + + /** client secret 가져오기 * */ + private String getClientSecret() { + return AppleLoginUtil.createClientSecret( + appleOAuthProperties.getTeamId(), + appleOAuthProperties.getClientId(), + appleOAuthProperties.getKeyId(), + appleOAuthProperties.getKeyPath(), + appleOAuthProperties.getBaseUrl()); + } +} diff --git a/Api/src/main/java/allchive/server/api/auth/service/helper/KakaoOauthHelper.java b/Api/src/main/java/allchive/server/api/auth/service/helper/KakaoOauthHelper.java new file mode 100644 index 00000000..14fe20ec --- /dev/null +++ b/Api/src/main/java/allchive/server/api/auth/service/helper/KakaoOauthHelper.java @@ -0,0 +1,91 @@ +package allchive.server.api.auth.service.helper; + +import static allchive.server.core.consts.AllchiveConst.KAKAO_OAUTH_QUERY_STRING; + +import allchive.server.core.annotation.Helper; +import allchive.server.core.dto.OIDCDecodePayload; +import allchive.server.core.properties.KakaoOAuthProperties; +import allchive.server.domain.domains.user.domain.enums.OauthInfo; +import allchive.server.domain.domains.user.domain.enums.OauthProvider; +import allchive.server.infrastructure.oauth.kakao.client.KakaoInfoClient; +import allchive.server.infrastructure.oauth.kakao.client.KakaoOauthClient; +import allchive.server.infrastructure.oauth.kakao.dto.KakaoTokenResponse; +import allchive.server.infrastructure.oauth.kakao.dto.KakaoUnlinkTarget; +import allchive.server.infrastructure.oauth.kakao.dto.OIDCPublicKeysResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Helper +@Slf4j +@RequiredArgsConstructor +public class KakaoOauthHelper { + private final KakaoOAuthProperties kakaoOauthProperties; + private final KakaoInfoClient kakaoInfoClient; + private final KakaoOauthClient kakaoOauthClient; + private final OauthOIDCHelper oauthOIDCHelper; + + /** link * */ + public String getKaKaoOauthLink(String referer) { + // TODO : 프론트 콜백 URL 알아내면 바꾸기 + return kakaoOauthProperties.getBaseUrl() + + String.format( + KAKAO_OAUTH_QUERY_STRING, + kakaoOauthProperties.getClientId(), + referer + "kakao/callback"); + } + + public String getKaKaoOauthLinkDev() { + return kakaoOauthProperties.getBaseUrl() + + String.format( + KAKAO_OAUTH_QUERY_STRING, + kakaoOauthProperties.getClientId(), + kakaoOauthProperties.getRedirectUrl()); + } + + /** token * */ + public KakaoTokenResponse getKakaoOauthToken(String code, String referer) { + // TODO : 프론트 콜백 URL 알아내면 바꾸기 + return kakaoOauthClient.kakaoAuth( + kakaoOauthProperties.getClientId(), + referer + "kakao/callback", + code, + kakaoOauthProperties.getClientSecret()); + } + + public KakaoTokenResponse getKakaoOauthTokenDev(String code) { + return kakaoOauthClient.kakaoAuth( + kakaoOauthProperties.getClientId(), + kakaoOauthProperties.getRedirectUrl(), + code, + kakaoOauthProperties.getClientSecret()); + } + + /** idtoken 분석 * */ + public OauthInfo getKakaoOauthInfoByIdToken(String idToken) { + OIDCDecodePayload oidcDecodePayload = getOIDCDecodePayload(idToken); + return OauthInfo.of(OauthProvider.KAKAO, oidcDecodePayload.getSub()); + } + + /** oidc decode * */ + public OIDCDecodePayload getOIDCDecodePayload(String token) { + OIDCPublicKeysResponse oidcPublicKeysResponse = kakaoOauthClient.getKakaoOIDCOpenKeys(); + return oauthOIDCHelper.getPayloadFromIdToken( + token, + kakaoOauthProperties.getBaseUrl(), + kakaoOauthProperties.getAppId(), + oidcPublicKeysResponse); + } + + /** kakao측 회원 탈퇴 * */ + public void withdrawKakaoOauthUser(String oid) { + String kakaoAdminKey = kakaoOauthProperties.getAdminKey(); + KakaoUnlinkTarget unlinkKaKaoTarget = KakaoUnlinkTarget.from(oid); + String header = "KakaoAK " + kakaoAdminKey; + log.info( + "{} {} {}", + header, + unlinkKaKaoTarget.getTargetIdType(), + unlinkKaKaoTarget.getAud()); + kakaoInfoClient.unlinkUser(header, unlinkKaKaoTarget); + } +} diff --git a/Api/src/main/java/allchive/server/api/auth/service/helper/OauthHelper.java b/Api/src/main/java/allchive/server/api/auth/service/helper/OauthHelper.java new file mode 100644 index 00000000..b76acbbb --- /dev/null +++ b/Api/src/main/java/allchive/server/api/auth/service/helper/OauthHelper.java @@ -0,0 +1,88 @@ +package allchive.server.api.auth.service.helper; + + +import allchive.server.api.auth.model.dto.response.OauthLoginLinkResponse; +import allchive.server.api.auth.model.dto.response.OauthTokenResponse; +import allchive.server.core.annotation.Helper; +import allchive.server.core.error.exception.InvalidOauthProviderException; +import allchive.server.domain.domains.user.domain.enums.OauthInfo; +import allchive.server.domain.domains.user.domain.enums.OauthProvider; +import lombok.RequiredArgsConstructor; + +@Helper +@RequiredArgsConstructor +public class OauthHelper { + private final KakaoOauthHelper kakaoOauthHelper; + private final AppleOauthHelper appleOauthHelper; + + /** oauth link 가져오기 * */ + public OauthLoginLinkResponse getOauthLinkDev(OauthProvider provider) { + switch (provider) { + case KAKAO: + return new OauthLoginLinkResponse(kakaoOauthHelper.getKaKaoOauthLinkDev()); + case APPLE: + return new OauthLoginLinkResponse(appleOauthHelper.getAppleOauthLinkDev()); + default: + throw InvalidOauthProviderException.EXCEPTION; + } + } + + public OauthLoginLinkResponse getOauthLink(OauthProvider provider, String referer) { + switch (provider) { + case KAKAO: + return new OauthLoginLinkResponse(kakaoOauthHelper.getKaKaoOauthLink(referer)); + case APPLE: + return new OauthLoginLinkResponse(appleOauthHelper.getAppleOAuthLink(referer)); + default: + throw InvalidOauthProviderException.EXCEPTION; + } + } + + /** idtoken 가져오기 * */ + public OauthTokenResponse getCredential(OauthProvider provider, String code, String referer) { + switch (provider) { + case KAKAO: + return OauthTokenResponse.from(kakaoOauthHelper.getKakaoOauthToken(code, referer)); + case APPLE: + return OauthTokenResponse.from(appleOauthHelper.getAppleOAuthToken(code, referer)); + default: + throw InvalidOauthProviderException.EXCEPTION; + } + } + + public OauthTokenResponse getCredentialDev(OauthProvider provider, String code) { + switch (provider) { + case KAKAO: + return OauthTokenResponse.from(kakaoOauthHelper.getKakaoOauthTokenDev(code)); + case APPLE: + return OauthTokenResponse.from(appleOauthHelper.getAppleOAuthTokenDev(code)); + default: + throw InvalidOauthProviderException.EXCEPTION; + } + } + + /** idtoken 분석 * */ + public OauthInfo getOauthInfo(OauthProvider provider, String idToken) { + switch (provider) { + case KAKAO: + return kakaoOauthHelper.getKakaoOauthInfoByIdToken(idToken); + case APPLE: + return appleOauthHelper.getAppleOAuthInfoByIdToken(idToken); + default: + throw InvalidOauthProviderException.EXCEPTION; + } + } + + /** 회원탈퇴 * */ + public void withdraw(OauthProvider provider, String oid, String appleAccessToken) { + switch (provider) { + case KAKAO: + kakaoOauthHelper.withdrawKakaoOauthUser(oid); + break; + case APPLE: + appleOauthHelper.withdrawAppleOauthUser(appleAccessToken); + default: + throw InvalidOauthProviderException.EXCEPTION; + } + } +} diff --git a/Api/src/main/java/allchive/server/api/auth/service/helper/OauthOIDCHelper.java b/Api/src/main/java/allchive/server/api/auth/service/helper/OauthOIDCHelper.java new file mode 100644 index 00000000..0e1dd043 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/auth/service/helper/OauthOIDCHelper.java @@ -0,0 +1,33 @@ +package allchive.server.api.auth.service.helper; + + +import allchive.server.core.annotation.Helper; +import allchive.server.core.dto.OIDCDecodePayload; +import allchive.server.core.jwt.JwtOIDCProvider; +import allchive.server.infrastructure.oauth.kakao.dto.OIDCPublicKeyDto; +import allchive.server.infrastructure.oauth.kakao.dto.OIDCPublicKeysResponse; +import lombok.RequiredArgsConstructor; + +@Helper +@RequiredArgsConstructor +public class OauthOIDCHelper { + private final JwtOIDCProvider jwtOIDCProvider; + + public OIDCDecodePayload getPayloadFromIdToken( + String token, String iss, String aud, OIDCPublicKeysResponse oidcPublicKeysResponse) { + String kid = getKidFromUnsignedIdToken(token, iss, aud); + + OIDCPublicKeyDto oidcPublicKeyDto = + oidcPublicKeysResponse.getKeys().stream() + .filter(o -> o.getKid().equals(kid)) + .findFirst() + .orElseThrow(); + + return jwtOIDCProvider.getOIDCTokenBody( + token, oidcPublicKeyDto.getN(), oidcPublicKeyDto.getE()); + } + + private String getKidFromUnsignedIdToken(String token, String iss, String aud) { + return jwtOIDCProvider.getKidFromUnsignedTokenHeader(token, iss, aud); + } +} diff --git a/Api/src/main/java/allchive/server/api/auth/service/helper/TokenGenerateHelper.java b/Api/src/main/java/allchive/server/api/auth/service/helper/TokenGenerateHelper.java new file mode 100644 index 00000000..23ed56db --- /dev/null +++ b/Api/src/main/java/allchive/server/api/auth/service/helper/TokenGenerateHelper.java @@ -0,0 +1,43 @@ +package allchive.server.api.auth.service.helper; + + +import allchive.server.api.auth.model.dto.response.OauthSignInResponse; +import allchive.server.core.annotation.Helper; +import allchive.server.core.jwt.JwtTokenProvider; +import allchive.server.domain.domains.user.adaptor.RefreshTokenAdaptor; +import allchive.server.domain.domains.user.domain.RefreshTokenEntity; +import allchive.server.domain.domains.user.domain.User; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +@Helper +@RequiredArgsConstructor +public class TokenGenerateHelper { + + private final JwtTokenProvider jwtTokenProvider; + + private final RefreshTokenAdaptor refreshTokenAdaptor; + + @Transactional + public OauthSignInResponse execute(User user) { + String newAccessToken = + jwtTokenProvider.generateAccessToken(user.getId(), user.getUserRole().getValue()); + String newRefreshToken = jwtTokenProvider.generateRefreshToken(user.getId()); + + RefreshTokenEntity newRefreshTokenEntity = + RefreshTokenEntity.builder() + .refreshToken(newRefreshToken) + .id(user.getId()) + .ttl(jwtTokenProvider.getRefreshTokenTTLSecond()) + .build(); + refreshTokenAdaptor.save(newRefreshTokenEntity); + + return OauthSignInResponse.of( + true, + null, + newAccessToken, + jwtTokenProvider.getAccessTokenTTLSecond(), + newRefreshToken, + jwtTokenProvider.getRefreshTokenTTLSecond()); + } +} diff --git a/Api/src/main/java/allchive/server/api/block/controller/BlockController.java b/Api/src/main/java/allchive/server/api/block/controller/BlockController.java new file mode 100644 index 00000000..6ad67635 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/block/controller/BlockController.java @@ -0,0 +1,43 @@ +package allchive.server.api.block.controller; + + +import allchive.server.api.block.model.dto.request.BlockRequest; +import allchive.server.api.block.model.dto.response.BlockResponse; +import allchive.server.api.block.model.dto.response.BlockUsersResponse; +import allchive.server.api.block.service.CreateBlockUseCase; +import allchive.server.api.block.service.DeleteBlockUseCase; +import allchive.server.api.block.service.GetBlockUseCase; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/blocks") +@RequiredArgsConstructor +@Slf4j +@Tag(name = "8. [block]") +public class BlockController { + private final CreateBlockUseCase createBlockUseCase; + private final DeleteBlockUseCase deleteBlockUseCase; + private final GetBlockUseCase getBlockUseCase; + + @Operation(summary = "유저를 차단합니다.") + @PostMapping() + public BlockResponse createBlock(@RequestBody BlockRequest blockRequest) { + return createBlockUseCase.execute(blockRequest); + } + + @Operation(summary = "유저 차단을 해제합니다.") + @DeleteMapping() + public BlockResponse deleteBlock(@RequestBody BlockRequest blockRequest) { + return deleteBlockUseCase.execute(blockRequest); + } + + @Operation(summary = "차단한 유저 정보를 가져옵니다.") + @GetMapping() + public BlockUsersResponse getBlock() { + return getBlockUseCase.execute(); + } +} diff --git a/Api/src/main/java/allchive/server/api/block/model/dto/request/BlockRequest.java b/Api/src/main/java/allchive/server/api/block/model/dto/request/BlockRequest.java new file mode 100644 index 00000000..f555cf73 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/block/model/dto/request/BlockRequest.java @@ -0,0 +1,11 @@ +package allchive.server.api.block.model.dto.request; + + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; + +@Getter +public class BlockRequest { + @Schema(defaultValue = "1", description = "차단할 유저의 id") + private Long userId; +} diff --git a/Api/src/main/java/allchive/server/api/block/model/dto/response/BlockResponse.java b/Api/src/main/java/allchive/server/api/block/model/dto/response/BlockResponse.java new file mode 100644 index 00000000..00ccbacf --- /dev/null +++ b/Api/src/main/java/allchive/server/api/block/model/dto/response/BlockResponse.java @@ -0,0 +1,21 @@ +package allchive.server.api.block.model.dto.response; + + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Getter; + +@Getter +public class BlockResponse { + @Schema(defaultValue = "이름", description = "차단한 유저의 닉네임") + private String nickname; + + @Builder + private BlockResponse(String nickname) { + this.nickname = nickname; + } + + public static BlockResponse from(String nickname) { + return BlockResponse.builder().nickname(nickname).build(); + } +} diff --git a/Api/src/main/java/allchive/server/api/block/model/dto/response/BlockUsersResponse.java b/Api/src/main/java/allchive/server/api/block/model/dto/response/BlockUsersResponse.java new file mode 100644 index 00000000..47553cd9 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/block/model/dto/response/BlockUsersResponse.java @@ -0,0 +1,21 @@ +package allchive.server.api.block.model.dto.response; + + +import allchive.server.api.block.model.vo.BlockUserVo; +import java.util.List; +import lombok.Builder; +import lombok.Getter; + +@Getter +public class BlockUsersResponse { + List users; + + @Builder + private BlockUsersResponse(List users) { + this.users = users; + } + + public static BlockUsersResponse from(List users) { + return BlockUsersResponse.builder().users(users).build(); + } +} diff --git a/Api/src/main/java/allchive/server/api/block/model/mapper/BlockMapper.java b/Api/src/main/java/allchive/server/api/block/model/mapper/BlockMapper.java new file mode 100644 index 00000000..547daf83 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/block/model/mapper/BlockMapper.java @@ -0,0 +1,29 @@ +package allchive.server.api.block.model.mapper; + + +import allchive.server.api.block.model.vo.BlockUserVo; +import allchive.server.core.annotation.Mapper; +import allchive.server.domain.domains.block.domain.Block; +import allchive.server.domain.domains.user.domain.User; +import java.util.List; + +@Mapper +public class BlockMapper { + public Block toEntity(Long blockFrom, Long blockUser) { + return Block.of(blockFrom, blockUser); + } + + public List toBlockUserVoList(List blockList, List users) { + return blockList.stream() + .map( + block -> { + User user = + users.stream() + .filter(u -> u.getId().equals(block.getBlockUser())) + .findFirst() + .orElseThrow(); + return BlockUserVo.of(user.getNickname(), block.getBlockUser()); + }) + .toList(); + } +} diff --git a/Api/src/main/java/allchive/server/api/block/model/vo/BlockUserVo.java b/Api/src/main/java/allchive/server/api/block/model/vo/BlockUserVo.java new file mode 100644 index 00000000..183694f5 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/block/model/vo/BlockUserVo.java @@ -0,0 +1,25 @@ +package allchive.server.api.block.model.vo; + + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Getter; + +@Getter +public class BlockUserVo { + @Schema(defaultValue = "닉네임", description = "차단한 유저의 닉네임") + private String nickname; + + @Schema(defaultValue = "1", description = "차단한 유저의 고유번호") + private Long id; + + @Builder + private BlockUserVo(String nickname, Long id) { + this.nickname = nickname; + this.id = id; + } + + public static BlockUserVo of(String nickname, Long id) { + return BlockUserVo.builder().nickname(nickname).id(id).build(); + } +} diff --git a/Api/src/main/java/allchive/server/api/block/service/CreateBlockUseCase.java b/Api/src/main/java/allchive/server/api/block/service/CreateBlockUseCase.java new file mode 100644 index 00000000..fed85f8a --- /dev/null +++ b/Api/src/main/java/allchive/server/api/block/service/CreateBlockUseCase.java @@ -0,0 +1,33 @@ +package allchive.server.api.block.service; + + +import allchive.server.api.block.model.dto.request.BlockRequest; +import allchive.server.api.block.model.dto.response.BlockResponse; +import allchive.server.api.block.model.mapper.BlockMapper; +import allchive.server.api.config.security.SecurityUtil; +import allchive.server.core.annotation.UseCase; +import allchive.server.domain.domains.block.domain.Block; +import allchive.server.domain.domains.block.service.BlockDomainService; +import allchive.server.domain.domains.block.validator.BlockValidator; +import allchive.server.domain.domains.user.adaptor.UserAdaptor; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +@UseCase +@RequiredArgsConstructor +public class CreateBlockUseCase { + private final BlockValidator blockValidator; + private final BlockMapper blockMapper; + private final BlockDomainService blockDomainService; + private final UserAdaptor userAdaptor; + + @Transactional + public BlockResponse execute(BlockRequest request) { + Long userId = SecurityUtil.getCurrentUserId(); + blockValidator.validateNotDuplicate(userId, request.getUserId()); + blockValidator.validateNotMyself(userId, request.getUserId()); + Block block = blockMapper.toEntity(userId, request.getUserId()); + blockDomainService.save(block); + return BlockResponse.from(userAdaptor.findById(request.getUserId()).getNickname()); + } +} diff --git a/Api/src/main/java/allchive/server/api/block/service/DeleteBlockUseCase.java b/Api/src/main/java/allchive/server/api/block/service/DeleteBlockUseCase.java new file mode 100644 index 00000000..257ad370 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/block/service/DeleteBlockUseCase.java @@ -0,0 +1,28 @@ +package allchive.server.api.block.service; + + +import allchive.server.api.block.model.dto.request.BlockRequest; +import allchive.server.api.block.model.dto.response.BlockResponse; +import allchive.server.api.config.security.SecurityUtil; +import allchive.server.core.annotation.UseCase; +import allchive.server.domain.domains.block.service.BlockDomainService; +import allchive.server.domain.domains.block.validator.BlockValidator; +import allchive.server.domain.domains.user.adaptor.UserAdaptor; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +@UseCase +@RequiredArgsConstructor +public class DeleteBlockUseCase { + private final BlockValidator blockValidator; + private final BlockDomainService blockDomainService; + private final UserAdaptor userAdaptor; + + @Transactional + public BlockResponse execute(BlockRequest request) { + Long userId = SecurityUtil.getCurrentUserId(); + blockValidator.validateExist(userId, request.getUserId()); + blockDomainService.deleteByBlockFromAndBlockUser(userId, request.getUserId()); + return BlockResponse.from(userAdaptor.findById(request.getUserId()).getNickname()); + } +} diff --git a/Api/src/main/java/allchive/server/api/block/service/GetBlockUseCase.java b/Api/src/main/java/allchive/server/api/block/service/GetBlockUseCase.java new file mode 100644 index 00000000..c0a4f3f2 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/block/service/GetBlockUseCase.java @@ -0,0 +1,31 @@ +package allchive.server.api.block.service; + + +import allchive.server.api.block.model.dto.response.BlockUsersResponse; +import allchive.server.api.block.model.mapper.BlockMapper; +import allchive.server.api.config.security.SecurityUtil; +import allchive.server.core.annotation.UseCase; +import allchive.server.domain.domains.block.adaptor.BlockAdaptor; +import allchive.server.domain.domains.block.domain.Block; +import allchive.server.domain.domains.user.adaptor.UserAdaptor; +import allchive.server.domain.domains.user.domain.User; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +@UseCase +@RequiredArgsConstructor +public class GetBlockUseCase { + private final BlockAdaptor blockAdaptor; + private final BlockMapper blockMapper; + private final UserAdaptor userAdaptor; + + @Transactional(readOnly = true) + public BlockUsersResponse execute() { + Long userId = SecurityUtil.getCurrentUserId(); + List blockList = blockAdaptor.findByBlockFrom(userId); + List userIds = blockList.stream().map(Block::getBlockUser).toList(); + List users = userAdaptor.findAllByIdIn(userIds); + return BlockUsersResponse.from(blockMapper.toBlockUserVoList(blockList, users)); + } +} diff --git a/Api/src/main/java/allchive/server/api/common/slice/SliceResponse.java b/Api/src/main/java/allchive/server/api/common/slice/SliceResponse.java new file mode 100644 index 00000000..a86cf861 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/common/slice/SliceResponse.java @@ -0,0 +1,15 @@ +package allchive.server.api.common.slice; + + +import java.util.List; +import org.springframework.data.domain.Slice; + +public record SliceResponse(List content, long page, int size, boolean hasNext) { + public static SliceResponse of(Slice slice) { + return new SliceResponse<>( + slice.getContent(), + slice.getNumber(), + slice.getNumberOfElements(), + slice.hasNext()); + } +} diff --git a/Api/src/main/java/allchive/server/api/common/util/UrlUtil.java b/Api/src/main/java/allchive/server/api/common/util/UrlUtil.java new file mode 100644 index 00000000..0500a20d --- /dev/null +++ b/Api/src/main/java/allchive/server/api/common/util/UrlUtil.java @@ -0,0 +1,37 @@ +package allchive.server.api.common.util; + +import static allchive.server.core.consts.AllchiveConst.*; + +import allchive.server.core.helper.SpringEnvironmentHelper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class UrlUtil { + private static SpringEnvironmentHelper springEnvironmentHelper; + + @Autowired + private UrlUtil(SpringEnvironmentHelper springEnvironmentHelper) { + UrlUtil.springEnvironmentHelper = springEnvironmentHelper; + } + + public static String toAssetUrl(String key) { + if (springEnvironmentHelper.isProdProfile()) { + return PROD_ASSET_URL + key; + } + return STAGING_ASSET_URL + key; + } + + public static String convertUrlToKey(String url) { + if (validateUrl(url)) { + return url.split("/", 4)[3]; + } + return url; + } + + private static Boolean validateUrl(String url) { + return url.contains(STAGING_ASSET_URL) + || url.contains(PROD_ASSET_URL) + || url.contains(S3_ASSET_URL); + } +} diff --git a/Api/src/main/java/allchive/server/api/config/GlobalExceptionHandler.java b/Api/src/main/java/allchive/server/api/config/GlobalExceptionHandler.java new file mode 100644 index 00000000..a1826b6a --- /dev/null +++ b/Api/src/main/java/allchive/server/api/config/GlobalExceptionHandler.java @@ -0,0 +1,114 @@ +package allchive.server.api.config; + +import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; + +import allchive.server.core.dto.ErrorReason; +import allchive.server.core.error.BaseDynamicException; +import allchive.server.core.error.BaseErrorException; +import allchive.server.core.error.ErrorResponse; +import allchive.server.core.error.GlobalErrorCode; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import javax.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.lang.Nullable; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { + @Override + protected ResponseEntity handleExceptionInternal( + Exception ex, + @Nullable Object body, + HttpHeaders headers, + HttpStatus statusCode, + WebRequest request) { + log.error("HandleInternalException", ex); + final HttpStatus status = (HttpStatus) statusCode; + final ErrorReason errorReason = + ErrorReason.of(status.value(), status.name(), ex.getMessage()); + final ErrorResponse errorResponse = ErrorResponse.from(errorReason); + return super.handleExceptionInternal(ex, errorResponse, headers, status, request); + } + + @Nullable + @Override + protected ResponseEntity handleMethodArgumentNotValid( + MethodArgumentNotValidException ex, + HttpHeaders headers, + HttpStatus status, + WebRequest request) { + final HttpStatus httpStatus = (HttpStatus) status; + final List errors = ex.getBindingResult().getFieldErrors(); + final Map fieldAndErrorMessages = + errors.stream() + .collect( + Collectors.toMap( + FieldError::getField, FieldError::getDefaultMessage)); + final String errorsToJsonString = + fieldAndErrorMessages.entrySet().stream() + .map(e -> e.getKey() + " : " + e.getValue()) + .collect(Collectors.joining("|")); + final ErrorReason errorReason = + ErrorReason.of(status.value(), httpStatus.name(), errorsToJsonString); + final ErrorResponse errorResponse = ErrorResponse.from(errorReason); + return ResponseEntity.status(HttpStatus.valueOf(errorReason.getStatus())) + .body(errorResponse); + } + + @Nullable + @Override + protected ResponseEntity handleHttpMessageNotReadable( + HttpMessageNotReadableException ex, + HttpHeaders headers, + HttpStatus status, + WebRequest request) { + log.error("HttpMessageNotReadableException", ex); + final GlobalErrorCode globalErrorCode = GlobalErrorCode.HTTP_MESSAGE_NOT_READABLE; + final ErrorReason errorReason = globalErrorCode.getErrorReason(); + final ErrorResponse errorResponse = ErrorResponse.from(errorReason); + return ResponseEntity.status(INTERNAL_SERVER_ERROR).body(errorResponse); + } + + // 비즈니스 로직 에러 처리 + @ExceptionHandler(BaseErrorException.class) + public ResponseEntity handleBaseErrorException( + BaseErrorException e, HttpServletRequest request) { + log.error("BaseErrorException", e); + final ErrorReason errorReason = e.getErrorCode().getErrorReason(); + final ErrorResponse errorResponse = ErrorResponse.from(errorReason); + return ResponseEntity.status(HttpStatus.valueOf(errorReason.getStatus())) + .body(errorResponse); + } + + // dynamic error 처리 + @ExceptionHandler(BaseDynamicException.class) + public ResponseEntity BaseDynamicExceptionHandler( + BaseDynamicException e, HttpServletRequest request) { + ErrorResponse errorResponse = + ErrorResponse.from(ErrorReason.of(e.getStatus(), e.getCode(), e.getMessage())); + return ResponseEntity.status(HttpStatus.valueOf(e.getStatus())).body(errorResponse); + } + + // 위에서 따로 처리하지 않은 에러를 모두 처리해줍니다. + @ExceptionHandler(Exception.class) + protected ResponseEntity handleException( + Exception e, HttpServletRequest request) { + log.error("Exception", e); + final GlobalErrorCode globalErrorCode = GlobalErrorCode._INTERNAL_SERVER_ERROR; + final ErrorReason errorReason = globalErrorCode.getErrorReason(); + final ErrorResponse errorResponse = ErrorResponse.from(errorReason); + return ResponseEntity.status(INTERNAL_SERVER_ERROR).body(errorResponse); + } +} diff --git a/Api/src/main/java/allchive/server/api/config/SchedulerConfig.java b/Api/src/main/java/allchive/server/api/config/SchedulerConfig.java new file mode 100644 index 00000000..a2b2c71b --- /dev/null +++ b/Api/src/main/java/allchive/server/api/config/SchedulerConfig.java @@ -0,0 +1,12 @@ +package allchive.server.api.config; + + +import allchive.server.api.SchedulerPackageLocation; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; + +@Configuration +@EnableScheduling +@EntityScan(basePackageClasses = SchedulerPackageLocation.class) +public class SchedulerConfig {} diff --git a/Api/src/main/java/allchive/server/api/config/SwaggerConfig.java b/Api/src/main/java/allchive/server/api/config/SwaggerConfig.java new file mode 100644 index 00000000..676a38d2 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/config/SwaggerConfig.java @@ -0,0 +1,52 @@ +package allchive.server.api.config; + + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.servers.Server; +import java.util.List; +import javax.servlet.ServletContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SwaggerConfig { + @Bean + public OpenAPI openAPI(ServletContext servletContext) { + String contextPath = servletContext.getContextPath(); + Server server = new Server().url(contextPath); + return new OpenAPI() + .servers(List.of(server)) + .components(authSetting()) + .info(swaggerInfo()) + .addSecurityItem(new SecurityRequirement().addList("access-token")); + } + + private Info swaggerInfo() { + License license = new License(); + license.setUrl("https://github.com/Central-MakeUs"); + license.setName("Allchive"); + + return new Info() + .version("v0.0.1") + .title("\"Allchive 서버 API문서\"") + .description("Allchive 서버의 API 문서 입니다.") + .license(license); + } + + private Components authSetting() { + return new Components() + .addSecuritySchemes( + "access-token", + new SecurityScheme() + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT") + .in(SecurityScheme.In.HEADER) + .name("Authorization")); + } +} diff --git a/Api/src/main/java/allchive/server/api/config/response/ResponseAdviser.java b/Api/src/main/java/allchive/server/api/config/response/ResponseAdviser.java new file mode 100644 index 00000000..a1424f25 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/config/response/ResponseAdviser.java @@ -0,0 +1,62 @@ +package allchive.server.api.config.response; + +import static allchive.server.core.consts.AllchiveConst.SwaggerPatterns; + +import javax.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.MethodParameter; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.http.server.ServletServerHttpRequest; +import org.springframework.http.server.ServletServerHttpResponse; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; +import org.springframework.web.util.ContentCachingRequestWrapper; + +@Slf4j +@RestControllerAdvice +public class ResponseAdviser implements ResponseBodyAdvice { + + @Override + public boolean supports( + MethodParameter returnType, Class> converterType) { + return true; + } + + @Override + public Object beforeBodyWrite( + Object body, + MethodParameter returnType, + MediaType selectedContentType, + Class selectedConverterType, + ServerHttpRequest request, + ServerHttpResponse response) { + HttpServletResponse servletResponse = + ((ServletServerHttpResponse) response).getServletResponse(); + ContentCachingRequestWrapper servletRequest = + new ContentCachingRequestWrapper( + ((ServletServerHttpRequest) request).getServletRequest()); + + for (String swaggerPattern : SwaggerPatterns) { + if (servletRequest.getRequestURL().toString().contains(swaggerPattern)) return body; + } + + HttpStatus resolve = HttpStatus.resolve(servletResponse.getStatus()); + + if (resolve == null) return body; + + if (resolve.is2xxSuccessful()) + return SuccessResponse.onSuccess(statusProvider(servletRequest.getMethod()), body); + + return body; + } + + private int statusProvider(String method) { + if (method.equals("POST")) return 201; + if (method.equals("DELETE")) return 204; + return 200; + } +} diff --git a/Api/src/main/java/allchive/server/api/config/response/SuccessResponse.java b/Api/src/main/java/allchive/server/api/config/response/SuccessResponse.java new file mode 100644 index 00000000..5c8d0ff1 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/config/response/SuccessResponse.java @@ -0,0 +1,35 @@ +package allchive.server.api.config.response; + + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Getter; + +@Getter +public class SuccessResponse { + @JsonProperty("status") + private int code; + + @JsonProperty("message") + private String message; + + @JsonProperty("data") + @JsonInclude(JsonInclude.Include.NON_NULL) + private T data; + + @Builder + private SuccessResponse(int code, String message, T data) { + this.code = code; + this.message = message; + this.data = data; + } + + public static SuccessResponse onSuccess(int code) { + return SuccessResponse.builder().code(code).message("요청에 성공하였습니다.").data(null).build(); + } + + public static SuccessResponse onSuccess(int code, T data) { + return SuccessResponse.builder().code(code).message("요청에 성공하였습니다.").data(data).build(); + } +} diff --git a/Api/src/main/java/allchive/server/api/config/security/AccessDeniedFilter.java b/Api/src/main/java/allchive/server/api/config/security/AccessDeniedFilter.java new file mode 100644 index 00000000..29b590a3 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/config/security/AccessDeniedFilter.java @@ -0,0 +1,59 @@ +package allchive.server.api.config.security; + +import static allchive.server.core.consts.AllchiveConst.SwaggerPatterns; + +import allchive.server.core.error.BaseErrorException; +import allchive.server.core.error.ErrorResponse; +import allchive.server.core.error.GlobalErrorCode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.stereotype.Component; +import org.springframework.util.PatternMatchUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +@RequiredArgsConstructor +@Component +public class AccessDeniedFilter extends OncePerRequestFilter { + + private final ObjectMapper objectMapper; + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + String servletPath = request.getServletPath(); + return PatternMatchUtils.simpleMatch(SwaggerPatterns, servletPath); + } + + @Override + protected void doFilterInternal( + HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + try { + filterChain.doFilter(request, response); + } catch (BaseErrorException e) { + handleException(response, getErrorResponse(e)); + } catch (AccessDeniedException e) { + ErrorResponse access_denied = + ErrorResponse.from(GlobalErrorCode.INVALID_ACCESS_TOKEN_ERROR.getErrorReason()); + handleException(response, access_denied); + } + } + + private ErrorResponse getErrorResponse(BaseErrorException e) { + return ErrorResponse.from(e.getErrorReason()); + } + + private void handleException(HttpServletResponse response, ErrorResponse errorResponse) + throws IOException { + response.setCharacterEncoding("UTF-8"); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setStatus(errorResponse.getStatus()); + response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); + } +} diff --git a/Api/src/main/java/allchive/server/api/config/security/AuthDetails.java b/Api/src/main/java/allchive/server/api/config/security/AuthDetails.java new file mode 100644 index 00000000..345655fc --- /dev/null +++ b/Api/src/main/java/allchive/server/api/config/security/AuthDetails.java @@ -0,0 +1,54 @@ +package allchive.server.api.config.security; + + +import java.util.Collection; +import java.util.Collections; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +@AllArgsConstructor +@Getter +public class AuthDetails implements UserDetails { + + private String userId; + + private String role; + + @Override + public Collection getAuthorities() { + return Collections.singleton(new SimpleGrantedAuthority("ROLE_" + role)); + } + + @Override + public String getPassword() { + return null; + } + + @Override + public String getUsername() { + return userId; + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } +} diff --git a/Api/src/main/java/allchive/server/api/config/security/CorsConfig.java b/Api/src/main/java/allchive/server/api/config/security/CorsConfig.java new file mode 100644 index 00000000..6eba6a95 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/config/security/CorsConfig.java @@ -0,0 +1,31 @@ +package allchive.server.api.config.security; + + +import allchive.server.core.helper.SpringEnvironmentHelper; +import java.util.ArrayList; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +@RequiredArgsConstructor +public class CorsConfig implements WebMvcConfigurer { + private final SpringEnvironmentHelper springEnvironmentHelper; + + @Override + public void addCorsMappings(CorsRegistry registry) { + ArrayList allowedOriginPatterns = new ArrayList<>(); + if (springEnvironmentHelper.isDevProfile()) { + allowedOriginPatterns.add("http://localhost:3000"); + } + allowedOriginPatterns.add("https://www.allchive.co.kr"); + allowedOriginPatterns.add("https://staging.allchive.co.kr"); + String[] patterns = allowedOriginPatterns.toArray(String[]::new); + registry.addMapping("/**") + .allowedMethods("*") + .allowedOriginPatterns(patterns) + .exposedHeaders("Set-Cookie") + .allowCredentials(true); + } +} diff --git a/Api/src/main/java/allchive/server/api/config/security/FilterConfig.java b/Api/src/main/java/allchive/server/api/config/security/FilterConfig.java new file mode 100644 index 00000000..965e2dfa --- /dev/null +++ b/Api/src/main/java/allchive/server/api/config/security/FilterConfig.java @@ -0,0 +1,26 @@ +package allchive.server.api.config.security; + + +import lombok.RequiredArgsConstructor; +import org.springframework.security.config.annotation.SecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.DefaultSecurityFilterChain; +import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class FilterConfig + extends SecurityConfigurerAdapter { + private final JwtTokenFilter jwtTokenFilter; + private final AccessDeniedFilter accessDeniedFilter; + private final JwtExceptionFilter jwtExceptionFilter; + + @Override + public void configure(HttpSecurity builder) { + builder.addFilterBefore(jwtTokenFilter, BasicAuthenticationFilter.class); + builder.addFilterBefore(jwtExceptionFilter, JwtTokenFilter.class); + builder.addFilterBefore(accessDeniedFilter, FilterSecurityInterceptor.class); + } +} diff --git a/Api/src/main/java/allchive/server/api/config/security/JwtExceptionFilter.java b/Api/src/main/java/allchive/server/api/config/security/JwtExceptionFilter.java new file mode 100644 index 00000000..173455a9 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/config/security/JwtExceptionFilter.java @@ -0,0 +1,44 @@ +package allchive.server.api.config.security; + + +import allchive.server.core.error.BaseErrorException; +import allchive.server.core.error.ErrorResponse; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +@RequiredArgsConstructor +@Component +public class JwtExceptionFilter extends OncePerRequestFilter { + private final ObjectMapper objectMapper; + + @Override + protected void doFilterInternal( + HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + try { + filterChain.doFilter(request, response); + } catch (BaseErrorException e) { + responseToClient(response, getErrorResponse(e)); + } + } + + private ErrorResponse getErrorResponse(BaseErrorException e) { + return ErrorResponse.from(e.getErrorReason()); + } + + private void responseToClient(HttpServletResponse response, ErrorResponse errorResponse) + throws IOException { + response.setCharacterEncoding("UTF-8"); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setStatus(errorResponse.getStatus()); + response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); + } +} diff --git a/Api/src/main/java/allchive/server/api/config/security/JwtTokenFilter.java b/Api/src/main/java/allchive/server/api/config/security/JwtTokenFilter.java new file mode 100644 index 00000000..719b11e2 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/config/security/JwtTokenFilter.java @@ -0,0 +1,58 @@ +package allchive.server.api.config.security; + +import static allchive.server.core.consts.AllchiveConst.AUTH_HEADER; +import static allchive.server.core.consts.AllchiveConst.BEARER; + +import allchive.server.core.jwt.JwtTokenProvider; +import allchive.server.core.jwt.dto.AccessTokenInfo; +import java.io.IOException; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +@RequiredArgsConstructor +@Component +public class JwtTokenFilter extends OncePerRequestFilter { + private final JwtTokenProvider jwtTokenProvider; + + @Override + protected void doFilterInternal( + HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + String token = resolveToken(request); + + if (token != null) { + Authentication authentication = getAuthentication(token); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + filterChain.doFilter(request, response); + } + + private String resolveToken(HttpServletRequest request) { + String rawHeader = request.getHeader(AUTH_HEADER); + + if (rawHeader != null + && rawHeader.length() > BEARER.length() + && rawHeader.startsWith(BEARER)) { + return rawHeader.substring(BEARER.length()); + } + return null; + } + + public Authentication getAuthentication(String token) { + AccessTokenInfo accessTokenInfo = jwtTokenProvider.parseAccessToken(token); + + UserDetails userDetails = + new AuthDetails(accessTokenInfo.getUserId().toString(), accessTokenInfo.getRole()); + return new UsernamePasswordAuthenticationToken( + userDetails, "user", userDetails.getAuthorities()); + } +} diff --git a/Api/src/main/java/allchive/server/api/config/security/SecurityConfig.java b/Api/src/main/java/allchive/server/api/config/security/SecurityConfig.java new file mode 100644 index 00000000..cfd71a98 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/config/security/SecurityConfig.java @@ -0,0 +1,68 @@ +package allchive.server.api.config.security; + +import static allchive.server.core.consts.AllchiveConst.SwaggerPatterns; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler; + +@RequiredArgsConstructor +@EnableWebSecurity() +public class SecurityConfig { + private final FilterConfig filterConfig; + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(8); + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http.formLogin().disable().cors().and().csrf().disable(); + + http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); + http.authorizeRequests().expressionHandler(expressionHandler()); + + http.authorizeRequests() + .antMatchers(SwaggerPatterns) + .permitAll() + .mvcMatchers("/auth/oauth/**") + .permitAll() + .mvcMatchers("/user/nickname") + .permitAll() + .mvcMatchers("/auth/token/refresh") + .permitAll() + .mvcMatchers("/**/health/**") + .permitAll() + .mvcMatchers("/example/**") + .permitAll() + .anyRequest() + .hasRole("USER"); + + http.apply(filterConfig); + + return http.build(); + } + + @Bean + public RoleHierarchyImpl roleHierarchy() { + RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl(); + roleHierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER"); + return roleHierarchy; + } + + @Bean + public DefaultWebSecurityExpressionHandler expressionHandler() { + DefaultWebSecurityExpressionHandler expressionHandler = + new DefaultWebSecurityExpressionHandler(); + expressionHandler.setRoleHierarchy(roleHierarchy()); + return expressionHandler; + } +} diff --git a/Api/src/main/java/allchive/server/api/config/security/SecurityUtil.java b/Api/src/main/java/allchive/server/api/config/security/SecurityUtil.java new file mode 100644 index 00000000..bf9b4faa --- /dev/null +++ b/Api/src/main/java/allchive/server/api/config/security/SecurityUtil.java @@ -0,0 +1,16 @@ +package allchive.server.api.config.security; + + +import allchive.server.core.error.exception.SecurityContextNotFoundException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; + +public class SecurityUtil { + public static Long getCurrentUserId() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null) { + throw SecurityContextNotFoundException.EXCEPTION; + } + return Long.valueOf(authentication.getName()); + } +} diff --git a/Api/src/main/java/allchive/server/api/content/controller/ContentController.java b/Api/src/main/java/allchive/server/api/content/controller/ContentController.java new file mode 100644 index 00000000..284e286e --- /dev/null +++ b/Api/src/main/java/allchive/server/api/content/controller/ContentController.java @@ -0,0 +1,52 @@ +package allchive.server.api.content.controller; + + +import allchive.server.api.content.model.dto.request.CreateContentRequest; +import allchive.server.api.content.model.dto.request.UpdateContentRequest; +import allchive.server.api.content.model.dto.response.ContentTagResponse; +import allchive.server.api.content.service.CreateContentUseCase; +import allchive.server.api.content.service.DeleteContentUseCase; +import allchive.server.api.content.service.GetContentUseCase; +import allchive.server.api.content.service.UpdateContentUseCase; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/contents") +@RequiredArgsConstructor +@Slf4j +@Tag(name = "4. [content]") +public class ContentController { + private final CreateContentUseCase createContentUseCase; + private final GetContentUseCase getContentUseCase; + private final DeleteContentUseCase deleteContentUseCase; + private final UpdateContentUseCase updateContentUseCase; + + @Operation(summary = "컨텐츠를 생성합니다.") + @PostMapping() + public void createContent(@RequestBody CreateContentRequest createContentRequest) { + createContentUseCase.execute(createContentRequest); + } + + @Operation(summary = "컨텐츠 내용을 가져옵니다.") + @GetMapping(value = "/{contentId}") + public ContentTagResponse createContent(@PathVariable Long contentId) { + return getContentUseCase.execute(contentId); + } + + @Operation(summary = "컨텐츠를 삭제합니다.") + @DeleteMapping(value = "/{contentId}") + public void deleteContent(@PathVariable Long contentId) { + deleteContentUseCase.execute(contentId); + } + + @Operation(summary = "컨텐츠를 수정합니다.") + @PatchMapping(value = "/{contentId}") + public void updateContent( + @PathVariable Long contentId, @RequestBody UpdateContentRequest request) { + updateContentUseCase.execute(contentId, request); + } +} diff --git a/Api/src/main/java/allchive/server/api/content/model/dto/request/CreateContentRequest.java b/Api/src/main/java/allchive/server/api/content/model/dto/request/CreateContentRequest.java new file mode 100644 index 00000000..ead4b71f --- /dev/null +++ b/Api/src/main/java/allchive/server/api/content/model/dto/request/CreateContentRequest.java @@ -0,0 +1,33 @@ +package allchive.server.api.content.model.dto.request; + + +import allchive.server.core.annotation.ValidEnum; +import allchive.server.domain.domains.content.domain.enums.ContentType; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; +import lombok.Getter; + +@Getter +public class CreateContentRequest { + @Schema(defaultValue = "image", description = "컨텐츠 타입") + @ValidEnum(target = ContentType.class) + private ContentType contentType; + + @Schema(defaultValue = "0", description = "아카이빙 고유번호") + private Long archivingId; + + @Schema(defaultValue = "제목", description = "제목") + private String title; + + @Schema(defaultValue = "링크", description = "링크") + private String link; + + @Schema(defaultValue = "이미지 url", description = "이미지 url") + private String imgUrl; + + @Schema(description = "태그 고유번호 리스트") + private List tagIds; + + @Schema(defaultValue = "메모", description = "메모") + private String memo; +} diff --git a/Api/src/main/java/allchive/server/api/content/model/dto/request/UpdateContentRequest.java b/Api/src/main/java/allchive/server/api/content/model/dto/request/UpdateContentRequest.java new file mode 100644 index 00000000..0298614e --- /dev/null +++ b/Api/src/main/java/allchive/server/api/content/model/dto/request/UpdateContentRequest.java @@ -0,0 +1,33 @@ +package allchive.server.api.content.model.dto.request; + + +import allchive.server.core.annotation.ValidEnum; +import allchive.server.domain.domains.content.domain.enums.ContentType; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; +import lombok.Getter; + +@Getter +public class UpdateContentRequest { + @Schema(defaultValue = "image", description = "컨텐츠 타입") + @ValidEnum(target = ContentType.class) + private ContentType contentType; + + @Schema(defaultValue = "0", description = "아카이빙 고유번호") + private Long archivingId; + + @Schema(defaultValue = "제목", description = "제목") + private String title; + + @Schema(defaultValue = "링크", description = "링크") + private String link; + + @Schema(defaultValue = "이미지 url", description = "이미지 url") + private String imgUrl; + + @Schema(description = "태그 고유번호 리스트") + private List tagIds; + + @Schema(defaultValue = "메모", description = "메모") + private String memo; +} diff --git a/Api/src/main/java/allchive/server/api/content/model/dto/response/ContentResponse.java b/Api/src/main/java/allchive/server/api/content/model/dto/response/ContentResponse.java new file mode 100644 index 00000000..dc7d27a7 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/content/model/dto/response/ContentResponse.java @@ -0,0 +1,72 @@ +package allchive.server.api.content.model.dto.response; + + +import allchive.server.api.common.util.UrlUtil; +import allchive.server.core.annotation.DateFormat; +import allchive.server.domain.domains.content.domain.Content; +import allchive.server.domain.domains.content.domain.enums.ContentType; +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDateTime; +import lombok.Builder; +import lombok.Getter; + +@Getter +public class ContentResponse { + @Schema(description = "컨텐츠 고유번호") + private Long contentId; + + @Schema(description = "컨텐츠 제목") + private String contentTitle; + + @Schema(description = "컨텐츠 종류") + private ContentType contentType; + + @Schema(defaultValue = "컨텐츠 링크", description = "컨텐츠 링크") + private String link; + + @Schema(defaultValue = "컨텐츠 이미지 url", description = "컨텐츠 이미지 url") + private String imgUrl; + + @Schema(defaultValue = "2023.07.02", description = "컨텐츠 생성일자") + @DateFormat + private LocalDateTime contentCreatedAt; + + @Schema(description = "컨텐츠 태그") + private String Tag; + + @Schema(description = "컨텐츠 태그 총 개수") + private Long TagCount; + + @Builder + private ContentResponse( + Long contentId, + String contentTitle, + ContentType contentType, + LocalDateTime contentCreatedAt, + String tag, + String link, + String imgUrl, + Long tagCount) { + this.contentId = contentId; + this.contentTitle = contentTitle; + this.contentType = contentType; + this.contentCreatedAt = contentCreatedAt; + this.link = link; + this.imgUrl = imgUrl; + Tag = tag; + TagCount = tagCount; + } + + public static ContentResponse of(Content content, String tag, Long tagCount) { + return ContentResponse.builder() + .contentId(content.getId()) + .contentTitle(content.getTitle()) + .contentType(content.getContentType()) + .link(content.getLinkUrl()) + .imgUrl(UrlUtil.toAssetUrl(content.getImageUrl())) + .contentCreatedAt(content.getCreatedAt()) + .tag(tag) + .tagCount(tagCount) + .build(); + } +} diff --git a/Api/src/main/java/allchive/server/api/content/model/dto/response/ContentTagResponse.java b/Api/src/main/java/allchive/server/api/content/model/dto/response/ContentTagResponse.java new file mode 100644 index 00000000..398b5259 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/content/model/dto/response/ContentTagResponse.java @@ -0,0 +1,83 @@ +package allchive.server.api.content.model.dto.response; + + +import allchive.server.api.common.util.UrlUtil; +import allchive.server.api.tag.model.dto.response.TagResponse; +import allchive.server.core.annotation.DateFormat; +import allchive.server.domain.domains.content.domain.Content; +import allchive.server.domain.domains.content.domain.enums.ContentType; +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDateTime; +import java.util.List; +import lombok.Builder; +import lombok.Getter; + +@Getter +public class ContentTagResponse { + @Schema(description = "컨텐츠 고유번호") + private Long contentId; + + @Schema(description = "컨텐츠 제목") + private String contentTitle; + + @Schema(description = "컨텐츠 종류") + private ContentType contentType; + + @Schema(description = "메모") + private String contentMemo; + + @Schema(defaultValue = "컨텐츠 링크", description = "컨텐츠 링크") + private String link; + + @Schema(defaultValue = "컨텐츠 이미지 url", description = "컨텐츠 이미지 url") + private String imgUrl; + + @Schema( + type = "string", + pattern = "yyyy.MM.dd", + defaultValue = "2023.07.02", + description = "컨텐츠 생성일자") + @DateFormat + private LocalDateTime contentCreatedAt; + + private List tagList; + + private Boolean isMine; + + @Builder + private ContentTagResponse( + Long contentId, + String contentTitle, + ContentType contentType, + String contentMemo, + String link, + String imgUrl, + LocalDateTime contentCreatedAt, + List tagList, + Boolean isMine) { + this.contentId = contentId; + this.contentTitle = contentTitle; + this.contentType = contentType; + this.contentMemo = contentMemo; + this.link = link; + this.imgUrl = imgUrl; + this.contentCreatedAt = contentCreatedAt; + this.tagList = tagList; + this.isMine = isMine; + } + + public static ContentTagResponse of( + Content content, List tagList, Boolean isMine) { + return ContentTagResponse.builder() + .contentId(content.getId()) + .contentTitle(content.getTitle()) + .contentType(content.getContentType()) + .contentMemo(content.getMemo()) + .link(content.getLinkUrl()) + .imgUrl(UrlUtil.toAssetUrl(content.getImageUrl())) + .contentCreatedAt(content.getCreatedAt()) + .tagList(tagList) + .isMine(isMine) + .build(); + } +} diff --git a/Api/src/main/java/allchive/server/api/content/model/mapper/ContentMapper.java b/Api/src/main/java/allchive/server/api/content/model/mapper/ContentMapper.java new file mode 100644 index 00000000..f4dda5ed --- /dev/null +++ b/Api/src/main/java/allchive/server/api/content/model/mapper/ContentMapper.java @@ -0,0 +1,50 @@ +package allchive.server.api.content.model.mapper; + + +import allchive.server.api.common.util.UrlUtil; +import allchive.server.api.content.model.dto.request.CreateContentRequest; +import allchive.server.api.content.model.dto.response.ContentResponse; +import allchive.server.api.content.model.dto.response.ContentTagResponse; +import allchive.server.api.tag.model.dto.response.TagResponse; +import allchive.server.core.annotation.Mapper; +import allchive.server.domain.domains.content.domain.Content; +import allchive.server.domain.domains.content.domain.ContentTagGroup; +import allchive.server.domain.domains.content.domain.Tag; +import java.util.List; + +@Mapper +public class ContentMapper { + public ContentResponse toContentResponse( + Content content, List contentTagGroupList) { + List tags = + contentTagGroupList.stream() + .filter(contentTagGroup -> contentTagGroup.getContent().equals(content)) + .toList(); + ContentTagGroup contentTagGroup = tags.stream().findFirst().orElse(null); + String tag = contentTagGroup == null ? null : contentTagGroup.getTag().getName(); + return ContentResponse.of(content, tag, (long) tags.size()); + } + + public Content toEntity(CreateContentRequest request) { + return Content.of( + request.getArchivingId(), + request.getContentType(), + UrlUtil.convertUrlToKey(request.getImgUrl()), + request.getLink(), + request.getTitle(), + request.getMemo()); + } + + public ContentTagResponse toContentTagResponse( + Content content, List contentTagGroupList, Boolean isMine) { + List tagResponseList = + contentTagGroupList.stream() + .map(contentTagGroup -> TagResponse.from(contentTagGroup.getTag())) + .toList(); + return ContentTagResponse.of(content, tagResponseList, isMine); + } + + public List toContentTagGroupEntityList(Content content, List tags) { + return tags.stream().map(tag -> ContentTagGroup.of(content, tag)).toList(); + } +} diff --git a/Api/src/main/java/allchive/server/api/content/service/CreateContentUseCase.java b/Api/src/main/java/allchive/server/api/content/service/CreateContentUseCase.java new file mode 100644 index 00000000..2afd7967 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/content/service/CreateContentUseCase.java @@ -0,0 +1,51 @@ +package allchive.server.api.content.service; + + +import allchive.server.api.config.security.SecurityUtil; +import allchive.server.api.content.model.dto.request.CreateContentRequest; +import allchive.server.api.content.model.mapper.ContentMapper; +import allchive.server.core.annotation.UseCase; +import allchive.server.domain.domains.archiving.validator.ArchivingValidator; +import allchive.server.domain.domains.content.adaptor.TagAdaptor; +import allchive.server.domain.domains.content.domain.Content; +import allchive.server.domain.domains.content.domain.ContentTagGroup; +import allchive.server.domain.domains.content.domain.Tag; +import allchive.server.domain.domains.content.service.ContentDomainService; +import allchive.server.domain.domains.content.service.ContentTagGroupDomainService; +import allchive.server.domain.domains.content.validator.TagValidator; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +@UseCase +@RequiredArgsConstructor +public class CreateContentUseCase { + private final ArchivingValidator archivingValidator; + private final ContentMapper contentMapper; + private final ContentDomainService contentDomainService; + private final TagValidator tagValidator; + private final TagAdaptor tagAdaptor; + private final ContentTagGroupDomainService contentTagGroupDomainService; + + @Transactional + public void execute(CreateContentRequest request) { + validateExecution(request); + Content content = contentMapper.toEntity(request); + createContentTagGroup(content, request.getTagIds()); + contentDomainService.save(content); + } + + private void validateExecution(CreateContentRequest request) { + Long userId = SecurityUtil.getCurrentUserId(); + archivingValidator.validateExistById(request.getArchivingId()); + archivingValidator.validateArchivingUser(request.getArchivingId(), userId); + tagValidator.validateExistTagsAndUser(request.getTagIds(), userId); + } + + private void createContentTagGroup(Content content, List tagIds) { + List tags = tagAdaptor.queryTagByTagIdIn(tagIds); + List contentTagGroupList = + contentMapper.toContentTagGroupEntityList(content, tags); + contentTagGroupDomainService.saveAll(contentTagGroupList); + } +} diff --git a/Api/src/main/java/allchive/server/api/content/service/DeleteContentUseCase.java b/Api/src/main/java/allchive/server/api/content/service/DeleteContentUseCase.java new file mode 100644 index 00000000..97c4043c --- /dev/null +++ b/Api/src/main/java/allchive/server/api/content/service/DeleteContentUseCase.java @@ -0,0 +1,40 @@ +package allchive.server.api.content.service; + + +import allchive.server.api.config.security.SecurityUtil; +import allchive.server.api.recycle.model.mapper.RecycleMapper; +import allchive.server.core.annotation.UseCase; +import allchive.server.domain.domains.content.service.ContentDomainService; +import allchive.server.domain.domains.content.validator.ContentValidator; +import allchive.server.domain.domains.recycle.domain.Recycle; +import allchive.server.domain.domains.recycle.domain.enums.RecycleType; +import allchive.server.domain.domains.recycle.service.RecycleDomainService; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +@UseCase +@RequiredArgsConstructor +public class DeleteContentUseCase { + private final ContentValidator contentValidator; + private final ContentDomainService contentDomainService; + private final RecycleMapper recycleMapper; + private final RecycleDomainService recycleDomainService; + + @Transactional + public void execute(Long contentId) { + Long userId = SecurityUtil.getCurrentUserId(); + validateExecution(contentId, userId); + contentDomainService.softDeleteById(contentId); + createRecycle(userId, contentId); + } + + private void validateExecution(Long contentId, Long userId) { + contentValidator.verifyUser(contentId, userId); + } + + private void createRecycle(Long userId, Long contentId) { + Recycle recycle = + recycleMapper.toContentRecycleEntity(userId, contentId, RecycleType.CONTENT); + recycleDomainService.save(recycle); + } +} diff --git a/Api/src/main/java/allchive/server/api/content/service/GetContentUseCase.java b/Api/src/main/java/allchive/server/api/content/service/GetContentUseCase.java new file mode 100644 index 00000000..e61fc1d7 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/content/service/GetContentUseCase.java @@ -0,0 +1,50 @@ +package allchive.server.api.content.service; + + +import allchive.server.api.config.security.SecurityUtil; +import allchive.server.api.content.model.dto.response.ContentTagResponse; +import allchive.server.api.content.model.mapper.ContentMapper; +import allchive.server.core.annotation.UseCase; +import allchive.server.domain.domains.archiving.adaptor.ArchivingAdaptor; +import allchive.server.domain.domains.archiving.domain.Archiving; +import allchive.server.domain.domains.archiving.validator.ArchivingValidator; +import allchive.server.domain.domains.content.adaptor.ContentAdaptor; +import allchive.server.domain.domains.content.adaptor.ContentTagGroupAdaptor; +import allchive.server.domain.domains.content.domain.Content; +import allchive.server.domain.domains.content.domain.ContentTagGroup; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +@UseCase +@RequiredArgsConstructor +public class GetContentUseCase { + private final ArchivingValidator archivingValidator; + private final ContentAdaptor contentAdaptor; + private final ContentTagGroupAdaptor contentTagGroupAdaptor; + private final ContentMapper contentMapper; + private final ArchivingAdaptor archivingAdaptor; + + @Transactional(readOnly = true) + public ContentTagResponse execute(Long contentId) { + Long userId = SecurityUtil.getCurrentUserId(); + Content content = contentAdaptor.findById(contentId); + validateExecution(content.getArchivingId(), userId); + List contentTagGroupList = + contentTagGroupAdaptor.queryContentTagGroupByContentWithTag(content); + Boolean isMine = calculateIsMine(content.getArchivingId(), userId); + return contentMapper.toContentTagResponse(content, contentTagGroupList, isMine); + } + + private void validateExecution(Long archivingId, Long userId) { + archivingValidator.validatePublicStatus(archivingId, userId); + } + + private Boolean calculateIsMine(Long archivingId, Long userId) { + Archiving archiving = archivingAdaptor.findById(archivingId); + if (archiving.getUserId().equals(userId)) { + return Boolean.TRUE; + } + return Boolean.FALSE; + } +} diff --git a/Api/src/main/java/allchive/server/api/content/service/UpdateContentUseCase.java b/Api/src/main/java/allchive/server/api/content/service/UpdateContentUseCase.java new file mode 100644 index 00000000..b2286199 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/content/service/UpdateContentUseCase.java @@ -0,0 +1,61 @@ +package allchive.server.api.content.service; + + +import allchive.server.api.common.util.UrlUtil; +import allchive.server.api.config.security.SecurityUtil; +import allchive.server.api.content.model.dto.request.UpdateContentRequest; +import allchive.server.api.content.model.mapper.ContentMapper; +import allchive.server.core.annotation.UseCase; +import allchive.server.domain.domains.content.adaptor.ContentAdaptor; +import allchive.server.domain.domains.content.adaptor.TagAdaptor; +import allchive.server.domain.domains.content.domain.Content; +import allchive.server.domain.domains.content.domain.ContentTagGroup; +import allchive.server.domain.domains.content.domain.Tag; +import allchive.server.domain.domains.content.service.ContentDomainService; +import allchive.server.domain.domains.content.service.ContentTagGroupDomainService; +import allchive.server.domain.domains.content.validator.ContentValidator; +import allchive.server.domain.domains.content.validator.TagValidator; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +@UseCase +@RequiredArgsConstructor +public class UpdateContentUseCase { + private final ContentValidator contentValidator; + private final TagValidator tagValidator; + private final ContentAdaptor contentAdaptor; + private final TagAdaptor tagAdaptor; + private final ContentMapper contentMapper; + private final ContentDomainService contentDomainService; + private final ContentTagGroupDomainService contentTagGroupDomainService; + + @Transactional + public void execute(Long contentId, UpdateContentRequest request) { + validateExecution(contentId, request); + regenerateContentTagGroup(contentId, request.getTagIds()); + contentDomainService.update( + contentId, + request.getContentType(), + request.getArchivingId(), + request.getLink(), + request.getMemo(), + UrlUtil.convertUrlToKey(request.getImgUrl()), + request.getTitle()); + } + + private void validateExecution(Long contentId, UpdateContentRequest request) { + Long userId = SecurityUtil.getCurrentUserId(); + contentValidator.verifyUser(contentId, userId); + tagValidator.validateExistTagsAndUser(request.getTagIds(), userId); + } + + private void regenerateContentTagGroup(Long contentId, List tagIds) { + Content content = contentAdaptor.findById(contentId); + contentTagGroupDomainService.deleteAllByContent(content); + List tags = tagAdaptor.queryTagByTagIdIn(tagIds); + List contentTagGroupList = + contentMapper.toContentTagGroupEntityList(content, tags); + contentTagGroupDomainService.saveAll(contentTagGroupList); + } +} diff --git a/Api/src/main/java/allchive/server/api/image/controller/ImageController.java b/Api/src/main/java/allchive/server/api/image/controller/ImageController.java new file mode 100644 index 00000000..8077a4d4 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/image/controller/ImageController.java @@ -0,0 +1,45 @@ +package allchive.server.api.image.controller; + + +import allchive.server.api.config.security.SecurityUtil; +import allchive.server.api.image.model.dto.response.ImageUrlResponse; +import allchive.server.infrastructure.s3.PresignedType; +import allchive.server.infrastructure.s3.S3PresignedUrlService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/") +@RequiredArgsConstructor +@Slf4j +@Tag(name = "a. [image]") +public class ImageController { + private final S3PresignedUrlService s3PresignedUrlService; + + @Operation(summary = "카테고리 관련 이미지 업로드 url 요청할수 있는 api 입니다.") + @GetMapping(value = "/archivings/image") + public ImageUrlResponse getArchivingPresignedUrl() { + Long userId = SecurityUtil.getCurrentUserId(); + return ImageUrlResponse.from( + s3PresignedUrlService.getPreSignedUrl(userId, PresignedType.ARCHIVING)); + } + + @Operation(summary = "컨텐츠 관련 이미지 업로드 url 요청할수 있는 api 입니다.") + @GetMapping(value = "/contents/image") + public ImageUrlResponse getContentPresignedUrl() { + Long userId = SecurityUtil.getCurrentUserId(); + return ImageUrlResponse.from( + s3PresignedUrlService.getPreSignedUrl(userId, PresignedType.CONTENT)); + } + + @Operation(summary = "컨텐츠 관련 이미지 업로드 url 요청할수 있는 api 입니다.") + @GetMapping(value = "/user/image") + public ImageUrlResponse getUserPresignedUrl() { + Long userId = SecurityUtil.getCurrentUserId(); + return ImageUrlResponse.from( + s3PresignedUrlService.getPreSignedUrl(userId, PresignedType.USER)); + } +} diff --git a/Api/src/main/java/allchive/server/api/image/model/dto/response/ImageUrlResponse.java b/Api/src/main/java/allchive/server/api/image/model/dto/response/ImageUrlResponse.java new file mode 100644 index 00000000..27d0bfb9 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/image/model/dto/response/ImageUrlResponse.java @@ -0,0 +1,22 @@ +package allchive.server.api.image.model.dto.response; + + +import allchive.server.infrastructure.s3.ImageUrlDto; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Getter; + +@Getter +public class ImageUrlResponse { + @Schema(defaultValue = "이미지 presigned url", description = "이미지 업로드에 사용") + private String url; + + @Builder + private ImageUrlResponse(String url) { + this.url = url; + } + + public static ImageUrlResponse from(ImageUrlDto imageUrlDto) { + return ImageUrlResponse.builder().url(imageUrlDto.getUrl()).build(); + } +} diff --git a/Api/src/main/java/allchive/server/api/recycle/controller/RecycleController.java b/Api/src/main/java/allchive/server/api/recycle/controller/RecycleController.java new file mode 100644 index 00000000..053f8a12 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/recycle/controller/RecycleController.java @@ -0,0 +1,43 @@ +package allchive.server.api.recycle.controller; + + +import allchive.server.api.recycle.model.dto.request.ClearDeletedObjectRequest; +import allchive.server.api.recycle.model.dto.request.RestoreDeletedObjectRequest; +import allchive.server.api.recycle.model.dto.response.DeletedObjectResponse; +import allchive.server.api.recycle.service.ClearDeletedObjectUseCase; +import allchive.server.api.recycle.service.GetDeletedObjectUseCase; +import allchive.server.api.recycle.service.RestoreDeletedObjectUseCase; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/recycles") +@RequiredArgsConstructor +@Slf4j +@Tag(name = "6. [recycle]") +public class RecycleController { + private final RestoreDeletedObjectUseCase restoreDeletedObjectUseCase; + private final GetDeletedObjectUseCase getDeletedObjectUseCase; + private final ClearDeletedObjectUseCase clearDeletedObjectUseCase; + + @Operation(summary = "삭제된 아카이빙, 컨텐츠를 복구합니다.") + @PatchMapping() + public void restoreDeletedObject(@RequestBody RestoreDeletedObjectRequest request) { + restoreDeletedObjectUseCase.execute(request); + } + + @Operation(summary = "삭제된 아카이빙, 컨텐츠를 가져옵니다.") + @GetMapping() + public DeletedObjectResponse getDeletedObject() { + return getDeletedObjectUseCase.execute(); + } + + @Operation(summary = "삭제된 아카이빙, 컨텐츠를 영구적으로 삭제합니다.") + @DeleteMapping() + public void clearDeletedObject(@RequestBody ClearDeletedObjectRequest request) { + clearDeletedObjectUseCase.execute(request); + } +} diff --git a/Api/src/main/java/allchive/server/api/recycle/model/dto/request/ClearDeletedObjectRequest.java b/Api/src/main/java/allchive/server/api/recycle/model/dto/request/ClearDeletedObjectRequest.java new file mode 100644 index 00000000..eca4c645 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/recycle/model/dto/request/ClearDeletedObjectRequest.java @@ -0,0 +1,16 @@ +package allchive.server.api.recycle.model.dto.request; + + +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; +import lombok.Getter; + +@Getter +public class ClearDeletedObjectRequest { + @ArraySchema(schema = @Schema(description = "영구 삭제할 아카이빙 id", type = "int", defaultValue = "1")) + private List archivingIds; + + @ArraySchema(schema = @Schema(description = "영구 삭제할 컨텐츠 id", type = "int", defaultValue = "1")) + private List contentIds; +} diff --git a/Api/src/main/java/allchive/server/api/recycle/model/dto/request/RestoreDeletedObjectRequest.java b/Api/src/main/java/allchive/server/api/recycle/model/dto/request/RestoreDeletedObjectRequest.java new file mode 100644 index 00000000..609bdb32 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/recycle/model/dto/request/RestoreDeletedObjectRequest.java @@ -0,0 +1,16 @@ +package allchive.server.api.recycle.model.dto.request; + + +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; +import lombok.Getter; + +@Getter +public class RestoreDeletedObjectRequest { + @ArraySchema(schema = @Schema(description = "복구할 아카이빙 id", type = "int", defaultValue = "1")) + private List archivingIds; + + @ArraySchema(schema = @Schema(description = "복구할 컨텐츠 id", type = "int", defaultValue = "1")) + private List contentIds; +} diff --git a/Api/src/main/java/allchive/server/api/recycle/model/dto/response/DeletedObjectResponse.java b/Api/src/main/java/allchive/server/api/recycle/model/dto/response/DeletedObjectResponse.java new file mode 100644 index 00000000..d5635cc0 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/recycle/model/dto/response/DeletedObjectResponse.java @@ -0,0 +1,26 @@ +package allchive.server.api.recycle.model.dto.response; + + +import allchive.server.api.archiving.model.dto.response.ArchivingResponse; +import allchive.server.api.content.model.dto.response.ContentResponse; +import java.util.List; +import lombok.Builder; +import lombok.Getter; + +@Getter +public class DeletedObjectResponse { + private List contents; + private List archivings; + + @Builder + private DeletedObjectResponse( + List contents, List archivings) { + this.contents = contents; + this.archivings = archivings; + } + + public static DeletedObjectResponse of( + List archivings, List contents) { + return DeletedObjectResponse.builder().archivings(archivings).contents(contents).build(); + } +} diff --git a/Api/src/main/java/allchive/server/api/recycle/model/mapper/RecycleMapper.java b/Api/src/main/java/allchive/server/api/recycle/model/mapper/RecycleMapper.java new file mode 100644 index 00000000..6cc87490 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/recycle/model/mapper/RecycleMapper.java @@ -0,0 +1,49 @@ +package allchive.server.api.recycle.model.mapper; + + +import allchive.server.api.archiving.model.dto.response.ArchivingResponse; +import allchive.server.api.content.model.dto.response.ContentResponse; +import allchive.server.api.content.model.mapper.ContentMapper; +import allchive.server.api.recycle.model.dto.response.DeletedObjectResponse; +import allchive.server.core.annotation.Mapper; +import allchive.server.domain.domains.archiving.domain.Archiving; +import allchive.server.domain.domains.content.domain.Content; +import allchive.server.domain.domains.content.domain.ContentTagGroup; +import allchive.server.domain.domains.recycle.domain.Recycle; +import allchive.server.domain.domains.recycle.domain.enums.RecycleType; +import java.util.List; +import lombok.RequiredArgsConstructor; + +@Mapper +@RequiredArgsConstructor +public class RecycleMapper { + private final ContentMapper contentMapper; + + public Recycle toContentRecycleEntity(Long userId, Long contentId, RecycleType type) { + return Recycle.of(type, contentId, null, userId); + } + + public Recycle toArchivingRecycleEntity(Long userId, Long archivingId) { + return Recycle.of(RecycleType.ARCHIVING, null, archivingId, userId); + } + + public DeletedObjectResponse toDeletedObjectResponse( + List archivingList, + Long userId, + List contents, + List contentTagGroups) { + List archivingResponses = + archivingList.stream() + .map( + archiving -> + ArchivingResponse.of( + archiving, + archiving.getPinUserId().contains(userId))) + .toList(); + List contentResponses = + contents.stream() + .map(content -> contentMapper.toContentResponse(content, contentTagGroups)) + .toList(); + return DeletedObjectResponse.of(archivingResponses, contentResponses); + } +} diff --git a/Api/src/main/java/allchive/server/api/recycle/service/ClearDeletedObjectUseCase.java b/Api/src/main/java/allchive/server/api/recycle/service/ClearDeletedObjectUseCase.java new file mode 100644 index 00000000..453ecc90 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/recycle/service/ClearDeletedObjectUseCase.java @@ -0,0 +1,57 @@ +package allchive.server.api.recycle.service; + + +import allchive.server.api.config.security.SecurityUtil; +import allchive.server.api.recycle.model.dto.request.ClearDeletedObjectRequest; +import allchive.server.core.annotation.UseCase; +import allchive.server.domain.domains.archiving.service.ArchivingDomainService; +import allchive.server.domain.domains.archiving.validator.ArchivingValidator; +import allchive.server.domain.domains.content.adaptor.ContentAdaptor; +import allchive.server.domain.domains.content.domain.Content; +import allchive.server.domain.domains.content.service.ContentDomainService; +import allchive.server.domain.domains.content.service.ContentTagGroupDomainService; +import allchive.server.domain.domains.content.validator.ContentValidator; +import allchive.server.domain.domains.recycle.service.RecycleDomainService; +import allchive.server.domain.domains.recycle.validator.RecycleValidator; +import allchive.server.domain.domains.user.service.ScrapDomainService; +import java.util.ArrayList; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +@UseCase +@RequiredArgsConstructor +public class ClearDeletedObjectUseCase { + private final RecycleValidator recycleValidator; + private final ArchivingValidator archivingValidator; + private final ContentValidator contentValidator; + private final ContentAdaptor contentAdaptor; + private final ContentTagGroupDomainService contentTagGroupDomainService; + private final ScrapDomainService scrapDomainService; + private final ArchivingDomainService archivingDomainService; + private final ContentDomainService contentDomainService; + private final RecycleDomainService recycleDomainService; + + // TODO: report 지우기 + @Transactional + public void execute(ClearDeletedObjectRequest request) { + Long userId = SecurityUtil.getCurrentUserId(); + recycleValidator.validateExist(request.getArchivingIds(), request.getContentIds(), userId); + archivingValidator.verifyUserInIdList(userId, request.getArchivingIds()); + contentValidator.verifyUserInIdList(userId, request.getContentIds()); + List contents = contentAdaptor.findAllByArchivingIds(request.getArchivingIds()); + List contentsId = contents.stream().map(Content::getId).toList(); + if (!request.getContentIds().isEmpty()) { + if (contentsId.isEmpty()) { + contentsId = new ArrayList<>(); + } + contentsId.addAll(request.getContentIds()); + } + scrapDomainService.deleteAllByArchivingIdIn(request.getArchivingIds()); + contentTagGroupDomainService.deleteByContentIn(contents); + contentDomainService.deleteAllById(contentsId); + archivingDomainService.deleteAllById(request.getArchivingIds()); + recycleDomainService.deleteAllByUserIdAndArchivingIdOrUserIdAndContentId( + request.getArchivingIds(), request.getContentIds(), userId); + } +} diff --git a/Api/src/main/java/allchive/server/api/recycle/service/ClearOldDeletedObjectUseCase.java b/Api/src/main/java/allchive/server/api/recycle/service/ClearOldDeletedObjectUseCase.java new file mode 100644 index 00000000..ed855e2f --- /dev/null +++ b/Api/src/main/java/allchive/server/api/recycle/service/ClearOldDeletedObjectUseCase.java @@ -0,0 +1,69 @@ +package allchive.server.api.recycle.service; + + +import allchive.server.core.annotation.UseCase; +import allchive.server.domain.domains.archiving.service.ArchivingDomainService; +import allchive.server.domain.domains.content.adaptor.ContentAdaptor; +import allchive.server.domain.domains.content.domain.Content; +import allchive.server.domain.domains.content.service.ContentDomainService; +import allchive.server.domain.domains.content.service.ContentTagGroupDomainService; +import allchive.server.domain.domains.recycle.adaptor.RecycleAdaptor; +import allchive.server.domain.domains.recycle.domain.Recycle; +import allchive.server.domain.domains.recycle.domain.enums.RecycleType; +import allchive.server.domain.domains.recycle.service.RecycleDomainService; +import allchive.server.domain.domains.user.service.ScrapDomainService; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@UseCase +@RequiredArgsConstructor +public class ClearOldDeletedObjectUseCase { + private final RecycleAdaptor recycleAdaptor; + private final ContentAdaptor contentAdaptor; + private final ContentTagGroupDomainService contentTagGroupDomainService; + private final ScrapDomainService scrapDomainService; + private final ArchivingDomainService archivingDomainService; + private final ContentDomainService contentDomainService; + private final RecycleDomainService recycleDomainService; + + /** 삭제 후 30일 지난 항목 제거 스케쥴러 매일 02:30에 수행 */ + @Scheduled(cron = "0 30 2 * * *") + @Transactional + public void executeSchedule() { + log.info("scheduler on"); + LocalDateTime deleteStandard = LocalDateTime.now().minusDays(30); + List recycles = recycleAdaptor.findAllByDeletedAtBefore(deleteStandard); + + List archivingIds = + recycles.stream() + .filter(recycle -> recycle.getRecycleType().equals(RecycleType.ARCHIVING)) + .map(Recycle::getArchivingId) + .toList(); + List contentIds = + recycles.stream() + .filter(recycle -> recycle.getRecycleType().equals(RecycleType.CONTENT)) + .map(Recycle::getContentId) + .toList(); + + if (contentIds.isEmpty()) { + contentIds = new ArrayList<>(); + } + List contents = contentAdaptor.findAllByArchivingIds(archivingIds); + for (Content content : contents) { + contentIds.add(content.getId()); + } + + scrapDomainService.deleteAllByArchivingIdIn(archivingIds); + contentTagGroupDomainService.deleteByContentIn(contents); + contentDomainService.deleteAllById(contentIds); + archivingDomainService.deleteAllById(archivingIds); + recycleDomainService.deleteAll(recycles); + log.info("scheduler off"); + } +} diff --git a/Api/src/main/java/allchive/server/api/recycle/service/GetDeletedObjectUseCase.java b/Api/src/main/java/allchive/server/api/recycle/service/GetDeletedObjectUseCase.java new file mode 100644 index 00000000..733b5dff --- /dev/null +++ b/Api/src/main/java/allchive/server/api/recycle/service/GetDeletedObjectUseCase.java @@ -0,0 +1,51 @@ +package allchive.server.api.recycle.service; + + +import allchive.server.api.config.security.SecurityUtil; +import allchive.server.api.recycle.model.dto.response.DeletedObjectResponse; +import allchive.server.api.recycle.model.mapper.RecycleMapper; +import allchive.server.core.annotation.UseCase; +import allchive.server.domain.domains.archiving.adaptor.ArchivingAdaptor; +import allchive.server.domain.domains.archiving.domain.Archiving; +import allchive.server.domain.domains.content.adaptor.ContentAdaptor; +import allchive.server.domain.domains.content.adaptor.ContentTagGroupAdaptor; +import allchive.server.domain.domains.content.domain.Content; +import allchive.server.domain.domains.content.domain.ContentTagGroup; +import allchive.server.domain.domains.recycle.adaptor.RecycleAdaptor; +import allchive.server.domain.domains.recycle.domain.Recycle; +import allchive.server.domain.domains.recycle.domain.enums.RecycleType; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +@UseCase +@RequiredArgsConstructor +public class GetDeletedObjectUseCase { + private final RecycleAdaptor recycleAdaptor; + private final ArchivingAdaptor archivingAdaptor; + private final ContentAdaptor contentAdaptor; + private final RecycleMapper recycleMapper; + private final ContentTagGroupAdaptor contentTagGroupAdaptor; + + @Transactional(readOnly = true) + public DeletedObjectResponse execute() { + Long userId = SecurityUtil.getCurrentUserId(); + List recycles = recycleAdaptor.findAllByUserId(userId); + List archivingIds = + recycles.stream() + .filter(recycle -> recycle.getRecycleType().equals(RecycleType.ARCHIVING)) + .map(Recycle::getArchivingId) + .toList(); + List contentIds = + recycles.stream() + .filter(recycle -> recycle.getRecycleType().equals(RecycleType.CONTENT)) + .map(Recycle::getContentId) + .toList(); + List archivings = archivingAdaptor.findAllByIdIn(archivingIds); + List contents = contentAdaptor.findAllByIdIn(contentIds); + List contentTagGroups = + contentTagGroupAdaptor.queryContentTagGroupByContentIn(contents); + return recycleMapper.toDeletedObjectResponse( + archivings, userId, contents, contentTagGroups); + } +} diff --git a/Api/src/main/java/allchive/server/api/recycle/service/RestoreDeletedObjectUseCase.java b/Api/src/main/java/allchive/server/api/recycle/service/RestoreDeletedObjectUseCase.java new file mode 100644 index 00000000..aa77aa15 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/recycle/service/RestoreDeletedObjectUseCase.java @@ -0,0 +1,37 @@ +package allchive.server.api.recycle.service; + + +import allchive.server.api.config.security.SecurityUtil; +import allchive.server.api.recycle.model.dto.request.RestoreDeletedObjectRequest; +import allchive.server.core.annotation.UseCase; +import allchive.server.domain.domains.archiving.service.ArchivingDomainService; +import allchive.server.domain.domains.archiving.validator.ArchivingValidator; +import allchive.server.domain.domains.content.service.ContentDomainService; +import allchive.server.domain.domains.content.validator.ContentValidator; +import allchive.server.domain.domains.recycle.service.RecycleDomainService; +import allchive.server.domain.domains.recycle.validator.RecycleValidator; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +@UseCase +@RequiredArgsConstructor +public class RestoreDeletedObjectUseCase { + private final RecycleValidator recycleValidator; + private final ArchivingValidator archivingValidator; + private final ContentValidator contentValidator; + private final ArchivingDomainService archivingDomainService; + private final ContentDomainService contentDomainService; + private final RecycleDomainService recycleDomainService; + + @Transactional + public void execute(RestoreDeletedObjectRequest request) { + Long userId = SecurityUtil.getCurrentUserId(); + recycleValidator.validateExist(request.getArchivingIds(), request.getContentIds(), userId); + archivingValidator.validateExistInIdList(request.getArchivingIds()); + contentValidator.validateExistInIdList(request.getContentIds()); + archivingDomainService.restoreInIdList(request.getArchivingIds()); + contentDomainService.restoreInIdList(request.getContentIds()); + recycleDomainService.deleteAllByUserIdAndArchivingIdOrUserIdAndContentId( + request.getArchivingIds(), request.getContentIds(), userId); + } +} diff --git a/Api/src/main/java/allchive/server/api/report/controller/ReportController.java b/Api/src/main/java/allchive/server/api/report/controller/ReportController.java new file mode 100644 index 00000000..ac3c0266 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/report/controller/ReportController.java @@ -0,0 +1,28 @@ +package allchive.server.api.report.controller; + + +import allchive.server.api.report.model.dto.request.CreateReportRequest; +import allchive.server.api.report.service.CreateReportUseCase; +import allchive.server.domain.domains.report.domain.enums.ReportObjectType; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/reports") +@RequiredArgsConstructor +@Slf4j +@Tag(name = "7. [report]") +public class ReportController { + private final CreateReportUseCase createReportUseCase; + + @Operation(summary = "아카이빙, 컨텐츠를 신고합니다.") + @PostMapping() + public void createReport( + @RequestParam("type") ReportObjectType type, + @RequestBody CreateReportRequest createReportRequest) { + createReportUseCase.execute(createReportRequest, type); + } +} diff --git a/Api/src/main/java/allchive/server/api/report/model/dto/request/CreateReportRequest.java b/Api/src/main/java/allchive/server/api/report/model/dto/request/CreateReportRequest.java new file mode 100644 index 00000000..de3d048f --- /dev/null +++ b/Api/src/main/java/allchive/server/api/report/model/dto/request/CreateReportRequest.java @@ -0,0 +1,20 @@ +package allchive.server.api.report.model.dto.request; + + +import allchive.server.core.annotation.ValidEnum; +import allchive.server.domain.domains.report.domain.enums.ReportedType; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; + +@Getter +public class CreateReportRequest { + @Schema(defaultValue = "기타기타기타기타기타", description = "사유 ('기타'에만 해당)") + private String reason; + + @Schema(defaultValue = "spam", description = "신고 종류") + @ValidEnum(target = ReportedType.class) + private ReportedType reportedType; + + @Schema(defaultValue = "1", description = "아카이빙, 컨텐츠 고유번호") + private Long id; +} diff --git a/Api/src/main/java/allchive/server/api/report/model/mapper/ReportMapper.java b/Api/src/main/java/allchive/server/api/report/model/mapper/ReportMapper.java new file mode 100644 index 00000000..68d61a63 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/report/model/mapper/ReportMapper.java @@ -0,0 +1,36 @@ +package allchive.server.api.report.model.mapper; + + +import allchive.server.api.report.model.dto.request.CreateReportRequest; +import allchive.server.core.annotation.Mapper; +import allchive.server.domain.domains.report.domain.Report; +import allchive.server.domain.domains.report.domain.enums.ReportObjectType; + +@Mapper +public class ReportMapper { + public Report toEntity( + CreateReportRequest request, ReportObjectType type, Long userId, Long reportedUserId) { + Report report = null; + switch (type) { + case CONTENT -> report = + Report.of( + type, + request.getReason(), + request.getReportedType(), + request.getId(), + null, + userId, + reportedUserId); + case ARCHIVING -> report = + Report.of( + type, + request.getReason(), + request.getReportedType(), + null, + request.getId(), + userId, + reportedUserId); + } + return report; + } +} diff --git a/Api/src/main/java/allchive/server/api/report/service/CreateReportUseCase.java b/Api/src/main/java/allchive/server/api/report/service/CreateReportUseCase.java new file mode 100644 index 00000000..07681e8b --- /dev/null +++ b/Api/src/main/java/allchive/server/api/report/service/CreateReportUseCase.java @@ -0,0 +1,49 @@ +package allchive.server.api.report.service; + + +import allchive.server.api.config.security.SecurityUtil; +import allchive.server.api.report.model.dto.request.CreateReportRequest; +import allchive.server.api.report.model.mapper.ReportMapper; +import allchive.server.core.annotation.UseCase; +import allchive.server.domain.domains.archiving.adaptor.ArchivingAdaptor; +import allchive.server.domain.domains.archiving.validator.ArchivingValidator; +import allchive.server.domain.domains.content.adaptor.ContentAdaptor; +import allchive.server.domain.domains.content.validator.ContentValidator; +import allchive.server.domain.domains.report.domain.Report; +import allchive.server.domain.domains.report.domain.enums.ReportObjectType; +import allchive.server.domain.domains.report.service.ReportDomainService; +import allchive.server.domain.domains.report.validator.ReportValidator; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +@UseCase +@RequiredArgsConstructor +public class CreateReportUseCase { + private final ReportValidator reportValidator; + private final ContentValidator contentValidator; + private final ContentAdaptor contentAdaptor; + private final ArchivingAdaptor archivingAdaptor; + private final ArchivingValidator archivingValidator; + private final ReportMapper reportMapper; + private final ReportDomainService reportDomainService; + + @Transactional + public void execute(CreateReportRequest request, ReportObjectType type) { + Long userId = SecurityUtil.getCurrentUserId(); + reportValidator.validateNotDuplicateReport(userId, request.getId(), type); + Long reportedUserId = 0L; + switch (type) { + case CONTENT -> { + contentValidator.validateExistById(request.getId()); + Long archivingId = contentAdaptor.findById(request.getId()).getArchivingId(); + reportedUserId = archivingAdaptor.findById(archivingId).getUserId(); + } + case ARCHIVING -> { + archivingValidator.validateExistById(request.getId()); + reportedUserId = archivingAdaptor.findById(request.getId()).getUserId(); + } + } + Report report = reportMapper.toEntity(request, type, userId, reportedUserId); + reportDomainService.save(report); + } +} diff --git a/Api/src/main/java/allchive/server/api/search/controller/SearchController.java b/Api/src/main/java/allchive/server/api/search/controller/SearchController.java new file mode 100644 index 00000000..d476f5a5 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/search/controller/SearchController.java @@ -0,0 +1,50 @@ +package allchive.server.api.search.controller; + + +import allchive.server.api.search.model.dto.request.SearchRequest; +import allchive.server.api.search.model.dto.response.SearchListResponse; +import allchive.server.api.search.model.dto.response.SearchResponse; +import allchive.server.api.search.model.enums.ArchivingType; +import allchive.server.api.search.service.GetLatestSearchListUseCase; +import allchive.server.api.search.service.GetRelativeSearchListUseCase; +import allchive.server.api.search.service.SearchArchivingUseCase; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.api.annotations.ParameterObject; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/searches") +@RequiredArgsConstructor +@Slf4j +@Tag(name = "9. [search]") +public class SearchController { + private final SearchArchivingUseCase searchArchivingUseCase; + private final GetLatestSearchListUseCase getLatestSearchListUseCase; + private final GetRelativeSearchListUseCase getRelativeSearchListUseCase; + + @Operation(summary = "검색어를 검색합니다.") + @PostMapping + public SearchResponse searchArchiving( + @ParameterObject @PageableDefault(size = 10) Pageable pageable, + @RequestParam("type") ArchivingType type, + @RequestBody SearchRequest request) { + return searchArchivingUseCase.execute(pageable, type, request); + } + + @Operation(summary = "최근 검색어 목록을 가져옵니다.", description = "5개만 드릴게요") + @GetMapping(value = "/latest") + public SearchListResponse getLatestSearchList() { + return getLatestSearchListUseCase.execute(); + } + + @Operation(summary = "검색어 자동 완성") + @PostMapping(value = "/relation") + public SearchListResponse getRelativeSearchList(@RequestBody SearchRequest request) { + return getRelativeSearchListUseCase.execute(request); + } +} diff --git a/Api/src/main/java/allchive/server/api/search/model/dto/request/SearchRequest.java b/Api/src/main/java/allchive/server/api/search/model/dto/request/SearchRequest.java new file mode 100644 index 00000000..84bfda69 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/search/model/dto/request/SearchRequest.java @@ -0,0 +1,11 @@ +package allchive.server.api.search.model.dto.request; + + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; + +@Getter +public class SearchRequest { + @Schema(defaultValue = "키워드", description = "검색 내용") + private String keyword; +} diff --git a/Api/src/main/java/allchive/server/api/search/model/dto/response/SearchListResponse.java b/Api/src/main/java/allchive/server/api/search/model/dto/response/SearchListResponse.java new file mode 100644 index 00000000..ac331af8 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/search/model/dto/response/SearchListResponse.java @@ -0,0 +1,22 @@ +package allchive.server.api.search.model.dto.response; + + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; +import lombok.Builder; +import lombok.Getter; + +@Getter +public class SearchListResponse { + @Schema(description = "검색어 목록") + private List keyword; + + @Builder + private SearchListResponse(List keyword) { + this.keyword = keyword; + } + + public static SearchListResponse from(List keywords) { + return SearchListResponse.builder().keyword(keywords).build(); + } +} diff --git a/Api/src/main/java/allchive/server/api/search/model/dto/response/SearchResponse.java b/Api/src/main/java/allchive/server/api/search/model/dto/response/SearchResponse.java new file mode 100644 index 00000000..7d9c7f78 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/search/model/dto/response/SearchResponse.java @@ -0,0 +1,35 @@ +package allchive.server.api.search.model.dto.response; + + +import allchive.server.api.archiving.model.dto.response.ArchivingResponse; +import allchive.server.api.common.slice.SliceResponse; +import lombok.Builder; +import lombok.Getter; + +@Getter +public class SearchResponse { + SliceResponse archivings; + SliceResponse community; + + @Builder + private SearchResponse( + SliceResponse archivings, + SliceResponse community) { + this.archivings = archivings; + this.community = community; + } + + public static SearchResponse forAll( + SliceResponse archivings, + SliceResponse community) { + return SearchResponse.builder().archivings(archivings).community(community).build(); + } + + public static SearchResponse forMy(SliceResponse archivings) { + return SearchResponse.builder().archivings(archivings).community(null).build(); + } + + public static SearchResponse forCommunity(SliceResponse community) { + return SearchResponse.builder().archivings(null).community(community).build(); + } +} diff --git a/Api/src/main/java/allchive/server/api/search/model/enums/ArchivingType.java b/Api/src/main/java/allchive/server/api/search/model/enums/ArchivingType.java new file mode 100644 index 00000000..571e9b36 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/search/model/enums/ArchivingType.java @@ -0,0 +1,7 @@ +package allchive.server.api.search.model.enums; + +public enum ArchivingType { + ALL, + MY, + COMMUNITY +} diff --git a/Api/src/main/java/allchive/server/api/search/service/GetLatestSearchListUseCase.java b/Api/src/main/java/allchive/server/api/search/service/GetLatestSearchListUseCase.java new file mode 100644 index 00000000..a392af40 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/search/service/GetLatestSearchListUseCase.java @@ -0,0 +1,27 @@ +package allchive.server.api.search.service; + + +import allchive.server.api.config.security.SecurityUtil; +import allchive.server.api.search.model.dto.response.SearchListResponse; +import allchive.server.core.annotation.UseCase; +import allchive.server.domain.domains.search.adaptor.LatestSearchAdaptor; +import allchive.server.domain.domains.search.domain.LatestSearch; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +@UseCase +@RequiredArgsConstructor +public class GetLatestSearchListUseCase { + private final LatestSearchAdaptor latestSearchAdaptor; + + @Transactional(readOnly = true) + public SearchListResponse execute() { + Long userId = SecurityUtil.getCurrentUserId(); + List keywords = + latestSearchAdaptor.findAllByUserIdOrderByCreatedAt(userId).stream() + .map(LatestSearch::getKeyword) + .toList(); + return SearchListResponse.from(keywords); + } +} diff --git a/Api/src/main/java/allchive/server/api/search/service/GetRelativeSearchListUseCase.java b/Api/src/main/java/allchive/server/api/search/service/GetRelativeSearchListUseCase.java new file mode 100644 index 00000000..744c1305 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/search/service/GetRelativeSearchListUseCase.java @@ -0,0 +1,41 @@ +package allchive.server.api.search.service; + +import static allchive.server.core.consts.AllchiveConst.SEARCH_KEY; +import static jodd.util.StringPool.ASTERISK; + +import allchive.server.api.search.model.dto.request.SearchRequest; +import allchive.server.api.search.model.dto.response.SearchListResponse; +import allchive.server.core.annotation.UseCase; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.springframework.data.redis.core.*; +import org.springframework.transaction.annotation.Transactional; + +@UseCase +@RequiredArgsConstructor +public class GetRelativeSearchListUseCase { + private final RedisTemplate redisTemplate; + + @Transactional + public SearchListResponse execute(SearchRequest request) { + ZSetOperations zSetOperations = redisTemplate.opsForZSet(); + List autoCompleteList = new ArrayList<>(); + Long rank = zSetOperations.rank(SEARCH_KEY, request.getKeyword()); + if (rank != null) { + Set rangeList = zSetOperations.range(SEARCH_KEY, rank, rank + 1000); + autoCompleteList = getAutoCompleteList(rangeList, request.getKeyword()); + } + return SearchListResponse.from(autoCompleteList); + } + + private List getAutoCompleteList(Set rangeList, String keyword) { + return rangeList.stream() + .filter(value -> value.endsWith(ASTERISK) && value.startsWith(keyword)) + .map(value -> StringUtils.removeEnd(value, ASTERISK)) + .limit(5) + .toList(); + } +} diff --git a/Api/src/main/java/allchive/server/api/search/service/RenewalTitleDataUseCase.java b/Api/src/main/java/allchive/server/api/search/service/RenewalTitleDataUseCase.java new file mode 100644 index 00000000..3f204ba7 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/search/service/RenewalTitleDataUseCase.java @@ -0,0 +1,49 @@ +package allchive.server.api.search.service; + +import static allchive.server.core.consts.AllchiveConst.ASTERISK; +import static allchive.server.core.consts.AllchiveConst.SEARCH_KEY; + +import allchive.server.core.annotation.UseCase; +import allchive.server.domain.domains.archiving.adaptor.ArchivingAdaptor; +import allchive.server.domain.domains.archiving.domain.Archiving; +import java.util.HashSet; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.transaction.annotation.Transactional; + +@UseCase +@Slf4j +@RequiredArgsConstructor +public class RenewalTitleDataUseCase { + private final ArchivingAdaptor archivingAdaptor; + private final RedisTemplate redisTemplate; + + private static final int TIME_LIMIT = 1; + + @Scheduled(cron = "0 0 3 * * *") + @Transactional(readOnly = true) + public void executeSchedule() { + log.info("renewal title scheduler on"); + redisTemplate.delete(SEARCH_KEY); + Set archivings = + new HashSet<>(archivingAdaptor.findAllByPublicStatus(Boolean.TRUE)); + archivings.forEach( + archiving -> { + redisTemplate + .opsForZSet() + .add(SEARCH_KEY, archiving.getTitle().trim() + ASTERISK, 0); + for (int index = 1; index < archiving.getTitle().length(); index++) { + redisTemplate + .opsForZSet() + .add( + SEARCH_KEY, + archiving.getTitle().trim().substring(0, index - 1), + 0); + } + }); + log.info("renewal title scheduler off"); + } +} diff --git a/Api/src/main/java/allchive/server/api/search/service/SearchArchivingUseCase.java b/Api/src/main/java/allchive/server/api/search/service/SearchArchivingUseCase.java new file mode 100644 index 00000000..df4bc9ea --- /dev/null +++ b/Api/src/main/java/allchive/server/api/search/service/SearchArchivingUseCase.java @@ -0,0 +1,89 @@ +package allchive.server.api.search.service; + + +import allchive.server.api.archiving.model.dto.response.ArchivingResponse; +import allchive.server.api.common.slice.SliceResponse; +import allchive.server.api.config.security.SecurityUtil; +import allchive.server.api.search.model.dto.request.SearchRequest; +import allchive.server.api.search.model.dto.response.SearchResponse; +import allchive.server.api.search.model.enums.ArchivingType; +import allchive.server.core.annotation.UseCase; +import allchive.server.domain.domains.archiving.adaptor.ArchivingAdaptor; +import allchive.server.domain.domains.block.adaptor.BlockAdaptor; +import allchive.server.domain.domains.block.domain.Block; +import allchive.server.domain.domains.search.adaptor.LatestSearchAdaptor; +import allchive.server.domain.domains.search.domain.LatestSearch; +import allchive.server.domain.domains.search.service.LatestSearchDomainService; +import allchive.server.domain.domains.user.adaptor.ScrapAdaptor; +import allchive.server.domain.domains.user.domain.Scrap; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.transaction.annotation.Transactional; + +@UseCase +@RequiredArgsConstructor +public class SearchArchivingUseCase { + private final ArchivingAdaptor archivingAdaptor; + private final ScrapAdaptor scrapAdaptor; + private final BlockAdaptor blockAdaptor; + private final LatestSearchAdaptor latestSearchAdaptor; + private final LatestSearchDomainService latestSearchDomainService; + + @Transactional + public SearchResponse execute(Pageable pageable, ArchivingType type, SearchRequest request) { + Long userId = SecurityUtil.getCurrentUserId(); + SliceResponse my = null; + SliceResponse community = null; + renewalLatestSearch(userId, request.getKeyword()); + switch (type) { + case ALL -> { + my = getMyArchivings(userId, request.getKeyword(), pageable); + community = getCommunityArchivings(userId, request.getKeyword(), pageable); + return SearchResponse.forAll(my, community); + } + case MY -> { + my = getMyArchivings(userId, request.getKeyword(), pageable); + return SearchResponse.forMy(my); + } + case COMMUNITY -> { + community = getCommunityArchivings(userId, request.getKeyword(), pageable); + return SearchResponse.forCommunity(community); + } + } + return null; + } + + private void renewalLatestSearch(Long userId, String keyword) { + List searches = latestSearchAdaptor.findAllByUserIdOrderByCreatedAt(userId); + if (searches.size() == 5) { + latestSearchDomainService.delete(searches.get(0)); + } + LatestSearch newSearch = LatestSearch.of(keyword, userId); + latestSearchDomainService.save(newSearch); + } + + private SliceResponse getMyArchivings( + Long userId, String keyword, Pageable pageable) { + Slice archivingSlices = + archivingAdaptor + .querySliceArchivingByUserIdAndKeywords(userId, keyword, pageable) + .map(archiving -> ArchivingResponse.of(archiving, Boolean.FALSE)); + return SliceResponse.of(archivingSlices); + } + + private SliceResponse getCommunityArchivings( + Long userId, String keyword, Pageable pageable) { + List archivingIdList = + scrapAdaptor.findAllByUserId(userId).stream().map(Scrap::getArchivingId).toList(); + List blockList = + blockAdaptor.findByBlockFrom(userId).stream().map(Block::getBlockUser).toList(); + Slice archivingSlices = + archivingAdaptor + .querySliceArchivingByKeywordExceptBlock( + archivingIdList, blockList, keyword, pageable) + .map(archiving -> ArchivingResponse.of(archiving, Boolean.FALSE)); + return SliceResponse.of(archivingSlices); + } +} diff --git a/Api/src/main/java/allchive/server/api/tag/controller/TagController.java b/Api/src/main/java/allchive/server/api/tag/controller/TagController.java new file mode 100644 index 00000000..202d5485 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/tag/controller/TagController.java @@ -0,0 +1,52 @@ +package allchive.server.api.tag.controller; + + +import allchive.server.api.tag.model.dto.request.CreateTagRequest; +import allchive.server.api.tag.model.dto.request.UpdateTagRequest; +import allchive.server.api.tag.model.dto.response.AllTagResponse; +import allchive.server.api.tag.service.CreateTagUseCase; +import allchive.server.api.tag.service.DeleteTagUseCase; +import allchive.server.api.tag.service.GetAllTagUseCase; +import allchive.server.api.tag.service.UpdateTagUseCase; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/tags") +@RequiredArgsConstructor +@Slf4j +@Tag(name = "5. [tag]") +public class TagController { + private final GetAllTagUseCase getAllTagUseCase; + private final CreateTagUseCase createTagUseCase; + private final UpdateTagUseCase updateTagUseCase; + private final DeleteTagUseCase deleteTagUseCase; + + @Operation(summary = "모든 태그를 가져옵니다.", description = "latest = true 면 최근 사용한 태그를 가져옵니다.") + @GetMapping() + public AllTagResponse getAllTag(@RequestParam("latest") Boolean latestStatus) { + return getAllTagUseCase.execute(latestStatus); + } + + @Operation(summary = "태그를 추가합니다.") + @PostMapping() + public void createTag(@RequestBody CreateTagRequest request) { + createTagUseCase.execute(request); + } + + @Operation(summary = "태그를 수정합니다.") + @PatchMapping(value = "/{tagId}") + public void updateTag( + @PathVariable("tagId") Long tagId, @RequestBody UpdateTagRequest request) { + updateTagUseCase.execute(tagId, request); + } + + @Operation(summary = "태그를 삭제합니다.") + @DeleteMapping(value = "/{tagId}") + public void deleteTag(@PathVariable("tagId") Long tagId) { + deleteTagUseCase.execute(tagId); + } +} diff --git a/Api/src/main/java/allchive/server/api/tag/model/dto/request/CreateTagRequest.java b/Api/src/main/java/allchive/server/api/tag/model/dto/request/CreateTagRequest.java new file mode 100644 index 00000000..c47a4b34 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/tag/model/dto/request/CreateTagRequest.java @@ -0,0 +1,9 @@ +package allchive.server.api.tag.model.dto.request; + + +import lombok.Getter; + +@Getter +public class CreateTagRequest { + private String name; +} diff --git a/Api/src/main/java/allchive/server/api/tag/model/dto/request/UpdateTagRequest.java b/Api/src/main/java/allchive/server/api/tag/model/dto/request/UpdateTagRequest.java new file mode 100644 index 00000000..f4c9e25c --- /dev/null +++ b/Api/src/main/java/allchive/server/api/tag/model/dto/request/UpdateTagRequest.java @@ -0,0 +1,11 @@ +package allchive.server.api.tag.model.dto.request; + + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; + +@Getter +public class UpdateTagRequest { + @Schema(description = "태그 이름") + private String name; +} diff --git a/Api/src/main/java/allchive/server/api/tag/model/dto/response/AllTagResponse.java b/Api/src/main/java/allchive/server/api/tag/model/dto/response/AllTagResponse.java new file mode 100644 index 00000000..4006e20a --- /dev/null +++ b/Api/src/main/java/allchive/server/api/tag/model/dto/response/AllTagResponse.java @@ -0,0 +1,20 @@ +package allchive.server.api.tag.model.dto.response; + + +import java.util.List; +import lombok.Builder; +import lombok.Getter; + +@Getter +public class AllTagResponse { + private List tags; + + @Builder + private AllTagResponse(List tags) { + this.tags = tags; + } + + public static AllTagResponse from(List tags) { + return AllTagResponse.builder().tags(tags).build(); + } +} diff --git a/Api/src/main/java/allchive/server/api/tag/model/dto/response/TagResponse.java b/Api/src/main/java/allchive/server/api/tag/model/dto/response/TagResponse.java new file mode 100644 index 00000000..b5a2a18d --- /dev/null +++ b/Api/src/main/java/allchive/server/api/tag/model/dto/response/TagResponse.java @@ -0,0 +1,26 @@ +package allchive.server.api.tag.model.dto.response; + + +import allchive.server.domain.domains.content.domain.Tag; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Getter; + +@Getter +public class TagResponse { + @Schema(description = "태그 고유번호") + private Long tagId; + + @Schema(description = "태그 이름") + private String name; + + @Builder + private TagResponse(Long tagId, String name) { + this.tagId = tagId; + this.name = name; + } + + public static TagResponse from(Tag tag) { + return TagResponse.builder().tagId(tag.getId()).name(tag.getName()).build(); + } +} diff --git a/Api/src/main/java/allchive/server/api/tag/model/mapper/TagMapper.java b/Api/src/main/java/allchive/server/api/tag/model/mapper/TagMapper.java new file mode 100644 index 00000000..95c495c1 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/tag/model/mapper/TagMapper.java @@ -0,0 +1,21 @@ +package allchive.server.api.tag.model.mapper; + + +import allchive.server.api.tag.model.dto.request.CreateTagRequest; +import allchive.server.api.tag.model.dto.response.AllTagResponse; +import allchive.server.api.tag.model.dto.response.TagResponse; +import allchive.server.core.annotation.Mapper; +import allchive.server.domain.domains.content.domain.Tag; +import java.util.List; + +@Mapper +public class TagMapper { + public AllTagResponse toAllTagResponse(List tagList) { + List tagResponseList = tagList.stream().map(TagResponse::from).toList(); + return AllTagResponse.from(tagResponseList); + } + + public Tag toEntity(CreateTagRequest request, Long userId) { + return Tag.of(request.getName(), userId); + } +} diff --git a/Api/src/main/java/allchive/server/api/tag/service/CreateTagUseCase.java b/Api/src/main/java/allchive/server/api/tag/service/CreateTagUseCase.java new file mode 100644 index 00000000..1583d44c --- /dev/null +++ b/Api/src/main/java/allchive/server/api/tag/service/CreateTagUseCase.java @@ -0,0 +1,25 @@ +package allchive.server.api.tag.service; + + +import allchive.server.api.config.security.SecurityUtil; +import allchive.server.api.tag.model.dto.request.CreateTagRequest; +import allchive.server.api.tag.model.mapper.TagMapper; +import allchive.server.core.annotation.UseCase; +import allchive.server.domain.domains.content.domain.Tag; +import allchive.server.domain.domains.content.service.TagDomainService; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +@UseCase +@RequiredArgsConstructor +public class CreateTagUseCase { + private final TagMapper tagMapper; + private final TagDomainService tagDomainService; + + @Transactional + public void execute(CreateTagRequest request) { + Long userId = SecurityUtil.getCurrentUserId(); + Tag tag = tagMapper.toEntity(request, userId); + tagDomainService.save(tag); + } +} diff --git a/Api/src/main/java/allchive/server/api/tag/service/DeleteTagUseCase.java b/Api/src/main/java/allchive/server/api/tag/service/DeleteTagUseCase.java new file mode 100644 index 00000000..a34a8db3 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/tag/service/DeleteTagUseCase.java @@ -0,0 +1,30 @@ +package allchive.server.api.tag.service; + + +import allchive.server.api.config.security.SecurityUtil; +import allchive.server.core.annotation.UseCase; +import allchive.server.domain.domains.content.adaptor.TagAdaptor; +import allchive.server.domain.domains.content.domain.Tag; +import allchive.server.domain.domains.content.service.ContentTagGroupDomainService; +import allchive.server.domain.domains.content.service.TagDomainService; +import allchive.server.domain.domains.content.validator.TagValidator; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +@UseCase +@RequiredArgsConstructor +public class DeleteTagUseCase { + private final TagValidator tagValidator; + private final TagAdaptor tagAdaptor; + private final ContentTagGroupDomainService contentTagGroupDomainService; + private final TagDomainService tagDomainService; + + @Transactional + public void execute(Long tagId) { + Long userId = SecurityUtil.getCurrentUserId(); + tagValidator.verifyUser(tagId, userId); + Tag tag = tagAdaptor.findById(tagId); + contentTagGroupDomainService.deleteByTag(tag); + tagDomainService.deleteById(tagId); + } +} diff --git a/Api/src/main/java/allchive/server/api/tag/service/GetAllTagUseCase.java b/Api/src/main/java/allchive/server/api/tag/service/GetAllTagUseCase.java new file mode 100644 index 00000000..499d4926 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/tag/service/GetAllTagUseCase.java @@ -0,0 +1,33 @@ +package allchive.server.api.tag.service; + + +import allchive.server.api.config.security.SecurityUtil; +import allchive.server.api.tag.model.dto.response.AllTagResponse; +import allchive.server.api.tag.model.mapper.TagMapper; +import allchive.server.core.annotation.UseCase; +import allchive.server.domain.domains.content.adaptor.TagAdaptor; +import allchive.server.domain.domains.content.domain.Tag; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +@UseCase +@RequiredArgsConstructor +public class GetAllTagUseCase { + private final TagAdaptor tagAdaptor; + private final TagMapper tagMapper; + + @Transactional(readOnly = true) + public AllTagResponse execute(Boolean latestStatus) { + List tags = getTagList(latestStatus); + return tagMapper.toAllTagResponse(tags); + } + + private List getTagList(Boolean latestStatus) { + Long userId = SecurityUtil.getCurrentUserId(); + if (latestStatus) { + return tagAdaptor.queryTagByUserIdOrderByUsedAt(userId); + } + return tagAdaptor.findAllByUserIdOrderByCreatedAtDesc(userId); + } +} diff --git a/Api/src/main/java/allchive/server/api/tag/service/UpdateTagUseCase.java b/Api/src/main/java/allchive/server/api/tag/service/UpdateTagUseCase.java new file mode 100644 index 00000000..881ec389 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/tag/service/UpdateTagUseCase.java @@ -0,0 +1,24 @@ +package allchive.server.api.tag.service; + + +import allchive.server.api.config.security.SecurityUtil; +import allchive.server.api.tag.model.dto.request.UpdateTagRequest; +import allchive.server.core.annotation.UseCase; +import allchive.server.domain.domains.content.service.TagDomainService; +import allchive.server.domain.domains.content.validator.TagValidator; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +@UseCase +@RequiredArgsConstructor +public class UpdateTagUseCase { + private final TagValidator tagValidator; + private final TagDomainService tagDomainService; + + @Transactional + public void execute(Long tagId, UpdateTagRequest request) { + Long userId = SecurityUtil.getCurrentUserId(); + tagValidator.verifyUser(tagId, userId); + tagDomainService.updateTag(tagId, request.getName()); + } +} diff --git a/Api/src/main/java/allchive/server/api/user/controller/UserController.java b/Api/src/main/java/allchive/server/api/user/controller/UserController.java new file mode 100644 index 00000000..d44da34c --- /dev/null +++ b/Api/src/main/java/allchive/server/api/user/controller/UserController.java @@ -0,0 +1,52 @@ +package allchive.server.api.user.controller; + + +import allchive.server.api.user.model.dto.request.CheckUserNicknameRequest; +import allchive.server.api.user.model.dto.request.UpdateUserInfoRequest; +import allchive.server.api.user.model.dto.response.GetUserInfoResponse; +import allchive.server.api.user.model.dto.response.GetUserProfileResponse; +import allchive.server.api.user.service.CheckUserNicknameUseCase; +import allchive.server.api.user.service.GetUserInfoUseCase; +import allchive.server.api.user.service.GetUserProfileUseCase; +import allchive.server.api.user.service.UpdateUserInfoUseCase; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/user") +@RequiredArgsConstructor +@Slf4j +@Tag(name = "2. [user]") +public class UserController { + private final GetUserProfileUseCase getUserProfileUseCase; + private final GetUserInfoUseCase getUserInfoUseCase; + private final UpdateUserInfoUseCase updateUserInfoUseCase; + private final CheckUserNicknameUseCase checkUserNicknameUseCase; + + @Operation(summary = "아카이빙 현황과 내 프로필을 가져옵니다.") + @GetMapping() + public GetUserProfileResponse getUserProfile() { + return getUserProfileUseCase.execute(); + } + + @Operation(summary = "내 정보를 가져옵니다.") + @GetMapping(value = "/info") + public GetUserInfoResponse getUserInfo() { + return getUserInfoUseCase.execute(); + } + + @Operation(summary = "내 정보를 수정합니다.") + @PostMapping(value = "/info") + public void getUserInfo(@RequestBody UpdateUserInfoRequest updateUserInfoRequest) { + updateUserInfoUseCase.execute(updateUserInfoRequest); + } + + @Operation(summary = "닉네임 중복체크합니다.") + @PostMapping(value = "/nickname") + public void checkUserNickname(@RequestBody CheckUserNicknameRequest request) { + checkUserNicknameUseCase.execute(request); + } +} diff --git a/Api/src/main/java/allchive/server/api/user/model/dto/request/CheckUserNicknameRequest.java b/Api/src/main/java/allchive/server/api/user/model/dto/request/CheckUserNicknameRequest.java new file mode 100644 index 00000000..817a6ed0 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/user/model/dto/request/CheckUserNicknameRequest.java @@ -0,0 +1,11 @@ +package allchive.server.api.user.model.dto.request; + + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; + +@Getter +public class CheckUserNicknameRequest { + @Schema(defaultValue = "닉네임", description = "닉네임") + private String nickname; +} diff --git a/Api/src/main/java/allchive/server/api/user/model/dto/request/UpdateUserInfoRequest.java b/Api/src/main/java/allchive/server/api/user/model/dto/request/UpdateUserInfoRequest.java new file mode 100644 index 00000000..825d8041 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/user/model/dto/request/UpdateUserInfoRequest.java @@ -0,0 +1,23 @@ +package allchive.server.api.user.model.dto.request; + + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; + +@Getter +public class UpdateUserInfoRequest { + @Schema( + defaultValue = + "https://asset.staging.allchive.co.kr.s3.ap-northeast-2.amazonaws.com/staging/user/1/e024eaf4-74a", + description = "프로필 이미지 url") + private String imgUrl; + + @Schema(defaultValue = "asd@asd.com", description = "이메일") + private String email; + + @Schema(defaultValue = "이름", description = "유저 이름") + private String name; + + @Schema(defaultValue = "닉네임", description = "닉네임") + private String nickname; +} diff --git a/Api/src/main/java/allchive/server/api/user/model/dto/response/GetUserInfoResponse.java b/Api/src/main/java/allchive/server/api/user/model/dto/response/GetUserInfoResponse.java new file mode 100644 index 00000000..7b4ce100 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/user/model/dto/response/GetUserInfoResponse.java @@ -0,0 +1,40 @@ +package allchive.server.api.user.model.dto.response; + + +import allchive.server.api.common.util.UrlUtil; +import allchive.server.domain.domains.user.domain.User; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Getter; + +@Getter +public class GetUserInfoResponse { + @Schema(defaultValue = "프로필 이미지 url", description = "프로필 이미지 url") + private String imgUrl; + + @Schema(defaultValue = "asd@asd.com", description = "이메일") + private String email; + + @Schema(defaultValue = "이름", description = "유저 이름") + private String name; + + @Schema(defaultValue = "닉네임", description = "닉네임") + private String nickname; + + @Builder + public GetUserInfoResponse(String imgUrl, String email, String name, String nickname) { + this.imgUrl = imgUrl; + this.email = email; + this.name = name; + this.nickname = nickname; + } + + public static GetUserInfoResponse from(User user) { + return GetUserInfoResponse.builder() + .imgUrl(UrlUtil.toAssetUrl(user.getProfileImgUrl())) + .email(user.getEmail()) + .name(user.getName()) + .nickname(user.getNickname()) + .build(); + } +} diff --git a/Api/src/main/java/allchive/server/api/user/model/dto/response/GetUserProfileResponse.java b/Api/src/main/java/allchive/server/api/user/model/dto/response/GetUserProfileResponse.java new file mode 100644 index 00000000..45e64bbb --- /dev/null +++ b/Api/src/main/java/allchive/server/api/user/model/dto/response/GetUserProfileResponse.java @@ -0,0 +1,47 @@ +package allchive.server.api.user.model.dto.response; + + +import allchive.server.api.common.util.UrlUtil; +import allchive.server.domain.domains.user.domain.User; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Getter; + +@Getter +public class GetUserProfileResponse { + @Schema(defaultValue = "닉네임", description = "닉네임") + private String nickname; + + @Schema(defaultValue = "프로필 이미지 url", description = "프로필 이미지 url") + private String imgUrl; + + @Schema(defaultValue = "0", description = "링크 개수") + private int linkCount; + + @Schema(defaultValue = "0", description = "이미지 개수") + private int imgCount; + + @Schema(defaultValue = "0", description = "공개 아카이브 개수") + private int publicArchivingCount; + + @Builder + private GetUserProfileResponse( + String nickname, String imgUrl, int linkCount, int imgCount, int publicArchivingCount) { + this.nickname = nickname; + this.imgUrl = imgUrl; + this.linkCount = linkCount; + this.imgCount = imgCount; + this.publicArchivingCount = publicArchivingCount; + } + + public static GetUserProfileResponse of( + User user, int linkCount, int imgCount, int publicArchivingCount) { + return GetUserProfileResponse.builder() + .nickname(user.getNickname()) + .imgUrl(UrlUtil.toAssetUrl(user.getProfileImgUrl())) + .linkCount(linkCount) + .imgCount(imgCount) + .publicArchivingCount(publicArchivingCount) + .build(); + } +} diff --git a/Api/src/main/java/allchive/server/api/user/model/mapper/UserMapper.java b/Api/src/main/java/allchive/server/api/user/model/mapper/UserMapper.java new file mode 100644 index 00000000..84468718 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/user/model/mapper/UserMapper.java @@ -0,0 +1,22 @@ +package allchive.server.api.user.model.mapper; + + +import allchive.server.api.user.model.dto.response.GetUserProfileResponse; +import allchive.server.core.annotation.Mapper; +import allchive.server.domain.domains.archiving.domain.Archiving; +import allchive.server.domain.domains.user.domain.User; +import java.util.List; + +@Mapper +public class UserMapper { + public GetUserProfileResponse toGetUserProfileResponse( + List archivingList, User user) { + int linkCount = 0, imgCount = 0, publicArchivingCount = 0; + for (Archiving archiving : archivingList) { + linkCount += archiving.getLinkCnt(); + imgCount += archiving.getImgCnt(); + publicArchivingCount += archiving.getPublicStatus() ? 1 : 0; + } + return GetUserProfileResponse.of(user, linkCount, imgCount, publicArchivingCount); + } +} diff --git a/Api/src/main/java/allchive/server/api/user/service/CheckUserNicknameUseCase.java b/Api/src/main/java/allchive/server/api/user/service/CheckUserNicknameUseCase.java new file mode 100644 index 00000000..de1a6e84 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/user/service/CheckUserNicknameUseCase.java @@ -0,0 +1,19 @@ +package allchive.server.api.user.service; + + +import allchive.server.api.user.model.dto.request.CheckUserNicknameRequest; +import allchive.server.core.annotation.UseCase; +import allchive.server.domain.domains.user.service.UserDomainService; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +@UseCase +@RequiredArgsConstructor +public class CheckUserNicknameUseCase { + private final UserDomainService userDomainService; + + @Transactional(readOnly = true) + public void execute(CheckUserNicknameRequest request) { + userDomainService.checkUserNickname(request.getNickname()); + } +} diff --git a/Api/src/main/java/allchive/server/api/user/service/GetUserInfoUseCase.java b/Api/src/main/java/allchive/server/api/user/service/GetUserInfoUseCase.java new file mode 100644 index 00000000..0cb03dca --- /dev/null +++ b/Api/src/main/java/allchive/server/api/user/service/GetUserInfoUseCase.java @@ -0,0 +1,26 @@ +package allchive.server.api.user.service; + + +import allchive.server.api.config.security.SecurityUtil; +import allchive.server.api.user.model.dto.response.GetUserInfoResponse; +import allchive.server.core.annotation.UseCase; +import allchive.server.domain.domains.user.adaptor.UserAdaptor; +import allchive.server.domain.domains.user.domain.User; +import allchive.server.domain.domains.user.validator.UserValidator; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +@UseCase +@RequiredArgsConstructor +public class GetUserInfoUseCase { + private final UserAdaptor userAdaptor; + private final UserValidator userValidator; + + @Transactional(readOnly = true) + public GetUserInfoResponse execute() { + Long userId = SecurityUtil.getCurrentUserId(); + userValidator.validateUserStatusNormal(userId); + User user = userAdaptor.findById(userId); + return GetUserInfoResponse.from(user); + } +} diff --git a/Api/src/main/java/allchive/server/api/user/service/GetUserProfileUseCase.java b/Api/src/main/java/allchive/server/api/user/service/GetUserProfileUseCase.java new file mode 100644 index 00000000..bf30180f --- /dev/null +++ b/Api/src/main/java/allchive/server/api/user/service/GetUserProfileUseCase.java @@ -0,0 +1,33 @@ +package allchive.server.api.user.service; + + +import allchive.server.api.config.security.SecurityUtil; +import allchive.server.api.user.model.dto.response.GetUserProfileResponse; +import allchive.server.api.user.model.mapper.UserMapper; +import allchive.server.core.annotation.UseCase; +import allchive.server.domain.domains.archiving.adaptor.ArchivingAdaptor; +import allchive.server.domain.domains.archiving.domain.Archiving; +import allchive.server.domain.domains.user.adaptor.UserAdaptor; +import allchive.server.domain.domains.user.domain.User; +import allchive.server.domain.domains.user.validator.UserValidator; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +@UseCase +@RequiredArgsConstructor +public class GetUserProfileUseCase { + private final ArchivingAdaptor archivingAdaptor; + private final UserAdaptor userAdaptor; + private final UserMapper userMapper; + private final UserValidator userValidator; + + @Transactional(readOnly = true) + public GetUserProfileResponse execute() { + Long userId = SecurityUtil.getCurrentUserId(); + userValidator.validateUserStatusNormal(userId); + User user = userAdaptor.findById(userId); + List archivingList = archivingAdaptor.findAllByUserId(userId); + return userMapper.toGetUserProfileResponse(archivingList, user); + } +} diff --git a/Api/src/main/java/allchive/server/api/user/service/UpdateUserInfoUseCase.java b/Api/src/main/java/allchive/server/api/user/service/UpdateUserInfoUseCase.java new file mode 100644 index 00000000..9485a883 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/user/service/UpdateUserInfoUseCase.java @@ -0,0 +1,27 @@ +package allchive.server.api.user.service; + + +import allchive.server.api.common.util.UrlUtil; +import allchive.server.api.config.security.SecurityUtil; +import allchive.server.api.user.model.dto.request.UpdateUserInfoRequest; +import allchive.server.core.annotation.UseCase; +import allchive.server.domain.domains.user.service.UserDomainService; +import allchive.server.domain.domains.user.validator.UserValidator; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +@UseCase +@RequiredArgsConstructor +public class UpdateUserInfoUseCase { + private final UserDomainService userDomainService; + private final UserValidator userValidator; + + @Transactional + public void execute(UpdateUserInfoRequest request) { + Long userId = SecurityUtil.getCurrentUserId(); + userValidator.validateUserStatusNormal(userId); + String imgKey = UrlUtil.convertUrlToKey(request.getImgUrl()); + userDomainService.updateUserInfo( + userId, request.getName(), request.getEmail(), request.getNickname(), imgKey); + } +} diff --git a/Api/src/main/resources/application-api.yml b/Api/src/main/resources/application-api.yml deleted file mode 100644 index c348dcd6..00000000 --- a/Api/src/main/resources/application-api.yml +++ /dev/null @@ -1,9 +0,0 @@ -spring: - profiles: - include: - - domain - - core - - infrastructure - mvc: - pathmatch: - matching-strategy: ant_path_matcher \ No newline at end of file diff --git a/Api/src/main/resources/application.yml b/Api/src/main/resources/application.yml new file mode 100644 index 00000000..c4f14e91 --- /dev/null +++ b/Api/src/main/resources/application.yml @@ -0,0 +1,26 @@ +server: + servlet: + context-path: /api + forward-headers-strategy: framework + +spring: + profiles: + include: + - domain + - core + - infrastructure + mvc: + pathmatch: + matching-strategy: ant_path_matcher + +springdoc: + default-consumes-media-type: application/json + default-produces-media-type: application/json + swagger-ui: + tags-sorter: alpha + +--- +spring: + config: + activate: + on-profile: dev \ No newline at end of file diff --git a/Core/build.gradle b/Core/build.gradle index cd78a759..f67e5636 100644 --- a/Core/build.gradle +++ b/Core/build.gradle @@ -2,8 +2,12 @@ bootJar.enabled = false jar.enabled = true dependencies { - implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.14.2' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.2' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.2' } \ No newline at end of file diff --git a/Core/src/main/java/allchive/server/.gitkeep b/Core/src/main/java/allchive/server/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/Core/src/main/java/allchive/server/core/annotation/Adaptor.java b/Core/src/main/java/allchive/server/core/annotation/Adaptor.java new file mode 100644 index 00000000..a84c36bb --- /dev/null +++ b/Core/src/main/java/allchive/server/core/annotation/Adaptor.java @@ -0,0 +1,15 @@ +package allchive.server.core.annotation; + + +import java.lang.annotation.*; +import org.springframework.core.annotation.AliasFor; +import org.springframework.stereotype.Component; + +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Component +public @interface Adaptor { + @AliasFor(annotation = Component.class) + String value() default ""; +} diff --git a/Core/src/main/java/allchive/server/core/annotation/DateFormat.java b/Core/src/main/java/allchive/server/core/annotation/DateFormat.java new file mode 100644 index 00000000..44b15b02 --- /dev/null +++ b/Core/src/main/java/allchive/server/core/annotation/DateFormat.java @@ -0,0 +1,16 @@ +package allchive.server.core.annotation; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; +import com.fasterxml.jackson.annotation.JsonFormat; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Target({TYPE_USE, FIELD}) +@Retention(RUNTIME) +@JacksonAnnotationsInside +@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy.MM.dd", timezone = "Asia/Seoul") +public @interface DateFormat {} diff --git a/Core/src/main/java/allchive/server/core/annotation/DomainService.java b/Core/src/main/java/allchive/server/core/annotation/DomainService.java new file mode 100644 index 00000000..385ad13c --- /dev/null +++ b/Core/src/main/java/allchive/server/core/annotation/DomainService.java @@ -0,0 +1,15 @@ +package allchive.server.core.annotation; + + +import java.lang.annotation.*; +import org.springframework.core.annotation.AliasFor; +import org.springframework.stereotype.Component; + +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Component +public @interface DomainService { + @AliasFor(annotation = Component.class) + String value() default ""; +} diff --git a/Core/src/main/java/allchive/server/core/annotation/Helper.java b/Core/src/main/java/allchive/server/core/annotation/Helper.java new file mode 100644 index 00000000..0291f0e3 --- /dev/null +++ b/Core/src/main/java/allchive/server/core/annotation/Helper.java @@ -0,0 +1,15 @@ +package allchive.server.core.annotation; + + +import java.lang.annotation.*; +import org.springframework.core.annotation.AliasFor; +import org.springframework.stereotype.Component; + +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Component +public @interface Helper { + @AliasFor(annotation = Component.class) + String value() default ""; +} diff --git a/Core/src/main/java/allchive/server/core/annotation/Mapper.java b/Core/src/main/java/allchive/server/core/annotation/Mapper.java new file mode 100644 index 00000000..99a13dc0 --- /dev/null +++ b/Core/src/main/java/allchive/server/core/annotation/Mapper.java @@ -0,0 +1,15 @@ +package allchive.server.core.annotation; + + +import java.lang.annotation.*; +import org.springframework.core.annotation.AliasFor; +import org.springframework.stereotype.Component; + +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Component +public @interface Mapper { + @AliasFor(annotation = Component.class) + String value() default ""; +} diff --git a/Core/src/main/java/allchive/server/core/annotation/UseCase.java b/Core/src/main/java/allchive/server/core/annotation/UseCase.java new file mode 100644 index 00000000..1926bd69 --- /dev/null +++ b/Core/src/main/java/allchive/server/core/annotation/UseCase.java @@ -0,0 +1,15 @@ +package allchive.server.core.annotation; + + +import java.lang.annotation.*; +import org.springframework.core.annotation.AliasFor; +import org.springframework.stereotype.Component; + +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Component +public @interface UseCase { + @AliasFor(annotation = Component.class) + String value() default ""; +} diff --git a/Core/src/main/java/allchive/server/core/annotation/ValidEnum.java b/Core/src/main/java/allchive/server/core/annotation/ValidEnum.java new file mode 100644 index 00000000..27ca8ce9 --- /dev/null +++ b/Core/src/main/java/allchive/server/core/annotation/ValidEnum.java @@ -0,0 +1,26 @@ +package allchive.server.core.annotation; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import allchive.server.core.validator.EnumValidator; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import javax.validation.Constraint; +import javax.validation.Payload; + +@Documented +@Target({ElementType.TYPE_USE, FIELD}) +@Retention(RUNTIME) +@Constraint(validatedBy = {EnumValidator.class}) +public @interface ValidEnum { + String message() default "올바른 값을 입력해주세요"; + + Class[] groups() default {}; + + Class[] payload() default {}; + + Class> target(); +} diff --git a/Core/src/main/java/allchive/server/core/annotation/Validator.java b/Core/src/main/java/allchive/server/core/annotation/Validator.java new file mode 100644 index 00000000..d950aa05 --- /dev/null +++ b/Core/src/main/java/allchive/server/core/annotation/Validator.java @@ -0,0 +1,15 @@ +package allchive.server.core.annotation; + + +import java.lang.annotation.*; +import org.springframework.core.annotation.AliasFor; +import org.springframework.stereotype.Component; + +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Component +public @interface Validator { + @AliasFor(annotation = Component.class) + String value() default ""; +} diff --git a/Core/src/main/java/allchive/server/core/common/CoreConfigurationPropertiesConfig.java b/Core/src/main/java/allchive/server/core/common/CoreConfigurationPropertiesConfig.java new file mode 100644 index 00000000..9a99ce02 --- /dev/null +++ b/Core/src/main/java/allchive/server/core/common/CoreConfigurationPropertiesConfig.java @@ -0,0 +1,16 @@ +package allchive.server.core.common; + + +import allchive.server.core.jwt.JwtProperties; +import allchive.server.core.properties.AppleOAuthProperties; +import allchive.server.core.properties.KakaoOAuthProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@EnableConfigurationProperties({ + JwtProperties.class, + KakaoOAuthProperties.class, + AppleOAuthProperties.class +}) +@Configuration +public class CoreConfigurationPropertiesConfig {} diff --git a/Core/src/main/java/allchive/server/core/consts/AllchiveConst.java b/Core/src/main/java/allchive/server/core/consts/AllchiveConst.java new file mode 100644 index 00000000..2a762c23 --- /dev/null +++ b/Core/src/main/java/allchive/server/core/consts/AllchiveConst.java @@ -0,0 +1,43 @@ +package allchive.server.core.consts; + +public class AllchiveConst { + public static final String AUTH_HEADER = "Authorization"; + public static final String BEARER = "Bearer "; + public static final String TOKEN_ROLE = "role"; + public static final String TOKEN_ISSUER = "ALLCHIVE"; + public static final String TOKEN_TYPE = "type"; + public static final String ACCESS_TOKEN = "ACCESS_TOKEN"; + public static final String REFRESH_TOKEN = "REFRESH_TOKEN"; + + public static final String KID = "kid"; + public static final String KR_YES = "예"; + public static final String KR_NO = "아니요"; + + public static final String PROD = "prod"; + public static final String DEV = "dev"; + + public static final int MILLI_TO_SECOND = 1000; + public static final int BAD_REQUEST = 400; + public static final int UNAUTHORIZED = 401; + public static final int FORBIDDEN = 403; + public static final int NOT_FOUND = 404; + public static final int INTERNAL_SERVER = 500; + + public static final String KAKAO_OAUTH_QUERY_STRING = + "/oauth/authorize?client_id=%s&redirect_uri=%s&response_type=code"; + + public static final String APPLE_OAUTH_QUERY_STRING = + "/auth/authorize?client_id=%s&redirect_uri=%s&response_type=code"; + + public static final String STAGING_ASSET_URL = "https://asset.staging.allchive.co.kr/"; + public static final String PROD_ASSET_URL = "https://asset.allchive.co.kr/"; + public static final String S3_ASSET_URL = + "https://asset.staging.allchive.co.kr.s3.ap-northeast-2.amazonaws.com/"; + + public static final String[] SwaggerPatterns = { + "/swagger-resources/**", "/swagger-ui/**", "/v3/api-docs/**", "/v3/api-docs", + }; + + public static final String SEARCH_KEY = "ARCHIVING_TITLE"; + public static final String ASTERISK = "*"; +} diff --git a/Core/src/main/java/allchive/server/core/dto/ErrorReason.java b/Core/src/main/java/allchive/server/core/dto/ErrorReason.java new file mode 100644 index 00000000..34cb5075 --- /dev/null +++ b/Core/src/main/java/allchive/server/core/dto/ErrorReason.java @@ -0,0 +1,23 @@ +package allchive.server.core.dto; + + +import lombok.Builder; +import lombok.Getter; + +@Getter +public class ErrorReason { + private final int status; + private final String code; + private final String reason; + + @Builder + private ErrorReason(int status, String code, String reason) { + this.status = status; + this.code = code; + this.reason = reason; + } + + public static ErrorReason of(int status, String code, String reason) { + return ErrorReason.builder().status(status).code(code).reason(reason).build(); + } +} diff --git a/Core/src/main/java/allchive/server/core/dto/OIDCDecodePayload.java b/Core/src/main/java/allchive/server/core/dto/OIDCDecodePayload.java new file mode 100644 index 00000000..056279e4 --- /dev/null +++ b/Core/src/main/java/allchive/server/core/dto/OIDCDecodePayload.java @@ -0,0 +1,18 @@ +package allchive.server.core.dto; + + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class OIDCDecodePayload { + /** issuer ex https://kauth.kakao.com */ + private String iss; + /** client id */ + private String aud; + /** oauth provider account unique id */ + private String sub; + + private String email; +} diff --git a/Core/src/main/java/allchive/server/core/error/BaseDynamicException.java b/Core/src/main/java/allchive/server/core/error/BaseDynamicException.java new file mode 100644 index 00000000..43605888 --- /dev/null +++ b/Core/src/main/java/allchive/server/core/error/BaseDynamicException.java @@ -0,0 +1,15 @@ +package allchive.server.core.error; + + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +@AllArgsConstructor +public class BaseDynamicException extends RuntimeException { + private final int status; + private final String code; + private final String message; +} diff --git a/Core/src/main/java/allchive/server/core/error/BaseErrorCode.java b/Core/src/main/java/allchive/server/core/error/BaseErrorCode.java new file mode 100644 index 00000000..7cc5707d --- /dev/null +++ b/Core/src/main/java/allchive/server/core/error/BaseErrorCode.java @@ -0,0 +1,8 @@ +package allchive.server.core.error; + + +import allchive.server.core.dto.ErrorReason; + +public interface BaseErrorCode { + public ErrorReason getErrorReason(); +} diff --git a/Core/src/main/java/allchive/server/core/error/BaseErrorException.java b/Core/src/main/java/allchive/server/core/error/BaseErrorException.java new file mode 100644 index 00000000..54a14601 --- /dev/null +++ b/Core/src/main/java/allchive/server/core/error/BaseErrorException.java @@ -0,0 +1,16 @@ +package allchive.server.core.error; + + +import allchive.server.core.dto.ErrorReason; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class BaseErrorException extends RuntimeException { + private BaseErrorCode errorCode; + + public ErrorReason getErrorReason() { + return this.errorCode.getErrorReason(); + } +} diff --git a/Core/src/main/java/allchive/server/core/error/ErrorResponse.java b/Core/src/main/java/allchive/server/core/error/ErrorResponse.java new file mode 100644 index 00000000..58626870 --- /dev/null +++ b/Core/src/main/java/allchive/server/core/error/ErrorResponse.java @@ -0,0 +1,32 @@ +package allchive.server.core.error; + + +import allchive.server.core.dto.ErrorReason; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import lombok.Builder; +import lombok.Getter; + +@Getter +public class ErrorResponse { + private final String timestamp = LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME); + private final boolean success = false; + private String code; + private int status; + private String reason; + + @Builder + private ErrorResponse(String code, int status, String reason) { + this.code = code; + this.status = status; + this.reason = reason; + } + + public static ErrorResponse from(ErrorReason errorReason) { + return ErrorResponse.builder() + .code(errorReason.getCode()) + .status(errorReason.getStatus()) + .reason(errorReason.getReason()) + .build(); + } +} diff --git a/Core/src/main/java/allchive/server/core/error/GlobalErrorCode.java b/Core/src/main/java/allchive/server/core/error/GlobalErrorCode.java new file mode 100644 index 00000000..f73bb7a8 --- /dev/null +++ b/Core/src/main/java/allchive/server/core/error/GlobalErrorCode.java @@ -0,0 +1,52 @@ +package allchive.server.core.error; + +import static allchive.server.core.consts.AllchiveConst.*; + +import allchive.server.core.dto.ErrorReason; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum GlobalErrorCode implements BaseErrorCode { + /** example * */ + EXAMPLE_ERROR(BAD_REQUEST, "GLOBAL_400_0", "에러 예시 입니다."), + + /** Server 오류 * */ + HTTP_MESSAGE_NOT_READABLE(BAD_REQUEST, "GLOBAL_400_1", "잘못된 형식의 값을 입력했습니다."), + _INTERNAL_SERVER_ERROR(INTERNAL_SERVER, "GLOBAL_500_1", "서버 오류. 관리자에게 문의 부탁드립니다."), + INVALID_OAUTH_PROVIDER(INTERNAL_SERVER, "GLOBAL_500_2", "지원하지 않는 OAuth Provider 입니다."), + + SECURITY_CONTEXT_NOT_FOUND(INTERNAL_SERVER, "GLOBAL_500_3", "security context not found"), + + /** 토큰 에러 * */ + // TODO : 에러 코드 정렬 + INVALID_TOKEN(UNAUTHORIZED, "AUTH_401_2", "올바르지 않은 토큰입니다."), + INVALID_ACCESS_TOKEN_ERROR(UNAUTHORIZED, "AUTH_401_4", "알맞은 accessToken 을 넣어주세요."), + EXPIRED_TOKEN(UNAUTHORIZED, "AUTH_401_3", "만료된 엑세스 토큰입니다"), + EXPIRED_REFRESH_TOKEN(UNAUTHORIZED, "AUTH_403_1", "인증 시간이 만료되었습니다. 재 로그인 해주세요."), + INVALID_AUTH_TOKEN(UNAUTHORIZED, "AUTH_401_2", "액세스 토큰이 유효하지 않습니다"), + INVALID_REFRESH_TOKEN(UNAUTHORIZED, "AUTH_401_4", "리프레시 토큰이 유효하지 않습니다"), + MISMATCH_REFRESH_TOKEN(UNAUTHORIZED, "AUTH_401_6", "리프레시 토큰의 유저 정보가 일치하지 않습니다"), + FORBIDDEN_ADMIN(FORBIDDEN, "AUTH_403_1", "권한이 부여되지 않은 사용자입니다"), + UNSUPPORTED_TOKEN(UNAUTHORIZED, "AUTH_401_7", "지원하지 않는 토큰입니다"), + INVALID_SIGNATURE(UNAUTHORIZED, "AUTH_401_8", "잘못된 JWT 서명입니다"), + NO_TOKEN(UNAUTHORIZED, "AUTH_401_1", "토큰이 존재하지 않습니다"), + + /** Feign Client 오류 */ + OTHER_SERVER_BAD_REQUEST(BAD_REQUEST, "FEIGN_400_1", "Other server bad request"), + OTHER_SERVER_UNAUTHORIZED(BAD_REQUEST, "FEIGN_400_2", "Other server unauthorized"), + OTHER_SERVER_FORBIDDEN(BAD_REQUEST, "FEIGN_400_3", "Other server forbidden"), + OTHER_SERVER_EXPIRED_TOKEN(BAD_REQUEST, "FEIGN_400_4", "Other server expired token"), + OTHER_SERVER_NOT_FOUND(BAD_REQUEST, "FEIGN_400_5", "Other server not found error"), + ; + + private int status; + private String code; + private String reason; + + @Override + public ErrorReason getErrorReason() { + return ErrorReason.of(status, code, reason); + } +} diff --git a/Core/src/main/java/allchive/server/core/error/exception/ExampleException.java b/Core/src/main/java/allchive/server/core/error/exception/ExampleException.java new file mode 100644 index 00000000..478a21d7 --- /dev/null +++ b/Core/src/main/java/allchive/server/core/error/exception/ExampleException.java @@ -0,0 +1,14 @@ +package allchive.server.core.error.exception; + + +import allchive.server.core.error.BaseErrorException; +import allchive.server.core.error.GlobalErrorCode; + +public class ExampleException extends BaseErrorException { + + public static final BaseErrorException EXCEPTION = new ExampleException(); + + private ExampleException() { + super(GlobalErrorCode.EXAMPLE_ERROR); + } +} diff --git a/Core/src/main/java/allchive/server/core/error/exception/ExpiredRefreshTokenException.java b/Core/src/main/java/allchive/server/core/error/exception/ExpiredRefreshTokenException.java new file mode 100644 index 00000000..beea48b6 --- /dev/null +++ b/Core/src/main/java/allchive/server/core/error/exception/ExpiredRefreshTokenException.java @@ -0,0 +1,14 @@ +package allchive.server.core.error.exception; + + +import allchive.server.core.error.BaseErrorException; +import allchive.server.core.error.GlobalErrorCode; + +public class ExpiredRefreshTokenException extends BaseErrorException { + + public static final BaseErrorException EXCEPTION = new ExpiredRefreshTokenException(); + + private ExpiredRefreshTokenException() { + super(GlobalErrorCode.EXPIRED_REFRESH_TOKEN); + } +} diff --git a/Core/src/main/java/allchive/server/core/error/exception/ExpiredTokenException.java b/Core/src/main/java/allchive/server/core/error/exception/ExpiredTokenException.java new file mode 100644 index 00000000..59c28d9e --- /dev/null +++ b/Core/src/main/java/allchive/server/core/error/exception/ExpiredTokenException.java @@ -0,0 +1,14 @@ +package allchive.server.core.error.exception; + + +import allchive.server.core.error.BaseErrorException; +import allchive.server.core.error.GlobalErrorCode; + +public class ExpiredTokenException extends BaseErrorException { + + public static final BaseErrorException EXCEPTION = new ExpiredTokenException(); + + private ExpiredTokenException() { + super(GlobalErrorCode.EXPIRED_TOKEN); + } +} diff --git a/Core/src/main/java/allchive/server/core/error/exception/InternalServerError.java b/Core/src/main/java/allchive/server/core/error/exception/InternalServerError.java new file mode 100644 index 00000000..3e4694ae --- /dev/null +++ b/Core/src/main/java/allchive/server/core/error/exception/InternalServerError.java @@ -0,0 +1,14 @@ +package allchive.server.core.error.exception; + + +import allchive.server.core.error.BaseErrorException; +import allchive.server.core.error.GlobalErrorCode; + +public class InternalServerError extends BaseErrorException { + + public static final BaseErrorException EXCEPTION = new InternalServerError(); + + private InternalServerError() { + super(GlobalErrorCode._INTERNAL_SERVER_ERROR); + } +} diff --git a/Core/src/main/java/allchive/server/core/error/exception/InvalidOauthProviderException.java b/Core/src/main/java/allchive/server/core/error/exception/InvalidOauthProviderException.java new file mode 100644 index 00000000..0be67540 --- /dev/null +++ b/Core/src/main/java/allchive/server/core/error/exception/InvalidOauthProviderException.java @@ -0,0 +1,14 @@ +package allchive.server.core.error.exception; + + +import allchive.server.core.error.BaseErrorException; +import allchive.server.core.error.GlobalErrorCode; + +public class InvalidOauthProviderException extends BaseErrorException { + + public static final BaseErrorException EXCEPTION = new InvalidOauthProviderException(); + + private InvalidOauthProviderException() { + super(GlobalErrorCode.INVALID_OAUTH_PROVIDER); + } +} diff --git a/Core/src/main/java/allchive/server/core/error/exception/InvalidTokenException.java b/Core/src/main/java/allchive/server/core/error/exception/InvalidTokenException.java new file mode 100644 index 00000000..88e9cf8c --- /dev/null +++ b/Core/src/main/java/allchive/server/core/error/exception/InvalidTokenException.java @@ -0,0 +1,14 @@ +package allchive.server.core.error.exception; + + +import allchive.server.core.error.BaseErrorException; +import allchive.server.core.error.GlobalErrorCode; + +public class InvalidTokenException extends BaseErrorException { + + public static final BaseErrorException EXCEPTION = new InvalidTokenException(); + + private InvalidTokenException() { + super(GlobalErrorCode.INVALID_TOKEN); + } +} diff --git a/Core/src/main/java/allchive/server/core/error/exception/OtherServerBadRequestException.java b/Core/src/main/java/allchive/server/core/error/exception/OtherServerBadRequestException.java new file mode 100644 index 00000000..83954612 --- /dev/null +++ b/Core/src/main/java/allchive/server/core/error/exception/OtherServerBadRequestException.java @@ -0,0 +1,14 @@ +package allchive.server.core.error.exception; + + +import allchive.server.core.error.BaseErrorException; +import allchive.server.core.error.GlobalErrorCode; + +public class OtherServerBadRequestException extends BaseErrorException { + + public static final BaseErrorException EXCEPTION = new OtherServerBadRequestException(); + + private OtherServerBadRequestException() { + super(GlobalErrorCode.OTHER_SERVER_BAD_REQUEST); + } +} diff --git a/Core/src/main/java/allchive/server/core/error/exception/OtherServerExpiredTokenException.java b/Core/src/main/java/allchive/server/core/error/exception/OtherServerExpiredTokenException.java new file mode 100644 index 00000000..c845115a --- /dev/null +++ b/Core/src/main/java/allchive/server/core/error/exception/OtherServerExpiredTokenException.java @@ -0,0 +1,14 @@ +package allchive.server.core.error.exception; + + +import allchive.server.core.error.BaseErrorException; +import allchive.server.core.error.GlobalErrorCode; + +public class OtherServerExpiredTokenException extends BaseErrorException { + + public static final BaseErrorException EXCEPTION = new OtherServerExpiredTokenException(); + + private OtherServerExpiredTokenException() { + super(GlobalErrorCode.OTHER_SERVER_EXPIRED_TOKEN); + } +} diff --git a/Core/src/main/java/allchive/server/core/error/exception/OtherServerForbiddenException.java b/Core/src/main/java/allchive/server/core/error/exception/OtherServerForbiddenException.java new file mode 100644 index 00000000..5cfd1123 --- /dev/null +++ b/Core/src/main/java/allchive/server/core/error/exception/OtherServerForbiddenException.java @@ -0,0 +1,14 @@ +package allchive.server.core.error.exception; + + +import allchive.server.core.error.BaseErrorException; +import allchive.server.core.error.GlobalErrorCode; + +public class OtherServerForbiddenException extends BaseErrorException { + + public static final BaseErrorException EXCEPTION = new OtherServerForbiddenException(); + + private OtherServerForbiddenException() { + super(GlobalErrorCode.OTHER_SERVER_FORBIDDEN); + } +} diff --git a/Core/src/main/java/allchive/server/core/error/exception/OtherServerNotFoundException.java b/Core/src/main/java/allchive/server/core/error/exception/OtherServerNotFoundException.java new file mode 100644 index 00000000..180b8a77 --- /dev/null +++ b/Core/src/main/java/allchive/server/core/error/exception/OtherServerNotFoundException.java @@ -0,0 +1,14 @@ +package allchive.server.core.error.exception; + + +import allchive.server.core.error.BaseErrorException; +import allchive.server.core.error.GlobalErrorCode; + +public class OtherServerNotFoundException extends BaseErrorException { + + public static final BaseErrorException EXCEPTION = new OtherServerNotFoundException(); + + private OtherServerNotFoundException() { + super(GlobalErrorCode.OTHER_SERVER_NOT_FOUND); + } +} diff --git a/Core/src/main/java/allchive/server/core/error/exception/OtherServerUnauthorizedException.java b/Core/src/main/java/allchive/server/core/error/exception/OtherServerUnauthorizedException.java new file mode 100644 index 00000000..8c10f6dc --- /dev/null +++ b/Core/src/main/java/allchive/server/core/error/exception/OtherServerUnauthorizedException.java @@ -0,0 +1,14 @@ +package allchive.server.core.error.exception; + + +import allchive.server.core.error.BaseErrorException; +import allchive.server.core.error.GlobalErrorCode; + +public class OtherServerUnauthorizedException extends BaseErrorException { + + public static final BaseErrorException EXCEPTION = new OtherServerUnauthorizedException(); + + private OtherServerUnauthorizedException() { + super(GlobalErrorCode.OTHER_SERVER_UNAUTHORIZED); + } +} diff --git a/Core/src/main/java/allchive/server/core/error/exception/SecurityContextNotFoundException.java b/Core/src/main/java/allchive/server/core/error/exception/SecurityContextNotFoundException.java new file mode 100644 index 00000000..ede13b6b --- /dev/null +++ b/Core/src/main/java/allchive/server/core/error/exception/SecurityContextNotFoundException.java @@ -0,0 +1,14 @@ +package allchive.server.core.error.exception; + + +import allchive.server.core.error.BaseErrorException; +import allchive.server.core.error.GlobalErrorCode; + +public class SecurityContextNotFoundException extends BaseErrorException { + + public static final BaseErrorException EXCEPTION = new SecurityContextNotFoundException(); + + private SecurityContextNotFoundException() { + super(GlobalErrorCode.SECURITY_CONTEXT_NOT_FOUND); + } +} diff --git a/Core/src/main/java/allchive/server/core/helper/SpringEnvironmentHelper.java b/Core/src/main/java/allchive/server/core/helper/SpringEnvironmentHelper.java new file mode 100644 index 00000000..9016edfd --- /dev/null +++ b/Core/src/main/java/allchive/server/core/helper/SpringEnvironmentHelper.java @@ -0,0 +1,29 @@ +package allchive.server.core.helper; + +import static allchive.server.core.consts.AllchiveConst.DEV; +import static allchive.server.core.consts.AllchiveConst.PROD; + +import java.util.Arrays; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class SpringEnvironmentHelper { + + private final Environment environment; + + public Boolean isProdProfile() { + String[] activeProfiles = environment.getActiveProfiles(); + List currentProfile = Arrays.stream(activeProfiles).toList(); + return currentProfile.contains(PROD); + } + + public Boolean isDevProfile() { + String[] activeProfiles = environment.getActiveProfiles(); + List currentProfile = Arrays.stream(activeProfiles).toList(); + return currentProfile.contains(DEV); + } +} diff --git a/Core/src/main/java/allchive/server/core/jwt/JwtOIDCProvider.java b/Core/src/main/java/allchive/server/core/jwt/JwtOIDCProvider.java new file mode 100644 index 00000000..ca3f5d8a --- /dev/null +++ b/Core/src/main/java/allchive/server/core/jwt/JwtOIDCProvider.java @@ -0,0 +1,83 @@ +package allchive.server.core.jwt; + +import static allchive.server.core.consts.AllchiveConst.KID; + +import allchive.server.core.dto.OIDCDecodePayload; +import allchive.server.core.error.exception.ExpiredTokenException; +import allchive.server.core.error.exception.InvalidTokenException; +import io.jsonwebtoken.*; +import java.math.BigInteger; +import java.security.Key; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.RSAPublicKeySpec; +import java.util.Base64; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +@Slf4j +public class JwtOIDCProvider { + public String getKidFromUnsignedTokenHeader(String token, String iss, String aud) { + return (String) getUnsignedTokenClaims(token, iss, aud).getHeader().get(KID); + } + + private Jwt getUnsignedTokenClaims(String token, String iss, String aud) { + try { + return Jwts.parserBuilder() + .requireAudience(aud) + .requireIssuer(iss) + .build() + .parseClaimsJwt(getUnsignedToken(token)); + } catch (ExpiredJwtException e) { + throw ExpiredTokenException.EXCEPTION; + } catch (Exception e) { + log.error(e.toString()); + throw InvalidTokenException.EXCEPTION; + } + } + + private String getUnsignedToken(String token) { + String[] splitToken = token.split("\\."); + if (splitToken.length != 3) throw InvalidTokenException.EXCEPTION; + return splitToken[0] + "." + splitToken[1] + "."; + } + + public OIDCDecodePayload getOIDCTokenBody(String token, String modulus, String exponent) { + Claims body = getOIDCTokenJws(token, modulus, exponent).getBody(); + return new OIDCDecodePayload( + body.getIssuer(), + body.getAudience(), + body.getSubject(), + body.get("email", String.class)); + } + + public Jws getOIDCTokenJws(String token, String modulus, String exponent) { + try { + return Jwts.parserBuilder() + .setSigningKey(getRSAPublicKey(modulus, exponent)) + .build() + .parseClaimsJws(token); + } catch (ExpiredJwtException e) { + throw ExpiredTokenException.EXCEPTION; + } catch (Exception e) { + log.error(e.toString()); + throw InvalidTokenException.EXCEPTION; + } + } + + private Key getRSAPublicKey(String modulus, String exponent) + throws NoSuchAlgorithmException, InvalidKeySpecException { + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + byte[] decodeN = Base64.getUrlDecoder().decode(modulus); + byte[] decodeE = Base64.getUrlDecoder().decode(exponent); + BigInteger n = new BigInteger(1, decodeN); + BigInteger e = new BigInteger(1, decodeE); + + RSAPublicKeySpec keySpec = new RSAPublicKeySpec(n, e); + return keyFactory.generatePublic(keySpec); + } +} diff --git a/Core/src/main/java/allchive/server/core/jwt/JwtProperties.java b/Core/src/main/java/allchive/server/core/jwt/JwtProperties.java new file mode 100644 index 00000000..284548b1 --- /dev/null +++ b/Core/src/main/java/allchive/server/core/jwt/JwtProperties.java @@ -0,0 +1,17 @@ +package allchive.server.core.jwt; + + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.ConstructorBinding; + +@Getter +@AllArgsConstructor +@ConstructorBinding +@ConfigurationProperties(prefix = "auth.jwt") +public class JwtProperties { + private String secretKey; + private Long accessExp; + private Long refreshExp; +} diff --git a/Core/src/main/java/allchive/server/core/jwt/JwtTokenProvider.java b/Core/src/main/java/allchive/server/core/jwt/JwtTokenProvider.java new file mode 100644 index 00000000..acde920c --- /dev/null +++ b/Core/src/main/java/allchive/server/core/jwt/JwtTokenProvider.java @@ -0,0 +1,123 @@ +package allchive.server.core.jwt; + +import static allchive.server.core.consts.AllchiveConst.*; + +import allchive.server.core.error.exception.ExpiredRefreshTokenException; +import allchive.server.core.error.exception.ExpiredTokenException; +import allchive.server.core.error.exception.InvalidTokenException; +import allchive.server.core.jwt.dto.AccessTokenInfo; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import java.nio.charset.StandardCharsets; +import java.security.Key; +import java.util.Date; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class JwtTokenProvider { + + private final JwtProperties jwtProperties; + + private Jws parse(String token) { + return Jwts.parserBuilder().setSigningKey(getSecretKey()).build().parseClaimsJws(token); + } + + private Jws getJws(String token) { + try { + return parse(token); + } catch (ExpiredJwtException e) { + throw ExpiredTokenException.EXCEPTION; + } catch (Exception e) { + throw InvalidTokenException.EXCEPTION; + } + } + + private Key getSecretKey() { + return Keys.hmacShaKeyFor(jwtProperties.getSecretKey().getBytes(StandardCharsets.UTF_8)); + } + + private String buildAccessToken( + Long id, Date issuedAt, Date accessTokenExpiresIn, String role) { + final Key encodedKey = getSecretKey(); + return Jwts.builder() + .setIssuer(TOKEN_ISSUER) + .setIssuedAt(issuedAt) + .setSubject(id.toString()) + .claim(TOKEN_TYPE, ACCESS_TOKEN) + .claim(TOKEN_ROLE, role) + .setExpiration(accessTokenExpiresIn) + .signWith(encodedKey) + .compact(); + } + + private String buildRefreshToken(Long id, Date issuedAt, Date accessTokenExpiresIn) { + final Key encodedKey = getSecretKey(); + return Jwts.builder() + .setIssuer(TOKEN_ISSUER) + .setIssuedAt(issuedAt) + .setSubject(id.toString()) + .claim(TOKEN_TYPE, REFRESH_TOKEN) + .setExpiration(accessTokenExpiresIn) + .signWith(encodedKey) + .compact(); + } + + public String generateAccessToken(Long id, String role) { + final Date issuedAt = new Date(); + final Date accessTokenExpiresIn = + new Date(issuedAt.getTime() + jwtProperties.getAccessExp() * MILLI_TO_SECOND); + + return buildAccessToken(id, issuedAt, accessTokenExpiresIn, role); + } + + public String generateRefreshToken(Long id) { + final Date issuedAt = new Date(); + final Date refreshTokenExpiresIn = + new Date(issuedAt.getTime() + jwtProperties.getRefreshExp() * MILLI_TO_SECOND); + return buildRefreshToken(id, issuedAt, refreshTokenExpiresIn); + } + + public boolean isAccessToken(String token) { + return getJws(token).getBody().get(TOKEN_TYPE).equals(ACCESS_TOKEN); + } + + public boolean isRefreshToken(String token) { + return getJws(token).getBody().get(TOKEN_TYPE).equals(REFRESH_TOKEN); + } + + public AccessTokenInfo parseAccessToken(String token) { + if (isAccessToken(token)) { + Claims claims = getJws(token).getBody(); + return AccessTokenInfo.builder() + .userId(Long.parseLong(claims.getSubject())) + .role((String) claims.get(TOKEN_ROLE)) + .build(); + } + throw InvalidTokenException.EXCEPTION; + } + + public Long parseRefreshToken(String token) { + try { + if (isRefreshToken(token)) { + Claims claims = parse(token).getBody(); + return Long.parseLong(claims.getSubject()); + } + } catch (ExpiredJwtException e) { + throw ExpiredRefreshTokenException.EXCEPTION; + } + throw InvalidTokenException.EXCEPTION; + } + + public Long getRefreshTokenTTLSecond() { + return jwtProperties.getRefreshExp(); + } + + public Long getAccessTokenTTLSecond() { + return jwtProperties.getAccessExp(); + } +} diff --git a/Core/src/main/java/allchive/server/core/jwt/dto/AccessTokenInfo.java b/Core/src/main/java/allchive/server/core/jwt/dto/AccessTokenInfo.java new file mode 100644 index 00000000..270d1efa --- /dev/null +++ b/Core/src/main/java/allchive/server/core/jwt/dto/AccessTokenInfo.java @@ -0,0 +1,14 @@ +package allchive.server.core.jwt.dto; + + +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; + +@Getter +@Builder +@ToString +public class AccessTokenInfo { + private final Long userId; + private final String role; +} diff --git a/Core/src/main/java/allchive/server/core/properties/AppleOAuthProperties.java b/Core/src/main/java/allchive/server/core/properties/AppleOAuthProperties.java new file mode 100644 index 00000000..5452eeff --- /dev/null +++ b/Core/src/main/java/allchive/server/core/properties/AppleOAuthProperties.java @@ -0,0 +1,24 @@ +package allchive.server.core.properties; + + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.ConstructorBinding; + +@Getter +@AllArgsConstructor +@ConstructorBinding +@ConfigurationProperties("oauth.apple") +public class AppleOAuthProperties { + private String baseUrl; + private String clientId; + private String appClientId; + private String keyId; + private String redirectUrl; + private String teamId; + private String scope; + private String keyPath; + private String webCallbackUrl; + private String appstoreIssuer; +} diff --git a/Core/src/main/java/allchive/server/core/properties/KakaoOAuthProperties.java b/Core/src/main/java/allchive/server/core/properties/KakaoOAuthProperties.java new file mode 100644 index 00000000..42d76217 --- /dev/null +++ b/Core/src/main/java/allchive/server/core/properties/KakaoOAuthProperties.java @@ -0,0 +1,20 @@ +package allchive.server.core.properties; + + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.ConstructorBinding; + +@Getter +@AllArgsConstructor +@ConstructorBinding +@ConfigurationProperties("oauth.kakao") +public class KakaoOAuthProperties { + private String baseUrl; + private String clientId; + private String clientSecret; + private String redirectUrl; + private String appId; + private String adminKey; +} diff --git a/Core/src/main/java/allchive/server/core/validator/EnumValidator.java b/Core/src/main/java/allchive/server/core/validator/EnumValidator.java new file mode 100644 index 00000000..9c174225 --- /dev/null +++ b/Core/src/main/java/allchive/server/core/validator/EnumValidator.java @@ -0,0 +1,30 @@ +package allchive.server.core.validator; + + +import allchive.server.core.annotation.ValidEnum; +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +public class EnumValidator implements ConstraintValidator { + private ValidEnum annotation; + + @Override + public void initialize(ValidEnum constraintAnnotation) { + this.annotation = constraintAnnotation; + } + + @Override + public boolean isValid(Enum value, ConstraintValidatorContext context) { + if (value == null) return false; + + Object[] enumValues = this.annotation.target().getEnumConstants(); + if (enumValues != null) { + for (Object enumValue : enumValues) { + if (value.toString().equals(enumValue.toString())) { + return true; + } + } + } + return false; + } +} diff --git a/Core/src/main/resources/application-core.yml b/Core/src/main/resources/application-core.yml index e69de29b..443a8af9 100644 --- a/Core/src/main/resources/application-core.yml +++ b/Core/src/main/resources/application-core.yml @@ -0,0 +1,28 @@ + +# jwt 설정 +auth: + jwt: + secret-key: ${JWT_SECRET_KEY:testkeytestkeytestkeytestkeytestkeytestkeytestkeytestkeytestkey} + access-exp: ${JWT_ACCESS_EXP:3600} + refresh-exp: ${JWT_REFRESH_EXP:3600} + +oauth: + kakao: + base-url: ${KAKAO_BASE_URL} + client-id: ${KAKAO_CLIENT} + client-secret: ${KAKAO_SECRET} + redirect-url: ${KAKAO_REDIRECT} + app-id: ${KAKAO_APP_ID} + admin-key: ${KAKAO_ADMIN_KEY} + + apple: + baseUrl: ${APPLE_BASE_URL} + clientId: ${APPLE_CLIENT} + appClientId: ${APPLE_APP_CLIENT_ID} + keyId: ${APPLE_KEY_ID} + redirectUrl: ${APPLE_REDIRECT} + teamId: ${APPLE_TEAM_ID} + scope: ${APPLE_SCOPE} + keyPath: ${APPLE_KEY_PATH} + webCallbackUrl: ${APPLE_WEB_CALLBACK} + appstoreIssuer: ${APPLE_APPSTORE_ISSUER} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..0d49ad7d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM openjdk:17-alpine + +ARG JAR_FILE=Api/build/libs/Api.jar +COPY ${JAR_FILE} app.jar + +ARG PROFILE=dev +ENV PROFILE=${PROFILE} + +ENTRYPOINT ["java","-Dspring.profiles.active=${PROFILE}", "-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"] diff --git a/Domain/build.gradle b/Domain/build.gradle index bdf67e23..81b129c8 100644 --- a/Domain/build.gradle +++ b/Domain/build.gradle @@ -1,25 +1,24 @@ -bootJar.enabled = false -jar.enabled = true +bootJar { enabled = false } +jar { enabled = true } dependencies { - api 'org.springframework.boot:spring-boot-starter-data-jpa' - api 'mysql:mysql-connector-java:8.0.33' - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.2' - testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.2' - - //QueryDsl - implementation "com.querydsl:querydsl-jpa:5.0.0:jakarta" - annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta" - annotationProcessor "jakarta.annotation:jakarta.annotation-api" - annotationProcessor "jakarta.persistence:jakarta.persistence-api" + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'mysql:mysql-connector-java:8.0.33' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-validation' implementation project(':Core') implementation project(':Infrastructure') -} -// Querydsl 설정부 -def generated = 'src/main/generated' + //QueryDsl + api ("com.querydsl:querydsl-core") // querydsl + api ("com.querydsl:querydsl-jpa") // querydsl + annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jpa" // querydsl JPAAnnotationProcessor 사용 지정 + annotationProcessor("jakarta.persistence:jakarta.persistence-api") + annotationProcessor("jakarta.annotation:jakarta.annotation-api") +} +def generated='src/main/generated' sourceSets { main.java.srcDirs += [ generated ] } @@ -30,4 +29,4 @@ tasks.withType(JavaCompile) { clean.doLast { file(generated).deleteDir() -} \ No newline at end of file +} diff --git a/Domain/src/main/java/allchive/server/.gitkeep b/Domain/src/main/java/allchive/server/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/Domain/src/main/java/allchive/server/domain/DomainPackageLocation.java b/Domain/src/main/java/allchive/server/domain/DomainPackageLocation.java new file mode 100644 index 00000000..eafe5767 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/DomainPackageLocation.java @@ -0,0 +1,3 @@ +package allchive.server.domain; + +public interface DomainPackageLocation {} diff --git a/Domain/src/main/java/allchive/server/domain/common/convertor/StringListConverter.java b/Domain/src/main/java/allchive/server/domain/common/convertor/StringListConverter.java new file mode 100644 index 00000000..6f1ae89b --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/common/convertor/StringListConverter.java @@ -0,0 +1,37 @@ +package allchive.server.domain.common.convertor; + + +import allchive.server.core.error.exception.InternalServerError; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.util.List; +import javax.persistence.AttributeConverter; + +public class StringListConverter implements AttributeConverter, String> { + private static final ObjectMapper mapper = + new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .configure(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES, false); + + @Override + public String convertToDatabaseColumn(List attribute) { + try { + return mapper.writeValueAsString(attribute); + } catch (JsonProcessingException e) { + throw InternalServerError.EXCEPTION; + } + } + + @Override + public List convertToEntityAttribute(String dbData) { + TypeReference> typeReference = new TypeReference>() {}; + try { + return mapper.readValue(dbData, typeReference); + } catch (IOException e) { + throw InternalServerError.EXCEPTION; + } + } +} diff --git a/Domain/src/main/java/allchive/server/domain/common/model/BaseTimeEntity.java b/Domain/src/main/java/allchive/server/domain/common/model/BaseTimeEntity.java new file mode 100644 index 00000000..f3a7dc07 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/common/model/BaseTimeEntity.java @@ -0,0 +1,23 @@ +package allchive.server.domain.common.model; + + +import java.time.LocalDateTime; +import javax.persistence.Column; +import javax.persistence.EntityListeners; +import javax.persistence.MappedSuperclass; +import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +@Getter +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public abstract class BaseTimeEntity { + + @Column(updatable = false) + @CreatedDate + private LocalDateTime createdAt; + + @Column @LastModifiedDate private LocalDateTime updatedAt; +} diff --git a/Domain/src/main/java/allchive/server/domain/common/util/SliceUtil.java b/Domain/src/main/java/allchive/server/domain/common/util/SliceUtil.java new file mode 100644 index 00000000..b8bc633e --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/common/util/SliceUtil.java @@ -0,0 +1,25 @@ +package allchive.server.domain.common.util; + + +import java.util.List; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; + +public class SliceUtil { + public static Slice toSlice(List contents, Pageable pageable) { + boolean hasNext = hasNext(contents, pageable); + return new SliceImpl<>( + hasNext ? getContent(contents, pageable) : contents, pageable, hasNext); + } + + // 다음 페이지 있는지 확인 + private static boolean hasNext(List content, Pageable pageable) { + return pageable.isPaged() && content.size() > pageable.getPageSize(); + } + + // 데이터 1개 빼고 반환 + private static List getContent(List content, Pageable pageable) { + return content.subList(0, pageable.getPageSize()); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/config/JpaConfig.java b/Domain/src/main/java/allchive/server/domain/config/JpaConfig.java new file mode 100644 index 00000000..9eb8429d --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/config/JpaConfig.java @@ -0,0 +1,14 @@ +package allchive.server.domain.config; + + +import allchive.server.domain.DomainPackageLocation; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +@Configuration +@EnableJpaAuditing +@EntityScan(basePackageClasses = DomainPackageLocation.class) +@EnableJpaRepositories(basePackageClasses = DomainPackageLocation.class) +public class JpaConfig {} diff --git a/Domain/src/main/java/allchive/server/domain/config/QueryDslConfig.java b/Domain/src/main/java/allchive/server/domain/config/QueryDslConfig.java new file mode 100644 index 00000000..84342642 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/config/QueryDslConfig.java @@ -0,0 +1,18 @@ +package allchive.server.domain.config; + + +import com.querydsl.jpa.impl.JPAQueryFactory; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class QueryDslConfig { + @PersistenceContext private EntityManager entityManager; + + @Bean + public JPAQueryFactory jpaQueryFactory() { + return new JPAQueryFactory(entityManager); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/archiving/adaptor/ArchivingAdaptor.java b/Domain/src/main/java/allchive/server/domain/domains/archiving/adaptor/ArchivingAdaptor.java new file mode 100644 index 00000000..b5c84f34 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/archiving/adaptor/ArchivingAdaptor.java @@ -0,0 +1,91 @@ +package allchive.server.domain.domains.archiving.adaptor; + + +import allchive.server.core.annotation.Adaptor; +import allchive.server.core.error.exception.InternalServerError; +import allchive.server.domain.domains.archiving.domain.Archiving; +import allchive.server.domain.domains.archiving.domain.enums.Category; +import allchive.server.domain.domains.archiving.exception.exceptions.ArchivingNotFoundException; +import allchive.server.domain.domains.archiving.repository.ArchivingRepository; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; + +@Adaptor +@RequiredArgsConstructor +public class ArchivingAdaptor { + private final ArchivingRepository archivingRepository; + + public void save(Archiving archiving) { + if (archiving.getCategory().equals(Category.ALL)) { + throw InternalServerError.EXCEPTION; + } + archivingRepository.save(archiving); + } + + public Archiving findById(Long archivingId) { + return archivingRepository + .findById(archivingId) + .orElseThrow(() -> ArchivingNotFoundException.EXCEPTION); + } + + public Slice querySliceArchivingExceptBlock( + List archivingIdList, + List blockList, + Category category, + Pageable pageable) { + return archivingRepository.querySliceArchivingExceptBlock( + archivingIdList, blockList, category, pageable); + } + + public Slice querySliceArchivingByUserId( + Long userId, Category category, Pageable pageable) { + return archivingRepository.querySliceArchivingByUserId(userId, category, pageable); + } + + public Slice querySliceArchivingByIdIn( + List archivingIdList, Category category, Pageable pageable) { + return archivingRepository.querySliceArchivingByIdIn(archivingIdList, category, pageable); + } + + public List queryArchivingByUserId(Long userId) { + return archivingRepository.queryArchivingByUserId(userId); + } + + public boolean queryArchivingExist(Long archivingId) { + return archivingRepository.queryArchivingExistById(archivingId); + } + + public List findAllByUserId(Long userId) { + return archivingRepository.findAllByUserId(userId); + } + + public List findAllByIdIn(List archivingIdList) { + return archivingRepository.findAllByIdIn(archivingIdList); + } + + public void saveAll(List archivings) { + archivingRepository.saveAll(archivings); + } + + public void deleteAllById(List archivingIds) { + archivingRepository.deleteAllById(archivingIds); + } + + public Slice querySliceArchivingByUserIdAndKeywords( + Long userId, String keyword, Pageable pageable) { + return archivingRepository.querySliceArchivingByUserIdAndKeywords( + userId, keyword, pageable); + } + + public Slice querySliceArchivingByKeywordExceptBlock( + List archivingIdList, List blockList, String keyword, Pageable pageable) { + return archivingRepository.querySliceArchivingByKeywordExceptBlock( + archivingIdList, blockList, keyword, pageable); + } + + public List findAllByPublicStatus(Boolean publicStatus) { + return archivingRepository.findAllByPublicStatus(publicStatus); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/archiving/domain/Archiving.java b/Domain/src/main/java/allchive/server/domain/domains/archiving/domain/Archiving.java new file mode 100644 index 00000000..57919d98 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/archiving/domain/Archiving.java @@ -0,0 +1,119 @@ +package allchive.server.domain.domains.archiving.domain; + + +import allchive.server.domain.common.model.BaseTimeEntity; +import allchive.server.domain.domains.archiving.domain.enums.Category; +import allchive.server.domain.domains.archiving.exception.exceptions.DeletedArchivingException; +import allchive.server.domain.domains.archiving.exception.exceptions.NoAuthurityUpdateArchivingException; +import allchive.server.domain.domains.archiving.exception.exceptions.NotPublicArchivingException; +import java.util.ArrayList; +import java.util.List; +import javax.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Table(name = "tbl_archiving") +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Archiving extends BaseTimeEntity { + @Id + @Column(name = "archiving_id", nullable = false, updatable = false) + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + // 카테고리 만든 사람 + private Long userId; + private String imageUrl; + private String title; + private Boolean publicStatus = Boolean.FALSE; + private Long scrapCnt; + private Long linkCnt; + private Long imgCnt; + private Boolean deleteStatus = Boolean.FALSE; + + @ElementCollection + @CollectionTable(name = "tbl_archiving_pin", joinColumns = @JoinColumn(name = "archiving_id")) + private List pinUserId = new ArrayList<>(); + + @Enumerated(EnumType.STRING) + private Category category; + + @Builder + private Archiving( + Long userId, + String imageUrl, + String title, + boolean publicStatus, + boolean deleteStatus, + Category category) { + this.userId = userId; + this.imageUrl = imageUrl; + this.title = title; + this.publicStatus = publicStatus; + this.deleteStatus = deleteStatus; + this.category = category; + this.scrapCnt = 0L; + this.linkCnt = 0L; + this.imgCnt = 0L; + } + + public static Archiving of( + Long userId, String title, String imageUrl, boolean publicStatus, Category category) { + return Archiving.builder() + .userId(userId) + .imageUrl(imageUrl) + .title(title) + .publicStatus(publicStatus) + .deleteStatus(Boolean.FALSE) + .category(category) + .build(); + } + + public void update(String title, String imageUrl, boolean publicStatus, Category category) { + this.title = title; + this.imageUrl = imageUrl; + this.publicStatus = publicStatus; + this.category = category; + } + + public void validateUser(Long userId) { + if (!this.userId.equals(userId)) { + throw NoAuthurityUpdateArchivingException.EXCEPTION; + } + } + + public void validatePublicStatus(Long userId) { + if (!this.publicStatus && !this.userId.equals(userId)) { + throw NotPublicArchivingException.EXCEPTION; + } + } + + public void validateDeleteStatus(Long userId) { + if (this.deleteStatus && !this.userId.equals(userId)) { + throw DeletedArchivingException.EXCEPTION; + } + } + + public void updateScrapCnt(int i) { + this.scrapCnt += i; + } + + public void addPinUserId(Long userId) { + this.getPinUserId().add(userId); + } + + public void deletePinUserId(Long userId) { + this.getPinUserId().remove(userId); + } + + public void delete() { + this.deleteStatus = Boolean.TRUE; + } + + public void restore() { + this.deleteStatus = Boolean.FALSE; + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/archiving/domain/enums/Category.java b/Domain/src/main/java/allchive/server/domain/domains/archiving/domain/enums/Category.java new file mode 100644 index 00000000..00afb9b4 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/archiving/domain/enums/Category.java @@ -0,0 +1,34 @@ +package allchive.server.domain.domains.archiving.domain.enums; + + +import allchive.server.domain.domains.user.domain.enums.OauthProvider; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import java.util.stream.Stream; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum Category { + ALL("ALL"), + FOOD("FOOD"), + LIFE("LIFE"), + HOME_LIVING("HOME_LIVING"), + SHOPPING("SHOPPING"), + SPORT("SPORT"), + SELF_IMPROVEMENT("SELF_IMPROVEMENT"), + TECH("TECH"), + DESIGN("DESIGN"), + TREND("TREND"); + + @JsonValue private String value; + + @JsonCreator + public static OauthProvider parsing(String inputValue) { + return Stream.of(OauthProvider.values()) + .filter(category -> category.getValue().equals(inputValue)) + .findFirst() + .orElse(null); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/archiving/exception/ArchivingErrorCode.java b/Domain/src/main/java/allchive/server/domain/domains/archiving/exception/ArchivingErrorCode.java new file mode 100644 index 00000000..3ba9d501 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/archiving/exception/ArchivingErrorCode.java @@ -0,0 +1,29 @@ +package allchive.server.domain.domains.archiving.exception; + +import static allchive.server.core.consts.AllchiveConst.*; + +import allchive.server.core.dto.ErrorReason; +import allchive.server.core.error.BaseErrorCode; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum ArchivingErrorCode implements BaseErrorCode { + NOT_PUBLIC_ARCHIVING(BAD_REQUEST, "ARCHIVING_400_1", "공개된 아카이빙이 아닙니다."), + DELETED_ARCHIVING(BAD_REQUEST, "ARCHIVING_400_2", "삭제된 아카이빙입니다."), + ALREADY_PINNED_ARCHIVING(BAD_REQUEST, "ARCHIVING_400_3", "이미 고정 아카이빙입니다."), + NOT_PINNED_ARCHIVING(BAD_REQUEST, "ARCHIVING_400_4", "고정되지않은 아카이빙입니다."), + + NO_AUTHORITY_UPDATE_ARCHIVING(FORBIDDEN, "ARCHIVING_403_1", "아카이빙 수정 권한이 없습니다."), + + ARCHIVING_NOT_FOUND(NOT_FOUND, "ARCHIVING_404_1", "아카이빙을 찾을 수 없습니다."); + private int status; + private String code; + private String reason; + + @Override + public ErrorReason getErrorReason() { + return ErrorReason.of(status, code, reason); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/archiving/exception/exceptions/AlreadyPinnedArchivingException.java b/Domain/src/main/java/allchive/server/domain/domains/archiving/exception/exceptions/AlreadyPinnedArchivingException.java new file mode 100644 index 00000000..609331d1 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/archiving/exception/exceptions/AlreadyPinnedArchivingException.java @@ -0,0 +1,14 @@ +package allchive.server.domain.domains.archiving.exception.exceptions; + + +import allchive.server.core.error.BaseErrorException; +import allchive.server.domain.domains.archiving.exception.ArchivingErrorCode; + +public class AlreadyPinnedArchivingException extends BaseErrorException { + + public static final BaseErrorException EXCEPTION = new AlreadyPinnedArchivingException(); + + private AlreadyPinnedArchivingException() { + super(ArchivingErrorCode.ALREADY_PINNED_ARCHIVING); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/archiving/exception/exceptions/ArchivingNotFoundException.java b/Domain/src/main/java/allchive/server/domain/domains/archiving/exception/exceptions/ArchivingNotFoundException.java new file mode 100644 index 00000000..4b7e820b --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/archiving/exception/exceptions/ArchivingNotFoundException.java @@ -0,0 +1,14 @@ +package allchive.server.domain.domains.archiving.exception.exceptions; + + +import allchive.server.core.error.BaseErrorException; +import allchive.server.domain.domains.archiving.exception.ArchivingErrorCode; + +public class ArchivingNotFoundException extends BaseErrorException { + + public static final BaseErrorException EXCEPTION = new ArchivingNotFoundException(); + + private ArchivingNotFoundException() { + super(ArchivingErrorCode.ARCHIVING_NOT_FOUND); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/archiving/exception/exceptions/DeletedArchivingException.java b/Domain/src/main/java/allchive/server/domain/domains/archiving/exception/exceptions/DeletedArchivingException.java new file mode 100644 index 00000000..decfbf2a --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/archiving/exception/exceptions/DeletedArchivingException.java @@ -0,0 +1,14 @@ +package allchive.server.domain.domains.archiving.exception.exceptions; + + +import allchive.server.core.error.BaseErrorException; +import allchive.server.domain.domains.archiving.exception.ArchivingErrorCode; + +public class DeletedArchivingException extends BaseErrorException { + + public static final BaseErrorException EXCEPTION = new DeletedArchivingException(); + + private DeletedArchivingException() { + super(ArchivingErrorCode.DELETED_ARCHIVING); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/archiving/exception/exceptions/NoAuthurityUpdateArchivingException.java b/Domain/src/main/java/allchive/server/domain/domains/archiving/exception/exceptions/NoAuthurityUpdateArchivingException.java new file mode 100644 index 00000000..28f17267 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/archiving/exception/exceptions/NoAuthurityUpdateArchivingException.java @@ -0,0 +1,14 @@ +package allchive.server.domain.domains.archiving.exception.exceptions; + + +import allchive.server.core.error.BaseErrorException; +import allchive.server.domain.domains.archiving.exception.ArchivingErrorCode; + +public class NoAuthurityUpdateArchivingException extends BaseErrorException { + + public static final BaseErrorException EXCEPTION = new NoAuthurityUpdateArchivingException(); + + private NoAuthurityUpdateArchivingException() { + super(ArchivingErrorCode.NO_AUTHORITY_UPDATE_ARCHIVING); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/archiving/exception/exceptions/NotPinnedArchivingException.java b/Domain/src/main/java/allchive/server/domain/domains/archiving/exception/exceptions/NotPinnedArchivingException.java new file mode 100644 index 00000000..9addcb92 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/archiving/exception/exceptions/NotPinnedArchivingException.java @@ -0,0 +1,14 @@ +package allchive.server.domain.domains.archiving.exception.exceptions; + + +import allchive.server.core.error.BaseErrorException; +import allchive.server.domain.domains.archiving.exception.ArchivingErrorCode; + +public class NotPinnedArchivingException extends BaseErrorException { + + public static final BaseErrorException EXCEPTION = new NotPinnedArchivingException(); + + private NotPinnedArchivingException() { + super(ArchivingErrorCode.NOT_PINNED_ARCHIVING); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/archiving/exception/exceptions/NotPublicArchivingException.java b/Domain/src/main/java/allchive/server/domain/domains/archiving/exception/exceptions/NotPublicArchivingException.java new file mode 100644 index 00000000..29240e3f --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/archiving/exception/exceptions/NotPublicArchivingException.java @@ -0,0 +1,14 @@ +package allchive.server.domain.domains.archiving.exception.exceptions; + + +import allchive.server.core.error.BaseErrorException; +import allchive.server.domain.domains.archiving.exception.ArchivingErrorCode; + +public class NotPublicArchivingException extends BaseErrorException { + + public static final BaseErrorException EXCEPTION = new NotPublicArchivingException(); + + private NotPublicArchivingException() { + super(ArchivingErrorCode.NOT_PUBLIC_ARCHIVING); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/archiving/repository/ArchivingCustomRepository.java b/Domain/src/main/java/allchive/server/domain/domains/archiving/repository/ArchivingCustomRepository.java new file mode 100644 index 00000000..03946df8 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/archiving/repository/ArchivingCustomRepository.java @@ -0,0 +1,28 @@ +package allchive.server.domain.domains.archiving.repository; + + +import allchive.server.domain.domains.archiving.domain.Archiving; +import allchive.server.domain.domains.archiving.domain.enums.Category; +import java.util.List; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; + +public interface ArchivingCustomRepository { + Slice querySliceArchivingExceptBlock( + List archivingIdList, List blockList, Category category, Pageable pageable); + + Slice querySliceArchivingByUserId(Long userId, Category category, Pageable pageable); + + Slice querySliceArchivingByIdIn( + List archivingIdList, Category category, Pageable pageable); + + List queryArchivingByUserId(Long userId); + + boolean queryArchivingExistById(Long archivingId); + + Slice querySliceArchivingByUserIdAndKeywords( + Long userId, String keyword, Pageable pageable); + + Slice querySliceArchivingByKeywordExceptBlock( + List archivingIdList, List blockList, String keyword, Pageable pageable); +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/archiving/repository/ArchivingCustomRepositoryImpl.java b/Domain/src/main/java/allchive/server/domain/domains/archiving/repository/ArchivingCustomRepositoryImpl.java new file mode 100644 index 00000000..c0cae02f --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/archiving/repository/ArchivingCustomRepositoryImpl.java @@ -0,0 +1,186 @@ +package allchive.server.domain.domains.archiving.repository; + +import static allchive.server.domain.domains.archiving.domain.QArchiving.archiving; + +import allchive.server.domain.common.util.SliceUtil; +import allchive.server.domain.domains.archiving.domain.Archiving; +import allchive.server.domain.domains.archiving.domain.enums.Category; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.CaseBuilder; +import com.querydsl.core.types.dsl.NumberExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.time.LocalDateTime; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; + +@RequiredArgsConstructor +public class ArchivingCustomRepositoryImpl implements ArchivingCustomRepository { + private final JPAQueryFactory queryFactory; + + @Override + public Slice querySliceArchivingExceptBlock( + List archivingIdList, + List blockList, + Category category, + Pageable pageable) { + List archivings = + queryFactory + .select(archiving) + .from(archiving) + .where( + userIdNotIn(blockList), + publicStatusTrue(), + categoryEq(category), + deleteStatusFalse()) + .orderBy(scrabListDesc(archivingIdList), scrapCntDesc(), createdAtDesc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize() + 1) + .fetch(); + return SliceUtil.toSlice(archivings, pageable); + } + + @Override + public Slice querySliceArchivingByUserId( + Long userId, Category category, Pageable pageable) { + List archivings = + queryFactory + .select(archiving) + .from(archiving) + .where(userIdEq(userId), categoryEq(category), deleteStatusFalse()) + .orderBy(pinDesc(userId), scrapCntDesc(), createdAtDesc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize() + 1) + .fetch(); + return SliceUtil.toSlice(archivings, pageable); + } + + @Override + public Slice querySliceArchivingByIdIn( + List archivingIdList, Category category, Pageable pageable) { + List archivings = + queryFactory + .select(archiving) + .from(archiving) + .where( + archivingIdListIn(archivingIdList), + publicStatusTrue(), + categoryEq(category), + deleteStatusFalse()) + .orderBy(scrapCntDesc(), createdAtDesc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize() + 1) + .fetch(); + return SliceUtil.toSlice(archivings, pageable); + } + + @Override + public List queryArchivingByUserId(Long userId) { + return queryFactory + .selectFrom(archiving) + .where(userIdEq(userId), deleteStatusFalse()) + .orderBy(categoryDesc(), createdAtDesc()) + .fetch(); + } + + @Override + public boolean queryArchivingExistById(Long archivingId) { + Archiving fetchOne = + queryFactory.selectFrom(archiving).where(archivingIdEq(archivingId)).fetchFirst(); + return fetchOne != null; + } + + @Override + public Slice querySliceArchivingByUserIdAndKeywords( + Long userId, String keyword, Pageable pageable) { + List archivings = + queryFactory + .selectFrom(archiving) + .where(userIdEq(userId), titleContain(keyword)) + .orderBy(createdAtDesc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize() + 1) + .fetch(); + return SliceUtil.toSlice(archivings, pageable); + } + + @Override + public Slice querySliceArchivingByKeywordExceptBlock( + List archivingIdList, List blockList, String keyword, Pageable pageable) { + List archivings = + queryFactory + .select(archiving) + .from(archiving) + .where( + userIdNotIn(blockList), + publicStatusTrue(), + deleteStatusFalse(), + titleContain(keyword)) + .orderBy(scrabListDesc(archivingIdList), scrapCntDesc(), createdAtDesc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize() + 1) + .fetch(); + return SliceUtil.toSlice(archivings, pageable); + } + + private BooleanExpression userIdNotIn(List blockList) { + return archiving.userId.notIn(blockList); + } + + private BooleanExpression publicStatusTrue() { + return archiving.publicStatus.eq(Boolean.TRUE); + } + + private BooleanExpression categoryEq(Category category) { + if (category.equals(Category.ALL)) { + return null; + } + return archiving.category.eq(category); + } + + private BooleanExpression deleteStatusFalse() { + return archiving.deleteStatus.eq(Boolean.FALSE); + } + + private BooleanExpression userIdEq(Long userId) { + return archiving.userId.eq(userId); + } + + private BooleanExpression archivingIdListIn(List archivingIdList) { + return archiving.id.in(archivingIdList); + } + + private BooleanExpression archivingIdEq(Long archivingId) { + return archiving.id.eq(archivingId); + } + + private BooleanExpression titleContain(String keyword) { + return archiving.title.contains(keyword); + } + + private OrderSpecifier scrabListDesc(List archivingIdList) { + NumberExpression pinStatus = + new CaseBuilder().when(archiving.id.in(archivingIdList)).then(1L).otherwise(0L); + return pinStatus.desc(); + } + + private OrderSpecifier pinDesc(Long userId) { + NumberExpression pinStatus = + new CaseBuilder().when(archiving.pinUserId.contains(userId)).then(1L).otherwise(0L); + return pinStatus.desc(); + } + + private OrderSpecifier scrapCntDesc() { + return archiving.scrapCnt.desc(); + } + + private OrderSpecifier createdAtDesc() { + return archiving.createdAt.desc(); + } + + private OrderSpecifier categoryDesc() { + return archiving.category.desc(); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/archiving/repository/ArchivingRepository.java b/Domain/src/main/java/allchive/server/domain/domains/archiving/repository/ArchivingRepository.java new file mode 100644 index 00000000..e6d2c4f3 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/archiving/repository/ArchivingRepository.java @@ -0,0 +1,15 @@ +package allchive.server.domain.domains.archiving.repository; + + +import allchive.server.domain.domains.archiving.domain.Archiving; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ArchivingRepository + extends JpaRepository, ArchivingCustomRepository { + List findAllByUserId(Long userId); + + List findAllByIdIn(List ids); + + List findAllByPublicStatus(Boolean publicStatus); +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/archiving/service/ArchivingDomainService.java b/Domain/src/main/java/allchive/server/domain/domains/archiving/service/ArchivingDomainService.java new file mode 100644 index 00000000..ffcc9f1f --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/archiving/service/ArchivingDomainService.java @@ -0,0 +1,61 @@ +package allchive.server.domain.domains.archiving.service; + + +import allchive.server.core.annotation.DomainService; +import allchive.server.domain.domains.archiving.adaptor.ArchivingAdaptor; +import allchive.server.domain.domains.archiving.domain.Archiving; +import allchive.server.domain.domains.archiving.domain.enums.Category; +import java.util.List; +import lombok.RequiredArgsConstructor; + +@DomainService +@RequiredArgsConstructor +public class ArchivingDomainService { + private final ArchivingAdaptor archivingAdaptor; + + public void save(Archiving archiving) { + archivingAdaptor.save(archiving); + } + + public void updateArchiving( + Archiving archiving, + String title, + String imageUrl, + boolean publicStatus, + Category category) { + archiving.update(title, imageUrl, publicStatus, category); + archivingAdaptor.save(archiving); + } + + public void updateScrapCount(Long archivingId, int i) { + Archiving archiving = archivingAdaptor.findById(archivingId); + archiving.updateScrapCnt(i); + archivingAdaptor.save(archiving); + } + + public void updatePin(Long archivingId, Long userId, boolean pin) { + Archiving archiving = archivingAdaptor.findById(archivingId); + if (pin) { + archiving.addPinUserId(userId); + } else { + archiving.deletePinUserId(userId); + } + archiving.updateScrapCnt(pin ? 1 : -1); + } + + public void softDeleteById(Long archivingId) { + Archiving archiving = archivingAdaptor.findById(archivingId); + archiving.delete(); + archivingAdaptor.save(archiving); + } + + public void restoreInIdList(List archivingIds) { + List archivings = archivingAdaptor.findAllByIdIn(archivingIds); + archivings.forEach(Archiving::restore); + archivingAdaptor.saveAll(archivings); + } + + public void deleteAllById(List archivingIds) { + archivingAdaptor.deleteAllById(archivingIds); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/archiving/validator/ArchivingValidator.java b/Domain/src/main/java/allchive/server/domain/domains/archiving/validator/ArchivingValidator.java new file mode 100644 index 00000000..0dd6ec08 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/archiving/validator/ArchivingValidator.java @@ -0,0 +1,69 @@ +package allchive.server.domain.domains.archiving.validator; + + +import allchive.server.core.annotation.Validator; +import allchive.server.domain.domains.archiving.adaptor.ArchivingAdaptor; +import allchive.server.domain.domains.archiving.domain.Archiving; +import allchive.server.domain.domains.archiving.exception.exceptions.AlreadyPinnedArchivingException; +import allchive.server.domain.domains.archiving.exception.exceptions.ArchivingNotFoundException; +import allchive.server.domain.domains.archiving.exception.exceptions.NoAuthurityUpdateArchivingException; +import allchive.server.domain.domains.archiving.exception.exceptions.NotPinnedArchivingException; +import java.util.List; +import lombok.RequiredArgsConstructor; + +@Validator +@RequiredArgsConstructor +public class ArchivingValidator { + private final ArchivingAdaptor archivingAdaptor; + + public void verifyUser(Long userId, Long archivingId) { + archivingAdaptor.findById(archivingId).validateUser(userId); + } + + public void validatePublicStatus(Long archivingId, Long userId) { + archivingAdaptor.findById(archivingId).validatePublicStatus(userId); + } + + public void validateDeleteStatus(Long archivingId, Long userId) { + archivingAdaptor.findById(archivingId).validateDeleteStatus(userId); + } + + public void validateExistById(Long archivingId) { + if (!archivingAdaptor.queryArchivingExist(archivingId)) { + throw ArchivingNotFoundException.EXCEPTION; + } + } + + public void validateAlreadyPinStatus(Long archivingId, Long userId) { + if (archivingAdaptor.findById(archivingId).getPinUserId().contains(userId)) { + throw AlreadyPinnedArchivingException.EXCEPTION; + } + } + + public void validateNotPinStatus(Long archivingId, Long userId) { + if (!archivingAdaptor.findById(archivingId).getPinUserId().contains(userId)) { + throw NotPinnedArchivingException.EXCEPTION; + } + } + + public void validateArchivingUser(Long archivingId, Long userId) { + archivingAdaptor.findById(archivingId).validateUser(userId); + } + + public void validateExistInIdList(List archivingIdList) { + List archivingList = archivingAdaptor.findAllByIdIn(archivingIdList); + if (archivingList.size() != archivingIdList.size()) { + throw ArchivingNotFoundException.EXCEPTION; + } + } + + public void verifyUserInIdList(Long userId, List archivingIds) { + List archivingList = archivingAdaptor.findAllByIdIn(archivingIds); + archivingList.forEach( + archiving -> { + if (!archiving.getUserId().equals(userId)) { + throw NoAuthurityUpdateArchivingException.EXCEPTION; + } + }); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/block/adaptor/BlockAdaptor.java b/Domain/src/main/java/allchive/server/domain/domains/block/adaptor/BlockAdaptor.java new file mode 100644 index 00000000..a7068d7c --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/block/adaptor/BlockAdaptor.java @@ -0,0 +1,34 @@ +package allchive.server.domain.domains.block.adaptor; + + +import allchive.server.core.annotation.Adaptor; +import allchive.server.domain.domains.block.domain.Block; +import allchive.server.domain.domains.block.repository.BlockRepository; +import java.util.List; +import lombok.RequiredArgsConstructor; + +@Adaptor +@RequiredArgsConstructor +public class BlockAdaptor { + private final BlockRepository blockRepository; + + public List findByBlockFrom(Long userId) { + return blockRepository.findAllByBlockFrom(userId); + } + + public boolean queryBlockExistByBlockFromAndBlockUser(Long blockFrom, Long blockUser) { + return blockRepository.queryBlockExistByBlockFromAndBlockUser(blockFrom, blockUser); + } + + public void save(Block block) { + blockRepository.save(block); + } + + public void deleteByBlockFromAndBlockUser(Long blockFrom, Long blockUser) { + blockRepository.deleteByBlockFromAndBlockUser(blockFrom, blockUser); + } + + public void queryDeleteBlockByBlockFromOrBlockUser(Long userId) { + blockRepository.queryDeleteBlockByBlockFromOrBlockUser(userId); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/block/domain/Block.java b/Domain/src/main/java/allchive/server/domain/domains/block/domain/Block.java new file mode 100644 index 00000000..dcc4e2bf --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/block/domain/Block.java @@ -0,0 +1,36 @@ +package allchive.server.domain.domains.block.domain; + + +import allchive.server.domain.common.model.BaseTimeEntity; +import javax.persistence.*; +import javax.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Table(name = "tbl_block") +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Block extends BaseTimeEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + // block 한 사람 + @NotNull private Long blockFrom; + + // Block 당한 유저 + private Long blockUser; + + @Builder + private Block(Long blockFrom, Long blockUser) { + this.blockFrom = blockFrom; + this.blockUser = blockUser; + } + + public static Block of(Long blockFrom, Long blockUser) { + return Block.builder().blockFrom(blockFrom).blockUser(blockUser).build(); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/block/exception/BlockErrorCode.java b/Domain/src/main/java/allchive/server/domain/domains/block/exception/BlockErrorCode.java new file mode 100644 index 00000000..887dcb38 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/block/exception/BlockErrorCode.java @@ -0,0 +1,26 @@ +package allchive.server.domain.domains.block.exception; + +import static allchive.server.core.consts.AllchiveConst.BAD_REQUEST; +import static allchive.server.core.consts.AllchiveConst.NOT_FOUND; + +import allchive.server.core.dto.ErrorReason; +import allchive.server.core.error.BaseErrorCode; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum BlockErrorCode implements BaseErrorCode { + DUPLICATED_BLOCK(BAD_REQUEST, "BLOCK_400_1", "이미 차단한 유저입니다."), + CANNOT_BLOCK_MYSELF(BAD_REQUEST, "BLOCK_400_2", "본인을 차단할 수 없습니다."), + + BLOCK_NOT_FOUND(NOT_FOUND, "BLOCK_404_1", "차단되지않은 유저입니다."); + private int status; + private String code; + private String reason; + + @Override + public ErrorReason getErrorReason() { + return ErrorReason.of(status, code, reason); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/block/exception/exceptions/BlockNotFoundException.java b/Domain/src/main/java/allchive/server/domain/domains/block/exception/exceptions/BlockNotFoundException.java new file mode 100644 index 00000000..ad9696a1 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/block/exception/exceptions/BlockNotFoundException.java @@ -0,0 +1,14 @@ +package allchive.server.domain.domains.block.exception.exceptions; + + +import allchive.server.core.error.BaseErrorException; +import allchive.server.domain.domains.block.exception.BlockErrorCode; + +public class BlockNotFoundException extends BaseErrorException { + + public static final BaseErrorException EXCEPTION = new BlockNotFoundException(); + + private BlockNotFoundException() { + super(BlockErrorCode.BLOCK_NOT_FOUND); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/block/exception/exceptions/CannotBlockMyselfException.java b/Domain/src/main/java/allchive/server/domain/domains/block/exception/exceptions/CannotBlockMyselfException.java new file mode 100644 index 00000000..aa3c8fec --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/block/exception/exceptions/CannotBlockMyselfException.java @@ -0,0 +1,14 @@ +package allchive.server.domain.domains.block.exception.exceptions; + + +import allchive.server.core.error.BaseErrorException; +import allchive.server.domain.domains.block.exception.BlockErrorCode; + +public class CannotBlockMyselfException extends BaseErrorException { + + public static final BaseErrorException EXCEPTION = new CannotBlockMyselfException(); + + private CannotBlockMyselfException() { + super(BlockErrorCode.CANNOT_BLOCK_MYSELF); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/block/exception/exceptions/DuplicatedBlockException.java b/Domain/src/main/java/allchive/server/domain/domains/block/exception/exceptions/DuplicatedBlockException.java new file mode 100644 index 00000000..b1772083 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/block/exception/exceptions/DuplicatedBlockException.java @@ -0,0 +1,14 @@ +package allchive.server.domain.domains.block.exception.exceptions; + + +import allchive.server.core.error.BaseErrorException; +import allchive.server.domain.domains.block.exception.BlockErrorCode; + +public class DuplicatedBlockException extends BaseErrorException { + + public static final BaseErrorException EXCEPTION = new DuplicatedBlockException(); + + private DuplicatedBlockException() { + super(BlockErrorCode.DUPLICATED_BLOCK); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/block/repository/BlockCustomRepository.java b/Domain/src/main/java/allchive/server/domain/domains/block/repository/BlockCustomRepository.java new file mode 100644 index 00000000..fa0e4a09 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/block/repository/BlockCustomRepository.java @@ -0,0 +1,7 @@ +package allchive.server.domain.domains.block.repository; + +public interface BlockCustomRepository { + boolean queryBlockExistByBlockFromAndBlockUser(Long blockFrom, Long blockUser); + + void queryDeleteBlockByBlockFromOrBlockUser(Long userId); +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/block/repository/BlockCustomRepositoryImpl.java b/Domain/src/main/java/allchive/server/domain/domains/block/repository/BlockCustomRepositoryImpl.java new file mode 100644 index 00000000..d135db43 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/block/repository/BlockCustomRepositoryImpl.java @@ -0,0 +1,40 @@ +package allchive.server.domain.domains.block.repository; + +import static allchive.server.domain.domains.block.domain.QBlock.block; + +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class BlockCustomRepositoryImpl implements BlockCustomRepository { + private final JPAQueryFactory queryFactory; + + @Override + public boolean queryBlockExistByBlockFromAndBlockUser(Long blockFrom, Long blockUser) { + Integer fetchOne = + queryFactory + .selectOne() + .from(block) + .where(blockFromEq(blockFrom), blockUserEq(blockUser)) + .fetchFirst(); // limit 1 + return fetchOne != null; + } + + @Override + public void queryDeleteBlockByBlockFromOrBlockUser(Long userId) { + queryFactory.delete(block).where(blockUserEqOrBlockFromEq(userId)); + } + + private BooleanExpression blockFromEq(Long blockFrom) { + return block.blockFrom.eq(blockFrom); + } + + private BooleanExpression blockUserEq(Long blockUser) { + return block.blockUser.eq(blockUser); + } + + private BooleanExpression blockUserEqOrBlockFromEq(Long userId) { + return blockFromEq(userId).or(blockUserEq(userId)); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/block/repository/BlockRepository.java b/Domain/src/main/java/allchive/server/domain/domains/block/repository/BlockRepository.java new file mode 100644 index 00000000..d911e687 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/block/repository/BlockRepository.java @@ -0,0 +1,12 @@ +package allchive.server.domain.domains.block.repository; + + +import allchive.server.domain.domains.block.domain.Block; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface BlockRepository extends JpaRepository, BlockCustomRepository { + List findAllByBlockFrom(Long userId); + + void deleteByBlockFromAndBlockUser(Long blockFrom, Long blockUser); +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/block/service/BlockDomainService.java b/Domain/src/main/java/allchive/server/domain/domains/block/service/BlockDomainService.java new file mode 100644 index 00000000..bfc7da06 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/block/service/BlockDomainService.java @@ -0,0 +1,25 @@ +package allchive.server.domain.domains.block.service; + + +import allchive.server.core.annotation.DomainService; +import allchive.server.domain.domains.block.adaptor.BlockAdaptor; +import allchive.server.domain.domains.block.domain.Block; +import lombok.RequiredArgsConstructor; + +@DomainService +@RequiredArgsConstructor +public class BlockDomainService { + private final BlockAdaptor blockAdaptor; + + public void save(Block block) { + blockAdaptor.save(block); + } + + public void deleteByBlockFromAndBlockUser(Long blockFrom, Long blockUser) { + blockAdaptor.deleteByBlockFromAndBlockUser(blockFrom, blockUser); + } + + public void queryDeleteBlockByBlockFromOrBlockUser(Long userId) { + blockAdaptor.queryDeleteBlockByBlockFromOrBlockUser(userId); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/block/validator/BlockValidator.java b/Domain/src/main/java/allchive/server/domain/domains/block/validator/BlockValidator.java new file mode 100644 index 00000000..b3926ba1 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/block/validator/BlockValidator.java @@ -0,0 +1,33 @@ +package allchive.server.domain.domains.block.validator; + + +import allchive.server.core.annotation.Validator; +import allchive.server.domain.domains.block.adaptor.BlockAdaptor; +import allchive.server.domain.domains.block.exception.exceptions.BlockNotFoundException; +import allchive.server.domain.domains.block.exception.exceptions.CannotBlockMyselfException; +import allchive.server.domain.domains.block.exception.exceptions.DuplicatedBlockException; +import lombok.RequiredArgsConstructor; + +@Validator +@RequiredArgsConstructor +public class BlockValidator { + private final BlockAdaptor blockAdaptor; + + public void validateNotDuplicate(Long blockFrom, Long blockUser) { + if (blockAdaptor.queryBlockExistByBlockFromAndBlockUser(blockFrom, blockUser)) { + throw DuplicatedBlockException.EXCEPTION; + } + } + + public void validateNotMyself(Long blockFrom, Long blockUser) { + if (blockFrom.equals(blockUser)) { + throw CannotBlockMyselfException.EXCEPTION; + } + } + + public void validateExist(Long blockFrom, Long blockUser) { + if (!blockAdaptor.queryBlockExistByBlockFromAndBlockUser(blockFrom, blockUser)) { + throw BlockNotFoundException.EXCEPTION; + } + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/content/adaptor/ContentAdaptor.java b/Domain/src/main/java/allchive/server/domain/domains/content/adaptor/ContentAdaptor.java new file mode 100644 index 00000000..b14cfdd7 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/content/adaptor/ContentAdaptor.java @@ -0,0 +1,59 @@ +package allchive.server.domain.domains.content.adaptor; + + +import allchive.server.core.annotation.Adaptor; +import allchive.server.domain.domains.content.domain.Content; +import allchive.server.domain.domains.content.exception.exceptions.ContentNotFoundException; +import allchive.server.domain.domains.content.repository.ContentRepository; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; + +@Adaptor +@RequiredArgsConstructor +public class ContentAdaptor { + private final ContentRepository contentRepository; + + public Slice querySliceContentByArchivingId(Long archivingId, Pageable pageable) { + return contentRepository.querySliceContentByArchivingId(archivingId, pageable); + } + + public void save(Content content) { + contentRepository.save(content); + } + + public Content findById(Long contentId) { + return contentRepository + .findById(contentId) + .orElseThrow(() -> ContentNotFoundException.EXCEPTION); + } + + public void deleteById(Long contentId) { + contentRepository.deleteById(contentId); + } + + public List findAllByIdIn(List contentIdList) { + return contentRepository.findAllByIdIn(contentIdList); + } + + public void saveAll(List contentList) { + contentRepository.saveAll(contentList); + } + + public void deleteAllById(List contentIds) { + contentRepository.deleteAllById(contentIds); + } + + public List findAllByArchivingIds(List archivingIds) { + return contentRepository.queryContentInArchivingIds(archivingIds); + } + + public boolean queryContentExistById(Long contentId) { + return contentRepository.queryContentExistById(contentId); + } + + public void deleteAllByArchivingIdIn(List archivingId) { + contentRepository.deleteAllByArchivingIdIn(archivingId); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/content/adaptor/ContentTagGroupAdaptor.java b/Domain/src/main/java/allchive/server/domain/domains/content/adaptor/ContentTagGroupAdaptor.java new file mode 100644 index 00000000..942c917e --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/content/adaptor/ContentTagGroupAdaptor.java @@ -0,0 +1,44 @@ +package allchive.server.domain.domains.content.adaptor; + + +import allchive.server.core.annotation.Adaptor; +import allchive.server.domain.domains.content.domain.Content; +import allchive.server.domain.domains.content.domain.ContentTagGroup; +import allchive.server.domain.domains.content.domain.Tag; +import allchive.server.domain.domains.content.repository.ContentTagGroupRepository; +import java.util.List; +import lombok.RequiredArgsConstructor; + +@Adaptor +@RequiredArgsConstructor +public class ContentTagGroupAdaptor { + private final ContentTagGroupRepository contentTagGroupRepository; + + public List queryContentTagGroupByContentIn(List contentList) { + return contentTagGroupRepository.queryContentTagGroupByContentIn(contentList); + } + + public void deleteByTag(Tag tag) { + contentTagGroupRepository.deleteByTag(tag); + } + + public void saveAll(List contentTagGroupList) { + contentTagGroupRepository.saveAll(contentTagGroupList); + } + + public void deleteAllByContentIn(List contents) { + contentTagGroupRepository.deleteAllByContentIn(contents); + } + + public void deleteAllByTagIn(List tagList) { + contentTagGroupRepository.deleteAllByTagIn(tagList); + } + + public List queryContentTagGroupByContentWithTag(Content content) { + return contentTagGroupRepository.queryContentTagGroupByContentWithTag(content); + } + + public void deleteAllByContent(Content content) { + contentTagGroupRepository.deleteAllByContent(content); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/content/adaptor/TagAdaptor.java b/Domain/src/main/java/allchive/server/domain/domains/content/adaptor/TagAdaptor.java new file mode 100644 index 00000000..5b6e53ae --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/content/adaptor/TagAdaptor.java @@ -0,0 +1,47 @@ +package allchive.server.domain.domains.content.adaptor; + + +import allchive.server.core.annotation.Adaptor; +import allchive.server.domain.domains.content.domain.Tag; +import allchive.server.domain.domains.content.exception.exceptions.TagNotFoundException; +import allchive.server.domain.domains.content.repository.TagRepository; +import java.util.List; +import lombok.RequiredArgsConstructor; + +@Adaptor +@RequiredArgsConstructor +public class TagAdaptor { + private final TagRepository tagRepository; + + public List queryTagByUserIdOrderByUsedAt(Long userId) { + return tagRepository.queryTagByUserIdOrderByUsedAt(userId); + } + + public List findAllByUserIdOrderByCreatedAtDesc(Long userId) { + return tagRepository.findAllByUserIdOrderByCreatedAtDesc(userId); + } + + public void save(Tag tag) { + tagRepository.save(tag); + } + + public Tag findById(Long tagId) { + return tagRepository.findById(tagId).orElseThrow(() -> TagNotFoundException.EXCEPTION); + } + + public void deleteById(Long tagId) { + tagRepository.deleteById(tagId); + } + + public List queryTagByTagIdIn(List tagIds) { + return tagRepository.queryTagByTagIdIn(tagIds); + } + + public List findAllByUserId(Long userId) { + return tagRepository.findAllByUserId(userId); + } + + public void deleteAll(List tagList) { + tagRepository.deleteAll(tagList); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/content/domain/Content.java b/Domain/src/main/java/allchive/server/domain/domains/content/domain/Content.java new file mode 100644 index 00000000..7d1280d5 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/content/domain/Content.java @@ -0,0 +1,92 @@ +package allchive.server.domain.domains.content.domain; + + +import allchive.server.domain.common.model.BaseTimeEntity; +import allchive.server.domain.domains.content.domain.enums.ContentType; +import javax.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Table(name = "tbl_content") +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Content extends BaseTimeEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private Long archivingId; + + @Enumerated(EnumType.STRING) + private ContentType contentType; + + private String imageUrl; + private String linkUrl; + private String title; + + @Column(columnDefinition = "TEXT") + private String memo; + + private boolean deleteStatus = Boolean.FALSE; + + @Builder + private Content( + Long archivingId, + ContentType contentType, + String imageUrl, + String linkUrl, + String title, + String memo, + boolean deleteStatus) { + this.archivingId = archivingId; + this.contentType = contentType; + this.imageUrl = imageUrl; + this.linkUrl = linkUrl; + this.title = title; + this.memo = memo; + this.deleteStatus = deleteStatus; + } + + public static Content of( + Long archivingId, + ContentType contentType, + String imageUrl, + String linkUrl, + String title, + String memo) { + return Content.builder() + .archivingId(archivingId) + .contentType(contentType) + .imageUrl(imageUrl) + .linkUrl(linkUrl) + .title(title) + .memo(memo) + .deleteStatus(Boolean.FALSE) + .build(); + } + + public void delete() { + this.deleteStatus = Boolean.TRUE; + } + + public void restore() { + this.deleteStatus = Boolean.FALSE; + } + + public void updateLinkContent(Long archivingId, String link, String title, String memo) { + this.archivingId = archivingId; + this.linkUrl = link; + this.title = title; + this.memo = memo; + } + + public void updateImageContent(Long archivingId, String imgUrl, String title, String memo) { + this.archivingId = archivingId; + this.imageUrl = imgUrl; + this.title = title; + this.memo = memo; + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/content/domain/ContentTagGroup.java b/Domain/src/main/java/allchive/server/domain/domains/content/domain/ContentTagGroup.java new file mode 100644 index 00000000..abe83c06 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/content/domain/ContentTagGroup.java @@ -0,0 +1,35 @@ +package allchive.server.domain.domains.content.domain; + + +import allchive.server.domain.common.model.BaseTimeEntity; +import javax.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Table(name = "tbl_content_tag_group") +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ContentTagGroup extends BaseTimeEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Content content; + + @ManyToOne(fetch = FetchType.LAZY) + private Tag tag; + + @Builder + private ContentTagGroup(Content content, Tag tag) { + this.content = content; + this.tag = tag; + } + + public static ContentTagGroup of(Content content, Tag tag) { + return ContentTagGroup.builder().content(content).tag(tag).build(); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/content/domain/Tag.java b/Domain/src/main/java/allchive/server/domain/domains/content/domain/Tag.java new file mode 100644 index 00000000..0f0981f6 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/content/domain/Tag.java @@ -0,0 +1,47 @@ +package allchive.server.domain.domains.content.domain; + + +import allchive.server.domain.common.model.BaseTimeEntity; +import allchive.server.domain.domains.content.exception.exceptions.NoAuthorityUpdateTagException; +import java.time.LocalDateTime; +import javax.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Table(name = "tbl_tag") +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Tag extends BaseTimeEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + private Long userId; + private LocalDateTime usedAt; + + @Builder + private Tag(String name, Long userId, LocalDateTime usedAt) { + this.name = name; + this.userId = userId; + this.usedAt = usedAt; + } + + @Builder + public static Tag of(String name, Long userId) { + return Tag.builder().name(name).userId(userId).usedAt(null).build(); + } + + public void validateUser(Long userId) { + if (!this.userId.equals(userId)) { + throw NoAuthorityUpdateTagException.EXCEPTION; + } + } + + public void updateName(String name) { + this.name = name; + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/content/domain/enums/ContentType.java b/Domain/src/main/java/allchive/server/domain/domains/content/domain/enums/ContentType.java new file mode 100644 index 00000000..fbec1562 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/content/domain/enums/ContentType.java @@ -0,0 +1,26 @@ +package allchive.server.domain.domains.content.domain.enums; + + +import allchive.server.domain.domains.user.domain.enums.OauthProvider; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import java.util.stream.Stream; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum ContentType { + IMAGE("image"), + LINK("link"); + + @JsonValue private String value; + + @JsonCreator + public static OauthProvider parsing(String inputValue) { + return Stream.of(OauthProvider.values()) + .filter(type -> type.getValue().equals(inputValue)) + .findFirst() + .orElse(null); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/content/exception/ContentErrorCode.java b/Domain/src/main/java/allchive/server/domain/domains/content/exception/ContentErrorCode.java new file mode 100644 index 00000000..58d96d31 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/content/exception/ContentErrorCode.java @@ -0,0 +1,30 @@ +package allchive.server.domain.domains.content.exception; + +import static allchive.server.core.consts.AllchiveConst.FORBIDDEN; +import static allchive.server.core.consts.AllchiveConst.NOT_FOUND; + +import allchive.server.core.dto.ErrorReason; +import allchive.server.core.error.BaseErrorCode; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum ContentErrorCode implements BaseErrorCode { + CONTENT_NOT_FOUND(NOT_FOUND, "CONTENT_404_1", "카테고리를 찾을 수 없습니다."), + + NO_AUTHORITY_UPDATE_CONTENT(FORBIDDEN, "TAG_403_1", "컨텐츠 수정 권한이 없습니다."), + + TAG_NOT_FOUND(NOT_FOUND, "TAG_404_1", "태그를 찾을 수 없습니다."), + + NO_AUTHORITY_UPDATE_TAG(FORBIDDEN, "TAG_403_1", "태그 수정 권한이 없습니다."); + + private int status; + private String code; + private String reason; + + @Override + public ErrorReason getErrorReason() { + return ErrorReason.of(status, code, reason); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/content/exception/exceptions/ContentNotFoundException.java b/Domain/src/main/java/allchive/server/domain/domains/content/exception/exceptions/ContentNotFoundException.java new file mode 100644 index 00000000..7f7b0b4e --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/content/exception/exceptions/ContentNotFoundException.java @@ -0,0 +1,14 @@ +package allchive.server.domain.domains.content.exception.exceptions; + + +import allchive.server.core.error.BaseErrorException; +import allchive.server.domain.domains.content.exception.ContentErrorCode; + +public class ContentNotFoundException extends BaseErrorException { + + public static final BaseErrorException EXCEPTION = new ContentNotFoundException(); + + private ContentNotFoundException() { + super(ContentErrorCode.CONTENT_NOT_FOUND); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/content/exception/exceptions/NoAuthorityUpdateContentException.java b/Domain/src/main/java/allchive/server/domain/domains/content/exception/exceptions/NoAuthorityUpdateContentException.java new file mode 100644 index 00000000..43c62176 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/content/exception/exceptions/NoAuthorityUpdateContentException.java @@ -0,0 +1,14 @@ +package allchive.server.domain.domains.content.exception.exceptions; + + +import allchive.server.core.error.BaseErrorException; +import allchive.server.domain.domains.content.exception.ContentErrorCode; + +public class NoAuthorityUpdateContentException extends BaseErrorException { + + public static final BaseErrorException EXCEPTION = new NoAuthorityUpdateContentException(); + + private NoAuthorityUpdateContentException() { + super(ContentErrorCode.NO_AUTHORITY_UPDATE_CONTENT); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/content/exception/exceptions/NoAuthorityUpdateException.java b/Domain/src/main/java/allchive/server/domain/domains/content/exception/exceptions/NoAuthorityUpdateException.java new file mode 100644 index 00000000..958a4901 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/content/exception/exceptions/NoAuthorityUpdateException.java @@ -0,0 +1,14 @@ +package allchive.server.domain.domains.content.exception.exceptions; + + +import allchive.server.core.error.BaseErrorException; +import allchive.server.domain.domains.content.exception.ContentErrorCode; + +public class NoAuthorityUpdateException extends BaseErrorException { + + public static final BaseErrorException EXCEPTION = new NoAuthorityUpdateException(); + + private NoAuthorityUpdateException() { + super(ContentErrorCode.NO_AUTHORITY_UPDATE_TAG); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/content/exception/exceptions/NoAuthorityUpdateTagException.java b/Domain/src/main/java/allchive/server/domain/domains/content/exception/exceptions/NoAuthorityUpdateTagException.java new file mode 100644 index 00000000..5e1a907a --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/content/exception/exceptions/NoAuthorityUpdateTagException.java @@ -0,0 +1,14 @@ +package allchive.server.domain.domains.content.exception.exceptions; + + +import allchive.server.core.error.BaseErrorException; +import allchive.server.domain.domains.content.exception.ContentErrorCode; + +public class NoAuthorityUpdateTagException extends BaseErrorException { + + public static final BaseErrorException EXCEPTION = new NoAuthorityUpdateTagException(); + + private NoAuthorityUpdateTagException() { + super(ContentErrorCode.NO_AUTHORITY_UPDATE_TAG); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/content/exception/exceptions/TagNotFoundException.java b/Domain/src/main/java/allchive/server/domain/domains/content/exception/exceptions/TagNotFoundException.java new file mode 100644 index 00000000..ff035a1f --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/content/exception/exceptions/TagNotFoundException.java @@ -0,0 +1,14 @@ +package allchive.server.domain.domains.content.exception.exceptions; + + +import allchive.server.core.error.BaseErrorException; +import allchive.server.domain.domains.content.exception.ContentErrorCode; + +public class TagNotFoundException extends BaseErrorException { + + public static final BaseErrorException EXCEPTION = new TagNotFoundException(); + + private TagNotFoundException() { + super(ContentErrorCode.TAG_NOT_FOUND); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/content/repository/ContentCustomRepository.java b/Domain/src/main/java/allchive/server/domain/domains/content/repository/ContentCustomRepository.java new file mode 100644 index 00000000..34e2fc7a --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/content/repository/ContentCustomRepository.java @@ -0,0 +1,15 @@ +package allchive.server.domain.domains.content.repository; + + +import allchive.server.domain.domains.content.domain.Content; +import java.util.List; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; + +public interface ContentCustomRepository { + Slice querySliceContentByArchivingId(Long archivingId, Pageable pageable); + + List queryContentInArchivingIds(List archivingIds); + + boolean queryContentExistById(Long id); +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/content/repository/ContentCustomRepositoryImpl.java b/Domain/src/main/java/allchive/server/domain/domains/content/repository/ContentCustomRepositoryImpl.java new file mode 100644 index 00000000..91ffbc07 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/content/repository/ContentCustomRepositoryImpl.java @@ -0,0 +1,64 @@ +package allchive.server.domain.domains.content.repository; + +import static allchive.server.domain.domains.content.domain.QContent.content; + +import allchive.server.domain.common.util.SliceUtil; +import allchive.server.domain.domains.content.domain.Content; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.time.LocalDateTime; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; + +@RequiredArgsConstructor +public class ContentCustomRepositoryImpl implements ContentCustomRepository { + private final JPAQueryFactory queryFactory; + + @Override + public Slice querySliceContentByArchivingId(Long archivingId, Pageable pageable) { + List archivings = + queryFactory + .selectFrom(content) + .where(archivingIdEq(archivingId)) + .orderBy(createdAtDesc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize() + 1) + .fetch(); + return SliceUtil.toSlice(archivings, pageable); + } + + @Override + public List queryContentInArchivingIds(List archivingIds) { + return queryFactory + .selectFrom(content) + .where(archivingIdIn(archivingIds)) + .orderBy(createdAtDesc()) + .fetch(); + } + + @Override + public boolean queryContentExistById(Long id) { + Integer fetchOne = + queryFactory.selectOne().from(content).where(idEq(id)).fetchFirst(); // limit 1 + return fetchOne != null; + } + + private BooleanExpression archivingIdEq(Long archivingId) { + return content.archivingId.eq(archivingId); + } + + private BooleanExpression archivingIdIn(List archivingIds) { + return content.archivingId.in(archivingIds); + } + + private BooleanExpression idEq(Long id) { + return content.id.eq(id); + } + + private OrderSpecifier createdAtDesc() { + return content.createdAt.desc(); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/content/repository/ContentRepository.java b/Domain/src/main/java/allchive/server/domain/domains/content/repository/ContentRepository.java new file mode 100644 index 00000000..88540676 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/content/repository/ContentRepository.java @@ -0,0 +1,12 @@ +package allchive.server.domain.domains.content.repository; + + +import allchive.server.domain.domains.content.domain.Content; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ContentRepository extends JpaRepository, ContentCustomRepository { + List findAllByIdIn(List contentIdList); + + void deleteAllByArchivingIdIn(List archivingId); +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/content/repository/ContentTagGroupCustomRepository.java b/Domain/src/main/java/allchive/server/domain/domains/content/repository/ContentTagGroupCustomRepository.java new file mode 100644 index 00000000..546979cb --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/content/repository/ContentTagGroupCustomRepository.java @@ -0,0 +1,12 @@ +package allchive.server.domain.domains.content.repository; + + +import allchive.server.domain.domains.content.domain.Content; +import allchive.server.domain.domains.content.domain.ContentTagGroup; +import java.util.List; + +public interface ContentTagGroupCustomRepository { + public List queryContentTagGroupByContentIn(List contentList); + + List queryContentTagGroupByContentWithTag(Content content); +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/content/repository/ContentTagGroupCustomRepositoryImpl.java b/Domain/src/main/java/allchive/server/domain/domains/content/repository/ContentTagGroupCustomRepositoryImpl.java new file mode 100644 index 00000000..57f47f4a --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/content/repository/ContentTagGroupCustomRepositoryImpl.java @@ -0,0 +1,52 @@ +package allchive.server.domain.domains.content.repository; + +import static allchive.server.domain.domains.content.domain.QContentTagGroup.contentTagGroup; +import static allchive.server.domain.domains.content.domain.QTag.tag; + +import allchive.server.domain.domains.content.domain.Content; +import allchive.server.domain.domains.content.domain.ContentTagGroup; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.time.LocalDateTime; +import java.util.List; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class ContentTagGroupCustomRepositoryImpl implements ContentTagGroupCustomRepository { + private final JPAQueryFactory queryFactory; + + @Override + public List queryContentTagGroupByContentIn(List contentList) { + return queryFactory + .selectFrom(contentTagGroup) + .join(contentTagGroup.tag, tag) + .fetchJoin() + .where(contentIdIn(contentList)) + .orderBy(createdAtDesc()) + .fetch(); + } + + @Override + public List queryContentTagGroupByContentWithTag(Content content) { + return queryFactory + .selectFrom(contentTagGroup) + .join(contentTagGroup.tag, tag) + .fetchJoin() + .where(contentEq(content)) + .orderBy(createdAtDesc()) + .fetch(); + } + + private BooleanExpression contentIdIn(List contentList) { + return contentTagGroup.content.in(contentList); + } + + private BooleanExpression contentEq(Content content) { + return contentTagGroup.content.eq(content); + } + + private OrderSpecifier createdAtDesc() { + return contentTagGroup.createdAt.desc(); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/content/repository/ContentTagGroupRepository.java b/Domain/src/main/java/allchive/server/domain/domains/content/repository/ContentTagGroupRepository.java new file mode 100644 index 00000000..ffa84d62 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/content/repository/ContentTagGroupRepository.java @@ -0,0 +1,21 @@ +package allchive.server.domain.domains.content.repository; + + +import allchive.server.domain.domains.content.domain.Content; +import allchive.server.domain.domains.content.domain.ContentTagGroup; +import allchive.server.domain.domains.content.domain.Tag; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ContentTagGroupRepository + extends JpaRepository, ContentTagGroupCustomRepository { + List findAllByContent(Content content); + + void deleteByTag(Tag tag); + + void deleteAllByContentIn(List contents); + + void deleteAllByTagIn(List tagList); + + void deleteAllByContent(Content content); +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/content/repository/TagCustomRepository.java b/Domain/src/main/java/allchive/server/domain/domains/content/repository/TagCustomRepository.java new file mode 100644 index 00000000..3c93394b --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/content/repository/TagCustomRepository.java @@ -0,0 +1,11 @@ +package allchive.server.domain.domains.content.repository; + + +import allchive.server.domain.domains.content.domain.Tag; +import java.util.List; + +public interface TagCustomRepository { + List queryTagByUserIdOrderByUsedAt(Long userId); + + List queryTagByTagIdIn(List tagIds); +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/content/repository/TagCustomRepositoryImpl.java b/Domain/src/main/java/allchive/server/domain/domains/content/repository/TagCustomRepositoryImpl.java new file mode 100644 index 00000000..5deb5775 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/content/repository/TagCustomRepositoryImpl.java @@ -0,0 +1,46 @@ +package allchive.server.domain.domains.content.repository; + +import static allchive.server.domain.domains.content.domain.QTag.tag; + +import allchive.server.domain.domains.content.domain.Tag; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.time.LocalDateTime; +import java.util.List; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class TagCustomRepositoryImpl implements TagCustomRepository { + private final JPAQueryFactory queryFactory; + + @Override + public List queryTagByUserIdOrderByUsedAt(Long userId) { + return queryFactory + .selectFrom(tag) + .where(usedAtNotNull(), tagUserIdEq(userId)) + .orderBy(createdAtDesc()) + .fetch(); + } + + @Override + public List queryTagByTagIdIn(List tagIds) { + return queryFactory.selectFrom(tag).where(tagIdIn(tagIds)).fetch(); + } + + private BooleanExpression tagUserIdEq(Long userId) { + return tag.userId.eq(userId); + } + + private BooleanExpression usedAtNotNull() { + return tag.usedAt.isNotNull(); + } + + private BooleanExpression tagIdIn(List tagIds) { + return tag.id.in(tagIds); + } + + private OrderSpecifier createdAtDesc() { + return tag.createdAt.desc(); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/content/repository/TagRepository.java b/Domain/src/main/java/allchive/server/domain/domains/content/repository/TagRepository.java new file mode 100644 index 00000000..50911c53 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/content/repository/TagRepository.java @@ -0,0 +1,12 @@ +package allchive.server.domain.domains.content.repository; + + +import allchive.server.domain.domains.content.domain.Tag; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface TagRepository extends JpaRepository, TagCustomRepository { + List findAllByUserIdOrderByCreatedAtDesc(Long userId); + + List findAllByUserId(Long userId); +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/content/service/ContentDomainService.java b/Domain/src/main/java/allchive/server/domain/domains/content/service/ContentDomainService.java new file mode 100644 index 00000000..47c902ed --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/content/service/ContentDomainService.java @@ -0,0 +1,55 @@ +package allchive.server.domain.domains.content.service; + + +import allchive.server.core.annotation.DomainService; +import allchive.server.domain.domains.content.adaptor.ContentAdaptor; +import allchive.server.domain.domains.content.domain.Content; +import allchive.server.domain.domains.content.domain.enums.ContentType; +import java.util.List; +import lombok.RequiredArgsConstructor; + +@DomainService +@RequiredArgsConstructor +public class ContentDomainService { + private final ContentAdaptor contentAdaptor; + + public void save(Content content) { + contentAdaptor.save(content); + } + + public void softDeleteById(Long contentId) { + Content content = contentAdaptor.findById(contentId); + content.delete(); + save(content); + } + + public void restoreInIdList(List contentIds) { + List contentList = contentAdaptor.findAllByIdIn(contentIds); + contentList.forEach(Content::restore); + contentAdaptor.saveAll(contentList); + } + + public void deleteAllById(List contentIds) { + contentAdaptor.deleteAllById(contentIds); + } + + public void deleteAllByArchivingIdIn(List archivingId) { + contentAdaptor.deleteAllByArchivingIdIn(archivingId); + } + + public void update( + Long contentId, + ContentType contentType, + Long archivingId, + String link, + String memo, + String imgUrl, + String title) { + Content content = contentAdaptor.findById(contentId); + switch (contentType) { + case LINK -> content.updateLinkContent(archivingId, link, title, memo); + case IMAGE -> content.updateImageContent(archivingId, imgUrl, title, memo); + } + contentAdaptor.save(content); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/content/service/ContentTagGroupDomainService.java b/Domain/src/main/java/allchive/server/domain/domains/content/service/ContentTagGroupDomainService.java new file mode 100644 index 00000000..55235fc3 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/content/service/ContentTagGroupDomainService.java @@ -0,0 +1,36 @@ +package allchive.server.domain.domains.content.service; + + +import allchive.server.core.annotation.DomainService; +import allchive.server.domain.domains.content.adaptor.ContentTagGroupAdaptor; +import allchive.server.domain.domains.content.domain.Content; +import allchive.server.domain.domains.content.domain.ContentTagGroup; +import allchive.server.domain.domains.content.domain.Tag; +import java.util.List; +import lombok.RequiredArgsConstructor; + +@DomainService +@RequiredArgsConstructor +public class ContentTagGroupDomainService { + private final ContentTagGroupAdaptor contentTagGroupAdaptor; + + public void deleteByTag(Tag tag) { + contentTagGroupAdaptor.deleteByTag(tag); + } + + public void saveAll(List contentTagGroupList) { + contentTagGroupAdaptor.saveAll(contentTagGroupList); + } + + public void deleteByContentIn(List contents) { + contentTagGroupAdaptor.deleteAllByContentIn(contents); + } + + public void deleteAllByTagIn(List tagList) { + contentTagGroupAdaptor.deleteAllByTagIn(tagList); + } + + public void deleteAllByContent(Content content) { + contentTagGroupAdaptor.deleteAllByContent(content); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/content/service/TagDomainService.java b/Domain/src/main/java/allchive/server/domain/domains/content/service/TagDomainService.java new file mode 100644 index 00000000..1ebbe4b0 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/content/service/TagDomainService.java @@ -0,0 +1,32 @@ +package allchive.server.domain.domains.content.service; + + +import allchive.server.core.annotation.DomainService; +import allchive.server.domain.domains.content.adaptor.TagAdaptor; +import allchive.server.domain.domains.content.domain.Tag; +import java.util.List; +import lombok.RequiredArgsConstructor; + +@DomainService +@RequiredArgsConstructor +public class TagDomainService { + private final TagAdaptor tagAdaptor; + + public void save(Tag tag) { + tagAdaptor.save(tag); + } + + public void updateTag(Long tagId, String name) { + Tag tag = tagAdaptor.findById(tagId); + tag.updateName(name); + tagAdaptor.save(tag); + } + + public void deleteById(Long tagId) { + tagAdaptor.deleteById(tagId); + } + + public void deleteAll(List tagList) { + tagAdaptor.deleteAll(tagList); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/content/validator/ContentValidator.java b/Domain/src/main/java/allchive/server/domain/domains/content/validator/ContentValidator.java new file mode 100644 index 00000000..5aea2a35 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/content/validator/ContentValidator.java @@ -0,0 +1,55 @@ +package allchive.server.domain.domains.content.validator; + + +import allchive.server.core.annotation.Validator; +import allchive.server.domain.domains.archiving.adaptor.ArchivingAdaptor; +import allchive.server.domain.domains.archiving.domain.Archiving; +import allchive.server.domain.domains.content.adaptor.ContentAdaptor; +import allchive.server.domain.domains.content.domain.Content; +import allchive.server.domain.domains.content.exception.exceptions.ContentNotFoundException; +import allchive.server.domain.domains.content.exception.exceptions.NoAuthorityUpdateContentException; +import java.util.List; +import lombok.RequiredArgsConstructor; + +@Validator +@RequiredArgsConstructor +public class ContentValidator { + private final ContentAdaptor contentAdaptor; + private final ArchivingAdaptor archivingAdaptor; + + public void validateExistInIdList(List contentIdList) { + List contentList = contentAdaptor.findAllByIdIn(contentIdList); + if (contentList.size() != contentIdList.size()) { + throw ContentNotFoundException.EXCEPTION; + } + } + + public void verifyUserInIdList(Long userId, List contentIds) { + List archivingIds = + contentAdaptor.findAllByIdIn(contentIds).stream() + .map(Content::getArchivingId) + .distinct() + .toList(); + List archivingList = archivingAdaptor.findAllByIdIn(archivingIds); + archivingList.forEach( + archiving -> { + if (!archiving.getUserId().equals(userId)) { + throw NoAuthorityUpdateContentException.EXCEPTION; + } + }); + } + + public void validateExistById(Long contentId) { + if (!contentAdaptor.queryContentExistById(contentId)) { + throw ContentNotFoundException.EXCEPTION; + } + } + + public void verifyUser(Long contentId, Long userId) { + Long archivingId = contentAdaptor.findById(contentId).getArchivingId(); + Long archivingUserId = archivingAdaptor.findById(archivingId).getUserId(); + if (!archivingUserId.equals(userId)) { + throw NoAuthorityUpdateContentException.EXCEPTION; + } + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/content/validator/TagValidator.java b/Domain/src/main/java/allchive/server/domain/domains/content/validator/TagValidator.java new file mode 100644 index 00000000..af25a203 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/content/validator/TagValidator.java @@ -0,0 +1,33 @@ +package allchive.server.domain.domains.content.validator; + + +import allchive.server.core.annotation.Validator; +import allchive.server.domain.domains.content.adaptor.TagAdaptor; +import allchive.server.domain.domains.content.domain.Tag; +import allchive.server.domain.domains.content.exception.exceptions.NoAuthorityUpdateTagException; +import allchive.server.domain.domains.content.exception.exceptions.TagNotFoundException; +import java.util.List; +import lombok.RequiredArgsConstructor; + +@Validator +@RequiredArgsConstructor +public class TagValidator { + private final TagAdaptor tagAdaptor; + + public void verifyUser(Long tagId, Long userId) { + tagAdaptor.findById(tagId).validateUser(userId); + } + + public void validateExistTagsAndUser(List tagIds, Long userId) { + List tags = tagAdaptor.queryTagByTagIdIn(tagIds); + if (tagIds.size() != tags.size()) { + throw TagNotFoundException.EXCEPTION; + } + tags.forEach( + tag -> { + if (!tag.getUserId().equals(userId)) { + throw NoAuthorityUpdateTagException.EXCEPTION; + } + }); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/recycle/adaptor/RecycleAdaptor.java b/Domain/src/main/java/allchive/server/domain/domains/recycle/adaptor/RecycleAdaptor.java new file mode 100644 index 00000000..b2055e21 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/recycle/adaptor/RecycleAdaptor.java @@ -0,0 +1,41 @@ +package allchive.server.domain.domains.recycle.adaptor; + + +import allchive.server.core.annotation.Adaptor; +import allchive.server.domain.domains.recycle.domain.Recycle; +import allchive.server.domain.domains.recycle.repository.RecycleRepository; +import java.time.LocalDateTime; +import java.util.List; +import lombok.RequiredArgsConstructor; + +@Adaptor +@RequiredArgsConstructor +public class RecycleAdaptor { + private final RecycleRepository recycleRepository; + + public void save(Recycle recycle) { + recycleRepository.save(recycle); + } + + public List queryRecycleByUserIdInArchivingIdListAndContentIdList( + List archivingIds, List contentIds, Long userId) { + return recycleRepository.queryRecycleByUserIdInArchivingIdListAndContentIdList( + archivingIds, contentIds, userId); + } + + public void deleteAll(List recycleList) { + recycleRepository.deleteAll(recycleList); + } + + public List findAllByUserId(Long userId) { + return recycleRepository.findAllByUserId(userId); + } + + public List findAllByDeletedAtBefore(LocalDateTime deleteStandard) { + return recycleRepository.findAllByDeletedAtBefore(deleteStandard); + } + + public void deleteAllByUserId(Long userId) { + recycleRepository.deleteAllByUserId(userId); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/recycle/domain/Recycle.java b/Domain/src/main/java/allchive/server/domain/domains/recycle/domain/Recycle.java new file mode 100644 index 00000000..b888aef8 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/recycle/domain/Recycle.java @@ -0,0 +1,54 @@ +package allchive.server.domain.domains.recycle.domain; + + +import allchive.server.domain.common.model.BaseTimeEntity; +import allchive.server.domain.domains.recycle.domain.enums.RecycleType; +import java.time.LocalDateTime; +import javax.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Table(name = "tbl_recycle") +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Recycle extends BaseTimeEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Enumerated(EnumType.STRING) + private RecycleType recycleType; + + private Long contentId; + private Long archivingId; + private Long userId; + private LocalDateTime deletedAt; + + @Builder + private Recycle( + RecycleType recycleType, + Long contentId, + Long archivingId, + Long userId, + LocalDateTime deletedAt) { + this.recycleType = recycleType; + this.contentId = contentId; + this.archivingId = archivingId; + this.userId = userId; + this.deletedAt = deletedAt; + } + + public static Recycle of( + RecycleType recycleType, Long contentId, Long archivingId, Long userId) { + return Recycle.builder() + .archivingId(archivingId) + .contentId(contentId) + .recycleType(recycleType) + .userId(userId) + .deletedAt(LocalDateTime.now()) + .build(); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/recycle/domain/enums/RecycleType.java b/Domain/src/main/java/allchive/server/domain/domains/recycle/domain/enums/RecycleType.java new file mode 100644 index 00000000..4b978a54 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/recycle/domain/enums/RecycleType.java @@ -0,0 +1,26 @@ +package allchive.server.domain.domains.recycle.domain.enums; + + +import allchive.server.domain.domains.user.domain.enums.OauthProvider; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import java.util.stream.Stream; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum RecycleType { + CONTENT("content"), + ARCHIVING("archiving"); + + @JsonValue private String value; + + @JsonCreator + public static OauthProvider parsing(String inputValue) { + return Stream.of(OauthProvider.values()) + .filter(type -> type.getValue().equals(inputValue)) + .findFirst() + .orElse(null); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/recycle/exception/RecycleErrorCode.java b/Domain/src/main/java/allchive/server/domain/domains/recycle/exception/RecycleErrorCode.java new file mode 100644 index 00000000..16f12274 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/recycle/exception/RecycleErrorCode.java @@ -0,0 +1,24 @@ +package allchive.server.domain.domains.recycle.exception; + +import static allchive.server.core.consts.AllchiveConst.NOT_FOUND; + +import allchive.server.core.dto.ErrorReason; +import allchive.server.core.error.BaseErrorCode; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum RecycleErrorCode implements BaseErrorCode { + RECYCLE_CONTENT_NOT_FOUND(NOT_FOUND, "RECYCLE_404_1", "삭제된 컨텐츠를 찾을 수 없습니다."), + RECYCLE_ARCHIVING_NOT_FOUND(NOT_FOUND, "RECYCLE_404_2", "삭제된 아카이빙을 찾을 수 없습니다."), + ; + private int status; + private String code; + private String reason; + + @Override + public ErrorReason getErrorReason() { + return ErrorReason.of(status, code, reason); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/recycle/exception/exceptions/RecycleArchivingNotFoundException.java b/Domain/src/main/java/allchive/server/domain/domains/recycle/exception/exceptions/RecycleArchivingNotFoundException.java new file mode 100644 index 00000000..177b461b --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/recycle/exception/exceptions/RecycleArchivingNotFoundException.java @@ -0,0 +1,14 @@ +package allchive.server.domain.domains.recycle.exception.exceptions; + + +import allchive.server.core.error.BaseErrorException; +import allchive.server.domain.domains.recycle.exception.RecycleErrorCode; + +public class RecycleArchivingNotFoundException extends BaseErrorException { + + public static final BaseErrorException EXCEPTION = new RecycleArchivingNotFoundException(); + + private RecycleArchivingNotFoundException() { + super(RecycleErrorCode.RECYCLE_ARCHIVING_NOT_FOUND); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/recycle/exception/exceptions/RecycleContentNotFoundException.java b/Domain/src/main/java/allchive/server/domain/domains/recycle/exception/exceptions/RecycleContentNotFoundException.java new file mode 100644 index 00000000..0301bce2 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/recycle/exception/exceptions/RecycleContentNotFoundException.java @@ -0,0 +1,14 @@ +package allchive.server.domain.domains.recycle.exception.exceptions; + + +import allchive.server.core.error.BaseErrorException; +import allchive.server.domain.domains.recycle.exception.RecycleErrorCode; + +public class RecycleContentNotFoundException extends BaseErrorException { + + public static final BaseErrorException EXCEPTION = new RecycleContentNotFoundException(); + + private RecycleContentNotFoundException() { + super(RecycleErrorCode.RECYCLE_CONTENT_NOT_FOUND); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/recycle/repository/RecycleCustomRepository.java b/Domain/src/main/java/allchive/server/domain/domains/recycle/repository/RecycleCustomRepository.java new file mode 100644 index 00000000..59d537b8 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/recycle/repository/RecycleCustomRepository.java @@ -0,0 +1,10 @@ +package allchive.server.domain.domains.recycle.repository; + + +import allchive.server.domain.domains.recycle.domain.Recycle; +import java.util.List; + +public interface RecycleCustomRepository { + List queryRecycleByUserIdInArchivingIdListAndContentIdList( + List archivingIds, List contentIds, Long userId); +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/recycle/repository/RecycleCustomRepositoryImpl.java b/Domain/src/main/java/allchive/server/domain/domains/recycle/repository/RecycleCustomRepositoryImpl.java new file mode 100644 index 00000000..682ce446 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/recycle/repository/RecycleCustomRepositoryImpl.java @@ -0,0 +1,35 @@ +package allchive.server.domain.domains.recycle.repository; + +import static allchive.server.domain.domains.recycle.domain.QRecycle.recycle; + +import allchive.server.domain.domains.recycle.domain.Recycle; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.time.LocalDateTime; +import java.util.List; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class RecycleCustomRepositoryImpl implements RecycleCustomRepository { + private final JPAQueryFactory queryFactory; + + @Override + public List queryRecycleByUserIdInArchivingIdListAndContentIdList( + List archivingIds, List contentIds, Long userId) { + return queryFactory + .selectFrom(recycle) + .where(archivingIdInOrContentIdIn(archivingIds, contentIds)) + .orderBy(createdAtDesc()) + .fetch(); + } + + private BooleanExpression archivingIdInOrContentIdIn( + List archivingIdList, List contentIdList) { + return recycle.archivingId.in(archivingIdList).or(recycle.contentId.in(contentIdList)); + } + + private OrderSpecifier createdAtDesc() { + return recycle.createdAt.desc(); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/recycle/repository/RecycleRepository.java b/Domain/src/main/java/allchive/server/domain/domains/recycle/repository/RecycleRepository.java new file mode 100644 index 00000000..a94c960a --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/recycle/repository/RecycleRepository.java @@ -0,0 +1,15 @@ +package allchive.server.domain.domains.recycle.repository; + + +import allchive.server.domain.domains.recycle.domain.Recycle; +import java.time.LocalDateTime; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface RecycleRepository extends JpaRepository, RecycleCustomRepository { + List findAllByUserId(Long userId); + + List findAllByDeletedAtBefore(LocalDateTime time); + + void deleteAllByUserId(Long userId); +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/recycle/service/RecycleDomainService.java b/Domain/src/main/java/allchive/server/domain/domains/recycle/service/RecycleDomainService.java new file mode 100644 index 00000000..9763eae5 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/recycle/service/RecycleDomainService.java @@ -0,0 +1,34 @@ +package allchive.server.domain.domains.recycle.service; + + +import allchive.server.core.annotation.DomainService; +import allchive.server.domain.domains.recycle.adaptor.RecycleAdaptor; +import allchive.server.domain.domains.recycle.domain.Recycle; +import java.util.List; +import lombok.RequiredArgsConstructor; + +@DomainService +@RequiredArgsConstructor +public class RecycleDomainService { + private final RecycleAdaptor recycleAdaptor; + + public void save(Recycle recycle) { + recycleAdaptor.save(recycle); + } + + public void deleteAllByUserIdAndArchivingIdOrUserIdAndContentId( + List archivingIds, List contentIds, Long userId) { + List recycleList = + recycleAdaptor.queryRecycleByUserIdInArchivingIdListAndContentIdList( + archivingIds, contentIds, userId); + recycleAdaptor.deleteAll(recycleList); + } + + public void deleteAll(List recycles) { + recycleAdaptor.deleteAll(recycles); + } + + public void deleteAllByUserId(Long userId) { + recycleAdaptor.deleteAllByUserId(userId); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/recycle/validator/RecycleValidator.java b/Domain/src/main/java/allchive/server/domain/domains/recycle/validator/RecycleValidator.java new file mode 100644 index 00000000..18c7e55e --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/recycle/validator/RecycleValidator.java @@ -0,0 +1,37 @@ +package allchive.server.domain.domains.recycle.validator; + + +import allchive.server.core.annotation.Validator; +import allchive.server.domain.domains.recycle.adaptor.RecycleAdaptor; +import allchive.server.domain.domains.recycle.domain.Recycle; +import allchive.server.domain.domains.recycle.domain.enums.RecycleType; +import allchive.server.domain.domains.recycle.exception.exceptions.RecycleArchivingNotFoundException; +import allchive.server.domain.domains.recycle.exception.exceptions.RecycleContentNotFoundException; +import java.util.List; +import lombok.RequiredArgsConstructor; + +@Validator +@RequiredArgsConstructor +public class RecycleValidator { + private final RecycleAdaptor recycleAdaptor; + + public void validateExist(List archivingIds, List contentIds, Long userId) { + List recycleList = + recycleAdaptor.queryRecycleByUserIdInArchivingIdListAndContentIdList( + archivingIds, contentIds, userId); + Long archivingCnt = + recycleList.stream() + .filter(recycle -> recycle.getRecycleType().equals(RecycleType.ARCHIVING)) + .count(); + if (archivingCnt != archivingIds.size()) { + throw RecycleArchivingNotFoundException.EXCEPTION; + } + Long contentCnt = + recycleList.stream() + .filter(recycle -> recycle.getRecycleType().equals(RecycleType.CONTENT)) + .count(); + if (contentCnt != contentIds.size()) { + throw RecycleContentNotFoundException.EXCEPTION; + } + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/report/adaptor/ReportAdaptor.java b/Domain/src/main/java/allchive/server/domain/domains/report/adaptor/ReportAdaptor.java new file mode 100644 index 00000000..16284ef0 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/report/adaptor/ReportAdaptor.java @@ -0,0 +1,29 @@ +package allchive.server.domain.domains.report.adaptor; + + +import allchive.server.core.annotation.Adaptor; +import allchive.server.domain.domains.report.domain.Report; +import allchive.server.domain.domains.report.repository.ReportRepository; +import lombok.RequiredArgsConstructor; + +@Adaptor +@RequiredArgsConstructor +public class ReportAdaptor { + private final ReportRepository reportRepository; + + public void save(Report report) { + reportRepository.save(report); + } + + public Boolean queryReportExistByUserIdAndContentId(Long userId, Long contentId) { + return reportRepository.queryReportExistByUserIdAndContentId(userId, contentId); + } + + public Boolean queryReportExistByUserIdAndArchivingId(Long userId, Long archivingId) { + return reportRepository.queryReportExistByUserIdAndArchivingId(userId, archivingId); + } + + public void deleteAllByReportedUserId(Long userId) { + reportRepository.deleteAllByReportedUserId(userId); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/report/domain/Report.java b/Domain/src/main/java/allchive/server/domain/domains/report/domain/Report.java new file mode 100644 index 00000000..4d408633 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/report/domain/Report.java @@ -0,0 +1,71 @@ +package allchive.server.domain.domains.report.domain; + + +import allchive.server.domain.common.model.BaseTimeEntity; +import allchive.server.domain.domains.report.domain.enums.ReportObjectType; +import allchive.server.domain.domains.report.domain.enums.ReportedType; +import javax.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Table(name = "tbl_report") +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Report extends BaseTimeEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Enumerated(EnumType.STRING) + private ReportObjectType reportObjectTypeType; + + private String reason; + + @Enumerated(EnumType.STRING) + private ReportedType reportedType; + + private Long contentId; + private Long archivingId; + private Long userId; // 신고한 유저 + private Long reportedUserId; // 신고 당한 유저 id + + @Builder + private Report( + ReportObjectType reportObjectTypeType, + String reason, + ReportedType reportedType, + Long contentId, + Long archivingId, + Long userId, + Long reportedUserId) { + this.reportObjectTypeType = reportObjectTypeType; + this.reason = reason; + this.reportedType = reportedType; + this.contentId = contentId; + this.archivingId = archivingId; + this.userId = userId; + this.reportedUserId = reportedUserId; + } + + public static Report of( + ReportObjectType reportObjectTypeType, + String reason, + ReportedType reportedType, + Long contentId, + Long archivingId, + Long userId, + Long reportedUserId) { + return Report.builder() + .reportObjectTypeType(reportObjectTypeType) + .reason(reason) + .reportedType(reportedType) + .contentId(contentId) + .archivingId(archivingId) + .userId(userId) + .reportedUserId(reportedUserId) + .build(); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/report/domain/enums/ReportObjectType.java b/Domain/src/main/java/allchive/server/domain/domains/report/domain/enums/ReportObjectType.java new file mode 100644 index 00000000..5c90c3c7 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/report/domain/enums/ReportObjectType.java @@ -0,0 +1,26 @@ +package allchive.server.domain.domains.report.domain.enums; + + +import allchive.server.domain.domains.user.domain.enums.OauthProvider; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import java.util.stream.Stream; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum ReportObjectType { + CONTENT("content"), + ARCHIVING("archiving"); + + @JsonValue private String value; + + @JsonCreator + public static OauthProvider parsing(String inputValue) { + return Stream.of(OauthProvider.values()) + .filter(type -> type.getValue().equals(inputValue)) + .findFirst() + .orElse(null); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/report/domain/enums/ReportedType.java b/Domain/src/main/java/allchive/server/domain/domains/report/domain/enums/ReportedType.java new file mode 100644 index 00000000..ad0f8cc4 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/report/domain/enums/ReportedType.java @@ -0,0 +1,32 @@ +package allchive.server.domain.domains.report.domain.enums; + + +import allchive.server.domain.domains.user.domain.enums.OauthProvider; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import java.util.stream.Stream; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum ReportedType { + SPAM("spam"), // 스팸이에요 + OBSCENE("obscene"), // 음란성 컨텐츠를 담고있어요 + PERSONAL_INFO("personalInfo"), // 개인정보를 노출하고 있어요 + INTELLECTUAL_PROPERTY("intellectualProperty"), // 지식재산권을 침해해요 + VIOLENCE("violence"), // 혐오/폭력 컨텐츠를 담고 있어요 + ILLEGAL_INFO("illegalInformation"), // 불법 정보를 포함하고 있어요 + FRAUD("fraud"), // 사기 또는 피싱성 링크를 포함하고 있어요 + ETC("etc"); // 기타 + + @JsonValue private String value; + + @JsonCreator + public static OauthProvider parsing(String inputValue) { + return Stream.of(OauthProvider.values()) + .filter(type -> type.getValue().equals(inputValue)) + .findFirst() + .orElse(null); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/report/exception/ReportErrorCode.java b/Domain/src/main/java/allchive/server/domain/domains/report/exception/ReportErrorCode.java new file mode 100644 index 00000000..d3d14c9b --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/report/exception/ReportErrorCode.java @@ -0,0 +1,24 @@ +package allchive.server.domain.domains.report.exception; + +import static allchive.server.core.consts.AllchiveConst.BAD_REQUEST; + +import allchive.server.core.dto.ErrorReason; +import allchive.server.core.error.BaseErrorCode; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum ReportErrorCode implements BaseErrorCode { + DUPLICATED_REPORT_CONTENT(BAD_REQUEST, "REPORT_400_1", "이미 신고한 컨텐츠입니다."), + DUPLICATED_REPORT_ARCHIVING(BAD_REQUEST, "REPORT_400_1", "이미 신고한 아카이빙입니다."), + ; + private int status; + private String code; + private String reason; + + @Override + public ErrorReason getErrorReason() { + return ErrorReason.of(status, code, reason); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/report/exception/exceptions/DuplicatedReportArchiving.java b/Domain/src/main/java/allchive/server/domain/domains/report/exception/exceptions/DuplicatedReportArchiving.java new file mode 100644 index 00000000..e66332e9 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/report/exception/exceptions/DuplicatedReportArchiving.java @@ -0,0 +1,14 @@ +package allchive.server.domain.domains.report.exception.exceptions; + + +import allchive.server.core.error.BaseErrorException; +import allchive.server.domain.domains.report.exception.ReportErrorCode; + +public class DuplicatedReportArchiving extends BaseErrorException { + + public static final BaseErrorException EXCEPTION = new DuplicatedReportArchiving(); + + private DuplicatedReportArchiving() { + super(ReportErrorCode.DUPLICATED_REPORT_ARCHIVING); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/report/exception/exceptions/DuplicatedReportContent.java b/Domain/src/main/java/allchive/server/domain/domains/report/exception/exceptions/DuplicatedReportContent.java new file mode 100644 index 00000000..df245952 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/report/exception/exceptions/DuplicatedReportContent.java @@ -0,0 +1,14 @@ +package allchive.server.domain.domains.report.exception.exceptions; + + +import allchive.server.core.error.BaseErrorException; +import allchive.server.domain.domains.report.exception.ReportErrorCode; + +public class DuplicatedReportContent extends BaseErrorException { + + public static final BaseErrorException EXCEPTION = new DuplicatedReportContent(); + + private DuplicatedReportContent() { + super(ReportErrorCode.DUPLICATED_REPORT_CONTENT); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/report/repository/ReportCustomRepository.java b/Domain/src/main/java/allchive/server/domain/domains/report/repository/ReportCustomRepository.java new file mode 100644 index 00000000..8c7a1f08 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/report/repository/ReportCustomRepository.java @@ -0,0 +1,7 @@ +package allchive.server.domain.domains.report.repository; + +public interface ReportCustomRepository { + Boolean queryReportExistByUserIdAndContentId(Long userId, Long contentId); + + Boolean queryReportExistByUserIdAndArchivingId(Long userId, Long archivingId); +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/report/repository/ReportCustomRepositoryImpl.java b/Domain/src/main/java/allchive/server/domain/domains/report/repository/ReportCustomRepositoryImpl.java new file mode 100644 index 00000000..d1ea0a1c --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/report/repository/ReportCustomRepositoryImpl.java @@ -0,0 +1,46 @@ +package allchive.server.domain.domains.report.repository; + +import static allchive.server.domain.domains.report.domain.QReport.report; + +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class ReportCustomRepositoryImpl implements ReportCustomRepository { + private final JPAQueryFactory queryFactory; + + @Override + public Boolean queryReportExistByUserIdAndContentId(Long userId, Long contentId) { + Integer fetchOne = + queryFactory + .selectOne() + .from(report) + .where(userIdEq(userId), contentIdEq(contentId)) + .fetchFirst(); // limit 1 + return fetchOne != null; + } + + @Override + public Boolean queryReportExistByUserIdAndArchivingId(Long userId, Long archivingId) { + Integer fetchOne = + queryFactory + .selectOne() + .from(report) + .where(userIdEq(userId), archivingIdEq(archivingId)) + .fetchFirst(); // limit 1 + return fetchOne != null; + } + + private BooleanExpression userIdEq(Long userId) { + return report.userId.eq(userId); + } + + private BooleanExpression contentIdEq(Long contentId) { + return report.contentId.eq(contentId); + } + + private BooleanExpression archivingIdEq(Long archivingId) { + return report.archivingId.eq(archivingId); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/report/repository/ReportRepository.java b/Domain/src/main/java/allchive/server/domain/domains/report/repository/ReportRepository.java new file mode 100644 index 00000000..763aa660 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/report/repository/ReportRepository.java @@ -0,0 +1,9 @@ +package allchive.server.domain.domains.report.repository; + + +import allchive.server.domain.domains.report.domain.Report; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ReportRepository extends JpaRepository, ReportCustomRepository { + void deleteAllByReportedUserId(Long userId); +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/report/service/ReportDomainService.java b/Domain/src/main/java/allchive/server/domain/domains/report/service/ReportDomainService.java new file mode 100644 index 00000000..00a0e4ad --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/report/service/ReportDomainService.java @@ -0,0 +1,21 @@ +package allchive.server.domain.domains.report.service; + + +import allchive.server.core.annotation.DomainService; +import allchive.server.domain.domains.report.adaptor.ReportAdaptor; +import allchive.server.domain.domains.report.domain.Report; +import lombok.RequiredArgsConstructor; + +@DomainService +@RequiredArgsConstructor +public class ReportDomainService { + private final ReportAdaptor reportAdaptor; + + public void save(Report report) { + reportAdaptor.save(report); + } + + public void deleteAllByReportedUserId(Long userId) { + reportAdaptor.deleteAllByReportedUserId(userId); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/report/validator/ReportValidator.java b/Domain/src/main/java/allchive/server/domain/domains/report/validator/ReportValidator.java new file mode 100644 index 00000000..676eae89 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/report/validator/ReportValidator.java @@ -0,0 +1,31 @@ +package allchive.server.domain.domains.report.validator; + + +import allchive.server.core.annotation.Validator; +import allchive.server.domain.domains.report.adaptor.ReportAdaptor; +import allchive.server.domain.domains.report.domain.enums.ReportObjectType; +import allchive.server.domain.domains.report.exception.exceptions.DuplicatedReportArchiving; +import allchive.server.domain.domains.report.exception.exceptions.DuplicatedReportContent; +import lombok.RequiredArgsConstructor; + +@Validator +@RequiredArgsConstructor +public class ReportValidator { + private final ReportAdaptor reportAdaptor; + + public void validateNotDuplicateReport(Long userId, Long objectId, ReportObjectType type) { + Boolean duplicateStatus = Boolean.FALSE; + switch (type) { + case CONTENT -> duplicateStatus = + reportAdaptor.queryReportExistByUserIdAndContentId(userId, objectId); + case ARCHIVING -> duplicateStatus = + reportAdaptor.queryReportExistByUserIdAndArchivingId(userId, objectId); + } + if (duplicateStatus) { + switch (type) { + case CONTENT -> throw DuplicatedReportContent.EXCEPTION; + case ARCHIVING -> throw DuplicatedReportArchiving.EXCEPTION; + } + } + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/search/adaptor/LatestSearchAdaptor.java b/Domain/src/main/java/allchive/server/domain/domains/search/adaptor/LatestSearchAdaptor.java new file mode 100644 index 00000000..c1087477 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/search/adaptor/LatestSearchAdaptor.java @@ -0,0 +1,30 @@ +package allchive.server.domain.domains.search.adaptor; + + +import allchive.server.core.annotation.Adaptor; +import allchive.server.domain.domains.search.domain.LatestSearch; +import allchive.server.domain.domains.search.repository.LatestSearchRepository; +import java.util.List; +import lombok.RequiredArgsConstructor; + +@Adaptor +@RequiredArgsConstructor +public class LatestSearchAdaptor { + private final LatestSearchRepository latestSearchRepository; + + public List findAllByUserIdOrderByCreatedAt(Long userId) { + return latestSearchRepository.findAllByUserIdOrderByCreatedAt(userId); + } + + public void delete(LatestSearch latestSearch) { + latestSearchRepository.delete(latestSearch); + } + + public void save(LatestSearch newSearch) { + latestSearchRepository.save(newSearch); + } + + public void deleteAllByUserId(Long userId) { + latestSearchRepository.deleteAllByUserId(userId); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/search/domain/LatestSearch.java b/Domain/src/main/java/allchive/server/domain/domains/search/domain/LatestSearch.java new file mode 100644 index 00000000..78b88f36 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/search/domain/LatestSearch.java @@ -0,0 +1,33 @@ +package allchive.server.domain.domains.search.domain; + + +import allchive.server.domain.common.model.BaseTimeEntity; +import javax.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** 레디스로 전환 고려? * */ +@Getter +@Table(name = "tbl_latest_search") +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class LatestSearch extends BaseTimeEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String keyword; + private Long userId; + + @Builder + private LatestSearch(String keyword, Long userId) { + this.keyword = keyword; + this.userId = userId; + } + + public static LatestSearch of(String keyword, Long userId) { + return LatestSearch.builder().keyword(keyword).userId(userId).build(); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/search/repository/LatestSearchRepository.java b/Domain/src/main/java/allchive/server/domain/domains/search/repository/LatestSearchRepository.java new file mode 100644 index 00000000..20ca1d86 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/search/repository/LatestSearchRepository.java @@ -0,0 +1,12 @@ +package allchive.server.domain.domains.search.repository; + + +import allchive.server.domain.domains.search.domain.LatestSearch; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface LatestSearchRepository extends JpaRepository { + List findAllByUserIdOrderByCreatedAt(Long userId); + + void deleteAllByUserId(Long userId); +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/search/service/LatestSearchDomainService.java b/Domain/src/main/java/allchive/server/domain/domains/search/service/LatestSearchDomainService.java new file mode 100644 index 00000000..533529ac --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/search/service/LatestSearchDomainService.java @@ -0,0 +1,25 @@ +package allchive.server.domain.domains.search.service; + + +import allchive.server.core.annotation.DomainService; +import allchive.server.domain.domains.search.adaptor.LatestSearchAdaptor; +import allchive.server.domain.domains.search.domain.LatestSearch; +import lombok.RequiredArgsConstructor; + +@DomainService +@RequiredArgsConstructor +public class LatestSearchDomainService { + private final LatestSearchAdaptor latestSearchAdaptor; + + public void delete(LatestSearch latestSearch) { + latestSearchAdaptor.delete(latestSearch); + } + + public void save(LatestSearch newSearch) { + latestSearchAdaptor.save(newSearch); + } + + public void deleteAllByUserId(Long userId) { + latestSearchAdaptor.deleteAllByUserId(userId); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/user/adaptor/RefreshTokenAdaptor.java b/Domain/src/main/java/allchive/server/domain/domains/user/adaptor/RefreshTokenAdaptor.java new file mode 100644 index 00000000..3b64e519 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/user/adaptor/RefreshTokenAdaptor.java @@ -0,0 +1,28 @@ +package allchive.server.domain.domains.user.adaptor; + + +import allchive.server.core.annotation.Adaptor; +import allchive.server.core.error.exception.ExpiredRefreshTokenException; +import allchive.server.domain.domains.user.domain.RefreshTokenEntity; +import allchive.server.domain.domains.user.repository.RefreshTokenRepository; +import lombok.RequiredArgsConstructor; + +@Adaptor +@RequiredArgsConstructor +public class RefreshTokenAdaptor { + private final RefreshTokenRepository refreshTokenRepository; + + public void save(RefreshTokenEntity newRefreshTokenEntityEntity) { + refreshTokenRepository.save(newRefreshTokenEntityEntity); + } + + public void deleteTokenByUserId(Long userId) { + refreshTokenRepository.deleteById(userId.toString()); + } + + public RefreshTokenEntity findTokenByRefreshToken(String refreshToken) { + return refreshTokenRepository + .findByRefreshToken(refreshToken) + .orElseThrow(() -> ExpiredRefreshTokenException.EXCEPTION); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/user/adaptor/ScrapAdaptor.java b/Domain/src/main/java/allchive/server/domain/domains/user/adaptor/ScrapAdaptor.java new file mode 100644 index 00000000..533d97eb --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/user/adaptor/ScrapAdaptor.java @@ -0,0 +1,42 @@ +package allchive.server.domain.domains.user.adaptor; + + +import allchive.server.core.annotation.Adaptor; +import allchive.server.domain.domains.user.domain.Scrap; +import allchive.server.domain.domains.user.domain.User; +import allchive.server.domain.domains.user.exception.exceptions.ScrapNotFoundException; +import allchive.server.domain.domains.user.repository.ScrapRepository; +import java.util.List; +import lombok.RequiredArgsConstructor; + +@Adaptor +@RequiredArgsConstructor +public class ScrapAdaptor { + private final ScrapRepository scrapRepository; + + public List findAllByUserId(Long userId) { + return scrapRepository.findAllByUserId(userId); + } + + public Scrap findByUserAndArchivingId(User user, Long archivingId) { + return scrapRepository + .findAllByUserAndArchivingId(user, archivingId) + .orElseThrow(() -> ScrapNotFoundException.EXCEPTION); + } + + public void delete(Scrap scrap) { + scrapRepository.delete(scrap); + } + + public void save(Scrap scrap) { + scrapRepository.save(scrap); + } + + public void deleteAllByArchivingIdIn(List archivingIds) { + scrapRepository.deleteAllByArchivingIdIn(archivingIds); + } + + public void deleteAllByUser(User user) { + scrapRepository.deleteAllByUser(user); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/user/adaptor/UserAdaptor.java b/Domain/src/main/java/allchive/server/domain/domains/user/adaptor/UserAdaptor.java new file mode 100644 index 00000000..b118c6ae --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/user/adaptor/UserAdaptor.java @@ -0,0 +1,42 @@ +package allchive.server.domain.domains.user.adaptor; + + +import allchive.server.core.annotation.Adaptor; +import allchive.server.domain.domains.user.domain.User; +import allchive.server.domain.domains.user.domain.enums.OauthInfo; +import allchive.server.domain.domains.user.exception.exceptions.UserNotFoundException; +import allchive.server.domain.domains.user.repository.UserRepository; +import java.util.List; +import lombok.RequiredArgsConstructor; + +@Adaptor +@RequiredArgsConstructor +public class UserAdaptor { + private final UserRepository userRepository; + + public Boolean exist(OauthInfo oauthInfo) { + return userRepository.existsByOauthInfo(oauthInfo); + } + + public User findByOauthInfo(OauthInfo oauthInfo) { + return userRepository + .findByOauthInfo(oauthInfo) + .orElseThrow(() -> UserNotFoundException.EXCEPTION); + } + + public void save(User user) { + userRepository.save(user); + } + + public User findById(Long userId) { + return userRepository.findById(userId).orElseThrow(() -> UserNotFoundException.EXCEPTION); + } + + public Boolean existsByNickname(String nickname) { + return userRepository.existsByNickname(nickname); + } + + public List findAllByIdIn(List userIds) { + return userRepository.findAllByIdIn(userIds); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/user/domain/RefreshTokenEntity.java b/Domain/src/main/java/allchive/server/domain/domains/user/domain/RefreshTokenEntity.java new file mode 100644 index 00000000..f47ab20b --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/user/domain/RefreshTokenEntity.java @@ -0,0 +1,29 @@ +package allchive.server.domain.domains.user.domain; + + +import lombok.Builder; +import lombok.Getter; +import org.springframework.data.annotation.Id; +import org.springframework.data.redis.core.RedisHash; +import org.springframework.data.redis.core.TimeToLive; +import org.springframework.data.redis.core.index.Indexed; + +@RedisHash(value = "refreshToken") +@Getter +public class RefreshTokenEntity { + @Id private Long id; + @Indexed private String refreshToken; + @TimeToLive // TTL + private Long ttl; + + @Builder + private RefreshTokenEntity(Long id, String refreshToken, Long ttl) { + this.id = id; + this.refreshToken = refreshToken; + this.ttl = ttl; + } + + public static RefreshTokenEntity of(Long id, String refreshToken, Long ttl) { + return RefreshTokenEntity.builder().id(id).refreshToken(refreshToken).ttl(ttl).build(); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/user/domain/Scrap.java b/Domain/src/main/java/allchive/server/domain/domains/user/domain/Scrap.java new file mode 100644 index 00000000..ebb7b2a2 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/user/domain/Scrap.java @@ -0,0 +1,34 @@ +package allchive.server.domain.domains.user.domain; + + +import allchive.server.domain.common.model.BaseTimeEntity; +import javax.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Table(name = "tbl_scrap") +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Scrap extends BaseTimeEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private User user; + + private Long archivingId; + + @Builder + private Scrap(User user, Long archivingId) { + this.user = user; + this.archivingId = archivingId; + } + + public static Scrap of(User user, Long archivingId) { + return Scrap.builder().user(user).archivingId(archivingId).build(); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/user/domain/User.java b/Domain/src/main/java/allchive/server/domain/domains/user/domain/User.java new file mode 100644 index 00000000..c5a87470 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/user/domain/User.java @@ -0,0 +1,107 @@ +package allchive.server.domain.domains.user.domain; + + +import allchive.server.domain.common.convertor.StringListConverter; +import allchive.server.domain.common.model.BaseTimeEntity; +import allchive.server.domain.domains.archiving.domain.enums.Category; +import allchive.server.domain.domains.user.domain.enums.OauthInfo; +import allchive.server.domain.domains.user.domain.enums.UserRole; +import allchive.server.domain.domains.user.domain.enums.UserState; +import allchive.server.domain.domains.user.exception.exceptions.AlreadyDeletedUserException; +import allchive.server.domain.domains.user.exception.exceptions.ForbiddenUserException; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import javax.persistence.*; +import javax.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Table(name = "tbl_user") +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class User extends BaseTimeEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + + @NotNull private String nickname; + + private String profileImgUrl; + + @Embedded private OauthInfo oauthInfo; + + @NotNull private LocalDateTime lastLoginAt; + + private String email; + + @Enumerated(EnumType.STRING) + private UserState userState = UserState.NORMAL; + + @Enumerated(EnumType.STRING) + private UserRole userRole = UserRole.USER; + + @Convert(converter = StringListConverter.class) + private List categories = new ArrayList<>(); + + @Builder + private User( + String nickname, + String profileImgUrl, + List categoryList, + OauthInfo oauthInfo) { + this.nickname = nickname; + this.profileImgUrl = profileImgUrl; + this.categories = categoryList; + this.oauthInfo = oauthInfo; + this.lastLoginAt = LocalDateTime.now(); + } + + public static User of( + String nickname, + String profileImgUrl, + List categoryList, + OauthInfo oauthInfo) { + return User.builder() + .nickname(nickname) + .profileImgUrl(profileImgUrl) + .categoryList(categoryList) + .oauthInfo(oauthInfo) + .build(); + } + + public void login() { + if (!UserState.NORMAL.equals(this.userState)) { + throw ForbiddenUserException.EXCEPTION; + } + lastLoginAt = LocalDateTime.now(); + } + + public void withdrawUser() { + if (UserState.DELETED.equals(this.userState)) { + throw AlreadyDeletedUserException.EXCEPTION; + } + this.userState = UserState.DELETED; + this.nickname = LocalDateTime.now() + "삭제한 유저"; + this.profileImgUrl = null; + this.email = null; + this.oauthInfo.withDrawOauthInfo(); + this.categories = new ArrayList<>(); + this.name = null; + } + + public void updateInfo(String name, String email, String nickname, String imgUrl) { + if (!UserState.NORMAL.equals(this.userState)) { + throw ForbiddenUserException.EXCEPTION; + } + this.name = name; + this.email = email; + this.nickname = nickname; + this.profileImgUrl = imgUrl; + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/user/domain/enums/FormInfo.java b/Domain/src/main/java/allchive/server/domain/domains/user/domain/enums/FormInfo.java new file mode 100644 index 00000000..b806f0bf --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/user/domain/enums/FormInfo.java @@ -0,0 +1,16 @@ +package allchive.server.domain.domains.user.domain.enums; + + +import javax.persistence.Embeddable; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Embeddable +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class FormInfo { + private String formId; + private String formPwd; + private String phone; +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/user/domain/enums/OauthInfo.java b/Domain/src/main/java/allchive/server/domain/domains/user/domain/enums/OauthInfo.java new file mode 100644 index 00000000..b99a3bc3 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/user/domain/enums/OauthInfo.java @@ -0,0 +1,34 @@ +package allchive.server.domain.domains.user.domain.enums; + + +import javax.persistence.Embeddable; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Embeddable +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class OauthInfo { + @Enumerated(EnumType.STRING) + private OauthProvider provider; + + private String oid; + + @Builder + private OauthInfo(OauthProvider provider, String oid) { + this.provider = provider; + this.oid = oid; + } + + public static OauthInfo of(OauthProvider provider, String oid) { + return OauthInfo.builder().provider(provider).oid(oid).build(); + } + + public void withDrawOauthInfo() { + this.oid = null; + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/user/domain/enums/OauthProvider.java b/Domain/src/main/java/allchive/server/domain/domains/user/domain/enums/OauthProvider.java new file mode 100644 index 00000000..307eceb8 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/user/domain/enums/OauthProvider.java @@ -0,0 +1,25 @@ +package allchive.server.domain.domains.user.domain.enums; + + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import java.util.stream.Stream; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum OauthProvider { + KAKAO("KAKAO"), + APPLE("APPLE"); + + @JsonValue private String value; + + @JsonCreator + public static OauthProvider parsing(String inputValue) { + return Stream.of(OauthProvider.values()) + .filter(oauthProvider -> oauthProvider.getValue().equals(inputValue)) + .findFirst() + .orElse(null); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/user/domain/enums/UserRole.java b/Domain/src/main/java/allchive/server/domain/domains/user/domain/enums/UserRole.java new file mode 100644 index 00000000..7612efa4 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/user/domain/enums/UserRole.java @@ -0,0 +1,14 @@ +package allchive.server.domain.domains.user.domain.enums; + + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum UserRole { + USER("USER"), + ADMIN("ADMIN"); + + private String value; +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/user/domain/enums/UserState.java b/Domain/src/main/java/allchive/server/domain/domains/user/domain/enums/UserState.java new file mode 100644 index 00000000..41cd08aa --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/user/domain/enums/UserState.java @@ -0,0 +1,19 @@ +package allchive.server.domain.domains.user.domain.enums; + + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public enum UserState { + NORMAL("NORMAL"), + // 탈퇴한유저 + DELETED("DELETED"), + // 영구정지 + FORBIDDEN("FORBIDDEN"), + // 일시정지 + SUSPENDED("SUSPENDED"); + + private String value; +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/user/exception/UserErrorCode.java b/Domain/src/main/java/allchive/server/domain/domains/user/exception/UserErrorCode.java new file mode 100644 index 00000000..62b39b0d --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/user/exception/UserErrorCode.java @@ -0,0 +1,32 @@ +package allchive.server.domain.domains.user.exception; + +import static allchive.server.core.consts.AllchiveConst.BAD_REQUEST; +import static allchive.server.core.consts.AllchiveConst.NOT_FOUND; + +import allchive.server.core.dto.ErrorReason; +import allchive.server.core.error.BaseErrorCode; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum UserErrorCode implements BaseErrorCode { + FORBIDDEN_USER(BAD_REQUEST, "USER_400_1", "접근 제한된 유저입니다."), + USER_ALREADY_SIGNUP(BAD_REQUEST, "USER_400_2", "이미 회원가입한 유저입니다."), + USER_ALREADY_DELETED(BAD_REQUEST, "USER_400_3", "이미 지워진 유저입니다."), + DUPLICATED_NICKNAME(BAD_REQUEST, "USER_400_4", "중복된 닉네임입니다."), + + USER_NOT_FOUND(NOT_FOUND, "USER_404_1", "유저 정보를 찾을 수 없습니다."), + + ALREADY_EXIST_SCRAP(BAD_REQUEST, "SCRAP_400_1", "이미 스크랩 되어있습니다."), + + SCRAP_NOT_FOUND(NOT_FOUND, "SCRAP_404_1", "스크랩 정보를 찾을 수 없습니다."); + private int status; + private String code; + private String reason; + + @Override + public ErrorReason getErrorReason() { + return ErrorReason.of(status, code, reason); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/user/exception/exceptions/AlreadyDeletedUserException.java b/Domain/src/main/java/allchive/server/domain/domains/user/exception/exceptions/AlreadyDeletedUserException.java new file mode 100644 index 00000000..510807cc --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/user/exception/exceptions/AlreadyDeletedUserException.java @@ -0,0 +1,14 @@ +package allchive.server.domain.domains.user.exception.exceptions; + + +import allchive.server.core.error.BaseErrorException; +import allchive.server.domain.domains.user.exception.UserErrorCode; + +public class AlreadyDeletedUserException extends BaseErrorException { + + public static final BaseErrorException EXCEPTION = new AlreadyDeletedUserException(); + + private AlreadyDeletedUserException() { + super(UserErrorCode.USER_ALREADY_DELETED); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/user/exception/exceptions/AlreadyExistScrapException.java b/Domain/src/main/java/allchive/server/domain/domains/user/exception/exceptions/AlreadyExistScrapException.java new file mode 100644 index 00000000..50845602 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/user/exception/exceptions/AlreadyExistScrapException.java @@ -0,0 +1,14 @@ +package allchive.server.domain.domains.user.exception.exceptions; + + +import allchive.server.core.error.BaseErrorException; +import allchive.server.domain.domains.user.exception.UserErrorCode; + +public class AlreadyExistScrapException extends BaseErrorException { + + public static final BaseErrorException EXCEPTION = new AlreadyExistScrapException(); + + private AlreadyExistScrapException() { + super(UserErrorCode.ALREADY_EXIST_SCRAP); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/user/exception/exceptions/AlreadySignUpUserException.java b/Domain/src/main/java/allchive/server/domain/domains/user/exception/exceptions/AlreadySignUpUserException.java new file mode 100644 index 00000000..93be5de2 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/user/exception/exceptions/AlreadySignUpUserException.java @@ -0,0 +1,14 @@ +package allchive.server.domain.domains.user.exception.exceptions; + + +import allchive.server.core.error.BaseErrorException; +import allchive.server.domain.domains.user.exception.UserErrorCode; + +public class AlreadySignUpUserException extends BaseErrorException { + + public static final BaseErrorException EXCEPTION = new AlreadySignUpUserException(); + + private AlreadySignUpUserException() { + super(UserErrorCode.USER_ALREADY_SIGNUP); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/user/exception/exceptions/DuplicatedNicknameException.java b/Domain/src/main/java/allchive/server/domain/domains/user/exception/exceptions/DuplicatedNicknameException.java new file mode 100644 index 00000000..5545d0f8 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/user/exception/exceptions/DuplicatedNicknameException.java @@ -0,0 +1,14 @@ +package allchive.server.domain.domains.user.exception.exceptions; + + +import allchive.server.core.error.BaseErrorException; +import allchive.server.domain.domains.user.exception.UserErrorCode; + +public class DuplicatedNicknameException extends BaseErrorException { + + public static final BaseErrorException EXCEPTION = new DuplicatedNicknameException(); + + private DuplicatedNicknameException() { + super(UserErrorCode.DUPLICATED_NICKNAME); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/user/exception/exceptions/ForbiddenUserException.java b/Domain/src/main/java/allchive/server/domain/domains/user/exception/exceptions/ForbiddenUserException.java new file mode 100644 index 00000000..556452f5 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/user/exception/exceptions/ForbiddenUserException.java @@ -0,0 +1,14 @@ +package allchive.server.domain.domains.user.exception.exceptions; + + +import allchive.server.core.error.BaseErrorException; +import allchive.server.domain.domains.user.exception.UserErrorCode; + +public class ForbiddenUserException extends BaseErrorException { + + public static final BaseErrorException EXCEPTION = new ForbiddenUserException(); + + private ForbiddenUserException() { + super(UserErrorCode.FORBIDDEN_USER); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/user/exception/exceptions/ScrapNotFoundException.java b/Domain/src/main/java/allchive/server/domain/domains/user/exception/exceptions/ScrapNotFoundException.java new file mode 100644 index 00000000..65386ee1 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/user/exception/exceptions/ScrapNotFoundException.java @@ -0,0 +1,14 @@ +package allchive.server.domain.domains.user.exception.exceptions; + + +import allchive.server.core.error.BaseErrorException; +import allchive.server.domain.domains.user.exception.UserErrorCode; + +public class ScrapNotFoundException extends BaseErrorException { + + public static final BaseErrorException EXCEPTION = new ScrapNotFoundException(); + + private ScrapNotFoundException() { + super(UserErrorCode.SCRAP_NOT_FOUND); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/user/exception/exceptions/UserNotFoundException.java b/Domain/src/main/java/allchive/server/domain/domains/user/exception/exceptions/UserNotFoundException.java new file mode 100644 index 00000000..86bdf763 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/user/exception/exceptions/UserNotFoundException.java @@ -0,0 +1,14 @@ +package allchive.server.domain.domains.user.exception.exceptions; + + +import allchive.server.core.error.BaseErrorException; +import allchive.server.domain.domains.user.exception.UserErrorCode; + +public class UserNotFoundException extends BaseErrorException { + + public static final BaseErrorException EXCEPTION = new UserNotFoundException(); + + private UserNotFoundException() { + super(UserErrorCode.USER_NOT_FOUND); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/user/repository/RefreshTokenRepository.java b/Domain/src/main/java/allchive/server/domain/domains/user/repository/RefreshTokenRepository.java new file mode 100644 index 00000000..5cfe4879 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/user/repository/RefreshTokenRepository.java @@ -0,0 +1,10 @@ +package allchive.server.domain.domains.user.repository; + + +import allchive.server.domain.domains.user.domain.RefreshTokenEntity; +import java.util.Optional; +import org.springframework.data.repository.CrudRepository; + +public interface RefreshTokenRepository extends CrudRepository { + Optional findByRefreshToken(String refreshToken); +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/user/repository/ScrapRepository.java b/Domain/src/main/java/allchive/server/domain/domains/user/repository/ScrapRepository.java new file mode 100644 index 00000000..4ccfba30 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/user/repository/ScrapRepository.java @@ -0,0 +1,18 @@ +package allchive.server.domain.domains.user.repository; + + +import allchive.server.domain.domains.user.domain.Scrap; +import allchive.server.domain.domains.user.domain.User; +import java.util.List; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ScrapRepository extends JpaRepository { + List findAllByUserId(Long userId); + + Optional findAllByUserAndArchivingId(User user, Long archivingId); + + void deleteAllByArchivingIdIn(List archivingId); + + void deleteAllByUser(User user); +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/user/repository/UserRepository.java b/Domain/src/main/java/allchive/server/domain/domains/user/repository/UserRepository.java new file mode 100644 index 00000000..938a4009 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/user/repository/UserRepository.java @@ -0,0 +1,18 @@ +package allchive.server.domain.domains.user.repository; + + +import allchive.server.domain.domains.user.domain.User; +import allchive.server.domain.domains.user.domain.enums.OauthInfo; +import java.util.List; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserRepository extends JpaRepository { + Optional findByOauthInfo(OauthInfo oauthInfo); + + boolean existsByOauthInfo(OauthInfo oauthInfo); + + boolean existsByNickname(String nickname); + + List findAllByIdIn(List userIds); +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/user/service/ScrapDomainService.java b/Domain/src/main/java/allchive/server/domain/domains/user/service/ScrapDomainService.java new file mode 100644 index 00000000..8400ee27 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/user/service/ScrapDomainService.java @@ -0,0 +1,32 @@ +package allchive.server.domain.domains.user.service; + + +import allchive.server.core.annotation.DomainService; +import allchive.server.domain.domains.user.adaptor.ScrapAdaptor; +import allchive.server.domain.domains.user.domain.Scrap; +import allchive.server.domain.domains.user.domain.User; +import java.util.List; +import lombok.RequiredArgsConstructor; + +@DomainService +@RequiredArgsConstructor +public class ScrapDomainService { + private final ScrapAdaptor scrapAdaptor; + + public void deleteScrapByUserAndArchivingId(User user, Long archivingId) { + Scrap scrap = scrapAdaptor.findByUserAndArchivingId(user, archivingId); + scrapAdaptor.delete(scrap); + } + + public void save(Scrap scrap) { + scrapAdaptor.save(scrap); + } + + public void deleteAllByArchivingIdIn(List archivingIds) { + scrapAdaptor.deleteAllByArchivingIdIn(archivingIds); + } + + public void deleteAllByUser(User user) { + scrapAdaptor.deleteAllByUser(user); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/user/service/UserDomainService.java b/Domain/src/main/java/allchive/server/domain/domains/user/service/UserDomainService.java new file mode 100644 index 00000000..5c49df2b --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/user/service/UserDomainService.java @@ -0,0 +1,61 @@ +package allchive.server.domain.domains.user.service; + + +import allchive.server.core.annotation.DomainService; +import allchive.server.domain.domains.archiving.domain.enums.Category; +import allchive.server.domain.domains.user.adaptor.UserAdaptor; +import allchive.server.domain.domains.user.domain.User; +import allchive.server.domain.domains.user.domain.enums.OauthInfo; +import allchive.server.domain.domains.user.exception.exceptions.DuplicatedNicknameException; +import allchive.server.domain.domains.user.validator.UserValidator; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +@DomainService +@RequiredArgsConstructor +public class UserDomainService { + private final UserAdaptor userAdaptor; + private final UserValidator userValidator; + + public Boolean checkUserCanLogin(OauthInfo oauthInfo) { + return userAdaptor.exist(oauthInfo); + } + + @Transactional + public User registerUser( + String nickname, + String profileImgUrl, + List categoryList, + OauthInfo oauthInfo) { + userValidator.validUserCanRegister(oauthInfo); + final User newUser = User.of(nickname, profileImgUrl, categoryList, oauthInfo); + userAdaptor.save(newUser); + return newUser; + } + + @Transactional + public User loginUser(OauthInfo oauthInfo) { + User user = userAdaptor.findByOauthInfo(oauthInfo); + user.login(); + return user; + } + + @Transactional + public void deleteUserById(Long userId) { + User user = userAdaptor.findById(userId); + user.withdrawUser(); + } + + public void updateUserInfo( + Long userId, String name, String email, String nickname, String imgUrl) { + User user = userAdaptor.findById(userId); + user.updateInfo(name, email, nickname, imgUrl); + } + + public void checkUserNickname(String nickname) { + if (userAdaptor.existsByNickname(nickname)) { + throw DuplicatedNicknameException.EXCEPTION; + } + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/user/service/UserTopicGroupService.java b/Domain/src/main/java/allchive/server/domain/domains/user/service/UserTopicGroupService.java new file mode 100644 index 00000000..0311422d --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/user/service/UserTopicGroupService.java @@ -0,0 +1,7 @@ +package allchive.server.domain.domains.user.service; + + +import allchive.server.core.annotation.DomainService; + +@DomainService +public class UserTopicGroupService {} diff --git a/Domain/src/main/java/allchive/server/domain/domains/user/validator/ScrapValidator.java b/Domain/src/main/java/allchive/server/domain/domains/user/validator/ScrapValidator.java new file mode 100644 index 00000000..153ee1e7 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/user/validator/ScrapValidator.java @@ -0,0 +1,28 @@ +package allchive.server.domain.domains.user.validator; + + +import allchive.server.core.annotation.Validator; +import allchive.server.domain.domains.user.adaptor.ScrapAdaptor; +import allchive.server.domain.domains.user.adaptor.UserAdaptor; +import allchive.server.domain.domains.user.domain.User; +import allchive.server.domain.domains.user.exception.exceptions.AlreadyExistScrapException; +import lombok.RequiredArgsConstructor; + +@Validator +@RequiredArgsConstructor +public class ScrapValidator { + private final UserAdaptor userAdaptor; + private final ScrapAdaptor scrapAdaptor; + + public void validateExistScrap(Long userId, Long archivingId) { + User user = userAdaptor.findById(userId); + if (isScrapExist(user, archivingId)) { + throw AlreadyExistScrapException.EXCEPTION; + } + } + + private Boolean isScrapExist(User user, Long archivingId) { + scrapAdaptor.findByUserAndArchivingId(user, archivingId); + return Boolean.TRUE; + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/user/validator/UserValidator.java b/Domain/src/main/java/allchive/server/domain/domains/user/validator/UserValidator.java new file mode 100644 index 00000000..a79f04f3 --- /dev/null +++ b/Domain/src/main/java/allchive/server/domain/domains/user/validator/UserValidator.java @@ -0,0 +1,30 @@ +package allchive.server.domain.domains.user.validator; + + +import allchive.server.core.annotation.Validator; +import allchive.server.domain.domains.user.adaptor.UserAdaptor; +import allchive.server.domain.domains.user.domain.enums.OauthInfo; +import allchive.server.domain.domains.user.domain.enums.UserState; +import allchive.server.domain.domains.user.exception.exceptions.AlreadySignUpUserException; +import allchive.server.domain.domains.user.exception.exceptions.ForbiddenUserException; +import lombok.RequiredArgsConstructor; + +@Validator +@RequiredArgsConstructor +public class UserValidator { + private final UserAdaptor userAdaptor; + + public void validUserCanRegister(OauthInfo oauthInfo) { + if (!checkUserCanRegister(oauthInfo)) throw AlreadySignUpUserException.EXCEPTION; + } + + public Boolean checkUserCanRegister(OauthInfo oauthInfo) { + return !userAdaptor.exist(oauthInfo); + } + + public void validateUserStatusNormal(Long userId) { + if (!userAdaptor.findById(userId).getUserState().equals(UserState.NORMAL)) { + throw ForbiddenUserException.EXCEPTION; + } + } +} diff --git a/Domain/src/main/resources/application-domain.yml b/Domain/src/main/resources/application-domain.yml index 77d92f67..6ec6aaf7 100644 --- a/Domain/src/main/resources/application-domain.yml +++ b/Domain/src/main/resources/application-domain.yml @@ -1,10 +1,13 @@ # common spring: datasource: - url: jdbc:mysql://${MYSQL_HOST}:3306/${DB_NAME}?useSSL=false&characterEncoding=UTF-8&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true&tinyInt1isBit=false + url: jdbc:mysql://${MYSQL_HOST}:${MYSQL_PORT}/${DB_NAME}?useSSL=false&characterEncoding=UTF-8&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true&tinyInt1isBit=false driver-class-name: com.mysql.cj.jdbc.Driver - password: ${MYSQL_PASSWORD} username: ${MYSQL_USERNAME} + password: ${MYSQL_PASSWORD} + hikari: + maxLifetime: 580000 + maximum-pool-size: 20 jpa: show-sql: ${SHOW_SQL:true} properties: @@ -14,12 +17,25 @@ spring: hibernate: ddl-auto: update defer-datasource-initialization: true - sql: - init: - mode: always open-in-view: false database-platform: org.hibernate.dialect.MySQL5InnoDBDialect + database: mysql + +--- +# local +spring: + config: + activate: + on-profile: local +logging: + level: + com.zaxxer.hikari.HikariConfig: DEBUG + com.zaxxer.hikari: TRACE + org.springframework.orm.jpa: DEBUG + org.springframework.transaction: DEBUG +# org.hibernate.SQL: debug +# org.hibernate.type: trace --- # dev spring: @@ -33,6 +49,7 @@ logging: com.zaxxer.hikari: TRACE org.springframework.orm.jpa: DEBUG org.springframework.transaction: DEBUG + org.hibernate.SQL: debug --- # prod diff --git a/Infrastructure/build.gradle b/Infrastructure/build.gradle index 5398ee84..86d0d1eb 100644 --- a/Infrastructure/build.gradle +++ b/Infrastructure/build.gradle @@ -2,9 +2,13 @@ bootJar { enabled = false } jar { enabled = true } dependencies { - api('com.slack.api:slack-api-client:1.28.0') - api 'io.github.openfeign:feign-httpclient:12.2' - api 'org.springframework.cloud:spring-cloud-starter-openfeign:4.0.1' + api 'org.springframework.boot:spring-boot-starter-data-redis' + api 'org.redisson:redisson:3.19.0' + api 'io.github.openfeign:feign-httpclient:12.1' + api 'org.springframework.cloud:spring-cloud-starter-openfeign:3.1.4' + api 'com.amazonaws:aws-java-sdk-s3control:1.12.372' + api 'com.nimbusds:nimbus-jose-jwt:3.10' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.2' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.2' api project(':Core') diff --git a/Infrastructure/src/main/java/allchive/server/.gitkeep b/Infrastructure/src/main/java/allchive/server/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/Infrastructure/src/main/java/allchive/server/infrastructure/feign/config/OpenFeignConfig.java b/Infrastructure/src/main/java/allchive/server/infrastructure/feign/config/OpenFeignConfig.java new file mode 100644 index 00000000..0313e290 --- /dev/null +++ b/Infrastructure/src/main/java/allchive/server/infrastructure/feign/config/OpenFeignConfig.java @@ -0,0 +1,24 @@ +package allchive.server.infrastructure.feign.config; + + +import allchive.server.infrastructure.oauth.BaseFeignClientClass; +import feign.Logger; +import feign.Retryer; +import java.util.concurrent.TimeUnit; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableFeignClients(basePackageClasses = BaseFeignClientClass.class) +public class OpenFeignConfig { + @Bean + Retryer.Default retryer() { + return new Retryer.Default(100L, TimeUnit.SECONDS.toMillis(3L), 5); + } + + @Bean + Logger.Level feignLoggerLevel() { + return Logger.Level.FULL; + } +} diff --git a/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/BaseFeignClientClass.java b/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/BaseFeignClientClass.java new file mode 100644 index 00000000..d6ec8cc7 --- /dev/null +++ b/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/BaseFeignClientClass.java @@ -0,0 +1,3 @@ +package allchive.server.infrastructure.oauth; + +public interface BaseFeignClientClass {} diff --git a/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/apple/client/AppleOAuthClient.java b/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/apple/client/AppleOAuthClient.java new file mode 100644 index 00000000..8021d124 --- /dev/null +++ b/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/apple/client/AppleOAuthClient.java @@ -0,0 +1,27 @@ +package allchive.server.infrastructure.oauth.apple.client; + + +import allchive.server.infrastructure.oauth.apple.config.AppleOAuthConfig; +import allchive.server.infrastructure.oauth.apple.response.AppleTokenResponse; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; + +@FeignClient( + name = "AppleOAuthClient", + url = "https://appleid.apple.com", + configuration = AppleOAuthConfig.class) +public interface AppleOAuthClient { + @PostMapping("/auth/token?grant_type=authorization_code") + AppleTokenResponse appleAuth( + @RequestParam("client_id") String clientId, + @RequestParam("redirect_uri") String redirectUri, + @RequestParam("code") String code, + @RequestParam("client_secret") String clientSecret); + + @PostMapping("/auth/revoke") + void revoke( + @RequestParam("client_id") String clientId, + @RequestParam("client_secret") String clientSecret, + @RequestParam("token") String accessToken); +} diff --git a/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/apple/client/AppleOIDCClient.java b/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/apple/client/AppleOIDCClient.java new file mode 100644 index 00000000..2b86819d --- /dev/null +++ b/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/apple/client/AppleOIDCClient.java @@ -0,0 +1,18 @@ +package allchive.server.infrastructure.oauth.apple.client; + + +import allchive.server.infrastructure.oauth.apple.config.AppleOAuthConfig; +import allchive.server.infrastructure.oauth.kakao.dto.OIDCPublicKeysResponse; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; + +@FeignClient( + name = "AppleOIDCClient", + url = "https://appleid.apple.com", + configuration = AppleOAuthConfig.class) +public interface AppleOIDCClient { + @Cacheable(cacheNames = "appleOIDC", cacheManager = "oidcCacheManager") + @GetMapping("/auth/keys") + OIDCPublicKeysResponse getAppleOIDCOpenKeys(); +} diff --git a/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/apple/config/AppleOAuthConfig.java b/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/apple/config/AppleOAuthConfig.java new file mode 100644 index 00000000..29793d41 --- /dev/null +++ b/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/apple/config/AppleOAuthConfig.java @@ -0,0 +1,23 @@ +package allchive.server.infrastructure.oauth.apple.config; + + +import feign.codec.Encoder; +import feign.codec.ErrorDecoder; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; + +@Import(AppleOAuthErrorDecoder.class) +public class AppleOAuthConfig { + + @Bean + @ConditionalOnMissingBean(value = ErrorDecoder.class) + public AppleOAuthErrorDecoder commonFeignErrorDecoder() { + return new AppleOAuthErrorDecoder(); + } + + @Bean + Encoder formEncoder() { + return new feign.form.FormEncoder(); + } +} diff --git a/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/apple/config/AppleOAuthErrorDecoder.java b/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/apple/config/AppleOAuthErrorDecoder.java new file mode 100644 index 00000000..94d4e3ff --- /dev/null +++ b/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/apple/config/AppleOAuthErrorDecoder.java @@ -0,0 +1,32 @@ +package allchive.server.infrastructure.oauth.apple.config; + + +import allchive.server.core.error.BaseDynamicException; +import com.amazonaws.util.IOUtils; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import feign.Response; +import feign.codec.ErrorDecoder; +import java.io.InputStream; +import lombok.SneakyThrows; + +public class AppleOAuthErrorDecoder implements ErrorDecoder { + @Override + @SneakyThrows + public Exception decode(String methodKey, Response response) { + InputStream inputStream = response.body().asInputStream(); + byte[] byteArray = IOUtils.toByteArray(inputStream); + String responseBody = new String(byteArray); + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode jsonNode = objectMapper.readTree(responseBody); + + String error = jsonNode.get("error") == null ? null : jsonNode.get("error").asText(); + String errorDescription = + jsonNode.get("error_description") == null + ? null + : jsonNode.get("error_description").asText(); + + System.out.println(jsonNode); + throw new BaseDynamicException(response.status(), error, errorDescription); + } +} diff --git a/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/apple/config/AppleOAuthErrorResponse.java b/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/apple/config/AppleOAuthErrorResponse.java new file mode 100644 index 00000000..cc9ab1c0 --- /dev/null +++ b/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/apple/config/AppleOAuthErrorResponse.java @@ -0,0 +1,29 @@ +package allchive.server.infrastructure.oauth.apple.config; + + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import feign.Response; +import java.io.IOException; +import java.io.InputStream; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class AppleOAuthErrorResponse { + private String error; + private String errorCode; + private String errorDescription; + + public static AppleOAuthErrorResponse from(Response response) { + try (InputStream bodyIs = response.body().asInputStream()) { + ObjectMapper mapper = new ObjectMapper(); + return mapper.readValue(bodyIs, AppleOAuthErrorResponse.class); + } catch (IOException e) { + return null; // + } + } +} diff --git a/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/apple/helper/AppleLoginUtil.java b/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/apple/helper/AppleLoginUtil.java new file mode 100644 index 00000000..e55b74e1 --- /dev/null +++ b/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/apple/helper/AppleLoginUtil.java @@ -0,0 +1,83 @@ +package allchive.server.infrastructure.oauth.apple.helper; + + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.crypto.ECDSASigner; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.interfaces.ECPrivateKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Date; +import org.bouncycastle.util.io.pem.PemObject; +import org.bouncycastle.util.io.pem.PemReader; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; + +public class AppleLoginUtil { + /** + * client_secret 생성 Apple Document URL ‣ + * https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens + * + * @return client_secret(jwt) + */ + public static String createClientSecret( + String teamId, String clientId, String keyId, String keyPath, String authUrl) { + + JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.ES256).keyID(keyId).build(); + JWTClaimsSet claimsSet = new JWTClaimsSet(); + Date now = new Date(); + + claimsSet.setIssuer(teamId); + claimsSet.setIssueTime(now); + claimsSet.setExpirationTime(new Date(now.getTime() + 3600000)); + claimsSet.setAudience(authUrl); + claimsSet.setSubject(clientId); + + SignedJWT jwt = new SignedJWT(header, claimsSet); + + PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(readPrivateKey(keyPath)); + try { + KeyFactory kf = KeyFactory.getInstance("EC"); + ECPrivateKey ecPrivateKey = (ECPrivateKey) kf.generatePrivate(spec); + JWSSigner jwsSigner = new ECDSASigner(ecPrivateKey.getS()); + jwt.sign(jwsSigner); + } catch (JOSEException e) { + e.printStackTrace(); + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + throw new RuntimeException(e); + } + + return jwt.serialize(); + } + + /** + * 파일에서 private key 획득 + * + * @return Private Key + */ + private static byte[] readPrivateKey(String keyPath) { + + Resource resource = new ClassPathResource(keyPath); + byte[] content = null; + + try (InputStream keyInputStream = resource.getInputStream(); + InputStreamReader keyReader = new InputStreamReader(keyInputStream); + PemReader pemReader = new PemReader(keyReader)) { + PemObject pemObject = pemReader.readPemObject(); + content = pemObject.getContent(); + } catch (IOException e) { + e.printStackTrace(); + } + + return content; + } +} diff --git a/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/apple/response/AppleTokenResponse.java b/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/apple/response/AppleTokenResponse.java new file mode 100644 index 00000000..90ee72c2 --- /dev/null +++ b/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/apple/response/AppleTokenResponse.java @@ -0,0 +1,16 @@ +package allchive.server.infrastructure.oauth.apple.response; + + +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@JsonNaming(SnakeCaseStrategy.class) +public class AppleTokenResponse { + private String accessToken; + private String refreshToken; + private String idToken; +} diff --git a/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/kakao/client/KakaoInfoClient.java b/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/kakao/client/KakaoInfoClient.java new file mode 100644 index 00000000..200cb760 --- /dev/null +++ b/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/kakao/client/KakaoInfoClient.java @@ -0,0 +1,19 @@ +package allchive.server.infrastructure.oauth.kakao.client; + + +import allchive.server.infrastructure.oauth.kakao.config.KakaoInfoConfig; +import allchive.server.infrastructure.oauth.kakao.dto.KakaoUnlinkTarget; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestHeader; + +@FeignClient( + name = "KakaoInfoClient", + url = "https://kapi.kakao.com", + configuration = KakaoInfoConfig.class) +public interface KakaoInfoClient { + @PostMapping(path = "/v1/user/unlink", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) + void unlinkUser( + @RequestHeader("Authorization") String adminKey, KakaoUnlinkTarget unlinkKaKaoTarget); +} diff --git a/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/kakao/client/KakaoOauthClient.java b/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/kakao/client/KakaoOauthClient.java new file mode 100644 index 00000000..3d9c1b95 --- /dev/null +++ b/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/kakao/client/KakaoOauthClient.java @@ -0,0 +1,30 @@ +package allchive.server.infrastructure.oauth.kakao.client; + + +import allchive.server.infrastructure.oauth.kakao.config.KakaoKauthConfig; +import allchive.server.infrastructure.oauth.kakao.dto.KakaoTokenResponse; +import allchive.server.infrastructure.oauth.kakao.dto.OIDCPublicKeysResponse; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; + +@FeignClient( + name = "KakaoAuthClient", + url = "https://kauth.kakao.com", + configuration = KakaoKauthConfig.class) +public interface KakaoOauthClient { + + @PostMapping( + "/oauth/token?grant_type=authorization_code&client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&code={CODE}&client_secret={CLIENT_SECRET}") + KakaoTokenResponse kakaoAuth( + @PathVariable("CLIENT_ID") String clientId, + @PathVariable("REDIRECT_URI") String redirectUri, + @PathVariable("CODE") String code, + @PathVariable("CLIENT_SECRET") String client_secret); + + @Cacheable(cacheNames = "KakaoOIDC", cacheManager = "oidcCacheManager") + @GetMapping("/.well-known/jwks.json") + OIDCPublicKeysResponse getKakaoOIDCOpenKeys(); +} diff --git a/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/kakao/config/KakaoInfoConfig.java b/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/kakao/config/KakaoInfoConfig.java new file mode 100644 index 00000000..8a1c68fc --- /dev/null +++ b/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/kakao/config/KakaoInfoConfig.java @@ -0,0 +1,23 @@ +package allchive.server.infrastructure.oauth.kakao.config; + + +import feign.codec.Encoder; +import feign.codec.ErrorDecoder; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; + +@Import(KakaoInfoErrorDecoder.class) +public class KakaoInfoConfig { + + @Bean + @ConditionalOnMissingBean(value = ErrorDecoder.class) + public KakaoInfoErrorDecoder commonFeignErrorDecoder() { + return new KakaoInfoErrorDecoder(); + } + + @Bean + Encoder formEncoder() { + return new feign.form.FormEncoder(); + } +} diff --git a/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/kakao/config/KakaoInfoErrorDecoder.java b/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/kakao/config/KakaoInfoErrorDecoder.java new file mode 100644 index 00000000..84035733 --- /dev/null +++ b/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/kakao/config/KakaoInfoErrorDecoder.java @@ -0,0 +1,31 @@ +package allchive.server.infrastructure.oauth.kakao.config; + + +import allchive.server.core.error.exception.OtherServerBadRequestException; +import allchive.server.core.error.exception.OtherServerExpiredTokenException; +import allchive.server.core.error.exception.OtherServerForbiddenException; +import allchive.server.core.error.exception.OtherServerUnauthorizedException; +import feign.FeignException; +import feign.Response; +import feign.codec.ErrorDecoder; + +public class KakaoInfoErrorDecoder implements ErrorDecoder { + + @Override + public Exception decode(String methodKey, Response response) { + if (response.status() >= 400) { + switch (response.status()) { + case 401: + throw OtherServerUnauthorizedException.EXCEPTION; + case 403: + throw OtherServerForbiddenException.EXCEPTION; + case 419: + throw OtherServerExpiredTokenException.EXCEPTION; + default: + throw OtherServerBadRequestException.EXCEPTION; + } + } + + return FeignException.errorStatus(methodKey, response); + } +} diff --git a/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/kakao/config/KakaoKauthConfig.java b/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/kakao/config/KakaoKauthConfig.java new file mode 100644 index 00000000..4f8d093e --- /dev/null +++ b/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/kakao/config/KakaoKauthConfig.java @@ -0,0 +1,23 @@ +package allchive.server.infrastructure.oauth.kakao.config; + + +import feign.codec.Encoder; +import feign.codec.ErrorDecoder; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; + +@Import(KauthErrorDecoder.class) +public class KakaoKauthConfig { + + @Bean + @ConditionalOnMissingBean(value = ErrorDecoder.class) + public KauthErrorDecoder commonFeignErrorDecoder() { + return new KauthErrorDecoder(); + } + + @Bean + Encoder formEncoder() { + return new feign.form.FormEncoder(); + } +} diff --git a/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/kakao/config/KauthErrorDecoder.java b/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/kakao/config/KauthErrorDecoder.java new file mode 100644 index 00000000..adea2f7c --- /dev/null +++ b/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/kakao/config/KauthErrorDecoder.java @@ -0,0 +1,23 @@ +package allchive.server.infrastructure.oauth.kakao.config; + + +import allchive.server.infrastructure.oauth.kakao.dto.KakaoKauthErrorResponse; +import allchive.server.infrastructure.oauth.kakao.exception.KakaoKauthErrorCode; +import feign.Response; +import feign.codec.ErrorDecoder; + +public class KauthErrorDecoder implements ErrorDecoder { + @Override + public Exception decode(String methodKey, Response response) { + KakaoKauthErrorResponse body = KakaoKauthErrorResponse.from(response); + + try { + KakaoKauthErrorCode kakaoKauthErrorCode = + KakaoKauthErrorCode.valueOf(body.getErrorCode()); + throw kakaoKauthErrorCode.getDynamicException(); + } catch (IllegalArgumentException e) { + KakaoKauthErrorCode koeInvalidRequest = KakaoKauthErrorCode.KOE_INVALID_REQUEST; + throw koeInvalidRequest.getDynamicException(); + } + } +} diff --git a/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/kakao/dto/KakaoInformationResponse.java b/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/kakao/dto/KakaoInformationResponse.java new file mode 100644 index 00000000..ff4a9e9b --- /dev/null +++ b/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/kakao/dto/KakaoInformationResponse.java @@ -0,0 +1,67 @@ +package allchive.server.infrastructure.oauth.kakao.dto; + + +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@JsonNaming(SnakeCaseStrategy.class) +public class KakaoInformationResponse { + + private Properties properties; + private String id; + + private KakaoAccount kakaoAccount; + + @Getter + @NoArgsConstructor + @JsonNaming(SnakeCaseStrategy.class) + public static class Properties { + private String nickname; + } + + @Getter + @NoArgsConstructor + @JsonNaming(SnakeCaseStrategy.class) + public static class KakaoAccount { + + private Profile profile; + private String email; + private String phoneNumber; + private String name; + + @Getter + @NoArgsConstructor + @JsonNaming(SnakeCaseStrategy.class) + public static class Profile { + private String profileImageUrl; + } + + public String getProfileImageUrl() { + return profile.getProfileImageUrl(); + } + } + + public String getId() { + return id; + } + + public String getEmail() { + return kakaoAccount.getEmail(); + } + + public String getPhoneNumber() { + return kakaoAccount.getPhoneNumber(); + } + + public String getName() { + return kakaoAccount.getName() != null ? kakaoAccount.getName() : properties.getNickname(); + } + + public String getProfileUrl() { + return kakaoAccount.getProfileImageUrl(); + } +} diff --git a/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/kakao/dto/KakaoKauthErrorResponse.java b/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/kakao/dto/KakaoKauthErrorResponse.java new file mode 100644 index 00000000..e45a713b --- /dev/null +++ b/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/kakao/dto/KakaoKauthErrorResponse.java @@ -0,0 +1,30 @@ +package allchive.server.infrastructure.oauth.kakao.dto; + + +import allchive.server.core.error.exception.InternalServerError; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import feign.Response; +import java.io.IOException; +import java.io.InputStream; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@JsonNaming(SnakeCaseStrategy.class) +public class KakaoKauthErrorResponse { + private String error; + private String errorCode; + private String errorDescription; + + public static KakaoKauthErrorResponse from(Response response) { + try (InputStream bodyIs = response.body().asInputStream()) { + ObjectMapper mapper = new ObjectMapper(); + return mapper.readValue(bodyIs, KakaoKauthErrorResponse.class); + } catch (IOException e) { + throw InternalServerError.EXCEPTION; + } + } +} diff --git a/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/kakao/dto/KakaoTokenResponse.java b/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/kakao/dto/KakaoTokenResponse.java new file mode 100644 index 00000000..67720997 --- /dev/null +++ b/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/kakao/dto/KakaoTokenResponse.java @@ -0,0 +1,16 @@ +package allchive.server.infrastructure.oauth.kakao.dto; + + +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@JsonNaming(SnakeCaseStrategy.class) +public class KakaoTokenResponse { + private String accessToken; + private String refreshToken; + private String idToken; +} diff --git a/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/kakao/dto/KakaoUnlinkTarget.java b/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/kakao/dto/KakaoUnlinkTarget.java new file mode 100644 index 00000000..cec7d3ed --- /dev/null +++ b/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/kakao/dto/KakaoUnlinkTarget.java @@ -0,0 +1,22 @@ +package allchive.server.infrastructure.oauth.kakao.dto; + + +import lombok.Getter; + +@Getter +public class KakaoUnlinkTarget { + + @feign.form.FormProperty("target_id_type") + private String targetIdType = "user_id"; + + @feign.form.FormProperty("target_id") + private String aud; + + private KakaoUnlinkTarget(String aud) { + this.aud = aud; + } + + public static KakaoUnlinkTarget from(String aud) { + return new KakaoUnlinkTarget(aud); + } +} diff --git a/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/kakao/dto/OIDCPublicKeyDto.java b/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/kakao/dto/OIDCPublicKeyDto.java new file mode 100644 index 00000000..1d877318 --- /dev/null +++ b/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/kakao/dto/OIDCPublicKeyDto.java @@ -0,0 +1,15 @@ +package allchive.server.infrastructure.oauth.kakao.dto; + + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class OIDCPublicKeyDto { + private String kid; + private String alg; + private String use; + private String n; + private String e; +} diff --git a/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/kakao/dto/OIDCPublicKeysResponse.java b/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/kakao/dto/OIDCPublicKeysResponse.java new file mode 100644 index 00000000..44327ee5 --- /dev/null +++ b/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/kakao/dto/OIDCPublicKeysResponse.java @@ -0,0 +1,12 @@ +package allchive.server.infrastructure.oauth.kakao.dto; + + +import java.util.List; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class OIDCPublicKeysResponse { + List keys; +} diff --git a/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/kakao/exception/KakaoKauthErrorCode.java b/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/kakao/exception/KakaoKauthErrorCode.java new file mode 100644 index 00000000..383cebd7 --- /dev/null +++ b/Infrastructure/src/main/java/allchive/server/infrastructure/oauth/kakao/exception/KakaoKauthErrorCode.java @@ -0,0 +1,56 @@ +package allchive.server.infrastructure.oauth.kakao.exception; + +import static allchive.server.core.consts.AllchiveConst.BAD_REQUEST; + +import allchive.server.core.dto.ErrorReason; +import allchive.server.core.error.BaseDynamicException; +import allchive.server.core.error.BaseErrorCode; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum KakaoKauthErrorCode implements BaseErrorCode { + KOE101(BAD_REQUEST, "KAKAO_KOE101", "invalid_client", "잘못된 앱 키 타입을 사용하거나 앱 키에 오타가 있을 경우"), + KOE009(BAD_REQUEST, "KAKAO_KOE009", "misconfigured", "등록되지 않은 플랫폼에서 액세스 토큰을 요청 하는 경우"), + KOE010( + BAD_REQUEST, + "KAKAO_KOE101", + "invalid_client", + "클라이언트 시크릿(Client secret) 기능을 사용하는 앱에서 토큰 요청 시 client_secret 값을 전달하지 않거나 정확하지 않은 값을 전달하는 경우"), + KOE303( + BAD_REQUEST, + "KAKAO_KOE303", + "invalid_grant", + "인가 코드 요청 시 사용한 redirect_uri와 액세스 토큰 요청 시 사용한 redirect_uri가 다른 경우"), + KOE319(BAD_REQUEST, "KAKAO_KOE319", "invalid_grant", "토큰 갱신 요청 시 리프레시 토큰을 전달하지 않는 경우"), + KOE320( + BAD_REQUEST, + "KAKAO_KOE320", + "invalid_grant", + "동일한 인가 코드를 두 번 이상 사용하거나, 이미 만료된 인가 코드를 사용한 경우, 혹은 인가 코드를 찾을 수 없는 경우"), + KOE322( + BAD_REQUEST, + "KAKAO_KOE322", + "invalid_grant", + "refresh_token을 찾을 수 없거나 이미 만료된 리프레시 토큰을 사용한 경우"), + KOE_INVALID_REQUEST(BAD_REQUEST, "KAKAO_KOE_INVALID_REQUEST", "invalid_request", "잘못된 요청인 경우"), + KOE400(BAD_REQUEST, "KAKAO_KOE400", "invalid_token", "ID 토큰 값이 전달되지 않았거나 올바른 형식이 아닌 ID 토큰인 경우"), + KOE401(BAD_REQUEST, "KAKAO_KOE401", "invalid_token", "ID 토큰을 발급한 인증 기관(iss)이 카카오 인증 서버"), + KOE402(BAD_REQUEST, "KAKAO_KOE402", "invalid_token", "서명이 올바르지 않아 유효한 ID 토큰이 아닌 경우"), + KOE403(BAD_REQUEST, "KAKAO_KOE403", "invalid_token", "만료된 ID 토큰인 경우"); + + private Integer status; + private String errorCode; + private String error; + private String reason; + + public BaseDynamicException getDynamicException() { + return new BaseDynamicException(status, errorCode, reason); + } + + @Override + public ErrorReason getErrorReason() { + return ErrorReason.builder().status(status).code(errorCode).reason(reason).build(); + } +} diff --git a/Infrastructure/src/main/java/allchive/server/infrastructure/redis/config/RedisCacheConfig.java b/Infrastructure/src/main/java/allchive/server/infrastructure/redis/config/RedisCacheConfig.java new file mode 100644 index 00000000..51d3a696 --- /dev/null +++ b/Infrastructure/src/main/java/allchive/server/infrastructure/redis/config/RedisCacheConfig.java @@ -0,0 +1,54 @@ +package allchive.server.infrastructure.redis.config; + + +import java.time.Duration; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@EnableCaching +@Configuration +public class RedisCacheConfig { + @Bean + @Primary + public CacheManager redisCacheManager(RedisConnectionFactory cf) { + RedisCacheConfiguration redisCacheConfiguration = + RedisCacheConfiguration.defaultCacheConfig() + .serializeKeysWith( + RedisSerializationContext.SerializationPair.fromSerializer( + new StringRedisSerializer())) + .serializeValuesWith( + RedisSerializationContext.SerializationPair.fromSerializer( + new GenericJackson2JsonRedisSerializer())) + .entryTtl(Duration.ofMinutes(30L)); + + return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(cf) + .cacheDefaults(redisCacheConfiguration) + .build(); + } + + @Bean + public CacheManager oidcCacheManager(RedisConnectionFactory cf) { + RedisCacheConfiguration redisCacheConfiguration = + RedisCacheConfiguration.defaultCacheConfig() + .serializeKeysWith( + RedisSerializationContext.SerializationPair.fromSerializer( + new StringRedisSerializer())) + .serializeValuesWith( + RedisSerializationContext.SerializationPair.fromSerializer( + new GenericJackson2JsonRedisSerializer())) + .entryTtl(Duration.ofDays(7L)); + + return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(cf) + .cacheDefaults(redisCacheConfiguration) + .build(); + } +} diff --git a/Infrastructure/src/main/java/allchive/server/infrastructure/redis/config/RedisConfig.java b/Infrastructure/src/main/java/allchive/server/infrastructure/redis/config/RedisConfig.java new file mode 100644 index 00000000..346a15d3 --- /dev/null +++ b/Infrastructure/src/main/java/allchive/server/infrastructure/redis/config/RedisConfig.java @@ -0,0 +1,56 @@ +package allchive.server.infrastructure.redis.config; + + +import java.time.Duration; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisKeyValueAdapter; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@EnableRedisRepositories( + basePackages = "allchive.server", + enableKeyspaceEvents = RedisKeyValueAdapter.EnableKeyspaceEvents.ON_STARTUP, + keyspaceNotificationsConfigParameter = "") +@Configuration +public class RedisConfig { + + @Value("${spring.redis.host}") + private String host; + + @Value("${spring.redis.port}") + private int port; + + @Value("${spring.redis.password}") + private String redisPassword; + + @Bean + public RedisConnectionFactory redisConnectionFactory() { + RedisStandaloneConfiguration redisConfig = new RedisStandaloneConfiguration(host, port); + + // if (redisPassword != null && !redisPassword.isBlank()) + // redisConfig.setPassword(redisPassword); + + LettuceClientConfiguration clientConfig = + LettuceClientConfiguration.builder() + .commandTimeout(Duration.ofSeconds(1)) + .shutdownTimeout(Duration.ZERO) + .build(); + return new LettuceConnectionFactory(redisConfig, clientConfig); + } + + @Bean + public RedisTemplate redisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory()); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new StringRedisSerializer()); + return redisTemplate; + } +} diff --git a/Infrastructure/src/main/java/allchive/server/infrastructure/redis/config/RedissonConfig.java b/Infrastructure/src/main/java/allchive/server/infrastructure/redis/config/RedissonConfig.java new file mode 100644 index 00000000..0fc00e30 --- /dev/null +++ b/Infrastructure/src/main/java/allchive/server/infrastructure/redis/config/RedissonConfig.java @@ -0,0 +1,27 @@ +package allchive.server.infrastructure.redis.config; + + +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class RedissonConfig { + @Value("${spring.redis.host}") + private String redisHost; + + @Value("${spring.redis.port}") + private int redisPort; + + private static final String REDISSON_HOST_PREFIX = "redis://"; + + @Bean + public RedissonClient redissonClient() { + Config config = new Config(); + config.useSingleServer().setAddress(REDISSON_HOST_PREFIX + redisHost + ":" + redisPort); + return Redisson.create(config); + } +} diff --git a/Infrastructure/src/main/java/allchive/server/infrastructure/s3/ImageUrlDto.java b/Infrastructure/src/main/java/allchive/server/infrastructure/s3/ImageUrlDto.java new file mode 100644 index 00000000..38b37418 --- /dev/null +++ b/Infrastructure/src/main/java/allchive/server/infrastructure/s3/ImageUrlDto.java @@ -0,0 +1,19 @@ +package allchive.server.infrastructure.s3; + + +import lombok.Builder; +import lombok.Getter; + +@Getter +public class ImageUrlDto { + private String url; + + @Builder + private ImageUrlDto(String url) { + this.url = url; + } + + public static ImageUrlDto of(String url) { + return ImageUrlDto.builder().url(url).build(); + } +} diff --git a/Infrastructure/src/main/java/allchive/server/infrastructure/s3/PresignedType.java b/Infrastructure/src/main/java/allchive/server/infrastructure/s3/PresignedType.java new file mode 100644 index 00000000..335ffdff --- /dev/null +++ b/Infrastructure/src/main/java/allchive/server/infrastructure/s3/PresignedType.java @@ -0,0 +1,7 @@ +package allchive.server.infrastructure.s3; + +public enum PresignedType { + USER, + ARCHIVING, + CONTENT +} diff --git a/Infrastructure/src/main/java/allchive/server/infrastructure/s3/S3Config.java b/Infrastructure/src/main/java/allchive/server/infrastructure/s3/S3Config.java new file mode 100644 index 00000000..670754c5 --- /dev/null +++ b/Infrastructure/src/main/java/allchive/server/infrastructure/s3/S3Config.java @@ -0,0 +1,29 @@ +package allchive.server.infrastructure.s3; + + +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.regions.Regions; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class S3Config { + @Value("${aws.access-key}") + private String accessKey; + + @Value("${aws.secret-key}") + private String secretKey; + + @Bean + public AmazonS3 amazonS3Client() { + BasicAWSCredentials basicAWSCredentials = new BasicAWSCredentials(accessKey, secretKey); + return AmazonS3ClientBuilder.standard() + .withRegion(Regions.AP_NORTHEAST_2) + .withCredentials(new AWSStaticCredentialsProvider(basicAWSCredentials)) + .build(); + } +} diff --git a/Infrastructure/src/main/java/allchive/server/infrastructure/s3/S3PresignedUrlService.java b/Infrastructure/src/main/java/allchive/server/infrastructure/s3/S3PresignedUrlService.java new file mode 100644 index 00000000..78211363 --- /dev/null +++ b/Infrastructure/src/main/java/allchive/server/infrastructure/s3/S3PresignedUrlService.java @@ -0,0 +1,67 @@ +package allchive.server.infrastructure.s3; + + +import allchive.server.core.error.exception.InternalServerError; +import com.amazonaws.HttpMethod; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.Headers; +import com.amazonaws.services.s3.model.CannedAccessControlList; +import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest; +import java.util.Date; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +public class S3PresignedUrlService { + private final AmazonS3 amazonS3; + + @Value("${aws.s3.bucket}") + private String bucket; + + @Value("${aws.s3.base-url}") + private String baseUrl; + + public ImageUrlDto getPreSignedUrl(Long id, PresignedType presignedType) { + String fileName = generateFileName(id, presignedType); + GeneratePresignedUrlRequest generatePresignedUrlRequest = + getGeneratePreSignedUrlRequest(fileName); + String url = amazonS3.generatePresignedUrl(generatePresignedUrlRequest).toString(); + return ImageUrlDto.of(url); + } + + private String generateFileName(Long id, PresignedType presignedType) { + String fileName; + switch (presignedType) { + case USER -> fileName = baseUrl + "/user/"; + case CONTENT -> fileName = baseUrl + "/content/"; + case ARCHIVING -> fileName = baseUrl + "/archiving/"; + default -> throw InternalServerError.EXCEPTION; + } + return fileName + id.toString() + "/" + UUID.randomUUID(); + } + + private GeneratePresignedUrlRequest getGeneratePreSignedUrlRequest(String fileName) { + GeneratePresignedUrlRequest generatePresignedUrlRequest = + new GeneratePresignedUrlRequest(bucket, fileName) + .withMethod(HttpMethod.PUT) + .withKey(fileName) + .withExpiration(getPreSignedUrlExpiration()); + generatePresignedUrlRequest.addRequestParameter( + Headers.S3_CANNED_ACL, CannedAccessControlList.PublicRead.toString()); + return generatePresignedUrlRequest; + } + + private Date getPreSignedUrlExpiration() { + Date expiration = new Date(); + long expTimeMillis = expiration.getTime(); + // 4분 + expTimeMillis += 1000 * 60 * 4; + expiration.setTime(expTimeMillis); + return expiration; + } +} diff --git a/Infrastructure/src/main/resources/application-infrastructure.yml b/Infrastructure/src/main/resources/application-infrastructure.yml index e69de29b..368bfafc 100644 --- a/Infrastructure/src/main/resources/application-infrastructure.yml +++ b/Infrastructure/src/main/resources/application-infrastructure.yml @@ -0,0 +1,30 @@ +aws: + access-key: ${AWS_ACCESS_KEY:testKey} + secret-key: ${AWS_SECRET_KEY:secretKey} + s3: + bucket: ${AWS_S3_BUCKET:bucket} + base-url: ${AWS_S3_BASEURL:baseUrl} + + + +--- +spring: + config: + activate: + on-profile: local + + redis: + host: localhost + port: 6379 + password: qlalfqjsgh + +--- +spring: + config: + activate: + on-profile: dev + + redis: + host: ${REDIS_HOST:localhost} + port: ${REDIS_PORT:6379} + password: ${REDIS_PASSWORD:qlalfqjsgh} \ No newline at end of file diff --git a/build.gradle b/build.gradle index 125cb6f5..6c665dda 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'org.springframework.boot' version '3.1.0' + id 'org.springframework.boot' version '2.7.13' id 'com.diffplug.spotless' version '6.11.0' } @@ -33,10 +33,12 @@ subprojects { } dependencies { - implementation 'org.springframework.boot:spring-boot-starter' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' + annotationProcessor "org.springframework.boot:spring-boot-configuration-processor" testImplementation 'org.springframework.boot:spring-boot-starter-test' + testCompileOnly 'org.projectlombok:lombok' + testAnnotationProcessor 'org.projectlombok:lombok' } test { diff --git a/config/nginx/Dockerfile b/config/nginx/Dockerfile new file mode 100644 index 00000000..32fc4fb9 --- /dev/null +++ b/config/nginx/Dockerfile @@ -0,0 +1,2 @@ +FROM nginx:1.21.4 +COPY ./default.conf /etc/nginx/conf.d/default.conf \ No newline at end of file diff --git a/config/nginx/default.conf b/config/nginx/default.conf new file mode 100644 index 00000000..e3b213a3 --- /dev/null +++ b/config/nginx/default.conf @@ -0,0 +1,18 @@ +server { + listen 80; + server_name staging.allchive.co.kr; + + autoindex_localtime on; + + if ($http_x_forwarded_proto != 'https') { + return 301 https://$host$request_uri; + } + + location /api { + proxy_pass http://server:8080; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $host; + proxy_set_header X-Real_IP $remote_addr; + proxy_redirect off; + } +} \ No newline at end of file diff --git a/config/nginx/default.prod.conf b/config/nginx/default.prod.conf new file mode 100644 index 00000000..dc7de01b --- /dev/null +++ b/config/nginx/default.prod.conf @@ -0,0 +1,18 @@ +server { + listen 80; + server_name www.allchive.co.kr; + + autoindex_localtime on; + + if ($http_x_forwarded_proto != 'https') { + return 301 https://$host$request_uri; + } + + location /api { + proxy_pass http://server:8080; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $host; + proxy_set_header X-Real_IP $remote_addr; + proxy_redirect off; + } +} \ No newline at end of file diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 00000000..dcf1f3fb --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,29 @@ +version: '3.7' + +services: + server: + image: sanghoonjeong/allchive-dev:latest + container_name: server + hostname: server + env_file: + - .env + environment: + - TZ=Asia/Seoul + expose: + - 8080 + logging: + driver: awslogs + options: + awslogs-group: "all-chive-dev" + awslogs-region: "ap-northeast-2" + awslogs-stream: "server" + + nginx: + depends_on: + - server + restart: always + build: + dockerfile: Dockerfile + context: './nginx' + ports: + - "80:80" \ No newline at end of file diff --git a/docker-compose.local.yml b/docker-compose.local.yml new file mode 100644 index 00000000..ddb2ad05 --- /dev/null +++ b/docker-compose.local.yml @@ -0,0 +1,9 @@ +version: '3.7' + +services: + redis: + image: redis + container_name: redis + hostname: redis + ports: + - "6379:6379" \ No newline at end of file diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 00000000..5c348044 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,29 @@ +version: '3.7' + +services: + server: + image: sanghoonjeong/allchive-prod:latest + container_name: server + hostname: server + env_file: + - .env + environment: + - TZ=Asia/Seoul + expose: + - 8080 + logging: + driver: awslogs + options: + awslogs-group: "all-chive-prod" + awslogs-region: "ap-northeast-2" + awslogs-stream: "server" + + nginx: + depends_on: + - server + restart: always + build: + dockerfile: Dockerfile + context: './nginx' + ports: + - "80:80" \ No newline at end of file