Compare commits
77 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41e0043222 | ||
|
|
bd0a8a8ed1 | ||
|
|
0d34fb8e7a | ||
|
|
ee0638fd3c | ||
|
|
2f39e0cae3 | ||
|
|
b1639ce59d | ||
|
|
98dc605da6 | ||
|
|
63d570613a | ||
|
|
b663b398a8 | ||
|
|
ddf39edf21 | ||
|
|
c997853456 | ||
|
|
05ddace884 | ||
|
|
b9a5196972 | ||
|
|
e777d6f99f | ||
|
|
7bcdfe8c21 | ||
|
|
f5b55d03d8 | ||
|
|
2b78cb1205 | ||
|
|
8ebe8f89b0 | ||
|
|
62ac5ce43a | ||
|
|
efa8b4fbd4 | ||
|
|
b25425afee | ||
|
|
5c4aaace00 | ||
|
|
f7476e96ef | ||
|
|
1a037d021e | ||
|
|
3d390dd672 | ||
|
|
a45924474d | ||
|
|
351fa14579 | ||
|
|
ea604f4c5c | ||
|
|
ec08a08d55 | ||
|
|
fffd4c60a3 | ||
|
|
95d995694e | ||
|
|
92ecb456e2 | ||
|
|
6e1ec4c1d8 | ||
|
|
c3218a0b8a | ||
|
|
07623323cb | ||
|
|
52cd14e1cf | ||
|
|
6cd4556f5e | ||
|
|
85e8a54ff3 | ||
|
|
d5f9867f64 | ||
|
|
2bae29b841 | ||
|
|
0d946cd400 | ||
|
|
bfba55e75d | ||
|
|
1c36c22a14 | ||
|
|
5f69d3eca5 | ||
|
|
26b0cc4dcb | ||
|
|
62abd6fc4c | ||
|
|
ed0e29b2a5 | ||
|
|
3aa18022a1 | ||
|
|
917c21bbca | ||
|
|
a755438cc4 | ||
|
|
b3ca77e897 | ||
|
|
7a53589937 | ||
|
|
6912ca24ff | ||
|
|
8bea12f6fd | ||
|
|
1a718b189b | ||
|
|
84850a8eb6 | ||
|
|
9cef2ddef3 | ||
|
|
5f73a8e4f3 | ||
|
|
a1e80852d4 | ||
|
|
2c480220ac | ||
|
|
2ff9132756 | ||
|
|
76cd25e11f | ||
|
|
527edda336 | ||
|
|
dc233daf71 | ||
|
|
1f2efe2da3 | ||
|
|
47ef05cebf | ||
|
|
a775d69af6 | ||
|
|
8e76b351bf | ||
|
|
1a58449cd3 | ||
|
|
80450a16a7 | ||
|
|
4d968d1b7b | ||
|
|
ce2109915d | ||
|
|
4c9ab85c5c | ||
|
|
ca5572803e | ||
|
|
d6aa2c3da9 | ||
|
|
1afb52af3b | ||
|
|
3ffe39a24f |
49
.github/workflows/cicd-branch-main.yml
vendored
Normal file
49
.github/workflows/cicd-branch-main.yml
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
name: cicd
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
name: Deploy
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: checkout src
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
java-version: '11'
|
||||
distribution: 'adopt'
|
||||
|
||||
- name: Grant execute permission for gradlew
|
||||
run: chmod 777 gradlew
|
||||
shell: bash
|
||||
|
||||
- name: Clean build test with Gradle # clean build test
|
||||
run: ./gradlew clean build test
|
||||
shell: bash
|
||||
|
||||
- name: Make zip file # zip 파일 생성
|
||||
run: zip -r ./springboot-blog.zip .
|
||||
shell: bash
|
||||
|
||||
- name: Configure AWS credentials # AWS 자격 증명
|
||||
uses: aws-actions/configure-aws-credentials@v1
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }}
|
||||
aws-region: ${{ secrets.AWS_REGION }}
|
||||
|
||||
- name: Upload to S3 # S3 업로드
|
||||
run: aws s3 cp --region ${{ secrets.AWS_REGION }} ./springboot-blog.zip s3://${{ secrets.AWS_S3_BUCKET }}/springboot-blog.zip
|
||||
|
||||
- name: Code Deploy # CodeDeploy에 배포 요청
|
||||
run: aws deploy create-deployment --application-name ${{ secrets.AWS_CODEDEPLOY_NAME }} --deployment-config-name CodeDeployDefault.AllAtOnce --deployment-group-name ${{ secrets.AWS_CODEDEPLOY_GROUP }} --s3-location bucket=${{ secrets.AWS_S3_BUCKET }},bundleType=zip,key=springboot-blog.zip
|
||||
62
.travis.yml
62
.travis.yml
@@ -1,62 +0,0 @@
|
||||
language: java
|
||||
jdk:
|
||||
- openjdk11
|
||||
|
||||
branches:
|
||||
only:
|
||||
- main
|
||||
|
||||
# travis CI 서버 home
|
||||
cache:
|
||||
directories:
|
||||
- '$HOME/.m2/repository'
|
||||
- '$HOME/.gradle'
|
||||
|
||||
script: "./gradlew clean build"
|
||||
|
||||
before_install:
|
||||
- chmod +x gradlew
|
||||
|
||||
before_deploy:
|
||||
- mkdir -p before-deploy
|
||||
- cp scripts/*.sh before-deploy/
|
||||
- cp appspec.yml before-deploy/
|
||||
- chmod +x build/libs/*.jar
|
||||
- cp build/libs/*.jar before-deploy/
|
||||
- cd before-deploy && zip -r before-deploy ./*
|
||||
- cd ../ && mkdir -p deploy
|
||||
- mv before-deploy/before-deploy.zip deploy/springboot-blog.zip
|
||||
|
||||
deploy:
|
||||
- provider: s3
|
||||
access_key_id: $AWS_ACESS_KEY
|
||||
secret_access_key: $AWS_SECRET_KEY
|
||||
bucket: blog-build-bucket
|
||||
region: ap-northeast-2
|
||||
skip_cleanup: true
|
||||
acl: private
|
||||
local_dir: deploy
|
||||
wait-until-deployed: true
|
||||
on:
|
||||
all_branches: true
|
||||
|
||||
- provider: codedeploy
|
||||
access_key_id: $AWS_ACESS_KEY
|
||||
secret_access_key: $AWS_SECRET_KEY
|
||||
bucket: blog-build-bucket
|
||||
key: springboot-blog.zip
|
||||
bundle_type: zip
|
||||
application: springboot-blog
|
||||
deployment_group: springboot-blog-group
|
||||
region: ap-northeast-2
|
||||
wait-until-deployed: true
|
||||
on:
|
||||
all_branches: true
|
||||
|
||||
|
||||
# CI 실행 완료 시 메일로 알람
|
||||
notifications:
|
||||
email:
|
||||
recipients:
|
||||
- creee77@gmail.com
|
||||
|
||||
130
README.md
130
README.md
@@ -13,10 +13,12 @@ https://www.jiniaslog.co.kr/
|
||||
* [기타 주요 라이브러리](#기타-주요-라이브러리)
|
||||
- [핵심 키워드](#핵심-키워드)
|
||||
- [시스템 아키텍쳐](#시스템-아키텍쳐)
|
||||
- [WAS 아키텍처](#WAS-아키텍처)
|
||||
- [E-R 다이어그램](#e-r-다이어그램)
|
||||
- [프로젝트 목적](#프로젝트-목적)
|
||||
* [블로그 프로젝트를 기획한 이유?](#블로그-프로젝트를-기획한-이유?)
|
||||
- [핵심 기능](#핵심-기능)
|
||||
* [헥사고날 아키텍처로 리아키텍처링](#헥사고날-아키텍처로-리아키텍처링)
|
||||
* [소셜 로그인](#소셜-로그인)
|
||||
* [로그 추적기](#로그-추적기)
|
||||
* [반응형 웹](#반응형-웹)
|
||||
@@ -61,6 +63,7 @@ https://www.jiniaslog.co.kr/
|
||||
- Spring Data JPA
|
||||
- Mybatis
|
||||
- EhCache
|
||||
- Qdsl
|
||||
|
||||
#### Build tool
|
||||
- Gradle
|
||||
@@ -71,7 +74,8 @@ https://www.jiniaslog.co.kr/
|
||||
#### Infra
|
||||
- AWS EC2
|
||||
- AWS S3
|
||||
- Travis CI
|
||||
- ~~Travis CI~~
|
||||
- Github Actions
|
||||
- AWS CodeDeploy
|
||||
- AWS Route53
|
||||
|
||||
@@ -85,6 +89,7 @@ https://www.jiniaslog.co.kr/
|
||||
- Lombok
|
||||
- Github-api
|
||||
- Toast Ui Editor
|
||||
- MapStruct
|
||||
|
||||
## 핵심 키워드
|
||||
|
||||
@@ -92,10 +97,21 @@ https://www.jiniaslog.co.kr/
|
||||
- AWS / 리눅스 기반 CI/CD 무중단 배포 인프라 구축
|
||||
- JPA, Hibernate를 사용한 도메인 설계
|
||||
- MVC 프레임워크 기반 백엔드 서버 구축
|
||||
- 헥사고날 아키텍처
|
||||
|
||||
## 시스템 아키텍쳐
|
||||

|
||||
|
||||
## WAS 아키텍처
|
||||
|
||||
### 헥사고날 아키텍처
|
||||
|
||||

|
||||
|
||||
### 패키징 구조 예시
|
||||
|
||||

|
||||
|
||||
## E-R 다이어그램
|
||||

|
||||
|
||||
@@ -124,21 +140,75 @@ https://www.jiniaslog.co.kr/
|
||||
앞으로 제 프로그래밍 공부와 개발 기록, 그리고 유지보수를 같이할 블로그를 만들어 보기로 결정했습니다.
|
||||
|
||||
|
||||
## 핵심 기능
|
||||
## 핵심 기능(릴리즈 후 추가 개선)
|
||||
|
||||
### 소셜 로그인
|
||||
|
||||
소셜 로그인 구현을 위해 스프링 시큐리티와 OAuth2 인증방식을 사용했으며,
|
||||
### 메모리가 부족한 프리티어 환경에서 원활한 운영을 위해 Swap으로 가상 메모리 설정
|
||||
|
||||
소셜 인증 제공자 추가로 인한 확장을 대비해 엑세스 토큰으로 받아오는 유저 정보를 OAuth2UserInfo 인터페이스로 추상화하여 파싱하도록 설계했습니다.
|
||||
(2022.05.12)
|
||||
|
||||
[Oauth2UserInfo인터페이스](https://github.com/jinia91/blog/blob/a1d9381d8675ef01fbe3cf7371fe642a1847a943/src/main/java/myblog/blog/member/auth/userinfo/Oauth2UserInfo.java#L8)
|
||||
프리티어환경에서 서버를 운영하면서 주기적으로 서버가 죽는 이슈가 있었고, 해당 이슈를 트러블 슈팅하며 가상메모리 설정을 진행하였습니다.
|
||||
|
||||
또한 확장성 있는 객체 생성을 위해, 객체 생성을 담당하는 클래스를 익명 인터페이스를 사용한 팩토리 메서드 패턴으로 구현하였습니다.
|
||||
해당 과정은 아래 링크 블로그 글을 통해 확인하실수 있습니다.
|
||||
|
||||
[UserInfoFactory 클래스](https://github.com/jinia91/blog/blob/a1d9381d8675ef01fbe3cf7371fe642a1847a943/src/main/java/myblog/blog/member/auth/UserInfoFactory.java#L18)
|
||||
[[트러블 슈팅]프리티어 환경에서 서버가 주기적으로 죽는 문제](https://www.jiniaslog.co.kr/article/view?articleId=1602)
|
||||
|
||||
### 로그 추적기(2022.02.03 기능 추가)
|
||||
### MapStruct를 사용해 보일러 플레이트 코드 제거
|
||||
|
||||
(2022.05.07)
|
||||
|
||||
기존에는 리플렉션을 통해 객체 매핑을 해주는 `ModelMapper` 를 사용하였으나,
|
||||
|
||||
런타임 시점에서 매번 리플렉션을 사용하며 객체에 접근하고 생성하는 메커니즘의한계, 커뮤니티들에서 report 되는 메모리 누수 이슈등을 보며 고민하다
|
||||
|
||||
인터페이스만 정의해주면 컴파일시점에서 매핑 코드를 자동으로 구현해주는 `MapStruct` 라이브러리로 스택 마이그레이션을 진행하였습니다.
|
||||
|
||||
[기술조사 결과`MapSturct` 라이브러리가 성능적으로 훨씬 뛰어나다고 판단하였으며(링크)](https://better-dev.netlify.app/java/2020/10/26/compare_objectmapper/),
|
||||
|
||||
해당 코드는 커밋내역으로 확인할 수 있습니다.
|
||||
|
||||
- [커밋내역](https://github.com/jinia91/blog/commit/efa8b4fbd41e7cdeccb56d959e356ad0ae1c935c)
|
||||
|
||||
|
||||
### JPQL로 작성된 기존 쿼리 Qdsl로 스택 마이그레이션
|
||||
|
||||
(진행중)
|
||||
|
||||
기존에는 JPA 메서드 쿼리와 JPQL, 그리고 마이바티스를 사용해 영속성 계층을 구현했습니다. 그중 JPQL은 String 기반으로 작성되어
|
||||
|
||||
컴파일단계에서 에러검증이 되지 않으며, 동적 쿼리 작성에 어려움이 있던 단점이 있었고 실제로 동적 쿼리를 작성하지 못해
|
||||
|
||||
두가지 메서드 쿼리를 분기처리하는 방식으로 구현한 코드도 존재했습니다.
|
||||
|
||||
Qdsl을 학습후 기존 JPQL을 걷어내고 Qdsl로 스택 마이그레이션을 진행하는 중입니다.
|
||||
|
||||
### CI 툴 트래비스에서 github 액션으로 스택 마이그레이션
|
||||
|
||||
(2022.04.09)
|
||||
|
||||
트래비스가 org에서 com으로 변경된뒤 기존 무료 정책에서 구독형 유료 모델로 변경되었으며
|
||||
|
||||
무료 credit을 모두 사용한 후 월 요금 30달러를 지불해야 하는 상황을 맞이했습니다.
|
||||
|
||||
개인적으로 개인프로젝트에서 서버도 아니고 ci 툴을 사용하기 위해 비용을 지불하는것은 불필요하다고 판단했고, 코드 관리가 github에서 이루어지는 만큼
|
||||
|
||||
ci역시 같은 github내에서 진행되는것이 보다 바람직하다고 판단하여 github actions로 스택마이그레이션을 진행하였습니다.
|
||||
|
||||
|
||||
|
||||
### 헥사고날 아키텍처로 리아키텍처링
|
||||
(2022-03-27 추가)
|
||||
|
||||
기존 아키텍처는 레이어드 아키텍처 기반에 어설프게 DDD를 적용한 난잡한 구조였습니다. 유지관리에 용이하고 확장에 유연한 보다 꺠끗한 아키텍처구축을 위해
|
||||
|
||||
헥사고날 아키텍처와 클린아키텍처의 개념을 공부하고 프로젝트 구조를 개선하는 작업을 진행했습니다.
|
||||
|
||||
해당과정에서 고민한점과 공부했던 부분들은 아래 블로그에 정리하였습니다.
|
||||
|
||||
[헥사고날 아키텍처 학습과 프로젝트 적용](https://www.jiniaslog.co.kr/article/view?articleId=1152)
|
||||
|
||||
### 로그 추적기
|
||||
(2022.02.03 기능 추가)
|
||||
|
||||
스프링 AOP 기술을 사용하여 프로젝트의 *Controller, *Service, *Repository 에 포인트컷을 지정, 로그를 찍고
|
||||
|
||||
@@ -158,6 +228,9 @@ https://www.jiniaslog.co.kr/
|
||||
|
||||
AOP 학습과 해당 기능 개발을 위해 [인프런, 김영한님의 스프링 핵심 원리 - 고급편](https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B3%A0%EA%B8%89%ED%8E%B8/dashboard) 을 참고했습니다.
|
||||
|
||||
|
||||
## 핵심 기능(릴리즈 전)
|
||||
|
||||
### 반응형 웹
|
||||
|
||||
부트스트랩을 이용하여 작은 모바일 환경은 물론 태블릿 대형 화면에서도 문제없이 작동하는 반응형 웹을 구현하였습니다.
|
||||
@@ -165,6 +238,18 @@ AOP 학습과 해당 기능 개발을 위해 [인프런, 김영한님의 스프
|
||||

|
||||
[구글 서치 콘솔에서 모바일 친화페이지 인증]
|
||||
|
||||
### 소셜 로그인
|
||||
|
||||
소셜 로그인 구현을 위해 스프링 시큐리티와 OAuth2 인증방식을 사용했으며,
|
||||
|
||||
소셜 인증 제공자 추가로 인한 확장을 대비해 엑세스 토큰으로 받아오는 유저 정보를 OAuth2UserInfo 인터페이스로 추상화하여 파싱하도록 설계했습니다.
|
||||
|
||||
[Oauth2UserInfo인터페이스](https://github.com/jinia91/blog/blob/a1d9381d8675ef01fbe3cf7371fe642a1847a943/src/main/java/myblog/blog/member/auth/userinfo/Oauth2UserInfo.java#L8)
|
||||
|
||||
또한 확장성 있는 객체 생성을 위해, 객체 생성을 담당하는 클래스를 익명 인터페이스를 사용한 팩토리 메서드 패턴으로 구현하였습니다.
|
||||
|
||||
[UserInfoFactory 클래스](https://github.com/jinia91/blog/blob/a1d9381d8675ef01fbe3cf7371fe642a1847a943/src/main/java/myblog/blog/member/auth/UserInfoFactory.java#L18)
|
||||
|
||||
### Toast Ui editor
|
||||
|
||||
#### 글작성은 마크다운으로
|
||||
@@ -173,7 +258,8 @@ AOP 학습과 해당 기능 개발을 위해 [인프런, 김영한님의 스프
|
||||
|
||||

|
||||
|
||||
#### 이미지와 썸네일 삽입시는 깃허브 이미지 서버로
|
||||
#### 이미지와 썸네일 삽입시는 ~~깃허브~~ aws s3 이미지 서버로
|
||||
(2022.03.21)
|
||||
|
||||
Toast Ui editor는 기본적으로 컨텐츠 내의 이미지 삽입을 blob으로 컨텐츠와 함께 병기하게 되는데 이경우 장황한 바이너리 코드로 DB에 부담이 되고
|
||||
|
||||
@@ -181,10 +267,17 @@ Toast Ui editor는 기본적으로 컨텐츠 내의 이미지 삽입을 blob으
|
||||
|
||||
1. 이미지 삽입시 후킹으로 blob 코드를 낚아챈 후
|
||||
2. 아작스로 해당 이미지 blob을 앱 서버로 보내기
|
||||
3. 앱서버에서 깃허브 api를 사용해 깃허브 레포지토리에 업로드
|
||||
4. 그리고 이미지의 blob대신 업로드된 url을 반환
|
||||
|
||||
위 과정을 통해 깃헙 레포지토리를 이미지 서버로 활용하고 url만 사용하게끔 로직을 작성했습니다.
|
||||
3. ~~3. 앱서버에서 깃허브 api를 사용해 깃허브 레포지토리에 업로드~~
|
||||
4. was에서 aws s3로 업로드
|
||||
5. 그리고 이미지의 blob대신 업로드된 url을 반환
|
||||
|
||||
깃헙 레포를 사용할경우 이미지 업로드시 무의미한 커밋이 찍혀 전략 패턴을 사용해 업로드 로직을 분리, 고도화한다음 aws S3로 업로드 서버를 변경하였습니다.
|
||||
|
||||
해당 과정은 아래 포스팅주소에서 확인할 수 있습니다.
|
||||
|
||||
[이미지 업로드를 전략패턴으로 재설계한뒤 aws s3로 전환하기](https://www.jiniaslog.co.kr/article/view?articleId=1103)
|
||||
|
||||
|
||||
[아작스로 이미지 업로드](https://github.com/jinia91/blog/blob/a1d9381d8675ef01fbe3cf7371fe642a1847a943/src/main/resources/static/js/thumbnail.js#L13)
|
||||
|
||||
@@ -309,14 +402,10 @@ Validated로 유효성 빈검사를 수행하여 실패 원자성을 유지하
|
||||
|
||||
tagify 라이브러리를 사용하여 태그 기능을 구현하였고 태그나 게시물 컨텐츠의 특정 문자열에 대하여 검색하는 기능을 만들었습니다.
|
||||
|
||||
이때 보다 빠른검색을 위해 게시글의 내용 컨텐츠 타입을 varchar로 저장하고 인덱스를 걸었습니다.
|
||||
검색 쿼리의 경우 "Like %s%" 를 사용하였기에 인덱스를 사용하지 못하기 때문에
|
||||
|
||||
검색 쿼리의 경우 "Like %s%" 를 사용하였기에 인덱스를 제대로 활용하여 레인지 스캔이 타지지는 않지만 인덱스 풀스캔은 타지기 때문에 인덱스가 없는것보다는 낫다고 판단했습니다.
|
||||
차후 성능상 문제가 있을 경우 형태소 분석기를 설치하여 mysql의 FTS기능을 지원하도록 개선해볼 예정입니다.
|
||||
|
||||
차후 성능상 문제가 있을 경우 형태소 분석기를 설치하여 mysql의 FTS기능을 지원하도록 리팩토링할 예정입니다.
|
||||
|
||||

|
||||
[검색 쿼리시 풀 인덱스 스캔이 타지는 모습]
|
||||
|
||||
### 오프셋 페이징을 사용한 페이징박스와 커서 페이징을 사용한 무한 스크롤
|
||||
|
||||
@@ -419,7 +508,10 @@ tagify 라이브러리를 사용하여 태그 기능을 구현하였고 태그
|
||||
|
||||
## 프로젝트 관련 추가 포스팅
|
||||
|
||||
- [기존 프로젝트 헥사고날 아키텍쳐로 리아키텍쳐링하기](https://www.jiniaslog.co.kr/article/view?articleId=1152)
|
||||
- [전략패턴을 적용하여 이미지 업로드 모듈을 새로운 전략으로 스택 마이그레이션하기](https://www.jiniaslog.co.kr/article/view?articleId=1103)
|
||||
- [스프링 JPA 환경에서 오프셋 페이징을 커서 페이징으로 개선하기](https://www.jiniaslog.co.kr/article/view?articleId=202)
|
||||
- [스프링에서 캐시 사용하여 프로젝트 성능 개선해보기](https://www.jiniaslog.co.kr/article/view?articleId=254)
|
||||
- [[CI/CD 무중단배포 프로젝트 적용하기] CI? CD? 기본 개념잡기 (1)](https://www.jiniaslog.co.kr/article/view?articleId=303)
|
||||
- [운영환경에서 정적 리소스의 버전관리와 브라우저의 캐시 문제](https://www.jiniaslog.co.kr/article/view?articleId=402)
|
||||
- [프리티어 환경에서 서버가 주기적으로 죽는 문제](https://www.jiniaslog.co.kr/article/view?articleId=1602)
|
||||
@@ -13,17 +13,16 @@ permissions:
|
||||
|
||||
hooks:
|
||||
AfterInstall:
|
||||
- location: stop.sh
|
||||
- location: scripts/stop.sh
|
||||
timeout: 600
|
||||
runas: root
|
||||
|
||||
ApplicationStart:
|
||||
- location: start.sh
|
||||
- location: scripts/start.sh
|
||||
timeout: 600
|
||||
runas: root
|
||||
|
||||
ValidateService:
|
||||
- location: health.sh
|
||||
- location: scripts/health.sh
|
||||
timeout: 600
|
||||
runas: root
|
||||
|
||||
runas: root
|
||||
42
build.gradle
42
build.gradle
@@ -2,7 +2,7 @@ plugins {
|
||||
id 'org.springframework.boot' version '2.5.6'
|
||||
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
|
||||
id "org.jetbrains.kotlin.jvm" version "1.5.0-RC"
|
||||
// id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
|
||||
id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
|
||||
id "com.github.node-gradle.node" version "3.1.0"
|
||||
id 'java'
|
||||
|
||||
@@ -44,9 +44,8 @@ dependencies {
|
||||
implementation group: 'com.google.code.gson', name: 'gson', version: '2.8.9'
|
||||
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.0'
|
||||
implementation 'io.sentry:sentry-spring-boot-starter:5.6.1'
|
||||
// implementation 'com.querydsl:querydsl-jpa'
|
||||
implementation 'com.querydsl:querydsl-jpa'
|
||||
implementation 'com.github.node-gradle:gradle-node-plugin:3.1.0'
|
||||
implementation group: 'org.modelmapper', name: 'modelmapper', version: '2.4.4'
|
||||
implementation group: 'org.kohsuke', name: 'github-api', version: '1.133'
|
||||
implementation group: 'org.apache.commons', name: 'commons-text', version: '1.9'
|
||||
implementation group: 'com.atlassian.commonmark', name: 'commonmark', version: '0.17.0'
|
||||
@@ -54,6 +53,12 @@ dependencies {
|
||||
implementation 'org.commonmark:commonmark-ext-gfm-tables:0.18.0'
|
||||
implementation group: 'org.jdom', name: 'jdom2', version: '2.0.6'
|
||||
implementation group: 'net.sf.ehcache', name: 'ehcache', version: '2.10.9.2'
|
||||
implementation group: 'com.amazonaws', name: 'aws-java-sdk-s3', version: '1.12.152'
|
||||
|
||||
implementation 'org.mapstruct:mapstruct:1.4.2.Final'
|
||||
annotationProcessor "org.mapstruct:mapstruct-processor:1.4.2.Final"
|
||||
annotationProcessor'org.projectlombok:lombok-mapstruct-binding:0.2.0'
|
||||
|
||||
|
||||
compileOnly 'org.projectlombok:lombok'
|
||||
developmentOnly 'org.springframework.boot:spring-boot-devtools'
|
||||
@@ -78,22 +83,21 @@ test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
// 쿼리DSL 설정
|
||||
//def querydslDir = "$buildDir/generated/querydsl"
|
||||
//
|
||||
//querydsl {
|
||||
// jpa = true
|
||||
// querydslSourcesDir = querydslDir
|
||||
//}
|
||||
//sourceSets {
|
||||
// main.java.srcDir querydslDir
|
||||
//}
|
||||
//configurations {
|
||||
// querydsl.extendsFrom compileClasspath
|
||||
//}
|
||||
//compileQuerydsl {
|
||||
// options.annotationProcessorPath = configurations.querydsl
|
||||
//}
|
||||
def querydslDir = "$buildDir/generated/querydsl"
|
||||
|
||||
querydsl {
|
||||
jpa = true
|
||||
querydslSourcesDir = querydslDir
|
||||
}
|
||||
sourceSets {
|
||||
main.java.srcDir querydslDir
|
||||
}
|
||||
configurations {
|
||||
querydsl.extendsFrom compileClasspath
|
||||
}
|
||||
compileQuerydsl {
|
||||
options.annotationProcessorPath = configurations.querydsl
|
||||
}
|
||||
|
||||
|
||||
compileTestKotlin {
|
||||
|
||||
@@ -24,6 +24,7 @@ then # $up_count >= 1 ("real" 문자열이 있는지 검증)
|
||||
else
|
||||
echo " > Health check의 응답을 알 수 없거나 혹은 실행상태가 아닙니다 "
|
||||
echo "> Health check: ${RESPONSE}"
|
||||
sleep 10
|
||||
fi
|
||||
|
||||
if [ ${RETRY_COUNT} -eq 10 ]
|
||||
|
||||
@@ -16,6 +16,6 @@ then
|
||||
else
|
||||
echo "> kill -15 $IDLE_PID"
|
||||
kill -15 ${IDLE_PID}
|
||||
sleep 5
|
||||
sleep 50
|
||||
fi
|
||||
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
package myblog.blog.article.adapter.incomming;
|
||||
|
||||
import myblog.blog.article.application.port.incomming.ArticleUseCase;
|
||||
import myblog.blog.article.application.port.incomming.TempArticleUseCase;
|
||||
import myblog.blog.article.application.port.incomming.ArticleQueriesUseCase;
|
||||
import myblog.blog.article.application.port.incomming.TagsQueriesUseCase;
|
||||
import myblog.blog.category.appliacation.port.incomming.CategoryQueriesUseCase;
|
||||
import myblog.blog.category.domain.CategoryNotFoundException;
|
||||
import myblog.blog.shared.application.port.incomming.LayoutRenderingUseCase;
|
||||
|
||||
import myblog.blog.article.application.port.incomming.request.ArticleCreateCommand;
|
||||
import myblog.blog.article.application.port.incomming.request.ArticleEditCommand;
|
||||
import myblog.blog.category.appliacation.port.incomming.response.CategoryViewForLayout;
|
||||
import myblog.blog.member.application.port.incomming.response.PrincipalDetails;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import myblog.blog.shared.utils.MetaTagBuildUtils;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.validation.Errors;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import javax.servlet.http.Cookie;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
@Controller
|
||||
@RequiredArgsConstructor
|
||||
public class ArticleController {
|
||||
|
||||
private final ArticleUseCase articleUseCase;
|
||||
private final ArticleQueriesUseCase articleQueriesUseCase;
|
||||
private final TempArticleUseCase tempArticleUseCase;
|
||||
private final TagsQueriesUseCase tagsQueriesUseCase;
|
||||
private final CategoryQueriesUseCase categoryQueriesUseCase;
|
||||
private final LayoutRenderingUseCase layoutRenderingUseCase;
|
||||
|
||||
@GetMapping("article/write")
|
||||
String getArticleWriteForm(Model model) {
|
||||
layoutRenderingUseCase.AddLayoutTo(model);
|
||||
model.addAttribute("categoryInput", categoryQueriesUseCase.findCategoryByTier(2));
|
||||
model.addAttribute("tagsInput", tagsQueriesUseCase.findAllTagDtos());
|
||||
model.addAttribute("articleDto", new ArticleForm());
|
||||
return "article/articleWriteForm";
|
||||
}
|
||||
|
||||
@PostMapping("article/write")
|
||||
@Transactional
|
||||
String writeArticle(@Validated ArticleForm articleForm,
|
||||
@AuthenticationPrincipal PrincipalDetails principal,
|
||||
Errors errors, Model model) {
|
||||
if (errors.hasErrors()) getArticleWriteForm(model);
|
||||
var command = ArticleCreateCommand.from(articleForm, principal.getMemberId());
|
||||
var articleId = articleUseCase.writeArticle(command);
|
||||
articleUseCase.backupArticle(articleId);
|
||||
tempArticleUseCase.deleteTemp();
|
||||
return "redirect:/article/view?articleId=" + articleId;
|
||||
}
|
||||
/*
|
||||
- 아티클 수정 폼 조회
|
||||
*/
|
||||
@GetMapping("/article/edit")
|
||||
String updateArticle(@RequestParam Long articleId, Model model) {
|
||||
var articleDto = articleQueriesUseCase.getArticleForEdit(articleId);
|
||||
layoutRenderingUseCase.AddLayoutTo(model);
|
||||
model.addAttribute("categoryInput", categoryQueriesUseCase.findCategoryByTier(2));
|
||||
model.addAttribute("tagsInput", tagsQueriesUseCase.findAllTagDtos());
|
||||
model.addAttribute("articleDto", articleDto);
|
||||
return "article/articleEditForm";
|
||||
}
|
||||
@PostMapping("/article/edit")
|
||||
@Transactional
|
||||
String editArticle(@RequestParam Long articleId,
|
||||
@ModelAttribute ArticleForm articleForm) {
|
||||
var command = ArticleEditCommand.from(articleId, articleForm);
|
||||
articleUseCase.editArticle(command);
|
||||
return "redirect:/article/view?articleId=" + articleId;
|
||||
}
|
||||
@PostMapping("/article/delete")
|
||||
@Transactional
|
||||
String deleteArticle(@RequestParam Long articleId) {
|
||||
articleUseCase.deleteArticle(articleId);
|
||||
return "redirect:/";
|
||||
}
|
||||
/*
|
||||
- 카테고리별 게시물 조회하기
|
||||
*/
|
||||
@Transactional
|
||||
@GetMapping("article/list")
|
||||
String getArticlesListByCategory(@RequestParam String category,
|
||||
@RequestParam int tier,
|
||||
@RequestParam int page,
|
||||
Model model) {
|
||||
int totalArticleCnt = getTotalArticleCntByCategory(category, categoryQueriesUseCase.getCategoryViewForLayout());
|
||||
var pagingBoxHandler =
|
||||
PagingBoxHandler.createOf(page, totalArticleCnt);
|
||||
var articleDtoList = articleQueriesUseCase.getArticlesByCategory(category, tier, pagingBoxHandler.getCurPageNum());
|
||||
for(var articleDto : articleDtoList) articleDto.parseAndRenderForView();
|
||||
layoutRenderingUseCase.AddLayoutTo(model);
|
||||
model.addAttribute("pagingBox", pagingBoxHandler);
|
||||
model.addAttribute("articleList", articleDtoList);
|
||||
return "article/articleList";
|
||||
}
|
||||
private int getTotalArticleCntByCategory(String category, CategoryViewForLayout categorys) {
|
||||
if (categorys.getTitle().equals(category)) return categorys.getCount();
|
||||
for (var categoryCnt : categorys.getCategoryTCountList()) {
|
||||
if (categoryCnt.getTitle().equals(category)) return categoryCnt.getCount();
|
||||
for (var categoryCntSub : categoryCnt.getCategoryTCountList()) {
|
||||
if (categoryCntSub.getTitle().equals(category)) return categoryCntSub.getCount();
|
||||
}
|
||||
}
|
||||
throw new CategoryNotFoundException();
|
||||
}
|
||||
/*
|
||||
- 태그별 게시물 조회하기
|
||||
*/
|
||||
@GetMapping("article/list/tag/")
|
||||
String getArticlesListByTag(@RequestParam Integer page,
|
||||
@RequestParam String tagName,
|
||||
Model model) {
|
||||
var articleList = articleQueriesUseCase.getArticlesByTag(tagName, page);
|
||||
for(var article : articleList) article.parseAndRenderForView();
|
||||
var pagingBoxHandler = PagingBoxHandler.createOf(page, (int)articleList.getTotalElements());
|
||||
layoutRenderingUseCase.AddLayoutTo(model);
|
||||
model.addAttribute("articleList", articleList);
|
||||
model.addAttribute("pagingBox", pagingBoxHandler);
|
||||
return "article/articleListByTag";
|
||||
}
|
||||
/*
|
||||
- 검색어별 게시물 조회하기
|
||||
*/
|
||||
@GetMapping("article/list/search/")
|
||||
String getArticlesListByKeyword(@RequestParam Integer page,
|
||||
@RequestParam String keyword,
|
||||
Model model) {
|
||||
var articleList = articleQueriesUseCase.getArticlesByKeyword(keyword, page);
|
||||
for(var article : articleList) article.parseAndRenderForView();
|
||||
var pagingBoxHandler = PagingBoxHandler.createOf(page, (int)articleList.getTotalElements());
|
||||
layoutRenderingUseCase.AddLayoutTo(model);
|
||||
model.addAttribute("articleList", articleList);
|
||||
model.addAttribute("pagingBox", pagingBoxHandler);
|
||||
return "article/articleListByKeyword";
|
||||
}
|
||||
/*
|
||||
- 아티클 상세 조회
|
||||
1. 로그인여부 검토
|
||||
2. 게시물 상세조회에 필요한 Dto 전처리
|
||||
3. 메타태그 작성위한 Dto 전처리
|
||||
4. Dto 담기
|
||||
5. 조회수 증가 검토
|
||||
*/
|
||||
@Transactional
|
||||
@GetMapping("/article/view")
|
||||
String readArticle(@RequestParam Long articleId,
|
||||
@AuthenticationPrincipal PrincipalDetails principal,
|
||||
@CookieValue(required = false, name = "view") String cookie,
|
||||
HttpServletResponse response, Model model) {
|
||||
addMemberInfoToModel(principal, model);
|
||||
var articleResponseForDetail = articleQueriesUseCase.getArticleForDetail(articleId);
|
||||
articleResponseForDetail.parseAndRenderForView();
|
||||
var articleTitlesSortByCategory = articleQueriesUseCase
|
||||
.getArticlesByCategoryForDetailView(articleResponseForDetail.getCategory());
|
||||
var metaTags = MetaTagBuildUtils.buildMetaTags(articleResponseForDetail.getTags());
|
||||
var substringContents = getSubStringContentsFrom(articleResponseForDetail.getContent());
|
||||
layoutRenderingUseCase.AddLayoutTo(model);
|
||||
model.addAttribute("article", articleResponseForDetail);
|
||||
model.addAttribute("metaTags",metaTags);
|
||||
model.addAttribute("metaContents",Jsoup.parse(substringContents).text());
|
||||
model.addAttribute("articlesSortBycategory", articleTitlesSortByCategory);
|
||||
if(needToAddHitThroughCheckingCookie(articleId, cookie, response)) articleUseCase.addHit(articleId);
|
||||
return "article/articleView";
|
||||
}
|
||||
|
||||
private void addMemberInfoToModel(PrincipalDetails principal, Model model) {
|
||||
if (principal != null) {
|
||||
model.addAttribute("member", principal.getMember());
|
||||
} else {
|
||||
model.addAttribute("member", null);
|
||||
}
|
||||
}
|
||||
|
||||
private String getSubStringContentsFrom(String content) {
|
||||
String substringContents = null;
|
||||
if(content.length()>200) {
|
||||
substringContents = content.substring(0, 200);
|
||||
}
|
||||
else substringContents = content;
|
||||
return substringContents;
|
||||
}
|
||||
|
||||
/*
|
||||
- 쿠키 추가 / 조회수 증가 검토
|
||||
*/
|
||||
private boolean needToAddHitThroughCheckingCookie(Long articleId, String cookie, HttpServletResponse response) {
|
||||
if (cookie == null) {
|
||||
Cookie viewCookie = new Cookie("view", articleId + "/");
|
||||
viewCookie.setComment("게시물 조회 확인용");
|
||||
viewCookie.setMaxAge(60 * 60);
|
||||
response.addCookie(viewCookie);
|
||||
return true;
|
||||
} else {
|
||||
boolean addHitAvailable = false;
|
||||
boolean isRead = false;
|
||||
String[] viewCookieList = cookie.split("/");
|
||||
for (String alreadyRead : viewCookieList) {
|
||||
if (alreadyRead.equals(String.valueOf(articleId))) {
|
||||
isRead = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!isRead) {
|
||||
cookie += articleId + "/";
|
||||
addHitAvailable = true;
|
||||
}
|
||||
response.addCookie(new Cookie("view", cookie));
|
||||
return addHitAvailable;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package myblog.blog.article.adapter.incomming.web;
|
||||
package myblog.blog.article.adapter.incomming;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
@@ -0,0 +1,51 @@
|
||||
package myblog.blog.article.adapter.incomming;
|
||||
|
||||
import myblog.blog.article.application.port.incomming.response.ArticleResponseForCardBox;
|
||||
import myblog.blog.article.application.port.incomming.ArticleQueriesUseCase;
|
||||
import myblog.blog.shared.application.port.incomming.LayoutRenderingUseCase;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import java.util.List;
|
||||
|
||||
import static myblog.blog.shared.utils.MarkdownUtils.*;
|
||||
|
||||
@Controller
|
||||
@RequiredArgsConstructor
|
||||
public class MainController {
|
||||
|
||||
private final ArticleQueriesUseCase articleQueriesUseCase;
|
||||
private final LayoutRenderingUseCase layoutRenderingUseCase;
|
||||
/*
|
||||
- 메인 화면 제어용 컨트롤러
|
||||
*/
|
||||
@GetMapping("/")
|
||||
String main(Model model) {
|
||||
var popularArticles = articleQueriesUseCase.getPopularArticles();
|
||||
layoutRenderingUseCase.AddLayoutTo(model);
|
||||
model.addAttribute("popularArticles", popularArticles);
|
||||
return "index";
|
||||
}
|
||||
|
||||
/*
|
||||
- 최신 아티클 무한스크롤로 조회
|
||||
*/
|
||||
@GetMapping("/main/article/{lastArticleId}")
|
||||
@ResponseBody List<ArticleResponseForCardBox> mainNextPage(@PathVariable(required = false) Long lastArticleId) {
|
||||
var articles = articleQueriesUseCase.getRecentArticles(lastArticleId);
|
||||
for(var article : articles) article.parseAndRenderForView();
|
||||
return articles;
|
||||
}
|
||||
|
||||
/*
|
||||
* - about me page
|
||||
* */
|
||||
@GetMapping("/aboutMe")
|
||||
String aboutMe(Model model) {
|
||||
layoutRenderingUseCase.AddLayoutTo(model);
|
||||
return "aboutMe";
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package myblog.blog.article.adapter.incomming.web;
|
||||
package myblog.blog.article.adapter.incomming;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
@@ -0,0 +1,28 @@
|
||||
package myblog.blog.article.adapter.incomming;
|
||||
|
||||
import myblog.blog.article.application.port.incomming.TempArticleUseCase;
|
||||
import myblog.blog.article.application.port.incomming.response.TempArticleDto;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/*
|
||||
- 임시 게시물 조회, 저장을 위한 rest 컨트롤러
|
||||
*/
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
public class TempArticleController {
|
||||
|
||||
private final TempArticleUseCase tempArticleUseCase;
|
||||
|
||||
@PostMapping("/article/temp/autoSave")
|
||||
public String autoSaveTemp(@RequestBody TempArticleDto tempArticleDto){
|
||||
tempArticleUseCase.saveTemp(tempArticleDto.getContent());
|
||||
return "저장성공";
|
||||
}
|
||||
|
||||
@GetMapping("/article/temp/getTemp")
|
||||
public @ResponseBody TempArticleDto getTempArticle(){
|
||||
return tempArticleUseCase.getTempArticle();
|
||||
}
|
||||
}
|
||||
@@ -1,305 +0,0 @@
|
||||
package myblog.blog.article.adapter.incomming.web;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import myblog.blog.article.application.port.incomming.ArticleUseCase;
|
||||
import myblog.blog.article.application.port.incomming.TempArticleUseCase;
|
||||
import myblog.blog.article.application.port.incomming.ArticleQueriesUseCase;
|
||||
import myblog.blog.article.application.port.incomming.TagsQueriesUseCase;
|
||||
import myblog.blog.article.application.port.request.ArticleCreateRequest;
|
||||
import myblog.blog.article.application.port.request.ArticleEditRequest;
|
||||
import myblog.blog.article.application.port.response.ArticleResponseByCategory;
|
||||
import myblog.blog.article.application.port.response.ArticleResponseForCardBox;
|
||||
import myblog.blog.article.application.port.response.ArticleResponseForDetail;
|
||||
import myblog.blog.article.application.port.response.ArticleResponseForEdit;
|
||||
import myblog.blog.category.service.CategoryService;
|
||||
import myblog.blog.category.dto.*;
|
||||
import myblog.blog.member.auth.PrincipalDetails;
|
||||
import myblog.blog.member.dto.MemberVo;
|
||||
import myblog.blog.shared.queries.LayoutRenderingQueries;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
import org.modelmapper.ModelMapper;
|
||||
|
||||
import org.springframework.data.domain.*;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.validation.Errors;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.servlet.http.Cookie;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static myblog.blog.shared.utils.MarkdownUtils.*;
|
||||
|
||||
@Controller
|
||||
@RequiredArgsConstructor
|
||||
public class ArticleController {
|
||||
|
||||
private final ArticleUseCase articleUseCase;
|
||||
private final ArticleQueriesUseCase articleQueriesUseCase;
|
||||
private final TempArticleUseCase tempArticleUseCase;
|
||||
private final TagsQueriesUseCase tagsQueriesUseCase;
|
||||
private final CategoryService categoryService;
|
||||
private final LayoutRenderingQueries layoutRenderingQueries;
|
||||
private final ModelMapper modelMapper;
|
||||
|
||||
@GetMapping("article/write")
|
||||
String getArticleWriteForm(Model model) {
|
||||
layoutRenderingQueries.AddLayoutTo(model);
|
||||
model.addAttribute("categoryInput", getCategoryDtosForForm());
|
||||
model.addAttribute("tagsInput", tagsQueriesUseCase.findAllTagDtos());
|
||||
model.addAttribute("articleDto", new ArticleForm());
|
||||
return "article/articleWriteForm";
|
||||
}
|
||||
/*
|
||||
- 아티클 작성 post 요청
|
||||
*/
|
||||
@PostMapping("article/write")
|
||||
@Transactional
|
||||
String writeArticle(@Validated ArticleForm articleForm,
|
||||
@AuthenticationPrincipal PrincipalDetails principal,
|
||||
Errors errors, Model model) {
|
||||
if (errors.hasErrors()) {
|
||||
getArticleWriteForm(model);
|
||||
}
|
||||
Long articleId = articleUseCase.writeArticle(ArticleCreateRequest.from(articleForm,principal.getMemberId()));
|
||||
articleUseCase.backupArticle(articleId);
|
||||
tempArticleUseCase.deleteTemp();
|
||||
return "redirect:/article/view?articleId=" + articleId;
|
||||
}
|
||||
/*
|
||||
- 아티클 수정 폼 조회
|
||||
*/
|
||||
@GetMapping("/article/edit")
|
||||
String updateArticle(@RequestParam Long articleId, Model model) {
|
||||
ArticleResponseForEdit articleDto = articleQueriesUseCase.getArticleForEdit(articleId);
|
||||
layoutRenderingQueries.AddLayoutTo(model);
|
||||
model.addAttribute("categoryInput", getCategoryDtosForForm());
|
||||
model.addAttribute("tagsInput", tagsQueriesUseCase.findAllTagDtos());
|
||||
model.addAttribute("articleDto", articleDto);
|
||||
return "article/articleEditForm";
|
||||
}
|
||||
|
||||
/*
|
||||
- 아티클 수정 요청
|
||||
*/
|
||||
@PostMapping("/article/edit")
|
||||
@Transactional
|
||||
String editArticle(@RequestParam Long articleId,
|
||||
@ModelAttribute ArticleForm articleForm) {
|
||||
articleUseCase.editArticle(ArticleEditRequest.from(articleId, articleForm));
|
||||
return "redirect:/article/view?articleId=" + articleId;
|
||||
}
|
||||
|
||||
/*
|
||||
- 아티클 삭제 요청
|
||||
*/
|
||||
@PostMapping("/article/delete")
|
||||
@Transactional
|
||||
String deleteArticle(@RequestParam Long articleId) {
|
||||
articleUseCase.deleteArticle(articleId);
|
||||
return "redirect:/";
|
||||
}
|
||||
|
||||
/*
|
||||
- 카테고리별 게시물 조회하기
|
||||
*/
|
||||
@Transactional
|
||||
@GetMapping("article/list")
|
||||
String getArticlesListByCategory(@RequestParam String category,
|
||||
@RequestParam Integer tier,
|
||||
@RequestParam Integer page,
|
||||
Model model) {
|
||||
PagingBoxHandler pagingBoxHandler =
|
||||
PagingBoxHandler.createOf(page, getTotalArticleCntByCategory(category, categoryService.getCategoryForView()));
|
||||
|
||||
Slice<ArticleResponseForCardBox> articleDtoList =
|
||||
articleQueriesUseCase.getArticlesByCategory(category, tier, pagingBoxHandler.getCurPageNum());
|
||||
|
||||
for(ArticleResponseForCardBox articleDto : articleDtoList){
|
||||
articleDto.setContent(Jsoup.parse(getHtmlRenderer().render(getParser().parse(articleDto.getContent()))).text());
|
||||
}
|
||||
|
||||
layoutRenderingQueries.AddLayoutTo(model);
|
||||
model.addAttribute("pagingBox", pagingBoxHandler);
|
||||
model.addAttribute("articleList", articleDtoList);
|
||||
|
||||
return "article/articleList";
|
||||
}
|
||||
|
||||
/*
|
||||
- 태그별 게시물 조회하기
|
||||
*/
|
||||
@Transactional
|
||||
@GetMapping("article/list/tag/")
|
||||
String getArticlesListByTag(@RequestParam Integer page,
|
||||
@RequestParam String tagName,
|
||||
Model model) {
|
||||
Page<ArticleResponseForCardBox> articleList =
|
||||
articleQueriesUseCase.getArticlesByTag(tagName, page);
|
||||
|
||||
for(ArticleResponseForCardBox article : articleList){
|
||||
article.setContent(Jsoup.parse(getHtmlRenderer().render(getParser().parse(article.getContent()))).text());
|
||||
}
|
||||
|
||||
PagingBoxHandler pagingBoxHandler =
|
||||
PagingBoxHandler.createOf(page, (int)articleList.getTotalElements());
|
||||
|
||||
layoutRenderingQueries.AddLayoutTo(model);
|
||||
model.addAttribute("articleList", articleList);
|
||||
model.addAttribute("pagingBox", pagingBoxHandler);
|
||||
|
||||
return "article/articleListByTag";
|
||||
}
|
||||
|
||||
/*
|
||||
- 검색어별 게시물 조회하기
|
||||
*/
|
||||
@Transactional
|
||||
@GetMapping("article/list/search/")
|
||||
String getArticlesListByKeyword(@RequestParam Integer page,
|
||||
@RequestParam String keyword,
|
||||
Model model) {
|
||||
Page<ArticleResponseForCardBox> articleList =
|
||||
articleQueriesUseCase.getArticlesByKeyword(keyword, page);
|
||||
|
||||
for(ArticleResponseForCardBox article : articleList){
|
||||
article.setContent(Jsoup.parse(getHtmlRenderer().render(getParser().parse(article.getContent()))).text());
|
||||
}
|
||||
|
||||
PagingBoxHandler pagingBoxHandler =
|
||||
PagingBoxHandler.createOf(page, (int)articleList.getTotalElements());
|
||||
|
||||
layoutRenderingQueries.AddLayoutTo(model);
|
||||
model.addAttribute("articleList", articleList);
|
||||
model.addAttribute("pagingBox", pagingBoxHandler);
|
||||
|
||||
return "article/articleListByKeyword";
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
- 아티클 상세 조회
|
||||
1. 로그인여부 검토
|
||||
2. 게시물 상세조회에 필요한 Dto 전처리
|
||||
3. 메타태그 작성위한 Dto 전처리
|
||||
4. Dto 담기
|
||||
5. 조회수 증가 검토
|
||||
*/
|
||||
@Transactional
|
||||
@GetMapping("/article/view")
|
||||
String readArticle(@RequestParam Long articleId,
|
||||
@AuthenticationPrincipal PrincipalDetails principal,
|
||||
@CookieValue(required = false, name = "view") String cookie,
|
||||
HttpServletResponse response,
|
||||
Model model) {
|
||||
// 1. 로그인 여부에 따라 뷰단에 회원정보 출력 여부 결정
|
||||
if (principal != null) {
|
||||
model.addAttribute("member", MemberVo.from(principal.getMember()));
|
||||
} else {
|
||||
model.addAttribute("member", null);
|
||||
}
|
||||
|
||||
/*
|
||||
2.화면단을 위한 처리
|
||||
*/
|
||||
ArticleResponseForDetail articleResponseForDetail = articleQueriesUseCase.getArticleForDetail(articleId);
|
||||
articleResponseForDetail.setContent(getHtmlRenderer().render(getParser().parse(articleResponseForDetail.getContent())));
|
||||
|
||||
List<ArticleResponseByCategory> articleTitlesSortByCategory =
|
||||
articleQueriesUseCase
|
||||
.getArticlesByCategoryForDetailView(articleResponseForDetail.getCategory());
|
||||
|
||||
// 3. 메타 태그용 Dto 전처리
|
||||
StringBuilder metaTags = new StringBuilder();
|
||||
for (String tag : articleResponseForDetail.getTags()) {
|
||||
metaTags.append(tag).append(", ");
|
||||
}
|
||||
|
||||
String substringContents = null;
|
||||
if(articleResponseForDetail.getContent().length()>200) {
|
||||
substringContents = articleResponseForDetail.getContent().substring(0, 200);
|
||||
}
|
||||
else substringContents = articleResponseForDetail.getContent();
|
||||
|
||||
// 4. 모델 담기
|
||||
layoutRenderingQueries.AddLayoutTo(model);
|
||||
model.addAttribute("article", articleResponseForDetail);
|
||||
model.addAttribute("metaTags",metaTags);
|
||||
model.addAttribute("metaContents",Jsoup.parse(substringContents).text());
|
||||
model.addAttribute("articlesSortBycategory", articleTitlesSortByCategory);
|
||||
|
||||
// 5. 조회수 증가 검토 및 증가
|
||||
if(needToAddHitThroughCheckingCookie(articleId, cookie, response)) articleUseCase.addHit(articleId);
|
||||
|
||||
return "article/articleView";
|
||||
}
|
||||
|
||||
/*
|
||||
- 쿠키 추가 / 조회수 증가 검토
|
||||
*/
|
||||
private boolean needToAddHitThroughCheckingCookie(Long articleId, String cookie, HttpServletResponse response) {
|
||||
if (cookie == null) {
|
||||
Cookie viewCookie = new Cookie("view", articleId + "/");
|
||||
viewCookie.setComment("게시물 조회 확인용");
|
||||
viewCookie.setMaxAge(60 * 60);
|
||||
response.addCookie(viewCookie);
|
||||
return true;
|
||||
} else {
|
||||
boolean addHitAvailable = false;
|
||||
boolean isRead = false;
|
||||
String[] viewCookieList = cookie.split("/");
|
||||
for (String alreadyRead : viewCookieList) {
|
||||
if (alreadyRead.equals(String.valueOf(articleId))) {
|
||||
isRead = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!isRead) {
|
||||
cookie += articleId + "/";
|
||||
addHitAvailable = true;
|
||||
}
|
||||
response.addCookie(new Cookie("view", cookie));
|
||||
return addHitAvailable;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
- 카테고리별 아티클 갯수 구하기
|
||||
*/
|
||||
private int getTotalArticleCntByCategory(String category, CategoryForView categorys) {
|
||||
|
||||
if (categorys.getTitle().equals(category)) {
|
||||
return categorys.getCount();
|
||||
} else {
|
||||
for (CategoryForView categoryCnt :
|
||||
categorys.getCategoryTCountList()) {
|
||||
if (categoryCnt.getTitle().equals(category))
|
||||
return categoryCnt.getCount();
|
||||
for (CategoryForView categoryCntSub : categoryCnt.getCategoryTCountList()) {
|
||||
if (categoryCntSub.getTitle().equals(category))
|
||||
return categoryCntSub.getCount();
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("'"+category+"' 라는 카테고리는 존재하지 않습니다.");
|
||||
}
|
||||
|
||||
/*
|
||||
- 아티클 폼에 필요한 카테고리 dtos
|
||||
*/
|
||||
private List<CategorySimpleView> getCategoryDtosForForm() {
|
||||
return categoryService
|
||||
.findCategoryByTier(2)
|
||||
.stream()
|
||||
.map(category -> modelMapper.map(category, CategorySimpleView.class))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package myblog.blog.article.adapter.incomming.web;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import myblog.blog.article.application.port.response.ArticleResponseForCardBox;
|
||||
import myblog.blog.article.application.port.incomming.ArticleQueriesUseCase;
|
||||
import myblog.blog.shared.queries.LayoutRenderingQueries;
|
||||
import org.jsoup.Jsoup;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import java.util.List;
|
||||
|
||||
import static myblog.blog.shared.utils.MarkdownUtils.*;
|
||||
|
||||
@Controller
|
||||
@RequiredArgsConstructor
|
||||
public class MainController {
|
||||
|
||||
private final ArticleQueriesUseCase articleQueriesUseCase;
|
||||
private final LayoutRenderingQueries layoutRenderingQueries;
|
||||
/*
|
||||
- 메인 화면 제어용 컨트롤러
|
||||
*/
|
||||
@GetMapping("/")
|
||||
public String main(Model model) {
|
||||
// Dto 전처리
|
||||
List<ArticleResponseForCardBox> popularArticles = articleQueriesUseCase.getPopularArticles();
|
||||
//
|
||||
layoutRenderingQueries.AddLayoutTo(model);
|
||||
model.addAttribute("popularArticles", popularArticles);
|
||||
return "index";
|
||||
}
|
||||
|
||||
/*
|
||||
- 최신 아티클 무한스크롤로 조회
|
||||
*/
|
||||
@GetMapping("/main/article/{lastArticleId}")
|
||||
public @ResponseBody
|
||||
List<ArticleResponseForCardBox> mainNextPage(@PathVariable(required = false) Long lastArticleId) {
|
||||
|
||||
// Entity to Dto
|
||||
List<ArticleResponseForCardBox> articles = articleQueriesUseCase.getRecentArticles(lastArticleId);
|
||||
|
||||
|
||||
// 화면렌더링을 위한 파싱
|
||||
for(ArticleResponseForCardBox article : articles){
|
||||
String content = Jsoup.parse(getHtmlRenderer().render(getParser().parse(article.getContent()))).text();
|
||||
if(content.length()>300) {
|
||||
content = content.substring(0, 300);
|
||||
}
|
||||
article.setContent(content);
|
||||
}
|
||||
|
||||
return articles;
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package myblog.blog.article.adapter.incomming.web;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import myblog.blog.article.domain.TempArticle;
|
||||
import myblog.blog.article.application.TempArticleService;
|
||||
import myblog.blog.article.application.port.response.TempArticleResponse;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/*
|
||||
- 임시 게시물 조회, 저장을 위한 rest 컨트롤러
|
||||
*/
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
public class TempArticleController {
|
||||
|
||||
private final TempArticleService tempArticleService;
|
||||
|
||||
/*
|
||||
- 임시 아티클 저장 요청
|
||||
*/
|
||||
@PostMapping("/article/temp/autoSave")
|
||||
public String autoSaveTemp(@RequestBody TempArticleResponse tempArticleResponse){
|
||||
|
||||
tempArticleService.saveTemp(new TempArticle(tempArticleResponse.getContent()));
|
||||
|
||||
return "저장성공";
|
||||
}
|
||||
|
||||
/*
|
||||
- 임시 아티클 조회
|
||||
*/
|
||||
@GetMapping("/article/temp/getTemp")
|
||||
public @ResponseBody
|
||||
TempArticleResponse getTempArticle(){
|
||||
|
||||
Optional<TempArticle> tempArticle = tempArticleService.getTempArticle();
|
||||
|
||||
TempArticleResponse tempArticleResponse = new TempArticleResponse();
|
||||
tempArticleResponse.setContent(tempArticle.orElse(new TempArticle()).getContent());
|
||||
|
||||
return tempArticleResponse;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package myblog.blog.article.adapter.outgoing.model;
|
||||
|
||||
import myblog.blog.shared.domain.ExternalErrorException;
|
||||
|
||||
public class GithubExternalErrorException extends ExternalErrorException {
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
package myblog.blog.article.adapter.outgoing.persistence;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import myblog.blog.article.application.port.outgoing.ArticleBackupRepositoryPort;
|
||||
import myblog.blog.article.application.port.outgoing.ArticleRepositoryPort;
|
||||
import myblog.blog.article.domain.Article;
|
||||
import myblog.blog.category.domain.Category;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Slice;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -19,6 +19,7 @@ public class ArticleRepositoryAdapter implements ArticleRepositoryPort {
|
||||
|
||||
private final JpaArticleRepository jpaArticleRepository;
|
||||
private final MybatisArticleRepository mybatisArticleRepository;
|
||||
private final QdslArticleRepository qdslArticleRepository;
|
||||
|
||||
@Override
|
||||
public List<Article> findTop6ByOrderByHitDesc() {
|
||||
@@ -31,28 +32,23 @@ public class ArticleRepositoryAdapter implements ArticleRepositoryPort {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Slice<Article> findByOrderByIdDesc(Pageable pageable) {
|
||||
return jpaArticleRepository.findByOrderByIdDesc(pageable);
|
||||
public List<Article> findByOrderByIdDesc(int page, int size) {
|
||||
return jpaArticleRepository.findByOrderByIdDesc(PageRequest.of(page,size)).getContent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Article> findByOrderByIdDescWithList(Pageable pageable) {
|
||||
return jpaArticleRepository.findByOrderByIdDescWithList(pageable);
|
||||
public List<Article> findByOrderByIdDesc(Long articleId, int size) {
|
||||
return qdslArticleRepository.findByOrderByIdDesc(articleId, size);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Article> findByOrderByIdDesc(Long articleId, Pageable pageable) {
|
||||
return jpaArticleRepository.findByOrderByIdDesc(articleId, pageable);
|
||||
public List<Article> findBySubCategoryOrderByIdDesc(int page,int size, String category) {
|
||||
return jpaArticleRepository.findBySubCategoryOrderByIdDesc(PageRequest.of(page, size),category).getContent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Slice<Article> findBySubCategoryOrderByIdDesc(Pageable pageable, String category) {
|
||||
return jpaArticleRepository.findBySubCategoryOrderByIdDesc(pageable,category);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Slice<Article> findBySupCategoryOrderByIdDesc(Pageable pageable, String category) {
|
||||
return jpaArticleRepository.findBySupCategoryOrderByIdDesc(pageable,category);
|
||||
public List<Article> findBySuperCategoryOrderByIdDesc(int page,int size, String category) {
|
||||
return jpaArticleRepository.findBySupCategoryOrderByIdDesc(PageRequest.of(page, size),category).getContent();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -67,7 +63,7 @@ public class ArticleRepositoryAdapter implements ArticleRepositoryPort {
|
||||
|
||||
@Override
|
||||
public Page<Article> findAllByKeywordOrderById(Pageable pageable, String keyword) {
|
||||
return jpaArticleRepository.findAllByArticleTagsOrderById(pageable, keyword);
|
||||
return jpaArticleRepository.findAllByKeywordOrderById(pageable, keyword);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package myblog.blog.article.adapter.outgoing.persistence;
|
||||
|
||||
import myblog.blog.article.adapter.outgoing.model.GithubExternalErrorException;
|
||||
import myblog.blog.article.domain.Article;
|
||||
import myblog.blog.shared.domain.ExternalErrorException;
|
||||
import org.kohsuke.github.GHRepository;
|
||||
import org.kohsuke.github.GitHub;
|
||||
import org.kohsuke.github.GitHubBuilder;
|
||||
@@ -29,6 +31,7 @@ public class GithubRepoArticleRepository {
|
||||
.commit();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
throw new GithubExternalErrorException();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,25 +28,6 @@ public interface JpaArticleRepository extends JpaRepository<Article, Long> {
|
||||
*/
|
||||
Slice<Article> findByOrderByIdDesc(Pageable pageable);
|
||||
|
||||
/*
|
||||
- 커서페이징으로 최신 게시물 가져오기
|
||||
- 첫번째 페이지용 쿼리
|
||||
*/
|
||||
@Query("select a " +
|
||||
"from Article a " +
|
||||
"order by a.id desc ")
|
||||
List<Article> findByOrderByIdDescWithList(Pageable pageable);
|
||||
|
||||
/*
|
||||
- 커서페이징으로 최신 게시물 가져오기
|
||||
- 커서 적용
|
||||
*/
|
||||
@Query("select a " +
|
||||
"from Article a " +
|
||||
"where a.id < :articleId " +
|
||||
"order by a.id desc")
|
||||
List<Article> findByOrderByIdDesc(@Param("articleId") Long articleId,Pageable pageable);
|
||||
|
||||
/*
|
||||
- 카테고리별(하위 카테고리) 페이징 처리해서 최신게시물순으로 Slice 가져오기
|
||||
*/
|
||||
|
||||
@@ -6,15 +6,13 @@ import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
public interface ArticleTagListsRepository extends JpaRepository<ArticleTagList, Long> {
|
||||
public interface JpaArticleTagListsRepository extends JpaRepository<ArticleTagList, Long> {
|
||||
|
||||
/*
|
||||
- 아티클 연관 태그 삭제 쿼리
|
||||
- cascade 필요시에는 아티클 삭제로 일괄 삭제하므로 해당쿼리는 연관태그 수정용
|
||||
*/
|
||||
@Transactional
|
||||
@Modifying
|
||||
@Query("delete from ArticleTagList t " +
|
||||
"where t.article =:article")
|
||||
@@ -0,0 +1,33 @@
|
||||
package myblog.blog.article.adapter.outgoing.persistence;
|
||||
|
||||
import com.querydsl.core.types.Predicate;
|
||||
import com.querydsl.jpa.impl.JPAQueryFactory;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import myblog.blog.article.domain.Article;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static myblog.blog.article.domain.QArticle.article;
|
||||
|
||||
@Repository
|
||||
@RequiredArgsConstructor
|
||||
public class QdslArticleRepository {
|
||||
|
||||
private final JPAQueryFactory queryFactory;
|
||||
|
||||
List<Article> findByOrderByIdDesc(Long articleId, int size) {
|
||||
return queryFactory
|
||||
.selectFrom(article)
|
||||
.where(cursorLt(articleId))
|
||||
.orderBy(article.id.desc())
|
||||
.limit(size)
|
||||
.fetch();
|
||||
}
|
||||
|
||||
private Predicate cursorLt(Long articleId) {
|
||||
return articleId == 0L ? null : article.id.lt(articleId);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package myblog.blog.article.application;
|
||||
|
||||
import myblog.blog.article.application.port.incomming.response.*;
|
||||
import myblog.blog.article.domain.Article;
|
||||
import myblog.blog.article.domain.Tags;
|
||||
import myblog.blog.category.appliacation.port.incomming.response.CategorySimpleDto;
|
||||
import myblog.blog.category.domain.Category;
|
||||
import org.mapstruct.*;
|
||||
|
||||
@Mapper(
|
||||
componentModel = "spring",
|
||||
injectionStrategy = InjectionStrategy.CONSTRUCTOR,
|
||||
unmappedTargetPolicy = ReportingPolicy.ERROR
|
||||
)
|
||||
public interface ArticleDtoMapper {
|
||||
ArticleResponseForCardBox cardBox(Article article);
|
||||
@Mappings({
|
||||
@Mapping(target = "articleTagList",ignore = true)
|
||||
})
|
||||
ArticleResponseForEdit edit(Article article);
|
||||
@Mappings({
|
||||
@Mapping(target = "tags",ignore = true),
|
||||
@Mapping(source = "article.category.title", target = "category"),
|
||||
@Mapping(source = "article.member.id", target = "memberId"),
|
||||
})
|
||||
ArticleResponseForDetail detail(Article article);
|
||||
ArticleResponseByCategory category(Article article);
|
||||
TagsResponse of(Tags tag);
|
||||
}
|
||||
@@ -2,33 +2,38 @@ package myblog.blog.article.application;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import myblog.blog.article.application.port.response.ArticleResponseForCardBox;
|
||||
import myblog.blog.article.domain.Article;
|
||||
import myblog.blog.article.domain.ArticleNotFoundException;
|
||||
import myblog.blog.category.domain.Category;
|
||||
|
||||
import myblog.blog.article.application.port.incomming.response.ArticleResponseForCardBox;
|
||||
import myblog.blog.article.application.port.incomming.ArticleQueriesUseCase;
|
||||
import myblog.blog.article.application.port.incomming.response.ArticleResponseByCategory;
|
||||
import myblog.blog.article.application.port.incomming.response.ArticleResponseForDetail;
|
||||
import myblog.blog.article.application.port.incomming.response.ArticleResponseForEdit;
|
||||
import myblog.blog.category.appliacation.port.incomming.CategoryUseCase;
|
||||
|
||||
import myblog.blog.article.application.port.outgoing.ArticleRepositoryPort;
|
||||
|
||||
import myblog.blog.article.domain.Article;
|
||||
import myblog.blog.category.domain.Category;
|
||||
import myblog.blog.article.application.port.response.ArticleResponseByCategory;
|
||||
import myblog.blog.article.application.port.response.ArticleResponseForDetail;
|
||||
import myblog.blog.article.application.port.response.ArticleResponseForEdit;
|
||||
|
||||
import myblog.blog.category.service.CategoryService;
|
||||
import org.modelmapper.ModelMapper;
|
||||
import myblog.blog.category.domain.CategoryNotFoundException;
|
||||
import myblog.blog.shared.utils.MapperUtils;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Slice;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Component
|
||||
@Transactional(readOnly = true)
|
||||
@RequiredArgsConstructor
|
||||
public class ArticleQueries implements ArticleQueriesUseCase {
|
||||
|
||||
private final ArticleRepositoryPort articleRepositoryPort;
|
||||
private final CategoryService categoryService;
|
||||
private final ModelMapper modelMapper;
|
||||
private final CategoryUseCase categoryUseCase;
|
||||
private final ArticleDtoMapper articleDtoMapper;
|
||||
|
||||
/*
|
||||
- 메인화면 위한 인기 아티클 6개 목록 가져오기
|
||||
@@ -41,50 +46,48 @@ public class ArticleQueries implements ArticleQueriesUseCase {
|
||||
public List<ArticleResponseForCardBox> getPopularArticles() {
|
||||
return articleRepositoryPort.findTop6ByOrderByHitDesc()
|
||||
.stream()
|
||||
.map(article -> modelMapper.map(article, ArticleResponseForCardBox.class))
|
||||
.map(articleDtoMapper::cardBox)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
/*
|
||||
- 메인화면 위한 최신 아티클 커서 페이징해서 가져오기
|
||||
- 레이아웃 렌더링 성능 향상을 위해 캐싱작업
|
||||
카테고리 변경 / 아티클 변경이 존재할경우 레이아웃 캐시 초기화
|
||||
카테고리 변경 / 아티클 변경이 존재할경우 레이아웃 캐시 초기화 정책
|
||||
*/
|
||||
@Override
|
||||
@Cacheable(value = "layoutRecentArticleCaching", key = "#lastArticleId")
|
||||
public List<ArticleResponseForCardBox> getRecentArticles(Long lastArticleId) {
|
||||
List<Article> articles = lastArticleId.equals(0L) ?
|
||||
articleRepositoryPort
|
||||
.findByOrderByIdDescWithList(PageRequest.of(0, 5))
|
||||
:
|
||||
articleRepositoryPort
|
||||
.findByOrderByIdDesc(lastArticleId, PageRequest.of(0, 5));
|
||||
List<Article> articles = articleRepositoryPort
|
||||
.findByOrderByIdDesc(lastArticleId, 5);
|
||||
return articles
|
||||
.stream()
|
||||
.map(article -> modelMapper.map(article, ArticleResponseForCardBox.class))
|
||||
.map(articleDtoMapper::cardBox)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
/*
|
||||
- 카테고리별 게시물 페이징 처리해서 가져오기
|
||||
- tier 1은 super / tier 2는 sub
|
||||
*/
|
||||
@Override
|
||||
public Slice<ArticleResponseForCardBox> getArticlesByCategory(String category, Integer tier, Integer page) {
|
||||
Slice<Article> articles;
|
||||
public List<ArticleResponseForCardBox> getArticlesByCategory(String category, int tier, int page) {
|
||||
List<Article> articles = null;
|
||||
page = pageResolve(page);
|
||||
|
||||
if (tier.equals(0)) {
|
||||
if (tier == 0) {
|
||||
articles = articleRepositoryPort
|
||||
.findByOrderByIdDesc(
|
||||
PageRequest.of(pageResolve(page), 5));
|
||||
.findByOrderByIdDesc(page, 5);
|
||||
}
|
||||
else {
|
||||
if (tier == 1) {
|
||||
articles = articleRepositoryPort
|
||||
.findBySupCategoryOrderByIdDesc(
|
||||
PageRequest.of(pageResolve(page), 5), category);
|
||||
|
||||
.findBySuperCategoryOrderByIdDesc(page, 5, category);
|
||||
}
|
||||
if (tier == 2) {
|
||||
articles = articleRepositoryPort
|
||||
.findBySubCategoryOrderByIdDesc(page, 5, category);
|
||||
}
|
||||
if(articles == null) throw new ArticleNotFoundException();
|
||||
|
||||
if(articles == null) throw new IllegalArgumentException("NotFoundArticleException");
|
||||
|
||||
return articles.map(article -> modelMapper.map(article, ArticleResponseForCardBox.class));
|
||||
return articles.stream().map(articleDtoMapper::cardBox).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -93,7 +96,7 @@ public class ArticleQueries implements ArticleQueriesUseCase {
|
||||
@Override
|
||||
public ArticleResponseForEdit getArticleForEdit(Long id){
|
||||
Article article = articleRepositoryPort.findArticleByIdFetchCategoryAndTags(id);
|
||||
ArticleResponseForEdit articleDto = modelMapper.map(article, ArticleResponseForEdit.class);
|
||||
ArticleResponseForEdit articleDto = articleDtoMapper.edit(article);
|
||||
List<String> articleTagStrings = article.getArticleTagLists()
|
||||
.stream()
|
||||
.map(articleTag -> articleTag.getTags().getName())
|
||||
@@ -107,8 +110,7 @@ public class ArticleQueries implements ArticleQueriesUseCase {
|
||||
@Override
|
||||
public ArticleResponseForDetail getArticleForDetail(Long id){
|
||||
Article article = articleRepositoryPort.findArticleByIdFetchCategoryAndTags(id);
|
||||
ArticleResponseForDetail articleResponseForDetail =
|
||||
modelMapper.map(article, ArticleResponseForDetail.class);
|
||||
ArticleResponseForDetail articleResponseForDetail = articleDtoMapper.detail(article);
|
||||
|
||||
List<String> tags =
|
||||
article.getArticleTagLists()
|
||||
@@ -125,10 +127,11 @@ public class ArticleQueries implements ArticleQueriesUseCase {
|
||||
*/
|
||||
@Override
|
||||
public List<ArticleResponseByCategory> getArticlesByCategoryForDetailView(String categoryName){
|
||||
Category category = categoryService.findCategory(categoryName);
|
||||
Category category = categoryUseCase.findCategory(categoryName)
|
||||
.orElseThrow(CategoryNotFoundException::new);
|
||||
return articleRepositoryPort.findTop6ByCategoryOrderByIdDesc(category)
|
||||
.stream()
|
||||
.map(article -> modelMapper.map(article, ArticleResponseByCategory.class))
|
||||
.map(articleDtoMapper::category)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
/*
|
||||
@@ -138,8 +141,7 @@ public class ArticleQueries implements ArticleQueriesUseCase {
|
||||
public Page<ArticleResponseForCardBox> getArticlesByTag(String tag, Integer page) {
|
||||
return articleRepositoryPort
|
||||
.findAllByArticleTagsOrderById(PageRequest.of(pageResolve(page), 5), tag)
|
||||
.map(article ->
|
||||
modelMapper.map(article, ArticleResponseForCardBox.class));
|
||||
.map(articleDtoMapper::cardBox);
|
||||
}
|
||||
/*
|
||||
- 검색어별 게시물 페이징 처리해서 가져오기
|
||||
@@ -148,8 +150,7 @@ public class ArticleQueries implements ArticleQueriesUseCase {
|
||||
public Page<ArticleResponseForCardBox> getArticlesByKeyword(String keyword, Integer page) {
|
||||
return articleRepositoryPort
|
||||
.findAllByKeywordOrderById(PageRequest.of(pageResolve(page),5), keyword)
|
||||
.map(article ->
|
||||
modelMapper.map(article, ArticleResponseForCardBox.class));
|
||||
.map(articleDtoMapper::cardBox);
|
||||
}
|
||||
/*
|
||||
- 페이지 시작점 0~1변경 메서드
|
||||
|
||||
@@ -2,20 +2,21 @@ package myblog.blog.article.application;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import myblog.blog.article.application.port.request.ArticleCreateRequest;
|
||||
import myblog.blog.article.application.port.request.ArticleEditRequest;
|
||||
import myblog.blog.article.application.port.incomming.request.ArticleCreateCommand;
|
||||
import myblog.blog.article.application.port.incomming.request.ArticleEditCommand;
|
||||
import myblog.blog.article.application.port.incomming.ArticleUseCase;
|
||||
import myblog.blog.article.application.port.incomming.TagUseCase;
|
||||
import myblog.blog.article.domain.ArticleNotFoundException;
|
||||
import myblog.blog.category.appliacation.port.incomming.CategoryUseCase;
|
||||
import myblog.blog.category.domain.CategoryNotFoundException;
|
||||
import myblog.blog.member.application.port.incomming.MemberQueriesUseCase;
|
||||
import myblog.blog.article.application.port.outgoing.ArticleBackupRepositoryPort;
|
||||
import myblog.blog.article.application.port.outgoing.ArticleRepositoryPort;
|
||||
|
||||
import myblog.blog.article.domain.Article;
|
||||
import myblog.blog.category.domain.Category;
|
||||
import myblog.blog.member.doamin.Member;
|
||||
|
||||
import myblog.blog.category.service.CategoryService;
|
||||
import myblog.blog.member.service.Oauth2MemberService;
|
||||
|
||||
import myblog.blog.member.doamin.NotFoundMemberException;
|
||||
import org.springframework.cache.annotation.CacheEvict;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -28,39 +29,42 @@ import java.util.List;
|
||||
public class ArticleService implements ArticleUseCase {
|
||||
|
||||
private final TagUseCase tagUseCase;
|
||||
private final CategoryService categoryService;
|
||||
private final Oauth2MemberService memberService;
|
||||
private final CategoryUseCase categoryUseCase;
|
||||
private final MemberQueriesUseCase memberQueriesUseCase;
|
||||
private final ArticleRepositoryPort articleRepositoryPort;
|
||||
private final ArticleBackupRepositoryPort articleBackupRepositoryPort;
|
||||
|
||||
@Override
|
||||
@CacheEvict(value = {"layoutCaching", "layoutRecentArticleCaching","seoCaching"}, allEntries = true)
|
||||
public Long writeArticle(ArticleCreateRequest articleCreateRequest) {
|
||||
Member writer = memberService.findById(articleCreateRequest.getMemberId());
|
||||
Category category = categoryService.findCategory(articleCreateRequest.getCategory());
|
||||
Article newArticle = new Article(articleCreateRequest.getTitle(),
|
||||
articleCreateRequest.getContent(),
|
||||
articleCreateRequest.getToc(),
|
||||
public Long writeArticle(ArticleCreateCommand articleCreateCommand) {
|
||||
var writer = memberQueriesUseCase.findById(articleCreateCommand.getMemberId())
|
||||
.orElseThrow(NotFoundMemberException::new);
|
||||
var category = categoryUseCase.findCategory(articleCreateCommand.getCategory())
|
||||
.orElseThrow(CategoryNotFoundException::new);
|
||||
var newArticle = new Article(articleCreateCommand.getTitle(),
|
||||
articleCreateCommand.getContent(),
|
||||
articleCreateCommand.getToc(),
|
||||
writer,
|
||||
articleCreateRequest.getThumbnailUrl(),
|
||||
articleCreateCommand.getThumbnailUrl(),
|
||||
category);
|
||||
articleRepositoryPort.save(newArticle);
|
||||
tagUseCase.createNewTagsAndArticleTagList(articleCreateRequest.getTags(), newArticle);
|
||||
tagUseCase.createNewTagsAndArticleTagList(articleCreateCommand.getTags(), newArticle);
|
||||
return newArticle.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
@CacheEvict(value = {"layoutCaching", "layoutRecentArticleCaching","seoCaching"}, allEntries = true)
|
||||
public void editArticle(ArticleEditRequest articleEditRequest) {
|
||||
Article article = articleRepositoryPort.findById(articleEditRequest.getArticleId())
|
||||
.orElseThrow(() -> new IllegalArgumentException("NotFoundArticleException"));
|
||||
Category category = categoryService.findCategory(articleEditRequest.getCategoryName());
|
||||
public void editArticle(ArticleEditCommand articleEditCommand) {
|
||||
var article = articleRepositoryPort.findById(articleEditCommand.getArticleId())
|
||||
.orElseThrow(ArticleNotFoundException::new);
|
||||
var category = categoryUseCase.findCategory(articleEditCommand.getCategoryName())
|
||||
.orElseThrow(CategoryNotFoundException::new);
|
||||
tagUseCase.deleteAllTagsWith(article);
|
||||
tagUseCase.createNewTagsAndArticleTagList(articleEditRequest.getTags(), article);
|
||||
article.edit(articleEditRequest.getContent(),
|
||||
articleEditRequest.getTitle(),
|
||||
articleEditRequest.getToc(),
|
||||
articleEditRequest.getThumbnailUrl(), category);
|
||||
tagUseCase.createNewTagsAndArticleTagList(articleEditCommand.getTags(), article);
|
||||
article.edit(articleEditCommand.getContent(),
|
||||
articleEditCommand.getTitle(),
|
||||
articleEditCommand.getToc(),
|
||||
articleEditCommand.getThumbnailUrl(), category);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -71,14 +75,14 @@ public class ArticleService implements ArticleUseCase {
|
||||
|
||||
@Override
|
||||
public void backupArticle(Long articleId) {
|
||||
Article article = articleRepositoryPort.findById(articleId)
|
||||
var article = articleRepositoryPort.findById(articleId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("NotFoundArticle"));
|
||||
articleBackupRepositoryPort.backup(article);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addHit(Long articleId) {
|
||||
Article article = articleRepositoryPort.findById(articleId)
|
||||
var article = articleRepositoryPort.findById(articleId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("NotFoundArticleException"));
|
||||
article.addHit();
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import myblog.blog.article.application.port.outgoing.TagRepositoryPort;
|
||||
import myblog.blog.article.domain.Tags;
|
||||
import myblog.blog.article.application.port.incomming.TagsQueriesUseCase;
|
||||
import myblog.blog.shared.utils.MapperUtils;
|
||||
import myblog.blog.article.application.port.response.TagsResponse;
|
||||
import myblog.blog.article.application.port.incomming.response.TagsResponse;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@@ -13,15 +13,16 @@ import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Component
|
||||
@Transactional
|
||||
@Transactional(readOnly = true)
|
||||
@RequiredArgsConstructor
|
||||
public class TagsQueries implements TagsQueriesUseCase {
|
||||
private final TagRepositoryPort tagRepositoryPort;
|
||||
private final ArticleDtoMapper articleDtoMapper;
|
||||
|
||||
public List<TagsResponse> findAllTagDtos(){
|
||||
List<Tags> tags = tagRepositoryPort.findAll();
|
||||
var tags = tagRepositoryPort.findAll();
|
||||
return tags.stream()
|
||||
.map(tag -> MapperUtils.getModelMapper().map(tag, TagsResponse.class))
|
||||
.map(articleDtoMapper::of)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package myblog.blog.article.application;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import myblog.blog.article.adapter.outgoing.persistence.ArticleTagListsRepository;
|
||||
import myblog.blog.article.adapter.outgoing.persistence.JpaArticleTagListsRepository;
|
||||
import myblog.blog.article.adapter.outgoing.persistence.JpaTagsRepository;
|
||||
import myblog.blog.article.application.port.incomming.TagUseCase;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -17,7 +17,7 @@ import java.util.*;
|
||||
@RequiredArgsConstructor
|
||||
public class TagsService implements TagUseCase {
|
||||
private final JpaTagsRepository jpaTagsRepository;
|
||||
private final ArticleTagListsRepository articleTagListsRepository;
|
||||
private final JpaArticleTagListsRepository articleTagListsRepository;
|
||||
/*
|
||||
- Json 객체로 넘어온 태그들을 파싱해서 신규 태그인경우 저장
|
||||
*/
|
||||
@@ -25,7 +25,7 @@ public class TagsService implements TagUseCase {
|
||||
public void createNewTagsAndArticleTagList(String names, Article article) {
|
||||
List<Map<String,String>> tagsDtoArrayList = MapperUtils.getGson().fromJson(names, ArrayList.class);
|
||||
for (var tagDto : tagsDtoArrayList) {
|
||||
Tags tag = findOrCreateTagFrom(tagDto);
|
||||
var tag = findOrCreateTagFrom(tagDto);
|
||||
articleTagListsRepository.save(new ArticleTagList(article, tag));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
package myblog.blog.article.application;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import myblog.blog.article.application.port.incomming.response.TempArticleDto;
|
||||
import myblog.blog.article.application.port.incomming.TempArticleUseCase;
|
||||
import myblog.blog.article.application.port.outgoing.TempArticleRepositoryPort;
|
||||
import myblog.blog.article.domain.TempArticle;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
@RequiredArgsConstructor
|
||||
@@ -20,23 +19,28 @@ public class TempArticleService implements TempArticleUseCase {
|
||||
- 자동 저장 로직
|
||||
- ID값 고정으로 머지를 작동시켜 임시글 DB에 1개 유지
|
||||
*/
|
||||
public void saveTemp(TempArticle tempArticle){
|
||||
tempArticleRepositoryPort.save(tempArticle);
|
||||
@Override
|
||||
public void saveTemp(String tempArticleContents){
|
||||
tempArticleRepositoryPort.save(new TempArticle(tempArticleContents));
|
||||
}
|
||||
|
||||
/*
|
||||
- 임시글 가져오기
|
||||
*/
|
||||
public Optional<TempArticle> getTempArticle(){
|
||||
return tempArticleRepositoryPort.findById(1L);
|
||||
@Override
|
||||
public TempArticleDto getTempArticle(){
|
||||
var tempArticle = tempArticleRepositoryPort.findById(1L);
|
||||
var tempArticleDto = new TempArticleDto();
|
||||
tempArticleDto.setContent(tempArticle.orElse(new TempArticle()).getContent());
|
||||
return tempArticleDto;
|
||||
}
|
||||
|
||||
/*
|
||||
- 임시글 삭제
|
||||
*/
|
||||
@Override
|
||||
public void deleteTemp(){
|
||||
Optional<TempArticle> deleteArticle = tempArticleRepositoryPort.findById(1L);
|
||||
var deleteArticle = tempArticleRepositoryPort.findById(1L);
|
||||
deleteArticle.ifPresent(tempArticleRepositoryPort::delete);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package myblog.blog.article.application.port.incomming;
|
||||
|
||||
import myblog.blog.article.application.port.response.ArticleResponseForCardBox;
|
||||
import myblog.blog.article.application.port.response.ArticleResponseByCategory;
|
||||
import myblog.blog.article.application.port.response.ArticleResponseForDetail;
|
||||
import myblog.blog.article.application.port.response.ArticleResponseForEdit;
|
||||
import myblog.blog.article.application.port.incomming.response.ArticleResponseForCardBox;
|
||||
import myblog.blog.article.application.port.incomming.response.ArticleResponseByCategory;
|
||||
import myblog.blog.article.application.port.incomming.response.ArticleResponseForDetail;
|
||||
import myblog.blog.article.application.port.incomming.response.ArticleResponseForEdit;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Slice;
|
||||
|
||||
@@ -12,7 +12,7 @@ import java.util.List;
|
||||
public interface ArticleQueriesUseCase {
|
||||
List<ArticleResponseForCardBox> getPopularArticles();
|
||||
List<ArticleResponseForCardBox> getRecentArticles(Long lastArticleId);
|
||||
Slice<ArticleResponseForCardBox> getArticlesByCategory(String category, Integer tier, Integer page);
|
||||
List<ArticleResponseForCardBox> getArticlesByCategory(String category, int tier, int page);
|
||||
ArticleResponseForEdit getArticleForEdit(Long id);
|
||||
ArticleResponseForDetail getArticleForDetail(Long id);
|
||||
List<ArticleResponseByCategory> getArticlesByCategoryForDetailView(String category);
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
package myblog.blog.article.application.port.incomming;
|
||||
|
||||
import myblog.blog.article.application.port.request.ArticleCreateRequest;
|
||||
import myblog.blog.article.application.port.request.ArticleEditRequest;
|
||||
import myblog.blog.article.application.port.incomming.request.ArticleCreateCommand;
|
||||
import myblog.blog.article.application.port.incomming.request.ArticleEditCommand;
|
||||
import myblog.blog.article.domain.Article;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface ArticleUseCase {
|
||||
Long writeArticle(ArticleCreateRequest articleCreateRequest);
|
||||
void editArticle(ArticleEditRequest articleEditRequest);
|
||||
Long writeArticle(ArticleCreateCommand articleCreateCommand);
|
||||
void editArticle(ArticleEditCommand articleEditCommand);
|
||||
void deleteArticle(Long articleId);
|
||||
void addHit(Long articleId);
|
||||
void backupArticle(Long articleId);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package myblog.blog.article.application.port.incomming;
|
||||
|
||||
import myblog.blog.article.application.port.response.TagsResponse;
|
||||
import myblog.blog.article.application.port.incomming.response.TagsResponse;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
package myblog.blog.article.application.port.incomming;
|
||||
|
||||
import myblog.blog.article.domain.TempArticle;
|
||||
|
||||
import java.util.Optional;
|
||||
import myblog.blog.article.application.port.incomming.response.TempArticleDto;
|
||||
|
||||
public interface TempArticleUseCase {
|
||||
void saveTemp(TempArticle tempArticle);
|
||||
Optional<TempArticle> getTempArticle();
|
||||
void saveTemp(String tempArticleContents);
|
||||
TempArticleDto getTempArticle();
|
||||
void deleteTemp();
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
package myblog.blog.article.application.port.request;
|
||||
package myblog.blog.article.application.port.incomming.request;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import myblog.blog.article.adapter.incomming.web.ArticleForm;
|
||||
import lombok.NoArgsConstructor;
|
||||
import myblog.blog.article.adapter.incomming.ArticleForm;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public class ArticleCreateRequest {
|
||||
@NoArgsConstructor
|
||||
public class ArticleCreateCommand {
|
||||
private Long memberId;
|
||||
private String title;
|
||||
private String content;
|
||||
@@ -15,8 +17,8 @@ public class ArticleCreateRequest {
|
||||
private String category;
|
||||
private String tags;
|
||||
|
||||
static public ArticleCreateRequest from(ArticleForm articleForm, Long memberId){
|
||||
return new ArticleCreateRequest(memberId,
|
||||
static public ArticleCreateCommand from(ArticleForm articleForm, Long memberId){
|
||||
return new ArticleCreateCommand(memberId,
|
||||
articleForm.getTitle(),
|
||||
articleForm.getContent(),
|
||||
articleForm.getToc(),
|
||||
@@ -1,12 +1,12 @@
|
||||
package myblog.blog.article.application.port.request;
|
||||
package myblog.blog.article.application.port.incomming.request;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import myblog.blog.article.adapter.incomming.web.ArticleForm;
|
||||
import myblog.blog.article.adapter.incomming.ArticleForm;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public class ArticleEditRequest {
|
||||
public class ArticleEditCommand {
|
||||
|
||||
private Long articleId;
|
||||
private String title;
|
||||
@@ -16,8 +16,8 @@ public class ArticleEditRequest {
|
||||
private String categoryName;
|
||||
private String tags;
|
||||
|
||||
static public ArticleEditRequest from(Long articleId, ArticleForm articleForm){
|
||||
return new ArticleEditRequest(articleId,
|
||||
static public ArticleEditCommand from(Long articleId, ArticleForm articleForm){
|
||||
return new ArticleEditCommand(articleId,
|
||||
articleForm.getTitle(),
|
||||
articleForm.getContent(),
|
||||
articleForm.getToc(),
|
||||
@@ -1,4 +1,4 @@
|
||||
package myblog.blog.article.application.port.response;
|
||||
package myblog.blog.article.application.port.incomming.response;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
@@ -0,0 +1,31 @@
|
||||
package myblog.blog.article.application.port.incomming.response;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.jsoup.Jsoup;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static myblog.blog.shared.utils.MarkdownUtils.getHtmlRenderer;
|
||||
import static myblog.blog.shared.utils.MarkdownUtils.getParser;
|
||||
|
||||
/*
|
||||
- 메인 화면 렌더링용 아티클 DTO
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
public class ArticleResponseForCardBox {
|
||||
private Long id;
|
||||
private String title;
|
||||
private String content;
|
||||
private String thumbnailUrl;
|
||||
private LocalDateTime createdDate;
|
||||
|
||||
public void parseAndRenderForView(){
|
||||
this.content = Jsoup.parse(getHtmlRenderer().render(getParser().parse(this.content))).text();
|
||||
if(content.length()>300) {
|
||||
content = content.substring(0, 300);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package myblog.blog.article.application.port.response;
|
||||
package myblog.blog.article.application.port.incomming.response;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
@@ -6,6 +6,9 @@ import lombok.Setter;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import static myblog.blog.shared.utils.MarkdownUtils.getHtmlRenderer;
|
||||
import static myblog.blog.shared.utils.MarkdownUtils.getParser;
|
||||
|
||||
/*
|
||||
- 아티클 상세조회용 DTO
|
||||
*/
|
||||
@@ -21,4 +24,8 @@ public class ArticleResponseForDetail {
|
||||
private String category;
|
||||
private List<String> tags;
|
||||
private LocalDateTime createdDate;
|
||||
|
||||
public void parseAndRenderForView(){
|
||||
this.content = getHtmlRenderer().render(getParser().parse(this.content));
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package myblog.blog.article.application.port.response;
|
||||
package myblog.blog.article.application.port.incomming.response;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
@@ -18,7 +18,6 @@ public class ArticleResponseForEdit {
|
||||
private String content;
|
||||
private String toc;
|
||||
private String thumbnailUrl;
|
||||
|
||||
private List<String> articleTagList = new ArrayList<>();
|
||||
private List<String> articleTagList;
|
||||
private Category category;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package myblog.blog.article.application.port.response;
|
||||
package myblog.blog.article.application.port.incomming.response;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package myblog.blog.article.application.port.response;
|
||||
package myblog.blog.article.application.port.incomming.response;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
@@ -7,6 +7,6 @@ import lombok.Setter;
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
public class TempArticleResponse {
|
||||
public class TempArticleDto {
|
||||
private String content;
|
||||
}
|
||||
@@ -12,11 +12,10 @@ import java.util.Optional;
|
||||
public interface ArticleRepositoryPort {
|
||||
List<Article> findTop6ByOrderByHitDesc();
|
||||
List<Article> findTop6ByCategoryOrderByIdDesc(Category category);
|
||||
Slice<Article> findByOrderByIdDesc(Pageable pageable);
|
||||
List<Article> findByOrderByIdDescWithList(Pageable pageable);
|
||||
List<Article> findByOrderByIdDesc(Long articleId, Pageable pageable);
|
||||
Slice<Article> findBySubCategoryOrderByIdDesc(Pageable pageable, String category);
|
||||
Slice<Article> findBySupCategoryOrderByIdDesc(Pageable pageable, String category);
|
||||
List<Article> findByOrderByIdDesc(int page, int size);
|
||||
List<Article> findByOrderByIdDesc(Long articleId, int size);
|
||||
List<Article> findBySubCategoryOrderByIdDesc(int page, int size, String category);
|
||||
List<Article> findBySuperCategoryOrderByIdDesc(int page, int size, String category);
|
||||
Article findArticleByIdFetchCategoryAndTags(Long articleId);
|
||||
Page<Article> findAllByArticleTagsOrderById(Pageable pageable, String tag);
|
||||
Page<Article> findAllByKeywordOrderById(Pageable pageable, String keyword);
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
package myblog.blog.article.application.port.response;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/*
|
||||
- 메인 화면 렌더링용 아티클 DTO
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
public class ArticleResponseForCardBox {
|
||||
private Long id;
|
||||
private String title;
|
||||
private String content;
|
||||
private String thumbnailUrl;
|
||||
private LocalDateTime createdDate;
|
||||
|
||||
}
|
||||
@@ -1,15 +1,18 @@
|
||||
package myblog.blog.article.domain;
|
||||
|
||||
import myblog.blog.shared.BasicEntity;
|
||||
import myblog.blog.shared.domain.BadRequestException;
|
||||
import myblog.blog.shared.domain.BasicEntity;
|
||||
import myblog.blog.category.domain.Category;
|
||||
import myblog.blog.comment.domain.Comment;
|
||||
import myblog.blog.member.doamin.Member;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.hibernate.annotations.OnDelete;
|
||||
import org.hibernate.annotations.OnDeleteAction;
|
||||
import javax.persistence.*;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.*;
|
||||
|
||||
/*
|
||||
@@ -27,8 +30,7 @@ import java.util.*;
|
||||
- fts 구현을 위한 인덱스 설정
|
||||
*/
|
||||
@Table(indexes = {
|
||||
@Index(name="i_article_title", columnList = "title"),
|
||||
@Index(name = "i_article_content", columnList = "content")
|
||||
@Index(name="i_article_title", columnList = "title")
|
||||
})
|
||||
@Getter
|
||||
public class Article extends BasicEntity {
|
||||
@@ -74,6 +76,11 @@ public class Article extends BasicEntity {
|
||||
|
||||
@Builder
|
||||
public Article(String title, String content, String toc, Member member, String thumbnailUrl, Category category) {
|
||||
if(StringUtils.isEmpty(title)) throw new BadRequestException("Article.tittle");
|
||||
if(StringUtils.isEmpty(content)) throw new BadRequestException("Article.content");
|
||||
if(member == null) throw new BadRequestException("Article.member");
|
||||
if(category == null) throw new BadRequestException("Article.category");
|
||||
|
||||
this.title = title;
|
||||
this.content = content;
|
||||
this.toc = toc;
|
||||
@@ -83,6 +90,13 @@ public class Article extends BasicEntity {
|
||||
this.hit = 0L;
|
||||
}
|
||||
|
||||
private String makeDefaultThumbOf(String thumbnailUrl) {
|
||||
var defaultThumbUrl = "https://cdn.pixabay.com/photo/2020/11/08/13/28/tree-5723734_1280.jpg";
|
||||
if (thumbnailUrl == null || thumbnailUrl.equals("")) {
|
||||
thumbnailUrl = defaultThumbUrl;
|
||||
}
|
||||
return thumbnailUrl;
|
||||
}
|
||||
/*
|
||||
- 아티클 수정을 위한 로직
|
||||
*/
|
||||
@@ -95,22 +109,8 @@ public class Article extends BasicEntity {
|
||||
this.thumbnailUrl = getThumbnailUrl();
|
||||
}
|
||||
}
|
||||
/*
|
||||
- 아티클 조회수 증가
|
||||
*/
|
||||
|
||||
public void addHit(){
|
||||
this.hit++;
|
||||
}
|
||||
|
||||
/*
|
||||
- 썸네일 기본 작성
|
||||
*/
|
||||
private String makeDefaultThumbOf(String thumbnailUrl) {
|
||||
String defaultThumbUrl = "https://cdn.pixabay.com/photo/2020/11/08/13/28/tree-5723734_1280.jpg";
|
||||
|
||||
if (thumbnailUrl == null || thumbnailUrl.equals("")) {
|
||||
thumbnailUrl = defaultThumbUrl;
|
||||
}
|
||||
return thumbnailUrl;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package myblog.blog.article.domain;
|
||||
|
||||
import myblog.blog.shared.domain.ResourceNotFoundException;
|
||||
|
||||
public class ArticleNotFoundException extends ResourceNotFoundException {
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package myblog.blog.article.domain;
|
||||
|
||||
import lombok.Getter;
|
||||
import myblog.blog.shared.BasicEntity;
|
||||
import myblog.blog.shared.domain.BasicEntity;
|
||||
|
||||
import javax.persistence.*;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package myblog.blog.article.domain;
|
||||
|
||||
import lombok.Getter;
|
||||
import myblog.blog.shared.BasicEntity;
|
||||
import myblog.blog.shared.domain.BasicEntity;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.util.ArrayList;
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
package myblog.blog.article.domain;
|
||||
|
||||
import lombok.Getter;
|
||||
import myblog.blog.shared.BasicEntity;
|
||||
import myblog.blog.shared.domain.BasicEntity;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/*
|
||||
- 임시 아티클 저장 Entity
|
||||
|
||||
16
src/main/java/myblog/blog/badge/APIController.java
Normal file
16
src/main/java/myblog/blog/badge/APIController.java
Normal file
@@ -0,0 +1,16 @@
|
||||
package myblog.blog.badge;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Target({ElementType.TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/open-api/v1")
|
||||
public @interface APIController {
|
||||
}
|
||||
16
src/main/java/myblog/blog/badge/BadgeController.java
Normal file
16
src/main/java/myblog/blog/badge/BadgeController.java
Normal file
@@ -0,0 +1,16 @@
|
||||
package myblog.blog.badge;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@APIController
|
||||
public class BadgeController {
|
||||
|
||||
@GetMapping("/badges")
|
||||
String generateBadges() {
|
||||
return "<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" width=\"100\" height=\"100%\">" +"<circle cx=\"50\" cy=\"50\" r=\"30\" fill=\"red\">" +"</svg>";
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package myblog.blog.base.config;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import org.modelmapper.ModelMapper;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class AppConfig {
|
||||
|
||||
/*
|
||||
- DTO <-> 엔티티 매퍼 빈등록
|
||||
*/
|
||||
@Bean
|
||||
public ModelMapper modelMapper(){
|
||||
ModelMapper modelMapper = new ModelMapper();
|
||||
modelMapper.getConfiguration()
|
||||
.setFieldAccessLevel(org.modelmapper.config.Configuration.AccessLevel.PRIVATE)
|
||||
.setFieldMatchingEnabled(true);
|
||||
return modelMapper;
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
package myblog.blog.base.log;
|
||||
|
||||
import io.sentry.Sentry;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class LogTracer {
|
||||
|
||||
private static final String START_PREFIX = "-->";
|
||||
private static final String COMPLETE_PREFIX = "<--";
|
||||
private static final String EX_PREFIX = "<X-";
|
||||
|
||||
private ThreadLocal<TraceId> traceIdHolder = new ThreadLocal<>();
|
||||
|
||||
public TraceStatus begin(String message){
|
||||
syncTraceId();
|
||||
TraceId traceId = traceIdHolder.get();
|
||||
Long startTimeMs = System.currentTimeMillis();
|
||||
log.info("[{}] {}{}",traceId.getId(), addSpace(START_PREFIX,
|
||||
traceId.getLevel()), message);
|
||||
return new TraceStatus(traceId, startTimeMs, message);
|
||||
}
|
||||
|
||||
private void syncTraceId() {
|
||||
TraceId traceId = traceIdHolder.get();
|
||||
if (traceId == null) {
|
||||
traceIdHolder.set(new TraceId());
|
||||
} else {
|
||||
traceIdHolder.set(traceId.createNextId());
|
||||
}
|
||||
}
|
||||
|
||||
public void end(TraceStatus traceStatus){
|
||||
complete(traceStatus, null);
|
||||
}
|
||||
|
||||
public void exception(TraceStatus traceStatus, Exception ex){
|
||||
complete(traceStatus, ex);
|
||||
}
|
||||
|
||||
private void complete(TraceStatus traceStatus, Exception ex) {
|
||||
Long stopTimeMs = System.currentTimeMillis();
|
||||
Long resultTimeMs = stopTimeMs - traceStatus.getStartTimesMs();
|
||||
TraceId traceId = traceStatus.getTraceId();
|
||||
if(ex == null){
|
||||
log.info("[{}] {} {} time = {}ms", traceId.getId(), addSpace(COMPLETE_PREFIX, traceId.getLevel()),
|
||||
traceStatus.getMessage(), resultTimeMs);
|
||||
} else {
|
||||
log.info("[{}] {} {} time = {}ms ex={}", traceId.getId(), addSpace(EX_PREFIX, traceId.getLevel()),
|
||||
traceStatus.getMessage(), resultTimeMs, ex.toString());
|
||||
Sentry.captureMessage(String.format("[%s] %s %s time = %sms ex = %s",
|
||||
traceId.getId(),addSpace(START_PREFIX, traceId.getLevel()),traceStatus.getMessage(), resultTimeMs, ex.toString()));
|
||||
}
|
||||
releaseTraceId();
|
||||
}
|
||||
|
||||
private void releaseTraceId() {
|
||||
TraceId traceId = traceIdHolder.get();
|
||||
if (traceId.isFirstLevel()) {
|
||||
traceIdHolder.remove(); //destroy
|
||||
} else {
|
||||
traceIdHolder.set(traceId.createPrevId());
|
||||
}
|
||||
}
|
||||
|
||||
private String addSpace(String prefix, int level) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for(int i = 0; i< level; i++){
|
||||
sb.append((i==level-1) ? "|" + prefix : "| ");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package myblog.blog.category.adapter.incomming;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import myblog.blog.category.appliacation.port.incomming.CategoryQueriesUseCase;
|
||||
import myblog.blog.category.appliacation.port.incomming.CategoryUseCase;
|
||||
import myblog.blog.category.appliacation.port.incomming.response.CategoryViewForLayout;
|
||||
import myblog.blog.category.appliacation.port.incomming.response.CategorySimpleDto;
|
||||
import myblog.blog.comment.application.port.incomming.CommentQueriesUseCase;
|
||||
import myblog.blog.comment.application.port.incomming.response.CommentDtoForLayout;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.validation.Errors;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Controller
|
||||
@RequiredArgsConstructor
|
||||
public class CategoryController {
|
||||
|
||||
private final CategoryUseCase categoryUseCase;
|
||||
private final CategoryQueriesUseCase categoryQueriesUseCase;
|
||||
private final CommentQueriesUseCase commentQueriesUseCase;
|
||||
private final CategoryListValidator categorylistValidator;
|
||||
|
||||
/*
|
||||
- 카테고리 수정폼 조회
|
||||
*/
|
||||
@GetMapping("/category/edit")
|
||||
String editCategoryForm(Model model) {
|
||||
|
||||
var categoryList = categoryQueriesUseCase.getCategorytCountList();
|
||||
var copyList = new ArrayList<>(List.copyOf(categoryList));
|
||||
copyList.remove(0);
|
||||
var categoryViewForLayout = CategoryViewForLayout.from(categoryList);
|
||||
var comments = commentQueriesUseCase.recentCommentListForLayout();
|
||||
|
||||
model.addAttribute("categoryForEdit", copyList);
|
||||
model.addAttribute("category", categoryViewForLayout);
|
||||
model.addAttribute("commentsList", comments);
|
||||
|
||||
return "admin/categoryEdit";
|
||||
}
|
||||
/*
|
||||
- 카테고리 수정 요청
|
||||
*/
|
||||
@PostMapping("/category/edit")
|
||||
@ResponseBody String editCategory(@RequestBody List<CategorySimpleDto> categoryList, Errors errors) {
|
||||
categorylistValidator.validate(categoryList, errors);
|
||||
categoryUseCase.changeCategory(categoryList);
|
||||
return "변경 성공";
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package myblog.blog.shared.exception;
|
||||
package myblog.blog.category.adapter.incomming;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -7,6 +7,7 @@ import org.springframework.validation.Validator;
|
||||
import org.springframework.validation.beanvalidation.SpringValidatorAdapter;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
|
||||
/*
|
||||
@@ -14,7 +15,7 @@ import java.util.List;
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class ListValidator implements Validator {
|
||||
public class CategoryListValidator implements Validator {
|
||||
|
||||
private final SpringValidatorAdapter springValidatorAdapter;
|
||||
|
||||
@@ -28,5 +29,8 @@ public class ListValidator implements Validator {
|
||||
for(Object object : (List)target){
|
||||
springValidatorAdapter.validate(object,errors);
|
||||
}
|
||||
if (errors.hasErrors()) {
|
||||
throw new InvalidCategoryRequestException(Objects.requireNonNull(errors.getFieldError()).getDefaultMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package myblog.blog.category.adapter.incomming;
|
||||
/*
|
||||
- REST 컨트롤러 상태 메세지 전송용 커스텀 에러
|
||||
*/
|
||||
public class InvalidCategoryRequestException extends RuntimeException {
|
||||
public InvalidCategoryRequestException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package myblog.blog.category.adapter.outgoing.persistence;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import myblog.blog.category.appliacation.port.outgoing.CategoryRepositoryPort;
|
||||
import myblog.blog.category.appliacation.port.incomming.response.CategorySimpleDto;
|
||||
import myblog.blog.category.domain.Category;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class CategoryRepositoryAdapter implements CategoryRepositoryPort {
|
||||
|
||||
private final JpaCategoryRepository jpaCategoryRepository;
|
||||
private final MybatisCategoryRepository mybatisCategoryRepository;
|
||||
|
||||
|
||||
@Override
|
||||
public Optional<Category> findByTitle(String title) {
|
||||
return jpaCategoryRepository.findByTitle(title);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Category> findAll() {
|
||||
return jpaCategoryRepository.findAll();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CategorySimpleDto> getCategoryCount() {
|
||||
return mybatisCategoryRepository.getCategoryCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Category> findAllByTierIs(int tier) {
|
||||
return jpaCategoryRepository.findAllByTierIs(tier);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Category> findAllWithoutDummy() {
|
||||
return jpaCategoryRepository.findAllWithoutDummy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteAll(List<Category> categoryListFromDb) {
|
||||
jpaCategoryRepository.deleteAll(categoryListFromDb);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(Category category) {
|
||||
jpaCategoryRepository.save(category);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package myblog.blog.category.repository;
|
||||
package myblog.blog.category.adapter.outgoing.persistence;
|
||||
|
||||
import myblog.blog.category.domain.Category;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
@@ -7,7 +7,7 @@ import org.springframework.data.jpa.repository.Query;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface CategoryRepository extends JpaRepository<Category, Long> {
|
||||
public interface JpaCategoryRepository extends JpaRepository<Category, Long> {
|
||||
|
||||
/*
|
||||
- 카테고리 이름으로 카테고리 찾기
|
||||
@@ -1,6 +1,6 @@
|
||||
package myblog.blog.category.repository;
|
||||
package myblog.blog.category.adapter.outgoing.persistence;
|
||||
|
||||
import myblog.blog.category.dto.CategorySimpleView;
|
||||
import myblog.blog.category.appliacation.port.incomming.response.CategorySimpleDto;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.springframework.stereotype.Repository;
|
||||
@@ -9,7 +9,7 @@ import java.util.List;
|
||||
|
||||
@Mapper
|
||||
@Repository
|
||||
public interface NaCategoryRepository {
|
||||
public interface MybatisCategoryRepository {
|
||||
|
||||
/*
|
||||
- 카테고리별 아티클 갯수 통계 쿼리
|
||||
@@ -23,6 +23,6 @@ public interface NaCategoryRepository {
|
||||
" group by c.title, b.title with rollup) e\n" +
|
||||
" right join category f on (e.title = f.title)\n" +
|
||||
" order by pOrder, cOrder ")
|
||||
List<CategorySimpleView> getCategoryCount();
|
||||
List<CategorySimpleDto> getCategoryCount();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package myblog.blog.category.appliacation;
|
||||
|
||||
import myblog.blog.article.application.port.incomming.response.*;
|
||||
import myblog.blog.article.domain.Article;
|
||||
import myblog.blog.article.domain.Tags;
|
||||
import myblog.blog.category.appliacation.port.incomming.response.CategorySimpleDto;
|
||||
import myblog.blog.category.domain.Category;
|
||||
import org.mapstruct.*;
|
||||
|
||||
@Mapper(
|
||||
componentModel = "spring",
|
||||
injectionStrategy = InjectionStrategy.CONSTRUCTOR,
|
||||
unmappedTargetPolicy = ReportingPolicy.ERROR
|
||||
)
|
||||
public interface CategoryDtoMapper {
|
||||
ArticleResponseByCategory category(Article article);
|
||||
@Mappings({
|
||||
@Mapping(target = "count",ignore = true),
|
||||
@Mapping(target = "POrder",ignore = true),
|
||||
@Mapping(target = "COrder",ignore = true)
|
||||
})
|
||||
CategorySimpleDto categorySimpleDto(Category category);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package myblog.blog.category.appliacation;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import myblog.blog.category.appliacation.port.incomming.CategoryQueriesUseCase;
|
||||
import myblog.blog.category.appliacation.port.incomming.response.CategorySimpleDto;
|
||||
import myblog.blog.category.appliacation.port.incomming.response.CategoryViewForLayout;
|
||||
import myblog.blog.category.appliacation.port.outgoing.CategoryRepositoryPort;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@Transactional(readOnly = true)
|
||||
@RequiredArgsConstructor
|
||||
public class CategoryQueries implements CategoryQueriesUseCase {
|
||||
|
||||
private final CategoryRepositoryPort categoryRepositoryPort;
|
||||
private final CategoryDtoMapper categoryDtoMapper;
|
||||
|
||||
/*
|
||||
- 카테고리와 카테고리별 아티클 수 찾기
|
||||
*/
|
||||
@Override
|
||||
public List<CategorySimpleDto> getCategorytCountList() {
|
||||
return categoryRepositoryPort.getCategoryCount();
|
||||
}
|
||||
|
||||
/*
|
||||
- getCategorytCountList()의 캐싱을 위한 전처리 매핑 로직
|
||||
- 레이아웃 렌더링 성능 향상을 위해 캐싱작업
|
||||
카테고리 변경 / 아티클 변경이 존재할경우 레이아웃 캐시 초기화
|
||||
*/
|
||||
@Cacheable(value = "layoutCaching", key = "0")
|
||||
@Override
|
||||
public CategoryViewForLayout getCategoryViewForLayout() {
|
||||
return CategoryViewForLayout.from(categoryRepositoryPort.getCategoryCount());
|
||||
}
|
||||
|
||||
/*
|
||||
- 티어별 카테고리 목록 찾기
|
||||
*/
|
||||
@Override
|
||||
public List<CategorySimpleDto> findCategoryByTier(int tier) {
|
||||
return categoryRepositoryPort.findAllByTierIs(tier)
|
||||
.stream()
|
||||
.map(categoryDtoMapper::categorySimpleDto)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package myblog.blog.category.appliacation;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import myblog.blog.category.domain.Category;
|
||||
import myblog.blog.category.appliacation.port.incomming.CategoryUseCase;
|
||||
import myblog.blog.category.appliacation.port.outgoing.CategoryRepositoryPort;
|
||||
import myblog.blog.category.appliacation.port.incomming.response.CategorySimpleDto;
|
||||
|
||||
import myblog.blog.category.domain.CategoryNotFoundException;
|
||||
import org.springframework.cache.annotation.CacheEvict;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
@RequiredArgsConstructor
|
||||
public class CategoryService implements CategoryUseCase {
|
||||
|
||||
private final CategoryRepositoryPort categoryRepositoryPort;
|
||||
|
||||
/*
|
||||
- 카테고리 이름으로 카테고리 찾기
|
||||
*/
|
||||
@Override
|
||||
public Optional<Category> findCategory(String title) {
|
||||
return categoryRepositoryPort.findByTitle(title);
|
||||
}
|
||||
|
||||
/*
|
||||
- 카테고리 이름으로 카테고리 찾기
|
||||
*/
|
||||
@Override
|
||||
public List<Category> getAllCategories() {
|
||||
return categoryRepositoryPort.findAll();
|
||||
}
|
||||
|
||||
/*
|
||||
- 카테고리 변경 로직
|
||||
1. 카테고리 리스트의 순서 작성
|
||||
2. 입력받은 카테고리리스트와 DB의 전체카테고리 리스트 두개를 큐로 처리하여 비교대조
|
||||
3. 해당 카테고리 변경처리
|
||||
3-1 해당 카테고리 존재시 더티체킹으로 업데이트 처리
|
||||
3-2 DB에 존재하지 않는경우 새로 생성
|
||||
3-3 DB에만 존재하는 카테고리는 삭제처리
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
@CacheEvict(value = {"layoutCaching", "seoCaching"}, allEntries = true)
|
||||
public void changeCategory(List<CategorySimpleDto> categoryList) {
|
||||
// 1.카테고리 리스트 순서 작성
|
||||
CategorySimpleDto.sortByOrder(categoryList);
|
||||
// 2. 기존 DB 저장된 카테고리 리스트 불러오기
|
||||
var categoryListFromDb = categoryRepositoryPort.findAllWithoutDummy();
|
||||
// 3. 카테고리 변경
|
||||
while (!categoryList.isEmpty()) {
|
||||
var categorySimpleDto = categoryList.get(0);
|
||||
categoryList.remove(0);
|
||||
if (categorySimpleDto.isSuperCategory()) {
|
||||
Category pCategory = null;
|
||||
if (categorySimpleDto.isNewCategory()) pCategory = createNewCategory(categorySimpleDto, null);
|
||||
else {
|
||||
pCategory = findMatchingCategory(categoryListFromDb, categorySimpleDto, pCategory);
|
||||
pCategory.updateCategory(categorySimpleDto.getTitle(), categorySimpleDto.getTier(), categorySimpleDto.getPOrder(), categorySimpleDto.getCOrder(), null);
|
||||
}
|
||||
|
||||
while (!categoryList.isEmpty()) {
|
||||
var subCategorySimpleDto = categoryList.get(0);
|
||||
if (subCategorySimpleDto.isSuperCategory()) break;
|
||||
categoryList.remove(0);
|
||||
Category cCategory = null;
|
||||
if (subCategorySimpleDto.isNewCategory()) cCategory = createNewCategory(subCategorySimpleDto, pCategory.getTitle());
|
||||
else {
|
||||
cCategory = findMatchingCategory(categoryListFromDb, subCategorySimpleDto, cCategory);
|
||||
cCategory.updateCategory(subCategorySimpleDto.getTitle(), subCategorySimpleDto.getTier(), subCategorySimpleDto.getPOrder(), subCategorySimpleDto.getCOrder(), pCategory);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 3-3 불일치 카테고리 전부 삭제
|
||||
categoryRepositoryPort.deleteAll(categoryListFromDb);
|
||||
}
|
||||
|
||||
private Category findMatchingCategory(List<Category> categoryListFromDb, CategorySimpleDto categorySimpleDto, Category category) {
|
||||
for (int i = 0; i < categoryListFromDb.size(); i++) {
|
||||
if (categoryListFromDb.get(i).getId().equals(categorySimpleDto.getId())) {
|
||||
category = categoryListFromDb.get(i);
|
||||
categoryListFromDb.remove(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return category;
|
||||
}
|
||||
|
||||
/*
|
||||
- 새로운 카테고리 생성하기
|
||||
- 상위 카테고리 존재 유무 분기
|
||||
*/
|
||||
private Category createNewCategory(CategorySimpleDto categorySimpleDto, String parent) {
|
||||
Category parentCategory = null;
|
||||
if (parent != null) {
|
||||
parentCategory = categoryRepositoryPort.findByTitle(parent)
|
||||
.orElseThrow(CategoryNotFoundException::new);
|
||||
}
|
||||
|
||||
Category category = Category.builder()
|
||||
.title(categorySimpleDto.getTitle())
|
||||
.pSortNum(categorySimpleDto.getPOrder())
|
||||
.cSortNum(categorySimpleDto.getCOrder())
|
||||
.tier(categorySimpleDto.getTier())
|
||||
.parents(parentCategory)
|
||||
.build();
|
||||
categoryRepositoryPort.save(category);
|
||||
return category;
|
||||
}
|
||||
|
||||
// /*
|
||||
// - 최초 필수 더미 카테고리 추가 코드
|
||||
// */
|
||||
// @PostConstruct
|
||||
// private void insertDummyCategory() {
|
||||
// if(categoryRepositoryPort.findByTitle("total")==null) {
|
||||
// Category category0 = Category.builder()
|
||||
// .tier(0)
|
||||
// .title("total")
|
||||
// .pSortNum(0)
|
||||
// .cSortNum(0)
|
||||
// .build();
|
||||
// categoryRepositoryPort.save(category0);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package myblog.blog.category.appliacation.port.incomming;
|
||||
|
||||
import myblog.blog.category.appliacation.port.incomming.response.CategorySimpleDto;
|
||||
import myblog.blog.category.appliacation.port.incomming.response.CategoryViewForLayout;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface CategoryQueriesUseCase {
|
||||
List<CategorySimpleDto> getCategorytCountList();
|
||||
CategoryViewForLayout getCategoryViewForLayout();
|
||||
List<CategorySimpleDto> findCategoryByTier(int tier);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package myblog.blog.category.appliacation.port.incomming;
|
||||
|
||||
import myblog.blog.category.appliacation.port.incomming.response.CategorySimpleDto;
|
||||
import myblog.blog.category.appliacation.port.incomming.response.CategoryViewForLayout;
|
||||
import myblog.blog.category.domain.Category;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface CategoryUseCase {
|
||||
Optional<Category> findCategory(String title);
|
||||
List<Category> getAllCategories();
|
||||
void changeCategory(List<CategorySimpleDto> categoryList);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package myblog.blog.category.appliacation.port.incomming.response;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import java.util.List;
|
||||
|
||||
/*
|
||||
- 범용 카테고리 DTO
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
public class CategorySimpleDto implements Cloneable {
|
||||
|
||||
private Long id;
|
||||
@NotBlank(message = "카테고리명은 공백일 수 없습니다.")
|
||||
private String title;
|
||||
private int tier;
|
||||
private int count;
|
||||
private int pOrder;
|
||||
private int cOrder;
|
||||
|
||||
@Override
|
||||
public CategorySimpleDto clone() {
|
||||
try {
|
||||
return (CategorySimpleDto) super.clone();
|
||||
} catch (CloneNotSupportedException e) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
/*
|
||||
- 카테고리 변경을 위해 카테고리의 순번을 작성하는 로직
|
||||
*/
|
||||
static public void sortByOrder(List<CategorySimpleDto> categoryList) {
|
||||
int pOrderIndex = 0;
|
||||
int cOrderIndex = 0;
|
||||
|
||||
//티어별 트리구조로 순서 작성 로직
|
||||
for (CategorySimpleDto categorySimpleDto : categoryList) {
|
||||
if (categorySimpleDto.getTier() == 1) {
|
||||
cOrderIndex = 0;
|
||||
categorySimpleDto.setPOrder(++pOrderIndex);
|
||||
categorySimpleDto.setCOrder(cOrderIndex);
|
||||
} else {
|
||||
categorySimpleDto.setPOrder(pOrderIndex);
|
||||
categorySimpleDto.setCOrder(++cOrderIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
public boolean isSuperCategory(){
|
||||
return tier == 1;
|
||||
}
|
||||
public boolean isNewCategory(){
|
||||
return id == null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package myblog.blog.category.appliacation.port.incomming.response;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/*
|
||||
- 레이아웃용 트리구조 카테고리 리스트
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
public class CategoryViewForLayout {
|
||||
|
||||
private int count;
|
||||
private String title;
|
||||
private Long id;
|
||||
private int pOrder;
|
||||
private int cOrder;
|
||||
// 트리구조를 갖기 위한 리스트
|
||||
private List<CategoryViewForLayout> categoryTCountList = new ArrayList<>();
|
||||
|
||||
/*
|
||||
- 스태틱 생성 메서드
|
||||
*/
|
||||
public static CategoryViewForLayout from(List<CategorySimpleDto> crList) {
|
||||
return recursiveBuildFromCategoryDto(0, crList);
|
||||
}
|
||||
|
||||
/*
|
||||
- 재귀호출로 트리구조 생성
|
||||
1. DTO객체 생성후 소스를 큐처리로 순차적 매핑
|
||||
2. Depth 변화시 재귀 호출 / 재귀 탈출
|
||||
3. 탈출시 상위 카테고리 list로 삽입하여 트리구조 작성
|
||||
*/
|
||||
private static CategoryViewForLayout recursiveBuildFromCategoryDto(int tier, List<CategorySimpleDto> source) {
|
||||
|
||||
CategoryViewForLayout categoryViewForLayout = new CategoryViewForLayout();
|
||||
|
||||
while (!source.isEmpty()) {
|
||||
CategorySimpleDto cSource = source.get(0);
|
||||
|
||||
if (cSource.getTier() == tier) {
|
||||
if(categoryViewForLayout.getTitle() != null
|
||||
&& !categoryViewForLayout.getTitle().equals(cSource.getTitle())){
|
||||
return categoryViewForLayout;
|
||||
}
|
||||
categoryViewForLayout.setTitle(cSource.getTitle());
|
||||
categoryViewForLayout.setCount(cSource.getCount());
|
||||
categoryViewForLayout.setId(cSource.getId());
|
||||
categoryViewForLayout.setCOrder(cSource.getCOrder());
|
||||
categoryViewForLayout.setPOrder(cSource.getPOrder());
|
||||
source.remove(0);
|
||||
} else if (cSource.getTier() > tier) {
|
||||
CategoryViewForLayout sub = recursiveBuildFromCategoryDto(tier + 1, source);
|
||||
categoryViewForLayout.getCategoryTCountList().add(sub);
|
||||
} else {
|
||||
return categoryViewForLayout;
|
||||
}
|
||||
}
|
||||
return categoryViewForLayout;
|
||||
}
|
||||
|
||||
private CategoryViewForLayout() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package myblog.blog.category.appliacation.port.outgoing;
|
||||
|
||||
import myblog.blog.category.appliacation.port.incomming.response.CategorySimpleDto;
|
||||
import myblog.blog.category.domain.Category;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface CategoryRepositoryPort {
|
||||
Optional<Category> findByTitle(String title);
|
||||
List<Category> findAll();
|
||||
List<CategorySimpleDto> getCategoryCount();
|
||||
List<Category> findAllByTierIs(int tier);
|
||||
List<Category> findAllWithoutDummy();
|
||||
void deleteAll(List<Category> categoryListFromDb);
|
||||
void save(Category category);
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package myblog.blog.category.controller;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import myblog.blog.shared.exception.CustomFormException;
|
||||
import myblog.blog.shared.exception.ListValidator;
|
||||
import myblog.blog.category.dto.CategoryForView;
|
||||
import myblog.blog.category.dto.CategorySimpleView;
|
||||
import myblog.blog.category.service.CategoryService;
|
||||
import myblog.blog.comment.dto.CommentDtoForLayout;
|
||||
import myblog.blog.comment.service.CommentService;
|
||||
import org.modelmapper.ModelMapper;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.validation.Errors;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Controller
|
||||
@RequiredArgsConstructor
|
||||
public class CategoryController {
|
||||
|
||||
private final CategoryService categoryService;
|
||||
private final CommentService commentService;
|
||||
private final ModelMapper modelMapper;
|
||||
private final ListValidator listValidator;
|
||||
|
||||
/*
|
||||
- 카테고리 수정폼 조회
|
||||
*/
|
||||
@GetMapping("/edit/category")
|
||||
public String editCategoryForm(Model model) {
|
||||
|
||||
// DTO 매핑 전처리
|
||||
List<CategorySimpleView> categoryList = categoryService.getCategorytCountList();
|
||||
List<CategorySimpleView> copyList = cloneList(categoryList);
|
||||
copyList.remove(0);
|
||||
CategoryForView categoryForView = CategoryForView.createCategory(categoryList);
|
||||
List<CommentDtoForLayout> comments = commentService.recentCommentList();
|
||||
//
|
||||
|
||||
model.addAttribute("categoryForEdit", copyList);
|
||||
model.addAttribute("category", categoryForView);
|
||||
model.addAttribute("commentsList", comments);
|
||||
|
||||
return "admin/categoryEdit";
|
||||
}
|
||||
|
||||
/*
|
||||
- 카테고리 수정 요청
|
||||
*/
|
||||
@PostMapping("/category/edit")
|
||||
public @ResponseBody
|
||||
String editCategory(@RequestBody List<CategorySimpleView> categoryList, Errors errors) {
|
||||
// List DTO 검증을 위한 커스텀 validator
|
||||
listValidator.validate(categoryList, errors);
|
||||
// 유효성 검사
|
||||
if (errors.hasErrors()) {
|
||||
throw new CustomFormException(Objects.requireNonNull(errors.getFieldError()).getDefaultMessage());
|
||||
}
|
||||
|
||||
categoryService.changeCategory(categoryList);
|
||||
return "변경 성공";
|
||||
}
|
||||
private List<CategorySimpleView> cloneList(List<CategorySimpleView> categoryList) {
|
||||
return categoryList
|
||||
.stream()
|
||||
.map(categoryNormalDto ->
|
||||
modelMapper.map(categoryNormalDto, CategorySimpleView.class))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,9 @@ package myblog.blog.category.domain;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
import myblog.blog.shared.domain.BasicEntity;
|
||||
import myblog.blog.article.domain.Article;
|
||||
import myblog.blog.shared.BasicEntity;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.util.ArrayList;
|
||||
@@ -55,7 +56,7 @@ public class Category extends BasicEntity {
|
||||
this.cSortNum = cSortNum;
|
||||
}
|
||||
|
||||
protected Category() {
|
||||
public Category() {
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package myblog.blog.category.domain;
|
||||
|
||||
import myblog.blog.shared.domain.ResourceNotFoundException;
|
||||
|
||||
public class CategoryNotFoundException extends ResourceNotFoundException {
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
package myblog.blog.category.dto;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/*
|
||||
- 레이아웃용 트리구조 카테고리 리스트
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
public class CategoryForView {
|
||||
|
||||
private int count;
|
||||
private String title;
|
||||
private Long id;
|
||||
private int pOrder;
|
||||
private int cOrder;
|
||||
// 트리구조를 갖기 위한 리스트
|
||||
private List<CategoryForView> categoryTCountList = new ArrayList<>();
|
||||
|
||||
/*
|
||||
- 스태틱 생성 메서드
|
||||
*/
|
||||
public static CategoryForView createCategory(List<CategorySimpleView> crList) {
|
||||
return recursiveBuildFromCategoryDto(0, crList);
|
||||
}
|
||||
|
||||
/*
|
||||
- 재귀호출로 트리구조 생성
|
||||
1. DTO객체 생성후 소스를 큐처리로 순차적 매핑
|
||||
2. Depth 변화시 재귀 호출 / 재귀 탈출
|
||||
3. 탈출시 상위 카테고리 list로 삽입하여 트리구조 작성
|
||||
*/
|
||||
private static CategoryForView recursiveBuildFromCategoryDto(int tier, List<CategorySimpleView> source) {
|
||||
|
||||
CategoryForView categoryForView = new CategoryForView();
|
||||
|
||||
while (!source.isEmpty()) {
|
||||
CategorySimpleView cSource = source.get(0);
|
||||
|
||||
if (cSource.getTier() == tier) {
|
||||
if(categoryForView.getTitle() != null
|
||||
&& !categoryForView.getTitle().equals(cSource.getTitle())){
|
||||
return categoryForView;
|
||||
}
|
||||
categoryForView.setTitle(cSource.getTitle());
|
||||
categoryForView.setCount(cSource.getCount());
|
||||
categoryForView.setId(cSource.getId());
|
||||
categoryForView.setCOrder(cSource.getCOrder());
|
||||
categoryForView.setPOrder(cSource.getPOrder());
|
||||
source.remove(0);
|
||||
} else if (cSource.getTier() > tier) {
|
||||
CategoryForView sub = recursiveBuildFromCategoryDto(tier + 1, source);
|
||||
categoryForView.getCategoryTCountList().add(sub);
|
||||
} else {
|
||||
return categoryForView;
|
||||
}
|
||||
}
|
||||
return categoryForView;
|
||||
}
|
||||
|
||||
private CategoryForView() {
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package myblog.blog.category.dto;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
|
||||
/*
|
||||
- 범용 카테고리 DTO
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
public class CategorySimpleView {
|
||||
|
||||
private Long id;
|
||||
@NotBlank(message = "카테고리명은 공백일 수 없습니다.")
|
||||
private String title;
|
||||
private int tier;
|
||||
private int count;
|
||||
private int pOrder;
|
||||
private int cOrder;
|
||||
|
||||
}
|
||||
@@ -1,208 +0,0 @@
|
||||
package myblog.blog.category.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import myblog.blog.category.domain.Category;
|
||||
import myblog.blog.category.dto.CategorySimpleView;
|
||||
import myblog.blog.category.dto.CategoryForView;
|
||||
import myblog.blog.category.repository.CategoryRepository;
|
||||
import myblog.blog.category.repository.NaCategoryRepository;
|
||||
import org.springframework.cache.annotation.CacheEvict;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
@RequiredArgsConstructor
|
||||
public class CategoryService {
|
||||
|
||||
private final CategoryRepository categoryRepository;
|
||||
private final NaCategoryRepository naCategoryRepository;
|
||||
|
||||
/*
|
||||
- 카테고리 이름으로 카테고리 찾기
|
||||
*/
|
||||
public Category findCategory(String title) {
|
||||
return categoryRepository.findByTitle(title)
|
||||
.orElseThrow(() -> new IllegalArgumentException("NotFoundCategoryException"));
|
||||
}
|
||||
|
||||
/*
|
||||
- 카테고리 이름으로 카테고리 찾기
|
||||
*/
|
||||
public List<Category> getAllCategories() {
|
||||
return categoryRepository.findAll();
|
||||
}
|
||||
|
||||
/*
|
||||
- 카테고리와 카테고리별 아티클 수 찾기
|
||||
*/
|
||||
public List<CategorySimpleView> getCategorytCountList() {
|
||||
return naCategoryRepository.getCategoryCount();
|
||||
}
|
||||
|
||||
/*
|
||||
- getCategorytCountList()의 캐싱을 위한 전처리 매핑 로직
|
||||
- 본래는 컨트롤러단에서 존재해야할 dto 매핑코드지만 캐싱을 위해 서비스단으로 이동
|
||||
- 레이아웃 렌더링 성능 향상을 위해 캐싱작업
|
||||
카테고리 변경 / 아티클 변경이 존재할경우 레이아웃 캐시 초기화
|
||||
*/
|
||||
@Cacheable(value = "layoutCaching", key = "0")
|
||||
public CategoryForView getCategoryForView() {
|
||||
return CategoryForView.createCategory(naCategoryRepository.getCategoryCount());
|
||||
}
|
||||
|
||||
/*
|
||||
- 티어별 카테고리 목록 찾기
|
||||
*/
|
||||
public List<Category> findCategoryByTier(int tier) {
|
||||
return categoryRepository.findAllByTierIs(tier);
|
||||
}
|
||||
|
||||
/*
|
||||
- 카테고리 변경 로직
|
||||
1. 카테고리 리스트의 순서 작성
|
||||
2. 입력받은 카테고리리스트와 DB의 전체카테고리 리스트 두개를 큐로 처리하여 비교대조
|
||||
3. 해당 카테고리 변경처리
|
||||
3-1 해당 카테고리 존재시 더티체킹으로 업데이트 처리
|
||||
3-2 DB에 존재하지 않는경우 새로 생성
|
||||
3-3 DB에만 존재하는 카테고리는 삭제처리
|
||||
*/
|
||||
@Transactional
|
||||
@CacheEvict(value = {"layoutCaching", "seoCaching"}, allEntries = true)
|
||||
public void changeCategory(List<CategorySimpleView> categoryList) {
|
||||
|
||||
// 1.카테고리 리스트 순서 작성
|
||||
sortingOrder(categoryList);
|
||||
// 2. 기존 DB 저장된 카테고리 리스트 불러오기
|
||||
List<Category> categoryListFromDb = categoryRepository.findAllWithoutDummy();
|
||||
|
||||
// 3. 카테고리 변경 루프
|
||||
while (!categoryList.isEmpty()) {
|
||||
CategorySimpleView categorySimpleView = categoryList.get(0);
|
||||
categoryList.remove(0);
|
||||
|
||||
// 부모카테고리인경우
|
||||
if (categorySimpleView.getTier() == 1) {
|
||||
|
||||
Category pCategory = null;
|
||||
|
||||
// 부모카테고리가 기존에 존재 x
|
||||
if (categorySimpleView.getId() == null) {
|
||||
pCategory = createNewCategory(categorySimpleView, null);
|
||||
}
|
||||
// 부모카테고리가 기존에 존재 o
|
||||
else {
|
||||
for (int i = 0; i < categoryListFromDb.size(); i++) {
|
||||
if (categoryListFromDb.get(i).getId().equals(categorySimpleView.getId())) {
|
||||
pCategory = categoryListFromDb.get(i);
|
||||
categoryListFromDb.remove(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
pCategory.updateCategory(
|
||||
categorySimpleView.getTitle(),
|
||||
categorySimpleView.getTier(),
|
||||
categorySimpleView.getPOrder(),
|
||||
categorySimpleView.getCOrder(),
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
while (!categoryList.isEmpty()) {
|
||||
|
||||
CategorySimpleView subCategorySimpleView = categoryList.get(0);
|
||||
if (subCategorySimpleView.getTier() == 1) break;
|
||||
categoryList.remove(0);
|
||||
|
||||
// 자식 카테고리인경우
|
||||
Category cCategory = null;
|
||||
// 카테고리가 기존에 존재 x
|
||||
if (subCategorySimpleView.getId() == null) {
|
||||
cCategory = createNewCategory(subCategorySimpleView, pCategory.getTitle());
|
||||
}
|
||||
// 카테고리가 기존에 존재 o
|
||||
else {
|
||||
for (int i = 0; i < categoryListFromDb.size(); i++) {
|
||||
if (categoryListFromDb.get(i).getId().equals(subCategorySimpleView.getId())) {
|
||||
cCategory = categoryListFromDb.get(i);
|
||||
categoryListFromDb.remove(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
cCategory.updateCategory(
|
||||
subCategorySimpleView.getTitle(),
|
||||
subCategorySimpleView.getTier(),
|
||||
subCategorySimpleView.getPOrder(),
|
||||
subCategorySimpleView.getCOrder(),
|
||||
pCategory);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 3-3 불일치 카테고리 전부 삭제
|
||||
categoryRepository.deleteAll(categoryListFromDb);
|
||||
}
|
||||
|
||||
/*
|
||||
- 새로운 카테고리 생성하기
|
||||
- 상위 카테고리 존재 유무 분기
|
||||
*/
|
||||
private Category createNewCategory(CategorySimpleView categorySimpleView, String parent) {
|
||||
Category parentCategory = null;
|
||||
if (parent != null) {
|
||||
parentCategory = categoryRepository.findByTitle(parent)
|
||||
.orElseThrow(() -> new IllegalArgumentException("NotFoundCategoryException"));
|
||||
}
|
||||
|
||||
Category category = Category.builder()
|
||||
.title(categorySimpleView.getTitle())
|
||||
.pSortNum(categorySimpleView.getPOrder())
|
||||
.cSortNum(categorySimpleView.getCOrder())
|
||||
.tier(categorySimpleView.getTier())
|
||||
.parents(parentCategory)
|
||||
.build();
|
||||
categoryRepository.save(category);
|
||||
return category;
|
||||
}
|
||||
|
||||
/*
|
||||
- 카테고리 변경을 위해 카테고리의 순번을 작성하는 로직
|
||||
*/
|
||||
private void sortingOrder(List<CategorySimpleView> categoryList) {
|
||||
int pOrderIndex = 0;
|
||||
int cOrderIndex = 0;
|
||||
|
||||
//티어별 트리구조로 순서 작성 로직
|
||||
for (CategorySimpleView categoryDto : categoryList) {
|
||||
|
||||
if (categoryDto.getTier() == 1) {
|
||||
cOrderIndex = 0;
|
||||
categoryDto.setPOrder(++pOrderIndex);
|
||||
categoryDto.setCOrder(cOrderIndex);
|
||||
} else {
|
||||
categoryDto.setPOrder(pOrderIndex);
|
||||
categoryDto.setCOrder(++cOrderIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
- 최초 필수 더미 카테고리 추가 코드
|
||||
*/
|
||||
// @PostConstruct
|
||||
private void insertDummyCategory() {
|
||||
if(categoryRepository.findByTitle("total")==null) {
|
||||
Category category0 = Category.builder()
|
||||
.tier(0)
|
||||
.title("total")
|
||||
.pSortNum(0)
|
||||
.cSortNum(0)
|
||||
.build();
|
||||
categoryRepository.save(category0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package myblog.blog.comment.adapter.incomming;
|
||||
|
||||
import myblog.blog.shared.domain.BadRequestException;
|
||||
|
||||
/*
|
||||
- REST 컨트롤러 상태 메세지 전송용 커스텀 에러
|
||||
*/
|
||||
public class CommentBadRequestException extends BadRequestException {
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package myblog.blog.comment.adapter.incomming;
|
||||
|
||||
import myblog.blog.comment.application.port.incomming.CommentQueriesUseCase;
|
||||
import myblog.blog.comment.application.port.incomming.CommentUseCase;
|
||||
import myblog.blog.comment.application.port.incomming.response.CommentDto;
|
||||
|
||||
import myblog.blog.member.application.port.incomming.response.PrincipalDetails;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import myblog.blog.member.application.port.incomming.response.MemberVo;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.validation.Errors;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import java.util.*;
|
||||
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
public class CommentController {
|
||||
|
||||
private final CommentUseCase commentUseCase;
|
||||
private final CommentQueriesUseCase commentQueriesUseCase;
|
||||
|
||||
/*
|
||||
- 아티클 조회시 아티클에 달린 댓글들 전체 조회
|
||||
*/
|
||||
@GetMapping("/comment/list/{articleId}")
|
||||
List<CommentDto> getCommentList(@PathVariable Long articleId){
|
||||
return commentQueriesUseCase.getCommentList(articleId);
|
||||
}
|
||||
|
||||
/*
|
||||
- 댓글 작성 요청
|
||||
*/
|
||||
@PostMapping("/comment/write")
|
||||
List<CommentDto> getCommentList(@RequestParam Long articleId,
|
||||
@RequestParam(required = false) Long parentId,
|
||||
@Validated @RequestBody CommentForm commentForm, Errors errors,
|
||||
@AuthenticationPrincipal PrincipalDetails principal){
|
||||
if (errors.hasErrors()) {
|
||||
throw new CommentBadRequestException();
|
||||
}
|
||||
MemberVo member = principal.getMember();
|
||||
// 부모 댓글인지 자식댓글인지 분기로 저장
|
||||
if(parentId != null){
|
||||
commentUseCase.saveCComment(commentForm.getContent(), commentForm.isSecret(), member.getId(), articleId, parentId);
|
||||
}
|
||||
else {
|
||||
commentUseCase.savePComment(commentForm.getContent(), commentForm.isSecret(), member.getId(), articleId);
|
||||
}
|
||||
return commentQueriesUseCase.getCommentList(articleId);
|
||||
}
|
||||
|
||||
/*
|
||||
- 댓글 삭제 요청
|
||||
*/
|
||||
@PostMapping("/comment/delete")
|
||||
List<CommentDto> deleteComment(@RequestParam Long articleId,
|
||||
@RequestParam Long commentId) {
|
||||
commentUseCase.deleteComment(commentId);
|
||||
return commentQueriesUseCase.getCommentList(articleId);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package myblog.blog.comment.dto;
|
||||
package myblog.blog.comment.adapter.incomming;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package myblog.blog.comment.adapter.outgoing.persistence;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import myblog.blog.comment.application.port.outgoing.CommentRepositoryPort;
|
||||
import myblog.blog.article.domain.Article;
|
||||
import myblog.blog.comment.domain.Comment;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class CommentRepositoryAdapter implements CommentRepositoryPort {
|
||||
private final JpaCommentRepository jpaCommentRepository;
|
||||
private final MybatisCommentRepository mybatisCommentRepository;
|
||||
|
||||
|
||||
@Override
|
||||
public int countCommentsByArticleAndTier(Article article, int tier) {
|
||||
return jpaCommentRepository.countCommentsByArticleAndTier(article, tier);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(Comment comment) {
|
||||
jpaCommentRepository.save(comment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Comment> findCommentsByArticleId(Long articleId) {
|
||||
return jpaCommentRepository.findCommentsByArticleId(articleId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteComment(Long commentId) {
|
||||
mybatisCommentRepository.deleteComment(commentId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Comment> findTop5ByOrderByIdDesc() {
|
||||
return jpaCommentRepository.findTop5ByOrderByIdDesc();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Comment> findById(Long parentId) {
|
||||
return jpaCommentRepository.findById(parentId);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package myblog.blog.comment.repository;
|
||||
package myblog.blog.comment.adapter.outgoing.persistence;
|
||||
|
||||
import myblog.blog.article.domain.Article;
|
||||
import myblog.blog.comment.domain.Comment;
|
||||
@@ -8,7 +8,7 @@ import org.springframework.data.repository.query.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface CommentRepository extends JpaRepository<Comment, Long> {
|
||||
public interface JpaCommentRepository extends JpaRepository<Comment, Long> {
|
||||
|
||||
/*
|
||||
- 특정 아티클에 해당하는 댓글 리스트 가져오기
|
||||
@@ -1,10 +1,10 @@
|
||||
package myblog.blog.comment.repository;
|
||||
package myblog.blog.comment.adapter.outgoing.persistence;
|
||||
|
||||
import org.apache.ibatis.annotations.Delete;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface NaCommentRepository {
|
||||
public interface MybatisCommentRepository {
|
||||
|
||||
/*
|
||||
- cascade 삭제처리
|
||||
@@ -0,0 +1,43 @@
|
||||
package myblog.blog.comment.application;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import myblog.blog.comment.application.port.incomming.CommentQueriesUseCase;
|
||||
import myblog.blog.comment.application.port.incomming.response.CommentDto;
|
||||
import myblog.blog.comment.application.port.incomming.response.CommentDtoForLayout;
|
||||
import myblog.blog.comment.application.port.outgoing.CommentRepositoryPort;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@Transactional(readOnly = true)
|
||||
@RequiredArgsConstructor
|
||||
public class CommentQueries implements CommentQueriesUseCase {
|
||||
private final CommentRepositoryPort commentRepositoryPort;
|
||||
|
||||
/*
|
||||
- 아티클에 달린 댓글 전체 가져오기
|
||||
*/
|
||||
@Override
|
||||
public List<CommentDto> getCommentList(Long articleId){
|
||||
return CommentDto.createCommentListFrom(commentRepositoryPort.findCommentsByArticleId(articleId),0);
|
||||
}
|
||||
/*
|
||||
- 최신 댓글 5개 가져오기
|
||||
- 레이아웃 렌더링 성능 향상을 위해 캐싱작업
|
||||
카테고리 변경 / 아티클 변경이 존재할경우 레이아웃 캐시 초기화
|
||||
DTO 매핑 로직 서비스단에서 처리
|
||||
*/
|
||||
@Cacheable(value = "layoutRecentCommentCaching", key = "0")
|
||||
@Override
|
||||
public List<CommentDtoForLayout> recentCommentListForLayout(){
|
||||
return commentRepositoryPort.findTop5ByOrderByIdDesc()
|
||||
.stream()
|
||||
.map(comment ->
|
||||
new CommentDtoForLayout(comment.getId(), comment.getArticle().getId(), comment.getContent(), comment.isSecret()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package myblog.blog.comment.application;
|
||||
|
||||
import myblog.blog.article.application.port.incomming.ArticleUseCase;
|
||||
import myblog.blog.comment.application.port.incomming.CommentUseCase;
|
||||
import myblog.blog.comment.application.port.outgoing.CommentRepositoryPort;
|
||||
|
||||
import myblog.blog.comment.domain.Comment;
|
||||
import myblog.blog.article.domain.Article;
|
||||
import myblog.blog.comment.domain.NotFoundParentCommnetException;
|
||||
import myblog.blog.member.doamin.Member;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import myblog.blog.member.application.port.incomming.MemberQueriesUseCase;
|
||||
import myblog.blog.member.doamin.NotFoundMemberException;
|
||||
import org.springframework.cache.annotation.CacheEvict;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
@RequiredArgsConstructor
|
||||
public class CommentService implements CommentUseCase {
|
||||
private final ArticleUseCase articleUseCase;
|
||||
private final MemberQueriesUseCase memberQueriesUseCase;
|
||||
private final CommentRepositoryPort commentRepositoryPort;
|
||||
|
||||
/*
|
||||
- 부모 댓글 저장
|
||||
*/
|
||||
@CacheEvict(value = "layoutRecentCommentCaching", allEntries = true)
|
||||
@Override
|
||||
public void savePComment(String content, boolean secret, Long memberId, Long articleId){
|
||||
var member = memberQueriesUseCase.findById(memberId)
|
||||
.orElseThrow(NotFoundMemberException::new);
|
||||
var article = articleUseCase.getArticle(articleId);
|
||||
|
||||
Comment comment = Comment.builder()
|
||||
.article(article)
|
||||
.content(content)
|
||||
.tier(0)
|
||||
.pOrder(commentRepositoryPort.countCommentsByArticleAndTier(article,0)+1)
|
||||
.member(member)
|
||||
.secret(secret)
|
||||
.build();
|
||||
commentRepositoryPort.save(comment);
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
- 자식 댓글 저장
|
||||
*/
|
||||
@CacheEvict(value = "layoutRecentCommentCaching", allEntries = true)
|
||||
@Override
|
||||
public void saveCComment(String content, boolean secret, Long memberId, Long articleId, Long parentId) {
|
||||
var member = memberQueriesUseCase.findById(memberId)
|
||||
.orElseThrow(NotFoundMemberException::new);;
|
||||
var article = articleUseCase.getArticle(articleId);
|
||||
var pComment = commentRepositoryPort.findById(parentId)
|
||||
.orElseThrow(NotFoundParentCommnetException::new);
|
||||
|
||||
var comment = Comment.builder()
|
||||
.article(article)
|
||||
.content(content)
|
||||
.tier(1)
|
||||
.pOrder(pComment.getPOrder())
|
||||
.member(member)
|
||||
.parents(pComment)
|
||||
.secret(secret)
|
||||
.build();
|
||||
commentRepositoryPort.save(comment);
|
||||
}
|
||||
|
||||
/*
|
||||
- 댓글 삭제
|
||||
*/
|
||||
@CacheEvict(value = "layoutRecentCommentCaching", allEntries = true)
|
||||
@Override
|
||||
public void deleteComment(Long commentId){
|
||||
commentRepositoryPort.deleteComment(commentId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package myblog.blog.comment.application.port.incomming;
|
||||
|
||||
import myblog.blog.comment.application.port.incomming.response.CommentDto;
|
||||
import myblog.blog.comment.application.port.incomming.response.CommentDtoForLayout;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface CommentQueriesUseCase {
|
||||
List<CommentDto> getCommentList(Long articleId);
|
||||
List<CommentDtoForLayout> recentCommentListForLayout();
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package myblog.blog.comment.application.port.incomming;
|
||||
|
||||
import myblog.blog.comment.application.port.incomming.response.CommentDto;
|
||||
import myblog.blog.comment.application.port.incomming.response.CommentDtoForLayout;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface CommentUseCase {
|
||||
void savePComment(String content, boolean secret, Long memberId, Long articleId);
|
||||
void saveCComment(String content, boolean secret, Long memberId, Long articleId, Long parentId);
|
||||
void deleteComment(Long commentId);
|
||||
}
|
||||
@@ -1,13 +1,11 @@
|
||||
package myblog.blog.comment.dto;
|
||||
package myblog.blog.comment.application.port.incomming.response;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import myblog.blog.comment.domain.Comment;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.*;
|
||||
|
||||
|
||||
/*
|
||||
@@ -36,7 +34,7 @@ public class CommentDto {
|
||||
2. Depth 변화시 재귀 호출 / 재귀 탈출
|
||||
3. 탈출시 상위 카테고리 list로 삽입하여 트리구조 작성
|
||||
*/
|
||||
public static List<CommentDto> listCreateFrom(List<Comment> commentSource, int dept) {
|
||||
public static List<CommentDto> createCommentListFrom(List<Comment> commentSource, int dept) {
|
||||
|
||||
ArrayList<CommentDto> commentDtoList = new ArrayList<>();
|
||||
|
||||
@@ -51,7 +49,7 @@ public class CommentDto {
|
||||
commentDtoList.add(new CommentDto(comment));
|
||||
commentSource.remove(0);
|
||||
} else if (comment.getTier() > dept) {
|
||||
List<CommentDto> childList = listCreateFrom(commentSource, dept + 1);
|
||||
List<CommentDto> childList = createCommentListFrom(commentSource, dept + 1);
|
||||
commentDtoList.get(commentDtoList.size() - 1)
|
||||
.setCommentDtoList(childList);
|
||||
} else {
|
||||
@@ -1,4 +1,4 @@
|
||||
package myblog.blog.comment.dto;
|
||||
package myblog.blog.comment.application.port.incomming.response;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
@@ -0,0 +1,16 @@
|
||||
package myblog.blog.comment.application.port.outgoing;
|
||||
|
||||
import myblog.blog.article.domain.Article;
|
||||
import myblog.blog.comment.domain.Comment;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface CommentRepositoryPort {
|
||||
int countCommentsByArticleAndTier(Article article, int tier);
|
||||
void save(Comment comment);
|
||||
List<Comment> findCommentsByArticleId(Long articleId);
|
||||
void deleteComment(Long commentId);
|
||||
List<Comment> findTop5ByOrderByIdDesc();
|
||||
Optional<Comment> findById(Long parentId);
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
package myblog.blog.comment.controller;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import myblog.blog.article.application.port.incomming.ArticleQueriesUseCase;
|
||||
import myblog.blog.article.domain.Article;
|
||||
import myblog.blog.article.application.ArticleService;
|
||||
import myblog.blog.comment.dto.CommentDto;
|
||||
import myblog.blog.comment.dto.CommentForm;
|
||||
import myblog.blog.comment.service.CommentService;
|
||||
import myblog.blog.shared.exception.CustomFormException;
|
||||
import myblog.blog.member.auth.PrincipalDetails;
|
||||
import myblog.blog.member.doamin.Member;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.validation.Errors;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
public class CommentController {
|
||||
|
||||
private final CommentService commentService;
|
||||
|
||||
/*
|
||||
- 아티클 조회시 아티클에 달린 댓글들 전체 조회
|
||||
*/
|
||||
@GetMapping("/comment/list/{articleId}")
|
||||
public List<CommentDto> getCommentList(@PathVariable Long articleId){
|
||||
return CommentDto.listCreateFrom(commentService.getCommentList(articleId),0);
|
||||
}
|
||||
|
||||
/*
|
||||
- 댓글 작성 요청
|
||||
*/
|
||||
@PostMapping("/comment/write")
|
||||
public List<CommentDto> getCommentList(@RequestParam Long articleId,
|
||||
@RequestParam(required = false) Long parentId,
|
||||
@Validated @RequestBody CommentForm commentForm, Errors errors,
|
||||
@AuthenticationPrincipal PrincipalDetails principal){
|
||||
if (errors.hasErrors()) {
|
||||
throw new CustomFormException(Objects.requireNonNull(errors.getFieldError()).getDefaultMessage());
|
||||
}
|
||||
|
||||
Member member = principal.getMember();
|
||||
// 부모 댓글인지 자식댓글인지 분기로 저장
|
||||
if(parentId != null){
|
||||
commentService.saveCComment(commentForm, member, articleId, parentId);
|
||||
}
|
||||
else {
|
||||
commentService.savePComment(commentForm, member, articleId);
|
||||
}
|
||||
|
||||
return CommentDto.listCreateFrom(commentService.getCommentList(articleId),0);
|
||||
}
|
||||
|
||||
/*
|
||||
- 댓글 삭제 요청
|
||||
*/
|
||||
@PostMapping("/comment/delete")
|
||||
public List<CommentDto> deleteComment(@RequestParam Long articleId,
|
||||
@RequestParam Long commentId) {
|
||||
commentService.deleteComment(commentId);
|
||||
return CommentDto.listCreateFrom(commentService.getCommentList(articleId),0);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,8 @@ package myblog.blog.comment.domain;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import myblog.blog.article.domain.Article;
|
||||
import myblog.blog.shared.BasicEntity;
|
||||
import myblog.blog.comment.adapter.incomming.CommentBadRequestException;
|
||||
import myblog.blog.shared.domain.BasicEntity;
|
||||
import myblog.blog.member.doamin.Member;
|
||||
import org.hibernate.annotations.OnDelete;
|
||||
import org.hibernate.annotations.OnDeleteAction;
|
||||
@@ -62,9 +63,40 @@ public class Comment extends BasicEntity {
|
||||
this.tier = tier;
|
||||
this.parents = parents;
|
||||
this.member = member;
|
||||
this.content = content;
|
||||
this.content = removeDuplicatedEnter(content);
|
||||
this.secret = secret;
|
||||
}
|
||||
|
||||
protected Comment() {}
|
||||
|
||||
/*
|
||||
- 중복 개행 개행 하나로 압축 알고리즘
|
||||
*/
|
||||
private String removeDuplicatedEnter(String content) {
|
||||
if(content == null || content.isEmpty()){
|
||||
throw new CommentBadRequestException();
|
||||
}
|
||||
char[] contentBox = new char[content.length()];
|
||||
int idx = 0;
|
||||
String zipWord = "\n\n";
|
||||
|
||||
for(int i = 0; i< content.length(); i++){
|
||||
contentBox[idx] = content.charAt(i);
|
||||
if(contentBox[idx] == '\n'&&idx >= 1){
|
||||
int tempIdx = idx;
|
||||
int length = 1;
|
||||
boolean isRemove = true;
|
||||
|
||||
for(int j = 0; j<2; j++){
|
||||
if(contentBox[tempIdx--] != zipWord.charAt(length--)){
|
||||
isRemove = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(isRemove) idx -= 1;
|
||||
}
|
||||
idx++;
|
||||
}
|
||||
return String.valueOf(contentBox).trim();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package myblog.blog.comment.domain;
|
||||
|
||||
import myblog.blog.shared.domain.ResourceNotFoundException;
|
||||
|
||||
public class NotFoundParentCommnetException extends ResourceNotFoundException {
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
package myblog.blog.comment.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import myblog.blog.article.application.port.incomming.ArticleUseCase;
|
||||
import myblog.blog.article.domain.Article;
|
||||
import myblog.blog.comment.domain.Comment;
|
||||
import myblog.blog.comment.dto.CommentDtoForLayout;
|
||||
import myblog.blog.comment.dto.CommentForm;
|
||||
import myblog.blog.comment.repository.CommentRepository;
|
||||
import myblog.blog.comment.repository.NaCommentRepository;
|
||||
import myblog.blog.member.doamin.Member;
|
||||
import org.springframework.cache.annotation.CacheEvict;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
@RequiredArgsConstructor
|
||||
public class CommentService {
|
||||
|
||||
private final ArticleUseCase articleUseCase;
|
||||
|
||||
private final CommentRepository commentRepository;
|
||||
private final NaCommentRepository naCommentRepository;
|
||||
|
||||
/*
|
||||
- 아티클에 달린 댓글 전체 가져오기
|
||||
*/
|
||||
public List<Comment> getCommentList(Long articleId){
|
||||
return commentRepository.findCommentsByArticleId(articleId);
|
||||
}
|
||||
|
||||
/*
|
||||
- 부모 댓글 저장
|
||||
*/
|
||||
@CacheEvict(value = "layoutRecentCommentCaching", allEntries = true)
|
||||
public void savePComment(CommentForm commentForm, Member member, Long articleId){
|
||||
|
||||
Article article = articleUseCase.getArticle(articleId);
|
||||
|
||||
Comment comment = Comment.builder()
|
||||
.article(article)
|
||||
.content(removeDuplicatedEnter(commentForm))
|
||||
.tier(0)
|
||||
.pOrder(commentRepository.countCommentsByArticleAndTier(article,0)+1)
|
||||
.member(member)
|
||||
.secret(commentForm.isSecret())
|
||||
.build();
|
||||
|
||||
commentRepository.save(comment);
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
- 자식 댓글 저장
|
||||
*/
|
||||
@CacheEvict(value = "layoutRecentCommentCaching", allEntries = true)
|
||||
public void saveCComment(CommentForm commentForm, Member member, Long articleId, Long parentId) {
|
||||
|
||||
Article article = articleUseCase.getArticle(articleId);
|
||||
Comment pComment = commentRepository.findById(parentId).get();
|
||||
|
||||
Comment comment = Comment.builder()
|
||||
.article(article)
|
||||
.content(removeDuplicatedEnter(commentForm))
|
||||
.tier(1)
|
||||
.pOrder(pComment.getPOrder())
|
||||
.member(member)
|
||||
.parents(pComment)
|
||||
.secret(commentForm.isSecret())
|
||||
.build();
|
||||
|
||||
commentRepository.save(comment);
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
- 댓글 삭제
|
||||
*/
|
||||
@CacheEvict(value = "layoutRecentCommentCaching", allEntries = true)
|
||||
public void deleteComment(Long commentId){
|
||||
naCommentRepository.deleteComment(commentId);
|
||||
}
|
||||
|
||||
/*
|
||||
- 최신 댓글 5개 가져오기
|
||||
- 레이아웃 렌더링 성능 향상을 위해 캐싱작업
|
||||
카테고리 변경 / 아티클 변경이 존재할경우 레이아웃 캐시 초기화
|
||||
DTO 매핑 로직 서비스단에서 처리
|
||||
*/
|
||||
@Cacheable(value = "layoutRecentCommentCaching", key = "0")
|
||||
public List<CommentDtoForLayout> recentCommentList(){
|
||||
return commentRepository.findTop5ByOrderByIdDesc()
|
||||
.stream()
|
||||
.map(comment ->
|
||||
new CommentDtoForLayout(comment.getId(), comment.getArticle().getId(), comment.getContent(), comment.isSecret()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/*
|
||||
- 중복 개행 개행 하나로 압축 알고리즘
|
||||
*/
|
||||
private String removeDuplicatedEnter(CommentForm commentForm) {
|
||||
|
||||
char[] contentBox = new char[commentForm.getContent().length()];
|
||||
int idx = 0;
|
||||
String zipWord = "\n\n";
|
||||
|
||||
for(int i = 0; i< commentForm.getContent().length(); i++){
|
||||
|
||||
contentBox[idx] = commentForm.getContent().charAt(i);
|
||||
|
||||
if(contentBox[idx] == '\n'&&idx >= 1){
|
||||
|
||||
int tempIdx = idx;
|
||||
int length = 1;
|
||||
boolean isRemove = true;
|
||||
|
||||
for(int j = 0; j<2; j++){
|
||||
|
||||
if(contentBox[tempIdx--] != zipWord.charAt(length--)){
|
||||
|
||||
isRemove = false;
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
if(isRemove) idx -= 1;
|
||||
}
|
||||
idx++;
|
||||
}
|
||||
return String.valueOf(contentBox).trim();
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package myblog.blog.img.controller;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import myblog.blog.img.dto.UploadImgDto;
|
||||
import myblog.blog.img.service.UploadImgService;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
public class UploadImgController {
|
||||
|
||||
private final UploadImgService uploadImgService;
|
||||
|
||||
/*
|
||||
- 썸네일 업로드 요청
|
||||
*/
|
||||
@PostMapping("/article/uploadImg")
|
||||
public @ResponseBody
|
||||
String imgUpload(@ModelAttribute UploadImgDto uploadImgDto) throws IOException {
|
||||
return uploadImgService.storeImg(uploadImgDto.getImg());
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
package myblog.blog.img.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.kohsuke.github.GHRepository;
|
||||
import org.kohsuke.github.GitHub;
|
||||
import org.kohsuke.github.GitHubBuilder;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
@RequiredArgsConstructor
|
||||
public class UploadImgService {
|
||||
|
||||
/*
|
||||
- 설정 파일로 잡아놓은 깃헙 이미지 레포지토리와 토큰
|
||||
*/
|
||||
@Value("${git.gitToken}")
|
||||
private String gitToken;
|
||||
|
||||
@Value("${git.imgRepo}")
|
||||
private String gitRepo;
|
||||
|
||||
@Value("${git.imgUrl}")
|
||||
private String imgUrl;
|
||||
|
||||
/*
|
||||
- 이미지 저장 로직
|
||||
1. 깃허브 Repo에 이미지 업로드
|
||||
2. 업로드된 Url 반환
|
||||
*/
|
||||
public String storeImg(MultipartFile multipartFile) throws IOException {
|
||||
if (multipartFile.isEmpty()) {
|
||||
throw new IllegalArgumentException("이미지가 존재하지 않습니다.");
|
||||
}
|
||||
|
||||
String storeFileName = createStoreFileName(multipartFile.getOriginalFilename());
|
||||
|
||||
GitHub gitHub = new GitHubBuilder().withOAuthToken(gitToken).build();
|
||||
GHRepository repository = gitHub.getRepository(gitRepo);
|
||||
repository.createContent().path("img/"+storeFileName)
|
||||
.content(multipartFile.getBytes()).message("thumbnail").branch("main").commit();
|
||||
|
||||
return imgUrl + storeFileName + "?raw=true";
|
||||
}
|
||||
|
||||
/*
|
||||
- 이미지 중복 방지용 무작위 파일 이름 생성기
|
||||
*/
|
||||
private String createStoreFileName(String originalFilename) {
|
||||
String ext = extractExt(originalFilename);
|
||||
String uuid = UUID.randomUUID().toString();
|
||||
return uuid + "." + ext;
|
||||
}
|
||||
|
||||
/*
|
||||
- 파일 이름 추출
|
||||
*/
|
||||
private String extractExt(String originalFilename) {
|
||||
int pos = originalFilename.lastIndexOf(".");
|
||||
return originalFilename.substring(pos + 1);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package myblog.blog.imgupload.adapter.incomming;
|
||||
|
||||
import myblog.blog.imgupload.application.port.incomming.ImgUploadUseCase;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
public class UploadImgController {
|
||||
|
||||
private final ImgUploadUseCase imgUploadUseCase;
|
||||
|
||||
@PostMapping("/article/uploadImg")
|
||||
String imgUpload(@ModelAttribute UploadImgForm uploadImgForm){
|
||||
return imgUploadUseCase.storeImg(uploadImgForm.getImg());
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,11 @@
|
||||
package myblog.blog.img.dto;
|
||||
package myblog.blog.imgupload.adapter.incomming;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
/*
|
||||
- 멀티파트 파일 래핑용 DTO
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
public class UploadImgDto {
|
||||
public class UploadImgForm {
|
||||
private MultipartFile img;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package myblog.blog.imgupload.adapter.outgoing;
|
||||
|
||||
import com.amazonaws.services.s3.AmazonS3;
|
||||
import com.amazonaws.services.s3.model.*;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import myblog.blog.imgupload.domain.ImageFile;
|
||||
import myblog.blog.imgupload.application.port.outgoing.ImgUploadStrategyPort;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* S3 이미지 업로드 전략 구현체
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class AwsS3ImgUploadStrategyAdapter implements ImgUploadStrategyPort {
|
||||
|
||||
private final AmazonS3 amazonS3;
|
||||
@Value("${cloud.aws.s3.bucket}")
|
||||
private String bucket;
|
||||
|
||||
public String uploadFile(ImageFile imageFile) {
|
||||
var file = imageFile.getMultipartFile();
|
||||
var metadata = createObjectMetadata(file);
|
||||
try(var inputStream = file.getInputStream()) {
|
||||
amazonS3.putObject(new PutObjectRequest(bucket, imageFile.getStoredFileName(), inputStream, metadata)
|
||||
.withCannedAcl(CannedAccessControlList.PublicRead));
|
||||
} catch (IOException e) {
|
||||
throw new IllegalArgumentException("파일 업로드에 실패했습니다.");
|
||||
}
|
||||
return amazonS3.getUrl(bucket, imageFile.getStoredFileName()).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* s3 api를 위한 dto 생성 메서드
|
||||
* @param file 업로드 요청된 이미지 파일
|
||||
* @return s3 api 요구 스펙 dto
|
||||
*/
|
||||
private ObjectMetadata createObjectMetadata(MultipartFile file) {
|
||||
var metadata = new ObjectMetadata();
|
||||
metadata.setContentLength(file.getSize());
|
||||
metadata.setContentType(file.getContentType());
|
||||
return metadata;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package myblog.blog.imgupload.adapter.outgoing;
|
||||
|
||||
import org.kohsuke.github.GHRepository;
|
||||
import org.kohsuke.github.GitHub;
|
||||
import org.kohsuke.github.GitHubBuilder;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 깃헙 레포 이미지 업로드 전략 구현체
|
||||
*/
|
||||
//@RequiredArgsConstructor
|
||||
//@Service
|
||||
@Deprecated
|
||||
public class GithubRepoImgUploadStrategyAdapter {
|
||||
|
||||
/*
|
||||
- 설정 파일로 잡아놓은 깃헙 이미지 레포지토리와 토큰
|
||||
*/
|
||||
@Value("${git.gitToken}")
|
||||
private String gitToken;
|
||||
|
||||
@Value("${git.imgRepo}")
|
||||
private String gitRepo;
|
||||
|
||||
@Value("${git.imgUrl}")
|
||||
private String rootUrl;
|
||||
|
||||
/*
|
||||
- 이미지 업로드 로직
|
||||
1. 깃허브 Repo에 이미지 업로드
|
||||
2. 업로드된 Url 반환
|
||||
*/
|
||||
public String uploadFile(MultipartFile multipartFile, String storeFileName) throws IOException {
|
||||
GitHub gitHub = new GitHubBuilder().withOAuthToken(gitToken).build();
|
||||
GHRepository repository = gitHub.getRepository(gitRepo);
|
||||
repository.createContent().path("img/"+storeFileName)
|
||||
.content(multipartFile.getBytes()).message("thumbnail").branch("main").commit();
|
||||
return rootUrl + storeFileName + "?raw=true";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package myblog.blog.imgupload.application;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import myblog.blog.imgupload.domain.ImageFile;
|
||||
import myblog.blog.imgupload.application.port.incomming.ImgUploadUseCase;
|
||||
import myblog.blog.imgupload.application.port.outgoing.ImgUploadStrategyPort;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
@RequiredArgsConstructor
|
||||
public class ImgUploadService implements ImgUploadUseCase {
|
||||
|
||||
private final ImgUploadStrategyPort imgUploadStrategyPort;
|
||||
|
||||
@Override
|
||||
public String storeImg(MultipartFile multipartFile) {
|
||||
validateFile(multipartFile);
|
||||
var imageFile = ImageFile.from(multipartFile);
|
||||
return imgUploadStrategyPort.uploadFile(imageFile);
|
||||
}
|
||||
|
||||
private void validateFile(MultipartFile multipartFile) {
|
||||
if (multipartFile.isEmpty()) {
|
||||
throw new IllegalArgumentException("이미지가 존재하지 않습니다.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package myblog.blog.imgupload.application.port.incomming;
|
||||
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
public interface ImgUploadUseCase {
|
||||
String storeImg(MultipartFile img);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package myblog.blog.imgupload.application.port.outgoing;
|
||||
|
||||
import myblog.blog.imgupload.domain.ImageFile;
|
||||
|
||||
/**
|
||||
* 파일 업로드 전략패턴 추상화 인터페이스
|
||||
*/
|
||||
public interface ImgUploadStrategyPort {
|
||||
String uploadFile(ImageFile imageFile);
|
||||
}
|
||||
38
src/main/java/myblog/blog/imgupload/domain/ImageFile.java
Normal file
38
src/main/java/myblog/blog/imgupload/domain/ImageFile.java
Normal file
@@ -0,0 +1,38 @@
|
||||
package myblog.blog.imgupload.domain;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public class ImageFile {
|
||||
String originalFileName;
|
||||
String storedFileName;
|
||||
MultipartFile multipartFile;
|
||||
|
||||
static public ImageFile from(MultipartFile multipartFile){
|
||||
return new ImageFile(multipartFile.getOriginalFilename(),
|
||||
createStoreFileName(multipartFile.getOriginalFilename()),
|
||||
multipartFile
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
- 이미지 중복 방지용 무작위 파일 이름 생성기
|
||||
*/
|
||||
private static String createStoreFileName(String originalFilename) {
|
||||
var ext = extractExt(originalFilename);
|
||||
var uuid = UUID.randomUUID().toString();
|
||||
return uuid + "." + ext;
|
||||
}
|
||||
/*
|
||||
- 파일 이름 추출
|
||||
*/
|
||||
private static String extractExt(String originalFilename) {
|
||||
var pos = originalFilename.lastIndexOf(".");
|
||||
return originalFilename.substring(pos + 1);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package myblog.blog.base;
|
||||
package myblog.blog.infra;
|
||||
|
||||
import org.hibernate.dialect.MySQL8Dialect;
|
||||
import org.hibernate.dialect.function.SQLFunctionTemplate;
|
||||
@@ -1,4 +1,4 @@
|
||||
package myblog.blog.main;
|
||||
package myblog.blog.infra;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.core.env.Environment;
|
||||
@@ -17,15 +17,11 @@ public class ProfileController {
|
||||
@GetMapping("/profile")
|
||||
public String profile(){
|
||||
List<String> profiles = Arrays.asList(env.getActiveProfiles());
|
||||
|
||||
List<String> realProfiles = Arrays.asList("real", "real1", "real2");
|
||||
|
||||
String defaultProfile = profiles.isEmpty()? "default" : profiles.get(0);
|
||||
|
||||
String defaultProfile = profiles.isEmpty() ? "default" : profiles.get(0);
|
||||
return profiles.stream()
|
||||
.filter(realProfiles::contains)
|
||||
.findAny()
|
||||
.orElse(defaultProfile);
|
||||
}
|
||||
|
||||
}
|
||||
33
src/main/java/myblog/blog/infra/config/AppConfig.java
Normal file
33
src/main/java/myblog/blog/infra/config/AppConfig.java
Normal file
@@ -0,0 +1,33 @@
|
||||
package myblog.blog.infra.config;
|
||||
|
||||
import com.amazonaws.auth.AWSCredentials;
|
||||
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 AppConfig {
|
||||
|
||||
@Value("${cloud.aws.credentials.accessKey}")
|
||||
private String AWS_ACCESS_KEY_ID;
|
||||
@Value("${cloud.aws.credentials.secretKey}")
|
||||
private String AWS_SECRET_ACCESS_KEY;
|
||||
|
||||
/**
|
||||
* AWS S3 설정
|
||||
*/
|
||||
@Bean
|
||||
public AmazonS3 S3() {
|
||||
AWSCredentials awsCredentials =
|
||||
new BasicAWSCredentials(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY);
|
||||
return AmazonS3ClientBuilder
|
||||
.standard()
|
||||
.withCredentials(new AWSStaticCredentialsProvider(awsCredentials)).withRegion(Regions.AP_NORTHEAST_2)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user