diff --git a/.github/workflows/uckgisagi-deploy.yml b/.github/workflows/uckgisagi-deploy.yml new file mode 100644 index 00000000..038f524b --- /dev/null +++ b/.github/workflows/uckgisagi-deploy.yml @@ -0,0 +1,76 @@ +name: CI + +on: + workflow_dispatch: + push: + branches: [ "develop" ] + +env: + S3_BUCKET_NAME: uckgisagi-bucket + PROJECT_NAME: uckgisagi-server + RESOURCE_AWS_PATH: ./src/main/resources/application-aws.yml + RESOURCE_JWT_PATH: ./src/main/resources/application-jwt.yml + RESOURCE_PROD_PATH: ./src/main/resources/application-prod.yml + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Set up JDK 11 + uses: actions/setup-java@v1 + with: + java-version: '11' + + - name: Set PROD Yml + uses: microsoft/variable-substitution@v1 + with: + files: ${{ env.RESOURCE_PROD_PATH }} + env: + spring.datasource.url: ${{ secrets.RDS_URL }} + spring.datasource.username: ${{ secrets.RDS_USERNAME }} + spring.datasource.password: ${{ secrets.RDS_PASSWORD }} + + - name: Set AWS Yml + uses: microsoft/variable-substitution@v1 + with: + files: ${{ env.RESOURCE_AWS_PATH }} + env: + cloud.aws.credentials.accessKey: ${{ secrets.AWS_ACCESS_KEY_ID }} + cloud.aws.credentials.secretKey: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + + - name: Set JWT Yml + uses: microsoft/variable-substitution@v1 + with: + files: ${{ env.RESOURCE_JWT_PATH }} + env: + jwt.secret: ${{ secrets.JWT_SECRET }} + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + shell: bash + + - name: Build with Gradle + run: ./gradlew build -x test + shell: bash + + - name: Make zip file + run: zip -r ./$GITHUB_SHA.zip . + shell: bash + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ap-northeast-2 + + - name: Upload to S3 + run: aws s3 cp --region ap-northeast-2 ./$GITHUB_SHA.zip s3://$S3_BUCKET_NAME/$GITHUB_SHA.zip + + - name: Code Deploy + run: aws deploy create-deployment --application-name uckgisagi-code-deploy --deployment-config-name CodeDeployDefault.AllAtOnce --deployment-group-name uckgisagi-code-deploy-group --s3-location bucket=$S3_BUCKET_NAME,bundleType=zip,key=$GITHUB_SHA.zip diff --git a/.gitignore b/.gitignore index 1d83520e..8ec90e8f 100644 --- a/.gitignore +++ b/.gitignore @@ -36,9 +36,8 @@ out/ ### VS Code ### .vscode/ - -### Firebase Admin ### -src/main/resources/firebase/*.json - ### querydsl ### -src/querydsl \ No newline at end of file +src/querydsl + +### h2 database ### +database \ No newline at end of file diff --git a/README.md b/README.md index 8e6c1b8f..6d56274e 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,35 @@ # uckgisagi-server -๐ŸŒ์–ต์ง€๋กœ ์ง€๊ตฌ๋ฅผ ์‚ฌ๋ž‘ํ•˜๋Š” ์ง€๊ตฌ์ธ๋“ค๐ŸŒฑ +# ๐ŸŒ ์–ต์ง€๋กœ ์ง€๊ตฌ๋ฅผ ์‚ฌ๋ž‘ํ•˜๋Š” ์ง€๊ตฌ์ธ๋“ค ๐ŸŒ + + + +## ๊ฐœ์š” +2022ํ•™๋…„๋„ 2ํ•™๊ธฐ ์˜คํ”ˆ์†Œ์Šค ๊ธฐ๋ฐ˜ ๊ธฐ์ดˆ ์„ค๊ณ„ ํ•™๊ธฐ ํ”„๋กœ์ ํŠธ ๊ณผ์ œ +## ๊นƒ ์ปค๋ฐ‹ ์ปจ๋ฒค์…˜ +* `feat`: ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ ๊ฐœ๋ฐœ +* `fix`: ๋ฒ„๊ทธ ์ˆ˜์ • +* `add`: ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ ์ถ”๊ฐ€ +* `chore`: ๋นŒ๋“œ ์—…๋ฌด ์ˆ˜์ • (build.gradle ์ˆ˜์ •) +* `refactor`: ์ฝ”๋“œ ๋ฆฌํŒฉํ† ๋ง +* `style`: ์ฝ”๋“œ ํฌ๋งทํŒ…, ์ฃผ์„ ์‚ญ์ œ ๋“ฑ ์ฝ”๋“œ ๋ณ€๊ฒฝ์ด ์—†๋Š” ๊ฒฝ์šฐ +* `test`: ํ…Œ์ŠคํŠธ ์ฝ”๋“œ, ๋ฆฌํŒฉํ† ๋ง ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ๊ฐœ๋ฐœ ๋ฐ ์ถ”๊ฐ€ +## ์ฝ”๋“œ ์ปจ๋ฒค์…˜ + + +## ์‚ฌ์šฉํ•œ ์Šคํƒ +### ๋ฐฑ์—”๋“œ +* ์Šคํ”„๋ง ๋ถ€ํŠธ 2.7.4 +* ์–ธ์–ด: ์ž๋ฐ” +* JAVA 11, Gradle +* MySQL +* Spring JPA +* QueryDsl +* Spring Security +* Swagger + * http://54.180.212.221/api/swagger-ui/index.html +* Sl4j Logging +* Validation +* ๊ทธ ์™ธ ์Šคํƒ€ํ„ฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ + +### ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค diff --git a/appspec.yml b/appspec.yml new file mode 100644 index 00000000..9ba53b47 --- /dev/null +++ b/appspec.yml @@ -0,0 +1,24 @@ +version: 0.0 +os: linux +files: + - source: / + destination: /home/ec2-user/uckgisagi-server/ + overwrite: yes + +permissions: + - object: / + pattern: "**" + owner: ec2-user + group: ec2-user + +hooks: + ApplicationStart: + - location: scripts/run_new_was.sh + timeout: 180 + runas: ec2-user +# - location: scripts/health_check.sh +# timeout: 180 +# runas: ec2-user + - location: scripts/switch.sh + timeout: 180 + runas: ec2-user \ No newline at end of file diff --git a/build.gradle b/build.gradle index 8c9adb28..a24459b0 100644 --- a/build.gradle +++ b/build.gradle @@ -22,7 +22,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-validation' // aws -// implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' + implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' // jetbrains annotation compileOnly 'org.jetbrains:annotations:23.0.0' @@ -63,8 +63,7 @@ dependencies { implementation 'org.json:json:20220924' // swagger - implementation group: 'io.springfox', name: 'springfox-swagger-ui', version: '2.9.2' - implementation group: 'io.springfox', name: 'springfox-swagger2', version: '2.9.2' + implementation group: 'io.springfox', name: 'springfox-boot-starter', version: '3.0.0' } jar { diff --git a/scripts/health_check.sh b/scripts/health_check.sh new file mode 100644 index 00000000..2353935b --- /dev/null +++ b/scripts/health_check.sh @@ -0,0 +1,29 @@ +CURRENT_PORT=$(cat /home/ec2-user/service_url.inc | grep -Po '[0-9]+' | tail -1) +TARGET_PORT=0 + +# Toggle port Number +if [ ${CURRENT_PORT} -eq 8081 ]; then + TARGET_PORT=8082 +elif [ ${CURRENT_PORT} -eq 8082 ]; then + TARGET_PORT=8081 +else + echo "> No WAS is connected to nginx" + exit 1 +fi + +echo "> Start health check of WAS at 'http://127.0.0.1:${TARGET_PORT}' ..." + +for RETRY_COUNT in 1 2 3 4 5 6 7 8 9 10 +do + echo "> #${RETRY_COUNT} trying..." + RESPONSE_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:${TARGET_PORT}/api/health) + + if [ ${RESPONSE_CODE} -eq 200 ]; then + echo "> New WAS successfully running" + exit 0 + elif [ ${RETRY_COUNT} -eq 10 ]; then + echo "> Health check failed." + exit 1 + fi + sleep 10 +done \ No newline at end of file diff --git a/scripts/run_new_was.sh b/scripts/run_new_was.sh new file mode 100644 index 00000000..88b727b2 --- /dev/null +++ b/scripts/run_new_was.sh @@ -0,0 +1,23 @@ +CURRENT_PORT=$(cat /home/ec2-user/service_url.inc | grep -Po '[0-9]+' | tail -1) +TARGET_PORT=0 + +echo "> Current port of running WAS is ${CURRENT_PORT}." + +if [ ${CURRENT_PORT} -eq 8081 ]; then + TARGET_PORT=8082 +elif [ ${CURRENT_PORT} -eq 8082 ]; then + TARGET_PORT=8081 +else + echo "> No WAS is connected to nginx" +fi + +TARGET_PID=$(lsof -Fp -i TCP:${TARGET_PORT} | grep -Po 'p[0-9]+' | grep -Po '[0-9]+') + +if [ ! -z ${TARGET_PID} ]; then + echo "> Kill WAS running at ${TARGET_PORT}." + sudo kill ${TARGET_PID} +fi + +nohup java -jar -Dserver.port=${TARGET_PORT} -Dspring.profiles.active=prod /home/ec2-user/uckgisagi-server/build/libs/*.jar > /home/ec2-user/nohup.out 2>&1 & +echo "> Now new WAS runs at ${TARGET_PORT}." +exit 0 \ No newline at end of file diff --git a/scripts/switch.sh b/scripts/switch.sh new file mode 100644 index 00000000..d384cae9 --- /dev/null +++ b/scripts/switch.sh @@ -0,0 +1,23 @@ +CURRENT_PORT=$(cat /home/ec2-user/service_url.inc | grep -Po '[0-9]+' | tail -1) +TARGET_PORT=0 + +echo "> Nginx currently proxies to ${CURRENT_PORT}." + +# Toggle port number +if [ ${CURRENT_PORT} -eq 8081 ]; then + TARGET_PORT=8082 +elif [ ${CURRENT_PORT} -eq 8082 ]; then + TARGET_PORT=8081 +else + echo "> No WAS is connected to nginx" + exit 1 +fi + +# Change proxying port into target port +echo "set \$service_url http://127.0.0.1:${TARGET_PORT};" | tee /home/ec2-user/service_url.inc + +echo "> Now Nginx proxies to ${TARGET_PORT}." +# Reload nginx +sudo service nginx reload + +echo "> Nginx reloaded." \ No newline at end of file diff --git a/src/main/java/server/uckgisagi/UckgisagiApplication.java b/src/main/java/server/uckgisagi/UckgisagiApplication.java index f6a38ec6..7ba66538 100644 --- a/src/main/java/server/uckgisagi/UckgisagiApplication.java +++ b/src/main/java/server/uckgisagi/UckgisagiApplication.java @@ -2,7 +2,13 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.scheduling.annotation.EnableScheduling; +import springfox.documentation.swagger2.annotations.EnableSwagger2; +@EnableJpaAuditing +@EnableFeignClients @SpringBootApplication public class UckgisagiApplication { diff --git a/src/main/java/server/uckgisagi/app/accusation/controller/AccusationController.java b/src/main/java/server/uckgisagi/app/accusation/controller/AccusationController.java new file mode 100644 index 00000000..8d65f996 --- /dev/null +++ b/src/main/java/server/uckgisagi/app/accusation/controller/AccusationController.java @@ -0,0 +1,30 @@ +package server.uckgisagi.app.accusation.controller; + +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import server.uckgisagi.app.accusation.dto.AccusationPostReqDto; +import server.uckgisagi.app.accusation.dto.AccusationPostResDto; +import server.uckgisagi.app.accusation.service.AccusationService; +import server.uckgisagi.common.dto.ApiSuccessResponse; +import server.uckgisagi.config.interceptor.Auth; +import server.uckgisagi.config.resolver.LoginUserId; +import springfox.documentation.annotations.ApiIgnore; + +import static server.uckgisagi.common.success.SuccessResponseResult.CREATED_ACCUSE_POST; + +@RestController +@RequiredArgsConstructor +public class AccusationController { + + private final AccusationService accusationService; + + @ApiOperation("[์ธ์ฆ] ๋‘˜๋Ÿฌ๋ณด๊ธฐ ํŽ˜์ด์ง€ - ๊ฒŒ์‹œ๊ธ€ ์‹ ๊ณ ํ•˜๊ธฐ") + @Auth + @PostMapping("/v1/post/accuse") + public ApiSuccessResponse accusePost(@RequestBody AccusationPostReqDto accusationPostReqDto, @ApiIgnore @LoginUserId Long userId) { + return ApiSuccessResponse.success(CREATED_ACCUSE_POST, accusationService.accusePost(accusationPostReqDto, userId)); + } +} diff --git a/src/main/java/server/uckgisagi/app/accusation/domain/entity/Accusation.java b/src/main/java/server/uckgisagi/app/accusation/domain/entity/Accusation.java new file mode 100644 index 00000000..5f0962a9 --- /dev/null +++ b/src/main/java/server/uckgisagi/app/accusation/domain/entity/Accusation.java @@ -0,0 +1,45 @@ +package server.uckgisagi.app.accusation.domain.entity; + +import lombok.*; +import server.uckgisagi.app.accusation.dto.AccusationPostResDto; +import server.uckgisagi.common.domain.AuditingTimeEntity; +import server.uckgisagi.app.post.domain.entity.Post; +import server.uckgisagi.app.user.domain.entity.User; + +import javax.persistence.*; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Accusation extends AuditingTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "accusation_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id") + private Post post; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name="user_id") + private User user; + + + private Accusation(final Post post, final User user) { + this.post = post; + this.user = user; + } + + public static Accusation newInstance(Post post, User user) { + return new Accusation(post, user); + } + + public AccusationPostResDto toAccusePostResponseDto(Accusation accusation){ + return AccusationPostResDto.builder() + .postId(accusation.getPost().getId()) + .userId(accusation.getUser().getId()) + .build(); + } +} diff --git a/src/main/java/server/uckgisagi/app/accusation/domain/repository/AccusationCustomRepository.java b/src/main/java/server/uckgisagi/app/accusation/domain/repository/AccusationCustomRepository.java new file mode 100644 index 00000000..14e38d3f --- /dev/null +++ b/src/main/java/server/uckgisagi/app/accusation/domain/repository/AccusationCustomRepository.java @@ -0,0 +1,13 @@ +package server.uckgisagi.app.accusation.domain.repository; + +import server.uckgisagi.app.accusation.domain.entity.Accusation; + +import java.util.List; +import java.util.Optional; + +public interface AccusationCustomRepository{ + + List findAllByPostId(Long postId); + + Optional findByUserIdAndPostId(Long userId, Long postId); +} diff --git a/src/main/java/server/uckgisagi/app/accusation/domain/repository/AccusationCustomRepositoryImpl.java b/src/main/java/server/uckgisagi/app/accusation/domain/repository/AccusationCustomRepositoryImpl.java new file mode 100644 index 00000000..05e655d0 --- /dev/null +++ b/src/main/java/server/uckgisagi/app/accusation/domain/repository/AccusationCustomRepositoryImpl.java @@ -0,0 +1,33 @@ +package server.uckgisagi.app.accusation.domain.repository; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import server.uckgisagi.app.accusation.domain.entity.Accusation; + +import java.util.List; +import java.util.Optional; + +import static server.uckgisagi.app.accusation.domain.entity.QAccusation.accusation; + + +@RequiredArgsConstructor +public class AccusationCustomRepositoryImpl implements AccusationCustomRepository { + + private final JPAQueryFactory query; + + + @Override + public List findAllByPostId(Long postId) { + return query.selectFrom(accusation) + .where(accusation.post.id.eq(postId)) + .fetch(); + } + + @Override + public Optional findByUserIdAndPostId(Long userId, Long postId) { + return query.selectFrom(accusation) + .where(accusation.user.id.eq(userId)) + .where(accusation.post.id.eq(postId)) + .stream().findFirst(); + } +} diff --git a/src/main/java/server/uckgisagi/app/accusation/domain/repository/AccusationRepository.java b/src/main/java/server/uckgisagi/app/accusation/domain/repository/AccusationRepository.java new file mode 100644 index 00000000..02dc9c08 --- /dev/null +++ b/src/main/java/server/uckgisagi/app/accusation/domain/repository/AccusationRepository.java @@ -0,0 +1,7 @@ +package server.uckgisagi.app.accusation.domain.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import server.uckgisagi.app.accusation.domain.entity.Accusation; + +public interface AccusationRepository extends JpaRepository, AccusationCustomRepository{ +} diff --git a/src/main/java/server/uckgisagi/app/accusation/dto/AccusationPostReqDto.java b/src/main/java/server/uckgisagi/app/accusation/dto/AccusationPostReqDto.java new file mode 100644 index 00000000..b3ac39e4 --- /dev/null +++ b/src/main/java/server/uckgisagi/app/accusation/dto/AccusationPostReqDto.java @@ -0,0 +1,22 @@ +package server.uckgisagi.app.accusation.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import server.uckgisagi.app.accusation.domain.entity.Accusation; +import server.uckgisagi.app.post.domain.entity.Post; +import server.uckgisagi.app.user.domain.entity.User; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AccusationPostReqDto { + + private Long postId; + + public Accusation toAccusationEntity(User user, Post post){ + return Accusation.newInstance(post, user); + } +} diff --git a/src/main/java/server/uckgisagi/app/accusation/dto/AccusationPostResDto.java b/src/main/java/server/uckgisagi/app/accusation/dto/AccusationPostResDto.java new file mode 100644 index 00000000..fa9e9edf --- /dev/null +++ b/src/main/java/server/uckgisagi/app/accusation/dto/AccusationPostResDto.java @@ -0,0 +1,17 @@ +package server.uckgisagi.app.accusation.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AccusationPostResDto { + + private Long postId; + + private Long userId; +} diff --git a/src/main/java/server/uckgisagi/app/accusation/service/AccusationService.java b/src/main/java/server/uckgisagi/app/accusation/service/AccusationService.java new file mode 100644 index 00000000..a43f45f3 --- /dev/null +++ b/src/main/java/server/uckgisagi/app/accusation/service/AccusationService.java @@ -0,0 +1,56 @@ +package server.uckgisagi.app.accusation.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import server.uckgisagi.app.accusation.dto.AccusationPostReqDto; +import server.uckgisagi.app.accusation.dto.AccusationPostResDto; +import server.uckgisagi.app.post.service.PostServiceUtils; +import server.uckgisagi.app.user.service.UserServiceUtils; +import server.uckgisagi.common.exception.custom.ConflictException; +import server.uckgisagi.app.accusation.domain.entity.Accusation; +import server.uckgisagi.app.accusation.domain.repository.AccusationRepository; +import server.uckgisagi.app.post.domain.entity.Post; +import server.uckgisagi.app.post.domain.entity.enumerate.PostStatus; +import server.uckgisagi.app.post.domain.repository.PostRepository; +import server.uckgisagi.app.user.domain.entity.User; +import server.uckgisagi.app.user.domain.repository.UserRepository; + +import java.util.List; +import java.util.Optional; + +import static server.uckgisagi.common.exception.ErrorResponseResult.CONFLICT_EXCEPTION; + +@Service +@RequiredArgsConstructor +public class AccusationService { + + private final AccusationRepository accusationRepository; + private final UserRepository userRepository; + private final PostRepository postRepository; + + + + @Transactional + public AccusationPostResDto accusePost(AccusationPostReqDto accusationPostReqDto, Long userId){ + + Optional repetition = accusationRepository.findByUserIdAndPostId(userId, accusationPostReqDto.getPostId()); + if(repetition.isPresent()){ + // ํ•ด๋‹น ์œ ์ €๊ฐ€ ์ด๋ฏธ ํ•ด๋‹น ๊ฒŒ์‹œ๋ฌผ์„ ์‹ ๊ณ ํ•œ ๋‚ด์—ญ์ด ์žˆ์„ ๊ฒฝ์šฐ, ๋ฆฌํ„ด bad Request + throw new ConflictException(String.format("์ด๋ฏธ ํ•ด๋‹น ๊ฒŒ์‹œ๊ธ€์— ๋Œ€ํ•œ ์‹ ๊ณ  ๋‚ด์—ญ์ด ์กด์žฌํ•ฉ๋‹ˆ๋‹ค."), CONFLICT_EXCEPTION); + } + + User user = UserServiceUtils.findByUserId(userRepository, userId); + Post post = PostServiceUtils.findByPostId(postRepository, accusationPostReqDto.getPostId()); + + Accusation accusation = accusationRepository.save(accusationPostReqDto.toAccusationEntity(user, post)); + + List accusations = accusationRepository.findAllByPostId(accusationPostReqDto.getPostId()); + if(accusations.size() >= 10){ + // ๊ฒŒ์‹œ๋ฌผ ์‹ ๊ณ ๊ฐ€ 10๋ฒˆ ์ด์ƒ์ผ ๊ฒฝ์šฐ ์•ˆ ๋ณด์ด๊ฒŒ ํ•˜๋Š” ๋กœ์ง + post.changeStatus(PostStatus.INACTIVE); + } + + return accusation.toAccusePostResponseDto(accusation); + } +} diff --git a/src/main/java/server/uckgisagi/app/auth/AuthController.java b/src/main/java/server/uckgisagi/app/auth/AuthController.java new file mode 100644 index 00000000..169f21f6 --- /dev/null +++ b/src/main/java/server/uckgisagi/app/auth/AuthController.java @@ -0,0 +1,43 @@ +package server.uckgisagi.app.auth; + +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import server.uckgisagi.app.auth.dto.request.LoginRequest; +import server.uckgisagi.app.auth.dto.request.TokenRequest; +import server.uckgisagi.app.auth.dto.response.LoginResponse; +import server.uckgisagi.app.auth.dto.response.TokenResponse; +import server.uckgisagi.app.auth.provider.AuthServiceProvider; +import server.uckgisagi.app.auth.service.AuthService; +import server.uckgisagi.app.auth.service.CreateTokenService; +import server.uckgisagi.common.dto.ApiSuccessResponse; +import server.uckgisagi.app.user.domain.entity.User; + +import javax.validation.Valid; + +import static server.uckgisagi.common.success.SuccessResponseResult.CREATED_REISSUE_TOKEN; + +@RestController +@RequiredArgsConstructor +public class AuthController { + + private final AuthServiceProvider authServiceProvider; + private final CreateTokenService createTokenService; + + @ApiOperation("ํšŒ์›๊ฐ€์ž… ๋ฐ ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ - ํšŒ์›๊ฐ€์ž… ๋ฐ ๋กœ๊ทธ์ธ") + @PostMapping("/v1/auth/login") + public ApiSuccessResponse login(@Valid @RequestBody LoginRequest request) { + AuthService authService = authServiceProvider.getAuthService(request.getSocialType()); + User user = authService.login(request.toServiceDto()); + TokenResponse tokenInfo = createTokenService.createTokenInfo(user.getId()); + return ApiSuccessResponse.success(LoginResponse.of(user, tokenInfo)); + } + + @ApiOperation("ํ† ํฐ ๋งŒ๋ฃŒ ์‹œ ์—‘์„ธ์Šค ํ† ํฐ ์žฌ๋ฐœ๊ธ‰ ์š”์ฒญ") + @PostMapping("/v1/auth/reissue") + public ApiSuccessResponse reissueToken(@Valid @RequestBody TokenRequest request) { + return ApiSuccessResponse.success(CREATED_REISSUE_TOKEN, createTokenService.reissueToken(request)); + } +} diff --git a/src/main/java/server/uckgisagi/app/auth/dto/request/LoginDto.java b/src/main/java/server/uckgisagi/app/auth/dto/request/LoginDto.java new file mode 100644 index 00000000..25cb6481 --- /dev/null +++ b/src/main/java/server/uckgisagi/app/auth/dto/request/LoginDto.java @@ -0,0 +1,24 @@ +package server.uckgisagi.app.auth.dto.request; + +import lombok.*; +import server.uckgisagi.app.user.dto.request.CreateUserDto; +import server.uckgisagi.app.user.domain.entity.enumerate.SocialType; + +@ToString +@Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class LoginDto { + + private String socialAccessToken; + private SocialType socialType; + private String fcmToken; + + public static LoginDto of(String socialAccessToken, SocialType socialType, String fcmToken) { + return new LoginDto(socialAccessToken, socialType, fcmToken); + } + + public CreateUserDto toCreateUserDto(String socialId, String nickname, String fcmToken) { + return CreateUserDto.of(nickname, socialId, socialType, fcmToken); + } +} diff --git a/src/main/java/server/uckgisagi/app/auth/dto/request/LoginRequest.java b/src/main/java/server/uckgisagi/app/auth/dto/request/LoginRequest.java new file mode 100644 index 00000000..d4e0d705 --- /dev/null +++ b/src/main/java/server/uckgisagi/app/auth/dto/request/LoginRequest.java @@ -0,0 +1,27 @@ +package server.uckgisagi.app.auth.dto.request; + +import lombok.*; +import server.uckgisagi.app.user.domain.entity.enumerate.SocialType; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +@ToString +@Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class LoginRequest { + + @NotBlank(message = "{auth.accessToken.notBlank}") + private String socialToken; + + @NotNull(message = "{auth.socialType.notNull}") + private SocialType socialType; + + @NotBlank(message = "{auth.fcmToken.notBlank}") + private String fcmToken; + + public LoginDto toServiceDto() { + return LoginDto.of(socialToken, socialType, fcmToken); + } +} diff --git a/src/main/java/server/uckgisagi/app/auth/dto/request/TokenRequest.java b/src/main/java/server/uckgisagi/app/auth/dto/request/TokenRequest.java new file mode 100644 index 00000000..3cb07b82 --- /dev/null +++ b/src/main/java/server/uckgisagi/app/auth/dto/request/TokenRequest.java @@ -0,0 +1,19 @@ +package server.uckgisagi.app.auth.dto.request; + +import lombok.*; + +import javax.validation.constraints.NotBlank; + +@ToString +@Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class TokenRequest { + + @NotBlank(message = "${auth.accessToken.notBlank}") + private String accessToken; + + @NotBlank(message = "${auth.refreshToken.notBlank}") + private String refreshToken; + +} diff --git a/src/main/java/server/uckgisagi/app/auth/dto/response/LoginResponse.java b/src/main/java/server/uckgisagi/app/auth/dto/response/LoginResponse.java new file mode 100644 index 00000000..f3a7c25c --- /dev/null +++ b/src/main/java/server/uckgisagi/app/auth/dto/response/LoginResponse.java @@ -0,0 +1,21 @@ +package server.uckgisagi.app.auth.dto.response; + +import lombok.*; +import server.uckgisagi.app.user.domain.entity.User; + +@ToString +@Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class LoginResponse { + + private Long userId; + private String nickname; + private String accessToken; + private String refreshToken; + + public static LoginResponse of(User user, TokenResponse token) { + return new LoginResponse(user.getId(), user.getNickname(), token.getAccessToken(), token.getRefreshToken()); + } + +} diff --git a/src/main/java/server/uckgisagi/app/auth/dto/response/TokenResponse.java b/src/main/java/server/uckgisagi/app/auth/dto/response/TokenResponse.java new file mode 100644 index 00000000..d597f0ec --- /dev/null +++ b/src/main/java/server/uckgisagi/app/auth/dto/response/TokenResponse.java @@ -0,0 +1,18 @@ +package server.uckgisagi.app.auth.dto.response; + +import lombok.*; + +@ToString +@Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class TokenResponse { + + private String accessToken; + private String refreshToken; + + public static TokenResponse of(String accessToken, String refreshToken) { + return new TokenResponse(accessToken, refreshToken); + } + +} diff --git a/src/main/java/server/uckgisagi/app/auth/provider/AuthServiceProvider.java b/src/main/java/server/uckgisagi/app/auth/provider/AuthServiceProvider.java new file mode 100644 index 00000000..0b669fbf --- /dev/null +++ b/src/main/java/server/uckgisagi/app/auth/provider/AuthServiceProvider.java @@ -0,0 +1,29 @@ +package server.uckgisagi.app.auth.provider; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import server.uckgisagi.app.auth.service.AuthService; +import server.uckgisagi.app.auth.service.impl.AppleAuthService; +import server.uckgisagi.app.user.domain.entity.enumerate.SocialType; + +import javax.annotation.PostConstruct; +import java.util.HashMap; +import java.util.Map; + +@Component +@RequiredArgsConstructor +public class AuthServiceProvider { + + private static final Map authServiceMap = new HashMap<>(); + + private final AppleAuthService appleAuthService; + + @PostConstruct // ์˜์กด์„ฑ ์ฃผ์ž…์ด ์™„๋ฃŒ๋œ ํ›„ ์ดˆ๊ธฐํ™” + public void initAuthServiceMap() { + authServiceMap.put(SocialType.APPLE, appleAuthService); + } + + public AuthService getAuthService(SocialType socialType) { + return authServiceMap.get(socialType); + } +} diff --git a/src/main/java/server/uckgisagi/app/auth/service/AuthService.java b/src/main/java/server/uckgisagi/app/auth/service/AuthService.java new file mode 100644 index 00000000..c3a7f724 --- /dev/null +++ b/src/main/java/server/uckgisagi/app/auth/service/AuthService.java @@ -0,0 +1,8 @@ +package server.uckgisagi.app.auth.service; + +import server.uckgisagi.app.auth.dto.request.LoginDto; +import server.uckgisagi.app.user.domain.entity.User; + +public interface AuthService { + User login(LoginDto request); +} diff --git a/src/main/java/server/uckgisagi/app/auth/service/CreateTokenService.java b/src/main/java/server/uckgisagi/app/auth/service/CreateTokenService.java new file mode 100644 index 00000000..3c455b8b --- /dev/null +++ b/src/main/java/server/uckgisagi/app/auth/service/CreateTokenService.java @@ -0,0 +1,31 @@ +package server.uckgisagi.app.auth.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import server.uckgisagi.app.auth.dto.request.TokenRequest; +import server.uckgisagi.app.auth.dto.response.TokenResponse; +import server.uckgisagi.common.exception.custom.UnAuthorizedException; +import server.uckgisagi.common.util.JwtUtils; + +@Service +@RequiredArgsConstructor +public class CreateTokenService { + + private final JwtUtils jwtProvider; + + @Transactional + public TokenResponse createTokenInfo(Long userId) { + return jwtProvider.createTokenByUserId(userId); + } + + @Transactional + public TokenResponse reissueToken(TokenRequest request) { + if (!jwtProvider.validateToken(request.getRefreshToken())) { + throw new UnAuthorizedException(String.format("๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ (%s) ์ด ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค", request.getRefreshToken())); + } + Long userId = jwtProvider.getUserIdFromJwt(request.getAccessToken()); + return jwtProvider.createTokenByUserId(userId); + } + +} diff --git a/src/main/java/server/uckgisagi/app/auth/service/impl/AppleAuthService.java b/src/main/java/server/uckgisagi/app/auth/service/impl/AppleAuthService.java new file mode 100644 index 00000000..bf5e1f59 --- /dev/null +++ b/src/main/java/server/uckgisagi/app/auth/service/impl/AppleAuthService.java @@ -0,0 +1,34 @@ +package server.uckgisagi.app.auth.service.impl; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import server.uckgisagi.app.auth.dto.request.LoginDto; +import server.uckgisagi.app.auth.service.AuthService; +import server.uckgisagi.app.user.service.UserService; +import server.uckgisagi.app.user.service.UserServiceUtils; +import server.uckgisagi.common.util.RandomNicknameUtils; +import server.uckgisagi.app.user.domain.entity.User; +import server.uckgisagi.app.user.domain.entity.enumerate.SocialType; +import server.uckgisagi.app.user.domain.repository.UserRepository; +import server.uckgisagi.external.client.apple.AppleTokenDecoder; + +@Service +@RequiredArgsConstructor +public class AppleAuthService implements AuthService { + + private static final SocialType SOCIAL_TYPE = SocialType.APPLE; + + private final AppleTokenDecoder appleTokenDecoder; + private final UserService userService; + private final UserRepository userRepository; + + @Override + public User login(LoginDto request) { + String socialId = appleTokenDecoder.getSocialIdFromIdToken(request.getSocialAccessToken()); + User user = UserServiceUtils.findUserBySocialIdAndSocialType(userRepository, socialId, SOCIAL_TYPE); + if (user == null) { + return userService.registerUser(request.toCreateUserDto(socialId, RandomNicknameUtils.generate(), request.getFcmToken())); + } + return user; + } +} diff --git a/src/main/java/server/uckgisagi/app/block/controller/BlockController.java b/src/main/java/server/uckgisagi/app/block/controller/BlockController.java new file mode 100644 index 00000000..046935da --- /dev/null +++ b/src/main/java/server/uckgisagi/app/block/controller/BlockController.java @@ -0,0 +1,48 @@ +package server.uckgisagi.app.block.controller; + +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import server.uckgisagi.app.block.dto.BlockUserDto; +import server.uckgisagi.app.block.service.BlockService; +import server.uckgisagi.app.post.dto.response.PreviewPostResponse; +import server.uckgisagi.common.dto.ApiSuccessResponse; +import server.uckgisagi.common.success.SuccessResponseResult; +import server.uckgisagi.config.interceptor.Auth; +import server.uckgisagi.config.resolver.LoginUserId; +import springfox.documentation.annotations.ApiIgnore; + +import java.util.List; + +import static server.uckgisagi.common.success.SuccessResponseResult.*; + +@RestController +@RequiredArgsConstructor +public class BlockController { + + private final BlockService blockService; + + @ApiOperation("[์ธ์ฆ] ์œ ์ € ์ฐจ๋‹จ ํŽ˜์ด์ง€ - ์œ ์ € ์ฐจ๋‹จํ•˜๊ธฐ") + @Auth + @PostMapping("/v1/block") + public ApiSuccessResponse blockUser(@RequestBody BlockUserDto blockUserRequestDto, @ApiIgnore @LoginUserId Long userId) { + blockService.blockUser(blockUserRequestDto, userId); + return ApiSuccessResponse.success(NO_CONTENT_BLOCK_USER); + } + + @ApiOperation("[์ธ์ฆ] ์œ ์ € ์ฐจ๋‹จ ๋ชฉ๋ก ํŽ˜์ด์ง€ - ์ฐจ๋‹จํ•œ ์œ ์ € ๋ชฉ๋ก ์กฐํšŒํ•˜๊ธฐ") + @Auth + @GetMapping("/v1/block/retrieve") + public ApiSuccessResponse> retrieveAllBlockedUser(@ApiIgnore @LoginUserId Long userId) { + return ApiSuccessResponse.success(OK_SEARCH_ALL_POST, blockService.retrieveAllBlockedUser(userId)); + } + + @ApiOperation("[์ธ์ฆ] ์œ ์ € ์ฐจ๋‹จ ํ•ด์ œ ํŽ˜์ด์ง€ - ์œ ์ € ์ฐจ๋‹จ ํ•ด์ œํ•˜๊ธฐ") + @Auth + @DeleteMapping("/v1/block/delete") + public ApiSuccessResponse deleteBlockUser(@RequestParam Long blockUserId, @ApiIgnore @LoginUserId Long userId) { + blockService.deleteBlockUser(blockUserId, userId); + return ApiSuccessResponse.success(NO_CONTENT_CANCEL_BLOCK_USER); + } + +} diff --git a/src/main/java/server/uckgisagi/app/block/domain/entity/Block.java b/src/main/java/server/uckgisagi/app/block/domain/entity/Block.java new file mode 100644 index 00000000..d886dc2d --- /dev/null +++ b/src/main/java/server/uckgisagi/app/block/domain/entity/Block.java @@ -0,0 +1,32 @@ +package server.uckgisagi.app.block.domain.entity; + +import lombok.*; +import server.uckgisagi.app.user.domain.entity.User; + +import javax.persistence.*; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Block { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "block_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name="user_id") + private User user; + + private Long blockUserId; + + private Block(final User user, final Long blockUserId){ + this.user = user; + this.blockUserId = blockUserId; + } + + public static Block newInstance(User user, Long blockUserId){ + return new Block(user, blockUserId); + } +} diff --git a/src/main/java/server/uckgisagi/app/block/domain/repository/BlockRepository.java b/src/main/java/server/uckgisagi/app/block/domain/repository/BlockRepository.java new file mode 100644 index 00000000..aa44b53f --- /dev/null +++ b/src/main/java/server/uckgisagi/app/block/domain/repository/BlockRepository.java @@ -0,0 +1,9 @@ +package server.uckgisagi.app.block.domain.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import server.uckgisagi.app.block.domain.entity.Block; + +@Repository +public interface BlockRepository extends JpaRepository, BlockRepositoryCustom { +} diff --git a/src/main/java/server/uckgisagi/app/block/domain/repository/BlockRepositoryCustom.java b/src/main/java/server/uckgisagi/app/block/domain/repository/BlockRepositoryCustom.java new file mode 100644 index 00000000..2efbb58c --- /dev/null +++ b/src/main/java/server/uckgisagi/app/block/domain/repository/BlockRepositoryCustom.java @@ -0,0 +1,7 @@ +package server.uckgisagi.app.block.domain.repository; + +import server.uckgisagi.app.block.domain.entity.Block; + +public interface BlockRepositoryCustom { + Block findByBlockUserId(Long blockUserId, Long userId); +} diff --git a/src/main/java/server/uckgisagi/app/block/domain/repository/BlockRepositoryCustomImpl.java b/src/main/java/server/uckgisagi/app/block/domain/repository/BlockRepositoryCustomImpl.java new file mode 100644 index 00000000..e193219e --- /dev/null +++ b/src/main/java/server/uckgisagi/app/block/domain/repository/BlockRepositoryCustomImpl.java @@ -0,0 +1,20 @@ +package server.uckgisagi.app.block.domain.repository; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import server.uckgisagi.app.block.domain.entity.Block; +import static server.uckgisagi.app.block.domain.entity.QBlock.block; + +@RequiredArgsConstructor +public class BlockRepositoryCustomImpl implements BlockRepositoryCustom{ + + private final JPAQueryFactory query; + + @Override + public Block findByBlockUserId(Long blockUserId, Long userId) { + return query.selectFrom(block) + .where(block.user.id.eq(userId)) + .where(block.blockUserId.eq(blockUserId)) + .fetchOne(); + } +} diff --git a/src/main/java/server/uckgisagi/app/block/dto/BlockUserDto.java b/src/main/java/server/uckgisagi/app/block/dto/BlockUserDto.java new file mode 100644 index 00000000..be2bc0e0 --- /dev/null +++ b/src/main/java/server/uckgisagi/app/block/dto/BlockUserDto.java @@ -0,0 +1,22 @@ +package server.uckgisagi.app.block.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class BlockUserDto { + + private Long blockUserId; + + public static BlockUserDto of(Long blockUserId) { + return BlockUserDto.builder() + .blockUserId(blockUserId) + .build(); + } +} + diff --git a/src/main/java/server/uckgisagi/app/block/service/BlockService.java b/src/main/java/server/uckgisagi/app/block/service/BlockService.java new file mode 100644 index 00000000..a6e33e6d --- /dev/null +++ b/src/main/java/server/uckgisagi/app/block/service/BlockService.java @@ -0,0 +1,56 @@ +package server.uckgisagi.app.block.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import server.uckgisagi.app.block.dto.BlockUserDto; +import server.uckgisagi.app.follow.service.FollowService; +import server.uckgisagi.app.user.service.UserServiceUtils; +import server.uckgisagi.app.block.domain.entity.Block; +import server.uckgisagi.app.block.domain.repository.BlockRepository; +import server.uckgisagi.app.user.domain.entity.User; +import server.uckgisagi.app.user.domain.repository.UserRepository; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class BlockService { + + private final UserRepository userRepository; + private final FollowService followService; + private final BlockRepository blockRepository; + + @Transactional + public void blockUser(BlockUserDto blockUserDto, Long userId) { + // ์ผ๋‹จ ์ฐจ๋‹จํ•˜๊ณ ์ž ํ•˜๋Š” ์œ ์ € unfollow + User user = userRepository.findUserByUserId(userId); // ์ฐจ๋‹จ ๋ฒ„ํŠผ ๋ˆ„๋ฅด๋Š” ์œ ์ € + User blockUser = userRepository.findUserByUserId(blockUserDto.getBlockUserId()); // ์ฐจ๋‹จ ๋‹นํ•˜๋Š” ์œ ์ € + followService.unfollowUser(blockUser.getId(), userId); + + // Block ํ…Œ์ด๋ธ”์— ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€ + blockRepository.save(Block.newInstance(user, blockUser.getId())); + } + + @Transactional + public List retrieveAllBlockedUser(Long userId){ + User me = UserServiceUtils.findByUserId(userRepository, userId); + List blockUserIds = me.getBlocks().stream() + .map(Block::getBlockUserId) + .collect(Collectors.toList()); + + return blockUserIds.stream() + .map(id -> BlockUserDto.of(id)) + .collect(Collectors.toList()); + } + + @Transactional + public void deleteBlockUser(Long blockUserId, Long userId) { + User blockUser = userRepository.findUserByUserId(blockUserId); + Block block = blockRepository.findByBlockUserId(blockUser.getId(), userId); + + blockRepository.delete(block); + } + +} diff --git a/src/main/java/server/uckgisagi/app/follow/FollowController.java b/src/main/java/server/uckgisagi/app/follow/FollowController.java new file mode 100644 index 00000000..3367cdc2 --- /dev/null +++ b/src/main/java/server/uckgisagi/app/follow/FollowController.java @@ -0,0 +1,41 @@ +package server.uckgisagi.app.follow; + +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import server.uckgisagi.app.follow.service.FollowService; +import server.uckgisagi.app.notification.provider.NotificationServiceProvider; +import server.uckgisagi.app.notification.service.NotificationService; +import server.uckgisagi.common.dto.ApiSuccessResponse; +import server.uckgisagi.common.success.SuccessResponseResult; +import server.uckgisagi.config.interceptor.Auth; +import server.uckgisagi.config.resolver.LoginUserId; +import server.uckgisagi.app.notification.domain.entity.enumerate.NotificationType; +import springfox.documentation.annotations.ApiIgnore; + +import static server.uckgisagi.common.success.SuccessResponseResult.*; + +@RestController +@RequiredArgsConstructor +public class FollowController { + + private final FollowService followService; + private final NotificationServiceProvider notificationServiceProvider; + + @ApiOperation("[์ธ์ฆ] ์œ ์ € ๊ฒ€์ƒ‰ ํŽ˜์ด์ง€ - ํŒ”๋กœ์šฐ ์‹ ์ฒญํ•˜๊ธฐ") + @Auth + @PostMapping("/v1/follow/{targetUserId}") + public ApiSuccessResponse followUser(@PathVariable Long targetUserId, @ApiIgnore @LoginUserId Long userId) { + NotificationService notificationService = notificationServiceProvider.getNotificationService(NotificationType.FOLLOW); + notificationService.sendNotification(userId, targetUserId, followService.followUser(targetUserId, userId)); + return ApiSuccessResponse.success(CREATED_NOTIFICATION); + } + + @ApiOperation("[์ธ์ฆ] ์œ ์ € ๊ฒ€์ƒ‰ ํŽ˜์ด์ง€ - ์–ธํŒ”๋กœ์šฐ ํ•˜๊ธฐ") + @Auth + @DeleteMapping("/v1/unfollow/{targetUserId}") + public ApiSuccessResponse unfollowUser(@PathVariable Long targetUserId, @ApiIgnore @LoginUserId Long userId) { + followService.unfollowUser(targetUserId, userId); + return ApiSuccessResponse.success(NO_CONTENT_UNFOLLOW_USER); + } +} diff --git a/src/main/java/server/uckgisagi/domain/follow/entity/Follow.java b/src/main/java/server/uckgisagi/app/follow/domain/entity/Follow.java similarity index 77% rename from src/main/java/server/uckgisagi/domain/follow/entity/Follow.java rename to src/main/java/server/uckgisagi/app/follow/domain/entity/Follow.java index 84942166..ab32faab 100644 --- a/src/main/java/server/uckgisagi/domain/follow/entity/Follow.java +++ b/src/main/java/server/uckgisagi/app/follow/domain/entity/Follow.java @@ -1,10 +1,10 @@ -package server.uckgisagi.domain.follow.entity; +package server.uckgisagi.app.follow.domain.entity; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; -import server.uckgisagi.domain.common.AuditingTimeEntity; -import server.uckgisagi.domain.user.entity.User; +import server.uckgisagi.common.domain.AuditingTimeEntity; +import server.uckgisagi.app.user.domain.entity.User; import javax.persistence.*; @@ -31,8 +31,7 @@ private Follow(final User followee, final User follower) { this.follower = follower; } - public static Follow of(User followee, User follower) { + public static Follow newInstance(User followee, User follower) { return new Follow(followee, follower); } - } diff --git a/src/main/java/server/uckgisagi/app/follow/domain/repository/FollowRepository.java b/src/main/java/server/uckgisagi/app/follow/domain/repository/FollowRepository.java new file mode 100644 index 00000000..828d679f --- /dev/null +++ b/src/main/java/server/uckgisagi/app/follow/domain/repository/FollowRepository.java @@ -0,0 +1,7 @@ +package server.uckgisagi.app.follow.domain.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import server.uckgisagi.app.follow.domain.entity.Follow; + +public interface FollowRepository extends JpaRepository, FollowRepositoryCustom { +} diff --git a/src/main/java/server/uckgisagi/app/follow/domain/repository/FollowRepositoryCustom.java b/src/main/java/server/uckgisagi/app/follow/domain/repository/FollowRepositoryCustom.java new file mode 100644 index 00000000..98b335fd --- /dev/null +++ b/src/main/java/server/uckgisagi/app/follow/domain/repository/FollowRepositoryCustom.java @@ -0,0 +1,12 @@ +package server.uckgisagi.app.follow.domain.repository; + +import server.uckgisagi.app.follow.domain.entity.Follow; +import server.uckgisagi.app.user.domain.entity.User; + +import java.util.List; + +public interface FollowRepositoryCustom { + boolean existsByTargetUserIdAndUserId(Long targetUserId, Long userId); + List findMyFollowingUserByUserId(Long userId); + Follow findFollowByFolloweeUserIdAndFollowerUserId(Long followeeUserId, Long followerUserId); +} diff --git a/src/main/java/server/uckgisagi/app/follow/domain/repository/FollowRepositoryCustomImpl.java b/src/main/java/server/uckgisagi/app/follow/domain/repository/FollowRepositoryCustomImpl.java new file mode 100644 index 00000000..480a79fa --- /dev/null +++ b/src/main/java/server/uckgisagi/app/follow/domain/repository/FollowRepositoryCustomImpl.java @@ -0,0 +1,46 @@ +package server.uckgisagi.app.follow.domain.repository; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import server.uckgisagi.app.follow.domain.entity.Follow; +import server.uckgisagi.app.user.domain.entity.User; + +import java.util.List; + +import static server.uckgisagi.app.follow.domain.entity.QFollow.*; + +@RequiredArgsConstructor +public class FollowRepositoryCustomImpl implements FollowRepositoryCustom { + + private final JPAQueryFactory query; + + @Override + public boolean existsByTargetUserIdAndUserId(Long targetUserId, Long userId) { + return query + .selectOne() + .from(follow) + .where( + follow.followee.id.eq(targetUserId), + follow.follower.id.eq(userId) + ).fetchFirst() != null; + } + + @Override + public List findMyFollowingUserByUserId(Long userId) { + return query + .select(follow.followee).distinct() + .from(follow) + .where(follow.follower.id.in(userId)) + .fetch(); + } + + @Override + public Follow findFollowByFolloweeUserIdAndFollowerUserId(Long followeeUserId, Long followerUserId) { + return query + .selectFrom(follow) + .where( + follow.followee.id.eq(followeeUserId), + follow.follower.id.eq(followerUserId) + ).fetchOne(); + } +} diff --git a/src/main/java/server/uckgisagi/app/follow/service/FollowService.java b/src/main/java/server/uckgisagi/app/follow/service/FollowService.java new file mode 100644 index 00000000..1c33240d --- /dev/null +++ b/src/main/java/server/uckgisagi/app/follow/service/FollowService.java @@ -0,0 +1,47 @@ +package server.uckgisagi.app.follow.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import server.uckgisagi.app.user.domain.dictionary.UserDictionary; +import server.uckgisagi.app.user.service.UserServiceUtils; +import server.uckgisagi.app.follow.domain.entity.Follow; +import server.uckgisagi.app.follow.domain.repository.FollowRepository; +import server.uckgisagi.app.user.domain.entity.User; +import server.uckgisagi.app.user.domain.repository.UserRepository; + +import java.util.List; + +// FIXME: 2022/11/18 user ์—ฐ๊ด€๊ด€๊ณ„ ๋ฉ”์„œ๋“œ ์˜ค๋ฅ˜ +@Service +@RequiredArgsConstructor +public class FollowService { + + private final UserRepository userRepository; + private final FollowRepository followRepository; + + @Transactional + public UserDictionary followUser(Long targetUserId, Long userId) { + User me = UserServiceUtils.findByUserId(userRepository, userId); + User targetUser = UserServiceUtils.findByUserId(userRepository, targetUserId); + + FollowServiceUtils.validateNotFollowingUser(followRepository, targetUser.getId(), me.getId()); + + followRepository.save(Follow.newInstance(targetUser, me)); +// targetUser.addFollower(followInfo); +// me.addFollowing(followInfo); + + return UserDictionary.from(List.of(me, targetUser)); + } + + @Transactional + public void unfollowUser(Long targetUserId, Long userId) { + UserServiceUtils.findByUserId(userRepository, userId); + UserServiceUtils.findByUserId(userRepository, targetUserId); + +// me.deleteFollowing(followInfo); +// friend.deleteFollower(followInfo); + + followRepository.delete(FollowServiceUtils.findByFolloweeUserIdAndFollowerUserId(followRepository, targetUserId, userId)); + } +} diff --git a/src/main/java/server/uckgisagi/app/follow/service/FollowServiceUtils.java b/src/main/java/server/uckgisagi/app/follow/service/FollowServiceUtils.java new file mode 100644 index 00000000..25425861 --- /dev/null +++ b/src/main/java/server/uckgisagi/app/follow/service/FollowServiceUtils.java @@ -0,0 +1,27 @@ +package server.uckgisagi.app.follow.service; + +import org.jetbrains.annotations.NotNull; +import server.uckgisagi.common.exception.custom.ConflictException; +import server.uckgisagi.common.exception.custom.NotFoundException; +import server.uckgisagi.app.follow.domain.entity.Follow; +import server.uckgisagi.app.follow.domain.repository.FollowRepository; + +import static server.uckgisagi.common.exception.ErrorResponseResult.*; + +public class FollowServiceUtils { + + @NotNull + public static Follow findByFolloweeUserIdAndFollowerUserId(FollowRepository followRepository, Long followeeUserId, Long followerUserId) { + Follow follow = followRepository.findFollowByFolloweeUserIdAndFollowerUserId(followeeUserId, followerUserId); + if (follow == null) { + throw new NotFoundException(String.format("์กด์žฌํ•˜์ง€ ์•Š๋Š” ํŒ”๋กœ์šฐ ๋Œ€์ƒ - ํŒ”๋กœ์›Œ (%s - %s) ๊ด€๊ณ„ ์ž…๋‹ˆ๋‹ค", followeeUserId, followerUserId), NOT_FOUND_FOLLOW_RELATION_EXCEPTION); + } + return follow; + } + + public static void validateNotFollowingUser(FollowRepository followRepository, Long targetUserId, Long userId) { + if (followRepository.existsByTargetUserIdAndUserId(targetUserId, userId)) { + throw new ConflictException(String.format("์ด๋ฏธ ํŒ”๋กœ์šฐ์ค‘์ธ ์œ ์ € (%s) ์ž…๋‹ˆ๋‹ค", targetUserId), CONFLICT_ALREADY_EXIST_FOLLOW_EXCEPTION); + } + } +} diff --git a/src/main/java/server/uckgisagi/app/home/HomeController.java b/src/main/java/server/uckgisagi/app/home/HomeController.java new file mode 100644 index 00000000..ea5664df --- /dev/null +++ b/src/main/java/server/uckgisagi/app/home/HomeController.java @@ -0,0 +1,62 @@ +package server.uckgisagi.app.home; + +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import server.uckgisagi.app.home.dto.response.HomePostResponse; +import server.uckgisagi.app.home.dto.response.HomeUserResponse; +import server.uckgisagi.app.home.service.HomeRetrieveService; +import server.uckgisagi.app.post.dto.response.PostResponse; +import server.uckgisagi.common.dto.ApiSuccessResponse; +import server.uckgisagi.config.interceptor.Auth; +import server.uckgisagi.config.resolver.LoginUserId; +import springfox.documentation.annotations.ApiIgnore; + +import java.util.List; + +import static server.uckgisagi.common.success.SuccessResponseResult.*; + +@RestController +@RequiredArgsConstructor +public class HomeController { + + private final HomeRetrieveService homeRetrieveService; + + @ApiOperation("[์ธ์ฆ] ๋ฉ”์ธ ํ™ˆ ํŽ˜์ด์ง€ - ๋‚˜์™€ ์นœ๊ตฌ ์ •๋ณด ๋ณด๊ธฐ") + @Auth + @GetMapping("/v1/home/user") + public ApiSuccessResponse retrieveMeAndFriendInfo(@ApiIgnore @LoginUserId Long userId) { + return ApiSuccessResponse.success(OK_SEARCH_MY_HOME_CONTENTS, homeRetrieveService.retrieveMeAndFriendInfo(userId)); + } + + @ApiOperation("[์ธ์ฆ] ๋ฉ”์ธ ํ™ˆ ํŽ˜์ด์ง€ - ๋‚˜์˜ ํฌ์ŠคํŠธ ์ •๋ณด ๋ณด๊ธฐ") + @Auth + @GetMapping("/v1/home/me") + public ApiSuccessResponse retrieveMyHomeContents(@ApiIgnore @LoginUserId Long userId) { + return ApiSuccessResponse.success(OK_SEARCH_MY_HOME_CONTENTS, homeRetrieveService.retrieveHomeContents(userId)); + } + + @ApiOperation("[์ธ์ฆ] ๋ฉ”์ธ ํ™ˆ ํŽ˜์ด์ง€ - ๋‚ ์งœ๋กœ ๋‚˜์˜ ํฌ์ŠคํŠธ ์ •๋ณด ์กฐํšŒํ•˜๊ธฐ") + @Auth + @GetMapping("/v1/home/me/post") + public ApiSuccessResponse> retrieveMyPostByDate(@RequestParam String date, @ApiIgnore @LoginUserId Long userId) { + return ApiSuccessResponse.success(OK_SEARCH_MY_HOME_CONTENTS, homeRetrieveService.retrievePostInfoByDate(date, userId)); + } + + @ApiOperation("[์ธ์ฆ] ๋ฉ”์ธ ํ™ˆ ํŽ˜์ด์ง€ - ์นœ๊ตฌ์˜ ํฌ์ŠคํŠธ ์ •๋ณด ๋ณด๊ธฐ") + @Auth + @GetMapping("/v1/home/{friendUserId}") + public ApiSuccessResponse retrieveFriendHomeContents(@PathVariable Long friendUserId) { + return ApiSuccessResponse.success(OK_SEARCH_FRIEND_HOME_CONTENTS, homeRetrieveService.retrieveHomeContents(friendUserId)); + } + + @ApiOperation("[์ธ์ฆ] ๋ฉ”์ธ ํ™ˆ ํŽ˜์ด์ง€ - ๋‚ ์งœ๋กœ ์นœ๊ตฌ์˜ ํฌ์ŠคํŠธ ์ •๋ณด ์กฐํšŒํ•˜๊ธฐ") + @Auth + @GetMapping("/v1/home/{friendUserId}/post") + public ApiSuccessResponse> retrieveFriendPostByDate(@RequestParam String date, @PathVariable Long friendUserId) { + return ApiSuccessResponse.success(OK_SEARCH_MY_HOME_CONTENTS, homeRetrieveService.retrievePostInfoByDate(date, friendUserId)); + } +} diff --git a/src/main/java/server/uckgisagi/app/home/dto/response/HomePostResponse.java b/src/main/java/server/uckgisagi/app/home/dto/response/HomePostResponse.java new file mode 100644 index 00000000..a0d2cd08 --- /dev/null +++ b/src/main/java/server/uckgisagi/app/home/dto/response/HomePostResponse.java @@ -0,0 +1,22 @@ +package server.uckgisagi.app.home.dto.response; + +import lombok.*; +import server.uckgisagi.app.post.dto.response.PostResponse; + +import java.time.LocalDate; +import java.util.List; + +@ToString +@Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class HomePostResponse { + + private List postDates; + private List posts; + + public static HomePostResponse of(List postDates, List posts) { + return new HomePostResponse(postDates, posts); + } + +} diff --git a/src/main/java/server/uckgisagi/app/home/dto/response/HomeUserResponse.java b/src/main/java/server/uckgisagi/app/home/dto/response/HomeUserResponse.java new file mode 100644 index 00000000..aa48074e --- /dev/null +++ b/src/main/java/server/uckgisagi/app/home/dto/response/HomeUserResponse.java @@ -0,0 +1,20 @@ +package server.uckgisagi.app.home.dto.response; + +import lombok.*; + +import java.util.List; + +@ToString +@Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class HomeUserResponse { + + private UserResponseDto myInfo; + private List friendInfo; + + public static HomeUserResponse of(UserResponseDto myInfo, List friendInfo) { + return new HomeUserResponse(myInfo, friendInfo); + } + +} diff --git a/src/main/java/server/uckgisagi/app/home/dto/response/TodayPostStatus.java b/src/main/java/server/uckgisagi/app/home/dto/response/TodayPostStatus.java new file mode 100644 index 00000000..2dab426d --- /dev/null +++ b/src/main/java/server/uckgisagi/app/home/dto/response/TodayPostStatus.java @@ -0,0 +1,7 @@ +package server.uckgisagi.app.home.dto.response; + +public enum TodayPostStatus { + ACTIVE, + INACTIVE, + ; +} diff --git a/src/main/java/server/uckgisagi/app/home/dto/response/UserResponseDto.java b/src/main/java/server/uckgisagi/app/home/dto/response/UserResponseDto.java new file mode 100644 index 00000000..d81b0a8c --- /dev/null +++ b/src/main/java/server/uckgisagi/app/home/dto/response/UserResponseDto.java @@ -0,0 +1,33 @@ +package server.uckgisagi.app.home.dto.response; + +import lombok.*; +import server.uckgisagi.app.user.domain.entity.User; +import server.uckgisagi.app.user.domain.entity.enumerate.UserGrade; + +@ToString +@Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class UserResponseDto { + + private Long userId; + private String nickname; + private UserGrade grade; + private TodayPostStatus postStatus; + + @Builder(access = AccessLevel.PACKAGE) + private UserResponseDto(final Long userId, final String nickname, final UserGrade grade, final TodayPostStatus postStatus) { + this.userId = userId; + this.nickname = nickname; + this.grade = grade; + this.postStatus = postStatus; + } + + public static UserResponseDto of(User user, TodayPostStatus postStatus) { + return UserResponseDto.builder() + .userId(user.getId()) + .nickname(user.getNickname()) + .grade(user.getGrade()) + .postStatus(postStatus) + .build(); + } +} diff --git a/src/main/java/server/uckgisagi/app/home/service/HomeRetrieveService.java b/src/main/java/server/uckgisagi/app/home/service/HomeRetrieveService.java new file mode 100644 index 00000000..66cc6fcb --- /dev/null +++ b/src/main/java/server/uckgisagi/app/home/service/HomeRetrieveService.java @@ -0,0 +1,116 @@ +package server.uckgisagi.app.home.service; + +import lombok.RequiredArgsConstructor; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import server.uckgisagi.app.home.dto.response.*; +import server.uckgisagi.app.post.dto.response.PostResponse; +import server.uckgisagi.app.user.service.UserServiceUtils; +import server.uckgisagi.app.follow.domain.repository.FollowRepository; +import server.uckgisagi.app.post.domain.entity.Post; +import server.uckgisagi.app.post.domain.repository.PostRepository; +import server.uckgisagi.app.user.domain.entity.User; +import server.uckgisagi.app.user.domain.repository.UserRepository; + +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class HomeRetrieveService implements HomeService { + + private final UserRepository userRepository; + private final PostRepository postRepository; + private final FollowRepository followRepository; + + private final LocalDate TODAY_DATE = LocalDate.now(ZoneId.of("Asia/Seoul")); + private final LocalDate THIS_MONTH_DATE = LocalDate.of(TODAY_DATE.getYear(), TODAY_DATE.getMonthValue(), START_DAY_OF_MONTH); + + private static final int START_DAY_OF_MONTH = 1; + private static final long ONE_MONTH = 1L; + + @Override + @Transactional(readOnly = true) + public HomeUserResponse retrieveMeAndFriendInfo(Long userId) { + User user = UserServiceUtils.findByUserId(userRepository, userId); + + UserResponseDto myInfoResponseDto = getMyInfoResponseDto(user); + List friendsInfoResponseDto = getFriendsInfoResponseDto(user); + + return HomeUserResponse.of(myInfoResponseDto, friendsInfoResponseDto); + } + + private List getFriendsInfoResponseDto(User user) { +// List friends = user.getMyFollowings(); + List friends = followRepository.findMyFollowingUserByUserId(user.getId()); + List friendsIds = friends.stream() + .map(User::getId) + .collect(Collectors.toList()); + List todayPostUsers = postRepository.findUserIdsByTodayDate(TODAY_DATE, friendsIds); + + return friends.stream() + .map(friend -> todayPostUsers.contains(friend) + ? UserResponseDto.of(friend, TodayPostStatus.ACTIVE) + : UserResponseDto.of(friend, TodayPostStatus.INACTIVE) + ) + .collect(Collectors.toList()); + } + + private UserResponseDto getMyInfoResponseDto(User user) { + return UserResponseDto.of( + user, + postRepository.existsByTodayDate(TODAY_DATE, user.getId()) + ? TodayPostStatus.ACTIVE + : TodayPostStatus.INACTIVE + ); + } + + @Override + @Transactional(readOnly = true) + public HomePostResponse retrieveHomeContents(Long userId) { + User user = UserServiceUtils.findByUserId(userRepository, userId); + return getHomePostResponse(postRepository.findPostByUserId(user.getId())); + } + + private HomePostResponse getHomePostResponse(List postByUserId) { + return HomePostResponse.of( + getPostDatesInThisMonth(postByUserId), + getPostResponses(postByUserId) + ); + } + + @Nullable + private List getPostDatesInThisMonth(List posts) { + return posts.stream() + .map(post -> { + LocalDate postCreatedAt = post.getCreatedAt().toLocalDate(); + return isWithinThisMonth(postCreatedAt) ? postCreatedAt : null; + }) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + private boolean isWithinThisMonth(LocalDate postCreatedAt) { + return postCreatedAt.getDayOfMonth() - START_DAY_OF_MONTH != 0 + ? postCreatedAt.isAfter(THIS_MONTH_DATE) && postCreatedAt.isBefore(THIS_MONTH_DATE.plusMonths(ONE_MONTH)) + : postCreatedAt.isEqual(THIS_MONTH_DATE); + } + + @Transactional(readOnly = true) + public List retrievePostInfoByDate(String date, Long userId) { + return getPostResponses(postRepository.findPostByDateAndUserId(LocalDate.parse(date, DateTimeFormatter.ISO_DATE), userId)); + } + + @NotNull + private List getPostResponses(List posts) { + return posts.stream() + .map(PostResponse::from) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/server/uckgisagi/app/home/service/HomeService.java b/src/main/java/server/uckgisagi/app/home/service/HomeService.java new file mode 100644 index 00000000..267cae0a --- /dev/null +++ b/src/main/java/server/uckgisagi/app/home/service/HomeService.java @@ -0,0 +1,25 @@ +package server.uckgisagi.app.home.service; + +import server.uckgisagi.app.home.dto.response.HomePostResponse; +import server.uckgisagi.app.home.dto.response.HomeUserResponse; + +/** + * @see HomeUserResponse + * @see HomePostResponse + * @see HomeRetrieveService + */ +public interface HomeService { + /** + * ํ™ˆ ์ƒ๋‹จ์— ๋‚˜์˜ค๋Š” ์œ ์ €์˜ ์ •๋ณด์™€ ์œ ์ €์˜ ์นœ๊ตฌ ์ •๋ณด ์กฐํšŒ + * @param userId ์œ ์ € ์•„์ด๋”” + * @return ์œ ์ €์˜ ์ •๋ณด์™€ ์œ ์ €์˜ ์นœ๊ตฌ ์ •๋ณด + */ + HomeUserResponse retrieveMeAndFriendInfo(Long userId); + + /** + * ํ™ˆ ์ปจํ…์ธ  ์กฐํšŒ + * @param userId ์œ ์ € ์•„์ด๋”” ํ˜น์€ ์นœ๊ตฌ์˜ ์œ ์ € ์•„์ด๋”” + * @return ํ•ด๋‹น ์œ ์ €์˜ ์ด๋ฒˆ๋‹ฌ ์ฑŒ๋ฆฐ์ง€ ๊ธ€ ์ž‘์„ฑ ์—ฌ๋ถ€ ์บ˜๋ฆฐ๋” ์ •๋ณด์™€ ์ฑŒ๋ฆฐ์ง€ ๊ธ€ ์ •๋ณด + */ + HomePostResponse retrieveHomeContents(Long userId); +} diff --git a/src/main/java/server/uckgisagi/app/image/client/FileStorageClient.java b/src/main/java/server/uckgisagi/app/image/client/FileStorageClient.java new file mode 100644 index 00000000..c92e6d64 --- /dev/null +++ b/src/main/java/server/uckgisagi/app/image/client/FileStorageClient.java @@ -0,0 +1,27 @@ +package server.uckgisagi.app.image.client; + +import org.springframework.web.multipart.MultipartFile; + +public interface FileStorageClient { + + /** + * AmazonS3 Bucket ์— ํŒŒ์ผ์„ ์ €์žฅํ•œ๋‹ค + * @param file ์œ ์ €๊ฐ€ ๋“ฑ๋กํ•˜๋ ค ํ•˜๋Š” ํŒŒ์ผ + * @param fileName ํŒŒ์ผ์˜ ๊ธฐ์กด์˜ ํ™•์žฅ์ž๋ฅผ ์œ ์ง€ํ•œ ์ฑ„ ๋ฐ˜ํ™˜๋œ ์œ ๋‹ˆํฌํ•œ ํŒŒ์ผ์˜ ์ด๋ฆ„ + */ + void uploadFile(MultipartFile file, String fileName); + + /** + * AmazonS3 Bucket ์— ์ €์žฅ๋œ ํŒŒ์ผ ๊ฒฝ๋กœ url ์„ ๊ฐ€์ ธ์˜จ๋‹ค + * @param fileName ํŒŒ์ผ์˜ ๊ธฐ์กด์˜ ํ™•์žฅ์ž๋ฅผ ์œ ์ง€ํ•œ ์ฑ„ ๋ฐ˜ํ™˜๋œ ์œ ๋‹ˆํฌํ•œ ํŒŒ์ผ์˜ ์ด๋ฆ„ + * @return AmazonS3 Bucket ์— ์ €์žฅ๋œ ํŒŒ์ผ ๊ฒฝ๋กœ url + */ + String getFileUrl(String fileName); + + /** + * + * @param bucketImageUrl + */ + void deleteFile(String bucketImageUrl); + +} diff --git a/src/main/java/server/uckgisagi/app/image/client/S3FileStorageClient.java b/src/main/java/server/uckgisagi/app/image/client/S3FileStorageClient.java new file mode 100644 index 00000000..27c9ee55 --- /dev/null +++ b/src/main/java/server/uckgisagi/app/image/client/S3FileStorageClient.java @@ -0,0 +1,52 @@ +package server.uckgisagi.app.image.client; + +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.CannedAccessControlList; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PutObjectRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; +import server.uckgisagi.common.exception.custom.InternalServerException; + +import java.io.IOException; +import java.io.InputStream; + +@Component +@RequiredArgsConstructor +public class S3FileStorageClient implements FileStorageClient { + + @Value("${cloud.aws.s3.bucket}") + private String bucket; + + private final AmazonS3 amazonS3; + + @Override + public void uploadFile(MultipartFile file, String fileName) { + try (InputStream inputStream = file.getInputStream()) { + amazonS3.putObject(new PutObjectRequest(bucket, fileName, inputStream, createObjectMetadata(file)) + .withCannedAcl(CannedAccessControlList.PublicRead)); + } catch (IOException e) { + throw new InternalServerException(String.format("ํŒŒ์ผ (%s) ์ž…๋ ฅ ์ŠคํŠธ๋ฆผ์„ ๊ฐ€์ ธ์˜ค๋Š” ์ค‘ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค", file.getOriginalFilename())); + } + } + + private ObjectMetadata createObjectMetadata(MultipartFile file) { + ObjectMetadata objectMetadata = new ObjectMetadata(); + objectMetadata.setContentType(file.getContentType()); + objectMetadata.setContentLength(file.getSize()); + return objectMetadata; + } + + @Override + public String getFileUrl(String fileName) { + return amazonS3.getUrl(bucket, fileName).toString(); + } + + @Override // TODO : ์ด๋ ‡๊ฒŒ ์‚ญ์ œํ•˜๋Š”๊ฑฐ ๋งž๋Š”์ง€ ํ™•์ธ + public void deleteFile(String bucketImageUrl) { + amazonS3.deleteObject(bucket, bucketImageUrl); + } + +} diff --git a/src/main/java/server/uckgisagi/app/image/provider/UploadProvider.java b/src/main/java/server/uckgisagi/app/image/provider/UploadProvider.java new file mode 100644 index 00000000..1d4a02c6 --- /dev/null +++ b/src/main/java/server/uckgisagi/app/image/provider/UploadProvider.java @@ -0,0 +1,22 @@ +package server.uckgisagi.app.image.provider; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; +import server.uckgisagi.app.image.client.S3FileStorageClient; +import server.uckgisagi.app.image.provider.dto.request.UploadFileRequest; + +@Component +@RequiredArgsConstructor +public class UploadProvider { + + private final S3FileStorageClient fileStorageClient; + + public String uploadFile(UploadFileRequest request, MultipartFile file) { + request.validateAvailableContentType(file.getContentType()); + String fileName = request.getFileNameWithBucketDirectory(file.getOriginalFilename()); + fileStorageClient.uploadFile(file, fileName); + return fileStorageClient.getFileUrl(fileName); + } + +} diff --git a/src/main/java/server/uckgisagi/app/image/provider/dto/request/ImageUploadFileRequest.java b/src/main/java/server/uckgisagi/app/image/provider/dto/request/ImageUploadFileRequest.java new file mode 100644 index 00000000..36bcc00e --- /dev/null +++ b/src/main/java/server/uckgisagi/app/image/provider/dto/request/ImageUploadFileRequest.java @@ -0,0 +1,25 @@ +package server.uckgisagi.app.image.provider.dto.request; + +import lombok.*; +import server.uckgisagi.common.type.FileType; + +@ToString +@Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class ImageUploadFileRequest implements UploadFileRequest { + + private FileType type; + + public static ImageUploadFileRequest from(FileType type) { + return new ImageUploadFileRequest(type); + } + +// public String getFileNameWithBucketDirectory(String originalFilename) { +// return type.createUniqueFileNameWithExtension(originalFilename); +// } + + private static final String SEPARATOR = "/"; + private static final String IMAGE_CONTENT_TYPE_TYPE = "image"; + +} diff --git a/src/main/java/server/uckgisagi/app/image/provider/dto/request/UploadFileRequest.java b/src/main/java/server/uckgisagi/app/image/provider/dto/request/UploadFileRequest.java new file mode 100644 index 00000000..b6ff8ced --- /dev/null +++ b/src/main/java/server/uckgisagi/app/image/provider/dto/request/UploadFileRequest.java @@ -0,0 +1,17 @@ +package server.uckgisagi.app.image.provider.dto.request; + +import server.uckgisagi.common.type.FileType; + +public interface UploadFileRequest { + + FileType getType(); + + default void validateAvailableContentType(String contentType) { + getType().validateAvailableContentType(contentType); + } + + default String getFileNameWithBucketDirectory(String originalFilename) { + return getType().createUniqueFileNameWithExtension(originalFilename); + } + +} diff --git a/src/main/java/server/uckgisagi/app/notification/NotificationController.java b/src/main/java/server/uckgisagi/app/notification/NotificationController.java new file mode 100644 index 00000000..e97866cd --- /dev/null +++ b/src/main/java/server/uckgisagi/app/notification/NotificationController.java @@ -0,0 +1,39 @@ +package server.uckgisagi.app.notification; + +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; +import server.uckgisagi.app.notification.provider.NotificationServiceProvider; +import server.uckgisagi.app.notification.service.NotificationService; +import server.uckgisagi.app.user.domain.dictionary.UserDictionary; +import server.uckgisagi.app.user.service.UserServiceUtils; +import server.uckgisagi.common.dto.ApiSuccessResponse; +import server.uckgisagi.common.success.SuccessResponseResult; +import server.uckgisagi.config.interceptor.Auth; +import server.uckgisagi.config.resolver.LoginUserId; +import server.uckgisagi.app.notification.domain.entity.enumerate.NotificationType; +import server.uckgisagi.app.user.domain.repository.UserRepository; +import springfox.documentation.annotations.ApiIgnore; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +public class NotificationController { + + private final NotificationServiceProvider notificationServiceProvider; + private final UserRepository userRepository; + + @ApiOperation("[์ธ์ฆ] ์นœ๊ตฌ ํ™ˆ ํŽ˜์ด์ง€ - ์นœ๊ตฌ๊ฐ€ ์˜ค๋Š˜ ์˜ฌ๋ฆฐ ์ธ์ฆ๊ธ€์ด ์—†๋Š” ๊ฒฝ์šฐ '์ฐŒ๋ฅด๊ธฐ'") + @Auth + @PostMapping("/v1/notification/{friendUserId}/poke") + public ApiSuccessResponse sendPokeNotification(@PathVariable Long friendUserId, @ApiIgnore @LoginUserId Long userId) { + NotificationService notificationService = notificationServiceProvider.getNotificationService(NotificationType.POKE); + notificationService.sendNotification(userId, friendUserId, UserDictionary.from(List.of( + UserServiceUtils.findByUserId(userRepository, userId), + UserServiceUtils.findByUserId(userRepository, friendUserId)))); + return ApiSuccessResponse.success(SuccessResponseResult.CREATED_NOTIFICATION); + } +} diff --git a/src/main/java/server/uckgisagi/app/notification/constant/NotificationConstants.java b/src/main/java/server/uckgisagi/app/notification/constant/NotificationConstants.java new file mode 100644 index 00000000..5f0a5871 --- /dev/null +++ b/src/main/java/server/uckgisagi/app/notification/constant/NotificationConstants.java @@ -0,0 +1,18 @@ +package server.uckgisagi.app.notification.constant; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class NotificationConstants { + public static final String POKE_TITLE = "๐Ÿšจ ํ—‰-! ์ดˆ-๋น„์ƒ! ๐Ÿšจ"; + public static final String POKE_BODY = "%s ๋‹˜์ด ํšŒ์›๋‹˜์„ ์ฐ”๋ €์–ด์š”! ์ง€๊ตฌ๋ฅผ ์–ต์ง€๋กœ ์‚ฌ๋ž‘ํ•˜๊ณ  ๊ธ€์„ ๋‚จ๊ฒจ๋ณด์„ธ์š”!"; + public static final String POKE_MESSAGE = "%s ๋‹˜์ด ํšŒ์›๋‹˜์„ ์ฐ”๋ €์Šต๋‹ˆ๋‹ค"; + + public static final String FOLLOW_TITLE = "%s ๋‹˜์ด ํŒ”๋กœ์šฐํ–ˆ์–ด์š”!"; + public static final String FOLLOW_BODY = "%s ๋‹˜์˜ ํ”ผ๋“œ๋ฅผ ํ™•์ธํ•ด๋ณด์ƒˆ์š”!"; + public static final String FOLLOW_MESSAGE = "%s ๋‹˜์ด ํšŒ์›๋‹˜์„ ํŒ”๋กœ์šฐํ•˜๊ธฐ ์‹œ์ž‘ํ–ˆ์Šต๋‹ˆ๋‹ค"; + + public static final String FAILURE_MESSAGE = "โŒ ์•Œ๋ฆผ ์ „์†ก ์‹คํŒจ โŒ"; + public static final String SUCCESS_MESSAGE = "โœ… ์•Œ๋ฆผ ์ „์†ก ์„ฑ๊ณต ๐Ÿš€"; +} diff --git a/src/main/java/server/uckgisagi/app/notification/domain/entity/Notifications.java b/src/main/java/server/uckgisagi/app/notification/domain/entity/Notifications.java new file mode 100644 index 00000000..7bb11a96 --- /dev/null +++ b/src/main/java/server/uckgisagi/app/notification/domain/entity/Notifications.java @@ -0,0 +1,51 @@ +package server.uckgisagi.app.notification.domain.entity; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import server.uckgisagi.app.notification.domain.entity.enumerate.NotificationType; +import server.uckgisagi.common.domain.AuditingTimeEntity; + +import javax.persistence.*; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Notifications extends AuditingTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "notification_id", nullable = false) + private Long id; + + @Column(nullable = false) + private Long userId; // ์•Œ๋žŒ์„ ๋ณด๋‚ด๋Š” ์œ ์ € ์•„์ด๋”” + + @Column(nullable = false) + private Long targetUserId; // ์•Œ๋žŒ์„ ๋ฐ›์„ ์œ ์ € ์•„์ด๋”” + + @Column(nullable = false, length = 20) + @Enumerated(EnumType.STRING) + private NotificationType type; + + @Column(columnDefinition = "TEXT", nullable = false) + private String message; + + @Builder(access = AccessLevel.PACKAGE) + private Notifications(final Long userId, final Long targetUserId, final NotificationType type, final String message) { + this.userId = userId; + this.targetUserId = targetUserId; + this.type = type; + this.message = message; + } + + public static Notifications newInstance(Long userId, Long targetUserId, NotificationType type, String message) { + return Notifications.builder() + .userId(userId) + .targetUserId(targetUserId) + .type(type) + .message(message) + .build(); + } +} diff --git a/src/main/java/server/uckgisagi/domain/notification/entity/enumerate/NotificationType.java b/src/main/java/server/uckgisagi/app/notification/domain/entity/enumerate/NotificationType.java similarity index 86% rename from src/main/java/server/uckgisagi/domain/notification/entity/enumerate/NotificationType.java rename to src/main/java/server/uckgisagi/app/notification/domain/entity/enumerate/NotificationType.java index d7da108b..b89f6301 100644 --- a/src/main/java/server/uckgisagi/domain/notification/entity/enumerate/NotificationType.java +++ b/src/main/java/server/uckgisagi/app/notification/domain/entity/enumerate/NotificationType.java @@ -1,4 +1,4 @@ -package server.uckgisagi.domain.notification.entity.enumerate; +package server.uckgisagi.app.notification.domain.entity.enumerate; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -23,5 +23,4 @@ public String getKey() { public String getValue() { return value; } - } diff --git a/src/main/java/server/uckgisagi/app/notification/domain/repository/NotificationRepository.java b/src/main/java/server/uckgisagi/app/notification/domain/repository/NotificationRepository.java new file mode 100644 index 00000000..f4e05536 --- /dev/null +++ b/src/main/java/server/uckgisagi/app/notification/domain/repository/NotificationRepository.java @@ -0,0 +1,7 @@ +package server.uckgisagi.app.notification.domain.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import server.uckgisagi.app.notification.domain.entity.Notifications; + +public interface NotificationRepository extends JpaRepository { +} diff --git a/src/main/java/server/uckgisagi/app/notification/provider/NotificationServiceProvider.java b/src/main/java/server/uckgisagi/app/notification/provider/NotificationServiceProvider.java new file mode 100644 index 00000000..2de3ec59 --- /dev/null +++ b/src/main/java/server/uckgisagi/app/notification/provider/NotificationServiceProvider.java @@ -0,0 +1,32 @@ +package server.uckgisagi.app.notification.provider; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import server.uckgisagi.app.notification.service.NotificationService; +import server.uckgisagi.app.notification.service.impl.FollowNotificationService; +import server.uckgisagi.app.notification.service.impl.PokeNotificationService; +import server.uckgisagi.app.notification.domain.entity.enumerate.NotificationType; + +import javax.annotation.PostConstruct; +import java.util.HashMap; +import java.util.Map; + +@Component +@RequiredArgsConstructor +public class NotificationServiceProvider { + + private static final Map notificationServiceMap = new HashMap<>(); + + private final PokeNotificationService pokeNotificationService; + private final FollowNotificationService followNotificationService; + + @PostConstruct + public void initNotificationServiceMap() { + notificationServiceMap.put(NotificationType.POKE, pokeNotificationService); + notificationServiceMap.put(NotificationType.FOLLOW, followNotificationService); + } + + public NotificationService getNotificationService(NotificationType notificationType) { + return notificationServiceMap.get(notificationType); + } +} diff --git a/src/main/java/server/uckgisagi/app/notification/service/NotificationService.java b/src/main/java/server/uckgisagi/app/notification/service/NotificationService.java new file mode 100644 index 00000000..32547882 --- /dev/null +++ b/src/main/java/server/uckgisagi/app/notification/service/NotificationService.java @@ -0,0 +1,7 @@ +package server.uckgisagi.app.notification.service; + +import server.uckgisagi.app.user.domain.dictionary.UserDictionary; + +public interface NotificationService { + void sendNotification(Long userId, Long targetUserId, UserDictionary dictionary); +} diff --git a/src/main/java/server/uckgisagi/app/notification/service/SendMessageService.java b/src/main/java/server/uckgisagi/app/notification/service/SendMessageService.java new file mode 100644 index 00000000..d1c1701e --- /dev/null +++ b/src/main/java/server/uckgisagi/app/notification/service/SendMessageService.java @@ -0,0 +1,118 @@ +package server.uckgisagi.app.notification.service; + +import com.google.api.core.ApiFutureCallback; +import com.google.api.core.ApiFutures; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.firebase.messaging.Message; +import com.google.firebase.messaging.Notification; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import server.uckgisagi.app.notification.domain.entity.Notifications; +import server.uckgisagi.app.notification.domain.entity.enumerate.NotificationType; +import server.uckgisagi.app.notification.domain.repository.NotificationRepository; +import server.uckgisagi.app.user.domain.entity.User; +import server.uckgisagi.config.firebase.FirebaseInitializer; + +import static server.uckgisagi.app.notification.constant.NotificationConstants.*; + +public abstract class SendMessageService { + + protected void sendMessageByNotificationType(NotificationRepository notificationRepository, User user, User friend, NotificationType type, Logger log) { + + setCommonFields(notificationRepository, user, friend, log); + switch (type) { + case POKE: + setPokeFields(); + break; + case FOLLOW: + setFollowFields(); + break; + } + sendAsyncFCM(); + } + + private void sendAsyncFCM() { + ApiFutures.addCallback( + FirebaseInitializer.getFirebaseMessaging().sendAsync(message), + getApiFutureCallback(), + MoreExecutors.directExecutor()); + } + + @NotNull + private ApiFutureCallback getApiFutureCallback() { + return new ApiFutureCallback() { + @Override + public void onFailure(Throwable t) { + log.error(FAILURE_MESSAGE); + } + + @Override + public void onSuccess(String result) { + notificationRepository.save( + Notifications.newInstance(userId, friendUserId, notificationType, notificationMessage) + ); + log.info(SUCCESS_MESSAGE); + } + }; + } + + private Message getPokeMessage() { + return Message.builder() + .setToken(targetToken) + .setNotification(getPokeNotification()) + .build(); + } + + private Notification getPokeNotification() { + return new Notification( + POKE_TITLE, + String.format(POKE_BODY, userNickname) + ); + } + + private Message getFollowMessage() { + return Message.builder() + .setToken(targetToken) + .setNotification(getFollowNotification()) + .build(); + } + + private Notification getFollowNotification() { + return new Notification( + String.format(FOLLOW_TITLE, userNickname), + String.format(FOLLOW_BODY, userNickname) + ); + } + + private NotificationRepository notificationRepository; + private Long userId; + private String userNickname; + private Long friendUserId; + private String targetToken; + private Logger log; + + private NotificationType notificationType; + private Message message; + private String notificationMessage; + + private void setFollowFields() { + this.notificationType = NotificationType.FOLLOW; + this.message = getFollowMessage(); + this.notificationMessage = String.format(FOLLOW_MESSAGE, userNickname); + } + + private void setPokeFields() { + this.notificationType = NotificationType.POKE; + this.message = getPokeMessage(); + this.notificationMessage = String.format(POKE_MESSAGE, userNickname); + } + + private void setCommonFields(NotificationRepository notificationRepository, User user, User friend, Logger log) { + this.notificationRepository = notificationRepository; + this.userId = user.getId(); + this.userNickname = user.getNickname(); + this.friendUserId = friend.getId(); + this.targetToken = friend.getUserFcmToken(); + this.log = log; + } +} diff --git a/src/main/java/server/uckgisagi/app/notification/service/impl/FollowNotificationService.java b/src/main/java/server/uckgisagi/app/notification/service/impl/FollowNotificationService.java new file mode 100644 index 00000000..0b88dfa9 --- /dev/null +++ b/src/main/java/server/uckgisagi/app/notification/service/impl/FollowNotificationService.java @@ -0,0 +1,32 @@ +package server.uckgisagi.app.notification.service.impl; + +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import server.uckgisagi.app.notification.service.NotificationService; +import server.uckgisagi.app.notification.service.SendMessageService; +import server.uckgisagi.app.notification.domain.entity.enumerate.NotificationType; +import server.uckgisagi.app.notification.domain.repository.NotificationRepository; +import server.uckgisagi.app.user.domain.dictionary.UserDictionary; + +@Service +@RequiredArgsConstructor +public class FollowNotificationService extends SendMessageService implements NotificationService { + + private static final Logger log = LoggerFactory.getLogger(FollowNotificationService.class); + + private final NotificationRepository notificationRepository; + + @Override + @Transactional + public void sendNotification(Long userId, Long targetUserId, UserDictionary dictionary) { + sendMessageByNotificationType( + notificationRepository, + dictionary.getUserByUserId(userId), + dictionary.getUserByUserId(targetUserId), + NotificationType.FOLLOW, log + ); + } +} diff --git a/src/main/java/server/uckgisagi/app/notification/service/impl/PokeNotificationService.java b/src/main/java/server/uckgisagi/app/notification/service/impl/PokeNotificationService.java new file mode 100644 index 00000000..2495fb4b --- /dev/null +++ b/src/main/java/server/uckgisagi/app/notification/service/impl/PokeNotificationService.java @@ -0,0 +1,32 @@ +package server.uckgisagi.app.notification.service.impl; + +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import server.uckgisagi.app.notification.service.NotificationService; +import server.uckgisagi.app.notification.service.SendMessageService; +import server.uckgisagi.app.notification.domain.entity.enumerate.NotificationType; +import server.uckgisagi.app.notification.domain.repository.NotificationRepository; +import server.uckgisagi.app.user.domain.dictionary.UserDictionary; + +@Service +@RequiredArgsConstructor +public class PokeNotificationService extends SendMessageService implements NotificationService { + + private static final Logger log = LoggerFactory.getLogger(PokeNotificationService.class); + + private final NotificationRepository notificationRepository; + + @Override + @Transactional + public void sendNotification(Long userId, Long targetUserId, UserDictionary dictionary) { + sendMessageByNotificationType( + notificationRepository, + dictionary.getUserByUserId(userId), + dictionary.getUserByUserId(targetUserId), + NotificationType.POKE, log + ); + } +} diff --git a/src/main/java/server/uckgisagi/app/post/controller/PostController.java b/src/main/java/server/uckgisagi/app/post/controller/PostController.java new file mode 100644 index 00000000..0b1a9490 --- /dev/null +++ b/src/main/java/server/uckgisagi/app/post/controller/PostController.java @@ -0,0 +1,43 @@ +package server.uckgisagi.app.post.controller; + +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import server.uckgisagi.app.post.dto.request.AddPostRequest; +import server.uckgisagi.app.post.dto.response.GradeResponse; +import server.uckgisagi.app.post.service.PostService; +import server.uckgisagi.common.dto.ApiSuccessResponse; +import server.uckgisagi.common.success.SuccessResponseResult; +import server.uckgisagi.config.interceptor.Auth; +import server.uckgisagi.config.resolver.LoginUserId; +import springfox.documentation.annotations.ApiIgnore; + +import javax.validation.Valid; + +import static server.uckgisagi.common.success.SuccessResponseResult.*; + +@RestController +@RequiredArgsConstructor +public class PostController { + + private final PostService postService; + + @ApiOperation("[์ธ์ฆ] ์ฑŒ๋ฆฐ์ง€ ์ž‘์„ฑ ํŽ˜์ด์ง€ - ์ฑŒ๋ฆฐ์ง€ ๊ธ€ ์ž‘์„ฑํ•˜๊ธฐ") + @Auth + @PostMapping("/v1/post") + public ApiSuccessResponse addPostWithImage(@Valid AddPostRequest request, + @RequestPart MultipartFile imageFile, + @ApiIgnore @LoginUserId Long userId) { + return ApiSuccessResponse.success(CREATED_CERTIFICATION_POST, postService.addPostWithImage(request, imageFile, userId)); + } + + @ApiOperation("[์ธ์ฆ] ์ฑŒ๋ฆฐ์ง€ ๊ธ€ ์‚ญ์ œ ํŽ˜์ด์ง€ - ์ฑŒ๋ฆฐ์ง€ ๊ธ€ ์‚ญ์ œํ•˜๊ธฐ") + @Auth + @DeleteMapping("/v1/post/delete") + public ApiSuccessResponse deletePost(@RequestParam Long postId, @ApiIgnore @LoginUserId Long userId) { + postService.deletePost(postId, userId); + return ApiSuccessResponse.success(NO_CONTENT_DELETE_POST); + } + +} diff --git a/src/main/java/server/uckgisagi/app/post/controller/PostRetrieveController.java b/src/main/java/server/uckgisagi/app/post/controller/PostRetrieveController.java new file mode 100644 index 00000000..e835701d --- /dev/null +++ b/src/main/java/server/uckgisagi/app/post/controller/PostRetrieveController.java @@ -0,0 +1,54 @@ +package server.uckgisagi.app.post.controller; + +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; +import server.uckgisagi.app.post.dto.response.DetailPostResponse; +import server.uckgisagi.app.post.dto.response.PreviewPostResponse; +import server.uckgisagi.app.post.service.PostRetrieveService; +import server.uckgisagi.common.dto.ApiSuccessResponse; +import server.uckgisagi.config.interceptor.Auth; +import server.uckgisagi.config.resolver.LoginUserId; +import springfox.documentation.annotations.ApiIgnore; + +import java.util.List; + +import static server.uckgisagi.common.success.SuccessResponseResult.*; + +@RestController +@RequiredArgsConstructor +public class PostRetrieveController { + + private final PostRetrieveService postRetrieveService; + + @ApiOperation("[์ธ์ฆ] ๋‘˜๋Ÿฌ๋ณด๊ธฐ ํŽ˜์ด์ง€ - ๋ชจ๋“  ์œ ์ €์˜ ์ฑŒ๋ฆฐ์ง€ ๊ธ€ ์กฐํšŒํ•˜๊ธฐ") + @Auth + @GetMapping("/v1/post/all") + public ApiSuccessResponse> retrieveAllPost(@ApiIgnore @LoginUserId Long userId) { + return ApiSuccessResponse.success(OK_SEARCH_ALL_POST, postRetrieveService.retrieveAllPost(userId)); + } + + @ApiOperation("[์ธ์ฆ] ๋‘˜๋Ÿฌ๋ณด๊ธฐ ํŽ˜์ด์ง€ - ์ฑŒ๋ฆฐ์ง€ ๊ธ€ ์ƒ์„ธ๋ณด๊ธฐ") + @Auth + @GetMapping("/v1/post/{postId}") + public ApiSuccessResponse retrieveDetailPost(@PathVariable Long postId, @ApiIgnore @LoginUserId Long userId) { + return ApiSuccessResponse.success(OK_SEARCH_POST, postRetrieveService.retrieveDetailPost(postId, userId)); + } + + @ApiOperation("[์ธ์ฆ] ์Šคํฌ๋žฉ ํŽ˜์ด์ง€ - ์œ ์ €๊ฐ€ ์Šคํฌ๋žฉํ•œ ์ฑŒ๋ฆฐ์ง€ ๊ธ€ ์กฐํšŒํ•˜๊ธฐ") + @Auth + @GetMapping("/v1/post/scrap") + public ApiSuccessResponse> retrieveScrapPost(@ApiIgnore @LoginUserId Long userId) { + return ApiSuccessResponse.success(OK_SEARCH_MY_SCRAP_POST, postRetrieveService.retrieveScrapPost(userId)); + } + + @ApiOperation("[์ธ์ฆ] ์Šคํฌ๋žฉ ํŽ˜์ด์ง€ - ์œ ์ €๊ฐ€ ์Šคํฌ๋žฉํ•œ ์ฑŒ๋ฆฐ์ง€ ๊ธ€ ์ƒ์„ธ๋ณด๊ธฐ") + @Auth + @GetMapping("/v1/post/scrap/{postId}") + public ApiSuccessResponse retrieveDetailScrapPost(@PathVariable Long postId, @ApiIgnore @LoginUserId Long userId) { + return ApiSuccessResponse.success(OK_SEARCH_MY_SCRAP_POST_DETAIL, postRetrieveService.retrieveDetailScrapPost(postId, userId)); + } + +} diff --git a/src/main/java/server/uckgisagi/app/post/domain/entity/Post.java b/src/main/java/server/uckgisagi/app/post/domain/entity/Post.java new file mode 100644 index 00000000..c9df38f5 --- /dev/null +++ b/src/main/java/server/uckgisagi/app/post/domain/entity/Post.java @@ -0,0 +1,61 @@ +package server.uckgisagi.app.post.domain.entity; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import server.uckgisagi.app.post.domain.entity.enumerate.PostStatus; +import server.uckgisagi.common.domain.AuditingTimeEntity; +import server.uckgisagi.app.user.domain.entity.User; + +import javax.persistence.*; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Post extends AuditingTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "post_id", nullable = false) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @Column(nullable = false) + private String imageUrl; + + @Column(nullable = false) + private String title; + + @Column(nullable = false) + private String content; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 10) + private PostStatus postStatus; + + @Builder(access = AccessLevel.PACKAGE) + private Post(final User user, final String imageUrl, final String title, final String content) { + this.user = user; + this.imageUrl = imageUrl; + this.title = title; + this.content = content; + this.postStatus = PostStatus.ACTIVE; + } + + public static Post newInstance(User user, String imageUrl, String title, String content) { + return Post.builder() + .user(user) + .imageUrl(imageUrl) + .title(title) + .content(content) + .build(); + } + + public void changeStatus(PostStatus postStatus){ + this.postStatus = postStatus; + } +} diff --git a/src/main/java/server/uckgisagi/app/post/domain/entity/enumerate/PostStatus.java b/src/main/java/server/uckgisagi/app/post/domain/entity/enumerate/PostStatus.java new file mode 100644 index 00000000..065693a0 --- /dev/null +++ b/src/main/java/server/uckgisagi/app/post/domain/entity/enumerate/PostStatus.java @@ -0,0 +1,7 @@ +package server.uckgisagi.app.post.domain.entity.enumerate; + +public enum PostStatus { + ACTIVE, + INACTIVE + ; +} diff --git a/src/main/java/server/uckgisagi/app/post/domain/repository/PostRepository.java b/src/main/java/server/uckgisagi/app/post/domain/repository/PostRepository.java new file mode 100644 index 00000000..27ff8f26 --- /dev/null +++ b/src/main/java/server/uckgisagi/app/post/domain/repository/PostRepository.java @@ -0,0 +1,7 @@ +package server.uckgisagi.app.post.domain.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import server.uckgisagi.app.post.domain.entity.Post; + +public interface PostRepository extends JpaRepository, PostRepositoryCustom { +} diff --git a/src/main/java/server/uckgisagi/app/post/domain/repository/PostRepositoryCustom.java b/src/main/java/server/uckgisagi/app/post/domain/repository/PostRepositoryCustom.java new file mode 100644 index 00000000..258184a8 --- /dev/null +++ b/src/main/java/server/uckgisagi/app/post/domain/repository/PostRepositoryCustom.java @@ -0,0 +1,17 @@ +package server.uckgisagi.app.post.domain.repository; + +import server.uckgisagi.app.post.domain.entity.Post; +import server.uckgisagi.app.user.domain.entity.User; + +import java.time.LocalDate; +import java.util.List; + +public interface PostRepositoryCustom { + Post findPostByPostId(Long postId); + List findPostByUserId(Long userId); + boolean existsByTodayDate(LocalDate today, Long userId); + List findUserIdsByTodayDate(LocalDate today, List userIds); + Post findByPostIdAndUserId(Long postId, Long userId); + List findAllByPostStatus(List blockUserIds); + List findPostByDateAndUserId(LocalDate date, Long userId); +} diff --git a/src/main/java/server/uckgisagi/app/post/domain/repository/PostRepositoryCustomImpl.java b/src/main/java/server/uckgisagi/app/post/domain/repository/PostRepositoryCustomImpl.java new file mode 100644 index 00000000..eb04c1ff --- /dev/null +++ b/src/main/java/server/uckgisagi/app/post/domain/repository/PostRepositoryCustomImpl.java @@ -0,0 +1,99 @@ +package server.uckgisagi.app.post.domain.repository; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import server.uckgisagi.app.post.domain.entity.enumerate.PostStatus; +import server.uckgisagi.app.post.domain.entity.Post; +import server.uckgisagi.app.user.domain.entity.User; + +import java.time.LocalDate; +import java.util.List; + +import static server.uckgisagi.app.post.domain.entity.QPost.*; + +@RequiredArgsConstructor +public class PostRepositoryCustomImpl implements PostRepositoryCustom { + + private final JPAQueryFactory query; + + private static final long ONE_DAY = 1L; + + @Override + public Post findPostByPostId(Long postId) { + return query + .selectFrom(post) + .where(post.id.eq(postId)) + .fetchFirst(); + } + + @Override + public List findPostByUserId(Long userId) { + return query + .selectFrom(post) + .where(post.user.id.eq(userId)) + .fetch(); + } + + @Override + public boolean existsByTodayDate(LocalDate today, Long userId) { + return query + .selectOne() + .from(post) + .where( + post.user.id.eq(userId), + post.createdAt.between( + today.atStartOfDay(), + today.atStartOfDay().plusDays(ONE_DAY) + )) + .fetchFirst() != null; + } + + + @Override + public List findUserIdsByTodayDate(LocalDate today, List userIds) { + return query + .select(post.user) + .from(post) + .where( + post.user.id.in(userIds), + post.createdAt.between( + today.atStartOfDay(), + today.atStartOfDay().plusDays(ONE_DAY) + )) + .fetch(); + } + + @Override + public Post findByPostIdAndUserId(Long postId, Long userId){ + return query + .selectFrom(post) + .where( + post.id.eq(postId), + post.user.id.eq(userId) + ) + .fetchOne(); + } + + @Override + public List findAllByPostStatus(List blockUserIds) { + return query.selectFrom(post) + .where( + post.postStatus.eq(PostStatus.ACTIVE), + post.user.id.notIn(blockUserIds) + ) + .orderBy(post.createdAt.desc()) + .fetch(); + } + + public List findPostByDateAndUserId(LocalDate date, Long userId) { + return query + .selectFrom(post) + .where( + post.user.id.eq(userId), + post.createdAt.between( + date.atStartOfDay(), + date.atStartOfDay().plusDays(ONE_DAY) + )) + .fetch(); + } +} diff --git a/src/main/java/server/uckgisagi/app/post/dto/request/AddPostRequest.java b/src/main/java/server/uckgisagi/app/post/dto/request/AddPostRequest.java new file mode 100644 index 00000000..fb13aa30 --- /dev/null +++ b/src/main/java/server/uckgisagi/app/post/dto/request/AddPostRequest.java @@ -0,0 +1,24 @@ +package server.uckgisagi.app.post.dto.request; + +import lombok.*; +import server.uckgisagi.app.post.domain.entity.Post; +import server.uckgisagi.app.user.domain.entity.User; + +import javax.validation.constraints.NotBlank; + +@ToString +@Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class AddPostRequest { + + @NotBlank + private String title; + + @NotBlank + private String content; + + public Post toPostEntity(User user, String imageUrl) { + return Post.newInstance(user, imageUrl, title, content); + } +} diff --git a/src/main/java/server/uckgisagi/app/post/dto/response/DetailPostResponse.java b/src/main/java/server/uckgisagi/app/post/dto/response/DetailPostResponse.java new file mode 100644 index 00000000..3f5bbe88 --- /dev/null +++ b/src/main/java/server/uckgisagi/app/post/dto/response/DetailPostResponse.java @@ -0,0 +1,42 @@ +package server.uckgisagi.app.post.dto.response; + +import lombok.*; +import server.uckgisagi.common.dto.AuditingTimeResponse; +import server.uckgisagi.app.post.domain.entity.Post; + +@ToString +@Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class DetailPostResponse extends AuditingTimeResponse { + + private Long postId; + private Long userId; + private String nickname; + private String imageUrl; + private String content; + private ScrapStatus scrapStatus; + + @Builder(access = AccessLevel.PACKAGE) + private DetailPostResponse(final Long postId, final Long userId, final String nickname, final String imageUrl, + final String content, final ScrapStatus scrapStatus) { + this.postId = postId; + this.userId = userId; + this.nickname = nickname; + this.imageUrl = imageUrl; + this.content = content; + this.scrapStatus = scrapStatus; + } + + public static DetailPostResponse of(Post post, ScrapStatus scrapStatus) { + DetailPostResponse response = DetailPostResponse.builder() + .postId(post.getId()) + .userId(post.getUser().getId()) + .nickname(post.getUser().getNickname()) + .imageUrl(post.getImageUrl()) + .content(post.getContent()) + .scrapStatus(scrapStatus) + .build(); + response.setBaseTime(post); + return response; + } +} diff --git a/src/main/java/server/uckgisagi/app/post/dto/response/GradeResponse.java b/src/main/java/server/uckgisagi/app/post/dto/response/GradeResponse.java new file mode 100644 index 00000000..0729fe3e --- /dev/null +++ b/src/main/java/server/uckgisagi/app/post/dto/response/GradeResponse.java @@ -0,0 +1,23 @@ +package server.uckgisagi.app.post.dto.response; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import server.uckgisagi.app.user.domain.entity.enumerate.UserGrade; + +@ToString +@Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class GradeResponse { + + private UserGrade grade; + + private GradeResponse(UserGrade grade) { + this.grade = grade; + } + + public static GradeResponse from(UserGrade grade) { + return new GradeResponse(grade); + } +} diff --git a/src/main/java/server/uckgisagi/app/post/dto/response/PostResponse.java b/src/main/java/server/uckgisagi/app/post/dto/response/PostResponse.java new file mode 100644 index 00000000..0d9433fe --- /dev/null +++ b/src/main/java/server/uckgisagi/app/post/dto/response/PostResponse.java @@ -0,0 +1,35 @@ +package server.uckgisagi.app.post.dto.response; + +import lombok.*; +import server.uckgisagi.common.dto.AuditingTimeResponse; +import server.uckgisagi.app.post.domain.entity.Post; + +@ToString +@Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class PostResponse extends AuditingTimeResponse { + + private Long postId; + private String imageUrl; + private String title; + private String content; + + @Builder(access = AccessLevel.PACKAGE) + private PostResponse(final Long postId, final String imageUrl, final String title, final String content) { + this.postId = postId; + this.imageUrl = imageUrl; + this.title = title; + this.content = content; + } + + public static PostResponse from(Post post) { + PostResponse response = PostResponse.builder() + .postId(post.getId()) + .imageUrl(post.getImageUrl()) + .title(post.getTitle()) + .content(post.getContent()) + .build(); + response.setBaseTime(post); + return response; + } +} diff --git a/src/main/java/server/uckgisagi/app/post/dto/response/PreviewPostResponse.java b/src/main/java/server/uckgisagi/app/post/dto/response/PreviewPostResponse.java new file mode 100644 index 00000000..8dcfee22 --- /dev/null +++ b/src/main/java/server/uckgisagi/app/post/dto/response/PreviewPostResponse.java @@ -0,0 +1,33 @@ +package server.uckgisagi.app.post.dto.response; + +import lombok.*; +import server.uckgisagi.app.post.domain.entity.Post; + +@ToString +@Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class PreviewPostResponse { + + private Long postId; + private String imageUrl; + private String content; + private ScrapStatus scrapStatus; + + @Builder(access = AccessLevel.PACKAGE) + private PreviewPostResponse(final Long postId, final String imageUrl, + final String content, final ScrapStatus scrapStatus) { + this.postId = postId; + this.imageUrl = imageUrl; + this.content = content; + this.scrapStatus = scrapStatus; + } + + public static PreviewPostResponse of(Post post, ScrapStatus scrapStatus) { + return PreviewPostResponse.builder() + .postId(post.getId()) + .imageUrl(post.getImageUrl()) + .content(post.getContent()) + .scrapStatus(scrapStatus) + .build(); + } +} diff --git a/src/main/java/server/uckgisagi/app/post/dto/response/ScrapStatus.java b/src/main/java/server/uckgisagi/app/post/dto/response/ScrapStatus.java new file mode 100644 index 00000000..c431ff38 --- /dev/null +++ b/src/main/java/server/uckgisagi/app/post/dto/response/ScrapStatus.java @@ -0,0 +1,7 @@ +package server.uckgisagi.app.post.dto.response; + +public enum ScrapStatus { + ACTIVE, + INACTIVE, + ; +} diff --git a/src/main/java/server/uckgisagi/app/post/service/PostRetrieveService.java b/src/main/java/server/uckgisagi/app/post/service/PostRetrieveService.java new file mode 100644 index 00000000..f7267020 --- /dev/null +++ b/src/main/java/server/uckgisagi/app/post/service/PostRetrieveService.java @@ -0,0 +1,70 @@ +package server.uckgisagi.app.post.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import server.uckgisagi.app.post.dto.response.DetailPostResponse; +import server.uckgisagi.app.post.dto.response.PreviewPostResponse; +import server.uckgisagi.app.post.dto.response.ScrapStatus; +import server.uckgisagi.app.user.service.UserServiceUtils; +import server.uckgisagi.app.block.domain.entity.Block; +import server.uckgisagi.app.post.domain.entity.Post; +import server.uckgisagi.app.post.domain.repository.PostRepository; +import server.uckgisagi.app.scrap.domain.repository.ScrapRepository; +import server.uckgisagi.app.user.domain.entity.User; +import server.uckgisagi.app.user.domain.repository.UserRepository; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class PostRetrieveService { + + private final UserRepository userRepository; + private final PostRepository postRepository; + private final ScrapRepository scrapRepository; + + public List retrieveAllPost(Long userId) { + User me = userRepository.findUserByUserId(userId); + List blockUserIds = me.getBlocks().stream() + .map(Block::getBlockUserId) + .collect(Collectors.toList()); + List scrapedPosts = scrapRepository.findScrapPostByUserId(userId, blockUserIds); + + return postRepository + .findAllByPostStatus(blockUserIds).stream() + .map(post -> scrapedPosts.contains(post) + ? PreviewPostResponse.of(post, ScrapStatus.ACTIVE) + : PreviewPostResponse.of(post, ScrapStatus.INACTIVE)) + .collect(Collectors.toList()); + } + + public List retrieveScrapPost(Long userId) { + List blockUserIds = userRepository.findUserByUserId(userId) + .getBlocks().stream() + .map(Block::getBlockUserId) + .collect(Collectors.toList()); + + return scrapRepository.findScrapPostByUserId(userId, blockUserIds).stream() + .map(post -> PreviewPostResponse.of(post, ScrapStatus.ACTIVE)) + .collect(Collectors.toList()); + } + + public DetailPostResponse retrieveDetailPost(Long postId, Long userId) { +// UserServiceUtils.validateExistUser(userRepository, userId); // TODO ๊ณผ์—ฐ ๋ชจ๋“  api ๋งˆ๋‹ค ์œ ์ € ์ฒดํฌ๋ฅผ ํ•ด์•ผํ•˜๋Š”๊ฐ€? + Post post = PostServiceUtils.findByPostId(postRepository, postId); + return scrapRepository.existsByPostAndUserId(post, userId) + ? DetailPostResponse.of(post, ScrapStatus.ACTIVE) + : DetailPostResponse.of(post, ScrapStatus.INACTIVE); + } + + public DetailPostResponse retrieveDetailScrapPost(Long postId, Long userId) { +// UserServiceUtils.validateExistUser(userRepository, userId); + return DetailPostResponse.of( + PostServiceUtils.findByPostId(postRepository, postId), + ScrapStatus.ACTIVE + ); + } +} diff --git a/src/main/java/server/uckgisagi/app/post/service/PostService.java b/src/main/java/server/uckgisagi/app/post/service/PostService.java new file mode 100644 index 00000000..0445860d --- /dev/null +++ b/src/main/java/server/uckgisagi/app/post/service/PostService.java @@ -0,0 +1,37 @@ +package server.uckgisagi.app.post.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; +import server.uckgisagi.app.image.provider.UploadProvider; +import server.uckgisagi.app.image.provider.dto.request.ImageUploadFileRequest; +import server.uckgisagi.app.post.dto.request.AddPostRequest; +import server.uckgisagi.app.post.dto.response.GradeResponse; +import server.uckgisagi.app.user.service.UserServiceUtils; +import server.uckgisagi.common.type.FileType; +import server.uckgisagi.app.post.domain.repository.PostRepository; +import server.uckgisagi.app.user.domain.entity.User; +import server.uckgisagi.app.user.domain.repository.UserRepository; + +@Service +@RequiredArgsConstructor +public class PostService { + + private final PostRepository postRepository; + private final UserRepository userRepository; + private final UploadProvider uploadProvider; + + @Transactional + public GradeResponse addPostWithImage(AddPostRequest request, MultipartFile imageFile, Long userId) { + String imageUrl = uploadProvider.uploadFile(ImageUploadFileRequest.from(FileType.POST_IMAGE), imageFile); + User user = UserServiceUtils.findByUserId(userRepository, userId); + user.addPosts(postRepository.save(request.toPostEntity(user, imageUrl))); + return GradeResponse.from(user.getGrade()); + } + + @Transactional + public void deletePost(Long postId, Long userId) { + postRepository.delete(PostServiceUtils.findByPostIdAndUserId(postRepository, postId, userId)); + } +} diff --git a/src/main/java/server/uckgisagi/app/post/service/PostServiceUtils.java b/src/main/java/server/uckgisagi/app/post/service/PostServiceUtils.java new file mode 100644 index 00000000..20bb8c78 --- /dev/null +++ b/src/main/java/server/uckgisagi/app/post/service/PostServiceUtils.java @@ -0,0 +1,29 @@ +package server.uckgisagi.app.post.service; + +import org.jetbrains.annotations.NotNull; +import server.uckgisagi.common.exception.custom.NotFoundException; +import server.uckgisagi.app.post.domain.entity.Post; +import server.uckgisagi.app.post.domain.repository.PostRepository; + +import static server.uckgisagi.common.exception.ErrorResponseResult.*; + +public class PostServiceUtils { + + @NotNull + public static Post findByPostId(PostRepository postRepository, Long postId) { + Post post = postRepository.findPostByPostId(postId); + if (post == null) { + throw new NotFoundException(String.format("์กด์žฌํ•˜์ง€ ์•Š๋Š” ๊ฒŒ์‹œ๊ธ€ (%s) ์ž…๋‹ˆ๋‹ค", postId), NOT_FOUND_POST_EXCEPTION); + } + return post; + } + + @NotNull + public static Post findByPostIdAndUserId(PostRepository postRepository, Long postId, Long userId) { + Post post = postRepository.findByPostIdAndUserId(postId, userId); + if (post == null) { + throw new NotFoundException(String.format("์กด์žฌํ•˜์ง€ ์•Š๋Š” ๊ฒŒ์‹œ๊ธ€ (%s) ์ž…๋‹ˆ๋‹ค", postId), NOT_FOUND_POST_EXCEPTION); + } + return post; + } +} diff --git a/src/main/java/server/uckgisagi/app/review/ReviewController.java b/src/main/java/server/uckgisagi/app/review/ReviewController.java new file mode 100644 index 00000000..c65a9d3d --- /dev/null +++ b/src/main/java/server/uckgisagi/app/review/ReviewController.java @@ -0,0 +1,40 @@ +package server.uckgisagi.app.review; + +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import server.uckgisagi.app.review.dto.request.AddReviewRequest; +import server.uckgisagi.app.review.dto.response.ReviewResponse; +import server.uckgisagi.common.dto.ApiSuccessResponse; +import server.uckgisagi.common.success.SuccessResponseResult; +import server.uckgisagi.config.interceptor.Auth; +import server.uckgisagi.config.resolver.LoginUserId; +import springfox.documentation.annotations.ApiIgnore; + +import javax.validation.Valid; +import java.util.List; + +import static server.uckgisagi.common.success.SuccessResponseResult.*; + +@RestController +@RequiredArgsConstructor +public class ReviewController { + + private final ReviewService reviewService; + + @ApiOperation("[์ธ์ฆ] ๋ฆฌํ•„ ์Šคํ…Œ์ด์…˜ ์ƒ์„ธ๋ณด๊ธฐ ํŽ˜์ด์ง€ - ํ›„๊ธฐ ๋“ฑ๋ก") + @Auth + @PostMapping("/v1/review") + public ApiSuccessResponse addReview(@Valid @RequestBody AddReviewRequest request, @ApiIgnore @LoginUserId Long userId) { + reviewService.addReview(request, userId); + return ApiSuccessResponse.success(CREATED_REVIEW_COMMENT); + } + + @ApiOperation("[์ธ์ฆ] ๋ฆฌํ•„ ์Šคํ…Œ์ด์…˜ ์ƒ์„ธ๋ณด๊ธฐ ํŽ˜์ด์ง€ - ํ›„๊ธฐ ์กฐํšŒ") + @Auth + @GetMapping("/v1/review/{storeId}") + public ApiSuccessResponse> retrieveStoreReview(@PathVariable Long storeId) { + return ApiSuccessResponse.success(OK_RETRIEVE_STORE_REVIEW, reviewService.retrieveReview(storeId)); + } + +} diff --git a/src/main/java/server/uckgisagi/app/review/ReviewService.java b/src/main/java/server/uckgisagi/app/review/ReviewService.java new file mode 100644 index 00000000..f1db6fb8 --- /dev/null +++ b/src/main/java/server/uckgisagi/app/review/ReviewService.java @@ -0,0 +1,40 @@ +package server.uckgisagi.app.review; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import server.uckgisagi.app.review.dto.request.AddReviewRequest; +import server.uckgisagi.app.review.dto.response.ReviewResponse; +import server.uckgisagi.app.store.service.StoreServiceUtils; +import server.uckgisagi.app.user.service.UserServiceUtils; +import server.uckgisagi.app.review.domain.repository.ReviewRepository; +import server.uckgisagi.app.store.domain.repository.StoreRepository; +import server.uckgisagi.app.user.domain.repository.UserRepository; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class ReviewService { + + private final ReviewRepository reviewRepository; + private final StoreRepository storeRepository; + private final UserRepository userRepository; + + @Transactional + public void addReview(AddReviewRequest request, Long userId) { + reviewRepository.save(request.toReviewEntity( + StoreServiceUtils.findByStoreId(storeRepository, request.getStoreId()), + UserServiceUtils.findByUserId(userRepository, userId) + )); + } + + @Transactional(readOnly = true) + public List retrieveReview(Long storeId) { + return StoreServiceUtils.findByStoreId(storeRepository, storeId) + .getReviews().stream() + .map(ReviewResponse::from) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/server/uckgisagi/domain/review/entity/Review.java b/src/main/java/server/uckgisagi/app/review/domain/entity/Review.java similarity index 77% rename from src/main/java/server/uckgisagi/domain/review/entity/Review.java rename to src/main/java/server/uckgisagi/app/review/domain/entity/Review.java index 3bc22b33..1ae58034 100644 --- a/src/main/java/server/uckgisagi/domain/review/entity/Review.java +++ b/src/main/java/server/uckgisagi/app/review/domain/entity/Review.java @@ -1,10 +1,10 @@ -package server.uckgisagi.domain.review.entity; +package server.uckgisagi.app.review.domain.entity; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; -import server.uckgisagi.domain.store.entity.Store; -import server.uckgisagi.domain.user.entity.User; +import server.uckgisagi.app.store.domain.entity.Store; +import server.uckgisagi.app.user.domain.entity.User; import javax.persistence.*; @@ -35,8 +35,7 @@ private Review(final Store store, final User user, final String comment) { this.comment = comment; } - public static Review of(Store store, User user, String comment) { + public static Review newInstance(Store store, User user, String comment) { return new Review(store, user, comment); } - } diff --git a/src/main/java/server/uckgisagi/domain/review/repository/ReviewRepository.java b/src/main/java/server/uckgisagi/app/review/domain/repository/ReviewRepository.java similarity index 55% rename from src/main/java/server/uckgisagi/domain/review/repository/ReviewRepository.java rename to src/main/java/server/uckgisagi/app/review/domain/repository/ReviewRepository.java index e0590de3..8e956c33 100644 --- a/src/main/java/server/uckgisagi/domain/review/repository/ReviewRepository.java +++ b/src/main/java/server/uckgisagi/app/review/domain/repository/ReviewRepository.java @@ -1,7 +1,7 @@ -package server.uckgisagi.domain.review.repository; +package server.uckgisagi.app.review.domain.repository; import org.springframework.data.jpa.repository.JpaRepository; -import server.uckgisagi.domain.review.entity.Review; +import server.uckgisagi.app.review.domain.entity.Review; public interface ReviewRepository extends JpaRepository { } diff --git a/src/main/java/server/uckgisagi/app/review/dto/request/AddReviewRequest.java b/src/main/java/server/uckgisagi/app/review/dto/request/AddReviewRequest.java new file mode 100644 index 00000000..cdcaa3db --- /dev/null +++ b/src/main/java/server/uckgisagi/app/review/dto/request/AddReviewRequest.java @@ -0,0 +1,29 @@ +package server.uckgisagi.app.review.dto.request; + +import lombok.*; +import server.uckgisagi.app.review.domain.entity.Review; +import server.uckgisagi.app.store.domain.entity.Store; +import server.uckgisagi.app.user.domain.entity.User; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +@ToString +@Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Builder(builderMethodName = "testBuilder") +public class AddReviewRequest { + + @NotNull + private Long storeId; + + @NotBlank + private String content; + + public Review toReviewEntity(Store store, User user) { + Review review = Review.newInstance(store, user, content); + store.addReview(review); + return review; + } +} diff --git a/src/main/java/server/uckgisagi/app/review/dto/response/ReviewResponse.java b/src/main/java/server/uckgisagi/app/review/dto/response/ReviewResponse.java new file mode 100644 index 00000000..e47703e3 --- /dev/null +++ b/src/main/java/server/uckgisagi/app/review/dto/response/ReviewResponse.java @@ -0,0 +1,29 @@ +package server.uckgisagi.app.review.dto.response; + +import lombok.*; +import server.uckgisagi.app.review.domain.entity.Review; + +@ToString +@Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class ReviewResponse { + + private Long reviewId; + private String nickname; + private String comment; + + @Builder(access = AccessLevel.PACKAGE) + private ReviewResponse(final Long reviewId, final String nickname, final String comment) { + this.reviewId = reviewId; + this.nickname = nickname; + this.comment = comment; + } + + public static ReviewResponse from(Review review) { + return ReviewResponse.builder() + .reviewId(review.getId()) + .nickname(review.getUser().getNickname()) + .comment(review.getComment()) + .build(); + } +} diff --git a/src/main/java/server/uckgisagi/app/scrap/ScrapController.java b/src/main/java/server/uckgisagi/app/scrap/ScrapController.java new file mode 100644 index 00000000..cdeac46f --- /dev/null +++ b/src/main/java/server/uckgisagi/app/scrap/ScrapController.java @@ -0,0 +1,36 @@ +package server.uckgisagi.app.scrap; + +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import server.uckgisagi.app.scrap.service.ScrapService; +import server.uckgisagi.common.dto.ApiSuccessResponse; +import server.uckgisagi.common.success.SuccessResponseResult; +import server.uckgisagi.config.interceptor.Auth; +import server.uckgisagi.config.resolver.LoginUserId; +import springfox.documentation.annotations.ApiIgnore; + +import static server.uckgisagi.common.success.SuccessResponseResult.*; + +@RestController +@RequiredArgsConstructor +public class ScrapController { + + private final ScrapService scrapService; + + @ApiOperation("[์ธ์ฆ] ์Šคํฌ๋žฉ์ด ๊ฐ€๋Šฅํ•œ ๋ชจ๋“  ํŽ˜์ด์ง€ - ์Šคํฌ๋žฉ ํ•˜๊ธฐ") + @Auth + @PostMapping("/v1/scrap/{postId}") + public ApiSuccessResponse addScrap(@PathVariable Long postId, @ApiIgnore @LoginUserId Long userId) { + scrapService.addScrap(postId, userId); + return ApiSuccessResponse.success(NO_CONTENT_SCRAP_POST); + } + + @ApiOperation("[์ธ์ฆ] ์Šคํฌ๋žฉ์ด ๊ฐ€๋Šฅํ•œ ๋ชจ๋“  ํŽ˜์ด์ง€ - ์Šคํฌ๋žฉ ์ทจ์†Œ ํ•˜๊ธฐ") + @Auth + @DeleteMapping("/v1/scrap/{postId}") + public ApiSuccessResponse deleteScrap(@PathVariable Long postId, @ApiIgnore @LoginUserId Long userId) { + scrapService.deleteScrap(postId, userId); + return ApiSuccessResponse.success(NO_CONTENT_CANCEL_SCRAP_POST); + } +} diff --git a/src/main/java/server/uckgisagi/domain/postscrap/entity/PostScrap.java b/src/main/java/server/uckgisagi/app/scrap/domain/entity/Scrap.java similarity index 55% rename from src/main/java/server/uckgisagi/domain/postscrap/entity/PostScrap.java rename to src/main/java/server/uckgisagi/app/scrap/domain/entity/Scrap.java index e29b5ea3..5b7b3bbe 100644 --- a/src/main/java/server/uckgisagi/domain/postscrap/entity/PostScrap.java +++ b/src/main/java/server/uckgisagi/app/scrap/domain/entity/Scrap.java @@ -1,18 +1,18 @@ -package server.uckgisagi.domain.postscrap.entity; +package server.uckgisagi.app.scrap.domain.entity; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; -import server.uckgisagi.domain.common.AuditingTimeEntity; -import server.uckgisagi.domain.post.entity.Post; -import server.uckgisagi.domain.user.entity.User; +import server.uckgisagi.common.domain.AuditingTimeEntity; +import server.uckgisagi.app.post.domain.entity.Post; +import server.uckgisagi.app.user.domain.entity.User; import javax.persistence.*; @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class PostScrap extends AuditingTimeEntity { +public class Scrap extends AuditingTimeEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -27,4 +27,12 @@ public class PostScrap extends AuditingTimeEntity { @JoinColumn(name = "post_id") private Post post; // ์Šคํฌ๋žฉํ•œ ๊ฒŒ์‹œ๋ฌผ ์•„์ด๋”” + private Scrap(final User user, final Post post) { + this.user = user; + this.post = post; + } + + public static Scrap newInstance(User user, Post post) { + return new Scrap(user, post); + } } diff --git a/src/main/java/server/uckgisagi/app/scrap/domain/repository/ScrapRepository.java b/src/main/java/server/uckgisagi/app/scrap/domain/repository/ScrapRepository.java new file mode 100644 index 00000000..450064ef --- /dev/null +++ b/src/main/java/server/uckgisagi/app/scrap/domain/repository/ScrapRepository.java @@ -0,0 +1,7 @@ +package server.uckgisagi.app.scrap.domain.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import server.uckgisagi.app.scrap.domain.entity.Scrap; + +public interface ScrapRepository extends JpaRepository, ScrapRepositoryCustom { +} diff --git a/src/main/java/server/uckgisagi/app/scrap/domain/repository/ScrapRepositoryCustom.java b/src/main/java/server/uckgisagi/app/scrap/domain/repository/ScrapRepositoryCustom.java new file mode 100644 index 00000000..baacad7e --- /dev/null +++ b/src/main/java/server/uckgisagi/app/scrap/domain/repository/ScrapRepositoryCustom.java @@ -0,0 +1,12 @@ +package server.uckgisagi.app.scrap.domain.repository; + +import server.uckgisagi.app.post.domain.entity.Post; +import server.uckgisagi.app.scrap.domain.entity.Scrap; + +import java.util.List; + +public interface ScrapRepositoryCustom { + List findScrapPostByUserId(Long userId, List blockUserIds); + boolean existsByPostAndUserId(Post post, Long userId); + Scrap findScrapByPostIdAndUserId(Long postId, Long userId); +} diff --git a/src/main/java/server/uckgisagi/app/scrap/domain/repository/ScrapRepositoryCustomImpl.java b/src/main/java/server/uckgisagi/app/scrap/domain/repository/ScrapRepositoryCustomImpl.java new file mode 100644 index 00000000..e8fc921e --- /dev/null +++ b/src/main/java/server/uckgisagi/app/scrap/domain/repository/ScrapRepositoryCustomImpl.java @@ -0,0 +1,52 @@ +package server.uckgisagi.app.scrap.domain.repository; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import server.uckgisagi.app.post.domain.entity.Post; +import server.uckgisagi.app.post.domain.entity.enumerate.PostStatus; +import server.uckgisagi.app.scrap.domain.entity.Scrap; + +import java.util.List; + +import static server.uckgisagi.app.scrap.domain.entity.QScrap.*; + +@RequiredArgsConstructor +public class ScrapRepositoryCustomImpl implements ScrapRepositoryCustom { + + private final JPAQueryFactory query; + + @Override + public List findScrapPostByUserId(Long userId, List blockUserIds) { + return query + .select(scrap.post).distinct() + .from(scrap) + .where( + scrap.post.postStatus.eq(PostStatus.ACTIVE), + scrap.post.user.id.notIn(blockUserIds), + scrap.user.id.eq(userId) + ) + .fetch(); + } + + @Override + public boolean existsByPostAndUserId(Post post, Long userId) { + return query + .selectOne() + .from(scrap) + .where( + scrap.post.eq(post), + scrap.user.id.eq(userId), + scrap.post.postStatus.eq(PostStatus.ACTIVE) + ).fetchFirst() != null; + } + + @Override + public Scrap findScrapByPostIdAndUserId(Long postId, Long userId) { + return query + .selectFrom(scrap) + .where( + scrap.user.id.eq(userId), + scrap.post.id.eq(postId) + ).fetchOne(); + } +} diff --git a/src/main/java/server/uckgisagi/app/scrap/service/ScrapService.java b/src/main/java/server/uckgisagi/app/scrap/service/ScrapService.java new file mode 100644 index 00000000..2c100339 --- /dev/null +++ b/src/main/java/server/uckgisagi/app/scrap/service/ScrapService.java @@ -0,0 +1,36 @@ +package server.uckgisagi.app.scrap.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import server.uckgisagi.app.post.service.PostServiceUtils; +import server.uckgisagi.app.user.service.UserServiceUtils; +import server.uckgisagi.app.post.domain.entity.Post; +import server.uckgisagi.app.post.domain.repository.PostRepository; +import server.uckgisagi.app.scrap.domain.entity.Scrap; +import server.uckgisagi.app.scrap.domain.repository.ScrapRepository; +import server.uckgisagi.app.user.domain.entity.User; +import server.uckgisagi.app.user.domain.repository.UserRepository; + +@Service +@RequiredArgsConstructor +public class ScrapService { + + private final ScrapRepository scrapRepository; + private final UserRepository userRepository; + private final PostRepository postRepository; + + @Transactional + public void addScrap(Long postId, Long userId) { + User user = UserServiceUtils.findByUserId(userRepository, userId); + Post post = PostServiceUtils.findByPostId(postRepository, postId); + if (!scrapRepository.existsByPostAndUserId(post, userId)) { + scrapRepository.save(Scrap.newInstance(user, post)); + } + } + + @Transactional + public void deleteScrap(Long postId, Long userId) { + scrapRepository.delete(ScrapServiceUtils.findByPostIdAndUserId(scrapRepository, postId, userId)); + } +} diff --git a/src/main/java/server/uckgisagi/app/scrap/service/ScrapServiceUtils.java b/src/main/java/server/uckgisagi/app/scrap/service/ScrapServiceUtils.java new file mode 100644 index 00000000..3a58d41c --- /dev/null +++ b/src/main/java/server/uckgisagi/app/scrap/service/ScrapServiceUtils.java @@ -0,0 +1,20 @@ +package server.uckgisagi.app.scrap.service; + +import org.jetbrains.annotations.NotNull; +import server.uckgisagi.common.exception.custom.NotFoundException; +import server.uckgisagi.app.scrap.domain.entity.Scrap; +import server.uckgisagi.app.scrap.domain.repository.ScrapRepository; + +import static server.uckgisagi.common.exception.ErrorResponseResult.*; + +public class ScrapServiceUtils { + + @NotNull + public static Scrap findByPostIdAndUserId(ScrapRepository scrapRepository, Long postId, Long userId) { + Scrap scrap = scrapRepository.findScrapByPostIdAndUserId(postId, userId); + if (scrap == null) { + throw new NotFoundException(String.format("์Šคํฌ๋žฉ๋˜์ง€ ์•Š์€ ๊ธ€ (%s) ์ž…๋‹ˆ๋‹ค", postId), NOT_FOUND_SCRAP_EXCEPTION); + } + return scrap; + } +} diff --git a/src/main/java/server/uckgisagi/app/store/StoreController.java b/src/main/java/server/uckgisagi/app/store/StoreController.java index 6347b0c8..b25a9f55 100644 --- a/src/main/java/server/uckgisagi/app/store/StoreController.java +++ b/src/main/java/server/uckgisagi/app/store/StoreController.java @@ -1,5 +1,6 @@ package server.uckgisagi.app.store; +import io.swagger.annotations.ApiOperation; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -8,6 +9,7 @@ import server.uckgisagi.app.store.dto.response.OneStoreResponse; import server.uckgisagi.app.store.service.StoreRetrieveService; import server.uckgisagi.common.dto.ApiSuccessResponse; +import server.uckgisagi.config.interceptor.Auth; import static server.uckgisagi.common.success.SuccessResponseResult.*; @@ -17,11 +19,15 @@ public class StoreController { private final StoreRetrieveService storeRetrieveService; + @ApiOperation("[์ธ์ฆ] ์‹ค์ฒœ์žฅ์†Œ ๋ณด๊ธฐ ํŽ˜์ด์ง€ - ๋ชจ๋“  ๋ฆฌํ•„์Šคํ…Œ์ด์…˜ ์ •๋ณด ์กฐํšŒ") + @Auth @GetMapping("/v1/store") public ApiSuccessResponse retrieveAllStore() { return ApiSuccessResponse.success(OK_RETRIEVE_ALL_STORE, storeRetrieveService.retrieveAllStore()); } + @ApiOperation("[์ธ์ฆ] ์‹ค์ฒœ์žฅ์†Œ ๋ณด๊ธฐ ํŽ˜์ด์ง€์—์„œ ๋งค์žฅ ์ •๋ณด ํด๋ฆญ ์‹œ - ๋ฆฌํ•„์Šคํ…Œ์ด์…˜ ์ƒ์„ธ ์ •๋ณด ์กฐํšŒ") + @Auth @GetMapping("/v1/store/{storeId}") public ApiSuccessResponse retrieveOneStore(@PathVariable Long storeId) { return ApiSuccessResponse.success(OK_RETRIEVE_ONE_STORE, storeRetrieveService.retrieveOneStore(storeId)); diff --git a/src/main/java/server/uckgisagi/domain/store/entity/Store.java b/src/main/java/server/uckgisagi/app/store/domain/entity/Store.java similarity index 85% rename from src/main/java/server/uckgisagi/domain/store/entity/Store.java rename to src/main/java/server/uckgisagi/app/store/domain/entity/Store.java index 2a42235b..d6abc68c 100644 --- a/src/main/java/server/uckgisagi/domain/store/entity/Store.java +++ b/src/main/java/server/uckgisagi/app/store/domain/entity/Store.java @@ -1,13 +1,12 @@ -package server.uckgisagi.domain.store.entity; +package server.uckgisagi.app.store.domain.entity; import lombok.AccessLevel; -import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import server.uckgisagi.domain.common.AuditingTimeEntity; -import server.uckgisagi.domain.review.entity.Review; -import server.uckgisagi.domain.store.entity.embedded.Coordinate; -import server.uckgisagi.domain.store.entity.enumerate.TagName; +import server.uckgisagi.common.domain.AuditingTimeEntity; +import server.uckgisagi.app.review.domain.entity.Review; +import server.uckgisagi.app.store.domain.entity.embedded.Coordinate; +import server.uckgisagi.app.store.domain.entity.enumerate.TagName; import javax.persistence.*; import java.util.ArrayList; @@ -73,5 +72,4 @@ public List getStoreTagValue() { .map(TagName::getValue) .collect(Collectors.toList()); } - } diff --git a/src/main/java/server/uckgisagi/domain/store/entity/StoreTag.java b/src/main/java/server/uckgisagi/app/store/domain/entity/StoreTag.java similarity index 93% rename from src/main/java/server/uckgisagi/domain/store/entity/StoreTag.java rename to src/main/java/server/uckgisagi/app/store/domain/entity/StoreTag.java index a319c689..e462f4f7 100644 --- a/src/main/java/server/uckgisagi/domain/store/entity/StoreTag.java +++ b/src/main/java/server/uckgisagi/app/store/domain/entity/StoreTag.java @@ -1,4 +1,4 @@ -package server.uckgisagi.domain.store.entity; +package server.uckgisagi.app.store.domain.entity; import lombok.AccessLevel; import lombok.Getter; @@ -32,5 +32,4 @@ private StoreTag(final Store store, final Tag tag) { public static StoreTag of(Store store, Tag tag) { return new StoreTag(store, tag); } - } diff --git a/src/main/java/server/uckgisagi/domain/store/entity/Tag.java b/src/main/java/server/uckgisagi/app/store/domain/entity/Tag.java similarity index 75% rename from src/main/java/server/uckgisagi/domain/store/entity/Tag.java rename to src/main/java/server/uckgisagi/app/store/domain/entity/Tag.java index b53f3c32..69172a64 100644 --- a/src/main/java/server/uckgisagi/domain/store/entity/Tag.java +++ b/src/main/java/server/uckgisagi/app/store/domain/entity/Tag.java @@ -1,9 +1,9 @@ -package server.uckgisagi.domain.store.entity; +package server.uckgisagi.app.store.domain.entity; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; -import server.uckgisagi.domain.store.entity.enumerate.TagName; +import server.uckgisagi.app.store.domain.entity.enumerate.TagName; import javax.persistence.*; @@ -19,5 +19,4 @@ public class Tag { @Enumerated(EnumType.STRING) @Column(nullable = false, length = 20) private TagName tagName; - } diff --git a/src/main/java/server/uckgisagi/domain/store/entity/embedded/Coordinate.java b/src/main/java/server/uckgisagi/app/store/domain/entity/embedded/Coordinate.java similarity index 87% rename from src/main/java/server/uckgisagi/domain/store/entity/embedded/Coordinate.java rename to src/main/java/server/uckgisagi/app/store/domain/entity/embedded/Coordinate.java index c681a792..c7af60c2 100644 --- a/src/main/java/server/uckgisagi/domain/store/entity/embedded/Coordinate.java +++ b/src/main/java/server/uckgisagi/app/store/domain/entity/embedded/Coordinate.java @@ -1,4 +1,4 @@ -package server.uckgisagi.domain.store.entity.embedded; +package server.uckgisagi.app.store.domain.entity.embedded; import lombok.*; @@ -17,5 +17,4 @@ public class Coordinate { public static Coordinate of(double xCoordinate, double yCoordinate) { return new Coordinate(xCoordinate, yCoordinate); } - -} \ No newline at end of file +} diff --git a/src/main/java/server/uckgisagi/domain/store/entity/enumerate/TagName.java b/src/main/java/server/uckgisagi/app/store/domain/entity/enumerate/TagName.java similarity index 89% rename from src/main/java/server/uckgisagi/domain/store/entity/enumerate/TagName.java rename to src/main/java/server/uckgisagi/app/store/domain/entity/enumerate/TagName.java index 1ed8a043..25310fdf 100644 --- a/src/main/java/server/uckgisagi/domain/store/entity/enumerate/TagName.java +++ b/src/main/java/server/uckgisagi/app/store/domain/entity/enumerate/TagName.java @@ -1,4 +1,4 @@ -package server.uckgisagi.domain.store.entity.enumerate; +package server.uckgisagi.app.store.domain.entity.enumerate; import lombok.AccessLevel; import lombok.Getter; @@ -26,5 +26,4 @@ public String getKey() { public String getValue() { return value; } - } diff --git a/src/main/java/server/uckgisagi/domain/store/repository/StoreRepository.java b/src/main/java/server/uckgisagi/app/store/domain/repository/StoreRepository.java similarity index 59% rename from src/main/java/server/uckgisagi/domain/store/repository/StoreRepository.java rename to src/main/java/server/uckgisagi/app/store/domain/repository/StoreRepository.java index 48334904..5467abdd 100644 --- a/src/main/java/server/uckgisagi/domain/store/repository/StoreRepository.java +++ b/src/main/java/server/uckgisagi/app/store/domain/repository/StoreRepository.java @@ -1,7 +1,7 @@ -package server.uckgisagi.domain.store.repository; +package server.uckgisagi.app.store.domain.repository; import org.springframework.data.jpa.repository.JpaRepository; -import server.uckgisagi.domain.store.entity.Store; +import server.uckgisagi.app.store.domain.entity.Store; public interface StoreRepository extends JpaRepository, StoreRepositoryCustom { } diff --git a/src/main/java/server/uckgisagi/domain/store/repository/StoreRepositoryCustom.java b/src/main/java/server/uckgisagi/app/store/domain/repository/StoreRepositoryCustom.java similarity index 57% rename from src/main/java/server/uckgisagi/domain/store/repository/StoreRepositoryCustom.java rename to src/main/java/server/uckgisagi/app/store/domain/repository/StoreRepositoryCustom.java index bbc94241..2ead1654 100644 --- a/src/main/java/server/uckgisagi/domain/store/repository/StoreRepositoryCustom.java +++ b/src/main/java/server/uckgisagi/app/store/domain/repository/StoreRepositoryCustom.java @@ -1,6 +1,6 @@ -package server.uckgisagi.domain.store.repository; +package server.uckgisagi.app.store.domain.repository; -import server.uckgisagi.domain.store.entity.Store; +import server.uckgisagi.app.store.domain.entity.Store; import java.util.List; diff --git a/src/main/java/server/uckgisagi/domain/store/repository/StoreRepositoryCustomImpl.java b/src/main/java/server/uckgisagi/app/store/domain/repository/StoreRepositoryCustomImpl.java similarity index 79% rename from src/main/java/server/uckgisagi/domain/store/repository/StoreRepositoryCustomImpl.java rename to src/main/java/server/uckgisagi/app/store/domain/repository/StoreRepositoryCustomImpl.java index 68918f97..e52b5e3c 100644 --- a/src/main/java/server/uckgisagi/domain/store/repository/StoreRepositoryCustomImpl.java +++ b/src/main/java/server/uckgisagi/app/store/domain/repository/StoreRepositoryCustomImpl.java @@ -1,12 +1,12 @@ -package server.uckgisagi.domain.store.repository; +package server.uckgisagi.app.store.domain.repository; import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; -import server.uckgisagi.domain.store.entity.Store; +import server.uckgisagi.app.store.domain.entity.Store; import java.util.List; -import static server.uckgisagi.domain.store.entity.QStore.*; +import static server.uckgisagi.app.store.domain.entity.QStore.*; @RequiredArgsConstructor public class StoreRepositoryCustomImpl implements StoreRepositoryCustom { @@ -28,5 +28,4 @@ public Store findStoreByStoreId(Long storeId) { .where(store.id.eq(storeId)) .fetchOne(); } - } diff --git a/src/main/java/server/uckgisagi/app/store/dto/response/OneStoreResponse.java b/src/main/java/server/uckgisagi/app/store/dto/response/OneStoreResponse.java index 7e43670b..8ee5bb99 100644 --- a/src/main/java/server/uckgisagi/app/store/dto/response/OneStoreResponse.java +++ b/src/main/java/server/uckgisagi/app/store/dto/response/OneStoreResponse.java @@ -1,7 +1,7 @@ package server.uckgisagi.app.store.dto.response; import lombok.*; -import server.uckgisagi.domain.store.entity.Store; +import server.uckgisagi.app.store.domain.entity.Store; import java.util.ArrayList; import java.util.List; @@ -45,5 +45,4 @@ public static OneStoreResponse from(Store store) { response.tags.addAll(store.getStoreTagValue()); return response; } - } diff --git a/src/main/java/server/uckgisagi/app/store/dto/response/PreviewStoreDto.java b/src/main/java/server/uckgisagi/app/store/dto/response/PreviewStoreDto.java index 47b0a1b1..2862ab02 100644 --- a/src/main/java/server/uckgisagi/app/store/dto/response/PreviewStoreDto.java +++ b/src/main/java/server/uckgisagi/app/store/dto/response/PreviewStoreDto.java @@ -1,7 +1,7 @@ package server.uckgisagi.app.store.dto.response; import lombok.*; -import server.uckgisagi.domain.store.entity.Store; +import server.uckgisagi.app.store.domain.entity.Store; @ToString @Getter @@ -29,5 +29,4 @@ public static PreviewStoreDto from(Store store) { .imageUrl(store.getImageUrl()) .build(); } - } diff --git a/src/main/java/server/uckgisagi/app/store/service/StoreRetrieveService.java b/src/main/java/server/uckgisagi/app/store/service/StoreRetrieveService.java index 274acce3..750483ea 100644 --- a/src/main/java/server/uckgisagi/app/store/service/StoreRetrieveService.java +++ b/src/main/java/server/uckgisagi/app/store/service/StoreRetrieveService.java @@ -6,8 +6,8 @@ import server.uckgisagi.app.store.dto.response.AllStoreResponse; import server.uckgisagi.app.store.dto.response.OneStoreResponse; import server.uckgisagi.app.store.dto.response.PreviewStoreDto; -import server.uckgisagi.domain.store.entity.Store; -import server.uckgisagi.domain.store.repository.StoreRepository; +import server.uckgisagi.app.store.domain.entity.Store; +import server.uckgisagi.app.store.domain.repository.StoreRepository; import java.util.List; import java.util.stream.Collectors; @@ -19,13 +19,15 @@ public class StoreRetrieveService { private final StoreRepository storeRepository; + private static final long LIMIT_SIZE = 5L; + public AllStoreResponse retrieveAllStore() { List allStore = storeRepository.findAllStore(); - List popularStoreResponseDto = allStore.stream().limit(5) + List popularStoreResponseDto = allStore.stream().limit(LIMIT_SIZE) .map(PreviewStoreDto::from) .collect(Collectors.toList()); - List restStoreResponseDto = allStore.stream().skip(5) + List restStoreResponseDto = allStore.stream().skip(LIMIT_SIZE) .map(PreviewStoreDto::from) .collect(Collectors.toList()); @@ -35,5 +37,4 @@ public AllStoreResponse retrieveAllStore() { public OneStoreResponse retrieveOneStore(Long storeId) { return OneStoreResponse.from(StoreServiceUtils.findByStoreId(storeRepository, storeId)); } - } diff --git a/src/main/java/server/uckgisagi/app/store/service/StoreService.java b/src/main/java/server/uckgisagi/app/store/service/StoreService.java index 582e5ec2..b61ec6cb 100644 --- a/src/main/java/server/uckgisagi/app/store/service/StoreService.java +++ b/src/main/java/server/uckgisagi/app/store/service/StoreService.java @@ -2,7 +2,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import server.uckgisagi.domain.store.repository.StoreRepository; +import server.uckgisagi.app.store.domain.repository.StoreRepository; @Service @RequiredArgsConstructor diff --git a/src/main/java/server/uckgisagi/app/store/service/StoreServiceUtils.java b/src/main/java/server/uckgisagi/app/store/service/StoreServiceUtils.java index a6e9a7d5..1f3d48bb 100644 --- a/src/main/java/server/uckgisagi/app/store/service/StoreServiceUtils.java +++ b/src/main/java/server/uckgisagi/app/store/service/StoreServiceUtils.java @@ -4,8 +4,8 @@ import lombok.NoArgsConstructor; import org.jetbrains.annotations.NotNull; import server.uckgisagi.common.exception.custom.NotFoundException; -import server.uckgisagi.domain.store.entity.Store; -import server.uckgisagi.domain.store.repository.StoreRepository; +import server.uckgisagi.app.store.domain.entity.Store; +import server.uckgisagi.app.store.domain.repository.StoreRepository; import static server.uckgisagi.common.exception.ErrorResponseResult.*; @@ -26,5 +26,4 @@ public static Store findByStoreId(StoreRepository storeRepository, Long storeId) } return store; } - } diff --git a/src/main/java/server/uckgisagi/app/user/UserController.java b/src/main/java/server/uckgisagi/app/user/UserController.java new file mode 100644 index 00000000..2ba418bc --- /dev/null +++ b/src/main/java/server/uckgisagi/app/user/UserController.java @@ -0,0 +1,41 @@ +package server.uckgisagi.app.user; + +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import server.uckgisagi.app.user.dto.response.SearchUserResponse; +import server.uckgisagi.app.user.service.UserService; +import server.uckgisagi.common.dto.ApiSuccessResponse; +import server.uckgisagi.common.success.SuccessResponseResult; +import server.uckgisagi.config.interceptor.Auth; +import server.uckgisagi.config.resolver.LoginUserId; +import springfox.documentation.annotations.ApiIgnore; + +import java.util.List; + +import static server.uckgisagi.common.success.SuccessResponseResult.OK_SEARCH_USER; + +@RestController +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + + @ApiOperation("[์ธ์ฆ] ์œ ์ € ๊ฒ€์ƒ‰ ํŽ˜์ด์ง€ - ์œ ์ € ๋‹‰๋„ค์ž„์œผ๋กœ ์ฐพ๊ธฐ") + @Auth + @GetMapping("/v1/user/search") + public ApiSuccessResponse> searchUserByNickname(@RequestParam String nickname, @ApiIgnore @LoginUserId Long userId) { + return ApiSuccessResponse.success(OK_SEARCH_USER, userService.searchUserByNickname(nickname, userId)); + } + + @ApiOperation("[์ธ์ฆ] ํšŒ์› ํƒˆํ‡ด ํŽ˜์ด์ง€ - ํšŒ์› ํƒˆํ‡ดํ•œ ์œ ์ € ์‚ญ์ œ") + @Auth + @DeleteMapping("/v1/user/delete") + public ApiSuccessResponse deleteUser(@ApiIgnore @LoginUserId Long userId) { + userService.deleteUser(userId); + return ApiSuccessResponse.success(OK_SEARCH_USER); + } +} diff --git a/src/main/java/server/uckgisagi/app/user/domain/dictionary/UserDictionary.java b/src/main/java/server/uckgisagi/app/user/domain/dictionary/UserDictionary.java new file mode 100644 index 00000000..bc6c635c --- /dev/null +++ b/src/main/java/server/uckgisagi/app/user/domain/dictionary/UserDictionary.java @@ -0,0 +1,33 @@ +package server.uckgisagi.app.user.domain.dictionary; + +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import server.uckgisagi.app.user.domain.entity.User; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public class UserDictionary { + + /** + * ์œ ์ €์˜ id ๊ฐ’์œผ๋กœ ํ•ด๋‹น ์œ ์ €๋ฅผ ์ €์žฅํ•˜๋Š” dictionary + */ + private final Map dictionary; + + public static UserDictionary from(List users) { + return new UserDictionary( + users.stream() + .collect( + Collectors.toMap( + user -> user.getId(), + user -> user + ) + )); + } + + public User getUserByUserId(Long userId) { + return dictionary.get(userId); + } +} diff --git a/src/main/java/server/uckgisagi/domain/user/entity/Token.java b/src/main/java/server/uckgisagi/app/user/domain/entity/Token.java similarity index 87% rename from src/main/java/server/uckgisagi/domain/user/entity/Token.java rename to src/main/java/server/uckgisagi/app/user/domain/entity/Token.java index 8735fc57..9171d8b3 100644 --- a/src/main/java/server/uckgisagi/domain/user/entity/Token.java +++ b/src/main/java/server/uckgisagi/app/user/domain/entity/Token.java @@ -1,9 +1,9 @@ -package server.uckgisagi.domain.user.entity; +package server.uckgisagi.app.user.domain.entity; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; -import server.uckgisagi.domain.common.AuditingTimeEntity; +import server.uckgisagi.common.domain.AuditingTimeEntity; import javax.persistence.*; @@ -31,5 +31,4 @@ private Token(final Long userId, final String fcmToken) { public static Token newInstance(Long userId, String fcmToken) { return new Token(userId, fcmToken); } - } diff --git a/src/main/java/server/uckgisagi/app/user/domain/entity/User.java b/src/main/java/server/uckgisagi/app/user/domain/entity/User.java new file mode 100644 index 00000000..a6ff411a --- /dev/null +++ b/src/main/java/server/uckgisagi/app/user/domain/entity/User.java @@ -0,0 +1,149 @@ +package server.uckgisagi.app.user.domain.entity; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import server.uckgisagi.app.block.domain.entity.Block; +import server.uckgisagi.app.user.domain.entity.embedded.SocialInfo; +import server.uckgisagi.app.user.domain.entity.enumerate.SocialType; +import server.uckgisagi.app.user.domain.entity.enumerate.UserGrade; +import server.uckgisagi.app.user.domain.entity.enumerate.UserStatus; +import server.uckgisagi.common.domain.AuditingTimeEntity; +import server.uckgisagi.app.follow.domain.entity.Follow; +import server.uckgisagi.app.post.domain.entity.Post; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class User extends AuditingTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "user_id", nullable = false) + private Long id; + + @Embedded + private SocialInfo socialInfo; + + @Column(nullable = false) + private String nickname; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 20) + private UserGrade grade; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 10) + private UserStatus status; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "token_id") + private Token token; + + @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) + private final List posts = new ArrayList<>(); + + @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) + private final List blocks = new ArrayList<>(); + + /** + * ๋‚˜๋ฅผ ํŒ”๋กœ์šฐํ•˜๋Š” ์œ ์ € List + */ + @OneToMany(mappedBy = "follower", cascade = CascadeType.ALL, orphanRemoval = true) + private final List followers = new ArrayList<>(); // ์—ฌ๊ธฐ ์žˆ๋Š” followee ๋Š” ๋‹ค this(User), follower ๋Š” ๋‚˜๋ฅผ ํŒ”๋กœ์šฐํ•˜๋Š” ์• ๋“ค + + /** + * ๋‚ด๊ฐ€ ํŒ”๋กœ์šฐํ•˜๋Š” ์œ ์ € List + */ + @OneToMany(mappedBy = "followee", cascade = CascadeType.ALL, orphanRemoval = true) + private final List followings = new ArrayList<>(); // ์—ฌ๊ธฐ ์žˆ๋Š” followee ๋Š” ๋‹ค ๋‚ด๊ฐ€ ํŒ”๋กœ์šฐํ•˜๋Š” friend, follower ๋Š” this(User) + + private User(final String socialId, final SocialType socialType, final String nickname) { + this.socialInfo = SocialInfo.of(socialId, socialType); + this.nickname = nickname; + this.grade = UserGrade.SQUIRE; + this.status = UserStatus.ACTIVE; + } + + public static User newInstance(String socialId, SocialType socialType, String nickname) { + return new User(socialId, socialType, nickname); + } + + public void setTokenInfo(Token token) { + this.token = token; + } + + public String getUserFcmToken() { + return this.token.getFcmToken(); + } + + /** + * ๋‚˜๋ฅผ ํŒ”๋กœ์šฐํ•˜๋Š” ์œ ์ € ์ถ”๊ฐ€
+ * @param follower ๋‚˜๋ฅผ ํŒ”๋กœ์šฐํ•˜๋Š” ์œ ์ € + */ + public void addFollower(Follow follower) { + this.followers.add(follower); + } + + /** + * ๋‚ด๊ฐ€ ํŒ”๋กœ์šฐํ•˜๋Š” ์œ ์ € ์ถ”๊ฐ€
+ * @param myFollowing ๋‚ด๊ฐ€ ํŒ”๋กœ์šฐํ•˜๋Š” ์œ ์ € + */ + public void addFollowing(Follow myFollowing) { + this.followings.add(myFollowing); + } + + public List getMyFollowers() { + return this.followers.stream() + .map(Follow::getFollower) + .collect(Collectors.toList()); + } + + public List getMyFollowings() { + return this.followings.stream() + .map(Follow::getFollowee) + .collect(Collectors.toList()); + } + + public void deleteFollower(Follow follower) { + this.followers.remove(follower); + } + + public void deleteFollowing(Follow following) { + this.followings.remove(following); + } + + public void addPosts(Post post) { + this.posts.add(post); + this.changeGrade(); + } + + private void changeGrade() { + switch (this.getPosts().size()) { + case 5: + this.grade = UserGrade.BARON; + break; + case 10: + this.grade = UserGrade.EARL; + break; + case 19: + this.grade = UserGrade.DUKE; + break; + case 32: + this.grade = UserGrade.LORD; + break; + case 53: + this.grade = UserGrade.KING; + break; + } + } + + void changeNickname(String nickname) { + this.nickname = nickname; + } +} diff --git a/src/main/java/server/uckgisagi/domain/user/entity/embedded/SocialInfo.java b/src/main/java/server/uckgisagi/app/user/domain/entity/embedded/SocialInfo.java similarity index 87% rename from src/main/java/server/uckgisagi/domain/user/entity/embedded/SocialInfo.java rename to src/main/java/server/uckgisagi/app/user/domain/entity/embedded/SocialInfo.java index b129f964..519869f5 100644 --- a/src/main/java/server/uckgisagi/domain/user/entity/embedded/SocialInfo.java +++ b/src/main/java/server/uckgisagi/app/user/domain/entity/embedded/SocialInfo.java @@ -1,10 +1,10 @@ -package server.uckgisagi.domain.user.entity.embedded; +package server.uckgisagi.app.user.domain.entity.embedded; import lombok.AccessLevel; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; -import server.uckgisagi.domain.user.entity.enumerate.SocialType; +import server.uckgisagi.app.user.domain.entity.enumerate.SocialType; import javax.persistence.Column; import javax.persistence.Embeddable; @@ -32,5 +32,4 @@ private SocialInfo(final String socialId, final SocialType socialType) { public static SocialInfo of(String socialId, SocialType socialType) { return new SocialInfo(socialId, socialType); } - } diff --git a/src/main/java/server/uckgisagi/domain/user/entity/enumerate/SocialType.java b/src/main/java/server/uckgisagi/app/user/domain/entity/enumerate/SocialType.java similarity index 81% rename from src/main/java/server/uckgisagi/domain/user/entity/enumerate/SocialType.java rename to src/main/java/server/uckgisagi/app/user/domain/entity/enumerate/SocialType.java index 7145cc32..818b6e93 100644 --- a/src/main/java/server/uckgisagi/domain/user/entity/enumerate/SocialType.java +++ b/src/main/java/server/uckgisagi/app/user/domain/entity/enumerate/SocialType.java @@ -1,4 +1,4 @@ -package server.uckgisagi.domain.user.entity.enumerate; +package server.uckgisagi.app.user.domain.entity.enumerate; import lombok.AccessLevel; import lombok.Getter; @@ -13,5 +13,4 @@ public enum SocialType { ; private final String value; - } diff --git a/src/main/java/server/uckgisagi/domain/user/entity/enumerate/UserGrade.java b/src/main/java/server/uckgisagi/app/user/domain/entity/enumerate/UserGrade.java similarity index 91% rename from src/main/java/server/uckgisagi/domain/user/entity/enumerate/UserGrade.java rename to src/main/java/server/uckgisagi/app/user/domain/entity/enumerate/UserGrade.java index 7c4a2b7e..2d89d8f3 100644 --- a/src/main/java/server/uckgisagi/domain/user/entity/enumerate/UserGrade.java +++ b/src/main/java/server/uckgisagi/app/user/domain/entity/enumerate/UserGrade.java @@ -1,4 +1,4 @@ -package server.uckgisagi.domain.user.entity.enumerate; +package server.uckgisagi.app.user.domain.entity.enumerate; import lombok.AccessLevel; import lombok.Getter; @@ -33,5 +33,4 @@ public String getValue() { public int getAccumulate() { return accumulate; } - } diff --git a/src/main/java/server/uckgisagi/domain/user/entity/enumerate/UserStatus.java b/src/main/java/server/uckgisagi/app/user/domain/entity/enumerate/UserStatus.java similarity index 50% rename from src/main/java/server/uckgisagi/domain/user/entity/enumerate/UserStatus.java rename to src/main/java/server/uckgisagi/app/user/domain/entity/enumerate/UserStatus.java index d04e3226..98ddfabf 100644 --- a/src/main/java/server/uckgisagi/domain/user/entity/enumerate/UserStatus.java +++ b/src/main/java/server/uckgisagi/app/user/domain/entity/enumerate/UserStatus.java @@ -1,4 +1,4 @@ -package server.uckgisagi.domain.user.entity.enumerate; +package server.uckgisagi.app.user.domain.entity.enumerate; public enum UserStatus { ACTIVE, diff --git a/src/main/java/server/uckgisagi/app/user/domain/repository/TokenRepository.java b/src/main/java/server/uckgisagi/app/user/domain/repository/TokenRepository.java new file mode 100644 index 00000000..d0ef9702 --- /dev/null +++ b/src/main/java/server/uckgisagi/app/user/domain/repository/TokenRepository.java @@ -0,0 +1,7 @@ +package server.uckgisagi.app.user.domain.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import server.uckgisagi.app.user.domain.entity.Token; + +public interface TokenRepository extends JpaRepository, TokenRepositoryCustom { +} diff --git a/src/main/java/server/uckgisagi/app/user/domain/repository/TokenRepositoryCustom.java b/src/main/java/server/uckgisagi/app/user/domain/repository/TokenRepositoryCustom.java new file mode 100644 index 00000000..4832f890 --- /dev/null +++ b/src/main/java/server/uckgisagi/app/user/domain/repository/TokenRepositoryCustom.java @@ -0,0 +1,5 @@ +package server.uckgisagi.app.user.domain.repository; + +public interface TokenRepositoryCustom { + String findFcmTokenByUserId(Long userId); +} diff --git a/src/main/java/server/uckgisagi/app/user/domain/repository/TokenRepositoryCustomImpl.java b/src/main/java/server/uckgisagi/app/user/domain/repository/TokenRepositoryCustomImpl.java new file mode 100644 index 00000000..4f0b155c --- /dev/null +++ b/src/main/java/server/uckgisagi/app/user/domain/repository/TokenRepositoryCustomImpl.java @@ -0,0 +1,21 @@ +package server.uckgisagi.app.user.domain.repository; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; + +import static server.uckgisagi.app.user.domain.entity.QToken.*; + +@RequiredArgsConstructor +public class TokenRepositoryCustomImpl implements TokenRepositoryCustom { + + private final JPAQueryFactory query; + + @Override + public String findFcmTokenByUserId(Long userId) { + return query + .select(token.fcmToken).distinct() + .from(token) + .where(token.userId.eq(userId)) + .fetchFirst(); + } +} diff --git a/src/main/java/server/uckgisagi/app/user/domain/repository/UserRepository.java b/src/main/java/server/uckgisagi/app/user/domain/repository/UserRepository.java new file mode 100644 index 00000000..ef8bcb16 --- /dev/null +++ b/src/main/java/server/uckgisagi/app/user/domain/repository/UserRepository.java @@ -0,0 +1,7 @@ +package server.uckgisagi.app.user.domain.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import server.uckgisagi.app.user.domain.entity.User; + +public interface UserRepository extends JpaRepository, UserRepositoryCustom { +} diff --git a/src/main/java/server/uckgisagi/app/user/domain/repository/UserRepositoryCustom.java b/src/main/java/server/uckgisagi/app/user/domain/repository/UserRepositoryCustom.java new file mode 100644 index 00000000..1d99e299 --- /dev/null +++ b/src/main/java/server/uckgisagi/app/user/domain/repository/UserRepositoryCustom.java @@ -0,0 +1,14 @@ +package server.uckgisagi.app.user.domain.repository; + +import server.uckgisagi.app.user.domain.entity.enumerate.SocialType; +import server.uckgisagi.app.user.domain.entity.User; + +import java.util.List; + +public interface UserRepositoryCustom { + User findUserByUserId(Long userId); + List findAllUserByNickname(String nickname, List blockUserIds); + User findUserBySocialIdAndSocialType(String socialId, SocialType socialType); + boolean existsBySocialIdAndSocialType(String socialId, SocialType socialType); + boolean existsByUserId(Long userId); +} diff --git a/src/main/java/server/uckgisagi/app/user/domain/repository/UserRepositoryCustomImpl.java b/src/main/java/server/uckgisagi/app/user/domain/repository/UserRepositoryCustomImpl.java new file mode 100644 index 00000000..f653df62 --- /dev/null +++ b/src/main/java/server/uckgisagi/app/user/domain/repository/UserRepositoryCustomImpl.java @@ -0,0 +1,65 @@ +package server.uckgisagi.app.user.domain.repository; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import server.uckgisagi.app.user.domain.entity.enumerate.SocialType; +import server.uckgisagi.app.user.domain.entity.User; + +import java.util.List; + +import static server.uckgisagi.app.user.domain.entity.QUser.*; + +@RequiredArgsConstructor +public class UserRepositoryCustomImpl implements UserRepositoryCustom { + + private final JPAQueryFactory query; + + @Override + public User findUserByUserId(Long userId) { + return query + .selectFrom(user) + .where(user.id.eq(userId)) + .fetchOne(); + } + + @Override + public List findAllUserByNickname(String nickname, List blockUserIds) { + return query + .selectFrom(user).distinct() + .where( + user.nickname.contains(nickname), + user.id.notIn(blockUserIds) + ) + .fetch(); + } + + @Override + public User findUserBySocialIdAndSocialType(String socialId, SocialType socialType) { + return query + .selectFrom(user) + .where( + user.socialInfo.socialId.eq(socialId), + user.socialInfo.socialType.eq(socialType) + ).fetchOne(); + } + + @Override + public boolean existsBySocialIdAndSocialType(String socialId, SocialType socialType) { + return query + .selectOne() + .from(user) + .where( + user.socialInfo.socialId.eq(socialId), + user.socialInfo.socialType.eq(socialType) + ).fetchFirst() != null; + } + + @Override + public boolean existsByUserId(Long userId) { + return query + .selectOne() + .from(user) + .where(user.id.eq(userId)) + .fetchFirst() != null; + } +} diff --git a/src/main/java/server/uckgisagi/app/user/dto/request/CreateUserDto.java b/src/main/java/server/uckgisagi/app/user/dto/request/CreateUserDto.java new file mode 100644 index 00000000..40162fb7 --- /dev/null +++ b/src/main/java/server/uckgisagi/app/user/dto/request/CreateUserDto.java @@ -0,0 +1,21 @@ +package server.uckgisagi.app.user.dto.request; + +import lombok.*; +import server.uckgisagi.app.user.domain.entity.enumerate.SocialType; + +@ToString +@Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class CreateUserDto { + + private String nickname; + private String socialId; + private SocialType socialType; + private String fcmToken; + + public static CreateUserDto of(String nickname, String socialId, SocialType socialType, String fcmToken) { + return new CreateUserDto(nickname, socialId, socialType, fcmToken); + } + +} diff --git a/src/main/java/server/uckgisagi/app/user/dto/response/FollowStatus.java b/src/main/java/server/uckgisagi/app/user/dto/response/FollowStatus.java new file mode 100644 index 00000000..128d4ca2 --- /dev/null +++ b/src/main/java/server/uckgisagi/app/user/dto/response/FollowStatus.java @@ -0,0 +1,25 @@ +package server.uckgisagi.app.user.dto.response; + +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import server.uckgisagi.common.model.EnumModel; + +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public enum FollowStatus implements EnumModel { + + ACTIVE("ํŒ”๋กœ์ž‰"), + INACTIVE("ํŒ”๋กœ์šฐ"), + ; + + private final String value; + + @Override + public String getKey() { + return name(); + } + + @Override + public String getValue() { + return value; + } +} diff --git a/src/main/java/server/uckgisagi/app/user/dto/response/SearchUserResponse.java b/src/main/java/server/uckgisagi/app/user/dto/response/SearchUserResponse.java new file mode 100644 index 00000000..6e7f3e88 --- /dev/null +++ b/src/main/java/server/uckgisagi/app/user/dto/response/SearchUserResponse.java @@ -0,0 +1,34 @@ +package server.uckgisagi.app.user.dto.response; + +import lombok.*; +import server.uckgisagi.app.user.domain.entity.User; +import server.uckgisagi.app.user.domain.entity.enumerate.UserGrade; + +@ToString +@Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class SearchUserResponse { + + private Long userId; + private String nickname; + private UserGrade grade; + private FollowStatus followStatus; + + @Builder(access = AccessLevel.PACKAGE) + private SearchUserResponse(Long userId, String nickname, UserGrade grade, FollowStatus followStatus) { + this.userId = userId; + this.nickname = nickname; + this.grade = grade; + this.followStatus = followStatus; + } + + public static SearchUserResponse of(User user, FollowStatus followStatus) { + return SearchUserResponse.builder() + .userId(user.getId()) + .nickname(user.getNickname()) + .grade(user.getGrade()) + .followStatus(followStatus) + .build(); + } + +} diff --git a/src/main/java/server/uckgisagi/app/user/service/UserService.java b/src/main/java/server/uckgisagi/app/user/service/UserService.java new file mode 100644 index 00000000..4c6f66d2 --- /dev/null +++ b/src/main/java/server/uckgisagi/app/user/service/UserService.java @@ -0,0 +1,58 @@ +package server.uckgisagi.app.user.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import server.uckgisagi.app.user.dto.request.CreateUserDto; +import server.uckgisagi.app.user.dto.response.FollowStatus; +import server.uckgisagi.app.user.dto.response.SearchUserResponse; +import server.uckgisagi.app.block.domain.entity.Block; +import server.uckgisagi.app.follow.domain.repository.FollowRepository; +import server.uckgisagi.app.user.domain.entity.Token; +import server.uckgisagi.app.user.domain.entity.User; +import server.uckgisagi.app.user.domain.repository.TokenRepository; +import server.uckgisagi.app.user.domain.repository.UserRepository; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class UserService { + + private final UserRepository userRepository; + private final TokenRepository tokenRepository; + private final FollowRepository followRepository; + + @Transactional + public User registerUser(CreateUserDto requestDto) { + UserServiceUtils.validateNotExistsUser(userRepository, requestDto.getSocialId(), requestDto.getSocialType()); + User user = userRepository.save(User.newInstance(requestDto.getSocialId(), requestDto.getSocialType(), requestDto.getNickname())); + user.setTokenInfo(tokenRepository.save(Token.newInstance(user.getId(), requestDto.getFcmToken()))); + return user; + } + + @Transactional(readOnly = true) + public List searchUserByNickname(String nickname, Long userId) { + User me = UserServiceUtils.findByUserId(userRepository, userId); + List myFollowings = followRepository.findMyFollowingUserByUserId(userId); + List blockedUserIds = me.getBlocks().stream() + .map(Block::getBlockUserId) + .collect(Collectors.toList()); + + return userRepository + .findAllUserByNickname(nickname, blockedUserIds).stream() + .filter(user -> !Objects.equals(user, me)) // JPA Entity ์— equals, hashCode ์˜ค๋ฒ„๋ผ์ด๋”ฉ์ด ์˜ณ์€๊ฐ€? + .map(user -> myFollowings.contains(user) + ? SearchUserResponse.of(user, FollowStatus.ACTIVE) + : SearchUserResponse.of(user, FollowStatus.INACTIVE) + ) + .collect(Collectors.toList()); + } + + @Transactional + public void deleteUser(Long userId) { + userRepository.delete(UserServiceUtils.findByUserId(userRepository, userId)); + } +} diff --git a/src/main/java/server/uckgisagi/app/user/service/UserServiceUtils.java b/src/main/java/server/uckgisagi/app/user/service/UserServiceUtils.java new file mode 100644 index 00000000..9e0aa699 --- /dev/null +++ b/src/main/java/server/uckgisagi/app/user/service/UserServiceUtils.java @@ -0,0 +1,41 @@ +package server.uckgisagi.app.user.service; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.jetbrains.annotations.NotNull; +import server.uckgisagi.common.exception.custom.ConflictException; +import server.uckgisagi.common.exception.custom.NotFoundException; +import server.uckgisagi.app.user.domain.entity.User; +import server.uckgisagi.app.user.domain.entity.enumerate.SocialType; +import server.uckgisagi.app.user.domain.repository.UserRepository; + +import static server.uckgisagi.common.exception.ErrorResponseResult.*; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class UserServiceUtils { + + static void validateNotExistsUser(UserRepository userRepository, String socialId, SocialType socialType) { + if (userRepository.existsBySocialIdAndSocialType(socialId, socialType)) { + throw new ConflictException(String.format("์ด๋ฏธ ์กด์žฌํ•˜๋Š” ์œ ์ € (%s - %s) ์ž…๋‹ˆ๋‹ค", socialId, socialType), CONFLICT_ALREADY_EXIST_USER_EXCEPTION); + } + } + + public static void validateExistUser(UserRepository userRepository, Long userId) { + if (!userRepository.existsByUserId(userId)) { + throw new NotFoundException(String.format("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ € (%s) ์ž…๋‹ˆ๋‹ค", userId), NOT_FOUND_USER_EXCEPTION); + } + } + + @NotNull + public static User findByUserId(UserRepository userRepository, Long userId) { + User user = userRepository.findUserByUserId(userId); + if (user == null) { + throw new NotFoundException(String.format("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ € (%s) ์ž…๋‹ˆ๋‹ค", userId), NOT_FOUND_USER_EXCEPTION); + } + return user; + } + + public static User findUserBySocialIdAndSocialType(UserRepository userRepository, String socialId, SocialType socialType) { + return userRepository.findUserBySocialIdAndSocialType(socialId, socialType); + } +} diff --git a/src/main/java/server/uckgisagi/domain/common/AuditingTimeEntity.java b/src/main/java/server/uckgisagi/common/domain/AuditingTimeEntity.java similarity index 85% rename from src/main/java/server/uckgisagi/domain/common/AuditingTimeEntity.java rename to src/main/java/server/uckgisagi/common/domain/AuditingTimeEntity.java index 18b0e8eb..21971bfd 100644 --- a/src/main/java/server/uckgisagi/domain/common/AuditingTimeEntity.java +++ b/src/main/java/server/uckgisagi/common/domain/AuditingTimeEntity.java @@ -1,4 +1,4 @@ -package server.uckgisagi.domain.common; +package server.uckgisagi.common.domain; import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Getter; @@ -23,4 +23,7 @@ public class AuditingTimeEntity { @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone = "Asia/Seoul") private LocalDateTime updatedAt; + public void setTestCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } } diff --git a/src/main/java/server/uckgisagi/common/dto/ApiSuccessResponse.java b/src/main/java/server/uckgisagi/common/dto/ApiSuccessResponse.java index 97101101..0f282a9b 100644 --- a/src/main/java/server/uckgisagi/common/dto/ApiSuccessResponse.java +++ b/src/main/java/server/uckgisagi/common/dto/ApiSuccessResponse.java @@ -17,7 +17,11 @@ public class ApiSuccessResponse { private T data; public static ApiSuccessResponse success(T data) { - return new ApiSuccessResponse<>(SuccessStatusCode.OK, "", null); + return new ApiSuccessResponse<>(SuccessStatusCode.OK, "", data); + } + + public static ApiSuccessResponse success(SuccessResponseResult responseResult) { + return new ApiSuccessResponse<>(responseResult.getStatusCode(), responseResult.getMessage(), null); } public static ApiSuccessResponse success(SuccessResponseResult responseResult, T data) { diff --git a/src/main/java/server/uckgisagi/common/dto/AuditingTimeResponse.java b/src/main/java/server/uckgisagi/common/dto/AuditingTimeResponse.java new file mode 100644 index 00000000..88de5125 --- /dev/null +++ b/src/main/java/server/uckgisagi/common/dto/AuditingTimeResponse.java @@ -0,0 +1,31 @@ +package server.uckgisagi.common.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import server.uckgisagi.common.domain.AuditingTimeEntity; + +import java.time.LocalDateTime; + +@ToString +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public abstract class AuditingTimeResponse { + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone = "Asia/Seoul") + private LocalDateTime createdAt; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone = "Asia/Seoul") + private LocalDateTime updatedAt; + + protected void setBaseTime(AuditingTimeEntity auditingTimeEntity) { + this.createdAt = auditingTimeEntity.getCreatedAt(); + this.updatedAt = auditingTimeEntity.getUpdatedAt(); + } + + protected void setCreatedTime(AuditingTimeEntity auditingTimeEntity) { + this.createdAt = auditingTimeEntity.getCreatedAt(); + } +} diff --git a/src/main/java/server/uckgisagi/common/exception/ErrorResponseResult.java b/src/main/java/server/uckgisagi/common/exception/ErrorResponseResult.java index 52d42046..de79495f 100644 --- a/src/main/java/server/uckgisagi/common/exception/ErrorResponseResult.java +++ b/src/main/java/server/uckgisagi/common/exception/ErrorResponseResult.java @@ -30,6 +30,9 @@ public enum ErrorResponseResult { NOT_FOUND_EXCEPTION(NOT_FOUND, "์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค"), NOT_FOUND_STORE_EXCEPTION(NOT_FOUND, "ํ•ด๋‹น ๋ฆฌํ•„ ์Šคํ…Œ์ด์…˜์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค"), NOT_FOUND_USER_EXCEPTION(NOT_FOUND, "ํƒˆํ‡ดํ•˜๊ฑฐ๋‚˜ ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ €์ž…๋‹ˆ๋‹ค"), + NOT_FOUND_POST_EXCEPTION(NOT_FOUND, "์กด์žฌํ•˜์ง€ ์•Š๋Š” ๊ฒŒ์‹œ๊ธ€์ž…๋‹ˆ๋‹ค"), + NOT_FOUND_SCRAP_EXCEPTION(NOT_FOUND, "์Šคํฌ๋žฉ๋˜์ง€ ์•Š์€ ๊ฒŒ์‹œ๊ธ€์ž…๋‹ˆ๋‹ค"), + NOT_FOUND_FOLLOW_RELATION_EXCEPTION(NOT_FOUND, "์กด์žฌํ•˜์ง€ ์•Š๋Š” ํŒ”๋กœ์šฐ ๊ด€๊ณ„์ž…๋‹ˆ๋‹ค"), // 405 Method Not Allowed METHOD_NOT_ALLOWED_EXCEPTION(METHOD_NOT_ALLOWED, "์ง€์›ํ•˜์ง€ ์•Š๋Š” ๋ฉ”์†Œ๋“œ ์ž…๋‹ˆ๋‹ค"), @@ -41,6 +44,7 @@ public enum ErrorResponseResult { CONFLICT_EXCEPTION(CONFLICT, "์ด๋ฏธ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค"), CONFLICT_ALREADY_EXIST_USER_EXCEPTION(CONFLICT, "์ด๋ฏธ ์กด์žฌํ•˜๋Š” ์œ ์ €์ž…๋‹ˆ๋‹ค"), CONFLICT_ALREADY_EXIST_STORE_EXCEPTION(CONFLICT, "์ด๋ฏธ ์กด์žฌํ•˜๋Š” ๋ฆฌํ•„์Šคํ…Œ์ด์…˜์ž…๋‹ˆ๋‹ค"), + CONFLICT_ALREADY_EXIST_FOLLOW_EXCEPTION(CONFLICT, "์ด๋ฏธ ํŒ”๋กœ์šฐ์ค‘์ธ ์œ ์ €์ž…๋‹ˆ๋‹ค"), // 415 Unsupported Media Type UNSUPPORTED_MEDIA_TYPE_EXCEPTION(UNSUPPORTED_MEDIA_TYPE, "ํ•ด๋‹นํ•˜๋Š” ๋ฏธ๋””์–ด ํƒ€์ž…์„ ์ง€์›ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."), diff --git a/src/main/java/server/uckgisagi/common/exception/custom/BusinessException.java b/src/main/java/server/uckgisagi/common/exception/custom/BusinessException.java new file mode 100644 index 00000000..829a72dd --- /dev/null +++ b/src/main/java/server/uckgisagi/common/exception/custom/BusinessException.java @@ -0,0 +1,9 @@ +package server.uckgisagi.common.exception.custom; + +import server.uckgisagi.common.exception.ErrorResponseResult; + +public class BusinessException extends UckGiSaGiException{ + public BusinessException(String message, ErrorResponseResult errorResponseResult) { + super(message, errorResponseResult); + } +} diff --git a/src/main/java/server/uckgisagi/common/success/SuccessResponseResult.java b/src/main/java/server/uckgisagi/common/success/SuccessResponseResult.java index e8e1d36c..a0a4e297 100644 --- a/src/main/java/server/uckgisagi/common/success/SuccessResponseResult.java +++ b/src/main/java/server/uckgisagi/common/success/SuccessResponseResult.java @@ -13,12 +13,17 @@ public enum SuccessResponseResult { // 200 OK SUCCESS_OK(OK, ""), OK_RETRIEVE_USER_INFO(OK, "์œ ์ €์˜ ์ •๋ณด๋ฅผ ์กฐํšŒํ–ˆ์Šต๋‹ˆ๋‹ค"), + OK_SEARCH_USER(OK, "๋‹‰๋„ค์ž„์œผ๋กœ ์œ ์ €๋ฅผ ๊ฒ€์ƒ‰ํ–ˆ์Šต๋‹ˆ๋‹ค"), OK_RETRIEVE_ONE_STORE(OK, "๋ฆฌํ•„ ์Šคํ…Œ์ด์…˜์„ ์กฐํšŒํ–ˆ์Šต๋‹ˆ๋‹ค"), OK_RETRIEVE_ALL_STORE(OK, "๋ชจ๋“  ๋ฆฌํ•„ ์Šคํ…Œ์ด์…˜์„ ์กฐํšŒํ–ˆ์Šต๋‹ˆ๋‹ค"), OK_RETRIEVE_STORE_REVIEW(OK, "๋ฆฌํ•„ ์Šคํ…Œ์ด์…˜์˜ ํ›„๊ธฐ๋ฅผ ์กฐํšŒํ–ˆ์Šต๋‹ˆ๋‹ค"), OK_SEARCH_STORE(OK, "๋ฆฌํ•„ ์Šคํ…Œ์ด์…˜์˜ ์œ„์น˜๋ฅผ ์กฐํšŒํ–ˆ์Šต๋‹ˆ๋‹ค"), - OK_SEARCH_MY_POST(OK, "๋‚˜์˜ ์ธ์ฆ ๊ธ€์„ ์กฐํšŒํ–ˆ์Šต๋‹ˆ๋‹ค"), - OK_SEARCH_ALL_POST(OK, "๋ชจ๋“  ์ธ์ฆ ๊ธ€์„ ์กฐํšŒํ–ˆ์Šต๋‹ˆ๋‹ค"), + OK_SEARCH_POST(OK, "์ฑŒ๋ฆฐ์ง€ ๊ธ€์„ ์กฐํšŒํ–ˆ์Šต๋‹ˆ๋‹ค"), + OK_SEARCH_MY_SCRAP_POST(OK, "์Šคํฌ๋žฉํ•œ ์ฑŒ๋ฆฐ์ง€ ๊ธ€์„ ๋ชจ๋‘ ์กฐํšŒํ–ˆ์Šต๋‹ˆ๋‹ค"), + OK_SEARCH_MY_SCRAP_POST_DETAIL(OK, "์Šคํฌ๋žฉํ•œ ์ฑŒ๋ฆฐ์ง€ ๊ธ€์„ ์กฐํšŒํ–ˆ์Šต๋‹ˆ๋‹ค"), + OK_SEARCH_ALL_POST(OK, "๋ชจ๋“  ์ฑŒ๋ฆฐ์ง€ ๊ธ€์„ ์กฐํšŒํ–ˆ์Šต๋‹ˆ๋‹ค"), + OK_SEARCH_MY_HOME_CONTENTS(OK, "๋‚˜์˜ ํ™ˆ ๋ทฐ๋ฅผ ์กฐํšŒํ–ˆ์Šต๋‹ˆ๋‹ค"), + OK_SEARCH_FRIEND_HOME_CONTENTS(OK, "์นœ๊ตฌ์˜ ํ™ˆ ๋ทฐ๋ฅผ ์กฐํšŒํ–ˆ์Šต๋‹ˆ๋‹ค"), // 201 CREATED SUCCESS_CREATED(CREATED, ""), @@ -28,12 +33,21 @@ public enum SuccessResponseResult { CREATED_STORE(CREATED, "์ƒˆ๋กœ์šด ๋ฆฌํ•„ ์Šคํ…Œ์ด์…˜์ด ๋“ฑ๋ก๋˜์—ˆ์Šต๋‹ˆ๋‹ค"), CREATED_CERTIFICATION_POST(CREATED, "์ƒˆ๋กœ์šด ์ธ์ฆ ํฌ์ŠคํŠธ๊ฐ€ ๋“ฑ๋ก๋˜์—ˆ์Šต๋‹ˆ๋‹ค"), CREATED_UPDATE_STORE(CREATED, "๋ฆฌํ•„ ์Šคํ…Œ์ด์…˜ ์ •๋ณด๊ฐ€ ์ˆ˜์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค"), + CREATED_NOTIFICATION(CREATED, "์•Œ๋žŒ์ด ์„ฑ๊ณต์ ์œผ๋กœ ์ „์†ก๋˜์—ˆ์Šต๋‹ˆ๋‹ค"), + CREATED_ACCUSE_POST(CREATED, "์‹ ๊ณ ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์ ‘์ˆ˜๋˜์—ˆ์Šต๋‹ˆ๋‹ค."), // 202 ACCEPTED SUCCESS_ACCEPTED(ACCEPTED, ""), // 204 NOT_CONTENT - SUCCESS_NO_CONTENT(NO_CONTENT, "") + SUCCESS_NO_CONTENT(NO_CONTENT, ""), + NO_CONTENT_DELETE_POST(NO_CONTENT, "๊ฒŒ์‹œ๋ฌผ ์‚ญ์ œ์— ์„ฑ๊ณตํ–ˆ์Šต๋‹ˆ๋‹ค."), + NO_CONTENT_UNFOLLOW_USER(NO_CONTENT, "์–ธํŒ”๋กœ์šฐ์— ์„ฑ๊ณตํ–ˆ์Šต๋‹ˆ๋‹ค"), + NO_CONTENT_SCRAP_POST(NO_CONTENT, "์ฑŒ๋ฆฐ์ง€ ๊ธ€ ์Šคํฌ๋žฉ์— ์„ฑ๊ณตํ–ˆ์Šต๋‹ˆ๋‹ค"), + NO_CONTENT_CANCEL_SCRAP_POST(NO_CONTENT, "์ฑŒ๋ฆฐ์ง€ ๊ธ€ ์Šคํฌ๋žฉ ์ทจ์†Œ์— ์„ฑ๊ณตํ–ˆ์Šต๋‹ˆ๋‹ค"), + NO_CONTENT_BLOCK_USER(NO_CONTENT, "์œ ์ € ์ฐจ๋‹จ์— ์„ฑ๊ณตํ–ˆ์Šต๋‹ˆ๋‹ค"), + NO_CONTENT_CANCEL_BLOCK_USER(NO_CONTENT, "์œ ์ € ์ฐจ๋‹จ ํ•ด์ œ์— ์„ฑ๊ณตํ–ˆ์Šต๋‹ˆ๋‹ค") + ; private final SuccessStatusCode statusCode; diff --git a/src/main/java/server/uckgisagi/common/type/FileContentType.java b/src/main/java/server/uckgisagi/common/type/FileContentType.java new file mode 100644 index 00000000..d131fd6f --- /dev/null +++ b/src/main/java/server/uckgisagi/common/type/FileContentType.java @@ -0,0 +1,31 @@ +package server.uckgisagi.common.type; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import server.uckgisagi.common.exception.custom.ValidationException; + +import static server.uckgisagi.common.exception.ErrorResponseResult.*; + +@Getter +@RequiredArgsConstructor +public enum FileContentType { + + IMAGE("image"), + ; + + private final String prefix; + + public void validateAvailableContentType(String contentType) { + if (contentType != null && contentType.contains(SEPARATOR) && prefix.equals(getContentTypePrefix(contentType))) { + return; + } + throw new ValidationException(String.format("ํ—ˆ์šฉ๋˜์ง€ ์•Š์€ ํŒŒ์ผ ํ˜•์‹ (%s) ์ž…๋‹ˆ๋‹ค", contentType), FORBIDDEN_FILE_TYPE_EXCEPTION); + } + + private static String getContentTypePrefix(String contentType) { + return contentType.split(SEPARATOR)[0]; + } + + private static final String SEPARATOR = "/"; + +} diff --git a/src/main/java/server/uckgisagi/common/type/FileType.java b/src/main/java/server/uckgisagi/common/type/FileType.java new file mode 100644 index 00000000..0c216517 --- /dev/null +++ b/src/main/java/server/uckgisagi/common/type/FileType.java @@ -0,0 +1,42 @@ +package server.uckgisagi.common.type; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import server.uckgisagi.common.exception.custom.ValidationException; +import server.uckgisagi.common.util.FileUtils; +import server.uckgisagi.common.util.UuidUtils; + +import static server.uckgisagi.common.exception.ErrorResponseResult.*; + +@Getter +@RequiredArgsConstructor +public enum FileType { + + STORE_IMAGE("๋ฆฌํ•„ ์Šคํ…Œ์ด์…˜ ๋งค์žฅ ์ด๋ฏธ์ง€", "uckgisagi-image/store/", FileContentType.IMAGE), + POST_IMAGE("(์œ ์ €) ์ธ์ฆ ์ด๋ฏธ์ง€", "uckgisagi-image/certification/", FileContentType.IMAGE), + ; + + private final String description; + private final String directory; + private final FileContentType contentType; + + public void validateAvailableContentType(String contentType) { + this.contentType.validateAvailableContentType(contentType); + } + + /** + * ํŒŒ์ผ์˜ ๊ธฐ์กด์˜ ํ™•์žฅ์ž๋ฅผ ์œ ์ง€ํ•œ ์ฑ„, ์œ ๋‹ˆํฌํ•œ ํŒŒ์ผ์˜ ์ด๋ฆ„์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + public String createUniqueFileNameWithExtension(String originFileName) { + if (originFileName == null) { + throw new ValidationException("์ž˜๋ชป๋œ ํŒŒ์ผ์˜ originFilename ์ž…๋‹ˆ๋‹ค", FORBIDDEN_FILE_TYPE_EXCEPTION); + } + String extension = FileUtils.getFileExtension(originFileName); + return getFileNameWithDirectory(UuidUtils.generate().concat(extension)); + } + + private String getFileNameWithDirectory(String fileName) { + return this.directory.concat(fileName); + } + +} diff --git a/src/main/java/server/uckgisagi/common/util/FileUtils.java b/src/main/java/server/uckgisagi/common/util/FileUtils.java new file mode 100644 index 00000000..97e32fd5 --- /dev/null +++ b/src/main/java/server/uckgisagi/common/util/FileUtils.java @@ -0,0 +1,28 @@ +package server.uckgisagi.common.util; + +import server.uckgisagi.common.exception.custom.ValidationException; + +import static server.uckgisagi.common.exception.ErrorResponseResult.*; + +public class FileUtils { + + /** + * ํŒŒ์ผ์˜ ํ™•์žฅ์ž๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
+ * ์ž˜๋ชป๋œ ํŒŒ์ผ์˜ ํ™•์žฅ์ž์ธ๊ฒฝ์šฐ throws ValidationException + * + * @param fileName ex) image.png + * @return ex) .png + */ + public static String getFileExtension(String fileName) { + try { + String extension = fileName.substring(fileName.lastIndexOf(".")); + if (extension.length() < 2) { + throw new ValidationException(String.format("์ž˜๋ชป๋œ ํ™•์žฅ์ž ํ˜•์‹์˜ ํŒŒ์ผ (%s) ์ž…๋‹ˆ๋‹ค", fileName), FORBIDDEN_FILE_TYPE_EXCEPTION); + } + return extension; + } catch (StringIndexOutOfBoundsException e) { + throw new ValidationException(String.format("์ž˜๋ชป๋œ ํ™•์žฅ์ž ํ˜•์‹์˜ ํŒŒ์ผ (%s) ์ž…๋‹ˆ๋‹ค", fileName), FORBIDDEN_FILE_TYPE_EXCEPTION); + } + } + +} diff --git a/src/main/java/server/uckgisagi/common/util/HttpHeaderUtils.java b/src/main/java/server/uckgisagi/common/util/HttpHeaderUtils.java new file mode 100644 index 00000000..b8717266 --- /dev/null +++ b/src/main/java/server/uckgisagi/common/util/HttpHeaderUtils.java @@ -0,0 +1,16 @@ +package server.uckgisagi.common.util; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class HttpHeaderUtils { + + public static final String AUTH_HEADER = "Authorization"; + public static final String BEARER_TOKEN = "Bearer "; + + public static String withBearerToken(String token) { + return BEARER_TOKEN.concat(token); + } + +} diff --git a/src/main/java/server/uckgisagi/common/util/JwtUtils.java b/src/main/java/server/uckgisagi/common/util/JwtUtils.java new file mode 100644 index 00000000..7d70a186 --- /dev/null +++ b/src/main/java/server/uckgisagi/common/util/JwtUtils.java @@ -0,0 +1,83 @@ +package server.uckgisagi.common.util; + +import io.jsonwebtoken.*; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.PropertySource; +import org.springframework.stereotype.Component; +import server.uckgisagi.app.auth.dto.response.TokenResponse; +import server.uckgisagi.config.security.JwtConstants; + +import java.security.Key; +import java.util.Date; + +@Slf4j +@Component +@PropertySource(value = "classpath:application-jwt.yml", factory = YamlPropertySourceFactory.class, ignoreResourceNotFound = true) +public class JwtUtils { + + private static final long ACCESS_TOKEN_EXPIRES_TIME = 14 * 24 * 2 * 30 * 60 * 1000L; // 30๋ถ„ + private static final long REFRESH_TOKEN_EXPIRES_TIME = 30 * 24 * 60 * 60 * 1000L; // 7์ผ + + private final Key secretKey; + + public JwtUtils(@Value("${jwt.secret}") String secretKey) { + byte[] keyBytes = Decoders.BASE64.decode(secretKey); + this.secretKey = Keys.hmacShaKeyFor(keyBytes); + } + + public TokenResponse createTokenByUserId(Long userId) { + long now = (new Date()).getTime(); + Date accessTokenExpiresIn = new Date(now + ACCESS_TOKEN_EXPIRES_TIME); + Date refreshTokenExpiresIn = new Date(now + REFRESH_TOKEN_EXPIRES_TIME); + + // accessToken ์ƒ์„ฑ + String accessToken = Jwts.builder() + .claim(JwtConstants.USER_ID, userId) + .setExpiration(accessTokenExpiresIn) + .signWith(secretKey, SignatureAlgorithm.HS512) + .compact(); + + String refreshToken = Jwts.builder() + .setExpiration(refreshTokenExpiresIn) + .signWith(secretKey, SignatureAlgorithm.HS512) + .compact(); + + return TokenResponse.of(accessToken, refreshToken); + } + + public boolean validateToken(String token) { + try { + Jwts.parserBuilder() + .setSigningKey(secretKey).build() + .parseClaimsJws(token); + return true; + } catch (SecurityException | MalformedJwtException e) { + log.info("Invalid JWT Token", e); + } catch (ExpiredJwtException e) { + log.info("Expired JWT Token", e); + } catch (UnsupportedJwtException e) { + log.info("Unsupported JWT Token", e); + } catch (IllegalArgumentException e) { + log.info("JWT claims string is empty", e); + } + return false; + } + + public Long getUserIdFromJwt(String accessToken) { + return parseClaims(accessToken).get(JwtConstants.USER_ID, Long.class); + } + + private Claims parseClaims(String accessToken) { + try { + return Jwts.parserBuilder() + .setSigningKey(secretKey).build() + .parseClaimsJws(accessToken) + .getBody(); + } catch (ExpiredJwtException e) { + return e.getClaims(); + } + } +} diff --git a/src/main/java/server/uckgisagi/common/util/RandomNicknameUtils.java b/src/main/java/server/uckgisagi/common/util/RandomNicknameUtils.java new file mode 100644 index 00000000..5bc69746 --- /dev/null +++ b/src/main/java/server/uckgisagi/common/util/RandomNicknameUtils.java @@ -0,0 +1,33 @@ +package server.uckgisagi.common.util; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class RandomNicknameUtils { + + private static final List NICK = new ArrayList<>(List.of( + "๋ฒŒ๋ชฉ ํ˜„์žฅ์— ๋ˆˆ๋ฌผ ํ˜๋ฆฌ๋Š”", "์“ฐ๋ ˆ๊ธฐ ์ค๋Š”", "๋ถ„๋ฆฌ์ˆ˜๊ฑฐํ•˜๋Š”", "ํ”ผ๋ˆˆ๋ฌผ ํ˜๋ฆฌ๋Š”", + "์ž”๋ฐ˜์—†๋Š”", "๋ถ„์น ํ•˜๋Š”", "ํ…€๋ธ”๋Ÿฌ ์”ป๋Š”", "์—์ฝ”๋ฐฑ ์“ฐ๋Š”", "ํ—Œํ˜ˆํ•˜๋Š”", + "์žฌํ™œ์šฉ ํ•˜๋Š”", "์ง‘์ด ๋ฌผ์— ์ž ๊ธฐ๋Š”", "์„ค๊ฑฐ์ง€ ํ•˜๋Š”", "๊ณจ๋จธ๋ฆฌ ์•“๋Š”")); + + private static final List NAME = new ArrayList<>(List.of( + "์กฐ์˜ˆ์กฑ", "๋ชฉ๋„๋ฆฌ ๋„๋งˆ๋ฑ€", "ํŒ๋‹ค", "๋ถ๊ทน๊ณฐ", "ํŽญ๊ท„", "ํฌ๋ฉ”๋ผ๋‹ˆ์•ˆ", + "๋ชจ๊ธฐ", "๊ฐœ๋ฏธ", "ํ˜ธ๋ž‘์ด", "๊ณ ์–‘์ด", "๊ธฐ๋ฆฐ", "์‚ต", "์บฅ๊ฑฐ๋ฃจ", + "๋ชฐ๋””๋ธŒ ์ฃผ๋ฏผ", "๋Œ€ํ•™์›์ƒ")); + + public static String generate() { + shuffleRandomNickname(); + return NICK.get(0) + " " + NAME.get(0); + } + + private static void shuffleRandomNickname() { + Collections.shuffle(NICK); + Collections.shuffle(NAME); + } + +} diff --git a/src/main/java/server/uckgisagi/common/util/UuidUtils.java b/src/main/java/server/uckgisagi/common/util/UuidUtils.java new file mode 100644 index 00000000..1a155d1c --- /dev/null +++ b/src/main/java/server/uckgisagi/common/util/UuidUtils.java @@ -0,0 +1,16 @@ +package server.uckgisagi.common.util; + +import lombok.Getter; + +import java.util.UUID; + +@Getter +public class UuidUtils { + + private static final String VERSION = "v1"; + + public static String generate() { + return String.format("%s-%s", VERSION, UUID.randomUUID()); + } + +} diff --git a/src/main/java/server/uckgisagi/config/aws/S3Config.java b/src/main/java/server/uckgisagi/config/aws/S3Config.java new file mode 100644 index 00000000..feb4ed00 --- /dev/null +++ b/src/main/java/server/uckgisagi/config/aws/S3Config.java @@ -0,0 +1,35 @@ +package server.uckgisagi.config.aws; + +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3Client; +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; +import org.springframework.context.annotation.PropertySource; +import server.uckgisagi.common.util.YamlPropertySourceFactory; + +@Configuration +@PropertySource(value = "classpath:application-aws.yml", factory = YamlPropertySourceFactory.class, ignoreResourceNotFound = true) +public class S3Config { + + @Value("${cloud.aws.credentials.accessKey}") + private String accessKey; + + @Value("${cloud.aws.credentials.secretKey}") + private String secretKey; + + @Value("${cloud.aws.region.static}") + private String region; + + @Bean + public AmazonS3Client amazonS3Client() { + BasicAWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey); + return (AmazonS3Client) AmazonS3ClientBuilder.standard() + .withRegion(region) + .withCredentials(new AWSStaticCredentialsProvider(awsCredentials)) + .build(); + } + +} diff --git a/src/main/java/server/uckgisagi/config/firebase/FcmInitializer.java b/src/main/java/server/uckgisagi/config/firebase/FirebaseInitializer.java similarity index 73% rename from src/main/java/server/uckgisagi/config/firebase/FcmInitializer.java rename to src/main/java/server/uckgisagi/config/firebase/FirebaseInitializer.java index ead8058f..5c5d46b4 100644 --- a/src/main/java/server/uckgisagi/config/firebase/FcmInitializer.java +++ b/src/main/java/server/uckgisagi/config/firebase/FirebaseInitializer.java @@ -3,26 +3,29 @@ import com.google.auth.oauth2.GoogleCredentials; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; +import com.google.firebase.messaging.FirebaseMessaging; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.PropertySource; import org.springframework.core.io.ClassPathResource; import org.springframework.stereotype.Component; import server.uckgisagi.common.util.YamlPropertySourceFactory; -import javax.annotation.PostConstruct; import java.io.IOException; @Slf4j @Component @PropertySource(value = "classpath:application-firebase.yml", factory = YamlPropertySourceFactory.class, ignoreResourceNotFound = true) -public class FcmInitializer { +public class FirebaseInitializer { @Value("${firebase.fcm.config.path}") private String firebaseConfigPath; - @PostConstruct - public void initFirebaseApp() throws IOException { + private static FirebaseApp firebaseApp; + + @Bean + public void initFirebase() throws IOException { ClassPathResource resource = new ClassPathResource(firebaseConfigPath); GoogleCredentials googleCredentials = GoogleCredentials.fromStream(resource.getInputStream()); FirebaseOptions options = FirebaseOptions.builder() @@ -30,9 +33,12 @@ public void initFirebaseApp() throws IOException { .build(); if (FirebaseApp.getApps().isEmpty()) { - FirebaseApp.initializeApp(options); + firebaseApp = FirebaseApp.initializeApp(options); log.info("โœ… FirebaseApp Initialization Complete ๐Ÿš€"); } } + public static FirebaseMessaging getFirebaseMessaging() { + return FirebaseMessaging.getInstance(firebaseApp); + } } diff --git a/src/main/java/server/uckgisagi/config/interceptor/Auth.java b/src/main/java/server/uckgisagi/config/interceptor/Auth.java new file mode 100644 index 00000000..427cf42d --- /dev/null +++ b/src/main/java/server/uckgisagi/config/interceptor/Auth.java @@ -0,0 +1,11 @@ +package server.uckgisagi.config.interceptor; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Auth { +} diff --git a/src/main/java/server/uckgisagi/config/interceptor/AuthInterceptor.java b/src/main/java/server/uckgisagi/config/interceptor/AuthInterceptor.java new file mode 100644 index 00000000..7415bc3e --- /dev/null +++ b/src/main/java/server/uckgisagi/config/interceptor/AuthInterceptor.java @@ -0,0 +1,33 @@ +package server.uckgisagi.config.interceptor; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; +import server.uckgisagi.config.security.JwtConstants; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@Component +@RequiredArgsConstructor +public class AuthInterceptor implements HandlerInterceptor { + + private final LoginCheckHandler loginCheckHandler; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + if (!(handler instanceof HandlerMethod)) { + return true; + } + HandlerMethod handlerMethod = (HandlerMethod) handler; + Auth auth = handlerMethod.getMethodAnnotation(Auth.class); + if (auth == null) { + return true; + } + Long userId = loginCheckHandler.getUserId(request); + request.setAttribute(JwtConstants.USER_ID, userId); + return true; + } + +} diff --git a/src/main/java/server/uckgisagi/config/interceptor/LoginCheckHandler.java b/src/main/java/server/uckgisagi/config/interceptor/LoginCheckHandler.java new file mode 100644 index 00000000..2f6bb0b4 --- /dev/null +++ b/src/main/java/server/uckgisagi/config/interceptor/LoginCheckHandler.java @@ -0,0 +1,32 @@ +package server.uckgisagi.config.interceptor; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import server.uckgisagi.common.exception.custom.UnAuthorizedException; +import server.uckgisagi.common.util.HttpHeaderUtils; +import server.uckgisagi.common.util.JwtUtils; + +import javax.servlet.http.HttpServletRequest; + +@Component +@RequiredArgsConstructor +public class LoginCheckHandler { + + private final JwtUtils jwtProvider; + + public Long getUserId(HttpServletRequest request) { + String bearerToken = request.getHeader(HttpHeaderUtils.AUTH_HEADER); + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(HttpHeaderUtils.BEARER_TOKEN)) { + String accessToken = bearerToken.substring(HttpHeaderUtils.BEARER_TOKEN.length()); + if (jwtProvider.validateToken(accessToken)) { + Long userId = jwtProvider.getUserIdFromJwt(accessToken); + if (userId != null) { + return userId; + } + } + } + throw new UnAuthorizedException(String.format("์ž˜๋ชป๋œ JWT (%s) ์ž…๋‹ˆ๋‹ค", bearerToken)); + } + +} diff --git a/src/main/java/server/uckgisagi/config/resolver/LoginUserId.java b/src/main/java/server/uckgisagi/config/resolver/LoginUserId.java new file mode 100644 index 00000000..2bb04a5d --- /dev/null +++ b/src/main/java/server/uckgisagi/config/resolver/LoginUserId.java @@ -0,0 +1,11 @@ +package server.uckgisagi.config.resolver; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface LoginUserId { +} diff --git a/src/main/java/server/uckgisagi/config/resolver/LoginUserIdResolver.java b/src/main/java/server/uckgisagi/config/resolver/LoginUserIdResolver.java new file mode 100644 index 00000000..8a94798b --- /dev/null +++ b/src/main/java/server/uckgisagi/config/resolver/LoginUserIdResolver.java @@ -0,0 +1,33 @@ +package server.uckgisagi.config.resolver; + +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; +import server.uckgisagi.common.exception.custom.InternalServerException; +import server.uckgisagi.config.interceptor.Auth; +import server.uckgisagi.config.security.JwtConstants; + +@Component +public class LoginUserIdResolver implements HandlerMethodArgumentResolver { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(LoginUserId.class) && parameter.getParameterType().equals(Long.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + if (parameter.getMethodAnnotation(Auth.class) == null) { + throw new InternalServerException("@Auth ์–ด๋…ธํ…Œ์ด์…˜์ด ํ•„์š”ํ•œ ์ปจํŠธ๋กค๋Ÿฌ์ž…๋‹ˆ๋‹ค"); + } + Object object = webRequest.getAttribute(JwtConstants.USER_ID, 0); + if (object == null) { + throw new InternalServerException(String.format("USER_ID ๋ฅผ ๊ฐ€์ ธ์˜ค์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ. (%s - %s)", parameter.getClass(), parameter.getMethod())); + } + return object; + } + +} diff --git a/src/main/java/server/uckgisagi/config/security/JwtConstants.java b/src/main/java/server/uckgisagi/config/security/JwtConstants.java new file mode 100644 index 00000000..0aa00136 --- /dev/null +++ b/src/main/java/server/uckgisagi/config/security/JwtConstants.java @@ -0,0 +1,11 @@ +package server.uckgisagi.config.security; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class JwtConstants { + + public static final String USER_ID = "USER_ID"; + +} diff --git a/src/main/java/server/uckgisagi/config/security/WebSecurityConfig.java b/src/main/java/server/uckgisagi/config/security/WebSecurityConfig.java index 5d03d6b7..4f48b68f 100644 --- a/src/main/java/server/uckgisagi/config/security/WebSecurityConfig.java +++ b/src/main/java/server/uckgisagi/config/security/WebSecurityConfig.java @@ -3,10 +3,12 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; @Configuration +@EnableWebSecurity public class WebSecurityConfig { /** @@ -34,5 +36,4 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .httpStrictTransportSecurity().disable(); return http.build(); } - } diff --git a/src/main/java/server/uckgisagi/config/swagger/SwaggerConfig.java b/src/main/java/server/uckgisagi/config/swagger/SwaggerConfig.java new file mode 100644 index 00000000..dbb57611 --- /dev/null +++ b/src/main/java/server/uckgisagi/config/swagger/SwaggerConfig.java @@ -0,0 +1,110 @@ +package server.uckgisagi.config.swagger; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import springfox.bean.validators.configuration.BeanValidatorPluginsConfiguration; +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.builders.ResponseBuilder; +import springfox.documentation.service.*; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.plugins.Docket; +import springfox.documentation.swagger.web.DocExpansion; +import springfox.documentation.swagger.web.UiConfiguration; +import springfox.documentation.swagger.web.UiConfigurationBuilder; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static springfox.documentation.builders.RequestHandlerSelectors.withClassAnnotation; + +@Configuration +@EnableSwagger2 +@Import(BeanValidatorPluginsConfiguration.class) +public class SwaggerConfig implements WebMvcConfigurer { + + private static final String API_NAME = "์–ต์ง€์‚ฌ์ง€"; + private static final String API_VERSION = "0.0.1"; + private static final String API_DESCRIPTION = "์–ต์ง€์‚ฌ์ง€ API ๋ช…์„ธ์„œ"; + + // springfox-swagger-ui ๊ฒฝ๋กœ ๋“ฑ๋ก + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("/swagger-ui/**") + .addResourceLocations("classpath:/META-INF/resources/webjars/springfox-swagger-ui/"); + registry.addResourceHandler("/webjars/**") + .addResourceLocations("classpath:/META-INF/resources/webjars/springfox-swagger-ui/"); + } + + @Bean + public UiConfiguration uiConfig() { + return UiConfigurationBuilder.builder() + .docExpansion(DocExpansion.LIST) + .build(); + } + + @Bean + public Docket api() { + return new Docket(DocumentationType.SWAGGER_2) + .apiInfo(apiInfo()) + .securitySchemes(authorization()) + .ignoredParameterTypes() + .select() + .apis(withClassAnnotation(RestController.class)) + .paths(PathSelectors.ant("/**")) + .build() + .useDefaultResponseMessages(false) + .globalResponses(HttpMethod.GET, this.createGlobalResponseMessages()) + .globalResponses(HttpMethod.POST, this.createGlobalResponseMessages()) + .globalResponses(HttpMethod.PUT, this.createGlobalResponseMessages()) + .globalResponses(HttpMethod.DELETE, this.createGlobalResponseMessages()); + } + + private List createGlobalResponseMessages() { + return Stream.of( + HttpStatus.BAD_REQUEST, + HttpStatus.UNAUTHORIZED, + HttpStatus.CONFLICT, + HttpStatus.FORBIDDEN, + HttpStatus.NOT_FOUND, + HttpStatus.INTERNAL_SERVER_ERROR, + HttpStatus.BAD_GATEWAY, + HttpStatus.SERVICE_UNAVAILABLE + ) + .map(this::createResponseMessage) + .collect(Collectors.toList()); + } + + private Response createResponseMessage(HttpStatus httpStatus) { + return new ResponseBuilder() + .code(String.valueOf(httpStatus.value())) + .description(httpStatus.getReasonPhrase()) + .build(); + } + + private ApiInfo apiInfo() { + return new ApiInfoBuilder() + .title(API_NAME) + .version(API_VERSION) + .description(API_DESCRIPTION) + .contact(new Contact(CREATOR, CONTACT_URL, EMAIL)) + .build(); + } + + private List authorization() { + return List.of(new ApiKey("Authorization", "Authorization", "header")); + } + + private static final String CREATOR = "Cho Chan Woo"; + private static final String EMAIL = "chocw0402@gmail.com"; + private static final String CONTACT_URL = "https://www.instagram.com/breakfast_wu/"; + +} diff --git a/src/main/java/server/uckgisagi/config/web/WebConfig.java b/src/main/java/server/uckgisagi/config/web/WebConfig.java new file mode 100644 index 00000000..67b602ff --- /dev/null +++ b/src/main/java/server/uckgisagi/config/web/WebConfig.java @@ -0,0 +1,30 @@ +package server.uckgisagi.config.web; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import server.uckgisagi.config.interceptor.AuthInterceptor; +import server.uckgisagi.config.resolver.LoginUserIdResolver; + +import java.util.List; + +@Configuration +@RequiredArgsConstructor +public class WebConfig implements WebMvcConfigurer { + + private final AuthInterceptor authInterceptor; + private final LoginUserIdResolver loginUserIdResolver; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(authInterceptor); + } + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(loginUserIdResolver); + } + +} diff --git a/src/main/java/server/uckgisagi/domain/follow/repository/FollowRepository.java b/src/main/java/server/uckgisagi/domain/follow/repository/FollowRepository.java deleted file mode 100644 index 9e21de2e..00000000 --- a/src/main/java/server/uckgisagi/domain/follow/repository/FollowRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package server.uckgisagi.domain.follow.repository; - -import org.springframework.data.jpa.repository.JpaRepository; -import server.uckgisagi.domain.follow.entity.Follow; - -public interface FollowRepository extends JpaRepository { -} diff --git a/src/main/java/server/uckgisagi/domain/notification/entity/Notification.java b/src/main/java/server/uckgisagi/domain/notification/entity/Notification.java deleted file mode 100644 index 9df49f3e..00000000 --- a/src/main/java/server/uckgisagi/domain/notification/entity/Notification.java +++ /dev/null @@ -1,33 +0,0 @@ -package server.uckgisagi.domain.notification.entity; - -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; -import server.uckgisagi.domain.common.AuditingTimeEntity; -import server.uckgisagi.domain.notification.entity.enumerate.NotificationType; - -import javax.persistence.*; - -@Entity -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Notification extends AuditingTimeEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "notification_id", nullable = false) - private Long id; - - @Column(nullable = false) - private Long userId; // ์•Œ๋žŒ์„ ๋ณด๋‚ด๋Š” ์œ ์ € ์•„์ด๋”” - - @Column(nullable = false) - private Long targetUserId; // ์•Œ๋žŒ์„ ๋ฐ›์„ ์œ ์ € ์•„์ด๋”” - - @Column(nullable = false, length = 20) - private NotificationType notificationType; - -// @Column(columnDefinition = "TEXT", nullable = false) -// private String notificationMessage; - -} diff --git a/src/main/java/server/uckgisagi/domain/notification/repository/NotificationRepository.java b/src/main/java/server/uckgisagi/domain/notification/repository/NotificationRepository.java deleted file mode 100644 index 08eefa4d..00000000 --- a/src/main/java/server/uckgisagi/domain/notification/repository/NotificationRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package server.uckgisagi.domain.notification.repository; - -import org.springframework.data.jpa.repository.JpaRepository; -import server.uckgisagi.domain.notification.entity.Notification; - -public interface NotificationRepository extends JpaRepository { -} diff --git a/src/main/java/server/uckgisagi/domain/post/entity/Post.java b/src/main/java/server/uckgisagi/domain/post/entity/Post.java deleted file mode 100644 index 7fb4bc09..00000000 --- a/src/main/java/server/uckgisagi/domain/post/entity/Post.java +++ /dev/null @@ -1,31 +0,0 @@ -package server.uckgisagi.domain.post.entity; - -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; -import server.uckgisagi.domain.common.AuditingTimeEntity; -import server.uckgisagi.domain.user.entity.User; - -import javax.persistence.*; - -@Entity -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Post extends AuditingTimeEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "post_id", nullable = false) - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id") - private User user; - - @Column(nullable = false) - private String title; - - @Column(nullable = false) - private String content; - -} diff --git a/src/main/java/server/uckgisagi/domain/post/repository/PostRepository.java b/src/main/java/server/uckgisagi/domain/post/repository/PostRepository.java deleted file mode 100644 index ddfedddb..00000000 --- a/src/main/java/server/uckgisagi/domain/post/repository/PostRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package server.uckgisagi.domain.post.repository; - -import org.springframework.data.jpa.repository.JpaRepository; -import server.uckgisagi.domain.post.entity.Post; - -public interface PostRepository extends JpaRepository { -} diff --git a/src/main/java/server/uckgisagi/domain/postscrap/repository/PostScrapRepository.java b/src/main/java/server/uckgisagi/domain/postscrap/repository/PostScrapRepository.java deleted file mode 100644 index 38f88ca2..00000000 --- a/src/main/java/server/uckgisagi/domain/postscrap/repository/PostScrapRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package server.uckgisagi.domain.postscrap.repository; - -import org.springframework.data.jpa.repository.JpaRepository; -import server.uckgisagi.domain.postscrap.entity.PostScrap; - -public interface PostScrapRepository extends JpaRepository { -} diff --git a/src/main/java/server/uckgisagi/domain/user/entity/User.java b/src/main/java/server/uckgisagi/domain/user/entity/User.java deleted file mode 100644 index 227dff6b..00000000 --- a/src/main/java/server/uckgisagi/domain/user/entity/User.java +++ /dev/null @@ -1,87 +0,0 @@ -package server.uckgisagi.domain.user.entity; - -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; -import server.uckgisagi.domain.common.AuditingTimeEntity; -import server.uckgisagi.domain.post.entity.Post; -import server.uckgisagi.domain.user.entity.embedded.SocialInfo; -import server.uckgisagi.domain.user.entity.enumerate.SocialType; -import server.uckgisagi.domain.user.entity.enumerate.UserGrade; -import server.uckgisagi.domain.user.entity.enumerate.UserStatus; - -import javax.persistence.*; -import java.util.ArrayList; -import java.util.List; - -@Entity -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class User extends AuditingTimeEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "user_id", nullable = false) - private Long id; - - @Embedded - private SocialInfo socialInfo; - - @Column(nullable = false) - private String nickname; - - @Enumerated(EnumType.STRING) - @Column(nullable = false, length = 20) - private UserGrade grade; - - @Enumerated(EnumType.STRING) - @Column(nullable = false, length = 10) - private UserStatus status; - - @OneToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "token_id") - private Token token; - - @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) - private final List posts = new ArrayList<>(); - - private User(final String socialId, final SocialType socialType, final String nickname) { - this.socialInfo = SocialInfo.of(socialId, socialType); - this.nickname = nickname; - this.grade = UserGrade.SQUIRE; - this.status = UserStatus.ACTIVE; - } - - public void setTokenInfo(Token token) { - this.token = token; - } - - void addPosts(Post post) { - this.posts.add(post); - } - - void changeNickname(String nickname) { - this.nickname = nickname; - } - - public void changeGrade(int count) { - switch (count) { - case 5: - this.grade = UserGrade.BARON; - break; - case 10: - this.grade = UserGrade.EARL; - break; - case 19: - this.grade = UserGrade.DUKE; - break; - case 32: - this.grade = UserGrade.LORD; - break; - case 53: - this.grade = UserGrade.KING; - break; - } - } - -} diff --git a/src/main/java/server/uckgisagi/domain/user/repository/UserRepository.java b/src/main/java/server/uckgisagi/domain/user/repository/UserRepository.java deleted file mode 100644 index 81dca8ee..00000000 --- a/src/main/java/server/uckgisagi/domain/user/repository/UserRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package server.uckgisagi.domain.user.repository; - -import org.springframework.data.jpa.repository.JpaRepository; -import server.uckgisagi.domain.user.entity.User; - -public interface UserRepository extends JpaRepository { -} diff --git a/src/main/java/server/uckgisagi/external/client/apple/AppleAuthApiClient.java b/src/main/java/server/uckgisagi/external/client/apple/AppleAuthApiClient.java new file mode 100644 index 00000000..a6ae4926 --- /dev/null +++ b/src/main/java/server/uckgisagi/external/client/apple/AppleAuthApiClient.java @@ -0,0 +1,13 @@ +package server.uckgisagi.external.client.apple; + +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import server.uckgisagi.external.client.apple.dto.ApplePublicKeyResponse; + +@FeignClient(name = "appleAuthApiClient", url = "https://appleid.apple.com/auth") +public interface AppleAuthApiClient { + + @GetMapping("/keys") + ApplePublicKeyResponse getAppleAuthPublicKey(); + +} \ No newline at end of file diff --git a/src/main/java/server/uckgisagi/external/client/apple/AppleTokenDecoder.java b/src/main/java/server/uckgisagi/external/client/apple/AppleTokenDecoder.java new file mode 100644 index 00000000..d0ac5c5e --- /dev/null +++ b/src/main/java/server/uckgisagi/external/client/apple/AppleTokenDecoder.java @@ -0,0 +1,8 @@ +package server.uckgisagi.external.client.apple; + + +import org.jetbrains.annotations.NotNull; + +public interface AppleTokenDecoder { + String getSocialIdFromIdToken(@NotNull String idToken); +} diff --git a/src/main/java/server/uckgisagi/external/client/apple/AppleTokenDecoderImpl.java b/src/main/java/server/uckgisagi/external/client/apple/AppleTokenDecoderImpl.java new file mode 100644 index 00000000..a060ffe0 --- /dev/null +++ b/src/main/java/server/uckgisagi/external/client/apple/AppleTokenDecoderImpl.java @@ -0,0 +1,69 @@ +package server.uckgisagi.external.client.apple; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.InvalidClaimException; +import io.jsonwebtoken.Jwts; +import lombok.RequiredArgsConstructor; +import org.jetbrains.annotations.NotNull; +import org.springframework.stereotype.Component; +import server.uckgisagi.common.exception.ErrorResponseResult; +import server.uckgisagi.common.exception.custom.ValidationException; +import server.uckgisagi.external.client.apple.dto.ApplePublicKeyResponse; + +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.RSAPublicKeySpec; +import java.util.Base64; +import java.util.Map; + +@Component +@RequiredArgsConstructor +public class AppleTokenDecoderImpl implements AppleTokenDecoder { + + private final AppleAuthApiClient appleApiCaller; + private final ObjectMapper objectMapper; // ์ง๋ ฌํ™”, ์—ญ์ง๋ ฌํ™” + + @Override + public String getSocialIdFromIdToken(@NotNull String idToken) { + String headerIdToken = idToken.split("\\.")[0]; + try { + Map header = objectMapper.readValue(new String(Base64.getDecoder().decode(headerIdToken), StandardCharsets.UTF_8), new TypeReference<>() {}); // JSON -> JAVA ๊ฐ์ฒด (์—ญ์ง๋ ฌํ™”) : readValue() + PublicKey publicKey = getPublicKey(header); + Claims claims = Jwts.parserBuilder() + .setSigningKey(publicKey) + .build() + .parseClaimsJws(idToken) + .getBody(); + return claims.getSubject(); + } catch (ExpiredJwtException e) { + throw new ValidationException(String.format("๋งŒ๋ฃŒ๋œ ์• ํ”Œ idToken (%s) ์ž…๋‹ˆ๋‹ค (reason: %s)", idToken, e.getMessage(), ErrorResponseResult.VALIDATION_AUTH_TOKEN_EXCEPTION)); + } catch (JsonProcessingException | InvalidKeySpecException | InvalidClaimException | NoSuchAlgorithmException | IllegalArgumentException e) { + throw new ValidationException(String.format("์ž˜๋ชป๋œ ์• ํ”Œ idToken (%s) ์ž…๋‹ˆ๋‹ค (reason: %s)", idToken, e.getMessage(), ErrorResponseResult.VALIDATION_AUTH_TOKEN_EXCEPTION)); + } + } + + private PublicKey getPublicKey(Map header) throws InvalidKeySpecException, NoSuchAlgorithmException { + ApplePublicKeyResponse response = appleApiCaller.getAppleAuthPublicKey(); + ApplePublicKeyResponse.JWKSetKey key = response.getMatchedPublicKey(header.get("kid"), header.get("alg")); + + byte[] nBytes = Base64.getUrlDecoder().decode(key.getN()); + byte[] eBytes = Base64.getUrlDecoder().decode(key.getE()); + + BigInteger n = new BigInteger(1, nBytes); + BigInteger e = new BigInteger(1, eBytes); + + RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec(n, e); + KeyFactory keyFactory = KeyFactory.getInstance(key.getKty()); + + return keyFactory.generatePublic(publicKeySpec); + } + +} diff --git a/src/main/java/server/uckgisagi/external/client/apple/dto/ApplePublicKeyResponse.java b/src/main/java/server/uckgisagi/external/client/apple/dto/ApplePublicKeyResponse.java new file mode 100644 index 00000000..895fee5a --- /dev/null +++ b/src/main/java/server/uckgisagi/external/client/apple/dto/ApplePublicKeyResponse.java @@ -0,0 +1,36 @@ +package server.uckgisagi.external.client.apple.dto; + +import lombok.*; +import server.uckgisagi.common.exception.custom.ValidationException; + +import java.util.List; + +@ToString +@Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class ApplePublicKeyResponse { + + private List keys; + + public JWKSetKey getMatchedPublicKey(String kid, String alg) { + return keys.stream() + .filter(key -> key.getKid().equals(kid) && key.getAlg().equals(alg)) + .findFirst() + .orElseThrow(() -> new ValidationException("์ผ์น˜ํ•˜๋Š” Public Key ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค")); + } + + @ToString + @Getter + @NoArgsConstructor(access = AccessLevel.PRIVATE) + @AllArgsConstructor(access = AccessLevel.PRIVATE) + public static class JWKSetKey { + private String alg; // ํ† ํฐ์„ ์•”ํ˜ธํ™”ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋˜๋Š” ์•”ํ˜ธํ™” ์•Œ๊ณ ๋ฆฌ์ฆ˜ + private String e; // RSA ๊ณต๊ฐœ ํ‚ค์˜ ์ง€์ˆ˜ ๊ฐ’ + private String kid; // ๊ฐœ๋ฐœ์ž ๊ณ„์ •์—์„œ ์–ป์€ 10์ž๋ฆฌ ์‹๋ณ„์ž ํ‚ค + private String kty; // ํ‚ค ์œ ํ˜• ๋งค๊ฐœ๋ณ€์ˆ˜ ์„ค์ • : "RSA"๋กœ ์„ค์ •ํ•ด์•ผํ•จ + private String n; // RSA ๊ณต๊ฐœ ํ‚ค์˜ ๋ชจ๋“ˆ๋Ÿฌ์Šค ๊ฐ’ + private String use; // ๊ณต๊ฐœ ํ‚ค์˜ ์˜๋„๋œ ์šฉ๋„ + } + +} \ No newline at end of file diff --git a/src/main/resources/application-aws.yml b/src/main/resources/application-aws.yml new file mode 100644 index 00000000..6989f93b --- /dev/null +++ b/src/main/resources/application-aws.yml @@ -0,0 +1,11 @@ +cloud: + aws: + credentials: + accessKey: ${AWS_ACCESS_KEY_ID} + secretKey: ${AWS_SECRET_ACCESS_KEY} + s3: + bucket: uckgisagi-bucket + region: + static: ap-northeast-2 + stack: + auto: false diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 627b8dd4..87b30c23 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -3,6 +3,10 @@ spring: activate: on-profile: dev + mvc: + pathmatch: + matching-strategy: ant_path_matcher + datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/uckgisagi_dev?useSSL=false&characterEncoding=UTF-8&serverTimezone=UTC diff --git a/src/main/resources/application-jwt.yml b/src/main/resources/application-jwt.yml new file mode 100644 index 00000000..56013228 --- /dev/null +++ b/src/main/resources/application-jwt.yml @@ -0,0 +1,2 @@ +jwt: + secret: ${JWT_SECRET} \ No newline at end of file diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml new file mode 100644 index 00000000..92a69c29 --- /dev/null +++ b/src/main/resources/application-prod.yml @@ -0,0 +1,30 @@ +spring: + config: + activate: + on-profile: prod + + mvc: + pathmatch: + matching-strategy: ant_path_matcher + + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: ${DB_URL} + username: ${DB_USERNAME} + password: ${DB_PASSWORD} + + jpa: + database: mysql + hibernate: + ddl-auto: none + properties: + hibernate: + show_sql: true + format_sql: true + default_batch_fetch_size: 1000 + +logging: + level: + org.hibernate.SQL: debug + org.hibernate.type: trace + com.amazonaws.util.EC2MetadataUtils: error \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index c8fc4e22..e8146094 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -8,4 +8,7 @@ server: spring: profiles: - default: dev \ No newline at end of file + default: prod + + + diff --git a/src/main/resources/firebase/uckgisagi-firebase-admin.json b/src/main/resources/firebase/uckgisagi-firebase-admin.json new file mode 100644 index 00000000..cef84aed --- /dev/null +++ b/src/main/resources/firebase/uckgisagi-firebase-admin.json @@ -0,0 +1,12 @@ +{ + "type": "service_account", + "project_id": "uckgisagi-server-4eae6", + "private_key_id": "961ca875112643d4b5031f67a3852f66f753e552", + "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDKtvWE0xaTIPMd\nQBV1DlIaWBfCZdUkQo7UryY8OTX0EUhFg6HNf+6koplYIrJ0dZPwoP6NdYOrdbm+\nun605Z4X+1l5aYEfFpMKWGLWFOxqk49hPnsZMFD0PsbnyTZ7+m/h/gmpfEpQ3nkj\nibzBQjuZLQzT5LaFzDgloy9Ku6RPNT1SsGsBZ3xULHOxMmkP+eWILzCU65f/kG4W\n4lyCoc/kBEP08rS2eIV1OLp3jZJ1mji0tEnVsT4jPOSN6oDXu4yTmEYEy6PvYy0N\noMqfBQAeMHuyqdqjfMY8HXBXaO+Q2Dyzt0SRsMyDSNFfOfyCUG5EOSopwqgq8imU\n8Y1FBMvlAgMBAAECggEAF6RyCx1Bb0RzBlDQj9ftPHRUxQ/yZWm71dNdrr1vbPlN\nCAp7pweKMjpijxRw4sNJz1E/jwkLI8a1tKh0ma2EHEDs5QuoixMrcBPx5w0Gq8Ft\nAgby/XOUpX/i2+qsR5ZkUSO7RcCgvEDOORZZ5OJQKCPIcLgmj4FLdRxMqjcrSS9z\ng8QfnDVxsOKj2WwFkwVUmg4KSC5DIatpzA7z3twvaYfzMsWhtIp783MyHACr9GAQ\n+pN16GDxAibKvP61YrXwRP+q8CpG/c2c9MLrBgk4W4IJ4kvEMeXGjjiu9+y2LIO7\nfyviZqf0+L5NZvOvpFAb+OH/rtqTAyrei/U0M02x6wKBgQD7XEsKqrlISCcb/MRu\nzD0PoP5nINt5CVpkJEgA1rHvGEGxP2tU3HdFr3q6qO1YQM9QVEcfqZPLOCbGEyr9\nnXs05TmHG+DC2jJW0jD2F2f+P9sUrjp2+4GjUQn/67cXgh420qFSbKVR1EDWcGKI\nDLOaxe/zyKzISCrw44Ql7XB0vwKBgQDOdM8Q6q6by7REIFjG5wRN8a+Ld9RTjBr2\nFmoywAOQBYF5A03aD1ojdOt65vcs5DEy4ECrpJwBDrIUo8uBxfiivW1Vdtn+Zxwl\ng13AXmjLyYWAU5KoPkvfJXIEAHZf7dcTzHELBz3Dks33X88UIo88QhWMSt8NQpZm\nIacBaEu0WwKBgHx8S+nffV2P5laVC4+39LGt0PCwNCGwgSTBVyubKIo6ICaxOu3P\nNf68FnMlQE6J4mJtKsBCkqB9ka5dRdhOyvr6X1BLfTfjKjUXagoms2kWpOCMHQZa\nLuz8MJCfY5Dv7xjFngGdLw7kqKvLAvFQIQ8Q4nKAuxmBrEqa0xKZki0vAoGACwNg\nKF7cgaMUMq4nDjU0nZPO8Xmq8en/ZjE76QklJ4GjrnjmpkM7Y7jQ9vVrKhHiLfyY\ndo+JYuUNytwR9xJAeS3xryVv64pEjhu73I8st/JAFOBgamkoUvcEZgJATk25s2ys\nexIf0Vb7db6+pSxSx7weuiUkUOjEbR5OclzF7RECgYEAyP3pdB2tH6dfzYuwrnkf\nNMFTQnedP6AWwl5C1+sRqcEOR/Ggk0zj3Y98eATJczdnPOhRabE5fFyva85bcLPB\nnaOd8h8VXn7ZABUmhZMC1y13tbrFLnSs/iF8x95JdO/KbAwOG8KnQuIYkGqKKKnp\n2zw/oiQMfkPLI4r3Tvy32lA=\n-----END PRIVATE KEY-----\n", + "client_email": "firebase-adminsdk-lriyj@uckgisagi-server-4eae6.iam.gserviceaccount.com", + "client_id": "103498691720091619822", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-lriyj%40uckgisagi-server-4eae6.iam.gserviceaccount.com" +} diff --git a/src/test/java/server/uckgisagi/app/ApplicationContextBeanTest.java b/src/test/java/server/uckgisagi/app/ApplicationContextBeanTest.java new file mode 100644 index 00000000..2cf3e093 --- /dev/null +++ b/src/test/java/server/uckgisagi/app/ApplicationContextBeanTest.java @@ -0,0 +1,54 @@ +package server.uckgisagi.app; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationContext; +import org.springframework.core.Ordered; +import org.springframework.web.servlet.HandlerMapping; + +import java.util.Map; + +@SpringBootTest(properties = {"spring.config.location=classpath:application-test.yml"}) +public class ApplicationContextBeanTest { + + @Autowired + private ApplicationContext applicationContext; + + @DisplayName("๋“ฑ๋ก๋œ ๋ชจ๋“  Bean ์กฐํšŒ") + @Test + void retrieve_all_beans() { + if (applicationContext != null) { + String[] beans = applicationContext.getBeanDefinitionNames(); + + for (String bean : beans) { + System.out.println(bean); + } + } + } + + /** + * order: 0 requestMappingHandlerMapping = RequestMappingHandlerMapping + *
+ * order: 1 viewControllerHandlerMapping = SimpleUrlHandlerMapping (WebConfig ์ˆ˜์ • ํ›„ ์ •์ƒ ๋“ฑ๋ก) + *
+ * order: 2 beanNameHandlerMapping = BeanNameUrlHandlerMapping + *
+ * order: 3 routerFunctionMapping = RouterFunctionMapping + *
+ * order: 2147483646 resourceHandlerMapping = SimpleUrlHandlerMapping + */ + @DisplayName("Spring ์—์„œ ์ž๋™์œผ๋กœ ์ถ”๊ฐ€ํ•˜๋Š” HnadlerMapping์˜ ์šฐ์„ ์ˆœ์œ„ ์กฐํšŒ") + @Test + void retrieve_HandlerMapping() { + Map matchingBeans = BeanFactoryUtils + .beansOfTypeIncludingAncestors(applicationContext, HandlerMapping.class, true, false); + matchingBeans.forEach((k, v) -> { + System.out.printf( + "order: %s %s = %s %n%n", + ((Ordered) v).getOrder(), k, v.getClass().getSimpleName()); + }); + } +} diff --git a/src/test/java/server/uckgisagi/app/accusation/service/AccusationServiceTest.java b/src/test/java/server/uckgisagi/app/accusation/service/AccusationServiceTest.java new file mode 100644 index 00000000..6de55a67 --- /dev/null +++ b/src/test/java/server/uckgisagi/app/accusation/service/AccusationServiceTest.java @@ -0,0 +1,67 @@ +/* +package server.uckgisagi.app.accusation.service; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; +import server.uckgisagi.app.accusation.dto.AccusationPostReqDto; +import server.uckgisagi.app.accusation.dto.AccusationPostResDto; +import server.uckgisagi.domain.accusation.repository.AccusationRepository; +import server.uckgisagi.domain.post.entity.Post; +import server.uckgisagi.domain.post.repository.PostRepository; +import server.uckgisagi.domain.user.entity.User; +import server.uckgisagi.domain.user.entity.enumerate.SocialType; +import server.uckgisagi.domain.user.repository.UserRepository; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@SpringBootTest +@Transactional +class AccusationServiceTest { + @Autowired + private UserRepository userRepository; + @Autowired + private PostRepository postRepository; + @Autowired + private AccusationRepository accusationRepository; + @Autowired + private AccusationService accusationService; + + @DisplayName("์‹ ๊ณ  ์„ฑ๊ณต") + @Test + void accusationTest(){ + List users = userRepository.saveAll(List.of( + User.newInstance("FirstSocialId", SocialType.APPLE, "์ฒซ๋ฒˆ์งธ"), + User.newInstance("SecondSocialId", SocialType.APPLE, "๋‘๋ฒˆ์งธ") + )); + + List posts = postRepository.saveAll(List.of( + Post.newInstance(users.get(0),"", "User1์˜ ์ฒซ ๋ฒˆ์งธ ๊ฒŒ์‹œ๋ฌผ", "์ œ๊ณง๋„ค"), + Post.newInstance(users.get(0),"", "User1์˜ ๋‘ ๋ฒˆ์งธ ๊ฒŒ์‹œ๋ฌผ", "์ œ๊ณง๋„ค"), + Post.newInstance(users.get(1),"", "User2์˜ ์ฒซ ๋ฒˆ์งธ ๊ฒŒ์‹œ๋ฌผ", "์ œ๊ณง๋„ค"), + Post.newInstance(users.get(1),"", "User2์˜ ๋‘ ๋ฒˆ์งธ ๊ฒŒ์‹œ๋ฌผ", "์ œ๊ณง๋„ค") + )); + + // ๋‘ ๋ฒˆ์งธ ์œ ์ €๊ฐ€ postId 1๋ฒˆ ๊ฒŒ์‹œ๋ฌผ์„ ์‹ ๊ณ  + AccusationPostResDto accusationPostResponseDto = accusationService.accusePost(new AccusationPostReqDto(0L),1L); + assertEquals(accusationPostResponseDto.getPostId(), accusationRepository.findByUserIdAndPostId(0L, 1L)); + + } + + @DisplayName("์ด๋ฏธ ์‹ ๊ณ  ๋‚ด์—ญ์ด ์กด์žฌํ•˜๋Š” ๊ฒฝ์šฐ ์‹คํŒจ") + @Test + void accusationConflictTest(){ + + } + + @DisplayName("์‹ ๊ณ  10๋ฒˆ ์ด์ƒ์ด๋ฉด ํ”ผ๋“œ์— ์•ˆ ๋ณด์ด๊ฒŒ ํ•จ.") + @Test + void accusationHideTest(){ + + } + +}*/ diff --git a/src/test/java/server/uckgisagi/app/auth/AuthServiceProviderTest.java b/src/test/java/server/uckgisagi/app/auth/AuthServiceProviderTest.java new file mode 100644 index 00000000..c6ce2b66 --- /dev/null +++ b/src/test/java/server/uckgisagi/app/auth/AuthServiceProviderTest.java @@ -0,0 +1,28 @@ +package server.uckgisagi.app.auth; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import server.uckgisagi.app.auth.provider.AuthServiceProvider; +import server.uckgisagi.app.auth.service.AuthService; +import server.uckgisagi.app.auth.service.impl.AppleAuthService; +import server.uckgisagi.app.user.domain.entity.enumerate.SocialType; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest(properties = {"spring.config.location=classpath:application-test.yml"}) +public class AuthServiceProviderTest { + + @Autowired + private AuthServiceProvider authServiceProvider; + + @DisplayName("AuthServiceProvider ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ๋“ฑ๋ก๋œ๋‹ค") + @Test + void AuthServiceProvider_register_success() { + final SocialType APPLE = SocialType.APPLE; + AuthService authService = authServiceProvider.getAuthService(APPLE); + + assertTrue(authService instanceof AppleAuthService); + } +} diff --git a/src/test/java/server/uckgisagi/app/auth/service/TokenServiceTest.java b/src/test/java/server/uckgisagi/app/auth/service/TokenServiceTest.java new file mode 100644 index 00000000..c57de042 --- /dev/null +++ b/src/test/java/server/uckgisagi/app/auth/service/TokenServiceTest.java @@ -0,0 +1,31 @@ +package server.uckgisagi.app.auth.service; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import server.uckgisagi.app.auth.dto.response.TokenResponse; + +import static org.junit.jupiter.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(properties = {"spring.config.location=classpath:application-test.yml"}) +public class TokenServiceTest { + + @Autowired + private CreateTokenService createTokenService; + + @Test + @DisplayName("์œ ์ € ์•„์ด๋””๋กœ ํ† ํฐ ์ƒ์„ฑํ•˜๊ธฐ") + void create_token_by_userId() { + final Long USER_ID = 0L; + + TokenResponse tokenInfo = createTokenService.createTokenInfo(USER_ID); + System.out.println(tokenInfo.getAccessToken()); + System.out.println(tokenInfo.getRefreshToken()); + + assertAll( + () -> assertThat(tokenInfo).isNotNull() + ); + } +} diff --git a/src/test/java/server/uckgisagi/app/follow/repository/FollowRepositoryTest.java b/src/test/java/server/uckgisagi/app/follow/repository/FollowRepositoryTest.java new file mode 100644 index 00000000..99b231ce --- /dev/null +++ b/src/test/java/server/uckgisagi/app/follow/repository/FollowRepositoryTest.java @@ -0,0 +1,90 @@ +package server.uckgisagi.app.follow.repository; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; +import server.uckgisagi.app.follow.domain.entity.Follow; +import server.uckgisagi.app.follow.domain.repository.FollowRepository; +import server.uckgisagi.app.user.domain.entity.User; +import server.uckgisagi.app.user.domain.entity.enumerate.SocialType; +import server.uckgisagi.app.user.domain.repository.UserRepository; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(properties = "spring.config.location=classpath:application-test.yml") +public class FollowRepositoryTest { + + @Autowired + private UserRepository userRepository; + + @Autowired + private FollowRepository followRepository; + + private User master; + private User firstTarget; + private User secondTarget; + + @BeforeEach + void setup() { + master = User.newInstance("firstSocialId", SocialType.APPLE, "FirstNickname"); + firstTarget = User.newInstance("secondSocialId", SocialType.APPLE, "SecondNickname"); + secondTarget = User.newInstance("thirdSocialId", SocialType.APPLE, "ThirdNickname"); + + userRepository.saveAll(List.of(master, firstTarget, secondTarget)); + + Follow follow = Follow.newInstance(firstTarget, master); + Follow follow1 = Follow.newInstance(secondTarget, master); + followRepository.saveAll(List.of(follow, follow1)); + + master.addFollowing(follow); + firstTarget.addFollower(follow); + + master.addFollowing(follow1); + secondTarget.addFollower(follow1); + } + + @AfterEach + void clean() { + followRepository.deleteAllInBatch(); + userRepository.deleteAllInBatch(); + } + + @DisplayName("๋‚ด๊ฐ€_ํŒ”๋กœ์šฐํ•˜๋Š”_์œ ์ €_๋ ˆํฌ์ง€ํ† ๋ฆฌ_ํ…Œ์ŠคํŠธ") + @Test + @Transactional + void followRepository_test() { + // when + System.out.println("================= findMyFollowingUserByUserId ================="); + /** + * inner join ์ฟผ๋ฆฌ ํ•œ๋ฐฉ + */ + List joinQuery = followRepository.findMyFollowingUserByUserId(master.getId()); + System.out.println(joinQuery.get(0).getId()); + + System.out.println("================= getMyFollowing ================="); + /** + * ์ฟผ๋ฆฌ ์•ˆ๋‚ ์•„๊ฐ + */ + List noQuery = master.getMyFollowings(); + System.out.println(noQuery.get(0).getId()); + + // then + assertAll( + () -> assertThat(joinQuery).isNotEmpty(), + () -> assertThat(noQuery).isNotEmpty(), + () -> { + joinQuery.forEach(user -> { + boolean contains = noQuery.contains(user); + assertTrue(contains); + }); + } + ); + } +} \ No newline at end of file diff --git a/src/test/java/server/uckgisagi/app/follow/service/FollowServiceTest.java b/src/test/java/server/uckgisagi/app/follow/service/FollowServiceTest.java new file mode 100644 index 00000000..0058957c --- /dev/null +++ b/src/test/java/server/uckgisagi/app/follow/service/FollowServiceTest.java @@ -0,0 +1,145 @@ +package server.uckgisagi.app.follow.service; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; +import server.uckgisagi.app.user.domain.dictionary.UserDictionary; +import server.uckgisagi.common.exception.custom.ConflictException; +import server.uckgisagi.common.exception.custom.NotFoundException; +import server.uckgisagi.app.follow.domain.entity.Follow; +import server.uckgisagi.app.follow.domain.repository.FollowRepository; +import server.uckgisagi.app.user.domain.entity.User; +import server.uckgisagi.app.user.domain.entity.enumerate.SocialType; +import server.uckgisagi.app.user.domain.repository.UserRepository; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(properties = "spring.config.location=classpath:application-test.yml") +@Transactional +public class FollowServiceTest { + + @Autowired + private FollowService followService; + + @Autowired + private UserRepository userRepository; + + @Autowired + private FollowRepository followRepository; + + @AfterEach + void clean() { + followRepository.deleteAllInBatch(); + userRepository.deleteAllInBatch(); + } + + @DisplayName("์œ ์ €_ํŒ”๋กœ์šฐํ•˜๊ธฐ_์„ฑ๊ณต") + @Test + void follow_user_success() { + final int FIRST = 0; + final int SECOND = 1; + // given + List users = userRepository.saveAll(List.of( + User.newInstance("FirstSocialId", SocialType.APPLE, "์ฒซ๋ฒˆ์งธ"), + User.newInstance("SecondSocialId", SocialType.APPLE, "๋‘๋ฒˆ์งธ") + )); + User user = users.get(FIRST); + User target = users.get(SECOND); + + // when + UserDictionary userDictionary = followService.followUser(target.getId(), user.getId()); + + // then + assertAll( + () -> assertThat(userDictionary).isNotNull() +// () -> assertThat(followUser.getFollowers()).isNotNull(), +// () -> assertThat(followUser.getFollowings()).isEmpty(), +// () -> assertThat(user.getFollowers()).isEmpty(), +// () -> assertThat(user.getFollowings()).isNotNull(), +// () -> assertEquals(followUser.getFollowers().get(FIRST), user.getFollowings().get(FIRST)), +// () -> assertEquals( +// followUser.getFollowers().get(FIRST) +// .getFollower().getId(), +// user.getId() +// ), +// () -> assertEquals( +// user.getFollowings().get(FIRST) +// .getFollowee().getId(), +// followUser.getId() +// ) + ); + } + + @DisplayName("์ด๋ฏธ_ํŒ”๋กœ์šฐํ•œ_์œ ์ €๋ฅผ_ํŒ”๋กœ์šฐํ•˜๋ ค๋Š”_๊ฒฝ์šฐ_์‹คํŒจ") + @Test + void already_follow_user_will_throw_exception() { + final int FIRST = 0; + final int SECOND = 1; + + List users = userRepository.saveAll(List.of( + User.newInstance("FirstSocialId", SocialType.APPLE, "์ฒซ๋ฒˆ์งธ"), + User.newInstance("SecondSocialId", SocialType.APPLE, "๋‘๋ฒˆ์งธ") + )); + User user = users.get(FIRST); + User target = users.get(SECOND); + + Follow followInfo = followRepository.save(Follow.newInstance(target, user)); + target.addFollower(followInfo); + user.addFollowing(followInfo); + + + assertThrows(ConflictException.class, () -> followService.followUser(target.getId(), user.getId())); + } + + @DisplayName("์œ ์ €_์–ธํŒ”๋กœ์šฐํ•˜๊ธฐ_์„ฑ๊ณต") + @Test + void unfollow_user_success() { + final int FIRST = 0; + final int SECOND = 1; + // given + List users = userRepository.saveAll(List.of( + User.newInstance("FirstSocialId", SocialType.APPLE, "์ฒซ๋ฒˆ์งธ"), + User.newInstance("SecondSocialId", SocialType.APPLE, "๋‘๋ฒˆ์งธ") + )); + User user = users.get(FIRST); + User friend = users.get(SECOND); + + Follow followInfo = followRepository.save(Follow.newInstance(friend, user)); +// friend.addFollower(followInfo); +// user.addFollowing(followInfo); + + // when + System.out.println("=============== UnFollow ================"); + followService.unfollowUser(friend.getId(), user.getId()); + + // then + System.out.println("=============== THEN ================"); + Follow follow = followRepository.findFollowByFolloweeUserIdAndFollowerUserId(friend.getId(), user.getId()); + assertAll( + () -> assertThat(follow).isNull(), + () -> assertThat(friend.getFollowers()).isEmpty(), + () -> assertThat(friend.getFollowers()).isEmpty() + ); + } + + @DisplayName("์ด๋ฏธ_์–ธํŒ”๋กœ์šฐํ•œ_์œ ์ €๋ฅผ_์–ธํŒ”๋กœ์šฐ_ํ• ๋•Œ_์‹คํŒจ") + @Test + void already_unfollow_user_will_throw_exception() { + final int FIRST = 0; + final int SECOND = 1; + List users = userRepository.saveAll(List.of( + User.newInstance("FirstSocialId", SocialType.APPLE, "์ฒซ๋ฒˆ์งธ"), + User.newInstance("SecondSocialId", SocialType.APPLE, "๋‘๋ฒˆ์งธ") + )); + User user = users.get(FIRST); + User friend = users.get(SECOND); + + assertThrows(NotFoundException.class, () -> followService.unfollowUser(friend.getId(), user.getId())); + } +} \ No newline at end of file diff --git a/src/test/java/server/uckgisagi/app/home/service/HomeRetrieveServiceTest.java b/src/test/java/server/uckgisagi/app/home/service/HomeRetrieveServiceTest.java new file mode 100644 index 00000000..6aec3c70 --- /dev/null +++ b/src/test/java/server/uckgisagi/app/home/service/HomeRetrieveServiceTest.java @@ -0,0 +1,219 @@ +package server.uckgisagi.app.home.service; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; +import server.uckgisagi.app.home.dto.response.HomePostResponse; +import server.uckgisagi.app.home.dto.response.HomeUserResponse; +import server.uckgisagi.app.home.dto.response.TodayPostStatus; +import server.uckgisagi.app.home.dto.response.UserResponseDto; +import server.uckgisagi.app.post.dto.response.PostResponse; +import server.uckgisagi.app.follow.domain.entity.Follow; +import server.uckgisagi.app.follow.domain.repository.FollowRepository; +import server.uckgisagi.app.post.domain.entity.Post; +import server.uckgisagi.app.post.domain.repository.PostRepository; +import server.uckgisagi.app.user.domain.entity.User; +import server.uckgisagi.app.user.domain.entity.enumerate.SocialType; +import server.uckgisagi.app.user.domain.repository.UserRepository; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneId; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; + + +@SpringBootTest(properties = {"spring.config.location=classpath:application-test.yml"}) +@Transactional +public class HomeRetrieveServiceTest { + + @Autowired + private HomeRetrieveService homeRetrieveService; + + @Autowired + private UserRepository userRepository; + + @Autowired + private PostRepository postRepository; + + @Autowired + private FollowRepository followRepository; + + private User master; + private User friend; + + @BeforeEach + void setup() { + // given + master = User.newInstance("SOCIAL_ID", SocialType.APPLE, "MasterUser"); + friend = User.newInstance("SOCIAL_ID", SocialType.APPLE, "Friend_1"); + + userRepository.saveAll(List.of(master, friend)); + followRepository.save(Follow.newInstance(friend, master)); + } + + @AfterEach + void clean() { + postRepository.deleteAllInBatch(); + followRepository.deleteAllInBatch(); + userRepository.deleteAllInBatch(); + } + + @DisplayName("์ƒ๋‹จ ๋ฐ” - ๋‚˜์™€ ๋‚ด ์นœ๊ตฌ ๋ชจ๋‘ ์˜ค๋Š˜ ์˜ฌ๋ฆฐ ์ธ์ฆ๊ธ€์ด ์—†์„ ๊ฒฝ์šฐ ์ •๋ณด ์กฐํšŒ ์„ฑ๊ณต") + @Test + void retrieve_me_and_friend_info_success_no_post_today() { + // when + HomeUserResponse response = homeRetrieveService.retrieveMeAndFriendInfo(master.getId()); + + // then + UserResponseDto myInfo = response.getMyInfo(); + UserResponseDto friendInfo = response.getFriendInfo().get(ONLY_FRIEND); + assertAll( + () -> assertThat(response).isNotNull(), + () -> assertThat(myInfo).isNotNull(), + () -> assertThat(response.getFriendInfo()).isNotEmpty(), + () -> assertEquals(myInfo.getUserId(), master.getId()), + () -> assertEquals(myInfo.getNickname(), master.getNickname()), + () -> assertEquals(myInfo.getGrade(), master.getGrade()), + () -> assertEquals(myInfo.getPostStatus(), TodayPostStatus.INACTIVE), + () -> assertEquals(friendInfo.getUserId(), friend.getId()), + () -> assertEquals(friendInfo.getNickname(), friend.getNickname()), + () -> assertEquals(friendInfo.getGrade(), friend.getGrade()), + () -> assertEquals(friendInfo.getPostStatus(), TodayPostStatus.INACTIVE) + ); + } + + @DisplayName("์ƒ๋‹จ ๋ฐ” - ๋‚˜๋งŒ ์˜ค๋Š˜ ์˜ฌ๋ฆฐ ์ธ์ฆ๊ธ€์ด ์žˆ๋Š” ๊ฒฝ์šฐ ์ •๋ณด ์กฐํšŒ ์„ฑ๊ณต") + @Test + void retrieve_me_and_friend_info_success_only_me_post_today() { + // given + postRepository.save(Post.newInstance(master, "imageUrl", "Title", "Content")); + + // when + HomeUserResponse response = homeRetrieveService.retrieveMeAndFriendInfo(master.getId()); + + // then + UserResponseDto myInfo = response.getMyInfo(); + UserResponseDto friendInfo = response.getFriendInfo().get(ONLY_FRIEND); + assertAll( + () -> assertThat(response).isNotNull(), + () -> assertThat(myInfo).isNotNull(), + () -> assertThat(response.getFriendInfo()).isNotEmpty(), + () -> assertEquals(myInfo.getUserId(), master.getId()), + () -> assertEquals(myInfo.getNickname(), master.getNickname()), + () -> assertEquals(myInfo.getGrade(), master.getGrade()), + () -> assertEquals(myInfo.getPostStatus(), TodayPostStatus.ACTIVE), + () -> assertEquals(friendInfo.getUserId(), friend.getId()), + () -> assertEquals(friendInfo.getNickname(), friend.getNickname()), + () -> assertEquals(friendInfo.getGrade(), friend.getGrade()), + () -> assertEquals(friendInfo.getPostStatus(), TodayPostStatus.INACTIVE) + ); + } + + @DisplayName("์ƒ๋‹จ ๋ฐ” - ์นœ๊ตฌ๋งŒ ์˜ค๋Š˜ ์˜ฌ๋ฆฐ ์ธ์ฆ๊ธ€์ด ์žˆ๋Š” ๊ฒฝ์šฐ ์ •๋ณด ์กฐํšŒ ์„ฑ๊ณต") + @Test + void retrieve_me_and_friend_info_success_only_friend_post_today() { + // given + postRepository.save(Post.newInstance(friend, "imageUrl", "Title", "Content")); + + // when + HomeUserResponse response = homeRetrieveService.retrieveMeAndFriendInfo(master.getId()); + + // then + UserResponseDto myInfo = response.getMyInfo(); + UserResponseDto friendInfo = response.getFriendInfo().get(ONLY_FRIEND); + assertAll( + () -> assertThat(response).isNotNull(), + () -> assertThat(myInfo).isNotNull(), + () -> assertThat(response.getFriendInfo()).isNotEmpty(), + () -> assertEquals(myInfo.getUserId(), master.getId()), + () -> assertEquals(myInfo.getNickname(), master.getNickname()), + () -> assertEquals(myInfo.getGrade(), master.getGrade()), + () -> assertEquals(myInfo.getPostStatus(), TodayPostStatus.INACTIVE), + () -> assertEquals(friendInfo.getUserId(), friend.getId()), + () -> assertEquals(friendInfo.getNickname(), friend.getNickname()), + () -> assertEquals(friendInfo.getGrade(), friend.getGrade()), + () -> assertEquals(friendInfo.getPostStatus(), TodayPostStatus.ACTIVE) + ); + } + + @DisplayName("์ƒ๋‹จ ๋ฐ” - ๋‚˜์™€ ์นœ๊ตฌ ๋ชจ๋‘ ์˜ค๋Š˜ ์˜ฌ๋ฆฐ ์ธ์ฆ๊ธ€์ด ์žˆ๋Š” ๊ฒฝ์šฐ ์ •๋ณด ์กฐํšŒ ์„ฑ๊ณต") + @Test + void retrieve_me_and_friend_info_success_me_and_friend_post_today() { + // given + postRepository.save(Post.newInstance(master, "imageUrl", "Title", "Content")); + postRepository.save(Post.newInstance(friend, "imageUrl", "Title", "Content")); + + // when + HomeUserResponse response = homeRetrieveService.retrieveMeAndFriendInfo(master.getId()); + + // then + UserResponseDto myInfo = response.getMyInfo(); + UserResponseDto friendInfo = response.getFriendInfo().get(ONLY_FRIEND); + assertAll( + () -> assertThat(response).isNotNull(), + () -> assertThat(myInfo).isNotNull(), + () -> assertThat(response.getFriendInfo()).isNotEmpty(), + () -> assertEquals(myInfo.getUserId(), master.getId()), + () -> assertEquals(myInfo.getNickname(), master.getNickname()), + () -> assertEquals(myInfo.getGrade(), master.getGrade()), + () -> assertEquals(myInfo.getPostStatus(), TodayPostStatus.ACTIVE), + () -> assertEquals(friendInfo.getUserId(), friend.getId()), + () -> assertEquals(friendInfo.getNickname(), friend.getNickname()), + () -> assertEquals(friendInfo.getGrade(), friend.getGrade()), + () -> assertEquals(friendInfo.getPostStatus(), TodayPostStatus.ACTIVE) + ); + } + + @DisplayName("๋‚˜์˜ ํ™ˆ ์ปจํ…์ธ  ์กฐํšŒ ์„ฑ๊ณต - ์˜ค๋Š˜ ์˜ฌ๋ฆฐ ์ธ์ฆ๊ธ€์ด ์—†๋Š” ๊ฒฝ์šฐ") + @Test + void retrieve_my_home_contents_success_not_exist_today_post() { + // given + + // when + + // then + } + + @DisplayName("๋‚˜์˜ ํ™ˆ ์ปจํ…์ธ  ์กฐํšŒ ์„ฑ๊ณต - ์˜ค๋Š˜ ์˜ฌ๋ฆฐ ์ธ์ฆ๊ธ€์ด ์žˆ๋Š” ๊ฒฝ์šฐ") + @Test + void retrieve_my_home_contents_success_exist_today_post() { + // given + Post oldPost1 = Post.newInstance(master, "imageUrl", "Title", "Content"); + Post oldPost2 = Post.newInstance(master, "imageUrl", "Title", "Content"); + Post oldPost3 = Post.newInstance(master, "imageUrl", "Title", "Content"); + + oldPost1.setTestCreatedAt(LocalDateTime.of(LocalDate.of(TODAY_DATE.getYear(), TODAY_DATE.getMonthValue(), 2), LOCAL_TIME)); + oldPost2.setTestCreatedAt(LocalDateTime.of(LocalDate.of(TODAY_DATE.getYear(), TODAY_DATE.getMonthValue(), 10), LOCAL_TIME)); + oldPost3.setTestCreatedAt(LocalDateTime.of(LocalDate.of(TODAY_DATE.getYear(), TODAY_DATE.getMonthValue(), 15), LOCAL_TIME)); + + Post todayPost = Post.newInstance(master, "imageUrl", "Title", "Content"); + + postRepository.saveAll(List.of(oldPost1, oldPost2, oldPost3, todayPost)); + + // when + HomePostResponse response = homeRetrieveService.retrieveHomeContents(master.getId()); + + // then + + List postDates = response.getPostDates(); + List postResponses = response.getPosts(); +// assertAll( +// () -> assertThat(response).isNotNull(), +// () -> assertThat(postDates).isNotEmpty(), +// () -> assertThat(postResponses).isNotEmpty() +// ); + } + + private final LocalDate TODAY_DATE = LocalDate.now(ZoneId.of("Asia/Seoul")); + + private static final LocalTime LOCAL_TIME = LocalTime.of(0, 0); + private static final int ONLY_FRIEND = 0; +} diff --git a/src/test/java/server/uckgisagi/app/notification/NotificationServiceProviderTest.java b/src/test/java/server/uckgisagi/app/notification/NotificationServiceProviderTest.java new file mode 100644 index 00000000..8869dd35 --- /dev/null +++ b/src/test/java/server/uckgisagi/app/notification/NotificationServiceProviderTest.java @@ -0,0 +1,33 @@ +package server.uckgisagi.app.notification; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import server.uckgisagi.app.notification.provider.NotificationServiceProvider; +import server.uckgisagi.app.notification.service.NotificationService; +import server.uckgisagi.app.notification.service.impl.FollowNotificationService; +import server.uckgisagi.app.notification.service.impl.PokeNotificationService; +import server.uckgisagi.app.notification.domain.entity.enumerate.NotificationType; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +@SpringBootTest(properties = {"spring.config.location=classpath:application-test.yml"}) +public class NotificationServiceProviderTest { + + @Autowired + private NotificationServiceProvider notificationServiceProvider; + + private static final NotificationType POKE_TYPE = NotificationType.POKE; + private static final NotificationType FOLLOW_TYPE = NotificationType.FOLLOW; + + @DisplayName("NotificationServiceProvider ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ๋“ฑ๋ก๋œ๋‹ค") + @Test + void AuthServiceProvider_register_success() { + NotificationService notificationService1 = notificationServiceProvider.getNotificationService(POKE_TYPE); + NotificationService notificationService2 = notificationServiceProvider.getNotificationService(FOLLOW_TYPE); + + assertTrue(notificationService1 instanceof PokeNotificationService); + assertTrue(notificationService2 instanceof FollowNotificationService); + } +} diff --git a/src/test/java/server/uckgisagi/app/review/service/ReviewServiceTest.java b/src/test/java/server/uckgisagi/app/review/service/ReviewServiceTest.java new file mode 100644 index 00000000..23357b54 --- /dev/null +++ b/src/test/java/server/uckgisagi/app/review/service/ReviewServiceTest.java @@ -0,0 +1,123 @@ +package server.uckgisagi.app.review.service; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; +import server.uckgisagi.app.review.ReviewService; +import server.uckgisagi.app.review.dto.request.AddReviewRequest; +import server.uckgisagi.app.review.dto.response.ReviewResponse; +import server.uckgisagi.common.exception.custom.NotFoundException; +import server.uckgisagi.app.review.domain.entity.Review; +import server.uckgisagi.app.review.domain.repository.ReviewRepository; +import server.uckgisagi.app.store.domain.entity.Store; +import server.uckgisagi.app.store.domain.repository.StoreRepository; +import server.uckgisagi.app.user.domain.entity.User; +import server.uckgisagi.app.user.domain.entity.enumerate.SocialType; +import server.uckgisagi.app.user.domain.repository.UserRepository; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(properties = {"spring.config.location=classpath:application-test.yml"}) +public class ReviewServiceTest { + + @Autowired + private ReviewService reviewService; + + @Autowired + private ReviewRepository reviewRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private StoreRepository storeRepository; + + private User user; + + @BeforeEach + void setup() { + user = User.newInstance("SOCIAL_ID", SocialType.APPLE, "NICKNAME"); + userRepository.save(user); + } + + @AfterEach + void clean() { + reviewRepository.deleteAllInBatch(); + userRepository.deleteAllInBatch(); + } + + @DisplayName("๋งค์žฅ ๋ฆฌ๋ทฐ ์ž‘์„ฑ ์„ฑ๊ณต") + @Test + @Transactional + void add_review_success() { + // given + AddReviewRequest request = AddReviewRequest.testBuilder() + .storeId(STORE_ID) + .content("Content") + .build(); + + // when + reviewService.addReview(request, user.getId()); + + // then + Store store = storeRepository.findStoreByStoreId(STORE_ID); + assertAll( + () -> assertThat(store.getReviews()).isNotEmpty() +// () -> assertEquals(store.getReviews().size(), 1) FIXME Debug ์‹œ ์ •์ƒ ๋™์ž‘ -> Test ์‹คํ–‰ ์‹œ store ์— review 2๊ฐœ add ๋จ (ใ…ˆ๋ฒ„๊ทธ) + ); + + // finally + store.getReviews().remove(0); + } + + @DisplayName("ํ•ด๋‹น ๋งค์žฅ์˜ ๋ฆฌ๋ทฐ ์กฐํšŒ ์„ฑ๊ณต") + @Test + @Transactional + void retrieve_review_success() { + // given + Store store = storeRepository.findStoreByStoreId(STORE_ID); + Review review = Review.newInstance(store, user, "Comment"); + store.addReview(review); + + // when + List response = reviewService.retrieveReview(STORE_ID); + + // then + assertAll( + () -> assertThat(response).isNotNull(), + () -> assertEquals(response.get(0).getReviewId(), review.getId()), + () -> assertEquals(response.get(0).getComment(), review.getComment()), + () -> assertEquals(response.get(0).getNickname(), review.getUser().getNickname()) + ); + + // finally + store.getReviews().remove(0); + } + + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ๋งค์žฅ์— ๋ฆฌ๋ทฐ ์ž‘์„ฑ ์‹œ ์‹คํŒจ") + @Test + void throw_NotFoundException_when_request_not_exist_store() { + AddReviewRequest request = AddReviewRequest.testBuilder() + .storeId(NOT_EXIST_STORE_ID) + .content("Content") + .build(); + + assertThrows(NotFoundException.class, () -> reviewService.addReview(request, user.getId())); + } + + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ๋งค์žฅ์˜ ๋ฆฌ๋ทฐ ์กฐํšŒ ์‹œ ์‹คํŒจ") + @Test + void throw_NotFoundException_when_retrieve_review_not_exist_store() { + assertThrows(NotFoundException.class, () -> reviewService.retrieveReview(NOT_EXIST_STORE_ID)); + } + + private static final Long STORE_ID = 1L; + private static final Long NOT_EXIST_STORE_ID = - 1L; +} diff --git a/src/test/java/server/uckgisagi/app/scrap/service/ScrapServiceTest.java b/src/test/java/server/uckgisagi/app/scrap/service/ScrapServiceTest.java new file mode 100644 index 00000000..a49cb0f3 --- /dev/null +++ b/src/test/java/server/uckgisagi/app/scrap/service/ScrapServiceTest.java @@ -0,0 +1,100 @@ +package server.uckgisagi.app.scrap.service; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; +import server.uckgisagi.common.exception.custom.NotFoundException; +import server.uckgisagi.app.post.domain.entity.Post; +import server.uckgisagi.app.post.domain.repository.PostRepository; +import server.uckgisagi.app.scrap.domain.entity.Scrap; +import server.uckgisagi.app.scrap.domain.repository.ScrapRepository; +import server.uckgisagi.app.user.domain.entity.User; +import server.uckgisagi.app.user.domain.entity.enumerate.SocialType; +import server.uckgisagi.app.user.domain.repository.UserRepository; + +import static org.junit.jupiter.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(properties = {"spring.config.location=classpath:application-test.yml"}) +public class ScrapServiceTest { + + @Autowired + private ScrapService scrapService; + + @Autowired + private ScrapRepository scrapRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private PostRepository postRepository; + + private Post post; + private User user; + + @BeforeEach + void setup() { + user = User.newInstance("SOCIAL_ID", SocialType.APPLE, "NICKNAME"); + post = Post.newInstance(user, "imageURL", "Title", "Content"); + + userRepository.save(user); + postRepository.save(post); + } + + @AfterEach + void clean() { + scrapRepository.deleteAllInBatch(); + postRepository.deleteAllInBatch(); + userRepository.deleteAllInBatch(); + } + + @DisplayName("์Šคํฌ๋žฉ ์„ฑ๊ณต") + @Test + @Transactional + void add_scrap_success() { + // given + final Long USER_ID = user.getId(); + final Long POST_ID = post.getId(); + + // when + scrapService.addScrap(POST_ID, USER_ID); + + // then + Scrap scrap = scrapRepository.findScrapByPostIdAndUserId(POST_ID, USER_ID); + assertThat(scrap).isNotNull(); + } + + @DisplayName("์Šคํฌ๋žฉ ์ทจ์†Œ ์„ฑ๊ณต") + @Test + @Transactional + void delete_scrap_success() { + // given + Scrap scrap = Scrap.newInstance(user, post); + scrapRepository.save(scrap); + + // when + scrapService.deleteScrap(post.getId(), user.getId()); + + // then + boolean isDeleteScrap = scrapRepository.findById(scrap.getId()).isEmpty(); + assertTrue(isDeleteScrap); + } + + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ๊ฒŒ์‹œ๋ฌผ ์Šคํฌ๋žฉ ์‹œ๋„ ์‹œ ์‹คํŒจ") + @Test + void throw_NotFoundException_when_request_not_exist_post() { + final Long NOT_EXIST_POST_ID = - 1L; + assertThrows(NotFoundException.class, () -> scrapService.addScrap(NOT_EXIST_POST_ID, user.getId())); + } + + @DisplayName("์Šคํฌ๋žฉํ•˜์ง€ ์•Š์€ ๊ฒŒ์‹œ๋ฌผ ์Šคํฌ๋žฉ ์‹œ๋„ ์‹œ ์‹คํŒจ") + @Test + void throw_NotFoundException_when_request_not_exist_scrap() { + assertThrows(NotFoundException.class, () -> scrapService.deleteScrap(post.getId(), user.getId())); + } +} diff --git a/src/test/java/server/uckgisagi/app/store/service/StoreServiceTest.java b/src/test/java/server/uckgisagi/app/store/service/StoreServiceTest.java new file mode 100644 index 00000000..623758e7 --- /dev/null +++ b/src/test/java/server/uckgisagi/app/store/service/StoreServiceTest.java @@ -0,0 +1,72 @@ +package server.uckgisagi.app.store.service; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; +import server.uckgisagi.app.store.dto.response.AllStoreResponse; +import server.uckgisagi.app.store.dto.response.OneStoreResponse; +import server.uckgisagi.common.exception.custom.NotFoundException; +import server.uckgisagi.app.store.domain.entity.Store; +import server.uckgisagi.app.store.domain.repository.StoreRepository; + +import static org.junit.jupiter.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(properties = {"spring.config.location=classpath:application-test.yml"}) +public class StoreServiceTest { + + @Autowired + private StoreRetrieveService storeRetrieveService; + + @Autowired + private StoreRepository storeRepository; + + @DisplayName("๋ชจ๋“  ๋ฆฌํ•„์Šคํ…Œ์ด์…˜ ์ •๋ณด ์กฐํšŒ ์„ฑ๊ณต (์ƒ๋‹จ ๋งค์žฅ ์ •๋ณด๊ฐ€ 5๊ฐœ)") + @Test + void all_store_info_retrieve_success() { + final int TOP_STORE_COUNT = 5; + // when + AllStoreResponse response = storeRetrieveService.retrieveAllStore(); + + // then + assertAll( + () -> assertThat(response).isNotNull(), + () -> assertEquals(response.getMostPopularStore().size(), TOP_STORE_COUNT) + ); + } + + @DisplayName("๋ฆฌํ•„์Šคํ…Œ์ด์…˜ ์ƒ์„ธ ์ •๋ณด ์กฐํšŒ ์„ฑ๊ณต") + @Test + @Transactional(readOnly = true) + void one_store_info_retrieve_success() { + // given + final Long STORE_ID = 1L; + Store store = storeRepository.findStoreByStoreId(STORE_ID); + + // when + OneStoreResponse response = storeRetrieveService.retrieveOneStore(STORE_ID); + + // then + assertAll( + () -> assertThat(store).isNotNull(), + () -> assertThat(response).isNotNull(), + () -> assertEquals(response.getStoreId(), store.getId()), + () -> assertEquals(response.getStoreName(), store.getName()), + () -> assertEquals(response.getDescription(), store.getDescription()), + () -> assertEquals(response.getAddress(), store.getAddress()), + () -> assertEquals(response.getImageUrl(), store.getImageUrl()), + () -> assertEquals(response.getWebSite(), store.getWebSite()), + () -> assertEquals(response.getPhoneNumber(), store.getPhoneNumber()), + () -> assertEquals(response.getTags(), store.getStoreTagValue()) + ); + } + + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ๋งค์žฅ ์ •๋ณด ์กฐํšŒ ์‹œ ์‹คํŒจ") + @Test + void throw_NotFoundException_when_request_not_exist_store() { + final Long NOT_EXIST_STORE_ID = - 1L; + assertThrows(NotFoundException.class, () -> storeRetrieveService.retrieveOneStore(NOT_EXIST_STORE_ID)); + } +} diff --git a/src/test/java/server/uckgisagi/config/TestConfig.java b/src/test/java/server/uckgisagi/config/TestConfig.java new file mode 100644 index 00000000..f820ae9c --- /dev/null +++ b/src/test/java/server/uckgisagi/config/TestConfig.java @@ -0,0 +1,21 @@ +package server.uckgisagi.config; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; + +@TestConfiguration +public class TestConfig { + + @PersistenceContext + private EntityManager entityManager; + + @Bean + public JPAQueryFactory jpaQueryFactory() { + return new JPAQueryFactory(entityManager); + } + +} diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml new file mode 100644 index 00000000..ab8c44e0 --- /dev/null +++ b/src/test/resources/application-test.yml @@ -0,0 +1,34 @@ +server: + port: 8080 + servlet: + encoding: + charset: UTF-8 + force: true + context-path: /api + +spring: + + mvc: + pathmatch: + matching-strategy: ant_path_matcher + + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/uckgisagi_dev?useSSL=false&characterEncoding=UTF-8&serverTimezone=UTC + username: root + + jpa: + database: mysql + hibernate: + ddl-auto: validate + properties: + hibernate: + show_sql: true + format_sql: true + default_batch_fetch_size: 1000 + +logging: + level: + org.hibernate.SQL: debug + org.hibernate.type: trace + com.amazonaws.util.EC2MetadataUtils: error \ No newline at end of file