From c743eaeea758d1b7b137e820be807fbddcfcac04 Mon Sep 17 00:00:00 2001 From: suhoon Date: Tue, 18 Jul 2023 17:10:46 +0900 Subject: [PATCH 01/72] writing --- .../dub/controller/PostController.java | 11 +++++++ .../com/likelion/dub/service/PostService.java | 31 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/src/main/java/com/likelion/dub/controller/PostController.java b/src/main/java/com/likelion/dub/controller/PostController.java index b6ad830..cc9f04a 100644 --- a/src/main/java/com/likelion/dub/controller/PostController.java +++ b/src/main/java/com/likelion/dub/controller/PostController.java @@ -62,6 +62,16 @@ public BaseResponse writePost(@RequestPart(value = "json") PostWritingRe } } + + @PostMapping("/writing") + public BaseResponse writing(@RequestPart(value = "json") PostWritingRequest dto) { + try { + postService.writing(dto.getTitle(), dto.getContent(), dto.getCategory()); + return new BaseResponse<>("글 작성 성공"); + } catch (BaseException e) { + return new BaseResponse<>(e.getStatus()); + } + } /** * post 보기 * @@ -70,6 +80,7 @@ public BaseResponse writePost(@RequestPart(value = "json") PostWritingRe */ @GetMapping("/read-post") public BaseResponse readPost(@RequestParam(value = "id", required = true) Long id) throws BaseException { + return new BaseResponse<>(postService.readPost(id)); } diff --git a/src/main/java/com/likelion/dub/service/PostService.java b/src/main/java/com/likelion/dub/service/PostService.java index 39f3f87..2818dc8 100644 --- a/src/main/java/com/likelion/dub/service/PostService.java +++ b/src/main/java/com/likelion/dub/service/PostService.java @@ -35,6 +35,37 @@ public class PostService { public List getAllClubs() { return this.postRepository.findAll(); } + + + public BaseResponse writing(String title, String content,int category)throws BaseException{ + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + //jwt token 오류 + if (authentication == null || !authentication.isAuthenticated()) { + return new BaseResponse(BaseResponseStatus.JWT_TOKEN_ERROR); + } + String email = authentication.getName(); + Member member = memberRepository.findByEmail(email).orElseThrow(); + Club club = member.getClub(); + String clubName = club.getClubName(); + + //이 club 글이 있으면 작성 불가 + postRepository.findByClubName(clubName) + .ifPresent(post -> { + throw new BaseException(BaseResponseStatus.USERS_EMPTY_USER_ID); + }); + + Post.PostBuilder postBuilder = Post.builder() + .clubName(clubName) + .title(title) + .content(content) + .category(category); + + + Post post = postBuilder.build(); + postRepository.save(post); + return new BaseResponse<>("글 작성 성공"); + } public BaseResponse writePost(String title, String content,int category, List files) throws BaseException { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); From c18c172c48cdb35c8cba05bdd8f8b201c0c5bd1b Mon Sep 17 00:00:00 2001 From: suhoon Date: Tue, 18 Jul 2023 17:26:52 +0900 Subject: [PATCH 02/72] =?UTF-8?q?=EA=B6=8C=ED=95=9C=20=EC=9E=84=EC=8B=9C?= =?UTF-8?q?=20=EB=AA=A8=EB=91=90=20=ED=97=88=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/likelion/dub/configuration/SecurityConfig.java | 6 +++--- src/main/resources/application.properties | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/likelion/dub/configuration/SecurityConfig.java b/src/main/java/com/likelion/dub/configuration/SecurityConfig.java index d7ca3e9..1a9ba14 100644 --- a/src/main/java/com/likelion/dub/configuration/SecurityConfig.java +++ b/src/main/java/com/likelion/dub/configuration/SecurityConfig.java @@ -46,9 +46,9 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .authorizeRequests() .requestMatchers("/app/member/sign-up", "/app/member/sign-in", "/app/member/email/{email}", "/app/member/stunum/{stunum}","/app/post/getAll").permitAll() //누구나 접근 가능 .requestMatchers(Swagger_url).permitAll() - .requestMatchers(HttpMethod.POST, "/app/member/test").hasRole("ADMIN") //admin 권한 필요 - .requestMatchers(HttpMethod.POST, "/app/post/write-post").hasRole("CLUB") //CLUB 권한 필요 - .anyRequest().authenticated() +// .requestMatchers(HttpMethod.POST, "/app/member/test").hasRole("ADMIN") //admin 권한 필요 +// .requestMatchers(HttpMethod.POST, "/app/post/write-post").hasRole("CLUB") //CLUB 권한 필요 + .anyRequest().permitAll() .and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 1396110..929482e 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,8 +1,8 @@ # MySQL ?? ?? spring.datasource.url=jdbc:mysql://localhost:3306/new_schema spring.datasource.username=root -spring.datasource.password=(120938yhok!) - spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver +spring.datasource.password=whqkr44## +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver # Hibernate ?? spring.jpa.show-sql=true From b840e0005d2f7aacbbcad05413e687179beb4672 Mon Sep 17 00:00:00 2001 From: suhoon Date: Tue, 18 Jul 2023 18:33:25 +0900 Subject: [PATCH 03/72] multipart --- src/main/resources/application.properties | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 929482e..120300b 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -11,3 +11,8 @@ spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect #jwt jwt.token.secret=VlwEyVBsYt9V7zq57TejMnVUyzblYcfPQye08f7MGVA9XkHa + +#multipart +spring.servlet.multipart.enabled=true +spring.servlet.multipart.max-file-size=10MB +spring.servlet.multipart.max-request-size=10MB From 5f834b2db24ec8a4bb692fe740c45fb48f62a6e2 Mon Sep 17 00:00:00 2001 From: suhoon Date: Tue, 18 Jul 2023 19:05:23 +0900 Subject: [PATCH 04/72] objectmapper --- .../com/likelion/dub/controller/PostController.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/likelion/dub/controller/PostController.java b/src/main/java/com/likelion/dub/controller/PostController.java index cc9f04a..efb1681 100644 --- a/src/main/java/com/likelion/dub/controller/PostController.java +++ b/src/main/java/com/likelion/dub/controller/PostController.java @@ -1,5 +1,8 @@ package com.likelion.dub.controller; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ser.Serializers; import com.likelion.dub.common.BaseException; import com.likelion.dub.common.BaseResponse; @@ -20,6 +23,7 @@ import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; +import springfox.documentation.spring.web.json.Json; import java.nio.file.Files; import java.util.List; @@ -64,12 +68,15 @@ public BaseResponse writePost(@RequestPart(value = "json") PostWritingRe @PostMapping("/writing") - public BaseResponse writing(@RequestPart(value = "json") PostWritingRequest dto) { + public BaseResponse writing(@RequestPart(value = "json") JsonNode json) { try { + ObjectMapper objectMapper = new ObjectMapper(); + PostWritingRequest dto = objectMapper.treeToValue(json, PostWritingRequest.class); + postService.writing(dto.getTitle(), dto.getContent(), dto.getCategory()); return new BaseResponse<>("글 작성 성공"); - } catch (BaseException e) { - return new BaseResponse<>(e.getStatus()); + } catch (BaseException | JsonProcessingException e) { + return new BaseResponse<>(e.getMessage()); } } /** From c8730fbcf8a7f7334f1e70a0b4548c5908b5bfe1 Mon Sep 17 00:00:00 2001 From: suhoon Date: Tue, 18 Jul 2023 19:15:24 +0900 Subject: [PATCH 05/72] hashmap --- .../com/likelion/dub/controller/PostController.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/likelion/dub/controller/PostController.java b/src/main/java/com/likelion/dub/controller/PostController.java index efb1681..eb2f094 100644 --- a/src/main/java/com/likelion/dub/controller/PostController.java +++ b/src/main/java/com/likelion/dub/controller/PostController.java @@ -27,6 +27,7 @@ import java.nio.file.Files; import java.util.List; +import java.util.Map; @RestController @RequestMapping("/app/post") @@ -68,14 +69,15 @@ public BaseResponse writePost(@RequestPart(value = "json") PostWritingRe @PostMapping("/writing") - public BaseResponse writing(@RequestPart(value = "json") JsonNode json) { + public BaseResponse writing(@RequestBody Map requestData) { try { - ObjectMapper objectMapper = new ObjectMapper(); - PostWritingRequest dto = objectMapper.treeToValue(json, PostWritingRequest.class); + String title = (String) requestData.get("title"); + String content = (String) requestData.get("content"); + int category = (int) requestData.get("category"); - postService.writing(dto.getTitle(), dto.getContent(), dto.getCategory()); + postService.writing(title, content, category); return new BaseResponse<>("글 작성 성공"); - } catch (BaseException | JsonProcessingException e) { + } catch (Exception e) { return new BaseResponse<>(e.getMessage()); } } From ef888a2ef4f6f7a43191d97d237910ab2f7bbb0c Mon Sep 17 00:00:00 2001 From: suhoon Date: Tue, 18 Jul 2023 19:24:24 +0900 Subject: [PATCH 06/72] json --- src/main/java/com/likelion/dub/controller/PostController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/likelion/dub/controller/PostController.java b/src/main/java/com/likelion/dub/controller/PostController.java index eb2f094..ed3848d 100644 --- a/src/main/java/com/likelion/dub/controller/PostController.java +++ b/src/main/java/com/likelion/dub/controller/PostController.java @@ -69,7 +69,7 @@ public BaseResponse writePost(@RequestPart(value = "json") PostWritingRe @PostMapping("/writing") - public BaseResponse writing(@RequestBody Map requestData) { + public BaseResponse writing(@RequestPart(value="json") Map requestData) { try { String title = (String) requestData.get("title"); String content = (String) requestData.get("content"); From 37f0c5fe90da1cc8b758a1558b399da8126f7da4 Mon Sep 17 00:00:00 2001 From: suhoon Date: Tue, 18 Jul 2023 19:40:06 +0900 Subject: [PATCH 07/72] mediatype --- .../java/com/likelion/dub/controller/PostController.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/likelion/dub/controller/PostController.java b/src/main/java/com/likelion/dub/controller/PostController.java index ed3848d..92e7adc 100644 --- a/src/main/java/com/likelion/dub/controller/PostController.java +++ b/src/main/java/com/likelion/dub/controller/PostController.java @@ -17,6 +17,7 @@ import com.likelion.dub.service.PostService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; @@ -57,7 +58,7 @@ public BaseResponse> getAllClubs() { * @return */ - @PostMapping("/write-post") + @PostMapping(value = "/write-post" , consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE}) public BaseResponse writePost(@RequestPart(value = "json") PostWritingRequest dto, @RequestPart(value = "images", required = false) List files) throws BaseException { try { postService.writePost(dto.getTitle(), dto.getContent(), dto.getCategory(), files); @@ -68,7 +69,7 @@ public BaseResponse writePost(@RequestPart(value = "json") PostWritingRe } - @PostMapping("/writing") + @PostMapping(value = "/writing",consumes = {MediaType.APPLICATION_JSON_VALUE}) public BaseResponse writing(@RequestPart(value="json") Map requestData) { try { String title = (String) requestData.get("title"); From 0f4e7461836f9095d8a704c70f81f9bad484c91b Mon Sep 17 00:00:00 2001 From: suhoon <82464990+s2hoon@users.noreply.github.com> Date: Thu, 20 Jul 2023 22:29:01 +0900 Subject: [PATCH 08/72] Create gradle.yml --- .github/workflows/gradle.yml | 131 +++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 .github/workflows/gradle.yml diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml new file mode 100644 index 0000000..13e611d --- /dev/null +++ b/.github/workflows/gradle.yml @@ -0,0 +1,131 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. +# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle + +name: Java CI with Gradle + +on: + push: + branches: + - main + - develop + pull_request: + branches: [ "main" ] + +permissions: + contents: read + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up JDK 11 + uses: actions/setup-java@v3 + with: + java-version: '11' + distribution: 'temurin' + ## gradle caching + - name: Gradle Caching + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + ## create application-dev.properties + - name: make application-dev.properties + if: contains(github.ref, 'develop') + run: | + cd ./src/main/resources + touch ./application-dev.properties + echo "${{ secrets.PROPERTIES_DEV }}" > ./application-dev.properties + shell: bash + + ## create application-prod.properties + - name: make application-prod.properties + if: contains(github.ref, 'main') # branch가 main 일 때, 나머지는 위와 동일 + run: | + cd ./src/main/resources + touch ./application-prod.properties + echo "${{ secrets.PROPERTIES_PROD }}" > ./application-prod.properties + shell: bash + + ## gradle build + - name: Build with Gradle + run: ./gradlew build -x test -x ktlintCheck -x ktlintTestSourceSetCheck -x ktlintMainSourceSetCheck -x ktlintKotlinScriptCheck + + ## docker build & push to production + - name: Docker build & push to prod + if: contains(github.ref, 'main') + run: | + docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} + docker build -f Dockerfile-prod -t ${{ secrets.DOCKER_REPO }}/dub-prod . + docker push ${{ secrets.DOCKER_REPO }}/dub-prod + + ## docker build & push to develop + - name: Docker build & push to dev + if: contains(github.ref, 'develop') + run: | + docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} + docker build -f Dockerfile-dev -t ${{ secrets.DOCKER_REPO }}/dub-dev . + docker push ${{ secrets.DOCKER_REPO }}/dub-dev + + + ## deploy to production + - name: Deploy to prod + uses: appleboy/ssh-action@master + id: deploy-prod + if: contains(github.ref, 'main') + with: + host: ${{ secrets.HOST_PROD }} + username: ec2-user + key: ${{ secrets.PRIVATE_KEY }} + envs: GITHUB_SHA + script: | + sudo docker rm -f $(docker ps -qa) + sudo docker pull ${{ secrets.DOCKER_REPO }}/dub-prod + docker-compose up -d + docker image prune -f + + ## deploy to develop + - name: Deploy to dev + uses: appleboy/ssh-action@master + id: deploy-dev + if: contains(github.ref, 'develop') + with: + host: ${{ secrets.HOST_DEV }} + username: ${{ secrets.USERNAME }} + password: ${{ secrets.PASSWORD }} + port: 22 + #key: ${{ secrets.PRIVATE_KEY }} + script: | + sudo docker rm -f $(docker ps -qa) + sudo docker pull ${{ secrets.DOCKER_REPO }}/dub-dev + docker-compose up -d + docker image prune -f + + ## time +current-time: + needs: CI-CD + runs-on: ubuntu-latest + steps: + - name: Get Current Time + uses: 1466587594/get-current-time@v2 + id: current-time + with: + format: YYYY-MM-DDTHH:mm:ss + utcOffset: "+09:00" # 기준이 UTC이기 때문에 한국시간인 KST를 맞추기 위해 +9시간 추가 + + - name: Print Current Time + run: echo "Current Time=${{steps.current-time.outputs.formattedTime}}" # current-time 에서 지정한 포맷대로 현재 시간 출력 + shell: bash + From 6be3ce9b0bb285927460fcdacfd6576f962c0f6b Mon Sep 17 00:00:00 2001 From: suhoon <82464990+s2hoon@users.noreply.github.com> Date: Thu, 20 Jul 2023 23:00:20 +0900 Subject: [PATCH 09/72] Update gradle.yml --- .github/workflows/gradle.yml | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 13e611d..b4a0db5 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -114,18 +114,18 @@ jobs: docker image prune -f ## time -current-time: - needs: CI-CD - runs-on: ubuntu-latest - steps: - - name: Get Current Time - uses: 1466587594/get-current-time@v2 - id: current-time - with: - format: YYYY-MM-DDTHH:mm:ss - utcOffset: "+09:00" # 기준이 UTC이기 때문에 한국시간인 KST를 맞추기 위해 +9시간 추가 + current-time: + needs: CI-CD + runs-on: ubuntu-latest + steps: + - name: Get Current Time + uses: 1466587594/get-current-time@v2 + id: current-time + with: + format: YYYY-MM-DDTHH:mm:ss + utcOffset: "+09:00" # 기준이 UTC이기 때문에 한국시간인 KST를 맞추기 위해 +9시간 추가 - - name: Print Current Time - run: echo "Current Time=${{steps.current-time.outputs.formattedTime}}" # current-time 에서 지정한 포맷대로 현재 시간 출력 - shell: bash + - name: Print Current Time + run: echo "Current Time=${{steps.current-time.outputs.formattedTime}}" # current-time 에서 지정한 포맷대로 현재 시간 출력 + shell: bash From 7f56e4090e5469eb3a093c7dde3d298ef3d10de7 Mon Sep 17 00:00:00 2001 From: suhoon <82464990+s2hoon@users.noreply.github.com> Date: Thu, 20 Jul 2023 23:04:17 +0900 Subject: [PATCH 10/72] Update gradle.yml --- .github/workflows/gradle.yml | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index b4a0db5..70df8fc 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -88,7 +88,7 @@ jobs: with: host: ${{ secrets.HOST_PROD }} username: ec2-user - key: ${{ secrets.PRIVATE_KEY }} + key: ${{ secrets.SSH_KEY }} envs: GITHUB_SHA script: | sudo docker rm -f $(docker ps -qa) @@ -103,8 +103,8 @@ jobs: if: contains(github.ref, 'develop') with: host: ${{ secrets.HOST_DEV }} - username: ${{ secrets.USERNAME }} - password: ${{ secrets.PASSWORD }} + username: ec2-user + key: ${{ secrets.SSH_KEY }} port: 22 #key: ${{ secrets.PRIVATE_KEY }} script: | @@ -113,19 +113,4 @@ jobs: docker-compose up -d docker image prune -f - ## time - current-time: - needs: CI-CD - runs-on: ubuntu-latest - steps: - - name: Get Current Time - uses: 1466587594/get-current-time@v2 - id: current-time - with: - format: YYYY-MM-DDTHH:mm:ss - utcOffset: "+09:00" # 기준이 UTC이기 때문에 한국시간인 KST를 맞추기 위해 +9시간 추가 - - - name: Print Current Time - run: echo "Current Time=${{steps.current-time.outputs.formattedTime}}" # current-time 에서 지정한 포맷대로 현재 시간 출력 - shell: bash From e3a20f0ce0a62c4ce6298f9f1f0d5f1b5ec2b81c Mon Sep 17 00:00:00 2001 From: suhoon <82464990+s2hoon@users.noreply.github.com> Date: Thu, 20 Jul 2023 23:07:14 +0900 Subject: [PATCH 11/72] Update gradle.yml --- .github/workflows/gradle.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 70df8fc..5950c94 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -24,6 +24,9 @@ jobs: runs-on: ubuntu-latest steps: + + - name: Make gradlew executable + run: chmod +x ./gradlew - uses: actions/checkout@v3 - name: Set up JDK 11 uses: actions/setup-java@v3 From ea088ba476f8f5163093823d1d0eda7e02331304 Mon Sep 17 00:00:00 2001 From: suhoon <82464990+s2hoon@users.noreply.github.com> Date: Thu, 20 Jul 2023 23:09:59 +0900 Subject: [PATCH 12/72] Update gradle.yml --- .github/workflows/gradle.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 5950c94..0e70f73 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -25,8 +25,7 @@ jobs: steps: - - name: Make gradlew executable - run: chmod +x ./gradlew + - uses: actions/checkout@v3 - name: Set up JDK 11 uses: actions/setup-java@v3 From 4cf729e20d5424a846c3cb4cd956f53b3249c760 Mon Sep 17 00:00:00 2001 From: suhoon Date: Thu, 20 Jul 2023 23:16:59 +0900 Subject: [PATCH 13/72] permission access for gradlew --- gradlew | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 gradlew diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 From c26d9670dcf3ad4885d6248f4c68e30b5c2d8299 Mon Sep 17 00:00:00 2001 From: suhoon Date: Thu, 20 Jul 2023 23:24:16 +0900 Subject: [PATCH 14/72] dockerfile --- .gitignore | 4 +++- Dockerfile-dev | 6 ++++++ Dockerfile-prod | 6 ++++++ 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 Dockerfile-dev create mode 100644 Dockerfile-prod diff --git a/.gitignore b/.gitignore index cbd5291..e4b746f 100644 --- a/.gitignore +++ b/.gitignore @@ -38,4 +38,6 @@ out/ ##application.properties## -application.properties \ No newline at end of file +application.properties +application-dev.properties +application-prod.properties diff --git a/Dockerfile-dev b/Dockerfile-dev new file mode 100644 index 0000000..01c806c --- /dev/null +++ b/Dockerfile-dev @@ -0,0 +1,6 @@ +## Dockerfile-dev +FROM openjdk:14-jdk-slim +EXPOSE 8080 +ARG JAR_FILE=/build/libs/dub-0.0.1-SNAPSHOT.jar +COPY ${JAR_FILE} app.jar +ENTRYPOINT ["java","-jar","-Dspring.profiles.active=dev","/app.jar"] \ No newline at end of file diff --git a/Dockerfile-prod b/Dockerfile-prod new file mode 100644 index 0000000..01c806c --- /dev/null +++ b/Dockerfile-prod @@ -0,0 +1,6 @@ +## Dockerfile-dev +FROM openjdk:14-jdk-slim +EXPOSE 8080 +ARG JAR_FILE=/build/libs/dub-0.0.1-SNAPSHOT.jar +COPY ${JAR_FILE} app.jar +ENTRYPOINT ["java","-jar","-Dspring.profiles.active=dev","/app.jar"] \ No newline at end of file From 995e8cb43756566b1a8760458e06742e24d54272 Mon Sep 17 00:00:00 2001 From: suhoon <82464990+s2hoon@users.noreply.github.com> Date: Thu, 20 Jul 2023 23:27:38 +0900 Subject: [PATCH 15/72] Update gradle.yml --- .github/workflows/gradle.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 0e70f73..6ed6811 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -27,10 +27,10 @@ jobs: - uses: actions/checkout@v3 - - name: Set up JDK 11 + - name: Set up JDK 19 uses: actions/setup-java@v3 with: - java-version: '11' + java-version: '19' distribution: 'temurin' ## gradle caching - name: Gradle Caching From fec04db4feec01210d50cf25c64743c6f9dabf92 Mon Sep 17 00:00:00 2001 From: suhoon <82464990+s2hoon@users.noreply.github.com> Date: Thu, 20 Jul 2023 23:35:58 +0900 Subject: [PATCH 16/72] Update gradle.yml --- .github/workflows/gradle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 6ed6811..a46197b 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -63,7 +63,7 @@ jobs: ## gradle build - name: Build with Gradle - run: ./gradlew build -x test -x ktlintCheck -x ktlintTestSourceSetCheck -x ktlintMainSourceSetCheck -x ktlintKotlinScriptCheck + run: ./gradlew build -x test ## docker build & push to production - name: Docker build & push to prod From 80236a60465d865de41317eb4fce2dd474cd2187 Mon Sep 17 00:00:00 2001 From: suhoon <82464990+s2hoon@users.noreply.github.com> Date: Thu, 20 Jul 2023 23:51:13 +0900 Subject: [PATCH 17/72] Update gradle.yml --- .github/workflows/gradle.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index a46197b..ccbbd0a 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -70,16 +70,16 @@ jobs: if: contains(github.ref, 'main') run: | docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} - docker build -f Dockerfile-prod -t ${{ secrets.DOCKER_REPO }}/dub-prod . - docker push ${{ secrets.DOCKER_REPO }}/dub-prod + docker build -f Dockerfile-prod -t ${{ secrets.DOCKER_USERNAME }}/dub-prod . + docker push ${{ secrets.DOCKER_USERNAME }}/dub-prod ## docker build & push to develop - name: Docker build & push to dev if: contains(github.ref, 'develop') run: | docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} - docker build -f Dockerfile-dev -t ${{ secrets.DOCKER_REPO }}/dub-dev . - docker push ${{ secrets.DOCKER_REPO }}/dub-dev + docker build -f Dockerfile-dev -t ${{ secrets.DOCKER_USERNAME }}/dub-dev . + docker push ${{ secrets.DOCKER_USERNAME }}/dub-dev ## deploy to production @@ -94,9 +94,9 @@ jobs: envs: GITHUB_SHA script: | sudo docker rm -f $(docker ps -qa) - sudo docker pull ${{ secrets.DOCKER_REPO }}/dub-prod - docker-compose up -d - docker image prune -f + sudo docker pull ${{ secrets.DOCKER_USERNAME }}/dub-prod + sudo docker run -d -p 8081:8081 ${{ secrets.DOCKER_USERNAME }}/dub-prod + sudo docker image prune -f ## deploy to develop - name: Deploy to dev @@ -111,8 +111,8 @@ jobs: #key: ${{ secrets.PRIVATE_KEY }} script: | sudo docker rm -f $(docker ps -qa) - sudo docker pull ${{ secrets.DOCKER_REPO }}/dub-dev - docker-compose up -d - docker image prune -f + sudo docker pull ${{ secrets.DOCKER_USERNAME }}/dub-dev + sudo docker run -d -p 8082:8082 ${{ secrets.DOCKER_USERNAME }}/dub-dev + sudo docker image prune -f From 18d08f56a15de27157291ad57e87c57ad505838d Mon Sep 17 00:00:00 2001 From: suhoon <82464990+s2hoon@users.noreply.github.com> Date: Thu, 20 Jul 2023 23:56:58 +0900 Subject: [PATCH 18/72] Update gradle.yml --- .github/workflows/gradle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index ccbbd0a..4ce06c0 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -89,7 +89,7 @@ jobs: if: contains(github.ref, 'main') with: host: ${{ secrets.HOST_PROD }} - username: ec2-user + username: ubuntu key: ${{ secrets.SSH_KEY }} envs: GITHUB_SHA script: | From 517ddb11b91cc961c9c4a3baef9183f34d5a5b98 Mon Sep 17 00:00:00 2001 From: suhoon Date: Fri, 21 Jul 2023 00:02:38 +0900 Subject: [PATCH 19/72] test cicd with gitactions --- .../dub/controller/MemberController.java | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/likelion/dub/controller/MemberController.java b/src/main/java/com/likelion/dub/controller/MemberController.java index 1eaedac..bbe2536 100644 --- a/src/main/java/com/likelion/dub/controller/MemberController.java +++ b/src/main/java/com/likelion/dub/controller/MemberController.java @@ -24,6 +24,12 @@ public class MemberController { private final MemberService memberService; + @GetMapping("testcicd") + public BaseResponse testcicd() { + return new BaseResponse<>("test complete"); + } + + /** * 이메일 중복체크 * @param email @@ -32,13 +38,13 @@ public class MemberController { @GetMapping("/email/{email}") public BaseResponse checkEmail(@PathVariable String email) { - boolean isEmailAvailable = memberService.checkEmail(email); - if (isEmailAvailable) { - String result = "이메일 사용 가능"; - return new BaseResponse<>(result); - } else { - return new BaseResponse(BaseResponseStatus.EMAIL_ALREADY_EXIST); - } + boolean isEmailAvailable = memberService.checkEmail(email); + if (isEmailAvailable) { + String result = "이메일 사용 가능"; + return new BaseResponse<>(result); + } else { + return new BaseResponse(BaseResponseStatus.EMAIL_ALREADY_EXIST); + } } From dcb52bdfcecc4ccad4e1d556ea8e7ae53fbc66db Mon Sep 17 00:00:00 2001 From: suhoon <82464990+s2hoon@users.noreply.github.com> Date: Fri, 21 Jul 2023 00:11:03 +0900 Subject: [PATCH 20/72] Update gradle.yml --- .github/workflows/gradle.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 4ce06c0..8df7a0f 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -93,7 +93,8 @@ jobs: key: ${{ secrets.SSH_KEY }} envs: GITHUB_SHA script: | - sudo docker rm -f $(docker ps -qa) + sudo docker stop $(docker ps -a -q) + sudo docker rm $(docker ps -a -q) sudo docker pull ${{ secrets.DOCKER_USERNAME }}/dub-prod sudo docker run -d -p 8081:8081 ${{ secrets.DOCKER_USERNAME }}/dub-prod sudo docker image prune -f @@ -110,7 +111,8 @@ jobs: port: 22 #key: ${{ secrets.PRIVATE_KEY }} script: | - sudo docker rm -f $(docker ps -qa) + sudo docker stop $(docker ps -a -q) + sudo docker rm $(docker ps -a -q) sudo docker pull ${{ secrets.DOCKER_USERNAME }}/dub-dev sudo docker run -d -p 8082:8082 ${{ secrets.DOCKER_USERNAME }}/dub-dev sudo docker image prune -f From 3ab4f239aafca64c0f8b4b269f10eb5ee86b96e5 Mon Sep 17 00:00:00 2001 From: suhoon <82464990+s2hoon@users.noreply.github.com> Date: Fri, 21 Jul 2023 00:20:40 +0900 Subject: [PATCH 21/72] Update gradle.yml --- .github/workflows/gradle.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 8df7a0f..bfd51aa 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -93,8 +93,8 @@ jobs: key: ${{ secrets.SSH_KEY }} envs: GITHUB_SHA script: | - sudo docker stop $(docker ps -a -q) - sudo docker rm $(docker ps -a -q) + sudo docker stop dub-prod + sudo docker rm -fv dub-prod sudo docker pull ${{ secrets.DOCKER_USERNAME }}/dub-prod sudo docker run -d -p 8081:8081 ${{ secrets.DOCKER_USERNAME }}/dub-prod sudo docker image prune -f @@ -111,8 +111,8 @@ jobs: port: 22 #key: ${{ secrets.PRIVATE_KEY }} script: | - sudo docker stop $(docker ps -a -q) - sudo docker rm $(docker ps -a -q) + sudo docker stop dub-dev + sudo docker rm -fv dub-dev sudo docker pull ${{ secrets.DOCKER_USERNAME }}/dub-dev sudo docker run -d -p 8082:8082 ${{ secrets.DOCKER_USERNAME }}/dub-dev sudo docker image prune -f From 7b4e4831b98bc39c0f786ba01bf56fd7094d853a Mon Sep 17 00:00:00 2001 From: suhoon Date: Fri, 21 Jul 2023 00:23:15 +0900 Subject: [PATCH 22/72] test cicd --- .../likelion/dub/controller/MemberController.java | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/likelion/dub/controller/MemberController.java b/src/main/java/com/likelion/dub/controller/MemberController.java index bbe2536..321287c 100644 --- a/src/main/java/com/likelion/dub/controller/MemberController.java +++ b/src/main/java/com/likelion/dub/controller/MemberController.java @@ -1,21 +1,15 @@ package com.likelion.dub.controller; -import com.fasterxml.jackson.databind.ser.Serializers; import com.likelion.dub.common.BaseException; import com.likelion.dub.common.BaseResponse; import com.likelion.dub.common.BaseResponseStatus; -import com.likelion.dub.domain.Club; import com.likelion.dub.domain.dto.MemberJoinRequest; import com.likelion.dub.domain.dto.MemberLoginRequest; -import com.likelion.dub.exception.AppException; -import com.likelion.dub.exception.Errorcode; import com.likelion.dub.service.MemberService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import static com.likelion.dub.common.BaseResponseStatus.WRONG_EMAIL; - @RestController @RequestMapping("/app/member") @RequiredArgsConstructor @@ -24,9 +18,9 @@ public class MemberController { private final MemberService memberService; - @GetMapping("testcicd") - public BaseResponse testcicd() { - return new BaseResponse<>("test complete"); + @GetMapping("/testcicd") + public ResponseEntity testcicd() { + return ResponseEntity.ok().body("test 성공"); } From 0617803cc7930670b0d6c7674127d10b4cec6014 Mon Sep 17 00:00:00 2001 From: suhoon <82464990+s2hoon@users.noreply.github.com> Date: Fri, 21 Jul 2023 00:27:04 +0900 Subject: [PATCH 23/72] Update gradle.yml --- .github/workflows/gradle.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index bfd51aa..c0ae456 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -96,7 +96,7 @@ jobs: sudo docker stop dub-prod sudo docker rm -fv dub-prod sudo docker pull ${{ secrets.DOCKER_USERNAME }}/dub-prod - sudo docker run -d -p 8081:8081 ${{ secrets.DOCKER_USERNAME }}/dub-prod + sudo docker run -d -p 8080:8080 ${{ secrets.DOCKER_USERNAME }}/dub-prod sudo docker image prune -f ## deploy to develop @@ -114,7 +114,7 @@ jobs: sudo docker stop dub-dev sudo docker rm -fv dub-dev sudo docker pull ${{ secrets.DOCKER_USERNAME }}/dub-dev - sudo docker run -d -p 8082:8082 ${{ secrets.DOCKER_USERNAME }}/dub-dev + sudo docker run -d -p 8080:8080 ${{ secrets.DOCKER_USERNAME }}/dub-dev sudo docker image prune -f From b3be1458fbec0eea0c76d09a1a75a5f49ae70ae6 Mon Sep 17 00:00:00 2001 From: suhoon Date: Fri, 21 Jul 2023 01:11:34 +0900 Subject: [PATCH 24/72] openjdk 19 --- Dockerfile-dev | 2 +- Dockerfile-prod | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile-dev b/Dockerfile-dev index 01c806c..3f9d023 100644 --- a/Dockerfile-dev +++ b/Dockerfile-dev @@ -1,5 +1,5 @@ ## Dockerfile-dev -FROM openjdk:14-jdk-slim +FROM openjdk:19-jdk-alpine EXPOSE 8080 ARG JAR_FILE=/build/libs/dub-0.0.1-SNAPSHOT.jar COPY ${JAR_FILE} app.jar diff --git a/Dockerfile-prod b/Dockerfile-prod index 01c806c..3f9d023 100644 --- a/Dockerfile-prod +++ b/Dockerfile-prod @@ -1,5 +1,5 @@ ## Dockerfile-dev -FROM openjdk:14-jdk-slim +FROM openjdk:19-jdk-alpine EXPOSE 8080 ARG JAR_FILE=/build/libs/dub-0.0.1-SNAPSHOT.jar COPY ${JAR_FILE} app.jar From 47086940a8923af4fb6ae0c590b800f2a66f1d27 Mon Sep 17 00:00:00 2001 From: suhoon Date: Fri, 21 Jul 2023 01:58:24 +0900 Subject: [PATCH 25/72] openjdk-19-alpine --- Dockerfile-dev | 2 +- Dockerfile-prod | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile-dev b/Dockerfile-dev index 3f9d023..5da3e54 100644 --- a/Dockerfile-dev +++ b/Dockerfile-dev @@ -1,5 +1,5 @@ ## Dockerfile-dev -FROM openjdk:19-jdk-alpine +FROM openjdk:19-alpine EXPOSE 8080 ARG JAR_FILE=/build/libs/dub-0.0.1-SNAPSHOT.jar COPY ${JAR_FILE} app.jar diff --git a/Dockerfile-prod b/Dockerfile-prod index 3f9d023..5da3e54 100644 --- a/Dockerfile-prod +++ b/Dockerfile-prod @@ -1,5 +1,5 @@ ## Dockerfile-dev -FROM openjdk:19-jdk-alpine +FROM openjdk:19-alpine EXPOSE 8080 ARG JAR_FILE=/build/libs/dub-0.0.1-SNAPSHOT.jar COPY ${JAR_FILE} app.jar From cd90b5e831fdbb5a252f7864895c66035462b5f7 Mon Sep 17 00:00:00 2001 From: suhoon Date: Fri, 21 Jul 2023 02:51:47 +0900 Subject: [PATCH 26/72] =?UTF-8?q?=EC=9D=B4=EB=A6=84=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile-prod | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile-prod b/Dockerfile-prod index 5da3e54..dba27ef 100644 --- a/Dockerfile-prod +++ b/Dockerfile-prod @@ -1,6 +1,6 @@ -## Dockerfile-dev +## Dockerfile-prod FROM openjdk:19-alpine EXPOSE 8080 ARG JAR_FILE=/build/libs/dub-0.0.1-SNAPSHOT.jar COPY ${JAR_FILE} app.jar -ENTRYPOINT ["java","-jar","-Dspring.profiles.active=dev","/app.jar"] \ No newline at end of file +ENTRYPOINT ["java","-jar","-Dspring.profiles.active=prod","/app.jar"] \ No newline at end of file From 4c3ab87727f671b411ee5a776ec7bd2a5bdc0501 Mon Sep 17 00:00:00 2001 From: suhoon Date: Fri, 21 Jul 2023 20:13:49 +0900 Subject: [PATCH 27/72] =?UTF-8?q?=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20?= =?UTF-8?q?=EC=A0=84=EC=B2=B4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dub/controller/MemberController.java | 55 +++++---- .../dub/controller/MypageController.java | 2 +- .../dub/controller/PostController.java | 1 - .../java/com/likelion/dub/domain/Club.java | 39 +++--- .../com/likelion/dub/domain/ClubImage.java | 72 +++++------ .../java/com/likelion/dub/domain/Comment.java | 39 ++++++ .../java/com/likelion/dub/domain/Image.java | 5 - .../java/com/likelion/dub/domain/Member.java | 17 ++- .../java/com/likelion/dub/domain/Post.java | 41 +++---- .../java/com/likelion/dub/domain/Tag.java | 31 ----- .../dub/domain/dto/ClubMemberJoinRequest.java | 40 +++++++ .../dub/domain/dto/MemberJoinRequest.java | 2 +- .../dub/domain/dto/MyPageResponse.java | 3 +- .../dub/repository/MemberRepository.java | 2 - .../likelion/dub/service/MemberService.java | 113 ++++++++++-------- .../com/likelion/dub/service/PostService.java | 69 ++--------- src/main/resources/application.properties | 2 +- 17 files changed, 269 insertions(+), 264 deletions(-) create mode 100644 src/main/java/com/likelion/dub/domain/Comment.java delete mode 100644 src/main/java/com/likelion/dub/domain/Tag.java create mode 100644 src/main/java/com/likelion/dub/domain/dto/ClubMemberJoinRequest.java diff --git a/src/main/java/com/likelion/dub/controller/MemberController.java b/src/main/java/com/likelion/dub/controller/MemberController.java index 321287c..aeb59b4 100644 --- a/src/main/java/com/likelion/dub/controller/MemberController.java +++ b/src/main/java/com/likelion/dub/controller/MemberController.java @@ -3,12 +3,18 @@ import com.likelion.dub.common.BaseException; import com.likelion.dub.common.BaseResponse; import com.likelion.dub.common.BaseResponseStatus; +import com.likelion.dub.domain.dto.ClubMemberJoinRequest; import com.likelion.dub.domain.dto.MemberJoinRequest; import com.likelion.dub.domain.dto.MemberLoginRequest; +import com.likelion.dub.domain.dto.PostWritingRequest; import com.likelion.dub.service.MemberService; import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; @RestController @RequestMapping("/app/member") @@ -42,33 +48,18 @@ public BaseResponse checkEmail(@PathVariable String email) { } - /** - * 학번 중복체크 - * @param stunum - * @return - */ - @GetMapping("/stunum/{stunum}") - public BaseResponse checkStunum(@PathVariable Long stunum) { - boolean isStunumAvailable = memberService.checkStunum(stunum); - if(isStunumAvailable) { - String result = "학번 사용 가능"; - return new BaseResponse<>(result); - } else { - return new BaseResponse(BaseResponseStatus.STU_NUM_ALREADY_EXIST); - } - } - /** - * 회원가입 + * 일반회원 회원가입 * @param dto * @return */ @PostMapping("/sign-up") public BaseResponse join(@RequestBody MemberJoinRequest dto ) throws BaseException{ try { - memberService.join(dto.getEmail(), dto.getName(), dto.getPassword(), dto.getStunum(), dto.getRole()); - String result = "회원 가입 완료"; + + memberService.join(dto.getEmail(), dto.getName(), dto.getPassword(), dto.getGender(), dto.getRole()); + String result = "(일반)회원 가입 완료"; return new BaseResponse<>(result); } catch(BaseException e){ @@ -76,6 +67,29 @@ public BaseResponse join(@RequestBody MemberJoinRequest dto ) throws Bas } } + /** + * 동아리회원 회원가입 + * @param dto + * @param file + * @return + * @throws BaseException + */ + @PostMapping(value = "/sign-up-club",consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE}) + public BaseResponse joinClub(@RequestPart(value = "json") ClubMemberJoinRequest dto, @RequestPart(value = "image", required = false) MultipartFile file) throws BaseException { + + try { + memberService.joinClub(dto.getEmail(), dto.getName(), dto.getPassword(), dto.getGender(), dto.getRole(), dto.getIntroduction(), dto.getGroupName(),dto.getCategory(), file); + + String result = "(동아리)회원 가입 완료"; + return new BaseResponse<>(result); + + } catch (BaseException e) { + return new BaseResponse<>(e.getStatus()); + } + } + + + /** * 로그인 * @param dto @@ -83,9 +97,10 @@ public BaseResponse join(@RequestBody MemberJoinRequest dto ) throws Bas */ @PostMapping("/sign-in") public BaseResponse login(@RequestBody MemberLoginRequest dto) { + try { String token = memberService.login(dto.getEmail(), dto.getPassword()); - return new BaseResponse<>("Bearer "+token); + return new BaseResponse<>("Bearer " + token); } catch (BaseException e) { BaseResponseStatus result = e.getStatus(); diff --git a/src/main/java/com/likelion/dub/controller/MypageController.java b/src/main/java/com/likelion/dub/controller/MypageController.java index 5d20cf5..6c812fa 100644 --- a/src/main/java/com/likelion/dub/controller/MypageController.java +++ b/src/main/java/com/likelion/dub/controller/MypageController.java @@ -47,7 +47,7 @@ public BaseResponse getMyPage(){ log.info(email.toString()); log.info(member.toString()); - MyPageResponse myPageResponse = new MyPageResponse(member.getEmail(), member.getName(), member.getStunum(), member.getRole()); + MyPageResponse myPageResponse = new MyPageResponse(member.getEmail(), member.getName(), member.getRole()); return new BaseResponse<>(myPageResponse); } catch (BaseException e) { return new BaseResponse(BaseResponseStatus.JWT_TOKEN_ERROR); diff --git a/src/main/java/com/likelion/dub/controller/PostController.java b/src/main/java/com/likelion/dub/controller/PostController.java index 92e7adc..82ea96c 100644 --- a/src/main/java/com/likelion/dub/controller/PostController.java +++ b/src/main/java/com/likelion/dub/controller/PostController.java @@ -114,7 +114,6 @@ public BaseResponse editPost(@RequestPart(value="json") PostEditRequest } String email = authentication.getName(); - postService.editPost(email, newTitle, newContent, newCategory, newImages); return new BaseResponse<>(BaseResponseStatus.SUCCESS); } diff --git a/src/main/java/com/likelion/dub/domain/Club.java b/src/main/java/com/likelion/dub/domain/Club.java index 5eb786e..d2b86fb 100644 --- a/src/main/java/com/likelion/dub/domain/Club.java +++ b/src/main/java/com/likelion/dub/domain/Club.java @@ -2,10 +2,7 @@ import jakarta.persistence.*; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.*; import java.util.ArrayList; import java.util.List; @@ -15,6 +12,8 @@ @Entity @AllArgsConstructor @Getter +@Setter +@Table(name = "club") public class Club { @Id @@ -22,32 +21,30 @@ public class Club { @Column(name = "club_id") private Long id; + + + @OneToMany(mappedBy = "club") + private List post = new ArrayList<>(); + @Column - private String image; + private String clubName; @Column @Lob private String introduction; - @Column - private String clubName; - - @OneToOne(fetch=FetchType.LAZY) - private Post post; + private String groupName; + @Column + private String category; + @Lob + @Column + private byte[] clubImage; @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY) @JoinColumn(name = "member_id") private Member member; - @OneToMany(mappedBy = "club") - private List tags = new ArrayList<>(); - - @OneToOne(mappedBy = "club", - cascade = {CascadeType.PERSIST, CascadeType.REMOVE}, - orphanRemoval = true) - private ClubImage clubImage; - public void setMember(Member member) { this.member = member; @@ -55,4 +52,10 @@ public void setMember(Member member) { } + + public void setPost(Post post) { + this.post.add(post); + post.setClub(this); + } + } diff --git a/src/main/java/com/likelion/dub/domain/ClubImage.java b/src/main/java/com/likelion/dub/domain/ClubImage.java index ee2c144..38bdec1 100644 --- a/src/main/java/com/likelion/dub/domain/ClubImage.java +++ b/src/main/java/com/likelion/dub/domain/ClubImage.java @@ -1,44 +1,30 @@ -package com.likelion.dub.domain; - - -import jakarta.persistence.*; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@Entity -@NoArgsConstructor -@AllArgsConstructor -@Builder -@Table(name = "ClubImage") -public class ClubImage { - @Id - @GeneratedValue(strategy= GenerationType.IDENTITY) - @Column - private Long id; - - @OneToOne - @JoinColumn(name = "club_id") - private Club club; - - @Column - private String origFileName; - - @Column - private String filePath; - public ClubImage(String origFileName, String filePath, Long fileSize){ - this.origFileName = origFileName; - this.filePath = filePath; - this.fileSize = fileSize; - } - - - private Long fileSize; - - - -} - +//package com.likelion.dub.domain; +// +// +//import jakarta.persistence.*; +//import lombok.AllArgsConstructor; +//import lombok.Builder; +//import lombok.Getter; +//import lombok.NoArgsConstructor; +// +//@Getter +//@Entity +//@NoArgsConstructor +//@AllArgsConstructor +//@Builder +//public class ClubImage { +// @Id +// @GeneratedValue(strategy= GenerationType.IDENTITY) +// @Column(name ="clubImage_id") +// private Long id; +// +// @OneToOne(mappedBy = "clubImage") +// private Club club; +// +// @Column +// private byte[] clubImage; +// +// +//} +// diff --git a/src/main/java/com/likelion/dub/domain/Comment.java b/src/main/java/com/likelion/dub/domain/Comment.java new file mode 100644 index 0000000..b54d693 --- /dev/null +++ b/src/main/java/com/likelion/dub/domain/Comment.java @@ -0,0 +1,39 @@ +package com.likelion.dub.domain; + +import jakarta.persistence.*; +import lombok.*; + +@Builder +@NoArgsConstructor +@Entity +@AllArgsConstructor +@Getter +@Setter +@Table(name = "comment") +public class Comment { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "comment_id") + private Long id; + + @Column + private Boolean is_public; + + @Column + private Boolean is_anonymous; + + @Column + private String username; + + @Column + @Lob + private String content; + + @ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @JoinColumn(name = "post_id") + private Post post; + + + + +} diff --git a/src/main/java/com/likelion/dub/domain/Image.java b/src/main/java/com/likelion/dub/domain/Image.java index aede6ef..60e108e 100644 --- a/src/main/java/com/likelion/dub/domain/Image.java +++ b/src/main/java/com/likelion/dub/domain/Image.java @@ -30,12 +30,7 @@ public Image(String origFileName, String filePath, Long fileSize){ this.fileSize = fileSize; } - public void setPost(Post post){ - this.post = post; - if(!post.getImage().contains(this)) - post.getImage().add(this); - } private Long fileSize; diff --git a/src/main/java/com/likelion/dub/domain/Member.java b/src/main/java/com/likelion/dub/domain/Member.java index 14cb6d2..12a9858 100644 --- a/src/main/java/com/likelion/dub/domain/Member.java +++ b/src/main/java/com/likelion/dub/domain/Member.java @@ -13,28 +13,33 @@ @AllArgsConstructor @Getter @Setter +@Table(name = "member") public class Member { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) //어플리케이션에서는 기본키 값을 미리 알수 없음, 엔티티를 저장하고 나서야 키 값 확인 기능 @Column(name = "member_id") private Long id; + + @OneToOne(mappedBy = "member") + private Club club; + + @OneToMany(mappedBy = "member") + private List post = new ArrayList<>(); + @Column private String email; @Column private String name; @Column private String password; + @Column - private Long stunum; + private String gender; + @Column private String role; - - @OneToOne(mappedBy = "member") - private Club club; - - public void setClub(Club club) { this.club = club; } diff --git a/src/main/java/com/likelion/dub/domain/Post.java b/src/main/java/com/likelion/dub/domain/Post.java index be1321b..f9b4d04 100644 --- a/src/main/java/com/likelion/dub/domain/Post.java +++ b/src/main/java/com/likelion/dub/domain/Post.java @@ -4,6 +4,7 @@ import lombok.*; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; @Builder @@ -12,11 +13,20 @@ @AllArgsConstructor @Getter @Setter +@Table(name ="post") public class Post { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + + @OneToOne(mappedBy = "post") + private Image image; + + + @OneToMany(mappedBy = "post") + private List comments = new ArrayList<>(); + @Column private String clubName; @Column @@ -27,39 +37,20 @@ public class Post { private String content; - @OneToOne(mappedBy="post") + @ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY) @JoinColumn(name = "club_id") private Club club; + @ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; - @OneToMany( - mappedBy = "post", - cascade = {CascadeType.PERSIST, CascadeType.REMOVE}, - orphanRemoval = true - ) - private List image = new ArrayList<>(); - - @Column - private int category; + public void setClub(Club club) { + this.club = club; - - public void addImage(Image image){ - this.image.add(image); - - if(image.getPost() != this) - image.setPost(this); } - public static class PostBuilder { - private List image = new ArrayList<>(); - - public PostBuilder addImage(Image image) { - this.image.add(image); - return this; - } - - } } \ No newline at end of file diff --git a/src/main/java/com/likelion/dub/domain/Tag.java b/src/main/java/com/likelion/dub/domain/Tag.java deleted file mode 100644 index 9ea3ad2..0000000 --- a/src/main/java/com/likelion/dub/domain/Tag.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.likelion.dub.domain; - - -import jakarta.persistence.*; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -import java.util.List; - -@Builder -@NoArgsConstructor -@Entity -@AllArgsConstructor -@Getter -public class Tag { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "tag_id") - private Long id; - - - @Column - private String tag_name; - - @ManyToOne - @JoinColumn(name = "club_id") - private Club club; - -} diff --git a/src/main/java/com/likelion/dub/domain/dto/ClubMemberJoinRequest.java b/src/main/java/com/likelion/dub/domain/dto/ClubMemberJoinRequest.java new file mode 100644 index 0000000..249803c --- /dev/null +++ b/src/main/java/com/likelion/dub/domain/dto/ClubMemberJoinRequest.java @@ -0,0 +1,40 @@ +package com.likelion.dub.domain.dto; + + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.annotation.Nullable; +import jakarta.persistence.Lob; +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.util.List; + +@Data +@AllArgsConstructor +public class ClubMemberJoinRequest { + + @JsonProperty + private String email; + @JsonProperty + private String name; + @JsonProperty + private String password; + @JsonProperty + private String gender; + @JsonProperty + private String role; + + @JsonProperty + private String groupName; + + @JsonProperty + private String category; + + @JsonProperty + @Lob + private String introduction; + + + + +} diff --git a/src/main/java/com/likelion/dub/domain/dto/MemberJoinRequest.java b/src/main/java/com/likelion/dub/domain/dto/MemberJoinRequest.java index 891ab47..85980cb 100644 --- a/src/main/java/com/likelion/dub/domain/dto/MemberJoinRequest.java +++ b/src/main/java/com/likelion/dub/domain/dto/MemberJoinRequest.java @@ -15,7 +15,7 @@ public class MemberJoinRequest { @JsonProperty private String password; @JsonProperty - private Long stunum; + private String gender; @JsonProperty private String role; diff --git a/src/main/java/com/likelion/dub/domain/dto/MyPageResponse.java b/src/main/java/com/likelion/dub/domain/dto/MyPageResponse.java index 3885b08..d8feb46 100644 --- a/src/main/java/com/likelion/dub/domain/dto/MyPageResponse.java +++ b/src/main/java/com/likelion/dub/domain/dto/MyPageResponse.java @@ -14,8 +14,7 @@ public class MyPageResponse { private String email; @JsonProperty private String username; - @JsonProperty - private Long stunum; + @JsonProperty private String role; diff --git a/src/main/java/com/likelion/dub/repository/MemberRepository.java b/src/main/java/com/likelion/dub/repository/MemberRepository.java index 51a2ba2..4b192a5 100644 --- a/src/main/java/com/likelion/dub/repository/MemberRepository.java +++ b/src/main/java/com/likelion/dub/repository/MemberRepository.java @@ -8,8 +8,6 @@ public interface MemberRepository extends JpaRepository { Optional findByEmail(String email); - Optional findByStunum(Long stunum); - diff --git a/src/main/java/com/likelion/dub/service/MemberService.java b/src/main/java/com/likelion/dub/service/MemberService.java index 73b62ac..ab9a259 100644 --- a/src/main/java/com/likelion/dub/service/MemberService.java +++ b/src/main/java/com/likelion/dub/service/MemberService.java @@ -3,27 +3,24 @@ import com.likelion.dub.common.BaseException; import com.likelion.dub.common.BaseResponseStatus; -import com.likelion.dub.domain.Club; -import com.likelion.dub.domain.Member; -import com.likelion.dub.domain.Post; -import com.likelion.dub.domain.Role; -import com.likelion.dub.exception.AppException; -import com.likelion.dub.exception.Errorcode; +import com.likelion.dub.domain.*; + import com.likelion.dub.repository.ClubRepository; import com.likelion.dub.repository.MemberRepository; + import com.likelion.dub.utils.JwtTokenUtil; import jakarta.persistence.EntityNotFoundException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; -import org.springframework.security.core.Authentication; + import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; -import java.util.Optional; +import java.io.IOException; -import static java.lang.Boolean.FALSE; -import static java.lang.Boolean.TRUE; +import java.util.Optional; @RequiredArgsConstructor @@ -33,8 +30,7 @@ public class MemberService { private final MemberRepository memberRepository; private final ClubRepository clubRepository; - - private final BCryptPasswordEncoder encoder; + private final BCryptPasswordEncoder bCryptPasswordEncoder; @@ -50,62 +46,83 @@ public boolean checkEmail(String email) { } - public boolean checkStunum(Long stunum) { - Optional member = memberRepository.findByStunum(stunum); - return !member.isPresent(); - } - - public void join(String email, String name, String password, Long stunum, String role) { + /** + * 일반회원 회원가입 + * @param email + * @param name + * @param password + * @param gender + * @param role + */ + public void join(String email, String name, String password, String gender, String role) { // 중복 이메일 검사 Optional existingMember = memberRepository.findByEmail(email); if (existingMember.isPresent()) { throw new BaseException(BaseResponseStatus.EMAIL_ALREADY_EXIST); } - //저장 - if (role.equals("CLUB")) { + Member member = new Member(); + member.setEmail(email); + member.setName(name); + String hashedPassword = bCryptPasswordEncoder.encode(password); + member.setPassword(hashedPassword); + member.setGender(gender); + member.setRole(role); + memberRepository.save(member); - Club club = Club.builder() - .clubName(name) - .build(); - clubRepository.save(club); - Member member = Member.builder() - .email(email) - .password(encoder.encode(password)) - .stunum(stunum) - .name(name) - .role(role) - .club(club) - .build(); - club.setMember(member); - memberRepository.save(member); + } + /** + * 동아리장 회원가입 + * @param email + * @param name + * @param password + * @param gender + * @param role + */ + public void joinClub(String email, String name, String password, String gender, String role, String introduction, String groupName,String category , MultipartFile file) { - } else if (role.equals("USER")) { - Member member = Member.builder() - .email(email) - .password(encoder.encode(password)) - .stunum(stunum) - .role(role) - .name(name) - .build(); - memberRepository.save(member); - } else if (role.equals("ADMIN")) { - //admin 나중에 구현 + // 중복 이메일 검사 + Optional existingMember = memberRepository.findByEmail(email); + if (existingMember.isPresent()) { + throw new BaseException(BaseResponseStatus.EMAIL_ALREADY_EXIST); } + try { + Member member = new Member(); + member.setEmail(email); + member.setName(name); + String hashedPassword = bCryptPasswordEncoder.encode(password); + member.setPassword(hashedPassword); + member.setGender(gender); + member.setRole(role); + memberRepository.save(member); + + Club club = new Club(); + club.setClubName(name); + club.setIntroduction(introduction); + club.setMember(member); + club.setGroupName(groupName); + club.setCategory(category); + if (file != null) { + club.setClubImage(file.getBytes()); + } + clubRepository.save(club); + member.setClub(club); + } catch (IOException e) { + throw new BaseException(BaseResponseStatus.FILE_SAVE_ERROR); + } } - public String login(String email, String password) throws BaseException { - //email 없음 + //email 중복확인 Member selectedUser = memberRepository.findByEmail(email) .orElseThrow(() -> new BaseException(BaseResponseStatus.WRONG_EMAIL)); //비밀번호 틀림 - if (!encoder.matches(password, selectedUser.getPassword())) { + if (!bCryptPasswordEncoder.matches(password, selectedUser.getPassword())) { throw new BaseException(BaseResponseStatus.WRONG_PASSWORD); } diff --git a/src/main/java/com/likelion/dub/service/PostService.java b/src/main/java/com/likelion/dub/service/PostService.java index 2818dc8..6df7148 100644 --- a/src/main/java/com/likelion/dub/service/PostService.java +++ b/src/main/java/com/likelion/dub/service/PostService.java @@ -37,7 +37,7 @@ public List getAllClubs() { } - public BaseResponse writing(String title, String content,int category)throws BaseException{ + public BaseResponse writing(String title, String content, int category) throws BaseException { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); //jwt token 오류 @@ -47,7 +47,7 @@ public BaseResponse writing(String title, String content,int category)th String email = authentication.getName(); Member member = memberRepository.findByEmail(email).orElseThrow(); Club club = member.getClub(); - String clubName = club.getClubName(); + String clubName = club.getClubName(); //이 club 글이 있으면 작성 불가 postRepository.findByClubName(clubName) @@ -55,18 +55,11 @@ public BaseResponse writing(String title, String content,int category)th throw new BaseException(BaseResponseStatus.USERS_EMPTY_USER_ID); }); - Post.PostBuilder postBuilder = Post.builder() - .clubName(clubName) - .title(title) - .content(content) - .category(category); - - Post post = postBuilder.build(); - postRepository.save(post); return new BaseResponse<>("글 작성 성공"); } - public BaseResponse writePost(String title, String content,int category, List files) throws BaseException { + + public BaseResponse writePost(String title, String content, int category, List files) throws BaseException { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); //jwt token 오류 @@ -76,47 +69,24 @@ public BaseResponse writePost(String title, String content,int category, String email = authentication.getName(); Member member = memberRepository.findByEmail(email).orElseThrow(); Club club = member.getClub(); - String clubName = club.getClubName(); + String clubName = club.getClubName(); //이 club 글이 있으면 작성 불가 postRepository.findByClubName(clubName) .ifPresent(post -> { - throw new BaseException(BaseResponseStatus.USERS_EMPTY_USER_ID); + throw new BaseException(BaseResponseStatus.USERS_EMPTY_USER_ID); }); - List imageList = fileHandler.parseFileInfo(files); - Post.PostBuilder postBuilder = Post.builder() - .clubName(clubName) - .title(title) - .content(content) - .category(category); - //Post post = Post.builder() - // .clubName(clubName) - // .image(imageList) - for (Image image : imageList) { - postBuilder.addImage(image); - } - - Post post = postBuilder.build(); - - //파일이 존재할 때만 처리 - if(!imageList.isEmpty()){ - for(Image image : imageList){ - imageRepository.save(image); - post.addImage(image); - } - } - postRepository.save(post); return new BaseResponse<>("글 작성 성공"); } - public Post readPost(Long id) throws BaseException{ + public Post readPost(Long id) throws BaseException { return postRepository.findById(id) .orElseThrow(() -> new BaseException(BaseResponseStatus.NOT_EXISTS_POST) - ); + ); } @@ -150,31 +120,10 @@ public BaseResponse deletePost(Long id) throws BaseException { return new BaseResponse(BaseResponseStatus.INVALID_MEMBER_JWT); } } - public void editPost(String email, String newTitle, String newContent, int newCategory, List newfiles) throws BaseException { - Member member = memberRepository.findByEmail(email) - .orElseThrow(() -> new BaseException(BaseResponseStatus.NO_SUCH_MEMBER_EXIST)); - String clubName = member.getClub().getClubName(); - Post post = postRepository.findByClubName(clubName) - .orElseThrow(()-> new BaseException(BaseResponseStatus.FAILED_GET_POST)); - List imageList = fileHandler.parseFileInfo(newfiles); - post.setTitle(newTitle); - post.setContent(newContent); - post.setCategory(newCategory); - post.setImage(imageList); - postRepository.save(post); - } +} -// public Member loadMemberByEmail(String email) throws BaseException { -// Member selectedUser = postRepository.findByEmail(email) -// .orElseThrow(() -> new BaseException(BaseResponseStatus.WRONG_EMAIL)); -// -// return selectedUser; -// -// } - - } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 120300b..326d8c7 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,5 +1,5 @@ # MySQL ?? ?? -spring.datasource.url=jdbc:mysql://localhost:3306/new_schema +spring.datasource.url=jdbc:mysql://localhost:3306/dub_schema spring.datasource.username=root spring.datasource.password=whqkr44## spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver From f201409fb0a17e1256af792316e15e5bb32794c5 Mon Sep 17 00:00:00 2001 From: suhoon Date: Sun, 23 Jul 2023 01:41:30 +0900 Subject: [PATCH 28/72] =?UTF-8?q?Image=20=EA=B4=80=EB=A0=A8=20table=20?= =?UTF-8?q?=EC=A0=84=EB=A9=B4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 7 +- .../dub/configuration/JwtTokenFilter.java | 6 - .../JwtTokenUtil.java | 2 +- .../likelion/dub/configuration/S3config.java | 30 +++++ .../dub/controller/MemberController.java | 7 -- .../java/com/likelion/dub/domain/Club.java | 5 +- .../com/likelion/dub/domain/ClubImage.java | 30 ----- .../java/com/likelion/dub/domain/Image.java | 37 ------ .../java/com/likelion/dub/domain/Member.java | 2 +- .../java/com/likelion/dub/domain/Post.java | 10 +- .../dub/repository/ImageRepository.java | 11 -- .../dub/repository/PostRepository.java | 3 +- .../com/likelion/dub/service/FileHandler.java | 115 ------------------ .../likelion/dub/service/ImageService.java | 12 -- .../likelion/dub/service/MemberService.java | 25 +++- .../com/likelion/dub/service/PostService.java | 11 +- src/main/resources/application.properties | 7 ++ 17 files changed, 74 insertions(+), 246 deletions(-) rename src/main/java/com/likelion/dub/{utils => configuration}/JwtTokenUtil.java (97%) create mode 100644 src/main/java/com/likelion/dub/configuration/S3config.java delete mode 100644 src/main/java/com/likelion/dub/domain/ClubImage.java delete mode 100644 src/main/java/com/likelion/dub/domain/Image.java delete mode 100644 src/main/java/com/likelion/dub/repository/ImageRepository.java delete mode 100644 src/main/java/com/likelion/dub/service/FileHandler.java delete mode 100644 src/main/java/com/likelion/dub/service/ImageService.java diff --git a/build.gradle b/build.gradle index a6b39e0..cc9810f 100644 --- a/build.gradle +++ b/build.gradle @@ -19,6 +19,8 @@ repositories { } dependencies { + + // spring init implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-web' @@ -34,12 +36,11 @@ dependencies { implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' implementation 'commons-io:commons-io:2.6' - - //swagger implementation 'io.springfox:springfox-boot-starter:3.0.0' implementation group: 'io.springfox', name: 'springfox-swagger-ui', version: '3.0.0' - + // s3 + implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' } diff --git a/src/main/java/com/likelion/dub/configuration/JwtTokenFilter.java b/src/main/java/com/likelion/dub/configuration/JwtTokenFilter.java index 4e16ed1..ba7793f 100644 --- a/src/main/java/com/likelion/dub/configuration/JwtTokenFilter.java +++ b/src/main/java/com/likelion/dub/configuration/JwtTokenFilter.java @@ -1,10 +1,6 @@ package com.likelion.dub.configuration; -import com.likelion.dub.domain.Member; import com.likelion.dub.service.MemberService; -import com.likelion.dub.utils.JwtTokenUtil; -import io.jsonwebtoken.Jwt; -import jakarta.servlet.Filter; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -15,12 +11,10 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; -import java.security.SignatureException; import java.util.List; /** diff --git a/src/main/java/com/likelion/dub/utils/JwtTokenUtil.java b/src/main/java/com/likelion/dub/configuration/JwtTokenUtil.java similarity index 97% rename from src/main/java/com/likelion/dub/utils/JwtTokenUtil.java rename to src/main/java/com/likelion/dub/configuration/JwtTokenUtil.java index 4688b21..aa8f62d 100644 --- a/src/main/java/com/likelion/dub/utils/JwtTokenUtil.java +++ b/src/main/java/com/likelion/dub/configuration/JwtTokenUtil.java @@ -1,4 +1,4 @@ -package com.likelion.dub.utils; +package com.likelion.dub.configuration; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwt; diff --git a/src/main/java/com/likelion/dub/configuration/S3config.java b/src/main/java/com/likelion/dub/configuration/S3config.java new file mode 100644 index 0000000..bf92051 --- /dev/null +++ b/src/main/java/com/likelion/dub/configuration/S3config.java @@ -0,0 +1,30 @@ +package com.likelion.dub.configuration; + + +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; + +@Configuration +public class S3config { + + @Value("${cloud.aws.credentials.access-key}") + private String accessKey; + @Value("${cloud.aws.credentials.secret-key}") + 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(); + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/dub/controller/MemberController.java b/src/main/java/com/likelion/dub/controller/MemberController.java index aeb59b4..d82dd96 100644 --- a/src/main/java/com/likelion/dub/controller/MemberController.java +++ b/src/main/java/com/likelion/dub/controller/MemberController.java @@ -6,7 +6,6 @@ import com.likelion.dub.domain.dto.ClubMemberJoinRequest; import com.likelion.dub.domain.dto.MemberJoinRequest; import com.likelion.dub.domain.dto.MemberLoginRequest; -import com.likelion.dub.domain.dto.PostWritingRequest; import com.likelion.dub.service.MemberService; import lombok.RequiredArgsConstructor; import org.springframework.http.MediaType; @@ -14,7 +13,6 @@ import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; -import java.util.List; @RestController @RequestMapping("/app/member") @@ -24,11 +22,6 @@ public class MemberController { private final MemberService memberService; - @GetMapping("/testcicd") - public ResponseEntity testcicd() { - return ResponseEntity.ok().body("test 성공"); - } - /** * 이메일 중복체크 diff --git a/src/main/java/com/likelion/dub/domain/Club.java b/src/main/java/com/likelion/dub/domain/Club.java index d2b86fb..28e3afc 100644 --- a/src/main/java/com/likelion/dub/domain/Club.java +++ b/src/main/java/com/likelion/dub/domain/Club.java @@ -7,7 +7,7 @@ import java.util.ArrayList; import java.util.List; -@Builder + @NoArgsConstructor @Entity @AllArgsConstructor @@ -37,9 +37,8 @@ public class Club { @Column private String category; - @Lob @Column - private byte[] clubImage; + private String clubImage; @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY) @JoinColumn(name = "member_id") diff --git a/src/main/java/com/likelion/dub/domain/ClubImage.java b/src/main/java/com/likelion/dub/domain/ClubImage.java deleted file mode 100644 index 38bdec1..0000000 --- a/src/main/java/com/likelion/dub/domain/ClubImage.java +++ /dev/null @@ -1,30 +0,0 @@ -//package com.likelion.dub.domain; -// -// -//import jakarta.persistence.*; -//import lombok.AllArgsConstructor; -//import lombok.Builder; -//import lombok.Getter; -//import lombok.NoArgsConstructor; -// -//@Getter -//@Entity -//@NoArgsConstructor -//@AllArgsConstructor -//@Builder -//public class ClubImage { -// @Id -// @GeneratedValue(strategy= GenerationType.IDENTITY) -// @Column(name ="clubImage_id") -// private Long id; -// -// @OneToOne(mappedBy = "clubImage") -// private Club club; -// -// @Column -// private byte[] clubImage; -// -// -//} -// - diff --git a/src/main/java/com/likelion/dub/domain/Image.java b/src/main/java/com/likelion/dub/domain/Image.java deleted file mode 100644 index 60e108e..0000000 --- a/src/main/java/com/likelion/dub/domain/Image.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.likelion.dub.domain; - -import jakarta.persistence.*; -import lombok.*; - -@Getter -@Entity -@NoArgsConstructor -@AllArgsConstructor -@Builder -@Table(name = "image") -public class Image { - @Id - @GeneratedValue(strategy= GenerationType.IDENTITY) - @Column - private Long id; - - @ManyToOne - @JoinColumn(name = "post_id") - private Post post; - - @Column - private String origFileName; - - @Column - private String filePath; - public Image(String origFileName, String filePath, Long fileSize){ - this.origFileName = origFileName; - this.filePath = filePath; - this.fileSize = fileSize; - } - - - - private Long fileSize; - -} diff --git a/src/main/java/com/likelion/dub/domain/Member.java b/src/main/java/com/likelion/dub/domain/Member.java index 12a9858..11274fc 100644 --- a/src/main/java/com/likelion/dub/domain/Member.java +++ b/src/main/java/com/likelion/dub/domain/Member.java @@ -7,7 +7,7 @@ import java.util.List; import java.util.Set; -@Builder + @NoArgsConstructor @Entity @AllArgsConstructor diff --git a/src/main/java/com/likelion/dub/domain/Post.java b/src/main/java/com/likelion/dub/domain/Post.java index f9b4d04..3d70294 100644 --- a/src/main/java/com/likelion/dub/domain/Post.java +++ b/src/main/java/com/likelion/dub/domain/Post.java @@ -7,7 +7,7 @@ import java.util.Comparator; import java.util.List; -@Builder + @NoArgsConstructor @Entity @AllArgsConstructor @@ -19,11 +19,6 @@ public class Post { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - - @OneToOne(mappedBy = "post") - private Image image; - - @OneToMany(mappedBy = "post") private List comments = new ArrayList<>(); @@ -36,6 +31,9 @@ public class Post { @Lob private String content; + @Column + private String postImage; + @ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY) @JoinColumn(name = "club_id") diff --git a/src/main/java/com/likelion/dub/repository/ImageRepository.java b/src/main/java/com/likelion/dub/repository/ImageRepository.java deleted file mode 100644 index 8fa6479..0000000 --- a/src/main/java/com/likelion/dub/repository/ImageRepository.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.likelion.dub.repository; - -import com.likelion.dub.domain.Image; -import jakarta.persistence.EntityManager; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -@Repository -public interface ImageRepository extends JpaRepository { - -} diff --git a/src/main/java/com/likelion/dub/repository/PostRepository.java b/src/main/java/com/likelion/dub/repository/PostRepository.java index 288614c..d4b980f 100644 --- a/src/main/java/com/likelion/dub/repository/PostRepository.java +++ b/src/main/java/com/likelion/dub/repository/PostRepository.java @@ -14,7 +14,6 @@ public interface PostRepository extends JpaRepository { List findAll(); Optional findByClubName(String clubName); Optional findById(Long id); - ; - // Optional findByEmail(String email); + } diff --git a/src/main/java/com/likelion/dub/service/FileHandler.java b/src/main/java/com/likelion/dub/service/FileHandler.java deleted file mode 100644 index edc25ad..0000000 --- a/src/main/java/com/likelion/dub/service/FileHandler.java +++ /dev/null @@ -1,115 +0,0 @@ -package com.likelion.dub.service; - -import com.likelion.dub.domain.Image; -import com.likelion.dub.domain.Post; -import com.likelion.dub.domain.dto.ImageDto; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.springframework.util.CollectionUtils; -import org.springframework.util.ObjectUtils; -import org.springframework.web.multipart.MultipartFile; - -import java.io.File; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.List; - -@Component -@RequiredArgsConstructor -@Slf4j -public class FileHandler { - private final ImageService imageService; - public List parseFileInfo(List multipartFiles) throws RuntimeException{ - List filelist = new ArrayList<>(); - if(!CollectionUtils.isEmpty(multipartFiles)) { - // 파일명을 업로드 한 날짜로 변환하여 저장 - LocalDateTime now = LocalDateTime.now(); - DateTimeFormatter dateTimeFormatter = - DateTimeFormatter.ofPattern("yyyyMMdd"); - String current_date = now.format(dateTimeFormatter); - - // 프로젝트 디렉터리 내의 저장을 위한 절대 경로 설정 - // 경로 구분자 File.separator 사용 - String absolutePath = new File("").getAbsolutePath() + File.separator + File.separator; - - // 파일을 저장할 세부 경로 지정 - - String path = "images" + File.separator + current_date; - File file = new File(path); - log.info(file.toString()); - - // 디렉터리가 존재하지 않을 경우 - if(!file.exists()) { - boolean wasSuccessful = file.mkdirs(); - - // 디렉터리 생성에 실패했을 경우 - if(!wasSuccessful) - System.out.println("file: was not successful"); - } - - // 다중 파일 처리 - for(MultipartFile multipartFile : multipartFiles) { - - // 파일의 확장자 추출 - String originalFileExtension; - String contentType = multipartFile.getContentType(); - log.info(contentType); - - // 확장자명이 존재하지 않을 경우 처리 x - if(ObjectUtils.isEmpty(contentType)) { - break; - } - else { // 확장자가 jpeg, png인 파일들만 받아서 처리 - if(contentType.contains("image/jpeg")) - originalFileExtension = ".jpg"; - else if(contentType.contains("image/png")) - originalFileExtension = ".png"; - else // 다른 확장자일 경우 처리 x - break; - } - - // 파일명 중복 피하고자 나노초까지 얻어와 지정 - String new_file_name = System.nanoTime() + originalFileExtension; - - // 파일 DTO 생성 - ImageDto imageDto = ImageDto.builder() - .origFileName(multipartFile.getOriginalFilename()) - .filePath(path + File.separator + new_file_name) - .fileSize(multipartFile.getSize()) - .build(); - log.info(imageDto.toString()); - // 파일 DTO 이용하여 Photo 엔티티 생성 - Image photo = new Image( - imageDto.getOrigFileName(), - imageDto.getFilePath(), - imageDto.getFileSize() - ); - - - // 생성 후 리스트에 추가 - filelist.add(photo); - - // 업로드 한 파일 데이터를 지정한 파일에 저장 - file = new File(absolutePath + path + File.separator + new_file_name); - try{ - multipartFile.transferTo(file); - } - catch(IOException e){ - throw new UncheckedIOException("not yet", e); - } - log.info(filelist.toString()); - // 파일 권한 설정(쓰기, 읽기) - file.setWritable(true); - file.setReadable(true); - } - } - - return filelist; - } - - -} diff --git a/src/main/java/com/likelion/dub/service/ImageService.java b/src/main/java/com/likelion/dub/service/ImageService.java deleted file mode 100644 index 9975601..0000000 --- a/src/main/java/com/likelion/dub/service/ImageService.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.likelion.dub.service; - -import lombok.Builder; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Builder -@Service -@RequiredArgsConstructor -public class ImageService { - -} diff --git a/src/main/java/com/likelion/dub/service/MemberService.java b/src/main/java/com/likelion/dub/service/MemberService.java index ab9a259..b579447 100644 --- a/src/main/java/com/likelion/dub/service/MemberService.java +++ b/src/main/java/com/likelion/dub/service/MemberService.java @@ -1,6 +1,8 @@ package com.likelion.dub.service; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.ObjectMetadata; import com.likelion.dub.common.BaseException; import com.likelion.dub.common.BaseResponseStatus; import com.likelion.dub.domain.*; @@ -8,7 +10,7 @@ import com.likelion.dub.repository.ClubRepository; import com.likelion.dub.repository.MemberRepository; -import com.likelion.dub.utils.JwtTokenUtil; +import com.likelion.dub.configuration.JwtTokenUtil; import jakarta.persistence.EntityNotFoundException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -32,6 +34,10 @@ public class MemberService { private final BCryptPasswordEncoder bCryptPasswordEncoder; + private final AmazonS3Client amazonS3Client; + + @Value("${cloud.aws.s3.bucket}") + private String bucket; @@ -80,6 +86,10 @@ public void join(String email, String name, String password, String gender, Stri * @param password * @param gender * @param role + * @param introduction + * @param groupName + * @param category + * @param file */ public void joinClub(String email, String name, String password, String gender, String role, String introduction, String groupName,String category , MultipartFile file) { @@ -89,6 +99,7 @@ public void joinClub(String email, String name, String password, String gender, throw new BaseException(BaseResponseStatus.EMAIL_ALREADY_EXIST); } try { + // 멤버 저장 Member member = new Member(); member.setEmail(email); member.setName(name); @@ -97,7 +108,7 @@ public void joinClub(String email, String name, String password, String gender, member.setGender(gender); member.setRole(role); memberRepository.save(member); - + // 동아리 저장 Club club = new Club(); club.setClubName(name); club.setIntroduction(introduction); @@ -105,9 +116,17 @@ public void joinClub(String email, String name, String password, String gender, club.setGroupName(groupName); club.setCategory(category); if (file != null) { - club.setClubImage(file.getBytes()); + // 프로필 사진 S3에 저장 + Long memberId = member.getId(); + String fileName = memberId + "ClubImage"; + ObjectMetadata metadata= new ObjectMetadata(); + metadata.setContentType(file.getContentType()); + metadata.setContentLength(file.getSize()); + amazonS3Client.putObject(bucket,fileName,file.getInputStream(),metadata); + club.setClubImage("https://dubs3.s3.ap-northeast-2.amazonaws.com/" + fileName); } clubRepository.save(club); + // 양방향 연관관계설정 member.setClub(club); } catch (IOException e) { diff --git a/src/main/java/com/likelion/dub/service/PostService.java b/src/main/java/com/likelion/dub/service/PostService.java index 6df7148..f942854 100644 --- a/src/main/java/com/likelion/dub/service/PostService.java +++ b/src/main/java/com/likelion/dub/service/PostService.java @@ -1,17 +1,13 @@ package com.likelion.dub.service; -import com.fasterxml.jackson.databind.ser.Serializers; + import com.likelion.dub.common.BaseException; import com.likelion.dub.common.BaseResponse; import com.likelion.dub.common.BaseResponseStatus; import com.likelion.dub.domain.Club; -import com.likelion.dub.domain.Image; import com.likelion.dub.domain.Member; import com.likelion.dub.domain.Post; -import com.likelion.dub.domain.dto.PostEditRequest; -import com.likelion.dub.exception.AppException; -import com.likelion.dub.exception.Errorcode; -import com.likelion.dub.repository.ImageRepository; + import com.likelion.dub.repository.MemberRepository; import com.likelion.dub.repository.PostRepository; import lombok.RequiredArgsConstructor; @@ -21,15 +17,12 @@ import org.springframework.web.multipart.MultipartFile; import java.util.List; -import java.util.Optional; @Service @RequiredArgsConstructor public class PostService { private final PostRepository postRepository; - private final ImageRepository imageRepository; private final MemberRepository memberRepository; - private final FileHandler fileHandler; public List getAllClubs() { diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 326d8c7..a31b439 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -16,3 +16,10 @@ jwt.token.secret=VlwEyVBsYt9V7zq57TejMnVUyzblYcfPQye08f7MGVA9XkHa spring.servlet.multipart.enabled=true spring.servlet.multipart.max-file-size=10MB spring.servlet.multipart.max-request-size=10MB + +#s3 +cloud.aws.s3.bucket=dubs3 +cloud.aws.stack.auto=false +cloud.aws.region.static=ap-northeast-2 +cloud.aws.credentials.accessKey=AKIAVNQICB3FCRQNW4PO +cloud.aws.credentials.secretKey=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 \ No newline at end of file From 25d1d858629ec008a6144e6c185ec1aca75f7347 Mon Sep 17 00:00:00 2001 From: suhoon <82464990+s2hoon@users.noreply.github.com> Date: Sun, 23 Jul 2023 01:48:07 +0900 Subject: [PATCH 29/72] Update gradle.yml --- .github/workflows/gradle.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index c0ae456..57fd8f3 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -93,8 +93,7 @@ jobs: key: ${{ secrets.SSH_KEY }} envs: GITHUB_SHA script: | - sudo docker stop dub-prod - sudo docker rm -fv dub-prod + docker stop $(docker ps -a -q) sudo docker pull ${{ secrets.DOCKER_USERNAME }}/dub-prod sudo docker run -d -p 8080:8080 ${{ secrets.DOCKER_USERNAME }}/dub-prod sudo docker image prune -f From 00894a3afe9c658922395a2d2fd32336b614282a Mon Sep 17 00:00:00 2001 From: suhoon Date: Sun, 23 Jul 2023 01:59:43 +0900 Subject: [PATCH 30/72] =?UTF-8?q?drop=20table=20=EC=95=88=EB=90=98?= =?UTF-8?q?=EB=8A=94=EA=B1=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/likelion/dub/controller/MemberController.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/java/com/likelion/dub/controller/MemberController.java b/src/main/java/com/likelion/dub/controller/MemberController.java index d82dd96..cc26556 100644 --- a/src/main/java/com/likelion/dub/controller/MemberController.java +++ b/src/main/java/com/likelion/dub/controller/MemberController.java @@ -23,6 +23,13 @@ public class MemberController { private final MemberService memberService; + + @GetMapping("/testRun") + public BaseResponse testRun() { + return new BaseResponse(BaseResponseStatus.SUCCESS); + } + + /** * 이메일 중복체크 * @param email From d9875c766094a9f7f55ffb39db7307a332805f60 Mon Sep 17 00:00:00 2001 From: suhoon <82464990+s2hoon@users.noreply.github.com> Date: Sun, 23 Jul 2023 02:04:24 +0900 Subject: [PATCH 31/72] Update gradle.yml --- .github/workflows/gradle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 57fd8f3..8a06dc7 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -95,7 +95,7 @@ jobs: script: | docker stop $(docker ps -a -q) sudo docker pull ${{ secrets.DOCKER_USERNAME }}/dub-prod - sudo docker run -d -p 8080:8080 ${{ secrets.DOCKER_USERNAME }}/dub-prod + sudo docker run -d -p 8080:8080 --name dub ${{ secrets.DOCKER_USERNAME }}/dub-prod sudo docker image prune -f ## deploy to develop From 0c6ce369d361aea2eebdf614edca609d6137c1b3 Mon Sep 17 00:00:00 2001 From: suhoon Date: Sun, 23 Jul 2023 02:20:53 +0900 Subject: [PATCH 32/72] test --- src/main/java/com/likelion/dub/controller/MemberController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/likelion/dub/controller/MemberController.java b/src/main/java/com/likelion/dub/controller/MemberController.java index cc26556..61350ed 100644 --- a/src/main/java/com/likelion/dub/controller/MemberController.java +++ b/src/main/java/com/likelion/dub/controller/MemberController.java @@ -24,7 +24,7 @@ public class MemberController { - @GetMapping("/testRun") + @GetMapping("/testRaun") public BaseResponse testRun() { return new BaseResponse(BaseResponseStatus.SUCCESS); } From 321151dfa7ebeb7e2e2e3b6f17093c48e14afd57 Mon Sep 17 00:00:00 2001 From: suhoon Date: Sun, 23 Jul 2023 02:23:43 +0900 Subject: [PATCH 33/72] test --- src/main/java/com/likelion/dub/controller/MemberController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/likelion/dub/controller/MemberController.java b/src/main/java/com/likelion/dub/controller/MemberController.java index 61350ed..9b33dc0 100644 --- a/src/main/java/com/likelion/dub/controller/MemberController.java +++ b/src/main/java/com/likelion/dub/controller/MemberController.java @@ -24,7 +24,7 @@ public class MemberController { - @GetMapping("/testRaun") + @GetMapping("/testRraun") public BaseResponse testRun() { return new BaseResponse(BaseResponseStatus.SUCCESS); } From a264bed8bc1e696702dcf237ab98ea9566a18583 Mon Sep 17 00:00:00 2001 From: suhoon <82464990+s2hoon@users.noreply.github.com> Date: Sun, 23 Jul 2023 02:27:25 +0900 Subject: [PATCH 34/72] Update gradle.yml --- .github/workflows/gradle.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 8a06dc7..1a0708c 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -93,7 +93,8 @@ jobs: key: ${{ secrets.SSH_KEY }} envs: GITHUB_SHA script: | - docker stop $(docker ps -a -q) + sudo docker stop dub + sudo docker rm dub sudo docker pull ${{ secrets.DOCKER_USERNAME }}/dub-prod sudo docker run -d -p 8080:8080 --name dub ${{ secrets.DOCKER_USERNAME }}/dub-prod sudo docker image prune -f From 108c2c7d72b5441f152759240898593eda9e8f02 Mon Sep 17 00:00:00 2001 From: suhoon Date: Mon, 24 Jul 2023 21:11:40 +0900 Subject: [PATCH 35/72] =?UTF-8?q?domain=20=EC=A0=84=EB=A9=B4=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/likelion/dub/DubApplication.java | 13 ++++++++ .../com/likelion/dub/domain/BaseEntity.java | 21 ++++++++++++ .../likelion/dub/domain/BaseTimeEntity.java | 22 +++++++++++++ .../java/com/likelion/dub/domain/Comment.java | 32 +++++++++++++++---- .../java/com/likelion/dub/domain/Post.java | 2 +- .../likelion/dub/service/MemberService.java | 5 ++- .../likelion/dub/service/MypageService.java | 4 ++- .../com/likelion/dub/service/PostService.java | 4 +++ src/main/resources/application.properties | 3 +- 9 files changed, 95 insertions(+), 11 deletions(-) create mode 100644 src/main/java/com/likelion/dub/domain/BaseEntity.java create mode 100644 src/main/java/com/likelion/dub/domain/BaseTimeEntity.java diff --git a/src/main/java/com/likelion/dub/DubApplication.java b/src/main/java/com/likelion/dub/DubApplication.java index b2dfed9..cb10830 100644 --- a/src/main/java/com/likelion/dub/DubApplication.java +++ b/src/main/java/com/likelion/dub/DubApplication.java @@ -2,7 +2,16 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.context.annotation.Bean; +import org.springframework.data.domain.AuditorAware; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import java.util.Optional; +import java.util.UUID; + + +@EnableJpaAuditing //(modifyOnCreate =false)를 넣으면 저장시점에 저장데이터만 입력 @SpringBootApplication public class DubApplication { @@ -10,4 +19,8 @@ public static void main(String[] args) { SpringApplication.run(DubApplication.class, args); } + @Bean + public AuditorAware auditorProvider() { + return () -> Optional.of(UUID.randomUUID().toString()); + } } diff --git a/src/main/java/com/likelion/dub/domain/BaseEntity.java b/src/main/java/com/likelion/dub/domain/BaseEntity.java new file mode 100644 index 0000000..a42557a --- /dev/null +++ b/src/main/java/com/likelion/dub/domain/BaseEntity.java @@ -0,0 +1,21 @@ +package com.likelion.dub.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +@EntityListeners(AuditingEntityListener.class) //이걸 생략하고 orm.xml에 등록하면 전체 적용이가능하다 +@MappedSuperclass +public abstract class BaseEntity extends BaseTimeEntity{ + + @CreatedBy + @Column(updatable = false) + private String createdBy; + @LastModifiedBy + @Column + private String lastModifiedBy; +} + diff --git a/src/main/java/com/likelion/dub/domain/BaseTimeEntity.java b/src/main/java/com/likelion/dub/domain/BaseTimeEntity.java new file mode 100644 index 0000000..e865ca7 --- /dev/null +++ b/src/main/java/com/likelion/dub/domain/BaseTimeEntity.java @@ -0,0 +1,22 @@ +package com.likelion.dub.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; +@EntityListeners(AuditingEntityListener.class) //이걸 생략하고 orm.xml에 등록하면 전체 적용이가능하다 +@MappedSuperclass +public abstract class BaseTimeEntity { + @CreatedDate + @Column(updatable = false) + private LocalDateTime createdDate; + @LastModifiedDate + @Column + private LocalDateTime lastModifiedDate; +} + + diff --git a/src/main/java/com/likelion/dub/domain/Comment.java b/src/main/java/com/likelion/dub/domain/Comment.java index b54d693..a8c5273 100644 --- a/src/main/java/com/likelion/dub/domain/Comment.java +++ b/src/main/java/com/likelion/dub/domain/Comment.java @@ -2,15 +2,18 @@ import jakarta.persistence.*; import lombok.*; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.util.ArrayList; +import java.util.List; + -@Builder -@NoArgsConstructor @Entity -@AllArgsConstructor @Getter @Setter +@NoArgsConstructor(access = AccessLevel.PROTECTED) @Table(name = "comment") -public class Comment { +public class Comment extends BaseEntity{ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "comment_id") @@ -22,18 +25,33 @@ public class Comment { @Column private Boolean is_anonymous; - @Column - private String username; @Column - @Lob private String content; @ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY) @JoinColumn(name = "post_id") private Post post; + @ManyToOne + @JoinColumn(name = "parent_comment_id") + private Comment parentComment; // 자기 참조 필드 + + @OneToMany(mappedBy = "parentComment", cascade = CascadeType.ALL, orphanRemoval = true) + private List childComments = new ArrayList<>(); // 대댓글들 + + + // 대댓글 추가 메서드 + public void addChildComment(Comment childComment) { + childComments.add(childComment); + childComment.setParentComment(this); + } + // 대댓글 삭제 메서드 + public void removeChildComment(Comment childComment) { + childComments.remove(childComment); + childComment.setParentComment(null); + } } diff --git a/src/main/java/com/likelion/dub/domain/Post.java b/src/main/java/com/likelion/dub/domain/Post.java index 3d70294..76aa1f7 100644 --- a/src/main/java/com/likelion/dub/domain/Post.java +++ b/src/main/java/com/likelion/dub/domain/Post.java @@ -14,7 +14,7 @@ @Getter @Setter @Table(name ="post") -public class Post { +public class Post extends BaseEntity{ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; diff --git a/src/main/java/com/likelion/dub/service/MemberService.java b/src/main/java/com/likelion/dub/service/MemberService.java index b579447..7b3e7c1 100644 --- a/src/main/java/com/likelion/dub/service/MemberService.java +++ b/src/main/java/com/likelion/dub/service/MemberService.java @@ -18,6 +18,7 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; @@ -25,8 +26,10 @@ import java.util.Optional; -@RequiredArgsConstructor + @Service +@Transactional +@RequiredArgsConstructor @Slf4j public class MemberService { private final MemberRepository memberRepository; diff --git a/src/main/java/com/likelion/dub/service/MypageService.java b/src/main/java/com/likelion/dub/service/MypageService.java index e270f3e..ee22e8c 100644 --- a/src/main/java/com/likelion/dub/service/MypageService.java +++ b/src/main/java/com/likelion/dub/service/MypageService.java @@ -9,11 +9,13 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.Optional; -@RequiredArgsConstructor @Service +@Transactional +@RequiredArgsConstructor @Slf4j public class MypageService { private final MemberRepository memberRepository; diff --git a/src/main/java/com/likelion/dub/service/PostService.java b/src/main/java/com/likelion/dub/service/PostService.java index f942854..eb00f8f 100644 --- a/src/main/java/com/likelion/dub/service/PostService.java +++ b/src/main/java/com/likelion/dub/service/PostService.java @@ -11,15 +11,19 @@ import com.likelion.dub.repository.MemberRepository; import com.likelion.dub.repository.PostRepository; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; import java.util.List; @Service +@Transactional @RequiredArgsConstructor +@Slf4j public class PostService { private final PostRepository postRepository; private final MemberRepository memberRepository; diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index a31b439..f2c47f5 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,9 +1,10 @@ # MySQL ?? ?? -spring.datasource.url=jdbc:mysql://localhost:3306/dub_schema +spring.datasource.url=jdbc:mysql://localhost:3306/dub_4 spring.datasource.username=root spring.datasource.password=whqkr44## spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver + # Hibernate ?? spring.jpa.show-sql=true spring.jpa.hibernate.ddl-auto=create From 1abf1ea76efecb5a8df44d6eef09e5e8b5773ad8 Mon Sep 17 00:00:00 2001 From: suhoon Date: Mon, 24 Jul 2023 22:37:52 +0900 Subject: [PATCH 36/72] =?UTF-8?q?=EA=B8=80=20=EC=9E=91=EC=84=B1=20api=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dub/controller/PostController.java | 9 +++- .../dub/domain/dto/PostWritingRequest.java | 8 ++-- .../com/likelion/dub/service/PostService.java | 42 +++++++++++++------ 3 files changed, 42 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/likelion/dub/controller/PostController.java b/src/main/java/com/likelion/dub/controller/PostController.java index 82ea96c..d933705 100644 --- a/src/main/java/com/likelion/dub/controller/PostController.java +++ b/src/main/java/com/likelion/dub/controller/PostController.java @@ -26,6 +26,7 @@ import org.springframework.web.multipart.MultipartFile; import springfox.documentation.spring.web.json.Json; +import java.io.IOException; import java.nio.file.Files; import java.util.List; import java.util.Map; @@ -58,13 +59,17 @@ public BaseResponse> getAllClubs() { * @return */ + + @PostMapping(value = "/write-post" , consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE}) - public BaseResponse writePost(@RequestPart(value = "json") PostWritingRequest dto, @RequestPart(value = "images", required = false) List files) throws BaseException { + public BaseResponse writePost(@RequestPart(value = "json") PostWritingRequest dto, @RequestPart(value = "image", required = false) MultipartFile file) throws BaseException { try { - postService.writePost(dto.getTitle(), dto.getContent(), dto.getCategory(), files); + postService.writePost(dto.getTitle(), dto.getContent(), file); return new BaseResponse<>("글 작성 성공"); } catch (BaseException e) { return new BaseResponse<>(e.getStatus()); + } catch (IOException e){ + return new BaseResponse<>(e.getMessage()); } } diff --git a/src/main/java/com/likelion/dub/domain/dto/PostWritingRequest.java b/src/main/java/com/likelion/dub/domain/dto/PostWritingRequest.java index 3822d20..1ef1f51 100644 --- a/src/main/java/com/likelion/dub/domain/dto/PostWritingRequest.java +++ b/src/main/java/com/likelion/dub/domain/dto/PostWritingRequest.java @@ -1,6 +1,8 @@ package com.likelion.dub.domain.dto; import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.persistence.Column; +import jakarta.persistence.Lob; import lombok.AllArgsConstructor; import lombok.Getter; import org.springframework.web.multipart.MultipartFile; @@ -11,13 +13,13 @@ @AllArgsConstructor public class PostWritingRequest { + @JsonProperty private String title; + + @Lob @JsonProperty private String content; - @JsonProperty - private int category; - diff --git a/src/main/java/com/likelion/dub/service/PostService.java b/src/main/java/com/likelion/dub/service/PostService.java index eb00f8f..1fcb0c7 100644 --- a/src/main/java/com/likelion/dub/service/PostService.java +++ b/src/main/java/com/likelion/dub/service/PostService.java @@ -1,6 +1,8 @@ package com.likelion.dub.service; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.ObjectMetadata; import com.likelion.dub.common.BaseException; import com.likelion.dub.common.BaseResponse; import com.likelion.dub.common.BaseResponseStatus; @@ -12,12 +14,14 @@ import com.likelion.dub.repository.PostRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; +import java.io.IOException; import java.util.List; @Service @@ -27,6 +31,10 @@ public class PostService { private final PostRepository postRepository; private final MemberRepository memberRepository; + private final AmazonS3Client amazonS3Client; + + @Value("${cloud.aws.s3.bucket}") + private String bucket; public List getAllClubs() { @@ -56,28 +64,38 @@ public BaseResponse writing(String title, String content, int category) return new BaseResponse<>("글 작성 성공"); } - public BaseResponse writePost(String title, String content, int category, List files) throws BaseException { + public BaseResponse writePost(String title, String content, MultipartFile file) throws BaseException, IOException { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - //jwt token 오류 - if (authentication == null || !authentication.isAuthenticated()) { - return new BaseResponse(BaseResponseStatus.JWT_TOKEN_ERROR); - } String email = authentication.getName(); Member member = memberRepository.findByEmail(email).orElseThrow(); Club club = member.getClub(); String clubName = club.getClubName(); - //이 club 글이 있으면 작성 불가 - postRepository.findByClubName(clubName) - .ifPresent(post -> { - throw new BaseException(BaseResponseStatus.USERS_EMPTY_USER_ID); - }); - - + // post 객체 생성 및 db 에 저장 + Post post = new Post(); + post.setClubName(clubName); + post.setClub(club); + post.setTitle(title); + post.setContent(content); + if (file != null) { + String fileName = member.getId() + "PostImage"; + // 포스터 사진 S3에 저장 + uploadPostImageToS3(fileName, file); + post.setPostImage("https://dubs3.s3.ap-northeast-2.amazonaws.com/" + fileName); + } + postRepository.save(post); return new BaseResponse<>("글 작성 성공"); } + private void uploadPostImageToS3(String fileName, MultipartFile file) throws IOException { + ObjectMetadata metadata = new ObjectMetadata(); + metadata.setContentType(file.getContentType()); + metadata.setContentLength(file.getSize()); + amazonS3Client.putObject(bucket, fileName, file.getInputStream(), metadata); + } + + public Post readPost(Long id) throws BaseException { return postRepository.findById(id) .orElseThrow(() -> From 2eb290e1ec87e4148a1d802819eaa8123cf15d1e Mon Sep 17 00:00:00 2001 From: suhoon Date: Thu, 27 Jul 2023 16:10:04 +0900 Subject: [PATCH 37/72] =?UTF-8?q?=EB=8F=99=EC=95=84=EB=A6=AC=20=EA=B8=80?= =?UTF-8?q?=20=EC=9E=91=EC=84=B1=20=EB=B0=8F=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 4 + .../java/com/likelion/dub/DubApplication.java | 10 +++ .../dub/common/BaseResponseStatus.java | 4 +- .../dub/controller/MemberController.java | 20 ++--- .../dub/controller/PostController.java | 79 ++++++++----------- .../java/com/likelion/dub/domain/Club.java | 2 +- .../java/com/likelion/dub/domain/Member.java | 3 + .../java/com/likelion/dub/domain/Post.java | 6 +- .../dub/domain/dto/ClubMemberJoinRequest.java | 1 + ...etRequest.java => GetAllPostResponse.java} | 16 ++-- .../dub/domain/dto/GetOnePostResponse.java | 28 +++++++ .../com/likelion/dub/domain/dto/ImageDto.java | 23 ------ .../dub/domain/dto/MemberJoinRequest.java | 2 + .../dub/domain/dto/PostWritingRequest.java | 2 + .../dub/domain/dto/WritingRequest.java | 26 ++++++ .../com/likelion/dub/service/PostService.java | 76 +++++++++++++----- src/main/resources/application.properties | 2 +- .../com/likelion/dub/DubApplicationTests.java | 4 +- .../likelion/dub/service/PostServiceTest.java | 68 ++++++++++++++++ 19 files changed, 260 insertions(+), 116 deletions(-) rename src/main/java/com/likelion/dub/domain/dto/{PostGetRequest.java => GetAllPostResponse.java} (57%) create mode 100644 src/main/java/com/likelion/dub/domain/dto/GetOnePostResponse.java delete mode 100644 src/main/java/com/likelion/dub/domain/dto/ImageDto.java create mode 100644 src/main/java/com/likelion/dub/domain/dto/WritingRequest.java create mode 100644 src/test/java/com/likelion/dub/service/PostServiceTest.java diff --git a/build.gradle b/build.gradle index cc9810f..372445d 100644 --- a/build.gradle +++ b/build.gradle @@ -31,6 +31,7 @@ dependencies { annotationProcessor 'org.projectlombok:lombok' runtimeOnly 'com.mysql:mysql-connector-j' testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.security:spring-security-test' // jwt implementation 'io.jsonwebtoken:jjwt-api:0.11.5' implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' @@ -42,6 +43,9 @@ dependencies { // s3 implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' + //프록시 json 화 + implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5-jakarta' + } tasks.named('test') { diff --git a/src/main/java/com/likelion/dub/DubApplication.java b/src/main/java/com/likelion/dub/DubApplication.java index cb10830..fabb9b1 100644 --- a/src/main/java/com/likelion/dub/DubApplication.java +++ b/src/main/java/com/likelion/dub/DubApplication.java @@ -1,5 +1,6 @@ package com.likelion.dub; +import com.fasterxml.jackson.datatype.hibernate5.jakarta.Hibernate5JakartaModule; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.domain.EntityScan; @@ -23,4 +24,13 @@ public static void main(String[] args) { public AuditorAware auditorProvider() { return () -> Optional.of(UUID.randomUUID().toString()); } + + //프록시 객체 JSON 화 +// @Bean +// public Hibernate5JakartaModule hibernate5JakartaModule() { +// Hibernate5JakartaModule hibernate5JakartaModule = new Hibernate5JakartaModule(); +// return hibernate5JakartaModule; +// } + + } diff --git a/src/main/java/com/likelion/dub/common/BaseResponseStatus.java b/src/main/java/com/likelion/dub/common/BaseResponseStatus.java index b687326..6e4c480 100644 --- a/src/main/java/com/likelion/dub/common/BaseResponseStatus.java +++ b/src/main/java/com/likelion/dub/common/BaseResponseStatus.java @@ -64,7 +64,9 @@ public enum BaseResponseStatus { WRONG_PASSWORD(false, 3202, "잘못된 비밀번호입니다."), DISABLED_MEMBER(false, 3203, "탈퇴한 회원입니다."), STU_NUM_ALREADY_EXIST(false, 3204, "이미 가입된 학번 입니다."), - NO_SUCH_MEMBER_EXIST(false, 3303, "존재하지 않는 회원입니다."), + NO_SUCH_MEMBER_EXIST(false, 3203, "존재하지 않는 회원입니다."), + + NO_SUCH_CLUB_EXIST(false, 3205, "존재하지 않는 동아리입니다."), // mypage diff --git a/src/main/java/com/likelion/dub/controller/MemberController.java b/src/main/java/com/likelion/dub/controller/MemberController.java index 9b33dc0..87b6c91 100644 --- a/src/main/java/com/likelion/dub/controller/MemberController.java +++ b/src/main/java/com/likelion/dub/controller/MemberController.java @@ -3,6 +3,7 @@ import com.likelion.dub.common.BaseException; import com.likelion.dub.common.BaseResponse; import com.likelion.dub.common.BaseResponseStatus; +import com.likelion.dub.domain.Member; import com.likelion.dub.domain.dto.ClubMemberJoinRequest; import com.likelion.dub.domain.dto.MemberJoinRequest; import com.likelion.dub.domain.dto.MemberLoginRequest; @@ -13,6 +14,8 @@ import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; +import java.util.List; + @RestController @RequestMapping("/app/member") @@ -24,12 +27,6 @@ public class MemberController { - @GetMapping("/testRraun") - public BaseResponse testRun() { - return new BaseResponse(BaseResponseStatus.SUCCESS); - } - - /** * 이메일 중복체크 * @param email @@ -110,13 +107,6 @@ public BaseResponse login(@RequestBody MemberLoginRequest dto) { } - /** - * test api - * @param dto - * @return - */ - @PostMapping("/test") - public ResponseEntity test(@RequestBody MemberJoinRequest dto) { - return ResponseEntity.ok().body("test 성공"); - } + + } diff --git a/src/main/java/com/likelion/dub/controller/PostController.java b/src/main/java/com/likelion/dub/controller/PostController.java index d933705..6ed48b4 100644 --- a/src/main/java/com/likelion/dub/controller/PostController.java +++ b/src/main/java/com/likelion/dub/controller/PostController.java @@ -1,35 +1,22 @@ package com.likelion.dub.controller; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.ser.Serializers; import com.likelion.dub.common.BaseException; import com.likelion.dub.common.BaseResponse; import com.likelion.dub.common.BaseResponseStatus; -import com.likelion.dub.domain.Club; -import com.likelion.dub.domain.Member; -import com.likelion.dub.common.BaseResponseStatus; import com.likelion.dub.domain.Post; -import com.likelion.dub.domain.dto.PostEditRequest; -import com.likelion.dub.domain.dto.PostWritingRequest; -import com.likelion.dub.service.MemberService; +import com.likelion.dub.domain.dto.*; import com.likelion.dub.service.PostService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; + import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; -import springfox.documentation.spring.web.json.Json; import java.io.IOException; -import java.nio.file.Files; import java.util.List; -import java.util.Map; @RestController @RequestMapping("/app/post") @@ -47,41 +34,46 @@ public class PostController { * @return all post */ @GetMapping("/getAll") - public BaseResponse> getAllClubs() { - return new BaseResponse<>(postService.getAllClubs()); + public BaseResponse> getAllClubs() { + return new BaseResponse<>(postService.getAllPost()); } - /** - * post 작성 - * - * @param dto - * @return - */ - +// /** +// * post 작성 +// * @param dto +// * @param file +// * @return +// * @throws BaseException +// */ +// @PostMapping(value = "/write-post" , consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE}) +// public BaseResponse writePost(@RequestPart(value = "json") PostWritingRequest dto, @RequestPart(value = "image", required = false) MultipartFile file) throws BaseException { +// try { +// postService.writePost(dto.getTitle(), dto.getContent(), file); +// return new BaseResponse<>("글 작성 성공"); +// } catch (BaseException e) { +// return new BaseResponse<>(e.getStatus()); +// } catch (IOException e){ +// return new BaseResponse<>(e.getMessage()); +// } +// } - @PostMapping(value = "/write-post" , consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE}) - public BaseResponse writePost(@RequestPart(value = "json") PostWritingRequest dto, @RequestPart(value = "image", required = false) MultipartFile file) throws BaseException { - try { - postService.writePost(dto.getTitle(), dto.getContent(), file); - return new BaseResponse<>("글 작성 성공"); - } catch (BaseException e) { - return new BaseResponse<>(e.getStatus()); - } catch (IOException e){ - return new BaseResponse<>(e.getMessage()); - } - } + /** + * post 작성 test + * @param writingRequest + * @return + */ - @PostMapping(value = "/writing",consumes = {MediaType.APPLICATION_JSON_VALUE}) - public BaseResponse writing(@RequestPart(value="json") Map requestData) { + @PostMapping(value = "/write-post",consumes = {MediaType.MULTIPART_FORM_DATA_VALUE}) + public BaseResponse writing(@ModelAttribute WritingRequest writingRequest) { try { - String title = (String) requestData.get("title"); - String content = (String) requestData.get("content"); - int category = (int) requestData.get("category"); + String title = writingRequest.getTitle(); + String content = writingRequest.getContent(); + MultipartFile file = writingRequest.getImage(); - postService.writing(title, content, category); + postService.writing(title, content, file); return new BaseResponse<>("글 작성 성공"); } catch (Exception e) { return new BaseResponse<>(e.getMessage()); @@ -94,14 +86,13 @@ public BaseResponse writing(@RequestPart(value="json") Map readPost(@RequestParam(value = "id", required = true) Long id) throws BaseException { - + public BaseResponse readPost(@RequestParam(value="id") Long id) throws BaseException { return new BaseResponse<>(postService.readPost(id)); } @DeleteMapping("delete-post") - public BaseResponse deletePost(@RequestParam(value="id",required = true) Long id) throws BaseException { + public BaseResponse deletePost(@RequestParam(value="id") Long id) throws BaseException { postService.deletePost(id); String result = "동아리 게시글 삭제 완료"; return new BaseResponse<>(result); diff --git a/src/main/java/com/likelion/dub/domain/Club.java b/src/main/java/com/likelion/dub/domain/Club.java index 28e3afc..742903d 100644 --- a/src/main/java/com/likelion/dub/domain/Club.java +++ b/src/main/java/com/likelion/dub/domain/Club.java @@ -1,6 +1,7 @@ package com.likelion.dub.domain; +import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.*; import lombok.*; @@ -22,7 +23,6 @@ public class Club { private Long id; - @OneToMany(mappedBy = "club") private List post = new ArrayList<>(); diff --git a/src/main/java/com/likelion/dub/domain/Member.java b/src/main/java/com/likelion/dub/domain/Member.java index 11274fc..e2c88cc 100644 --- a/src/main/java/com/likelion/dub/domain/Member.java +++ b/src/main/java/com/likelion/dub/domain/Member.java @@ -1,5 +1,6 @@ package com.likelion.dub.domain; +import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.*; import lombok.*; @@ -23,6 +24,8 @@ public class Member { @OneToOne(mappedBy = "member") private Club club; + + @OneToMany(mappedBy = "member") private List post = new ArrayList<>(); diff --git a/src/main/java/com/likelion/dub/domain/Post.java b/src/main/java/com/likelion/dub/domain/Post.java index 76aa1f7..26ca014 100644 --- a/src/main/java/com/likelion/dub/domain/Post.java +++ b/src/main/java/com/likelion/dub/domain/Post.java @@ -1,5 +1,6 @@ package com.likelion.dub.domain; +import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.*; import lombok.*; @@ -19,6 +20,8 @@ public class Post extends BaseEntity{ @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + + @JsonIgnore @OneToMany(mappedBy = "post") private List comments = new ArrayList<>(); @@ -35,11 +38,12 @@ public class Post extends BaseEntity{ private String postImage; + @JsonIgnore @ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY) @JoinColumn(name = "club_id") private Club club; - + @JsonIgnore @ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY) @JoinColumn(name = "member_id") private Member member; diff --git a/src/main/java/com/likelion/dub/domain/dto/ClubMemberJoinRequest.java b/src/main/java/com/likelion/dub/domain/dto/ClubMemberJoinRequest.java index 249803c..9711002 100644 --- a/src/main/java/com/likelion/dub/domain/dto/ClubMemberJoinRequest.java +++ b/src/main/java/com/likelion/dub/domain/dto/ClubMemberJoinRequest.java @@ -20,6 +20,7 @@ public class ClubMemberJoinRequest { @JsonProperty private String password; @JsonProperty + @Nullable private String gender; @JsonProperty private String role; diff --git a/src/main/java/com/likelion/dub/domain/dto/PostGetRequest.java b/src/main/java/com/likelion/dub/domain/dto/GetAllPostResponse.java similarity index 57% rename from src/main/java/com/likelion/dub/domain/dto/PostGetRequest.java rename to src/main/java/com/likelion/dub/domain/dto/GetAllPostResponse.java index 4c591c6..c826475 100644 --- a/src/main/java/com/likelion/dub/domain/dto/PostGetRequest.java +++ b/src/main/java/com/likelion/dub/domain/dto/GetAllPostResponse.java @@ -1,17 +1,19 @@ package com.likelion.dub.domain.dto; -import com.fasterxml.jackson.annotation.JsonProperty; + import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; -@Getter @AllArgsConstructor -public class PostGetRequest { +@Getter +@Setter +@NoArgsConstructor +public class GetAllPostResponse { - @JsonProperty - private String clubName; - @JsonProperty + private Long id; private String title; - + private String clubName; } diff --git a/src/main/java/com/likelion/dub/domain/dto/GetOnePostResponse.java b/src/main/java/com/likelion/dub/domain/dto/GetOnePostResponse.java new file mode 100644 index 0000000..f36199f --- /dev/null +++ b/src/main/java/com/likelion/dub/domain/dto/GetOnePostResponse.java @@ -0,0 +1,28 @@ +package com.likelion.dub.domain.dto; + +import com.likelion.dub.domain.Comment; +import jakarta.persistence.Lob; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.List; + +@AllArgsConstructor +@Getter +@Setter +@NoArgsConstructor +public class GetOnePostResponse { + + + private String title; + private String clubName; + @Lob + private String content; + + private List comments; + + private String postImage; + +} diff --git a/src/main/java/com/likelion/dub/domain/dto/ImageDto.java b/src/main/java/com/likelion/dub/domain/dto/ImageDto.java deleted file mode 100644 index 1b7a328..0000000 --- a/src/main/java/com/likelion/dub/domain/dto/ImageDto.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.likelion.dub.domain.dto; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Builder -@Getter -@AllArgsConstructor -@NoArgsConstructor -public class ImageDto { - @JsonProperty - private String origFileName; - - @JsonProperty - private String filePath; - - @JsonProperty - private Long fileSize; - -} diff --git a/src/main/java/com/likelion/dub/domain/dto/MemberJoinRequest.java b/src/main/java/com/likelion/dub/domain/dto/MemberJoinRequest.java index 85980cb..ccaa797 100644 --- a/src/main/java/com/likelion/dub/domain/dto/MemberJoinRequest.java +++ b/src/main/java/com/likelion/dub/domain/dto/MemberJoinRequest.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.annotation.Nullable; import lombok.AllArgsConstructor; import lombok.Getter; @@ -15,6 +16,7 @@ public class MemberJoinRequest { @JsonProperty private String password; @JsonProperty + @Nullable private String gender; @JsonProperty private String role; diff --git a/src/main/java/com/likelion/dub/domain/dto/PostWritingRequest.java b/src/main/java/com/likelion/dub/domain/dto/PostWritingRequest.java index 1ef1f51..6b07ba8 100644 --- a/src/main/java/com/likelion/dub/domain/dto/PostWritingRequest.java +++ b/src/main/java/com/likelion/dub/domain/dto/PostWritingRequest.java @@ -23,4 +23,6 @@ public class PostWritingRequest { + + } diff --git a/src/main/java/com/likelion/dub/domain/dto/WritingRequest.java b/src/main/java/com/likelion/dub/domain/dto/WritingRequest.java new file mode 100644 index 0000000..c6e1021 --- /dev/null +++ b/src/main/java/com/likelion/dub/domain/dto/WritingRequest.java @@ -0,0 +1,26 @@ +package com.likelion.dub.domain.dto; + + +import jakarta.annotation.Nullable; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.web.multipart.MultipartFile; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class WritingRequest { + + private String title; + private String content; + + @Nullable + private MultipartFile image; + + + + +} diff --git a/src/main/java/com/likelion/dub/service/PostService.java b/src/main/java/com/likelion/dub/service/PostService.java index 1fcb0c7..73ec10f 100644 --- a/src/main/java/com/likelion/dub/service/PostService.java +++ b/src/main/java/com/likelion/dub/service/PostService.java @@ -10,6 +10,8 @@ import com.likelion.dub.domain.Member; import com.likelion.dub.domain.Post; +import com.likelion.dub.domain.dto.GetAllPostResponse; +import com.likelion.dub.domain.dto.GetOnePostResponse; import com.likelion.dub.repository.MemberRepository; import com.likelion.dub.repository.PostRepository; import lombok.RequiredArgsConstructor; @@ -22,7 +24,9 @@ import org.springframework.web.multipart.MultipartFile; import java.io.IOException; +import java.util.ArrayList; import java.util.List; +import java.util.Optional; @Service @Transactional @@ -37,28 +41,45 @@ public class PostService { private String bucket; - public List getAllClubs() { - return this.postRepository.findAll(); + public List getAllPost() { + + List allPosts = postRepository.findAll(); + List getAllPostResponses =new ArrayList<>(); + for (Post allPost : allPosts) { + GetAllPostResponse getAllPostResponse = new GetAllPostResponse(); + getAllPostResponse.setId(allPost.getId()); + getAllPostResponse.setTitle(allPost.getTitle()); + getAllPostResponse.setClubName(allPost.getClubName()); + getAllPostResponses.add(getAllPostResponse); + } + return getAllPostResponses; } - public BaseResponse writing(String title, String content, int category) throws BaseException { + public BaseResponse writing(String title, String content, MultipartFile file) throws BaseException,IOException { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - //jwt token 오류 - if (authentication == null || !authentication.isAuthenticated()) { - return new BaseResponse(BaseResponseStatus.JWT_TOKEN_ERROR); - } String email = authentication.getName(); - Member member = memberRepository.findByEmail(email).orElseThrow(); + Member member = memberRepository.findByEmail(email).orElseThrow(() -> new BaseException(BaseResponseStatus.NO_SUCH_MEMBER_EXIST)); Club club = member.getClub(); + if (club == null) { + throw new BaseException(BaseResponseStatus.NO_SUCH_CLUB_EXIST); + } String clubName = club.getClubName(); - - //이 club 글이 있으면 작성 불가 - postRepository.findByClubName(clubName) - .ifPresent(post -> { - throw new BaseException(BaseResponseStatus.USERS_EMPTY_USER_ID); - }); + // post 객체 생성 및 db 에 저장 + Post post = new Post(); + post.setClubName(clubName); + post.setClub(club); + post.setMember(member); + post.setTitle(title); + post.setContent(content); + if (file != null) { + String fileName = member.getId() + "PostImage"; + // 포스터 사진 S3에 저장 + uploadPostImageToS3(fileName, file); + post.setPostImage("https://dubs3.s3.ap-northeast-2.amazonaws.com/" + fileName); + } + postRepository.save(post); return new BaseResponse<>("글 작성 성공"); @@ -68,14 +89,17 @@ public BaseResponse writing(String title, String content, int category) public BaseResponse writePost(String title, String content, MultipartFile file) throws BaseException, IOException { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); String email = authentication.getName(); - Member member = memberRepository.findByEmail(email).orElseThrow(); + Member member = memberRepository.findByEmail(email).orElseThrow(() -> new BaseException(BaseResponseStatus.NO_SUCH_MEMBER_EXIST)); Club club = member.getClub(); + if (club == null) { + throw new BaseException(BaseResponseStatus.NO_SUCH_CLUB_EXIST); + } String clubName = club.getClubName(); - // post 객체 생성 및 db 에 저장 Post post = new Post(); post.setClubName(clubName); post.setClub(club); + post.setMember(member); post.setTitle(title); post.setContent(content); if (file != null) { @@ -85,6 +109,8 @@ public BaseResponse writePost(String title, String content, MultipartFil post.setPostImage("https://dubs3.s3.ap-northeast-2.amazonaws.com/" + fileName); } postRepository.save(post); + + return new BaseResponse<>("글 작성 성공"); } @@ -96,12 +122,20 @@ private void uploadPostImageToS3(String fileName, MultipartFile file) throws IOE } - public Post readPost(Long id) throws BaseException { - return postRepository.findById(id) - .orElseThrow(() -> - new BaseException(BaseResponseStatus.NOT_EXISTS_POST) + public GetOnePostResponse readPost(Long id) throws BaseException { + Optional post = postRepository.findById(id); + GetOnePostResponse getOnePostResponse = new GetOnePostResponse(); + getOnePostResponse.setClubName(post.get().getClubName()); + getOnePostResponse.setTitle(post.get().getTitle()); + getOnePostResponse.setContent(post.get().getContent()); + getOnePostResponse.setPostImage(post.get().getPostImage()); + + List comments = null; + getOnePostResponse.setComments(comments); + + return getOnePostResponse; + - ); } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index f2c47f5..d7139ac 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,5 +1,5 @@ # MySQL ?? ?? -spring.datasource.url=jdbc:mysql://localhost:3306/dub_4 +spring.datasource.url=jdbc:mysql://localhost:3306/hihi spring.datasource.username=root spring.datasource.password=whqkr44## spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver diff --git a/src/test/java/com/likelion/dub/DubApplicationTests.java b/src/test/java/com/likelion/dub/DubApplicationTests.java index 89d5ba9..d1732ff 100644 --- a/src/test/java/com/likelion/dub/DubApplicationTests.java +++ b/src/test/java/com/likelion/dub/DubApplicationTests.java @@ -3,10 +3,10 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -//@SpringBootTest +@SpringBootTest class DubApplicationTests { - //@Test + @Test void contextLoads() { } diff --git a/src/test/java/com/likelion/dub/service/PostServiceTest.java b/src/test/java/com/likelion/dub/service/PostServiceTest.java new file mode 100644 index 0000000..e2c75d5 --- /dev/null +++ b/src/test/java/com/likelion/dub/service/PostServiceTest.java @@ -0,0 +1,68 @@ +package com.likelion.dub.service; + +import com.likelion.dub.common.BaseException; +import com.likelion.dub.common.BaseResponse; +import com.likelion.dub.domain.Club; +import com.likelion.dub.domain.Member; +import com.likelion.dub.domain.Post; +import com.likelion.dub.repository.MemberRepository; +import com.likelion.dub.repository.PostRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; + +import org.springframework.boot.test.context.SpringBootTest; + +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +@SpringBootTest +@Transactional +class PostServiceTest { + @Autowired MemberRepository memberRepository; + + + @Autowired MemberService memberService; + + @Autowired PostRepository postRepository; + + @Autowired PostService postService; + + + @BeforeEach + void createClub(){ + MockMultipartFile file = new MockMultipartFile("file", "test.txt", "text/plain", "Test file content.".getBytes()); + memberService.joinClub("suhoon@naver.com", "name", "password", "gender", "CLUB","introduction","groupName","category",file); + } + + @Test + @WithMockUser(username = "suhoon@naver.com",roles="CLUB") + void testWritePost() throws BaseException, IOException { + //given + + Member member = new Member(1L, null, null, "suhoon@naver.com", "name", "password", "gender", "CLUB"); + Club club = new Club(1L, null, "name", "introduction", "groupName", "category", "clubImage", member); + MockMultipartFile file = new MockMultipartFile("file", "test.txt", "text/plain", "Test file content.".getBytes()); + //when + postService.writePost("title", "content", file); + + //then + + + + + + } +} \ No newline at end of file From 6801a663b1159190c2b80912b046a4948807fc10 Mon Sep 17 00:00:00 2001 From: suhoon Date: Wed, 23 Aug 2023 22:02:43 +0900 Subject: [PATCH 38/72] =?UTF-8?q?feat:=20JspController=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 21 +++++------ .../dub/controller/JspController.java | 35 +++++++++++++++++++ .../dub/domain/dto/MemberJoinRequest.java | 2 ++ src/main/resources/application.properties | 4 +-- src/main/resources/templates/JoinView.html | 34 ++++++++++++++++++ src/main/resources/templates/loginView.html | 11 ++++++ src/main/resources/templates/mainView.html | 11 ++++++ 7 files changed, 106 insertions(+), 12 deletions(-) create mode 100644 src/main/java/com/likelion/dub/controller/JspController.java create mode 100644 src/main/resources/templates/JoinView.html create mode 100644 src/main/resources/templates/loginView.html create mode 100644 src/main/resources/templates/mainView.html diff --git a/build.gradle b/build.gradle index 372445d..abecc44 100644 --- a/build.gradle +++ b/build.gradle @@ -20,32 +20,33 @@ repositories { dependencies { - // spring init + // spring + developmentOnly 'org.springframework.boot:spring-boot-devtools' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'mysql:mysql-connector-java:8.0.26' implementation 'org.springframework.boot:spring-boot-starter-security' + // mysql + implementation 'mysql:mysql-connector-java:8.0.26' + runtimeOnly 'com.mysql:mysql-connector-j' + // lombok compileOnly 'org.projectlombok:lombok' - developmentOnly 'org.springframework.boot:spring-boot-devtools' annotationProcessor 'org.projectlombok:lombok' - runtimeOnly 'com.mysql:mysql-connector-j' - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'org.springframework.security:spring-security-test' // jwt implementation 'io.jsonwebtoken:jjwt-api:0.11.5' implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' implementation 'commons-io:commons-io:2.6' - //swagger + // swagger implementation 'io.springfox:springfox-boot-starter:3.0.0' implementation group: 'io.springfox', name: 'springfox-swagger-ui', version: '3.0.0' // s3 implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' - - //프록시 json 화 + // 프록시 json화 implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5-jakarta' - + // test + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.security:spring-security-test' } tasks.named('test') { diff --git a/src/main/java/com/likelion/dub/controller/JspController.java b/src/main/java/com/likelion/dub/controller/JspController.java new file mode 100644 index 0000000..0e4282e --- /dev/null +++ b/src/main/java/com/likelion/dub/controller/JspController.java @@ -0,0 +1,35 @@ +package com.likelion.dub.controller; + +import com.likelion.dub.domain.Member; +import com.likelion.dub.domain.dto.MemberJoinRequest; +import com.likelion.dub.service.MemberService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class JspController { + @Autowired + private MemberService memberService; + + @GetMapping("/login") + public String loginView(Model model) { + String result = "loginVIew"; + model.addAttribute("result", result); + return "loginView"; + } + + @GetMapping("/main") + public String mainView(Model model) { + String result = "mainView"; + model.addAttribute("result", result); + return "mainView"; + } + + @GetMapping("/join") + public String joinView(Model model) { + model.addAttribute("memberJoinRequest", new MemberJoinRequest()); + return "joinView"; + } +} diff --git a/src/main/java/com/likelion/dub/domain/dto/MemberJoinRequest.java b/src/main/java/com/likelion/dub/domain/dto/MemberJoinRequest.java index ccaa797..cdb7188 100644 --- a/src/main/java/com/likelion/dub/domain/dto/MemberJoinRequest.java +++ b/src/main/java/com/likelion/dub/domain/dto/MemberJoinRequest.java @@ -5,9 +5,11 @@ import jakarta.annotation.Nullable; import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.NoArgsConstructor; @AllArgsConstructor @Getter +@NoArgsConstructor public class MemberJoinRequest { @JsonProperty private String email; diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index d7139ac..140f0f6 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -15,8 +15,8 @@ jwt.token.secret=VlwEyVBsYt9V7zq57TejMnVUyzblYcfPQye08f7MGVA9XkHa #multipart spring.servlet.multipart.enabled=true -spring.servlet.multipart.max-file-size=10MB -spring.servlet.multipart.max-request-size=10MB +spring.servlet.multipart.max-file-size=100MB +spring.servlet.multipart.max-request-size=100MB #s3 cloud.aws.s3.bucket=dubs3 diff --git a/src/main/resources/templates/JoinView.html b/src/main/resources/templates/JoinView.html new file mode 100644 index 0000000..78e7a53 --- /dev/null +++ b/src/main/resources/templates/JoinView.html @@ -0,0 +1,34 @@ + + + + + 회원 가입 + + +
+ +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + + diff --git a/src/main/resources/templates/loginView.html b/src/main/resources/templates/loginView.html new file mode 100644 index 0000000..1420c13 --- /dev/null +++ b/src/main/resources/templates/loginView.html @@ -0,0 +1,11 @@ + + + + + + loginView + + +

+ + diff --git a/src/main/resources/templates/mainView.html b/src/main/resources/templates/mainView.html new file mode 100644 index 0000000..bd9b240 --- /dev/null +++ b/src/main/resources/templates/mainView.html @@ -0,0 +1,11 @@ + + + + + + mainView + + +

+ + From b57fc2e7961c42a0aa5f7322e4ff2021956e3823 Mon Sep 17 00:00:00 2001 From: suhoon Date: Wed, 23 Aug 2023 22:16:21 +0900 Subject: [PATCH 39/72] =?UTF-8?q?fix:=20readPost=20PathVariable=20?= =?UTF-8?q?=EB=B0=A9=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/likelion/dub/controller/PostController.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/likelion/dub/controller/PostController.java b/src/main/java/com/likelion/dub/controller/PostController.java index 6ed48b4..1402be3 100644 --- a/src/main/java/com/likelion/dub/controller/PostController.java +++ b/src/main/java/com/likelion/dub/controller/PostController.java @@ -85,8 +85,8 @@ public BaseResponse writing(@ModelAttribute WritingRequest writingReques * @param id * @return */ - @GetMapping("/read-post") - public BaseResponse readPost(@RequestParam(value="id") Long id) throws BaseException { + @GetMapping("/read-post/{id}") + public BaseResponse readPost(@PathVariable Long id) throws BaseException { return new BaseResponse<>(postService.readPost(id)); } From 739f4bc50d07ff6c65fb67a3cbb4d584245ba862 Mon Sep 17 00:00:00 2001 From: suhoon <82464990+s2hoon@users.noreply.github.com> Date: Wed, 23 Aug 2023 22:34:11 +0900 Subject: [PATCH 40/72] Docs: Readme --- README.md | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ecafca9..591c8cb 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,72 @@ +--- + + # dub_club_wanted_BE -동아리 버전 원티드 +동아리 홍보와, 동아리 지원을 할수있는 플랫폼 + + +--- + + +# 핵심기능 + + +1. 회원 가입: + + +일반 회원과 동아리 회원으로 나누어 가입 가능합니다. + + +2. 동아리 등록: + + +동아리 회원은 동아리의 프로필 이미지, 태그, 소개글 등을 등록할 수 있습니다. + + +3. 동아리 공고 및 지원서 양식 등록: + + +동아리 회원은 동아리 내에서 공고를 올릴 수 있으며, 지원자들에게 지원서 양식을 제공할 수 있습니다. + + +4. 태그별 동아리 조회: + + +사용자는 태그별로 동아리를 검색하고 조회할 수 있습니다. + + +5. 동아리 지원서 다운로드 및 제출: + + +일반 회원은 동아리의 지원서 양식을 다운로드하여 작성한 후, 해당 동아리에 제출할 수 있습니다. + + +6. 동아리 회원별 지원자 조회: + + +동아리 회원은 자신의 동아리에 지원한 회원들을 조회할 수 있습니다. + + +7. 합격/불합격 통보 보내기: + + +동아리 회원은 지원자들에게 합격 또는 불합격 통보를 보낼 수 있습니다. + + + +--- + + +# ERD + + +![db설계 최종](https://github.com/s2hoon/dub_club_wanted_BE/assets/82464990/532c6e73-719f-4645-a798-1fe47da878c3) + + +--- + +# 성능개선 + + + + From a8e7f56933fff14ea09e4f9e0fa82fbbad15b9b4 Mon Sep 17 00:00:00 2001 From: suhoon <82464990+s2hoon@users.noreply.github.com> Date: Wed, 23 Aug 2023 22:40:03 +0900 Subject: [PATCH 41/72] Docs: Readme --- README.md | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 591c8cb..8b08b43 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ # dub_club_wanted_BE + +## dub 동아리 홍보와, 동아리 지원을 할수있는 플랫폼 @@ -11,46 +13,45 @@ # 핵심기능 -1. 회원 가입: - +1. 회원 가입 -일반 회원과 동아리 회원으로 나누어 가입 가능합니다. + ```일반 회원과 동아리 회원으로 나누어 가입이 가능합니다.``` -2. 동아리 등록: +2. 동아리 등록 -동아리 회원은 동아리의 프로필 이미지, 태그, 소개글 등을 등록할 수 있습니다. + ```동아리 회원은 동아리의 프로필 이미지, 태그, 소개글 등을 등록할 수 있습니다.``` -3. 동아리 공고 및 지원서 양식 등록: +3. 동아리 공고 및 지원서 양식 등록 -동아리 회원은 동아리 내에서 공고를 올릴 수 있으며, 지원자들에게 지원서 양식을 제공할 수 있습니다. + ```동아리 회원은 동아리 내에서 공고를 올릴 수 있으며, 지원자들에게 지원서 양식을 제공할 수 있습니다.``` -4. 태그별 동아리 조회: +4. 태그별 동아리 조회 -사용자는 태그별로 동아리를 검색하고 조회할 수 있습니다. + ```사용자는 태그별로 동아리를 검색하고 조회할 수 있습니다.``` -5. 동아리 지원서 다운로드 및 제출: +5. 동아리 지원서 다운로드 및 제출 -일반 회원은 동아리의 지원서 양식을 다운로드하여 작성한 후, 해당 동아리에 제출할 수 있습니다. + ```일반 회원은 동아리의 지원서 양식을 다운로드하여 작성한 후, 해당 동아리에 제출할 수 있습니다.``` -6. 동아리 회원별 지원자 조회: +6. 동아리 회원별 지원자 조회 -동아리 회원은 자신의 동아리에 지원한 회원들을 조회할 수 있습니다. + ```동아리 회원은 자신의 동아리에 지원한 회원들을 조회할 수 있습니다.``` -7. 합격/불합격 통보 보내기: +7. 합격/불합격 통보 보내기 -동아리 회원은 지원자들에게 합격 또는 불합격 통보를 보낼 수 있습니다. + ```동아리 회원은 지원자들에게 합격 또는 불합격 통보를 보낼 수 있습니다.``` From b78270129fb25cc40a50b7d824800f77ca12710f Mon Sep 17 00:00:00 2001 From: suhoon Date: Thu, 24 Aug 2023 00:24:57 +0900 Subject: [PATCH 42/72] =?UTF-8?q?fix:=20BaseException=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/likelion/dub/common/BaseResponse.java | 16 ++----- .../dub/controller/MemberController.java | 24 ++++------ .../dub/controller/MypageController.java | 4 +- .../dub/controller/PostController.java | 45 +++++++++++-------- .../com/likelion/dub/service/PostService.java | 29 ++++++------ 5 files changed, 55 insertions(+), 63 deletions(-) diff --git a/src/main/java/com/likelion/dub/common/BaseResponse.java b/src/main/java/com/likelion/dub/common/BaseResponse.java index efc5549..9b02e7c 100644 --- a/src/main/java/com/likelion/dub/common/BaseResponse.java +++ b/src/main/java/com/likelion/dub/common/BaseResponse.java @@ -21,26 +21,16 @@ public class BaseResponse { // 해당 필드가 null인 경우 JSON에 표현되지 않는다. @JsonInclude(JsonInclude.Include.NON_NULL) private T result; - // 요청 성공 - public BaseResponse(T result) { - this.isSuccess = SUCCESS.isSuccess(); - this.code = SUCCESS.getCode(); - this.message = SUCCESS.getMessage(); + public BaseResponse(BaseResponseStatus status, T result) { + this(status); this.result = result; } - // 요청에 실패한 경우 + // 요청 실패 public BaseResponse(BaseResponseStatus status) { this.isSuccess = status.isSuccess(); this.code = status.getCode(); this.message = status.getMessage(); } - - public BaseResponse(T result, BaseResponseStatus existsLike) { - this.isSuccess = existsLike.isSuccess(); - this.code = existsLike.getCode(); - this.message = existsLike.getMessage(); - this.result = result; - } } \ No newline at end of file diff --git a/src/main/java/com/likelion/dub/controller/MemberController.java b/src/main/java/com/likelion/dub/controller/MemberController.java index 87b6c91..93bcb67 100644 --- a/src/main/java/com/likelion/dub/controller/MemberController.java +++ b/src/main/java/com/likelion/dub/controller/MemberController.java @@ -26,7 +26,6 @@ public class MemberController { private final MemberService memberService; - /** * 이메일 중복체크 * @param email @@ -38,7 +37,7 @@ public BaseResponse checkEmail(@PathVariable String email) { boolean isEmailAvailable = memberService.checkEmail(email); if (isEmailAvailable) { String result = "이메일 사용 가능"; - return new BaseResponse<>(result); + return new BaseResponse<>(BaseResponseStatus.SUCCESS,result); } else { return new BaseResponse(BaseResponseStatus.EMAIL_ALREADY_EXIST); } @@ -47,17 +46,17 @@ public BaseResponse checkEmail(@PathVariable String email) { /** - * 일반회원 회원가입 + * 일반회원가입 * @param dto * @return */ @PostMapping("/sign-up") - public BaseResponse join(@RequestBody MemberJoinRequest dto ) throws BaseException{ + public BaseResponse join(@RequestBody MemberJoinRequest dto ) { try { memberService.join(dto.getEmail(), dto.getName(), dto.getPassword(), dto.getGender(), dto.getRole()); String result = "(일반)회원 가입 완료"; - return new BaseResponse<>(result); + return new BaseResponse<>(BaseResponseStatus.SUCCESS,result); } catch(BaseException e){ return new BaseResponse(e.getStatus()); @@ -69,16 +68,15 @@ public BaseResponse join(@RequestBody MemberJoinRequest dto ) throws Bas * @param dto * @param file * @return - * @throws BaseException */ @PostMapping(value = "/sign-up-club",consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE}) - public BaseResponse joinClub(@RequestPart(value = "json") ClubMemberJoinRequest dto, @RequestPart(value = "image", required = false) MultipartFile file) throws BaseException { + public BaseResponse joinClub(@RequestPart(value = "json") ClubMemberJoinRequest dto, @RequestPart(value = "image", required = false) MultipartFile file) { try { memberService.joinClub(dto.getEmail(), dto.getName(), dto.getPassword(), dto.getGender(), dto.getRole(), dto.getIntroduction(), dto.getGroupName(),dto.getCategory(), file); String result = "(동아리)회원 가입 완료"; - return new BaseResponse<>(result); + return new BaseResponse<>(BaseResponseStatus.SUCCESS,result); } catch (BaseException e) { return new BaseResponse<>(e.getStatus()); @@ -86,9 +84,7 @@ public BaseResponse joinClub(@RequestPart(value = "json") ClubMemberJoin } - - /** - * 로그인 + /** 로그인 * @param dto * @return */ @@ -97,14 +93,12 @@ public BaseResponse login(@RequestBody MemberLoginRequest dto) { try { String token = memberService.login(dto.getEmail(), dto.getPassword()); - return new BaseResponse<>("Bearer " + token); + return new BaseResponse<>(BaseResponseStatus.SUCCESS,"Bearer " + token); } catch (BaseException e) { - BaseResponseStatus result = e.getStatus(); - return new BaseResponse(result); + return new BaseResponse(e.getStatus()); } - } diff --git a/src/main/java/com/likelion/dub/controller/MypageController.java b/src/main/java/com/likelion/dub/controller/MypageController.java index 6c812fa..eff6d7f 100644 --- a/src/main/java/com/likelion/dub/controller/MypageController.java +++ b/src/main/java/com/likelion/dub/controller/MypageController.java @@ -48,7 +48,7 @@ public BaseResponse getMyPage(){ log.info(member.toString()); MyPageResponse myPageResponse = new MyPageResponse(member.getEmail(), member.getName(), member.getRole()); - return new BaseResponse<>(myPageResponse); + return new BaseResponse<>(BaseResponseStatus.SUCCESS,myPageResponse); } catch (BaseException e) { return new BaseResponse(BaseResponseStatus.JWT_TOKEN_ERROR); } @@ -84,7 +84,7 @@ public BaseResponse changePassword(@RequestBody PasswordRequest password mypageService.save(member); String result = "비밀번호 수정 완료"; - return new BaseResponse<>(result); + return new BaseResponse<>(BaseResponseStatus.SUCCESS, result); } diff --git a/src/main/java/com/likelion/dub/controller/PostController.java b/src/main/java/com/likelion/dub/controller/PostController.java index 1402be3..27f915e 100644 --- a/src/main/java/com/likelion/dub/controller/PostController.java +++ b/src/main/java/com/likelion/dub/controller/PostController.java @@ -28,18 +28,19 @@ public class PostController { /** - * 동아리 글 전체 조회 - * - * @param - * @return all post + * 동아리글 전체 조회 + * @return */ @GetMapping("/getAll") - public BaseResponse> getAllClubs() { - return new BaseResponse<>(postService.getAllPost()); - + public BaseResponse> getAllPost() { + try{ + List getAllPostResponses = postService.getAllPost(); + return new BaseResponse<>(BaseResponseStatus.SUCCESS,getAllPostResponses); + }catch(BaseException e){ + return new BaseResponse<>(e.getStatus()); + } } - - + // /** // * post 작성 // * @param dto @@ -61,7 +62,7 @@ public BaseResponse> getAllClubs() { /** - * post 작성 test + * post 작성 * @param writingRequest * @return */ @@ -72,33 +73,39 @@ public BaseResponse writing(@ModelAttribute WritingRequest writingReques String title = writingRequest.getTitle(); String content = writingRequest.getContent(); MultipartFile file = writingRequest.getImage(); - postService.writing(title, content, file); - return new BaseResponse<>("글 작성 성공"); - } catch (Exception e) { - return new BaseResponse<>(e.getMessage()); + return new BaseResponse<>(BaseResponseStatus.SUCCESS,"글 작성 성공"); + } catch (BaseException e) { + return new BaseResponse<>(e.getStatus()); } + } /** * post 보기 - * * @param id * @return */ @GetMapping("/read-post/{id}") public BaseResponse readPost(@PathVariable Long id) throws BaseException { - return new BaseResponse<>(postService.readPost(id)); + + try{ + GetOnePostResponse getOnePostResponse = postService.readPost(id); + return new BaseResponse<>(BaseResponseStatus.SUCCESS, getOnePostResponse); + }catch(BaseException e){ + return new BaseResponse<>(e.getStatus()); + } + } @DeleteMapping("delete-post") - public BaseResponse deletePost(@RequestParam(value="id") Long id) throws BaseException { + public BaseResponse deletePost(@RequestParam(value="id") Long id) { postService.deletePost(id); String result = "동아리 게시글 삭제 완료"; - return new BaseResponse<>(result); + return new BaseResponse<>(BaseResponseStatus.SUCCESS,result); } @PutMapping("/edit-post") - public BaseResponse editPost(@RequestPart(value="json") PostEditRequest dto, @RequestPart(value="images", required = false) List images) throws BaseException{ + public BaseResponse editPost(@RequestPart(value="json") PostEditRequest dto, @RequestPart(value="images", required = false) List images) { String newTitle = dto.getTitle(); String newContent = dto.getContent(); int newCategory = dto.getCategory(); diff --git a/src/main/java/com/likelion/dub/service/PostService.java b/src/main/java/com/likelion/dub/service/PostService.java index 73ec10f..ea6ac50 100644 --- a/src/main/java/com/likelion/dub/service/PostService.java +++ b/src/main/java/com/likelion/dub/service/PostService.java @@ -56,7 +56,7 @@ public List getAllPost() { } - public BaseResponse writing(String title, String content, MultipartFile file) throws BaseException,IOException { + public void writing(String title, String content, MultipartFile file) throws BaseException { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); String email = authentication.getName(); @@ -73,20 +73,22 @@ public BaseResponse writing(String title, String content, MultipartFile post.setMember(member); post.setTitle(title); post.setContent(content); - if (file != null) { - String fileName = member.getId() + "PostImage"; - // 포스터 사진 S3에 저장 - uploadPostImageToS3(fileName, file); - post.setPostImage("https://dubs3.s3.ap-northeast-2.amazonaws.com/" + fileName); + try { + if (file != null) { + String fileName = member.getId() + "PostImage"; + // 포스터 사진 S3에 저장 + uploadPostImageToS3(fileName, file); + post.setPostImage("https://dubs3.s3.ap-northeast-2.amazonaws.com/" + fileName); + } + }catch(IOException e){ + throw new BaseException(BaseResponseStatus.FILE_SAVE_ERROR); } postRepository.save(post); - - return new BaseResponse<>("글 작성 성공"); } - public BaseResponse writePost(String title, String content, MultipartFile file) throws BaseException, IOException { + public void writePost(String title, String content, MultipartFile file) throws BaseException, IOException { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); String email = authentication.getName(); Member member = memberRepository.findByEmail(email).orElseThrow(() -> new BaseException(BaseResponseStatus.NO_SUCH_MEMBER_EXIST)); @@ -111,7 +113,7 @@ public BaseResponse writePost(String title, String content, MultipartFil postRepository.save(post); - return new BaseResponse<>("글 작성 성공"); + } private void uploadPostImageToS3(String fileName, MultipartFile file) throws IOException { @@ -140,7 +142,7 @@ public GetOnePostResponse readPost(Long id) throws BaseException { } - public BaseResponse deletePost(Long id) throws BaseException { + public void deletePost(Long id) throws BaseException { // 저장 되어있는 post 가져오기 Post post = postRepository.findById(id).orElseThrow(() -> new BaseException(BaseResponseStatus.NOT_EXISTS_POST)); @@ -149,7 +151,6 @@ public BaseResponse deletePost(Long id) throws BaseException { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); //jwt token 오류 if (authentication == null || !authentication.isAuthenticated()) { - return new BaseResponse(BaseResponseStatus.JWT_TOKEN_ERROR); } // 로그인 된 이메일 String email = authentication.getName(); @@ -164,9 +165,9 @@ public BaseResponse deletePost(Long id) throws BaseException { //post id 가 같은지 검사, 같으면 삭제, 다르면 오류출력 if (member_post.getId() == id) { postRepository.deleteById(id); - return new BaseResponse<>("삭제 완료"); + } else { - return new BaseResponse(BaseResponseStatus.INVALID_MEMBER_JWT); + } } From c60df0524c808f21b7fef087b4866b3fd475b82c Mon Sep 17 00:00:00 2001 From: suhoon Date: Thu, 24 Aug 2023 00:58:23 +0900 Subject: [PATCH 43/72] =?UTF-8?q?chore:=20SecurityConfig=20=EB=B0=8F=20Exc?= =?UTF-8?q?eoption=20Enum=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/likelion/dub/DubApplication.java | 2 - .../dub/common/BaseResponseStatus.java | 86 ++++++------------- .../dub/configuration/SecurityConfig.java | 1 - .../dub/controller/PostController.java | 21 +---- .../dub/domain/dto/GetAllPostResponse.java | 2 + .../com/likelion/dub/service/PostService.java | 50 +++-------- .../likelion/dub/service/PostServiceTest.java | 2 +- 7 files changed, 40 insertions(+), 124 deletions(-) diff --git a/src/main/java/com/likelion/dub/DubApplication.java b/src/main/java/com/likelion/dub/DubApplication.java index fabb9b1..e04e8ef 100644 --- a/src/main/java/com/likelion/dub/DubApplication.java +++ b/src/main/java/com/likelion/dub/DubApplication.java @@ -1,9 +1,7 @@ package com.likelion.dub; -import com.fasterxml.jackson.datatype.hibernate5.jakarta.Hibernate5JakartaModule; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.autoconfigure.domain.EntityScan; import org.springframework.context.annotation.Bean; import org.springframework.data.domain.AuditorAware; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; diff --git a/src/main/java/com/likelion/dub/common/BaseResponseStatus.java b/src/main/java/com/likelion/dub/common/BaseResponseStatus.java index 6e4c480..4552e5e 100644 --- a/src/main/java/com/likelion/dub/common/BaseResponseStatus.java +++ b/src/main/java/com/likelion/dub/common/BaseResponseStatus.java @@ -6,90 +6,52 @@ public enum BaseResponseStatus { /** - * 1000 : Successed + * 1000 : Success */ SUCCESS(true, 1000, "요청에 성공했습니다."), - EXISTS_LIKE_AND_EXISTS_SCRAP(true,1010,"좋아요와 스크랩 한 게시물 입니다."), - NOT_EXISTS_LIKE_NOT_EXISTS_SCRAP(true,1011,"좋아요와 스크랩 하지 않은 게시물입니다."), - EXISTS_LIKE_AND_NOT_EXISTS_SCRAP(true,1012,"좋아요 한 게시물이지만 스크랩은 하지 않은 게시물 입니다."), - NOT_EXISTS_LIKE_EXISTS_SCRAP(true,1013,"좋아요 하지 않은 게시물이지만 스크랩은 한 게시물입니다."), + /** - * 2XXX : Request 내용 오류 + * 2XXX : Common */ - // Common REQUEST_ERROR(false, 2000, "입력값을 확인해주세요."), FILE_SAVE_ERROR(false, 2001, "파일 저장에 실패하였습니다."), FILE_DELETE_ERROR(false, 2002, "파일 삭제에 실패하였습니다."), - // album - NO_ACCESS_TO_VIBE(false, 2100, "해당 바이브에 대한 접근 권한이 없습니다."), - - // comment - POST_COMMENT_INVALID_BODY(false, 2200,"댓글의 글자수를 확인해주세요."), - - // member - - // mypage - INVALID_MEMBER_JWT(false,2300,"권한이 없는 회원의 접근입니다."), - EMPTY_PROFILE_IMAGE(false, 2301, "프로필 이미지를 입력해주세요."), - - JWT_TOKEN_ERROR(false, 2302, "jwt 토큰을 확인해주세요"), - - // post - USERS_EMPTY_USER_ID(false, 2010, "유저 아이디 값을 확인해주세요."), - POST_POSTS_INVALID_TITLE(false, 2011, "제목의 글자수를 확인해주세요."), - POST_POSTS_INVALID_BODY(false, 2012, "내용의 글자수를 확인해주세요."), - - // vibe - - /** - * 3XXX : 내부 오류 + * 3XXX : Member */ - // Common - RESPONSE_ERROR(false, 3000, "값을 불러오는 데 실패하였습니다."), + INVALID_MEMBER_JWT(false,3000,"권한이 없는 회원의 접근입니다."), + EMPTY_PROFILE_IMAGE(false, 3001, "프로필 이미지를 입력해주세요."), - // album + JWT_TOKEN_ERROR(false, 3002, "jwt 토큰을 확인해주세요"), - // comment - NOT_FOUND_COMMENT(false, 3100, "해당 댓글이 존재하지 않습니다."), - NOT_FOUND_SUB_COMMENT(false, 3101, "해당 대댓글이 존재하지 않습니다."), + USERS_EMPTY_USER_ID(false, 3003, "유저 아이디 값을 확인해주세요."), + EMAIL_ALREADY_EXIST(false, 3004, "이미 가입된 이메일 주소입니다."), + WRONG_EMAIL(false, 3005, "잘못된 이메일 주소입니다."), + WRONG_PASSWORD(false, 3006, "잘못된 비밀번호입니다."), + STU_NUM_ALREADY_EXIST(false, 3007, "이미 가입된 학번 입니다."), + NO_SUCH_MEMBER_EXIST(false, 3008, "존재하지 않는 회원입니다."), + NO_SUCH_CLUB_EXIST(false, 3009, "존재하지 않는 동아리입니다."), - // member - EMAIL_ALREADY_EXIST(false, 3200, "이미 가입된 이메일 주소입니다."), - WRONG_EMAIL(false, 3201, "잘못된 이메일 주소입니다."), - WRONG_PASSWORD(false, 3202, "잘못된 비밀번호입니다."), - DISABLED_MEMBER(false, 3203, "탈퇴한 회원입니다."), - STU_NUM_ALREADY_EXIST(false, 3204, "이미 가입된 학번 입니다."), - NO_SUCH_MEMBER_EXIST(false, 3203, "존재하지 않는 회원입니다."), + /** + * 4XXX : Post + */ + NOT_EXISTS_POST(false,4000,"게시물이 존재하지 않습니다."), + FAILED_GET_POST(false,4001,"게시물 조회에 실패하였습니다."), + NOT_EXISTS_TAG_NAME_POST(true,4002,"해당 태그를 가진 게시물이 없습니다."), + DELETE_FAIL_POST(false, 4003, "게시물 삭제에 실패하였습니다."); - NO_SUCH_CLUB_EXIST(false, 3205, "존재하지 않는 동아리입니다."), - // mypage + /** + * 5xxx: Mypage + */ - // post - DELETE_FAIL_POST(false, 3010, "게시물 삭제에 실패하였습니다."), - FAILED_GET_POST(false,3011,"게시물 조회에 실패하였습니다."), - NOT_EXISTS_TAG_NAME_POST(true,3011,"해당 태그를 가진 게시물이 없습니다."), - NOT_EXISTS_POST(false,3012,"게시물이 존재하지 않습니다."), - // vibe - SAVE_TEMPORARY_FILE_FAILED(false, 3500, "이미지 파일 전달 실패. 요청을 다시 전송해주세요."), - EMPTY_IMAGE(false, 3501, "이미지를 보내 주세요"), - EXTERNAL_API_FAILED(false, 3502,"외부 API 호출 실패"), - NO_PROPER_VIDEO(false, 3503, "적절한 음악이 없습니다. (주어진 정보가 너무 복잡한 경우 발생) "), - /** - * 4XXX : DB, server 오류 - */ - DBCONN_ERROR(false, 4000, "데이터베이스 연결 오류"), - SERVER_ERROR(false, 4001, "서버와의 연결에 실패하였습니다."), - // notice - NO_SUCH_NOTICE(false, 5000, "존재하진 않는 알림"); private final boolean isSuccess; private final int code; private final String message; diff --git a/src/main/java/com/likelion/dub/configuration/SecurityConfig.java b/src/main/java/com/likelion/dub/configuration/SecurityConfig.java index 1a9ba14..555b1b7 100644 --- a/src/main/java/com/likelion/dub/configuration/SecurityConfig.java +++ b/src/main/java/com/likelion/dub/configuration/SecurityConfig.java @@ -45,7 +45,6 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { //.cors().and() .authorizeRequests() .requestMatchers("/app/member/sign-up", "/app/member/sign-in", "/app/member/email/{email}", "/app/member/stunum/{stunum}","/app/post/getAll").permitAll() //누구나 접근 가능 - .requestMatchers(Swagger_url).permitAll() // .requestMatchers(HttpMethod.POST, "/app/member/test").hasRole("ADMIN") //admin 권한 필요 // .requestMatchers(HttpMethod.POST, "/app/post/write-post").hasRole("CLUB") //CLUB 권한 필요 .anyRequest().permitAll() diff --git a/src/main/java/com/likelion/dub/controller/PostController.java b/src/main/java/com/likelion/dub/controller/PostController.java index 27f915e..dbe04fe 100644 --- a/src/main/java/com/likelion/dub/controller/PostController.java +++ b/src/main/java/com/likelion/dub/controller/PostController.java @@ -40,25 +40,6 @@ public BaseResponse> getAllPost() { return new BaseResponse<>(e.getStatus()); } } - -// /** -// * post 작성 -// * @param dto -// * @param file -// * @return -// * @throws BaseException -// */ -// @PostMapping(value = "/write-post" , consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE}) -// public BaseResponse writePost(@RequestPart(value = "json") PostWritingRequest dto, @RequestPart(value = "image", required = false) MultipartFile file) throws BaseException { -// try { -// postService.writePost(dto.getTitle(), dto.getContent(), file); -// return new BaseResponse<>("글 작성 성공"); -// } catch (BaseException e) { -// return new BaseResponse<>(e.getStatus()); -// } catch (IOException e){ -// return new BaseResponse<>(e.getMessage()); -// } -// } /** @@ -68,7 +49,7 @@ public BaseResponse> getAllPost() { */ @PostMapping(value = "/write-post",consumes = {MediaType.MULTIPART_FORM_DATA_VALUE}) - public BaseResponse writing(@ModelAttribute WritingRequest writingRequest) { + public BaseResponse writePost(@ModelAttribute WritingRequest writingRequest) { try { String title = writingRequest.getTitle(); String content = writingRequest.getContent(); diff --git a/src/main/java/com/likelion/dub/domain/dto/GetAllPostResponse.java b/src/main/java/com/likelion/dub/domain/dto/GetAllPostResponse.java index c826475..b5e0bbf 100644 --- a/src/main/java/com/likelion/dub/domain/dto/GetAllPostResponse.java +++ b/src/main/java/com/likelion/dub/domain/dto/GetAllPostResponse.java @@ -16,4 +16,6 @@ public class GetAllPostResponse { private String title; private String clubName; + private String clubImage; + } diff --git a/src/main/java/com/likelion/dub/service/PostService.java b/src/main/java/com/likelion/dub/service/PostService.java index ea6ac50..0c875ba 100644 --- a/src/main/java/com/likelion/dub/service/PostService.java +++ b/src/main/java/com/likelion/dub/service/PostService.java @@ -3,6 +3,7 @@ import com.amazonaws.services.s3.AmazonS3Client; import com.amazonaws.services.s3.model.ObjectMetadata; +import com.fasterxml.jackson.databind.ser.Serializers; import com.likelion.dub.common.BaseException; import com.likelion.dub.common.BaseResponse; import com.likelion.dub.common.BaseResponseStatus; @@ -45,11 +46,12 @@ public List getAllPost() { List allPosts = postRepository.findAll(); List getAllPostResponses =new ArrayList<>(); - for (Post allPost : allPosts) { + for (Post post : allPosts) { GetAllPostResponse getAllPostResponse = new GetAllPostResponse(); - getAllPostResponse.setId(allPost.getId()); - getAllPostResponse.setTitle(allPost.getTitle()); - getAllPostResponse.setClubName(allPost.getClubName()); + getAllPostResponse.setId(post.getId()); + getAllPostResponse.setTitle(post.getTitle()); + getAllPostResponse.setClubName(post.getClubName()); + getAllPostResponse.setClubImage(post.getClub().getClubImage()); getAllPostResponses.add(getAllPostResponse); } return getAllPostResponses; @@ -79,43 +81,15 @@ public void writing(String title, String content, MultipartFile file) throws Bas // 포스터 사진 S3에 저장 uploadPostImageToS3(fileName, file); post.setPostImage("https://dubs3.s3.ap-northeast-2.amazonaws.com/" + fileName); + postRepository.save(post); } }catch(IOException e){ throw new BaseException(BaseResponseStatus.FILE_SAVE_ERROR); } - postRepository.save(post); } - public void writePost(String title, String content, MultipartFile file) throws BaseException, IOException { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - String email = authentication.getName(); - Member member = memberRepository.findByEmail(email).orElseThrow(() -> new BaseException(BaseResponseStatus.NO_SUCH_MEMBER_EXIST)); - Club club = member.getClub(); - if (club == null) { - throw new BaseException(BaseResponseStatus.NO_SUCH_CLUB_EXIST); - } - String clubName = club.getClubName(); - // post 객체 생성 및 db 에 저장 - Post post = new Post(); - post.setClubName(clubName); - post.setClub(club); - post.setMember(member); - post.setTitle(title); - post.setContent(content); - if (file != null) { - String fileName = member.getId() + "PostImage"; - // 포스터 사진 S3에 저장 - uploadPostImageToS3(fileName, file); - post.setPostImage("https://dubs3.s3.ap-northeast-2.amazonaws.com/" + fileName); - } - postRepository.save(post); - - - - } - private void uploadPostImageToS3(String fileName, MultipartFile file) throws IOException { ObjectMetadata metadata = new ObjectMetadata(); metadata.setContentType(file.getContentType()); @@ -125,12 +99,12 @@ private void uploadPostImageToS3(String fileName, MultipartFile file) throws IOE public GetOnePostResponse readPost(Long id) throws BaseException { - Optional post = postRepository.findById(id); + Post post = postRepository.findById(id).orElseThrow(() -> new BaseException(BaseResponseStatus.NOT_EXISTS_POST)); GetOnePostResponse getOnePostResponse = new GetOnePostResponse(); - getOnePostResponse.setClubName(post.get().getClubName()); - getOnePostResponse.setTitle(post.get().getTitle()); - getOnePostResponse.setContent(post.get().getContent()); - getOnePostResponse.setPostImage(post.get().getPostImage()); + getOnePostResponse.setClubName(post.getClubName()); + getOnePostResponse.setTitle(post.getTitle()); + getOnePostResponse.setContent(post.getContent()); + getOnePostResponse.setPostImage(post.getPostImage()); List comments = null; getOnePostResponse.setComments(comments); diff --git a/src/test/java/com/likelion/dub/service/PostServiceTest.java b/src/test/java/com/likelion/dub/service/PostServiceTest.java index e2c75d5..2863463 100644 --- a/src/test/java/com/likelion/dub/service/PostServiceTest.java +++ b/src/test/java/com/likelion/dub/service/PostServiceTest.java @@ -56,7 +56,7 @@ void testWritePost() throws BaseException, IOException { Club club = new Club(1L, null, "name", "introduction", "groupName", "category", "clubImage", member); MockMultipartFile file = new MockMultipartFile("file", "test.txt", "text/plain", "Test file content.".getBytes()); //when - postService.writePost("title", "content", file); + postService.writing("title", "content", file); //then From 9e37af73f8b5b4dffc28dd2bfd625ef94ae119fc Mon Sep 17 00:00:00 2001 From: suhoon Date: Fri, 8 Sep 2023 01:46:12 +0900 Subject: [PATCH 44/72] =?UTF-8?q?feat:=20=EC=A7=80=EC=9B=90=EC=84=9C=20?= =?UTF-8?q?=EC=96=91=EC=8B=9D=20=EB=93=B1=EB=A1=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- images/20230330/691422510048300.png | Bin 34784 -> 0 bytes images/20230330/82016473748200.png | Bin 213120 -> 0 bytes .../dub/configuration/JwtTokenFilter.java | 16 +++---- .../dub/configuration/JwtTokenUtil.java | 8 +++- .../dub/configuration/SecurityConfig.java | 7 ++-- .../dub/controller/ClubController.java | 34 +++++++++++++++ .../dub/controller/JspController.java | 6 ++- .../java/com/likelion/dub/domain/Club.java | 5 ++- .../dub/repository/MypageRepository.java | 6 --- .../dub/repository/PostRepository.java | 2 +- .../com/likelion/dub/service/ClubService.java | 39 ++++++++++++++++++ .../likelion/dub/service/MemberService.java | 6 +-- .../com/likelion/dub/service/PostService.java | 3 ++ .../likelion/dub/service/PostServiceTest.java | 17 ++------ 14 files changed, 111 insertions(+), 38 deletions(-) delete mode 100644 images/20230330/691422510048300.png delete mode 100644 images/20230330/82016473748200.png create mode 100644 src/main/java/com/likelion/dub/controller/ClubController.java delete mode 100644 src/main/java/com/likelion/dub/repository/MypageRepository.java create mode 100644 src/main/java/com/likelion/dub/service/ClubService.java diff --git a/images/20230330/691422510048300.png b/images/20230330/691422510048300.png deleted file mode 100644 index 3bccc11fc7ad51b0629986431fcdd770ae1f8226..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34784 zcmeFYS5#Bo7d@&Xs1y+p>7dl0(gMvQj4yB6uCuB2$_YrfkWo^`)(o^W-pYAyrS9gSuQXM4`}qsmM_518#j5Eld1|pz|BbnfxjAF+v~v$L{O9kJZ_LnMcmv&+SHU{BABEj} z5%u&_{{0vK-Wopw@e?oHepLM;LjS+d-iA{a|GN;@PtK(A@8;V{^g!5uH*-FD3kd)B znbNC%F!_H2Rs4T*Amt=s*wxvKmv}yJ46TgS&H`GYsC=$mMWl4fejaVSGZu*(iOo6zge0$f6%gB z#c{sAhX}jHW_#YlVA1G^gd+?}br1@KgG7no*5-U!LGzXkxJA3m(Q4k+1!lw|h9UTL z$O4752;P-H`Ay<8vt(iFxLm;spdJiA%+HSDk~J#No0`;YYim22F_8#HG%ue*=M~1_ zr-vsOxG}U)`$e*cO&>1v^6+9^iZH;JH;a2_}E1c^3!aH-TvSb_rxuq_gGk6G)RQPKQ;2P#<`A136~a z0|8a1xb+Ohv9-{sOY1*rg2sP_PLw6y{k(=OG?BnBM{<P!_?xxIaML+^ z^i2q7;|D|w4fL9;;=60@r)MnRPI@+bg`NT!3VLN<2VE_-d)aDy_V#j|_NLmJ5k1J5s`1cS227wbkmh5 zg!%s&S4Dp$x#iRj{oyzL*r?e~z~`#99oJ{NJ>%_%-*6Z&XuN#`^KpFjM9{3|ZJ(30 z&uHM*(%q}-~6cX!zs3=<*FQ_q4@A67kEC6$)Cjh2^_uF^tV?Y zLJ5pk)ZM1aJ-+(~$HSpjDs>Dfk1<)Bb7#Z$w&23{)G1fjvCCaZa81;0-t@DN1CPJT z(KEVlp}j0pAt!0fTfN&=t#*B*QkGxo^4q}J7S`#5qaW_!F|RoMJh!oTcN8qC^!0*| zT`gQ)78;yA*yigzbUzF3etr3MXRNejp~-g0S?F>0UZTRq@1e~m|1OE91}v9Qfq)`4 zNKXN>wToX0#4NV>O((FCD5AAwT}EyDj(6eayV9@r@`4W4k+p6%4NjXtQ3n)5qb7D-K&4RO5Nu!!PD~5jN(m zw$AF|?rZ4K_V%%(y@jzHS!i|tBFJY79JKjl-S_HyV9BrnHuvg87g?)k)BN0xxA@D$ zug4SXBXEzt1Q!Ad`h)ro%`+HEsqm8O?;3-}>(l(7_j^C1jjx_F$-eu}6xYY@H%qx$ zNG<=C+LSX=*7Xo5smS$UCpHrOs{;fShrN9K%H1M}UCyqa&1X2Bh5g=2Z-U2|{AC=Q zsNZj&b|0=*{;vI1uVF3QcntsGedm};$>M{{SbqL)JGHPgL=tK$Voig{_s2ge^a zeRqBKAXqnn%x6Ba4+K4{&a2Okvmli%$-8N{p4ryA>`Frn=bQBt6|gMJ#mAib_wx@P zy+GFZCz>3sB;8{79LW;*aC}uIGMRfDli>TY!;@oQXe{vJ@~`J6pIIUnjXuR;JiN`w z$*~mXhFi$;?XO1P)_dcb9I581y7to+67tk|X`0LlYw;KrSB`w~$~Vp0EE2}Xy#C{W z#~0&EUEh2Yrpg@pMl}U6sk7p4gsc6r$5>G~ruz0LIIPH?D5*{gYRXsUum0{bu8!iM$aU3d=DHZ6K&!}6#~WGSEdR3naWM2q;)=lGvIXOx ziP}FN3tD0KRa#S%@f}cjC{ycf-*?{4mDr@p9`H-RytcofLz#;6em0jL!cIH0X|c_m znqBf2GHtYtCTLTQ^p6rMxf^@t6`no$q+RwB%^vttNE4*G?yRfPvN~^4O9E|K##Y@wDNWR@D0mw zt4xs^nAGl_-P>cD`sHQz$_K?e3Cx2(PjU4>f0H(`cu=7YxB`>`=Tio$X%9h6x&`^% zY>#pjAD9}rR6qOn>O5lOlkz}_OCi;^M7r^}^`+y;yX;O>2Ga55X7+Q8gN-T=DXC+e zO&W{~Ba;FgaGDQnc}m~yW(9AQ8Ci6V%E@NR=Gtoys%T%=0rh4 zc~d1>CC$BbJU-Mv?ktoa!&CS&_5nO8xZ~aHs@!Kf%5z}g7rRtQSd-_@YO0V4Hi1p< zy-=snp2Bakzk7RwsnEe^`p;+&c^+78%c zT|b%awfq#@TdW)E7R~X`!&=7yRBG0V>qhO~)RWSkIg9&Nt|Jvpq9c|sec{>uhtYh8 zVlb<+mB~vAvkd*>3ZqiM3`R*g75gJY8XVOFROuR;>W+uSvnL&k6T5+6)R+nz_nQnfs#1o+Hz$GI-Ns@%<_ z?_sExu?C3TL}gpibdC&t2m2cSt-7p^^X}Z)?>z{Wh!Q(ob`FF1vCk?g3nxDX6>~Cc z__kZ@$eo+73e2!+J}oHnvaSyOX?N!+t6qo~_oAISrLr@bSj{$d-p*RTnDD zqgPTAw>H-1H$%zOK{LTjPBNHbPbE~?62~Fu&b@16FE#J7C4~2A41Dqq8i$k(HI)1*h@_Eyii$z-KG!1 zW6?7?Dm}%NzR$;9Q=X5UdG^rUQFH2UjK0ez{TA}}TObq%akuq$T3YT+WPxmZz+n!i zxzt0LPOy9YLY;%ndR!y}b(!JnJlNe7Qy;C#qz^LpgW5UcdxBRc>Gl0Nqywsmt`@BKpm1c(qYOLhVk@Oh{mWLMg~i z3?`d$Aa+8El_48ik0D0+fWRX@fnO9azrHEh9{l_SBIqU~Teb-dBp@(5iY?ucP(j#? zfQfO443#`@I7+T*Itq1N;?`upr_)kL6Jh}BcS5-Jv^-VM;6mW(pQ zGGT^op;D8<=VDeQ){e}z=BY{j@H5E#R4SKl1lII$Mv`nW&b<#cUOD6z!I6pp!WgR` zjAYIMEIAc6TLjkT!Rw@UA%Z&WDvTP#nUrhoYDh5IzKl>P>`~uH9S=h}fMxLQO|qoh z+wro^aF zxFH>TQB&-^EiNuVD@s+=lrR8Dh@cgfd)t2xCn4*zZ&98*dAgRssKNPOOG*)rEYZ#~ zT_dNjILMQLEROKFL}o!xCv5F*^8XrTlR>lvo;A&h+9Tpw#9s^Ew7ZicIgyoUssBog zhAvsqys>{>cYC2ZQD{n|o;7@EsWVgD_G?C!(}=+}m3Uds^(xK=J9LwmH#Umj{9T-| z6Y7_eepiyl;s)Y6qqYX&K`)(+EQ*7VBwbjCd(a4AG|E{iHgz2(Tw^~{E&H%(MeURQ z!)xOxFGu8?m8CiZzO2D8INM=_FC*D~SpKS{u&r!Vf0FIP`w_`{;mSObKp{!*rk}og zkNP%7Iy99SP~nt~;e z#-~k*6V_6D9~+aHps0ApzH1$#LjCjl1zR7=`0RY|koJ_Zwr;$NB{xkIEghT1)deop zdk0*^+pnRm0ZapVkE!^b05Yf51q|`hx^m0rzb|6-bJkzq7W6uHW{XHRe~}yCGW@IU z&}l8B3%GnN#MCnmroFd0+tw;q$kDPFgnNWwe(J4yYt7X|A1sh5uDt-(2ee=9wjP7+ ziU1{%8X()nTDL;~zxk1sdd2s;mYvw&KhOu;6g4Be^ahoPSMH=2gpU8}AW6I;i#YVbUo?0v zTQ)HjlU=QhrIhi0PPuhQLgi!xwrjwC`V401%`T=Sb~1mLHwW%hP%vzzF8`(p#Scu>rbao$^iSaOB z0BF4%L#PhP8O-?M(jP~4NZd3`w~M!bg8o^Q8u)ivmEYqGIs@Lcf)gR@= z$P2wI+9A1=xZk<9MZR(trSCo=uApS}%94U@TFO%i=ib|?l@4glDLIw)7%JY zEI&o_7_w4FOlEP??@W?u#9_k{#)B5f=K%pB$97PfOx;u**NvetT!exR14MMRcmw|Sk1x2mF!Qa`7Rh9}>^F9nHe-SJ_CVaruvrOBgXF=J;}a zSMI2SYo5ViqLT&tpSBk3vK%MMey~w2SO=*xTa5DL#02tD@=(v{!Yvw3r*ea;6S`@>TyjOUjiH&!Jrk&r;YqvLZn zN5l^W+gUVK&otPYRwq|-IrEu|bt}&*j~-@{AOFn=e&+8{ugtRX{_Pj?LY*%DCqCp@ zb@L{uRE{(>rdV&^>+WF%-L?gAwzr3Z0mW${5v}2#pyd-{#WgYZyn~3jE5PP{`W=Cz z5`^-;c$R0?<$0ZC!Umu_sBPLM*%v1rZpt98;`f1FNe{!dAoO0KmSW8ILB{PN%P zx%)@C_HRy3#oR7#`j4gz>NVSGH*_xzarn`BlrF!Sn6U&n1UGqMt(}>!!?YK?*-u2-TXtYaTcJ25(aaqKHi-ka8Wp+&n2foHXK z(F61P1Rd9~MY7x_XpTw!eHoBCQiz78b0+$}GH9UZqIBsN3D{SS&YXHFO4jL3!#gX8 z{{Yd{F$b~!bbkWieg*@eq$_In37E4bAbMVZbeX*G3g^nzcgqIk^Zw-e*xnqYM*Rp0 z%*A(ocm8nV0fg~^No^>9DA`@c--~{@ky`fe;SPXuil@4Bbb;2N9@wP|1-zwoP_qSG zC*f#$T|`SA+Kn}Imw|qvXN?t<`Z4!xrqY-NI)mo-ErGxJF(G$lD%Bk9dvG^}12hmg($XAY zo$~PwBfa@aR7iyXN9_H-AqOwibb^>HuA*R~{h#g4AHQQLYj@(h2q-|^KD?Lx&_1tf z(fRoNOAGlEOFGqAu)Ee!>eD)NJ~)Y2j{SpiL$a1vRd1&K-t-@hY}-aINnY`%@SQj5 z^}PgGEb^+9#*ZFpKC;-(H^V^3JldVNXHqp(G|U$BkMz{Or@%9m}J%aW@E zF{peqpb82RKqi>8c?6Jtj(JcLV9#0YS(9QT2K~d`EMo9E*otZJr`81pJrWpr;0Ip_Bu|0feDON7;JZ6ZOSxvz`z=LU={wm|yyj022a757 z^V0?63_lTTyjhQQ;2I*2qRbJ-HLg}bASQFIHc59-#^(rs&4@!mVOYzqZiZ~n;s<~H zj)RA51YL%Rg5kY3ra%cs4fe8?zJ!|X{sj?>={twgDSEFXlGl7+|9ZJZ-MF<^BkdXFCPqW zR{lZz91>zQSbGM*hc}+zr=e3rTQkEOmVy?BgLud8ZQalFX92$Zt}NDA6&f{=6LFr~ z1OjOxB}Cm_15ceETPP2ZVISo#tez$}JfIYAp>u6*%$1EDggtg1Q>PRo8Scchp=Z)* z5*SE&mwcgvPu{2(j@2{X46(8`&NW78DG%I)ljs@Fzc}14i&Wx2<+c+CmlU;vb(Hv- z0tV!w)=e#2X^k6}36)52ZFX8>?NAX^%+u=nYHK2;m<%BKdqC*?k-oc$V(Q zA^tWha4*AM2j50M%)Hh!C8czE;?uM}X?dXEQS<#fTxbbp=hFK)&N@$Ol6k}FH_umt zzJ%i&tv?m^a6s4;53@F0WUN3d*P{Fj{JVCEL7{es@I-k3eJ88WK^J#eOuK*`d#g2k zLp6o$z&_schgvVYe11;`627xy25Sn1xiX4y!o{Y|GtN6aoGb=m`P`gC&npOlXK%8S zcAE4EwS7u$h_p$8QmvniK$6}XT{&l=*41>r&LFAL(KhoEAnd9C+9ACO{AFya%NSNp zN(!s=F0_nADL6~q#Ys+Y)0{6M@k~i-ve(ul(7?@zS)*zDHWSYI4SC~34nwQ5A{Hd% zyC7GIt(unKI%*)WXeidEjfJ@BK2<3@xKqU#?3tbFTxz**U2Jp2^*Ls@eOGb{fGn~e&CLT`A2Ma zN?v)wJ%WBqg?xN?l`(b0?>^(Ft|d{CA1DJ<fqo@V{3wl*gma- z0#x6$jta@@_Hz+Ar37uOPSMPBL8jsPj#kj$%ZVOMV};;OS9D`gz>Vbl*>9~?XjE<} z_$~#CEARw!Q70Y);BDjk7<+b&!3e|K*z4x(86gy)oNx8~TE6TWx$015%wfoxS^@`7 zRKML`D=QZx*GYK`)m$XcCKVvbH~t<@vtUb5DWS{WwTPkB7VJ~gRm^%-x#O3z{%L3N zPk4Ug2q+q3HAAX`6EzYR>fMBA@rS<$l)pAr(}{rI*>=a6zQaFWF?gUw;YRj&(Tsxisk{guEF6%n@1x6S5Wh=-YZtEw*~y zuPV6qvi}O!XEDgsJL84k**afb!v$N9&IT!3*qHYc`+DzougmEw4M?fxu0$P&>2^*y z{(PuPhT>`NeQJTCFCF_%-({&Zs8aH z+fHn%%>MXAYD!DVBk?$e>DC%kl&&UlFw2x})Tc#3h`HO7Jsm+Lj$Tgs9>iO@NVCIN z^~a~v0D~|=m?6z`ra|9yEd|rubYIR_N(T$IF3uM2fA+K@m1?=&-ugeE1t02NYis)( zX#ois{qZHAA|+rx65G6VJm3-IaX0VQAJOvkk+(^yA!4x5OOaJSC55-|0U|2VczFlz zxWo0>7xWiHqI!Qpx2TKb>~2LQr#Wl!=%sqHjCJ0p@Warn3Y*aZ^OfM6-( zOCdF&2rQceFD%Eq>^RIiFg&96r9>IGKI8xCD-{)shJM@niTx92Z$%c3c@eUeUuIfu zPYd^m+;QKjj1-RYDYvd~xr9lr&;Z=cO#+7Z%j>9Mt%V}KfT@aGi_P0zqJO1lKacY) zc_Bioda@_`Jd~yJ3nfo*p`#T6g?RNS$^|?g5(wmA@<_5$BENCmlWnB+*0l7Mz``vH zvE|7mS<6k8OljNq(F6HKB5RLWVfGIfDQPgH4E~HvLv`Ai<$q~GaWm97q&G8wWy?BS z*hws+JV7x6NRG_d)?PGilpT^gugD-}|0TNtg^ZMz*a1Lwt+pl_vGID0AknMPa1~T|w5wYua zK~Cf?S!JsvDwcjX`HK!bthwvFkolvCZN2d3Do7AV2uXXr8mRlCT_R9W zp*}Y-J@k$!%@_#FFAC91D!+G&y2WWycsN^1)O%5a-J4Nai^poag*+QDPvlAj6m!Gg zvYQ6HNz^l3Ri#%7qm{JR;S@g8$?8h>79(Q=FZouNCi~57421Ucox(s|qv7VjA%x z$+CO%z6RyBJt^ROjb?p*miPmqvHM6y3wUWb@%3!~^7le4*CBUh+<<0jyX zp*2F-P@j;C1f>r~(P-d1K%pI3_QHX;w%5PDz`fET88+lv_Ml+SY_wU2%(2pa7W~0F z`KUdDR+3>bIg*a^j-(Szlm{6>3n=}vSgbG2`&)-eV?7*I=TSP6Z^RfzWh<@ciEU^E zYTx$~BCh>HpQ_Z*e{92bzla=)D;&wP0j1Vkum*6tz7dM25y{!(o?Eov&6+lvJh4uB zskZMQ5ON%ei-qo_h$O#|q$(v>5<8rliXn7Cm}yf!Man1)$|mDin?c3A*D1Lq351f> zu;xwlfPpfgrif%`n^j)z%+<%KLJ+%Kg=xbmdhU<;-1HwIIL!3OsGEC2x+peK>uxBi zsGLqc$Y1R~Gsw8c&x>Wsh|yJH@>~_YIy>1~|Ffwo7dlPJLq}jp&0j?*UN#7M*uDcm zdtED=e75b5+zTK>2eOFYM5JZ~T`(h%N)D$Hj4TfE zegLsP>AL!cFNjmv$kOxOh6t9?YJ%CGE!Mc*6csIHo?d&;q-R$E6uZ~Pd5~qcSiIO_Ji|7 z%WD#X3zsnUn3qkKFGP}u(gbQSq?@l4abgTl=nINk8BexNf?iq}Mww~} zv~DB+0DaCKXW%dySTl6f<>q2RNT7%=6ZAFF#=J-#pJ%h9?7_ogzYxOGVqvS1r03Ss z0Or(AVc7KmQ3R;@SiFx<=$ffelZr2(cmbWxFIG~jn*&Ty@{yTrc5S1KDXgHFUp=6w zmDl=7I1v;}8M6_O;Y6{>{ypt}=oNBZof7=WUEX{6wG6fe%Q2W5^^ocXv3KRPe@%#L zkGS=;67#bP+@M}sW?q>&(i6q>a3D|iIHavD9ee75K`_7kW?XdFqi0ADz6GgwC72fe zZsWtJbr@@fcHREd;^+06!K3Z{+G7@$pR7^o_UyHt;F3)jUm2>ul+d|9!OrC7j9 zcUm~L6cW%|BV;F;&py3>6kUFvUX&&M>i#mbn^T-M<}86pu2Btg`AC!?$)hPCwuKYtM)aoy#pEa0m=DMgX#K=z?()=iU+T2fob?;%HpW-L1spc z#ol|9GB>}jvv@AfyVDe}R>lZ+>=R~#OCo>==?JEbb3q1b$CAbZ6ZsZ}V~g~N5wHOl zrF<~iL^HrMsEd9)j2pQ>$|3pycIVGz^3<52U7X{mPFGq$N-;$#@1IUvy3>ghor>H{>46B63s)!bzMJ9*1^BdJykUrix9h`PEs7jxY zye)XX3e}j(`slpMSNfIfV}4QgozYL*Zy3k!{)@}c3AZ>UF1KEOgV3Q95HVn2z7X|l zcOWp0joOmk9s2QO!F3(e;8F!U+75~)nwte~tDHR86q}$01;)cc&BsIlav&Z^IKtIi z9FLXtnyLUkUIKkXHy(}OIvzsB!@Ly5Rh~<^C+sBpeTrqs4B>2yOkYR^T7}67X^P$6 zY?*Q$5&oS01Y^qR1z=)iYeM^4-PtWlF(Ea+M#4d#oxGakZ`1S={?GiQ_eT?Ps{zrk4xf%Hv*?3e z1EpSJ%?88DxLf5{d)K#k*h1X@@PuAYtaSMtDQJ3JyN{>($dHh8wazKWLo`bgAL?&Q zG*3)S=C|JL!LegHyt)!BOL98M!B<2ydb?)C6a(fZT46naye9ukK*-^WRc99u1BNw1 z<)dR4=+h-Fv|stXq8m4WZtI3uQCU{eTUR-ePDQj}h0515kkn1v&N`i=P7}jrrtlw> zxjE&2A=n3Hc-b)ko%v4VB0{`Fwu;1{KPdFM8a!`hj`RPMs^yp$&tO-$L<=7E6c9(lJ0Z11%%S|K% z&BxK|wQ#oA8aPnT(qOK*k&;-2uGjs$dDLK&mR4k{_MdA8IlnZ(Th{BHOgHVc*;vL0 zi#^Z?QB`!6LLhy!`RnwS;EN-^bD;5WeC@z#8AEBJt^A?S$H=iQaJ&*6r-&ob%` zzEZ+_ps6Jqm@8X9Hj*j7I0Koi$lac*FVB*+E0ei699=aVTLcr9F;$h{(l2jrEtVI5 z`)kTC;n)44meTecub1hHpHQQ+M`hY|&V4HVmp!a3Y?9mOYUdbvF|AxjS&{-p) zReAx6VTwNL(-PZ13^!;lU_M~<-JY>&hm&k_3n?;Jp zo1M?Zo8naJ%%;h8iGyb~$2+FJMtTBM!WC^4Zi|ZdvJ4Qrq-4h%9cEae;EU0E8?&Dx z+m^RaFFhdQBB2y^y6!oxcb*!p>lv+`mZG|UfOcDKkW|sYI|&)@cCwl1Oe?>xk@tYB zo%OFj@FOw9gk#Vo?Uxcp^?yOwpKX=ca|pZ-&h=UAf2nVL6ZQ@;Iglaie-@P`>=)(u zy7Vgx{*PsY!zHi*g5+&*A23DMDpKKBZSaUu=PZOxqz|72^kd6hSN0kb%q|1-<9INK z+rAggD*dII&1d)hni*4Yb_V0%_gM@ogN|YZ>~6Nxk~QN-KP11LD|qyUa?R}VKZ|K3 zAcX=rhvEGWEo8NqixPMeEKg7G@^-0Hj=!cwJ-h-D;lEAA5))Z zFMGR~VdSq?&9*bOP179Q8F^%bkGVyuR=bk;8vIUgG+2(lZSW*Xj8k^r1e{=5uysa>h|!?SKb8P~YJJMD zzM`^DmefNmY~tVT=OdCG;$J5;sRf>5za?6yrNl)3%PoGr2=~3wc$d{*Z4acFf0Ygi z9BrTm>m1C-w6nwp&HOG`ol6c2t`=gu&Jljko-|dm7-d{1Ix%v4p{8&f`7LGpS4p(v z($}Oc(ZCRi7io|T4a$-8#Sc1%c2R*&bG3uJi*1YX9CF=+ktXzIu!xM7GsrLr)xO`R z7er|bv?>#Rg_f?ZWo=jS`j(;BJQwS2B`=|Jj)O)H|b05=>+G>yZuB~%EMib z$X%|mBiP@B-L~wrQfGj1kn0upRUczHVem&FRA+>n-uYj^>K9P*K*K-*g>qkv>>Hd8 zc1$>t--9X#;ra+Z^dG2!Kr)j#ez+V2*Nl9C9X}7Y2{k=C-ueAT?CWlcF1z!%dcE{lggg&o-FA?x@w9J9r5kou{V221 za)$d#Uw=uPQFMyeNFn&xS0%!IcaT(^GnYR><8-^)8TX#0n4_V>@bAC9`CkMoQ7908 zZ#gG%a9YdM!#(J8p(qsvn`@ozkyLA3?N>s`AKG+T6TFPJfxBDVF1;|32M2`|*_ZqH z@A<0@s@U^*CS9`JfTPn}c3Q&kQ~IcUo_#|J%h_dK{D{C?d?E@X`OQvt--y@RRE9jR~<`ehm*bs~?VZo6v ziUqBHLNz?jR|&*@bDP=y85TaQIPjuA(Y`yO8;A3sDbTA2T?R3Znt^xV z)VAMui=V+Og)}**oSw{jmk?!a#q1)&aVx{MEky-9|E-ONVYHx5K2j0a;l9SV9iBrU zl~#eN0++5DAVe^7eSz{38NH47@s!m$EhT#HLe2@mGF|!Jti+8(KsYJxT3?vu|0rB zV=AdU)^gUaLRm&0#9BuDSM23$^ucyyqQPV6zZ$hziBW@+aa_`!EKB0+TKxF)1~1(E z2Kzph*r)l#Ql}Y{7gW*Bq@i_xDcSDQ@7d!wVaerCVF?G8EZX;gXz{k7<3^Jop71PL zD`%GH;*zAj{03;k!ZYUL@zFmt6El!tRRAgLJnVJw7d44;ni#6%$Rb}TS&mh=3K-f0W&r_I7?zl%$JZHo? zsmbtPqh51lF#HQ;|V zWo3jwrSSBEX-S*cspdMR3`~3MHRsgcHeKMR*WSFK;<4>ui1F>1-Kc^-J|JaTFD!DB zQgC-=ilXOhQ!;GTH^$Fi;u~j1wwe|fwFJ}U24-TvDXhK&;-GGr>gWgDS4#)hZx4(ryt9& zyf=CR;SImd7z*kSXZ=GmxR*vn1&{tq6w@>bsB=w8 zya!(b_Ss0D++2Yr)wZG6^bB<=G7E_zoaX&W`XmMBIIYB& zj~F!u{}MZo;y-yL#pQhlo;|^xTqacY$Fa^J38S8>jN%39ZVTivN02(=d#j|#yn+*; zJQT|G`zLrJoUp$~m@LRs5?D$79MkrHQYBeXqa?uUYj&#k`>gTz)3piZs%hfleGknt zSDBBuX6k)eG$3yz<%gG6s8KeBpEB-7yX+0F<+W%NOKx`?mAjrCZRaW%$6(^j>Jy{? zWNj_4blt;wacc1(qAubgWi=y)$VZq6AhoTu^RK=|Szy|lXfZp`Kq~mS$i%`lIVSJ_ zOk`qJdwg2Lyl&%xl#5(*t@owBmwPNmSE5TT%w-8X;xg3xd1ji~98UtNpMk%5hlU#o z=IY2aP~lz}VM^*W(VbE$&SqNLsA?#`0Z4t-WxM31%j)k%{%3A~!RJ7*UKE%e)s7y? zlomXe?E1r;%#pbzjZz1x4|c$vw^7|9pvuMKn8zL?-cH7tt(mfS5$-+7QVoFy=8xt7 zOYBv~!K_Y0%Us6k5hGovv+VRW+HjfS1@S{LZZ(x@2r<5m3ep*z!Q3KN@z><(!$n zPMN#4Fl`oaS!9N6^eX!EUsS&Z`~wrl@Zm!7L|(2OzIZk}E`*57GDM9bswKbsFR}9K zIHrrbE}OwTNR+(4ZhMRfa|-<^5eH8+5qffJQxXfk+){2xNDBn^V-Uc^`|6-*#A3C> z=Rn#TAN;Y){Hh){EZFKHmE!ooAAx@s`}d0{O;i>LEApK0O+HQzIC`Lv4a)B{Rl=Sl z*mb(VKlQ8u>GOR82z`cX@Xcd08J=kOVp} z$r*e7+WO~AIo+)tX5hWJJ0~ltDgTSlr2MBhtX;__sayTFz)BEPN!z4OL!5}+QwSh> zecU#?QFlFf^|h0*E~?4gvqN-tb8D@pp)z>6j&6pHt-<`&VPD*0lRLIPkaOZ@h^;lU zoum^*`;w}t?Cn6(|ELe;=Zq?p%t`&<`)h2vJwCF~K$?WZ(+7!EULtQ_GDaQ(&)Z3# zbJ_7D{n8l7Nnmei;a+?3|8pt-?+GQ1XffDQ&{2}i;h_$GKovrKe*92-2S{4^kG8-M zC0aCf-STeyN1t5ojwy5t^4XSXR^@pTeZCfSfi=J@1YhxZXGmV|@A6?U5Olm-AQTX~ ziPK&5!i=(dmFluaZ4irj?6#1+XYl1D{>Qd0R|0Ni$R4tUd2$Oa12J<|xH?hZ@zY=a z-yUfXXg@s!`B?5fKo)vDY4BK(#?Kx3Sf^2JPi{k+xo-I&4Em@W_u2@O-Wi5xy;*UW z{`HNE=mkV&)~h+UZUG*^x;@z=4y5X_wd253Zg39s1QvF!RWfMY9MxsebK2XTJH5(AR(#G;192gLRwOzkB zDy*cqTS`&mP{QTtdNrQkUS2W@dr@2xN8Ga92qRng@-z1Qx7^<Wbr!p+#rT7Z=kQj>d*1*}c&mdF%0d3XD$r^7(R>ZnDZ*qdaVT zxZLI+If3qf)QXk~VhKJu-JV^e>ml$aux&9g%mZN(nwpExvG%Bql0$VJ0DQUu?;s6Yz04n-X!sCs0Ax zUsNZOFj7NaWlnv=-o-At^wBmrsne+NRaH}k<;!~eHe_vS6Vho&Q0x||t2{t8yK_xP z)1_Rt5vL>U;vySS6W^hrrxjS$}~e&p!CyqqtxnqHBPUef)I*j$-gWK>*5#=>cM5|Bbg7$UmTAU2;IjMmS?EHs6??{;@D{;;!@VB|U~fn9LDaN%GB51UNP zs6!l<8a2Fi3rM|>qz-*s!h*w`{7P}JcML!^U3-hQ&2Y{5#zL?PP^x7-`jtZ5fsI=F zR;2NJ(Z_G+TOQ)e3yK%))AbXqAM=>aA@^YH!Qgo9c0I$lY2=f3pP)Vj9|c7#<$@@J z5f7$#!D}c($%L&f;gehSM^==fc+odxl%Y@|&@rtg5c8aRb%UigCZV;o+iR0W`Ul2S zm-toXm6$HZ9ITzld`L;T_Ii+dI;A4B%)1NAgM7#MAz`JSkMHjSFvhIRRD?B^o`0_E z*H`}B0K1B>=5wzOsdZ`GFw&6tGA}$sZV$NQwAM4)X?A1q;m}s^z7=^m~qZA&r7?oeSH<`Gegx z$Gqfx=O$vQM$eV*m?4jyKQxDFOObob#1fgH3qv<-ZOF7n9TJ3JShLpjE0*f0yL9K$ z5~B1ieqO^j&OOfSLU3kWx0HuTYN_>cI9E%Umo2EWA=(@}Ua_t%GZ)WS3&-bY&h0&Z z|BO&PaAE3?&e}(H7GBn%!Nl`JcY4!h8Z%JxFwe;s%giZl(t>%GeGghYs!_ zv`4Rq3>jDaX|`un9<@a;_>Kc{=_YC8Ww{h@5f?FNc8~}rdTN(BoZZcFs0DUX=jc6m&EhL0 zDI&;ZrBXvTY8w(?s8V@906y_@HZ+FiZf%AFd}+d#dE4QMyCBm@1?ti|tniI!e3Cq7 zqTs%1TV5+#+V?0@_w{{BF7im{5t2J`ZzKao*Hx|0DQUA0H~;+T3X!NR{l^e$n?LW` zGpyN`4*^SY6iVgMus3`DuWddL#X;(&Y5d)MQ+!>Sd7wf={&c_0{$qon*gpY4G{H%S zbliLoI-o*o!odx+^lc3&y?3n+JrL>+P4<}Rg}ziPW0csSt30&#hYGV^)$Vk?71<*Q z+Ci_4Sr-#%Duo&zDbN~-n5Z|lcP;g%;mdZoO81W8wUw-cp3v8=56sCz?HlW_Gp*(X z19Tu~9#OAhVhZLX8Mx2H8MUQEpVZYohd! zZP{5x3(LtFOg#lqR-Rv3onGUQ_nmaxXnToBCImnAp6#g?Lp&Q&{K=DGDQfx$#~Hh$JSRcKH)gYyo)LY2P+L3#G@Qq;oSK%EUQ`f0Gx#Kz|9OrMq4-bTgF zPJ*VwK8{DS?HVvH{EpAJcR)$xQgqdvb{1b%uM)`crQ<3z%zi^0FOOto>x!ARsCmu6 zPzHRL-MQ?BN&;l&*R(#*la_bWB6ope<=!v7{Yh;)-VV7*i>=YYFCnBhzTrp&K3amrWc2%-K?Q@f7XP1XR)&W_E z02sHKKMxXIit5CkjL;>^b1;8uFf0q~I!>DrZkW)Q!m%(d{dp%!#R8pl%Bf3mT^Hb9 z6b(1;8dOT*XMas=cv+@(sY3L3v#NEBa~G7W8>c2WY2WpU$-Gu)8Mow!0Wr}8o+|HB z@4R!2%0;)yUb#CNW*FeGxa~_(uZb$h9D%RwglW98-Z!eKlZ4k_t#65P$6=`1~Wo{!w zZY-bNhA@0ogdvUlW^KG{5=)pivk%|x0;#Nb)ddAFCs(oByh?o-6^Uy8X?{7ulP)0t za)Dy5ZSP~~nSg3oa|4ZAODc~uo4GCo0uG85#n^YYzy-Ym^#Y}0bOS%r(+oi`3ZHQO z@4gN~tOYMkKsax+E4)SeG0jh=t$&8YDs>kXPqg8gTquU76QP5!gc zCcINqG0|LCav@e3@iK;urN>c&NPn+cK^oX08g zJlc1D_xC>Y)4YGk>uw9?0JDHU^d@nTbF$#`EccUN1D+;qAa&wcmWveC_P zh=o8xpIdQ;wDVBuxhAXYm*hQl7^vCx%>yqG%pNo~w4&utfCYVFJ9i#sQ!GC{I33iy5qA|pPN2^PNttt z3C1k`T<@IUm^08e4MEcWQa!56`S(5nTkl@zw;y&H65p^9%HZnqu{#T|ZskW zk`%I=n?Jm~FpTdV-=3i2ssNakfpvddZQbn)G7OzhyUi)j8tx$0dh<;1DM_vX|PVp3?TR^wmF=Gmg&s4oi^Qr73)uv6%+JLl_IA+QihvA>9 zbsVp1>QT(YPnruR!+PuA<4aYDh8(f82`bn2%Ok9W!KohqW;{;k3M;=*&}KpZ)NZ>6 zXL%n>d86r^)~3aS339TsPc<%CQ5Lrfp4@RD>wnXLQr!`~dXHRH`fzbHGdI`S%&6Gd zQ}COrQrzDgVoG!HvFP{n9OB*w=XTc&=`9ik>|&zDpMwlgp6$Wh0AKGv^WjE!v!qq| z`$7yApL5LD8P!<4W)z-RvNA}Wapo1dsaiGl@zI5pLt47WuCe`!^*hTYA&;MYzeBK| zfJ8BLfYBRQkrXdOn%R#>W>20?;QmP9#EaF@6iNA+ExP8~%?quc_HC#fCU~Ux=MNi? zafss`6jOD`5<{0yH?v<~s6#R@`}{9CNkd=H^9@|dN^t$PNlf3G96Y+QLahK{SoEJ| z1;NXhrmDxS_yy5_Mcj^~Xh6Wu_ZJl>U5uVjMZ6nN-a&;}d*`PKpzY)6?e~m0Oh3yd zagu*7`x6O^%iUd^#B?wz*MHV2-9(8aYzI3V5y{udm(AWWPBu!$vHs{~lg}EvF;HB) zd3!%{%TBj4S)oxBO_>%lxwgu@vK333B^HsBNOSAwyw&v#BO9JBeUv6mR$3>&rXh~` zm?PA4-Yw0%?AoQZc^%L@k`+oP}3-YR6M{ns2XfyImY0g2e9*LSwpC*IXIZ;P?T zF|hG=@Ykm!$Q`$<5$2P~*AnWg?Sp~P99x=cr-$Mrg$7f3rpgu4Et45O>9LJ^ z@)uky?VV>5!s)bDI%k6okDuN5H!x_sqa)wRyHgG2R_qH$`E@Ygq2z-8^VVO=?dU<5 z(+zqMl3*Fow=w#(NF_$**%r-2TjQwoR~>Af(##T;@$Apb$u~Mt3fJcLtANmF4REnJ zZC07vj2>*S*agro3Pl*Q()?<^X7)6xJ=O@C$7X>uiHtBht=bBw!@{0$9s|@p ziUQT|lg}{ylc?nxdav>0gHFC8z0z3Uc@gL8e-AIpAZ2&?k&G!oO$u=@59YLPG^$wt z^X3db=P(yH*6^3JAEhD|=X9s|D~y@}03PVFk7 zVx$yAUu4qyUY2F6ZNR?S{))ODYqB)1J&He_^~h)wV!pv0?2N>ir@#uW_fskTlwseP z8I+O#gbv0iHin-Om0vFQo%9+H!F0J?IQ#{DugdSoLurk%FyK`^yy72uuiVK7yeN}E z#wf%3oWD~Otu^od^NXI2<)0HOkEFSa^UR}+>u`Vm1;%~HH5tXb&@h<0C!b}S z1O}^Ui~Y2jsz&i%T~DM*HTJOGRRMpm!u8PI8AD^OI&?9JiZH5HAjlu~#*;-y;mH;U z`)bdPt1IF5!5znUx`L4VGvhX689sk@#WT2UXan>=4Qxo=`s-1ET>xU_J6lNAlW$n} zhA?rxjkBBKY?djVBDDy#204BoTItfQycFutc`GkQ-mB)y7iPnpE2Zz9-^R$vM^#u-HiTCXh5*Z(T3je|-2YcSPH zr-A{QX9&*J0ELqg8HqR$4io01yjHU@ap_Q9I{#vI$m9)$zb^pvpRun;y=L)@tm+H2 zQ?-AFqN&3ewtmk&icnpcC}pLKT4WL)2?22cBe9X=M3Pax25` z2@p*Fib;>yN}1( zlZZ*=|A#04j}fGN=%7tNNB+eY5i9HYbJ)Wzwt7^C|7OwQtnhS||H5L&FP&k#w5uk&8bIwWD3#zhigsK%C?6AgGLd>MZhUP`=5Wx+Jm zSeQM+>zPaUTSB6FgUcbjxWVhrvD5XpgOl_GSF`D-L&59DLbbChx-V+&Su5kc|9f*% zosuTuOfv5(4cYn|HLCo^;JHl^_lz$%IMQ(*Iv}itYfOiee(J?JV7s~j?#XpT;C^we-;p@mHXZM?_5cENvd?rYxiq5797|5023hM4ofBwrD##%67Hw|Gnx-tGrr z#4`2-w%WrW!~2M2hk>dJ;&eeA*bl#_L1`s_2+R5{^<_x?T7Fvmb6HnlJ&0=Y*L;7n zRa1y*^~-zi(r@`{0mg|t8DxT?mKk79gS?eyJDfmc=JyymmDcx=#1g1weOJYc^nDym zhj^mhdznX(8nVA%F;5n!3DQaGArSF$2~P@o0m4yqKj%Kudex_x z3c=+Xzj~pSsrr+r(&2_O^jStB2|7G{>uL^(nB5}d_>$V@|7d<)K*?V#GWNx*4(et2 zn+4edZ0bhlQ46f)NFBaLA|q`P{Bh{BJ@fste&$4XpuUi}wlqn%PjIx-HSu^~3@GPk8{u3KhBh1Xbtj z=?hjsW@77|3<~q-9?pgfBzr-61Lu$Wl057pypA*Y)P4&u^jBr5%e=p?)-xQ+VSEAc zkB6i2f{FKb5<2)c8K$PPZh#YkXn8j^dexFV)@>jbrF3FO%yTmen0PYTmAj>Eb&Yob zKy&-Z6{NYyt@EX0^-nlf z2afF$C_YZUJHe9-8-z7z^%48;nGoJ5RPQ&~neqmXu=Md|(--BRpl(k~egCU=?H0$wIO;JXX6L z*19#OQPV#AncfRuD4d5dxigdxX^B)wUYw`A+S{#;?wZNyXD|RoFoXag*HG*;a$nR&XsM|l>{LVxwHs+IXJ}q$mpJ1Ki@t0H zXEgK;Ier!FVZ`URUs-prsrXWpd+cq3Tu^5#UmL9Q_47J*BC7LvDZ-?K2-&;ERoTHO zFK7*G$ar;=4jH4k8dSE_(rE^-LF(p3V~~zfR#;-i83sDFc%$)!j)L35l6}+9hQiMWdem! zxMKN`XuOffvas7CU@)InZt)B7q5eBU&XddUpGYN60u{_Cy6=k-P+tB5Kjb@0%R7$K zKV?pSK2HxoLS7P|3YIO7EYchc&M;OQJrg%L*wlxXz#m@tvxHOuAWB7p%l!-=pI%kH zm&QzcfE&VnJCY^yUzA}L|N4Ak^jzXh*ZV~I95yBK z{PKf>Jh#c%^FyoW-zMg|QgR9NGEDu>4%%}wcqWOWo-2=vNB>7>riFU$&)AdPVZBmi zNo<9+d^gcy8dKs%XR)%_CF3z~H~5Kz7`+)(2!yQU63%3Fm11H=Hc7p%-$XoAVU>e7}?UbT74H?Kp^#x|O z6jB1ouFqrcoebDH5-0?)L84koWsa0wUc@d*=2heQ--kVOcyY6CMEK?vYMCw-hQXG^ z)LTrdUUG3>BgO!)b`%!QShIx z$EKqc%w6qlwe2K*o|(7l+zR~3`0j{GF@|PpSmYn?l@7Uzy01%Jftj49B@V#}qXcRD z6-fB!HbMHQprCR%LlVkewNN{5JLi$*(#DCTiej5`3TV_06|x_^-h}&kK9o3AHAPVr z?0=g*@^(njM+e@}zF7CyF>^*l>}M1$@NILW1LaC(E~}pdZ6L+oWDm%q zOD1P}xfOW^UYUtiAWUXI-#?94xJcJ)DoSI_{xDEZh2_@6#Mu+Pu>Nq&+Ai7DaFE(_ zGv17ipi=X+&k3cWd=y!s@W&znt~lytEFROVFYL%Ga#?sR=ch0R#7NvZ4we)5fvGBg;~xE*vWU6tl`Q#F>e@eYv@}P&LmJ3fO5%AW5!gSrD|>GmY*hT z+7F+f+ddl?LSwborOUqh+JR<@ygCDQtO5t#&}1x_>|ba%Ow}TVoaB}Eb!-y^(+QF5 zFA7X2FXWSpDlPp>V8qBX_E~epmYVHYg&)+Nf;Egq>?_=-~>xr9}tSb?90*$Elw$WRlJ(Ubnm>(sgu0 zJib5P>qk@~OV>t)jkf2%DBtO3Ei=U5pIBlkv(Rg~=aDt4YB*A3D5WCz zIM)-(Gw~|V%vUjLYjfb6l9S5K4~~+~eSCECj3J`8!;~f4j)e%Oq6 zLuVd*YK)WjKxDoDt__r0uE=CeQylmDhZ+OsF!sqe#`2|F6R!q`mc4$(8 zZqRK2CCntnx;C7u1*o61!+%yN(gC(2GTndvx1rZ4aD>Xx5%*++Vz<5*4!*w$XBZNR zmxCv4FM3rT2X%b_{4tJm`7W@qySFVlMPf9iqQ$5TKt~sF@EDhuYd31&TYmuNFOl^G z??1sw_|?EsUw1N3K$Qs706K5{1~z0<<53amwX%|t@PY4*|Ma8(}xi;8M(&haXK9wW7N|(JvCL(MZ z_)TNXEN5eSV5hfW)ta3phDw(KI0)PZbpYoUANS^`>E!NGsTTPj?xfArRxi=<#X#nC z6v;e#r(#lTStnAtALAl?rJ_TS?8ji#CzbR6hG23sd*GDA`rybeur=meFQ*|KOlw(U z+fE)1uR!2CoSxORU?vzJ&@6)J4j+m)JSEfW6b`gFG;tYQIM<(9s+Oa^>&o3mnJdK` z%*$#`Z`U}<=f2YTfrZ`o*Ys=;C%D&@jLlD!ki2LJUIxl|Al;E^b&zMKK3LnL_kuxF zv0Aj|%z%Nu@cKiSi9@JTCC3>_W^0qZGVo|)3NVioJ?@u}nAhFEKsvUW5 z-Hulj>UWxW*RAq6uWE7ioEd1(r&Ao`C2p)IvD6zirzSoRW>8ZYD#eYQkeoAqVBxsW#bu9fyv?dSh}g_P$5 z^d`!k=agj+q$<5}UPMfU@lIZn9n;Xe9!=UWIfpFJP*1+KAX<($w=)f^)waAU{O5PA z{CbX(<7e!6Wp{*WjXX?bY}9X}aET!jH;SOxxKC#M<_Co)dzL@B-tr|iX{|kQ&DME@ zu71_oSAE4N@?wAnwbJMo$UU2TFHW7l5M;(y@cHelulcPH;LpYj z?!8Xz?ieFRng*Xv7O7o$4354s?JLrW7yTx9ZBA{<$3?Y3@6K1mi!DzzN-k4cgL21o z!&!ez+824wVL0_?JyM;z>T{yB`{7X3cnGR-k2~!6Diswu0aAW-9#V|rL3Y04?kiU$ z*a}iZ(}W)=Es;bLkRL7HsH{cfV%FNL2>;MDTA`zylB)o3pFA)V7s?Uzr$UZjHM5!L zV2|atX*am6v6GGYbf$mi*X_G6S!+^e_{|NybD61`VaTk_3I^q~a7DB!YAxc@)4a}| z-}~Dhh2BhL3@r5I;RC;t3T-od_C}|xoqd$Jouytw?0uxtP&}tG#2b}HLM@&)pnB*) zX}A{AA9Zq{twd=dv~ZK=RTUw(hs02Ud@DT_1a;==uHqN_=42b8)o!RK14~NnWXe@V z6!bxsL96W@B7 zdKq==n3rVT9Y}Tumu^BXI4|~G%c!&xbyn5qr(*trr=)IYu9S{9l~;02S~Hn{5pgJe zmC#}o9N3TgbP+Ic+y1VDBh9zWba< zQ4T&)f@Kq|f_i8Nq$saTzOWG+f@pI$u(Y>$&2`J8M zXs>a%Gsh-(2oc)^d942qvsS!_%R?Je<>5sDlG^cI<(myk*;lqUx@sf9VD&ntdK;(4 zadjW3cD{}GrsY*FR-Ec1;8DJQW2yegg&6$aR307UobZI7P;TmHTd&0456?;zPInS! z+ZQ}_Y>^on7q;0gC9YR>*84Dzep&W%cXm1z(xo$M`uEc&s10b+?VGdP&nGw<6({>1 zY}C+-zq{?oTjOSDzrX-r4)NLa2J&#&Cej?a+d!b18w7ttJLv5+s5I@YV=`l7-%L!ZKU`SFx3UD4x=Mr^Es|&k4`qq3>D$F6a z;P@el1dr2O_~=mmiQkczF|>~!0fM~t7FHYOZ~5L`hEMUHw5EKOKdhXdpwB2=!GHPU zeS&TeE6hAPiu1sSaiNUsn66KLkTJ%0S{?#_i=X{+wacpZ;Wggrd+Wn_`Dhgtxe$Gm zt_ISe5d*9_EfyJDkT`VR9V6SVx+`!a{!=3*=*jnJ3s|?{P>d=zE{C|+Il?P{SDO0G zF^v`>9ZW;)6B%5Lb*Z=lJ~EUIeX&&OmMb$FOCp4YLE-_M@y$MVXJyikEI7=fI^o>E z?~suL$-cDA8P?_>{+!;{4_@}3{n-N_NrqvR0&F=|}{ z-TjgWe))T&FRtem6jS@jgHcCAN#N)C5X*&Wz>Y@#=MAt&&{Lt5MB^?7G**ioptmRl z7=E)w$JARL|6a)f#YqB>GqR^ zsAs#Y1;sU%!R|GH$BsN~H}wxQF8Z)d)G)24o~~H|3BP%OUk{C0=lp4PBcaJ(?#sU1 zrP{5OJ-u3Eq&0*!^;x{qP&?$iIbitobS5EYt@iJ%EBRO3jh3xirfP8qj6&3&_23T@ zo=UP6ew>w_1Vr}Z0xkc;-YuJdOr+f;Ou5g{9ZjIKo#%&3oL@AoRn*N`yt=G*a-hx4 zdo%8o1c-xS66x#h`U0@JFWBn*x!kK$iSsU}TL{_qQ&w7GD7~rFQV`XOdAsVdKp}lH zF5cX8+Uf5>^%-CT?U|$;3Vq35&UCQu&V2PFA#IfjnR2Q;7M(~_|C86~PVru1P0>vQ zOn7mIm}P9$ox_%MV!0EM0UF3nSU{YJk!KTSB(rF zS5pA+QXS^H=KEd6r5p@zR- zmO#QL)jhKnJq;z>90QP(6`tk7*kzkLyX&4yisRh+G7k9K^pM%6AN3No8Q!GHos=-E zlY+Gs)6*JHfg%DFppKTX`3uqhy$u!2r9Q8_*DsJYc(wjRf}T96k9MX>nC_kDMUM#$ z|LTq6@jTIMZU9_&39r4~98yP0_hIbv7{yelGmmi~V95dG8t9!FTXKkv)ZCr)4cuqF z>?{ZD=NN?`zmwnLIgxw6$fWSYmpJefAfHVtcPM6=ULV~*oi2AubE{d$)s06v3>{dP z(DXf62|(G+M#=||M&h{}Od~!qtub4JV&#GA_YNd`J)%9DX+`nM>TL4ePfO|EfMjbD zNLFfo+v*g6j|Aa2H+Y-v>B@ zG}PKS51Tu{b;>UqO(IJ+-GVT2QE0FBl%jnoPqm4$LCww==IXxHVxC8amtNtW9*LLL9e>c~BIjr#h_|zt8CV?V+tJTi;q_bx2 z*h6SsJUS@&prb)|C$n{WhFPJ z`JzY{#w7Cc4XP%It0FCD%nd+g0tb_9R+6=f?23)*P0LdNcHgOJcNdYqu;b`*(*TBy zK00Zb)X}*A^QjQp0asOS>JRdIA6|2BDpt;b{({2Wq<=xVY>)T~dBl)#))zBLOBS<; zY_J}t-SGxV9hm(j1UfEulJo$xL!w&&fK7U|9D>@(Uw>We-4?l2T<6u3RWFd(QzB#; zK}?7gz*lJnAc}D9Urg&deCNBR&`UqpQ&AbLtG!3-xN^Q+7Y**H4BfMoTn6_M&y@pS z?l;+kRI*0+Lt#TPf&RpZ2P{zI^|^}C;wR&Ry9;b!4lBBCPXSySo4iEi1Q3;1p&S4y zL%fuiwrP1#pjRtqeNDetGoCGbCiG|!-@84>6zBX7R5ho+^H8nJNw2F#5F-h3!>9lA zVsg&7fdUhl_ryyCdxncK@B?p7h?Pv1ryEaH0ih8CqDY?ETk0zZAs21d*H)dbo--b% z>!r!&;-*e{kTc{eh%!ebXX6k+<;);>+slRm%hBuQnSM^Qp2^ufCx zcLih@c6DSec!?v0R+1mG>&`Y_v%oYF2S;#|%z+?@YWTKDcnj9~ z{&dHIq3`$2EU4MW%n4G@U&?lC-Gm}P9H(sMriNbD@uR2>wY}26;YWtIm)~D%M;P`w zpxf6gw{FeQ16d!?*+sVr3i~b*<6s5zDM;+C+`h20n*7Z{mO17T2*tl3W-YWXlOdu! z<&;2CGHoEbaGuzTvSuwGzq}~TTR7R@^groRZoNIl^vC0W0D57y3{vuf#m@avk>U)t z%PsWyc`AWI3K1b~rAp4Bb9Wt{4dtyX*@k5f@tC+nKSrX4K1R3@^#nz;<>F&L@frug zS#34g)_2y&X>n?yG$wUE^=>7WiXvg5-7TRkDoFXnJ4w!w$KbH-9NNLLphc=i=}U>} z+Q5V#-;3b%&l?UXatOX5J{H)>PYJQO+V%`VOZ3*6z_U^7OrBFb696w;8Xu%K+_eSX zK+j|KG)NO$vzJFlN-q}(dTeUQ+Spiu5{sB)a(=)eppmLcoxo=-kD2U@@V=$WN`P}> zR1W7W{Qf1e=-z+H%n?F5jd}F)_m3ZhwxLC>ZZYJaOB7C3=F92`~VctnV|}RdbbO;e77$vmBr+TF~A?{@oQ;d zaz*Hpz_(E72OTH)FN-ioppFAz2t#glM~bZnmKk};mN0=RjV92?Tv05+7geM)6oYBGPqdq~? z`?#VZXd zQ&B=MP!WT@*Q)_xv>29Xc)2X4FMS)W0KZ|zIJY!H<-0jXi|=l!DX zSSnqpg~jgYepzcmf5rYyx<`_?qN0yfddS-XFA5lgFOdn^3JGTqZWVR)h^qrF(mPMrq*reajyxhcaMg?8hmQ=jI{-+Yo& zbc5}4-T?16-XX|d)Hv;5hAXT*Gok}#=XwerI62PHJRU6uekQ%mv!JfRnx?d>Xu=h~ zD;(gO^i2AujxzSX@d~MyfbR2!f$zFmG3^r01`U(!sg&ntO5eN&YHre?W`(oC?oQ{B zucQ{M>UsE|FeE2!2f|!lvMc2)$ju&My`WkPT3bwa6ov_e_Bkj=586z58@`~x3du8^H z`L}=Nr-H9)_4wBS1w&z;-BmJZd?G{+zLS*r)joj98K>e9tC)P*0IbI@WZoVnk8hGG z0KGklocwkS&v=1@A@r!43#uu|~YJ-8dbn#=nvs^1_s zy63^2Wk%0(fCTlJq`%>v$KUx&>7{0>>LsZ&=_%@Ci=Iwx3ZW!jT%W;5!9;3rJOx)2 zxkt)f&Aro_iz+phlJ~o=y;RCR@=`YFKV;Hwvs%~{9EIxyvLS5ylohJB+iPUUIDVK5Grk^fXR9(c}TfXCVLM5r#s0r6V0mg&EjrDxY z1Vxjk_9HZlw0;QP}fXf*EA>DQ#fjHSvwZT0t; zHSh;g_7rmI*6?~3KG`CbJjDhHlC&6KGpcFvDP#@88(h6*X9Qi6=~bMQ8MHPgd4VH? z)uZ1*lxB3lwB6~S-=b5HBEz~4O76TMau$~r__wgwOiESqrtfq#SD2N13^u&K`pmd9 z(8!XKegr^HoG?Z@lVw;WsBvNb?0=)#a{Jbc+gUo5m0*2~ref{_eL9c)G~6i9gF+LU zTxXf>y<_JX|0Si=cci)+HZo8}-umezcG#z$L{xS<}F4$Rg%_W!WnLE z1&5+`Py6fUBAw+v4z&`QTxDXmPbY!C6|snIvQo*6zl` zS}!`)-)oMC`GEY`W;5hGu#MbD1Y{lN!&19+@gIA9B|mu31HQ$(?ozs0d)EU7DE0Ft^08!aZnXkap!8sw=N&oxGqE4 z(#KL=&o8TqLN)Y`3hifE@C!NIf7T|Rns(8EC|O9&l%}ZU!B0Q@wp&Ab-v3cgdp z%m*R`-dn4PrPsdK)qwOW5X(H@1hKs3?e2skjxqdsm?)aT;iy|Y@ui_yzxozI1ad35 zR$nnGj_uIdXw4$3#gooO)gvA@;9GB?U;=SV_Ud?h%2iG@{QJCsID*@K|546vwUz0b za5u6xLA@VMw`iZVI7YDDZOL*3Ka4#V{)d7iw2@uAQ$+9OybH|g0d?i64VKF|O7#F7 z@3~?Gkt<0D!E5?mX{a#6godr{1X8J%OmT>uC(ua7Ugb#K-I)(O8mMwQf6sUGgYQC` zR=1BJciK`Kq=JQ_y*vB@2i7vNhA*ovhwFv$8_U_M0Mspdk-bAo#MRJ)RwtexAdfa~ zhs^q%B!&4go6sF73ih_Uo$GVfueTEa4gHZmsSkv)U-%tght1(VDt1bFA8DVo%fAG zSh@kWkHw;W6TI#7Z+VU|o-zyb16bPaeYdQAjJ`?ip5e-7s}bplH@O`|8xvmTvP!G> zdtAFnV|T}1EyCpL0rukjMr zD-^RKOI-LjJ_A}D2ZlEVBjlVt{!=RAHDDHNi7i!OPKiTy?ftS5-Fp6q;-Oem&l2{F zyb88ZP7MZ9E>J11avC9BPiUwJhlUFvGK%bvnZ)_-bMxE`aQ)aKqO+UCe%VsoM^od_ zL+cA2Jk_)0wrDr36>XFey1VaVJGsfFKzjEbn(PP}hFcG@eAAdBonudCO1W9q8XvjN z^~6P{>gQ<_Q+tC{*$os>y`r4ULysk6Q3nrmzZE_D_;f7|)<0d9CmD6$HXD$xHu;qA z(*>(Idt@?YpK;yl!{Ag}%AMo{;wO|i`}%e0 z(_+Q*hvzukGx&>l)G3;4+(#b5Y!6;m+D(b2&@LI8W>7$-XVgi=-ZjRqb0Wu=@{l0s zAa3qz-kbfXavr!3jztg2W8(jiOJXgqUg8*Jd|T?>9)zoh=v-6Kjb68pW|+kFaFU^B z5(Jbjo^zPIx@wBx?|Cinq5bc5YYS0I6Nw9C>}6+HI!8qpvsJWH1DA7loC(>@xhPk6 HW^eun7Kid` diff --git a/images/20230330/82016473748200.png b/images/20230330/82016473748200.png deleted file mode 100644 index 47bf1370fe0608a180129f46d2ff1a5acf2457c0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 213120 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

)0#o2gnoyy?DEkj`Q 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 V%X70Q restful api 기반 토큰 이므로 세션 필요x .and() .addFilterBefore(new JwtTokenFilter(memberService, secretKey), UsernamePasswordAuthenticationFilter.class) .build(); diff --git a/src/main/java/com/likelion/dub/controller/ClubController.java b/src/main/java/com/likelion/dub/controller/ClubController.java new file mode 100644 index 0000000..aa105cf --- /dev/null +++ b/src/main/java/com/likelion/dub/controller/ClubController.java @@ -0,0 +1,34 @@ +package com.likelion.dub.controller; + + +import com.likelion.dub.common.BaseException; +import com.likelion.dub.common.BaseResponse; +import com.likelion.dub.common.BaseResponseStatus; +import com.likelion.dub.service.ClubService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/app/club") +@RequiredArgsConstructor +@CrossOrigin(origins = "*", allowedHeaders = "*") //Cors 제거 +public class ClubController { + + + private final ClubService clubService; + + + + @PostMapping("/uploadForm") + public BaseResponse uploadForm(@RequestBody String url) { + try { + clubService.uploadForm(url); + String result = "동아리 지원서 양식 등록 완료"; + return new BaseResponse<>(BaseResponseStatus.SUCCESS, result); + } catch (BaseException e) { + return new BaseResponse<>(e.getStatus()); + } + + } + +} diff --git a/src/main/java/com/likelion/dub/controller/JspController.java b/src/main/java/com/likelion/dub/controller/JspController.java index 0e4282e..d4881fb 100644 --- a/src/main/java/com/likelion/dub/controller/JspController.java +++ b/src/main/java/com/likelion/dub/controller/JspController.java @@ -1,6 +1,6 @@ package com.likelion.dub.controller; -import com.likelion.dub.domain.Member; + import com.likelion.dub.domain.dto.MemberJoinRequest; import com.likelion.dub.service.MemberService; import org.springframework.beans.factory.annotation.Autowired; @@ -8,11 +8,15 @@ import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; +import java.util.function.BiFunction; +import java.util.function.Function; + @Controller public class JspController { @Autowired private MemberService memberService; + @GetMapping("/login") public String loginView(Model model) { String result = "loginVIew"; diff --git a/src/main/java/com/likelion/dub/domain/Club.java b/src/main/java/com/likelion/dub/domain/Club.java index 742903d..7d31f27 100644 --- a/src/main/java/com/likelion/dub/domain/Club.java +++ b/src/main/java/com/likelion/dub/domain/Club.java @@ -40,6 +40,9 @@ public class Club { @Column private String clubImage; + @Column + private String form; + @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY) @JoinColumn(name = "member_id") private Member member; @@ -50,8 +53,6 @@ public void setMember(Member member) { member.setClub(this); } - - public void setPost(Post post) { this.post.add(post); post.setClub(this); diff --git a/src/main/java/com/likelion/dub/repository/MypageRepository.java b/src/main/java/com/likelion/dub/repository/MypageRepository.java deleted file mode 100644 index dfb07fa..0000000 --- a/src/main/java/com/likelion/dub/repository/MypageRepository.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.likelion.dub.repository; - -import org.springframework.data.jpa.repository.JpaRepository; - -public interface MypageRepository { -} diff --git a/src/main/java/com/likelion/dub/repository/PostRepository.java b/src/main/java/com/likelion/dub/repository/PostRepository.java index d4b980f..76320c6 100644 --- a/src/main/java/com/likelion/dub/repository/PostRepository.java +++ b/src/main/java/com/likelion/dub/repository/PostRepository.java @@ -9,7 +9,7 @@ import java.util.List; import java.util.Optional; -@Repository + public interface PostRepository extends JpaRepository { List findAll(); Optional findByClubName(String clubName); diff --git a/src/main/java/com/likelion/dub/service/ClubService.java b/src/main/java/com/likelion/dub/service/ClubService.java new file mode 100644 index 0000000..9f4f3c7 --- /dev/null +++ b/src/main/java/com/likelion/dub/service/ClubService.java @@ -0,0 +1,39 @@ +package com.likelion.dub.service; + + +import com.likelion.dub.common.BaseException; +import com.likelion.dub.common.BaseResponseStatus; +import com.likelion.dub.domain.Club; +import com.likelion.dub.domain.Member; +import com.likelion.dub.repository.ClubRepository; +import com.likelion.dub.repository.MemberRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +@Service +@Transactional +@RequiredArgsConstructor +@Slf4j +public class ClubService { + + private final ClubRepository clubRepository; + private final MemberRepository memberRepository; + + public void uploadForm(String url) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String email = authentication.getName(); + Member member = memberRepository.findByEmail(email).orElseThrow(() -> new BaseException(BaseResponseStatus.WRONG_EMAIL)); + String clubName = member.getClub().getClubName(); + // ClubName 이 존재하는지 확인 + Club club = clubRepository.findByClubName(clubName).orElseThrow(() -> new BaseException(BaseResponseStatus.NO_SUCH_CLUB_EXIST)); + club.setForm(url); + clubRepository.save(club); + + } +} diff --git a/src/main/java/com/likelion/dub/service/MemberService.java b/src/main/java/com/likelion/dub/service/MemberService.java index 7b3e7c1..7a94cb7 100644 --- a/src/main/java/com/likelion/dub/service/MemberService.java +++ b/src/main/java/com/likelion/dub/service/MemberService.java @@ -140,15 +140,15 @@ public void joinClub(String email, String name, String password, String gender, public String login(String email, String password) throws BaseException { //email 중복확인 - Member selectedUser = memberRepository.findByEmail(email) + Member member = memberRepository.findByEmail(email) .orElseThrow(() -> new BaseException(BaseResponseStatus.WRONG_EMAIL)); //비밀번호 틀림 - if (!bCryptPasswordEncoder.matches(password, selectedUser.getPassword())) { + if (!bCryptPasswordEncoder.matches(password, member.getPassword())) { throw new BaseException(BaseResponseStatus.WRONG_PASSWORD); } - String token = JwtTokenUtil.createToken(selectedUser.getEmail(),selectedUser.getRole(), key, expireTimeMs); + String token = JwtTokenUtil.createToken(member.getEmail(),member.getRole(),member.getName(), key, expireTimeMs); return token; } diff --git a/src/main/java/com/likelion/dub/service/PostService.java b/src/main/java/com/likelion/dub/service/PostService.java index 0c875ba..beae1ea 100644 --- a/src/main/java/com/likelion/dub/service/PostService.java +++ b/src/main/java/com/likelion/dub/service/PostService.java @@ -46,6 +46,9 @@ public List getAllPost() { List allPosts = postRepository.findAll(); List getAllPostResponses =new ArrayList<>(); + + + for (Post post : allPosts) { GetAllPostResponse getAllPostResponse = new GetAllPostResponse(); getAllPostResponse.setId(post.getId()); diff --git a/src/test/java/com/likelion/dub/service/PostServiceTest.java b/src/test/java/com/likelion/dub/service/PostServiceTest.java index 2863463..a08d62c 100644 --- a/src/test/java/com/likelion/dub/service/PostServiceTest.java +++ b/src/test/java/com/likelion/dub/service/PostServiceTest.java @@ -7,12 +7,14 @@ import com.likelion.dub.domain.Post; import com.likelion.dub.repository.MemberRepository; import com.likelion.dub.repository.PostRepository; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.couchbase.CouchbaseProperties; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.mock.web.MockMultipartFile; @@ -47,21 +49,10 @@ void createClub(){ memberService.joinClub("suhoon@naver.com", "name", "password", "gender", "CLUB","introduction","groupName","category",file); } + @Test - @WithMockUser(username = "suhoon@naver.com",roles="CLUB") + @WithMockUser(username = "suhoon@naver.com", roles = "CLUB") void testWritePost() throws BaseException, IOException { - //given - - Member member = new Member(1L, null, null, "suhoon@naver.com", "name", "password", "gender", "CLUB"); - Club club = new Club(1L, null, "name", "introduction", "groupName", "category", "clubImage", member); - MockMultipartFile file = new MockMultipartFile("file", "test.txt", "text/plain", "Test file content.".getBytes()); - //when - postService.writing("title", "content", file); - - //then - - - } From 7fe03542a47ffba3b2ffc7e7d0767d10dc597cbe Mon Sep 17 00:00:00 2001 From: suhoon Date: Fri, 8 Sep 2023 01:51:14 +0900 Subject: [PATCH 45/72] =?UTF-8?q?feat:=20=EB=8F=99=EC=95=84=EB=A6=AC=20?= =?UTF-8?q?=EC=86=8C=EA=B0=9C=EA=B8=80=20=EC=9E=91=EC=84=B1=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/likelion/dub/controller/ClubController.java | 11 ++++++++++- .../java/com/likelion/dub/service/ClubService.java | 13 +++++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/likelion/dub/controller/ClubController.java b/src/main/java/com/likelion/dub/controller/ClubController.java index aa105cf..4769800 100644 --- a/src/main/java/com/likelion/dub/controller/ClubController.java +++ b/src/main/java/com/likelion/dub/controller/ClubController.java @@ -18,7 +18,6 @@ public class ClubController { private final ClubService clubService; - @PostMapping("/uploadForm") public BaseResponse uploadForm(@RequestBody String url) { try { @@ -31,4 +30,14 @@ public BaseResponse uploadForm(@RequestBody String url) { } + @PostMapping("/writeIntro") + public BaseResponse writeInfo(@RequestBody String introduction) { + try { + clubService.updateIntroduce(introduction); + String result = "동아리 소개글 작성 완료"; + return new BaseResponse<>(BaseResponseStatus.SUCCESS, result); + } catch (BaseException e) { + return new BaseResponse<>(e.getStatus()); + } + } } diff --git a/src/main/java/com/likelion/dub/service/ClubService.java b/src/main/java/com/likelion/dub/service/ClubService.java index 9f4f3c7..f4b94a4 100644 --- a/src/main/java/com/likelion/dub/service/ClubService.java +++ b/src/main/java/com/likelion/dub/service/ClubService.java @@ -14,7 +14,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.Optional; @Service @Transactional @@ -30,10 +29,20 @@ public void uploadForm(String url) { String email = authentication.getName(); Member member = memberRepository.findByEmail(email).orElseThrow(() -> new BaseException(BaseResponseStatus.WRONG_EMAIL)); String clubName = member.getClub().getClubName(); - // ClubName 이 존재하는지 확인 Club club = clubRepository.findByClubName(clubName).orElseThrow(() -> new BaseException(BaseResponseStatus.NO_SUCH_CLUB_EXIST)); club.setForm(url); clubRepository.save(club); } + + public void updateIntroduce(String introduction) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String email = authentication.getName(); + Member member = memberRepository.findByEmail(email).orElseThrow(() -> new BaseException(BaseResponseStatus.WRONG_EMAIL)); + String clubName = member.getClub().getClubName(); + Club club = clubRepository.findByClubName(clubName).orElseThrow(() -> new BaseException(BaseResponseStatus.NO_SUCH_CLUB_EXIST)); + club.setIntroduction(introduction); + clubRepository.save(club); + } + } From a96fbb20c753d34d774694b7427dbf9a136f0b32 Mon Sep 17 00:00:00 2001 From: suhoon Date: Fri, 8 Sep 2023 13:55:17 +0900 Subject: [PATCH 46/72] =?UTF-8?q?feat:=20=EB=8F=99=EC=95=84=EB=A6=AC=20?= =?UTF-8?q?=EC=82=AC=EC=A7=84=EB=93=B1=EB=A1=9D/=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dub/controller/ClubController.java | 13 ++++++++ .../com/likelion/dub/service/ClubService.java | 32 +++++++++++++++++++ .../likelion/dub/service/MemberService.java | 3 +- 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/likelion/dub/controller/ClubController.java b/src/main/java/com/likelion/dub/controller/ClubController.java index 4769800..c2558d2 100644 --- a/src/main/java/com/likelion/dub/controller/ClubController.java +++ b/src/main/java/com/likelion/dub/controller/ClubController.java @@ -7,6 +7,7 @@ import com.likelion.dub.service.ClubService; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; @RestController @RequestMapping("/app/club") @@ -40,4 +41,16 @@ public BaseResponse writeInfo(@RequestBody String introduction) { return new BaseResponse<>(e.getStatus()); } } + + @PostMapping("/uploadClubImage") + public BaseResponse uploadClubImage(@ModelAttribute MultipartFile image) { + try { + clubService.updateClubImage(image); + String result = "동아리 사진 등록 완료"; + return new BaseResponse<>(BaseResponseStatus.SUCCESS, result); + } catch (BaseException e) { + return new BaseResponse<>(e.getStatus()); + } + } + } diff --git a/src/main/java/com/likelion/dub/service/ClubService.java b/src/main/java/com/likelion/dub/service/ClubService.java index f4b94a4..4385727 100644 --- a/src/main/java/com/likelion/dub/service/ClubService.java +++ b/src/main/java/com/likelion/dub/service/ClubService.java @@ -1,6 +1,8 @@ package com.likelion.dub.service; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.ObjectMetadata; import com.likelion.dub.common.BaseException; import com.likelion.dub.common.BaseResponseStatus; import com.likelion.dub.domain.Club; @@ -9,10 +11,14 @@ import com.likelion.dub.repository.MemberRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; @Service @@ -23,6 +29,11 @@ public class ClubService { private final ClubRepository clubRepository; private final MemberRepository memberRepository; + private final AmazonS3Client amazonS3Client; + + @Value("${cloud.aws.s3.bucket}") + private String bucket; + public void uploadForm(String url) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); @@ -45,4 +56,25 @@ public void updateIntroduce(String introduction) { clubRepository.save(club); } + + public void updateClubImage(MultipartFile file) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String email = authentication.getName(); + Member member = memberRepository.findByEmail(email).orElseThrow(() -> new BaseException(BaseResponseStatus.WRONG_EMAIL)); + Club club = member.getClub(); + String clubName = club.getClubName(); + String fileName = clubName + "_" + "ClubImage"; + ObjectMetadata metadata = new ObjectMetadata(); + metadata.setContentType(file.getContentType()); + metadata.setContentLength(file.getSize()); + try { + amazonS3Client.putObject(bucket, fileName, file.getInputStream(), metadata); + club.setClubImage("https://dubs3.s3.ap-northeast-2.amazonaws.com/" + fileName); + } catch (IOException e) { + throw new BaseException(BaseResponseStatus.FILE_SAVE_ERROR); + } + clubRepository.save(club); + + } + } diff --git a/src/main/java/com/likelion/dub/service/MemberService.java b/src/main/java/com/likelion/dub/service/MemberService.java index 7a94cb7..78736db 100644 --- a/src/main/java/com/likelion/dub/service/MemberService.java +++ b/src/main/java/com/likelion/dub/service/MemberService.java @@ -120,8 +120,7 @@ public void joinClub(String email, String name, String password, String gender, club.setCategory(category); if (file != null) { // 프로필 사진 S3에 저장 - Long memberId = member.getId(); - String fileName = memberId + "ClubImage"; + String fileName = name + "_" + "ClubImage"; ObjectMetadata metadata= new ObjectMetadata(); metadata.setContentType(file.getContentType()); metadata.setContentLength(file.getSize()); From 77cfbc100af9cbc4956f88cf0f50768e30af2709 Mon Sep 17 00:00:00 2001 From: suhoon Date: Fri, 8 Sep 2023 14:12:48 +0900 Subject: [PATCH 47/72] =?UTF-8?q?feat:=20=EB=8F=99=EC=95=84=EB=A6=AC=20?= =?UTF-8?q?=ED=83=9C=EA=B7=B8=20=EB=93=B1=EB=A1=9D/=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dub/controller/ClubController.java | 13 +++++++++++++ .../dub/domain/dto/UpdateTagRequest.java | 18 ++++++++++++++++++ .../com/likelion/dub/service/ClubService.java | 10 ++++++++++ 3 files changed, 41 insertions(+) create mode 100644 src/main/java/com/likelion/dub/domain/dto/UpdateTagRequest.java diff --git a/src/main/java/com/likelion/dub/controller/ClubController.java b/src/main/java/com/likelion/dub/controller/ClubController.java index c2558d2..5df1e30 100644 --- a/src/main/java/com/likelion/dub/controller/ClubController.java +++ b/src/main/java/com/likelion/dub/controller/ClubController.java @@ -1,9 +1,11 @@ package com.likelion.dub.controller; +import com.fasterxml.jackson.databind.ser.Serializers; import com.likelion.dub.common.BaseException; import com.likelion.dub.common.BaseResponse; import com.likelion.dub.common.BaseResponseStatus; +import com.likelion.dub.domain.dto.UpdateTagRequest; import com.likelion.dub.service.ClubService; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; @@ -53,4 +55,15 @@ public BaseResponse uploadClubImage(@ModelAttribute MultipartFile image) } } + @PostMapping("/updateTag") + public BaseResponse updateTag(@RequestBody UpdateTagRequest updateTagRequest) { + try{ + clubService.updateTag(updateTagRequest.getGroupName(), updateTagRequest.getCategory()); + String result = "동아리 태그 등록 완료"; + return new BaseResponse<>(BaseResponseStatus.SUCCESS, result); + } catch (BaseException e) { + return new BaseResponse<>(e.getStatus()); + } + + } } diff --git a/src/main/java/com/likelion/dub/domain/dto/UpdateTagRequest.java b/src/main/java/com/likelion/dub/domain/dto/UpdateTagRequest.java new file mode 100644 index 0000000..9122bdc --- /dev/null +++ b/src/main/java/com/likelion/dub/domain/dto/UpdateTagRequest.java @@ -0,0 +1,18 @@ +package com.likelion.dub.domain.dto; + + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class UpdateTagRequest { + @JsonProperty + private String groupName; + + @JsonProperty + private String category; + + +} diff --git a/src/main/java/com/likelion/dub/service/ClubService.java b/src/main/java/com/likelion/dub/service/ClubService.java index 4385727..88f3e05 100644 --- a/src/main/java/com/likelion/dub/service/ClubService.java +++ b/src/main/java/com/likelion/dub/service/ClubService.java @@ -77,4 +77,14 @@ public void updateClubImage(MultipartFile file) { } + public void updateTag(String groupName, String category) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String email = authentication.getName(); + Member member = memberRepository.findByEmail(email).orElseThrow(() -> new BaseException(BaseResponseStatus.WRONG_EMAIL)); + Club club = member.getClub(); + club.setGroupName(groupName); + club.setCategory(category); + clubRepository.save(club); + } + } From ed3874ee265c70bfe2f79901311a01c1d1698d73 Mon Sep 17 00:00:00 2001 From: suhoon Date: Fri, 8 Sep 2023 16:16:23 +0900 Subject: [PATCH 48/72] =?UTF-8?q?fix:=20mypage=20controller=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dub/controller/MypageController.java | 91 ------------------- .../likelion/dub/service/MemberService.java | 6 ++ .../likelion/dub/service/MypageService.java | 39 -------- 3 files changed, 6 insertions(+), 130 deletions(-) delete mode 100644 src/main/java/com/likelion/dub/controller/MypageController.java delete mode 100644 src/main/java/com/likelion/dub/service/MypageService.java diff --git a/src/main/java/com/likelion/dub/controller/MypageController.java b/src/main/java/com/likelion/dub/controller/MypageController.java deleted file mode 100644 index eff6d7f..0000000 --- a/src/main/java/com/likelion/dub/controller/MypageController.java +++ /dev/null @@ -1,91 +0,0 @@ -package com.likelion.dub.controller; - -import com.fasterxml.jackson.databind.ser.Serializers; -import com.likelion.dub.common.BaseException; -import com.likelion.dub.common.BaseResponse; -import com.likelion.dub.common.BaseResponseStatus; -import com.likelion.dub.domain.Member; -import com.likelion.dub.domain.dto.MyPageResponse; -import com.likelion.dub.domain.dto.PasswordRequest; -import com.likelion.dub.service.MemberService; -import com.likelion.dub.service.MypageService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.crypto.bcrypt.BCrypt; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.web.bind.annotation.*; - -@RestController -@RequestMapping("/app/mypage") -@RequiredArgsConstructor -@Slf4j -@CrossOrigin(origins = "*", allowedHeaders = "*") //Cors 제거 -public class MypageController { - private final MypageService mypageService; - - - /** - * 마이페이지 정보조회 - * @return - */ - @GetMapping("/getInfo") - public BaseResponse getMyPage(){ - try { - //Spring Security Context 에서 인증 정보를 가져옴 - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - if (authentication == null || !authentication.isAuthenticated()) { - return new BaseResponse(BaseResponseStatus.INVALID_MEMBER_JWT); - } - //사용자 정보를 추출하여 마이페이지 정보 반환 - String email = authentication.getName(); - Member member = mypageService.loadMemberByEmail(email); - - log.info(email.toString()); - log.info(member.toString()); - - MyPageResponse myPageResponse = new MyPageResponse(member.getEmail(), member.getName(), member.getRole()); - return new BaseResponse<>(BaseResponseStatus.SUCCESS,myPageResponse); - } catch (BaseException e) { - return new BaseResponse(BaseResponseStatus.JWT_TOKEN_ERROR); - } - - } - - - /** - * 비밀번호 수정 - * @param passwordRequest - * @return - */ - @PutMapping("/password") - public BaseResponse changePassword(@RequestBody PasswordRequest passwordRequest) { - String currentPassword = passwordRequest.getCurrentPassword(); - String newPassword = passwordRequest.getNewPassword(); - - - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - //jwt token 오류 - if (authentication == null || !authentication.isAuthenticated()) { - return new BaseResponse(BaseResponseStatus.JWT_TOKEN_ERROR); - } - String email = authentication.getName(); - Member member = mypageService.loadMemberByEmail(email); - //current password wrong - if (!BCrypt.checkpw(passwordRequest.getCurrentPassword(), member.getPassword())) { - return new BaseResponse(BaseResponseStatus.WRONG_PASSWORD); - } - //새로운 비밀번호 암호화하여 업데이트 - String hashedPassword = BCrypt.hashpw(passwordRequest.getNewPassword(), BCrypt.gensalt()); - member.setPassword(hashedPassword); - mypageService.save(member); - - String result = "비밀번호 수정 완료"; - return new BaseResponse<>(BaseResponseStatus.SUCCESS, result); - - } - -} diff --git a/src/main/java/com/likelion/dub/service/MemberService.java b/src/main/java/com/likelion/dub/service/MemberService.java index 78736db..9b7baf1 100644 --- a/src/main/java/com/likelion/dub/service/MemberService.java +++ b/src/main/java/com/likelion/dub/service/MemberService.java @@ -153,6 +153,12 @@ public String login(String email, String password) throws BaseException { } + public void getInfo() { + + + } + + public void changePassword(Long id, String password) { Member member = memberRepository.findById(id) diff --git a/src/main/java/com/likelion/dub/service/MypageService.java b/src/main/java/com/likelion/dub/service/MypageService.java deleted file mode 100644 index ee22e8c..0000000 --- a/src/main/java/com/likelion/dub/service/MypageService.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.likelion.dub.service; - - -import com.likelion.dub.common.BaseException; -import com.likelion.dub.common.BaseResponseStatus; -import com.likelion.dub.domain.Member; -import com.likelion.dub.repository.MemberRepository; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.Optional; - -@Service -@Transactional -@RequiredArgsConstructor -@Slf4j -public class MypageService { - private final MemberRepository memberRepository; - private final BCryptPasswordEncoder encoder; - - - public Member loadMemberByEmail(String email) throws BaseException { - Member selectedUser = memberRepository.findByEmail(email) - .orElseThrow(() -> new BaseException(BaseResponseStatus.WRONG_EMAIL)); - - return selectedUser; - - } - - public Member save(Member member) { - memberRepository.save(member); - return member; - - } - -} From b930f76da92880e615789de7e604b20c69634253 Mon Sep 17 00:00:00 2001 From: suhoon Date: Fri, 8 Sep 2023 16:34:51 +0900 Subject: [PATCH 49/72] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dub/controller/MemberController.java | 49 ++++++++++++------- .../java/com/likelion/dub/domain/Club.java | 4 ++ .../dub/domain/dto/GetMemberInfoResponse.java | 25 ++++++++++ .../dub/domain/dto/MyPageResponse.java | 22 --------- .../likelion/dub/service/MemberService.java | 17 +++++-- 5 files changed, 75 insertions(+), 42 deletions(-) create mode 100644 src/main/java/com/likelion/dub/domain/dto/GetMemberInfoResponse.java delete mode 100644 src/main/java/com/likelion/dub/domain/dto/MyPageResponse.java diff --git a/src/main/java/com/likelion/dub/controller/MemberController.java b/src/main/java/com/likelion/dub/controller/MemberController.java index 93bcb67..42bb945 100644 --- a/src/main/java/com/likelion/dub/controller/MemberController.java +++ b/src/main/java/com/likelion/dub/controller/MemberController.java @@ -5,6 +5,7 @@ import com.likelion.dub.common.BaseResponseStatus; import com.likelion.dub.domain.Member; import com.likelion.dub.domain.dto.ClubMemberJoinRequest; +import com.likelion.dub.domain.dto.GetMemberInfoResponse; import com.likelion.dub.domain.dto.MemberJoinRequest; import com.likelion.dub.domain.dto.MemberLoginRequest; import com.likelion.dub.service.MemberService; @@ -28,6 +29,7 @@ public class MemberController { /** * 이메일 중복체크 + * * @param email * @return */ @@ -37,7 +39,7 @@ public BaseResponse checkEmail(@PathVariable String email) { boolean isEmailAvailable = memberService.checkEmail(email); if (isEmailAvailable) { String result = "이메일 사용 가능"; - return new BaseResponse<>(BaseResponseStatus.SUCCESS,result); + return new BaseResponse<>(BaseResponseStatus.SUCCESS, result); } else { return new BaseResponse(BaseResponseStatus.EMAIL_ALREADY_EXIST); } @@ -47,36 +49,37 @@ public BaseResponse checkEmail(@PathVariable String email) { /** * 일반회원가입 + * * @param dto * @return */ @PostMapping("/sign-up") - public BaseResponse join(@RequestBody MemberJoinRequest dto ) { - try { - - memberService.join(dto.getEmail(), dto.getName(), dto.getPassword(), dto.getGender(), dto.getRole()); - String result = "(일반)회원 가입 완료"; - return new BaseResponse<>(BaseResponseStatus.SUCCESS,result); - } - catch(BaseException e){ - return new BaseResponse(e.getStatus()); - } + public BaseResponse join(@RequestBody MemberJoinRequest dto) { + try { + + memberService.join(dto.getEmail(), dto.getName(), dto.getPassword(), dto.getGender(), dto.getRole()); + String result = "(일반)회원 가입 완료"; + return new BaseResponse<>(BaseResponseStatus.SUCCESS, result); + } catch (BaseException e) { + return new BaseResponse(e.getStatus()); + } } /** * 동아리회원 회원가입 + * * @param dto * @param file * @return */ - @PostMapping(value = "/sign-up-club",consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE}) - public BaseResponse joinClub(@RequestPart(value = "json") ClubMemberJoinRequest dto, @RequestPart(value = "image", required = false) MultipartFile file) { + @PostMapping(value = "/sign-up-club", consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE}) + public BaseResponse joinClub(@RequestPart(value = "json") ClubMemberJoinRequest dto, @RequestPart(value = "image", required = false) MultipartFile file) { try { - memberService.joinClub(dto.getEmail(), dto.getName(), dto.getPassword(), dto.getGender(), dto.getRole(), dto.getIntroduction(), dto.getGroupName(),dto.getCategory(), file); + memberService.joinClub(dto.getEmail(), dto.getName(), dto.getPassword(), dto.getGender(), dto.getRole(), dto.getIntroduction(), dto.getGroupName(), dto.getCategory(), file); String result = "(동아리)회원 가입 완료"; - return new BaseResponse<>(BaseResponseStatus.SUCCESS,result); + return new BaseResponse<>(BaseResponseStatus.SUCCESS, result); } catch (BaseException e) { return new BaseResponse<>(e.getStatus()); @@ -84,7 +87,9 @@ public BaseResponse joinClub(@RequestPart(value = "json") ClubMemberJoin } - /** 로그인 + /** + * 로그인 + * * @param dto * @return */ @@ -93,7 +98,7 @@ public BaseResponse login(@RequestBody MemberLoginRequest dto) { try { String token = memberService.login(dto.getEmail(), dto.getPassword()); - return new BaseResponse<>(BaseResponseStatus.SUCCESS,"Bearer " + token); + return new BaseResponse<>(BaseResponseStatus.SUCCESS, "Bearer " + token); } catch (BaseException e) { return new BaseResponse(e.getStatus()); @@ -102,5 +107,15 @@ public BaseResponse login(@RequestBody MemberLoginRequest dto) { } + @GetMapping("/getInfo") + public BaseResponse getInfo() { + try { + GetMemberInfoResponse getMemberInfoResponse = memberService.getInfo(); + return new BaseResponse<>(BaseResponseStatus.SUCCESS, getMemberInfoResponse); + } catch (BaseException e) { + return new BaseResponse(e.getStatus()); + } + } + } diff --git a/src/main/java/com/likelion/dub/domain/Club.java b/src/main/java/com/likelion/dub/domain/Club.java index 7d31f27..2d1f9e2 100644 --- a/src/main/java/com/likelion/dub/domain/Club.java +++ b/src/main/java/com/likelion/dub/domain/Club.java @@ -23,6 +23,8 @@ public class Club { private Long id; + + @JsonIgnore @OneToMany(mappedBy = "club") private List post = new ArrayList<>(); @@ -43,6 +45,8 @@ public class Club { @Column private String form; + + @JsonIgnore @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY) @JoinColumn(name = "member_id") private Member member; diff --git a/src/main/java/com/likelion/dub/domain/dto/GetMemberInfoResponse.java b/src/main/java/com/likelion/dub/domain/dto/GetMemberInfoResponse.java new file mode 100644 index 0000000..967a312 --- /dev/null +++ b/src/main/java/com/likelion/dub/domain/dto/GetMemberInfoResponse.java @@ -0,0 +1,25 @@ +package com.likelion.dub.domain.dto; + + +import com.likelion.dub.domain.Club; +import jakarta.persistence.Column; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@AllArgsConstructor +@Getter +@Setter +@NoArgsConstructor +public class GetMemberInfoResponse { + private String email; + private String name; + + private String gender; + private String role; + + private Club club; + + +} diff --git a/src/main/java/com/likelion/dub/domain/dto/MyPageResponse.java b/src/main/java/com/likelion/dub/domain/dto/MyPageResponse.java deleted file mode 100644 index d8feb46..0000000 --- a/src/main/java/com/likelion/dub/domain/dto/MyPageResponse.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.likelion.dub.domain.dto; - - -import com.fasterxml.jackson.annotation.JsonProperty; -import jakarta.persistence.Column; -import jakarta.persistence.JoinColumn; -import lombok.AllArgsConstructor; -import lombok.Getter; - -@AllArgsConstructor -@Getter -public class MyPageResponse { - @JsonProperty - private String email; - @JsonProperty - private String username; - - @JsonProperty - private String role; - - -} diff --git a/src/main/java/com/likelion/dub/service/MemberService.java b/src/main/java/com/likelion/dub/service/MemberService.java index 9b7baf1..d41d011 100644 --- a/src/main/java/com/likelion/dub/service/MemberService.java +++ b/src/main/java/com/likelion/dub/service/MemberService.java @@ -7,6 +7,7 @@ import com.likelion.dub.common.BaseResponseStatus; import com.likelion.dub.domain.*; +import com.likelion.dub.domain.dto.GetMemberInfoResponse; import com.likelion.dub.repository.ClubRepository; import com.likelion.dub.repository.MemberRepository; @@ -16,6 +17,8 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -153,9 +156,17 @@ public String login(String email, String password) throws BaseException { } - public void getInfo() { - - + public GetMemberInfoResponse getInfo() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String email = authentication.getName(); + Member member = memberRepository.findByEmail(email).orElseThrow(() -> new BaseException(BaseResponseStatus.NO_SUCH_MEMBER_EXIST)); + GetMemberInfoResponse getMemberInfoResponse = new GetMemberInfoResponse(); + getMemberInfoResponse.setName(member.getName()); + getMemberInfoResponse.setGender(member.getGender()); + getMemberInfoResponse.setRole(member.getRole()); + getMemberInfoResponse.setEmail(member.getEmail()); + getMemberInfoResponse.setClub(member.getClub()); + return getMemberInfoResponse; } From 5b620a3cb0e82ff8e468c01df3cfec6f847141ba Mon Sep 17 00:00:00 2001 From: suhoon Date: Fri, 8 Sep 2023 16:51:06 +0900 Subject: [PATCH 50/72] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=20=EB=B9=84?= =?UTF-8?q?=EB=B0=80=EB=B2=88=ED=98=B8=20=EC=88=98=EC=A0=95=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dub/controller/MemberController.java | 30 ++++++++++++++----- ...wordRequest.java => ChangePwdRequest.java} | 2 +- .../likelion/dub/service/MemberService.java | 18 +++++++---- 3 files changed, 35 insertions(+), 15 deletions(-) rename src/main/java/com/likelion/dub/domain/dto/{PasswordRequest.java => ChangePwdRequest.java} (90%) diff --git a/src/main/java/com/likelion/dub/controller/MemberController.java b/src/main/java/com/likelion/dub/controller/MemberController.java index 42bb945..0605309 100644 --- a/src/main/java/com/likelion/dub/controller/MemberController.java +++ b/src/main/java/com/likelion/dub/controller/MemberController.java @@ -4,10 +4,7 @@ import com.likelion.dub.common.BaseResponse; import com.likelion.dub.common.BaseResponseStatus; import com.likelion.dub.domain.Member; -import com.likelion.dub.domain.dto.ClubMemberJoinRequest; -import com.likelion.dub.domain.dto.GetMemberInfoResponse; -import com.likelion.dub.domain.dto.MemberJoinRequest; -import com.likelion.dub.domain.dto.MemberLoginRequest; +import com.likelion.dub.domain.dto.*; import com.likelion.dub.service.MemberService; import lombok.RequiredArgsConstructor; import org.springframework.http.MediaType; @@ -29,7 +26,6 @@ public class MemberController { /** * 이메일 중복체크 - * * @param email * @return */ @@ -49,7 +45,6 @@ public BaseResponse checkEmail(@PathVariable String email) { /** * 일반회원가입 - * * @param dto * @return */ @@ -67,7 +62,6 @@ public BaseResponse join(@RequestBody MemberJoinRequest dto) { /** * 동아리회원 회원가입 - * * @param dto * @param file * @return @@ -89,7 +83,6 @@ public BaseResponse joinClub(@RequestPart(value = "json") ClubMemberJoin /** * 로그인 - * * @param dto * @return */ @@ -107,6 +100,10 @@ public BaseResponse login(@RequestBody MemberLoginRequest dto) { } + /** + * 회원정보 조회 + * @return + */ @GetMapping("/getInfo") public BaseResponse getInfo() { try { @@ -118,4 +115,21 @@ public BaseResponse getInfo() { } + /** + * 비밀번호 수정 + * @param changePwdRequest + * @return + */ + @PutMapping("/changePwd") + public BaseResponse changePwd(@RequestBody ChangePwdRequest changePwdRequest) { + try { + memberService.changePassword(changePwdRequest.getCurrentPassword(), changePwdRequest.getNewPassword()); + String result = "비밀번호 수정 완료"; + return new BaseResponse<>(BaseResponseStatus.SUCCESS, result); + } catch (BaseException e) { + return new BaseResponse(e.getStatus()); + } + + + } } diff --git a/src/main/java/com/likelion/dub/domain/dto/PasswordRequest.java b/src/main/java/com/likelion/dub/domain/dto/ChangePwdRequest.java similarity index 90% rename from src/main/java/com/likelion/dub/domain/dto/PasswordRequest.java rename to src/main/java/com/likelion/dub/domain/dto/ChangePwdRequest.java index 736c0ab..4fd8de1 100644 --- a/src/main/java/com/likelion/dub/domain/dto/PasswordRequest.java +++ b/src/main/java/com/likelion/dub/domain/dto/ChangePwdRequest.java @@ -6,7 +6,7 @@ @AllArgsConstructor @Getter -public class PasswordRequest { +public class ChangePwdRequest { @JsonProperty diff --git a/src/main/java/com/likelion/dub/service/MemberService.java b/src/main/java/com/likelion/dub/service/MemberService.java index d41d011..fd4f276 100644 --- a/src/main/java/com/likelion/dub/service/MemberService.java +++ b/src/main/java/com/likelion/dub/service/MemberService.java @@ -170,12 +170,18 @@ public GetMemberInfoResponse getInfo() { } - public void changePassword(Long id, String password) { - - Member member = memberRepository.findById(id) - .orElseThrow(() -> new EntityNotFoundException("Member not found with id " + id)); - member.setPassword(password); - memberRepository.save(member); + public void changePassword(String currentPassword,String newPassword) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String email = authentication.getName(); + Member member = memberRepository.findByEmail(email).orElseThrow(() -> new BaseException(BaseResponseStatus.NO_SUCH_MEMBER_EXIST)); + if (bCryptPasswordEncoder.matches(currentPassword, member.getPassword())) { + // 비밀번호가 일치할 때 처리 + String hashedPassword = bCryptPasswordEncoder.encode(newPassword); + member.setPassword(hashedPassword); + memberRepository.save(member); + } else { + throw new BaseException(BaseResponseStatus.WRONG_PASSWORD); + } } } From cb3d16e79ac419f944a4f3d1d15738aea1effb5c Mon Sep 17 00:00:00 2001 From: suhoon Date: Fri, 8 Sep 2023 17:00:49 +0900 Subject: [PATCH 51/72] =?UTF-8?q?chore:=20Security=20Config=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/likelion/dub/configuration/SecurityConfig.java | 6 +++--- .../java/com/likelion/dub/controller/MemberController.java | 4 +--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/likelion/dub/configuration/SecurityConfig.java b/src/main/java/com/likelion/dub/configuration/SecurityConfig.java index 15b016e..c11f9da 100644 --- a/src/main/java/com/likelion/dub/configuration/SecurityConfig.java +++ b/src/main/java/com/likelion/dub/configuration/SecurityConfig.java @@ -44,9 +44,9 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .csrf().disable() //.cors().and() // cors 활성화 .authorizeRequests() - .requestMatchers("/app/member/sign-up", "/app/member/sign-in", "/app/member/email/{email}", "/app/member/stunum/{stunum}","/app/post/getAll").permitAll() //누구나 접근 가능 - .requestMatchers( "/app/club/**").hasRole("CLUB") //CLUB 권한 필요 - .anyRequest().permitAll() + .requestMatchers("/app/post/write-post", "/app/post/delete-post", "/app/post/rewrite-post").hasRole("CLUB") // Post 작성 Club 권한 필요 + .requestMatchers( "/app/club/**").hasRole("CLUB") // Club 에 관한거 CLUB 권한 필요 + .anyRequest().permitAll() // 나머지는 누구나 접근가능 .and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 세션 사용 비활성화 -> restful api 기반 토큰 이므로 세션 필요x diff --git a/src/main/java/com/likelion/dub/controller/MemberController.java b/src/main/java/com/likelion/dub/controller/MemberController.java index 0605309..d570033 100644 --- a/src/main/java/com/likelion/dub/controller/MemberController.java +++ b/src/main/java/com/likelion/dub/controller/MemberController.java @@ -3,16 +3,14 @@ import com.likelion.dub.common.BaseException; import com.likelion.dub.common.BaseResponse; import com.likelion.dub.common.BaseResponseStatus; -import com.likelion.dub.domain.Member; import com.likelion.dub.domain.dto.*; import com.likelion.dub.service.MemberService; import lombok.RequiredArgsConstructor; import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; -import java.util.List; + @RestController From 6b4dd1908423c8a74268facb5c66c93a5aef0889 Mon Sep 17 00:00:00 2001 From: suhoon Date: Fri, 8 Sep 2023 17:03:30 +0900 Subject: [PATCH 52/72] =?UTF-8?q?fix:=20Post=20=EC=9E=91=EC=84=B1=EC=8B=9C?= =?UTF-8?q?=20postimage=20filename=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/likelion/dub/service/PostService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/likelion/dub/service/PostService.java b/src/main/java/com/likelion/dub/service/PostService.java index beae1ea..130a757 100644 --- a/src/main/java/com/likelion/dub/service/PostService.java +++ b/src/main/java/com/likelion/dub/service/PostService.java @@ -80,7 +80,7 @@ public void writing(String title, String content, MultipartFile file) throws Bas post.setContent(content); try { if (file != null) { - String fileName = member.getId() + "PostImage"; + String fileName = clubName + "_" + "PostImage"; // 포스터 사진 S3에 저장 uploadPostImageToS3(fileName, file); post.setPostImage("https://dubs3.s3.ap-northeast-2.amazonaws.com/" + fileName); From b270121b739981594dcd2277bd45d34068dab633 Mon Sep 17 00:00:00 2001 From: suhoon Date: Fri, 8 Sep 2023 17:23:03 +0900 Subject: [PATCH 53/72] =?UTF-8?q?fix:=20Post=20=EC=9E=91=EC=84=B1=EC=8B=9C?= =?UTF-8?q?=20image=20=EC=97=86=EC=9D=84=EB=95=8C=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95,=20=EB=B0=8F=20Post=20=ED=95=98=EB=82=98?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=EC=97=90=20form=20=ED=95=84=EB=93=9C?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dub/domain/dto/GetAllPostResponse.java | 1 + .../dub/domain/dto/GetOnePostResponse.java | 2 ++ .../com/likelion/dub/service/PostService.java | 18 +++++++----------- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/likelion/dub/domain/dto/GetAllPostResponse.java b/src/main/java/com/likelion/dub/domain/dto/GetAllPostResponse.java index b5e0bbf..0d19ffb 100644 --- a/src/main/java/com/likelion/dub/domain/dto/GetAllPostResponse.java +++ b/src/main/java/com/likelion/dub/domain/dto/GetAllPostResponse.java @@ -18,4 +18,5 @@ public class GetAllPostResponse { private String clubImage; + } diff --git a/src/main/java/com/likelion/dub/domain/dto/GetOnePostResponse.java b/src/main/java/com/likelion/dub/domain/dto/GetOnePostResponse.java index f36199f..369c6e3 100644 --- a/src/main/java/com/likelion/dub/domain/dto/GetOnePostResponse.java +++ b/src/main/java/com/likelion/dub/domain/dto/GetOnePostResponse.java @@ -25,4 +25,6 @@ public class GetOnePostResponse { private String postImage; + private String form; + } diff --git a/src/main/java/com/likelion/dub/service/PostService.java b/src/main/java/com/likelion/dub/service/PostService.java index 130a757..4a3cece 100644 --- a/src/main/java/com/likelion/dub/service/PostService.java +++ b/src/main/java/com/likelion/dub/service/PostService.java @@ -47,21 +47,20 @@ public List getAllPost() { List allPosts = postRepository.findAll(); List getAllPostResponses =new ArrayList<>(); - - for (Post post : allPosts) { GetAllPostResponse getAllPostResponse = new GetAllPostResponse(); getAllPostResponse.setId(post.getId()); getAllPostResponse.setTitle(post.getTitle()); getAllPostResponse.setClubName(post.getClubName()); getAllPostResponse.setClubImage(post.getClub().getClubImage()); + getAllPostResponses.add(getAllPostResponse); } return getAllPostResponses; } - public void writing(String title, String content, MultipartFile file) throws BaseException { + public void writing(String title, String content, MultipartFile image) throws BaseException { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); String email = authentication.getName(); @@ -79,13 +78,14 @@ public void writing(String title, String content, MultipartFile file) throws Bas post.setTitle(title); post.setContent(content); try { - if (file != null) { + if (image != null) { String fileName = clubName + "_" + "PostImage"; // 포스터 사진 S3에 저장 - uploadPostImageToS3(fileName, file); + uploadPostImageToS3(fileName, image); post.setPostImage("https://dubs3.s3.ap-northeast-2.amazonaws.com/" + fileName); - postRepository.save(post); } + postRepository.save(post); + }catch(IOException e){ throw new BaseException(BaseResponseStatus.FILE_SAVE_ERROR); } @@ -108,14 +108,10 @@ public GetOnePostResponse readPost(Long id) throws BaseException { getOnePostResponse.setTitle(post.getTitle()); getOnePostResponse.setContent(post.getContent()); getOnePostResponse.setPostImage(post.getPostImage()); - + getOnePostResponse.setForm((post.getClub().getForm())); List comments = null; getOnePostResponse.setComments(comments); - return getOnePostResponse; - - - } From 7ae697f0ed07eb5a04c9ed4e99de9e28a28e8ef6 Mon Sep 17 00:00:00 2001 From: suhoon Date: Sun, 24 Sep 2023 00:29:24 +0900 Subject: [PATCH 54/72] =?UTF-8?q?feat:=20=EC=B9=B4=EC=B9=B4=EC=98=A4=20?= =?UTF-8?q?=EC=86=8C=EC=85=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dub/configuration/ClientConfig.java | 14 ++++ .../dub/controller/MemberController.java | 29 +++++++- .../java/com/likelion/dub/domain/Member.java | 18 +++-- .../dub/domain/dto/OAuth/KakaoApiClient.java | 69 +++++++++++++++++++ .../domain/dto/OAuth/KakaoInfoResponse.java | 41 +++++++++++ .../domain/dto/OAuth/KakaoLoginParams.java | 26 +++++++ .../dub/domain/dto/OAuth/KakaoTokens.java | 28 ++++++++ .../dub/domain/dto/OAuth/OAuthApiClient.java | 7 ++ .../domain/dto/OAuth/OAuthInfoResponse.java | 7 ++ .../domain/dto/OAuth/OAuthLoginParams.java | 8 +++ .../dub/domain/dto/OAuth/OAuthProvider.java | 6 ++ .../dto/OAuth/RequestOAuthInfoService.java | 24 +++++++ .../likelion/dub/exception/AppException.java | 15 ---- .../com/likelion/dub/exception/Errorcode.java | 49 ------------- .../dub/exception/ExceptionManager.java | 19 ----- .../likelion/dub/service/MemberService.java | 43 +++++++++--- src/main/resources/application.properties | 18 ++++- 17 files changed, 318 insertions(+), 103 deletions(-) create mode 100644 src/main/java/com/likelion/dub/configuration/ClientConfig.java create mode 100644 src/main/java/com/likelion/dub/domain/dto/OAuth/KakaoApiClient.java create mode 100644 src/main/java/com/likelion/dub/domain/dto/OAuth/KakaoInfoResponse.java create mode 100644 src/main/java/com/likelion/dub/domain/dto/OAuth/KakaoLoginParams.java create mode 100644 src/main/java/com/likelion/dub/domain/dto/OAuth/KakaoTokens.java create mode 100644 src/main/java/com/likelion/dub/domain/dto/OAuth/OAuthApiClient.java create mode 100644 src/main/java/com/likelion/dub/domain/dto/OAuth/OAuthInfoResponse.java create mode 100644 src/main/java/com/likelion/dub/domain/dto/OAuth/OAuthLoginParams.java create mode 100644 src/main/java/com/likelion/dub/domain/dto/OAuth/OAuthProvider.java create mode 100644 src/main/java/com/likelion/dub/domain/dto/OAuth/RequestOAuthInfoService.java delete mode 100644 src/main/java/com/likelion/dub/exception/AppException.java delete mode 100644 src/main/java/com/likelion/dub/exception/Errorcode.java delete mode 100644 src/main/java/com/likelion/dub/exception/ExceptionManager.java diff --git a/src/main/java/com/likelion/dub/configuration/ClientConfig.java b/src/main/java/com/likelion/dub/configuration/ClientConfig.java new file mode 100644 index 0000000..a57788d --- /dev/null +++ b/src/main/java/com/likelion/dub/configuration/ClientConfig.java @@ -0,0 +1,14 @@ +package com.likelion.dub.configuration; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class ClientConfig { + + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/dub/controller/MemberController.java b/src/main/java/com/likelion/dub/controller/MemberController.java index d570033..35e3904 100644 --- a/src/main/java/com/likelion/dub/controller/MemberController.java +++ b/src/main/java/com/likelion/dub/controller/MemberController.java @@ -3,11 +3,24 @@ import com.likelion.dub.common.BaseException; import com.likelion.dub.common.BaseResponse; import com.likelion.dub.common.BaseResponseStatus; -import com.likelion.dub.domain.dto.*; +import com.likelion.dub.domain.dto.ChangePwdRequest; +import com.likelion.dub.domain.dto.ClubMemberJoinRequest; +import com.likelion.dub.domain.dto.GetMemberInfoResponse; +import com.likelion.dub.domain.dto.MemberJoinRequest; +import com.likelion.dub.domain.dto.MemberLoginRequest; +import com.likelion.dub.domain.dto.OAuth.KakaoLoginParams; import com.likelion.dub.service.MemberService; import lombok.RequiredArgsConstructor; import org.springframework.http.MediaType; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -98,6 +111,18 @@ public BaseResponse login(@RequestBody MemberLoginRequest dto) { } + @PostMapping("/loginKakao") + public BaseResponse loginKakao(@RequestBody KakaoLoginParams params) { + try { + String token = memberService.loginKakao(params); + return new BaseResponse<>(BaseResponseStatus.SUCCESS, "Bearer " + token); + } catch (BaseException e) { + return new BaseResponse(e.getStatus()); + } + } + + + /** * 회원정보 조회 * @return diff --git a/src/main/java/com/likelion/dub/domain/Member.java b/src/main/java/com/likelion/dub/domain/Member.java index e2c88cc..b48b672 100644 --- a/src/main/java/com/likelion/dub/domain/Member.java +++ b/src/main/java/com/likelion/dub/domain/Member.java @@ -1,12 +1,19 @@ package com.likelion.dub.domain; -import com.fasterxml.jackson.annotation.JsonIgnore; -import jakarta.persistence.*; -import lombok.*; - +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; import java.util.ArrayList; import java.util.List; -import java.util.Set; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; @NoArgsConstructor @@ -43,6 +50,7 @@ public class Member { private String role; + public void setClub(Club club) { this.club = club; } diff --git a/src/main/java/com/likelion/dub/domain/dto/OAuth/KakaoApiClient.java b/src/main/java/com/likelion/dub/domain/dto/OAuth/KakaoApiClient.java new file mode 100644 index 0000000..cdac14c --- /dev/null +++ b/src/main/java/com/likelion/dub/domain/dto/OAuth/KakaoApiClient.java @@ -0,0 +1,69 @@ +package com.likelion.dub.domain.dto.OAuth; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +@Component +@RequiredArgsConstructor +public class KakaoApiClient implements OAuthApiClient { + + private static final String GRANT_TYPE = "authorization_code"; + + @Value("${oauth.kakao.url.auth}") + private String authUrl; + + @Value("${oauth.kakao.url.api}") + private String apiUrl; + + @Value("${oauth.kakao.client-id}") + private String clientId; + + private final RestTemplate restTemplate; + + @Override + public OAuthProvider oAuthProvider() { + return OAuthProvider.KAKAO; + } + + @Override + public String requestAccessToken(OAuthLoginParams params) { + String url = authUrl + "/oauth/token"; + + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + MultiValueMap body = params.makeBody(); + body.add("grant_type", GRANT_TYPE); + body.add("client_id", clientId); + + HttpEntity request = new HttpEntity<>(body, httpHeaders); + + KakaoTokens response = restTemplate.postForObject(url, request, KakaoTokens.class); + + assert response != null; + return response.getAccessToken(); + } + + @Override + public OAuthInfoResponse requestOauthInfo(String accessToken) { + String url = apiUrl + "/v2/user/me"; + + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + httpHeaders.set("Authorization", "Bearer " + accessToken); + + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("property_keys", "[\"kakao_account.email\", \"kakao_account.profile\"]"); + + HttpEntity request = new HttpEntity<>(body, httpHeaders); + + return restTemplate.postForObject(url, request, KakaoInfoResponse.class); + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/dub/domain/dto/OAuth/KakaoInfoResponse.java b/src/main/java/com/likelion/dub/domain/dto/OAuth/KakaoInfoResponse.java new file mode 100644 index 0000000..49059ec --- /dev/null +++ b/src/main/java/com/likelion/dub/domain/dto/OAuth/KakaoInfoResponse.java @@ -0,0 +1,41 @@ +package com.likelion.dub.domain.dto.OAuth; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; + +@Getter +@JsonIgnoreProperties(ignoreUnknown = true) +public class KakaoInfoResponse implements OAuthInfoResponse { + + @JsonProperty("kakao_account") + private KakaoAccount kakaoAccount; + + @Getter + @JsonIgnoreProperties(ignoreUnknown = true) + static class KakaoAccount { + private KakaoProfile profile; + private String email; + } + + @Getter + @JsonIgnoreProperties(ignoreUnknown = true) + static class KakaoProfile { + private String nickname; + } + + @Override + public String getEmail() { + return kakaoAccount.email; + } + + @Override + public String getNickname() { + return kakaoAccount.profile.nickname; + } + + @Override + public OAuthProvider getOAuthProvider() { + return OAuthProvider.KAKAO; + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/dub/domain/dto/OAuth/KakaoLoginParams.java b/src/main/java/com/likelion/dub/domain/dto/OAuth/KakaoLoginParams.java new file mode 100644 index 0000000..c287efb --- /dev/null +++ b/src/main/java/com/likelion/dub/domain/dto/OAuth/KakaoLoginParams.java @@ -0,0 +1,26 @@ +package com.likelion.dub.domain.dto.OAuth; + + +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +@Getter +@NoArgsConstructor +public class KakaoLoginParams implements OAuthLoginParams { + + private String authorizationCode; + + @Override + public OAuthProvider oAuthProvider() { + return OAuthProvider.KAKAO; + } + @Override + public MultiValueMap makeBody() { + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("code", authorizationCode); + return body; + } + +} diff --git a/src/main/java/com/likelion/dub/domain/dto/OAuth/KakaoTokens.java b/src/main/java/com/likelion/dub/domain/dto/OAuth/KakaoTokens.java new file mode 100644 index 0000000..1bfb940 --- /dev/null +++ b/src/main/java/com/likelion/dub/domain/dto/OAuth/KakaoTokens.java @@ -0,0 +1,28 @@ +package com.likelion.dub.domain.dto.OAuth; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class KakaoTokens { + + @JsonProperty("access_token") + private String accessToken; + + @JsonProperty("token_type") + private String tokenType; + + @JsonProperty("refresh_token") + private String refreshToken; + + @JsonProperty("expires_in") + private String expiresIn; + + @JsonProperty("refresh_token_expires_in") + private String refreshTokenExpiresIn; + + @JsonProperty("scope") + private String scope; +} \ No newline at end of file diff --git a/src/main/java/com/likelion/dub/domain/dto/OAuth/OAuthApiClient.java b/src/main/java/com/likelion/dub/domain/dto/OAuth/OAuthApiClient.java new file mode 100644 index 0000000..0f35d2a --- /dev/null +++ b/src/main/java/com/likelion/dub/domain/dto/OAuth/OAuthApiClient.java @@ -0,0 +1,7 @@ +package com.likelion.dub.domain.dto.OAuth; + +public interface OAuthApiClient { + OAuthProvider oAuthProvider(); + String requestAccessToken(OAuthLoginParams params); + OAuthInfoResponse requestOauthInfo(String accessToken); +} \ No newline at end of file diff --git a/src/main/java/com/likelion/dub/domain/dto/OAuth/OAuthInfoResponse.java b/src/main/java/com/likelion/dub/domain/dto/OAuth/OAuthInfoResponse.java new file mode 100644 index 0000000..f45773c --- /dev/null +++ b/src/main/java/com/likelion/dub/domain/dto/OAuth/OAuthInfoResponse.java @@ -0,0 +1,7 @@ +package com.likelion.dub.domain.dto.OAuth; + +public interface OAuthInfoResponse { + String getEmail(); + String getNickname(); + OAuthProvider getOAuthProvider(); +} \ No newline at end of file diff --git a/src/main/java/com/likelion/dub/domain/dto/OAuth/OAuthLoginParams.java b/src/main/java/com/likelion/dub/domain/dto/OAuth/OAuthLoginParams.java new file mode 100644 index 0000000..a7ae5da --- /dev/null +++ b/src/main/java/com/likelion/dub/domain/dto/OAuth/OAuthLoginParams.java @@ -0,0 +1,8 @@ +package com.likelion.dub.domain.dto.OAuth; + +import org.springframework.util.MultiValueMap; + +public interface OAuthLoginParams { + OAuthProvider oAuthProvider(); + MultiValueMap makeBody(); +} diff --git a/src/main/java/com/likelion/dub/domain/dto/OAuth/OAuthProvider.java b/src/main/java/com/likelion/dub/domain/dto/OAuth/OAuthProvider.java new file mode 100644 index 0000000..c159837 --- /dev/null +++ b/src/main/java/com/likelion/dub/domain/dto/OAuth/OAuthProvider.java @@ -0,0 +1,6 @@ +package com.likelion.dub.domain.dto.OAuth; + +public enum OAuthProvider { + KAKAO, NAVER +} + diff --git a/src/main/java/com/likelion/dub/domain/dto/OAuth/RequestOAuthInfoService.java b/src/main/java/com/likelion/dub/domain/dto/OAuth/RequestOAuthInfoService.java new file mode 100644 index 0000000..bb934ba --- /dev/null +++ b/src/main/java/com/likelion/dub/domain/dto/OAuth/RequestOAuthInfoService.java @@ -0,0 +1,24 @@ +package com.likelion.dub.domain.dto.OAuth; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.springframework.stereotype.Component; + +@Component +public class RequestOAuthInfoService { + private final Map clients; + + public RequestOAuthInfoService(List clients) { + this.clients = clients.stream().collect( + Collectors.toUnmodifiableMap(OAuthApiClient::oAuthProvider, Function.identity()) + ); + } + + public OAuthInfoResponse request(OAuthLoginParams params) { + OAuthApiClient client = clients.get(params.oAuthProvider()); + String accessToken = client.requestAccessToken(params); + return client.requestOauthInfo(accessToken); + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/dub/exception/AppException.java b/src/main/java/com/likelion/dub/exception/AppException.java deleted file mode 100644 index 0e63314..0000000 --- a/src/main/java/com/likelion/dub/exception/AppException.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.likelion.dub.exception; - - -import lombok.AllArgsConstructor; -import lombok.Getter; - -@AllArgsConstructor -@Getter -public class AppException extends RuntimeException{ - private Errorcode errorCode; - - - - -} diff --git a/src/main/java/com/likelion/dub/exception/Errorcode.java b/src/main/java/com/likelion/dub/exception/Errorcode.java deleted file mode 100644 index 93306df..0000000 --- a/src/main/java/com/likelion/dub/exception/Errorcode.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.likelion.dub.exception; - -import lombok.AllArgsConstructor; -import lombok.Getter; -import org.springframework.http.HttpStatus; - -@AllArgsConstructor -@Getter -public enum Errorcode { - - //Member - //이메일 중복 체크 - EMAIL_CHECK_COMPLETE(HttpStatus.OK,1000,"이메일 사용 가능"), - EMAIL_DUPLICATED(HttpStatus.CONFLICT, 2000, "중복된 이메일이 있습니다"), - - //학번 중복 체크 - STU_NUM_CHECK_COMPLETE(HttpStatus.OK,1000,"학번 사용 가능"), - STU_NUM_DUPLICATED(HttpStatus.CONFLICT, 2000, "중복된 학번이 있습니다"), - - //회원가입 - JOIN_COMPLETE(HttpStatus.OK, 1000, "회원 가입 완료"), - JOIN_FAILED(HttpStatus.OK, 2000, "회원 가입 실패"), - - //로그인 - LOGIN_COMPLETE(HttpStatus.OK, 1000, "로그인 완료"), - LOGIN_FAILED(HttpStatus.OK, 2000, "로그인 실패"), - - - - //Post - - - //Club - - //Admin - - SUCCESS(HttpStatus.OK, 1000, "OK"), - USERNAME_DUPLICATED(HttpStatus.CONFLICT,2000, "중복된 이름이 있습니다."), - INVALID_PASSWORD(HttpStatus.UNAUTHORIZED, 2001,"패스워드가 맞지 않습니다."), - CLUB_EXIST(HttpStatus.CONFLICT, 3000,"이미 작성한 글이 있습니다." ), - ID_DOES_NOT_EXIST(HttpStatus.CONFLICT, 3001,"글이 존재하지 않습니다.") - ; - - private HttpStatus httpStatus; - private Integer code; - private String message; - - -} diff --git a/src/main/java/com/likelion/dub/exception/ExceptionManager.java b/src/main/java/com/likelion/dub/exception/ExceptionManager.java deleted file mode 100644 index cb7d77b..0000000 --- a/src/main/java/com/likelion/dub/exception/ExceptionManager.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.likelion.dub.exception; - -import com.fasterxml.jackson.databind.ser.Serializers; -import com.likelion.dub.common.BaseException; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.RestControllerAdvice; - -@RestControllerAdvice -public class ExceptionManager { - @ExceptionHandler(BaseException.class) - public ResponseEntity baseExceptionHandler(BaseException e) { - return ResponseEntity.status(HttpStatus.OK) - .body(e.getStatus() + " "+e.getMessage()); - - } - -} diff --git a/src/main/java/com/likelion/dub/service/MemberService.java b/src/main/java/com/likelion/dub/service/MemberService.java index fd4f276..ac6f20f 100644 --- a/src/main/java/com/likelion/dub/service/MemberService.java +++ b/src/main/java/com/likelion/dub/service/MemberService.java @@ -5,18 +5,20 @@ import com.amazonaws.services.s3.model.ObjectMetadata; import com.likelion.dub.common.BaseException; import com.likelion.dub.common.BaseResponseStatus; -import com.likelion.dub.domain.*; - +import com.likelion.dub.configuration.JwtTokenUtil; +import com.likelion.dub.domain.Club; +import com.likelion.dub.domain.Member; import com.likelion.dub.domain.dto.GetMemberInfoResponse; +import com.likelion.dub.domain.dto.OAuth.OAuthInfoResponse; +import com.likelion.dub.domain.dto.OAuth.OAuthLoginParams; +import com.likelion.dub.domain.dto.OAuth.RequestOAuthInfoService; import com.likelion.dub.repository.ClubRepository; import com.likelion.dub.repository.MemberRepository; - -import com.likelion.dub.configuration.JwtTokenUtil; -import jakarta.persistence.EntityNotFoundException; +import java.io.IOException; +import java.util.Optional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; - import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; @@ -24,10 +26,6 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; -import java.io.IOException; - -import java.util.Optional; - @Service @@ -42,6 +40,8 @@ public class MemberService { private final AmazonS3Client amazonS3Client; + private final RequestOAuthInfoService requestOAuthInfoService; + @Value("${cloud.aws.s3.bucket}") private String bucket; @@ -156,6 +156,29 @@ public String login(String email, String password) throws BaseException { } + + public String loginKakao(OAuthLoginParams params) { + OAuthInfoResponse oAuthInfoResponse = requestOAuthInfoService.request(params); + Long memberId = findOrCreateMember(oAuthInfoResponse); + Member member = memberRepository.findById(memberId).orElseThrow(); + return JwtTokenUtil.createToken(member.getEmail(),member.getRole(),member.getName(), key, expireTimeMs); + } + + private Long findOrCreateMember(OAuthInfoResponse oAuthInfoResponse) { + return memberRepository.findByEmail(oAuthInfoResponse.getEmail()) + .map(Member::getId) + .orElseGet(() -> newMember(oAuthInfoResponse)); + } + + private Long newMember(OAuthInfoResponse oAuthInfoResponse) { + Member member = new Member(); + member.setEmail(oAuthInfoResponse.getEmail()); + member.setName(oAuthInfoResponse.getNickname()); + return memberRepository.save(member).getId(); + } + + + public GetMemberInfoResponse getInfo() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); String email = authentication.getName(); diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 140f0f6..b2247d9 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,11 +1,11 @@ -# MySQL ?? ?? +# MySQL spring.datasource.url=jdbc:mysql://localhost:3306/hihi spring.datasource.username=root spring.datasource.password=whqkr44## spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver -# Hibernate ?? +# Hibernate spring.jpa.show-sql=true spring.jpa.hibernate.ddl-auto=create spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect @@ -23,4 +23,16 @@ cloud.aws.s3.bucket=dubs3 cloud.aws.stack.auto=false cloud.aws.region.static=ap-northeast-2 cloud.aws.credentials.accessKey=AKIAVNQICB3FCRQNW4PO -cloud.aws.credentials.secretKey=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 \ No newline at end of file +cloud.aws.credentials.secretKey=IDXJGDRngkZS9H1vBpWjl+OYdQvGK5mwNR2e6ZSL + + +# Kakao OAuth ?? +oauth.kakao.client-id=adb755f1f1d479bf65f4eaadf6ec483b +oauth.kakao.url.auth=https://kauth.kakao.com +oauth.kakao.url.api=https://kapi.kakao.com + +# Naver OAuth ?? +oauth.naver.secret=QmzgfY82j9 +oauth.naver.client-id=UCJ4dA_B8TDcKXUT3FJv +oauth.naver.url.auth=https://nid.naver.com +oauth.naver.url.api=https://openapi.naver.com \ No newline at end of file From b187e2b3c71c4e75e945fdfd06e2f5d7b80f0077 Mon Sep 17 00:00:00 2001 From: suhoon Date: Mon, 25 Sep 2023 15:05:11 +0900 Subject: [PATCH 55/72] =?UTF-8?q?feat:=20thymeleaf=20=EB=B0=A9=EC=8B=9D?= =?UTF-8?q?=EC=9D=98=20=EC=B9=B4=EC=B9=B4=EC=98=A4=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 9 +++- .../dub/controller/JspController.java | 47 +++++++++++++++--- .../dub/controller/MemberController.java | 1 + .../domain/dto/OAuth/KakaoLoginParams.java | 3 ++ .../likelion/dub/service/MemberService.java | 1 - .../RequestOAuthInfoService.java | 6 ++- .../image/kakao_login_medium_narrow.png | Bin 0 -> 2946 bytes src/main/resources/templates/loginView.html | 9 ++-- 8 files changed, 62 insertions(+), 14 deletions(-) rename src/main/java/com/likelion/dub/{domain/dto/OAuth => service}/RequestOAuthInfoService.java (74%) create mode 100644 src/main/resources/static/image/kakao_login_medium_narrow.png diff --git a/build.gradle b/build.gradle index abecc44..c9292d4 100644 --- a/build.gradle +++ b/build.gradle @@ -19,8 +19,11 @@ repositories { } dependencies { - + // json 요청 + implementation group: 'org.json', name: 'json', version: '20210307' // spring + implementation 'org.springframework:spring-web' + // spring boot developmentOnly 'org.springframework.boot:spring-boot-devtools' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' @@ -47,6 +50,10 @@ dependencies { // test testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' + + //thymeleaf + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf:3.0.1' + } tasks.named('test') { diff --git a/src/main/java/com/likelion/dub/controller/JspController.java b/src/main/java/com/likelion/dub/controller/JspController.java index d4881fb..09df229 100644 --- a/src/main/java/com/likelion/dub/controller/JspController.java +++ b/src/main/java/com/likelion/dub/controller/JspController.java @@ -3,27 +3,58 @@ import com.likelion.dub.domain.dto.MemberJoinRequest; import com.likelion.dub.service.MemberService; -import org.springframework.beans.factory.annotation.Autowired; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.GetMapping; - -import java.util.function.BiFunction; -import java.util.function.Function; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.client.RestTemplate; @Controller +@RequiredArgsConstructor public class JspController { - @Autowired - private MemberService memberService; + + private final MemberService memberService; + private final RestTemplate restTemplate; + @GetMapping("/login") - public String loginView(Model model) { + public String loginView() { String result = "loginVIew"; - model.addAttribute("result", result); + return "loginView"; } + @GetMapping("/redirect") + public @ResponseBody String kakaoCallback(String code) { + String url = "http://localhost:8080/app/member/loginKakao"; + + // 요청 바디 생성 + MultiValueMap requestBody = new LinkedMultiValueMap<>(); + requestBody.add("authorizationCode", code); + + // HTTP 헤더 설정 + HttpHeaders headers = new HttpHeaders(); + System.out.println(code); + headers.setContentType(MediaType.APPLICATION_JSON); + // JSONObject를 JSON 문자열로 변환하여 출력 + System.out.println(requestBody.toString()); + // HttpEntity 생성 + HttpEntity> requestEntity = new HttpEntity<>(requestBody, headers); + + + System.out.println("HttpEntity: " + requestEntity); + // restTemplate을 사용하여 JSON 요청 보내기 + String response = restTemplate.postForObject(url, requestEntity, String.class); + return "Bearer " + response; + } + @GetMapping("/main") public String mainView(Model model) { String result = "mainView"; diff --git a/src/main/java/com/likelion/dub/controller/MemberController.java b/src/main/java/com/likelion/dub/controller/MemberController.java index 35e3904..eea951a 100644 --- a/src/main/java/com/likelion/dub/controller/MemberController.java +++ b/src/main/java/com/likelion/dub/controller/MemberController.java @@ -114,6 +114,7 @@ public BaseResponse login(@RequestBody MemberLoginRequest dto) { @PostMapping("/loginKakao") public BaseResponse loginKakao(@RequestBody KakaoLoginParams params) { try { + System.out.println(params.getAuthorizationCode()); String token = memberService.loginKakao(params); return new BaseResponse<>(BaseResponseStatus.SUCCESS, "Bearer " + token); } catch (BaseException e) { diff --git a/src/main/java/com/likelion/dub/domain/dto/OAuth/KakaoLoginParams.java b/src/main/java/com/likelion/dub/domain/dto/OAuth/KakaoLoginParams.java index c287efb..e09b87b 100644 --- a/src/main/java/com/likelion/dub/domain/dto/OAuth/KakaoLoginParams.java +++ b/src/main/java/com/likelion/dub/domain/dto/OAuth/KakaoLoginParams.java @@ -3,13 +3,16 @@ import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.Setter; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @Getter +@Setter @NoArgsConstructor public class KakaoLoginParams implements OAuthLoginParams { + private String authorizationCode; @Override diff --git a/src/main/java/com/likelion/dub/service/MemberService.java b/src/main/java/com/likelion/dub/service/MemberService.java index ac6f20f..164563d 100644 --- a/src/main/java/com/likelion/dub/service/MemberService.java +++ b/src/main/java/com/likelion/dub/service/MemberService.java @@ -11,7 +11,6 @@ import com.likelion.dub.domain.dto.GetMemberInfoResponse; import com.likelion.dub.domain.dto.OAuth.OAuthInfoResponse; import com.likelion.dub.domain.dto.OAuth.OAuthLoginParams; -import com.likelion.dub.domain.dto.OAuth.RequestOAuthInfoService; import com.likelion.dub.repository.ClubRepository; import com.likelion.dub.repository.MemberRepository; import java.io.IOException; diff --git a/src/main/java/com/likelion/dub/domain/dto/OAuth/RequestOAuthInfoService.java b/src/main/java/com/likelion/dub/service/RequestOAuthInfoService.java similarity index 74% rename from src/main/java/com/likelion/dub/domain/dto/OAuth/RequestOAuthInfoService.java rename to src/main/java/com/likelion/dub/service/RequestOAuthInfoService.java index bb934ba..d8e9a7c 100644 --- a/src/main/java/com/likelion/dub/domain/dto/OAuth/RequestOAuthInfoService.java +++ b/src/main/java/com/likelion/dub/service/RequestOAuthInfoService.java @@ -1,5 +1,9 @@ -package com.likelion.dub.domain.dto.OAuth; +package com.likelion.dub.service; +import com.likelion.dub.domain.dto.OAuth.OAuthApiClient; +import com.likelion.dub.domain.dto.OAuth.OAuthInfoResponse; +import com.likelion.dub.domain.dto.OAuth.OAuthLoginParams; +import com.likelion.dub.domain.dto.OAuth.OAuthProvider; import java.util.List; import java.util.Map; import java.util.function.Function; diff --git a/src/main/resources/static/image/kakao_login_medium_narrow.png b/src/main/resources/static/image/kakao_login_medium_narrow.png new file mode 100644 index 0000000000000000000000000000000000000000..09bb358843a08576d0029ff8427120c9f5f5c36d GIT binary patch literal 2946 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 swC^J>2!{xpAaAe;Fon)X-38(Q1DqPh - loginView + My Spring-Thymeleaf Project -

+ + + + + From ff8647d615cd6b3ffa001eb3e6d844c5f3d35e76 Mon Sep 17 00:00:00 2001 From: suhoon Date: Mon, 25 Sep 2023 18:12:34 +0900 Subject: [PATCH 56/72] =?UTF-8?q?feat:=20thymeleaf=20=EB=B0=A9=EC=8B=9D=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dub/controller/JspController.java | 43 +++++-------------- src/main/resources/application.properties | 2 +- src/main/resources/templates/mainView.html | 35 +++++++++++++-- 3 files changed, 43 insertions(+), 37 deletions(-) diff --git a/src/main/java/com/likelion/dub/controller/JspController.java b/src/main/java/com/likelion/dub/controller/JspController.java index 09df229..01f7ff6 100644 --- a/src/main/java/com/likelion/dub/controller/JspController.java +++ b/src/main/java/com/likelion/dub/controller/JspController.java @@ -4,19 +4,17 @@ import com.likelion.dub.domain.dto.MemberJoinRequest; import com.likelion.dub.service.MemberService; import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; +import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.client.RestTemplate; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; @Controller @RequiredArgsConstructor +@CrossOrigin(origins = "*", allowedHeaders = "*") //Cors 제거 public class JspController { private final MemberService memberService; @@ -26,39 +24,18 @@ public class JspController { @GetMapping("/login") public String loginView() { - String result = "loginVIew"; - return "loginView"; } @GetMapping("/redirect") - public @ResponseBody String kakaoCallback(String code) { - String url = "http://localhost:8080/app/member/loginKakao"; - - // 요청 바디 생성 - MultiValueMap requestBody = new LinkedMultiValueMap<>(); - requestBody.add("authorizationCode", code); - - // HTTP 헤더 설정 - HttpHeaders headers = new HttpHeaders(); - System.out.println(code); - headers.setContentType(MediaType.APPLICATION_JSON); - // JSONObject를 JSON 문자열로 변환하여 출력 - System.out.println(requestBody.toString()); - // HttpEntity 생성 - HttpEntity> requestEntity = new HttpEntity<>(requestBody, headers); - - - System.out.println("HttpEntity: " + requestEntity); - // restTemplate을 사용하여 JSON 요청 보내기 - String response = restTemplate.postForObject(url, requestEntity, String.class); - return "Bearer " + response; + public String kakaoCallback(@RequestParam("code") String code, RedirectAttributes redirectAttributes) { + // code 값을 main 페이지로 전달 + redirectAttributes.addFlashAttribute("authorizationCode", code); + return "redirect:/mainView"; // mainView 페이지로 리다이렉트 } - @GetMapping("/main") - public String mainView(Model model) { - String result = "mainView"; - model.addAttribute("result", result); + @GetMapping("/mainView") + public String mainView() { return "mainView"; } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index b2247d9..ad1571a 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -27,7 +27,7 @@ cloud.aws.credentials.secretKey=IDXJGDRngkZS9H1vBpWjl+OYdQvGK5mwNR2e6ZSL # Kakao OAuth ?? -oauth.kakao.client-id=adb755f1f1d479bf65f4eaadf6ec483b +oauth.kakao.client-id=7f3162e5e7ea72180f4d8a7494e6f05c oauth.kakao.url.auth=https://kauth.kakao.com oauth.kakao.url.api=https://kapi.kakao.com diff --git a/src/main/resources/templates/mainView.html b/src/main/resources/templates/mainView.html index bd9b240..af07e40 100644 --- a/src/main/resources/templates/mainView.html +++ b/src/main/resources/templates/mainView.html @@ -1,11 +1,40 @@ - + - mainView + Title -

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + From 40c4bbb9ae8cbeeea562d30636a4734e7e2a2bb3 Mon Sep 17 00:00:00 2001 From: suhoon Date: Mon, 25 Sep 2023 18:16:00 +0900 Subject: [PATCH 57/72] =?UTF-8?q?fix:=20jsp=20controller=20url=20=EC=A3=BC?= =?UTF-8?q?=EC=86=8C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/likelion/dub/controller/JspController.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/likelion/dub/controller/JspController.java b/src/main/java/com/likelion/dub/controller/JspController.java index 01f7ff6..fcbc2c2 100644 --- a/src/main/java/com/likelion/dub/controller/JspController.java +++ b/src/main/java/com/likelion/dub/controller/JspController.java @@ -8,6 +8,7 @@ import org.springframework.ui.Model; import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.client.RestTemplate; import org.springframework.web.servlet.mvc.support.RedirectAttributes; @@ -15,6 +16,7 @@ @Controller @RequiredArgsConstructor @CrossOrigin(origins = "*", allowedHeaders = "*") //Cors 제거 +@RequestMapping("/app/jsp") public class JspController { private final MemberService memberService; From b6034712e238b2e1c8906a1e3f8c4232f9b65d4a Mon Sep 17 00:00:00 2001 From: suhoon Date: Thu, 28 Sep 2023 03:25:33 +0900 Subject: [PATCH 58/72] =?UTF-8?q?feat:=20=EC=B9=B4=EC=B9=B4=EC=98=A4=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dub/controller/JspController.java | 2 +- .../domain/dto/OAuth/KakaoLoginParams.java | 2 + .../dub/controller/MemberControllerTest.java | 53 ++++++++++++++++++ .../likelion/dub/service/PostServiceTest.java | 56 +++---------------- 4 files changed, 65 insertions(+), 48 deletions(-) create mode 100644 src/test/java/com/likelion/dub/controller/MemberControllerTest.java diff --git a/src/main/java/com/likelion/dub/controller/JspController.java b/src/main/java/com/likelion/dub/controller/JspController.java index fcbc2c2..84aae1c 100644 --- a/src/main/java/com/likelion/dub/controller/JspController.java +++ b/src/main/java/com/likelion/dub/controller/JspController.java @@ -24,7 +24,7 @@ public class JspController { - @GetMapping("/login") + @RequestMapping("/login") public String loginView() { return "loginView"; } diff --git a/src/main/java/com/likelion/dub/domain/dto/OAuth/KakaoLoginParams.java b/src/main/java/com/likelion/dub/domain/dto/OAuth/KakaoLoginParams.java index e09b87b..b21cdd3 100644 --- a/src/main/java/com/likelion/dub/domain/dto/OAuth/KakaoLoginParams.java +++ b/src/main/java/com/likelion/dub/domain/dto/OAuth/KakaoLoginParams.java @@ -1,6 +1,7 @@ package com.likelion.dub.domain.dto.OAuth; +import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -10,6 +11,7 @@ @Getter @Setter @NoArgsConstructor +@AllArgsConstructor public class KakaoLoginParams implements OAuthLoginParams { diff --git a/src/test/java/com/likelion/dub/controller/MemberControllerTest.java b/src/test/java/com/likelion/dub/controller/MemberControllerTest.java new file mode 100644 index 0000000..617c144 --- /dev/null +++ b/src/test/java/com/likelion/dub/controller/MemberControllerTest.java @@ -0,0 +1,53 @@ +package com.likelion.dub.controller; + + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.likelion.dub.domain.dto.OAuth.KakaoLoginParams; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + + +@SpringBootTest +@AutoConfigureMockMvc +@Transactional +class MemberControllerTest { + + + @Autowired + protected MockMvc mvc; + @Autowired + protected ObjectMapper objectMapper; + + private static final String authorizationCode = "4lKRW_1z0BdjGGEQrYSoAZY_hUZt1BmoxebFeR44jqjww27vcmVgKZpCvik3odo2dmxWgwopyWAAAAGK19gfBQ"; + + @Test + public void 카카오_로그인_회원가입() throws Exception { + //given + KakaoLoginParams params = new KakaoLoginParams(authorizationCode); + //when + ResultActions resultActions = mvc.perform(post("/app/member/loginKakao") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(params)) + .accept(MediaType.APPLICATION_JSON)) + .andDo(print()); + + //then + resultActions.andExpect(status().isOk()).andExpect(jsonPath("$.is_Success").value(true)) + .andExpect(jsonPath("$.code").value(1000)); + + + } + + +} \ No newline at end of file diff --git a/src/test/java/com/likelion/dub/service/PostServiceTest.java b/src/test/java/com/likelion/dub/service/PostServiceTest.java index a08d62c..b127ed8 100644 --- a/src/test/java/com/likelion/dub/service/PostServiceTest.java +++ b/src/test/java/com/likelion/dub/service/PostServiceTest.java @@ -1,59 +1,21 @@ package com.likelion.dub.service; -import com.likelion.dub.common.BaseException; -import com.likelion.dub.common.BaseResponse; -import com.likelion.dub.domain.Club; -import com.likelion.dub.domain.Member; -import com.likelion.dub.domain.Post; -import com.likelion.dub.repository.MemberRepository; -import com.likelion.dub.repository.PostRepository; -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.transaction.Transactional; import org.springframework.beans.factory.annotation.Autowired; - -import org.springframework.boot.autoconfigure.couchbase.CouchbaseProperties; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; - -import org.springframework.mock.web.MockMultipartFile; -import org.springframework.security.test.context.support.WithMockUser; -import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.multipart.MultipartFile; - -import java.io.IOException; -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.*; +import org.springframework.test.web.servlet.MockMvc; @SpringBootTest +@AutoConfigureMockMvc @Transactional class PostServiceTest { - @Autowired MemberRepository memberRepository; - - - @Autowired MemberService memberService; - - @Autowired PostRepository postRepository; - - @Autowired PostService postService; - - - @BeforeEach - void createClub(){ - MockMultipartFile file = new MockMultipartFile("file", "test.txt", "text/plain", "Test file content.".getBytes()); - memberService.joinClub("suhoon@naver.com", "name", "password", "gender", "CLUB","introduction","groupName","category",file); - } - - @Test - @WithMockUser(username = "suhoon@naver.com", roles = "CLUB") - void testWritePost() throws BaseException, IOException { + @Autowired + protected MockMvc mvc; + @Autowired + protected ObjectMapper objectMapper; - } } \ No newline at end of file From ad7ae35025f341bfa6cd604fdfc5a97c05251d30 Mon Sep 17 00:00:00 2001 From: suhoon Date: Thu, 28 Sep 2023 03:26:36 +0900 Subject: [PATCH 59/72] =?UTF-8?q?docs:=20=EC=B9=B4=EC=B9=B4=EC=98=A4=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=A3=BC=EC=84=9D=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dub/controller/MemberController.java | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/likelion/dub/controller/MemberController.java b/src/main/java/com/likelion/dub/controller/MemberController.java index eea951a..4816e70 100644 --- a/src/main/java/com/likelion/dub/controller/MemberController.java +++ b/src/main/java/com/likelion/dub/controller/MemberController.java @@ -24,8 +24,6 @@ import org.springframework.web.multipart.MultipartFile; - - @RestController @RequestMapping("/app/member") @RequiredArgsConstructor @@ -37,6 +35,7 @@ public class MemberController { /** * 이메일 중복체크 + * * @param email * @return */ @@ -56,6 +55,7 @@ public BaseResponse checkEmail(@PathVariable String email) { /** * 일반회원가입 + * * @param dto * @return */ @@ -63,7 +63,8 @@ public BaseResponse checkEmail(@PathVariable String email) { public BaseResponse join(@RequestBody MemberJoinRequest dto) { try { - memberService.join(dto.getEmail(), dto.getName(), dto.getPassword(), dto.getGender(), dto.getRole()); + memberService.join(dto.getEmail(), dto.getName(), dto.getPassword(), dto.getGender(), + dto.getRole()); String result = "(일반)회원 가입 완료"; return new BaseResponse<>(BaseResponseStatus.SUCCESS, result); } catch (BaseException e) { @@ -73,15 +74,20 @@ public BaseResponse join(@RequestBody MemberJoinRequest dto) { /** * 동아리회원 회원가입 + * * @param dto * @param file * @return */ - @PostMapping(value = "/sign-up-club", consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE}) - public BaseResponse joinClub(@RequestPart(value = "json") ClubMemberJoinRequest dto, @RequestPart(value = "image", required = false) MultipartFile file) { + @PostMapping(value = "/sign-up-club", consumes = {MediaType.APPLICATION_JSON_VALUE, + MediaType.MULTIPART_FORM_DATA_VALUE}) + public BaseResponse joinClub(@RequestPart(value = "json") ClubMemberJoinRequest dto, + @RequestPart(value = "image", required = false) MultipartFile file) { try { - memberService.joinClub(dto.getEmail(), dto.getName(), dto.getPassword(), dto.getGender(), dto.getRole(), dto.getIntroduction(), dto.getGroupName(), dto.getCategory(), file); + memberService.joinClub(dto.getEmail(), dto.getName(), dto.getPassword(), + dto.getGender(), dto.getRole(), dto.getIntroduction(), dto.getGroupName(), + dto.getCategory(), file); String result = "(동아리)회원 가입 완료"; return new BaseResponse<>(BaseResponseStatus.SUCCESS, result); @@ -94,6 +100,7 @@ public BaseResponse joinClub(@RequestPart(value = "json") ClubMemberJoin /** * 로그인 + * * @param dto * @return */ @@ -110,7 +117,12 @@ public BaseResponse login(@RequestBody MemberLoginRequest dto) { } - + /** + * 카카오 로그인 + * + * @param params + * @return + */ @PostMapping("/loginKakao") public BaseResponse loginKakao(@RequestBody KakaoLoginParams params) { try { @@ -123,9 +135,9 @@ public BaseResponse loginKakao(@RequestBody KakaoLoginParams params) { } - /** * 회원정보 조회 + * * @return */ @GetMapping("/getInfo") @@ -141,13 +153,15 @@ public BaseResponse getInfo() { /** * 비밀번호 수정 + * * @param changePwdRequest * @return */ @PutMapping("/changePwd") public BaseResponse changePwd(@RequestBody ChangePwdRequest changePwdRequest) { try { - memberService.changePassword(changePwdRequest.getCurrentPassword(), changePwdRequest.getNewPassword()); + memberService.changePassword(changePwdRequest.getCurrentPassword(), + changePwdRequest.getNewPassword()); String result = "비밀번호 수정 완료"; return new BaseResponse<>(BaseResponseStatus.SUCCESS, result); } catch (BaseException e) { From 344c0b15bd6c94d077adf1208077ae4f2343c2a2 Mon Sep 17 00:00:00 2001 From: suhoon Date: Thu, 28 Sep 2023 16:09:48 +0900 Subject: [PATCH 60/72] docs: readme update --- README.md | 48 +++++++++++++++++++----------------------------- 1 file changed, 19 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 8b08b43..bb50914 100644 --- a/README.md +++ b/README.md @@ -9,58 +9,48 @@ --- +# 핵심기능 -# 핵심기능 +#### 1. 회원 가입 +```일반 회원과 동아리 회원으로 나누어 가입이 가능합니다.``` -1. 회원 가입 +#### 2. 동아리 등록 - ```일반 회원과 동아리 회원으로 나누어 가입이 가능합니다.``` +```동아리 회원은 동아리의 프로필 이미지, 태그, 소개글 등을 등록할 수 있습니다.``` +#### 3. 동아리 공고 및 지원서 양식 등록 -2. 동아리 등록 +```동아리 회원은 동아리 내에서 공고를 올릴 수 있으며, 지원자들에게 지원서 양식을 제공할 수 있습니다.``` +#### 4. 태그별 동아리 조회 - ```동아리 회원은 동아리의 프로필 이미지, 태그, 소개글 등을 등록할 수 있습니다.``` +```사용자는 태그별로 동아리를 검색하고 조회할 수 있습니다.``` +#### 5. 동아리 지원서 다운로드 및 제출 -3. 동아리 공고 및 지원서 양식 등록 +```일반 회원은 동아리의 지원서 양식을 다운로드하여 작성한 후, 해당 동아리에 제출할 수 있습니다.``` +#### 6. 동아리 회원별 지원자 조회 - ```동아리 회원은 동아리 내에서 공고를 올릴 수 있으며, 지원자들에게 지원서 양식을 제공할 수 있습니다.``` +```동아리 회원은 자신의 동아리에 지원한 회원들을 조회할 수 있습니다.``` +#### 7. 합격/불합격 통보 보내기 -4. 태그별 동아리 조회 - - - ```사용자는 태그별로 동아리를 검색하고 조회할 수 있습니다.``` - - -5. 동아리 지원서 다운로드 및 제출 - - - ```일반 회원은 동아리의 지원서 양식을 다운로드하여 작성한 후, 해당 동아리에 제출할 수 있습니다.``` - - -6. 동아리 회원별 지원자 조회 - - - ```동아리 회원은 자신의 동아리에 지원한 회원들을 조회할 수 있습니다.``` - - -7. 합격/불합격 통보 보내기 +```동아리 회원은 지원자들에게 합격 또는 불합격 통보를 보낼 수 있습니다.``` +--- - ```동아리 회원은 지원자들에게 합격 또는 불합격 통보를 보낼 수 있습니다.``` +# System Architecture +--- +# CI/CD Flow --- - # ERD - ![db설계 최종](https://github.com/s2hoon/dub_club_wanted_BE/assets/82464990/532c6e73-719f-4645-a798-1fe47da878c3) From 3b03b94750adaf88d8d4a2e8bd69da108ebf231e Mon Sep 17 00:00:00 2001 From: suhoon Date: Sun, 1 Oct 2023 01:17:26 +0900 Subject: [PATCH 61/72] =?UTF-8?q?chore:=20dto=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EB=B3=84=EB=A1=9C=20=EB=82=98=EB=88=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dub/controller/ClubController.java | 12 ++-- .../dub/controller/JspController.java | 6 +- .../dub/controller/MemberController.java | 14 ++--- .../dub/controller/PostController.java | 55 ++++++++++++------- .../dto/{ => Club}/UpdateTagRequest.java | 3 +- .../dto/{ => Member}/ChangePwdRequest.java | 3 +- .../{ => Member}/ClubMemberJoinRequest.java | 6 +- .../{ => Member}/GetMemberInfoResponse.java | 4 +- .../dto/{ => Member}/MemberJoinRequest.java | 5 +- .../dto/{ => Member}/MemberLoginRequest.java | 2 +- .../dto/{ => Post}/GetAllPostResponse.java | 2 +- .../dto/{ => Post}/GetOnePostResponse.java | 6 +- .../dto/{ => Post}/PostEditRequest.java | 3 +- .../dto/{ => Post}/PostWritingRequest.java | 9 +-- .../domain/dto/{ => Post}/WritingRequest.java | 4 +- .../likelion/dub/service/MemberService.java | 31 ++++++----- .../com/likelion/dub/service/PostService.java | 27 ++++----- 17 files changed, 96 insertions(+), 96 deletions(-) rename src/main/java/com/likelion/dub/domain/dto/{ => Club}/UpdateTagRequest.java (86%) rename src/main/java/com/likelion/dub/domain/dto/{ => Member}/ChangePwdRequest.java (86%) rename src/main/java/com/likelion/dub/domain/dto/{ => Member}/ClubMemberJoinRequest.java (91%) rename src/main/java/com/likelion/dub/domain/dto/{ => Member}/GetMemberInfoResponse.java (83%) rename src/main/java/com/likelion/dub/domain/dto/{ => Member}/MemberJoinRequest.java (91%) rename src/main/java/com/likelion/dub/domain/dto/{ => Member}/MemberLoginRequest.java (86%) rename src/main/java/com/likelion/dub/domain/dto/{ => Post}/GetAllPostResponse.java (88%) rename src/main/java/com/likelion/dub/domain/dto/{ => Post}/GetOnePostResponse.java (85%) rename src/main/java/com/likelion/dub/domain/dto/{ => Post}/PostEditRequest.java (87%) rename src/main/java/com/likelion/dub/domain/dto/{ => Post}/PostWritingRequest.java (66%) rename src/main/java/com/likelion/dub/domain/dto/{ => Post}/WritingRequest.java (90%) diff --git a/src/main/java/com/likelion/dub/controller/ClubController.java b/src/main/java/com/likelion/dub/controller/ClubController.java index 5df1e30..cc7c58f 100644 --- a/src/main/java/com/likelion/dub/controller/ClubController.java +++ b/src/main/java/com/likelion/dub/controller/ClubController.java @@ -1,14 +1,18 @@ package com.likelion.dub.controller; -import com.fasterxml.jackson.databind.ser.Serializers; import com.likelion.dub.common.BaseException; import com.likelion.dub.common.BaseResponse; import com.likelion.dub.common.BaseResponseStatus; -import com.likelion.dub.domain.dto.UpdateTagRequest; +import com.likelion.dub.domain.dto.Club.UpdateTagRequest; import com.likelion.dub.service.ClubService; import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @RestController @@ -57,7 +61,7 @@ public BaseResponse uploadClubImage(@ModelAttribute MultipartFile image) @PostMapping("/updateTag") public BaseResponse updateTag(@RequestBody UpdateTagRequest updateTagRequest) { - try{ + try { clubService.updateTag(updateTagRequest.getGroupName(), updateTagRequest.getCategory()); String result = "동아리 태그 등록 완료"; return new BaseResponse<>(BaseResponseStatus.SUCCESS, result); diff --git a/src/main/java/com/likelion/dub/controller/JspController.java b/src/main/java/com/likelion/dub/controller/JspController.java index 84aae1c..882ea11 100644 --- a/src/main/java/com/likelion/dub/controller/JspController.java +++ b/src/main/java/com/likelion/dub/controller/JspController.java @@ -1,7 +1,7 @@ package com.likelion.dub.controller; -import com.likelion.dub.domain.dto.MemberJoinRequest; +import com.likelion.dub.domain.dto.Member.MemberJoinRequest; import com.likelion.dub.service.MemberService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Controller; @@ -23,14 +23,14 @@ public class JspController { private final RestTemplate restTemplate; - @RequestMapping("/login") public String loginView() { return "loginView"; } @GetMapping("/redirect") - public String kakaoCallback(@RequestParam("code") String code, RedirectAttributes redirectAttributes) { + public String kakaoCallback(@RequestParam("code") String code, + RedirectAttributes redirectAttributes) { // code 값을 main 페이지로 전달 redirectAttributes.addFlashAttribute("authorizationCode", code); return "redirect:/mainView"; // mainView 페이지로 리다이렉트 diff --git a/src/main/java/com/likelion/dub/controller/MemberController.java b/src/main/java/com/likelion/dub/controller/MemberController.java index 4816e70..d7c3377 100644 --- a/src/main/java/com/likelion/dub/controller/MemberController.java +++ b/src/main/java/com/likelion/dub/controller/MemberController.java @@ -3,11 +3,11 @@ import com.likelion.dub.common.BaseException; import com.likelion.dub.common.BaseResponse; import com.likelion.dub.common.BaseResponseStatus; -import com.likelion.dub.domain.dto.ChangePwdRequest; -import com.likelion.dub.domain.dto.ClubMemberJoinRequest; -import com.likelion.dub.domain.dto.GetMemberInfoResponse; -import com.likelion.dub.domain.dto.MemberJoinRequest; -import com.likelion.dub.domain.dto.MemberLoginRequest; +import com.likelion.dub.domain.dto.Member.ChangePwdRequest; +import com.likelion.dub.domain.dto.Member.ClubMemberJoinRequest; +import com.likelion.dub.domain.dto.Member.GetMemberInfoResponse; +import com.likelion.dub.domain.dto.Member.MemberJoinRequest; +import com.likelion.dub.domain.dto.Member.MemberLoginRequest; import com.likelion.dub.domain.dto.OAuth.KakaoLoginParams; import com.likelion.dub.service.MemberService; import lombok.RequiredArgsConstructor; @@ -41,7 +41,6 @@ public class MemberController { */ @GetMapping("/email/{email}") public BaseResponse checkEmail(@PathVariable String email) { - boolean isEmailAvailable = memberService.checkEmail(email); if (isEmailAvailable) { String result = "이메일 사용 가능"; @@ -62,7 +61,6 @@ public BaseResponse checkEmail(@PathVariable String email) { @PostMapping("/sign-up") public BaseResponse join(@RequestBody MemberJoinRequest dto) { try { - memberService.join(dto.getEmail(), dto.getName(), dto.getPassword(), dto.getGender(), dto.getRole()); String result = "(일반)회원 가입 완료"; @@ -83,7 +81,6 @@ public BaseResponse join(@RequestBody MemberJoinRequest dto) { MediaType.MULTIPART_FORM_DATA_VALUE}) public BaseResponse joinClub(@RequestPart(value = "json") ClubMemberJoinRequest dto, @RequestPart(value = "image", required = false) MultipartFile file) { - try { memberService.joinClub(dto.getEmail(), dto.getName(), dto.getPassword(), dto.getGender(), dto.getRole(), dto.getIntroduction(), dto.getGroupName(), @@ -106,7 +103,6 @@ public BaseResponse joinClub(@RequestPart(value = "json") ClubMemberJoin */ @PostMapping("/sign-in") public BaseResponse login(@RequestBody MemberLoginRequest dto) { - try { String token = memberService.login(dto.getEmail(), dto.getPassword()); return new BaseResponse<>(BaseResponseStatus.SUCCESS, "Bearer " + token); diff --git a/src/main/java/com/likelion/dub/controller/PostController.java b/src/main/java/com/likelion/dub/controller/PostController.java index dbe04fe..e6407ef 100644 --- a/src/main/java/com/likelion/dub/controller/PostController.java +++ b/src/main/java/com/likelion/dub/controller/PostController.java @@ -3,40 +3,51 @@ import com.likelion.dub.common.BaseException; import com.likelion.dub.common.BaseResponse; import com.likelion.dub.common.BaseResponseStatus; -import com.likelion.dub.domain.Post; -import com.likelion.dub.domain.dto.*; +import com.likelion.dub.domain.dto.Post.GetAllPostResponse; +import com.likelion.dub.domain.dto.Post.GetOnePostResponse; +import com.likelion.dub.domain.dto.Post.PostEditRequest; +import com.likelion.dub.domain.dto.Post.WritingRequest; import com.likelion.dub.service.PostService; +import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.MediaType; - import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; -import java.io.IOException; -import java.util.List; - @RestController @RequestMapping("/app/post") @RequiredArgsConstructor @CrossOrigin(origins = "*", allowedHeaders = "*") //Cors 제거 @Slf4j public class PostController { + private final PostService postService; /** * 동아리글 전체 조회 + * * @return */ @GetMapping("/getAll") public BaseResponse> getAllPost() { - try{ + try { List getAllPostResponses = postService.getAllPost(); - return new BaseResponse<>(BaseResponseStatus.SUCCESS,getAllPostResponses); - }catch(BaseException e){ + return new BaseResponse<>(BaseResponseStatus.SUCCESS, getAllPostResponses); + } catch (BaseException e) { return new BaseResponse<>(e.getStatus()); } } @@ -44,35 +55,38 @@ public BaseResponse> getAllPost() { /** * post 작성 + * * @param writingRequest * @return */ - @PostMapping(value = "/write-post",consumes = {MediaType.MULTIPART_FORM_DATA_VALUE}) + @PostMapping(value = "/write-post", consumes = {MediaType.MULTIPART_FORM_DATA_VALUE}) public BaseResponse writePost(@ModelAttribute WritingRequest writingRequest) { try { - String title = writingRequest.getTitle(); - String content = writingRequest.getContent(); + String title = writingRequest.getTitle(); + String content = writingRequest.getContent(); MultipartFile file = writingRequest.getImage(); postService.writing(title, content, file); - return new BaseResponse<>(BaseResponseStatus.SUCCESS,"글 작성 성공"); + return new BaseResponse<>(BaseResponseStatus.SUCCESS, "글 작성 성공"); } catch (BaseException e) { return new BaseResponse<>(e.getStatus()); } } + /** * post 보기 + * * @param id * @return */ @GetMapping("/read-post/{id}") public BaseResponse readPost(@PathVariable Long id) throws BaseException { - try{ + try { GetOnePostResponse getOnePostResponse = postService.readPost(id); return new BaseResponse<>(BaseResponseStatus.SUCCESS, getOnePostResponse); - }catch(BaseException e){ + } catch (BaseException e) { return new BaseResponse<>(e.getStatus()); } @@ -80,13 +94,15 @@ public BaseResponse readPost(@PathVariable Long id) throws B @DeleteMapping("delete-post") - public BaseResponse deletePost(@RequestParam(value="id") Long id) { + public BaseResponse deletePost(@RequestParam(value = "id") Long id) { postService.deletePost(id); String result = "동아리 게시글 삭제 완료"; - return new BaseResponse<>(BaseResponseStatus.SUCCESS,result); + return new BaseResponse<>(BaseResponseStatus.SUCCESS, result); } + @PutMapping("/edit-post") - public BaseResponse editPost(@RequestPart(value="json") PostEditRequest dto, @RequestPart(value="images", required = false) List images) { + public BaseResponse editPost(@RequestPart(value = "json") PostEditRequest dto, + @RequestPart(value = "images", required = false) List images) { String newTitle = dto.getTitle(); String newContent = dto.getContent(); int newCategory = dto.getCategory(); @@ -98,7 +114,6 @@ public BaseResponse editPost(@RequestPart(value="json") PostEditRequest } String email = authentication.getName(); - return new BaseResponse<>(BaseResponseStatus.SUCCESS); } diff --git a/src/main/java/com/likelion/dub/domain/dto/UpdateTagRequest.java b/src/main/java/com/likelion/dub/domain/dto/Club/UpdateTagRequest.java similarity index 86% rename from src/main/java/com/likelion/dub/domain/dto/UpdateTagRequest.java rename to src/main/java/com/likelion/dub/domain/dto/Club/UpdateTagRequest.java index 9122bdc..79054c2 100644 --- a/src/main/java/com/likelion/dub/domain/dto/UpdateTagRequest.java +++ b/src/main/java/com/likelion/dub/domain/dto/Club/UpdateTagRequest.java @@ -1,4 +1,4 @@ -package com.likelion.dub.domain.dto; +package com.likelion.dub.domain.dto.Club; import com.fasterxml.jackson.annotation.JsonProperty; @@ -8,6 +8,7 @@ @Getter @AllArgsConstructor public class UpdateTagRequest { + @JsonProperty private String groupName; diff --git a/src/main/java/com/likelion/dub/domain/dto/ChangePwdRequest.java b/src/main/java/com/likelion/dub/domain/dto/Member/ChangePwdRequest.java similarity index 86% rename from src/main/java/com/likelion/dub/domain/dto/ChangePwdRequest.java rename to src/main/java/com/likelion/dub/domain/dto/Member/ChangePwdRequest.java index 4fd8de1..3995aea 100644 --- a/src/main/java/com/likelion/dub/domain/dto/ChangePwdRequest.java +++ b/src/main/java/com/likelion/dub/domain/dto/Member/ChangePwdRequest.java @@ -1,4 +1,4 @@ -package com.likelion.dub.domain.dto; +package com.likelion.dub.domain.dto.Member; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; @@ -15,5 +15,4 @@ public class ChangePwdRequest { private String newPassword; - } diff --git a/src/main/java/com/likelion/dub/domain/dto/ClubMemberJoinRequest.java b/src/main/java/com/likelion/dub/domain/dto/Member/ClubMemberJoinRequest.java similarity index 91% rename from src/main/java/com/likelion/dub/domain/dto/ClubMemberJoinRequest.java rename to src/main/java/com/likelion/dub/domain/dto/Member/ClubMemberJoinRequest.java index 9711002..a48058d 100644 --- a/src/main/java/com/likelion/dub/domain/dto/ClubMemberJoinRequest.java +++ b/src/main/java/com/likelion/dub/domain/dto/Member/ClubMemberJoinRequest.java @@ -1,4 +1,4 @@ -package com.likelion.dub.domain.dto; +package com.likelion.dub.domain.dto.Member; import com.fasterxml.jackson.annotation.JsonProperty; @@ -7,8 +7,6 @@ import lombok.AllArgsConstructor; import lombok.Data; -import java.util.List; - @Data @AllArgsConstructor public class ClubMemberJoinRequest { @@ -36,6 +34,4 @@ public class ClubMemberJoinRequest { private String introduction; - - } diff --git a/src/main/java/com/likelion/dub/domain/dto/GetMemberInfoResponse.java b/src/main/java/com/likelion/dub/domain/dto/Member/GetMemberInfoResponse.java similarity index 83% rename from src/main/java/com/likelion/dub/domain/dto/GetMemberInfoResponse.java rename to src/main/java/com/likelion/dub/domain/dto/Member/GetMemberInfoResponse.java index 967a312..8fa8412 100644 --- a/src/main/java/com/likelion/dub/domain/dto/GetMemberInfoResponse.java +++ b/src/main/java/com/likelion/dub/domain/dto/Member/GetMemberInfoResponse.java @@ -1,8 +1,7 @@ -package com.likelion.dub.domain.dto; +package com.likelion.dub.domain.dto.Member; import com.likelion.dub.domain.Club; -import jakarta.persistence.Column; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @@ -13,6 +12,7 @@ @Setter @NoArgsConstructor public class GetMemberInfoResponse { + private String email; private String name; diff --git a/src/main/java/com/likelion/dub/domain/dto/MemberJoinRequest.java b/src/main/java/com/likelion/dub/domain/dto/Member/MemberJoinRequest.java similarity index 91% rename from src/main/java/com/likelion/dub/domain/dto/MemberJoinRequest.java rename to src/main/java/com/likelion/dub/domain/dto/Member/MemberJoinRequest.java index cdb7188..c550864 100644 --- a/src/main/java/com/likelion/dub/domain/dto/MemberJoinRequest.java +++ b/src/main/java/com/likelion/dub/domain/dto/Member/MemberJoinRequest.java @@ -1,4 +1,4 @@ -package com.likelion.dub.domain.dto; +package com.likelion.dub.domain.dto.Member; import com.fasterxml.jackson.annotation.JsonProperty; @@ -11,6 +11,7 @@ @Getter @NoArgsConstructor public class MemberJoinRequest { + @JsonProperty private String email; @JsonProperty @@ -24,6 +25,4 @@ public class MemberJoinRequest { private String role; - - } diff --git a/src/main/java/com/likelion/dub/domain/dto/MemberLoginRequest.java b/src/main/java/com/likelion/dub/domain/dto/Member/MemberLoginRequest.java similarity index 86% rename from src/main/java/com/likelion/dub/domain/dto/MemberLoginRequest.java rename to src/main/java/com/likelion/dub/domain/dto/Member/MemberLoginRequest.java index ec5e55a..dd9e3e5 100644 --- a/src/main/java/com/likelion/dub/domain/dto/MemberLoginRequest.java +++ b/src/main/java/com/likelion/dub/domain/dto/Member/MemberLoginRequest.java @@ -1,4 +1,4 @@ -package com.likelion.dub.domain.dto; +package com.likelion.dub.domain.dto.Member; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/src/main/java/com/likelion/dub/domain/dto/GetAllPostResponse.java b/src/main/java/com/likelion/dub/domain/dto/Post/GetAllPostResponse.java similarity index 88% rename from src/main/java/com/likelion/dub/domain/dto/GetAllPostResponse.java rename to src/main/java/com/likelion/dub/domain/dto/Post/GetAllPostResponse.java index 0d19ffb..f6f9c29 100644 --- a/src/main/java/com/likelion/dub/domain/dto/GetAllPostResponse.java +++ b/src/main/java/com/likelion/dub/domain/dto/Post/GetAllPostResponse.java @@ -1,4 +1,4 @@ -package com.likelion.dub.domain.dto; +package com.likelion.dub.domain.dto.Post; import lombok.AllArgsConstructor; diff --git a/src/main/java/com/likelion/dub/domain/dto/GetOnePostResponse.java b/src/main/java/com/likelion/dub/domain/dto/Post/GetOnePostResponse.java similarity index 85% rename from src/main/java/com/likelion/dub/domain/dto/GetOnePostResponse.java rename to src/main/java/com/likelion/dub/domain/dto/Post/GetOnePostResponse.java index 369c6e3..9a830a9 100644 --- a/src/main/java/com/likelion/dub/domain/dto/GetOnePostResponse.java +++ b/src/main/java/com/likelion/dub/domain/dto/Post/GetOnePostResponse.java @@ -1,14 +1,12 @@ -package com.likelion.dub.domain.dto; +package com.likelion.dub.domain.dto.Post; -import com.likelion.dub.domain.Comment; import jakarta.persistence.Lob; +import java.util.List; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import java.util.List; - @AllArgsConstructor @Getter @Setter diff --git a/src/main/java/com/likelion/dub/domain/dto/PostEditRequest.java b/src/main/java/com/likelion/dub/domain/dto/Post/PostEditRequest.java similarity index 87% rename from src/main/java/com/likelion/dub/domain/dto/PostEditRequest.java rename to src/main/java/com/likelion/dub/domain/dto/Post/PostEditRequest.java index 393b639..e10fc8c 100644 --- a/src/main/java/com/likelion/dub/domain/dto/PostEditRequest.java +++ b/src/main/java/com/likelion/dub/domain/dto/Post/PostEditRequest.java @@ -1,4 +1,4 @@ -package com.likelion.dub.domain.dto; +package com.likelion.dub.domain.dto.Post; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; @@ -7,6 +7,7 @@ @Getter @AllArgsConstructor public class PostEditRequest { + @JsonProperty private String title; @JsonProperty diff --git a/src/main/java/com/likelion/dub/domain/dto/PostWritingRequest.java b/src/main/java/com/likelion/dub/domain/dto/Post/PostWritingRequest.java similarity index 66% rename from src/main/java/com/likelion/dub/domain/dto/PostWritingRequest.java rename to src/main/java/com/likelion/dub/domain/dto/Post/PostWritingRequest.java index 6b07ba8..041cef4 100644 --- a/src/main/java/com/likelion/dub/domain/dto/PostWritingRequest.java +++ b/src/main/java/com/likelion/dub/domain/dto/Post/PostWritingRequest.java @@ -1,13 +1,9 @@ -package com.likelion.dub.domain.dto; +package com.likelion.dub.domain.dto.Post; import com.fasterxml.jackson.annotation.JsonProperty; -import jakarta.persistence.Column; import jakarta.persistence.Lob; import lombok.AllArgsConstructor; import lombok.Getter; -import org.springframework.web.multipart.MultipartFile; - -import java.util.List; @Getter @AllArgsConstructor @@ -22,7 +18,4 @@ public class PostWritingRequest { private String content; - - - } diff --git a/src/main/java/com/likelion/dub/domain/dto/WritingRequest.java b/src/main/java/com/likelion/dub/domain/dto/Post/WritingRequest.java similarity index 90% rename from src/main/java/com/likelion/dub/domain/dto/WritingRequest.java rename to src/main/java/com/likelion/dub/domain/dto/Post/WritingRequest.java index c6e1021..9f313f2 100644 --- a/src/main/java/com/likelion/dub/domain/dto/WritingRequest.java +++ b/src/main/java/com/likelion/dub/domain/dto/Post/WritingRequest.java @@ -1,4 +1,4 @@ -package com.likelion.dub.domain.dto; +package com.likelion.dub.domain.dto.Post; import jakarta.annotation.Nullable; @@ -21,6 +21,4 @@ public class WritingRequest { private MultipartFile image; - - } diff --git a/src/main/java/com/likelion/dub/service/MemberService.java b/src/main/java/com/likelion/dub/service/MemberService.java index 164563d..8c575f5 100644 --- a/src/main/java/com/likelion/dub/service/MemberService.java +++ b/src/main/java/com/likelion/dub/service/MemberService.java @@ -8,7 +8,7 @@ import com.likelion.dub.configuration.JwtTokenUtil; import com.likelion.dub.domain.Club; import com.likelion.dub.domain.Member; -import com.likelion.dub.domain.dto.GetMemberInfoResponse; +import com.likelion.dub.domain.dto.Member.GetMemberInfoResponse; import com.likelion.dub.domain.dto.OAuth.OAuthInfoResponse; import com.likelion.dub.domain.dto.OAuth.OAuthLoginParams; import com.likelion.dub.repository.ClubRepository; @@ -26,12 +26,12 @@ import org.springframework.web.multipart.MultipartFile; - @Service @Transactional @RequiredArgsConstructor @Slf4j public class MemberService { + private final MemberRepository memberRepository; private final ClubRepository clubRepository; @@ -45,7 +45,6 @@ public class MemberService { private String bucket; - @Value("${jwt.token.secret}") private String key; private Long expireTimeMs = 1000 * 60 * 60L; //1시간 @@ -57,9 +56,9 @@ public boolean checkEmail(String email) { } - /** * 일반회원 회원가입 + * * @param email * @param name * @param password @@ -86,6 +85,7 @@ public void join(String email, String name, String password, String gender, Stri /** * 동아리장 회원가입 + * * @param email * @param name * @param password @@ -96,7 +96,8 @@ public void join(String email, String name, String password, String gender, Stri * @param category * @param file */ - public void joinClub(String email, String name, String password, String gender, String role, String introduction, String groupName,String category , MultipartFile file) { + public void joinClub(String email, String name, String password, String gender, String role, + String introduction, String groupName, String category, MultipartFile file) { // 중복 이메일 검사 Optional existingMember = memberRepository.findByEmail(email); @@ -123,10 +124,10 @@ public void joinClub(String email, String name, String password, String gender, if (file != null) { // 프로필 사진 S3에 저장 String fileName = name + "_" + "ClubImage"; - ObjectMetadata metadata= new ObjectMetadata(); + ObjectMetadata metadata = new ObjectMetadata(); metadata.setContentType(file.getContentType()); metadata.setContentLength(file.getSize()); - amazonS3Client.putObject(bucket,fileName,file.getInputStream(),metadata); + amazonS3Client.putObject(bucket, fileName, file.getInputStream(), metadata); club.setClubImage("https://dubs3.s3.ap-northeast-2.amazonaws.com/" + fileName); } clubRepository.save(club); @@ -149,18 +150,19 @@ public String login(String email, String password) throws BaseException { throw new BaseException(BaseResponseStatus.WRONG_PASSWORD); } - String token = JwtTokenUtil.createToken(member.getEmail(),member.getRole(),member.getName(), key, expireTimeMs); + String token = JwtTokenUtil.createToken(member.getEmail(), member.getRole(), + member.getName(), key, expireTimeMs); return token; } - public String loginKakao(OAuthLoginParams params) { OAuthInfoResponse oAuthInfoResponse = requestOAuthInfoService.request(params); Long memberId = findOrCreateMember(oAuthInfoResponse); Member member = memberRepository.findById(memberId).orElseThrow(); - return JwtTokenUtil.createToken(member.getEmail(),member.getRole(),member.getName(), key, expireTimeMs); + return JwtTokenUtil.createToken(member.getEmail(), member.getRole(), member.getName(), key, + expireTimeMs); } private Long findOrCreateMember(OAuthInfoResponse oAuthInfoResponse) { @@ -177,11 +179,11 @@ private Long newMember(OAuthInfoResponse oAuthInfoResponse) { } - public GetMemberInfoResponse getInfo() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); String email = authentication.getName(); - Member member = memberRepository.findByEmail(email).orElseThrow(() -> new BaseException(BaseResponseStatus.NO_SUCH_MEMBER_EXIST)); + Member member = memberRepository.findByEmail(email) + .orElseThrow(() -> new BaseException(BaseResponseStatus.NO_SUCH_MEMBER_EXIST)); GetMemberInfoResponse getMemberInfoResponse = new GetMemberInfoResponse(); getMemberInfoResponse.setName(member.getName()); getMemberInfoResponse.setGender(member.getGender()); @@ -192,10 +194,11 @@ public GetMemberInfoResponse getInfo() { } - public void changePassword(String currentPassword,String newPassword) { + public void changePassword(String currentPassword, String newPassword) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); String email = authentication.getName(); - Member member = memberRepository.findByEmail(email).orElseThrow(() -> new BaseException(BaseResponseStatus.NO_SUCH_MEMBER_EXIST)); + Member member = memberRepository.findByEmail(email) + .orElseThrow(() -> new BaseException(BaseResponseStatus.NO_SUCH_MEMBER_EXIST)); if (bCryptPasswordEncoder.matches(currentPassword, member.getPassword())) { // 비밀번호가 일치할 때 처리 String hashedPassword = bCryptPasswordEncoder.encode(newPassword); diff --git a/src/main/java/com/likelion/dub/service/PostService.java b/src/main/java/com/likelion/dub/service/PostService.java index 4a3cece..8143d87 100644 --- a/src/main/java/com/likelion/dub/service/PostService.java +++ b/src/main/java/com/likelion/dub/service/PostService.java @@ -3,18 +3,18 @@ import com.amazonaws.services.s3.AmazonS3Client; import com.amazonaws.services.s3.model.ObjectMetadata; -import com.fasterxml.jackson.databind.ser.Serializers; import com.likelion.dub.common.BaseException; -import com.likelion.dub.common.BaseResponse; import com.likelion.dub.common.BaseResponseStatus; import com.likelion.dub.domain.Club; import com.likelion.dub.domain.Member; import com.likelion.dub.domain.Post; - -import com.likelion.dub.domain.dto.GetAllPostResponse; -import com.likelion.dub.domain.dto.GetOnePostResponse; +import com.likelion.dub.domain.dto.Post.GetAllPostResponse; +import com.likelion.dub.domain.dto.Post.GetOnePostResponse; import com.likelion.dub.repository.MemberRepository; import com.likelion.dub.repository.PostRepository; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -24,16 +24,12 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - @Service @Transactional @RequiredArgsConstructor @Slf4j public class PostService { + private final PostRepository postRepository; private final MemberRepository memberRepository; private final AmazonS3Client amazonS3Client; @@ -45,7 +41,7 @@ public class PostService { public List getAllPost() { List allPosts = postRepository.findAll(); - List getAllPostResponses =new ArrayList<>(); + List getAllPostResponses = new ArrayList<>(); for (Post post : allPosts) { GetAllPostResponse getAllPostResponse = new GetAllPostResponse(); @@ -64,7 +60,8 @@ public void writing(String title, String content, MultipartFile image) throws Ba Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); String email = authentication.getName(); - Member member = memberRepository.findByEmail(email).orElseThrow(() -> new BaseException(BaseResponseStatus.NO_SUCH_MEMBER_EXIST)); + Member member = memberRepository.findByEmail(email) + .orElseThrow(() -> new BaseException(BaseResponseStatus.NO_SUCH_MEMBER_EXIST)); Club club = member.getClub(); if (club == null) { throw new BaseException(BaseResponseStatus.NO_SUCH_CLUB_EXIST); @@ -86,7 +83,7 @@ public void writing(String title, String content, MultipartFile image) throws Ba } postRepository.save(post); - }catch(IOException e){ + } catch (IOException e) { throw new BaseException(BaseResponseStatus.FILE_SAVE_ERROR); } @@ -102,7 +99,8 @@ private void uploadPostImageToS3(String fileName, MultipartFile file) throws IOE public GetOnePostResponse readPost(Long id) throws BaseException { - Post post = postRepository.findById(id).orElseThrow(() -> new BaseException(BaseResponseStatus.NOT_EXISTS_POST)); + Post post = postRepository.findById(id) + .orElseThrow(() -> new BaseException(BaseResponseStatus.NOT_EXISTS_POST)); GetOnePostResponse getOnePostResponse = new GetOnePostResponse(); getOnePostResponse.setClubName(post.getClubName()); getOnePostResponse.setTitle(post.getTitle()); @@ -134,7 +132,6 @@ public void deletePost(Long id) throws BaseException { // 로그인 된 post Post member_post = postRepository.findByClubName(clubName).orElseThrow(); - //post id 가 같은지 검사, 같으면 삭제, 다르면 오류출력 if (member_post.getId() == id) { postRepository.deleteById(id); From 1cdbaf27fdcbd0226691843ff9b885edc6465c48 Mon Sep 17 00:00:00 2001 From: suhoon Date: Sun, 1 Oct 2023 23:51:31 +0900 Subject: [PATCH 62/72] =?UTF-8?q?chore:=20=ED=8F=B4=EB=8D=94=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 13 +++++++++- .../BaseException.java | 2 +- .../BaseResponse.java | 5 ++-- .../BaseResponseStatus.java | 14 ++++------- .../dub/controller/ClubController.java | 8 +++--- .../dub/controller/JspController.java | 2 +- .../dub/controller/MemberController.java | 18 ++++++------- .../dub/controller/PostController.java | 14 +++++------ .../java/com/likelion/dub/domain/Member.java | 6 ++--- .../dto/Club/UpdateTagRequest.java | 2 +- .../dto/Member/ChangePwdRequest.java | 2 +- .../dto/Member/ClubMemberJoinRequest.java | 2 +- .../dto/Member/GetMemberInfoResponse.java | 2 +- .../dto/Member/MemberJoinRequest.java | 2 +- .../dto/Member/MemberLoginRequest.java | 2 +- .../dto/OAuth/KakaoApiClient.java | 2 +- .../dto/OAuth/KakaoInfoResponse.java | 4 ++- .../dto/OAuth/KakaoLoginParams.java | 3 ++- .../{domain => }/dto/OAuth/KakaoTokens.java | 2 +- .../dto/OAuth/OAuthApiClient.java | 5 +++- .../dto/OAuth/OAuthInfoResponse.java | 5 +++- .../dto/OAuth/OAuthLoginParams.java | 4 ++- .../{domain => }/dto/OAuth/OAuthProvider.java | 2 +- .../dto/Post/GetAllPostResponse.java | 2 +- .../dto/Post/GetOnePostResponse.java | 2 +- .../dto/Post/PostEditRequest.java | 2 +- .../dto/Post/PostWritingRequest.java | 2 +- .../{domain => }/dto/Post/WritingRequest.java | 2 +- .../com/likelion/dub/service/ClubService.java | 25 +++++++++++-------- .../likelion/dub/service/MemberService.java | 10 ++++---- .../com/likelion/dub/service/PostService.java | 8 +++--- .../dub/service/RequestOAuthInfoService.java | 9 ++++--- .../dub/controller/MemberControllerTest.java | 2 +- 33 files changed, 104 insertions(+), 81 deletions(-) rename src/main/java/com/likelion/dub/{common => baseResponse}/BaseException.java (84%) rename src/main/java/com/likelion/dub/{common => baseResponse}/BaseResponse.java (90%) rename src/main/java/com/likelion/dub/{common => baseResponse}/BaseResponseStatus.java (80%) rename src/main/java/com/likelion/dub/{domain => }/dto/Club/UpdateTagRequest.java (86%) rename src/main/java/com/likelion/dub/{domain => }/dto/Member/ChangePwdRequest.java (86%) rename src/main/java/com/likelion/dub/{domain => }/dto/Member/ClubMemberJoinRequest.java (93%) rename src/main/java/com/likelion/dub/{domain => }/dto/Member/GetMemberInfoResponse.java (89%) rename src/main/java/com/likelion/dub/{domain => }/dto/Member/MemberJoinRequest.java (91%) rename src/main/java/com/likelion/dub/{domain => }/dto/Member/MemberLoginRequest.java (86%) rename src/main/java/com/likelion/dub/{domain => }/dto/OAuth/KakaoApiClient.java (98%) rename src/main/java/com/likelion/dub/{domain => }/dto/OAuth/KakaoInfoResponse.java (95%) rename src/main/java/com/likelion/dub/{domain => }/dto/OAuth/KakaoLoginParams.java (94%) rename src/main/java/com/likelion/dub/{domain => }/dto/OAuth/KakaoTokens.java (92%) rename src/main/java/com/likelion/dub/{domain => }/dto/OAuth/OAuthApiClient.java (81%) rename src/main/java/com/likelion/dub/{domain => }/dto/OAuth/OAuthInfoResponse.java (74%) rename src/main/java/com/likelion/dub/{domain => }/dto/OAuth/OAuthLoginParams.java (79%) rename src/main/java/com/likelion/dub/{domain => }/dto/OAuth/OAuthProvider.java (53%) rename src/main/java/com/likelion/dub/{domain => }/dto/Post/GetAllPostResponse.java (88%) rename src/main/java/com/likelion/dub/{domain => }/dto/Post/GetOnePostResponse.java (91%) rename src/main/java/com/likelion/dub/{domain => }/dto/Post/PostEditRequest.java (88%) rename src/main/java/com/likelion/dub/{domain => }/dto/Post/PostWritingRequest.java (88%) rename src/main/java/com/likelion/dub/{domain => }/dto/Post/WritingRequest.java (90%) diff --git a/README.md b/README.md index bb50914..e51e20b 100644 --- a/README.md +++ b/README.md @@ -41,18 +41,29 @@ --- -# System Architecture +# API 엔드포인트 목록 및 사용법 + +https://woozy-cuticle-bfb.notion.site/dub_-wanted-5f89e6bcf87142eca927893ff04703f6?pvs=4 + --- # CI/CD Flow +1. main branch 에 Push 또는 Merge +2. Github 에 작성해둔 workflow file 로 Github Actions 수행 +3. build, docker image build, docker image push 수행 +4. EC2 인스턴스에서 docker image pull 후, run + --- # ERD ![db설계 최종](https://github.com/s2hoon/dub_club_wanted_BE/assets/82464990/532c6e73-719f-4645-a798-1fe47da878c3) +--- + +# Project Structure --- diff --git a/src/main/java/com/likelion/dub/common/BaseException.java b/src/main/java/com/likelion/dub/baseResponse/BaseException.java similarity index 84% rename from src/main/java/com/likelion/dub/common/BaseException.java rename to src/main/java/com/likelion/dub/baseResponse/BaseException.java index 5c74c07..49d3557 100644 --- a/src/main/java/com/likelion/dub/common/BaseException.java +++ b/src/main/java/com/likelion/dub/baseResponse/BaseException.java @@ -1,4 +1,4 @@ -package com.likelion.dub.common; +package com.likelion.dub.baseResponse; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/src/main/java/com/likelion/dub/common/BaseResponse.java b/src/main/java/com/likelion/dub/baseResponse/BaseResponse.java similarity index 90% rename from src/main/java/com/likelion/dub/common/BaseResponse.java rename to src/main/java/com/likelion/dub/baseResponse/BaseResponse.java index 9b02e7c..38e64dc 100644 --- a/src/main/java/com/likelion/dub/common/BaseResponse.java +++ b/src/main/java/com/likelion/dub/baseResponse/BaseResponse.java @@ -1,12 +1,10 @@ -package com.likelion.dub.common; +package com.likelion.dub.baseResponse; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyOrder; import lombok.Getter; -import static com.likelion.dub.common.BaseResponseStatus.SUCCESS; - @Getter @JsonPropertyOrder({"isSuccess", "code", "message", "result"}) public class BaseResponse { @@ -21,6 +19,7 @@ public class BaseResponse { // 해당 필드가 null인 경우 JSON에 표현되지 않는다. @JsonInclude(JsonInclude.Include.NON_NULL) private T result; + // 요청 성공 public BaseResponse(BaseResponseStatus status, T result) { this(status); diff --git a/src/main/java/com/likelion/dub/common/BaseResponseStatus.java b/src/main/java/com/likelion/dub/baseResponse/BaseResponseStatus.java similarity index 80% rename from src/main/java/com/likelion/dub/common/BaseResponseStatus.java rename to src/main/java/com/likelion/dub/baseResponse/BaseResponseStatus.java index 4552e5e..cebff42 100644 --- a/src/main/java/com/likelion/dub/common/BaseResponseStatus.java +++ b/src/main/java/com/likelion/dub/baseResponse/BaseResponseStatus.java @@ -1,4 +1,4 @@ -package com.likelion.dub.common; +package com.likelion.dub.baseResponse; import lombok.Getter; @@ -10,7 +10,6 @@ public enum BaseResponseStatus { */ SUCCESS(true, 1000, "요청에 성공했습니다."), - /** * 2XXX : Common */ @@ -21,7 +20,7 @@ public enum BaseResponseStatus { /** * 3XXX : Member */ - INVALID_MEMBER_JWT(false,3000,"권한이 없는 회원의 접근입니다."), + INVALID_MEMBER_JWT(false, 3000, "권한이 없는 회원의 접근입니다."), EMPTY_PROFILE_IMAGE(false, 3001, "프로필 이미지를 입력해주세요."), JWT_TOKEN_ERROR(false, 3002, "jwt 토큰을 확인해주세요"), @@ -35,13 +34,12 @@ public enum BaseResponseStatus { NO_SUCH_MEMBER_EXIST(false, 3008, "존재하지 않는 회원입니다."), NO_SUCH_CLUB_EXIST(false, 3009, "존재하지 않는 동아리입니다."), - /** * 4XXX : Post */ - NOT_EXISTS_POST(false,4000,"게시물이 존재하지 않습니다."), - FAILED_GET_POST(false,4001,"게시물 조회에 실패하였습니다."), - NOT_EXISTS_TAG_NAME_POST(true,4002,"해당 태그를 가진 게시물이 없습니다."), + NOT_EXISTS_POST(false, 4000, "게시물이 존재하지 않습니다."), + FAILED_GET_POST(false, 4001, "게시물 조회에 실패하였습니다."), + NOT_EXISTS_TAG_NAME_POST(true, 4002, "해당 태그를 가진 게시물이 없습니다."), DELETE_FAIL_POST(false, 4003, "게시물 삭제에 실패하였습니다."); @@ -50,8 +48,6 @@ public enum BaseResponseStatus { */ - - private final boolean isSuccess; private final int code; private final String message; diff --git a/src/main/java/com/likelion/dub/controller/ClubController.java b/src/main/java/com/likelion/dub/controller/ClubController.java index cc7c58f..c8d5888 100644 --- a/src/main/java/com/likelion/dub/controller/ClubController.java +++ b/src/main/java/com/likelion/dub/controller/ClubController.java @@ -1,10 +1,10 @@ package com.likelion.dub.controller; -import com.likelion.dub.common.BaseException; -import com.likelion.dub.common.BaseResponse; -import com.likelion.dub.common.BaseResponseStatus; -import com.likelion.dub.domain.dto.Club.UpdateTagRequest; +import com.likelion.dub.baseResponse.BaseException; +import com.likelion.dub.baseResponse.BaseResponse; +import com.likelion.dub.baseResponse.BaseResponseStatus; +import com.likelion.dub.dto.Club.UpdateTagRequest; import com.likelion.dub.service.ClubService; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.CrossOrigin; diff --git a/src/main/java/com/likelion/dub/controller/JspController.java b/src/main/java/com/likelion/dub/controller/JspController.java index 882ea11..02c3a45 100644 --- a/src/main/java/com/likelion/dub/controller/JspController.java +++ b/src/main/java/com/likelion/dub/controller/JspController.java @@ -1,7 +1,7 @@ package com.likelion.dub.controller; -import com.likelion.dub.domain.dto.Member.MemberJoinRequest; +import com.likelion.dub.dto.Member.MemberJoinRequest; import com.likelion.dub.service.MemberService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Controller; diff --git a/src/main/java/com/likelion/dub/controller/MemberController.java b/src/main/java/com/likelion/dub/controller/MemberController.java index d7c3377..3abe3a2 100644 --- a/src/main/java/com/likelion/dub/controller/MemberController.java +++ b/src/main/java/com/likelion/dub/controller/MemberController.java @@ -1,14 +1,14 @@ package com.likelion.dub.controller; -import com.likelion.dub.common.BaseException; -import com.likelion.dub.common.BaseResponse; -import com.likelion.dub.common.BaseResponseStatus; -import com.likelion.dub.domain.dto.Member.ChangePwdRequest; -import com.likelion.dub.domain.dto.Member.ClubMemberJoinRequest; -import com.likelion.dub.domain.dto.Member.GetMemberInfoResponse; -import com.likelion.dub.domain.dto.Member.MemberJoinRequest; -import com.likelion.dub.domain.dto.Member.MemberLoginRequest; -import com.likelion.dub.domain.dto.OAuth.KakaoLoginParams; +import com.likelion.dub.baseResponse.BaseException; +import com.likelion.dub.baseResponse.BaseResponse; +import com.likelion.dub.baseResponse.BaseResponseStatus; +import com.likelion.dub.dto.Member.ChangePwdRequest; +import com.likelion.dub.dto.Member.ClubMemberJoinRequest; +import com.likelion.dub.dto.Member.GetMemberInfoResponse; +import com.likelion.dub.dto.Member.MemberJoinRequest; +import com.likelion.dub.dto.Member.MemberLoginRequest; +import com.likelion.dub.dto.OAuth.KakaoLoginParams; import com.likelion.dub.service.MemberService; import lombok.RequiredArgsConstructor; import org.springframework.http.MediaType; diff --git a/src/main/java/com/likelion/dub/controller/PostController.java b/src/main/java/com/likelion/dub/controller/PostController.java index e6407ef..0211f9f 100644 --- a/src/main/java/com/likelion/dub/controller/PostController.java +++ b/src/main/java/com/likelion/dub/controller/PostController.java @@ -1,12 +1,12 @@ package com.likelion.dub.controller; -import com.likelion.dub.common.BaseException; -import com.likelion.dub.common.BaseResponse; -import com.likelion.dub.common.BaseResponseStatus; -import com.likelion.dub.domain.dto.Post.GetAllPostResponse; -import com.likelion.dub.domain.dto.Post.GetOnePostResponse; -import com.likelion.dub.domain.dto.Post.PostEditRequest; -import com.likelion.dub.domain.dto.Post.WritingRequest; +import com.likelion.dub.baseResponse.BaseException; +import com.likelion.dub.baseResponse.BaseResponse; +import com.likelion.dub.baseResponse.BaseResponseStatus; +import com.likelion.dub.dto.Post.GetAllPostResponse; +import com.likelion.dub.dto.Post.GetOnePostResponse; +import com.likelion.dub.dto.Post.PostEditRequest; +import com.likelion.dub.dto.Post.WritingRequest; import com.likelion.dub.service.PostService; import java.util.List; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/likelion/dub/domain/Member.java b/src/main/java/com/likelion/dub/domain/Member.java index b48b672..a8038b1 100644 --- a/src/main/java/com/likelion/dub/domain/Member.java +++ b/src/main/java/com/likelion/dub/domain/Member.java @@ -23,8 +23,10 @@ @Setter @Table(name = "member") public class Member { + @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) //어플리케이션에서는 기본키 값을 미리 알수 없음, 엔티티를 저장하고 나서야 키 값 확인 기능 + @GeneratedValue(strategy = GenerationType.IDENTITY) + //어플리케이션에서는 기본키 값을 미리 알수 없음, 엔티티를 저장하고 나서야 키 값 확인 기능 @Column(name = "member_id") private Long id; @@ -32,7 +34,6 @@ public class Member { private Club club; - @OneToMany(mappedBy = "member") private List post = new ArrayList<>(); @@ -50,7 +51,6 @@ public class Member { private String role; - public void setClub(Club club) { this.club = club; } diff --git a/src/main/java/com/likelion/dub/domain/dto/Club/UpdateTagRequest.java b/src/main/java/com/likelion/dub/dto/Club/UpdateTagRequest.java similarity index 86% rename from src/main/java/com/likelion/dub/domain/dto/Club/UpdateTagRequest.java rename to src/main/java/com/likelion/dub/dto/Club/UpdateTagRequest.java index 79054c2..abcd366 100644 --- a/src/main/java/com/likelion/dub/domain/dto/Club/UpdateTagRequest.java +++ b/src/main/java/com/likelion/dub/dto/Club/UpdateTagRequest.java @@ -1,4 +1,4 @@ -package com.likelion.dub.domain.dto.Club; +package com.likelion.dub.dto.Club; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/src/main/java/com/likelion/dub/domain/dto/Member/ChangePwdRequest.java b/src/main/java/com/likelion/dub/dto/Member/ChangePwdRequest.java similarity index 86% rename from src/main/java/com/likelion/dub/domain/dto/Member/ChangePwdRequest.java rename to src/main/java/com/likelion/dub/dto/Member/ChangePwdRequest.java index 3995aea..b4a2ea7 100644 --- a/src/main/java/com/likelion/dub/domain/dto/Member/ChangePwdRequest.java +++ b/src/main/java/com/likelion/dub/dto/Member/ChangePwdRequest.java @@ -1,4 +1,4 @@ -package com.likelion.dub.domain.dto.Member; +package com.likelion.dub.dto.Member; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; diff --git a/src/main/java/com/likelion/dub/domain/dto/Member/ClubMemberJoinRequest.java b/src/main/java/com/likelion/dub/dto/Member/ClubMemberJoinRequest.java similarity index 93% rename from src/main/java/com/likelion/dub/domain/dto/Member/ClubMemberJoinRequest.java rename to src/main/java/com/likelion/dub/dto/Member/ClubMemberJoinRequest.java index a48058d..9b5aab4 100644 --- a/src/main/java/com/likelion/dub/domain/dto/Member/ClubMemberJoinRequest.java +++ b/src/main/java/com/likelion/dub/dto/Member/ClubMemberJoinRequest.java @@ -1,4 +1,4 @@ -package com.likelion.dub.domain.dto.Member; +package com.likelion.dub.dto.Member; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/src/main/java/com/likelion/dub/domain/dto/Member/GetMemberInfoResponse.java b/src/main/java/com/likelion/dub/dto/Member/GetMemberInfoResponse.java similarity index 89% rename from src/main/java/com/likelion/dub/domain/dto/Member/GetMemberInfoResponse.java rename to src/main/java/com/likelion/dub/dto/Member/GetMemberInfoResponse.java index 8fa8412..89b2c63 100644 --- a/src/main/java/com/likelion/dub/domain/dto/Member/GetMemberInfoResponse.java +++ b/src/main/java/com/likelion/dub/dto/Member/GetMemberInfoResponse.java @@ -1,4 +1,4 @@ -package com.likelion.dub.domain.dto.Member; +package com.likelion.dub.dto.Member; import com.likelion.dub.domain.Club; diff --git a/src/main/java/com/likelion/dub/domain/dto/Member/MemberJoinRequest.java b/src/main/java/com/likelion/dub/dto/Member/MemberJoinRequest.java similarity index 91% rename from src/main/java/com/likelion/dub/domain/dto/Member/MemberJoinRequest.java rename to src/main/java/com/likelion/dub/dto/Member/MemberJoinRequest.java index c550864..77b9584 100644 --- a/src/main/java/com/likelion/dub/domain/dto/Member/MemberJoinRequest.java +++ b/src/main/java/com/likelion/dub/dto/Member/MemberJoinRequest.java @@ -1,4 +1,4 @@ -package com.likelion.dub.domain.dto.Member; +package com.likelion.dub.dto.Member; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/src/main/java/com/likelion/dub/domain/dto/Member/MemberLoginRequest.java b/src/main/java/com/likelion/dub/dto/Member/MemberLoginRequest.java similarity index 86% rename from src/main/java/com/likelion/dub/domain/dto/Member/MemberLoginRequest.java rename to src/main/java/com/likelion/dub/dto/Member/MemberLoginRequest.java index dd9e3e5..2426f5f 100644 --- a/src/main/java/com/likelion/dub/domain/dto/Member/MemberLoginRequest.java +++ b/src/main/java/com/likelion/dub/dto/Member/MemberLoginRequest.java @@ -1,4 +1,4 @@ -package com.likelion.dub.domain.dto.Member; +package com.likelion.dub.dto.Member; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/src/main/java/com/likelion/dub/domain/dto/OAuth/KakaoApiClient.java b/src/main/java/com/likelion/dub/dto/OAuth/KakaoApiClient.java similarity index 98% rename from src/main/java/com/likelion/dub/domain/dto/OAuth/KakaoApiClient.java rename to src/main/java/com/likelion/dub/dto/OAuth/KakaoApiClient.java index cdac14c..9485729 100644 --- a/src/main/java/com/likelion/dub/domain/dto/OAuth/KakaoApiClient.java +++ b/src/main/java/com/likelion/dub/dto/OAuth/KakaoApiClient.java @@ -1,4 +1,4 @@ -package com.likelion.dub.domain.dto.OAuth; +package com.likelion.dub.dto.OAuth; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; diff --git a/src/main/java/com/likelion/dub/domain/dto/OAuth/KakaoInfoResponse.java b/src/main/java/com/likelion/dub/dto/OAuth/KakaoInfoResponse.java similarity index 95% rename from src/main/java/com/likelion/dub/domain/dto/OAuth/KakaoInfoResponse.java rename to src/main/java/com/likelion/dub/dto/OAuth/KakaoInfoResponse.java index 49059ec..a3ec807 100644 --- a/src/main/java/com/likelion/dub/domain/dto/OAuth/KakaoInfoResponse.java +++ b/src/main/java/com/likelion/dub/dto/OAuth/KakaoInfoResponse.java @@ -1,4 +1,4 @@ -package com.likelion.dub.domain.dto.OAuth; +package com.likelion.dub.dto.OAuth; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; @@ -14,6 +14,7 @@ public class KakaoInfoResponse implements OAuthInfoResponse { @Getter @JsonIgnoreProperties(ignoreUnknown = true) static class KakaoAccount { + private KakaoProfile profile; private String email; } @@ -21,6 +22,7 @@ static class KakaoAccount { @Getter @JsonIgnoreProperties(ignoreUnknown = true) static class KakaoProfile { + private String nickname; } diff --git a/src/main/java/com/likelion/dub/domain/dto/OAuth/KakaoLoginParams.java b/src/main/java/com/likelion/dub/dto/OAuth/KakaoLoginParams.java similarity index 94% rename from src/main/java/com/likelion/dub/domain/dto/OAuth/KakaoLoginParams.java rename to src/main/java/com/likelion/dub/dto/OAuth/KakaoLoginParams.java index b21cdd3..01119cf 100644 --- a/src/main/java/com/likelion/dub/domain/dto/OAuth/KakaoLoginParams.java +++ b/src/main/java/com/likelion/dub/dto/OAuth/KakaoLoginParams.java @@ -1,4 +1,4 @@ -package com.likelion.dub.domain.dto.OAuth; +package com.likelion.dub.dto.OAuth; import lombok.AllArgsConstructor; @@ -21,6 +21,7 @@ public class KakaoLoginParams implements OAuthLoginParams { public OAuthProvider oAuthProvider() { return OAuthProvider.KAKAO; } + @Override public MultiValueMap makeBody() { MultiValueMap body = new LinkedMultiValueMap<>(); diff --git a/src/main/java/com/likelion/dub/domain/dto/OAuth/KakaoTokens.java b/src/main/java/com/likelion/dub/dto/OAuth/KakaoTokens.java similarity index 92% rename from src/main/java/com/likelion/dub/domain/dto/OAuth/KakaoTokens.java rename to src/main/java/com/likelion/dub/dto/OAuth/KakaoTokens.java index 1bfb940..6356b21 100644 --- a/src/main/java/com/likelion/dub/domain/dto/OAuth/KakaoTokens.java +++ b/src/main/java/com/likelion/dub/dto/OAuth/KakaoTokens.java @@ -1,4 +1,4 @@ -package com.likelion.dub.domain.dto.OAuth; +package com.likelion.dub.dto.OAuth; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Getter; diff --git a/src/main/java/com/likelion/dub/domain/dto/OAuth/OAuthApiClient.java b/src/main/java/com/likelion/dub/dto/OAuth/OAuthApiClient.java similarity index 81% rename from src/main/java/com/likelion/dub/domain/dto/OAuth/OAuthApiClient.java rename to src/main/java/com/likelion/dub/dto/OAuth/OAuthApiClient.java index 0f35d2a..c19ab52 100644 --- a/src/main/java/com/likelion/dub/domain/dto/OAuth/OAuthApiClient.java +++ b/src/main/java/com/likelion/dub/dto/OAuth/OAuthApiClient.java @@ -1,7 +1,10 @@ -package com.likelion.dub.domain.dto.OAuth; +package com.likelion.dub.dto.OAuth; public interface OAuthApiClient { + OAuthProvider oAuthProvider(); + String requestAccessToken(OAuthLoginParams params); + OAuthInfoResponse requestOauthInfo(String accessToken); } \ No newline at end of file diff --git a/src/main/java/com/likelion/dub/domain/dto/OAuth/OAuthInfoResponse.java b/src/main/java/com/likelion/dub/dto/OAuth/OAuthInfoResponse.java similarity index 74% rename from src/main/java/com/likelion/dub/domain/dto/OAuth/OAuthInfoResponse.java rename to src/main/java/com/likelion/dub/dto/OAuth/OAuthInfoResponse.java index f45773c..c205276 100644 --- a/src/main/java/com/likelion/dub/domain/dto/OAuth/OAuthInfoResponse.java +++ b/src/main/java/com/likelion/dub/dto/OAuth/OAuthInfoResponse.java @@ -1,7 +1,10 @@ -package com.likelion.dub.domain.dto.OAuth; +package com.likelion.dub.dto.OAuth; public interface OAuthInfoResponse { + String getEmail(); + String getNickname(); + OAuthProvider getOAuthProvider(); } \ No newline at end of file diff --git a/src/main/java/com/likelion/dub/domain/dto/OAuth/OAuthLoginParams.java b/src/main/java/com/likelion/dub/dto/OAuth/OAuthLoginParams.java similarity index 79% rename from src/main/java/com/likelion/dub/domain/dto/OAuth/OAuthLoginParams.java rename to src/main/java/com/likelion/dub/dto/OAuth/OAuthLoginParams.java index a7ae5da..ed8ebb9 100644 --- a/src/main/java/com/likelion/dub/domain/dto/OAuth/OAuthLoginParams.java +++ b/src/main/java/com/likelion/dub/dto/OAuth/OAuthLoginParams.java @@ -1,8 +1,10 @@ -package com.likelion.dub.domain.dto.OAuth; +package com.likelion.dub.dto.OAuth; import org.springframework.util.MultiValueMap; public interface OAuthLoginParams { + OAuthProvider oAuthProvider(); + MultiValueMap makeBody(); } diff --git a/src/main/java/com/likelion/dub/domain/dto/OAuth/OAuthProvider.java b/src/main/java/com/likelion/dub/dto/OAuth/OAuthProvider.java similarity index 53% rename from src/main/java/com/likelion/dub/domain/dto/OAuth/OAuthProvider.java rename to src/main/java/com/likelion/dub/dto/OAuth/OAuthProvider.java index c159837..892ea00 100644 --- a/src/main/java/com/likelion/dub/domain/dto/OAuth/OAuthProvider.java +++ b/src/main/java/com/likelion/dub/dto/OAuth/OAuthProvider.java @@ -1,4 +1,4 @@ -package com.likelion.dub.domain.dto.OAuth; +package com.likelion.dub.dto.OAuth; public enum OAuthProvider { KAKAO, NAVER diff --git a/src/main/java/com/likelion/dub/domain/dto/Post/GetAllPostResponse.java b/src/main/java/com/likelion/dub/dto/Post/GetAllPostResponse.java similarity index 88% rename from src/main/java/com/likelion/dub/domain/dto/Post/GetAllPostResponse.java rename to src/main/java/com/likelion/dub/dto/Post/GetAllPostResponse.java index f6f9c29..340643d 100644 --- a/src/main/java/com/likelion/dub/domain/dto/Post/GetAllPostResponse.java +++ b/src/main/java/com/likelion/dub/dto/Post/GetAllPostResponse.java @@ -1,4 +1,4 @@ -package com.likelion.dub.domain.dto.Post; +package com.likelion.dub.dto.Post; import lombok.AllArgsConstructor; diff --git a/src/main/java/com/likelion/dub/domain/dto/Post/GetOnePostResponse.java b/src/main/java/com/likelion/dub/dto/Post/GetOnePostResponse.java similarity index 91% rename from src/main/java/com/likelion/dub/domain/dto/Post/GetOnePostResponse.java rename to src/main/java/com/likelion/dub/dto/Post/GetOnePostResponse.java index 9a830a9..fc40a4d 100644 --- a/src/main/java/com/likelion/dub/domain/dto/Post/GetOnePostResponse.java +++ b/src/main/java/com/likelion/dub/dto/Post/GetOnePostResponse.java @@ -1,4 +1,4 @@ -package com.likelion.dub.domain.dto.Post; +package com.likelion.dub.dto.Post; import jakarta.persistence.Lob; import java.util.List; diff --git a/src/main/java/com/likelion/dub/domain/dto/Post/PostEditRequest.java b/src/main/java/com/likelion/dub/dto/Post/PostEditRequest.java similarity index 88% rename from src/main/java/com/likelion/dub/domain/dto/Post/PostEditRequest.java rename to src/main/java/com/likelion/dub/dto/Post/PostEditRequest.java index e10fc8c..5ba0771 100644 --- a/src/main/java/com/likelion/dub/domain/dto/Post/PostEditRequest.java +++ b/src/main/java/com/likelion/dub/dto/Post/PostEditRequest.java @@ -1,4 +1,4 @@ -package com.likelion.dub.domain.dto.Post; +package com.likelion.dub.dto.Post; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; diff --git a/src/main/java/com/likelion/dub/domain/dto/Post/PostWritingRequest.java b/src/main/java/com/likelion/dub/dto/Post/PostWritingRequest.java similarity index 88% rename from src/main/java/com/likelion/dub/domain/dto/Post/PostWritingRequest.java rename to src/main/java/com/likelion/dub/dto/Post/PostWritingRequest.java index 041cef4..8a1297d 100644 --- a/src/main/java/com/likelion/dub/domain/dto/Post/PostWritingRequest.java +++ b/src/main/java/com/likelion/dub/dto/Post/PostWritingRequest.java @@ -1,4 +1,4 @@ -package com.likelion.dub.domain.dto.Post; +package com.likelion.dub.dto.Post; import com.fasterxml.jackson.annotation.JsonProperty; import jakarta.persistence.Lob; diff --git a/src/main/java/com/likelion/dub/domain/dto/Post/WritingRequest.java b/src/main/java/com/likelion/dub/dto/Post/WritingRequest.java similarity index 90% rename from src/main/java/com/likelion/dub/domain/dto/Post/WritingRequest.java rename to src/main/java/com/likelion/dub/dto/Post/WritingRequest.java index 9f313f2..cdb529e 100644 --- a/src/main/java/com/likelion/dub/domain/dto/Post/WritingRequest.java +++ b/src/main/java/com/likelion/dub/dto/Post/WritingRequest.java @@ -1,4 +1,4 @@ -package com.likelion.dub.domain.dto.Post; +package com.likelion.dub.dto.Post; import jakarta.annotation.Nullable; diff --git a/src/main/java/com/likelion/dub/service/ClubService.java b/src/main/java/com/likelion/dub/service/ClubService.java index 88f3e05..831cb43 100644 --- a/src/main/java/com/likelion/dub/service/ClubService.java +++ b/src/main/java/com/likelion/dub/service/ClubService.java @@ -3,12 +3,13 @@ import com.amazonaws.services.s3.AmazonS3Client; import com.amazonaws.services.s3.model.ObjectMetadata; -import com.likelion.dub.common.BaseException; -import com.likelion.dub.common.BaseResponseStatus; +import com.likelion.dub.baseResponse.BaseException; +import com.likelion.dub.baseResponse.BaseResponseStatus; import com.likelion.dub.domain.Club; import com.likelion.dub.domain.Member; import com.likelion.dub.repository.ClubRepository; import com.likelion.dub.repository.MemberRepository; +import java.io.IOException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -18,8 +19,6 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; -import java.io.IOException; - @Service @Transactional @@ -38,9 +37,11 @@ public class ClubService { public void uploadForm(String url) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); String email = authentication.getName(); - Member member = memberRepository.findByEmail(email).orElseThrow(() -> new BaseException(BaseResponseStatus.WRONG_EMAIL)); + Member member = memberRepository.findByEmail(email) + .orElseThrow(() -> new BaseException(BaseResponseStatus.WRONG_EMAIL)); String clubName = member.getClub().getClubName(); - Club club = clubRepository.findByClubName(clubName).orElseThrow(() -> new BaseException(BaseResponseStatus.NO_SUCH_CLUB_EXIST)); + Club club = clubRepository.findByClubName(clubName) + .orElseThrow(() -> new BaseException(BaseResponseStatus.NO_SUCH_CLUB_EXIST)); club.setForm(url); clubRepository.save(club); @@ -49,9 +50,11 @@ public void uploadForm(String url) { public void updateIntroduce(String introduction) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); String email = authentication.getName(); - Member member = memberRepository.findByEmail(email).orElseThrow(() -> new BaseException(BaseResponseStatus.WRONG_EMAIL)); + Member member = memberRepository.findByEmail(email) + .orElseThrow(() -> new BaseException(BaseResponseStatus.WRONG_EMAIL)); String clubName = member.getClub().getClubName(); - Club club = clubRepository.findByClubName(clubName).orElseThrow(() -> new BaseException(BaseResponseStatus.NO_SUCH_CLUB_EXIST)); + Club club = clubRepository.findByClubName(clubName) + .orElseThrow(() -> new BaseException(BaseResponseStatus.NO_SUCH_CLUB_EXIST)); club.setIntroduction(introduction); clubRepository.save(club); } @@ -60,7 +63,8 @@ public void updateIntroduce(String introduction) { public void updateClubImage(MultipartFile file) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); String email = authentication.getName(); - Member member = memberRepository.findByEmail(email).orElseThrow(() -> new BaseException(BaseResponseStatus.WRONG_EMAIL)); + Member member = memberRepository.findByEmail(email) + .orElseThrow(() -> new BaseException(BaseResponseStatus.WRONG_EMAIL)); Club club = member.getClub(); String clubName = club.getClubName(); String fileName = clubName + "_" + "ClubImage"; @@ -80,7 +84,8 @@ public void updateClubImage(MultipartFile file) { public void updateTag(String groupName, String category) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); String email = authentication.getName(); - Member member = memberRepository.findByEmail(email).orElseThrow(() -> new BaseException(BaseResponseStatus.WRONG_EMAIL)); + Member member = memberRepository.findByEmail(email) + .orElseThrow(() -> new BaseException(BaseResponseStatus.WRONG_EMAIL)); Club club = member.getClub(); club.setGroupName(groupName); club.setCategory(category); diff --git a/src/main/java/com/likelion/dub/service/MemberService.java b/src/main/java/com/likelion/dub/service/MemberService.java index 8c575f5..2e60441 100644 --- a/src/main/java/com/likelion/dub/service/MemberService.java +++ b/src/main/java/com/likelion/dub/service/MemberService.java @@ -3,14 +3,14 @@ import com.amazonaws.services.s3.AmazonS3Client; import com.amazonaws.services.s3.model.ObjectMetadata; -import com.likelion.dub.common.BaseException; -import com.likelion.dub.common.BaseResponseStatus; +import com.likelion.dub.baseResponse.BaseException; +import com.likelion.dub.baseResponse.BaseResponseStatus; import com.likelion.dub.configuration.JwtTokenUtil; import com.likelion.dub.domain.Club; import com.likelion.dub.domain.Member; -import com.likelion.dub.domain.dto.Member.GetMemberInfoResponse; -import com.likelion.dub.domain.dto.OAuth.OAuthInfoResponse; -import com.likelion.dub.domain.dto.OAuth.OAuthLoginParams; +import com.likelion.dub.dto.Member.GetMemberInfoResponse; +import com.likelion.dub.dto.OAuth.OAuthInfoResponse; +import com.likelion.dub.dto.OAuth.OAuthLoginParams; import com.likelion.dub.repository.ClubRepository; import com.likelion.dub.repository.MemberRepository; import java.io.IOException; diff --git a/src/main/java/com/likelion/dub/service/PostService.java b/src/main/java/com/likelion/dub/service/PostService.java index 8143d87..ac39a52 100644 --- a/src/main/java/com/likelion/dub/service/PostService.java +++ b/src/main/java/com/likelion/dub/service/PostService.java @@ -3,13 +3,13 @@ import com.amazonaws.services.s3.AmazonS3Client; import com.amazonaws.services.s3.model.ObjectMetadata; -import com.likelion.dub.common.BaseException; -import com.likelion.dub.common.BaseResponseStatus; +import com.likelion.dub.baseResponse.BaseException; +import com.likelion.dub.baseResponse.BaseResponseStatus; import com.likelion.dub.domain.Club; import com.likelion.dub.domain.Member; import com.likelion.dub.domain.Post; -import com.likelion.dub.domain.dto.Post.GetAllPostResponse; -import com.likelion.dub.domain.dto.Post.GetOnePostResponse; +import com.likelion.dub.dto.Post.GetAllPostResponse; +import com.likelion.dub.dto.Post.GetOnePostResponse; import com.likelion.dub.repository.MemberRepository; import com.likelion.dub.repository.PostRepository; import java.io.IOException; diff --git a/src/main/java/com/likelion/dub/service/RequestOAuthInfoService.java b/src/main/java/com/likelion/dub/service/RequestOAuthInfoService.java index d8e9a7c..65a08ea 100644 --- a/src/main/java/com/likelion/dub/service/RequestOAuthInfoService.java +++ b/src/main/java/com/likelion/dub/service/RequestOAuthInfoService.java @@ -1,9 +1,9 @@ package com.likelion.dub.service; -import com.likelion.dub.domain.dto.OAuth.OAuthApiClient; -import com.likelion.dub.domain.dto.OAuth.OAuthInfoResponse; -import com.likelion.dub.domain.dto.OAuth.OAuthLoginParams; -import com.likelion.dub.domain.dto.OAuth.OAuthProvider; +import com.likelion.dub.dto.OAuth.OAuthApiClient; +import com.likelion.dub.dto.OAuth.OAuthInfoResponse; +import com.likelion.dub.dto.OAuth.OAuthLoginParams; +import com.likelion.dub.dto.OAuth.OAuthProvider; import java.util.List; import java.util.Map; import java.util.function.Function; @@ -12,6 +12,7 @@ @Component public class RequestOAuthInfoService { + private final Map clients; public RequestOAuthInfoService(List clients) { diff --git a/src/test/java/com/likelion/dub/controller/MemberControllerTest.java b/src/test/java/com/likelion/dub/controller/MemberControllerTest.java index 617c144..5a247b2 100644 --- a/src/test/java/com/likelion/dub/controller/MemberControllerTest.java +++ b/src/test/java/com/likelion/dub/controller/MemberControllerTest.java @@ -7,7 +7,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.fasterxml.jackson.databind.ObjectMapper; -import com.likelion.dub.domain.dto.OAuth.KakaoLoginParams; +import com.likelion.dub.dto.OAuth.KakaoLoginParams; import jakarta.transaction.Transactional; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; From 41d8be47911767b4cb9f8c3dc7b4ed1231713b32 Mon Sep 17 00:00:00 2001 From: suhoon <82464990+s2hoon@users.noreply.github.com> Date: Mon, 2 Oct 2023 00:50:22 +0900 Subject: [PATCH 63/72] docs: readme update --- README.md | 119 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 117 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e51e20b..1306f49 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ https://woozy-cuticle-bfb.notion.site/dub_-wanted-5f89e6bcf87142eca927893ff04703 --- # CI/CD Flow +![CIcd](https://github.com/s2hoon/dub_club_wanted_BE/assets/82464990/120399f7-7d09-4996-a8ac-631c4024a4fe) 1. main branch 에 Push 또는 Merge 2. Github 에 작성해둔 workflow file 로 Github Actions 수행 @@ -58,15 +59,129 @@ https://woozy-cuticle-bfb.notion.site/dub_-wanted-5f89e6bcf87142eca927893ff04703 --- # ERD +![기존](https://github.com/s2hoon/dub_club_wanted_BE/assets/82464990/ced95902-d874-4bc8-8d39-5b5a0da96a9e) -![db설계 최종](https://github.com/s2hoon/dub_club_wanted_BE/assets/82464990/532c6e73-719f-4645-a798-1fe47da878c3) --- -# Project Structure +## Project Structure + +```java +├─baseResponse +├─configuration +├─controller +├─domain +├─dto +│ ├─Club +│ ├─Comment +│ ├─Member +│ ├─OAuth +│ └─Post +├─repository +└─service +``` + +> +---baseResponse +| BaseException.java +| BaseResponse.java +| BaseResponseStatus.java +> +1. `BaseException.java`: + - `BaseException`은 `RuntimeException` 클래스를 상속받고 있습니다. 이는 실행 중 발생할 수 있는 예외를 나타내는 클래스입니다. + - `BaseException` 클래스는 `BaseResponseStatus`를 필드 값으로 가지고 있습니다. 즉, 예외가 발생했을 때 해당 예외의 원인을 `BaseResponseStatus`로 표시할 수 있습니다. +2. `BaseResponse.java`: + - 모든 응답(Response) 객체는 이 클래스를 통해 생성됩니다. + - 오버로딩(Overloading)을 통해 생성자가 구현되어 있으며, `result`를 매개변수로 받으면 요청이 성공한 경우를 나타내고, `result`를 매개변수로 받지 않으면 요청이 실패한 경우를 나타냅니다. +3. `BaseResponseStatus.java`: + - 이 클래스는 열거형(Enum) 파일로 정의되어 있으며, 코드값과 메시지를 필드로 가지고 있습니다. + `BaseResponse`나 `BaseException`에서 사용되는 상태 코드와 메시지를 정의하는 데 사용됩니다. 이러한 정의를 통해 각각의 상태를 나타내고 관리할 수 있습니다. + +> +---configuration +| ClientConfig.java +| EncoderConfig.java +| JwtTokenFilter.java +| JwtTokenUtil.java +| S3config.java +| SecurityConfig.java +> +1. `ClientConfig.java`: + - 이 파일은 `RestTemplate`을 사용하기 위해 Spring 빈으로 등록합니다. + - 모든 Rest API 요청은 이 빈을 통해 진행됩니다. `RestTemplate`은 HTTP 요청을 쉽게 수행하고 응답을 처리하는 데 사용됩니다. +2. `EncoderConfig.java`: + - 이 파일은 `BCryptPasswordEncoder`를 Spring 빈으로 등록합니다. + - 모든 비밀번호는 이 빈을 통해 암호화됩니다. `BCryptPasswordEncoder`는 보안성이 높은 비밀번호 해싱을 제공하는 데 사용됩니다. +3. `JwtTokenFilter.java`: + - 이 파일은 JWT(Jason Web Token) 토큰을 검증하고 권한을 부여하는 로직을 포함합니다. + - Spring Security 과 함께 사용됩니다. +4. `JwtTokenUtil.java`: + - 이 파일은 JWT 토큰을 생성하는 로직을 포함합니다. + - JWT는 인증된 사용자를 식별하고 정보를 보호하기 위한 토큰 방식입니다. +5. `S3config.java`: + - 이 파일은 Amazon S3를 사용하기 위한 설정 파일입니다. + - Amazon S3는 클라우드 기반의 파일 저장소이며, 파일 업로드 및 다운로드와 같은 작업을 수행하기 위한 설정을 제공합니다. +6. `SecurityConfig`: + - 이 파일은 Spring Security를 적용하기 위한 설정 파일입니다. + - Spring Security를 사용하여 사용자 인증, 권한 부여 및 보안 설정을 관리합니다. + +> +---controller +| ClubController.java +| JspController.java +| MemberController.java +| PostController.java +> + +> +---service +| ClubService.java +| MemberService.java +| PostService.java +| RequestOAuthInfoService.java +> + +> +---repository +| ClubRepository.java +| MemberRepository.java +| PostRepository.java +> +1. `controller` 디렉토리: + - 이 디렉토리는 컨트롤러(Controller) 클래스들을 포함합니다. + - 컨트롤러는 클라이언트의 HTTP 요청을 처리하고 해당 요청에 대한 비즈니스 로직을 호출하며, 결과를 클라이언트에게 반환합니다. +2. `service` 디렉토리: + - 이 디렉토리는 서비스(Service) 인터페이스 및 구현 클래스들을 포함합니다. + - 서비스는 비즈니스 로직을 추상화하고, 컨트롤러와 리포지토리 간의 중간 계층 역할을 합니다. 비즈니스 로직을 수행하고 데이터베이스와 상호작용하는데 사용됩니다. +3. `repository` 디렉토리: + - 이 디렉토리는 리포지토리(Repository) 인터페이스들을 포함합니다. + - 리포지토리는 데이터베이스와의 상호작용을 담당하며, 데이터베이스에서 데이터를 검색하고 조작하는 데 사용됩니다. + +> +---domain +| BaseEntity.java +| BaseTimeEntity.java +| Club.java +| Comment.java +| Member.java +| Post.java +| Role.java +> +1. `BaseEntity.java`: + - 엔티티의 공통적인 속성을 정의하고, 이러한 속성을 상속받아 재사용될 수 있도록 합니다. + - `createdBy` 와 `lastModifiedBy` 가 필드로 추가 되어 있습니다. +2. `BaseTimeEntity.java`: + - 엔티티의 공통적인 속성을 정의하고, 이러한 속성을 상속받아 재사용될 수 있도록 합니다. + - `createdDate` , `lastModifiedDate` 가 필드로 추가 되어 있습니다. +3. `Club.java`: + - 동아리에 관한 엔티티 입니다. +4. `Comment.java`: + - 댓글에 관한 엔티티 입니다. +5. `Member.java`: + - 회원에 관한 엔티티 입니다. +6. `Post.java`: + - 게시물에 관한 엔티티 입니다. +7. `Role.java`: + - 역할을 열거형(Enum)으로 정의하여 사용자의 권한을 관리하는 데 사용됩니다. --- + + # 성능개선 From 88f6debe4a45851065b467a998e63c34f3c00dd8 Mon Sep 17 00:00:00 2001 From: suhoon Date: Sat, 7 Oct 2023 01:51:35 +0900 Subject: [PATCH 64/72] =?UTF-8?q?feat:=20=EC=86=8C=EC=85=9C=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EB=A1=9C=EA=B7=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/likelion/dub/controller/MemberController.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/likelion/dub/controller/MemberController.java b/src/main/java/com/likelion/dub/controller/MemberController.java index 3abe3a2..c5a2be0 100644 --- a/src/main/java/com/likelion/dub/controller/MemberController.java +++ b/src/main/java/com/likelion/dub/controller/MemberController.java @@ -11,6 +11,7 @@ import com.likelion.dub.dto.OAuth.KakaoLoginParams; import com.likelion.dub.service.MemberService; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.GetMapping; @@ -28,6 +29,7 @@ @RequestMapping("/app/member") @RequiredArgsConstructor @CrossOrigin(origins = "*", allowedHeaders = "*") //Cors 제거 +@Slf4j public class MemberController { private final MemberService memberService; @@ -122,8 +124,10 @@ public BaseResponse login(@RequestBody MemberLoginRequest dto) { @PostMapping("/loginKakao") public BaseResponse loginKakao(@RequestBody KakaoLoginParams params) { try { - System.out.println(params.getAuthorizationCode()); + log.info("params = {}", params.getAuthorizationCode()); String token = memberService.loginKakao(params); + log.info("JWT token = {}", token); + return new BaseResponse<>(BaseResponseStatus.SUCCESS, "Bearer " + token); } catch (BaseException e) { return new BaseResponse(e.getStatus()); From f526ac271829953e01ffb7891da2164c5cb5f590 Mon Sep 17 00:00:00 2001 From: suhoon Date: Sun, 15 Oct 2023 00:52:45 +0900 Subject: [PATCH 65/72] =?UTF-8?q?feat:=20=EC=B9=B4=EC=B9=B4=EC=98=A4=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=EC=8B=9C=20member=20role=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/likelion/dub/service/MemberService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/likelion/dub/service/MemberService.java b/src/main/java/com/likelion/dub/service/MemberService.java index 2e60441..b10714a 100644 --- a/src/main/java/com/likelion/dub/service/MemberService.java +++ b/src/main/java/com/likelion/dub/service/MemberService.java @@ -175,6 +175,7 @@ private Long newMember(OAuthInfoResponse oAuthInfoResponse) { Member member = new Member(); member.setEmail(oAuthInfoResponse.getEmail()); member.setName(oAuthInfoResponse.getNickname()); + member.setRole("USER"); return memberRepository.save(member).getId(); } From 09adaf043506ae9854f77d9907795de09a1f01f8 Mon Sep 17 00:00:00 2001 From: suhoon Date: Sun, 22 Oct 2023 13:47:13 +0900 Subject: [PATCH 66/72] =?UTF-8?q?refactor:=20=EB=B3=80=EA=B2=BD=EA=B0=90?= =?UTF-8?q?=EC=A7=80=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/likelion/dub/controller/MemberController.java | 8 ++++++-- .../java/com/likelion/dub/service/MemberService.java | 9 ++++----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/likelion/dub/controller/MemberController.java b/src/main/java/com/likelion/dub/controller/MemberController.java index c5a2be0..27e9a58 100644 --- a/src/main/java/com/likelion/dub/controller/MemberController.java +++ b/src/main/java/com/likelion/dub/controller/MemberController.java @@ -13,6 +13,8 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.MediaType; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -82,7 +84,7 @@ public BaseResponse join(@RequestBody MemberJoinRequest dto) { @PostMapping(value = "/sign-up-club", consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE}) public BaseResponse joinClub(@RequestPart(value = "json") ClubMemberJoinRequest dto, - @RequestPart(value = "image", required = false) MultipartFile file) { + @RequestPart(value = "image", required = false) MultipartFile file) { try { memberService.joinClub(dto.getEmail(), dto.getName(), dto.getPassword(), dto.getGender(), dto.getRole(), dto.getIntroduction(), dto.getGroupName(), @@ -160,7 +162,9 @@ public BaseResponse getInfo() { @PutMapping("/changePwd") public BaseResponse changePwd(@RequestBody ChangePwdRequest changePwdRequest) { try { - memberService.changePassword(changePwdRequest.getCurrentPassword(), + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String email = authentication.getName(); + memberService.changePassword(email, changePwdRequest.getCurrentPassword(), changePwdRequest.getNewPassword()); String result = "비밀번호 수정 완료"; return new BaseResponse<>(BaseResponseStatus.SUCCESS, result); diff --git a/src/main/java/com/likelion/dub/service/MemberService.java b/src/main/java/com/likelion/dub/service/MemberService.java index b10714a..5f47bea 100644 --- a/src/main/java/com/likelion/dub/service/MemberService.java +++ b/src/main/java/com/likelion/dub/service/MemberService.java @@ -97,7 +97,7 @@ public void join(String email, String name, String password, String gender, Stri * @param file */ public void joinClub(String email, String name, String password, String gender, String role, - String introduction, String groupName, String category, MultipartFile file) { + String introduction, String groupName, String category, MultipartFile file) { // 중복 이메일 검사 Optional existingMember = memberRepository.findByEmail(email); @@ -195,16 +195,15 @@ public GetMemberInfoResponse getInfo() { } - public void changePassword(String currentPassword, String newPassword) { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - String email = authentication.getName(); + public void changePassword(String email, String currentPassword, String newPassword) { Member member = memberRepository.findByEmail(email) .orElseThrow(() -> new BaseException(BaseResponseStatus.NO_SUCH_MEMBER_EXIST)); if (bCryptPasswordEncoder.matches(currentPassword, member.getPassword())) { // 비밀번호가 일치할 때 처리 String hashedPassword = bCryptPasswordEncoder.encode(newPassword); member.setPassword(hashedPassword); - memberRepository.save(member); + //service 단 @Transactional 로 인해 save 호출이 필요없음. 즉, flush() 가 자동으로됨 + //memberRepository.save(member); } else { throw new BaseException(BaseResponseStatus.WRONG_PASSWORD); } From fc6e313492ed7514800ba338016b43af8bdd191a Mon Sep 17 00:00:00 2001 From: suhoon Date: Sun, 22 Oct 2023 13:55:33 +0900 Subject: [PATCH 67/72] =?UTF-8?q?refactor=20:=20stream=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/likelion/dub/service/PostService.java | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/likelion/dub/service/PostService.java b/src/main/java/com/likelion/dub/service/PostService.java index ac39a52..af35360 100644 --- a/src/main/java/com/likelion/dub/service/PostService.java +++ b/src/main/java/com/likelion/dub/service/PostService.java @@ -13,8 +13,8 @@ import com.likelion.dub.repository.MemberRepository; import com.likelion.dub.repository.PostRepository; import java.io.IOException; -import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -41,17 +41,11 @@ public class PostService { public List getAllPost() { List allPosts = postRepository.findAll(); - List getAllPostResponses = new ArrayList<>(); - - for (Post post : allPosts) { - GetAllPostResponse getAllPostResponse = new GetAllPostResponse(); - getAllPostResponse.setId(post.getId()); - getAllPostResponse.setTitle(post.getTitle()); - getAllPostResponse.setClubName(post.getClubName()); - getAllPostResponse.setClubImage(post.getClub().getClubImage()); - - getAllPostResponses.add(getAllPostResponse); - } + // stream 사용 + List getAllPostResponses = allPosts.stream() + .map(post -> new GetAllPostResponse(post.getId(), post.getTitle(), post.getClubName(), + post.getClub().getClubImage())) + .collect(Collectors.toList()); return getAllPostResponses; } From 76ac2f4caeba4f88935d0df112048134afbff9a9 Mon Sep 17 00:00:00 2001 From: suhoon Date: Sun, 22 Oct 2023 19:48:59 +0900 Subject: [PATCH 68/72] =?UTF-8?q?refactor=20:=20fetch=20join=20=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20N+1=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/likelion/dub/controller/MemberController.java | 4 +--- .../com/likelion/dub/controller/PostController.java | 9 +++++---- .../dub/dto/Member/ClubMemberJoinRequest.java | 11 +++++++---- .../likelion/dub/dto/Member/MemberJoinRequest.java | 2 ++ .../likelion/dub/dto/Member/MemberLoginRequest.java | 5 +++-- .../com/likelion/dub/repository/PostRepository.java | 10 ++++++---- .../java/com/likelion/dub/service/MemberService.java | 2 +- .../java/com/likelion/dub/service/PostService.java | 4 +--- 8 files changed, 26 insertions(+), 21 deletions(-) diff --git a/src/main/java/com/likelion/dub/controller/MemberController.java b/src/main/java/com/likelion/dub/controller/MemberController.java index 27e9a58..1fc5d4a 100644 --- a/src/main/java/com/likelion/dub/controller/MemberController.java +++ b/src/main/java/com/likelion/dub/controller/MemberController.java @@ -12,7 +12,6 @@ import com.likelion.dub.service.MemberService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.http.MediaType; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.CrossOrigin; @@ -81,8 +80,7 @@ public BaseResponse join(@RequestBody MemberJoinRequest dto) { * @param file * @return */ - @PostMapping(value = "/sign-up-club", consumes = {MediaType.APPLICATION_JSON_VALUE, - MediaType.MULTIPART_FORM_DATA_VALUE}) + @PostMapping(value = "/sign-up-club") public BaseResponse joinClub(@RequestPart(value = "json") ClubMemberJoinRequest dto, @RequestPart(value = "image", required = false) MultipartFile file) { try { diff --git a/src/main/java/com/likelion/dub/controller/PostController.java b/src/main/java/com/likelion/dub/controller/PostController.java index 0211f9f..bf42384 100644 --- a/src/main/java/com/likelion/dub/controller/PostController.java +++ b/src/main/java/com/likelion/dub/controller/PostController.java @@ -11,7 +11,6 @@ import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.http.MediaType; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.CrossOrigin; @@ -60,13 +59,15 @@ public BaseResponse> getAllPost() { * @return */ - @PostMapping(value = "/write-post", consumes = {MediaType.MULTIPART_FORM_DATA_VALUE}) + @PostMapping(value = "/write-post") public BaseResponse writePost(@ModelAttribute WritingRequest writingRequest) { try { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String email = authentication.getName(); String title = writingRequest.getTitle(); String content = writingRequest.getContent(); MultipartFile file = writingRequest.getImage(); - postService.writing(title, content, file); + postService.writing(email, title, content, file); return new BaseResponse<>(BaseResponseStatus.SUCCESS, "글 작성 성공"); } catch (BaseException e) { return new BaseResponse<>(e.getStatus()); @@ -102,7 +103,7 @@ public BaseResponse deletePost(@RequestParam(value = "id") Long id) { @PutMapping("/edit-post") public BaseResponse editPost(@RequestPart(value = "json") PostEditRequest dto, - @RequestPart(value = "images", required = false) List images) { + @RequestPart(value = "images", required = false) List images) { String newTitle = dto.getTitle(); String newContent = dto.getContent(); int newCategory = dto.getCategory(); diff --git a/src/main/java/com/likelion/dub/dto/Member/ClubMemberJoinRequest.java b/src/main/java/com/likelion/dub/dto/Member/ClubMemberJoinRequest.java index 9b5aab4..330efc4 100644 --- a/src/main/java/com/likelion/dub/dto/Member/ClubMemberJoinRequest.java +++ b/src/main/java/com/likelion/dub/dto/Member/ClubMemberJoinRequest.java @@ -4,11 +4,14 @@ import com.fasterxml.jackson.annotation.JsonProperty; import jakarta.annotation.Nullable; import jakarta.persistence.Lob; -import lombok.AllArgsConstructor; -import lombok.Data; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; -@Data -@AllArgsConstructor + +@Getter +@Setter +@NoArgsConstructor public class ClubMemberJoinRequest { @JsonProperty diff --git a/src/main/java/com/likelion/dub/dto/Member/MemberJoinRequest.java b/src/main/java/com/likelion/dub/dto/Member/MemberJoinRequest.java index 77b9584..d0ad1f2 100644 --- a/src/main/java/com/likelion/dub/dto/Member/MemberJoinRequest.java +++ b/src/main/java/com/likelion/dub/dto/Member/MemberJoinRequest.java @@ -6,10 +6,12 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.Setter; @AllArgsConstructor @Getter @NoArgsConstructor +@Setter public class MemberJoinRequest { @JsonProperty diff --git a/src/main/java/com/likelion/dub/dto/Member/MemberLoginRequest.java b/src/main/java/com/likelion/dub/dto/Member/MemberLoginRequest.java index 2426f5f..7f374b2 100644 --- a/src/main/java/com/likelion/dub/dto/Member/MemberLoginRequest.java +++ b/src/main/java/com/likelion/dub/dto/Member/MemberLoginRequest.java @@ -2,10 +2,11 @@ import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.NoArgsConstructor; -@AllArgsConstructor + +@NoArgsConstructor @Getter public class MemberLoginRequest { diff --git a/src/main/java/com/likelion/dub/repository/PostRepository.java b/src/main/java/com/likelion/dub/repository/PostRepository.java index 76320c6..1c9330e 100644 --- a/src/main/java/com/likelion/dub/repository/PostRepository.java +++ b/src/main/java/com/likelion/dub/repository/PostRepository.java @@ -1,18 +1,20 @@ package com.likelion.dub.repository; -import com.likelion.dub.domain.Member; import com.likelion.dub.domain.Post; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - import java.util.List; import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; public interface PostRepository extends JpaRepository { + + @Query("select p from Post p join fetch p.club") List findAll(); + Optional findByClubName(String clubName); + Optional findById(Long id); diff --git a/src/main/java/com/likelion/dub/service/MemberService.java b/src/main/java/com/likelion/dub/service/MemberService.java index 5f47bea..0f81882 100644 --- a/src/main/java/com/likelion/dub/service/MemberService.java +++ b/src/main/java/com/likelion/dub/service/MemberService.java @@ -131,7 +131,7 @@ public void joinClub(String email, String name, String password, String gender, club.setClubImage("https://dubs3.s3.ap-northeast-2.amazonaws.com/" + fileName); } clubRepository.save(club); - // 양방향 연관관계설정 + // 양방향 연관관계설정 //변경 감지 member.setClub(club); } catch (IOException e) { diff --git a/src/main/java/com/likelion/dub/service/PostService.java b/src/main/java/com/likelion/dub/service/PostService.java index af35360..4a4424c 100644 --- a/src/main/java/com/likelion/dub/service/PostService.java +++ b/src/main/java/com/likelion/dub/service/PostService.java @@ -50,10 +50,8 @@ public List getAllPost() { } - public void writing(String title, String content, MultipartFile image) throws BaseException { + public void writing(String email, String title, String content, MultipartFile image) throws BaseException { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - String email = authentication.getName(); Member member = memberRepository.findByEmail(email) .orElseThrow(() -> new BaseException(BaseResponseStatus.NO_SUCH_MEMBER_EXIST)); Club club = member.getClub(); From 74f67718dcab6dac3d79f6efbdd51fd758f0111a Mon Sep 17 00:00:00 2001 From: suhoon Date: Mon, 23 Oct 2023 00:22:37 +0900 Subject: [PATCH 69/72] =?UTF-8?q?refactor=20:=20security,jwt=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=84=A4=EC=A0=95=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 80 ++++++++-------- .../java/com/likelion/dub/DubApplication.java | 26 ++---- .../dub/configuration/JwtTokenFilter.java | 93 +++++++++---------- .../dub/configuration/RoleMappingConfig.java | 20 ++++ .../dub/configuration/SecurityConfig.java | 35 ++----- .../dub/controller/ClubController.java | 3 +- .../dub/controller/JspController.java | 5 +- .../dub/controller/MemberController.java | 3 +- .../dub/controller/PostController.java | 3 +- 9 files changed, 122 insertions(+), 146 deletions(-) create mode 100644 src/main/java/com/likelion/dub/configuration/RoleMappingConfig.java diff --git a/build.gradle b/build.gradle index c9292d4..c012ec3 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ plugins { - id 'java' - id 'org.springframework.boot' version '3.0.4' - id 'io.spring.dependency-management' version '1.1.0' + id 'java' + id 'org.springframework.boot' version '3.0.4' + id 'io.spring.dependency-management' version '1.1.0' } group = 'com.likelion' @@ -9,53 +9,49 @@ version = '0.0.1-SNAPSHOT' sourceCompatibility = '19' configurations { - compileOnly { - extendsFrom annotationProcessor - } + compileOnly { + extendsFrom annotationProcessor + } } repositories { - mavenCentral() + mavenCentral() } dependencies { - // json 요청 - implementation group: 'org.json', name: 'json', version: '20210307' - // spring - implementation 'org.springframework:spring-web' - // spring boot - developmentOnly 'org.springframework.boot:spring-boot-devtools' - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' - implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.springframework.boot:spring-boot-starter-security' - // mysql - implementation 'mysql:mysql-connector-java:8.0.26' - runtimeOnly 'com.mysql:mysql-connector-j' - // lombok - compileOnly 'org.projectlombok:lombok' - annotationProcessor 'org.projectlombok:lombok' - // jwt - implementation 'io.jsonwebtoken:jjwt-api:0.11.5' - implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' - implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' - implementation 'commons-io:commons-io:2.6' - // swagger - implementation 'io.springfox:springfox-boot-starter:3.0.0' - implementation group: 'io.springfox', name: 'springfox-swagger-ui', version: '3.0.0' - // s3 - implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' - // 프록시 json화 - implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5-jakarta' - // test - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'org.springframework.security:spring-security-test' - - //thymeleaf - implementation 'org.springframework.boot:spring-boot-starter-thymeleaf:3.0.1' + // json 요청 + implementation group: 'org.json', name: 'json', version: '20210307' + // spring + implementation 'org.springframework:spring-web' + // spring boot + developmentOnly 'org.springframework.boot:spring-boot-devtools' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-security' + // mysql + implementation 'mysql:mysql-connector-java:8.0.26' + runtimeOnly 'com.mysql:mysql-connector-j' + // lombok + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + // jwt + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' + implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' + implementation 'commons-io:commons-io:2.6' + + // s3 + implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' + // test + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.security:spring-security-test' + + //thymeleaf + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf:3.0.1' } tasks.named('test') { - useJUnitPlatform() + useJUnitPlatform() } diff --git a/src/main/java/com/likelion/dub/DubApplication.java b/src/main/java/com/likelion/dub/DubApplication.java index e04e8ef..21f7c93 100644 --- a/src/main/java/com/likelion/dub/DubApplication.java +++ b/src/main/java/com/likelion/dub/DubApplication.java @@ -1,34 +1,26 @@ package com.likelion.dub; +import java.util.Optional; +import java.util.UUID; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.data.domain.AuditorAware; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; -import java.util.Optional; -import java.util.UUID; - @EnableJpaAuditing //(modifyOnCreate =false)를 넣으면 저장시점에 저장데이터만 입력 @SpringBootApplication public class DubApplication { - public static void main(String[] args) { - SpringApplication.run(DubApplication.class, args); - } - - @Bean - public AuditorAware auditorProvider() { - return () -> Optional.of(UUID.randomUUID().toString()); - } + public static void main(String[] args) { + SpringApplication.run(DubApplication.class, args); + } - //프록시 객체 JSON 화 -// @Bean -// public Hibernate5JakartaModule hibernate5JakartaModule() { -// Hibernate5JakartaModule hibernate5JakartaModule = new Hibernate5JakartaModule(); -// return hibernate5JakartaModule; -// } + @Bean + public AuditorAware auditorProvider() { + return () -> Optional.of(UUID.randomUUID().toString()); + } } diff --git a/src/main/java/com/likelion/dub/configuration/JwtTokenFilter.java b/src/main/java/com/likelion/dub/configuration/JwtTokenFilter.java index 79a0b13..ae3b4ac 100644 --- a/src/main/java/com/likelion/dub/configuration/JwtTokenFilter.java +++ b/src/main/java/com/likelion/dub/configuration/JwtTokenFilter.java @@ -1,96 +1,89 @@ package com.likelion.dub.configuration; -import com.likelion.dub.service.MemberService; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; +import java.util.Map; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; -import java.io.IOException; -import java.util.List; - /** - * JwtTokenFilter는 Spring Security에서 인증된 사용자의 요청에 대해 실행됩니다. - * doFilterInternal() 메소드를 통해 요청에 대한 필터링이 이루어지며, 인증 토큰을 추출하여 이를 이용해 인증된 사용자의 권한을 설정합니다 - * 이후 요청이 컨트롤러로 전달됩니다. 따라서 JwtTokenFilter는 인증된 사용자가 보호된 자원에 접근할 때마다 실행됩니다. + * JwtTokenFilter는 Spring Security에서 인증된 사용자의 요청에 대해 실행됩니다. doFilterInternal() 메소드를 통해 요청에 대한 필터링이 이루어지며, 인증 토큰을 추출하여 이를 + * 이용해 인증된 사용자의 권한을 설정합니다 이후 요청이 컨트롤러로 전달됩니다. 따라서 JwtTokenFilter는 인증된 사용자가 보호된 자원에 접근할 때마다 실행됩니다. */ @Slf4j +@Component @RequiredArgsConstructor public class JwtTokenFilter extends OncePerRequestFilter { - private final MemberService memberService; - private final String secretKey; + private final Map roleMapping; + @Value("${jwt.token.secret}") + private String secretKey; @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { final String authorization = request.getHeader(HttpHeaders.AUTHORIZATION); //요청 헤더에서 Authorization 헤더를 읽음 log.info("authorization:{}", authorization); - - - //토큰이 없거나, Bearer token 이 아니면 종료 - if (authorization == null || !authorization.startsWith("Bearer ")) { - log.error("No Token or not Bearer token"); - filterChain.doFilter(request, response); + // 토큰이 있는지 확인 + if (checkHaveToken(request, response, filterChain, authorization)) { return; } //token 꺼내기 String token = authorization.split(" ")[1]; //token expired 면 종료 - if (JwtTokenUtil.isExpired(token, secretKey)) { - log.error("token Expired"); - filterChain.doFilter(request, response); + if (checkIsExpired(request, response, filterChain, token)) { return; } - - //email Token 에서 꺼내기 String email = JwtTokenUtil.getEmail(token, secretKey); - log.info("email:{}", email); - - //name Token 에서 꺼내기 - String name = JwtTokenUtil.getName(token, secretKey); - log.info("name:{}", name); - - //role Token 에서 꺼내기 String role = JwtTokenUtil.getRole(token, secretKey); + log.info("email:{}", email); log.info("role:{}", role); - - if ( "USER".equals(role)){ - //권한 부여 + String authority = roleMapping.get(role); + log.info("authority ={} ", authority); + if (authority != null) { + // 권한 부여 UsernamePasswordAuthenticationToken authenticationToken = - new UsernamePasswordAuthenticationToken(email, null, List.of(new SimpleGrantedAuthority("ROLE_USER"))); - //detail 을 넣어줍니다 + new UsernamePasswordAuthenticationToken(email, null, + List.of(new SimpleGrantedAuthority(authority))); authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authenticationToken); - filterChain.doFilter(request, response); } - else if ("CLUB".equals(role)) { - //권한 부여 - UsernamePasswordAuthenticationToken authenticationToken = - new UsernamePasswordAuthenticationToken(email, null, List.of(new SimpleGrantedAuthority("ROLE_CLUB"))); - //detail 을 넣어줍니다 - authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); - SecurityContextHolder.getContext().setAuthentication(authenticationToken); + filterChain.doFilter(request, response); + } + + private boolean checkIsExpired(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain, + String token) throws IOException, ServletException { + if (JwtTokenUtil.isExpired(token, secretKey)) { + log.error("token Expired"); filterChain.doFilter(request, response); + return true; } - else if ("ADMIN".equals(role)) { - //권한 부여 - UsernamePasswordAuthenticationToken authenticationToken = - new UsernamePasswordAuthenticationToken(email, null, List.of(new SimpleGrantedAuthority("ROLE_ADMIN"))); - //detail 을 넣어줍니다 - authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); - SecurityContextHolder.getContext().setAuthentication(authenticationToken); + return false; + } + + private boolean checkHaveToken(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain, + String authorization) throws IOException, ServletException { + //토큰이 없거나, Bearer token 이 아니면 종료 + if (authorization == null || !authorization.startsWith("Bearer ")) { + log.error("No Token or not Bearer token"); filterChain.doFilter(request, response); + return true; } - - + return false; } + } diff --git a/src/main/java/com/likelion/dub/configuration/RoleMappingConfig.java b/src/main/java/com/likelion/dub/configuration/RoleMappingConfig.java new file mode 100644 index 0000000..13d332a --- /dev/null +++ b/src/main/java/com/likelion/dub/configuration/RoleMappingConfig.java @@ -0,0 +1,20 @@ +package com.likelion.dub.configuration; + + +import java.util.HashMap; +import java.util.Map; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class RoleMappingConfig { + @Bean + public Map roleMapping() { + Map roleMapping = new HashMap<>(); + roleMapping.put("USER", "ROLE_USER"); + roleMapping.put("CLUB", "ROLE_CLUB"); + roleMapping.put("ADMIN", "ROLE_ADMIN"); + return roleMapping; + } + +} diff --git a/src/main/java/com/likelion/dub/configuration/SecurityConfig.java b/src/main/java/com/likelion/dub/configuration/SecurityConfig.java index c11f9da..a359c5c 100644 --- a/src/main/java/com/likelion/dub/configuration/SecurityConfig.java +++ b/src/main/java/com/likelion/dub/configuration/SecurityConfig.java @@ -1,13 +1,9 @@ package com.likelion.dub.configuration; -import com.likelion.dub.domain.Role; -import com.likelion.dub.service.MemberService; import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; 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; @@ -19,43 +15,26 @@ @RequiredArgsConstructor public class SecurityConfig { - private final MemberService memberService; - private static final String[] Swagger_url = { - /* swagger v2 */ - "/v2/api-docs", - "/swagger-resources", - "/swagger-resources/**", - "/configuration/ui", - "/configuration/security", - "/swagger-ui.html", - "/webjars/**", - /* swagger v3 */ - "/v3/api-docs/**", - "/swagger-ui/**" - }; - @Value("${jwt.token.secret}") - private String secretKey; - + private final JwtTokenFilter jwtTokenFilter; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http .httpBasic().disable() .csrf().disable() - //.cors().and() // cors 활성화 + .cors().disable() .authorizeRequests() - .requestMatchers("/app/post/write-post", "/app/post/delete-post", "/app/post/rewrite-post").hasRole("CLUB") // Post 작성 Club 권한 필요 - .requestMatchers( "/app/club/**").hasRole("CLUB") // Club 에 관한거 CLUB 권한 필요 + .requestMatchers("/app/post/write-post", "/app/post/delete-post", "/app/post/rewrite-post") + .hasRole("CLUB") // Post 작성 Club 권한 필요 + .requestMatchers("/app/club/**").hasRole("CLUB") // Club 에 관한거 CLUB 권한 필요 .anyRequest().permitAll() // 나머지는 누구나 접근가능 .and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 세션 사용 비활성화 -> restful api 기반 토큰 이므로 세션 필요x .and() - .addFilterBefore(new JwtTokenFilter(memberService, secretKey), UsernamePasswordAuthenticationFilter.class) + .addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class) .build(); - - - + } } diff --git a/src/main/java/com/likelion/dub/controller/ClubController.java b/src/main/java/com/likelion/dub/controller/ClubController.java index c8d5888..b4d4799 100644 --- a/src/main/java/com/likelion/dub/controller/ClubController.java +++ b/src/main/java/com/likelion/dub/controller/ClubController.java @@ -7,7 +7,6 @@ import com.likelion.dub.dto.Club.UpdateTagRequest; import com.likelion.dub.service.ClubService; import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -18,7 +17,7 @@ @RestController @RequestMapping("/app/club") @RequiredArgsConstructor -@CrossOrigin(origins = "*", allowedHeaders = "*") //Cors 제거 +//@CrossOrigin(origins = "*", allowedHeaders = "*") //Cors 제거 public class ClubController { diff --git a/src/main/java/com/likelion/dub/controller/JspController.java b/src/main/java/com/likelion/dub/controller/JspController.java index 02c3a45..1875b32 100644 --- a/src/main/java/com/likelion/dub/controller/JspController.java +++ b/src/main/java/com/likelion/dub/controller/JspController.java @@ -6,7 +6,6 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -15,7 +14,7 @@ @Controller @RequiredArgsConstructor -@CrossOrigin(origins = "*", allowedHeaders = "*") //Cors 제거 +//@CrossOrigin(origins = "*", allowedHeaders = "*") //Cors 제거 @RequestMapping("/app/jsp") public class JspController { @@ -30,7 +29,7 @@ public String loginView() { @GetMapping("/redirect") public String kakaoCallback(@RequestParam("code") String code, - RedirectAttributes redirectAttributes) { + RedirectAttributes redirectAttributes) { // code 값을 main 페이지로 전달 redirectAttributes.addFlashAttribute("authorizationCode", code); return "redirect:/mainView"; // mainView 페이지로 리다이렉트 diff --git a/src/main/java/com/likelion/dub/controller/MemberController.java b/src/main/java/com/likelion/dub/controller/MemberController.java index 1fc5d4a..d0a05a5 100644 --- a/src/main/java/com/likelion/dub/controller/MemberController.java +++ b/src/main/java/com/likelion/dub/controller/MemberController.java @@ -14,7 +14,6 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -29,7 +28,7 @@ @RestController @RequestMapping("/app/member") @RequiredArgsConstructor -@CrossOrigin(origins = "*", allowedHeaders = "*") //Cors 제거 +//@CrossOrigin(origins = "*", allowedHeaders = "*") //Cors 제거 @Slf4j public class MemberController { diff --git a/src/main/java/com/likelion/dub/controller/PostController.java b/src/main/java/com/likelion/dub/controller/PostController.java index bf42384..820df5a 100644 --- a/src/main/java/com/likelion/dub/controller/PostController.java +++ b/src/main/java/com/likelion/dub/controller/PostController.java @@ -13,7 +13,6 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; @@ -29,7 +28,7 @@ @RestController @RequestMapping("/app/post") @RequiredArgsConstructor -@CrossOrigin(origins = "*", allowedHeaders = "*") //Cors 제거 +//@CrossOrigin(origins = "*", allowedHeaders = "*") //Cors 제거 @Slf4j public class PostController { From 779b42449b298373e968cb468351764ef56f8ca4 Mon Sep 17 00:00:00 2001 From: suhoon Date: Tue, 24 Oct 2023 23:38:19 +0900 Subject: [PATCH 70/72] =?UTF-8?q?refactor=20:=20Entity=20=EB=B0=8F=20?= =?UTF-8?q?=EC=97=B0=EA=B4=80=EA=B4=80=EA=B3=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/likelion/dub/DubApplication.java | 14 +-- .../dub/configuration/AwareAuditor.java | 16 +++ .../dub/configuration/EncoderConfig.java | 2 +- .../dub/configuration/JwtTokenFilter.java | 1 + .../dub/controller/MemberController.java | 93 ++++---------- .../dub/controller/PostController.java | 13 -- .../com/likelion/dub/domain/BaseEntity.java | 6 +- .../java/com/likelion/dub/domain/Club.java | 60 ++++------ .../com/likelion/dub/domain/ClubApply.java | 39 ++++++ .../com/likelion/dub/domain/ClubHashTag.java | 40 +++++++ .../java/com/likelion/dub/domain/Comment.java | 57 --------- .../java/com/likelion/dub/domain/HashTag.java | 31 +++++ .../java/com/likelion/dub/domain/Member.java | 24 ++-- .../java/com/likelion/dub/domain/Post.java | 59 ++++----- .../dub/dto/Member/ClubMemberJoinRequest.java | 7 +- .../dub/dto/Member/ToClubRequest.java | 30 +++++ .../dub/repository/MemberRepository.java | 5 +- .../dub/repository/PostRepository.java | 2 - .../com/likelion/dub/service/ClubService.java | 5 +- .../likelion/dub/service/MemberService.java | 113 +++++------------- .../com/likelion/dub/service/PostService.java | 24 +--- .../{configuration => util}/JwtTokenUtil.java | 15 +-- src/main/resources/application.properties | 10 +- .../dub/repository/ClubRepositoryTest.java | 51 ++++++++ .../dub/repository/MemberRepositoryTest.java | 68 +++++++++++ .../dub/service/MemberServiceTest.java | 95 +++++++++++++++ .../likelion/dub/service/PostServiceTest.java | 21 ---- .../sql/club-repository-test-data.sql | 4 + .../sql/member-repository-test-data.sql | 4 + .../sql/member-service-test-data.sql | 4 + .../resources/test-application.properties | 30 +++++ 31 files changed, 543 insertions(+), 400 deletions(-) create mode 100644 src/main/java/com/likelion/dub/configuration/AwareAuditor.java create mode 100644 src/main/java/com/likelion/dub/domain/ClubApply.java create mode 100644 src/main/java/com/likelion/dub/domain/ClubHashTag.java delete mode 100644 src/main/java/com/likelion/dub/domain/Comment.java create mode 100644 src/main/java/com/likelion/dub/domain/HashTag.java create mode 100644 src/main/java/com/likelion/dub/dto/Member/ToClubRequest.java rename src/main/java/com/likelion/dub/{configuration => util}/JwtTokenUtil.java (78%) create mode 100644 src/test/java/com/likelion/dub/repository/ClubRepositoryTest.java create mode 100644 src/test/java/com/likelion/dub/repository/MemberRepositoryTest.java create mode 100644 src/test/java/com/likelion/dub/service/MemberServiceTest.java delete mode 100644 src/test/java/com/likelion/dub/service/PostServiceTest.java create mode 100644 src/test/resources/sql/club-repository-test-data.sql create mode 100644 src/test/resources/sql/member-repository-test-data.sql create mode 100644 src/test/resources/sql/member-service-test-data.sql create mode 100644 src/test/resources/test-application.properties diff --git a/src/main/java/com/likelion/dub/DubApplication.java b/src/main/java/com/likelion/dub/DubApplication.java index 21f7c93..dbdc11b 100644 --- a/src/main/java/com/likelion/dub/DubApplication.java +++ b/src/main/java/com/likelion/dub/DubApplication.java @@ -1,26 +1,16 @@ package com.likelion.dub; -import java.util.Optional; -import java.util.UUID; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; -import org.springframework.data.domain.AuditorAware; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; -@EnableJpaAuditing //(modifyOnCreate =false)를 넣으면 저장시점에 저장데이터만 입력 +@EnableJpaAuditing @SpringBootApplication public class DubApplication { public static void main(String[] args) { SpringApplication.run(DubApplication.class, args); } - - @Bean - public AuditorAware auditorProvider() { - return () -> Optional.of(UUID.randomUUID().toString()); - } - - + } diff --git a/src/main/java/com/likelion/dub/configuration/AwareAuditor.java b/src/main/java/com/likelion/dub/configuration/AwareAuditor.java new file mode 100644 index 0000000..4f09eba --- /dev/null +++ b/src/main/java/com/likelion/dub/configuration/AwareAuditor.java @@ -0,0 +1,16 @@ +package com.likelion.dub.configuration; + +import java.util.Optional; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.domain.AuditorAware; +import org.springframework.security.core.context.SecurityContextHolder; + +@Configuration +public class AwareAuditor implements AuditorAware { + @Override + public Optional getCurrentAuditor() { + String email = SecurityContextHolder.getContext().getAuthentication().getName(); + return Optional.of(email); + } + +} diff --git a/src/main/java/com/likelion/dub/configuration/EncoderConfig.java b/src/main/java/com/likelion/dub/configuration/EncoderConfig.java index d4fd441..3bd4a84 100644 --- a/src/main/java/com/likelion/dub/configuration/EncoderConfig.java +++ b/src/main/java/com/likelion/dub/configuration/EncoderConfig.java @@ -8,7 +8,7 @@ @Configuration public class EncoderConfig { @Bean - public BCryptPasswordEncoder encoder(){ + public BCryptPasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder(); } } diff --git a/src/main/java/com/likelion/dub/configuration/JwtTokenFilter.java b/src/main/java/com/likelion/dub/configuration/JwtTokenFilter.java index ae3b4ac..52aeb6f 100644 --- a/src/main/java/com/likelion/dub/configuration/JwtTokenFilter.java +++ b/src/main/java/com/likelion/dub/configuration/JwtTokenFilter.java @@ -1,5 +1,6 @@ package com.likelion.dub.configuration; +import com.likelion.dub.util.JwtTokenUtil; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; diff --git a/src/main/java/com/likelion/dub/controller/MemberController.java b/src/main/java/com/likelion/dub/controller/MemberController.java index d0a05a5..c80766a 100644 --- a/src/main/java/com/likelion/dub/controller/MemberController.java +++ b/src/main/java/com/likelion/dub/controller/MemberController.java @@ -4,10 +4,10 @@ import com.likelion.dub.baseResponse.BaseResponse; import com.likelion.dub.baseResponse.BaseResponseStatus; import com.likelion.dub.dto.Member.ChangePwdRequest; -import com.likelion.dub.dto.Member.ClubMemberJoinRequest; import com.likelion.dub.dto.Member.GetMemberInfoResponse; import com.likelion.dub.dto.Member.MemberJoinRequest; import com.likelion.dub.dto.Member.MemberLoginRequest; +import com.likelion.dub.dto.Member.ToClubRequest; import com.likelion.dub.dto.OAuth.KakaoLoginParams; import com.likelion.dub.service.MemberService; import lombok.RequiredArgsConstructor; @@ -20,88 +20,53 @@ import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.multipart.MultipartFile; @RestController @RequestMapping("/app/member") @RequiredArgsConstructor -//@CrossOrigin(origins = "*", allowedHeaders = "*") //Cors 제거 @Slf4j public class MemberController { private final MemberService memberService; - - /** - * 이메일 중복체크 - * - * @param email - * @return - */ + // 이메일 중복체크 @GetMapping("/email/{email}") public BaseResponse checkEmail(@PathVariable String email) { - boolean isEmailAvailable = memberService.checkEmail(email); - if (isEmailAvailable) { - String result = "이메일 사용 가능"; - return new BaseResponse<>(BaseResponseStatus.SUCCESS, result); - } else { - return new BaseResponse(BaseResponseStatus.EMAIL_ALREADY_EXIST); + try { + boolean isEmailAvailable = memberService.checkEmail(email); + return new BaseResponse<>(BaseResponseStatus.SUCCESS, "이메일 사용 가능"); + } catch (BaseException e) { + return new BaseResponse(e.getStatus()); } - } - - /** - * 일반회원가입 - * - * @param dto - * @return - */ + // 일반 회원가입 @PostMapping("/sign-up") - public BaseResponse join(@RequestBody MemberJoinRequest dto) { + public BaseResponse join(@RequestBody MemberJoinRequest memberJoinRequest) { try { - memberService.join(dto.getEmail(), dto.getName(), dto.getPassword(), dto.getGender(), - dto.getRole()); - String result = "(일반)회원 가입 완료"; - return new BaseResponse<>(BaseResponseStatus.SUCCESS, result); + memberService.join(memberJoinRequest); + return new BaseResponse<>(BaseResponseStatus.SUCCESS, "(일반) 회원 가입 완료"); } catch (BaseException e) { return new BaseResponse(e.getStatus()); } } - /** - * 동아리회원 회원가입 - * - * @param dto - * @param file - * @return - */ - @PostMapping(value = "/sign-up-club") - public BaseResponse joinClub(@RequestPart(value = "json") ClubMemberJoinRequest dto, - @RequestPart(value = "image", required = false) MultipartFile file) { + // 동아리 회원으로 전환 + @PostMapping("/toClub") + public BaseResponse toClub(@RequestBody ToClubRequest toClubRequest) { try { - memberService.joinClub(dto.getEmail(), dto.getName(), dto.getPassword(), - dto.getGender(), dto.getRole(), dto.getIntroduction(), dto.getGroupName(), - dto.getCategory(), file); - - String result = "(동아리)회원 가입 완료"; - return new BaseResponse<>(BaseResponseStatus.SUCCESS, result); - + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String email = authentication.getName(); + memberService.transferToClub(email, toClubRequest); + return new BaseResponse<>(BaseResponseStatus.SUCCESS, "동아리 회원으로 전환 완료"); } catch (BaseException e) { return new BaseResponse<>(e.getStatus()); } } - - /** - * 로그인 - * - * @param dto - * @return - */ + // 일반 로그인 @PostMapping("/sign-in") public BaseResponse login(@RequestBody MemberLoginRequest dto) { try { @@ -114,12 +79,7 @@ public BaseResponse login(@RequestBody MemberLoginRequest dto) { } - /** - * 카카오 로그인 - * - * @param params - * @return - */ + // 카카오 로그인 @PostMapping("/loginKakao") public BaseResponse loginKakao(@RequestBody KakaoLoginParams params) { try { @@ -134,11 +94,7 @@ public BaseResponse loginKakao(@RequestBody KakaoLoginParams params) { } - /** - * 회원정보 조회 - * - * @return - */ + // 회원 정보 조회 @GetMapping("/getInfo") public BaseResponse getInfo() { try { @@ -150,12 +106,7 @@ public BaseResponse getInfo() { } - /** - * 비밀번호 수정 - * - * @param changePwdRequest - * @return - */ + // 비밀번호 수정 @PutMapping("/changePwd") public BaseResponse changePwd(@RequestBody ChangePwdRequest changePwdRequest) { try { diff --git a/src/main/java/com/likelion/dub/controller/PostController.java b/src/main/java/com/likelion/dub/controller/PostController.java index 820df5a..cd832c3 100644 --- a/src/main/java/com/likelion/dub/controller/PostController.java +++ b/src/main/java/com/likelion/dub/controller/PostController.java @@ -28,18 +28,12 @@ @RestController @RequestMapping("/app/post") @RequiredArgsConstructor -//@CrossOrigin(origins = "*", allowedHeaders = "*") //Cors 제거 @Slf4j public class PostController { private final PostService postService; - /** - * 동아리글 전체 조회 - * - * @return - */ @GetMapping("/getAll") public BaseResponse> getAllPost() { try { @@ -51,13 +45,6 @@ public BaseResponse> getAllPost() { } - /** - * post 작성 - * - * @param writingRequest - * @return - */ - @PostMapping(value = "/write-post") public BaseResponse writePost(@ModelAttribute WritingRequest writingRequest) { try { diff --git a/src/main/java/com/likelion/dub/domain/BaseEntity.java b/src/main/java/com/likelion/dub/domain/BaseEntity.java index a42557a..61cfab1 100644 --- a/src/main/java/com/likelion/dub/domain/BaseEntity.java +++ b/src/main/java/com/likelion/dub/domain/BaseEntity.java @@ -9,13 +9,13 @@ @EntityListeners(AuditingEntityListener.class) //이걸 생략하고 orm.xml에 등록하면 전체 적용이가능하다 @MappedSuperclass -public abstract class BaseEntity extends BaseTimeEntity{ +public abstract class BaseEntity extends BaseTimeEntity { @CreatedBy - @Column(updatable = false) + @Column(updatable = false, length = 512) private String createdBy; @LastModifiedBy - @Column + @Column(length = 512) private String lastModifiedBy; } diff --git a/src/main/java/com/likelion/dub/domain/Club.java b/src/main/java/com/likelion/dub/domain/Club.java index 2d1f9e2..e06faae 100644 --- a/src/main/java/com/likelion/dub/domain/Club.java +++ b/src/main/java/com/likelion/dub/domain/Club.java @@ -1,12 +1,21 @@ package com.likelion.dub.domain; -import com.fasterxml.jackson.annotation.JsonIgnore; -import jakarta.persistence.*; -import lombok.*; - -import java.util.ArrayList; -import java.util.List; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.Lob; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; @NoArgsConstructor @@ -22,44 +31,27 @@ public class Club { @Column(name = "club_id") private Long id; + @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; - @JsonIgnore - @OneToMany(mappedBy = "club") - private List post = new ArrayList<>(); - - @Column + @Column(length = 32, nullable = false) private String clubName; - @Column @Lob - private String introduction; - @Column - private String groupName; @Column - private String category; - - @Column - private String clubImage; - - @Column - private String form; + private String introduction; + @Column(length = 32, nullable = false) + private String groupName; - @JsonIgnore - @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY) - @JoinColumn(name = "member_id") - private Member member; + @Column(length = 512, nullable = false) + private String clubImageUrl; - public void setMember(Member member) { - this.member = member; - member.setClub(this); - } + @Column(length = 512, nullable = false) + private String applyFormUrl; - public void setPost(Post post) { - this.post.add(post); - post.setClub(this); - } } diff --git a/src/main/java/com/likelion/dub/domain/ClubApply.java b/src/main/java/com/likelion/dub/domain/ClubApply.java new file mode 100644 index 0000000..e5d5500 --- /dev/null +++ b/src/main/java/com/likelion/dub/domain/ClubApply.java @@ -0,0 +1,39 @@ +package com.likelion.dub.domain; + + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@NoArgsConstructor +@Entity +@AllArgsConstructor +@Getter +@Setter +@Table(name = "club_apply") +public class ClubApply extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; + + + @ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @JoinColumn(name = "club_id") + private Club club; + +} diff --git a/src/main/java/com/likelion/dub/domain/ClubHashTag.java b/src/main/java/com/likelion/dub/domain/ClubHashTag.java new file mode 100644 index 0000000..ee32ad4 --- /dev/null +++ b/src/main/java/com/likelion/dub/domain/ClubHashTag.java @@ -0,0 +1,40 @@ +package com.likelion.dub.domain; + + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Table(name = "club_hashtag") +public class ClubHashTag { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + + @ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @JoinColumn(name = "club_id") + private Club club; + + + @ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @JoinColumn(name = "hashtag_id") + private HashTag hashTag; + +} diff --git a/src/main/java/com/likelion/dub/domain/Comment.java b/src/main/java/com/likelion/dub/domain/Comment.java deleted file mode 100644 index a8c5273..0000000 --- a/src/main/java/com/likelion/dub/domain/Comment.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.likelion.dub.domain; - -import jakarta.persistence.*; -import lombok.*; -import org.springframework.data.jpa.domain.support.AuditingEntityListener; - -import java.util.ArrayList; -import java.util.List; - - -@Entity -@Getter -@Setter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@Table(name = "comment") -public class Comment extends BaseEntity{ - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "comment_id") - private Long id; - - @Column - private Boolean is_public; - - @Column - private Boolean is_anonymous; - - - @Column - private String content; - - @ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY) - @JoinColumn(name = "post_id") - private Post post; - - @ManyToOne - @JoinColumn(name = "parent_comment_id") - private Comment parentComment; // 자기 참조 필드 - - @OneToMany(mappedBy = "parentComment", cascade = CascadeType.ALL, orphanRemoval = true) - private List childComments = new ArrayList<>(); // 대댓글들 - - - // 대댓글 추가 메서드 - public void addChildComment(Comment childComment) { - childComments.add(childComment); - childComment.setParentComment(this); - } - - // 대댓글 삭제 메서드 - public void removeChildComment(Comment childComment) { - childComments.remove(childComment); - childComment.setParentComment(null); - } - - -} diff --git a/src/main/java/com/likelion/dub/domain/HashTag.java b/src/main/java/com/likelion/dub/domain/HashTag.java new file mode 100644 index 0000000..90a61c4 --- /dev/null +++ b/src/main/java/com/likelion/dub/domain/HashTag.java @@ -0,0 +1,31 @@ +package com.likelion.dub.domain; + + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Table(name = "hashtag") +public class HashTag { + + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(length = 32, nullable = false) + private String tagName; + +} diff --git a/src/main/java/com/likelion/dub/domain/Member.java b/src/main/java/com/likelion/dub/domain/Member.java index a8038b1..29144d9 100644 --- a/src/main/java/com/likelion/dub/domain/Member.java +++ b/src/main/java/com/likelion/dub/domain/Member.java @@ -5,11 +5,8 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; -import jakarta.persistence.OneToMany; import jakarta.persistence.OneToOne; import jakarta.persistence.Table; -import java.util.ArrayList; -import java.util.List; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @@ -26,7 +23,6 @@ public class Member { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - //어플리케이션에서는 기본키 값을 미리 알수 없음, 엔티티를 저장하고 나서야 키 값 확인 기능 @Column(name = "member_id") private Long id; @@ -34,26 +30,20 @@ public class Member { private Club club; - @OneToMany(mappedBy = "member") - private List post = new ArrayList<>(); - - @Column + @Column(length = 512, nullable = false) private String email; - @Column + + @Column(length = 32, nullable = false) private String name; - @Column + + @Column(length = 32, nullable = false) private String password; - @Column + @Column(length = 32, nullable = true) private String gender; - @Column + @Column(length = 32, nullable = false) private String role; - public void setClub(Club club) { - this.club = club; - } - - } diff --git a/src/main/java/com/likelion/dub/domain/Post.java b/src/main/java/com/likelion/dub/domain/Post.java index 26ca014..cec4c16 100644 --- a/src/main/java/com/likelion/dub/domain/Post.java +++ b/src/main/java/com/likelion/dub/domain/Post.java @@ -1,12 +1,20 @@ package com.likelion.dub.domain; -import com.fasterxml.jackson.annotation.JsonIgnore; -import jakarta.persistence.*; -import lombok.*; - -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.Lob; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; @NoArgsConstructor @@ -14,45 +22,26 @@ @AllArgsConstructor @Getter @Setter -@Table(name ="post") -public class Post extends BaseEntity{ +@Table(name = "post") +public class Post extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - - @JsonIgnore - @OneToMany(mappedBy = "post") - private List comments = new ArrayList<>(); - - @Column - private String clubName; - @Column - private String title; - - @Column - @Lob - private String content; - - @Column - private String postImage; - - - @JsonIgnore @ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY) @JoinColumn(name = "club_id") private Club club; - @JsonIgnore - @ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY) - @JoinColumn(name = "member_id") - private Member member; + @Column(length = 32, nullable = false) + private String postTitle; - public void setClub(Club club) { - this.club = club; + @Lob + @Column(nullable = false) + private String content; - } + @Column(length = 512) + private String postImage; } \ No newline at end of file diff --git a/src/main/java/com/likelion/dub/dto/Member/ClubMemberJoinRequest.java b/src/main/java/com/likelion/dub/dto/Member/ClubMemberJoinRequest.java index 330efc4..51fee6c 100644 --- a/src/main/java/com/likelion/dub/dto/Member/ClubMemberJoinRequest.java +++ b/src/main/java/com/likelion/dub/dto/Member/ClubMemberJoinRequest.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.annotation.JsonProperty; -import jakarta.annotation.Nullable; import jakarta.persistence.Lob; import lombok.Getter; import lombok.NoArgsConstructor; @@ -20,14 +19,12 @@ public class ClubMemberJoinRequest { private String name; @JsonProperty private String password; - @JsonProperty - @Nullable - private String gender; + @JsonProperty private String role; @JsonProperty - private String groupName; + private String group; @JsonProperty private String category; diff --git a/src/main/java/com/likelion/dub/dto/Member/ToClubRequest.java b/src/main/java/com/likelion/dub/dto/Member/ToClubRequest.java new file mode 100644 index 0000000..ad6100a --- /dev/null +++ b/src/main/java/com/likelion/dub/dto/Member/ToClubRequest.java @@ -0,0 +1,30 @@ +package com.likelion.dub.dto.Member; + + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.persistence.Lob; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class ToClubRequest { + + @JsonProperty + private String clubName; + + @Lob + @JsonProperty + private String introduction; + + @JsonProperty + private String group; + + @JsonProperty + private String clubImageUrl; + + @JsonProperty + private String formUrl; + + +} diff --git a/src/main/java/com/likelion/dub/repository/MemberRepository.java b/src/main/java/com/likelion/dub/repository/MemberRepository.java index 4b192a5..a6705e4 100644 --- a/src/main/java/com/likelion/dub/repository/MemberRepository.java +++ b/src/main/java/com/likelion/dub/repository/MemberRepository.java @@ -1,14 +1,11 @@ package com.likelion.dub.repository; import com.likelion.dub.domain.Member; -import org.springframework.data.jpa.repository.JpaRepository; - import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; public interface MemberRepository extends JpaRepository { Optional findByEmail(String email); - - } diff --git a/src/main/java/com/likelion/dub/repository/PostRepository.java b/src/main/java/com/likelion/dub/repository/PostRepository.java index 1c9330e..847d4b9 100644 --- a/src/main/java/com/likelion/dub/repository/PostRepository.java +++ b/src/main/java/com/likelion/dub/repository/PostRepository.java @@ -13,8 +13,6 @@ public interface PostRepository extends JpaRepository { @Query("select p from Post p join fetch p.club") List findAll(); - Optional findByClubName(String clubName); - Optional findById(Long id); diff --git a/src/main/java/com/likelion/dub/service/ClubService.java b/src/main/java/com/likelion/dub/service/ClubService.java index 831cb43..533d4c5 100644 --- a/src/main/java/com/likelion/dub/service/ClubService.java +++ b/src/main/java/com/likelion/dub/service/ClubService.java @@ -42,7 +42,7 @@ public void uploadForm(String url) { String clubName = member.getClub().getClubName(); Club club = clubRepository.findByClubName(clubName) .orElseThrow(() -> new BaseException(BaseResponseStatus.NO_SUCH_CLUB_EXIST)); - club.setForm(url); + club.setApplyFormUrl(url); clubRepository.save(club); } @@ -73,7 +73,7 @@ public void updateClubImage(MultipartFile file) { metadata.setContentLength(file.getSize()); try { amazonS3Client.putObject(bucket, fileName, file.getInputStream(), metadata); - club.setClubImage("https://dubs3.s3.ap-northeast-2.amazonaws.com/" + fileName); + club.setClubImageUrl("https://dubs3.s3.ap-northeast-2.amazonaws.com/" + fileName); } catch (IOException e) { throw new BaseException(BaseResponseStatus.FILE_SAVE_ERROR); } @@ -88,7 +88,6 @@ public void updateTag(String groupName, String category) { .orElseThrow(() -> new BaseException(BaseResponseStatus.WRONG_EMAIL)); Club club = member.getClub(); club.setGroupName(groupName); - club.setCategory(category); clubRepository.save(club); } diff --git a/src/main/java/com/likelion/dub/service/MemberService.java b/src/main/java/com/likelion/dub/service/MemberService.java index 0f81882..5274f65 100644 --- a/src/main/java/com/likelion/dub/service/MemberService.java +++ b/src/main/java/com/likelion/dub/service/MemberService.java @@ -2,18 +2,18 @@ import com.amazonaws.services.s3.AmazonS3Client; -import com.amazonaws.services.s3.model.ObjectMetadata; import com.likelion.dub.baseResponse.BaseException; import com.likelion.dub.baseResponse.BaseResponseStatus; -import com.likelion.dub.configuration.JwtTokenUtil; import com.likelion.dub.domain.Club; import com.likelion.dub.domain.Member; import com.likelion.dub.dto.Member.GetMemberInfoResponse; +import com.likelion.dub.dto.Member.MemberJoinRequest; +import com.likelion.dub.dto.Member.ToClubRequest; import com.likelion.dub.dto.OAuth.OAuthInfoResponse; import com.likelion.dub.dto.OAuth.OAuthLoginParams; import com.likelion.dub.repository.ClubRepository; import com.likelion.dub.repository.MemberRepository; -import java.io.IOException; +import com.likelion.dub.util.JwtTokenUtil; import java.util.Optional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -23,7 +23,6 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.multipart.MultipartFile; @Service @@ -34,112 +33,56 @@ public class MemberService { private final MemberRepository memberRepository; private final ClubRepository clubRepository; - private final BCryptPasswordEncoder bCryptPasswordEncoder; - private final AmazonS3Client amazonS3Client; - private final RequestOAuthInfoService requestOAuthInfoService; @Value("${cloud.aws.s3.bucket}") private String bucket; - @Value("${jwt.token.secret}") private String key; private Long expireTimeMs = 1000 * 60 * 60L; //1시간 - public boolean checkEmail(String email) { Optional member = memberRepository.findByEmail(email); - return !member.isPresent(); + if (member.isPresent()) { + throw new BaseException(BaseResponseStatus.EMAIL_ALREADY_EXIST); + } + return true; } - - /** - * 일반회원 회원가입 - * - * @param email - * @param name - * @param password - * @param gender - * @param role - */ - public void join(String email, String name, String password, String gender, String role) { + public void join(MemberJoinRequest memberJoinRequest) { // 중복 이메일 검사 - Optional existingMember = memberRepository.findByEmail(email); + Optional existingMember = memberRepository.findByEmail(memberJoinRequest.getEmail()); if (existingMember.isPresent()) { throw new BaseException(BaseResponseStatus.EMAIL_ALREADY_EXIST); } - Member member = new Member(); - member.setEmail(email); - member.setName(name); - String hashedPassword = bCryptPasswordEncoder.encode(password); + member.setEmail(memberJoinRequest.getEmail()); + member.setName(memberJoinRequest.getName()); + String hashedPassword = bCryptPasswordEncoder.encode(memberJoinRequest.getPassword()); member.setPassword(hashedPassword); - member.setGender(gender); - member.setRole(role); + member.setGender(memberJoinRequest.getGender()); + member.setRole(memberJoinRequest.getRole()); memberRepository.save(member); - } - /** - * 동아리장 회원가입 - * - * @param email - * @param name - * @param password - * @param gender - * @param role - * @param introduction - * @param groupName - * @param category - * @param file - */ - public void joinClub(String email, String name, String password, String gender, String role, - String introduction, String groupName, String category, MultipartFile file) { - - // 중복 이메일 검사 - Optional existingMember = memberRepository.findByEmail(email); - if (existingMember.isPresent()) { - throw new BaseException(BaseResponseStatus.EMAIL_ALREADY_EXIST); - } - try { - // 멤버 저장 - Member member = new Member(); - member.setEmail(email); - member.setName(name); - String hashedPassword = bCryptPasswordEncoder.encode(password); - member.setPassword(hashedPassword); - member.setGender(gender); - member.setRole(role); - memberRepository.save(member); - // 동아리 저장 - Club club = new Club(); - club.setClubName(name); - club.setIntroduction(introduction); - club.setMember(member); - club.setGroupName(groupName); - club.setCategory(category); - if (file != null) { - // 프로필 사진 S3에 저장 - String fileName = name + "_" + "ClubImage"; - ObjectMetadata metadata = new ObjectMetadata(); - metadata.setContentType(file.getContentType()); - metadata.setContentLength(file.getSize()); - amazonS3Client.putObject(bucket, fileName, file.getInputStream(), metadata); - club.setClubImage("https://dubs3.s3.ap-northeast-2.amazonaws.com/" + fileName); - } - clubRepository.save(club); - // 양방향 연관관계설정 //변경 감지 - member.setClub(club); - - } catch (IOException e) { - throw new BaseException(BaseResponseStatus.FILE_SAVE_ERROR); - } - + public void transferToClub(String email, ToClubRequest toClubRequest) { + Member member = memberRepository.findByEmail(email) + .orElseThrow(() -> new BaseException(BaseResponseStatus.NO_SUCH_MEMBER_EXIST)); + Club club = new Club(); + club.setClubName(toClubRequest.getClubName()); + club.setIntroduction(toClubRequest.getIntroduction()); + club.setGroupName(toClubRequest.getGroup()); + club.setClubImageUrl(toClubRequest.getClubImageUrl()); + club.setApplyFormUrl(toClubRequest.getFormUrl()); + club.setMember(member); + member.setClub(club); //변경감지 + clubRepository.save(club); } + public String login(String email, String password) throws BaseException { //email 중복확인 Member member = memberRepository.findByEmail(email) @@ -190,7 +133,6 @@ public GetMemberInfoResponse getInfo() { getMemberInfoResponse.setGender(member.getGender()); getMemberInfoResponse.setRole(member.getRole()); getMemberInfoResponse.setEmail(member.getEmail()); - getMemberInfoResponse.setClub(member.getClub()); return getMemberInfoResponse; } @@ -209,5 +151,6 @@ public void changePassword(String email, String currentPassword, String newPassw } } + } diff --git a/src/main/java/com/likelion/dub/service/PostService.java b/src/main/java/com/likelion/dub/service/PostService.java index 4a4424c..fa1d54f 100644 --- a/src/main/java/com/likelion/dub/service/PostService.java +++ b/src/main/java/com/likelion/dub/service/PostService.java @@ -43,8 +43,8 @@ public List getAllPost() { List allPosts = postRepository.findAll(); // stream 사용 List getAllPostResponses = allPosts.stream() - .map(post -> new GetAllPostResponse(post.getId(), post.getTitle(), post.getClubName(), - post.getClub().getClubImage())) + .map(post -> new GetAllPostResponse(post.getId(), post.getPostTitle(), post.getClub().getClubName(), + post.getClub().getClubImageUrl())) .collect(Collectors.toList()); return getAllPostResponses; } @@ -61,10 +61,8 @@ public void writing(String email, String title, String content, MultipartFile im String clubName = club.getClubName(); // post 객체 생성 및 db 에 저장 Post post = new Post(); - post.setClubName(clubName); post.setClub(club); - post.setMember(member); - post.setTitle(title); + post.setPostTitle(title); post.setContent(content); try { if (image != null) { @@ -94,13 +92,11 @@ public GetOnePostResponse readPost(Long id) throws BaseException { Post post = postRepository.findById(id) .orElseThrow(() -> new BaseException(BaseResponseStatus.NOT_EXISTS_POST)); GetOnePostResponse getOnePostResponse = new GetOnePostResponse(); - getOnePostResponse.setClubName(post.getClubName()); - getOnePostResponse.setTitle(post.getTitle()); + getOnePostResponse.setClubName(post.getClub().getClubName()); + getOnePostResponse.setTitle(post.getPostTitle()); getOnePostResponse.setContent(post.getContent()); getOnePostResponse.setPostImage(post.getPostImage()); - getOnePostResponse.setForm((post.getClub().getForm())); - List comments = null; - getOnePostResponse.setComments(comments); + getOnePostResponse.setForm((post.getClub().getApplyFormUrl())); return getOnePostResponse; } @@ -121,16 +117,8 @@ public void deletePost(Long id) throws BaseException { Member member = memberRepository.findByEmail(email).orElseThrow(); // 로그인 된 클럽 String clubName = member.getClub().getClubName(); - // 로그인 된 post - Post member_post = postRepository.findByClubName(clubName).orElseThrow(); - //post id 가 같은지 검사, 같으면 삭제, 다르면 오류출력 - if (member_post.getId() == id) { - postRepository.deleteById(id); - } else { - - } } diff --git a/src/main/java/com/likelion/dub/configuration/JwtTokenUtil.java b/src/main/java/com/likelion/dub/util/JwtTokenUtil.java similarity index 78% rename from src/main/java/com/likelion/dub/configuration/JwtTokenUtil.java rename to src/main/java/com/likelion/dub/util/JwtTokenUtil.java index 8c41378..edbbb89 100644 --- a/src/main/java/com/likelion/dub/configuration/JwtTokenUtil.java +++ b/src/main/java/com/likelion/dub/util/JwtTokenUtil.java @@ -1,10 +1,8 @@ -package com.likelion.dub.configuration; +package com.likelion.dub.util; import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Jwt; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; - import java.util.Date; public class JwtTokenUtil { @@ -18,22 +16,20 @@ public static String getEmail(String token, String secretKey) { return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token) .getBody().get("email", String.class); } + public static String getName(String token, String secretKey) { return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token) .getBody().get("name", String.class); } - public static boolean isExpired(String token, String secretKey){ + public static boolean isExpired(String token, String secretKey) { return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token) .getBody().getExpiration().before(new Date()); } - /** - * - JWT에서 claims는 사용자 정보를 담은 JSON 객체입니다. claims에는 고유한 ID, 발행자, 만료 시간, 사용자 이름 등이 포함될 수 있습니다. - */ - public static String createToken(String email,String role,String name, String key, Long expireTimeMs) { + + public static String createToken(String email, String role, String name, String key, Long expireTimeMs) { Claims claims = Jwts.claims(); claims.put("email", email); claims.put("role", role); @@ -48,5 +44,4 @@ public static String createToken(String email,String role,String name, String ke } - } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index ad1571a..54d2a52 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,36 +1,28 @@ # MySQL -spring.datasource.url=jdbc:mysql://localhost:3306/hihi +spring.datasource.url=jdbc:mysql://localhost:3306/ttt spring.datasource.username=root spring.datasource.password=whqkr44## spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver - - # Hibernate spring.jpa.show-sql=true spring.jpa.hibernate.ddl-auto=create spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect - #jwt jwt.token.secret=VlwEyVBsYt9V7zq57TejMnVUyzblYcfPQye08f7MGVA9XkHa - #multipart spring.servlet.multipart.enabled=true spring.servlet.multipart.max-file-size=100MB spring.servlet.multipart.max-request-size=100MB - #s3 cloud.aws.s3.bucket=dubs3 cloud.aws.stack.auto=false cloud.aws.region.static=ap-northeast-2 cloud.aws.credentials.accessKey=AKIAVNQICB3FCRQNW4PO cloud.aws.credentials.secretKey=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 - - # Kakao OAuth ?? oauth.kakao.client-id=7f3162e5e7ea72180f4d8a7494e6f05c oauth.kakao.url.auth=https://kauth.kakao.com oauth.kakao.url.api=https://kapi.kakao.com - # Naver OAuth ?? oauth.naver.secret=QmzgfY82j9 oauth.naver.client-id=UCJ4dA_B8TDcKXUT3FJv diff --git a/src/test/java/com/likelion/dub/repository/ClubRepositoryTest.java b/src/test/java/com/likelion/dub/repository/ClubRepositoryTest.java new file mode 100644 index 0000000..37501ca --- /dev/null +++ b/src/test/java/com/likelion/dub/repository/ClubRepositoryTest.java @@ -0,0 +1,51 @@ +package com.likelion.dub.repository; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import com.likelion.dub.domain.Club; +import com.likelion.dub.domain.Member; +import java.util.Optional; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.Sql.ExecutionPhase; +import org.springframework.test.context.jdbc.SqlGroup; + +@DataJpaTest(showSql = true) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@TestPropertySource("classpath:test-application.properties") +@SqlGroup({ + @Sql(value = "/sql/member-repository-test-data.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD), + @Sql(value = "/sql/club-repository-test-data.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) +}) +public class ClubRepositoryTest { + + @Autowired + private ClubRepository clubRepository; + + + @Test + public void findByClubName_으로_동아리를_가져올수있다() { + //given + //when + Optional club = clubRepository.findByClubName("멋쟁이사자"); + + //then + assertThat(club.isPresent()).isTrue(); + } + + @Test + public void findByClubName_으로_동아리장을_가져올수_있다() { + //given + //when + Optional club = clubRepository.findByClubName("멋쟁이사자"); + Optional member = Optional.ofNullable(club.get().getMember()); + //then + assertThat(member.isPresent()).isTrue(); + } + + +} \ No newline at end of file diff --git a/src/test/java/com/likelion/dub/repository/MemberRepositoryTest.java b/src/test/java/com/likelion/dub/repository/MemberRepositoryTest.java new file mode 100644 index 0000000..87425d7 --- /dev/null +++ b/src/test/java/com/likelion/dub/repository/MemberRepositoryTest.java @@ -0,0 +1,68 @@ +package com.likelion.dub.repository; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import com.likelion.dub.domain.Member; +import java.util.Optional; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.jdbc.Sql; + + +@DataJpaTest(showSql = true) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@TestPropertySource("classpath:test-application.properties") +@Sql("/sql/member-repository-test-data.sql") +public class MemberRepositoryTest { + + + @Autowired + private MemberRepository memberRepository; + + + @Test + void findById_로_멤버_찾아오기() { + //given + //when + Optional member = memberRepository.findById(1L); + + //then + assertThat(member.isPresent()).isTrue(); + + } + + @Test + void findById_는_데이터가_없으면_Optional_empty_를_반환한다() { + //given + //when + Optional member = memberRepository.findById(3L); + //then + assertThat(member.isEmpty()).isTrue(); + + } + + @Test + void findByEmail_로_멤버_데이터를_찾아올_수_있다() { + // given + // when + Optional member = memberRepository.findByEmail("suhoon@naver.com"); + + // then + assertThat(member.isPresent()).isTrue(); + } + + @Test + void findByEmail는_데이터가_없으면_Optional_empty_를_내려준다() { + // given + // when + Optional member = memberRepository.findByEmail("fafafafa@naver.com"); + + // then + assertThat(member.isEmpty()).isTrue(); + } + + +} \ No newline at end of file diff --git a/src/test/java/com/likelion/dub/service/MemberServiceTest.java b/src/test/java/com/likelion/dub/service/MemberServiceTest.java new file mode 100644 index 0000000..859c013 --- /dev/null +++ b/src/test/java/com/likelion/dub/service/MemberServiceTest.java @@ -0,0 +1,95 @@ +package com.likelion.dub.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.Sql.ExecutionPhase; +import org.springframework.test.context.jdbc.SqlGroup; +import org.springframework.transaction.annotation.Transactional; + + +@SpringBootTest +@Transactional +@TestPropertySource("classpath:test-application.properties") +@SqlGroup({ + @Sql(value = "/sql/member-service-test-data.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD), +}) +public class MemberServiceTest { + + @Autowired + private MemberService memberService; + + @Test + void checkEmail_로_이메일_중복체크를할수있다() { + //given + String email = "new_email"; + //when + boolean checkEmail = memberService.checkEmail(email); + //then + assertThat(checkEmail).isTrue(); + } + + + @Test + void join_으로_회원가입을할수있다() { + //given + String email = "suhoon@naver.com"; + String name = "조수훈"; + String password = "124"; + String gender = "남자"; + String role = "CLUB"; + + //when + + //then + } + + @Test + void transferToClub() { + //given + + //when + + //then + } + + @Test + void login() { + //given + + //when + + //then + } + + @Test + void loginKakao() { + //given + + //when + + //then + } + + @Test + void getInfo() { + //given + + //when + + //then + } + + @Test + void changePassword() { + //given + + //when + + //then + } +} \ No newline at end of file diff --git a/src/test/java/com/likelion/dub/service/PostServiceTest.java b/src/test/java/com/likelion/dub/service/PostServiceTest.java deleted file mode 100644 index b127ed8..0000000 --- a/src/test/java/com/likelion/dub/service/PostServiceTest.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.likelion.dub.service; - -import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.transaction.Transactional; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.web.servlet.MockMvc; - -@SpringBootTest -@AutoConfigureMockMvc -@Transactional -class PostServiceTest { - - @Autowired - protected MockMvc mvc; - @Autowired - protected ObjectMapper objectMapper; - - -} \ No newline at end of file diff --git a/src/test/resources/sql/club-repository-test-data.sql b/src/test/resources/sql/club-repository-test-data.sql new file mode 100644 index 0000000..e65a31c --- /dev/null +++ b/src/test/resources/sql/club-repository-test-data.sql @@ -0,0 +1,4 @@ +insert into `club` (`club_id`, `apply_form_url`, `club_image_url`, `club_name`, `group_name`, `introduction`,`member_id`) +values (1, 'naver.com','naver.com','멋쟁이사자','코딩동아리','안녕하세요!!멋재이사자입니다.',1); +insert into `club` (`club_id`, `apply_form_url`, `club_image_url`, `club_name`, `group_name`, `introduction`,`member_id`) +values (2, 'nav2er.com','nav2er.com','멋쟁이사자2','코딩동아리2','안녕하세요!!멋재이사자입니다.',2); \ No newline at end of file diff --git a/src/test/resources/sql/member-repository-test-data.sql b/src/test/resources/sql/member-repository-test-data.sql new file mode 100644 index 0000000..689ea9c --- /dev/null +++ b/src/test/resources/sql/member-repository-test-data.sql @@ -0,0 +1,4 @@ +insert into `member` (`member_id`, `email`, `gender`, `name`, `password`, `role`) +values (1, 'suhoon@naver.com','남자','조수훈','1234','CLUB'); +insert into `member` (`member_id`, `email`, `gender`, `name`, `password`, `role`) +values (2, 'sohoon@naver.com','여자','조소훈','1234','USER'); \ No newline at end of file diff --git a/src/test/resources/sql/member-service-test-data.sql b/src/test/resources/sql/member-service-test-data.sql new file mode 100644 index 0000000..689ea9c --- /dev/null +++ b/src/test/resources/sql/member-service-test-data.sql @@ -0,0 +1,4 @@ +insert into `member` (`member_id`, `email`, `gender`, `name`, `password`, `role`) +values (1, 'suhoon@naver.com','남자','조수훈','1234','CLUB'); +insert into `member` (`member_id`, `email`, `gender`, `name`, `password`, `role`) +values (2, 'sohoon@naver.com','여자','조소훈','1234','USER'); \ No newline at end of file diff --git a/src/test/resources/test-application.properties b/src/test/resources/test-application.properties new file mode 100644 index 0000000..54d2a52 --- /dev/null +++ b/src/test/resources/test-application.properties @@ -0,0 +1,30 @@ +# MySQL +spring.datasource.url=jdbc:mysql://localhost:3306/ttt +spring.datasource.username=root +spring.datasource.password=whqkr44## +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver +# Hibernate +spring.jpa.show-sql=true +spring.jpa.hibernate.ddl-auto=create +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect +#jwt +jwt.token.secret=VlwEyVBsYt9V7zq57TejMnVUyzblYcfPQye08f7MGVA9XkHa +#multipart +spring.servlet.multipart.enabled=true +spring.servlet.multipart.max-file-size=100MB +spring.servlet.multipart.max-request-size=100MB +#s3 +cloud.aws.s3.bucket=dubs3 +cloud.aws.stack.auto=false +cloud.aws.region.static=ap-northeast-2 +cloud.aws.credentials.accessKey=AKIAVNQICB3FCRQNW4PO +cloud.aws.credentials.secretKey=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 +# Kakao OAuth ?? +oauth.kakao.client-id=7f3162e5e7ea72180f4d8a7494e6f05c +oauth.kakao.url.auth=https://kauth.kakao.com +oauth.kakao.url.api=https://kapi.kakao.com +# Naver OAuth ?? +oauth.naver.secret=QmzgfY82j9 +oauth.naver.client-id=UCJ4dA_B8TDcKXUT3FJv +oauth.naver.url.auth=https://nid.naver.com +oauth.naver.url.api=https://openapi.naver.com \ No newline at end of file From 13c57518d36d12a72c64c28b143aaff8a4a4afba Mon Sep 17 00:00:00 2001 From: suhoon Date: Wed, 25 Oct 2023 01:27:25 +0900 Subject: [PATCH 71/72] =?UTF-8?q?refactor=20:=20clubEntity=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/likelion/dub/domain/Club.java | 10 ++++- .../com/likelion/dub/domain/ClubApply.java | 10 +++++ .../java/com/likelion/dub/domain/Member.java | 2 +- .../dub/dto/Member/ToClubRequest.java | 14 +++++- .../com/likelion/dub/service/ClubService.java | 2 +- .../likelion/dub/service/MemberService.java | 11 +++-- .../com/likelion/dub/service/PostService.java | 1 - src/main/resources/application.properties | 2 +- .../dub/repository/ClubRepositoryTest.java | 7 ++- .../dub/service/MemberServiceTest.java | 45 +++++++++++++++---- .../sql/club-repository-test-data.sql | 17 +++++-- .../sql/member-repository-test-data.sql | 2 +- .../sql/member-service-test-data.sql | 2 +- .../resources/test-application.properties | 2 +- 14 files changed, 95 insertions(+), 32 deletions(-) diff --git a/src/main/java/com/likelion/dub/domain/Club.java b/src/main/java/com/likelion/dub/domain/Club.java index e06faae..fc85bb7 100644 --- a/src/main/java/com/likelion/dub/domain/Club.java +++ b/src/main/java/com/likelion/dub/domain/Club.java @@ -50,8 +50,14 @@ public class Club { @Column(length = 512, nullable = false) private String clubImageUrl; - @Column(length = 512, nullable = false) - private String applyFormUrl; + @Column(length = 512) + private String question1; + + @Column(length = 512) + private String question2; + + @Column(length = 512) + private String question3; } diff --git a/src/main/java/com/likelion/dub/domain/ClubApply.java b/src/main/java/com/likelion/dub/domain/ClubApply.java index e5d5500..294e2f9 100644 --- a/src/main/java/com/likelion/dub/domain/ClubApply.java +++ b/src/main/java/com/likelion/dub/domain/ClubApply.java @@ -2,6 +2,7 @@ import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; @@ -36,4 +37,13 @@ public class ClubApply extends BaseEntity { @JoinColumn(name = "club_id") private Club club; + @Column(length = 512) + private String answer1; + + @Column(length = 512) + private String answer2; + + @Column(length = 512) + private String answer3; + } diff --git a/src/main/java/com/likelion/dub/domain/Member.java b/src/main/java/com/likelion/dub/domain/Member.java index 29144d9..ae55f46 100644 --- a/src/main/java/com/likelion/dub/domain/Member.java +++ b/src/main/java/com/likelion/dub/domain/Member.java @@ -36,7 +36,7 @@ public class Member { @Column(length = 32, nullable = false) private String name; - @Column(length = 32, nullable = false) + @Column(length = 512, nullable = false) private String password; @Column(length = 32, nullable = true) diff --git a/src/main/java/com/likelion/dub/dto/Member/ToClubRequest.java b/src/main/java/com/likelion/dub/dto/Member/ToClubRequest.java index ad6100a..1a281b3 100644 --- a/src/main/java/com/likelion/dub/dto/Member/ToClubRequest.java +++ b/src/main/java/com/likelion/dub/dto/Member/ToClubRequest.java @@ -3,11 +3,15 @@ import com.fasterxml.jackson.annotation.JsonProperty; import jakarta.persistence.Lob; +import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.Setter; @Getter +@Setter @NoArgsConstructor +@AllArgsConstructor public class ToClubRequest { @JsonProperty @@ -24,7 +28,13 @@ public class ToClubRequest { private String clubImageUrl; @JsonProperty - private String formUrl; - + private String question1; + + @JsonProperty + private String question2; + + @JsonProperty + private String question3; + } diff --git a/src/main/java/com/likelion/dub/service/ClubService.java b/src/main/java/com/likelion/dub/service/ClubService.java index 533d4c5..b48f846 100644 --- a/src/main/java/com/likelion/dub/service/ClubService.java +++ b/src/main/java/com/likelion/dub/service/ClubService.java @@ -42,7 +42,7 @@ public void uploadForm(String url) { String clubName = member.getClub().getClubName(); Club club = clubRepository.findByClubName(clubName) .orElseThrow(() -> new BaseException(BaseResponseStatus.NO_SUCH_CLUB_EXIST)); - club.setApplyFormUrl(url); + clubRepository.save(club); } diff --git a/src/main/java/com/likelion/dub/service/MemberService.java b/src/main/java/com/likelion/dub/service/MemberService.java index 5274f65..2655acf 100644 --- a/src/main/java/com/likelion/dub/service/MemberService.java +++ b/src/main/java/com/likelion/dub/service/MemberService.java @@ -52,7 +52,7 @@ public boolean checkEmail(String email) { return true; } - public void join(MemberJoinRequest memberJoinRequest) { + public String join(MemberJoinRequest memberJoinRequest) { // 중복 이메일 검사 Optional existingMember = memberRepository.findByEmail(memberJoinRequest.getEmail()); if (existingMember.isPresent()) { @@ -64,11 +64,12 @@ public void join(MemberJoinRequest memberJoinRequest) { String hashedPassword = bCryptPasswordEncoder.encode(memberJoinRequest.getPassword()); member.setPassword(hashedPassword); member.setGender(memberJoinRequest.getGender()); - member.setRole(memberJoinRequest.getRole()); + member.setRole("USER"); memberRepository.save(member); + return member.getName(); } - public void transferToClub(String email, ToClubRequest toClubRequest) { + public String transferToClub(String email, ToClubRequest toClubRequest) { Member member = memberRepository.findByEmail(email) .orElseThrow(() -> new BaseException(BaseResponseStatus.NO_SUCH_MEMBER_EXIST)); Club club = new Club(); @@ -76,10 +77,12 @@ public void transferToClub(String email, ToClubRequest toClubRequest) { club.setIntroduction(toClubRequest.getIntroduction()); club.setGroupName(toClubRequest.getGroup()); club.setClubImageUrl(toClubRequest.getClubImageUrl()); - club.setApplyFormUrl(toClubRequest.getFormUrl()); + club.setQuestion1("지원동기??"); club.setMember(member); member.setClub(club); //변경감지 + member.setRole("CLUB"); //변경감지 clubRepository.save(club); + return club.getClubName(); } diff --git a/src/main/java/com/likelion/dub/service/PostService.java b/src/main/java/com/likelion/dub/service/PostService.java index fa1d54f..14c279d 100644 --- a/src/main/java/com/likelion/dub/service/PostService.java +++ b/src/main/java/com/likelion/dub/service/PostService.java @@ -96,7 +96,6 @@ public GetOnePostResponse readPost(Long id) throws BaseException { getOnePostResponse.setTitle(post.getPostTitle()); getOnePostResponse.setContent(post.getContent()); getOnePostResponse.setPostImage(post.getPostImage()); - getOnePostResponse.setForm((post.getClub().getApplyFormUrl())); return getOnePostResponse; } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 54d2a52..f4c01cf 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,5 +1,5 @@ # MySQL -spring.datasource.url=jdbc:mysql://localhost:3306/ttt +spring.datasource.url=jdbc:mysql://localhost:3306/tttt spring.datasource.username=root spring.datasource.password=whqkr44## spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver diff --git a/src/test/java/com/likelion/dub/repository/ClubRepositoryTest.java b/src/test/java/com/likelion/dub/repository/ClubRepositoryTest.java index 37501ca..ebd4099 100644 --- a/src/test/java/com/likelion/dub/repository/ClubRepositoryTest.java +++ b/src/test/java/com/likelion/dub/repository/ClubRepositoryTest.java @@ -18,7 +18,6 @@ @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) @TestPropertySource("classpath:test-application.properties") @SqlGroup({ - @Sql(value = "/sql/member-repository-test-data.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD), @Sql(value = "/sql/club-repository-test-data.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) }) public class ClubRepositoryTest { @@ -31,17 +30,17 @@ public class ClubRepositoryTest { public void findByClubName_으로_동아리를_가져올수있다() { //given //when - Optional club = clubRepository.findByClubName("멋쟁이사자"); + Optional club = clubRepository.findByClubName("멋쟁이사지"); //then assertThat(club.isPresent()).isTrue(); } - + @Test public void findByClubName_으로_동아리장을_가져올수_있다() { //given //when - Optional club = clubRepository.findByClubName("멋쟁이사자"); + Optional club = clubRepository.findByClubName("멋쟁이사지"); Optional member = Optional.ofNullable(club.get().getMember()); //then assertThat(member.isPresent()).isTrue(); diff --git a/src/test/java/com/likelion/dub/service/MemberServiceTest.java b/src/test/java/com/likelion/dub/service/MemberServiceTest.java index 859c013..ef6dd06 100644 --- a/src/test/java/com/likelion/dub/service/MemberServiceTest.java +++ b/src/test/java/com/likelion/dub/service/MemberServiceTest.java @@ -1,10 +1,16 @@ package com.likelion.dub.service; -import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import com.likelion.dub.dto.Member.MemberJoinRequest; +import com.likelion.dub.dto.Member.ToClubRequest; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; +import org.mockito.BDDMockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.jdbc.Sql; import org.springframework.test.context.jdbc.Sql.ExecutionPhase; @@ -22,6 +28,9 @@ public class MemberServiceTest { @Autowired private MemberService memberService; + @MockBean + private BCryptPasswordEncoder bCryptPasswordEncoder; + @Test void checkEmail_로_이메일_중복체크를할수있다() { @@ -30,7 +39,7 @@ public class MemberServiceTest { //when boolean checkEmail = memberService.checkEmail(email); //then - assertThat(checkEmail).isTrue(); + Assertions.assertThat(checkEmail).isTrue(); } @@ -42,28 +51,46 @@ public class MemberServiceTest { String password = "124"; String gender = "남자"; String role = "CLUB"; - + MemberJoinRequest memberJoinRequest = new MemberJoinRequest(email, name, password, gender, role); //when - + String memberName = memberService.join(memberJoinRequest); //then + Assertions.assertThat(memberName).isEqualTo("조수훈"); } @Test - void transferToClub() { + void transferToClub_으로동아리전환을할수있다() { //given - + String email = "suhoon@naver.com"; + String club_name = "UMC"; + String introduction = "안녕하세요UMC입니다"; + String group_name = "코딩동아리"; + String club_image_url = "naver.com"; + String question1 = "지원동기??"; + ToClubRequest toClubRequest = new ToClubRequest(); + toClubRequest.setClubName(club_name); + toClubRequest.setIntroduction(introduction); + toClubRequest.setGroup(group_name); + toClubRequest.setClubImageUrl(club_image_url); + toClubRequest.setQuestion1(question1); //when - + String clubName = memberService.transferToClub(email, toClubRequest); //then + Assertions.assertThat(clubName).isEqualTo("UMC"); + } @Test - void login() { + void login_으로_로그인할수가있다() { //given - + String email = "suhoon@naver.com"; + String password = "1234"; + BDDMockito.given(bCryptPasswordEncoder.matches(any(), any())).willReturn(true); //when + String token = memberService.login(email, password); //then + Assertions.assertThat(token).isNotEmpty(); } @Test diff --git a/src/test/resources/sql/club-repository-test-data.sql b/src/test/resources/sql/club-repository-test-data.sql index e65a31c..8d61f7a 100644 --- a/src/test/resources/sql/club-repository-test-data.sql +++ b/src/test/resources/sql/club-repository-test-data.sql @@ -1,4 +1,13 @@ -insert into `club` (`club_id`, `apply_form_url`, `club_image_url`, `club_name`, `group_name`, `introduction`,`member_id`) -values (1, 'naver.com','naver.com','멋쟁이사자','코딩동아리','안녕하세요!!멋재이사자입니다.',1); -insert into `club` (`club_id`, `apply_form_url`, `club_image_url`, `club_name`, `group_name`, `introduction`,`member_id`) -values (2, 'nav2er.com','nav2er.com','멋쟁이사자2','코딩동아리2','안녕하세요!!멋재이사자입니다.',2); \ No newline at end of file +-- Inserting data into the "member" table +INSERT INTO `member` (`member_id`, `email`, `gender`, `name`, `password`, `role`) +VALUES (1, 'suhoon@naver.com', '남자', '조수훈', '1234', 'USER'); + +INSERT INTO `member` (`member_id`, `email`, `gender`, `name`, `password`, `role`) +VALUES (2, 'sohoon@naver.com', '여자', '조소훈', '1234', 'USER'); + +-- Inserting data into the "club" table +INSERT INTO `club` (`club_id`, `club_image_url`, `club_name`, `group_name`, `introduction`, `question1`, `member_id`) +VALUES (1, 'naver.com', '멋쟁이사지', '코딩동아리', '안녕하세요!!멋재이사자입니다.', '지원동기??', 1); + +INSERT INTO `club` (`club_id`, `club_image_url`, `club_name`, `group_name`, `introduction`, `question1`, `member_id`) +VALUES (2, 'naver.com', 'UMC','코딩동아리2', '안녕하세요!!UMC입니다.', '지원동기??', 2); diff --git a/src/test/resources/sql/member-repository-test-data.sql b/src/test/resources/sql/member-repository-test-data.sql index 689ea9c..72bffe9 100644 --- a/src/test/resources/sql/member-repository-test-data.sql +++ b/src/test/resources/sql/member-repository-test-data.sql @@ -1,4 +1,4 @@ insert into `member` (`member_id`, `email`, `gender`, `name`, `password`, `role`) -values (1, 'suhoon@naver.com','남자','조수훈','1234','CLUB'); +values (1, 'suhoon@naver.com','남자','조수훈','1234','USER'); insert into `member` (`member_id`, `email`, `gender`, `name`, `password`, `role`) values (2, 'sohoon@naver.com','여자','조소훈','1234','USER'); \ No newline at end of file diff --git a/src/test/resources/sql/member-service-test-data.sql b/src/test/resources/sql/member-service-test-data.sql index 689ea9c..72bffe9 100644 --- a/src/test/resources/sql/member-service-test-data.sql +++ b/src/test/resources/sql/member-service-test-data.sql @@ -1,4 +1,4 @@ insert into `member` (`member_id`, `email`, `gender`, `name`, `password`, `role`) -values (1, 'suhoon@naver.com','남자','조수훈','1234','CLUB'); +values (1, 'suhoon@naver.com','남자','조수훈','1234','USER'); insert into `member` (`member_id`, `email`, `gender`, `name`, `password`, `role`) values (2, 'sohoon@naver.com','여자','조소훈','1234','USER'); \ No newline at end of file diff --git a/src/test/resources/test-application.properties b/src/test/resources/test-application.properties index 54d2a52..f4c01cf 100644 --- a/src/test/resources/test-application.properties +++ b/src/test/resources/test-application.properties @@ -1,5 +1,5 @@ # MySQL -spring.datasource.url=jdbc:mysql://localhost:3306/ttt +spring.datasource.url=jdbc:mysql://localhost:3306/tttt spring.datasource.username=root spring.datasource.password=whqkr44## spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver From 47c76fc14a35d10c2c8ad28c48df05c8d2b4f441 Mon Sep 17 00:00:00 2001 From: suhoon Date: Sat, 30 Mar 2024 12:13:36 +0900 Subject: [PATCH 72/72] =?UTF-8?q?reset:=20DDD=20=EC=9D=B4=EC=A0=84=20?= =?UTF-8?q?=EC=BB=A4=EB=B0=8B=EC=9C=BC=EB=A1=9C=20=EB=A1=A4=EB=B0=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 132 +----------------------------------------------------- 1 file changed, 2 insertions(+), 130 deletions(-) diff --git a/README.md b/README.md index 1306f49..33d791d 100644 --- a/README.md +++ b/README.md @@ -43,12 +43,10 @@ # API 엔드포인트 목록 및 사용법 -https://woozy-cuticle-bfb.notion.site/dub_-wanted-5f89e6bcf87142eca927893ff04703f6?pvs=4 - - --- # CI/CD Flow + ![CIcd](https://github.com/s2hoon/dub_club_wanted_BE/assets/82464990/120399f7-7d09-4996-a8ac-631c4024a4fe) 1. main branch 에 Push 또는 Merge @@ -59,131 +57,5 @@ https://woozy-cuticle-bfb.notion.site/dub_-wanted-5f89e6bcf87142eca927893ff04703 --- # ERD -![기존](https://github.com/s2hoon/dub_club_wanted_BE/assets/82464990/ced95902-d874-4bc8-8d39-5b5a0da96a9e) - - ---- - -## Project Structure - -```java -├─baseResponse -├─configuration -├─controller -├─domain -├─dto -│ ├─Club -│ ├─Comment -│ ├─Member -│ ├─OAuth -│ └─Post -├─repository -└─service -``` - -> +---baseResponse -| BaseException.java -| BaseResponse.java -| BaseResponseStatus.java -> -1. `BaseException.java`: - - `BaseException`은 `RuntimeException` 클래스를 상속받고 있습니다. 이는 실행 중 발생할 수 있는 예외를 나타내는 클래스입니다. - - `BaseException` 클래스는 `BaseResponseStatus`를 필드 값으로 가지고 있습니다. 즉, 예외가 발생했을 때 해당 예외의 원인을 `BaseResponseStatus`로 표시할 수 있습니다. -2. `BaseResponse.java`: - - 모든 응답(Response) 객체는 이 클래스를 통해 생성됩니다. - - 오버로딩(Overloading)을 통해 생성자가 구현되어 있으며, `result`를 매개변수로 받으면 요청이 성공한 경우를 나타내고, `result`를 매개변수로 받지 않으면 요청이 실패한 경우를 나타냅니다. -3. `BaseResponseStatus.java`: - - 이 클래스는 열거형(Enum) 파일로 정의되어 있으며, 코드값과 메시지를 필드로 가지고 있습니다. - `BaseResponse`나 `BaseException`에서 사용되는 상태 코드와 메시지를 정의하는 데 사용됩니다. 이러한 정의를 통해 각각의 상태를 나타내고 관리할 수 있습니다. - -> +---configuration -| ClientConfig.java -| EncoderConfig.java -| JwtTokenFilter.java -| JwtTokenUtil.java -| S3config.java -| SecurityConfig.java -> -1. `ClientConfig.java`: - - 이 파일은 `RestTemplate`을 사용하기 위해 Spring 빈으로 등록합니다. - - 모든 Rest API 요청은 이 빈을 통해 진행됩니다. `RestTemplate`은 HTTP 요청을 쉽게 수행하고 응답을 처리하는 데 사용됩니다. -2. `EncoderConfig.java`: - - 이 파일은 `BCryptPasswordEncoder`를 Spring 빈으로 등록합니다. - - 모든 비밀번호는 이 빈을 통해 암호화됩니다. `BCryptPasswordEncoder`는 보안성이 높은 비밀번호 해싱을 제공하는 데 사용됩니다. -3. `JwtTokenFilter.java`: - - 이 파일은 JWT(Jason Web Token) 토큰을 검증하고 권한을 부여하는 로직을 포함합니다. - - Spring Security 과 함께 사용됩니다. -4. `JwtTokenUtil.java`: - - 이 파일은 JWT 토큰을 생성하는 로직을 포함합니다. - - JWT는 인증된 사용자를 식별하고 정보를 보호하기 위한 토큰 방식입니다. -5. `S3config.java`: - - 이 파일은 Amazon S3를 사용하기 위한 설정 파일입니다. - - Amazon S3는 클라우드 기반의 파일 저장소이며, 파일 업로드 및 다운로드와 같은 작업을 수행하기 위한 설정을 제공합니다. -6. `SecurityConfig`: - - 이 파일은 Spring Security를 적용하기 위한 설정 파일입니다. - - Spring Security를 사용하여 사용자 인증, 권한 부여 및 보안 설정을 관리합니다. - -> +---controller -| ClubController.java -| JspController.java -| MemberController.java -| PostController.java -> - -> +---service -| ClubService.java -| MemberService.java -| PostService.java -| RequestOAuthInfoService.java -> - -> +---repository -| ClubRepository.java -| MemberRepository.java -| PostRepository.java -> -1. `controller` 디렉토리: - - 이 디렉토리는 컨트롤러(Controller) 클래스들을 포함합니다. - - 컨트롤러는 클라이언트의 HTTP 요청을 처리하고 해당 요청에 대한 비즈니스 로직을 호출하며, 결과를 클라이언트에게 반환합니다. -2. `service` 디렉토리: - - 이 디렉토리는 서비스(Service) 인터페이스 및 구현 클래스들을 포함합니다. - - 서비스는 비즈니스 로직을 추상화하고, 컨트롤러와 리포지토리 간의 중간 계층 역할을 합니다. 비즈니스 로직을 수행하고 데이터베이스와 상호작용하는데 사용됩니다. -3. `repository` 디렉토리: - - 이 디렉토리는 리포지토리(Repository) 인터페이스들을 포함합니다. - - 리포지토리는 데이터베이스와의 상호작용을 담당하며, 데이터베이스에서 데이터를 검색하고 조작하는 데 사용됩니다. - -> +---domain -| BaseEntity.java -| BaseTimeEntity.java -| Club.java -| Comment.java -| Member.java -| Post.java -| Role.java -> -1. `BaseEntity.java`: - - 엔티티의 공통적인 속성을 정의하고, 이러한 속성을 상속받아 재사용될 수 있도록 합니다. - - `createdBy` 와 `lastModifiedBy` 가 필드로 추가 되어 있습니다. -2. `BaseTimeEntity.java`: - - 엔티티의 공통적인 속성을 정의하고, 이러한 속성을 상속받아 재사용될 수 있도록 합니다. - - `createdDate` , `lastModifiedDate` 가 필드로 추가 되어 있습니다. -3. `Club.java`: - - 동아리에 관한 엔티티 입니다. -4. `Comment.java`: - - 댓글에 관한 엔티티 입니다. -5. `Member.java`: - - 회원에 관한 엔티티 입니다. -6. `Post.java`: - - 게시물에 관한 엔티티 입니다. -7. `Role.java`: - - 역할을 열거형(Enum)으로 정의하여 사용자의 권한을 관리하는 데 사용됩니다. - ---- - - - -# 성능개선 - - - +![erd](https://github.com/s2hoon/dub_club_wanted_BE/assets/82464990/d333d553-4d3a-45b5-a492-7f9ee254873e)