From 731f0fa1714887cf0151333d37a32259d4d32fef Mon Sep 17 00:00:00 2001 From: Doy Date: Fri, 1 Sep 2023 16:32:48 +0900 Subject: [PATCH 01/34] =?UTF-8?q?[BE]=20Fix/#366=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=88=98=ED=96=89=20=EC=8B=9C=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=20=ED=8C=A8=ED=84=B4=20=EA=B9=A8=EC=A7=80=EB=8A=94=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=ED=95=B4=EA=B2=B0=20(#367)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 테스트 로그 패턴 설정 오류 수정 - 잘못된 로그 패턴 설정으로 인한 'LOG_PATTERN IS_UNDEFINED' 메시지 출력 오류 수정 * chore: 프론트엔드, 백엔드 develop 분리에 따른 워크플로우 수정 * fix: 테스트 로그 설정 파일명 변경 - springProperty 지원을 위해 파일명 변경 * feat: 로그 내용 및 설정 보완(색상 적용) - 로그에서 로거, 레벨, PID 확인 가능하도록 내용 보완 - 테스트 로그의 경우 프로젝트 패키지에 해당하는 로그만 DEBUG 레벨로 설정 - 콘솔 로그 색상 적용 * feat: 로그 내용 보완 - 스레드 출력하도록 수정 --- .github/workflows/be-merge-dev.yml | 2 +- .github/workflows/be-pull-request.yml | 2 +- .../src/main/resources/application-dev.yml | 4 ++-- .../src/main/resources/application-prod.yml | 5 ++--- backend/src/main/resources/application.yml | 5 ++--- backend/src/main/resources/logback-spring.xml | 17 ++++++++--------- backend/src/test/resources/application.yml | 4 +--- backend/src/test/resources/logback-spring.xml | 19 +++++++++++++++++++ backend/src/test/resources/logback-test.xml | 17 ----------------- 9 files changed, 36 insertions(+), 39 deletions(-) create mode 100644 backend/src/test/resources/logback-spring.xml delete mode 100644 backend/src/test/resources/logback-test.xml diff --git a/.github/workflows/be-merge-dev.yml b/.github/workflows/be-merge-dev.yml index 608e8650..66a000a8 100644 --- a/.github/workflows/be-merge-dev.yml +++ b/.github/workflows/be-merge-dev.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: pull_request: - branches: [ develop ] + branches: [ develop-BE ] types: [ closed ] paths: backend/** diff --git a/.github/workflows/be-pull-request.yml b/.github/workflows/be-pull-request.yml index ab62a78e..0f5de95b 100644 --- a/.github/workflows/be-pull-request.yml +++ b/.github/workflows/be-pull-request.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: pull_request: - branches: [ main, develop ] + branches: [ main, develop-BE ] paths: backend/** permissions: diff --git a/backend/src/main/resources/application-dev.yml b/backend/src/main/resources/application-dev.yml index 703003e8..122d5ea2 100644 --- a/backend/src/main/resources/application-dev.yml +++ b/backend/src/main/resources/application-dev.yml @@ -23,8 +23,8 @@ logging: # jar 파일 실행 시, jar 파일과 동일한 디렉토리 내 log 디렉토리에 생성됨 path: /home/ubuntu/backend/build/log pattern: - console: "%d{yyyy-MM-dd HH:mm:ss.SSS} %msg %n" - file: "%d{yyyy-MM-dd HH:mm:ss.SSS} %msg %n" + console: "%green(%d{yyyy-MM-dd HH:mm:ss.SSS}) [%thread] ${PID} %highlight(%5level) %cyan(%logger) - %msg%n" + file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] ${PID} %5level %logger - %msg%n" oauth: kakao: diff --git a/backend/src/main/resources/application-prod.yml b/backend/src/main/resources/application-prod.yml index b88d6f7d..59dc54ba 100644 --- a/backend/src/main/resources/application-prod.yml +++ b/backend/src/main/resources/application-prod.yml @@ -10,11 +10,10 @@ spring: logging: file: - # jar 파일 실행 시, jar 파일과 동일한 디렉토리 내 log 디렉토리에 생성됨 path: /home/ubuntu/backend/build/log pattern: - console: "%d{yyyy-MM-dd HH:mm:ss.SSS} %msg %n" - file: "%d{yyyy-MM-dd HH:mm:ss.SSS} %msg %n" + console: "%green(%d{yyyy-MM-dd HH:mm:ss.SSS}) [%thread] ${PID} %highlight(%5level) %cyan(%logger) - %msg%n" + file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] ${PID} %5level %logger - %msg%n" oauth: kakao: diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 6a1d55ec..4d2cf048 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -22,11 +22,10 @@ logging: sql: BasicBinder: TRACE file: - # jar 파일 실행 시, jar 파일과 동일한 디렉토리 내 log 디렉토리에 생성됨 path: ./log pattern: - console: "%d{yyyy-MM-dd HH:mm:ss.SSS} %msg %n" - file: "%d{yyyy-MM-dd HH:mm:ss.SSS} %msg %n" + console: "%green(%d{yyyy-MM-dd HH:mm:ss.SSS}) [%thread] ${PID} %highlight(%5level) %cyan(%logger) - %msg%n" + file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] ${PID} %5level %logger - %msg%n" oauth: kakao: diff --git a/backend/src/main/resources/logback-spring.xml b/backend/src/main/resources/logback-spring.xml index da5246e9..6ca7c8d1 100644 --- a/backend/src/main/resources/logback-spring.xml +++ b/backend/src/main/resources/logback-spring.xml @@ -1,17 +1,16 @@ - + + - - - - ${LOG_PATTERN} - - + + + ${CONSOLE_LOG_PATTERN} + @@ -24,7 +23,7 @@ ${LOG_PATH}/warn.log - ${LOG_PATTERN} + ${FILE_LOG_PATTERN} @@ -38,7 +37,7 @@ ${LOG_PATH}/error.log - ${LOG_PATTERN} + ${FILE_LOG_PATTERN} diff --git a/backend/src/test/resources/application.yml b/backend/src/test/resources/application.yml index 109e8daf..65e0aadb 100644 --- a/backend/src/test/resources/application.yml +++ b/backend/src/test/resources/application.yml @@ -21,10 +21,8 @@ spring: logging: level: org.hibernate.orm.jdbc.bind: trace - org.hibernate.type: trace - org.hibernate.stat: debug pattern: - console: "%d{yyyy-MM-dd HH:mm:ss.SSS} %msg %n" + console: "%green(%d{yyyy-MM-dd HH:mm:ss.SSS}) [%thread] ${PID} %highlight(%5level) %cyan(%logger) - %msg%n" oauth: kakao: diff --git a/backend/src/test/resources/logback-spring.xml b/backend/src/test/resources/logback-spring.xml new file mode 100644 index 00000000..0bf60ac9 --- /dev/null +++ b/backend/src/test/resources/logback-spring.xml @@ -0,0 +1,19 @@ + + + + + + + + ${LOG_PATTERN} + + + + + + + + + + + diff --git a/backend/src/test/resources/logback-test.xml b/backend/src/test/resources/logback-test.xml deleted file mode 100644 index af76e11c..00000000 --- a/backend/src/test/resources/logback-test.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - ${LOG_PATTERN} - - - - - - - - - From 6c887a6eda2ede43ef8a0d67862d3858eb95bdb3 Mon Sep 17 00:00:00 2001 From: zun <50602742+cpot5620@users.noreply.github.com> Date: Fri, 1 Sep 2023 16:42:54 +0900 Subject: [PATCH 02/34] =?UTF-8?q?refactor:=20DataBaseCleanup=20JdbcTemplat?= =?UTF-8?q?e=20=EC=A0=81=EC=9A=A9=20(#371)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mapbefine/mapbefine/DatabaseCleanup.java | 81 ++++++------------- 1 file changed, 23 insertions(+), 58 deletions(-) diff --git a/backend/src/test/java/com/mapbefine/mapbefine/DatabaseCleanup.java b/backend/src/test/java/com/mapbefine/mapbefine/DatabaseCleanup.java index 91694b9b..d5883ea4 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/DatabaseCleanup.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/DatabaseCleanup.java @@ -1,12 +1,12 @@ package com.mapbefine.mapbefine; -import jakarta.persistence.Entity; -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; -import jakarta.persistence.metamodel.EntityType; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; import java.util.List; -import java.util.Set; import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; @@ -14,85 +14,50 @@ public class DatabaseCleanup implements InitializingBean { private static final String TRUNCATE_SQL_MESSAGE = "TRUNCATE TABLE %s"; - private static final String ID_RESET_SQL_MESSAGE = "ALTER TABLE %s ALTER COLUMN ID RESTART WITH 1"; private static final String SET_REFERENTIAL_INTEGRITY_SQL_MESSAGE = "SET REFERENTIAL_INTEGRITY %s"; - private static final String UNDERSCORE = "_"; private static final String DISABLE_REFERENTIAL_QUERY = String.format(SET_REFERENTIAL_INTEGRITY_SQL_MESSAGE, false); private static final String ENABLE_REFERENTIAL_QUERY = String.format(SET_REFERENTIAL_INTEGRITY_SQL_MESSAGE, true); - @PersistenceContext - private EntityManager entityManager; + @Autowired + private JdbcTemplate jdbcTemplate; - private List tableNames; + private List tableNames = new ArrayList<>(); @Override - public void afterPropertiesSet() { - Set> entities = entityManager.getMetamodel() - .getEntities(); + public void afterPropertiesSet() throws SQLException { + ResultSet rs = jdbcTemplate.getDataSource() + .getConnection() + .getMetaData() + .getTables(null, "PUBLIC", null, new String[]{"TABLE"}); - tableNames = entities.stream() - .filter(this::isEntity) - .map(this::convertTableNameFromCamelCaseToSnakeCase) - .toList(); - } - - private boolean isEntity(final EntityType entityType) { - return entityType.getJavaType() - .getAnnotation(Entity.class) != null; - } + while (rs.next()) { + String tableName = rs.getString("TABLE_NAME"); - private String convertTableNameFromCamelCaseToSnakeCase(EntityType entityType) { - StringBuilder tableNameSnake = new StringBuilder(); - String classNameOfEntity = entityType.getName(); - - for (char letter : classNameOfEntity.toCharArray()) { - addUnderScoreForCapitalLetter(tableNameSnake, letter); - tableNameSnake.append(letter); - } - - return tableNameSnake.substring(1).toLowerCase(); - } - - private void addUnderScoreForCapitalLetter(StringBuilder tableNameSnake, char letter) { - if (Character.isUpperCase(letter)) { - tableNameSnake.append(UNDERSCORE); + this.tableNames.add(tableName); } } @Transactional public void execute() { - executeSqlWithReferentialIntegrityDisabled(this::executeTruncate); - } - - private void executeSqlWithReferentialIntegrityDisabled(Runnable sqlExecutor) { disableReferentialIntegrity(); - sqlExecutor.run(); + executeTruncate(); enableReferentialIntegrity(); } private void disableReferentialIntegrity() { - entityManager.flush(); - - entityManager.createNativeQuery(DISABLE_REFERENTIAL_QUERY) - .executeUpdate(); - } - - private void enableReferentialIntegrity() { - - entityManager.createNativeQuery(ENABLE_REFERENTIAL_QUERY) - .executeUpdate(); + jdbcTemplate.execute(DISABLE_REFERENTIAL_QUERY); } private void executeTruncate() { for (String tableName : tableNames) { String TRUNCATE_QUERY = String.format(TRUNCATE_SQL_MESSAGE, tableName); - String ID_RESET_QUERY = String.format(ID_RESET_SQL_MESSAGE, tableName); - entityManager.createNativeQuery(TRUNCATE_QUERY) - .executeUpdate(); - entityManager.createNativeQuery(ID_RESET_QUERY) - .executeUpdate(); + jdbcTemplate.execute(TRUNCATE_QUERY); } } + private void enableReferentialIntegrity() { + jdbcTemplate.execute(ENABLE_REFERENTIAL_QUERY); + } + } From 80873a65a237c4e3ac306b9d7c85a081a06c7f31 Mon Sep 17 00:00:00 2001 From: Doy Date: Tue, 5 Sep 2023 17:54:40 +0900 Subject: [PATCH 03/34] =?UTF-8?q?[BE]=20Refactor/#376=20=EB=A1=9C=EA=B9=85?= =?UTF-8?q?=20=ED=99=98=EA=B2=BD=20=EA=B0=9C=EC=84=A0=EC=9D=84=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20=EC=84=A4=EC=A0=95=20=ED=8C=8C=EC=9D=BC=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=84=B0=EB=A7=81=20(#377)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 로깅 전략 보완에 따른 설정 파일 수정 - 운영 환경 별 로그 종류(콘솔, 파일), 레벨 변경 (PR 첨부 설명 참조) - logback-spring.xml 에서 Appender 분리 - application-*.xml 에서 로그 패턴 값 삭제 * chore: 에러 로그 슬랙 알림을 위한 의존성 추가, 관련 주석 작성 * remove: 테스트 설정 파일에 불필요한 로그 설정 삭제 * style: 불필요한 빈 줄 삭제 * chore: 콘솔 파일 로그 설정 삭제로 인한 불필요한 설정 삭제 * chore: 로그 파일 롤링 용량 설정 모든 레벨 통일 --- backend/.gitignore | 3 - backend/build.gradle | 1 + .../src/main/resources/application-dev.yml | 4 - .../src/main/resources/application-prod.yml | 3 - backend/src/main/resources/application.yml | 5 - .../src/main/resources/console-appender.xml | 7 ++ .../main/resources/file-debug-appender.xml | 19 ++++ .../main/resources/file-error-appender.xml | 19 ++++ .../resources/file-hibernate-appender.xml | 14 +++ .../src/main/resources/file-info-appender.xml | 19 ++++ .../src/main/resources/file-warn-appender.xml | 19 ++++ backend/src/main/resources/logback-spring.xml | 95 ++++++++++--------- backend/src/test/resources/application.yml | 2 - backend/src/test/resources/logback-spring.xml | 19 ---- 14 files changed, 146 insertions(+), 83 deletions(-) create mode 100644 backend/src/main/resources/console-appender.xml create mode 100644 backend/src/main/resources/file-debug-appender.xml create mode 100644 backend/src/main/resources/file-error-appender.xml create mode 100644 backend/src/main/resources/file-hibernate-appender.xml create mode 100644 backend/src/main/resources/file-info-appender.xml create mode 100644 backend/src/main/resources/file-warn-appender.xml delete mode 100644 backend/src/test/resources/logback-spring.xml diff --git a/backend/.gitignore b/backend/.gitignore index ad0ff1ee..c2065bc2 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -35,6 +35,3 @@ out/ ### VS Code ### .vscode/ - -### Logging ### -.log diff --git a/backend/build.gradle b/backend/build.gradle index be07241a..ceb7e1b5 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -28,6 +28,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-webflux' + implementation group: 'com.github.maricn', name: 'logback-slack-appender', version: '1.6.1' implementation 'mysql:mysql-connector-java:8.0.32' diff --git a/backend/src/main/resources/application-dev.yml b/backend/src/main/resources/application-dev.yml index 122d5ea2..d3fe125a 100644 --- a/backend/src/main/resources/application-dev.yml +++ b/backend/src/main/resources/application-dev.yml @@ -20,11 +20,7 @@ logging: sql: BasicBinder: TRACE file: - # jar 파일 실행 시, jar 파일과 동일한 디렉토리 내 log 디렉토리에 생성됨 path: /home/ubuntu/backend/build/log - pattern: - console: "%green(%d{yyyy-MM-dd HH:mm:ss.SSS}) [%thread] ${PID} %highlight(%5level) %cyan(%logger) - %msg%n" - file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] ${PID} %5level %logger - %msg%n" oauth: kakao: diff --git a/backend/src/main/resources/application-prod.yml b/backend/src/main/resources/application-prod.yml index 59dc54ba..913b933c 100644 --- a/backend/src/main/resources/application-prod.yml +++ b/backend/src/main/resources/application-prod.yml @@ -11,9 +11,6 @@ spring: logging: file: path: /home/ubuntu/backend/build/log - pattern: - console: "%green(%d{yyyy-MM-dd HH:mm:ss.SSS}) [%thread] ${PID} %highlight(%5level) %cyan(%logger) - %msg%n" - file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] ${PID} %5level %logger - %msg%n" oauth: kakao: diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 4d2cf048..3a75b6dd 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -21,11 +21,6 @@ logging: descriptor: sql: BasicBinder: TRACE - file: - path: ./log - pattern: - console: "%green(%d{yyyy-MM-dd HH:mm:ss.SSS}) [%thread] ${PID} %highlight(%5level) %cyan(%logger) - %msg%n" - file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] ${PID} %5level %logger - %msg%n" oauth: kakao: diff --git a/backend/src/main/resources/console-appender.xml b/backend/src/main/resources/console-appender.xml new file mode 100644 index 00000000..ab72b3b0 --- /dev/null +++ b/backend/src/main/resources/console-appender.xml @@ -0,0 +1,7 @@ + + + + ${LOG_PATTERN_COLORED} + + + diff --git a/backend/src/main/resources/file-debug-appender.xml b/backend/src/main/resources/file-debug-appender.xml new file mode 100644 index 00000000..925177fa --- /dev/null +++ b/backend/src/main/resources/file-debug-appender.xml @@ -0,0 +1,19 @@ + + + ${LOG_PATH}/debug/debug.log + + DEBUG + ACCEPT + DENY + + + ${LOG_PATTERN_DEFAULT} + + + ${LOG_PATH}/debug/debug-%d{yyyy-MM-dd}.%i.log + 100MB + 7 + 1GB + + + diff --git a/backend/src/main/resources/file-error-appender.xml b/backend/src/main/resources/file-error-appender.xml new file mode 100644 index 00000000..7820c08e --- /dev/null +++ b/backend/src/main/resources/file-error-appender.xml @@ -0,0 +1,19 @@ + + + ${LOG_PATH}/error/error.log + + ERROR + ACCEPT + DENY + + + ${LOG_PATTERN_DEFAULT} + + + ${LOG_PATH}/error/error-%d{yyyy-MM-dd}.%i.log + 100MB + 30 + 3GB + + + diff --git a/backend/src/main/resources/file-hibernate-appender.xml b/backend/src/main/resources/file-hibernate-appender.xml new file mode 100644 index 00000000..cf66c306 --- /dev/null +++ b/backend/src/main/resources/file-hibernate-appender.xml @@ -0,0 +1,14 @@ + + + ${LOG_PATH}/hibernate/hibernate.log + + ${LOG_PATTERN_DEFAULT} + + + ${LOG_PATH}/hibernate/hibernate-%d{yyyy-MM-dd}.%i.log + 100MB + 7 + 1GB + + + diff --git a/backend/src/main/resources/file-info-appender.xml b/backend/src/main/resources/file-info-appender.xml new file mode 100644 index 00000000..8fc1cdf7 --- /dev/null +++ b/backend/src/main/resources/file-info-appender.xml @@ -0,0 +1,19 @@ + + + ${LOG_PATH}/info/info.log + + INFO + ACCEPT + DENY + + + ${LOG_PATTERN_DEFAULT} + + + ${LOG_PATH}/info/info-%d{yyyy-MM-dd}.%i.log + 100MB + 7 + 1GB + + + diff --git a/backend/src/main/resources/file-warn-appender.xml b/backend/src/main/resources/file-warn-appender.xml new file mode 100644 index 00000000..24ffa0f3 --- /dev/null +++ b/backend/src/main/resources/file-warn-appender.xml @@ -0,0 +1,19 @@ + + + ${LOG_PATH}/warn/warn.log + + WARN + ACCEPT + DENY + + + ${LOG_PATTERN_DEFAULT} + + + ${LOG_PATH}/warn/warn-%d{yyyy-MM-dd}.%i.log + 100MB + 30 + 3GB + + + diff --git a/backend/src/main/resources/logback-spring.xml b/backend/src/main/resources/logback-spring.xml index 6ca7c8d1..3a6ab859 100644 --- a/backend/src/main/resources/logback-spring.xml +++ b/backend/src/main/resources/logback-spring.xml @@ -1,50 +1,51 @@ + + + - - - - - - - - - - ${CONSOLE_LOG_PATTERN} - - - - - - - - WARN - ACCEPT - DENY - - ${LOG_PATH}/warn.log - - ${FILE_LOG_PATTERN} - - - - - - - ERROR - ACCEPT - DENY - - - ${LOG_PATH}/error.log - - ${FILE_LOG_PATTERN} - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/src/test/resources/application.yml b/backend/src/test/resources/application.yml index 65e0aadb..b0c95fea 100644 --- a/backend/src/test/resources/application.yml +++ b/backend/src/test/resources/application.yml @@ -21,8 +21,6 @@ spring: logging: level: org.hibernate.orm.jdbc.bind: trace - pattern: - console: "%green(%d{yyyy-MM-dd HH:mm:ss.SSS}) [%thread] ${PID} %highlight(%5level) %cyan(%logger) - %msg%n" oauth: kakao: diff --git a/backend/src/test/resources/logback-spring.xml b/backend/src/test/resources/logback-spring.xml deleted file mode 100644 index 0bf60ac9..00000000 --- a/backend/src/test/resources/logback-spring.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - ${LOG_PATTERN} - - - - - - - - - - - From e61a66c9dbb2fd0164ae5d9f48d8abf2954215fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A4=80=ED=8C=8D=28junpak=29?= <112045553+junpakPark@users.noreply.github.com> Date: Tue, 5 Sep 2023 18:12:11 +0900 Subject: [PATCH 04/34] =?UTF-8?q?[BE]=20Chore/#372=20submodule=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=20(#375)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: submodule 적용 Co-authored-by: jaeyeon kim * chore: workflow access token 적용 Co-authored-by: jaeyeon kim --------- Co-authored-by: jaeyeon kim Co-authored-by: yoondgu --- .github/workflows/be-merge-dev.yml | 4 ++- .github/workflows/be-merge-prod.yml | 2 ++ .github/workflows/be-pull-request.yml | 2 ++ .gitmodules | 3 ++ .../src/main/resources/application-dev.yml | 36 ------------------- .../src/main/resources/application-prod.yml | 26 -------------- backend/src/main/resources/config | 1 + 7 files changed, 11 insertions(+), 63 deletions(-) create mode 100644 .gitmodules delete mode 100644 backend/src/main/resources/application-dev.yml delete mode 100644 backend/src/main/resources/application-prod.yml create mode 160000 backend/src/main/resources/config diff --git a/.github/workflows/be-merge-dev.yml b/.github/workflows/be-merge-dev.yml index 66a000a8..adfdf87c 100644 --- a/.github/workflows/be-merge-dev.yml +++ b/.github/workflows/be-merge-dev.yml @@ -24,7 +24,9 @@ jobs: with: java-version: '17' distribution: 'temurin' - + submodules: recursive + token: ${{ secrets.SUBMODULE_ACCESS_TOKEN }} + - name: 테스트 환경변수 설정 run: | echo "TEST_JWT_SECRET_KEY=${{ secrets.TEST_JWT_SECRET_KEY }}" >> $GITHUB_ENV diff --git a/.github/workflows/be-merge-prod.yml b/.github/workflows/be-merge-prod.yml index cbbdddd3..7a96bed6 100644 --- a/.github/workflows/be-merge-prod.yml +++ b/.github/workflows/be-merge-prod.yml @@ -24,6 +24,8 @@ jobs: with: java-version: '17' distribution: 'temurin' + submodules: recursive + token: ${{ secrets.SUBMODULE_ACCESS_TOKEN }} - name: 테스트 환경변수 설정 run: | diff --git a/.github/workflows/be-pull-request.yml b/.github/workflows/be-pull-request.yml index 0f5de95b..bdcb594e 100644 --- a/.github/workflows/be-pull-request.yml +++ b/.github/workflows/be-pull-request.yml @@ -24,6 +24,8 @@ jobs: with: java-version: '17' distribution: 'temurin' + submodules: recursive + token: ${{ secrets.SUBMODULE_ACCESS_TOKEN }} - name: 테스트 환경변수 설정 run: | diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..caf7b497 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "backend/src/main/resources/config"] + path = backend/src/main/resources/config + url = https://github.com/map-befine-official/map-befine-config.git diff --git a/backend/src/main/resources/application-dev.yml b/backend/src/main/resources/application-dev.yml deleted file mode 100644 index d3fe125a..00000000 --- a/backend/src/main/resources/application-dev.yml +++ /dev/null @@ -1,36 +0,0 @@ -spring: - datasource: - driver-class-name: com.mysql.cj.jdbc.Driver - url: ${MYSQL_DATABASE_URL} - username: ${MYSQL_DATABASE_USER_NAME} - password: ${MYSQL_DATABASE_PASSWORD} - jpa: - properties: - hibernate: - format_sql: true - show-sql: true - hibernate: - ddl-auto: update -logging: - level: - org: - hibernate: - type: - descriptor: - sql: - BasicBinder: TRACE - file: - path: /home/ubuntu/backend/build/log - -oauth: - kakao: - client_id: ${OAUTH_KAKAO_CLIENT_ID} - redirect_uri: ${OAUTH_KAKAO_REDIRECT_URI} - client_secret: ${OAUTH_KAKAO_CLIENT_SECRET} - scope: ${OAUTH_KAKAO_SCOPE} - -security: - jwt: - token: - secret-key: ${JWT_TOKEN_SECRET_KEY} - expire-length: ${JWT_TOKEN_EXPIRE_LENGTH} diff --git a/backend/src/main/resources/application-prod.yml b/backend/src/main/resources/application-prod.yml deleted file mode 100644 index 913b933c..00000000 --- a/backend/src/main/resources/application-prod.yml +++ /dev/null @@ -1,26 +0,0 @@ -spring: - datasource: - driver-class-name: com.mysql.cj.jdbc.Driver - url: ${MYSQL_DATABASE_URL} - username: ${MYSQL_DATABASE_USER_NAME} - password: ${MYSQL_DATABASE_PASSWORD} - jpa: - hibernate: - ddl-auto: none - -logging: - file: - path: /home/ubuntu/backend/build/log - -oauth: - kakao: - client_id: ${OAUTH_KAKAO_CLIENT_ID} - redirect_uri: ${OAUTH_KAKAO_REDIRECT_URI} - client_secret: ${OAUTH_KAKAO_CLIENT_SECRET} - scope: ${OAUTH_KAKAO_SCOPE} -security: - jwt: - token: - secret-key: ${JWT_TOKEN_SECRET_KEY} - expire-length: ${JWT_TOKEN_EXPIRE_LENGTH} - diff --git a/backend/src/main/resources/config b/backend/src/main/resources/config new file mode 160000 index 00000000..304eb085 --- /dev/null +++ b/backend/src/main/resources/config @@ -0,0 +1 @@ +Subproject commit 304eb085dbabd64f669cc49bfd75e464d0488753 From b88267b73616dfda82b25ca032927cf09fb6b790 Mon Sep 17 00:00:00 2001 From: Doy Date: Wed, 6 Sep 2023 13:19:18 +0900 Subject: [PATCH 05/34] =?UTF-8?q?feat:=20=EC=9A=B4=EC=98=81=20=EC=84=9C?= =?UTF-8?q?=EB=B2=84=20500=20=EC=97=90=EB=9F=AC=20=EC=8A=AC=EB=9E=99=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EC=A0=81=EC=9A=A9,=20=EB=A1=9C=EC=BB=AC?= =?UTF-8?q?=20=ED=99=98=EA=B2=BD=EB=B3=80=EC=88=98=20=EC=84=9C=EB=B8=8C?= =?UTF-8?q?=EB=AA=A8=EB=93=88=EC=97=90=20=EC=A0=80=EC=9E=A5=20(#379)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/main/resources/application.yml | 36 ------------------- backend/src/main/resources/config | 2 +- backend/src/main/resources/logback-spring.xml | 5 ++- .../main/resources/slack-error-appender.xml | 17 +++++++++ 4 files changed, 20 insertions(+), 40 deletions(-) delete mode 100644 backend/src/main/resources/application.yml create mode 100644 backend/src/main/resources/slack-error-appender.xml diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml deleted file mode 100644 index 3a75b6dd..00000000 --- a/backend/src/main/resources/application.yml +++ /dev/null @@ -1,36 +0,0 @@ -spring: - datasource: - username: sa - url: jdbc:h2:mem:mapbefine;MODE=MySQL - h2: - console: - enabled: true - path: /h2-console - jpa: - properties: - hibernate: - format_sql: true - show-sql: true - hibernate: - ddl-auto: create -logging: - level: - org: - hibernate: - type: - descriptor: - sql: - BasicBinder: TRACE - -oauth: - kakao: - client_id: ${OAUTH_KAKAO_CLIENT_ID} - redirect_uri: ${OAUTH_KAKAO_REDIRECT_URI} - client_secret: ${OAUTH_KAKAO_CLIENT_SECRET} - scope: ${OAUTH_KAKAO_SCOPE} - -security: - jwt: - token: - secret-key: ${JWT_TOKEN_SECRET_KEY} - expire-length: ${JWT_TOKEN_EXPIRE_LENGTH} diff --git a/backend/src/main/resources/config b/backend/src/main/resources/config index 304eb085..6bcc4157 160000 --- a/backend/src/main/resources/config +++ b/backend/src/main/resources/config @@ -1 +1 @@ -Subproject commit 304eb085dbabd64f669cc49bfd75e464d0488753 +Subproject commit 6bcc4157c1375c7b3688d1cfd77074d6b0ada624 diff --git a/backend/src/main/resources/logback-spring.xml b/backend/src/main/resources/logback-spring.xml index 3a6ab859..1bfbf177 100644 --- a/backend/src/main/resources/logback-spring.xml +++ b/backend/src/main/resources/logback-spring.xml @@ -34,18 +34,17 @@ - - + - + diff --git a/backend/src/main/resources/slack-error-appender.xml b/backend/src/main/resources/slack-error-appender.xml new file mode 100644 index 00000000..a16bd597 --- /dev/null +++ b/backend/src/main/resources/slack-error-appender.xml @@ -0,0 +1,17 @@ + + + + + ERROR + ACCEPT + DENY + + ${SLACK_INCOMING_WEBHOOK_URI} + ERROR-ALARM + :rotating_light: + true + + ${LOG_PATTERN} + + + From 877dd06c28cd89f5fb095eaea70d8f8d2cd94e3e Mon Sep 17 00:00:00 2001 From: kpeel5839 <89840550+kpeel5839@users.noreply.github.com> Date: Wed, 6 Sep 2023 13:46:43 +0900 Subject: [PATCH 06/34] =?UTF-8?q?hotfix:=20yml=20=EB=AC=B8=EB=B2=95?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#381)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/main/resources/config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/resources/config b/backend/src/main/resources/config index 6bcc4157..ebb17a89 160000 --- a/backend/src/main/resources/config +++ b/backend/src/main/resources/config @@ -1 +1 @@ -Subproject commit 6bcc4157c1375c7b3688d1cfd77074d6b0ada624 +Subproject commit ebb17a895391a6cec3a893d48f632c72b0234326 From 339b4ad12f954fae17aafc104b6c7d15b4d3efbf Mon Sep 17 00:00:00 2001 From: Doy Date: Wed, 6 Sep 2023 14:27:13 +0900 Subject: [PATCH 07/34] =?UTF-8?q?fix:=20=EC=9B=8C=ED=81=AC=ED=94=8C?= =?UTF-8?q?=EB=A1=9C=EC=9A=B0=20=EC=84=9C=EB=B8=8C=EB=AA=A8=EB=93=88=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0=20(#384)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/be-merge-dev.yml | 5 +- .github/workflows/be-merge-prod.yml | 49 +++++++++---------- .github/workflows/be-pull-request.yml | 69 ++++++++++++++------------- 3 files changed, 63 insertions(+), 60 deletions(-) diff --git a/.github/workflows/be-merge-dev.yml b/.github/workflows/be-merge-dev.yml index adfdf87c..33b2781d 100644 --- a/.github/workflows/be-merge-dev.yml +++ b/.github/workflows/be-merge-dev.yml @@ -19,13 +19,14 @@ jobs: steps: - uses: actions/checkout@v3 + with: + submodules: recursive + token: ${{ secrets.SUBMODULE_ACCESS_TOKEN }} - name: Set up JDK 17 uses: actions/setup-java@v3 with: java-version: '17' distribution: 'temurin' - submodules: recursive - token: ${{ secrets.SUBMODULE_ACCESS_TOKEN }} - name: 테스트 환경변수 설정 run: | diff --git a/.github/workflows/be-merge-prod.yml b/.github/workflows/be-merge-prod.yml index 7a96bed6..3ba5433e 100644 --- a/.github/workflows/be-merge-prod.yml +++ b/.github/workflows/be-merge-prod.yml @@ -18,33 +18,34 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 - - name: Set up JDK 17 - uses: actions/setup-java@v3 - with: - java-version: '17' - distribution: 'temurin' - submodules: recursive - token: ${{ secrets.SUBMODULE_ACCESS_TOKEN }} + - uses: actions/checkout@v3 + with: + submodules: recursive + token: ${{ secrets.SUBMODULE_ACCESS_TOKEN }} + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + + - name: 테스트 환경변수 설정 + run: | + echo "TEST_JWT_SECRET_KEY=${{ secrets.TEST_JWT_SECRET_KEY }}" >> $GITHUB_ENV + echo "TEST_JWT_EXPIRE_LENGTH=${{ secrets.TEST_JWT_EXPIRE_LENGTH }}" >> $GITHUB_ENV - - name: 테스트 환경변수 설정 - run: | - echo "TEST_JWT_SECRET_KEY=${{ secrets.TEST_JWT_SECRET_KEY }}" >> $GITHUB_ENV - echo "TEST_JWT_EXPIRE_LENGTH=${{ secrets.TEST_JWT_EXPIRE_LENGTH }}" >> $GITHUB_ENV + - name: gradlew 실행 권한 부여 + run: chmod +x gradlew + working-directory: backend - - name: gradlew 실행 권한 부여 - run: chmod +x gradlew - working-directory: backend + - name: Gradle build 시작 + run: ./gradlew clean build + working-directory: backend - - name: Gradle build 시작 - run: ./gradlew clean build - working-directory: backend - - - name: jar 파일 artifact에 업로드 - uses: actions/upload-artifact@v3 - with: - name: BackendApplication - path: backend/build/libs/mapbefine.jar + - name: jar 파일 artifact에 업로드 + uses: actions/upload-artifact@v3 + with: + name: BackendApplication + path: backend/build/libs/mapbefine.jar deploy: runs-on: [ self-hosted, prod ] diff --git a/.github/workflows/be-pull-request.yml b/.github/workflows/be-pull-request.yml index bdcb594e..cea976b9 100644 --- a/.github/workflows/be-pull-request.yml +++ b/.github/workflows/be-pull-request.yml @@ -18,37 +18,38 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 - - name: Set up JDK 17 - uses: actions/setup-java@v3 - with: - java-version: '17' - distribution: 'temurin' - submodules: recursive - token: ${{ secrets.SUBMODULE_ACCESS_TOKEN }} - - - name: 테스트 환경변수 설정 - run: | - echo "TEST_JWT_SECRET_KEY=${{ secrets.TEST_JWT_SECRET_KEY }}" >> $GITHUB_ENV - echo "TEST_JWT_EXPIRE_LENGTH=${{ secrets.TEST_JWT_EXPIRE_LENGTH }}" >> $GITHUB_ENV - - - name: gradlew 실행 권한 부여 - run: chmod +x gradlew - working-directory: backend - - - name: Gradle build 시작 - run: ./gradlew clean build - working-directory: backend - - - name: 테스트 결과를 PR에 코멘트로 등록합니다 - uses: EnricoMi/publish-unit-test-result-action@v1 - if: always() - with: - files: 'backend/build/test-results/test/TEST-*.xml' - - - name: 테스트 실패 시, 실패한 코드 라인에 Check 코멘트를 등록합니다 - uses: mikepenz/action-junit-report@v3 - if: always() - with: - report_paths: '**/build/test-results/test/TEST-*.xml' - token: ${{ github.token }} + - uses: actions/checkout@v3 + with: + submodules: recursive + token: ${{ secrets.SUBMODULE_ACCESS_TOKEN }} + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + + - name: 테스트 환경변수 설정 + run: | + echo "TEST_JWT_SECRET_KEY=${{ secrets.TEST_JWT_SECRET_KEY }}" >> $GITHUB_ENV + echo "TEST_JWT_EXPIRE_LENGTH=${{ secrets.TEST_JWT_EXPIRE_LENGTH }}" >> $GITHUB_ENV + + - name: gradlew 실행 권한 부여 + run: chmod +x gradlew + working-directory: backend + + - name: Gradle build 시작 + run: ./gradlew clean build + working-directory: backend + + - name: 테스트 결과를 PR에 코멘트로 등록합니다 + uses: EnricoMi/publish-unit-test-result-action@v1 + if: always() + with: + files: 'backend/build/test-results/test/TEST-*.xml' + + - name: 테스트 실패 시, 실패한 코드 라인에 Check 코멘트를 등록합니다 + uses: mikepenz/action-junit-report@v3 + if: always() + with: + report_paths: '**/build/test-results/test/TEST-*.xml' + token: ${{ github.token }} From 34f646ed797e12bfea87b06f7d04cb4f2fd69dad Mon Sep 17 00:00:00 2001 From: Doy Date: Wed, 6 Sep 2023 14:35:40 +0900 Subject: [PATCH 08/34] =?UTF-8?q?chore:=20=EC=9B=8C=ED=81=AC=ED=94=8C?= =?UTF-8?q?=EB=A1=9C=EC=9A=B0=20=EC=84=9C=EB=B8=8C=EB=AA=A8=EB=93=88=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0=20=ED=99=95=EC=9D=B8?= =?UTF-8?q?=EC=9D=84=20=EC=9C=84=ED=95=9C=20push=20(#385)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/main/resources/logback-spring.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/main/resources/logback-spring.xml b/backend/src/main/resources/logback-spring.xml index 1bfbf177..848d8f9d 100644 --- a/backend/src/main/resources/logback-spring.xml +++ b/backend/src/main/resources/logback-spring.xml @@ -1,4 +1,5 @@ + From cc91595a3d3760df1b896106ec98de4fe1890a39 Mon Sep 17 00:00:00 2001 From: kpeel5839 <89840550+kpeel5839@users.noreply.github.com> Date: Mon, 11 Sep 2023 13:23:11 +0900 Subject: [PATCH 09/34] Feat/#386 image (#391) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore : S3 의존성 추가 * feat : Amazon S3 Component 추가 * feat : S3 에 업로드 될 Image 의 이름을 설정해주는 ImageName 과 UploadFile 추가 * feat : S3 에 업로드 될 Image 의 이름을 설정해주는 ImageNae 과 UploadFile 추가 * feat : 파일을 업로드하고 해당하는 URL 을 반환하는 Service 구현 * chore : s3 환경 설정 추가 * feat : String image -> MultipartFile image 로 변경 * feat : @RequestPart 적용 및 S3 upload 로직 추가 * chore : Test 시 Profile 설정 * test : 테스트 시 S3 에 접근하지 않도록 하기 위해 Profile 별로 S3Service Bean 구분 * test : 추가된 Image 저장 기능에 맞춰 일부 Test 수정 * chore : S3, CloudFront 환경설정 적용 * test : RestDocs 수정중 --- backend/build.gradle | 4 + .../mapbefine/common/config/S3Config.java | 19 +++++ .../pin/application/PinCommandService.java | 10 ++- .../dto/request/PinImageCreateRequest.java | 4 +- .../pin/presentation/PinController.java | 16 +++- .../mapbefine/s3/application/S3Service.java | 11 +++ .../s3/application/S3ServiceImpl.java | 43 ++++++++++ .../mapbefine/s3/domain/ImageName.java | 34 ++++++++ .../mapbefine/s3/domain/S3Client.java | 47 +++++++++++ .../mapbefine/s3/domain/UploadFile.java | 81 +++++++++++++++++++ .../application/TopicCommandService.java | 9 ++- .../topic/dto/request/TopicCreateRequest.java | 17 +++- .../TopicCreateRequestWithOutImage.java | 14 ++++ .../topic/presentation/TopicController.java | 14 +++- backend/src/main/resources/config | 2 +- .../com/mapbefine/mapbefine/FileFixture.java | 11 +++ .../com/mapbefine/mapbefine/StubFile.java | 63 +++++++++++++++ .../mapbefine/TestS3ServiceImpl.java | 18 +++++ .../oauth/application/OauthServiceTest.java | 1 - .../oauth/domain/OauthMemberTest.java | 1 - .../mapbefine/pin/PinIntegrationTest.java | 15 ++-- .../application/PinCommandServiceTest.java | 12 ++- .../pin/presentation/PinControllerTest.java | 19 +++-- .../mapbefine/topic/TopicFixture.java | 4 +- .../mapbefine/topic/TopicIntegrationTest.java | 46 ++++++----- .../presentation/TopicControllerTest.java | 25 +++++- backend/src/test/resources/application.yml | 5 ++ backend/src/test/resources/test.png | 0 28 files changed, 492 insertions(+), 53 deletions(-) create mode 100644 backend/src/main/java/com/mapbefine/mapbefine/common/config/S3Config.java create mode 100644 backend/src/main/java/com/mapbefine/mapbefine/s3/application/S3Service.java create mode 100644 backend/src/main/java/com/mapbefine/mapbefine/s3/application/S3ServiceImpl.java create mode 100644 backend/src/main/java/com/mapbefine/mapbefine/s3/domain/ImageName.java create mode 100644 backend/src/main/java/com/mapbefine/mapbefine/s3/domain/S3Client.java create mode 100644 backend/src/main/java/com/mapbefine/mapbefine/s3/domain/UploadFile.java create mode 100644 backend/src/main/java/com/mapbefine/mapbefine/topic/dto/request/TopicCreateRequestWithOutImage.java create mode 100644 backend/src/test/java/com/mapbefine/mapbefine/FileFixture.java create mode 100644 backend/src/test/java/com/mapbefine/mapbefine/StubFile.java create mode 100644 backend/src/test/java/com/mapbefine/mapbefine/TestS3ServiceImpl.java create mode 100644 backend/src/test/resources/test.png diff --git a/backend/build.gradle b/backend/build.gradle index ceb7e1b5..c4192d94 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -45,6 +45,10 @@ dependencies { testImplementation 'io.rest-assured:spring-mock-mvc' testImplementation 'org.assertj:assertj-core:3.19.0' + // S3 + implementation platform('com.amazonaws:aws-java-sdk-bom:1.11.1000') + implementation 'com.amazonaws:aws-java-sdk-s3' + compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' diff --git a/backend/src/main/java/com/mapbefine/mapbefine/common/config/S3Config.java b/backend/src/main/java/com/mapbefine/mapbefine/common/config/S3Config.java new file mode 100644 index 00000000..204a09bc --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/common/config/S3Config.java @@ -0,0 +1,19 @@ +package com.mapbefine.mapbefine.common.config; + +import com.amazonaws.regions.Regions; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class S3Config { + + @Bean + public AmazonS3 amazonS3() { + return AmazonS3ClientBuilder.standard() + .withRegion(Regions.AP_NORTHEAST_2) + .build(); + } + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/pin/application/PinCommandService.java b/backend/src/main/java/com/mapbefine/mapbefine/pin/application/PinCommandService.java index 80f0cd35..2d563e4f 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/pin/application/PinCommandService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/pin/application/PinCommandService.java @@ -21,6 +21,7 @@ import com.mapbefine.mapbefine.pin.dto.request.PinUpdateRequest; import com.mapbefine.mapbefine.pin.exception.PinException.PinBadRequestException; import com.mapbefine.mapbefine.pin.exception.PinException.PinForbiddenException; +import com.mapbefine.mapbefine.s3.application.S3Service; import com.mapbefine.mapbefine.topic.domain.Topic; import com.mapbefine.mapbefine.topic.domain.TopicRepository; import com.mapbefine.mapbefine.topic.exception.TopicException.TopicBadRequestException; @@ -40,22 +41,24 @@ public class PinCommandService { private final TopicRepository topicRepository; private final MemberRepository memberRepository; private final PinImageRepository pinImageRepository; + private final S3Service s3Service; public PinCommandService( PinRepository pinRepository, LocationRepository locationRepository, TopicRepository topicRepository, MemberRepository memberRepository, - PinImageRepository pinImageRepository + PinImageRepository pinImageRepository, + S3Service s3Service ) { this.pinRepository = pinRepository; this.locationRepository = locationRepository; this.topicRepository = topicRepository; this.memberRepository = memberRepository; this.pinImageRepository = pinImageRepository; + this.s3Service = s3Service; } - public long save(AuthMember authMember, PinCreateRequest request) { Topic topic = findTopic(request.topicId()); validatePinCreateOrUpdate(authMember, topic); @@ -138,8 +141,9 @@ public void removeById(AuthMember authMember, Long pinId) { public void addImage(AuthMember authMember, PinImageCreateRequest request) { Pin pin = findPin(request.pinId()); validatePinCreateOrUpdate(authMember, pin.getTopic()); + String image = s3Service.upload(request.image()); - PinImage pinImage = PinImage.createPinImageAssociatedWithPin(request.imageUrl(), pin); + PinImage pinImage = PinImage.createPinImageAssociatedWithPin(image, pin); pinImageRepository.save(pinImage); } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/pin/dto/request/PinImageCreateRequest.java b/backend/src/main/java/com/mapbefine/mapbefine/pin/dto/request/PinImageCreateRequest.java index d43e74ee..4ad5ef73 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/pin/dto/request/PinImageCreateRequest.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/pin/dto/request/PinImageCreateRequest.java @@ -1,7 +1,9 @@ package com.mapbefine.mapbefine.pin.dto.request; +import org.springframework.web.multipart.MultipartFile; + public record PinImageCreateRequest( Long pinId, - String imageUrl + MultipartFile image ) { } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/pin/presentation/PinController.java b/backend/src/main/java/com/mapbefine/mapbefine/pin/presentation/PinController.java index b85d6cb9..2334d1c9 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/pin/presentation/PinController.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/pin/presentation/PinController.java @@ -12,6 +12,7 @@ import java.net.URI; import java.util.List; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -21,7 +22,9 @@ import org.springframework.web.bind.annotation.RequestBody; 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; @RestController @RequestMapping("/pins") @@ -92,9 +95,16 @@ public ResponseEntity> findAllPinsByMemberId( } @LoginRequired - @PostMapping("/images") - public ResponseEntity addImage(AuthMember member, @RequestBody PinImageCreateRequest request) { - pinCommandService.addImage(member, request); + @PostMapping( + value = "/images", + consumes = {MediaType.MULTIPART_FORM_DATA_VALUE, MediaType.APPLICATION_JSON_VALUE} + ) + public ResponseEntity addImage( + AuthMember member, + @RequestPart Long pinId, + @RequestPart MultipartFile image + ) { + pinCommandService.addImage(member, new PinImageCreateRequest(pinId, image)); return ResponseEntity.status(HttpStatus.CREATED).build(); } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/s3/application/S3Service.java b/backend/src/main/java/com/mapbefine/mapbefine/s3/application/S3Service.java new file mode 100644 index 00000000..a451f3b4 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/s3/application/S3Service.java @@ -0,0 +1,11 @@ +package com.mapbefine.mapbefine.s3.application; + +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +@Service +public interface S3Service { + + String upload(MultipartFile multipartFile); + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/s3/application/S3ServiceImpl.java b/backend/src/main/java/com/mapbefine/mapbefine/s3/application/S3ServiceImpl.java new file mode 100644 index 00000000..843dc18c --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/s3/application/S3ServiceImpl.java @@ -0,0 +1,43 @@ +package com.mapbefine.mapbefine.s3.application; + +import com.mapbefine.mapbefine.s3.domain.S3Client; +import com.mapbefine.mapbefine.s3.domain.UploadFile; +import java.io.IOException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +@Service +@Profile("!test") +public class S3ServiceImpl implements S3Service { + + @Value("${prefix.upload.path}") + private String prefixUploadPath; + private final S3Client s3Client; + + public S3ServiceImpl(S3Client s3Client) { + this.s3Client = s3Client; + } + + @Override + public String upload(MultipartFile multipartFile) { + try { + UploadFile uploadFile = UploadFile.of(multipartFile); + s3Client.upload(uploadFile); + return getUploadPath(uploadFile); + } catch (IOException exception) { + throw new RuntimeException(exception); + } + } + + private String getUploadPath(final UploadFile uploadFile) { + return String.join( + "/", + prefixUploadPath, + uploadFile.getOriginalFilename() + ); + } + +} + diff --git a/backend/src/main/java/com/mapbefine/mapbefine/s3/domain/ImageName.java b/backend/src/main/java/com/mapbefine/mapbefine/s3/domain/ImageName.java new file mode 100644 index 00000000..c53553f6 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/s3/domain/ImageName.java @@ -0,0 +1,34 @@ +package com.mapbefine.mapbefine.s3.domain; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +public class ImageName { + + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSSSSS"); + private static final String EXTENSION_DELIMITER = "."; + + private final String fileName; + + private ImageName(String fileName) { + this.fileName = fileName; + } + + public static ImageName from(String originalFileName) { + String fileName = FORMATTER.format(LocalDateTime.now()); + String extension = getExtension(originalFileName); + + return new ImageName(fileName + extension); + } + + private static String getExtension(String originalFileName) { + return originalFileName.substring( + originalFileName.lastIndexOf(EXTENSION_DELIMITER) + ); + } + + public String getFileName() { + return fileName; + } + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/s3/domain/S3Client.java b/backend/src/main/java/com/mapbefine/mapbefine/s3/domain/S3Client.java new file mode 100644 index 00000000..ab9775b5 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/s3/domain/S3Client.java @@ -0,0 +1,47 @@ +package com.mapbefine.mapbefine.s3.domain; + +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.DeleteObjectRequest; +import com.amazonaws.services.s3.model.PutObjectRequest; +import java.io.File; +import java.io.IOException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +@Component +public class S3Client { + + @Value("${s3.bucket}") + private String bucket; + private final AmazonS3 amazonS3; + + public S3Client(AmazonS3 amazonS3) { + this.amazonS3 = amazonS3; + } + + public void upload(MultipartFile multipartFile) { + File tempFile = null; + + try { + tempFile = File.createTempFile("upload_", ".tmp"); + multipartFile.transferTo(tempFile); + amazonS3.putObject(new PutObjectRequest(bucket, multipartFile.getOriginalFilename(), tempFile)); + } catch (IOException e) { // TODO: 2023/09/07 Exception 을 수정 + throw new RuntimeException(e); + } finally { + removeTempFileIfExists(tempFile); + } + } + + private void removeTempFileIfExists(final File tempFile) { + if (tempFile != null && tempFile.exists()) { + tempFile.delete(); + } + } + + public void delete(String key) { + amazonS3.deleteObject(new DeleteObjectRequest(bucket, key)); + } + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/s3/domain/UploadFile.java b/backend/src/main/java/com/mapbefine/mapbefine/s3/domain/UploadFile.java new file mode 100644 index 00000000..79a3b502 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/s3/domain/UploadFile.java @@ -0,0 +1,81 @@ +package com.mapbefine.mapbefine.s3.domain; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import org.springframework.core.io.Resource; +import org.springframework.web.multipart.MultipartFile; + +public class UploadFile implements MultipartFile { + + private final String fileName; + private final byte[] bytes; + + private UploadFile( + String fileName, + byte[] bytes + ) { + this.fileName = fileName; + this.bytes = bytes; + } + + public static UploadFile of( + MultipartFile multipartFile + ) throws IOException { + ImageName imageName = ImageName.from(multipartFile.getOriginalFilename()); + byte[] multipartFileBytes = multipartFile.getBytes(); + + return new UploadFile(imageName.getFileName(), multipartFileBytes); + } + + @Override + public String getName() { + return fileName; + } + + @Override + public String getOriginalFilename() { + return fileName; + } + + @Override + public String getContentType() { + return null; + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public long getSize() { + return 0; + } + + @Override + public byte[] getBytes() throws IOException { + return bytes; + } + + @Override + public InputStream getInputStream() throws IOException { + return new ByteArrayInputStream(bytes); + } + + @Override + public Resource getResource() { + return MultipartFile.super + .getResource(); + } + + @Override + public void transferTo(File dest) throws IOException, IllegalStateException { + try (FileOutputStream fileOutputStream = new FileOutputStream(dest)) { + fileOutputStream.write(bytes); + } + } + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/application/TopicCommandService.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/application/TopicCommandService.java index 77b77643..1466c02d 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/topic/application/TopicCommandService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/application/TopicCommandService.java @@ -14,6 +14,7 @@ import com.mapbefine.mapbefine.pin.domain.PinRepository; import com.mapbefine.mapbefine.pin.exception.PinException.PinBadRequestException; import com.mapbefine.mapbefine.pin.exception.PinException.PinForbiddenException; +import com.mapbefine.mapbefine.s3.application.S3Service; import com.mapbefine.mapbefine.topic.domain.Topic; import com.mapbefine.mapbefine.topic.domain.TopicRepository; import com.mapbefine.mapbefine.topic.dto.request.TopicCreateRequest; @@ -35,15 +36,18 @@ public class TopicCommandService { private final TopicRepository topicRepository; private final PinRepository pinRepository; private final MemberRepository memberRepository; + private final S3Service s3Service; public TopicCommandService( TopicRepository topicRepository, PinRepository pinRepository, - MemberRepository memberRepository + MemberRepository memberRepository, + S3Service s3Service ) { this.topicRepository = topicRepository; this.pinRepository = pinRepository; this.memberRepository = memberRepository; + this.s3Service = s3Service; } public Long saveTopic(AuthMember member, TopicCreateRequest request) { @@ -61,11 +65,12 @@ public Long saveTopic(AuthMember member, TopicCreateRequest request) { private Topic convertToTopic(AuthMember member, TopicCreateRequest request) { Member creator = findCreatorByAuthMember(member); + String image = s3Service.upload(request.image()); return Topic.createTopicAssociatedWithCreator( request.name(), request.description(), - request.image(), + image, request.publicity(), request.permissionType(), creator diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/dto/request/TopicCreateRequest.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/dto/request/TopicCreateRequest.java index e7dc64c5..211149fc 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/topic/dto/request/TopicCreateRequest.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/dto/request/TopicCreateRequest.java @@ -3,13 +3,28 @@ import com.mapbefine.mapbefine.topic.domain.PermissionType; import com.mapbefine.mapbefine.topic.domain.Publicity; import java.util.List; +import org.springframework.web.multipart.MultipartFile; public record TopicCreateRequest( String name, - String image, + MultipartFile image, String description, Publicity publicity, PermissionType permissionType, List pins ) { + + public static TopicCreateRequest of( + TopicCreateRequestWithOutImage request, + MultipartFile image + ) { + return new TopicCreateRequest( + request.name(), + image, + request.description(), + request.publicity(), + request.permissionType(), + request.pins() + ); + } } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/dto/request/TopicCreateRequestWithOutImage.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/dto/request/TopicCreateRequestWithOutImage.java new file mode 100644 index 00000000..2f59da22 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/dto/request/TopicCreateRequestWithOutImage.java @@ -0,0 +1,14 @@ +package com.mapbefine.mapbefine.topic.dto.request; + +import com.mapbefine.mapbefine.topic.domain.PermissionType; +import com.mapbefine.mapbefine.topic.domain.Publicity; +import java.util.List; + +public record TopicCreateRequestWithOutImage( + String name, + String description, + Publicity publicity, + PermissionType permissionType, + List pins +) { +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/presentation/TopicController.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/presentation/TopicController.java index fa1842db..a614d0e1 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/topic/presentation/TopicController.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/presentation/TopicController.java @@ -5,6 +5,7 @@ import com.mapbefine.mapbefine.topic.application.TopicCommandService; import com.mapbefine.mapbefine.topic.application.TopicQueryService; import com.mapbefine.mapbefine.topic.dto.request.TopicCreateRequest; +import com.mapbefine.mapbefine.topic.dto.request.TopicCreateRequestWithOutImage; import com.mapbefine.mapbefine.topic.dto.request.TopicMergeRequest; import com.mapbefine.mapbefine.topic.dto.request.TopicUpdateRequest; import com.mapbefine.mapbefine.topic.dto.response.TopicDetailResponse; @@ -20,7 +21,9 @@ import org.springframework.web.bind.annotation.RequestBody; 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; @RestController @RequestMapping("/topics") @@ -39,8 +42,15 @@ public TopicController( @LoginRequired @PostMapping("/new") - public ResponseEntity create(AuthMember member, @RequestBody TopicCreateRequest request) { - Long topicId = topicCommandService.saveTopic(member, request); + public ResponseEntity create( + AuthMember member, + @RequestPart TopicCreateRequestWithOutImage request, + @RequestPart MultipartFile image + ) { + System.out.println(request); + System.out.println(image); + TopicCreateRequest topicCreateRequest = TopicCreateRequest.of(request, image); + Long topicId = topicCommandService.saveTopic(member, topicCreateRequest); return ResponseEntity.created(URI.create("/topics/" + topicId)) .build(); diff --git a/backend/src/main/resources/config b/backend/src/main/resources/config index ebb17a89..9842778b 160000 --- a/backend/src/main/resources/config +++ b/backend/src/main/resources/config @@ -1 +1 @@ -Subproject commit ebb17a895391a6cec3a893d48f632c72b0234326 +Subproject commit 9842778b9712e4c1eb5c918e2fa254edbafb8a04 diff --git a/backend/src/test/java/com/mapbefine/mapbefine/FileFixture.java b/backend/src/test/java/com/mapbefine/mapbefine/FileFixture.java new file mode 100644 index 00000000..7acc3fec --- /dev/null +++ b/backend/src/test/java/com/mapbefine/mapbefine/FileFixture.java @@ -0,0 +1,11 @@ +package com.mapbefine.mapbefine; + +import org.springframework.web.multipart.MultipartFile; + +public class FileFixture { + + public static MultipartFile createFile() { + return new StubFile(); + } + +} diff --git a/backend/src/test/java/com/mapbefine/mapbefine/StubFile.java b/backend/src/test/java/com/mapbefine/mapbefine/StubFile.java new file mode 100644 index 00000000..f90b4407 --- /dev/null +++ b/backend/src/test/java/com/mapbefine/mapbefine/StubFile.java @@ -0,0 +1,63 @@ +package com.mapbefine.mapbefine; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Base64; +import org.springframework.web.multipart.MultipartFile; + +public class StubFile implements MultipartFile { + + private final String fileName; + private final byte[] bytes; + + public StubFile() { + this.fileName = "yyyyMMddHHmmssSSSSSS"; + this.bytes = fileName.getBytes(); + } + + @Override + public String getName() { + return fileName; + } + + @Override + public String getOriginalFilename() { + return fileName; + } + + @Override + public String getContentType() { + return "text/plain"; + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public long getSize() { + return 0; + } + + @Override + public byte[] getBytes() throws IOException { + return Base64.getEncoder() + .encode(bytes); + } + + @Override + public InputStream getInputStream() { + return InputStream.nullInputStream(); + } + + @Override + public void transferTo(final File dest) throws IOException, IllegalStateException { + FileOutputStream fileOutputStream = new FileOutputStream(dest); + fileOutputStream.write(this.bytes); + fileOutputStream.close(); + } + +} diff --git a/backend/src/test/java/com/mapbefine/mapbefine/TestS3ServiceImpl.java b/backend/src/test/java/com/mapbefine/mapbefine/TestS3ServiceImpl.java new file mode 100644 index 00000000..b53f80df --- /dev/null +++ b/backend/src/test/java/com/mapbefine/mapbefine/TestS3ServiceImpl.java @@ -0,0 +1,18 @@ +package com.mapbefine.mapbefine; + +import com.mapbefine.mapbefine.s3.application.S3Service; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +@Service +@Profile("test") +public class TestS3ServiceImpl implements S3Service { + + @Override + public String upload(MultipartFile multipartFile) { + System.out.println("TestS3ServiceImple Upload Method Called !!!!!!!!!!!!!!!!!!!!!!!!!!!!"); + return "https://mapbefine.github.io/favicon.png"; + } + +} diff --git a/backend/src/test/java/com/mapbefine/mapbefine/oauth/application/OauthServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/oauth/application/OauthServiceTest.java index 12d143ee..2d5c7122 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/oauth/application/OauthServiceTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/oauth/application/OauthServiceTest.java @@ -83,7 +83,6 @@ void tearDown() { void getAuthCodeRequestUrl_success() { // when String url = oauthService.getAuthCodeRequestUrl(OauthServerType.KAKAO); - System.out.println(oauthMember.toRegisterMember().getMemberInfo().getNickName()); // then assertThat(url).isEqualTo("https://kauth.kakao.com/oauth/authorize?" diff --git a/backend/src/test/java/com/mapbefine/mapbefine/oauth/domain/OauthMemberTest.java b/backend/src/test/java/com/mapbefine/mapbefine/oauth/domain/OauthMemberTest.java index 06e29413..beb6c1b8 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/oauth/domain/OauthMemberTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/oauth/domain/OauthMemberTest.java @@ -22,7 +22,6 @@ void toRegisterMember() { .getMemberInfo() .getNickName(); - System.out.println(expected); assertThat(expected).contains("모험가"); } } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/pin/PinIntegrationTest.java b/backend/src/test/java/com/mapbefine/mapbefine/pin/PinIntegrationTest.java index 0949a5cc..c1d6b653 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/pin/PinIntegrationTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/pin/PinIntegrationTest.java @@ -12,7 +12,6 @@ import com.mapbefine.mapbefine.member.domain.MemberRepository; import com.mapbefine.mapbefine.member.domain.Role; import com.mapbefine.mapbefine.pin.dto.request.PinCreateRequest; -import com.mapbefine.mapbefine.pin.dto.request.PinImageCreateRequest; import com.mapbefine.mapbefine.pin.dto.response.PinImageResponse; import com.mapbefine.mapbefine.pin.dto.response.PinResponse; import com.mapbefine.mapbefine.topic.TopicFixture; @@ -21,6 +20,7 @@ import io.restassured.RestAssured; import io.restassured.response.ExtractableResponse; import io.restassured.response.Response; +import java.io.File; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -31,8 +31,6 @@ class PinIntegrationTest extends IntegrationTest { - private static final String BASE_IMAGE = "https://map-befine-official.github.io/favicon.png"; - private Topic topic; private Location location; private Member member; @@ -187,15 +185,22 @@ void addImage_Success() { // when ExtractableResponse response = createPinImage(pinId); + System.out.println(response); + // then assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value()); } private ExtractableResponse createPinImage(long pinId) { + String imageFilePath = getClass().getClassLoader() + .getResource("test.png") + .getPath(); + File mockFile = new File(imageFilePath); + return RestAssured.given().log().all() .header(AUTHORIZATION, authHeader) - .contentType(MediaType.APPLICATION_JSON_VALUE) - .body(new PinImageCreateRequest(pinId, BASE_IMAGE)) + .multiPart("image", mockFile, MediaType.MULTIPART_FORM_DATA_VALUE) + .multiPart("pinId", pinId, MediaType.APPLICATION_JSON_VALUE) .when().post("/pins/images") .then().log().all() .extract(); diff --git a/backend/src/test/java/com/mapbefine/mapbefine/pin/application/PinCommandServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/pin/application/PinCommandServiceTest.java index ef41b89f..362242e9 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/pin/application/PinCommandServiceTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/pin/application/PinCommandServiceTest.java @@ -3,6 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import com.mapbefine.mapbefine.FileFixture; import com.mapbefine.mapbefine.auth.domain.AuthMember; import com.mapbefine.mapbefine.auth.domain.member.Admin; import com.mapbefine.mapbefine.auth.domain.member.Guest; @@ -33,11 +34,13 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.multipart.MultipartFile; @ServiceTest class PinCommandServiceTest { - private static final String BASE_IMAGE = "https://map-befine-official.github.io/favicon.png"; + private static final MultipartFile BASE_IMAGE_FILE = FileFixture.createFile(); + private static final String BASE_IMAGE = "https://mapbefine.github.io/favicon.png"; @Autowired private PinCommandService pinCommandService; @@ -192,7 +195,7 @@ void addImage_Success() { // then pinImageRepository.findById(pinImageId) .ifPresentOrElse( - found -> assertThat(found.getImageUrl()).isEqualTo(BASE_IMAGE), + found -> assertThat(found.getImageUrl()).isNotNull(), Assertions::fail ); } @@ -204,7 +207,8 @@ void addImage_FailByForbidden() { long pinId = pinCommandService.save(authMember, createRequest); // when, then - assertThatThrownBy(() -> pinCommandService.addImage(new Guest(), new PinImageCreateRequest(pinId, BASE_IMAGE))) + assertThatThrownBy(() -> pinCommandService.addImage(new Guest(), new PinImageCreateRequest(pinId, + BASE_IMAGE_FILE))) .isInstanceOf(PinForbiddenException.class); } @@ -227,7 +231,7 @@ void removeImageById_Success() { } private long savePinImageAndGetId(long pinId) { - pinCommandService.addImage(authMember, new PinImageCreateRequest(pinId, BASE_IMAGE)); + pinCommandService.addImage(authMember, new PinImageCreateRequest(pinId, BASE_IMAGE_FILE)); List pinImages = pinQueryService.findDetailById(authMember, pinId) .images(); PinImageResponse pinImage = pinImages.get(0); diff --git a/backend/src/test/java/com/mapbefine/mapbefine/pin/presentation/PinControllerTest.java b/backend/src/test/java/com/mapbefine/mapbefine/pin/presentation/PinControllerTest.java index 74252c88..c9cbdb24 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/pin/presentation/PinControllerTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/pin/presentation/PinControllerTest.java @@ -3,9 +3,11 @@ import static org.apache.http.HttpHeaders.AUTHORIZATION; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import com.mapbefine.mapbefine.FileFixture; import com.mapbefine.mapbefine.common.RestDocsIntegration; import com.mapbefine.mapbefine.pin.application.PinCommandService; import com.mapbefine.mapbefine.pin.application.PinQueryService; @@ -15,6 +17,7 @@ import com.mapbefine.mapbefine.pin.dto.response.PinDetailResponse; import com.mapbefine.mapbefine.pin.dto.response.PinImageResponse; import com.mapbefine.mapbefine.pin.dto.response.PinResponse; +import java.io.File; import java.time.LocalDateTime; import java.util.List; import org.junit.jupiter.api.DisplayName; @@ -146,16 +149,22 @@ void findAll() throws Exception { @Test @DisplayName("핀 이미지 추가") void addImage() throws Exception { - PinImageCreateRequest pinImageCreateRequest = new PinImageCreateRequest( - 1L, - "https://map-befine-official.github.io/favicon.png" - ); + String imageFilePath = getClass().getClassLoader() + .getResource("test.png") + .getPath(); + File mockFile = new File(imageFilePath); + +// PinImageCreateRequest pinImageCreateRequest = new PinImageCreateRequest( +// 1L, +// FileFixture.createFile() +// ); mockMvc.perform( MockMvcRequestBuilders.post("/pins/images") .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(pinImageCreateRequest)) + .content(objectMapper.writeValueAsString(1L)) + .content(objectMapper.writeValueAsString(mockFile)) ).andDo(restDocs.document()); } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/topic/TopicFixture.java b/backend/src/test/java/com/mapbefine/mapbefine/topic/TopicFixture.java index 818e81f3..ed8a196a 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/topic/TopicFixture.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/topic/TopicFixture.java @@ -1,6 +1,7 @@ package com.mapbefine.mapbefine.topic; +import com.mapbefine.mapbefine.FileFixture; import com.mapbefine.mapbefine.member.domain.Member; import com.mapbefine.mapbefine.topic.domain.PermissionType; import com.mapbefine.mapbefine.topic.domain.Publicity; @@ -62,7 +63,8 @@ public static TopicCreateRequest createPublicAndAllMembersCreateRequestWithPins( ) { return new TopicCreateRequest( "아무나 읽을 수 있는 토픽", - IMAGE_URL, + FileFixture.createFile(), +// IMAGE_URL, "아무나 읽을 수 있는 토픽입니다.", Publicity.PUBLIC, PermissionType.ALL_MEMBERS, diff --git a/backend/src/test/java/com/mapbefine/mapbefine/topic/TopicIntegrationTest.java b/backend/src/test/java/com/mapbefine/mapbefine/topic/TopicIntegrationTest.java index 833e5333..f344ce9a 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/topic/TopicIntegrationTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/topic/TopicIntegrationTest.java @@ -3,6 +3,7 @@ import static org.apache.http.HttpHeaders.AUTHORIZATION; import static org.assertj.core.api.Assertions.assertThat; +import com.mapbefine.mapbefine.FileFixture; import com.mapbefine.mapbefine.bookmark.domain.Bookmark; import com.mapbefine.mapbefine.bookmark.domain.BookmarkRepository; import com.mapbefine.mapbefine.common.IntegrationTest; @@ -21,12 +22,14 @@ import com.mapbefine.mapbefine.topic.domain.Topic; import com.mapbefine.mapbefine.topic.domain.TopicRepository; import com.mapbefine.mapbefine.topic.dto.request.TopicCreateRequest; +import com.mapbefine.mapbefine.topic.dto.request.TopicCreateRequestWithOutImage; import com.mapbefine.mapbefine.topic.dto.request.TopicMergeRequest; import com.mapbefine.mapbefine.topic.dto.request.TopicUpdateRequest; import com.mapbefine.mapbefine.topic.dto.response.TopicDetailResponse; import com.mapbefine.mapbefine.topic.dto.response.TopicResponse; import io.restassured.*; import io.restassured.response.*; +import java.io.File; import java.util.Collections; import java.util.List; import org.junit.jupiter.api.BeforeEach; @@ -35,6 +38,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; class TopicIntegrationTest extends IntegrationTest { @@ -70,9 +74,8 @@ void setMember() { @Test @DisplayName("Pin 목록 없이 Topic을 생성하면 201을 반환한다") void createNewTopicWithoutPins_Success() { - TopicCreateRequest 준팍의_또간집 = new TopicCreateRequest( + TopicCreateRequestWithOutImage 준팍의_또간집 = new TopicCreateRequestWithOutImage( "준팍의 또간집", - "https://map-befine-official.github.io/favicon.png", "준팍이 2번 이상 간집 ", Publicity.PUBLIC, PermissionType.ALL_MEMBERS, @@ -87,12 +90,24 @@ void createNewTopicWithoutPins_Success() { assertThat(response.header("Location")).isNotBlank(); } - private ExtractableResponse createNewTopic(TopicCreateRequest request, String authHeader) { + private ExtractableResponse createNewTopic(TopicCreateRequestWithOutImage request, String authHeader) { + String imageFilePath = getClass().getClassLoader() + .getResource("test.png") + .getPath(); + File mockFile = new File(imageFilePath); + +// MockMultipartFile mockFile = new MockMultipartFile( // 이것은 왜 그런 것일까?? +// "test", +// "test.png", +// "image/png", +// "byteCode".getBytes() +// ); + return RestAssured.given() .log().all() .header(AUTHORIZATION, authHeader) - .contentType(MediaType.APPLICATION_JSON_VALUE) - .body(request) + .multiPart("image", mockFile, MediaType.MULTIPART_FORM_DATA_VALUE) + .multiPart("request", request, MediaType.APPLICATION_JSON_VALUE) .when().post("/topics/new") .then().log().all() .extract(); @@ -109,9 +124,8 @@ void createNewTopicWithPins_Success() { .map(Pin::getId) .toList(); - TopicCreateRequest 준팍의_또간집 = new TopicCreateRequest( + TopicCreateRequestWithOutImage 준팍의_또간집 = new TopicCreateRequestWithOutImage( "준팍의 또간집", - "https://map-befine-official.github.io/favicon.png", "준팍이 2번 이상 간집 ", Publicity.PUBLIC, PermissionType.ALL_MEMBERS, @@ -130,17 +144,15 @@ void createNewTopicWithPins_Success() { @DisplayName("여러개의 토픽을 병합하면 201을 반환한다") void createMergeTopic_Success() { // given - TopicCreateRequest 준팍의_또간집 = new TopicCreateRequest( + TopicCreateRequestWithOutImage 준팍의_또간집 = new TopicCreateRequestWithOutImage( "준팍의 또간집", - "https://map-befine-official.github.io/favicon.png", "준팍이 2번 이상 간집 ", Publicity.PUBLIC, PermissionType.ALL_MEMBERS, Collections.emptyList() ); - TopicCreateRequest 준팍의_또안간집 = new TopicCreateRequest( + TopicCreateRequestWithOutImage 준팍의_또안간집 = new TopicCreateRequestWithOutImage( "준팍의 또안간집", - "https://map-befine-official.github.io/favicon.png", "준팍이 2번 이상 안간집 ", Publicity.PUBLIC, PermissionType.ALL_MEMBERS, @@ -180,9 +192,8 @@ void createMergeTopic_Success() { @DisplayName("Topic을 수정하면 200을 반환한다") void updateTopic_Success() { ExtractableResponse newTopic = createNewTopic( - new TopicCreateRequest( + new TopicCreateRequestWithOutImage( "준팍의 또간집", - "https://map-befine-official.github.io/favicon.png", "준팍이 두번 간집", Publicity.PUBLIC, PermissionType.ALL_MEMBERS, @@ -217,9 +228,8 @@ void updateTopic_Success() { @DisplayName("Topic을 삭제하면 204를 반환한다") void deleteTopic_Success() { ExtractableResponse newTopic = createNewTopic( - new TopicCreateRequest( + new TopicCreateRequestWithOutImage( "준팍의 또간집", - "https://map-befine-official.github.io/favicon.png", "준팍이 두번 간집 ", Publicity.PUBLIC, PermissionType.ALL_MEMBERS, @@ -262,9 +272,8 @@ void findTopics_Success() { @DisplayName("Topic 상세 정보를 조회하면 200을 반환한다") void findTopicDetail_Success() { //given - TopicCreateRequest request = new TopicCreateRequest( + TopicCreateRequestWithOutImage request = new TopicCreateRequestWithOutImage( "topicName", - "https://map-befine-official.github.io/favicon.png", "description", Publicity.PUBLIC, PermissionType.ALL_MEMBERS, @@ -291,9 +300,8 @@ void findTopicDetail_Success() { @DisplayName("Topic 상세 정보 여러개를 조회하면 200을 반환한다") void findTopicDetailsByIds_Success() { //given - TopicCreateRequest request = new TopicCreateRequest( + TopicCreateRequestWithOutImage request = new TopicCreateRequestWithOutImage( "topicName", - "https://map-befine-official.github.io/favicon.png", "description", Publicity.PUBLIC, PermissionType.ALL_MEMBERS, diff --git a/backend/src/test/java/com/mapbefine/mapbefine/topic/presentation/TopicControllerTest.java b/backend/src/test/java/com/mapbefine/mapbefine/topic/presentation/TopicControllerTest.java index 4fd2e7f9..2e15e271 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/topic/presentation/TopicControllerTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/topic/presentation/TopicControllerTest.java @@ -3,7 +3,9 @@ import static org.apache.http.HttpHeaders.AUTHORIZATION; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import com.mapbefine.mapbefine.FileFixture; import com.mapbefine.mapbefine.common.RestDocsIntegration; import com.mapbefine.mapbefine.pin.dto.response.PinResponse; import com.mapbefine.mapbefine.topic.application.TopicCommandService; @@ -11,17 +13,28 @@ import com.mapbefine.mapbefine.topic.domain.PermissionType; import com.mapbefine.mapbefine.topic.domain.Publicity; import com.mapbefine.mapbefine.topic.dto.request.TopicCreateRequest; +import com.mapbefine.mapbefine.topic.dto.request.TopicCreateRequestWithOutImage; import com.mapbefine.mapbefine.topic.dto.request.TopicMergeRequest; import com.mapbefine.mapbefine.topic.dto.request.TopicUpdateRequest; import com.mapbefine.mapbefine.topic.dto.response.TopicDetailResponse; import com.mapbefine.mapbefine.topic.dto.response.TopicResponse; +import java.io.File; import java.time.LocalDateTime; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Map.Entry; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.web.servlet.request.MockMultipartHttpServletRequestBuilder; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; class TopicControllerTest extends RestDocsIntegration { // TODO: 2023/07/25 Image 칼람 추가됨으로 인해 수정 필요 @@ -57,21 +70,25 @@ class TopicControllerTest extends RestDocsIntegration { // TODO: 2023/07/25 Imag @DisplayName("토픽 새로 생성") void create() throws Exception { given(topicCommandService.saveTopic(any(), any())).willReturn(1L); + File mockFile = new File(getClass().getClassLoader().getResource("test.png").getPath()); + MultiValueMap param = new LinkedMultiValueMap<>(); - TopicCreateRequest topicCreateRequest = new TopicCreateRequest( + TopicCreateRequestWithOutImage request = new TopicCreateRequestWithOutImage( "준팍의 안갈집", - "https://map-befine-official.github.io/favicon.png", "준팍이 두번 다시 안갈집", Publicity.PUBLIC, PermissionType.ALL_MEMBERS, List.of(1L, 2L, 3L) ); + param.add("image", mockFile); + param.add("request", request); + mockMvc.perform( MockMvcRequestBuilders.post("/topics/new") .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(topicCreateRequest)) + .contentType(MediaType.MULTIPART_FORM_DATA_VALUE) + .content(objectMapper.writeValueAsString(mockFile)) ).andDo(restDocs.document()); } diff --git a/backend/src/test/resources/application.yml b/backend/src/test/resources/application.yml index b0c95fea..283d08bc 100644 --- a/backend/src/test/resources/application.yml +++ b/backend/src/test/resources/application.yml @@ -1,10 +1,15 @@ spring: + profiles: + active: test + datasource: url: jdbc:h2:mem:mapbefine;MODE=MySQL username: sa + sql: init: data-locations: + jpa: show-sql: true hibernate: diff --git a/backend/src/test/resources/test.png b/backend/src/test/resources/test.png new file mode 100644 index 00000000..e69de29b From 8c90435e8735381957476f72d28988ca3d2664d5 Mon Sep 17 00:00:00 2001 From: kpeel5839 <89840550+kpeel5839@users.noreply.github.com> Date: Tue, 12 Sep 2023 17:24:06 +0900 Subject: [PATCH 10/34] =?UTF-8?q?refactor=20:=20S3=20Bean=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#396)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/mapbefine/mapbefine/common/config/S3Config.java | 7 +++++++ .../mapbefine/topic/presentation/TopicController.java | 8 +++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/backend/src/main/java/com/mapbefine/mapbefine/common/config/S3Config.java b/backend/src/main/java/com/mapbefine/mapbefine/common/config/S3Config.java index 204a09bc..5d609979 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/common/config/S3Config.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/common/config/S3Config.java @@ -1,5 +1,6 @@ package com.mapbefine.mapbefine.common.config; +import com.amazonaws.auth.InstanceProfileCredentialsProvider; import com.amazonaws.regions.Regions; import com.amazonaws.services.s3.AmazonS3; import com.amazonaws.services.s3.AmazonS3ClientBuilder; @@ -9,10 +10,16 @@ @Configuration public class S3Config { + @Bean + public InstanceProfileCredentialsProvider instanceProfileCredentialsProvider() { + return InstanceProfileCredentialsProvider.getInstance(); + } + @Bean public AmazonS3 amazonS3() { return AmazonS3ClientBuilder.standard() .withRegion(Regions.AP_NORTHEAST_2) + .withCredentials(instanceProfileCredentialsProvider()) .build(); } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/presentation/TopicController.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/presentation/TopicController.java index a614d0e1..fffae8fb 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/topic/presentation/TopicController.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/presentation/TopicController.java @@ -12,6 +12,7 @@ import com.mapbefine.mapbefine.topic.dto.response.TopicResponse; import java.net.URI; import java.util.List; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -41,14 +42,15 @@ public TopicController( } @LoginRequired - @PostMapping("/new") + @PostMapping( + value = "/new", + consumes = {MediaType.MULTIPART_FORM_DATA_VALUE, MediaType.APPLICATION_JSON_VALUE} + ) public ResponseEntity create( AuthMember member, @RequestPart TopicCreateRequestWithOutImage request, @RequestPart MultipartFile image ) { - System.out.println(request); - System.out.println(image); TopicCreateRequest topicCreateRequest = TopicCreateRequest.of(request, image); Long topicId = topicCommandService.saveTopic(member, topicCreateRequest); From 8e56ec8f287aae146f30a31ec6729bab44f32c54 Mon Sep 17 00:00:00 2001 From: Doy Date: Tue, 12 Sep 2023 17:28:18 +0900 Subject: [PATCH 11/34] =?UTF-8?q?[BE]=20Refactor/#390=20=EC=A7=80=EB=8F=84?= =?UTF-8?q?=20=EB=B0=8F=20=ED=95=80=20=EC=83=81=EC=84=B8=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20API=EC=97=90=20=EC=88=98=EC=A0=95=20=EA=B6=8C?= =?UTF-8?q?=ED=95=9C=20=EC=97=AC=EB=B6=80=20=ED=95=84=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#392)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 토픽 상세 조회 내용에 수정 권한 추가 * refactor: 토픽 상세 조회 DTO 정적 팩터리 메서드명 수정 * feat: 핀 상세 조회 내용에 수정 권한 추가 * fix: 토픽 RestDocs 깨지는 문제 수정, API 목차 순서 조정 - 목록 조회 API 기준으로 순서 조정, API 네이밍 보완 * refactor: 토픽, 핀 수정 권한 여부 필드명 직관적으로 수정 - hasUpdatePermission -> canUpdate * refactor: Guest 전용 토픽 상세조회 DTO 정적 팩터리 메서드 정의 * refactor: 메서드 순서 정리, Guest 전용 토픽 조회 DTO 정적 팩터리 메서드 정의 * refactor: Guest 전용 토픽 List 조회 DTO 정적 팩터리 메서드 정의 --- backend/src/docs/asciidoc/topic.adoc | 23 ++++++----- .../pin/application/PinQueryService.java | 2 +- .../pin/dto/response/PinDetailResponse.java | 4 +- .../topic/application/TopicQueryService.java | 39 +++++++----------- .../dto/response/TopicDetailResponse.java | 40 ++++++++++++++++++- .../topic/dto/response/TopicResponse.java | 17 ++++++++ .../presentation/LocationControllerTest.java | 2 +- .../application/PinCommandServiceTest.java | 12 +++--- .../pin/application/PinQueryServiceTest.java | 2 +- .../pin/presentation/PinControllerTest.java | 1 + .../application/TopicQueryServiceTest.java | 25 +++++++----- .../presentation/TopicControllerTest.java | 1 + 12 files changed, 111 insertions(+), 57 deletions(-) diff --git a/backend/src/docs/asciidoc/topic.adoc b/backend/src/docs/asciidoc/topic.adoc index af191240..524f94a3 100644 --- a/backend/src/docs/asciidoc/topic.adoc +++ b/backend/src/docs/asciidoc/topic.adoc @@ -1,24 +1,29 @@ == 토픽 -=== 토픽 목록 조회 +=== 토픽 전체 목록 조회 operation::topic-controller-test/find-all[snippets='http-request,http-response'] -=== 토픽 인기 목록 조회 +=== 최신 토픽 목록 조회 -operation::location-controller-test/find-nearby-topics-sorted-by-pin-count[snippets='http-request,http-response'] +operation::topic-controller-test/find-all-by-order-by-updated-at-desc[snippets='http-request,http-response'] + +=== 인기 급상승 토픽 목록 조회 (즐겨찾기 기준) + +operation::topic-controller-test/find-all-best-topics[snippets='http-request,http-response'] === 멤버별 토픽 목록 조회 operation::topic-controller-test/find-all-topics-by-member-id[snippets='http-request,http-response'] +=== 주변 인기 토픽 목록 조회 (핀 개수 기준) + +operation::location-controller-test/find-nearby-topics-sorted-by-pin-count[snippets='http-request,http-response'] + === 토픽 상세 조회 operation::topic-controller-test/find-by-id[snippets='http-request,http-response'] -=== 최신 토픽 목록 조회 -operation::topic-controller-test/find-all-by-order-by-updated-at-desc[snippets='http-request,http-response'] - === 토픽 생성 operation::topic-controller-test/create[snippets='http-request,http-response'] @@ -27,7 +32,8 @@ operation::topic-controller-test/create[snippets='http-request,http-response'] operation::topic-controller-test/merge-and-create[snippets='http-request,http-response'] -== 토픽에 핀 추가 +=== 토픽에 핀 추가 + operation::topic-controller-test/copy-pin[snippets='http-request,http-response'] === 토픽 수정 @@ -38,6 +44,3 @@ operation::topic-controller-test/update[snippets='http-request,http-response'] operation::topic-controller-test/delete[snippets='http-request,http-response'] -=== 인기 급상승 토픽 조회 - -operation::topic-controller-test/find-all-best-topics[snippets='http-request,http-response'] diff --git a/backend/src/main/java/com/mapbefine/mapbefine/pin/application/PinQueryService.java b/backend/src/main/java/com/mapbefine/mapbefine/pin/application/PinQueryService.java index f5a0b23a..6956b55d 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/pin/application/PinQueryService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/pin/application/PinQueryService.java @@ -40,7 +40,7 @@ public PinDetailResponse findDetailById(AuthMember member, Long pinId) { .orElseThrow(() -> new PinNotFoundException(PIN_NOT_FOUND, pinId)); validateReadAuth(member, pin.getTopic()); - return PinDetailResponse.from(pin); + return PinDetailResponse.of(pin, member.canPinCreateOrUpdate(pin.getTopic())); } private void validateReadAuth(AuthMember member, Topic topic) { diff --git a/backend/src/main/java/com/mapbefine/mapbefine/pin/dto/response/PinDetailResponse.java b/backend/src/main/java/com/mapbefine/mapbefine/pin/dto/response/PinDetailResponse.java index c1f241fc..33c3cea7 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/pin/dto/response/PinDetailResponse.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/pin/dto/response/PinDetailResponse.java @@ -13,11 +13,12 @@ public record PinDetailResponse( String creator, double latitude, double longitude, + Boolean canUpdate, LocalDateTime updatedAt, List images ) { - public static PinDetailResponse from(Pin pin) { + public static PinDetailResponse of(Pin pin, Boolean canUpdate) { PinInfo pinInfo = pin.getPinInfo(); return new PinDetailResponse( @@ -28,6 +29,7 @@ public static PinDetailResponse from(Pin pin) { pin.getCreator().getMemberInfo().getNickName(), pin.getLatitude(), pin.getLongitude(), + canUpdate, pin.getUpdatedAt(), PinImageResponse.from(pin.getPinImages()) ); diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/application/TopicQueryService.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/application/TopicQueryService.java index 23cd0ae8..aa19d281 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/topic/application/TopicQueryService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/application/TopicQueryService.java @@ -52,7 +52,7 @@ private List getGuestTopicResponses(AuthMember authMember) { return topicRepository.findAll() .stream() .filter(authMember::canRead) - .map(topic -> TopicResponse.from(topic, Boolean.FALSE, Boolean.FALSE)) + .map(TopicResponse::fromGuestQuery) .toList(); } @@ -99,13 +99,12 @@ private boolean isBookMarked(List bookMarkedTopics, Topic topic) { return bookMarkedTopics.contains(topic); } - public TopicDetailResponse findDetailById(AuthMember authMember, Long topicId) { Topic topic = findTopic(topicId); validateReadableTopic(authMember, topic); if (Objects.isNull(authMember.getMemberId())) { - return TopicDetailResponse.from(topic, Boolean.FALSE, Boolean.FALSE); + return TopicDetailResponse.fromGuestQuery(topic); } Member member = findMemberById(authMember.getMemberId()); @@ -113,10 +112,11 @@ public TopicDetailResponse findDetailById(AuthMember authMember, Long topicId) { List topicsInAtlas = findTopicsInAtlas(member); List topicsInBookMark = findBookMarkedTopics(member); - return TopicDetailResponse.from( + return TopicDetailResponse.of( topic, isInAtlas(topicsInAtlas, topic), - isBookMarked(topicsInBookMark, topic) + isBookMarked(topicsInBookMark, topic), + authMember.canTopicUpdate(topic) ); } @@ -140,18 +140,12 @@ public List findDetailsByIds(AuthMember authMember, List getGuestTopicDetailResponses(List topics) { - return topics.stream() - .map(topic -> TopicDetailResponse.from(topic, Boolean.FALSE, Boolean.FALSE)) - .toList(); - } - private List getUserTopicDetailResponses(AuthMember authMember, List topics) { Member member = findMemberById(authMember.getMemberId()); @@ -159,10 +153,11 @@ private List getUserTopicDetailResponses(AuthMember authMem List topicsInBookMark = findBookMarkedTopics(member); return topics.stream() - .map(topic -> TopicDetailResponse.from( + .map(topic -> TopicDetailResponse.of( topic, isInAtlas(topicsInAtlas, topic), - isBookMarked(topicsInBookMark, topic) + isBookMarked(topicsInBookMark, topic), + authMember.canTopicUpdate(topic) )) .toList(); } @@ -194,7 +189,7 @@ public List findAllTopicsByMemberId(AuthMember authMember, Long m return topicRepository.findByCreatorId(memberId) .stream() .filter(authMember::canRead) - .map(topic -> TopicResponse.from(topic, Boolean.FALSE, Boolean.FALSE)) + .map(TopicResponse::fromGuestQuery) .toList(); } @@ -247,11 +242,8 @@ private List getGuestNewestTopicResponse(AuthMember authMember) { .map(Pin::getTopic) .distinct() .filter(authMember::canRead) - .map(topic -> TopicResponse.from( - topic, - Boolean.FALSE, - Boolean.FALSE - )).toList(); + .map(TopicResponse::fromGuestQuery) + .toList(); } public List findAllBestTopics(AuthMember authMember) { @@ -266,11 +258,8 @@ private List getGuestBestTopicResponse(AuthMember authMember) { .stream() .filter(authMember::canRead) .sorted(Comparator.comparing(Topic::countBookmarks).reversed()) - .map(topic -> TopicResponse.from( - topic, - Boolean.FALSE, - Boolean.FALSE - )).toList(); + .map(TopicResponse::fromGuestQuery) + .toList(); } private List getUserBestTopicResponse(AuthMember authMember) { diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/dto/response/TopicDetailResponse.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/dto/response/TopicDetailResponse.java index f83563a8..f46f6dbf 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/topic/dto/response/TopicDetailResponse.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/dto/response/TopicDetailResponse.java @@ -16,10 +16,17 @@ public record TopicDetailResponse( Boolean isInAtlas, Integer bookmarkCount, Boolean isBookmarked, + Boolean canUpdate, LocalDateTime updatedAt, List pins ) { - public static TopicDetailResponse from(Topic topic, Boolean isInAtlas, Boolean isBookmarked) { + + public static TopicDetailResponse of( + Topic topic, + Boolean isInAtlas, + Boolean isBookmarked, + Boolean canUpdate + ) { List pinResponses = topic.getPins().stream() .map(PinResponse::from) .toList(); @@ -36,8 +43,39 @@ public static TopicDetailResponse from(Topic topic, Boolean isInAtlas, Boolean i isInAtlas, topic.countBookmarks(), isBookmarked, + canUpdate, + topic.getUpdatedAt(), + pinResponses + ); + } + + public static TopicDetailResponse fromGuestQuery(Topic topic) { + List pinResponses = topic.getPins().stream() + .map(PinResponse::from) + .toList(); + + TopicInfo topicInfo = topic.getTopicInfo(); + + return new TopicDetailResponse( + topic.getId(), + topicInfo.getName(), + topicInfo.getDescription(), + topicInfo.getImageUrl(), + topic.getCreator().getMemberInfo().getNickName(), + topic.countPins(), + Boolean.FALSE, + topic.countBookmarks(), + Boolean.FALSE, + Boolean.FALSE, topic.getUpdatedAt(), pinResponses ); } + + public static List ofGuestQueries(List topics) { + + return topics.stream() + .map(TopicDetailResponse::fromGuestQuery) + .toList(); + } } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/dto/response/TopicResponse.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/dto/response/TopicResponse.java index 73165788..9cb31309 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/topic/dto/response/TopicResponse.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/dto/response/TopicResponse.java @@ -15,6 +15,7 @@ public record TopicResponse( Boolean isBookmarked, LocalDateTime updatedAt ) { + public static TopicResponse from(Topic topic, Boolean isInAtlas, Boolean isBookmarked) { TopicInfo topicInfo = topic.getTopicInfo(); @@ -31,4 +32,20 @@ public static TopicResponse from(Topic topic, Boolean isInAtlas, Boolean isBookm ); } + public static TopicResponse fromGuestQuery(Topic topic) { + TopicInfo topicInfo = topic.getTopicInfo(); + + return new TopicResponse( + topic.getId(), + topicInfo.getName(), + topicInfo.getImageUrl(), + topic.getCreator().getMemberInfo().getNickName(), + topic.countPins(), + Boolean.FALSE, + topic.countBookmarks(), + Boolean.FALSE, + topic.getUpdatedAt() + ); + } + } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/location/presentation/LocationControllerTest.java b/backend/src/test/java/com/mapbefine/mapbefine/location/presentation/LocationControllerTest.java index 8ca9b7f4..b7cb901b 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/location/presentation/LocationControllerTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/location/presentation/LocationControllerTest.java @@ -53,7 +53,7 @@ void setUp() { @Test @DisplayName("현재 위치를 기준 토픽의 핀 개수로 나열한다.") - void findNearbyTopicsSortedByPinCount_Success() throws Exception { + void findNearbyTopicsSortedByPinCount() throws Exception { //given double latitude = 37; double longitude = 127; diff --git a/backend/src/test/java/com/mapbefine/mapbefine/pin/application/PinCommandServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/pin/application/PinCommandServiceTest.java index 362242e9..bc806c28 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/pin/application/PinCommandServiceTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/pin/application/PinCommandServiceTest.java @@ -94,7 +94,7 @@ void saveIfExistLocation_Success() { assertThat(savedLocation.getId()).isEqualTo(location.getId()); assertThat(actual).usingRecursiveComparison() .ignoringFields("updatedAt") - .isEqualTo(PinDetailResponse.from(pin)); + .isEqualTo(PinDetailResponse.of(pin, Boolean.TRUE)); } @Test @@ -124,7 +124,7 @@ void saveIfNotExistLocation_Success() { assertThat(savedLocation.getId()).isNotEqualTo(location.getId()); assertThat(actual).usingRecursiveComparison() .ignoringFields("updatedAt") - .isEqualTo(PinDetailResponse.from(pin)); + .isEqualTo(PinDetailResponse.of(pin, Boolean.TRUE)); List locations = locationRepository.findAll(); assertThat(locations).hasSize(2); @@ -224,10 +224,10 @@ void removeImageById_Success() { // then pinImageRepository.findById(pinImageId) - .ifPresentOrElse( - found -> assertThat(found.isDeleted()).isTrue(), - Assertions::fail - ); + .ifPresentOrElse( + found -> assertThat(found.isDeleted()).isTrue(), + Assertions::fail + ); } private long savePinImageAndGetId(long pinId) { diff --git a/backend/src/test/java/com/mapbefine/mapbefine/pin/application/PinQueryServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/pin/application/PinQueryServiceTest.java index 0815dbf9..d1b3537f 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/pin/application/PinQueryServiceTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/pin/application/PinQueryServiceTest.java @@ -108,7 +108,7 @@ void findDetailById_Success() { // then assertThat(actual).usingRecursiveComparison() .ignoringFields("updatedAt") - .isEqualTo(PinDetailResponse.from(pin)); + .isEqualTo(PinDetailResponse.of(pin, Boolean.TRUE)); } @Test diff --git a/backend/src/test/java/com/mapbefine/mapbefine/pin/presentation/PinControllerTest.java b/backend/src/test/java/com/mapbefine/mapbefine/pin/presentation/PinControllerTest.java index c9cbdb24..01a1fc0c 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/pin/presentation/PinControllerTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/pin/presentation/PinControllerTest.java @@ -96,6 +96,7 @@ void findById() throws Exception { "매튜", 37, 127, + Boolean.FALSE, LocalDateTime.now(), List.of(new PinImageResponse(1L, BASE_IMAGES.get(0))) ); diff --git a/backend/src/test/java/com/mapbefine/mapbefine/topic/application/TopicQueryServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/topic/application/TopicQueryServiceTest.java index 003cfb7f..302d7aac 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/topic/application/TopicQueryServiceTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/topic/application/TopicQueryServiceTest.java @@ -235,7 +235,7 @@ void findDetailByIds_Fail2() { @Test @DisplayName("모든 토픽을 조회할 때, 즐겨찾기 여부를 함께 반환한다.") - public void findAllReadableWithBookmark_Success() { + void findAllReadableWithBookmark_Success() { //given Topic topic1 = TopicFixture.createPublicAndAllMembersTopic(member); Topic topic2 = TopicFixture.createPublicAndAllMembersTopic(member); @@ -258,7 +258,7 @@ public void findAllReadableWithBookmark_Success() { @Test @DisplayName("모든 토픽을 조회할 때, 로그인 유저가 아니면 즐겨찾기 여부가 항상 False다") - public void findAllReadableWithoutBookmark_Success() { + void findAllReadableWithoutBookmark_Success() { //given Topic topic1 = TopicFixture.createPublicAndAllMembersTopic(member); Topic topic2 = TopicFixture.createPublicAndAllMembersTopic(member); @@ -280,8 +280,8 @@ public void findAllReadableWithoutBookmark_Success() { } @Test - @DisplayName("토픽 상세조회시, 즐겨찾기 여부를 함께 반환한다.") - public void findWithBookmarkStatus_Success() { + @DisplayName("토픽 상세조회시, 즐겨찾기 여부, 모아보기 여부, 수정 권한 여부를 함께 반환한다.") + void findWithBookmarkStatus_Success() { //given Topic topic = TopicFixture.createPublicAndAllMembersTopic(member); topicRepository.save(topic); @@ -295,12 +295,13 @@ public void findWithBookmarkStatus_Success() { assertThat(topicDetail.id()).isEqualTo(topic.getId()); assertThat(topicDetail.isBookmarked()).isEqualTo(Boolean.TRUE); - + assertThat(topicDetail.isInAtlas()).isEqualTo(Boolean.FALSE); + assertThat(topicDetail.canUpdate()).isEqualTo(Boolean.TRUE); } @Test - @DisplayName("토픽 상세조회시, 로그인 유저가 아니라면 즐겨찾기 여부가 항상 False다.") - public void findWithoutBookmarkStatus_Success() { + @DisplayName("토픽 상세조회시, 로그인 유저가 아니라면 즐겨찾기 여부, 모아보기 여부, 수정 권한 여부가 항상 False다.") + void findWithoutBookmarkStatus_Success() { //given Topic topic = TopicFixture.createPublicAndAllMembersTopic(member); topicRepository.save(topic); @@ -314,11 +315,13 @@ public void findWithoutBookmarkStatus_Success() { assertThat(topicDetail.id()).isEqualTo(topic.getId()); assertThat(topicDetail.isBookmarked()).isEqualTo(Boolean.FALSE); + assertThat(topicDetail.isInAtlas()).isEqualTo(Boolean.FALSE); + assertThat(topicDetail.canUpdate()).isEqualTo(Boolean.FALSE); } @Test @DisplayName("여러 토픽 조회시, 즐겨 찾기 여부를 함께 반환한다.") - public void findDetailsWithBookmarkStatus_Success() { + void findDetailsWithBookmarkStatus_Success() { //given Topic topic1 = TopicFixture.createPublicAndAllMembersTopic(member); Topic topic2 = TopicFixture.createPublicAndAllMembersTopic(member); @@ -342,7 +345,7 @@ public void findDetailsWithBookmarkStatus_Success() { @Test @DisplayName("여러 토픽 조회시, 로그인 유저가 아니라면 즐겨 찾기 여부가 항상 False다.") - public void findDetailsWithoutBookmarkStatus_Success() { + void findDetailsWithoutBookmarkStatus_Success() { //given Topic topic1 = TopicFixture.createPublicAndAllMembersTopic(member); Topic topic2 = TopicFixture.createPublicAndAllMembersTopic(member); @@ -421,7 +424,7 @@ void findAllByOrderByUpdatedAtDesc_Success() { @Test @DisplayName("즐겨찾기가 많이 있는 토픽 순서대로 조회할 수 있다.") - public void findAllBestTopics_Success1() { + void findAllBestTopics_Success1() { //given Member otherMember = MemberFixture.create( "otherMember", @@ -456,7 +459,7 @@ public void findAllBestTopics_Success1() { @Test @DisplayName("즐겨찾기 순서대로 조회하더라도, private 토픽인 경우 조회할 수 없다.") - public void findAllBestTopics_Success2() { + void findAllBestTopics_Success2() { //given Member otherMember = MemberFixture.create( "otherMember", diff --git a/backend/src/test/java/com/mapbefine/mapbefine/topic/presentation/TopicControllerTest.java b/backend/src/test/java/com/mapbefine/mapbefine/topic/presentation/TopicControllerTest.java index 2e15e271..ba19dc2b 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/topic/presentation/TopicControllerTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/topic/presentation/TopicControllerTest.java @@ -177,6 +177,7 @@ void findById() throws Exception { Boolean.FALSE, 0, Boolean.FALSE, + Boolean.FALSE, LocalDateTime.now(), List.of( new PinResponse( From 43fe3695bbbe0e5e93d07b20bf04cdf694d3e43f Mon Sep 17 00:00:00 2001 From: Doy Date: Tue, 12 Sep 2023 18:16:06 +0900 Subject: [PATCH 12/34] =?UTF-8?q?refactor:=20Info,=20Debug=20=EB=A0=88?= =?UTF-8?q?=EB=B2=A8=20=EC=BB=A4=EC=8A=A4=ED=85=80=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EB=A7=8C=20=EC=B6=9C=EB=A0=A5=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20(#397)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/main/resources/logback-spring.xml | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/backend/src/main/resources/logback-spring.xml b/backend/src/main/resources/logback-spring.xml index 848d8f9d..4bbde1b2 100644 --- a/backend/src/main/resources/logback-spring.xml +++ b/backend/src/main/resources/logback-spring.xml @@ -4,8 +4,9 @@ - - + + @@ -21,16 +22,15 @@ - - + - - + + - + @@ -42,10 +42,13 @@ - + + + + From de374f8d5889f5734eccfd23d43f8e3ed4401fef Mon Sep 17 00:00:00 2001 From: kpeel5839 <89840550+kpeel5839@users.noreply.github.com> Date: Wed, 13 Sep 2023 15:10:24 +0900 Subject: [PATCH 13/34] =?UTF-8?q?feat=20:=20pin=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=EC=8B=9C=20image=20upload=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#401)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pin/application/PinCommandService.java | 18 ++++++++++++---- .../pin/presentation/PinController.java | 10 ++++++--- .../s3/application/S3ServiceImpl.java | 2 +- .../mapbefine/s3/domain/UploadFile.java | 6 +++--- .../mapbefine/pin/PinIntegrationTest.java | 21 ++++++++++++++----- .../application/PinCommandServiceTest.java | 21 +++++++++---------- .../pin/presentation/PinControllerTest.java | 13 +++++++++--- 7 files changed, 61 insertions(+), 30 deletions(-) diff --git a/backend/src/main/java/com/mapbefine/mapbefine/pin/application/PinCommandService.java b/backend/src/main/java/com/mapbefine/mapbefine/pin/application/PinCommandService.java index 2d563e4f..bb72b26e 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/pin/application/PinCommandService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/pin/application/PinCommandService.java @@ -25,10 +25,12 @@ import com.mapbefine.mapbefine.topic.domain.Topic; import com.mapbefine.mapbefine.topic.domain.TopicRepository; import com.mapbefine.mapbefine.topic.exception.TopicException.TopicBadRequestException; +import java.util.List; import java.util.NoSuchElementException; import java.util.Objects; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; @Transactional @Service @@ -59,7 +61,11 @@ public PinCommandService( this.s3Service = s3Service; } - public long save(AuthMember authMember, PinCreateRequest request) { + public long save( + AuthMember authMember, + List images, + PinCreateRequest request + ) { Topic topic = findTopic(request.topicId()); validatePinCreateOrUpdate(authMember, topic); @@ -71,6 +77,8 @@ public long save(AuthMember authMember, PinCreateRequest request) { topic, member ); + + images.forEach(image -> addImageToPin(image, pin)); pinRepository.save(pin); return pin.getId(); @@ -141,10 +149,12 @@ public void removeById(AuthMember authMember, Long pinId) { public void addImage(AuthMember authMember, PinImageCreateRequest request) { Pin pin = findPin(request.pinId()); validatePinCreateOrUpdate(authMember, pin.getTopic()); - String image = s3Service.upload(request.image()); + addImageToPin(request.image(), pin); + } - PinImage pinImage = PinImage.createPinImageAssociatedWithPin(image, pin); - pinImageRepository.save(pinImage); + private void addImageToPin(MultipartFile image, Pin pin) { + String imageUrl = s3Service.upload(image); + PinImage.createPinImageAssociatedWithPin(imageUrl, pin); } public void removeImageById(AuthMember authMember, Long pinImageId) { diff --git a/backend/src/main/java/com/mapbefine/mapbefine/pin/presentation/PinController.java b/backend/src/main/java/com/mapbefine/mapbefine/pin/presentation/PinController.java index 2334d1c9..083b46c3 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/pin/presentation/PinController.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/pin/presentation/PinController.java @@ -39,9 +39,13 @@ public PinController(PinCommandService pinCommandService, PinQueryService pinQue } @LoginRequired - @PostMapping - public ResponseEntity add(AuthMember member, @RequestBody PinCreateRequest request) { - long savedId = pinCommandService.save(member, request); + @PostMapping(consumes = {MediaType.MULTIPART_FORM_DATA_VALUE, MediaType.APPLICATION_JSON_VALUE}) + public ResponseEntity add( + AuthMember member, + @RequestPart List images, + @RequestPart PinCreateRequest request + ) { + long savedId = pinCommandService.save(member, images, request); return ResponseEntity.created(URI.create("/pins/" + savedId)) .build(); diff --git a/backend/src/main/java/com/mapbefine/mapbefine/s3/application/S3ServiceImpl.java b/backend/src/main/java/com/mapbefine/mapbefine/s3/application/S3ServiceImpl.java index 843dc18c..55f68f42 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/s3/application/S3ServiceImpl.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/s3/application/S3ServiceImpl.java @@ -23,7 +23,7 @@ public S3ServiceImpl(S3Client s3Client) { @Override public String upload(MultipartFile multipartFile) { try { - UploadFile uploadFile = UploadFile.of(multipartFile); + UploadFile uploadFile = UploadFile.from(multipartFile); s3Client.upload(uploadFile); return getUploadPath(uploadFile); } catch (IOException exception) { diff --git a/backend/src/main/java/com/mapbefine/mapbefine/s3/domain/UploadFile.java b/backend/src/main/java/com/mapbefine/mapbefine/s3/domain/UploadFile.java index 79a3b502..7166cf1f 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/s3/domain/UploadFile.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/s3/domain/UploadFile.java @@ -21,7 +21,7 @@ private UploadFile( this.bytes = bytes; } - public static UploadFile of( + public static UploadFile from( MultipartFile multipartFile ) throws IOException { ImageName imageName = ImageName.from(multipartFile.getOriginalFilename()); @@ -56,12 +56,12 @@ public long getSize() { } @Override - public byte[] getBytes() throws IOException { + public byte[] getBytes() { return bytes; } @Override - public InputStream getInputStream() throws IOException { + public InputStream getInputStream() { return new ByteArrayInputStream(bytes); } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/pin/PinIntegrationTest.java b/backend/src/test/java/com/mapbefine/mapbefine/pin/PinIntegrationTest.java index c1d6b653..f69a1e2d 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/pin/PinIntegrationTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/pin/PinIntegrationTest.java @@ -12,6 +12,7 @@ import com.mapbefine.mapbefine.member.domain.MemberRepository; import com.mapbefine.mapbefine.member.domain.Role; import com.mapbefine.mapbefine.pin.dto.request.PinCreateRequest; +import com.mapbefine.mapbefine.pin.dto.response.PinDetailResponse; import com.mapbefine.mapbefine.pin.dto.response.PinImageResponse; import com.mapbefine.mapbefine.pin.dto.response.PinResponse; import com.mapbefine.mapbefine.topic.TopicFixture; @@ -70,7 +71,7 @@ void saveTopicAndLocation() { topic.getId(), "pin", "description", - "기존에 없는 주소", + "address", "legalDongCode", 37, 126 @@ -79,7 +80,7 @@ void saveTopicAndLocation() { topic.getId(), "pine2", "description", - "기존에 없는 주소", + "address", "legalDongCode", 37.12345, 126.12345 @@ -98,10 +99,17 @@ void addIfExistDuplicateLocation_Success() { } private ExtractableResponse createPin(PinCreateRequest request) { - return RestAssured.given().log().all() + String imageFilePath = getClass().getClassLoader() + .getResource("test.png") + .getPath(); + + return RestAssured.given() + .log().all() .header(AUTHORIZATION, authHeader) - .contentType(MediaType.APPLICATION_JSON_VALUE) - .body(request) + .multiPart("images", new File(imageFilePath), MediaType.MULTIPART_FORM_DATA_VALUE) + .multiPart("images", new File(imageFilePath), MediaType.MULTIPART_FORM_DATA_VALUE) + .multiPart("images", new File(imageFilePath), MediaType.MULTIPART_FORM_DATA_VALUE) + .multiPart("request", request, MediaType.APPLICATION_JSON_VALUE) .when().post("/pins") .then().log().all() .extract(); @@ -152,6 +160,9 @@ void findDetail_Success() { // when ExtractableResponse response = findById(pinId); + PinDetailResponse as = response.as(PinDetailResponse.class); + System.out.println(as); + // then assertThat(response.jsonPath().getString("name")) .isEqualTo(createRequestNoDuplicateLocation.name()); diff --git a/backend/src/test/java/com/mapbefine/mapbefine/pin/application/PinCommandServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/pin/application/PinCommandServiceTest.java index bc806c28..7bd8ca55 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/pin/application/PinCommandServiceTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/pin/application/PinCommandServiceTest.java @@ -85,7 +85,7 @@ void setUp() { @DisplayName("핀을 저장하려는 위치(Location)가 존재하면 해당 위치에 핀을 저장한다.") void saveIfExistLocation_Success() { // given, when - long savedPinId = pinCommandService.save(authMember, createRequest); + long savedPinId = pinCommandService.save(authMember, List.of(BASE_IMAGE_FILE), createRequest); PinDetailResponse actual = pinQueryService.findDetailById(authMember, savedPinId); Pin pin = pinRepository.findById(savedPinId).get(); Location savedLocation = pin.getLocation(); @@ -115,7 +115,7 @@ void saveIfNotExistLocation_Success() { ); // when - long savedPinId = pinCommandService.save(authMember, createRequest); + long savedPinId = pinCommandService.save(authMember, List.of(BASE_IMAGE_FILE), createRequest); PinDetailResponse actual = pinQueryService.findDetailById(authMember, savedPinId); Pin pin = pinRepository.findById(savedPinId).get(); Location savedLocation = pin.getLocation(); @@ -136,15 +136,14 @@ void saveIfNotExistLocation_Success() { @Test @DisplayName("권한이 없는 토픽에 핀을 저장하면 예외를 발생시킨다.") void save_FailByForbidden() { - - assertThatThrownBy(() -> pinCommandService.save(new Guest(), createRequest)) + assertThatThrownBy(() -> pinCommandService.save(new Guest(), List.of(BASE_IMAGE_FILE), createRequest)) .isInstanceOf(PinForbiddenException.class); } @Test @DisplayName("권한이 없는 토픽에 핀을 수정하면 예외를 발생시킨다.") void update_FailByForbidden() { - long pinId = pinCommandService.save(authMember, createRequest); + long pinId = pinCommandService.save(authMember, List.of(BASE_IMAGE_FILE), createRequest); assertThatThrownBy(() -> pinCommandService.update( new Guest(), pinId, new PinUpdateRequest("name", "description")) @@ -156,7 +155,7 @@ void update_FailByForbidden() { @DisplayName("핀을 삭제하면 해당 핀을 soft delete 하고, 해당 핀의 이미지들도 soft delete 한다.") void removeById_Success() { // given - long pinId = pinCommandService.save(authMember, createRequest); + long pinId = pinCommandService.save(authMember, List.of(BASE_IMAGE_FILE), createRequest); Pin pin = pinRepository.findById(pinId).get(); PinImage.createPinImageAssociatedWithPin(BASE_IMAGE, pin); @@ -177,7 +176,7 @@ void removeById_Success() { @Test @DisplayName("권한이 없는 토픽의 핀을 삭제하면 예외를 발생시킨다.") void removeById_FailByForbidden() { - long pinId = pinCommandService.save(authMember, createRequest); + long pinId = pinCommandService.save(authMember, List.of(BASE_IMAGE_FILE), createRequest); assertThatThrownBy(() -> pinCommandService.removeById(new Guest(), pinId)) .isInstanceOf(PinForbiddenException.class); @@ -187,7 +186,7 @@ void removeById_FailByForbidden() { @DisplayName("핀 id를 전달받아 해당하는 핀에 핀 이미지를 추가한다.") void addImage_Success() { // given - long pinId = pinCommandService.save(authMember, createRequest); + long pinId = pinCommandService.save(authMember, List.of(BASE_IMAGE_FILE), createRequest); // when long pinImageId = savePinImageAndGetId(pinId); @@ -204,7 +203,7 @@ void addImage_Success() { @DisplayName("권한이 없는 토픽의 핀에 핀 이미지를 추가하면 예외를 발생시킨다.") void addImage_FailByForbidden() { // given - long pinId = pinCommandService.save(authMember, createRequest); + long pinId = pinCommandService.save(authMember, List.of(BASE_IMAGE_FILE), createRequest); // when, then assertThatThrownBy(() -> pinCommandService.addImage(new Guest(), new PinImageCreateRequest(pinId, @@ -216,7 +215,7 @@ void addImage_FailByForbidden() { @DisplayName("핀 이미지의 id를 전달받아 해당하는 핀 이미지를 soft delete 한다.") void removeImageById_Success() { // given - long pinId = pinCommandService.save(authMember, createRequest); + long pinId = pinCommandService.save(authMember, List.of(BASE_IMAGE_FILE), createRequest); long pinImageId = savePinImageAndGetId(pinId); // when @@ -241,7 +240,7 @@ private long savePinImageAndGetId(long pinId) { @Test @DisplayName("권한이 없는 토픽의 핀 이미지를 삭제하면 예외를 발생시킨다.") void removeImageById_FailByForbidden() { - long pinId = pinCommandService.save(authMember, createRequest); + long pinId = pinCommandService.save(authMember, List.of(BASE_IMAGE_FILE), createRequest); long pinImageId = savePinImageAndGetId(pinId); assertThatThrownBy(() -> pinCommandService.removeImageById(new Guest(), pinImageId)) diff --git a/backend/src/test/java/com/mapbefine/mapbefine/pin/presentation/PinControllerTest.java b/backend/src/test/java/com/mapbefine/mapbefine/pin/presentation/PinControllerTest.java index 01a1fc0c..88876fbd 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/pin/presentation/PinControllerTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/pin/presentation/PinControllerTest.java @@ -25,6 +25,8 @@ import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; class PinControllerTest extends RestDocsIntegration { @@ -40,7 +42,9 @@ class PinControllerTest extends RestDocsIntegration { @Test @DisplayName("핀 추가") void add() throws Exception { - given(pinCommandService.save(any(), any())).willReturn(1L); + given(pinCommandService.save(any(), any(), any())).willReturn(1L); + File mockFile = new File(getClass().getClassLoader().getResource("test.png").getPath()); + MultiValueMap param = new LinkedMultiValueMap<>(); PinCreateRequest pinCreateRequest = new PinCreateRequest( 1L, @@ -52,11 +56,14 @@ void add() throws Exception { 127 ); + param.add("images", List.of(mockFile)); + param.add("request", pinCreateRequest); + mockMvc.perform( MockMvcRequestBuilders.post("/pins") .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(pinCreateRequest)) + .contentType(MediaType.MULTIPART_FORM_DATA_VALUE) + .content(objectMapper.writeValueAsString(param)) ).andDo(restDocs.document()); } From 4722faa13ae909afb9f69c55bc7f665dd6e153af Mon Sep 17 00:00:00 2001 From: zun <50602742+cpot5620@users.noreply.github.com> Date: Fri, 15 Sep 2023 14:54:52 +0900 Subject: [PATCH 14/34] =?UTF-8?q?[BE]=20Feat/#378=20Admin=20API=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#405)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 전체 회원 조회 기능 구현 * feat: 회원 삭제(탈퇴) 기능 구현 * feat: 회원 삭제(탈퇴)시 Pin/Topic Soft-deleting 구현 * refactor: Admin DTO 분리 * feat: Member 상세 정보 조회 기능 구현 * feat: Topic 삭제 및 이미지 삭제 기능 구현 * feat: Pin 삭제 및 이미지 삭제 기능 구현 * feat: Admin API 구현 * refactor: Member 상태(차단, 탈퇴 등) 필드에 따른 로그인 로직 수정 * refactor: @SqlDelete 삭제 및 JPQL 대체 * feat: AdminInterceptor 구현 * test: Repository soft-deleting 테스트 구현 * test: AdminQueryService 테스트 구현 * test: AdminCommandService 테스트 구현 * test: AdminController Restdocs 테스트 구현 * test: AdminInterceptor Mocking * test: 통합 테스트 구현 * refactor: 오탈자 수정 * refactor: Auth 관련 예외 클래스 추가 * refactor: 불필요한 메서드 제거 * refactor: findMemberById 예외 수정 * test: GithubActions 실패 테스트 수정 * refactor: isAdmin() 메서드 추가 * refactor: 회원 삭제(탈퇴)시, 추가 정보(즐겨찾기 등) 삭제 --- backend/src/docs/asciidoc/admin.adoc | 29 ++ backend/src/docs/asciidoc/index.adoc | 1 + .../application/AdminCommandService.java | 127 ++++++++ .../admin/application/AdminQueryService.java | 61 ++++ .../admin/dto/AdminMemberDetailResponse.java | 48 +++ .../admin/dto/AdminMemberResponse.java | 27 ++ .../admin/presentation/AdminController.java | 78 +++++ .../atlas/domain/AtlasRepository.java | 1 + .../auth/application/AuthService.java | 12 + .../mapbefine/auth/domain/AuthMember.java | 3 + .../mapbefine/auth/domain/member/Admin.java | 6 + .../mapbefine/auth/domain/member/Guest.java | 5 + .../mapbefine/auth/domain/member/User.java | 6 + .../auth/exception/AuthErrorCode.java | 21 ++ .../auth/exception/AuthException.java | 23 ++ .../mapbefine/common/config/AuthConfig.java | 9 +- .../interceptor/AdminAuthInterceptor.java | 71 +++++ .../common/interceptor/AuthInterceptor.java | 13 +- .../application/MemberQueryService.java | 1 + .../mapbefine/member/domain/Member.java | 22 +- .../mapbefine/member/domain/MemberInfo.java | 23 +- .../member/domain/MemberRepository.java | 3 + .../mapbefine/member/domain/Status.java | 15 + .../member/exception/MemberErrorCode.java | 1 + .../member/exception/MemberException.java | 7 + .../oauth/application/OauthService.java | 12 + .../AuthCodeRequestUrlProviderComposite.java | 2 +- .../domain/OauthMemberClientComposite.java | 2 +- ...OathException.java => OauthException.java} | 2 +- .../domain/PermissionRepository.java | 1 + .../exception/PermissionErrorCode.java | 3 +- .../pin/application/PinQueryService.java | 2 +- .../pin/domain/PinImageRepository.java | 5 +- .../mapbefine/pin/domain/PinRepository.java | 6 +- .../topic/application/TopicQueryService.java | 4 +- .../mapbefine/topic/domain/Topic.java | 4 + .../mapbefine/topic/domain/TopicInfo.java | 9 + .../topic/domain/TopicRepository.java | 6 +- .../mapbefine/admin/AdminIntegrationTest.java | 249 ++++++++++++++++ .../application/AdminCommandServiceTest.java | 280 ++++++++++++++++++ .../application/AdminQueryServiceTest.java | 135 +++++++++ .../presentation/AdminControllerTest.java | 172 +++++++++++ .../mapbefine/member/MemberFixture.java | 8 + .../member/domain/MemberInfoTest.java | 14 +- .../mapbefine/member/domain/MemberTest.java | 5 +- .../pin/domain/PinImageRepositoryTest.java | 27 ++ .../pin/domain/PinRepositoryTest.java | 50 +++- .../topic/domain/TopicRepositoryTest.java | 36 ++- 48 files changed, 1600 insertions(+), 47 deletions(-) create mode 100644 backend/src/docs/asciidoc/admin.adoc create mode 100644 backend/src/main/java/com/mapbefine/mapbefine/admin/application/AdminCommandService.java create mode 100644 backend/src/main/java/com/mapbefine/mapbefine/admin/application/AdminQueryService.java create mode 100644 backend/src/main/java/com/mapbefine/mapbefine/admin/dto/AdminMemberDetailResponse.java create mode 100644 backend/src/main/java/com/mapbefine/mapbefine/admin/dto/AdminMemberResponse.java create mode 100644 backend/src/main/java/com/mapbefine/mapbefine/admin/presentation/AdminController.java create mode 100644 backend/src/main/java/com/mapbefine/mapbefine/auth/exception/AuthErrorCode.java create mode 100644 backend/src/main/java/com/mapbefine/mapbefine/auth/exception/AuthException.java create mode 100644 backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AdminAuthInterceptor.java create mode 100644 backend/src/main/java/com/mapbefine/mapbefine/member/domain/Status.java rename backend/src/main/java/com/mapbefine/mapbefine/oauth/exception/{OathException.java => OauthException.java} (94%) create mode 100644 backend/src/test/java/com/mapbefine/mapbefine/admin/AdminIntegrationTest.java create mode 100644 backend/src/test/java/com/mapbefine/mapbefine/admin/application/AdminCommandServiceTest.java create mode 100644 backend/src/test/java/com/mapbefine/mapbefine/admin/application/AdminQueryServiceTest.java create mode 100644 backend/src/test/java/com/mapbefine/mapbefine/admin/presentation/AdminControllerTest.java diff --git a/backend/src/docs/asciidoc/admin.adoc b/backend/src/docs/asciidoc/admin.adoc new file mode 100644 index 00000000..3d2137c4 --- /dev/null +++ b/backend/src/docs/asciidoc/admin.adoc @@ -0,0 +1,29 @@ +== 관리자 기능 + +=== 전체 회원 조회 + +operation::admin-controller-test/find-all-member-details[snippets='http-request,http-response'] + +=== 회원 상세 조회 + +operation::admin-controller-test/find-member[snippets='http-request,http-response'] + +=== 회원 차단(삭제) + +operation::admin-controller-test/delete-member[snippets='http-request,http-response'] + +=== 토픽 삭제 + +operation::admin-controller-test/delete-topic[snippets='http-request,http-response'] + +=== 토픽 이미지 삭제 + +operation::admin-controller-test/delete-topic-image[snippets='http-request,http-response'] + +=== 핀 삭제 + +operation::admin-controller-test/delete-pin[snippets='http-request,http-response'] + +=== 핀 이미지 삭제 + +operation::admin-controller-test/delete-pin-image[snippets='http-request,http-response'] \ No newline at end of file diff --git a/backend/src/docs/asciidoc/index.adoc b/backend/src/docs/asciidoc/index.adoc index c15272d2..a90d89df 100644 --- a/backend/src/docs/asciidoc/index.adoc +++ b/backend/src/docs/asciidoc/index.adoc @@ -15,3 +15,4 @@ include::member.adoc[] include::permission.adoc[] include::oauth.adoc[] include::bookmark.adoc[] +include::admin.adoc[] diff --git a/backend/src/main/java/com/mapbefine/mapbefine/admin/application/AdminCommandService.java b/backend/src/main/java/com/mapbefine/mapbefine/admin/application/AdminCommandService.java new file mode 100644 index 00000000..0dfaf518 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/admin/application/AdminCommandService.java @@ -0,0 +1,127 @@ +package com.mapbefine.mapbefine.admin.application; + +import static com.mapbefine.mapbefine.permission.exception.PermissionErrorCode.PERMISSION_FORBIDDEN_BY_NOT_ADMIN; +import static com.mapbefine.mapbefine.topic.exception.TopicErrorCode.TOPIC_NOT_FOUND; + +import com.mapbefine.mapbefine.atlas.domain.AtlasRepository; +import com.mapbefine.mapbefine.auth.domain.AuthMember; +import com.mapbefine.mapbefine.bookmark.domain.BookmarkRepository; +import com.mapbefine.mapbefine.member.domain.Member; +import com.mapbefine.mapbefine.member.domain.MemberRepository; +import com.mapbefine.mapbefine.member.domain.Role; +import com.mapbefine.mapbefine.member.domain.Status; +import com.mapbefine.mapbefine.permission.domain.PermissionRepository; +import com.mapbefine.mapbefine.permission.exception.PermissionException.PermissionForbiddenException; +import com.mapbefine.mapbefine.pin.domain.Pin; +import com.mapbefine.mapbefine.pin.domain.PinImageRepository; +import com.mapbefine.mapbefine.pin.domain.PinRepository; +import com.mapbefine.mapbefine.topic.domain.Topic; +import com.mapbefine.mapbefine.topic.domain.TopicRepository; +import com.mapbefine.mapbefine.topic.exception.TopicException; +import java.util.List; +import java.util.NoSuchElementException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +public class AdminCommandService { + + private final MemberRepository memberRepository; + private final TopicRepository topicRepository; + private final PinRepository pinRepository; + private final PinImageRepository pinImageRepository; + private final PermissionRepository permissionRepository; + private final AtlasRepository atlasRepository; + private final BookmarkRepository bookmarkRepository; + + public AdminCommandService( + MemberRepository memberRepository, + TopicRepository topicRepository, + PinRepository pinRepository, + PinImageRepository pinImageRepository, + PermissionRepository permissionRepository, + AtlasRepository atlasRepository, + BookmarkRepository bookmarkRepository + ) { + this.memberRepository = memberRepository; + this.topicRepository = topicRepository; + this.pinRepository = pinRepository; + this.pinImageRepository = pinImageRepository; + this.permissionRepository = permissionRepository; + this.atlasRepository = atlasRepository; + this.bookmarkRepository = bookmarkRepository; + } + + public void blockMember(AuthMember authMember, Long memberId) { + validateAdminPermission(authMember); + + Member member = findMemberById(memberId); + member.updateStatus(Status.BLOCKED); + + deleteAllRelatedMember(member); + } + + private void deleteAllRelatedMember(Member member) { + List pinIds = extractPinIdsByMember(member); + Long memberId = member.getId(); + + pinImageRepository.deleteAllByPinIds(pinIds); + topicRepository.deleteAllByMemberId(memberId); + pinRepository.deleteAllByMemberId(memberId); + permissionRepository.deleteAllByMemberId(memberId); + atlasRepository.deleteAllByMemberId(memberId); + bookmarkRepository.deleteAllByMemberId(memberId); + } + + private Member findMemberById(Long id) { + return memberRepository.findById(id) + .orElseThrow(() -> new NoSuchElementException("findMemberByAuthMember; member not found; id=" + id)); + } + + private void validateAdminPermission(AuthMember authMember) { + if (authMember.isRole(Role.ADMIN)) { + return; + } + + throw new PermissionForbiddenException(PERMISSION_FORBIDDEN_BY_NOT_ADMIN); + } + + private List extractPinIdsByMember(Member member) { + return member.getCreatedPins() + .stream() + .map(Pin::getId) + .toList(); + } + + public void deleteTopic(AuthMember authMember, Long topicId) { + validateAdminPermission(authMember); + + topicRepository.deleteById(topicId); + } + + public void deleteTopicImage(AuthMember authMember, Long topicId) { + validateAdminPermission(authMember); + + Topic topic = findTopicById(topicId); + topic.removeImage(); + } + + private Topic findTopicById(Long topicId) { + return topicRepository.findById(topicId) + .orElseThrow(() -> new TopicException.TopicNotFoundException(TOPIC_NOT_FOUND, List.of(topicId))); + } + + public void deletePin(AuthMember authMember, Long pinId) { + validateAdminPermission(authMember); + + pinRepository.deleteById(pinId); + } + + public void deletePinImage(AuthMember authMember, Long pinImageId) { + validateAdminPermission(authMember); + + pinImageRepository.deleteById(pinImageId); + } + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/admin/application/AdminQueryService.java b/backend/src/main/java/com/mapbefine/mapbefine/admin/application/AdminQueryService.java new file mode 100644 index 00000000..f7ca346a --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/admin/application/AdminQueryService.java @@ -0,0 +1,61 @@ +package com.mapbefine.mapbefine.admin.application; + +import com.mapbefine.mapbefine.admin.dto.AdminMemberDetailResponse; +import com.mapbefine.mapbefine.admin.dto.AdminMemberResponse; +import com.mapbefine.mapbefine.auth.domain.AuthMember; +import com.mapbefine.mapbefine.member.domain.Member; +import com.mapbefine.mapbefine.member.domain.MemberRepository; +import com.mapbefine.mapbefine.member.domain.Role; +import com.mapbefine.mapbefine.permission.exception.PermissionErrorCode; +import com.mapbefine.mapbefine.permission.exception.PermissionException.PermissionForbiddenException; +import com.mapbefine.mapbefine.pin.domain.Pin; +import com.mapbefine.mapbefine.topic.domain.Topic; +import java.util.List; +import java.util.NoSuchElementException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +public class AdminQueryService { + + private final MemberRepository memberRepository; + + public AdminQueryService(MemberRepository memberRepository) { + this.memberRepository = memberRepository; + } + + public List findAllMemberDetails(AuthMember authMember) { + validateAdminPermission(authMember); + + List members = memberRepository.findAllByMemberInfoRole(Role.USER); + + return members.stream() + .map(AdminMemberResponse::from) + .toList(); + } + + private Member findMemberById(Long id) { + return memberRepository.findById(id) + .orElseThrow(() -> new NoSuchElementException("findMemberByAuthMember; member not found; id=" + id)); + } + + private void validateAdminPermission(AuthMember authMember) { + if (authMember.isRole(Role.ADMIN)) { + return; + } + + throw new PermissionForbiddenException(PermissionErrorCode.PERMISSION_FORBIDDEN_BY_NOT_ADMIN); + } + + public AdminMemberDetailResponse findMemberDetail(AuthMember authMember, Long memberId) { + validateAdminPermission(authMember); + + Member findMember = findMemberById(memberId); + List topics = findMember.getCreatedTopics(); + List pins = findMember.getCreatedPins(); + + return AdminMemberDetailResponse.of(findMember, topics, pins); + } + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/admin/dto/AdminMemberDetailResponse.java b/backend/src/main/java/com/mapbefine/mapbefine/admin/dto/AdminMemberDetailResponse.java new file mode 100644 index 00000000..fbd5e04d --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/admin/dto/AdminMemberDetailResponse.java @@ -0,0 +1,48 @@ +package com.mapbefine.mapbefine.admin.dto; + +import com.mapbefine.mapbefine.member.domain.Member; +import com.mapbefine.mapbefine.member.domain.MemberInfo; +import com.mapbefine.mapbefine.pin.domain.Pin; +import com.mapbefine.mapbefine.pin.dto.response.PinResponse; +import com.mapbefine.mapbefine.topic.domain.Topic; +import com.mapbefine.mapbefine.topic.dto.response.TopicResponse; +import java.time.LocalDateTime; +import java.util.List; + +public record AdminMemberDetailResponse( + Long id, + String nickName, + String email, + String imageUrl, + List topics, + List pins, + LocalDateTime updatedAt +) { + + // TODO: 2023/09/12 topics, pins 모두 member를 통해 얻어올 수 있다. Service에서 꺼내서 넘겨줄 것인가 ? 아니면 DTO에서 꺼내올 것인가 ? + public static AdminMemberDetailResponse of( + Member member, + List topics, + List pins + ) { + MemberInfo memberInfo = member.getMemberInfo(); + List topicResponses = topics.stream() + .map(TopicResponse::fromGuestQuery) + .toList(); + List pinResponses = pins.stream() + .map(PinResponse::from) + .toList(); + + return new AdminMemberDetailResponse( + member.getId(), + memberInfo.getNickName(), + memberInfo.getEmail(), + memberInfo.getImageUrl(), + topicResponses, + pinResponses, + member.getUpdatedAt() + ); + } + + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/admin/dto/AdminMemberResponse.java b/backend/src/main/java/com/mapbefine/mapbefine/admin/dto/AdminMemberResponse.java new file mode 100644 index 00000000..f041f8f1 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/admin/dto/AdminMemberResponse.java @@ -0,0 +1,27 @@ +package com.mapbefine.mapbefine.admin.dto; + +import com.mapbefine.mapbefine.member.domain.Member; +import com.mapbefine.mapbefine.member.domain.MemberInfo; +import java.time.LocalDateTime; + +public record AdminMemberResponse( + Long id, + String nickName, + String email, + String imageUrl, + LocalDateTime updatedAt +) { + + public static AdminMemberResponse from(Member member) { + MemberInfo memberInfo = member.getMemberInfo(); + + return new AdminMemberResponse( + member.getId(), + memberInfo.getNickName(), + memberInfo.getEmail(), + memberInfo.getImageUrl(), + member.getUpdatedAt() + ); + } + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/admin/presentation/AdminController.java b/backend/src/main/java/com/mapbefine/mapbefine/admin/presentation/AdminController.java new file mode 100644 index 00000000..2a34fec1 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/admin/presentation/AdminController.java @@ -0,0 +1,78 @@ +package com.mapbefine.mapbefine.admin.presentation; + +import com.mapbefine.mapbefine.admin.application.AdminCommandService; +import com.mapbefine.mapbefine.admin.application.AdminQueryService; +import com.mapbefine.mapbefine.admin.dto.AdminMemberDetailResponse; +import com.mapbefine.mapbefine.admin.dto.AdminMemberResponse; +import com.mapbefine.mapbefine.auth.domain.AuthMember; +import java.util.List; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/admin") +public class AdminController { + + private final AdminQueryService adminQueryService; + private final AdminCommandService adminCommandService; + + + public AdminController(AdminQueryService adminQueryService, AdminCommandService adminCommandService) { + this.adminQueryService = adminQueryService; + this.adminCommandService = adminCommandService; + } + + @GetMapping("/members") + public ResponseEntity> findAllMembers(AuthMember authMember) { + List responses = adminQueryService.findAllMemberDetails(authMember); + + return ResponseEntity.ok(responses); + } + + @DeleteMapping("/members/{memberId}") + public ResponseEntity deleteMember(AuthMember authMember, @PathVariable Long memberId) { + adminCommandService.blockMember(authMember, memberId); + + return ResponseEntity.noContent().build(); + } + + @GetMapping("/members/{memberId}") + public ResponseEntity findMember(AuthMember authMember, @PathVariable Long memberId) { + AdminMemberDetailResponse response = adminQueryService.findMemberDetail(authMember, memberId); + + return ResponseEntity.ok(response); + } + + @DeleteMapping("/topics/{topicId}") + public ResponseEntity deleteTopic(AuthMember authMember, @PathVariable Long topicId) { + adminCommandService.deleteTopic(authMember, topicId); + + return ResponseEntity.noContent().build(); + } + + @DeleteMapping("/topics/{topicId}/images") + public ResponseEntity deleteTopicImage(AuthMember authMember, @PathVariable Long topicId) { + adminCommandService.deleteTopicImage(authMember, topicId); + + return ResponseEntity.noContent().build(); + } + + @DeleteMapping("/pins/{pinId}") + public ResponseEntity deletePin(AuthMember authMember, @PathVariable Long pinId) { + adminCommandService.deletePin(authMember, pinId); + + return ResponseEntity.noContent().build(); + } + + @DeleteMapping("/pins/images/{imageId}") + public ResponseEntity deletePinImage(AuthMember authMember, @PathVariable Long imageId) { + adminCommandService.deletePinImage(authMember, imageId); + + return ResponseEntity.noContent().build(); + } + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/atlas/domain/AtlasRepository.java b/backend/src/main/java/com/mapbefine/mapbefine/atlas/domain/AtlasRepository.java index 242d6992..bd37a9ba 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/atlas/domain/AtlasRepository.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/atlas/domain/AtlasRepository.java @@ -10,4 +10,5 @@ public interface AtlasRepository extends JpaRepository { void deleteByMemberIdAndTopicId(Long memberId, Long topicId); + void deleteAllByMemberId(Long memberId); } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/application/AuthService.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/application/AuthService.java index 538e1244..11b2e343 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/auth/application/AuthService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/application/AuthService.java @@ -8,6 +8,7 @@ import com.mapbefine.mapbefine.topic.domain.Topic; import java.util.List; import java.util.Objects; +import java.util.Optional; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -60,4 +61,15 @@ private List getCreatedTopics(Member member) { .toList(); } + public boolean isAdmin(Long memberId) { + if (Objects.isNull(memberId)) { + return false; + } + + Optional member = memberRepository.findById(memberId); + + return member.map(Member::isAdmin) + .orElse(false); + } + } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/AuthMember.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/AuthMember.java index c795b1ec..d27f7cc2 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/AuthMember.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/AuthMember.java @@ -1,5 +1,6 @@ package com.mapbefine.mapbefine.auth.domain; +import com.mapbefine.mapbefine.member.domain.Role; import com.mapbefine.mapbefine.topic.domain.Topic; import java.util.List; @@ -27,6 +28,8 @@ protected AuthMember( public abstract boolean canPinCreateOrUpdate(Topic topic); + public abstract boolean isRole(Role role); + public Long getMemberId() { return memberId; } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/Admin.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/Admin.java index 3d97522c..f6a54648 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/Admin.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/Admin.java @@ -1,6 +1,7 @@ package com.mapbefine.mapbefine.auth.domain.member; import com.mapbefine.mapbefine.auth.domain.AuthMember; +import com.mapbefine.mapbefine.member.domain.Role; import com.mapbefine.mapbefine.topic.domain.Topic; import java.util.Collections; @@ -34,4 +35,9 @@ public boolean canPinCreateOrUpdate(Topic topic) { return true; } + @Override + public boolean isRole(Role role) { + return Role.ADMIN == role; + } + } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/Guest.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/Guest.java index 6c8c0edc..dcd8dbeb 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/Guest.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/Guest.java @@ -1,6 +1,7 @@ package com.mapbefine.mapbefine.auth.domain.member; import com.mapbefine.mapbefine.auth.domain.AuthMember; +import com.mapbefine.mapbefine.member.domain.Role; import com.mapbefine.mapbefine.topic.domain.Topic; import com.mapbefine.mapbefine.topic.domain.TopicStatus; import java.util.Collections; @@ -36,4 +37,8 @@ public boolean canPinCreateOrUpdate(Topic topic) { return false; } + @Override + public boolean isRole(Role role) { + return Role.GUEST == role; + } } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/User.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/User.java index d2444d49..ed2ea7b8 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/User.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/User.java @@ -1,6 +1,7 @@ package com.mapbefine.mapbefine.auth.domain.member; import com.mapbefine.mapbefine.auth.domain.AuthMember; +import com.mapbefine.mapbefine.member.domain.Role; import com.mapbefine.mapbefine.topic.domain.Topic; import com.mapbefine.mapbefine.topic.domain.TopicStatus; import java.util.List; @@ -54,4 +55,9 @@ private boolean hasPermission(Long topicId) { return createdTopic.contains(topicId) || topicsWithPermission.contains(topicId); } + @Override + public boolean isRole(Role role) { + return Role.USER == role; + } + } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/exception/AuthErrorCode.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/exception/AuthErrorCode.java new file mode 100644 index 00000000..7cbf7c20 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/exception/AuthErrorCode.java @@ -0,0 +1,21 @@ +package com.mapbefine.mapbefine.auth.exception; + +import lombok.Getter; + +@Getter +public enum AuthErrorCode { + ILLEGAL_MEMBER_ID("03100", "로그인에 실패하였습니다."), + ILLEGAL_TOKEN("03101", "로그인에 실패하였습니다."), + FORBIDDEN_ADMIN_ACCESS("03102", "로그인에 실패하였습니다."), + BLOCKING_MEMBER_ACCESS("03103", "로그인에 실패하였습니다."), + ; + + private final String code; + private final String message; + + AuthErrorCode(String code, String message) { + this.code = code; + this.message = message; + } + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/exception/AuthException.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/exception/AuthException.java new file mode 100644 index 00000000..4f384f9e --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/exception/AuthException.java @@ -0,0 +1,23 @@ +package com.mapbefine.mapbefine.auth.exception; + +import com.mapbefine.mapbefine.common.exception.ErrorCode; +import com.mapbefine.mapbefine.common.exception.ForbiddenException; +import com.mapbefine.mapbefine.common.exception.UnauthorizedException; + +public class AuthException { + + public static class AuthUnauthorizedException extends UnauthorizedException { + + public AuthUnauthorizedException(AuthErrorCode errorCode) { + super(new ErrorCode<>(errorCode.getCode(), errorCode.getMessage())); + } + } + + public static class AuthForbiddenException extends ForbiddenException { + + public AuthForbiddenException(AuthErrorCode errorCode) { + super(new ErrorCode<>(errorCode.getCode(), errorCode.getMessage())); + } + } + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/common/config/AuthConfig.java b/backend/src/main/java/com/mapbefine/mapbefine/common/config/AuthConfig.java index 02cb0062..c972f561 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/common/config/AuthConfig.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/common/config/AuthConfig.java @@ -1,5 +1,6 @@ package com.mapbefine.mapbefine.common.config; +import com.mapbefine.mapbefine.common.interceptor.AdminAuthInterceptor; import com.mapbefine.mapbefine.common.interceptor.AuthInterceptor; import com.mapbefine.mapbefine.common.resolver.AuthArgumentResolver; import java.util.List; @@ -11,20 +12,26 @@ @Configuration public class AuthConfig implements WebMvcConfigurer { + private final AdminAuthInterceptor adminAuthInterceptor; private final AuthInterceptor authInterceptor; private final AuthArgumentResolver authArgumentResolver; public AuthConfig( + AdminAuthInterceptor adminAuthInterceptor, AuthInterceptor authInterceptor, AuthArgumentResolver authArgumentResolver ) { + this.adminAuthInterceptor = adminAuthInterceptor; this.authInterceptor = authInterceptor; this.authArgumentResolver = authArgumentResolver; } @Override public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(authInterceptor); + registry.addInterceptor(authInterceptor) + .excludePathPatterns("/admin/**"); + registry.addInterceptor(adminAuthInterceptor) + .addPathPatterns("/admin/**"); } @Override diff --git a/backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AdminAuthInterceptor.java b/backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AdminAuthInterceptor.java new file mode 100644 index 00000000..9f5c8424 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AdminAuthInterceptor.java @@ -0,0 +1,71 @@ +package com.mapbefine.mapbefine.common.interceptor; + +import com.mapbefine.mapbefine.auth.application.AuthService; +import com.mapbefine.mapbefine.auth.dto.AuthInfo; +import com.mapbefine.mapbefine.auth.exception.AuthErrorCode; +import com.mapbefine.mapbefine.auth.exception.AuthException; +import com.mapbefine.mapbefine.auth.infrastructure.AuthorizationExtractor; +import com.mapbefine.mapbefine.auth.infrastructure.JwtTokenProvider; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.util.Objects; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; + +@Component +public class AdminAuthInterceptor implements HandlerInterceptor { + + private final AuthorizationExtractor authorizationExtractor; + private final AuthService authService; + private final JwtTokenProvider jwtTokenProvider; + + public AdminAuthInterceptor( + AuthorizationExtractor authorizationExtractor, + AuthService authService, + JwtTokenProvider jwtTokenProvider + ) { + this.authorizationExtractor = authorizationExtractor; + this.authService = authService; + this.jwtTokenProvider = jwtTokenProvider; + } + + @Override + public boolean preHandle( + HttpServletRequest request, + HttpServletResponse response, + Object handler + ) throws Exception { + if (!(handler instanceof HandlerMethod)) { + return true; + } + + Long memberId = extractMemberIdFromToken(request); + + validateAdmin(memberId); + request.setAttribute("memberId", memberId); + + return true; + } + + private Long extractMemberIdFromToken(HttpServletRequest request) { + AuthInfo authInfo = authorizationExtractor.extract(request); + if (Objects.isNull(authInfo)) { + return null; + } + String accessToken = authInfo.accessToken(); + if (jwtTokenProvider.validateToken(accessToken)) { + return Long.parseLong(jwtTokenProvider.getPayload(accessToken)); + } + throw new AuthException.AuthUnauthorizedException(AuthErrorCode.ILLEGAL_TOKEN); + } + + private void validateAdmin(Long memberId) { + if (authService.isAdmin(memberId)) { + return; + } + + throw new AuthException.AuthForbiddenException(AuthErrorCode.FORBIDDEN_ADMIN_ACCESS); + } + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AuthInterceptor.java b/backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AuthInterceptor.java index 32a52d38..01c98c73 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AuthInterceptor.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AuthInterceptor.java @@ -3,10 +3,11 @@ import com.mapbefine.mapbefine.auth.application.AuthService; import com.mapbefine.mapbefine.auth.domain.AuthMember; import com.mapbefine.mapbefine.auth.dto.AuthInfo; +import com.mapbefine.mapbefine.auth.exception.AuthErrorCode; +import com.mapbefine.mapbefine.auth.exception.AuthException; +import com.mapbefine.mapbefine.auth.exception.AuthException.AuthUnauthorizedException; import com.mapbefine.mapbefine.auth.infrastructure.AuthorizationExtractor; import com.mapbefine.mapbefine.auth.infrastructure.JwtTokenProvider; -import com.mapbefine.mapbefine.common.exception.ErrorCode; -import com.mapbefine.mapbefine.common.exception.UnauthorizedException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.util.Arrays; @@ -18,10 +19,6 @@ @Component public class AuthInterceptor implements HandlerInterceptor { - private static final String UNAUTHORIZED_ERROR_MESSAGE = "로그인에 실패하였습니다."; - private static final ErrorCode ILLEGAL_MEMBER_ID = new ErrorCode("03100", UNAUTHORIZED_ERROR_MESSAGE); - private static final ErrorCode ILLEGAL_TOKEN = new ErrorCode("03101", UNAUTHORIZED_ERROR_MESSAGE); - private final AuthorizationExtractor authorizationExtractor; private final AuthService authService; private final JwtTokenProvider jwtTokenProvider; @@ -65,7 +62,7 @@ private void validateMember(Long memberId) { return; } - throw new UnauthorizedException(ILLEGAL_MEMBER_ID); + throw new AuthUnauthorizedException(AuthErrorCode.ILLEGAL_MEMBER_ID); } private boolean isAuthMemberNotRequired(HandlerMethod handlerMethod) { @@ -86,7 +83,7 @@ private Long extractMemberIdFromToken(HttpServletRequest request) { } String accessToken = authInfo.accessToken(); if (!jwtTokenProvider.validateToken(accessToken)) { - throw new UnauthorizedException(ILLEGAL_TOKEN); + throw new AuthException.AuthUnauthorizedException(AuthErrorCode.ILLEGAL_TOKEN); } return Long.parseLong(jwtTokenProvider.getPayload(accessToken)); } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/member/application/MemberQueryService.java b/backend/src/main/java/com/mapbefine/mapbefine/member/application/MemberQueryService.java index c032cf36..df5296f9 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/member/application/MemberQueryService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/member/application/MemberQueryService.java @@ -37,6 +37,7 @@ private Member findMemberById(Long id) { .orElseThrow(() -> new MemberNotFoundException(MemberErrorCode.MEMBER_NOT_FOUND, id)); } + // TODO: 2023/09/13 차단된 or 탈퇴한 사용자 필터링 필요 public List findAll() { return memberRepository.findAll() .stream() diff --git a/backend/src/main/java/com/mapbefine/mapbefine/member/domain/Member.java b/backend/src/main/java/com/mapbefine/mapbefine/member/domain/Member.java index 71de6abf..1d64325e 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/member/domain/Member.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/member/domain/Member.java @@ -64,13 +64,15 @@ public static Member of( String email, String imageUrl, Role role, + Status status, OauthId oauthId ) { MemberInfo memberInfo = MemberInfo.of( nickName, email, imageUrl, - role + role, + status ); return new Member(memberInfo, oauthId); @@ -85,7 +87,7 @@ public static Member ofRandomNickname( ) { String nickName = createNickname(nickname); - return Member.of(nickName, email, imageUrl, role, oauthId); + return Member.of(nickName, email, imageUrl, role, Status.NORMAL, oauthId); } private static String createNickname(String nickname) { @@ -111,7 +113,8 @@ public void update( nickName, email, imageUrl, - memberInfo.getRole() + memberInfo.getRole(), + memberInfo.getStatus() ); } @@ -149,4 +152,17 @@ public List getTopicsWithPermissions() { .toList(); } + public boolean isNormalStatus() { + return memberInfo.getStatus() == Status.NORMAL; + } + + public void updateStatus(Status status) { + memberInfo = MemberInfo.of( + memberInfo.getNickName(), + memberInfo.getEmail(), + memberInfo.getImageUrl(), + memberInfo.getRole(), + status + ); + } } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/member/domain/MemberInfo.java b/backend/src/main/java/com/mapbefine/mapbefine/member/domain/MemberInfo.java index c15054c3..3cc5dfa4 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/member/domain/MemberInfo.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/member/domain/MemberInfo.java @@ -16,6 +16,7 @@ import java.util.Objects; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.ColumnDefault; @Embeddable @NoArgsConstructor(access = PROTECTED) @@ -38,33 +39,43 @@ public class MemberInfo { @Column(nullable = false) private Role role; + @Enumerated(EnumType.STRING) + @ColumnDefault(value = "NORMAL") + @Column(nullable = false) + private Status status; + private MemberInfo( String nickName, String email, Image imageUrl, - Role role + Role role, + Status status ) { this.nickName = nickName; this.email = email; this.imageUrl = imageUrl; this.role = role; + this.status = status; } public static MemberInfo of( String nickName, String email, String imageUrl, - Role role + Role role, + Status status ) { validateNickName(nickName); validateEmail(email); validateRole(role); + validateStatus(status); return new MemberInfo( nickName, email, Image.from(imageUrl), - role + role, + status ); } @@ -93,6 +104,12 @@ private static void validateRole(Role role) { } } + private static void validateStatus(Status status) { + if (Objects.isNull(status)) { + throw new IllegalArgumentException("validateStatus; member status is null;"); + } + } + public String getImageUrl() { return imageUrl.getImageUrl(); } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/member/domain/MemberRepository.java b/backend/src/main/java/com/mapbefine/mapbefine/member/domain/MemberRepository.java index 16b57c05..65125aa1 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/member/domain/MemberRepository.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/member/domain/MemberRepository.java @@ -1,5 +1,6 @@ package com.mapbefine.mapbefine.member.domain; +import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; @@ -13,4 +14,6 @@ public interface MemberRepository extends JpaRepository { Optional findByOauthId(OauthId oauthId); + List findAllByMemberInfoRole(Role role); + } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/member/domain/Status.java b/backend/src/main/java/com/mapbefine/mapbefine/member/domain/Status.java new file mode 100644 index 00000000..bf594c09 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/member/domain/Status.java @@ -0,0 +1,15 @@ +package com.mapbefine.mapbefine.member.domain; + +public enum Status { + NORMAL("STATUS_NORMAL", "정상 사용자"), + DELETE("STATAUS_DELETE", "탈퇴한 사용자"), + BLOCKED("STATUS_BLOCKED", "차단된 사용자"); + + private final String key; + private final String title; + + Status(String key, String title) { + this.key = key; + this.title = title; + } +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/member/exception/MemberErrorCode.java b/backend/src/main/java/com/mapbefine/mapbefine/member/exception/MemberErrorCode.java index c02d2ab1..b0c5d034 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/member/exception/MemberErrorCode.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/member/exception/MemberErrorCode.java @@ -9,6 +9,7 @@ public enum MemberErrorCode { ILLEGAL_NICKNAME_LENGTH("05001", "닉네임 길이는 최소 1 자에서 20자여야 합니다."), ILLEGAL_EMAIL_NULL("05002", "이메일은 필수로 입력해야합니다."), ILLEGAL_EMAIL_PATTERN("05003", "올바르지 않은 이메일 형식입니다."), + FORBIDDEN_MEMBER_STATUS("05100", "탈퇴 혹은 차단된 회원입니다."), MEMBER_NOT_FOUND("05400", "존재하지 않는 회원입니다."), ; diff --git a/backend/src/main/java/com/mapbefine/mapbefine/member/exception/MemberException.java b/backend/src/main/java/com/mapbefine/mapbefine/member/exception/MemberException.java index ec98e9d0..89a68948 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/member/exception/MemberException.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/member/exception/MemberException.java @@ -2,6 +2,7 @@ import com.mapbefine.mapbefine.common.exception.BadRequestException; import com.mapbefine.mapbefine.common.exception.ErrorCode; +import com.mapbefine.mapbefine.common.exception.ForbiddenException; import com.mapbefine.mapbefine.common.exception.NotFoundException; public class MemberException { @@ -18,5 +19,11 @@ public MemberNotFoundException(MemberErrorCode errorCode, Long id) { } } + public static class MemberForbiddenException extends ForbiddenException { + public MemberForbiddenException(MemberErrorCode errorCode, Long id) { + super(new ErrorCode<>(errorCode.getCode(), errorCode.getMessage(), id)); + } + } + } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/oauth/application/OauthService.java b/backend/src/main/java/com/mapbefine/mapbefine/oauth/application/OauthService.java index d75526a0..50681719 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/oauth/application/OauthService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/oauth/application/OauthService.java @@ -1,5 +1,7 @@ package com.mapbefine.mapbefine.oauth.application; +import com.mapbefine.mapbefine.auth.exception.AuthErrorCode; +import com.mapbefine.mapbefine.auth.exception.AuthException.AuthUnauthorizedException; import com.mapbefine.mapbefine.auth.infrastructure.JwtTokenProvider; import com.mapbefine.mapbefine.member.domain.Member; import com.mapbefine.mapbefine.member.domain.MemberRepository; @@ -39,6 +41,8 @@ public LoginInfoResponse login(OauthServerType oauthServerType, String code) { Member savedMember = memberRepository.findByOauthId(oauthMember.getOauthId()) .orElseGet(() -> register(oauthMember)); + validateMemberStatus(savedMember); + String accessToken = jwtTokenProvider.createToken(String.valueOf(savedMember.getId())); return LoginInfoResponse.of(accessToken, savedMember); @@ -49,4 +53,12 @@ private Member register(OauthMember oauthMember) { return memberRepository.save(oauthMember.toRegisterMember()); } + private void validateMemberStatus(Member member) { + if (member.isNormalStatus()) { + return; + } + + throw new AuthUnauthorizedException(AuthErrorCode.BLOCKING_MEMBER_ACCESS); + } + } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/oauth/domain/AuthCodeRequestUrlProviderComposite.java b/backend/src/main/java/com/mapbefine/mapbefine/oauth/domain/AuthCodeRequestUrlProviderComposite.java index a2a3c518..2ad2295d 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/oauth/domain/AuthCodeRequestUrlProviderComposite.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/oauth/domain/AuthCodeRequestUrlProviderComposite.java @@ -4,7 +4,7 @@ import static java.util.function.UnaryOperator.identity; import static java.util.stream.Collectors.toMap; -import com.mapbefine.mapbefine.oauth.exception.OathException.OauthNotFoundException; +import com.mapbefine.mapbefine.oauth.exception.OauthException.OauthNotFoundException; import java.util.Map; import java.util.Optional; import java.util.Set; diff --git a/backend/src/main/java/com/mapbefine/mapbefine/oauth/domain/OauthMemberClientComposite.java b/backend/src/main/java/com/mapbefine/mapbefine/oauth/domain/OauthMemberClientComposite.java index f5fb9648..eba1172d 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/oauth/domain/OauthMemberClientComposite.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/oauth/domain/OauthMemberClientComposite.java @@ -4,7 +4,7 @@ import static java.util.function.UnaryOperator.identity; import static java.util.stream.Collectors.toMap; -import com.mapbefine.mapbefine.oauth.exception.OathException.OauthNotFoundException; +import com.mapbefine.mapbefine.oauth.exception.OauthException.OauthNotFoundException; import java.util.Map; import java.util.Optional; import java.util.Set; diff --git a/backend/src/main/java/com/mapbefine/mapbefine/oauth/exception/OathException.java b/backend/src/main/java/com/mapbefine/mapbefine/oauth/exception/OauthException.java similarity index 94% rename from backend/src/main/java/com/mapbefine/mapbefine/oauth/exception/OathException.java rename to backend/src/main/java/com/mapbefine/mapbefine/oauth/exception/OauthException.java index a18d234c..3dc67e35 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/oauth/exception/OathException.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/oauth/exception/OauthException.java @@ -4,7 +4,7 @@ import com.mapbefine.mapbefine.common.exception.NotFoundException; import com.mapbefine.mapbefine.oauth.domain.OauthServerType; -public class OathException { +public class OauthException { public static class OauthNotFoundException extends NotFoundException { public OauthNotFoundException(OauthErrorCode errorCode, OauthServerType oauthServerType) { diff --git a/backend/src/main/java/com/mapbefine/mapbefine/permission/domain/PermissionRepository.java b/backend/src/main/java/com/mapbefine/mapbefine/permission/domain/PermissionRepository.java index da8dca9b..7d14c061 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/permission/domain/PermissionRepository.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/permission/domain/PermissionRepository.java @@ -9,4 +9,5 @@ public interface PermissionRepository extends JpaRepository { boolean existsByTopicIdAndMemberId(Long topicId, Long memberId); + void deleteAllByMemberId(Long memberId); } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/permission/exception/PermissionErrorCode.java b/backend/src/main/java/com/mapbefine/mapbefine/permission/exception/PermissionErrorCode.java index 5a0f6aa8..c3f3f699 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/permission/exception/PermissionErrorCode.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/permission/exception/PermissionErrorCode.java @@ -9,7 +9,8 @@ public enum PermissionErrorCode { ILLEGAL_PERMISSION_ID("07001", "유효하지 않은 권한 정보입니다."), FORBIDDEN_ADD_PERMISSION_GUEST("07300", "로그인하지 않은 사용자는 권한을 줄 수 없습니다."), FORBIDDEN_ADD_PERMISSION("07301", "지도를 생성한 사용자가 아니면 권한을 줄 수 없습니다."), - PERMISSION_NOT_FOUND("07400", "존재하지 않는 권한 정보입니다.") + PERMISSION_NOT_FOUND("07400", "존재하지 않는 권한 정보입니다."), + PERMISSION_FORBIDDEN_BY_NOT_ADMIN("07401", "어드민 계정만 접근 가능합니다."), ; private final String code; diff --git a/backend/src/main/java/com/mapbefine/mapbefine/pin/application/PinQueryService.java b/backend/src/main/java/com/mapbefine/mapbefine/pin/application/PinQueryService.java index 6956b55d..0e2aa2bf 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/pin/application/PinQueryService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/pin/application/PinQueryService.java @@ -52,7 +52,7 @@ private void validateReadAuth(AuthMember member, Topic topic) { } public List findAllPinsByMemberId(AuthMember authMember, Long memberId) { - return pinRepository.findByCreatorId(memberId) + return pinRepository.findAllByCreatorId(memberId) .stream() .filter(pin -> authMember.canRead(pin.getTopic())) .map(PinResponse::from) diff --git a/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinImageRepository.java b/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinImageRepository.java index 36f53800..6e25d0af 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinImageRepository.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinImageRepository.java @@ -18,6 +18,9 @@ public interface PinImageRepository extends JpaRepository { @Query("update PinImage p set p.isDeleted = true where p.id = :id") void deleteById(@Param("id") Long id); + @Modifying(clearAutomatically = true) + @Query("update PinImage p set p.isDeleted = true where p.pin.id in :pinIds") + void deleteAllByPinIds(@Param("pinIds") List pinIds); + List findAllByPinId(Long pinId); - } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinRepository.java b/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinRepository.java index 11a2d95d..60d29dc9 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinRepository.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinRepository.java @@ -18,9 +18,13 @@ public interface PinRepository extends JpaRepository { @Query("update Pin p set p.isDeleted = true where p.id = :pinId") void deleteById(@Param("pinId") Long pinId); + @Modifying(clearAutomatically = true) + @Query("update Pin p set p.isDeleted = true where p.creator.id = :memberId") + void deleteAllByMemberId(@Param("memberId") Long memberId); + List findAllByTopicId(Long topicId); - List findByCreatorId(Long creatorId); + List findAllByCreatorId(Long creatorId); List findAllByOrderByUpdatedAtDesc(); } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/application/TopicQueryService.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/application/TopicQueryService.java index aa19d281..de0efdda 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/topic/application/TopicQueryService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/application/TopicQueryService.java @@ -186,7 +186,7 @@ private void validateReadableTopics(AuthMember member, List topics) { public List findAllTopicsByMemberId(AuthMember authMember, Long memberId) { if (Objects.isNull(authMember.getMemberId())) { - return topicRepository.findByCreatorId(memberId) + return topicRepository.findAllByCreatorId(memberId) .stream() .filter(authMember::canRead) .map(TopicResponse::fromGuestQuery) @@ -198,7 +198,7 @@ public List findAllTopicsByMemberId(AuthMember authMember, Long m List topicsInAtlas = findTopicsInAtlas(member); List topicsInBookMark = findBookMarkedTopics(member); - return topicRepository.findByCreatorId(memberId) + return topicRepository.findAllByCreatorId(memberId) .stream() .filter(authMember::canRead) .map(topic -> TopicResponse.from( diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/Topic.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/Topic.java index a41c807e..d5128022 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/Topic.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/Topic.java @@ -114,4 +114,8 @@ public int countBookmarks() { return bookmarks.size(); } + public void removeImage() { + this.topicInfo = topicInfo.removeImage(); + } + } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicInfo.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicInfo.java index 3f5ffd59..dff5da7e 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicInfo.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicInfo.java @@ -88,4 +88,13 @@ private static Image createImage(String imageUrl) { public String getImageUrl() { return image.getImageUrl(); } + + public TopicInfo removeImage() { + return new TopicInfo( + this.name, + this.description, + DEFAULT_IMAGE + ); + } + } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicRepository.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicRepository.java index 1ef9fb6f..e6eb95db 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicRepository.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicRepository.java @@ -16,8 +16,12 @@ public interface TopicRepository extends JpaRepository { @Query("update Topic t set t.isDeleted = true where t.id = :topicId") void deleteById(@Param("topicId") Long topicId); + @Modifying(clearAutomatically = true) + @Query("update Topic t set t.isDeleted = true where t.creator.id = :memberId") + void deleteAllByMemberId(@Param("memberId") Long memberId); + boolean existsById(Long id); - List findByCreatorId(Long creatorId); + List findAllByCreatorId(Long creatorId); } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/admin/AdminIntegrationTest.java b/backend/src/test/java/com/mapbefine/mapbefine/admin/AdminIntegrationTest.java new file mode 100644 index 00000000..c7d7c26f --- /dev/null +++ b/backend/src/test/java/com/mapbefine/mapbefine/admin/AdminIntegrationTest.java @@ -0,0 +1,249 @@ +package com.mapbefine.mapbefine.admin; + +import static io.restassured.RestAssured.given; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; + +import com.mapbefine.mapbefine.admin.dto.AdminMemberDetailResponse; +import com.mapbefine.mapbefine.admin.dto.AdminMemberResponse; +import com.mapbefine.mapbefine.common.IntegrationTest; +import com.mapbefine.mapbefine.location.LocationFixture; +import com.mapbefine.mapbefine.location.domain.Location; +import com.mapbefine.mapbefine.location.domain.LocationRepository; +import com.mapbefine.mapbefine.member.MemberFixture; +import com.mapbefine.mapbefine.member.domain.Member; +import com.mapbefine.mapbefine.member.domain.MemberRepository; +import com.mapbefine.mapbefine.member.domain.Role; +import com.mapbefine.mapbefine.pin.PinFixture; +import com.mapbefine.mapbefine.pin.PinImageFixture; +import com.mapbefine.mapbefine.pin.domain.Pin; +import com.mapbefine.mapbefine.pin.domain.PinImage; +import com.mapbefine.mapbefine.pin.domain.PinImageRepository; +import com.mapbefine.mapbefine.pin.domain.PinRepository; +import com.mapbefine.mapbefine.topic.TopicFixture; +import com.mapbefine.mapbefine.topic.domain.Topic; +import com.mapbefine.mapbefine.topic.domain.TopicRepository; +import io.restassured.common.mapper.TypeRef; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; + +class AdminIntegrationTest extends IntegrationTest { + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private TopicRepository topicRepository; + + @Autowired + private PinRepository pinRepository; + + @Autowired + private LocationRepository locationRepository; + + @Autowired + private PinImageRepository pinImageRepository; + + private Location location; + private Member admin; + private Member member; + private Topic topic; + private Pin pin; + private PinImage pinImage; + + @BeforeEach + void setup() { + admin = memberRepository.save(MemberFixture.create("Admin", "admin@naver.com", Role.ADMIN)); + member = memberRepository.save(MemberFixture.create("member", "member@gmail.com", Role.USER)); + topic = topicRepository.save(TopicFixture.createByName("topic", member)); + location = locationRepository.save(LocationFixture.create()); + pin = pinRepository.save(PinFixture.create(location, topic, member)); + pinImage = pinImageRepository.save(PinImageFixture.create(pin)); + } + + @Test + @DisplayName("어드민일 경우, 회원을 전체 조회할 수 있다.") + void findAllMembers_Success() { + //given + Member member1 = MemberFixture.create("member1", "member1@gmail.com", Role.USER); + Member member2 = MemberFixture.create("member2", "member2@gmail.com", Role.USER); + memberRepository.save(member1); + memberRepository.save(member2); + + //when + List response = given().log().all() + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(admin)) + .accept(MediaType.APPLICATION_JSON_VALUE) + .when().get("/admin/members") + .then().log().all() + .extract() + .as(new TypeRef<>() { + }); + + //then + List expected = List.of( + AdminMemberResponse.from(member), + AdminMemberResponse.from(member1), + AdminMemberResponse.from(member2) + ); + + assertThat(response).usingRecursiveComparison() + .ignoringFields("updatedAt") + .isEqualTo(expected); + } + + @Test + @DisplayName("어드민이 아닐 경우, 회원을 전체 조회할 수 없다.") + void findAllMembers_Fail() { + given().log().all() + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(member)) + .accept(MediaType.APPLICATION_JSON_VALUE) + .when().get("/admin/members/" + member.getId()) + .then().log().all() + .statusCode(HttpStatus.FORBIDDEN.value()); + } + + @Test + @DisplayName("어드민일 경우, 특정 회원의 상세 정보를 조회할 수 있다.") + void findMemberDetail_Success() { + //given when + AdminMemberDetailResponse response = given().log().all() + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(admin)) + .accept(MediaType.APPLICATION_JSON_VALUE) + .when().get("/admin/members/" + member.getId()) + .then().log().all() + .extract() + .as(new TypeRef<>() { + }); + + //then + + AdminMemberDetailResponse expected = AdminMemberDetailResponse.of( + member, + member.getCreatedTopics(), + member.getCreatedPins() + ); + + assertThat(response).usingRecursiveComparison() + .ignoringFields("updatedAt") + .ignoringFields("topics.updatedAt") + .isEqualTo(expected); + } + + @Test + @DisplayName("어드민이 아닐 경우, 특정 회원의 상세 정보를 조회할 수 없다.") + void findMemberDetail_Fail() { + given().log().all() + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(member)) + .accept(MediaType.APPLICATION_JSON_VALUE) + .when().get("/admin/members/" + member.getId()) + .then().log().all() + .statusCode(HttpStatus.FORBIDDEN.value()); + } + + @Test + @DisplayName("어드민일 경우, 특정 회원을 삭제(차단)할 수 있다.") + void deleteMember_Success() { + given().log().all() + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(admin)) + .when().delete("/admin/members/" + member.getId()) + .then().log().all() + .statusCode(HttpStatus.NO_CONTENT.value()); + } + + @Test + @DisplayName("어드민이 아닐 경우, 특정 회원을 삭제(차단)할 수 없다.") + void deleteMember_Fail() { + given().log().all() + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(member)) + .when().delete("/admin/members/" + member.getId()) + .then().log().all() + .statusCode(HttpStatus.FORBIDDEN.value()); + } + + @Test + @DisplayName("어드민일 경우, 특정 토픽을 삭제할 수 있다.") + void deleteTopic_Success() { + given().log().all() + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(admin)) + .when().delete("/admin/topics/" + topic.getId()) + .then().log().all() + .statusCode(HttpStatus.NO_CONTENT.value()); + } + + @Test + @DisplayName("어드민이 아닐 경우, 특정 토픽을 삭제할 수 없다.") + void deleteTopic_Fail() { + given().log().all() + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(member)) + .when().delete("/admin/topics/" + topic.getId()) + .then().log().all() + .statusCode(HttpStatus.FORBIDDEN.value()); + } + + @Test + @DisplayName("어드민일 경우, 특정 토픽 이미지를 삭제할 수 있다.") + void deleteTopicImage_Success() { + given().log().all() + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(admin)) + .when().delete("/admin/topics/" + topic.getId() + "/images") + .then().log().all() + .statusCode(HttpStatus.NO_CONTENT.value()); + } + + @Test + @DisplayName("어드민이 아닐 경우, 특정 토픽 이미지를 삭제할 수 없다.") + void deleteTopicImage_Fail() { + given().log().all() + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(member)) + .when().delete("/admin/topics/" + topic.getId() + "/images") + .then().log().all() + .statusCode(HttpStatus.FORBIDDEN.value()); + } + + @Test + @DisplayName("어드민일 경우, 특정 핀을 삭제할 수 있다.") + void deletePin_Success() { + given().log().all() + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(admin)) + .when().delete("/admin/pins/" + pin.getId()) + .then().log().all() + .statusCode(HttpStatus.NO_CONTENT.value()); + } + + @Test + @DisplayName("어드민이 아닐 경우, 특정 핀을 삭제할 수 없다.") + void deletePin_Fail() { + given().log().all() + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(member)) + .when().delete("/admin/pins/" + pin.getId()) + .then().log().all() + .statusCode(HttpStatus.FORBIDDEN.value()); + } + + @Test + @DisplayName("어드민일 경우, 특정 핀 이미지를 삭제할 수 있다.") + void deletePinImage_Success() { + given().log().all() + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(admin)) + .when().delete("/admin/pins/images/" + pinImage.getId()) + .then().log().all() + .statusCode(HttpStatus.NO_CONTENT.value()); + } + + @Test + @DisplayName("어드민이 아닐 경우, 특정 핀 이미지를 삭제할 수 없다.") + void deletePinImage_Fail() { + given().log().all() + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(member)) + .when().delete("/admin/pins/images/" + pinImage.getId()) + .then().log().all() + .statusCode(HttpStatus.FORBIDDEN.value()); + } + +} diff --git a/backend/src/test/java/com/mapbefine/mapbefine/admin/application/AdminCommandServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/admin/application/AdminCommandServiceTest.java new file mode 100644 index 00000000..12e49f77 --- /dev/null +++ b/backend/src/test/java/com/mapbefine/mapbefine/admin/application/AdminCommandServiceTest.java @@ -0,0 +1,280 @@ +package com.mapbefine.mapbefine.admin.application; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.mapbefine.mapbefine.atlas.domain.Atlas; +import com.mapbefine.mapbefine.atlas.domain.AtlasRepository; +import com.mapbefine.mapbefine.auth.domain.AuthMember; +import com.mapbefine.mapbefine.bookmark.domain.Bookmark; +import com.mapbefine.mapbefine.bookmark.domain.BookmarkRepository; +import com.mapbefine.mapbefine.common.annotation.ServiceTest; +import com.mapbefine.mapbefine.location.LocationFixture; +import com.mapbefine.mapbefine.location.domain.Location; +import com.mapbefine.mapbefine.location.domain.LocationRepository; +import com.mapbefine.mapbefine.member.MemberFixture; +import com.mapbefine.mapbefine.member.domain.Member; +import com.mapbefine.mapbefine.member.domain.MemberRepository; +import com.mapbefine.mapbefine.member.domain.Role; +import com.mapbefine.mapbefine.member.domain.Status; +import com.mapbefine.mapbefine.permission.domain.Permission; +import com.mapbefine.mapbefine.permission.domain.PermissionRepository; +import com.mapbefine.mapbefine.permission.exception.PermissionException.PermissionForbiddenException; +import com.mapbefine.mapbefine.pin.PinFixture; +import com.mapbefine.mapbefine.pin.PinImageFixture; +import com.mapbefine.mapbefine.pin.domain.Pin; +import com.mapbefine.mapbefine.pin.domain.PinImage; +import com.mapbefine.mapbefine.pin.domain.PinImageRepository; +import com.mapbefine.mapbefine.pin.domain.PinRepository; +import com.mapbefine.mapbefine.topic.TopicFixture; +import com.mapbefine.mapbefine.topic.domain.Topic; +import com.mapbefine.mapbefine.topic.domain.TopicInfo; +import com.mapbefine.mapbefine.topic.domain.TopicRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +@ServiceTest +class AdminCommandServiceTest { + + @Autowired + private AdminCommandService adminCommandService; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private TopicRepository topicRepository; + + @Autowired + private PinRepository pinRepository; + + @Autowired + private LocationRepository locationRepository; + + @Autowired + private PinImageRepository pinImageRepository; + + @Autowired + private AtlasRepository atlasRepository; + + @Autowired + private PermissionRepository permissionRepository; + + @Autowired + private BookmarkRepository bookmarkRepository; + + private Location location; + private Member admin; + private Member member; + private Topic topic; + private Pin pin; + private PinImage pinImage; + + @BeforeEach + void setup() { + admin = memberRepository.save(MemberFixture.create("Admin", "admin@naver.com", Role.ADMIN)); + member = memberRepository.save(MemberFixture.create("member", "member@gmail.com", Role.USER)); + topic = topicRepository.save(TopicFixture.createByName("topic", member)); + location = locationRepository.save(LocationFixture.create()); + pin = pinRepository.save(PinFixture.create(location, topic, member)); + pinImage = pinImageRepository.save(PinImageFixture.create(pin)); + } + + @DisplayName("Member를 차단(탈퇴시킬)할 경우, Member가 생성한 토픽, 핀, 핀 이미지가 soft-deleting 된다.") + @Test + void blockMember_Success() { + //given + AuthMember adminAuthMember = MemberFixture.createUser(admin); + Bookmark bookmark = Bookmark.createWithAssociatedTopicAndMember(topic, member); + Atlas atlas = Atlas.createWithAssociatedMember(topic, member); + Permission permission = Permission.createPermissionAssociatedWithTopicAndMember(topic, member); + + bookmarkRepository.save(bookmark); + atlasRepository.save(atlas); + permissionRepository.save(permission); + + assertAll(() -> { + assertThat(member.getMemberInfo().getStatus()).isEqualTo(Status.NORMAL); + assertThat(topic.isDeleted()).isFalse(); + assertThat(pin.isDeleted()).isFalse(); + assertThat(pinImage.isDeleted()).isFalse(); + assertThat(bookmarkRepository.existsByMemberIdAndTopicId(member.getId(), topic.getId())).isTrue(); + assertThat(atlasRepository.existsByMemberIdAndTopicId(member.getId(), topic.getId())).isTrue(); + assertThat(permissionRepository.existsByTopicIdAndMemberId(topic.getId(), member.getId())).isTrue(); + }); + + //when + adminCommandService.blockMember(adminAuthMember, member.getId()); + + //then + Topic deletedTopic = topicRepository.findById(topic.getId()).get(); + Pin deletedPin = pinRepository.findById(pin.getId()).get(); + PinImage deletedPinImage = pinImageRepository.findById(pinImage.getId()).get(); + + assertAll(() -> { + assertThat(member.getMemberInfo().getStatus()).isEqualTo(Status.BLOCKED); + assertThat(deletedTopic.isDeleted()).isTrue(); + assertThat(deletedPin.isDeleted()).isTrue(); + assertThat(deletedPinImage.isDeleted()).isTrue(); + assertThat(bookmarkRepository.existsByMemberIdAndTopicId(member.getId(), topic.getId())).isFalse(); + assertThat(atlasRepository.existsByMemberIdAndTopicId(member.getId(), topic.getId())).isFalse(); + assertThat(permissionRepository.existsByTopicIdAndMemberId(topic.getId(), member.getId())).isFalse(); + }); + } + + @DisplayName("Admin이 아닐 경우, Member를 차단(탈퇴시킬)할 수 없다.") + @Test + void blockMember_Fail() { + //given + AuthMember userAuthMember = MemberFixture.createUser(member); + Member otherMember = MemberFixture.create("otherMember", "otherMember@email.com", Role.USER); + memberRepository.save(otherMember); + + assertAll(() -> { + assertThat(member.getMemberInfo().getStatus()).isEqualTo(Status.NORMAL); + assertThat(topic.isDeleted()).isFalse(); + assertThat(pin.isDeleted()).isFalse(); + assertThat(pinImage.isDeleted()).isFalse(); + }); + + //when then + Long otherMemberId = otherMember.getId(); + assertThatThrownBy(() -> adminCommandService.blockMember(userAuthMember, otherMemberId)) + .isInstanceOf(PermissionForbiddenException.class); + } + + @DisplayName("Admin은 토픽을 삭제시킬 수 있다.") + @Test + void deleteTopic_Success() { + //given + AuthMember adminAuthMember = MemberFixture.createUser(admin); + + assertThat(topic.isDeleted()).isFalse(); + + //when + adminCommandService.deleteTopic(adminAuthMember, topic.getId()); + + //then + Topic deletedTopic = topicRepository.findById(topic.getId()).get(); + + assertThat(deletedTopic.isDeleted()).isTrue(); + } + + @DisplayName("Admin이 아닐 경우, 토픽을 삭제시킬 수 없다.") + @Test + void deleteTopic_Fail() { + //given + AuthMember userAuthMember = MemberFixture.createUser(member); + + assertThat(topic.isDeleted()).isFalse(); + + //when then + Long topicId = topic.getId(); + assertThatThrownBy(() -> adminCommandService.deleteTopic(userAuthMember, topicId)) + .isInstanceOf(PermissionForbiddenException.class); + } + + @DisplayName("Admin은 토픽 이미지를 삭제할 수 있다.") + @Test + void deleteTopicImage_Success() { + //given + AuthMember adminAuthMember = MemberFixture.createUser(admin); + TopicInfo topicInfo = topic.getTopicInfo(); + + topic.updateTopicInfo(topicInfo.getName(), topicInfo.getDescription(), "https://imageUrl.png"); + + assertThat(topic.getTopicInfo().getImageUrl()).isEqualTo("https://imageUrl.png"); + + //when + adminCommandService.deleteTopicImage(adminAuthMember, topic.getId()); + + //then + Topic imageDeletedTopic = topicRepository.findById(topic.getId()).get(); + + assertThat(imageDeletedTopic.getTopicInfo().getImageUrl()) + .isEqualTo("https://map-befine-official.github.io/favicon.png"); + } + + @DisplayName("Admin이 아닐 경우, 이미지를 삭제할 수 없다.") + @Test + void deleteTopicImage_Fail() { + //given + AuthMember userAuthMember = MemberFixture.createUser(member); + TopicInfo topicInfo = topic.getTopicInfo(); + + topic.updateTopicInfo(topicInfo.getName(), topicInfo.getDescription(), "https://imageUrl.png"); + + assertThat(topic.getTopicInfo().getImageUrl()).isEqualTo("https://imageUrl.png"); + + //when then + Long topicId = topic.getId(); + assertThatThrownBy(() -> adminCommandService.deleteTopicImage(userAuthMember, topicId)) + .isInstanceOf(PermissionForbiddenException.class); + } + + @DisplayName("Admin은 핀을 삭제할 수 있다.") + @Test + void deletePin_Success() { + //given + AuthMember adminAuthMember = MemberFixture.createUser(admin); + + assertThat(pin.isDeleted()).isFalse(); + + //when + adminCommandService.deletePin(adminAuthMember, pin.getId()); + + //then + Pin deletedPin = pinRepository.findById(pin.getId()).get(); + + assertThat(deletedPin.isDeleted()).isTrue(); + } + + @DisplayName("Admin이 아닐 경우, 핀을 삭제할 수 없다.") + @Test + void deletePin_Fail() { + //given + AuthMember userAuthMember = MemberFixture.createUser(member); + + assertThat(pin.isDeleted()).isFalse(); + + //when then + Long pinId = pin.getId(); + assertThatThrownBy(() -> adminCommandService.deletePin(userAuthMember, pinId)) + .isInstanceOf(PermissionForbiddenException.class); + } + + @DisplayName("Admin인 경우, 핀 이미지를 삭제할 수 있다.") + @Test + void deletePinImage_Success() { + //given + AuthMember adminAuthMember = MemberFixture.createUser(admin); + + assertThat(pinImage.isDeleted()).isFalse(); + + //when + adminCommandService.deletePinImage(adminAuthMember, pinImage.getId()); + + //then + PinImage deletedPinImage = pinImageRepository.findById(pinImage.getId()).get(); + + assertThat(deletedPinImage.isDeleted()).isTrue(); + } + + @DisplayName("Admin이 아닐 경우, 핀 이미지를 삭제할 수 없다.") + @Test + void deletePinImage_Fail() { + //given + AuthMember userAuthMember = MemberFixture.createUser(member); + + assertThat(pinImage.isDeleted()).isFalse(); + + //when then + Long pinImageId = pinImage.getId(); + assertThatThrownBy(() -> adminCommandService.deletePinImage(userAuthMember, pinImageId)) + .isInstanceOf(PermissionForbiddenException.class); + } + +} \ No newline at end of file diff --git a/backend/src/test/java/com/mapbefine/mapbefine/admin/application/AdminQueryServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/admin/application/AdminQueryServiceTest.java new file mode 100644 index 00000000..33599110 --- /dev/null +++ b/backend/src/test/java/com/mapbefine/mapbefine/admin/application/AdminQueryServiceTest.java @@ -0,0 +1,135 @@ +package com.mapbefine.mapbefine.admin.application; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.mapbefine.mapbefine.admin.dto.AdminMemberDetailResponse; +import com.mapbefine.mapbefine.admin.dto.AdminMemberResponse; +import com.mapbefine.mapbefine.auth.domain.AuthMember; +import com.mapbefine.mapbefine.common.annotation.ServiceTest; +import com.mapbefine.mapbefine.location.LocationFixture; +import com.mapbefine.mapbefine.location.domain.Location; +import com.mapbefine.mapbefine.location.domain.LocationRepository; +import com.mapbefine.mapbefine.member.MemberFixture; +import com.mapbefine.mapbefine.member.domain.Member; +import com.mapbefine.mapbefine.member.domain.MemberRepository; +import com.mapbefine.mapbefine.member.domain.Role; +import com.mapbefine.mapbefine.permission.exception.PermissionException.PermissionForbiddenException; +import com.mapbefine.mapbefine.pin.PinFixture; +import com.mapbefine.mapbefine.pin.domain.Pin; +import com.mapbefine.mapbefine.pin.domain.PinRepository; +import com.mapbefine.mapbefine.topic.TopicFixture; +import com.mapbefine.mapbefine.topic.domain.Topic; +import com.mapbefine.mapbefine.topic.domain.TopicRepository; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +@ServiceTest +class AdminQueryServiceTest { + + @Autowired + private AdminQueryService adminQueryService; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private TopicRepository topicRepository; + + @Autowired + private PinRepository pinRepository; + + @Autowired + private LocationRepository locationRepository; + + private Location location; + private Member admin; + private Member member; + private Topic topic; + private Pin pin; + + @BeforeEach + void setup() { + admin = memberRepository.save(MemberFixture.create("Admin", "admin@naver.com", Role.ADMIN)); + member = memberRepository.save(MemberFixture.create("member", "member@gmail.com", Role.USER)); + topic = topicRepository.save(TopicFixture.createByName("topic", member)); + location = locationRepository.save(LocationFixture.create()); + pin = pinRepository.save(PinFixture.create(location, topic, member)); + } + + @Test + @DisplayName("사용자와 관련된 세부(민감한 정보 포함) 정보를 모두 조회할 수 있다.") + void findMemberDetail_Success() { + //given + AuthMember adminAuthMember = MemberFixture.createUser(admin); + + //when + AdminMemberDetailResponse response = adminQueryService.findMemberDetail(adminAuthMember, member.getId()); + + //then + assertThat(response).usingRecursiveComparison() + .ignoringFields("updatedAt") + .isEqualTo(AdminMemberDetailResponse.of(member, List.of(topic), List.of(pin))); + } + + @Test + @DisplayName("Admin이 아닌 경우, 사용자와 관련된 세부(민감한 정보 포함) 정보를 모두 조회할 수 없다.") + void findMemberDetail_Fail() { + //given + AuthMember userAuthMember = MemberFixture.createUser(member); + + //when //then + Long memberId = member.getId(); + assertThatThrownBy(() -> adminQueryService.findMemberDetail(userAuthMember, memberId)) + .isInstanceOf(PermissionForbiddenException.class); + } + + @Test + @DisplayName("모든 사용자와 관련된 세부 정보를 모두 조회할 수 있다.") + void findAllMemberDetails_Success() { + //given + AuthMember adminAuthMember = MemberFixture.createUser(admin); + + ArrayList members = new ArrayList<>(); + members.add(member); + for (int i = 0; i < 10; i++) { + Member saveMember = MemberFixture.create("member" + i, "member" + i + "@email.com", Role.USER); + members.add(memberRepository.save(saveMember)); + } + + //when + List responses = adminQueryService.findAllMemberDetails(adminAuthMember); + //then + List expected = members.stream() + .map(AdminMemberResponse::from) + .toList(); + + assertThat(responses).usingRecursiveComparison() + .ignoringFields("updatedAt") + .ignoringCollectionOrderInFields() + .isEqualTo(expected); + } + + @Test + @DisplayName("Admin이 아닌 경우 모든 사용자와 관련된 세부 정보를 모두 조회할 수 없다.") + void findAllMemberDetails_Fail() { + //given + AuthMember memberAuthMember = MemberFixture.createUser(member); + + ArrayList members = new ArrayList<>(); + members.add(member); + for (int i = 0; i < 10; i++) { + Member saveMember = MemberFixture.create("member" + i, "member" + i + "@email.com", Role.USER); + members.add(memberRepository.save(saveMember)); + } + + //when //then + assertThatThrownBy(() -> adminQueryService.findAllMemberDetails(memberAuthMember)) + .isInstanceOf(PermissionForbiddenException.class); + } + +} \ No newline at end of file diff --git a/backend/src/test/java/com/mapbefine/mapbefine/admin/presentation/AdminControllerTest.java b/backend/src/test/java/com/mapbefine/mapbefine/admin/presentation/AdminControllerTest.java new file mode 100644 index 00000000..edceb5d3 --- /dev/null +++ b/backend/src/test/java/com/mapbefine/mapbefine/admin/presentation/AdminControllerTest.java @@ -0,0 +1,172 @@ +package com.mapbefine.mapbefine.admin.presentation; + +import static org.apache.http.HttpHeaders.AUTHORIZATION; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doNothing; + +import com.mapbefine.mapbefine.admin.application.AdminCommandService; +import com.mapbefine.mapbefine.admin.application.AdminQueryService; +import com.mapbefine.mapbefine.admin.dto.AdminMemberDetailResponse; +import com.mapbefine.mapbefine.admin.dto.AdminMemberResponse; +import com.mapbefine.mapbefine.common.RestDocsIntegration; +import com.mapbefine.mapbefine.common.interceptor.AdminAuthInterceptor; +import com.mapbefine.mapbefine.pin.dto.response.PinResponse; +import com.mapbefine.mapbefine.topic.dto.response.TopicResponse; +import java.time.LocalDateTime; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +class AdminControllerTest extends RestDocsIntegration { + + private static final List TOPIC_RESPONSES = List.of(new TopicResponse( + 1L, + "준팍의 또 토픽", + "https://map-befine-official.github.io/favicon.png", + "준팍", + 3, + Boolean.FALSE, + 5, + Boolean.FALSE, + LocalDateTime.now() + ), new TopicResponse( + 2L, + "준팍의 두번째 토픽", + "https://map-befine-official.github.io/favicon.png", + "준팍", + 5, + Boolean.FALSE, + 3, + Boolean.FALSE, + LocalDateTime.now() + )); + private static final List PIN_RESPONSES = List.of( + new PinResponse( + 1L, + "매튜의 산스장", + "지번 주소", + "매튜가 사랑하는 산스장", + "매튜", + 37, + 127 + ), new PinResponse( + 2L, + "매튜의 안갈집", + "지번 주소", + "매튜가 두번은 안 갈 집", + "매튜", + 37, + 127 + ) + ); + + @MockBean + private AdminCommandService adminCommandService; + + @MockBean + private AdminQueryService adminQueryService; + + @MockBean + private AdminAuthInterceptor adminAuthInterceptor; + + @BeforeEach + void setAll() throws Exception { + given(adminAuthInterceptor.preHandle(any(), any(), any())).willReturn(true); + } + + @DisplayName("멤버 목록 조회") + @Test + void findAllMemberDetails() throws Exception { + List response = List.of( + new AdminMemberResponse(1L, "쥬니", "zuny@naver.com", "https://zuny.png", LocalDateTime.now()), + new AdminMemberResponse(2L, "세인", "semin@naver.com", "https://semin.png", LocalDateTime.now()) + ); + + given(adminQueryService.findAllMemberDetails(any())).willReturn(response); + + mockMvc.perform( + MockMvcRequestBuilders.get("/admin/members") + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) + ).andDo(restDocs.document()); + } + + @DisplayName("멤버 상세 조회") + @Test + void findMember() throws Exception { + AdminMemberDetailResponse response = new AdminMemberDetailResponse( + 1L, + "쥬니", + "zuny@naver.com", + "https://image.png", + TOPIC_RESPONSES, + PIN_RESPONSES, + LocalDateTime.now() + ); + + given(adminQueryService.findMemberDetail(any(), any())).willReturn(response); + + mockMvc.perform( + MockMvcRequestBuilders.get("/admin/members/1") + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) + ).andDo(restDocs.document()); + } + + @DisplayName("멤버 차단(블랙리스트)") + @Test + void deleteMember() throws Exception { + doNothing().when(adminCommandService).blockMember(any(), any()); + + mockMvc.perform( + MockMvcRequestBuilders.delete("/admin/members/1") + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) + ).andDo(restDocs.document()); + } + + @DisplayName("토픽 삭제") + @Test + void deleteTopic() throws Exception { + doNothing().when(adminCommandService).deleteTopic(any(), any()); + + mockMvc.perform( + MockMvcRequestBuilders.delete("/admin/topics/1") + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) + ).andDo(restDocs.document()); + } + + @DisplayName("토픽 이미지 삭제") + @Test + void deleteTopicImage() throws Exception { + doNothing().when(adminCommandService).deleteTopicImage(any(), any()); + + mockMvc.perform( + MockMvcRequestBuilders.delete("/admin/topics/1/images") + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) + ).andDo(restDocs.document()); + } + + @DisplayName("핀 삭제") + @Test + void deletePin() throws Exception { + doNothing().when(adminCommandService).deletePin(any(), any()); + + mockMvc.perform( + MockMvcRequestBuilders.delete("/admin/pins/1") + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) + ).andDo(restDocs.document()); + } + + @DisplayName("토픽 이미지 삭제") + @Test + void deletePinImage() throws Exception { + doNothing().when(adminCommandService).deletePinImage(any(), any()); + + mockMvc.perform( + MockMvcRequestBuilders.delete("/admin/pins/images/1") + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) + ).andDo(restDocs.document()); + } +} \ No newline at end of file diff --git a/backend/src/test/java/com/mapbefine/mapbefine/member/MemberFixture.java b/backend/src/test/java/com/mapbefine/mapbefine/member/MemberFixture.java index d6943a99..6fd7bc14 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/member/MemberFixture.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/member/MemberFixture.java @@ -1,10 +1,12 @@ package com.mapbefine.mapbefine.member; import com.mapbefine.mapbefine.auth.domain.AuthMember; +import com.mapbefine.mapbefine.auth.domain.member.Admin; import com.mapbefine.mapbefine.auth.domain.member.User; import com.mapbefine.mapbefine.member.domain.Member; import com.mapbefine.mapbefine.member.domain.OauthId; import com.mapbefine.mapbefine.member.domain.Role; +import com.mapbefine.mapbefine.member.domain.Status; import com.mapbefine.mapbefine.oauth.domain.OauthServerType; import com.mapbefine.mapbefine.topic.domain.Topic; @@ -16,6 +18,7 @@ public static Member create(String name, String email, Role role) { email, "https://map-befine-official.github.io/favicon.png", role, + Status.NORMAL, new OauthId(1L, OauthServerType.KAKAO) ); } @@ -26,11 +29,16 @@ public static Member createWithOauthId(String name, String email, Role role, Oau email, "https://map-befine-official.github.io/favicon.png", role, + Status.NORMAL, oauthId) ; } public static AuthMember createUser(Member member) { + if (member.isAdmin()) { + return new Admin(member.getId()); + } + return new User( member.getId(), member.getCreatedTopics().stream().map(Topic::getId).toList(), diff --git a/backend/src/test/java/com/mapbefine/mapbefine/member/domain/MemberInfoTest.java b/backend/src/test/java/com/mapbefine/mapbefine/member/domain/MemberInfoTest.java index 7c4d4a20..1ed6e737 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/member/domain/MemberInfoTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/member/domain/MemberInfoTest.java @@ -22,6 +22,7 @@ class Validate { private final String VALID_EMAIL = "member@naver.com"; private final String VALID_IMAGE_URL = "https://map-befine-official.github.io/favicon.png"; private final Role VALID_ROLE = Role.ADMIN; + private final Status VALID_STATUS = Status.NORMAL; @Test @DisplayName("정확한 값을 입력하면 객체가 생성된다") @@ -31,7 +32,8 @@ void success() { VALID_NICK_NAME, VALID_EMAIL, VALID_IMAGE_URL, - VALID_ROLE + VALID_ROLE, + VALID_STATUS ); //then @@ -52,7 +54,8 @@ void whenNameIsInvalid_thenFail(String invalidNickName) { invalidNickName, VALID_EMAIL, VALID_IMAGE_URL, - VALID_ROLE + VALID_ROLE, + VALID_STATUS )).isInstanceOf(MemberBadRequestException.class); } @@ -67,7 +70,8 @@ void whenEmailIsInvalid_thenFail(String invalidEmail) { VALID_NICK_NAME, invalidEmail, VALID_IMAGE_URL, - VALID_ROLE + VALID_ROLE, + VALID_STATUS )).isInstanceOf(MemberBadRequestException.class); } @@ -81,7 +85,8 @@ void whenImageUrlIsInvalid_thenFail() { VALID_NICK_NAME, VALID_EMAIL, invalidImageUrl, - VALID_ROLE + VALID_ROLE, + VALID_STATUS )).isInstanceOf(ImageBadRequestException.class); } @@ -94,6 +99,7 @@ void whenRoleIsInvalid_thenFail() { VALID_EMAIL, VALID_IMAGE_URL, null + ,VALID_STATUS )).isInstanceOf(IllegalArgumentException.class); } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/member/domain/MemberTest.java b/backend/src/test/java/com/mapbefine/mapbefine/member/domain/MemberTest.java index 0ee9b697..7fcb4b10 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/member/domain/MemberTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/member/domain/MemberTest.java @@ -16,6 +16,7 @@ void createMember_success() { String email = "member@naver.com"; String imageUrl = "https://map-befine-official.github.io/favicon.png"; Role role = Role.ADMIN; + Status status = Status.NORMAL; // when Member member = Member.of( @@ -23,7 +24,9 @@ void createMember_success() { email, imageUrl, role, - new OauthId(1L, OauthServerType.KAKAO)); + status, + new OauthId(1L, OauthServerType.KAKAO) + ); // then assertThat(member).isNotNull(); diff --git a/backend/src/test/java/com/mapbefine/mapbefine/pin/domain/PinImageRepositoryTest.java b/backend/src/test/java/com/mapbefine/mapbefine/pin/domain/PinImageRepositoryTest.java index ad36bb0d..a8604271 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/pin/domain/PinImageRepositoryTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/pin/domain/PinImageRepositoryTest.java @@ -14,6 +14,7 @@ import com.mapbefine.mapbefine.topic.TopicFixture; import com.mapbefine.mapbefine.topic.domain.Topic; import com.mapbefine.mapbefine.topic.domain.TopicRepository; +import java.util.List; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -87,4 +88,30 @@ void deleteAllByPinId_Success() { .extractingResultOf("isDeleted") .containsOnly(true); } + + @Test + @DisplayName("여러 핀을 Id로 삭제하면, 핀 이미지들도 soft-deleting 된다.") + void deleteAllByMemberId_Success() { + //given + Pin otherPin = pinRepository.save(PinFixture.create(pin.getLocation(), topic, member)); + + PinImage pinImage1 = PinImageFixture.create(pin); + PinImage pinImage2 = PinImageFixture.create(otherPin); + pinImageRepository.save(pinImage1); + pinImageRepository.save(pinImage2); + + //when + assertThat(pinImage1.isDeleted()).isFalse(); + assertThat(pinImage2.isDeleted()).isFalse(); + pinImageRepository.deleteAllByPinIds(List.of(pin.getId(),otherPin.getId())); + + //then + assertThat(pinImageRepository.findAllByPinId(pin.getId())) + .extractingResultOf("isDeleted") + .containsOnly(true); + assertThat(pinImageRepository.findAllByPinId(otherPin.getId())) + .extractingResultOf("isDeleted") + .containsOnly(true); + } + } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/pin/domain/PinRepositoryTest.java b/backend/src/test/java/com/mapbefine/mapbefine/pin/domain/PinRepositoryTest.java index 508d938e..24bf7cde 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/pin/domain/PinRepositoryTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/pin/domain/PinRepositoryTest.java @@ -3,7 +3,6 @@ import static org.assertj.core.api.Assertions.assertThat; import com.mapbefine.mapbefine.location.LocationFixture; -import com.mapbefine.mapbefine.location.domain.Coordinate; import com.mapbefine.mapbefine.location.domain.Location; import com.mapbefine.mapbefine.location.domain.LocationRepository; import com.mapbefine.mapbefine.member.MemberFixture; @@ -24,10 +23,6 @@ @DataJpaTest class PinRepositoryTest { - private static final Coordinate DEFAULT_COORDINATE = Coordinate.of( - 37.5152933, - 127.1029866 - ); @Autowired private TopicRepository topicRepository; @@ -88,4 +83,49 @@ void deleteAllByTopicId_Success() { .containsOnly(true); } + + @Test + @DisplayName("Member ID로 모든 핀을 soft-delete 할 수 있다.") + void deleteAllByMemberId_Success() { + //given + for (int i = 0; i < 10; i++) { + pinRepository.save(PinFixture.create(location, topic, member)); + } + + //when + assertThat(member.getCreatedPins()).hasSize(10) + .extractingResultOf("isDeleted") + .containsOnly(false); + pinRepository.deleteAllByMemberId(member.getId()); + + //then + List deletedPins = pinRepository.findAllByCreatorId(member.getId()); + assertThat(deletedPins).extractingResultOf("isDeleted") + .containsOnly(true); + } + + @Test + @DisplayName("다른 토픽에 존재하는 핀들이여도, Member ID로 모든 핀을 soft-delete 할 수 있다.") + void deleteAllByMemberIdInOtherTopics_Success() { + //given + Topic otherTopic = TopicFixture.createByName("otherTopic", member); + topicRepository.save(otherTopic); + + for (int i = 0; i < 10; i++) { + pinRepository.save(PinFixture.create(location, topic, member)); + pinRepository.save(PinFixture.create(location, otherTopic, member)); + } + + //when + assertThat(member.getCreatedPins()).hasSize(20) + .extractingResultOf("isDeleted") + .containsOnly(false); + pinRepository.deleteAllByMemberId(member.getId()); + + //then + List deletedPins = pinRepository.findAllByCreatorId(member.getId()); + assertThat(deletedPins).extractingResultOf("isDeleted") + .containsOnly(true); + } + } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/topic/domain/TopicRepositoryTest.java b/backend/src/test/java/com/mapbefine/mapbefine/topic/domain/TopicRepositoryTest.java index 3280df30..2a783b70 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/topic/domain/TopicRepositoryTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/topic/domain/TopicRepositoryTest.java @@ -6,6 +6,8 @@ import com.mapbefine.mapbefine.member.domain.Member; import com.mapbefine.mapbefine.member.domain.MemberRepository; import com.mapbefine.mapbefine.member.domain.Role; +import com.mapbefine.mapbefine.topic.TopicFixture; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -25,24 +27,14 @@ class TopicRepositoryTest { @BeforeEach void setUp() { - member = MemberFixture.create("member", "member@naver.com", Role.USER); - memberRepository.save(member); + member = memberRepository.save(MemberFixture.create("member", "member@naver.com", Role.USER)); } @Test @DisplayName("토픽을 삭제하면, soft-deleting 된다.") void deleteById_Success() { //given - Topic topic = Topic.createTopicAssociatedWithCreator( - "토픽", - "토픽설명", - "https://example.com/image.jpg", - Publicity.PUBLIC, - PermissionType.ALL_MEMBERS, - member - ); - - topicRepository.save(topic); + Topic topic = topicRepository.save(TopicFixture.createByName("Topic", member)); assertThat(topic.isDeleted()).isFalse(); @@ -54,4 +46,24 @@ void deleteById_Success() { assertThat(deletedTopic.isDeleted()).isTrue(); } + @Test + @DisplayName("Member Id로 모든 토픽을 삭제하면, soft-deleting 된다.") + void deleteAllByMemberId_Success() { + //given + for (int i = 0; i < 10; i++) { + topicRepository.save(TopicFixture.createByName("topic" + i, member)); + } + assertThat(member.getCreatedTopics()).hasSize(10) + .extractingResultOf("isDeleted") + .containsOnly(false); + + //when + topicRepository.deleteAllByMemberId(member.getId()); + + //then + List deletedTopics = topicRepository.findAllByCreatorId(member.getId()); + assertThat(deletedTopics).hasSize(10) + .extractingResultOf("isDeleted") + .containsOnly(true); + } } From a623d8f45c76294df00381b2c2f71b8483d46808 Mon Sep 17 00:00:00 2001 From: zun <50602742+cpot5620@users.noreply.github.com> Date: Fri, 15 Sep 2023 14:59:17 +0900 Subject: [PATCH 15/34] =?UTF-8?q?Revert=20"[BE]=20Feat/#378=20Admin=20API?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84=20(#405)"=20(#414)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 4722faa13ae909afb9f69c55bc7f665dd6e153af. --- backend/src/docs/asciidoc/admin.adoc | 29 -- backend/src/docs/asciidoc/index.adoc | 1 - .../application/AdminCommandService.java | 127 -------- .../admin/application/AdminQueryService.java | 61 ---- .../admin/dto/AdminMemberDetailResponse.java | 48 --- .../admin/dto/AdminMemberResponse.java | 27 -- .../admin/presentation/AdminController.java | 78 ----- .../atlas/domain/AtlasRepository.java | 1 - .../auth/application/AuthService.java | 12 - .../mapbefine/auth/domain/AuthMember.java | 3 - .../mapbefine/auth/domain/member/Admin.java | 6 - .../mapbefine/auth/domain/member/Guest.java | 5 - .../mapbefine/auth/domain/member/User.java | 6 - .../auth/exception/AuthErrorCode.java | 21 -- .../auth/exception/AuthException.java | 23 -- .../mapbefine/common/config/AuthConfig.java | 9 +- .../interceptor/AdminAuthInterceptor.java | 71 ----- .../common/interceptor/AuthInterceptor.java | 13 +- .../application/MemberQueryService.java | 1 - .../mapbefine/member/domain/Member.java | 22 +- .../mapbefine/member/domain/MemberInfo.java | 23 +- .../member/domain/MemberRepository.java | 3 - .../mapbefine/member/domain/Status.java | 15 - .../member/exception/MemberErrorCode.java | 1 - .../member/exception/MemberException.java | 7 - .../oauth/application/OauthService.java | 12 - .../AuthCodeRequestUrlProviderComposite.java | 2 +- .../domain/OauthMemberClientComposite.java | 2 +- ...OauthException.java => OathException.java} | 2 +- .../domain/PermissionRepository.java | 1 - .../exception/PermissionErrorCode.java | 3 +- .../pin/application/PinQueryService.java | 2 +- .../pin/domain/PinImageRepository.java | 5 +- .../mapbefine/pin/domain/PinRepository.java | 6 +- .../topic/application/TopicQueryService.java | 4 +- .../mapbefine/topic/domain/Topic.java | 4 - .../mapbefine/topic/domain/TopicInfo.java | 9 - .../topic/domain/TopicRepository.java | 6 +- .../mapbefine/admin/AdminIntegrationTest.java | 249 ---------------- .../application/AdminCommandServiceTest.java | 280 ------------------ .../application/AdminQueryServiceTest.java | 135 --------- .../presentation/AdminControllerTest.java | 172 ----------- .../mapbefine/member/MemberFixture.java | 8 - .../member/domain/MemberInfoTest.java | 14 +- .../mapbefine/member/domain/MemberTest.java | 5 +- .../pin/domain/PinImageRepositoryTest.java | 27 -- .../pin/domain/PinRepositoryTest.java | 50 +--- .../topic/domain/TopicRepositoryTest.java | 36 +-- 48 files changed, 47 insertions(+), 1600 deletions(-) delete mode 100644 backend/src/docs/asciidoc/admin.adoc delete mode 100644 backend/src/main/java/com/mapbefine/mapbefine/admin/application/AdminCommandService.java delete mode 100644 backend/src/main/java/com/mapbefine/mapbefine/admin/application/AdminQueryService.java delete mode 100644 backend/src/main/java/com/mapbefine/mapbefine/admin/dto/AdminMemberDetailResponse.java delete mode 100644 backend/src/main/java/com/mapbefine/mapbefine/admin/dto/AdminMemberResponse.java delete mode 100644 backend/src/main/java/com/mapbefine/mapbefine/admin/presentation/AdminController.java delete mode 100644 backend/src/main/java/com/mapbefine/mapbefine/auth/exception/AuthErrorCode.java delete mode 100644 backend/src/main/java/com/mapbefine/mapbefine/auth/exception/AuthException.java delete mode 100644 backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AdminAuthInterceptor.java delete mode 100644 backend/src/main/java/com/mapbefine/mapbefine/member/domain/Status.java rename backend/src/main/java/com/mapbefine/mapbefine/oauth/exception/{OauthException.java => OathException.java} (94%) delete mode 100644 backend/src/test/java/com/mapbefine/mapbefine/admin/AdminIntegrationTest.java delete mode 100644 backend/src/test/java/com/mapbefine/mapbefine/admin/application/AdminCommandServiceTest.java delete mode 100644 backend/src/test/java/com/mapbefine/mapbefine/admin/application/AdminQueryServiceTest.java delete mode 100644 backend/src/test/java/com/mapbefine/mapbefine/admin/presentation/AdminControllerTest.java diff --git a/backend/src/docs/asciidoc/admin.adoc b/backend/src/docs/asciidoc/admin.adoc deleted file mode 100644 index 3d2137c4..00000000 --- a/backend/src/docs/asciidoc/admin.adoc +++ /dev/null @@ -1,29 +0,0 @@ -== 관리자 기능 - -=== 전체 회원 조회 - -operation::admin-controller-test/find-all-member-details[snippets='http-request,http-response'] - -=== 회원 상세 조회 - -operation::admin-controller-test/find-member[snippets='http-request,http-response'] - -=== 회원 차단(삭제) - -operation::admin-controller-test/delete-member[snippets='http-request,http-response'] - -=== 토픽 삭제 - -operation::admin-controller-test/delete-topic[snippets='http-request,http-response'] - -=== 토픽 이미지 삭제 - -operation::admin-controller-test/delete-topic-image[snippets='http-request,http-response'] - -=== 핀 삭제 - -operation::admin-controller-test/delete-pin[snippets='http-request,http-response'] - -=== 핀 이미지 삭제 - -operation::admin-controller-test/delete-pin-image[snippets='http-request,http-response'] \ No newline at end of file diff --git a/backend/src/docs/asciidoc/index.adoc b/backend/src/docs/asciidoc/index.adoc index a90d89df..c15272d2 100644 --- a/backend/src/docs/asciidoc/index.adoc +++ b/backend/src/docs/asciidoc/index.adoc @@ -15,4 +15,3 @@ include::member.adoc[] include::permission.adoc[] include::oauth.adoc[] include::bookmark.adoc[] -include::admin.adoc[] diff --git a/backend/src/main/java/com/mapbefine/mapbefine/admin/application/AdminCommandService.java b/backend/src/main/java/com/mapbefine/mapbefine/admin/application/AdminCommandService.java deleted file mode 100644 index 0dfaf518..00000000 --- a/backend/src/main/java/com/mapbefine/mapbefine/admin/application/AdminCommandService.java +++ /dev/null @@ -1,127 +0,0 @@ -package com.mapbefine.mapbefine.admin.application; - -import static com.mapbefine.mapbefine.permission.exception.PermissionErrorCode.PERMISSION_FORBIDDEN_BY_NOT_ADMIN; -import static com.mapbefine.mapbefine.topic.exception.TopicErrorCode.TOPIC_NOT_FOUND; - -import com.mapbefine.mapbefine.atlas.domain.AtlasRepository; -import com.mapbefine.mapbefine.auth.domain.AuthMember; -import com.mapbefine.mapbefine.bookmark.domain.BookmarkRepository; -import com.mapbefine.mapbefine.member.domain.Member; -import com.mapbefine.mapbefine.member.domain.MemberRepository; -import com.mapbefine.mapbefine.member.domain.Role; -import com.mapbefine.mapbefine.member.domain.Status; -import com.mapbefine.mapbefine.permission.domain.PermissionRepository; -import com.mapbefine.mapbefine.permission.exception.PermissionException.PermissionForbiddenException; -import com.mapbefine.mapbefine.pin.domain.Pin; -import com.mapbefine.mapbefine.pin.domain.PinImageRepository; -import com.mapbefine.mapbefine.pin.domain.PinRepository; -import com.mapbefine.mapbefine.topic.domain.Topic; -import com.mapbefine.mapbefine.topic.domain.TopicRepository; -import com.mapbefine.mapbefine.topic.exception.TopicException; -import java.util.List; -import java.util.NoSuchElementException; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@Service -@Transactional -public class AdminCommandService { - - private final MemberRepository memberRepository; - private final TopicRepository topicRepository; - private final PinRepository pinRepository; - private final PinImageRepository pinImageRepository; - private final PermissionRepository permissionRepository; - private final AtlasRepository atlasRepository; - private final BookmarkRepository bookmarkRepository; - - public AdminCommandService( - MemberRepository memberRepository, - TopicRepository topicRepository, - PinRepository pinRepository, - PinImageRepository pinImageRepository, - PermissionRepository permissionRepository, - AtlasRepository atlasRepository, - BookmarkRepository bookmarkRepository - ) { - this.memberRepository = memberRepository; - this.topicRepository = topicRepository; - this.pinRepository = pinRepository; - this.pinImageRepository = pinImageRepository; - this.permissionRepository = permissionRepository; - this.atlasRepository = atlasRepository; - this.bookmarkRepository = bookmarkRepository; - } - - public void blockMember(AuthMember authMember, Long memberId) { - validateAdminPermission(authMember); - - Member member = findMemberById(memberId); - member.updateStatus(Status.BLOCKED); - - deleteAllRelatedMember(member); - } - - private void deleteAllRelatedMember(Member member) { - List pinIds = extractPinIdsByMember(member); - Long memberId = member.getId(); - - pinImageRepository.deleteAllByPinIds(pinIds); - topicRepository.deleteAllByMemberId(memberId); - pinRepository.deleteAllByMemberId(memberId); - permissionRepository.deleteAllByMemberId(memberId); - atlasRepository.deleteAllByMemberId(memberId); - bookmarkRepository.deleteAllByMemberId(memberId); - } - - private Member findMemberById(Long id) { - return memberRepository.findById(id) - .orElseThrow(() -> new NoSuchElementException("findMemberByAuthMember; member not found; id=" + id)); - } - - private void validateAdminPermission(AuthMember authMember) { - if (authMember.isRole(Role.ADMIN)) { - return; - } - - throw new PermissionForbiddenException(PERMISSION_FORBIDDEN_BY_NOT_ADMIN); - } - - private List extractPinIdsByMember(Member member) { - return member.getCreatedPins() - .stream() - .map(Pin::getId) - .toList(); - } - - public void deleteTopic(AuthMember authMember, Long topicId) { - validateAdminPermission(authMember); - - topicRepository.deleteById(topicId); - } - - public void deleteTopicImage(AuthMember authMember, Long topicId) { - validateAdminPermission(authMember); - - Topic topic = findTopicById(topicId); - topic.removeImage(); - } - - private Topic findTopicById(Long topicId) { - return topicRepository.findById(topicId) - .orElseThrow(() -> new TopicException.TopicNotFoundException(TOPIC_NOT_FOUND, List.of(topicId))); - } - - public void deletePin(AuthMember authMember, Long pinId) { - validateAdminPermission(authMember); - - pinRepository.deleteById(pinId); - } - - public void deletePinImage(AuthMember authMember, Long pinImageId) { - validateAdminPermission(authMember); - - pinImageRepository.deleteById(pinImageId); - } - -} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/admin/application/AdminQueryService.java b/backend/src/main/java/com/mapbefine/mapbefine/admin/application/AdminQueryService.java deleted file mode 100644 index f7ca346a..00000000 --- a/backend/src/main/java/com/mapbefine/mapbefine/admin/application/AdminQueryService.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.mapbefine.mapbefine.admin.application; - -import com.mapbefine.mapbefine.admin.dto.AdminMemberDetailResponse; -import com.mapbefine.mapbefine.admin.dto.AdminMemberResponse; -import com.mapbefine.mapbefine.auth.domain.AuthMember; -import com.mapbefine.mapbefine.member.domain.Member; -import com.mapbefine.mapbefine.member.domain.MemberRepository; -import com.mapbefine.mapbefine.member.domain.Role; -import com.mapbefine.mapbefine.permission.exception.PermissionErrorCode; -import com.mapbefine.mapbefine.permission.exception.PermissionException.PermissionForbiddenException; -import com.mapbefine.mapbefine.pin.domain.Pin; -import com.mapbefine.mapbefine.topic.domain.Topic; -import java.util.List; -import java.util.NoSuchElementException; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@Service -@Transactional(readOnly = true) -public class AdminQueryService { - - private final MemberRepository memberRepository; - - public AdminQueryService(MemberRepository memberRepository) { - this.memberRepository = memberRepository; - } - - public List findAllMemberDetails(AuthMember authMember) { - validateAdminPermission(authMember); - - List members = memberRepository.findAllByMemberInfoRole(Role.USER); - - return members.stream() - .map(AdminMemberResponse::from) - .toList(); - } - - private Member findMemberById(Long id) { - return memberRepository.findById(id) - .orElseThrow(() -> new NoSuchElementException("findMemberByAuthMember; member not found; id=" + id)); - } - - private void validateAdminPermission(AuthMember authMember) { - if (authMember.isRole(Role.ADMIN)) { - return; - } - - throw new PermissionForbiddenException(PermissionErrorCode.PERMISSION_FORBIDDEN_BY_NOT_ADMIN); - } - - public AdminMemberDetailResponse findMemberDetail(AuthMember authMember, Long memberId) { - validateAdminPermission(authMember); - - Member findMember = findMemberById(memberId); - List topics = findMember.getCreatedTopics(); - List pins = findMember.getCreatedPins(); - - return AdminMemberDetailResponse.of(findMember, topics, pins); - } - -} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/admin/dto/AdminMemberDetailResponse.java b/backend/src/main/java/com/mapbefine/mapbefine/admin/dto/AdminMemberDetailResponse.java deleted file mode 100644 index fbd5e04d..00000000 --- a/backend/src/main/java/com/mapbefine/mapbefine/admin/dto/AdminMemberDetailResponse.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.mapbefine.mapbefine.admin.dto; - -import com.mapbefine.mapbefine.member.domain.Member; -import com.mapbefine.mapbefine.member.domain.MemberInfo; -import com.mapbefine.mapbefine.pin.domain.Pin; -import com.mapbefine.mapbefine.pin.dto.response.PinResponse; -import com.mapbefine.mapbefine.topic.domain.Topic; -import com.mapbefine.mapbefine.topic.dto.response.TopicResponse; -import java.time.LocalDateTime; -import java.util.List; - -public record AdminMemberDetailResponse( - Long id, - String nickName, - String email, - String imageUrl, - List topics, - List pins, - LocalDateTime updatedAt -) { - - // TODO: 2023/09/12 topics, pins 모두 member를 통해 얻어올 수 있다. Service에서 꺼내서 넘겨줄 것인가 ? 아니면 DTO에서 꺼내올 것인가 ? - public static AdminMemberDetailResponse of( - Member member, - List topics, - List pins - ) { - MemberInfo memberInfo = member.getMemberInfo(); - List topicResponses = topics.stream() - .map(TopicResponse::fromGuestQuery) - .toList(); - List pinResponses = pins.stream() - .map(PinResponse::from) - .toList(); - - return new AdminMemberDetailResponse( - member.getId(), - memberInfo.getNickName(), - memberInfo.getEmail(), - memberInfo.getImageUrl(), - topicResponses, - pinResponses, - member.getUpdatedAt() - ); - } - - -} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/admin/dto/AdminMemberResponse.java b/backend/src/main/java/com/mapbefine/mapbefine/admin/dto/AdminMemberResponse.java deleted file mode 100644 index f041f8f1..00000000 --- a/backend/src/main/java/com/mapbefine/mapbefine/admin/dto/AdminMemberResponse.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.mapbefine.mapbefine.admin.dto; - -import com.mapbefine.mapbefine.member.domain.Member; -import com.mapbefine.mapbefine.member.domain.MemberInfo; -import java.time.LocalDateTime; - -public record AdminMemberResponse( - Long id, - String nickName, - String email, - String imageUrl, - LocalDateTime updatedAt -) { - - public static AdminMemberResponse from(Member member) { - MemberInfo memberInfo = member.getMemberInfo(); - - return new AdminMemberResponse( - member.getId(), - memberInfo.getNickName(), - memberInfo.getEmail(), - memberInfo.getImageUrl(), - member.getUpdatedAt() - ); - } - -} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/admin/presentation/AdminController.java b/backend/src/main/java/com/mapbefine/mapbefine/admin/presentation/AdminController.java deleted file mode 100644 index 2a34fec1..00000000 --- a/backend/src/main/java/com/mapbefine/mapbefine/admin/presentation/AdminController.java +++ /dev/null @@ -1,78 +0,0 @@ -package com.mapbefine.mapbefine.admin.presentation; - -import com.mapbefine.mapbefine.admin.application.AdminCommandService; -import com.mapbefine.mapbefine.admin.application.AdminQueryService; -import com.mapbefine.mapbefine.admin.dto.AdminMemberDetailResponse; -import com.mapbefine.mapbefine.admin.dto.AdminMemberResponse; -import com.mapbefine.mapbefine.auth.domain.AuthMember; -import java.util.List; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/admin") -public class AdminController { - - private final AdminQueryService adminQueryService; - private final AdminCommandService adminCommandService; - - - public AdminController(AdminQueryService adminQueryService, AdminCommandService adminCommandService) { - this.adminQueryService = adminQueryService; - this.adminCommandService = adminCommandService; - } - - @GetMapping("/members") - public ResponseEntity> findAllMembers(AuthMember authMember) { - List responses = adminQueryService.findAllMemberDetails(authMember); - - return ResponseEntity.ok(responses); - } - - @DeleteMapping("/members/{memberId}") - public ResponseEntity deleteMember(AuthMember authMember, @PathVariable Long memberId) { - adminCommandService.blockMember(authMember, memberId); - - return ResponseEntity.noContent().build(); - } - - @GetMapping("/members/{memberId}") - public ResponseEntity findMember(AuthMember authMember, @PathVariable Long memberId) { - AdminMemberDetailResponse response = adminQueryService.findMemberDetail(authMember, memberId); - - return ResponseEntity.ok(response); - } - - @DeleteMapping("/topics/{topicId}") - public ResponseEntity deleteTopic(AuthMember authMember, @PathVariable Long topicId) { - adminCommandService.deleteTopic(authMember, topicId); - - return ResponseEntity.noContent().build(); - } - - @DeleteMapping("/topics/{topicId}/images") - public ResponseEntity deleteTopicImage(AuthMember authMember, @PathVariable Long topicId) { - adminCommandService.deleteTopicImage(authMember, topicId); - - return ResponseEntity.noContent().build(); - } - - @DeleteMapping("/pins/{pinId}") - public ResponseEntity deletePin(AuthMember authMember, @PathVariable Long pinId) { - adminCommandService.deletePin(authMember, pinId); - - return ResponseEntity.noContent().build(); - } - - @DeleteMapping("/pins/images/{imageId}") - public ResponseEntity deletePinImage(AuthMember authMember, @PathVariable Long imageId) { - adminCommandService.deletePinImage(authMember, imageId); - - return ResponseEntity.noContent().build(); - } - -} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/atlas/domain/AtlasRepository.java b/backend/src/main/java/com/mapbefine/mapbefine/atlas/domain/AtlasRepository.java index bd37a9ba..242d6992 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/atlas/domain/AtlasRepository.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/atlas/domain/AtlasRepository.java @@ -10,5 +10,4 @@ public interface AtlasRepository extends JpaRepository { void deleteByMemberIdAndTopicId(Long memberId, Long topicId); - void deleteAllByMemberId(Long memberId); } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/application/AuthService.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/application/AuthService.java index 11b2e343..538e1244 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/auth/application/AuthService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/application/AuthService.java @@ -8,7 +8,6 @@ import com.mapbefine.mapbefine.topic.domain.Topic; import java.util.List; import java.util.Objects; -import java.util.Optional; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -61,15 +60,4 @@ private List getCreatedTopics(Member member) { .toList(); } - public boolean isAdmin(Long memberId) { - if (Objects.isNull(memberId)) { - return false; - } - - Optional member = memberRepository.findById(memberId); - - return member.map(Member::isAdmin) - .orElse(false); - } - } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/AuthMember.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/AuthMember.java index d27f7cc2..c795b1ec 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/AuthMember.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/AuthMember.java @@ -1,6 +1,5 @@ package com.mapbefine.mapbefine.auth.domain; -import com.mapbefine.mapbefine.member.domain.Role; import com.mapbefine.mapbefine.topic.domain.Topic; import java.util.List; @@ -28,8 +27,6 @@ protected AuthMember( public abstract boolean canPinCreateOrUpdate(Topic topic); - public abstract boolean isRole(Role role); - public Long getMemberId() { return memberId; } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/Admin.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/Admin.java index f6a54648..3d97522c 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/Admin.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/Admin.java @@ -1,7 +1,6 @@ package com.mapbefine.mapbefine.auth.domain.member; import com.mapbefine.mapbefine.auth.domain.AuthMember; -import com.mapbefine.mapbefine.member.domain.Role; import com.mapbefine.mapbefine.topic.domain.Topic; import java.util.Collections; @@ -35,9 +34,4 @@ public boolean canPinCreateOrUpdate(Topic topic) { return true; } - @Override - public boolean isRole(Role role) { - return Role.ADMIN == role; - } - } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/Guest.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/Guest.java index dcd8dbeb..6c8c0edc 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/Guest.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/Guest.java @@ -1,7 +1,6 @@ package com.mapbefine.mapbefine.auth.domain.member; import com.mapbefine.mapbefine.auth.domain.AuthMember; -import com.mapbefine.mapbefine.member.domain.Role; import com.mapbefine.mapbefine.topic.domain.Topic; import com.mapbefine.mapbefine.topic.domain.TopicStatus; import java.util.Collections; @@ -37,8 +36,4 @@ public boolean canPinCreateOrUpdate(Topic topic) { return false; } - @Override - public boolean isRole(Role role) { - return Role.GUEST == role; - } } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/User.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/User.java index ed2ea7b8..d2444d49 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/User.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/User.java @@ -1,7 +1,6 @@ package com.mapbefine.mapbefine.auth.domain.member; import com.mapbefine.mapbefine.auth.domain.AuthMember; -import com.mapbefine.mapbefine.member.domain.Role; import com.mapbefine.mapbefine.topic.domain.Topic; import com.mapbefine.mapbefine.topic.domain.TopicStatus; import java.util.List; @@ -55,9 +54,4 @@ private boolean hasPermission(Long topicId) { return createdTopic.contains(topicId) || topicsWithPermission.contains(topicId); } - @Override - public boolean isRole(Role role) { - return Role.USER == role; - } - } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/exception/AuthErrorCode.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/exception/AuthErrorCode.java deleted file mode 100644 index 7cbf7c20..00000000 --- a/backend/src/main/java/com/mapbefine/mapbefine/auth/exception/AuthErrorCode.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.mapbefine.mapbefine.auth.exception; - -import lombok.Getter; - -@Getter -public enum AuthErrorCode { - ILLEGAL_MEMBER_ID("03100", "로그인에 실패하였습니다."), - ILLEGAL_TOKEN("03101", "로그인에 실패하였습니다."), - FORBIDDEN_ADMIN_ACCESS("03102", "로그인에 실패하였습니다."), - BLOCKING_MEMBER_ACCESS("03103", "로그인에 실패하였습니다."), - ; - - private final String code; - private final String message; - - AuthErrorCode(String code, String message) { - this.code = code; - this.message = message; - } - -} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/exception/AuthException.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/exception/AuthException.java deleted file mode 100644 index 4f384f9e..00000000 --- a/backend/src/main/java/com/mapbefine/mapbefine/auth/exception/AuthException.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.mapbefine.mapbefine.auth.exception; - -import com.mapbefine.mapbefine.common.exception.ErrorCode; -import com.mapbefine.mapbefine.common.exception.ForbiddenException; -import com.mapbefine.mapbefine.common.exception.UnauthorizedException; - -public class AuthException { - - public static class AuthUnauthorizedException extends UnauthorizedException { - - public AuthUnauthorizedException(AuthErrorCode errorCode) { - super(new ErrorCode<>(errorCode.getCode(), errorCode.getMessage())); - } - } - - public static class AuthForbiddenException extends ForbiddenException { - - public AuthForbiddenException(AuthErrorCode errorCode) { - super(new ErrorCode<>(errorCode.getCode(), errorCode.getMessage())); - } - } - -} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/common/config/AuthConfig.java b/backend/src/main/java/com/mapbefine/mapbefine/common/config/AuthConfig.java index c972f561..02cb0062 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/common/config/AuthConfig.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/common/config/AuthConfig.java @@ -1,6 +1,5 @@ package com.mapbefine.mapbefine.common.config; -import com.mapbefine.mapbefine.common.interceptor.AdminAuthInterceptor; import com.mapbefine.mapbefine.common.interceptor.AuthInterceptor; import com.mapbefine.mapbefine.common.resolver.AuthArgumentResolver; import java.util.List; @@ -12,26 +11,20 @@ @Configuration public class AuthConfig implements WebMvcConfigurer { - private final AdminAuthInterceptor adminAuthInterceptor; private final AuthInterceptor authInterceptor; private final AuthArgumentResolver authArgumentResolver; public AuthConfig( - AdminAuthInterceptor adminAuthInterceptor, AuthInterceptor authInterceptor, AuthArgumentResolver authArgumentResolver ) { - this.adminAuthInterceptor = adminAuthInterceptor; this.authInterceptor = authInterceptor; this.authArgumentResolver = authArgumentResolver; } @Override public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(authInterceptor) - .excludePathPatterns("/admin/**"); - registry.addInterceptor(adminAuthInterceptor) - .addPathPatterns("/admin/**"); + registry.addInterceptor(authInterceptor); } @Override diff --git a/backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AdminAuthInterceptor.java b/backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AdminAuthInterceptor.java deleted file mode 100644 index 9f5c8424..00000000 --- a/backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AdminAuthInterceptor.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.mapbefine.mapbefine.common.interceptor; - -import com.mapbefine.mapbefine.auth.application.AuthService; -import com.mapbefine.mapbefine.auth.dto.AuthInfo; -import com.mapbefine.mapbefine.auth.exception.AuthErrorCode; -import com.mapbefine.mapbefine.auth.exception.AuthException; -import com.mapbefine.mapbefine.auth.infrastructure.AuthorizationExtractor; -import com.mapbefine.mapbefine.auth.infrastructure.JwtTokenProvider; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import java.util.Objects; -import org.springframework.stereotype.Component; -import org.springframework.web.method.HandlerMethod; -import org.springframework.web.servlet.HandlerInterceptor; - -@Component -public class AdminAuthInterceptor implements HandlerInterceptor { - - private final AuthorizationExtractor authorizationExtractor; - private final AuthService authService; - private final JwtTokenProvider jwtTokenProvider; - - public AdminAuthInterceptor( - AuthorizationExtractor authorizationExtractor, - AuthService authService, - JwtTokenProvider jwtTokenProvider - ) { - this.authorizationExtractor = authorizationExtractor; - this.authService = authService; - this.jwtTokenProvider = jwtTokenProvider; - } - - @Override - public boolean preHandle( - HttpServletRequest request, - HttpServletResponse response, - Object handler - ) throws Exception { - if (!(handler instanceof HandlerMethod)) { - return true; - } - - Long memberId = extractMemberIdFromToken(request); - - validateAdmin(memberId); - request.setAttribute("memberId", memberId); - - return true; - } - - private Long extractMemberIdFromToken(HttpServletRequest request) { - AuthInfo authInfo = authorizationExtractor.extract(request); - if (Objects.isNull(authInfo)) { - return null; - } - String accessToken = authInfo.accessToken(); - if (jwtTokenProvider.validateToken(accessToken)) { - return Long.parseLong(jwtTokenProvider.getPayload(accessToken)); - } - throw new AuthException.AuthUnauthorizedException(AuthErrorCode.ILLEGAL_TOKEN); - } - - private void validateAdmin(Long memberId) { - if (authService.isAdmin(memberId)) { - return; - } - - throw new AuthException.AuthForbiddenException(AuthErrorCode.FORBIDDEN_ADMIN_ACCESS); - } - -} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AuthInterceptor.java b/backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AuthInterceptor.java index 01c98c73..32a52d38 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AuthInterceptor.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AuthInterceptor.java @@ -3,11 +3,10 @@ import com.mapbefine.mapbefine.auth.application.AuthService; import com.mapbefine.mapbefine.auth.domain.AuthMember; import com.mapbefine.mapbefine.auth.dto.AuthInfo; -import com.mapbefine.mapbefine.auth.exception.AuthErrorCode; -import com.mapbefine.mapbefine.auth.exception.AuthException; -import com.mapbefine.mapbefine.auth.exception.AuthException.AuthUnauthorizedException; import com.mapbefine.mapbefine.auth.infrastructure.AuthorizationExtractor; import com.mapbefine.mapbefine.auth.infrastructure.JwtTokenProvider; +import com.mapbefine.mapbefine.common.exception.ErrorCode; +import com.mapbefine.mapbefine.common.exception.UnauthorizedException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.util.Arrays; @@ -19,6 +18,10 @@ @Component public class AuthInterceptor implements HandlerInterceptor { + private static final String UNAUTHORIZED_ERROR_MESSAGE = "로그인에 실패하였습니다."; + private static final ErrorCode ILLEGAL_MEMBER_ID = new ErrorCode("03100", UNAUTHORIZED_ERROR_MESSAGE); + private static final ErrorCode ILLEGAL_TOKEN = new ErrorCode("03101", UNAUTHORIZED_ERROR_MESSAGE); + private final AuthorizationExtractor authorizationExtractor; private final AuthService authService; private final JwtTokenProvider jwtTokenProvider; @@ -62,7 +65,7 @@ private void validateMember(Long memberId) { return; } - throw new AuthUnauthorizedException(AuthErrorCode.ILLEGAL_MEMBER_ID); + throw new UnauthorizedException(ILLEGAL_MEMBER_ID); } private boolean isAuthMemberNotRequired(HandlerMethod handlerMethod) { @@ -83,7 +86,7 @@ private Long extractMemberIdFromToken(HttpServletRequest request) { } String accessToken = authInfo.accessToken(); if (!jwtTokenProvider.validateToken(accessToken)) { - throw new AuthException.AuthUnauthorizedException(AuthErrorCode.ILLEGAL_TOKEN); + throw new UnauthorizedException(ILLEGAL_TOKEN); } return Long.parseLong(jwtTokenProvider.getPayload(accessToken)); } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/member/application/MemberQueryService.java b/backend/src/main/java/com/mapbefine/mapbefine/member/application/MemberQueryService.java index df5296f9..c032cf36 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/member/application/MemberQueryService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/member/application/MemberQueryService.java @@ -37,7 +37,6 @@ private Member findMemberById(Long id) { .orElseThrow(() -> new MemberNotFoundException(MemberErrorCode.MEMBER_NOT_FOUND, id)); } - // TODO: 2023/09/13 차단된 or 탈퇴한 사용자 필터링 필요 public List findAll() { return memberRepository.findAll() .stream() diff --git a/backend/src/main/java/com/mapbefine/mapbefine/member/domain/Member.java b/backend/src/main/java/com/mapbefine/mapbefine/member/domain/Member.java index 1d64325e..71de6abf 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/member/domain/Member.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/member/domain/Member.java @@ -64,15 +64,13 @@ public static Member of( String email, String imageUrl, Role role, - Status status, OauthId oauthId ) { MemberInfo memberInfo = MemberInfo.of( nickName, email, imageUrl, - role, - status + role ); return new Member(memberInfo, oauthId); @@ -87,7 +85,7 @@ public static Member ofRandomNickname( ) { String nickName = createNickname(nickname); - return Member.of(nickName, email, imageUrl, role, Status.NORMAL, oauthId); + return Member.of(nickName, email, imageUrl, role, oauthId); } private static String createNickname(String nickname) { @@ -113,8 +111,7 @@ public void update( nickName, email, imageUrl, - memberInfo.getRole(), - memberInfo.getStatus() + memberInfo.getRole() ); } @@ -152,17 +149,4 @@ public List getTopicsWithPermissions() { .toList(); } - public boolean isNormalStatus() { - return memberInfo.getStatus() == Status.NORMAL; - } - - public void updateStatus(Status status) { - memberInfo = MemberInfo.of( - memberInfo.getNickName(), - memberInfo.getEmail(), - memberInfo.getImageUrl(), - memberInfo.getRole(), - status - ); - } } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/member/domain/MemberInfo.java b/backend/src/main/java/com/mapbefine/mapbefine/member/domain/MemberInfo.java index 3cc5dfa4..c15054c3 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/member/domain/MemberInfo.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/member/domain/MemberInfo.java @@ -16,7 +16,6 @@ import java.util.Objects; import lombok.Getter; import lombok.NoArgsConstructor; -import org.hibernate.annotations.ColumnDefault; @Embeddable @NoArgsConstructor(access = PROTECTED) @@ -39,43 +38,33 @@ public class MemberInfo { @Column(nullable = false) private Role role; - @Enumerated(EnumType.STRING) - @ColumnDefault(value = "NORMAL") - @Column(nullable = false) - private Status status; - private MemberInfo( String nickName, String email, Image imageUrl, - Role role, - Status status + Role role ) { this.nickName = nickName; this.email = email; this.imageUrl = imageUrl; this.role = role; - this.status = status; } public static MemberInfo of( String nickName, String email, String imageUrl, - Role role, - Status status + Role role ) { validateNickName(nickName); validateEmail(email); validateRole(role); - validateStatus(status); return new MemberInfo( nickName, email, Image.from(imageUrl), - role, - status + role ); } @@ -104,12 +93,6 @@ private static void validateRole(Role role) { } } - private static void validateStatus(Status status) { - if (Objects.isNull(status)) { - throw new IllegalArgumentException("validateStatus; member status is null;"); - } - } - public String getImageUrl() { return imageUrl.getImageUrl(); } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/member/domain/MemberRepository.java b/backend/src/main/java/com/mapbefine/mapbefine/member/domain/MemberRepository.java index 65125aa1..16b57c05 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/member/domain/MemberRepository.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/member/domain/MemberRepository.java @@ -1,6 +1,5 @@ package com.mapbefine.mapbefine.member.domain; -import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; @@ -14,6 +13,4 @@ public interface MemberRepository extends JpaRepository { Optional findByOauthId(OauthId oauthId); - List findAllByMemberInfoRole(Role role); - } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/member/domain/Status.java b/backend/src/main/java/com/mapbefine/mapbefine/member/domain/Status.java deleted file mode 100644 index bf594c09..00000000 --- a/backend/src/main/java/com/mapbefine/mapbefine/member/domain/Status.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.mapbefine.mapbefine.member.domain; - -public enum Status { - NORMAL("STATUS_NORMAL", "정상 사용자"), - DELETE("STATAUS_DELETE", "탈퇴한 사용자"), - BLOCKED("STATUS_BLOCKED", "차단된 사용자"); - - private final String key; - private final String title; - - Status(String key, String title) { - this.key = key; - this.title = title; - } -} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/member/exception/MemberErrorCode.java b/backend/src/main/java/com/mapbefine/mapbefine/member/exception/MemberErrorCode.java index b0c5d034..c02d2ab1 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/member/exception/MemberErrorCode.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/member/exception/MemberErrorCode.java @@ -9,7 +9,6 @@ public enum MemberErrorCode { ILLEGAL_NICKNAME_LENGTH("05001", "닉네임 길이는 최소 1 자에서 20자여야 합니다."), ILLEGAL_EMAIL_NULL("05002", "이메일은 필수로 입력해야합니다."), ILLEGAL_EMAIL_PATTERN("05003", "올바르지 않은 이메일 형식입니다."), - FORBIDDEN_MEMBER_STATUS("05100", "탈퇴 혹은 차단된 회원입니다."), MEMBER_NOT_FOUND("05400", "존재하지 않는 회원입니다."), ; diff --git a/backend/src/main/java/com/mapbefine/mapbefine/member/exception/MemberException.java b/backend/src/main/java/com/mapbefine/mapbefine/member/exception/MemberException.java index 89a68948..ec98e9d0 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/member/exception/MemberException.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/member/exception/MemberException.java @@ -2,7 +2,6 @@ import com.mapbefine.mapbefine.common.exception.BadRequestException; import com.mapbefine.mapbefine.common.exception.ErrorCode; -import com.mapbefine.mapbefine.common.exception.ForbiddenException; import com.mapbefine.mapbefine.common.exception.NotFoundException; public class MemberException { @@ -19,11 +18,5 @@ public MemberNotFoundException(MemberErrorCode errorCode, Long id) { } } - public static class MemberForbiddenException extends ForbiddenException { - public MemberForbiddenException(MemberErrorCode errorCode, Long id) { - super(new ErrorCode<>(errorCode.getCode(), errorCode.getMessage(), id)); - } - } - } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/oauth/application/OauthService.java b/backend/src/main/java/com/mapbefine/mapbefine/oauth/application/OauthService.java index 50681719..d75526a0 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/oauth/application/OauthService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/oauth/application/OauthService.java @@ -1,7 +1,5 @@ package com.mapbefine.mapbefine.oauth.application; -import com.mapbefine.mapbefine.auth.exception.AuthErrorCode; -import com.mapbefine.mapbefine.auth.exception.AuthException.AuthUnauthorizedException; import com.mapbefine.mapbefine.auth.infrastructure.JwtTokenProvider; import com.mapbefine.mapbefine.member.domain.Member; import com.mapbefine.mapbefine.member.domain.MemberRepository; @@ -41,8 +39,6 @@ public LoginInfoResponse login(OauthServerType oauthServerType, String code) { Member savedMember = memberRepository.findByOauthId(oauthMember.getOauthId()) .orElseGet(() -> register(oauthMember)); - validateMemberStatus(savedMember); - String accessToken = jwtTokenProvider.createToken(String.valueOf(savedMember.getId())); return LoginInfoResponse.of(accessToken, savedMember); @@ -53,12 +49,4 @@ private Member register(OauthMember oauthMember) { return memberRepository.save(oauthMember.toRegisterMember()); } - private void validateMemberStatus(Member member) { - if (member.isNormalStatus()) { - return; - } - - throw new AuthUnauthorizedException(AuthErrorCode.BLOCKING_MEMBER_ACCESS); - } - } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/oauth/domain/AuthCodeRequestUrlProviderComposite.java b/backend/src/main/java/com/mapbefine/mapbefine/oauth/domain/AuthCodeRequestUrlProviderComposite.java index 2ad2295d..a2a3c518 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/oauth/domain/AuthCodeRequestUrlProviderComposite.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/oauth/domain/AuthCodeRequestUrlProviderComposite.java @@ -4,7 +4,7 @@ import static java.util.function.UnaryOperator.identity; import static java.util.stream.Collectors.toMap; -import com.mapbefine.mapbefine.oauth.exception.OauthException.OauthNotFoundException; +import com.mapbefine.mapbefine.oauth.exception.OathException.OauthNotFoundException; import java.util.Map; import java.util.Optional; import java.util.Set; diff --git a/backend/src/main/java/com/mapbefine/mapbefine/oauth/domain/OauthMemberClientComposite.java b/backend/src/main/java/com/mapbefine/mapbefine/oauth/domain/OauthMemberClientComposite.java index eba1172d..f5fb9648 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/oauth/domain/OauthMemberClientComposite.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/oauth/domain/OauthMemberClientComposite.java @@ -4,7 +4,7 @@ import static java.util.function.UnaryOperator.identity; import static java.util.stream.Collectors.toMap; -import com.mapbefine.mapbefine.oauth.exception.OauthException.OauthNotFoundException; +import com.mapbefine.mapbefine.oauth.exception.OathException.OauthNotFoundException; import java.util.Map; import java.util.Optional; import java.util.Set; diff --git a/backend/src/main/java/com/mapbefine/mapbefine/oauth/exception/OauthException.java b/backend/src/main/java/com/mapbefine/mapbefine/oauth/exception/OathException.java similarity index 94% rename from backend/src/main/java/com/mapbefine/mapbefine/oauth/exception/OauthException.java rename to backend/src/main/java/com/mapbefine/mapbefine/oauth/exception/OathException.java index 3dc67e35..a18d234c 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/oauth/exception/OauthException.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/oauth/exception/OathException.java @@ -4,7 +4,7 @@ import com.mapbefine.mapbefine.common.exception.NotFoundException; import com.mapbefine.mapbefine.oauth.domain.OauthServerType; -public class OauthException { +public class OathException { public static class OauthNotFoundException extends NotFoundException { public OauthNotFoundException(OauthErrorCode errorCode, OauthServerType oauthServerType) { diff --git a/backend/src/main/java/com/mapbefine/mapbefine/permission/domain/PermissionRepository.java b/backend/src/main/java/com/mapbefine/mapbefine/permission/domain/PermissionRepository.java index 7d14c061..da8dca9b 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/permission/domain/PermissionRepository.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/permission/domain/PermissionRepository.java @@ -9,5 +9,4 @@ public interface PermissionRepository extends JpaRepository { boolean existsByTopicIdAndMemberId(Long topicId, Long memberId); - void deleteAllByMemberId(Long memberId); } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/permission/exception/PermissionErrorCode.java b/backend/src/main/java/com/mapbefine/mapbefine/permission/exception/PermissionErrorCode.java index c3f3f699..5a0f6aa8 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/permission/exception/PermissionErrorCode.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/permission/exception/PermissionErrorCode.java @@ -9,8 +9,7 @@ public enum PermissionErrorCode { ILLEGAL_PERMISSION_ID("07001", "유효하지 않은 권한 정보입니다."), FORBIDDEN_ADD_PERMISSION_GUEST("07300", "로그인하지 않은 사용자는 권한을 줄 수 없습니다."), FORBIDDEN_ADD_PERMISSION("07301", "지도를 생성한 사용자가 아니면 권한을 줄 수 없습니다."), - PERMISSION_NOT_FOUND("07400", "존재하지 않는 권한 정보입니다."), - PERMISSION_FORBIDDEN_BY_NOT_ADMIN("07401", "어드민 계정만 접근 가능합니다."), + PERMISSION_NOT_FOUND("07400", "존재하지 않는 권한 정보입니다.") ; private final String code; diff --git a/backend/src/main/java/com/mapbefine/mapbefine/pin/application/PinQueryService.java b/backend/src/main/java/com/mapbefine/mapbefine/pin/application/PinQueryService.java index 0e2aa2bf..6956b55d 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/pin/application/PinQueryService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/pin/application/PinQueryService.java @@ -52,7 +52,7 @@ private void validateReadAuth(AuthMember member, Topic topic) { } public List findAllPinsByMemberId(AuthMember authMember, Long memberId) { - return pinRepository.findAllByCreatorId(memberId) + return pinRepository.findByCreatorId(memberId) .stream() .filter(pin -> authMember.canRead(pin.getTopic())) .map(PinResponse::from) diff --git a/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinImageRepository.java b/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinImageRepository.java index 6e25d0af..36f53800 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinImageRepository.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinImageRepository.java @@ -18,9 +18,6 @@ public interface PinImageRepository extends JpaRepository { @Query("update PinImage p set p.isDeleted = true where p.id = :id") void deleteById(@Param("id") Long id); - @Modifying(clearAutomatically = true) - @Query("update PinImage p set p.isDeleted = true where p.pin.id in :pinIds") - void deleteAllByPinIds(@Param("pinIds") List pinIds); - List findAllByPinId(Long pinId); + } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinRepository.java b/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinRepository.java index 60d29dc9..11a2d95d 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinRepository.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinRepository.java @@ -18,13 +18,9 @@ public interface PinRepository extends JpaRepository { @Query("update Pin p set p.isDeleted = true where p.id = :pinId") void deleteById(@Param("pinId") Long pinId); - @Modifying(clearAutomatically = true) - @Query("update Pin p set p.isDeleted = true where p.creator.id = :memberId") - void deleteAllByMemberId(@Param("memberId") Long memberId); - List findAllByTopicId(Long topicId); - List findAllByCreatorId(Long creatorId); + List findByCreatorId(Long creatorId); List findAllByOrderByUpdatedAtDesc(); } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/application/TopicQueryService.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/application/TopicQueryService.java index de0efdda..aa19d281 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/topic/application/TopicQueryService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/application/TopicQueryService.java @@ -186,7 +186,7 @@ private void validateReadableTopics(AuthMember member, List topics) { public List findAllTopicsByMemberId(AuthMember authMember, Long memberId) { if (Objects.isNull(authMember.getMemberId())) { - return topicRepository.findAllByCreatorId(memberId) + return topicRepository.findByCreatorId(memberId) .stream() .filter(authMember::canRead) .map(TopicResponse::fromGuestQuery) @@ -198,7 +198,7 @@ public List findAllTopicsByMemberId(AuthMember authMember, Long m List topicsInAtlas = findTopicsInAtlas(member); List topicsInBookMark = findBookMarkedTopics(member); - return topicRepository.findAllByCreatorId(memberId) + return topicRepository.findByCreatorId(memberId) .stream() .filter(authMember::canRead) .map(topic -> TopicResponse.from( diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/Topic.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/Topic.java index d5128022..a41c807e 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/Topic.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/Topic.java @@ -114,8 +114,4 @@ public int countBookmarks() { return bookmarks.size(); } - public void removeImage() { - this.topicInfo = topicInfo.removeImage(); - } - } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicInfo.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicInfo.java index dff5da7e..3f5ffd59 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicInfo.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicInfo.java @@ -88,13 +88,4 @@ private static Image createImage(String imageUrl) { public String getImageUrl() { return image.getImageUrl(); } - - public TopicInfo removeImage() { - return new TopicInfo( - this.name, - this.description, - DEFAULT_IMAGE - ); - } - } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicRepository.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicRepository.java index e6eb95db..1ef9fb6f 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicRepository.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicRepository.java @@ -16,12 +16,8 @@ public interface TopicRepository extends JpaRepository { @Query("update Topic t set t.isDeleted = true where t.id = :topicId") void deleteById(@Param("topicId") Long topicId); - @Modifying(clearAutomatically = true) - @Query("update Topic t set t.isDeleted = true where t.creator.id = :memberId") - void deleteAllByMemberId(@Param("memberId") Long memberId); - boolean existsById(Long id); - List findAllByCreatorId(Long creatorId); + List findByCreatorId(Long creatorId); } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/admin/AdminIntegrationTest.java b/backend/src/test/java/com/mapbefine/mapbefine/admin/AdminIntegrationTest.java deleted file mode 100644 index c7d7c26f..00000000 --- a/backend/src/test/java/com/mapbefine/mapbefine/admin/AdminIntegrationTest.java +++ /dev/null @@ -1,249 +0,0 @@ -package com.mapbefine.mapbefine.admin; - -import static io.restassured.RestAssured.given; -import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.http.HttpHeaders.AUTHORIZATION; - -import com.mapbefine.mapbefine.admin.dto.AdminMemberDetailResponse; -import com.mapbefine.mapbefine.admin.dto.AdminMemberResponse; -import com.mapbefine.mapbefine.common.IntegrationTest; -import com.mapbefine.mapbefine.location.LocationFixture; -import com.mapbefine.mapbefine.location.domain.Location; -import com.mapbefine.mapbefine.location.domain.LocationRepository; -import com.mapbefine.mapbefine.member.MemberFixture; -import com.mapbefine.mapbefine.member.domain.Member; -import com.mapbefine.mapbefine.member.domain.MemberRepository; -import com.mapbefine.mapbefine.member.domain.Role; -import com.mapbefine.mapbefine.pin.PinFixture; -import com.mapbefine.mapbefine.pin.PinImageFixture; -import com.mapbefine.mapbefine.pin.domain.Pin; -import com.mapbefine.mapbefine.pin.domain.PinImage; -import com.mapbefine.mapbefine.pin.domain.PinImageRepository; -import com.mapbefine.mapbefine.pin.domain.PinRepository; -import com.mapbefine.mapbefine.topic.TopicFixture; -import com.mapbefine.mapbefine.topic.domain.Topic; -import com.mapbefine.mapbefine.topic.domain.TopicRepository; -import io.restassured.common.mapper.TypeRef; -import java.util.List; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; - -class AdminIntegrationTest extends IntegrationTest { - - @Autowired - private MemberRepository memberRepository; - - @Autowired - private TopicRepository topicRepository; - - @Autowired - private PinRepository pinRepository; - - @Autowired - private LocationRepository locationRepository; - - @Autowired - private PinImageRepository pinImageRepository; - - private Location location; - private Member admin; - private Member member; - private Topic topic; - private Pin pin; - private PinImage pinImage; - - @BeforeEach - void setup() { - admin = memberRepository.save(MemberFixture.create("Admin", "admin@naver.com", Role.ADMIN)); - member = memberRepository.save(MemberFixture.create("member", "member@gmail.com", Role.USER)); - topic = topicRepository.save(TopicFixture.createByName("topic", member)); - location = locationRepository.save(LocationFixture.create()); - pin = pinRepository.save(PinFixture.create(location, topic, member)); - pinImage = pinImageRepository.save(PinImageFixture.create(pin)); - } - - @Test - @DisplayName("어드민일 경우, 회원을 전체 조회할 수 있다.") - void findAllMembers_Success() { - //given - Member member1 = MemberFixture.create("member1", "member1@gmail.com", Role.USER); - Member member2 = MemberFixture.create("member2", "member2@gmail.com", Role.USER); - memberRepository.save(member1); - memberRepository.save(member2); - - //when - List response = given().log().all() - .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(admin)) - .accept(MediaType.APPLICATION_JSON_VALUE) - .when().get("/admin/members") - .then().log().all() - .extract() - .as(new TypeRef<>() { - }); - - //then - List expected = List.of( - AdminMemberResponse.from(member), - AdminMemberResponse.from(member1), - AdminMemberResponse.from(member2) - ); - - assertThat(response).usingRecursiveComparison() - .ignoringFields("updatedAt") - .isEqualTo(expected); - } - - @Test - @DisplayName("어드민이 아닐 경우, 회원을 전체 조회할 수 없다.") - void findAllMembers_Fail() { - given().log().all() - .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(member)) - .accept(MediaType.APPLICATION_JSON_VALUE) - .when().get("/admin/members/" + member.getId()) - .then().log().all() - .statusCode(HttpStatus.FORBIDDEN.value()); - } - - @Test - @DisplayName("어드민일 경우, 특정 회원의 상세 정보를 조회할 수 있다.") - void findMemberDetail_Success() { - //given when - AdminMemberDetailResponse response = given().log().all() - .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(admin)) - .accept(MediaType.APPLICATION_JSON_VALUE) - .when().get("/admin/members/" + member.getId()) - .then().log().all() - .extract() - .as(new TypeRef<>() { - }); - - //then - - AdminMemberDetailResponse expected = AdminMemberDetailResponse.of( - member, - member.getCreatedTopics(), - member.getCreatedPins() - ); - - assertThat(response).usingRecursiveComparison() - .ignoringFields("updatedAt") - .ignoringFields("topics.updatedAt") - .isEqualTo(expected); - } - - @Test - @DisplayName("어드민이 아닐 경우, 특정 회원의 상세 정보를 조회할 수 없다.") - void findMemberDetail_Fail() { - given().log().all() - .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(member)) - .accept(MediaType.APPLICATION_JSON_VALUE) - .when().get("/admin/members/" + member.getId()) - .then().log().all() - .statusCode(HttpStatus.FORBIDDEN.value()); - } - - @Test - @DisplayName("어드민일 경우, 특정 회원을 삭제(차단)할 수 있다.") - void deleteMember_Success() { - given().log().all() - .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(admin)) - .when().delete("/admin/members/" + member.getId()) - .then().log().all() - .statusCode(HttpStatus.NO_CONTENT.value()); - } - - @Test - @DisplayName("어드민이 아닐 경우, 특정 회원을 삭제(차단)할 수 없다.") - void deleteMember_Fail() { - given().log().all() - .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(member)) - .when().delete("/admin/members/" + member.getId()) - .then().log().all() - .statusCode(HttpStatus.FORBIDDEN.value()); - } - - @Test - @DisplayName("어드민일 경우, 특정 토픽을 삭제할 수 있다.") - void deleteTopic_Success() { - given().log().all() - .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(admin)) - .when().delete("/admin/topics/" + topic.getId()) - .then().log().all() - .statusCode(HttpStatus.NO_CONTENT.value()); - } - - @Test - @DisplayName("어드민이 아닐 경우, 특정 토픽을 삭제할 수 없다.") - void deleteTopic_Fail() { - given().log().all() - .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(member)) - .when().delete("/admin/topics/" + topic.getId()) - .then().log().all() - .statusCode(HttpStatus.FORBIDDEN.value()); - } - - @Test - @DisplayName("어드민일 경우, 특정 토픽 이미지를 삭제할 수 있다.") - void deleteTopicImage_Success() { - given().log().all() - .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(admin)) - .when().delete("/admin/topics/" + topic.getId() + "/images") - .then().log().all() - .statusCode(HttpStatus.NO_CONTENT.value()); - } - - @Test - @DisplayName("어드민이 아닐 경우, 특정 토픽 이미지를 삭제할 수 없다.") - void deleteTopicImage_Fail() { - given().log().all() - .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(member)) - .when().delete("/admin/topics/" + topic.getId() + "/images") - .then().log().all() - .statusCode(HttpStatus.FORBIDDEN.value()); - } - - @Test - @DisplayName("어드민일 경우, 특정 핀을 삭제할 수 있다.") - void deletePin_Success() { - given().log().all() - .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(admin)) - .when().delete("/admin/pins/" + pin.getId()) - .then().log().all() - .statusCode(HttpStatus.NO_CONTENT.value()); - } - - @Test - @DisplayName("어드민이 아닐 경우, 특정 핀을 삭제할 수 없다.") - void deletePin_Fail() { - given().log().all() - .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(member)) - .when().delete("/admin/pins/" + pin.getId()) - .then().log().all() - .statusCode(HttpStatus.FORBIDDEN.value()); - } - - @Test - @DisplayName("어드민일 경우, 특정 핀 이미지를 삭제할 수 있다.") - void deletePinImage_Success() { - given().log().all() - .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(admin)) - .when().delete("/admin/pins/images/" + pinImage.getId()) - .then().log().all() - .statusCode(HttpStatus.NO_CONTENT.value()); - } - - @Test - @DisplayName("어드민이 아닐 경우, 특정 핀 이미지를 삭제할 수 없다.") - void deletePinImage_Fail() { - given().log().all() - .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(member)) - .when().delete("/admin/pins/images/" + pinImage.getId()) - .then().log().all() - .statusCode(HttpStatus.FORBIDDEN.value()); - } - -} diff --git a/backend/src/test/java/com/mapbefine/mapbefine/admin/application/AdminCommandServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/admin/application/AdminCommandServiceTest.java deleted file mode 100644 index 12e49f77..00000000 --- a/backend/src/test/java/com/mapbefine/mapbefine/admin/application/AdminCommandServiceTest.java +++ /dev/null @@ -1,280 +0,0 @@ -package com.mapbefine.mapbefine.admin.application; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.assertAll; - -import com.mapbefine.mapbefine.atlas.domain.Atlas; -import com.mapbefine.mapbefine.atlas.domain.AtlasRepository; -import com.mapbefine.mapbefine.auth.domain.AuthMember; -import com.mapbefine.mapbefine.bookmark.domain.Bookmark; -import com.mapbefine.mapbefine.bookmark.domain.BookmarkRepository; -import com.mapbefine.mapbefine.common.annotation.ServiceTest; -import com.mapbefine.mapbefine.location.LocationFixture; -import com.mapbefine.mapbefine.location.domain.Location; -import com.mapbefine.mapbefine.location.domain.LocationRepository; -import com.mapbefine.mapbefine.member.MemberFixture; -import com.mapbefine.mapbefine.member.domain.Member; -import com.mapbefine.mapbefine.member.domain.MemberRepository; -import com.mapbefine.mapbefine.member.domain.Role; -import com.mapbefine.mapbefine.member.domain.Status; -import com.mapbefine.mapbefine.permission.domain.Permission; -import com.mapbefine.mapbefine.permission.domain.PermissionRepository; -import com.mapbefine.mapbefine.permission.exception.PermissionException.PermissionForbiddenException; -import com.mapbefine.mapbefine.pin.PinFixture; -import com.mapbefine.mapbefine.pin.PinImageFixture; -import com.mapbefine.mapbefine.pin.domain.Pin; -import com.mapbefine.mapbefine.pin.domain.PinImage; -import com.mapbefine.mapbefine.pin.domain.PinImageRepository; -import com.mapbefine.mapbefine.pin.domain.PinRepository; -import com.mapbefine.mapbefine.topic.TopicFixture; -import com.mapbefine.mapbefine.topic.domain.Topic; -import com.mapbefine.mapbefine.topic.domain.TopicInfo; -import com.mapbefine.mapbefine.topic.domain.TopicRepository; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; - -@ServiceTest -class AdminCommandServiceTest { - - @Autowired - private AdminCommandService adminCommandService; - - @Autowired - private MemberRepository memberRepository; - - @Autowired - private TopicRepository topicRepository; - - @Autowired - private PinRepository pinRepository; - - @Autowired - private LocationRepository locationRepository; - - @Autowired - private PinImageRepository pinImageRepository; - - @Autowired - private AtlasRepository atlasRepository; - - @Autowired - private PermissionRepository permissionRepository; - - @Autowired - private BookmarkRepository bookmarkRepository; - - private Location location; - private Member admin; - private Member member; - private Topic topic; - private Pin pin; - private PinImage pinImage; - - @BeforeEach - void setup() { - admin = memberRepository.save(MemberFixture.create("Admin", "admin@naver.com", Role.ADMIN)); - member = memberRepository.save(MemberFixture.create("member", "member@gmail.com", Role.USER)); - topic = topicRepository.save(TopicFixture.createByName("topic", member)); - location = locationRepository.save(LocationFixture.create()); - pin = pinRepository.save(PinFixture.create(location, topic, member)); - pinImage = pinImageRepository.save(PinImageFixture.create(pin)); - } - - @DisplayName("Member를 차단(탈퇴시킬)할 경우, Member가 생성한 토픽, 핀, 핀 이미지가 soft-deleting 된다.") - @Test - void blockMember_Success() { - //given - AuthMember adminAuthMember = MemberFixture.createUser(admin); - Bookmark bookmark = Bookmark.createWithAssociatedTopicAndMember(topic, member); - Atlas atlas = Atlas.createWithAssociatedMember(topic, member); - Permission permission = Permission.createPermissionAssociatedWithTopicAndMember(topic, member); - - bookmarkRepository.save(bookmark); - atlasRepository.save(atlas); - permissionRepository.save(permission); - - assertAll(() -> { - assertThat(member.getMemberInfo().getStatus()).isEqualTo(Status.NORMAL); - assertThat(topic.isDeleted()).isFalse(); - assertThat(pin.isDeleted()).isFalse(); - assertThat(pinImage.isDeleted()).isFalse(); - assertThat(bookmarkRepository.existsByMemberIdAndTopicId(member.getId(), topic.getId())).isTrue(); - assertThat(atlasRepository.existsByMemberIdAndTopicId(member.getId(), topic.getId())).isTrue(); - assertThat(permissionRepository.existsByTopicIdAndMemberId(topic.getId(), member.getId())).isTrue(); - }); - - //when - adminCommandService.blockMember(adminAuthMember, member.getId()); - - //then - Topic deletedTopic = topicRepository.findById(topic.getId()).get(); - Pin deletedPin = pinRepository.findById(pin.getId()).get(); - PinImage deletedPinImage = pinImageRepository.findById(pinImage.getId()).get(); - - assertAll(() -> { - assertThat(member.getMemberInfo().getStatus()).isEqualTo(Status.BLOCKED); - assertThat(deletedTopic.isDeleted()).isTrue(); - assertThat(deletedPin.isDeleted()).isTrue(); - assertThat(deletedPinImage.isDeleted()).isTrue(); - assertThat(bookmarkRepository.existsByMemberIdAndTopicId(member.getId(), topic.getId())).isFalse(); - assertThat(atlasRepository.existsByMemberIdAndTopicId(member.getId(), topic.getId())).isFalse(); - assertThat(permissionRepository.existsByTopicIdAndMemberId(topic.getId(), member.getId())).isFalse(); - }); - } - - @DisplayName("Admin이 아닐 경우, Member를 차단(탈퇴시킬)할 수 없다.") - @Test - void blockMember_Fail() { - //given - AuthMember userAuthMember = MemberFixture.createUser(member); - Member otherMember = MemberFixture.create("otherMember", "otherMember@email.com", Role.USER); - memberRepository.save(otherMember); - - assertAll(() -> { - assertThat(member.getMemberInfo().getStatus()).isEqualTo(Status.NORMAL); - assertThat(topic.isDeleted()).isFalse(); - assertThat(pin.isDeleted()).isFalse(); - assertThat(pinImage.isDeleted()).isFalse(); - }); - - //when then - Long otherMemberId = otherMember.getId(); - assertThatThrownBy(() -> adminCommandService.blockMember(userAuthMember, otherMemberId)) - .isInstanceOf(PermissionForbiddenException.class); - } - - @DisplayName("Admin은 토픽을 삭제시킬 수 있다.") - @Test - void deleteTopic_Success() { - //given - AuthMember adminAuthMember = MemberFixture.createUser(admin); - - assertThat(topic.isDeleted()).isFalse(); - - //when - adminCommandService.deleteTopic(adminAuthMember, topic.getId()); - - //then - Topic deletedTopic = topicRepository.findById(topic.getId()).get(); - - assertThat(deletedTopic.isDeleted()).isTrue(); - } - - @DisplayName("Admin이 아닐 경우, 토픽을 삭제시킬 수 없다.") - @Test - void deleteTopic_Fail() { - //given - AuthMember userAuthMember = MemberFixture.createUser(member); - - assertThat(topic.isDeleted()).isFalse(); - - //when then - Long topicId = topic.getId(); - assertThatThrownBy(() -> adminCommandService.deleteTopic(userAuthMember, topicId)) - .isInstanceOf(PermissionForbiddenException.class); - } - - @DisplayName("Admin은 토픽 이미지를 삭제할 수 있다.") - @Test - void deleteTopicImage_Success() { - //given - AuthMember adminAuthMember = MemberFixture.createUser(admin); - TopicInfo topicInfo = topic.getTopicInfo(); - - topic.updateTopicInfo(topicInfo.getName(), topicInfo.getDescription(), "https://imageUrl.png"); - - assertThat(topic.getTopicInfo().getImageUrl()).isEqualTo("https://imageUrl.png"); - - //when - adminCommandService.deleteTopicImage(adminAuthMember, topic.getId()); - - //then - Topic imageDeletedTopic = topicRepository.findById(topic.getId()).get(); - - assertThat(imageDeletedTopic.getTopicInfo().getImageUrl()) - .isEqualTo("https://map-befine-official.github.io/favicon.png"); - } - - @DisplayName("Admin이 아닐 경우, 이미지를 삭제할 수 없다.") - @Test - void deleteTopicImage_Fail() { - //given - AuthMember userAuthMember = MemberFixture.createUser(member); - TopicInfo topicInfo = topic.getTopicInfo(); - - topic.updateTopicInfo(topicInfo.getName(), topicInfo.getDescription(), "https://imageUrl.png"); - - assertThat(topic.getTopicInfo().getImageUrl()).isEqualTo("https://imageUrl.png"); - - //when then - Long topicId = topic.getId(); - assertThatThrownBy(() -> adminCommandService.deleteTopicImage(userAuthMember, topicId)) - .isInstanceOf(PermissionForbiddenException.class); - } - - @DisplayName("Admin은 핀을 삭제할 수 있다.") - @Test - void deletePin_Success() { - //given - AuthMember adminAuthMember = MemberFixture.createUser(admin); - - assertThat(pin.isDeleted()).isFalse(); - - //when - adminCommandService.deletePin(adminAuthMember, pin.getId()); - - //then - Pin deletedPin = pinRepository.findById(pin.getId()).get(); - - assertThat(deletedPin.isDeleted()).isTrue(); - } - - @DisplayName("Admin이 아닐 경우, 핀을 삭제할 수 없다.") - @Test - void deletePin_Fail() { - //given - AuthMember userAuthMember = MemberFixture.createUser(member); - - assertThat(pin.isDeleted()).isFalse(); - - //when then - Long pinId = pin.getId(); - assertThatThrownBy(() -> adminCommandService.deletePin(userAuthMember, pinId)) - .isInstanceOf(PermissionForbiddenException.class); - } - - @DisplayName("Admin인 경우, 핀 이미지를 삭제할 수 있다.") - @Test - void deletePinImage_Success() { - //given - AuthMember adminAuthMember = MemberFixture.createUser(admin); - - assertThat(pinImage.isDeleted()).isFalse(); - - //when - adminCommandService.deletePinImage(adminAuthMember, pinImage.getId()); - - //then - PinImage deletedPinImage = pinImageRepository.findById(pinImage.getId()).get(); - - assertThat(deletedPinImage.isDeleted()).isTrue(); - } - - @DisplayName("Admin이 아닐 경우, 핀 이미지를 삭제할 수 없다.") - @Test - void deletePinImage_Fail() { - //given - AuthMember userAuthMember = MemberFixture.createUser(member); - - assertThat(pinImage.isDeleted()).isFalse(); - - //when then - Long pinImageId = pinImage.getId(); - assertThatThrownBy(() -> adminCommandService.deletePinImage(userAuthMember, pinImageId)) - .isInstanceOf(PermissionForbiddenException.class); - } - -} \ No newline at end of file diff --git a/backend/src/test/java/com/mapbefine/mapbefine/admin/application/AdminQueryServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/admin/application/AdminQueryServiceTest.java deleted file mode 100644 index 33599110..00000000 --- a/backend/src/test/java/com/mapbefine/mapbefine/admin/application/AdminQueryServiceTest.java +++ /dev/null @@ -1,135 +0,0 @@ -package com.mapbefine.mapbefine.admin.application; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import com.mapbefine.mapbefine.admin.dto.AdminMemberDetailResponse; -import com.mapbefine.mapbefine.admin.dto.AdminMemberResponse; -import com.mapbefine.mapbefine.auth.domain.AuthMember; -import com.mapbefine.mapbefine.common.annotation.ServiceTest; -import com.mapbefine.mapbefine.location.LocationFixture; -import com.mapbefine.mapbefine.location.domain.Location; -import com.mapbefine.mapbefine.location.domain.LocationRepository; -import com.mapbefine.mapbefine.member.MemberFixture; -import com.mapbefine.mapbefine.member.domain.Member; -import com.mapbefine.mapbefine.member.domain.MemberRepository; -import com.mapbefine.mapbefine.member.domain.Role; -import com.mapbefine.mapbefine.permission.exception.PermissionException.PermissionForbiddenException; -import com.mapbefine.mapbefine.pin.PinFixture; -import com.mapbefine.mapbefine.pin.domain.Pin; -import com.mapbefine.mapbefine.pin.domain.PinRepository; -import com.mapbefine.mapbefine.topic.TopicFixture; -import com.mapbefine.mapbefine.topic.domain.Topic; -import com.mapbefine.mapbefine.topic.domain.TopicRepository; -import java.util.ArrayList; -import java.util.List; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; - -@ServiceTest -class AdminQueryServiceTest { - - @Autowired - private AdminQueryService adminQueryService; - - @Autowired - private MemberRepository memberRepository; - - @Autowired - private TopicRepository topicRepository; - - @Autowired - private PinRepository pinRepository; - - @Autowired - private LocationRepository locationRepository; - - private Location location; - private Member admin; - private Member member; - private Topic topic; - private Pin pin; - - @BeforeEach - void setup() { - admin = memberRepository.save(MemberFixture.create("Admin", "admin@naver.com", Role.ADMIN)); - member = memberRepository.save(MemberFixture.create("member", "member@gmail.com", Role.USER)); - topic = topicRepository.save(TopicFixture.createByName("topic", member)); - location = locationRepository.save(LocationFixture.create()); - pin = pinRepository.save(PinFixture.create(location, topic, member)); - } - - @Test - @DisplayName("사용자와 관련된 세부(민감한 정보 포함) 정보를 모두 조회할 수 있다.") - void findMemberDetail_Success() { - //given - AuthMember adminAuthMember = MemberFixture.createUser(admin); - - //when - AdminMemberDetailResponse response = adminQueryService.findMemberDetail(adminAuthMember, member.getId()); - - //then - assertThat(response).usingRecursiveComparison() - .ignoringFields("updatedAt") - .isEqualTo(AdminMemberDetailResponse.of(member, List.of(topic), List.of(pin))); - } - - @Test - @DisplayName("Admin이 아닌 경우, 사용자와 관련된 세부(민감한 정보 포함) 정보를 모두 조회할 수 없다.") - void findMemberDetail_Fail() { - //given - AuthMember userAuthMember = MemberFixture.createUser(member); - - //when //then - Long memberId = member.getId(); - assertThatThrownBy(() -> adminQueryService.findMemberDetail(userAuthMember, memberId)) - .isInstanceOf(PermissionForbiddenException.class); - } - - @Test - @DisplayName("모든 사용자와 관련된 세부 정보를 모두 조회할 수 있다.") - void findAllMemberDetails_Success() { - //given - AuthMember adminAuthMember = MemberFixture.createUser(admin); - - ArrayList members = new ArrayList<>(); - members.add(member); - for (int i = 0; i < 10; i++) { - Member saveMember = MemberFixture.create("member" + i, "member" + i + "@email.com", Role.USER); - members.add(memberRepository.save(saveMember)); - } - - //when - List responses = adminQueryService.findAllMemberDetails(adminAuthMember); - //then - List expected = members.stream() - .map(AdminMemberResponse::from) - .toList(); - - assertThat(responses).usingRecursiveComparison() - .ignoringFields("updatedAt") - .ignoringCollectionOrderInFields() - .isEqualTo(expected); - } - - @Test - @DisplayName("Admin이 아닌 경우 모든 사용자와 관련된 세부 정보를 모두 조회할 수 없다.") - void findAllMemberDetails_Fail() { - //given - AuthMember memberAuthMember = MemberFixture.createUser(member); - - ArrayList members = new ArrayList<>(); - members.add(member); - for (int i = 0; i < 10; i++) { - Member saveMember = MemberFixture.create("member" + i, "member" + i + "@email.com", Role.USER); - members.add(memberRepository.save(saveMember)); - } - - //when //then - assertThatThrownBy(() -> adminQueryService.findAllMemberDetails(memberAuthMember)) - .isInstanceOf(PermissionForbiddenException.class); - } - -} \ No newline at end of file diff --git a/backend/src/test/java/com/mapbefine/mapbefine/admin/presentation/AdminControllerTest.java b/backend/src/test/java/com/mapbefine/mapbefine/admin/presentation/AdminControllerTest.java deleted file mode 100644 index edceb5d3..00000000 --- a/backend/src/test/java/com/mapbefine/mapbefine/admin/presentation/AdminControllerTest.java +++ /dev/null @@ -1,172 +0,0 @@ -package com.mapbefine.mapbefine.admin.presentation; - -import static org.apache.http.HttpHeaders.AUTHORIZATION; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.doNothing; - -import com.mapbefine.mapbefine.admin.application.AdminCommandService; -import com.mapbefine.mapbefine.admin.application.AdminQueryService; -import com.mapbefine.mapbefine.admin.dto.AdminMemberDetailResponse; -import com.mapbefine.mapbefine.admin.dto.AdminMemberResponse; -import com.mapbefine.mapbefine.common.RestDocsIntegration; -import com.mapbefine.mapbefine.common.interceptor.AdminAuthInterceptor; -import com.mapbefine.mapbefine.pin.dto.response.PinResponse; -import com.mapbefine.mapbefine.topic.dto.response.TopicResponse; -import java.time.LocalDateTime; -import java.util.List; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; - -class AdminControllerTest extends RestDocsIntegration { - - private static final List TOPIC_RESPONSES = List.of(new TopicResponse( - 1L, - "준팍의 또 토픽", - "https://map-befine-official.github.io/favicon.png", - "준팍", - 3, - Boolean.FALSE, - 5, - Boolean.FALSE, - LocalDateTime.now() - ), new TopicResponse( - 2L, - "준팍의 두번째 토픽", - "https://map-befine-official.github.io/favicon.png", - "준팍", - 5, - Boolean.FALSE, - 3, - Boolean.FALSE, - LocalDateTime.now() - )); - private static final List PIN_RESPONSES = List.of( - new PinResponse( - 1L, - "매튜의 산스장", - "지번 주소", - "매튜가 사랑하는 산스장", - "매튜", - 37, - 127 - ), new PinResponse( - 2L, - "매튜의 안갈집", - "지번 주소", - "매튜가 두번은 안 갈 집", - "매튜", - 37, - 127 - ) - ); - - @MockBean - private AdminCommandService adminCommandService; - - @MockBean - private AdminQueryService adminQueryService; - - @MockBean - private AdminAuthInterceptor adminAuthInterceptor; - - @BeforeEach - void setAll() throws Exception { - given(adminAuthInterceptor.preHandle(any(), any(), any())).willReturn(true); - } - - @DisplayName("멤버 목록 조회") - @Test - void findAllMemberDetails() throws Exception { - List response = List.of( - new AdminMemberResponse(1L, "쥬니", "zuny@naver.com", "https://zuny.png", LocalDateTime.now()), - new AdminMemberResponse(2L, "세인", "semin@naver.com", "https://semin.png", LocalDateTime.now()) - ); - - given(adminQueryService.findAllMemberDetails(any())).willReturn(response); - - mockMvc.perform( - MockMvcRequestBuilders.get("/admin/members") - .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) - ).andDo(restDocs.document()); - } - - @DisplayName("멤버 상세 조회") - @Test - void findMember() throws Exception { - AdminMemberDetailResponse response = new AdminMemberDetailResponse( - 1L, - "쥬니", - "zuny@naver.com", - "https://image.png", - TOPIC_RESPONSES, - PIN_RESPONSES, - LocalDateTime.now() - ); - - given(adminQueryService.findMemberDetail(any(), any())).willReturn(response); - - mockMvc.perform( - MockMvcRequestBuilders.get("/admin/members/1") - .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) - ).andDo(restDocs.document()); - } - - @DisplayName("멤버 차단(블랙리스트)") - @Test - void deleteMember() throws Exception { - doNothing().when(adminCommandService).blockMember(any(), any()); - - mockMvc.perform( - MockMvcRequestBuilders.delete("/admin/members/1") - .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) - ).andDo(restDocs.document()); - } - - @DisplayName("토픽 삭제") - @Test - void deleteTopic() throws Exception { - doNothing().when(adminCommandService).deleteTopic(any(), any()); - - mockMvc.perform( - MockMvcRequestBuilders.delete("/admin/topics/1") - .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) - ).andDo(restDocs.document()); - } - - @DisplayName("토픽 이미지 삭제") - @Test - void deleteTopicImage() throws Exception { - doNothing().when(adminCommandService).deleteTopicImage(any(), any()); - - mockMvc.perform( - MockMvcRequestBuilders.delete("/admin/topics/1/images") - .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) - ).andDo(restDocs.document()); - } - - @DisplayName("핀 삭제") - @Test - void deletePin() throws Exception { - doNothing().when(adminCommandService).deletePin(any(), any()); - - mockMvc.perform( - MockMvcRequestBuilders.delete("/admin/pins/1") - .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) - ).andDo(restDocs.document()); - } - - @DisplayName("토픽 이미지 삭제") - @Test - void deletePinImage() throws Exception { - doNothing().when(adminCommandService).deletePinImage(any(), any()); - - mockMvc.perform( - MockMvcRequestBuilders.delete("/admin/pins/images/1") - .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) - ).andDo(restDocs.document()); - } -} \ No newline at end of file diff --git a/backend/src/test/java/com/mapbefine/mapbefine/member/MemberFixture.java b/backend/src/test/java/com/mapbefine/mapbefine/member/MemberFixture.java index 6fd7bc14..d6943a99 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/member/MemberFixture.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/member/MemberFixture.java @@ -1,12 +1,10 @@ package com.mapbefine.mapbefine.member; import com.mapbefine.mapbefine.auth.domain.AuthMember; -import com.mapbefine.mapbefine.auth.domain.member.Admin; import com.mapbefine.mapbefine.auth.domain.member.User; import com.mapbefine.mapbefine.member.domain.Member; import com.mapbefine.mapbefine.member.domain.OauthId; import com.mapbefine.mapbefine.member.domain.Role; -import com.mapbefine.mapbefine.member.domain.Status; import com.mapbefine.mapbefine.oauth.domain.OauthServerType; import com.mapbefine.mapbefine.topic.domain.Topic; @@ -18,7 +16,6 @@ public static Member create(String name, String email, Role role) { email, "https://map-befine-official.github.io/favicon.png", role, - Status.NORMAL, new OauthId(1L, OauthServerType.KAKAO) ); } @@ -29,16 +26,11 @@ public static Member createWithOauthId(String name, String email, Role role, Oau email, "https://map-befine-official.github.io/favicon.png", role, - Status.NORMAL, oauthId) ; } public static AuthMember createUser(Member member) { - if (member.isAdmin()) { - return new Admin(member.getId()); - } - return new User( member.getId(), member.getCreatedTopics().stream().map(Topic::getId).toList(), diff --git a/backend/src/test/java/com/mapbefine/mapbefine/member/domain/MemberInfoTest.java b/backend/src/test/java/com/mapbefine/mapbefine/member/domain/MemberInfoTest.java index 1ed6e737..7c4d4a20 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/member/domain/MemberInfoTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/member/domain/MemberInfoTest.java @@ -22,7 +22,6 @@ class Validate { private final String VALID_EMAIL = "member@naver.com"; private final String VALID_IMAGE_URL = "https://map-befine-official.github.io/favicon.png"; private final Role VALID_ROLE = Role.ADMIN; - private final Status VALID_STATUS = Status.NORMAL; @Test @DisplayName("정확한 값을 입력하면 객체가 생성된다") @@ -32,8 +31,7 @@ void success() { VALID_NICK_NAME, VALID_EMAIL, VALID_IMAGE_URL, - VALID_ROLE, - VALID_STATUS + VALID_ROLE ); //then @@ -54,8 +52,7 @@ void whenNameIsInvalid_thenFail(String invalidNickName) { invalidNickName, VALID_EMAIL, VALID_IMAGE_URL, - VALID_ROLE, - VALID_STATUS + VALID_ROLE )).isInstanceOf(MemberBadRequestException.class); } @@ -70,8 +67,7 @@ void whenEmailIsInvalid_thenFail(String invalidEmail) { VALID_NICK_NAME, invalidEmail, VALID_IMAGE_URL, - VALID_ROLE, - VALID_STATUS + VALID_ROLE )).isInstanceOf(MemberBadRequestException.class); } @@ -85,8 +81,7 @@ void whenImageUrlIsInvalid_thenFail() { VALID_NICK_NAME, VALID_EMAIL, invalidImageUrl, - VALID_ROLE, - VALID_STATUS + VALID_ROLE )).isInstanceOf(ImageBadRequestException.class); } @@ -99,7 +94,6 @@ void whenRoleIsInvalid_thenFail() { VALID_EMAIL, VALID_IMAGE_URL, null - ,VALID_STATUS )).isInstanceOf(IllegalArgumentException.class); } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/member/domain/MemberTest.java b/backend/src/test/java/com/mapbefine/mapbefine/member/domain/MemberTest.java index 7fcb4b10..0ee9b697 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/member/domain/MemberTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/member/domain/MemberTest.java @@ -16,7 +16,6 @@ void createMember_success() { String email = "member@naver.com"; String imageUrl = "https://map-befine-official.github.io/favicon.png"; Role role = Role.ADMIN; - Status status = Status.NORMAL; // when Member member = Member.of( @@ -24,9 +23,7 @@ void createMember_success() { email, imageUrl, role, - status, - new OauthId(1L, OauthServerType.KAKAO) - ); + new OauthId(1L, OauthServerType.KAKAO)); // then assertThat(member).isNotNull(); diff --git a/backend/src/test/java/com/mapbefine/mapbefine/pin/domain/PinImageRepositoryTest.java b/backend/src/test/java/com/mapbefine/mapbefine/pin/domain/PinImageRepositoryTest.java index a8604271..ad36bb0d 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/pin/domain/PinImageRepositoryTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/pin/domain/PinImageRepositoryTest.java @@ -14,7 +14,6 @@ import com.mapbefine.mapbefine.topic.TopicFixture; import com.mapbefine.mapbefine.topic.domain.Topic; import com.mapbefine.mapbefine.topic.domain.TopicRepository; -import java.util.List; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -88,30 +87,4 @@ void deleteAllByPinId_Success() { .extractingResultOf("isDeleted") .containsOnly(true); } - - @Test - @DisplayName("여러 핀을 Id로 삭제하면, 핀 이미지들도 soft-deleting 된다.") - void deleteAllByMemberId_Success() { - //given - Pin otherPin = pinRepository.save(PinFixture.create(pin.getLocation(), topic, member)); - - PinImage pinImage1 = PinImageFixture.create(pin); - PinImage pinImage2 = PinImageFixture.create(otherPin); - pinImageRepository.save(pinImage1); - pinImageRepository.save(pinImage2); - - //when - assertThat(pinImage1.isDeleted()).isFalse(); - assertThat(pinImage2.isDeleted()).isFalse(); - pinImageRepository.deleteAllByPinIds(List.of(pin.getId(),otherPin.getId())); - - //then - assertThat(pinImageRepository.findAllByPinId(pin.getId())) - .extractingResultOf("isDeleted") - .containsOnly(true); - assertThat(pinImageRepository.findAllByPinId(otherPin.getId())) - .extractingResultOf("isDeleted") - .containsOnly(true); - } - } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/pin/domain/PinRepositoryTest.java b/backend/src/test/java/com/mapbefine/mapbefine/pin/domain/PinRepositoryTest.java index 24bf7cde..508d938e 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/pin/domain/PinRepositoryTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/pin/domain/PinRepositoryTest.java @@ -3,6 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import com.mapbefine.mapbefine.location.LocationFixture; +import com.mapbefine.mapbefine.location.domain.Coordinate; import com.mapbefine.mapbefine.location.domain.Location; import com.mapbefine.mapbefine.location.domain.LocationRepository; import com.mapbefine.mapbefine.member.MemberFixture; @@ -23,6 +24,10 @@ @DataJpaTest class PinRepositoryTest { + private static final Coordinate DEFAULT_COORDINATE = Coordinate.of( + 37.5152933, + 127.1029866 + ); @Autowired private TopicRepository topicRepository; @@ -83,49 +88,4 @@ void deleteAllByTopicId_Success() { .containsOnly(true); } - - @Test - @DisplayName("Member ID로 모든 핀을 soft-delete 할 수 있다.") - void deleteAllByMemberId_Success() { - //given - for (int i = 0; i < 10; i++) { - pinRepository.save(PinFixture.create(location, topic, member)); - } - - //when - assertThat(member.getCreatedPins()).hasSize(10) - .extractingResultOf("isDeleted") - .containsOnly(false); - pinRepository.deleteAllByMemberId(member.getId()); - - //then - List deletedPins = pinRepository.findAllByCreatorId(member.getId()); - assertThat(deletedPins).extractingResultOf("isDeleted") - .containsOnly(true); - } - - @Test - @DisplayName("다른 토픽에 존재하는 핀들이여도, Member ID로 모든 핀을 soft-delete 할 수 있다.") - void deleteAllByMemberIdInOtherTopics_Success() { - //given - Topic otherTopic = TopicFixture.createByName("otherTopic", member); - topicRepository.save(otherTopic); - - for (int i = 0; i < 10; i++) { - pinRepository.save(PinFixture.create(location, topic, member)); - pinRepository.save(PinFixture.create(location, otherTopic, member)); - } - - //when - assertThat(member.getCreatedPins()).hasSize(20) - .extractingResultOf("isDeleted") - .containsOnly(false); - pinRepository.deleteAllByMemberId(member.getId()); - - //then - List deletedPins = pinRepository.findAllByCreatorId(member.getId()); - assertThat(deletedPins).extractingResultOf("isDeleted") - .containsOnly(true); - } - } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/topic/domain/TopicRepositoryTest.java b/backend/src/test/java/com/mapbefine/mapbefine/topic/domain/TopicRepositoryTest.java index 2a783b70..3280df30 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/topic/domain/TopicRepositoryTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/topic/domain/TopicRepositoryTest.java @@ -6,8 +6,6 @@ import com.mapbefine.mapbefine.member.domain.Member; import com.mapbefine.mapbefine.member.domain.MemberRepository; import com.mapbefine.mapbefine.member.domain.Role; -import com.mapbefine.mapbefine.topic.TopicFixture; -import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -27,14 +25,24 @@ class TopicRepositoryTest { @BeforeEach void setUp() { - member = memberRepository.save(MemberFixture.create("member", "member@naver.com", Role.USER)); + member = MemberFixture.create("member", "member@naver.com", Role.USER); + memberRepository.save(member); } @Test @DisplayName("토픽을 삭제하면, soft-deleting 된다.") void deleteById_Success() { //given - Topic topic = topicRepository.save(TopicFixture.createByName("Topic", member)); + Topic topic = Topic.createTopicAssociatedWithCreator( + "토픽", + "토픽설명", + "https://example.com/image.jpg", + Publicity.PUBLIC, + PermissionType.ALL_MEMBERS, + member + ); + + topicRepository.save(topic); assertThat(topic.isDeleted()).isFalse(); @@ -46,24 +54,4 @@ void deleteById_Success() { assertThat(deletedTopic.isDeleted()).isTrue(); } - @Test - @DisplayName("Member Id로 모든 토픽을 삭제하면, soft-deleting 된다.") - void deleteAllByMemberId_Success() { - //given - for (int i = 0; i < 10; i++) { - topicRepository.save(TopicFixture.createByName("topic" + i, member)); - } - assertThat(member.getCreatedTopics()).hasSize(10) - .extractingResultOf("isDeleted") - .containsOnly(false); - - //when - topicRepository.deleteAllByMemberId(member.getId()); - - //then - List deletedTopics = topicRepository.findAllByCreatorId(member.getId()); - assertThat(deletedTopics).hasSize(10) - .extractingResultOf("isDeleted") - .containsOnly(true); - } } From cd84646f86e44903346fd3857d4764804379ea2e Mon Sep 17 00:00:00 2001 From: zun <50602742+cpot5620@users.noreply.github.com> Date: Fri, 15 Sep 2023 15:11:43 +0900 Subject: [PATCH 16/34] =?UTF-8?q?[BE]=20Feat/#378=20Admin=20API=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#415)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 전체 회원 조회 기능 구현 * feat: 회원 삭제(탈퇴) 기능 구현 * feat: 회원 삭제(탈퇴)시 Pin/Topic Soft-deleting 구현 * refactor: Admin DTO 분리 * feat: Member 상세 정보 조회 기능 구현 * feat: Topic 삭제 및 이미지 삭제 기능 구현 * feat: Pin 삭제 및 이미지 삭제 기능 구현 * feat: Admin API 구현 * refactor: Member 상태(차단, 탈퇴 등) 필드에 따른 로그인 로직 수정 * refactor: @SqlDelete 삭제 및 JPQL 대체 * feat: AdminInterceptor 구현 * test: Repository soft-deleting 테스트 구현 * test: AdminQueryService 테스트 구현 * test: AdminCommandService 테스트 구현 * test: AdminController Restdocs 테스트 구현 * test: AdminInterceptor Mocking * test: 통합 테스트 구현 * refactor: 오탈자 수정 * refactor: Auth 관련 예외 클래스 추가 * refactor: 불필요한 메서드 제거 * refactor: findMemberById 예외 수정 * test: GithubActions 실패 테스트 수정 * refactor: isAdmin() 메서드 추가 * refactor: 회원 삭제(탈퇴)시, 추가 정보(즐겨찾기 등) 삭제 * refactor: Member status 기본값 설정 * remove: Member status 기본값 설정 삭제 --- backend/src/docs/asciidoc/admin.adoc | 29 ++ backend/src/docs/asciidoc/index.adoc | 1 + .../application/AdminCommandService.java | 127 ++++++++ .../admin/application/AdminQueryService.java | 61 ++++ .../admin/dto/AdminMemberDetailResponse.java | 48 +++ .../admin/dto/AdminMemberResponse.java | 27 ++ .../admin/presentation/AdminController.java | 78 +++++ .../atlas/domain/AtlasRepository.java | 1 + .../auth/application/AuthService.java | 12 + .../mapbefine/auth/domain/AuthMember.java | 3 + .../mapbefine/auth/domain/member/Admin.java | 6 + .../mapbefine/auth/domain/member/Guest.java | 5 + .../mapbefine/auth/domain/member/User.java | 6 + .../auth/exception/AuthErrorCode.java | 21 ++ .../auth/exception/AuthException.java | 23 ++ .../mapbefine/common/config/AuthConfig.java | 9 +- .../interceptor/AdminAuthInterceptor.java | 71 +++++ .../common/interceptor/AuthInterceptor.java | 13 +- .../application/MemberQueryService.java | 1 + .../mapbefine/member/domain/Member.java | 22 +- .../mapbefine/member/domain/MemberInfo.java | 21 +- .../member/domain/MemberRepository.java | 3 + .../mapbefine/member/domain/Status.java | 15 + .../member/exception/MemberErrorCode.java | 1 + .../member/exception/MemberException.java | 7 + .../oauth/application/OauthService.java | 12 + .../AuthCodeRequestUrlProviderComposite.java | 2 +- .../domain/OauthMemberClientComposite.java | 2 +- ...OathException.java => OauthException.java} | 2 +- .../domain/PermissionRepository.java | 1 + .../exception/PermissionErrorCode.java | 3 +- .../pin/application/PinQueryService.java | 2 +- .../pin/domain/PinImageRepository.java | 5 +- .../mapbefine/pin/domain/PinRepository.java | 6 +- .../topic/application/TopicQueryService.java | 4 +- .../mapbefine/topic/domain/Topic.java | 4 + .../mapbefine/topic/domain/TopicInfo.java | 9 + .../topic/domain/TopicRepository.java | 6 +- .../mapbefine/admin/AdminIntegrationTest.java | 249 ++++++++++++++++ .../application/AdminCommandServiceTest.java | 280 ++++++++++++++++++ .../application/AdminQueryServiceTest.java | 135 +++++++++ .../presentation/AdminControllerTest.java | 172 +++++++++++ .../mapbefine/member/MemberFixture.java | 8 + .../member/domain/MemberInfoTest.java | 14 +- .../mapbefine/member/domain/MemberTest.java | 5 +- .../pin/domain/PinImageRepositoryTest.java | 27 ++ .../pin/domain/PinRepositoryTest.java | 50 +++- .../topic/domain/TopicRepositoryTest.java | 36 ++- 48 files changed, 1598 insertions(+), 47 deletions(-) create mode 100644 backend/src/docs/asciidoc/admin.adoc create mode 100644 backend/src/main/java/com/mapbefine/mapbefine/admin/application/AdminCommandService.java create mode 100644 backend/src/main/java/com/mapbefine/mapbefine/admin/application/AdminQueryService.java create mode 100644 backend/src/main/java/com/mapbefine/mapbefine/admin/dto/AdminMemberDetailResponse.java create mode 100644 backend/src/main/java/com/mapbefine/mapbefine/admin/dto/AdminMemberResponse.java create mode 100644 backend/src/main/java/com/mapbefine/mapbefine/admin/presentation/AdminController.java create mode 100644 backend/src/main/java/com/mapbefine/mapbefine/auth/exception/AuthErrorCode.java create mode 100644 backend/src/main/java/com/mapbefine/mapbefine/auth/exception/AuthException.java create mode 100644 backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AdminAuthInterceptor.java create mode 100644 backend/src/main/java/com/mapbefine/mapbefine/member/domain/Status.java rename backend/src/main/java/com/mapbefine/mapbefine/oauth/exception/{OathException.java => OauthException.java} (94%) create mode 100644 backend/src/test/java/com/mapbefine/mapbefine/admin/AdminIntegrationTest.java create mode 100644 backend/src/test/java/com/mapbefine/mapbefine/admin/application/AdminCommandServiceTest.java create mode 100644 backend/src/test/java/com/mapbefine/mapbefine/admin/application/AdminQueryServiceTest.java create mode 100644 backend/src/test/java/com/mapbefine/mapbefine/admin/presentation/AdminControllerTest.java diff --git a/backend/src/docs/asciidoc/admin.adoc b/backend/src/docs/asciidoc/admin.adoc new file mode 100644 index 00000000..3d2137c4 --- /dev/null +++ b/backend/src/docs/asciidoc/admin.adoc @@ -0,0 +1,29 @@ +== 관리자 기능 + +=== 전체 회원 조회 + +operation::admin-controller-test/find-all-member-details[snippets='http-request,http-response'] + +=== 회원 상세 조회 + +operation::admin-controller-test/find-member[snippets='http-request,http-response'] + +=== 회원 차단(삭제) + +operation::admin-controller-test/delete-member[snippets='http-request,http-response'] + +=== 토픽 삭제 + +operation::admin-controller-test/delete-topic[snippets='http-request,http-response'] + +=== 토픽 이미지 삭제 + +operation::admin-controller-test/delete-topic-image[snippets='http-request,http-response'] + +=== 핀 삭제 + +operation::admin-controller-test/delete-pin[snippets='http-request,http-response'] + +=== 핀 이미지 삭제 + +operation::admin-controller-test/delete-pin-image[snippets='http-request,http-response'] \ No newline at end of file diff --git a/backend/src/docs/asciidoc/index.adoc b/backend/src/docs/asciidoc/index.adoc index c15272d2..a90d89df 100644 --- a/backend/src/docs/asciidoc/index.adoc +++ b/backend/src/docs/asciidoc/index.adoc @@ -15,3 +15,4 @@ include::member.adoc[] include::permission.adoc[] include::oauth.adoc[] include::bookmark.adoc[] +include::admin.adoc[] diff --git a/backend/src/main/java/com/mapbefine/mapbefine/admin/application/AdminCommandService.java b/backend/src/main/java/com/mapbefine/mapbefine/admin/application/AdminCommandService.java new file mode 100644 index 00000000..0dfaf518 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/admin/application/AdminCommandService.java @@ -0,0 +1,127 @@ +package com.mapbefine.mapbefine.admin.application; + +import static com.mapbefine.mapbefine.permission.exception.PermissionErrorCode.PERMISSION_FORBIDDEN_BY_NOT_ADMIN; +import static com.mapbefine.mapbefine.topic.exception.TopicErrorCode.TOPIC_NOT_FOUND; + +import com.mapbefine.mapbefine.atlas.domain.AtlasRepository; +import com.mapbefine.mapbefine.auth.domain.AuthMember; +import com.mapbefine.mapbefine.bookmark.domain.BookmarkRepository; +import com.mapbefine.mapbefine.member.domain.Member; +import com.mapbefine.mapbefine.member.domain.MemberRepository; +import com.mapbefine.mapbefine.member.domain.Role; +import com.mapbefine.mapbefine.member.domain.Status; +import com.mapbefine.mapbefine.permission.domain.PermissionRepository; +import com.mapbefine.mapbefine.permission.exception.PermissionException.PermissionForbiddenException; +import com.mapbefine.mapbefine.pin.domain.Pin; +import com.mapbefine.mapbefine.pin.domain.PinImageRepository; +import com.mapbefine.mapbefine.pin.domain.PinRepository; +import com.mapbefine.mapbefine.topic.domain.Topic; +import com.mapbefine.mapbefine.topic.domain.TopicRepository; +import com.mapbefine.mapbefine.topic.exception.TopicException; +import java.util.List; +import java.util.NoSuchElementException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +public class AdminCommandService { + + private final MemberRepository memberRepository; + private final TopicRepository topicRepository; + private final PinRepository pinRepository; + private final PinImageRepository pinImageRepository; + private final PermissionRepository permissionRepository; + private final AtlasRepository atlasRepository; + private final BookmarkRepository bookmarkRepository; + + public AdminCommandService( + MemberRepository memberRepository, + TopicRepository topicRepository, + PinRepository pinRepository, + PinImageRepository pinImageRepository, + PermissionRepository permissionRepository, + AtlasRepository atlasRepository, + BookmarkRepository bookmarkRepository + ) { + this.memberRepository = memberRepository; + this.topicRepository = topicRepository; + this.pinRepository = pinRepository; + this.pinImageRepository = pinImageRepository; + this.permissionRepository = permissionRepository; + this.atlasRepository = atlasRepository; + this.bookmarkRepository = bookmarkRepository; + } + + public void blockMember(AuthMember authMember, Long memberId) { + validateAdminPermission(authMember); + + Member member = findMemberById(memberId); + member.updateStatus(Status.BLOCKED); + + deleteAllRelatedMember(member); + } + + private void deleteAllRelatedMember(Member member) { + List pinIds = extractPinIdsByMember(member); + Long memberId = member.getId(); + + pinImageRepository.deleteAllByPinIds(pinIds); + topicRepository.deleteAllByMemberId(memberId); + pinRepository.deleteAllByMemberId(memberId); + permissionRepository.deleteAllByMemberId(memberId); + atlasRepository.deleteAllByMemberId(memberId); + bookmarkRepository.deleteAllByMemberId(memberId); + } + + private Member findMemberById(Long id) { + return memberRepository.findById(id) + .orElseThrow(() -> new NoSuchElementException("findMemberByAuthMember; member not found; id=" + id)); + } + + private void validateAdminPermission(AuthMember authMember) { + if (authMember.isRole(Role.ADMIN)) { + return; + } + + throw new PermissionForbiddenException(PERMISSION_FORBIDDEN_BY_NOT_ADMIN); + } + + private List extractPinIdsByMember(Member member) { + return member.getCreatedPins() + .stream() + .map(Pin::getId) + .toList(); + } + + public void deleteTopic(AuthMember authMember, Long topicId) { + validateAdminPermission(authMember); + + topicRepository.deleteById(topicId); + } + + public void deleteTopicImage(AuthMember authMember, Long topicId) { + validateAdminPermission(authMember); + + Topic topic = findTopicById(topicId); + topic.removeImage(); + } + + private Topic findTopicById(Long topicId) { + return topicRepository.findById(topicId) + .orElseThrow(() -> new TopicException.TopicNotFoundException(TOPIC_NOT_FOUND, List.of(topicId))); + } + + public void deletePin(AuthMember authMember, Long pinId) { + validateAdminPermission(authMember); + + pinRepository.deleteById(pinId); + } + + public void deletePinImage(AuthMember authMember, Long pinImageId) { + validateAdminPermission(authMember); + + pinImageRepository.deleteById(pinImageId); + } + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/admin/application/AdminQueryService.java b/backend/src/main/java/com/mapbefine/mapbefine/admin/application/AdminQueryService.java new file mode 100644 index 00000000..f7ca346a --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/admin/application/AdminQueryService.java @@ -0,0 +1,61 @@ +package com.mapbefine.mapbefine.admin.application; + +import com.mapbefine.mapbefine.admin.dto.AdminMemberDetailResponse; +import com.mapbefine.mapbefine.admin.dto.AdminMemberResponse; +import com.mapbefine.mapbefine.auth.domain.AuthMember; +import com.mapbefine.mapbefine.member.domain.Member; +import com.mapbefine.mapbefine.member.domain.MemberRepository; +import com.mapbefine.mapbefine.member.domain.Role; +import com.mapbefine.mapbefine.permission.exception.PermissionErrorCode; +import com.mapbefine.mapbefine.permission.exception.PermissionException.PermissionForbiddenException; +import com.mapbefine.mapbefine.pin.domain.Pin; +import com.mapbefine.mapbefine.topic.domain.Topic; +import java.util.List; +import java.util.NoSuchElementException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +public class AdminQueryService { + + private final MemberRepository memberRepository; + + public AdminQueryService(MemberRepository memberRepository) { + this.memberRepository = memberRepository; + } + + public List findAllMemberDetails(AuthMember authMember) { + validateAdminPermission(authMember); + + List members = memberRepository.findAllByMemberInfoRole(Role.USER); + + return members.stream() + .map(AdminMemberResponse::from) + .toList(); + } + + private Member findMemberById(Long id) { + return memberRepository.findById(id) + .orElseThrow(() -> new NoSuchElementException("findMemberByAuthMember; member not found; id=" + id)); + } + + private void validateAdminPermission(AuthMember authMember) { + if (authMember.isRole(Role.ADMIN)) { + return; + } + + throw new PermissionForbiddenException(PermissionErrorCode.PERMISSION_FORBIDDEN_BY_NOT_ADMIN); + } + + public AdminMemberDetailResponse findMemberDetail(AuthMember authMember, Long memberId) { + validateAdminPermission(authMember); + + Member findMember = findMemberById(memberId); + List topics = findMember.getCreatedTopics(); + List pins = findMember.getCreatedPins(); + + return AdminMemberDetailResponse.of(findMember, topics, pins); + } + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/admin/dto/AdminMemberDetailResponse.java b/backend/src/main/java/com/mapbefine/mapbefine/admin/dto/AdminMemberDetailResponse.java new file mode 100644 index 00000000..fbd5e04d --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/admin/dto/AdminMemberDetailResponse.java @@ -0,0 +1,48 @@ +package com.mapbefine.mapbefine.admin.dto; + +import com.mapbefine.mapbefine.member.domain.Member; +import com.mapbefine.mapbefine.member.domain.MemberInfo; +import com.mapbefine.mapbefine.pin.domain.Pin; +import com.mapbefine.mapbefine.pin.dto.response.PinResponse; +import com.mapbefine.mapbefine.topic.domain.Topic; +import com.mapbefine.mapbefine.topic.dto.response.TopicResponse; +import java.time.LocalDateTime; +import java.util.List; + +public record AdminMemberDetailResponse( + Long id, + String nickName, + String email, + String imageUrl, + List topics, + List pins, + LocalDateTime updatedAt +) { + + // TODO: 2023/09/12 topics, pins 모두 member를 통해 얻어올 수 있다. Service에서 꺼내서 넘겨줄 것인가 ? 아니면 DTO에서 꺼내올 것인가 ? + public static AdminMemberDetailResponse of( + Member member, + List topics, + List pins + ) { + MemberInfo memberInfo = member.getMemberInfo(); + List topicResponses = topics.stream() + .map(TopicResponse::fromGuestQuery) + .toList(); + List pinResponses = pins.stream() + .map(PinResponse::from) + .toList(); + + return new AdminMemberDetailResponse( + member.getId(), + memberInfo.getNickName(), + memberInfo.getEmail(), + memberInfo.getImageUrl(), + topicResponses, + pinResponses, + member.getUpdatedAt() + ); + } + + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/admin/dto/AdminMemberResponse.java b/backend/src/main/java/com/mapbefine/mapbefine/admin/dto/AdminMemberResponse.java new file mode 100644 index 00000000..f041f8f1 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/admin/dto/AdminMemberResponse.java @@ -0,0 +1,27 @@ +package com.mapbefine.mapbefine.admin.dto; + +import com.mapbefine.mapbefine.member.domain.Member; +import com.mapbefine.mapbefine.member.domain.MemberInfo; +import java.time.LocalDateTime; + +public record AdminMemberResponse( + Long id, + String nickName, + String email, + String imageUrl, + LocalDateTime updatedAt +) { + + public static AdminMemberResponse from(Member member) { + MemberInfo memberInfo = member.getMemberInfo(); + + return new AdminMemberResponse( + member.getId(), + memberInfo.getNickName(), + memberInfo.getEmail(), + memberInfo.getImageUrl(), + member.getUpdatedAt() + ); + } + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/admin/presentation/AdminController.java b/backend/src/main/java/com/mapbefine/mapbefine/admin/presentation/AdminController.java new file mode 100644 index 00000000..2a34fec1 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/admin/presentation/AdminController.java @@ -0,0 +1,78 @@ +package com.mapbefine.mapbefine.admin.presentation; + +import com.mapbefine.mapbefine.admin.application.AdminCommandService; +import com.mapbefine.mapbefine.admin.application.AdminQueryService; +import com.mapbefine.mapbefine.admin.dto.AdminMemberDetailResponse; +import com.mapbefine.mapbefine.admin.dto.AdminMemberResponse; +import com.mapbefine.mapbefine.auth.domain.AuthMember; +import java.util.List; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/admin") +public class AdminController { + + private final AdminQueryService adminQueryService; + private final AdminCommandService adminCommandService; + + + public AdminController(AdminQueryService adminQueryService, AdminCommandService adminCommandService) { + this.adminQueryService = adminQueryService; + this.adminCommandService = adminCommandService; + } + + @GetMapping("/members") + public ResponseEntity> findAllMembers(AuthMember authMember) { + List responses = adminQueryService.findAllMemberDetails(authMember); + + return ResponseEntity.ok(responses); + } + + @DeleteMapping("/members/{memberId}") + public ResponseEntity deleteMember(AuthMember authMember, @PathVariable Long memberId) { + adminCommandService.blockMember(authMember, memberId); + + return ResponseEntity.noContent().build(); + } + + @GetMapping("/members/{memberId}") + public ResponseEntity findMember(AuthMember authMember, @PathVariable Long memberId) { + AdminMemberDetailResponse response = adminQueryService.findMemberDetail(authMember, memberId); + + return ResponseEntity.ok(response); + } + + @DeleteMapping("/topics/{topicId}") + public ResponseEntity deleteTopic(AuthMember authMember, @PathVariable Long topicId) { + adminCommandService.deleteTopic(authMember, topicId); + + return ResponseEntity.noContent().build(); + } + + @DeleteMapping("/topics/{topicId}/images") + public ResponseEntity deleteTopicImage(AuthMember authMember, @PathVariable Long topicId) { + adminCommandService.deleteTopicImage(authMember, topicId); + + return ResponseEntity.noContent().build(); + } + + @DeleteMapping("/pins/{pinId}") + public ResponseEntity deletePin(AuthMember authMember, @PathVariable Long pinId) { + adminCommandService.deletePin(authMember, pinId); + + return ResponseEntity.noContent().build(); + } + + @DeleteMapping("/pins/images/{imageId}") + public ResponseEntity deletePinImage(AuthMember authMember, @PathVariable Long imageId) { + adminCommandService.deletePinImage(authMember, imageId); + + return ResponseEntity.noContent().build(); + } + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/atlas/domain/AtlasRepository.java b/backend/src/main/java/com/mapbefine/mapbefine/atlas/domain/AtlasRepository.java index 242d6992..bd37a9ba 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/atlas/domain/AtlasRepository.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/atlas/domain/AtlasRepository.java @@ -10,4 +10,5 @@ public interface AtlasRepository extends JpaRepository { void deleteByMemberIdAndTopicId(Long memberId, Long topicId); + void deleteAllByMemberId(Long memberId); } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/application/AuthService.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/application/AuthService.java index 538e1244..11b2e343 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/auth/application/AuthService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/application/AuthService.java @@ -8,6 +8,7 @@ import com.mapbefine.mapbefine.topic.domain.Topic; import java.util.List; import java.util.Objects; +import java.util.Optional; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -60,4 +61,15 @@ private List getCreatedTopics(Member member) { .toList(); } + public boolean isAdmin(Long memberId) { + if (Objects.isNull(memberId)) { + return false; + } + + Optional member = memberRepository.findById(memberId); + + return member.map(Member::isAdmin) + .orElse(false); + } + } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/AuthMember.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/AuthMember.java index c795b1ec..d27f7cc2 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/AuthMember.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/AuthMember.java @@ -1,5 +1,6 @@ package com.mapbefine.mapbefine.auth.domain; +import com.mapbefine.mapbefine.member.domain.Role; import com.mapbefine.mapbefine.topic.domain.Topic; import java.util.List; @@ -27,6 +28,8 @@ protected AuthMember( public abstract boolean canPinCreateOrUpdate(Topic topic); + public abstract boolean isRole(Role role); + public Long getMemberId() { return memberId; } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/Admin.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/Admin.java index 3d97522c..f6a54648 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/Admin.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/Admin.java @@ -1,6 +1,7 @@ package com.mapbefine.mapbefine.auth.domain.member; import com.mapbefine.mapbefine.auth.domain.AuthMember; +import com.mapbefine.mapbefine.member.domain.Role; import com.mapbefine.mapbefine.topic.domain.Topic; import java.util.Collections; @@ -34,4 +35,9 @@ public boolean canPinCreateOrUpdate(Topic topic) { return true; } + @Override + public boolean isRole(Role role) { + return Role.ADMIN == role; + } + } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/Guest.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/Guest.java index 6c8c0edc..dcd8dbeb 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/Guest.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/Guest.java @@ -1,6 +1,7 @@ package com.mapbefine.mapbefine.auth.domain.member; import com.mapbefine.mapbefine.auth.domain.AuthMember; +import com.mapbefine.mapbefine.member.domain.Role; import com.mapbefine.mapbefine.topic.domain.Topic; import com.mapbefine.mapbefine.topic.domain.TopicStatus; import java.util.Collections; @@ -36,4 +37,8 @@ public boolean canPinCreateOrUpdate(Topic topic) { return false; } + @Override + public boolean isRole(Role role) { + return Role.GUEST == role; + } } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/User.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/User.java index d2444d49..ed2ea7b8 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/User.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/User.java @@ -1,6 +1,7 @@ package com.mapbefine.mapbefine.auth.domain.member; import com.mapbefine.mapbefine.auth.domain.AuthMember; +import com.mapbefine.mapbefine.member.domain.Role; import com.mapbefine.mapbefine.topic.domain.Topic; import com.mapbefine.mapbefine.topic.domain.TopicStatus; import java.util.List; @@ -54,4 +55,9 @@ private boolean hasPermission(Long topicId) { return createdTopic.contains(topicId) || topicsWithPermission.contains(topicId); } + @Override + public boolean isRole(Role role) { + return Role.USER == role; + } + } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/exception/AuthErrorCode.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/exception/AuthErrorCode.java new file mode 100644 index 00000000..7cbf7c20 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/exception/AuthErrorCode.java @@ -0,0 +1,21 @@ +package com.mapbefine.mapbefine.auth.exception; + +import lombok.Getter; + +@Getter +public enum AuthErrorCode { + ILLEGAL_MEMBER_ID("03100", "로그인에 실패하였습니다."), + ILLEGAL_TOKEN("03101", "로그인에 실패하였습니다."), + FORBIDDEN_ADMIN_ACCESS("03102", "로그인에 실패하였습니다."), + BLOCKING_MEMBER_ACCESS("03103", "로그인에 실패하였습니다."), + ; + + private final String code; + private final String message; + + AuthErrorCode(String code, String message) { + this.code = code; + this.message = message; + } + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/exception/AuthException.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/exception/AuthException.java new file mode 100644 index 00000000..4f384f9e --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/exception/AuthException.java @@ -0,0 +1,23 @@ +package com.mapbefine.mapbefine.auth.exception; + +import com.mapbefine.mapbefine.common.exception.ErrorCode; +import com.mapbefine.mapbefine.common.exception.ForbiddenException; +import com.mapbefine.mapbefine.common.exception.UnauthorizedException; + +public class AuthException { + + public static class AuthUnauthorizedException extends UnauthorizedException { + + public AuthUnauthorizedException(AuthErrorCode errorCode) { + super(new ErrorCode<>(errorCode.getCode(), errorCode.getMessage())); + } + } + + public static class AuthForbiddenException extends ForbiddenException { + + public AuthForbiddenException(AuthErrorCode errorCode) { + super(new ErrorCode<>(errorCode.getCode(), errorCode.getMessage())); + } + } + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/common/config/AuthConfig.java b/backend/src/main/java/com/mapbefine/mapbefine/common/config/AuthConfig.java index 02cb0062..c972f561 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/common/config/AuthConfig.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/common/config/AuthConfig.java @@ -1,5 +1,6 @@ package com.mapbefine.mapbefine.common.config; +import com.mapbefine.mapbefine.common.interceptor.AdminAuthInterceptor; import com.mapbefine.mapbefine.common.interceptor.AuthInterceptor; import com.mapbefine.mapbefine.common.resolver.AuthArgumentResolver; import java.util.List; @@ -11,20 +12,26 @@ @Configuration public class AuthConfig implements WebMvcConfigurer { + private final AdminAuthInterceptor adminAuthInterceptor; private final AuthInterceptor authInterceptor; private final AuthArgumentResolver authArgumentResolver; public AuthConfig( + AdminAuthInterceptor adminAuthInterceptor, AuthInterceptor authInterceptor, AuthArgumentResolver authArgumentResolver ) { + this.adminAuthInterceptor = adminAuthInterceptor; this.authInterceptor = authInterceptor; this.authArgumentResolver = authArgumentResolver; } @Override public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(authInterceptor); + registry.addInterceptor(authInterceptor) + .excludePathPatterns("/admin/**"); + registry.addInterceptor(adminAuthInterceptor) + .addPathPatterns("/admin/**"); } @Override diff --git a/backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AdminAuthInterceptor.java b/backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AdminAuthInterceptor.java new file mode 100644 index 00000000..9f5c8424 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AdminAuthInterceptor.java @@ -0,0 +1,71 @@ +package com.mapbefine.mapbefine.common.interceptor; + +import com.mapbefine.mapbefine.auth.application.AuthService; +import com.mapbefine.mapbefine.auth.dto.AuthInfo; +import com.mapbefine.mapbefine.auth.exception.AuthErrorCode; +import com.mapbefine.mapbefine.auth.exception.AuthException; +import com.mapbefine.mapbefine.auth.infrastructure.AuthorizationExtractor; +import com.mapbefine.mapbefine.auth.infrastructure.JwtTokenProvider; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.util.Objects; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; + +@Component +public class AdminAuthInterceptor implements HandlerInterceptor { + + private final AuthorizationExtractor authorizationExtractor; + private final AuthService authService; + private final JwtTokenProvider jwtTokenProvider; + + public AdminAuthInterceptor( + AuthorizationExtractor authorizationExtractor, + AuthService authService, + JwtTokenProvider jwtTokenProvider + ) { + this.authorizationExtractor = authorizationExtractor; + this.authService = authService; + this.jwtTokenProvider = jwtTokenProvider; + } + + @Override + public boolean preHandle( + HttpServletRequest request, + HttpServletResponse response, + Object handler + ) throws Exception { + if (!(handler instanceof HandlerMethod)) { + return true; + } + + Long memberId = extractMemberIdFromToken(request); + + validateAdmin(memberId); + request.setAttribute("memberId", memberId); + + return true; + } + + private Long extractMemberIdFromToken(HttpServletRequest request) { + AuthInfo authInfo = authorizationExtractor.extract(request); + if (Objects.isNull(authInfo)) { + return null; + } + String accessToken = authInfo.accessToken(); + if (jwtTokenProvider.validateToken(accessToken)) { + return Long.parseLong(jwtTokenProvider.getPayload(accessToken)); + } + throw new AuthException.AuthUnauthorizedException(AuthErrorCode.ILLEGAL_TOKEN); + } + + private void validateAdmin(Long memberId) { + if (authService.isAdmin(memberId)) { + return; + } + + throw new AuthException.AuthForbiddenException(AuthErrorCode.FORBIDDEN_ADMIN_ACCESS); + } + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AuthInterceptor.java b/backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AuthInterceptor.java index 32a52d38..01c98c73 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AuthInterceptor.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AuthInterceptor.java @@ -3,10 +3,11 @@ import com.mapbefine.mapbefine.auth.application.AuthService; import com.mapbefine.mapbefine.auth.domain.AuthMember; import com.mapbefine.mapbefine.auth.dto.AuthInfo; +import com.mapbefine.mapbefine.auth.exception.AuthErrorCode; +import com.mapbefine.mapbefine.auth.exception.AuthException; +import com.mapbefine.mapbefine.auth.exception.AuthException.AuthUnauthorizedException; import com.mapbefine.mapbefine.auth.infrastructure.AuthorizationExtractor; import com.mapbefine.mapbefine.auth.infrastructure.JwtTokenProvider; -import com.mapbefine.mapbefine.common.exception.ErrorCode; -import com.mapbefine.mapbefine.common.exception.UnauthorizedException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.util.Arrays; @@ -18,10 +19,6 @@ @Component public class AuthInterceptor implements HandlerInterceptor { - private static final String UNAUTHORIZED_ERROR_MESSAGE = "로그인에 실패하였습니다."; - private static final ErrorCode ILLEGAL_MEMBER_ID = new ErrorCode("03100", UNAUTHORIZED_ERROR_MESSAGE); - private static final ErrorCode ILLEGAL_TOKEN = new ErrorCode("03101", UNAUTHORIZED_ERROR_MESSAGE); - private final AuthorizationExtractor authorizationExtractor; private final AuthService authService; private final JwtTokenProvider jwtTokenProvider; @@ -65,7 +62,7 @@ private void validateMember(Long memberId) { return; } - throw new UnauthorizedException(ILLEGAL_MEMBER_ID); + throw new AuthUnauthorizedException(AuthErrorCode.ILLEGAL_MEMBER_ID); } private boolean isAuthMemberNotRequired(HandlerMethod handlerMethod) { @@ -86,7 +83,7 @@ private Long extractMemberIdFromToken(HttpServletRequest request) { } String accessToken = authInfo.accessToken(); if (!jwtTokenProvider.validateToken(accessToken)) { - throw new UnauthorizedException(ILLEGAL_TOKEN); + throw new AuthException.AuthUnauthorizedException(AuthErrorCode.ILLEGAL_TOKEN); } return Long.parseLong(jwtTokenProvider.getPayload(accessToken)); } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/member/application/MemberQueryService.java b/backend/src/main/java/com/mapbefine/mapbefine/member/application/MemberQueryService.java index c032cf36..df5296f9 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/member/application/MemberQueryService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/member/application/MemberQueryService.java @@ -37,6 +37,7 @@ private Member findMemberById(Long id) { .orElseThrow(() -> new MemberNotFoundException(MemberErrorCode.MEMBER_NOT_FOUND, id)); } + // TODO: 2023/09/13 차단된 or 탈퇴한 사용자 필터링 필요 public List findAll() { return memberRepository.findAll() .stream() diff --git a/backend/src/main/java/com/mapbefine/mapbefine/member/domain/Member.java b/backend/src/main/java/com/mapbefine/mapbefine/member/domain/Member.java index 71de6abf..1d64325e 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/member/domain/Member.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/member/domain/Member.java @@ -64,13 +64,15 @@ public static Member of( String email, String imageUrl, Role role, + Status status, OauthId oauthId ) { MemberInfo memberInfo = MemberInfo.of( nickName, email, imageUrl, - role + role, + status ); return new Member(memberInfo, oauthId); @@ -85,7 +87,7 @@ public static Member ofRandomNickname( ) { String nickName = createNickname(nickname); - return Member.of(nickName, email, imageUrl, role, oauthId); + return Member.of(nickName, email, imageUrl, role, Status.NORMAL, oauthId); } private static String createNickname(String nickname) { @@ -111,7 +113,8 @@ public void update( nickName, email, imageUrl, - memberInfo.getRole() + memberInfo.getRole(), + memberInfo.getStatus() ); } @@ -149,4 +152,17 @@ public List getTopicsWithPermissions() { .toList(); } + public boolean isNormalStatus() { + return memberInfo.getStatus() == Status.NORMAL; + } + + public void updateStatus(Status status) { + memberInfo = MemberInfo.of( + memberInfo.getNickName(), + memberInfo.getEmail(), + memberInfo.getImageUrl(), + memberInfo.getRole(), + status + ); + } } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/member/domain/MemberInfo.java b/backend/src/main/java/com/mapbefine/mapbefine/member/domain/MemberInfo.java index c15054c3..08276157 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/member/domain/MemberInfo.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/member/domain/MemberInfo.java @@ -38,33 +38,42 @@ public class MemberInfo { @Column(nullable = false) private Role role; + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private Status status; + private MemberInfo( String nickName, String email, Image imageUrl, - Role role + Role role, + Status status ) { this.nickName = nickName; this.email = email; this.imageUrl = imageUrl; this.role = role; + this.status = status; } public static MemberInfo of( String nickName, String email, String imageUrl, - Role role + Role role, + Status status ) { validateNickName(nickName); validateEmail(email); validateRole(role); + validateStatus(status); return new MemberInfo( nickName, email, Image.from(imageUrl), - role + role, + status ); } @@ -93,6 +102,12 @@ private static void validateRole(Role role) { } } + private static void validateStatus(Status status) { + if (Objects.isNull(status)) { + throw new IllegalArgumentException("validateStatus; member status is null;"); + } + } + public String getImageUrl() { return imageUrl.getImageUrl(); } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/member/domain/MemberRepository.java b/backend/src/main/java/com/mapbefine/mapbefine/member/domain/MemberRepository.java index 16b57c05..65125aa1 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/member/domain/MemberRepository.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/member/domain/MemberRepository.java @@ -1,5 +1,6 @@ package com.mapbefine.mapbefine.member.domain; +import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; @@ -13,4 +14,6 @@ public interface MemberRepository extends JpaRepository { Optional findByOauthId(OauthId oauthId); + List findAllByMemberInfoRole(Role role); + } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/member/domain/Status.java b/backend/src/main/java/com/mapbefine/mapbefine/member/domain/Status.java new file mode 100644 index 00000000..bf594c09 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/member/domain/Status.java @@ -0,0 +1,15 @@ +package com.mapbefine.mapbefine.member.domain; + +public enum Status { + NORMAL("STATUS_NORMAL", "정상 사용자"), + DELETE("STATAUS_DELETE", "탈퇴한 사용자"), + BLOCKED("STATUS_BLOCKED", "차단된 사용자"); + + private final String key; + private final String title; + + Status(String key, String title) { + this.key = key; + this.title = title; + } +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/member/exception/MemberErrorCode.java b/backend/src/main/java/com/mapbefine/mapbefine/member/exception/MemberErrorCode.java index c02d2ab1..b0c5d034 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/member/exception/MemberErrorCode.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/member/exception/MemberErrorCode.java @@ -9,6 +9,7 @@ public enum MemberErrorCode { ILLEGAL_NICKNAME_LENGTH("05001", "닉네임 길이는 최소 1 자에서 20자여야 합니다."), ILLEGAL_EMAIL_NULL("05002", "이메일은 필수로 입력해야합니다."), ILLEGAL_EMAIL_PATTERN("05003", "올바르지 않은 이메일 형식입니다."), + FORBIDDEN_MEMBER_STATUS("05100", "탈퇴 혹은 차단된 회원입니다."), MEMBER_NOT_FOUND("05400", "존재하지 않는 회원입니다."), ; diff --git a/backend/src/main/java/com/mapbefine/mapbefine/member/exception/MemberException.java b/backend/src/main/java/com/mapbefine/mapbefine/member/exception/MemberException.java index ec98e9d0..89a68948 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/member/exception/MemberException.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/member/exception/MemberException.java @@ -2,6 +2,7 @@ import com.mapbefine.mapbefine.common.exception.BadRequestException; import com.mapbefine.mapbefine.common.exception.ErrorCode; +import com.mapbefine.mapbefine.common.exception.ForbiddenException; import com.mapbefine.mapbefine.common.exception.NotFoundException; public class MemberException { @@ -18,5 +19,11 @@ public MemberNotFoundException(MemberErrorCode errorCode, Long id) { } } + public static class MemberForbiddenException extends ForbiddenException { + public MemberForbiddenException(MemberErrorCode errorCode, Long id) { + super(new ErrorCode<>(errorCode.getCode(), errorCode.getMessage(), id)); + } + } + } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/oauth/application/OauthService.java b/backend/src/main/java/com/mapbefine/mapbefine/oauth/application/OauthService.java index d75526a0..50681719 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/oauth/application/OauthService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/oauth/application/OauthService.java @@ -1,5 +1,7 @@ package com.mapbefine.mapbefine.oauth.application; +import com.mapbefine.mapbefine.auth.exception.AuthErrorCode; +import com.mapbefine.mapbefine.auth.exception.AuthException.AuthUnauthorizedException; import com.mapbefine.mapbefine.auth.infrastructure.JwtTokenProvider; import com.mapbefine.mapbefine.member.domain.Member; import com.mapbefine.mapbefine.member.domain.MemberRepository; @@ -39,6 +41,8 @@ public LoginInfoResponse login(OauthServerType oauthServerType, String code) { Member savedMember = memberRepository.findByOauthId(oauthMember.getOauthId()) .orElseGet(() -> register(oauthMember)); + validateMemberStatus(savedMember); + String accessToken = jwtTokenProvider.createToken(String.valueOf(savedMember.getId())); return LoginInfoResponse.of(accessToken, savedMember); @@ -49,4 +53,12 @@ private Member register(OauthMember oauthMember) { return memberRepository.save(oauthMember.toRegisterMember()); } + private void validateMemberStatus(Member member) { + if (member.isNormalStatus()) { + return; + } + + throw new AuthUnauthorizedException(AuthErrorCode.BLOCKING_MEMBER_ACCESS); + } + } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/oauth/domain/AuthCodeRequestUrlProviderComposite.java b/backend/src/main/java/com/mapbefine/mapbefine/oauth/domain/AuthCodeRequestUrlProviderComposite.java index a2a3c518..2ad2295d 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/oauth/domain/AuthCodeRequestUrlProviderComposite.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/oauth/domain/AuthCodeRequestUrlProviderComposite.java @@ -4,7 +4,7 @@ import static java.util.function.UnaryOperator.identity; import static java.util.stream.Collectors.toMap; -import com.mapbefine.mapbefine.oauth.exception.OathException.OauthNotFoundException; +import com.mapbefine.mapbefine.oauth.exception.OauthException.OauthNotFoundException; import java.util.Map; import java.util.Optional; import java.util.Set; diff --git a/backend/src/main/java/com/mapbefine/mapbefine/oauth/domain/OauthMemberClientComposite.java b/backend/src/main/java/com/mapbefine/mapbefine/oauth/domain/OauthMemberClientComposite.java index f5fb9648..eba1172d 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/oauth/domain/OauthMemberClientComposite.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/oauth/domain/OauthMemberClientComposite.java @@ -4,7 +4,7 @@ import static java.util.function.UnaryOperator.identity; import static java.util.stream.Collectors.toMap; -import com.mapbefine.mapbefine.oauth.exception.OathException.OauthNotFoundException; +import com.mapbefine.mapbefine.oauth.exception.OauthException.OauthNotFoundException; import java.util.Map; import java.util.Optional; import java.util.Set; diff --git a/backend/src/main/java/com/mapbefine/mapbefine/oauth/exception/OathException.java b/backend/src/main/java/com/mapbefine/mapbefine/oauth/exception/OauthException.java similarity index 94% rename from backend/src/main/java/com/mapbefine/mapbefine/oauth/exception/OathException.java rename to backend/src/main/java/com/mapbefine/mapbefine/oauth/exception/OauthException.java index a18d234c..3dc67e35 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/oauth/exception/OathException.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/oauth/exception/OauthException.java @@ -4,7 +4,7 @@ import com.mapbefine.mapbefine.common.exception.NotFoundException; import com.mapbefine.mapbefine.oauth.domain.OauthServerType; -public class OathException { +public class OauthException { public static class OauthNotFoundException extends NotFoundException { public OauthNotFoundException(OauthErrorCode errorCode, OauthServerType oauthServerType) { diff --git a/backend/src/main/java/com/mapbefine/mapbefine/permission/domain/PermissionRepository.java b/backend/src/main/java/com/mapbefine/mapbefine/permission/domain/PermissionRepository.java index da8dca9b..7d14c061 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/permission/domain/PermissionRepository.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/permission/domain/PermissionRepository.java @@ -9,4 +9,5 @@ public interface PermissionRepository extends JpaRepository { boolean existsByTopicIdAndMemberId(Long topicId, Long memberId); + void deleteAllByMemberId(Long memberId); } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/permission/exception/PermissionErrorCode.java b/backend/src/main/java/com/mapbefine/mapbefine/permission/exception/PermissionErrorCode.java index 5a0f6aa8..c3f3f699 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/permission/exception/PermissionErrorCode.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/permission/exception/PermissionErrorCode.java @@ -9,7 +9,8 @@ public enum PermissionErrorCode { ILLEGAL_PERMISSION_ID("07001", "유효하지 않은 권한 정보입니다."), FORBIDDEN_ADD_PERMISSION_GUEST("07300", "로그인하지 않은 사용자는 권한을 줄 수 없습니다."), FORBIDDEN_ADD_PERMISSION("07301", "지도를 생성한 사용자가 아니면 권한을 줄 수 없습니다."), - PERMISSION_NOT_FOUND("07400", "존재하지 않는 권한 정보입니다.") + PERMISSION_NOT_FOUND("07400", "존재하지 않는 권한 정보입니다."), + PERMISSION_FORBIDDEN_BY_NOT_ADMIN("07401", "어드민 계정만 접근 가능합니다."), ; private final String code; diff --git a/backend/src/main/java/com/mapbefine/mapbefine/pin/application/PinQueryService.java b/backend/src/main/java/com/mapbefine/mapbefine/pin/application/PinQueryService.java index 6956b55d..0e2aa2bf 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/pin/application/PinQueryService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/pin/application/PinQueryService.java @@ -52,7 +52,7 @@ private void validateReadAuth(AuthMember member, Topic topic) { } public List findAllPinsByMemberId(AuthMember authMember, Long memberId) { - return pinRepository.findByCreatorId(memberId) + return pinRepository.findAllByCreatorId(memberId) .stream() .filter(pin -> authMember.canRead(pin.getTopic())) .map(PinResponse::from) diff --git a/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinImageRepository.java b/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinImageRepository.java index 36f53800..6e25d0af 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinImageRepository.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinImageRepository.java @@ -18,6 +18,9 @@ public interface PinImageRepository extends JpaRepository { @Query("update PinImage p set p.isDeleted = true where p.id = :id") void deleteById(@Param("id") Long id); + @Modifying(clearAutomatically = true) + @Query("update PinImage p set p.isDeleted = true where p.pin.id in :pinIds") + void deleteAllByPinIds(@Param("pinIds") List pinIds); + List findAllByPinId(Long pinId); - } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinRepository.java b/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinRepository.java index 11a2d95d..60d29dc9 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinRepository.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinRepository.java @@ -18,9 +18,13 @@ public interface PinRepository extends JpaRepository { @Query("update Pin p set p.isDeleted = true where p.id = :pinId") void deleteById(@Param("pinId") Long pinId); + @Modifying(clearAutomatically = true) + @Query("update Pin p set p.isDeleted = true where p.creator.id = :memberId") + void deleteAllByMemberId(@Param("memberId") Long memberId); + List findAllByTopicId(Long topicId); - List findByCreatorId(Long creatorId); + List findAllByCreatorId(Long creatorId); List findAllByOrderByUpdatedAtDesc(); } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/application/TopicQueryService.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/application/TopicQueryService.java index aa19d281..de0efdda 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/topic/application/TopicQueryService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/application/TopicQueryService.java @@ -186,7 +186,7 @@ private void validateReadableTopics(AuthMember member, List topics) { public List findAllTopicsByMemberId(AuthMember authMember, Long memberId) { if (Objects.isNull(authMember.getMemberId())) { - return topicRepository.findByCreatorId(memberId) + return topicRepository.findAllByCreatorId(memberId) .stream() .filter(authMember::canRead) .map(TopicResponse::fromGuestQuery) @@ -198,7 +198,7 @@ public List findAllTopicsByMemberId(AuthMember authMember, Long m List topicsInAtlas = findTopicsInAtlas(member); List topicsInBookMark = findBookMarkedTopics(member); - return topicRepository.findByCreatorId(memberId) + return topicRepository.findAllByCreatorId(memberId) .stream() .filter(authMember::canRead) .map(topic -> TopicResponse.from( diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/Topic.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/Topic.java index a41c807e..d5128022 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/Topic.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/Topic.java @@ -114,4 +114,8 @@ public int countBookmarks() { return bookmarks.size(); } + public void removeImage() { + this.topicInfo = topicInfo.removeImage(); + } + } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicInfo.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicInfo.java index 3f5ffd59..dff5da7e 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicInfo.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicInfo.java @@ -88,4 +88,13 @@ private static Image createImage(String imageUrl) { public String getImageUrl() { return image.getImageUrl(); } + + public TopicInfo removeImage() { + return new TopicInfo( + this.name, + this.description, + DEFAULT_IMAGE + ); + } + } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicRepository.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicRepository.java index 1ef9fb6f..e6eb95db 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicRepository.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicRepository.java @@ -16,8 +16,12 @@ public interface TopicRepository extends JpaRepository { @Query("update Topic t set t.isDeleted = true where t.id = :topicId") void deleteById(@Param("topicId") Long topicId); + @Modifying(clearAutomatically = true) + @Query("update Topic t set t.isDeleted = true where t.creator.id = :memberId") + void deleteAllByMemberId(@Param("memberId") Long memberId); + boolean existsById(Long id); - List findByCreatorId(Long creatorId); + List findAllByCreatorId(Long creatorId); } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/admin/AdminIntegrationTest.java b/backend/src/test/java/com/mapbefine/mapbefine/admin/AdminIntegrationTest.java new file mode 100644 index 00000000..c7d7c26f --- /dev/null +++ b/backend/src/test/java/com/mapbefine/mapbefine/admin/AdminIntegrationTest.java @@ -0,0 +1,249 @@ +package com.mapbefine.mapbefine.admin; + +import static io.restassured.RestAssured.given; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; + +import com.mapbefine.mapbefine.admin.dto.AdminMemberDetailResponse; +import com.mapbefine.mapbefine.admin.dto.AdminMemberResponse; +import com.mapbefine.mapbefine.common.IntegrationTest; +import com.mapbefine.mapbefine.location.LocationFixture; +import com.mapbefine.mapbefine.location.domain.Location; +import com.mapbefine.mapbefine.location.domain.LocationRepository; +import com.mapbefine.mapbefine.member.MemberFixture; +import com.mapbefine.mapbefine.member.domain.Member; +import com.mapbefine.mapbefine.member.domain.MemberRepository; +import com.mapbefine.mapbefine.member.domain.Role; +import com.mapbefine.mapbefine.pin.PinFixture; +import com.mapbefine.mapbefine.pin.PinImageFixture; +import com.mapbefine.mapbefine.pin.domain.Pin; +import com.mapbefine.mapbefine.pin.domain.PinImage; +import com.mapbefine.mapbefine.pin.domain.PinImageRepository; +import com.mapbefine.mapbefine.pin.domain.PinRepository; +import com.mapbefine.mapbefine.topic.TopicFixture; +import com.mapbefine.mapbefine.topic.domain.Topic; +import com.mapbefine.mapbefine.topic.domain.TopicRepository; +import io.restassured.common.mapper.TypeRef; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; + +class AdminIntegrationTest extends IntegrationTest { + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private TopicRepository topicRepository; + + @Autowired + private PinRepository pinRepository; + + @Autowired + private LocationRepository locationRepository; + + @Autowired + private PinImageRepository pinImageRepository; + + private Location location; + private Member admin; + private Member member; + private Topic topic; + private Pin pin; + private PinImage pinImage; + + @BeforeEach + void setup() { + admin = memberRepository.save(MemberFixture.create("Admin", "admin@naver.com", Role.ADMIN)); + member = memberRepository.save(MemberFixture.create("member", "member@gmail.com", Role.USER)); + topic = topicRepository.save(TopicFixture.createByName("topic", member)); + location = locationRepository.save(LocationFixture.create()); + pin = pinRepository.save(PinFixture.create(location, topic, member)); + pinImage = pinImageRepository.save(PinImageFixture.create(pin)); + } + + @Test + @DisplayName("어드민일 경우, 회원을 전체 조회할 수 있다.") + void findAllMembers_Success() { + //given + Member member1 = MemberFixture.create("member1", "member1@gmail.com", Role.USER); + Member member2 = MemberFixture.create("member2", "member2@gmail.com", Role.USER); + memberRepository.save(member1); + memberRepository.save(member2); + + //when + List response = given().log().all() + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(admin)) + .accept(MediaType.APPLICATION_JSON_VALUE) + .when().get("/admin/members") + .then().log().all() + .extract() + .as(new TypeRef<>() { + }); + + //then + List expected = List.of( + AdminMemberResponse.from(member), + AdminMemberResponse.from(member1), + AdminMemberResponse.from(member2) + ); + + assertThat(response).usingRecursiveComparison() + .ignoringFields("updatedAt") + .isEqualTo(expected); + } + + @Test + @DisplayName("어드민이 아닐 경우, 회원을 전체 조회할 수 없다.") + void findAllMembers_Fail() { + given().log().all() + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(member)) + .accept(MediaType.APPLICATION_JSON_VALUE) + .when().get("/admin/members/" + member.getId()) + .then().log().all() + .statusCode(HttpStatus.FORBIDDEN.value()); + } + + @Test + @DisplayName("어드민일 경우, 특정 회원의 상세 정보를 조회할 수 있다.") + void findMemberDetail_Success() { + //given when + AdminMemberDetailResponse response = given().log().all() + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(admin)) + .accept(MediaType.APPLICATION_JSON_VALUE) + .when().get("/admin/members/" + member.getId()) + .then().log().all() + .extract() + .as(new TypeRef<>() { + }); + + //then + + AdminMemberDetailResponse expected = AdminMemberDetailResponse.of( + member, + member.getCreatedTopics(), + member.getCreatedPins() + ); + + assertThat(response).usingRecursiveComparison() + .ignoringFields("updatedAt") + .ignoringFields("topics.updatedAt") + .isEqualTo(expected); + } + + @Test + @DisplayName("어드민이 아닐 경우, 특정 회원의 상세 정보를 조회할 수 없다.") + void findMemberDetail_Fail() { + given().log().all() + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(member)) + .accept(MediaType.APPLICATION_JSON_VALUE) + .when().get("/admin/members/" + member.getId()) + .then().log().all() + .statusCode(HttpStatus.FORBIDDEN.value()); + } + + @Test + @DisplayName("어드민일 경우, 특정 회원을 삭제(차단)할 수 있다.") + void deleteMember_Success() { + given().log().all() + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(admin)) + .when().delete("/admin/members/" + member.getId()) + .then().log().all() + .statusCode(HttpStatus.NO_CONTENT.value()); + } + + @Test + @DisplayName("어드민이 아닐 경우, 특정 회원을 삭제(차단)할 수 없다.") + void deleteMember_Fail() { + given().log().all() + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(member)) + .when().delete("/admin/members/" + member.getId()) + .then().log().all() + .statusCode(HttpStatus.FORBIDDEN.value()); + } + + @Test + @DisplayName("어드민일 경우, 특정 토픽을 삭제할 수 있다.") + void deleteTopic_Success() { + given().log().all() + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(admin)) + .when().delete("/admin/topics/" + topic.getId()) + .then().log().all() + .statusCode(HttpStatus.NO_CONTENT.value()); + } + + @Test + @DisplayName("어드민이 아닐 경우, 특정 토픽을 삭제할 수 없다.") + void deleteTopic_Fail() { + given().log().all() + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(member)) + .when().delete("/admin/topics/" + topic.getId()) + .then().log().all() + .statusCode(HttpStatus.FORBIDDEN.value()); + } + + @Test + @DisplayName("어드민일 경우, 특정 토픽 이미지를 삭제할 수 있다.") + void deleteTopicImage_Success() { + given().log().all() + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(admin)) + .when().delete("/admin/topics/" + topic.getId() + "/images") + .then().log().all() + .statusCode(HttpStatus.NO_CONTENT.value()); + } + + @Test + @DisplayName("어드민이 아닐 경우, 특정 토픽 이미지를 삭제할 수 없다.") + void deleteTopicImage_Fail() { + given().log().all() + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(member)) + .when().delete("/admin/topics/" + topic.getId() + "/images") + .then().log().all() + .statusCode(HttpStatus.FORBIDDEN.value()); + } + + @Test + @DisplayName("어드민일 경우, 특정 핀을 삭제할 수 있다.") + void deletePin_Success() { + given().log().all() + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(admin)) + .when().delete("/admin/pins/" + pin.getId()) + .then().log().all() + .statusCode(HttpStatus.NO_CONTENT.value()); + } + + @Test + @DisplayName("어드민이 아닐 경우, 특정 핀을 삭제할 수 없다.") + void deletePin_Fail() { + given().log().all() + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(member)) + .when().delete("/admin/pins/" + pin.getId()) + .then().log().all() + .statusCode(HttpStatus.FORBIDDEN.value()); + } + + @Test + @DisplayName("어드민일 경우, 특정 핀 이미지를 삭제할 수 있다.") + void deletePinImage_Success() { + given().log().all() + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(admin)) + .when().delete("/admin/pins/images/" + pinImage.getId()) + .then().log().all() + .statusCode(HttpStatus.NO_CONTENT.value()); + } + + @Test + @DisplayName("어드민이 아닐 경우, 특정 핀 이미지를 삭제할 수 없다.") + void deletePinImage_Fail() { + given().log().all() + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(member)) + .when().delete("/admin/pins/images/" + pinImage.getId()) + .then().log().all() + .statusCode(HttpStatus.FORBIDDEN.value()); + } + +} diff --git a/backend/src/test/java/com/mapbefine/mapbefine/admin/application/AdminCommandServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/admin/application/AdminCommandServiceTest.java new file mode 100644 index 00000000..12e49f77 --- /dev/null +++ b/backend/src/test/java/com/mapbefine/mapbefine/admin/application/AdminCommandServiceTest.java @@ -0,0 +1,280 @@ +package com.mapbefine.mapbefine.admin.application; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.mapbefine.mapbefine.atlas.domain.Atlas; +import com.mapbefine.mapbefine.atlas.domain.AtlasRepository; +import com.mapbefine.mapbefine.auth.domain.AuthMember; +import com.mapbefine.mapbefine.bookmark.domain.Bookmark; +import com.mapbefine.mapbefine.bookmark.domain.BookmarkRepository; +import com.mapbefine.mapbefine.common.annotation.ServiceTest; +import com.mapbefine.mapbefine.location.LocationFixture; +import com.mapbefine.mapbefine.location.domain.Location; +import com.mapbefine.mapbefine.location.domain.LocationRepository; +import com.mapbefine.mapbefine.member.MemberFixture; +import com.mapbefine.mapbefine.member.domain.Member; +import com.mapbefine.mapbefine.member.domain.MemberRepository; +import com.mapbefine.mapbefine.member.domain.Role; +import com.mapbefine.mapbefine.member.domain.Status; +import com.mapbefine.mapbefine.permission.domain.Permission; +import com.mapbefine.mapbefine.permission.domain.PermissionRepository; +import com.mapbefine.mapbefine.permission.exception.PermissionException.PermissionForbiddenException; +import com.mapbefine.mapbefine.pin.PinFixture; +import com.mapbefine.mapbefine.pin.PinImageFixture; +import com.mapbefine.mapbefine.pin.domain.Pin; +import com.mapbefine.mapbefine.pin.domain.PinImage; +import com.mapbefine.mapbefine.pin.domain.PinImageRepository; +import com.mapbefine.mapbefine.pin.domain.PinRepository; +import com.mapbefine.mapbefine.topic.TopicFixture; +import com.mapbefine.mapbefine.topic.domain.Topic; +import com.mapbefine.mapbefine.topic.domain.TopicInfo; +import com.mapbefine.mapbefine.topic.domain.TopicRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +@ServiceTest +class AdminCommandServiceTest { + + @Autowired + private AdminCommandService adminCommandService; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private TopicRepository topicRepository; + + @Autowired + private PinRepository pinRepository; + + @Autowired + private LocationRepository locationRepository; + + @Autowired + private PinImageRepository pinImageRepository; + + @Autowired + private AtlasRepository atlasRepository; + + @Autowired + private PermissionRepository permissionRepository; + + @Autowired + private BookmarkRepository bookmarkRepository; + + private Location location; + private Member admin; + private Member member; + private Topic topic; + private Pin pin; + private PinImage pinImage; + + @BeforeEach + void setup() { + admin = memberRepository.save(MemberFixture.create("Admin", "admin@naver.com", Role.ADMIN)); + member = memberRepository.save(MemberFixture.create("member", "member@gmail.com", Role.USER)); + topic = topicRepository.save(TopicFixture.createByName("topic", member)); + location = locationRepository.save(LocationFixture.create()); + pin = pinRepository.save(PinFixture.create(location, topic, member)); + pinImage = pinImageRepository.save(PinImageFixture.create(pin)); + } + + @DisplayName("Member를 차단(탈퇴시킬)할 경우, Member가 생성한 토픽, 핀, 핀 이미지가 soft-deleting 된다.") + @Test + void blockMember_Success() { + //given + AuthMember adminAuthMember = MemberFixture.createUser(admin); + Bookmark bookmark = Bookmark.createWithAssociatedTopicAndMember(topic, member); + Atlas atlas = Atlas.createWithAssociatedMember(topic, member); + Permission permission = Permission.createPermissionAssociatedWithTopicAndMember(topic, member); + + bookmarkRepository.save(bookmark); + atlasRepository.save(atlas); + permissionRepository.save(permission); + + assertAll(() -> { + assertThat(member.getMemberInfo().getStatus()).isEqualTo(Status.NORMAL); + assertThat(topic.isDeleted()).isFalse(); + assertThat(pin.isDeleted()).isFalse(); + assertThat(pinImage.isDeleted()).isFalse(); + assertThat(bookmarkRepository.existsByMemberIdAndTopicId(member.getId(), topic.getId())).isTrue(); + assertThat(atlasRepository.existsByMemberIdAndTopicId(member.getId(), topic.getId())).isTrue(); + assertThat(permissionRepository.existsByTopicIdAndMemberId(topic.getId(), member.getId())).isTrue(); + }); + + //when + adminCommandService.blockMember(adminAuthMember, member.getId()); + + //then + Topic deletedTopic = topicRepository.findById(topic.getId()).get(); + Pin deletedPin = pinRepository.findById(pin.getId()).get(); + PinImage deletedPinImage = pinImageRepository.findById(pinImage.getId()).get(); + + assertAll(() -> { + assertThat(member.getMemberInfo().getStatus()).isEqualTo(Status.BLOCKED); + assertThat(deletedTopic.isDeleted()).isTrue(); + assertThat(deletedPin.isDeleted()).isTrue(); + assertThat(deletedPinImage.isDeleted()).isTrue(); + assertThat(bookmarkRepository.existsByMemberIdAndTopicId(member.getId(), topic.getId())).isFalse(); + assertThat(atlasRepository.existsByMemberIdAndTopicId(member.getId(), topic.getId())).isFalse(); + assertThat(permissionRepository.existsByTopicIdAndMemberId(topic.getId(), member.getId())).isFalse(); + }); + } + + @DisplayName("Admin이 아닐 경우, Member를 차단(탈퇴시킬)할 수 없다.") + @Test + void blockMember_Fail() { + //given + AuthMember userAuthMember = MemberFixture.createUser(member); + Member otherMember = MemberFixture.create("otherMember", "otherMember@email.com", Role.USER); + memberRepository.save(otherMember); + + assertAll(() -> { + assertThat(member.getMemberInfo().getStatus()).isEqualTo(Status.NORMAL); + assertThat(topic.isDeleted()).isFalse(); + assertThat(pin.isDeleted()).isFalse(); + assertThat(pinImage.isDeleted()).isFalse(); + }); + + //when then + Long otherMemberId = otherMember.getId(); + assertThatThrownBy(() -> adminCommandService.blockMember(userAuthMember, otherMemberId)) + .isInstanceOf(PermissionForbiddenException.class); + } + + @DisplayName("Admin은 토픽을 삭제시킬 수 있다.") + @Test + void deleteTopic_Success() { + //given + AuthMember adminAuthMember = MemberFixture.createUser(admin); + + assertThat(topic.isDeleted()).isFalse(); + + //when + adminCommandService.deleteTopic(adminAuthMember, topic.getId()); + + //then + Topic deletedTopic = topicRepository.findById(topic.getId()).get(); + + assertThat(deletedTopic.isDeleted()).isTrue(); + } + + @DisplayName("Admin이 아닐 경우, 토픽을 삭제시킬 수 없다.") + @Test + void deleteTopic_Fail() { + //given + AuthMember userAuthMember = MemberFixture.createUser(member); + + assertThat(topic.isDeleted()).isFalse(); + + //when then + Long topicId = topic.getId(); + assertThatThrownBy(() -> adminCommandService.deleteTopic(userAuthMember, topicId)) + .isInstanceOf(PermissionForbiddenException.class); + } + + @DisplayName("Admin은 토픽 이미지를 삭제할 수 있다.") + @Test + void deleteTopicImage_Success() { + //given + AuthMember adminAuthMember = MemberFixture.createUser(admin); + TopicInfo topicInfo = topic.getTopicInfo(); + + topic.updateTopicInfo(topicInfo.getName(), topicInfo.getDescription(), "https://imageUrl.png"); + + assertThat(topic.getTopicInfo().getImageUrl()).isEqualTo("https://imageUrl.png"); + + //when + adminCommandService.deleteTopicImage(adminAuthMember, topic.getId()); + + //then + Topic imageDeletedTopic = topicRepository.findById(topic.getId()).get(); + + assertThat(imageDeletedTopic.getTopicInfo().getImageUrl()) + .isEqualTo("https://map-befine-official.github.io/favicon.png"); + } + + @DisplayName("Admin이 아닐 경우, 이미지를 삭제할 수 없다.") + @Test + void deleteTopicImage_Fail() { + //given + AuthMember userAuthMember = MemberFixture.createUser(member); + TopicInfo topicInfo = topic.getTopicInfo(); + + topic.updateTopicInfo(topicInfo.getName(), topicInfo.getDescription(), "https://imageUrl.png"); + + assertThat(topic.getTopicInfo().getImageUrl()).isEqualTo("https://imageUrl.png"); + + //when then + Long topicId = topic.getId(); + assertThatThrownBy(() -> adminCommandService.deleteTopicImage(userAuthMember, topicId)) + .isInstanceOf(PermissionForbiddenException.class); + } + + @DisplayName("Admin은 핀을 삭제할 수 있다.") + @Test + void deletePin_Success() { + //given + AuthMember adminAuthMember = MemberFixture.createUser(admin); + + assertThat(pin.isDeleted()).isFalse(); + + //when + adminCommandService.deletePin(adminAuthMember, pin.getId()); + + //then + Pin deletedPin = pinRepository.findById(pin.getId()).get(); + + assertThat(deletedPin.isDeleted()).isTrue(); + } + + @DisplayName("Admin이 아닐 경우, 핀을 삭제할 수 없다.") + @Test + void deletePin_Fail() { + //given + AuthMember userAuthMember = MemberFixture.createUser(member); + + assertThat(pin.isDeleted()).isFalse(); + + //when then + Long pinId = pin.getId(); + assertThatThrownBy(() -> adminCommandService.deletePin(userAuthMember, pinId)) + .isInstanceOf(PermissionForbiddenException.class); + } + + @DisplayName("Admin인 경우, 핀 이미지를 삭제할 수 있다.") + @Test + void deletePinImage_Success() { + //given + AuthMember adminAuthMember = MemberFixture.createUser(admin); + + assertThat(pinImage.isDeleted()).isFalse(); + + //when + adminCommandService.deletePinImage(adminAuthMember, pinImage.getId()); + + //then + PinImage deletedPinImage = pinImageRepository.findById(pinImage.getId()).get(); + + assertThat(deletedPinImage.isDeleted()).isTrue(); + } + + @DisplayName("Admin이 아닐 경우, 핀 이미지를 삭제할 수 없다.") + @Test + void deletePinImage_Fail() { + //given + AuthMember userAuthMember = MemberFixture.createUser(member); + + assertThat(pinImage.isDeleted()).isFalse(); + + //when then + Long pinImageId = pinImage.getId(); + assertThatThrownBy(() -> adminCommandService.deletePinImage(userAuthMember, pinImageId)) + .isInstanceOf(PermissionForbiddenException.class); + } + +} \ No newline at end of file diff --git a/backend/src/test/java/com/mapbefine/mapbefine/admin/application/AdminQueryServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/admin/application/AdminQueryServiceTest.java new file mode 100644 index 00000000..33599110 --- /dev/null +++ b/backend/src/test/java/com/mapbefine/mapbefine/admin/application/AdminQueryServiceTest.java @@ -0,0 +1,135 @@ +package com.mapbefine.mapbefine.admin.application; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.mapbefine.mapbefine.admin.dto.AdminMemberDetailResponse; +import com.mapbefine.mapbefine.admin.dto.AdminMemberResponse; +import com.mapbefine.mapbefine.auth.domain.AuthMember; +import com.mapbefine.mapbefine.common.annotation.ServiceTest; +import com.mapbefine.mapbefine.location.LocationFixture; +import com.mapbefine.mapbefine.location.domain.Location; +import com.mapbefine.mapbefine.location.domain.LocationRepository; +import com.mapbefine.mapbefine.member.MemberFixture; +import com.mapbefine.mapbefine.member.domain.Member; +import com.mapbefine.mapbefine.member.domain.MemberRepository; +import com.mapbefine.mapbefine.member.domain.Role; +import com.mapbefine.mapbefine.permission.exception.PermissionException.PermissionForbiddenException; +import com.mapbefine.mapbefine.pin.PinFixture; +import com.mapbefine.mapbefine.pin.domain.Pin; +import com.mapbefine.mapbefine.pin.domain.PinRepository; +import com.mapbefine.mapbefine.topic.TopicFixture; +import com.mapbefine.mapbefine.topic.domain.Topic; +import com.mapbefine.mapbefine.topic.domain.TopicRepository; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +@ServiceTest +class AdminQueryServiceTest { + + @Autowired + private AdminQueryService adminQueryService; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private TopicRepository topicRepository; + + @Autowired + private PinRepository pinRepository; + + @Autowired + private LocationRepository locationRepository; + + private Location location; + private Member admin; + private Member member; + private Topic topic; + private Pin pin; + + @BeforeEach + void setup() { + admin = memberRepository.save(MemberFixture.create("Admin", "admin@naver.com", Role.ADMIN)); + member = memberRepository.save(MemberFixture.create("member", "member@gmail.com", Role.USER)); + topic = topicRepository.save(TopicFixture.createByName("topic", member)); + location = locationRepository.save(LocationFixture.create()); + pin = pinRepository.save(PinFixture.create(location, topic, member)); + } + + @Test + @DisplayName("사용자와 관련된 세부(민감한 정보 포함) 정보를 모두 조회할 수 있다.") + void findMemberDetail_Success() { + //given + AuthMember adminAuthMember = MemberFixture.createUser(admin); + + //when + AdminMemberDetailResponse response = adminQueryService.findMemberDetail(adminAuthMember, member.getId()); + + //then + assertThat(response).usingRecursiveComparison() + .ignoringFields("updatedAt") + .isEqualTo(AdminMemberDetailResponse.of(member, List.of(topic), List.of(pin))); + } + + @Test + @DisplayName("Admin이 아닌 경우, 사용자와 관련된 세부(민감한 정보 포함) 정보를 모두 조회할 수 없다.") + void findMemberDetail_Fail() { + //given + AuthMember userAuthMember = MemberFixture.createUser(member); + + //when //then + Long memberId = member.getId(); + assertThatThrownBy(() -> adminQueryService.findMemberDetail(userAuthMember, memberId)) + .isInstanceOf(PermissionForbiddenException.class); + } + + @Test + @DisplayName("모든 사용자와 관련된 세부 정보를 모두 조회할 수 있다.") + void findAllMemberDetails_Success() { + //given + AuthMember adminAuthMember = MemberFixture.createUser(admin); + + ArrayList members = new ArrayList<>(); + members.add(member); + for (int i = 0; i < 10; i++) { + Member saveMember = MemberFixture.create("member" + i, "member" + i + "@email.com", Role.USER); + members.add(memberRepository.save(saveMember)); + } + + //when + List responses = adminQueryService.findAllMemberDetails(adminAuthMember); + //then + List expected = members.stream() + .map(AdminMemberResponse::from) + .toList(); + + assertThat(responses).usingRecursiveComparison() + .ignoringFields("updatedAt") + .ignoringCollectionOrderInFields() + .isEqualTo(expected); + } + + @Test + @DisplayName("Admin이 아닌 경우 모든 사용자와 관련된 세부 정보를 모두 조회할 수 없다.") + void findAllMemberDetails_Fail() { + //given + AuthMember memberAuthMember = MemberFixture.createUser(member); + + ArrayList members = new ArrayList<>(); + members.add(member); + for (int i = 0; i < 10; i++) { + Member saveMember = MemberFixture.create("member" + i, "member" + i + "@email.com", Role.USER); + members.add(memberRepository.save(saveMember)); + } + + //when //then + assertThatThrownBy(() -> adminQueryService.findAllMemberDetails(memberAuthMember)) + .isInstanceOf(PermissionForbiddenException.class); + } + +} \ No newline at end of file diff --git a/backend/src/test/java/com/mapbefine/mapbefine/admin/presentation/AdminControllerTest.java b/backend/src/test/java/com/mapbefine/mapbefine/admin/presentation/AdminControllerTest.java new file mode 100644 index 00000000..edceb5d3 --- /dev/null +++ b/backend/src/test/java/com/mapbefine/mapbefine/admin/presentation/AdminControllerTest.java @@ -0,0 +1,172 @@ +package com.mapbefine.mapbefine.admin.presentation; + +import static org.apache.http.HttpHeaders.AUTHORIZATION; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doNothing; + +import com.mapbefine.mapbefine.admin.application.AdminCommandService; +import com.mapbefine.mapbefine.admin.application.AdminQueryService; +import com.mapbefine.mapbefine.admin.dto.AdminMemberDetailResponse; +import com.mapbefine.mapbefine.admin.dto.AdminMemberResponse; +import com.mapbefine.mapbefine.common.RestDocsIntegration; +import com.mapbefine.mapbefine.common.interceptor.AdminAuthInterceptor; +import com.mapbefine.mapbefine.pin.dto.response.PinResponse; +import com.mapbefine.mapbefine.topic.dto.response.TopicResponse; +import java.time.LocalDateTime; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +class AdminControllerTest extends RestDocsIntegration { + + private static final List TOPIC_RESPONSES = List.of(new TopicResponse( + 1L, + "준팍의 또 토픽", + "https://map-befine-official.github.io/favicon.png", + "준팍", + 3, + Boolean.FALSE, + 5, + Boolean.FALSE, + LocalDateTime.now() + ), new TopicResponse( + 2L, + "준팍의 두번째 토픽", + "https://map-befine-official.github.io/favicon.png", + "준팍", + 5, + Boolean.FALSE, + 3, + Boolean.FALSE, + LocalDateTime.now() + )); + private static final List PIN_RESPONSES = List.of( + new PinResponse( + 1L, + "매튜의 산스장", + "지번 주소", + "매튜가 사랑하는 산스장", + "매튜", + 37, + 127 + ), new PinResponse( + 2L, + "매튜의 안갈집", + "지번 주소", + "매튜가 두번은 안 갈 집", + "매튜", + 37, + 127 + ) + ); + + @MockBean + private AdminCommandService adminCommandService; + + @MockBean + private AdminQueryService adminQueryService; + + @MockBean + private AdminAuthInterceptor adminAuthInterceptor; + + @BeforeEach + void setAll() throws Exception { + given(adminAuthInterceptor.preHandle(any(), any(), any())).willReturn(true); + } + + @DisplayName("멤버 목록 조회") + @Test + void findAllMemberDetails() throws Exception { + List response = List.of( + new AdminMemberResponse(1L, "쥬니", "zuny@naver.com", "https://zuny.png", LocalDateTime.now()), + new AdminMemberResponse(2L, "세인", "semin@naver.com", "https://semin.png", LocalDateTime.now()) + ); + + given(adminQueryService.findAllMemberDetails(any())).willReturn(response); + + mockMvc.perform( + MockMvcRequestBuilders.get("/admin/members") + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) + ).andDo(restDocs.document()); + } + + @DisplayName("멤버 상세 조회") + @Test + void findMember() throws Exception { + AdminMemberDetailResponse response = new AdminMemberDetailResponse( + 1L, + "쥬니", + "zuny@naver.com", + "https://image.png", + TOPIC_RESPONSES, + PIN_RESPONSES, + LocalDateTime.now() + ); + + given(adminQueryService.findMemberDetail(any(), any())).willReturn(response); + + mockMvc.perform( + MockMvcRequestBuilders.get("/admin/members/1") + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) + ).andDo(restDocs.document()); + } + + @DisplayName("멤버 차단(블랙리스트)") + @Test + void deleteMember() throws Exception { + doNothing().when(adminCommandService).blockMember(any(), any()); + + mockMvc.perform( + MockMvcRequestBuilders.delete("/admin/members/1") + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) + ).andDo(restDocs.document()); + } + + @DisplayName("토픽 삭제") + @Test + void deleteTopic() throws Exception { + doNothing().when(adminCommandService).deleteTopic(any(), any()); + + mockMvc.perform( + MockMvcRequestBuilders.delete("/admin/topics/1") + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) + ).andDo(restDocs.document()); + } + + @DisplayName("토픽 이미지 삭제") + @Test + void deleteTopicImage() throws Exception { + doNothing().when(adminCommandService).deleteTopicImage(any(), any()); + + mockMvc.perform( + MockMvcRequestBuilders.delete("/admin/topics/1/images") + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) + ).andDo(restDocs.document()); + } + + @DisplayName("핀 삭제") + @Test + void deletePin() throws Exception { + doNothing().when(adminCommandService).deletePin(any(), any()); + + mockMvc.perform( + MockMvcRequestBuilders.delete("/admin/pins/1") + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) + ).andDo(restDocs.document()); + } + + @DisplayName("토픽 이미지 삭제") + @Test + void deletePinImage() throws Exception { + doNothing().when(adminCommandService).deletePinImage(any(), any()); + + mockMvc.perform( + MockMvcRequestBuilders.delete("/admin/pins/images/1") + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) + ).andDo(restDocs.document()); + } +} \ No newline at end of file diff --git a/backend/src/test/java/com/mapbefine/mapbefine/member/MemberFixture.java b/backend/src/test/java/com/mapbefine/mapbefine/member/MemberFixture.java index d6943a99..6fd7bc14 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/member/MemberFixture.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/member/MemberFixture.java @@ -1,10 +1,12 @@ package com.mapbefine.mapbefine.member; import com.mapbefine.mapbefine.auth.domain.AuthMember; +import com.mapbefine.mapbefine.auth.domain.member.Admin; import com.mapbefine.mapbefine.auth.domain.member.User; import com.mapbefine.mapbefine.member.domain.Member; import com.mapbefine.mapbefine.member.domain.OauthId; import com.mapbefine.mapbefine.member.domain.Role; +import com.mapbefine.mapbefine.member.domain.Status; import com.mapbefine.mapbefine.oauth.domain.OauthServerType; import com.mapbefine.mapbefine.topic.domain.Topic; @@ -16,6 +18,7 @@ public static Member create(String name, String email, Role role) { email, "https://map-befine-official.github.io/favicon.png", role, + Status.NORMAL, new OauthId(1L, OauthServerType.KAKAO) ); } @@ -26,11 +29,16 @@ public static Member createWithOauthId(String name, String email, Role role, Oau email, "https://map-befine-official.github.io/favicon.png", role, + Status.NORMAL, oauthId) ; } public static AuthMember createUser(Member member) { + if (member.isAdmin()) { + return new Admin(member.getId()); + } + return new User( member.getId(), member.getCreatedTopics().stream().map(Topic::getId).toList(), diff --git a/backend/src/test/java/com/mapbefine/mapbefine/member/domain/MemberInfoTest.java b/backend/src/test/java/com/mapbefine/mapbefine/member/domain/MemberInfoTest.java index 7c4d4a20..1ed6e737 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/member/domain/MemberInfoTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/member/domain/MemberInfoTest.java @@ -22,6 +22,7 @@ class Validate { private final String VALID_EMAIL = "member@naver.com"; private final String VALID_IMAGE_URL = "https://map-befine-official.github.io/favicon.png"; private final Role VALID_ROLE = Role.ADMIN; + private final Status VALID_STATUS = Status.NORMAL; @Test @DisplayName("정확한 값을 입력하면 객체가 생성된다") @@ -31,7 +32,8 @@ void success() { VALID_NICK_NAME, VALID_EMAIL, VALID_IMAGE_URL, - VALID_ROLE + VALID_ROLE, + VALID_STATUS ); //then @@ -52,7 +54,8 @@ void whenNameIsInvalid_thenFail(String invalidNickName) { invalidNickName, VALID_EMAIL, VALID_IMAGE_URL, - VALID_ROLE + VALID_ROLE, + VALID_STATUS )).isInstanceOf(MemberBadRequestException.class); } @@ -67,7 +70,8 @@ void whenEmailIsInvalid_thenFail(String invalidEmail) { VALID_NICK_NAME, invalidEmail, VALID_IMAGE_URL, - VALID_ROLE + VALID_ROLE, + VALID_STATUS )).isInstanceOf(MemberBadRequestException.class); } @@ -81,7 +85,8 @@ void whenImageUrlIsInvalid_thenFail() { VALID_NICK_NAME, VALID_EMAIL, invalidImageUrl, - VALID_ROLE + VALID_ROLE, + VALID_STATUS )).isInstanceOf(ImageBadRequestException.class); } @@ -94,6 +99,7 @@ void whenRoleIsInvalid_thenFail() { VALID_EMAIL, VALID_IMAGE_URL, null + ,VALID_STATUS )).isInstanceOf(IllegalArgumentException.class); } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/member/domain/MemberTest.java b/backend/src/test/java/com/mapbefine/mapbefine/member/domain/MemberTest.java index 0ee9b697..7fcb4b10 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/member/domain/MemberTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/member/domain/MemberTest.java @@ -16,6 +16,7 @@ void createMember_success() { String email = "member@naver.com"; String imageUrl = "https://map-befine-official.github.io/favicon.png"; Role role = Role.ADMIN; + Status status = Status.NORMAL; // when Member member = Member.of( @@ -23,7 +24,9 @@ void createMember_success() { email, imageUrl, role, - new OauthId(1L, OauthServerType.KAKAO)); + status, + new OauthId(1L, OauthServerType.KAKAO) + ); // then assertThat(member).isNotNull(); diff --git a/backend/src/test/java/com/mapbefine/mapbefine/pin/domain/PinImageRepositoryTest.java b/backend/src/test/java/com/mapbefine/mapbefine/pin/domain/PinImageRepositoryTest.java index ad36bb0d..a8604271 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/pin/domain/PinImageRepositoryTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/pin/domain/PinImageRepositoryTest.java @@ -14,6 +14,7 @@ import com.mapbefine.mapbefine.topic.TopicFixture; import com.mapbefine.mapbefine.topic.domain.Topic; import com.mapbefine.mapbefine.topic.domain.TopicRepository; +import java.util.List; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -87,4 +88,30 @@ void deleteAllByPinId_Success() { .extractingResultOf("isDeleted") .containsOnly(true); } + + @Test + @DisplayName("여러 핀을 Id로 삭제하면, 핀 이미지들도 soft-deleting 된다.") + void deleteAllByMemberId_Success() { + //given + Pin otherPin = pinRepository.save(PinFixture.create(pin.getLocation(), topic, member)); + + PinImage pinImage1 = PinImageFixture.create(pin); + PinImage pinImage2 = PinImageFixture.create(otherPin); + pinImageRepository.save(pinImage1); + pinImageRepository.save(pinImage2); + + //when + assertThat(pinImage1.isDeleted()).isFalse(); + assertThat(pinImage2.isDeleted()).isFalse(); + pinImageRepository.deleteAllByPinIds(List.of(pin.getId(),otherPin.getId())); + + //then + assertThat(pinImageRepository.findAllByPinId(pin.getId())) + .extractingResultOf("isDeleted") + .containsOnly(true); + assertThat(pinImageRepository.findAllByPinId(otherPin.getId())) + .extractingResultOf("isDeleted") + .containsOnly(true); + } + } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/pin/domain/PinRepositoryTest.java b/backend/src/test/java/com/mapbefine/mapbefine/pin/domain/PinRepositoryTest.java index 508d938e..24bf7cde 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/pin/domain/PinRepositoryTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/pin/domain/PinRepositoryTest.java @@ -3,7 +3,6 @@ import static org.assertj.core.api.Assertions.assertThat; import com.mapbefine.mapbefine.location.LocationFixture; -import com.mapbefine.mapbefine.location.domain.Coordinate; import com.mapbefine.mapbefine.location.domain.Location; import com.mapbefine.mapbefine.location.domain.LocationRepository; import com.mapbefine.mapbefine.member.MemberFixture; @@ -24,10 +23,6 @@ @DataJpaTest class PinRepositoryTest { - private static final Coordinate DEFAULT_COORDINATE = Coordinate.of( - 37.5152933, - 127.1029866 - ); @Autowired private TopicRepository topicRepository; @@ -88,4 +83,49 @@ void deleteAllByTopicId_Success() { .containsOnly(true); } + + @Test + @DisplayName("Member ID로 모든 핀을 soft-delete 할 수 있다.") + void deleteAllByMemberId_Success() { + //given + for (int i = 0; i < 10; i++) { + pinRepository.save(PinFixture.create(location, topic, member)); + } + + //when + assertThat(member.getCreatedPins()).hasSize(10) + .extractingResultOf("isDeleted") + .containsOnly(false); + pinRepository.deleteAllByMemberId(member.getId()); + + //then + List deletedPins = pinRepository.findAllByCreatorId(member.getId()); + assertThat(deletedPins).extractingResultOf("isDeleted") + .containsOnly(true); + } + + @Test + @DisplayName("다른 토픽에 존재하는 핀들이여도, Member ID로 모든 핀을 soft-delete 할 수 있다.") + void deleteAllByMemberIdInOtherTopics_Success() { + //given + Topic otherTopic = TopicFixture.createByName("otherTopic", member); + topicRepository.save(otherTopic); + + for (int i = 0; i < 10; i++) { + pinRepository.save(PinFixture.create(location, topic, member)); + pinRepository.save(PinFixture.create(location, otherTopic, member)); + } + + //when + assertThat(member.getCreatedPins()).hasSize(20) + .extractingResultOf("isDeleted") + .containsOnly(false); + pinRepository.deleteAllByMemberId(member.getId()); + + //then + List deletedPins = pinRepository.findAllByCreatorId(member.getId()); + assertThat(deletedPins).extractingResultOf("isDeleted") + .containsOnly(true); + } + } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/topic/domain/TopicRepositoryTest.java b/backend/src/test/java/com/mapbefine/mapbefine/topic/domain/TopicRepositoryTest.java index 3280df30..2a783b70 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/topic/domain/TopicRepositoryTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/topic/domain/TopicRepositoryTest.java @@ -6,6 +6,8 @@ import com.mapbefine.mapbefine.member.domain.Member; import com.mapbefine.mapbefine.member.domain.MemberRepository; import com.mapbefine.mapbefine.member.domain.Role; +import com.mapbefine.mapbefine.topic.TopicFixture; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -25,24 +27,14 @@ class TopicRepositoryTest { @BeforeEach void setUp() { - member = MemberFixture.create("member", "member@naver.com", Role.USER); - memberRepository.save(member); + member = memberRepository.save(MemberFixture.create("member", "member@naver.com", Role.USER)); } @Test @DisplayName("토픽을 삭제하면, soft-deleting 된다.") void deleteById_Success() { //given - Topic topic = Topic.createTopicAssociatedWithCreator( - "토픽", - "토픽설명", - "https://example.com/image.jpg", - Publicity.PUBLIC, - PermissionType.ALL_MEMBERS, - member - ); - - topicRepository.save(topic); + Topic topic = topicRepository.save(TopicFixture.createByName("Topic", member)); assertThat(topic.isDeleted()).isFalse(); @@ -54,4 +46,24 @@ void deleteById_Success() { assertThat(deletedTopic.isDeleted()).isTrue(); } + @Test + @DisplayName("Member Id로 모든 토픽을 삭제하면, soft-deleting 된다.") + void deleteAllByMemberId_Success() { + //given + for (int i = 0; i < 10; i++) { + topicRepository.save(TopicFixture.createByName("topic" + i, member)); + } + assertThat(member.getCreatedTopics()).hasSize(10) + .extractingResultOf("isDeleted") + .containsOnly(false); + + //when + topicRepository.deleteAllByMemberId(member.getId()); + + //then + List deletedTopics = topicRepository.findAllByCreatorId(member.getId()); + assertThat(deletedTopics).hasSize(10) + .extractingResultOf("isDeleted") + .containsOnly(true); + } } From 5985872fe7a5f7a8440de30a60c6db66d78988a9 Mon Sep 17 00:00:00 2001 From: Doy Date: Fri, 15 Sep 2023 15:39:31 +0900 Subject: [PATCH 17/34] =?UTF-8?q?[BE]=20Feature/#399=20=EB=82=B4=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4(=ED=9A=8C=EC=9B=90=20=EB=8B=89=EB=84=A4?= =?UTF-8?q?=EC=9E=84)=20=EC=88=98=EC=A0=95=20API=20=EA=B5=AC=ED=98=84=20(#?= =?UTF-8?q?408)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 사용하지 않는 MemberRepository 메서드 삭제 * refactor: 회원 업데이트 부분 변경으로 시그니처 변경 - 현재 회원 update에서 변경되는 부분만 인자로 남겨둠 - update 시, Member 에서 MemberInfo.getXX을 하는 대신 MemberInfo에서 부분 변경된 객체를 새로 반환하도록 수정 * feat: 회원 정보 수정 API 구현 및 테스트 작성 * test: JwtTokenProviderTest 작성 - 로컬에서 Postman 테스트 시 이 테스트를 사용하면 쉽게 토큰 발급 후 활용 가능 * chore: 로컬 환경용 더미데이터 sql 작성 * chore: 로컬 환경 data.sql을 위한 서브모듈 변경 * docs: 기능 명세서 및 테스트 코드 용어 정리 (유저, 멤버 -> 회원) * chore: 로그 환경설정 파일 디렉터리 분리 * feat: 닉네임 중복 검증 구현 * refactor: 회원의 이메일 Unique 제약조건 삭제 - 닉네임, OauthId로 회원을 식별할 수 있다. - 같은 이메일로 네이버, 카카오에 가입한 사람이 소셜 로그인으로 두 계정을 만들 경우, 동일한 이메일이 저장될 수도 있다. * refactor: 모호한 메서드명 수정 * refactor: Email이 Unique하지 않음에 따라 테스트에서 사용하는 조회 쿼리 변경 - findByEmail 대신 findById - 기본키가 아닌 유일키로 조회하는 건 테이블 구조 변경 여지가 있으므로 findById 사용 * refactor: 내 정보 수정 API URI 변경 * fix: 디렉터리 분리에 따른 로그 설정 파일 appender 경로 수정 --- backend/docs/README.md | 10 +-- backend/src/docs/asciidoc/bookmark.adoc | 4 +- backend/src/docs/asciidoc/member.adoc | 14 +-- backend/src/docs/asciidoc/permission.adoc | 4 +- backend/src/docs/asciidoc/pin.adoc | 2 +- backend/src/docs/asciidoc/topic.adoc | 2 +- .../application/MemberCommandService.java | 43 +++++++++ .../mapbefine/member/domain/Member.java | 14 +-- .../mapbefine/member/domain/MemberInfo.java | 7 +- .../member/domain/MemberRepository.java | 8 +- .../mapbefine/member/domain/Role.java | 2 +- .../dto/request/MemberUpdateRequest.java | 6 ++ .../member/exception/MemberErrorCode.java | 1 + .../member/exception/MemberException.java | 7 ++ .../member/presentation/MemberController.java | 16 +++- backend/src/main/resources/config | 2 +- backend/src/main/resources/data-default.sql | 16 ++++ backend/src/main/resources/logback-spring.xml | 18 ++-- .../{ => logback}/console-appender.xml | 0 .../{ => logback}/file-debug-appender.xml | 0 .../{ => logback}/file-error-appender.xml | 0 .../{ => logback}/file-hibernate-appender.xml | 0 .../{ => logback}/file-info-appender.xml | 0 .../{ => logback}/file-warn-appender.xml | 0 .../{ => logback}/slack-error-appender.xml | 0 .../application/AtlasCommandServiceTest.java | 2 +- .../infrastructure/JwtTokenProviderTest.java | 29 ++++++ .../bookmark/BookmarkIntegrationTest.java | 4 +- .../BookmarkCommandServiceTest.java | 4 +- .../presentation/BookmarkControllerTest.java | 6 +- .../member/MemberIntegrationTest.java | 38 +++++--- .../application/MemberCommandServiceTest.java | 89 +++++++++++++++++++ .../application/MemberQueryServiceTest.java | 14 +-- .../member/domain/MemberInfoTest.java | 39 ++++++-- .../presentation/MemberControllerTest.java | 27 ++++-- .../oauth/application/OauthServiceTest.java | 4 +- .../presentation/OauthControllerTest.java | 2 +- .../permission/PermissionIntegrationTest.java | 4 +- .../PermissionCommandServiceTest.java | 8 +- .../mapbefine/pin/PinIntegrationTest.java | 2 +- .../pin/application/PinQueryServiceTest.java | 2 +- .../pin/presentation/PinControllerTest.java | 2 +- .../mapbefine/topic/TopicFixture.java | 4 +- .../mapbefine/topic/TopicIntegrationTest.java | 2 +- .../application/TopicCommandServiceTest.java | 4 +- .../application/TopicQueryServiceTest.java | 14 +-- .../topic/domain/TopicStatusTest.java | 2 +- .../presentation/TopicControllerTest.java | 2 +- 48 files changed, 367 insertions(+), 113 deletions(-) create mode 100644 backend/src/main/java/com/mapbefine/mapbefine/member/application/MemberCommandService.java create mode 100644 backend/src/main/java/com/mapbefine/mapbefine/member/dto/request/MemberUpdateRequest.java create mode 100644 backend/src/main/resources/data-default.sql rename backend/src/main/resources/{ => logback}/console-appender.xml (100%) rename backend/src/main/resources/{ => logback}/file-debug-appender.xml (100%) rename backend/src/main/resources/{ => logback}/file-error-appender.xml (100%) rename backend/src/main/resources/{ => logback}/file-hibernate-appender.xml (100%) rename backend/src/main/resources/{ => logback}/file-info-appender.xml (100%) rename backend/src/main/resources/{ => logback}/file-warn-appender.xml (100%) rename backend/src/main/resources/{ => logback}/slack-error-appender.xml (100%) create mode 100644 backend/src/test/java/com/mapbefine/mapbefine/auth/infrastructure/JwtTokenProviderTest.java create mode 100644 backend/src/test/java/com/mapbefine/mapbefine/member/application/MemberCommandServiceTest.java diff --git a/backend/docs/README.md b/backend/docs/README.md index 54684f71..4834bd96 100644 --- a/backend/docs/README.md +++ b/backend/docs/README.md @@ -39,19 +39,19 @@ - 핀 상세 조회 -#### 유저 핀 +#### 회원 핀 -- 유저 핀 생성 +- 회원 핀 생성 -- 유저 핀 정보 수정 +- 회원 핀 정보 수정 - name, description 만 수정 가능하다. - description 은 1000자 까지만 가능하다. -- 유저 핀 삭제 +- 회원 핀 삭제 - Delete 는 Soft Delete --- -- 유저 핀 목록 조회 +- 회원 핀 목록 조회 - 페이지 네이션 (무한 스크롤, 일단 15개) diff --git a/backend/src/docs/asciidoc/bookmark.adoc b/backend/src/docs/asciidoc/bookmark.adoc index d416a87d..64acd65e 100644 --- a/backend/src/docs/asciidoc/bookmark.adoc +++ b/backend/src/docs/asciidoc/bookmark.adoc @@ -1,10 +1,10 @@ == 즐겨찾기 -=== 토픽을 유저의 즐겨찾기에 추가 +=== 토픽을 회원의 즐겨찾기에 추가 operation::bookmark-controller-test/add-topic-in-bookmark[snippets='http-request,http-response'] -=== 유저의 토픽 즐겨찾기 삭제 +=== 회원의 토픽 즐겨찾기 삭제 operation::bookmark-controller-test/delete-topic-in-bookmark[snippets='http-request,http-response'] diff --git a/backend/src/docs/asciidoc/member.adoc b/backend/src/docs/asciidoc/member.adoc index 94ffcb6e..114112e4 100644 --- a/backend/src/docs/asciidoc/member.adoc +++ b/backend/src/docs/asciidoc/member.adoc @@ -1,25 +1,25 @@ -== 유저 +== 회원 -=== 유저 목록 조회 +=== 회원 목록 조회 operation::member-controller-test/find-all-member[snippets='http-request,http-response'] -=== 유저 단일 조회 +=== 회원 단일 조회 operation::member-controller-test/find-member-by-id[snippets='http-request,http-response'] -=== 유저의 나의 지도 목록 조회 +=== 회원의 나의 지도 목록 조회 operation::member-controller-test/find-my-all-topics[snippets='http-request,http-response'] -=== 유저의 나의 핀 목록 조회 +=== 회원의 나의 핀 목록 조회 operation::member-controller-test/find-my-all-pins[snippets='http-request,http-response'] -=== 유저의 모아보기 조회 +=== 회원의 모아보기 조회 operation::member-controller-test/find-all-topics-in-atlas[snippets='http-request,http-response'] -=== 유저의 즐겨찾기 조회 +=== 회원의 즐겨찾기 조회 operation::member-controller-test/find-all-topics-in-bookmark[snippets='http-request,http-response'] diff --git a/backend/src/docs/asciidoc/permission.adoc b/backend/src/docs/asciidoc/permission.adoc index c704d7ed..4b232e68 100644 --- a/backend/src/docs/asciidoc/permission.adoc +++ b/backend/src/docs/asciidoc/permission.adoc @@ -8,10 +8,10 @@ operation::permission-controller-test/add-permission[snippets='http-request,http operation::permission-controller-test/delete-permission[snippets='http-request,http-response'] -=== 토픽에 권한을 가진 유저 목록 조회 +=== 토픽에 권한을 가진 회원 목록 조회 operation::permission-controller-test/find-all-topic-permissions[snippets='http-request,http-response'] -=== 토픽에 권한을 가진 유저 단일 조회 +=== 토픽에 권한을 가진 회원 단일 조회 operation::permission-controller-test/find-permission-by-id[snippets='http-request,http-response'] diff --git a/backend/src/docs/asciidoc/pin.adoc b/backend/src/docs/asciidoc/pin.adoc index 244d156e..33b15e36 100644 --- a/backend/src/docs/asciidoc/pin.adoc +++ b/backend/src/docs/asciidoc/pin.adoc @@ -8,7 +8,7 @@ operation::pin-controller-test/find-all[snippets='http-request,http-response'] operation::pin-controller-test/find-by-id[snippets='http-request,http-response'] -=== 멤버별 핀 목록 조회 +=== 회원별 핀 목록 조회 operation::pin-controller-test/find-all-pins-by-member-id[snippets='http-request,http-response'] diff --git a/backend/src/docs/asciidoc/topic.adoc b/backend/src/docs/asciidoc/topic.adoc index 524f94a3..42356837 100644 --- a/backend/src/docs/asciidoc/topic.adoc +++ b/backend/src/docs/asciidoc/topic.adoc @@ -12,7 +12,7 @@ operation::topic-controller-test/find-all-by-order-by-updated-at-desc[snippets=' operation::topic-controller-test/find-all-best-topics[snippets='http-request,http-response'] -=== 멤버별 토픽 목록 조회 +=== 회원별 토픽 목록 조회 operation::topic-controller-test/find-all-topics-by-member-id[snippets='http-request,http-response'] diff --git a/backend/src/main/java/com/mapbefine/mapbefine/member/application/MemberCommandService.java b/backend/src/main/java/com/mapbefine/mapbefine/member/application/MemberCommandService.java new file mode 100644 index 00000000..d7c315f4 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/member/application/MemberCommandService.java @@ -0,0 +1,43 @@ +package com.mapbefine.mapbefine.member.application; + +import com.mapbefine.mapbefine.auth.domain.AuthMember; +import com.mapbefine.mapbefine.member.domain.Member; +import com.mapbefine.mapbefine.member.domain.MemberRepository; +import com.mapbefine.mapbefine.member.dto.request.MemberUpdateRequest; +import com.mapbefine.mapbefine.member.exception.MemberErrorCode; +import com.mapbefine.mapbefine.member.exception.MemberException.MemberConflictException; +import java.util.NoSuchElementException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +public class MemberCommandService { + + private final MemberRepository memberRepository; + + public MemberCommandService(MemberRepository memberRepository) { + this.memberRepository = memberRepository; + } + + public void updateInfoById(AuthMember authMember, MemberUpdateRequest request) { + Member member = findMemberById(authMember.getMemberId()); + String nickName = request.nickName(); + + validateNicknameDuplicated(nickName); + + member.update(nickName); + } + + private Member findMemberById(Long memberId) { + return memberRepository.findById(memberId) + .orElseThrow(() -> new NoSuchElementException("findMemberById; memberId=" + memberId)); + } + + private void validateNicknameDuplicated(String nickName) { + if (memberRepository.existsByMemberInfoNickName(nickName)) { + throw new MemberConflictException(MemberErrorCode.ILLEGAL_NICKNAME_ALREADY_EXISTS, nickName); + } + } + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/member/domain/Member.java b/backend/src/main/java/com/mapbefine/mapbefine/member/domain/Member.java index 1d64325e..31ebf243 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/member/domain/Member.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/member/domain/Member.java @@ -100,22 +100,14 @@ private static String createNickname(String nickname) { private static String createNicknameSuffix() { return randomUUID() .toString() - .replaceAll("-", "") + .replace("-", "") .substring(0, DEFAULT_NICKNAME_SUFFIX_LENGTH); } public void update( - String nickName, - String email, - String imageUrl + String nickName ) { - memberInfo = MemberInfo.of( - nickName, - email, - imageUrl, - memberInfo.getRole(), - memberInfo.getStatus() - ); + memberInfo = memberInfo.createUpdatedMemberInfo(nickName); } public void addTopic(Topic topic) { diff --git a/backend/src/main/java/com/mapbefine/mapbefine/member/domain/MemberInfo.java b/backend/src/main/java/com/mapbefine/mapbefine/member/domain/MemberInfo.java index 08276157..ee36d80a 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/member/domain/MemberInfo.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/member/domain/MemberInfo.java @@ -28,7 +28,7 @@ public class MemberInfo { @Column(nullable = false, length = 20, unique = true) private String nickName; - @Column(nullable = false, unique = true) + @Column(nullable = false) private String email; @Column(nullable = false) @@ -108,6 +108,11 @@ private static void validateStatus(Status status) { } } + public MemberInfo createUpdatedMemberInfo(String nickName) { + + return MemberInfo.of(nickName, this.email, this.imageUrl.getImageUrl(), this.role, this.status); + } + public String getImageUrl() { return imageUrl.getImageUrl(); } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/member/domain/MemberRepository.java b/backend/src/main/java/com/mapbefine/mapbefine/member/domain/MemberRepository.java index 65125aa1..05be31d2 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/member/domain/MemberRepository.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/member/domain/MemberRepository.java @@ -6,14 +6,12 @@ public interface MemberRepository extends JpaRepository { - Optional findByMemberInfoEmail(String email); - - boolean existsByMemberInfoEmail(String email); - - Optional findByOauthIdOauthServerId(Long oauthServerId); + Optional findById(Long id); Optional findByOauthId(OauthId oauthId); + boolean existsByMemberInfoNickName(String nickName); + List findAllByMemberInfoRole(Role role); } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/member/domain/Role.java b/backend/src/main/java/com/mapbefine/mapbefine/member/domain/Role.java index 9222f524..96b74983 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/member/domain/Role.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/member/domain/Role.java @@ -9,7 +9,7 @@ public enum Role { ADMIN("ROLE_ADMIN", "운영자"), - USER("ROLE_USER", "로그인 유저"), + USER("ROLE_USER", "로그인 회원"), GUEST("ROLE_GUEST", "손님"); private final String key; diff --git a/backend/src/main/java/com/mapbefine/mapbefine/member/dto/request/MemberUpdateRequest.java b/backend/src/main/java/com/mapbefine/mapbefine/member/dto/request/MemberUpdateRequest.java new file mode 100644 index 00000000..bada64c9 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/member/dto/request/MemberUpdateRequest.java @@ -0,0 +1,6 @@ +package com.mapbefine.mapbefine.member.dto.request; + +public record MemberUpdateRequest( + String nickName +) { +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/member/exception/MemberErrorCode.java b/backend/src/main/java/com/mapbefine/mapbefine/member/exception/MemberErrorCode.java index b0c5d034..98c42af4 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/member/exception/MemberErrorCode.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/member/exception/MemberErrorCode.java @@ -11,6 +11,7 @@ public enum MemberErrorCode { ILLEGAL_EMAIL_PATTERN("05003", "올바르지 않은 이메일 형식입니다."), FORBIDDEN_MEMBER_STATUS("05100", "탈퇴 혹은 차단된 회원입니다."), MEMBER_NOT_FOUND("05400", "존재하지 않는 회원입니다."), + ILLEGAL_NICKNAME_ALREADY_EXISTS("05900", "이미 존재하는 닉네임입니다."), ; private final String code; diff --git a/backend/src/main/java/com/mapbefine/mapbefine/member/exception/MemberException.java b/backend/src/main/java/com/mapbefine/mapbefine/member/exception/MemberException.java index 89a68948..b582c64c 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/member/exception/MemberException.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/member/exception/MemberException.java @@ -1,6 +1,7 @@ package com.mapbefine.mapbefine.member.exception; import com.mapbefine.mapbefine.common.exception.BadRequestException; +import com.mapbefine.mapbefine.common.exception.ConflictException; import com.mapbefine.mapbefine.common.exception.ErrorCode; import com.mapbefine.mapbefine.common.exception.ForbiddenException; import com.mapbefine.mapbefine.common.exception.NotFoundException; @@ -25,5 +26,11 @@ public MemberForbiddenException(MemberErrorCode errorCode, Long id) { } } + public static class MemberConflictException extends ConflictException { + public MemberConflictException(MemberErrorCode errorCode, String value) { + super(new ErrorCode<>(errorCode.getCode(), errorCode.getMessage(), value)); + } + } + } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/member/presentation/MemberController.java b/backend/src/main/java/com/mapbefine/mapbefine/member/presentation/MemberController.java index 119ab4fd..a9e63c5e 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/member/presentation/MemberController.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/member/presentation/MemberController.java @@ -2,7 +2,9 @@ import com.mapbefine.mapbefine.auth.domain.AuthMember; import com.mapbefine.mapbefine.common.interceptor.LoginRequired; +import com.mapbefine.mapbefine.member.application.MemberCommandService; import com.mapbefine.mapbefine.member.application.MemberQueryService; +import com.mapbefine.mapbefine.member.dto.request.MemberUpdateRequest; import com.mapbefine.mapbefine.member.dto.response.MemberDetailResponse; import com.mapbefine.mapbefine.member.dto.response.MemberResponse; import com.mapbefine.mapbefine.pin.dto.response.PinResponse; @@ -10,7 +12,9 @@ import java.util.List; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -18,9 +22,11 @@ @RequestMapping("/members") public class MemberController { + private final MemberCommandService memberCommandService; private final MemberQueryService memberQueryService; - public MemberController(MemberQueryService memberQueryService) { + public MemberController(MemberCommandService memberCommandService, MemberQueryService memberQueryService) { + this.memberCommandService = memberCommandService; this.memberQueryService = memberQueryService; } @@ -72,4 +78,12 @@ public ResponseEntity> findAllTopicsInBookmark(AuthMember au return ResponseEntity.ok(responses); } + @LoginRequired + @PatchMapping("/my/profiles") + public ResponseEntity updateMyInfo(AuthMember authMember, @RequestBody MemberUpdateRequest request) { + memberCommandService.updateInfoById(authMember, request); + + return ResponseEntity.ok().build(); + } + } diff --git a/backend/src/main/resources/config b/backend/src/main/resources/config index 9842778b..0b448b77 160000 --- a/backend/src/main/resources/config +++ b/backend/src/main/resources/config @@ -1 +1 @@ -Subproject commit 9842778b9712e4c1eb5c918e2fa254edbafb8a04 +Subproject commit 0b448b7781204387184f5ce47a575a5b2d955128 diff --git a/backend/src/main/resources/data-default.sql b/backend/src/main/resources/data-default.sql new file mode 100644 index 00000000..75f9969c --- /dev/null +++ b/backend/src/main/resources/data-default.sql @@ -0,0 +1,16 @@ +INSERT INTO member (nick_name, email, image_url, role, + oauth_server_id, oauth_server_type, + created_at, updated_at) +VALUES ('dummyMember', 'dummy@gmail.com', 'https://map-befine-official.github.io/favicon.png', 'USER', + 1L, 'KAKAO', + now(), now()); + +INSERT INTO topic (name, image_url, description, + permission_type, publicity, + member_id, + created_at, updated_at) +VALUES ('dummyTopic', 'https://map-befine-official.github.io/favicon.png', 'description', + 'ALL_MEMBERS', 'PUBLIC', + 1L, + now(), now()) +; diff --git a/backend/src/main/resources/logback-spring.xml b/backend/src/main/resources/logback-spring.xml index 4bbde1b2..944c99dd 100644 --- a/backend/src/main/resources/logback-spring.xml +++ b/backend/src/main/resources/logback-spring.xml @@ -9,7 +9,7 @@ value="%green(%d{yyyy-MM-dd HH:mm:ss.SSS}) [%thread] ${PID} %highlight(%-5level) %cyan(%logger) - %msg%n"/> - + @@ -17,10 +17,10 @@ - - - - + + + + @@ -36,10 +36,10 @@ - - - - + + + + diff --git a/backend/src/main/resources/console-appender.xml b/backend/src/main/resources/logback/console-appender.xml similarity index 100% rename from backend/src/main/resources/console-appender.xml rename to backend/src/main/resources/logback/console-appender.xml diff --git a/backend/src/main/resources/file-debug-appender.xml b/backend/src/main/resources/logback/file-debug-appender.xml similarity index 100% rename from backend/src/main/resources/file-debug-appender.xml rename to backend/src/main/resources/logback/file-debug-appender.xml diff --git a/backend/src/main/resources/file-error-appender.xml b/backend/src/main/resources/logback/file-error-appender.xml similarity index 100% rename from backend/src/main/resources/file-error-appender.xml rename to backend/src/main/resources/logback/file-error-appender.xml diff --git a/backend/src/main/resources/file-hibernate-appender.xml b/backend/src/main/resources/logback/file-hibernate-appender.xml similarity index 100% rename from backend/src/main/resources/file-hibernate-appender.xml rename to backend/src/main/resources/logback/file-hibernate-appender.xml diff --git a/backend/src/main/resources/file-info-appender.xml b/backend/src/main/resources/logback/file-info-appender.xml similarity index 100% rename from backend/src/main/resources/file-info-appender.xml rename to backend/src/main/resources/logback/file-info-appender.xml diff --git a/backend/src/main/resources/file-warn-appender.xml b/backend/src/main/resources/logback/file-warn-appender.xml similarity index 100% rename from backend/src/main/resources/file-warn-appender.xml rename to backend/src/main/resources/logback/file-warn-appender.xml diff --git a/backend/src/main/resources/slack-error-appender.xml b/backend/src/main/resources/logback/slack-error-appender.xml similarity index 100% rename from backend/src/main/resources/slack-error-appender.xml rename to backend/src/main/resources/logback/slack-error-appender.xml diff --git a/backend/src/test/java/com/mapbefine/mapbefine/atlas/application/AtlasCommandServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/atlas/application/AtlasCommandServiceTest.java index ba20c8b5..28b4975c 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/atlas/application/AtlasCommandServiceTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/atlas/application/AtlasCommandServiceTest.java @@ -105,7 +105,7 @@ void validateReadPermission_fail() { } @Test - @DisplayName("멤버 ID와 TopicId가 있을 경우, atlas에서 해당 topic을 비운다.") + @DisplayName("회원 ID와 TopicId가 있을 경우, atlas에서 해당 topic을 비운다.") void remove_Success() { Long topicId = topic.getId(); Long memberId = authMember.getMemberId(); diff --git a/backend/src/test/java/com/mapbefine/mapbefine/auth/infrastructure/JwtTokenProviderTest.java b/backend/src/test/java/com/mapbefine/mapbefine/auth/infrastructure/JwtTokenProviderTest.java new file mode 100644 index 00000000..8081cf84 --- /dev/null +++ b/backend/src/test/java/com/mapbefine/mapbefine/auth/infrastructure/JwtTokenProviderTest.java @@ -0,0 +1,29 @@ +package com.mapbefine.mapbefine.auth.infrastructure; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; + +@SpringBootTest(classes = JwtTokenProvider.class) +@TestPropertySource(locations = "classpath:application.yml") +class JwtTokenProviderTest { + + @Autowired + private JwtTokenProvider jwtTokenProvider; + + @Test + @DisplayName("payload를 받아 JWT 토큰을 생성한다.") + void createToken() { + String payload = "1"; + + String token = jwtTokenProvider.createToken(payload); + + assertThat(jwtTokenProvider.getPayload(token)) + .isEqualTo(payload); + } + +} diff --git a/backend/src/test/java/com/mapbefine/mapbefine/bookmark/BookmarkIntegrationTest.java b/backend/src/test/java/com/mapbefine/mapbefine/bookmark/BookmarkIntegrationTest.java index bbe7e6ff..022fefba 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/bookmark/BookmarkIntegrationTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/bookmark/BookmarkIntegrationTest.java @@ -33,7 +33,7 @@ public class BookmarkIntegrationTest extends IntegrationTest { private BookmarkRepository bookmarkRepository; @Test - @DisplayName("유저가 토픽을 즐겨찾기 목록에 추가하면, 201을 반환한다.") + @DisplayName("회원이 토픽을 즐겨찾기 목록에 추가하면, 201을 반환한다.") void addTopicInBookmark_Success() { //given Member creator = MemberFixture.create("member", "member@naver.com", Role.USER); @@ -63,7 +63,7 @@ void addTopicInBookmark_Success() { } @Test - @DisplayName("유저의 즐겨찾기 토픽을 삭제하면, 204를 반환한다.") + @DisplayName("회원의 즐겨찾기 토픽을 삭제하면, 204를 반환한다.") void deleteTopicInBookmark_Success() { //given Member creator = MemberFixture.create("member", "member@naver.com", Role.USER); diff --git a/backend/src/test/java/com/mapbefine/mapbefine/bookmark/application/BookmarkCommandServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/bookmark/application/BookmarkCommandServiceTest.java index 76429f97..4db118c8 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/bookmark/application/BookmarkCommandServiceTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/bookmark/application/BookmarkCommandServiceTest.java @@ -39,7 +39,7 @@ class BookmarkCommandServiceTest { private TestEntityManager testEntityManager; @Test - @DisplayName("다른 유저의 토픽을 즐겨찾기에 추가할 수 있다.") + @DisplayName("다른 회원의 토픽을 즐겨찾기에 추가할 수 있다.") public void addTopicInBookmark_Success() { //given Member creator = MemberFixture.create( @@ -72,7 +72,7 @@ public void addTopicInBookmark_Success() { } @Test - @DisplayName("권한이 없는 다른 유저의 토픽을 즐겨찾기에 추가할 수 없다.") + @DisplayName("권한이 없는 다른 회원의 토픽을 즐겨찾기에 추가할 수 없다.") public void addTopicInBookmark_Fail1() { //given Member creator = MemberFixture.create( diff --git a/backend/src/test/java/com/mapbefine/mapbefine/bookmark/presentation/BookmarkControllerTest.java b/backend/src/test/java/com/mapbefine/mapbefine/bookmark/presentation/BookmarkControllerTest.java index ff3e2307..b65443d9 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/bookmark/presentation/BookmarkControllerTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/bookmark/presentation/BookmarkControllerTest.java @@ -19,7 +19,7 @@ class BookmarkControllerTest extends RestDocsIntegration { @Test - @DisplayName("토픽을 유저의 즐겨찾기에 추가") + @DisplayName("토픽을 회원의 즐겨찾기에 추가") public void addTopicInBookmark() throws Exception { given(bookmarkCommandService.addTopicInBookmark(any(), any())).willReturn(1L); @@ -31,7 +31,7 @@ public void addTopicInBookmark() throws Exception { } @Test - @DisplayName("유저의 토픽 즐겨찾기 목록 삭제") + @DisplayName("회원의 토픽 즐겨찾기 목록 삭제") public void deleteTopicInBookmark() throws Exception { doNothing().when(bookmarkCommandService).deleteTopicInBookmark(any(), any()); @@ -42,4 +42,4 @@ public void deleteTopicInBookmark() throws Exception { ).andDo(restDocs.document()); } -} \ No newline at end of file +} diff --git a/backend/src/test/java/com/mapbefine/mapbefine/member/MemberIntegrationTest.java b/backend/src/test/java/com/mapbefine/mapbefine/member/MemberIntegrationTest.java index afdbad42..aaf288b2 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/member/MemberIntegrationTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/member/MemberIntegrationTest.java @@ -1,7 +1,7 @@ package com.mapbefine.mapbefine.member; import static com.mapbefine.mapbefine.oauth.domain.OauthServerType.KAKAO; -import static io.restassured.RestAssured.given; +import static io.restassured.RestAssured.*; import static org.apache.http.HttpHeaders.AUTHORIZATION; import static org.assertj.core.api.Assertions.assertThat; @@ -10,12 +10,12 @@ import com.mapbefine.mapbefine.member.domain.MemberRepository; import com.mapbefine.mapbefine.member.domain.OauthId; import com.mapbefine.mapbefine.member.domain.Role; +import com.mapbefine.mapbefine.member.dto.request.MemberUpdateRequest; import com.mapbefine.mapbefine.member.dto.response.MemberDetailResponse; import com.mapbefine.mapbefine.member.dto.response.MemberResponse; -import io.restassured.RestAssured; -import io.restassured.common.mapper.TypeRef; -import io.restassured.response.ExtractableResponse; -import io.restassured.response.Response; +import io.restassured.*; +import io.restassured.common.mapper.*; +import io.restassured.response.*; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -69,7 +69,7 @@ public void setUp() { } @Test - @DisplayName("유저 목록을 조회한다.") + @DisplayName("회원 목록을 조회한다.") void findAllMember() { // given, when ExtractableResponse response = given().log().all() @@ -94,7 +94,7 @@ void findAllMember() { } @Test - @DisplayName("유저를 단일 조회한다.") + @DisplayName("회원을 단일 조회한다.") void findMemberById() { // given, when ExtractableResponse response = given().log().all() @@ -115,7 +115,7 @@ void findMemberById() { } @Test - @DisplayName("로그인 유저가 내 지도 목록을 조회하면, 200을 반환한다.") + @DisplayName("로그인 회원이 내 지도 목록을 조회하면, 200을 반환한다.") void findMyAllTopics_Success() { //when ExtractableResponse response = given().log().all() @@ -131,7 +131,7 @@ void findMyAllTopics_Success() { @Test - @DisplayName("로그인 유저가 내 지도 목록을 조회하면, 200을 반환한다.") + @DisplayName("로그인 회원이 내 지도 목록을 조회하면, 200을 반환한다.") void findMyAllPins_Success() { //when ExtractableResponse response = given().log().all() @@ -147,14 +147,13 @@ void findMyAllPins_Success() { @Test - @DisplayName("유저의 즐겨찾기 토픽 목록을 조회하면, 200을 반환한다.") + @DisplayName("회원의 즐겨찾기 토픽 목록을 조회하면, 200을 반환한다.") void findTopicsInBookmarks_Success() { //when ExtractableResponse response = given().log().all() .header(AUTHORIZATION, creatorAuthHeader) .accept(MediaType.APPLICATION_JSON_VALUE) .when().get("/members/my/bookmarks") - .then().log().all() .extract(); @@ -170,7 +169,6 @@ void findTopicsFromAtlas_Success() { .log().all() .header(HttpHeaders.AUTHORIZATION, creatorAuthHeader) .when().get("/members/my/atlas") - .then().log().all() .extract(); @@ -178,5 +176,21 @@ void findTopicsFromAtlas_Success() { assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); } + @Test + @DisplayName("회원 정보를 정상적으로 수정하면, 200을 반환한다") + void updateMemberInfo_Success() { + // when + ExtractableResponse response = RestAssured.given() + .log().all() + .header(HttpHeaders.AUTHORIZATION, creatorAuthHeader) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(new MemberUpdateRequest("new nickname")) + .when().patch("/members/my/profiles") + .then().log().all() + .extract(); + // then + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); + } + } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/member/application/MemberCommandServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/member/application/MemberCommandServiceTest.java new file mode 100644 index 00000000..388a52b9 --- /dev/null +++ b/backend/src/test/java/com/mapbefine/mapbefine/member/application/MemberCommandServiceTest.java @@ -0,0 +1,89 @@ +package com.mapbefine.mapbefine.member.application; + +import static java.util.Collections.emptyList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +import com.mapbefine.mapbefine.auth.domain.AuthMember; +import com.mapbefine.mapbefine.auth.domain.member.User; +import com.mapbefine.mapbefine.common.annotation.ServiceTest; +import com.mapbefine.mapbefine.member.MemberFixture; +import com.mapbefine.mapbefine.member.domain.Member; +import com.mapbefine.mapbefine.member.domain.MemberInfo; +import com.mapbefine.mapbefine.member.domain.MemberRepository; +import com.mapbefine.mapbefine.member.domain.Role; +import com.mapbefine.mapbefine.member.dto.request.MemberUpdateRequest; +import com.mapbefine.mapbefine.member.exception.MemberException.MemberConflictException; +import java.util.NoSuchElementException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +@ServiceTest +class MemberCommandServiceTest { + + @Autowired + private MemberCommandService memberCommandService; + @Autowired + private MemberRepository memberRepository; + private Member member; + private AuthMember authMember; + + @BeforeEach + void setUp() { + member = memberRepository.save(MemberFixture.create("member", "member@gmail.com", Role.USER)); + authMember = new User(member.getId(), emptyList(), emptyList()); + } + + @Test + @DisplayName("회원 정보를 수정한다.") + void updateInfoById_Success() { + // given + String expected = "new nickname"; + MemberUpdateRequest request = new MemberUpdateRequest(expected); + + // when + memberCommandService.updateInfoById(authMember, request); + memberRepository.flush(); + + // then + memberRepository.findById(member.getId()) + .map(Member::getMemberInfo) + .ifPresentOrElse( + actual -> { + assertThat(actual).usingRecursiveComparison() + .ignoringFields("nickName") + .isEqualTo(member.getMemberInfo()); + assertThat(actual.getNickName()).isEqualTo(expected); + }, + Assertions::fail + ); + } + + @Test + @DisplayName("존재하지 않는 회원의 정보를 수정하면 예외가 발생한다.") + void updateInfoById_FailByNotExistingMember() { + // given + MemberUpdateRequest request = new MemberUpdateRequest("new nickname"); + User notExistingMember = new User(100L, emptyList(), emptyList()); + + // when, then + assertThatThrownBy(() -> memberCommandService.updateInfoById(notExistingMember, request)) + .isInstanceOf(NoSuchElementException.class); + } + + @Test + @DisplayName("회원 정보 수정 시, 이미 존재하는 닉네임을 설정하면 예외가 발생한다.") + void updateInfoById_FailByDuplicatedNickName() { + // given + MemberInfo memberInfo = member.getMemberInfo(); + MemberUpdateRequest request = new MemberUpdateRequest(memberInfo.getNickName()); + + // when, then + assertThatThrownBy(() -> memberCommandService.updateInfoById(authMember, request)) + .isInstanceOf(MemberConflictException.class); + } + +} diff --git a/backend/src/test/java/com/mapbefine/mapbefine/member/application/MemberQueryServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/member/application/MemberQueryServiceTest.java index 7152ad0c..2e0d8a4c 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/member/application/MemberQueryServiceTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/member/application/MemberQueryServiceTest.java @@ -52,7 +52,6 @@ class MemberQueryServiceTest { @Autowired private LocationRepository locationRepository; - private AuthMember authMember; private Member member; private List topics; @@ -78,7 +77,7 @@ private void createTopics(Member member) { } @Test - @DisplayName("유저 목록을 조회한다.") + @DisplayName("회원 목록을 조회한다.") void findAllMember() { // given Member member2 = memberRepository.save( @@ -101,7 +100,7 @@ void findAllMember() { } @Test - @DisplayName("유저를 단일 조회한다.") + @DisplayName("회원을 단일 조회한다.") void findMemberById() { // given Member member = memberRepository.save( @@ -117,7 +116,7 @@ void findMemberById() { } @Test - @DisplayName("조회하려는 유저가 없는 경우 예외를 반환한다.") + @DisplayName("조회하려는 회원이 없는 경우 예외를 반환한다.") void findMemberById_whenNoneExists_thenFail() { // given when then assertThatThrownBy(() -> memberQueryService.findById(Long.MAX_VALUE)) @@ -127,7 +126,7 @@ void findMemberById_whenNoneExists_thenFail() { @Test @DisplayName("즐겨찾기 목록에 추가 된 토픽을 조회할 수 있다") - public void findAllTopicsInBookmark_success() { + void findAllTopicsInBookmark_success() { // when List allTopicsInBookmark = memberQueryService.findAllTopicsInBookmark(authMember); @@ -143,7 +142,7 @@ public void findAllTopicsInBookmark_success() { @Test - @DisplayName("멤버 ID를 이용해 모아보기할 모든 Topic들을 가져올 수 있다.") + @DisplayName("회원 ID를 이용해 모아보기할 모든 Topic들을 가져올 수 있다.") void findAtlasByMember_Success() { // when List allTopicsInAtlas = memberQueryService.findAllTopicsInAtlas(authMember); @@ -175,7 +174,7 @@ void findMyAllTopics_Success() { } @Test - @DisplayName("로그인한 유저가 생성한 모든 핀을 가져올 수 있다.") + @DisplayName("로그인한 회원이 생성한 모든 핀을 가져올 수 있다.") void findMyAllPins_Success() { // given Location location = locationRepository.save(LocationFixture.create()); @@ -199,4 +198,5 @@ void findMyAllPins_Success() { .isEqualTo(pinIds); } + } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/member/domain/MemberInfoTest.java b/backend/src/test/java/com/mapbefine/mapbefine/member/domain/MemberInfoTest.java index 1ed6e737..d1907931 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/member/domain/MemberInfoTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/member/domain/MemberInfoTest.java @@ -2,6 +2,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.SoftAssertions.assertSoftly; import com.mapbefine.mapbefine.common.exception.BadRequestException.ImageBadRequestException; import com.mapbefine.mapbefine.member.exception.MemberException.MemberBadRequestException; @@ -25,8 +26,8 @@ class Validate { private final Status VALID_STATUS = Status.NORMAL; @Test - @DisplayName("정확한 값을 입력하면 객체가 생성된다") - void success() { + @DisplayName("유효한 정보를 입력했을 때 객체를 생성할 수 있다.") + void create_Success() { //given when MemberInfo memberInfo = MemberInfo.of( VALID_NICK_NAME, @@ -44,11 +45,35 @@ void success() { assertThat(memberInfo.getRole()).isEqualTo(VALID_ROLE); } + @Test + @DisplayName("일부 정보(닉네임)만 변경한 회원 정보를 생성할 수 있다.") + void createUpdatedMemberInfo() { + // given + MemberInfo before = MemberInfo.of( + "member", + "member@naver.com", + "https://map-befine-official.github.io/favicon.png", + Role.ADMIN, + Status.NORMAL); + + // when + String expected = "newNickName"; + MemberInfo patched = before.createUpdatedMemberInfo(expected); + + // then + assertSoftly(softly -> { + assertThat(patched).usingRecursiveComparison() + .ignoringFields("nickName") + .isEqualTo(before); + assertThat(patched.getNickName()).isEqualTo(expected); + }); + } + @ParameterizedTest @NullSource @ValueSource(strings = {"", "aaaaaaaaaaaaaaaaaaaaa"}) - @DisplayName("유효한 이름이 아닌 경우 예외가 발생한다") - void whenNameIsInvalid_thenFail(String invalidNickName) { + @DisplayName("유효한 닉네임이 아닌 경우 예외가 발생한다") + void validateNickName(String invalidNickName) { //given when then assertThatThrownBy(() -> MemberInfo.of( invalidNickName, @@ -64,7 +89,7 @@ void whenNameIsInvalid_thenFail(String invalidNickName) { @EmptySource @ValueSource(strings = "member") @DisplayName("유효한 이메일이 아닌 경우 예외가 발생한다") - void whenEmailIsInvalid_thenFail(String invalidEmail) { + void validateEmail(String invalidEmail) { //given when then assertThatThrownBy(() -> MemberInfo.of( VALID_NICK_NAME, @@ -77,7 +102,7 @@ void whenEmailIsInvalid_thenFail(String invalidEmail) { @Test @DisplayName("올바르지 않은 형식의 Image Url 이 들어오는 경우 예외가 발생한다.") - void whenImageUrlIsInvalid_thenFail() { + void validateImageUrl() { String invalidImageUrl = "image.png"; //given when then @@ -92,7 +117,7 @@ void whenImageUrlIsInvalid_thenFail() { @Test @DisplayName("유효하지 않은 Role 이 들어오는 경우 예외가 발생한다.") - void whenRoleIsInvalid_thenFail() { + void validateRole() { //given when then assertThatThrownBy(() -> MemberInfo.of( VALID_NICK_NAME, diff --git a/backend/src/test/java/com/mapbefine/mapbefine/member/presentation/MemberControllerTest.java b/backend/src/test/java/com/mapbefine/mapbefine/member/presentation/MemberControllerTest.java index 7efbf8c3..df12531d 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/member/presentation/MemberControllerTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/member/presentation/MemberControllerTest.java @@ -7,6 +7,7 @@ import com.mapbefine.mapbefine.common.RestDocsIntegration; import com.mapbefine.mapbefine.location.LocationFixture; import com.mapbefine.mapbefine.member.application.MemberQueryService; +import com.mapbefine.mapbefine.member.dto.request.MemberUpdateRequest; import com.mapbefine.mapbefine.member.dto.response.MemberDetailResponse; import com.mapbefine.mapbefine.member.dto.response.MemberResponse; import com.mapbefine.mapbefine.pin.dto.response.PinResponse; @@ -16,6 +17,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; class MemberControllerTest extends RestDocsIntegration { @@ -24,7 +26,7 @@ class MemberControllerTest extends RestDocsIntegration { private MemberQueryService memberQueryService; @Test - @DisplayName("유저 목록 조회") + @DisplayName("회원 목록 조회") void findAllMember() throws Exception { List memberResponses = List.of( new MemberResponse( @@ -48,7 +50,7 @@ void findAllMember() throws Exception { } @Test - @DisplayName("유저 단일 조회") + @DisplayName("회원 단일 조회") void findMemberById() throws Exception { MemberDetailResponse memberDetailResponse = new MemberDetailResponse( 1L, @@ -67,7 +69,7 @@ void findMemberById() throws Exception { } @Test - @DisplayName("유저의 모아보기 목록 조회") + @DisplayName("회원의 모아보기 목록 조회") void findAllTopicsInAtlas() throws Exception { List responses = List.of( new TopicResponse( @@ -102,7 +104,7 @@ void findAllTopicsInAtlas() throws Exception { } @Test - @DisplayName("유저의 즐겨찾기 목록 조회") + @DisplayName("회원의 즐겨찾기 목록 조회") void findAllTopicsInBookmark() throws Exception { List responses = List.of( new TopicResponse( @@ -137,7 +139,7 @@ void findAllTopicsInBookmark() throws Exception { } @Test - @DisplayName("유저의 지도 목록 조회") + @DisplayName("회원의 지도 목록 조회") void findMyAllTopics() throws Exception { List responses = List.of( new TopicResponse( @@ -172,7 +174,7 @@ void findMyAllTopics() throws Exception { } @Test - @DisplayName("유저의 핀 목록 조회") + @DisplayName("회원의 핀 목록 조회") void findMyAllPins() throws Exception { List responses = List.of( new PinResponse( @@ -202,4 +204,17 @@ void findMyAllPins() throws Exception { ).andDo(restDocs.document()); } + @Test + @DisplayName("회원의 정보 수정") + void updateMyInfo() throws Exception { + MemberUpdateRequest request = new MemberUpdateRequest("새로운 닉네임"); + + mockMvc.perform( + MockMvcRequestBuilders.patch("/members/my/profiles") + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + ).andDo(restDocs.document()); + } + } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/oauth/application/OauthServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/oauth/application/OauthServiceTest.java index 2d5c7122..be752de1 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/oauth/application/OauthServiceTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/oauth/application/OauthServiceTest.java @@ -97,7 +97,7 @@ void getAuthCodeRequestUrl_success() { void loginAndRegister_success() { // when LoginInfoResponse response = oauthService.login(KAKAO, "auth"); - Member savedMember = memberRepository.findByMemberInfoEmail("yshert@naver.com") + Member savedMember = memberRepository.findById(response.member().id()) .orElseThrow(NoSuchElementException::new); // then @@ -114,7 +114,7 @@ void loginAndRegister_success() { void login() { // given LoginInfoResponse firstLogin = oauthService.login(KAKAO, "auth"); - Member savedMember = memberRepository.findByMemberInfoEmail(firstLogin.member().email()) + Member savedMember = memberRepository.findById(firstLogin.member().id()) .orElseThrow(NoSuchElementException::new); // when diff --git a/backend/src/test/java/com/mapbefine/mapbefine/oauth/presentation/OauthControllerTest.java b/backend/src/test/java/com/mapbefine/mapbefine/oauth/presentation/OauthControllerTest.java index 0ed9e5d1..60fc1b56 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/oauth/presentation/OauthControllerTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/oauth/presentation/OauthControllerTest.java @@ -37,7 +37,7 @@ void redirection() throws Exception { } @Test - @DisplayName("소셜 로그인 성공시 로그인한 유저 정보 반환") + @DisplayName("소셜 로그인 성공시 로그인한 회원 정보 반환") void login() throws Exception { // given String code = "auth_code"; diff --git a/backend/src/test/java/com/mapbefine/mapbefine/permission/PermissionIntegrationTest.java b/backend/src/test/java/com/mapbefine/mapbefine/permission/PermissionIntegrationTest.java index 983dd8e0..cfa164f3 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/permission/PermissionIntegrationTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/permission/PermissionIntegrationTest.java @@ -79,7 +79,7 @@ public void setUp() { } @Test - @DisplayName("Topic 을 만든자가 특정 유저에게 권한을 준다.") + @DisplayName("Topic 을 만든자가 특정 회원에게 권한을 준다.") void addPermission() { // given Topic topic = topicRepository.save(TopicFixture.createByName("topicName", creator)); @@ -100,7 +100,7 @@ void addPermission() { } @Test - @DisplayName("Topic 을 만든자가 특정 유저에게 권한을 삭제한다.") + @DisplayName("Topic 을 만든자가 특정 회원에게 권한을 삭제한다.") void deletePermission() { // given Topic topic = topicRepository.save(TopicFixture.createByName("topicName", creator)); diff --git a/backend/src/test/java/com/mapbefine/mapbefine/permission/application/PermissionCommandServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/permission/application/PermissionCommandServiceTest.java index 9fe7ec61..73a86dc3 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/permission/application/PermissionCommandServiceTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/permission/application/PermissionCommandServiceTest.java @@ -99,7 +99,7 @@ void saveMemberTopicPermissionByCreator() { } @Test - @DisplayName("Creator 가 아닌 유저가 권한을 주려는 경우 예외가 발생한다.") + @DisplayName("Creator 가 아닌 회원이 권한을 주려는 경우 예외가 발생한다.") void saveMemberTopicPermissionByUser() { // given Member creator = memberRepository.save( @@ -127,7 +127,7 @@ void saveMemberTopicPermissionByUser() { } @Test - @DisplayName("Guest 가 유저에게 권한을 주려는 경우 예외가 발생한다.") + @DisplayName("Guest 가 회원에게 권한을 주려는 경우 예외가 발생한다.") void saveMemberTopicPermissionByGuest() { // given Member creator = memberRepository.save( @@ -262,7 +262,7 @@ void deleteMemberTopicPermissionByCreator() { } @Test - @DisplayName("creator 가 아닌 유저가 권한을 삭제하는 경우 예외가 발생한다.") + @DisplayName("creator 가 아닌 회원이 권한을 삭제하는 경우 예외가 발생한다.") void deleteMemberTopicPermissionByUser() { // given Member creator = memberRepository.save( @@ -289,7 +289,7 @@ void deleteMemberTopicPermissionByUser() { } @Test - @DisplayName("존재하지 않는 유저의 권한을 삭제하려 하려는 경우 예외가 발생한다.") + @DisplayName("존재하지 않는 회원의 권한을 삭제하려 하려는 경우 예외가 발생한다.") void deleteMemberTopicPermissionByCreator_whenNoneExistsPermission_thenFail() { // given Member creator = memberRepository.save( diff --git a/backend/src/test/java/com/mapbefine/mapbefine/pin/PinIntegrationTest.java b/backend/src/test/java/com/mapbefine/mapbefine/pin/PinIntegrationTest.java index f69a1e2d..c154156b 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/pin/PinIntegrationTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/pin/PinIntegrationTest.java @@ -242,7 +242,7 @@ void removeImage_Success() { @Test - @DisplayName("멤버별 Pin 목록을 조회하면 200을 반환한다") + @DisplayName("회원별 Pin 목록을 조회하면 200을 반환한다") void findAllPinsByMemberId_Success() { //given createPinAndGetId(createRequestDuplicateLocation); diff --git a/backend/src/test/java/com/mapbefine/mapbefine/pin/application/PinQueryServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/pin/application/PinQueryServiceTest.java index d1b3537f..25d0f59e 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/pin/application/PinQueryServiceTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/pin/application/PinQueryServiceTest.java @@ -131,7 +131,7 @@ void findDetailById_FailByForbidden() { } @Test - @DisplayName("멤버 Id를 이용하여 그 멤버가 만든 모든 Pin을 확인할 수 있다.") + @DisplayName("회원 Id를 이용하여 그 회원이 만든 모든 Pin을 확인할 수 있다.") void findAllPinsByMemberId_success() { // given List expected = pinRepository.saveAll(List.of( diff --git a/backend/src/test/java/com/mapbefine/mapbefine/pin/presentation/PinControllerTest.java b/backend/src/test/java/com/mapbefine/mapbefine/pin/presentation/PinControllerTest.java index 88876fbd..b5c9eafc 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/pin/presentation/PinControllerTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/pin/presentation/PinControllerTest.java @@ -188,7 +188,7 @@ void removeImage() throws Exception { @Test - @DisplayName("멤버 Id를 입력하면 해당 멤버가 만든 핀 목록을 조회할 수 있다.") + @DisplayName("회원 Id를 입력하면 해당 회원이 만든 핀 목록을 조회할 수 있다.") void findAllPinsByMemberId() throws Exception { List pinResponses = List.of( new PinResponse( diff --git a/backend/src/test/java/com/mapbefine/mapbefine/topic/TopicFixture.java b/backend/src/test/java/com/mapbefine/mapbefine/topic/TopicFixture.java index ed8a196a..a8c4d93c 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/topic/TopicFixture.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/topic/TopicFixture.java @@ -16,8 +16,8 @@ public class TopicFixture { public static Topic createPrivateAndGroupOnlyTopic(Member member) { return Topic.createTopicAssociatedWithCreator( - "토픽 멤버만 읽을 수 있는 토픽", - "토픽 멤버만 읽을 수 있습니다.", + "토픽 회원만 읽을 수 있는 토픽", + "토픽 회원만 읽을 수 있습니다.", IMAGE_URL, Publicity.PRIVATE, PermissionType.GROUP_ONLY, diff --git a/backend/src/test/java/com/mapbefine/mapbefine/topic/TopicIntegrationTest.java b/backend/src/test/java/com/mapbefine/mapbefine/topic/TopicIntegrationTest.java index f344ce9a..80c13a9f 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/topic/TopicIntegrationTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/topic/TopicIntegrationTest.java @@ -404,7 +404,7 @@ void findAllByOrderByUpdatedAtDesc_Success() { } @Test - @DisplayName("멤버별 Topic 목록을 조회하면 200을 반환한다") + @DisplayName("회원별 Topic 목록을 조회하면 200을 반환한다") void findAllTopicsByMemberId_Success() { // when ExtractableResponse response = RestAssured diff --git a/backend/src/test/java/com/mapbefine/mapbefine/topic/application/TopicCommandServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/topic/application/TopicCommandServiceTest.java index be526608..813ba6e7 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/topic/application/TopicCommandServiceTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/topic/application/TopicCommandServiceTest.java @@ -303,8 +303,8 @@ public void updateTopicInfo_Success() { topicRepository.save(topic); //when - assertThat(topic.getTopicInfo().getName()).isEqualTo("토픽 멤버만 읽을 수 있는 토픽"); - assertThat(topic.getTopicInfo().getDescription()).isEqualTo("토픽 멤버만 읽을 수 있습니다."); + assertThat(topic.getTopicInfo().getName()).isEqualTo("토픽 회원만 읽을 수 있는 토픽"); + assertThat(topic.getTopicInfo().getDescription()).isEqualTo("토픽 회원만 읽을 수 있습니다."); AuthMember user = MemberFixture.createUser(member); TopicUpdateRequest request = new TopicUpdateRequest( diff --git a/backend/src/test/java/com/mapbefine/mapbefine/topic/application/TopicQueryServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/topic/application/TopicQueryServiceTest.java index 302d7aac..5196a7f6 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/topic/application/TopicQueryServiceTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/topic/application/TopicQueryServiceTest.java @@ -80,8 +80,8 @@ void findAllReadable_Success1() { assertThat(topics).extractingResultOf("name") .containsExactlyInAnyOrder( "아무나 읽을 수 있는 토픽", - "토픽 멤버만 읽을 수 있는 토픽", - "토픽 멤버만 읽을 수 있는 토픽" + "토픽 회원만 읽을 수 있는 토픽", + "토픽 회원만 읽을 수 있는 토픽" ); } @@ -194,7 +194,7 @@ void findDetailByIds_Success2() { //then assertThat(details).hasSize(2); assertThat(details).extractingResultOf("name") - .containsExactlyInAnyOrder("아무나 읽을 수 있는 토픽", "토픽 멤버만 읽을 수 있는 토픽"); + .containsExactlyInAnyOrder("아무나 읽을 수 있는 토픽", "토픽 회원만 읽을 수 있는 토픽"); } @Test @@ -257,7 +257,7 @@ void findAllReadableWithBookmark_Success() { } @Test - @DisplayName("모든 토픽을 조회할 때, 로그인 유저가 아니면 즐겨찾기 여부가 항상 False다") + @DisplayName("모든 토픽을 조회할 때, 로그인 회원이 아니면 즐겨찾기 여부가 항상 False다") void findAllReadableWithoutBookmark_Success() { //given Topic topic1 = TopicFixture.createPublicAndAllMembersTopic(member); @@ -300,7 +300,7 @@ void findWithBookmarkStatus_Success() { } @Test - @DisplayName("토픽 상세조회시, 로그인 유저가 아니라면 즐겨찾기 여부, 모아보기 여부, 수정 권한 여부가 항상 False다.") + @DisplayName("토픽 상세조회시, 로그인 회원이 아니라면 즐겨찾기 여부, 모아보기 여부, 수정 권한 여부가 항상 False다.") void findWithoutBookmarkStatus_Success() { //given Topic topic = TopicFixture.createPublicAndAllMembersTopic(member); @@ -344,7 +344,7 @@ void findDetailsWithBookmarkStatus_Success() { } @Test - @DisplayName("여러 토픽 조회시, 로그인 유저가 아니라면 즐겨 찾기 여부가 항상 False다.") + @DisplayName("여러 토픽 조회시, 로그인 회원이 아니라면 즐겨 찾기 여부가 항상 False다.") void findDetailsWithoutBookmarkStatus_Success() { //given Topic topic1 = TopicFixture.createPublicAndAllMembersTopic(member); @@ -368,7 +368,7 @@ void findDetailsWithoutBookmarkStatus_Success() { } @Test - @DisplayName("멤버 Id를 이용하여 그 멤버가 만든 모든 Topic을 확인할 수 있다.") + @DisplayName("회원 Id를 이용하여 그 회원이 만든 모든 Topic을 확인할 수 있다.") void findAllTopicsByMemberId_Success() { //given AuthMember authMember = new Admin(member.getId()); diff --git a/backend/src/test/java/com/mapbefine/mapbefine/topic/domain/TopicStatusTest.java b/backend/src/test/java/com/mapbefine/mapbefine/topic/domain/TopicStatusTest.java index 74667963..bde0c806 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/topic/domain/TopicStatusTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/topic/domain/TopicStatusTest.java @@ -104,7 +104,7 @@ public void whenPermissionIsNull_Fail() { class Update { @Test - @DisplayName("권한 범위가 모든 멤버이면, 공개 범위를 혼자 볼 지도로 설정할 때 예외가 발생한다") + @DisplayName("권한 범위가 모든 회원이면, 공개 범위를 혼자 볼 지도로 설정할 때 예외가 발생한다") void whenAllMembersAndPrivate_thenFail() { //given TopicStatus topicStatus = TopicStatus.of(Publicity.PUBLIC, PermissionType.ALL_MEMBERS); diff --git a/backend/src/test/java/com/mapbefine/mapbefine/topic/presentation/TopicControllerTest.java b/backend/src/test/java/com/mapbefine/mapbefine/topic/presentation/TopicControllerTest.java index ba19dc2b..da72796f 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/topic/presentation/TopicControllerTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/topic/presentation/TopicControllerTest.java @@ -219,7 +219,7 @@ void findAllByOrderByUpdatedAtDesc() throws Exception { } @Test - @DisplayName("멤버 Id를 입력하면 해당 멤버가 만든 지도 목록을 조회할 수 있다.") + @DisplayName("회원 Id를 입력하면 해당 회원이 만든 지도 목록을 조회할 수 있다.") void findAllTopicsByMemberId() throws Exception { given(topicQueryService.findAllTopicsByMemberId(any(), any())).willReturn(RESPONSES); From 05c73efb96163194aaacd8bc6125de63fb9c8912 Mon Sep 17 00:00:00 2001 From: Doy Date: Fri, 15 Sep 2023 16:11:34 +0900 Subject: [PATCH 18/34] =?UTF-8?q?[BE]=20Refactor/#406=20=ED=86=A0=ED=94=BD?= =?UTF-8?q?=20=EA=B6=8C=ED=95=9C=EC=9D=84=20=EA=B0=80=EC=A7=84=20=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20=EC=8B=9C?= =?UTF-8?q?=20=EA=B3=B5=EA=B0=9C=20=EC=97=AC=EB=B6=80=EB=A5=BC=20=ED=95=A8?= =?UTF-8?q?=EA=BB=98=20=EB=B0=98=ED=99=98=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=20(#412)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 사용하지 않는 MemberRepository 메서드 삭제 * refactor: 회원 업데이트 부분 변경으로 시그니처 변경 - 현재 회원 update에서 변경되는 부분만 인자로 남겨둠 - update 시, Member 에서 MemberInfo.getXX을 하는 대신 MemberInfo에서 부분 변경된 객체를 새로 반환하도록 수정 * feat: 회원 정보 수정 API 구현 및 테스트 작성 * test: JwtTokenProviderTest 작성 - 로컬에서 Postman 테스트 시 이 테스트를 사용하면 쉽게 토큰 발급 후 활용 가능 * chore: 로컬 환경용 더미데이터 sql 작성 * chore: 로컬 환경 data.sql을 위한 서브모듈 변경 * docs: 기능 명세서 및 테스트 코드 용어 정리 (유저, 멤버 -> 회원) * chore: 로그 환경설정 파일 디렉터리 분리 * feat: 닉네임 중복 검증 구현 * refactor: 회원의 이메일 Unique 제약조건 삭제 - 닉네임, OauthId로 회원을 식별할 수 있다. - 같은 이메일로 네이버, 카카오에 가입한 사람이 소셜 로그인으로 두 계정을 만들 경우, 동일한 이메일이 저장될 수도 있다. * refactor: 모호한 메서드명 수정 * refactor: Email이 Unique하지 않음에 따라 테스트에서 사용하는 조회 쿼리 변경 - findByEmail 대신 findById - 기본키가 아닌 유일키로 조회하는 건 테이블 구조 변경 여지가 있으므로 findById 사용 * refactor: 내 정보 수정 API URI 변경 * refactor: 토픽 권한 회원 목록 조회 API를 접근 정보(권한 회원 목록 및 공개 여부) 조회로 명세 변경 - 관련 검토가 필요한 API 설계 및 구현 내용에 대한 TODO 주석 작성 * fix: 디렉터리 분리에 따른 로그 설정 파일 appender 경로 수정 * fix: 디렉터리 분리에 따른 로그 설정 파일 appender 경로 수정 * refactor: 실수로 바꾼 기존 메서드명 원복 * refactor: 불필요한 import문 제거 * refactor: 불필요한 접근제어자, 중복 코드 제거 * docs: Restdocs API 네이밍 반영 * fix: 내 정보 수정 RestDocs 스니펫 누락 추가 --- backend/src/docs/asciidoc/member.adoc | 4 ++ backend/src/docs/asciidoc/permission.adoc | 4 +- .../application/PermissionQueryService.java | 36 +++++++++++---- .../permission/domain/Permission.java | 1 + ...va => PermissionMemberDetailResponse.java} | 6 +-- ...e.java => PermissionedMemberResponse.java} | 6 +-- .../response/TopicAccessDetailResponse.java | 10 ++++ .../presentation/PermissionController.java | 17 +++---- .../mapbefine/topic/domain/Topic.java | 3 ++ .../mapbefine/topic/domain/TopicStatus.java | 2 +- .../topic/exception/TopicException.java | 4 ++ .../mapbefine/atlas/AtlasIntegrationTest.java | 7 ++- .../bookmark/BookmarkIntegrationTest.java | 7 ++- .../mapbefine/common/IntegrationTest.java | 2 +- .../permission/PermissionIntegrationTest.java | 39 ++++++++-------- .../PermissionQueryServiceTest.java | 39 +++++++++------- .../PermissionControllerTest.java | 46 ++++++------------- .../mapbefine/pin/PinIntegrationTest.java | 5 +- .../pin/presentation/PinControllerTest.java | 3 -- .../mapbefine/topic/TopicIntegrationTest.java | 4 -- .../presentation/TopicControllerTest.java | 10 ---- 21 files changed, 132 insertions(+), 123 deletions(-) rename backend/src/main/java/com/mapbefine/mapbefine/permission/dto/response/{PermissionDetailResponse.java => PermissionMemberDetailResponse.java} (73%) rename backend/src/main/java/com/mapbefine/mapbefine/permission/dto/response/{PermissionResponse.java => PermissionedMemberResponse.java} (68%) create mode 100644 backend/src/main/java/com/mapbefine/mapbefine/permission/dto/response/TopicAccessDetailResponse.java diff --git a/backend/src/docs/asciidoc/member.adoc b/backend/src/docs/asciidoc/member.adoc index 114112e4..520b3943 100644 --- a/backend/src/docs/asciidoc/member.adoc +++ b/backend/src/docs/asciidoc/member.adoc @@ -23,3 +23,7 @@ operation::member-controller-test/find-all-topics-in-atlas[snippets='http-reques === 회원의 즐겨찾기 조회 operation::member-controller-test/find-all-topics-in-bookmark[snippets='http-request,http-response'] + +=== 회원의 내 정보 수정 + +operation::member-controller-test/update-my-info[snippets='http-request,http-response'] diff --git a/backend/src/docs/asciidoc/permission.adoc b/backend/src/docs/asciidoc/permission.adoc index 4b232e68..bfb10520 100644 --- a/backend/src/docs/asciidoc/permission.adoc +++ b/backend/src/docs/asciidoc/permission.adoc @@ -8,9 +8,9 @@ operation::permission-controller-test/add-permission[snippets='http-request,http operation::permission-controller-test/delete-permission[snippets='http-request,http-response'] -=== 토픽에 권한을 가진 회원 목록 조회 +=== 토픽 접근 정보(권한 회원 목록 및 공개 여부) 조회 -operation::permission-controller-test/find-all-topic-permissions[snippets='http-request,http-response'] +operation::permission-controller-test/find-topic-access-detail-by-topic-id[snippets='http-request,http-response'] === 토픽에 권한을 가진 회원 단일 조회 diff --git a/backend/src/main/java/com/mapbefine/mapbefine/permission/application/PermissionQueryService.java b/backend/src/main/java/com/mapbefine/mapbefine/permission/application/PermissionQueryService.java index 7765d151..5b4bb4ab 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/permission/application/PermissionQueryService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/permission/application/PermissionQueryService.java @@ -4,9 +4,16 @@ import com.mapbefine.mapbefine.permission.domain.Permission; import com.mapbefine.mapbefine.permission.domain.PermissionRepository; -import com.mapbefine.mapbefine.permission.dto.response.PermissionDetailResponse; -import com.mapbefine.mapbefine.permission.dto.response.PermissionResponse; +import com.mapbefine.mapbefine.permission.dto.response.PermissionMemberDetailResponse; +import com.mapbefine.mapbefine.permission.dto.response.PermissionedMemberResponse; +import com.mapbefine.mapbefine.permission.dto.response.TopicAccessDetailResponse; import com.mapbefine.mapbefine.permission.exception.PermissionException.PermissionNotFoundException; +import com.mapbefine.mapbefine.topic.domain.Publicity; +import com.mapbefine.mapbefine.topic.domain.Topic; +import com.mapbefine.mapbefine.topic.domain.TopicRepository; +import com.mapbefine.mapbefine.topic.domain.TopicStatus; +import com.mapbefine.mapbefine.topic.exception.TopicErrorCode; +import com.mapbefine.mapbefine.topic.exception.TopicException.TopicNotFoundException; import java.util.List; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -16,22 +23,35 @@ public class PermissionQueryService { private final PermissionRepository permissionRepository; + private final TopicRepository topicRepository; - public PermissionQueryService(PermissionRepository permissionRepository) { + public PermissionQueryService(PermissionRepository permissionRepository, TopicRepository topicRepository) { this.permissionRepository = permissionRepository; + this.topicRepository = topicRepository; } - public List findAllTopicPermissions(Long topicId) { - return permissionRepository.findAllByTopicId(topicId) + public TopicAccessDetailResponse findTopicAccessDetailById(Long topicId) { + Publicity publicity = findTopicPublicityById(topicId); + /// TODO: 2023/09/15 이럴거면 topic.getPermissions 로 하는 게 나을 수도 있나? TopicController 에서 하는 게 더 자연스러운 것 같기도.. + List permissionedMembers = permissionRepository.findAllByTopicId(topicId) .stream() - .map(PermissionResponse::from) + .map(PermissionedMemberResponse::from) .toList(); + + return new TopicAccessDetailResponse(publicity, permissionedMembers); + } + + private Publicity findTopicPublicityById(Long topicId) { + return topicRepository.findById(topicId) + .map(Topic::getTopicStatus) + .map(TopicStatus::getPublicity) + .orElseThrow(() -> new TopicNotFoundException(TopicErrorCode.TOPIC_NOT_FOUND, topicId)); } - public PermissionDetailResponse findPermissionById(Long permissionId) { + public PermissionMemberDetailResponse findPermissionById(Long permissionId) { Permission permission = permissionRepository.findById(permissionId) .orElseThrow(() -> new PermissionNotFoundException(PERMISSION_NOT_FOUND, permissionId)); - return PermissionDetailResponse.from(permission); + return PermissionMemberDetailResponse.from(permission); } } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/permission/domain/Permission.java b/backend/src/main/java/com/mapbefine/mapbefine/permission/domain/Permission.java index e58678b1..8f6bf1ca 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/permission/domain/Permission.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/permission/domain/Permission.java @@ -19,6 +19,7 @@ @Getter public class Permission extends BaseTimeEntity { + // TODO 매핑 테이블인데 Id를 가져야 할까? @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; diff --git a/backend/src/main/java/com/mapbefine/mapbefine/permission/dto/response/PermissionDetailResponse.java b/backend/src/main/java/com/mapbefine/mapbefine/permission/dto/response/PermissionMemberDetailResponse.java similarity index 73% rename from backend/src/main/java/com/mapbefine/mapbefine/permission/dto/response/PermissionDetailResponse.java rename to backend/src/main/java/com/mapbefine/mapbefine/permission/dto/response/PermissionMemberDetailResponse.java index 967fa8eb..d42946de 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/permission/dto/response/PermissionDetailResponse.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/permission/dto/response/PermissionMemberDetailResponse.java @@ -4,14 +4,14 @@ import com.mapbefine.mapbefine.permission.domain.Permission; import java.time.LocalDateTime; -public record PermissionDetailResponse( +public record PermissionMemberDetailResponse( Long id, LocalDateTime updatedAt, MemberDetailResponse memberDetailResponse ) { - public static PermissionDetailResponse from(Permission permission) { - return new PermissionDetailResponse( + public static PermissionMemberDetailResponse from(Permission permission) { + return new PermissionMemberDetailResponse( permission.getId(), permission.getUpdatedAt(), MemberDetailResponse.from(permission.getMember()) diff --git a/backend/src/main/java/com/mapbefine/mapbefine/permission/dto/response/PermissionResponse.java b/backend/src/main/java/com/mapbefine/mapbefine/permission/dto/response/PermissionedMemberResponse.java similarity index 68% rename from backend/src/main/java/com/mapbefine/mapbefine/permission/dto/response/PermissionResponse.java rename to backend/src/main/java/com/mapbefine/mapbefine/permission/dto/response/PermissionedMemberResponse.java index 9c4f2368..dda91d86 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/permission/dto/response/PermissionResponse.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/permission/dto/response/PermissionedMemberResponse.java @@ -3,13 +3,13 @@ import com.mapbefine.mapbefine.member.dto.response.MemberResponse; import com.mapbefine.mapbefine.permission.domain.Permission; -public record PermissionResponse( +public record PermissionedMemberResponse( Long id, MemberResponse memberResponse ) { - public static PermissionResponse from(Permission permission) { - return new PermissionResponse( + public static PermissionedMemberResponse from(Permission permission) { + return new PermissionedMemberResponse( permission.getId(), MemberResponse.from(permission.getMember()) ); diff --git a/backend/src/main/java/com/mapbefine/mapbefine/permission/dto/response/TopicAccessDetailResponse.java b/backend/src/main/java/com/mapbefine/mapbefine/permission/dto/response/TopicAccessDetailResponse.java new file mode 100644 index 00000000..7d296566 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/permission/dto/response/TopicAccessDetailResponse.java @@ -0,0 +1,10 @@ +package com.mapbefine.mapbefine.permission.dto.response; + +import com.mapbefine.mapbefine.topic.domain.Publicity; +import java.util.List; + +public record TopicAccessDetailResponse( + Publicity publicity, + List permissionedMembers +) { +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/permission/presentation/PermissionController.java b/backend/src/main/java/com/mapbefine/mapbefine/permission/presentation/PermissionController.java index 750b9bc4..188348ea 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/permission/presentation/PermissionController.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/permission/presentation/PermissionController.java @@ -5,9 +5,8 @@ import com.mapbefine.mapbefine.permission.application.PermissionCommandService; import com.mapbefine.mapbefine.permission.application.PermissionQueryService; import com.mapbefine.mapbefine.permission.dto.request.PermissionRequest; -import com.mapbefine.mapbefine.permission.dto.response.PermissionDetailResponse; -import com.mapbefine.mapbefine.permission.dto.response.PermissionResponse; -import java.util.List; +import com.mapbefine.mapbefine.permission.dto.response.PermissionMemberDetailResponse; +import com.mapbefine.mapbefine.permission.dto.response.TopicAccessDetailResponse; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; @@ -51,16 +50,18 @@ public ResponseEntity deleteMemberTopicPermission(AuthMember authMember, @ @LoginRequired @GetMapping("/topics/{topicId}") - public ResponseEntity> findAllTopicPermissions(@PathVariable Long topicId) { - List responses = permissionQueryService.findAllTopicPermissions(topicId); + public ResponseEntity findTopicAccessDetailByTopicId(@PathVariable Long topicId) { + TopicAccessDetailResponse response = permissionQueryService.findTopicAccessDetailById(topicId); - return ResponseEntity.ok(responses); + return ResponseEntity.ok(response); } + // TODO 이 API를 쓰는 곳이 있나? + 결국 특정 회원을 조회하는 건데 어떤 API인지 알기 어렵다.. + // 회원 정보 조회는 /members 에서 하는 걸로 충분하지 않나? 재사용성이 떨어진다. 테스트의 DisplayName도 매칭이 안된다. @LoginRequired @GetMapping("/{permissionId}") - public ResponseEntity findPermissionById(@PathVariable Long permissionId) { - PermissionDetailResponse response = permissionQueryService.findPermissionById(permissionId); + public ResponseEntity findPermissionById(@PathVariable Long permissionId) { + PermissionMemberDetailResponse response = permissionQueryService.findPermissionById(permissionId); return ResponseEntity.ok(response); } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/Topic.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/Topic.java index d5128022..a407bcdb 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/Topic.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/Topic.java @@ -114,6 +114,9 @@ public int countBookmarks() { return bookmarks.size(); } + public Publicity getPublicity() { + return topicStatus.getPublicity(); + } public void removeImage() { this.topicInfo = topicInfo.removeImage(); } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicStatus.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicStatus.java index 987a19a5..88a79387 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicStatus.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicStatus.java @@ -1,7 +1,7 @@ package com.mapbefine.mapbefine.topic.domain; -import static com.mapbefine.mapbefine.topic.exception.TopicErrorCode.ILLEGAL_PERMISSION_NULL; import static com.mapbefine.mapbefine.topic.exception.TopicErrorCode.ILLEGAL_PERMISSION_FOR_PUBLICITY_PRIVATE; +import static com.mapbefine.mapbefine.topic.exception.TopicErrorCode.ILLEGAL_PERMISSION_NULL; import static com.mapbefine.mapbefine.topic.exception.TopicErrorCode.ILLEGAL_PERMISSION_UPDATE; import static com.mapbefine.mapbefine.topic.exception.TopicErrorCode.ILLEGAL_PUBLICITY_FOR_PERMISSION_ALL_MEMBERS; import static com.mapbefine.mapbefine.topic.exception.TopicErrorCode.ILLEGAL_PUBLICITY_NULL; diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/exception/TopicException.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/exception/TopicException.java index 2c2afb9f..e9729943 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/topic/exception/TopicException.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/exception/TopicException.java @@ -23,6 +23,10 @@ public static class TopicNotFoundException extends NotFoundException { public TopicNotFoundException(TopicErrorCode errorCode, List ids) { super(new ErrorCode<>(errorCode.getCode(), errorCode.getMessage(), ids)); } + + public TopicNotFoundException(TopicErrorCode errorCode, Long id) { + super(new ErrorCode<>(errorCode.getCode(), errorCode.getMessage(), id)); + } } } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/atlas/AtlasIntegrationTest.java b/backend/src/test/java/com/mapbefine/mapbefine/atlas/AtlasIntegrationTest.java index fa681250..57fb46dc 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/atlas/AtlasIntegrationTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/atlas/AtlasIntegrationTest.java @@ -15,16 +15,15 @@ import com.mapbefine.mapbefine.topic.TopicFixture; import com.mapbefine.mapbefine.topic.domain.Topic; import com.mapbefine.mapbefine.topic.domain.TopicRepository; -import io.restassured.RestAssured; -import io.restassured.response.ExtractableResponse; -import io.restassured.response.Response; +import io.restassured.*; +import io.restassured.response.*; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; -public class AtlasIntegrationTest extends IntegrationTest { +class AtlasIntegrationTest extends IntegrationTest { @Autowired TopicRepository topicRepository; diff --git a/backend/src/test/java/com/mapbefine/mapbefine/bookmark/BookmarkIntegrationTest.java b/backend/src/test/java/com/mapbefine/mapbefine/bookmark/BookmarkIntegrationTest.java index 022fefba..aecb86d0 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/bookmark/BookmarkIntegrationTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/bookmark/BookmarkIntegrationTest.java @@ -1,6 +1,6 @@ package com.mapbefine.mapbefine.bookmark; -import static io.restassured.RestAssured.given; +import static io.restassured.RestAssured.*; import static org.apache.http.HttpHeaders.AUTHORIZATION; import static org.assertj.core.api.Assertions.assertThat; @@ -14,14 +14,13 @@ import com.mapbefine.mapbefine.topic.TopicFixture; import com.mapbefine.mapbefine.topic.domain.Topic; import com.mapbefine.mapbefine.topic.domain.TopicRepository; -import io.restassured.response.ExtractableResponse; -import io.restassured.response.Response; +import io.restassured.response.*; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; -public class BookmarkIntegrationTest extends IntegrationTest { +class BookmarkIntegrationTest extends IntegrationTest { @Autowired private MemberRepository memberRepository; diff --git a/backend/src/test/java/com/mapbefine/mapbefine/common/IntegrationTest.java b/backend/src/test/java/com/mapbefine/mapbefine/common/IntegrationTest.java index 9a91e94a..e20e5caa 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/common/IntegrationTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/common/IntegrationTest.java @@ -1,7 +1,7 @@ package com.mapbefine.mapbefine.common; import com.mapbefine.mapbefine.DatabaseCleanup; -import io.restassured.RestAssured; +import io.restassured.*; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.springframework.beans.factory.annotation.Autowired; diff --git a/backend/src/test/java/com/mapbefine/mapbefine/permission/PermissionIntegrationTest.java b/backend/src/test/java/com/mapbefine/mapbefine/permission/PermissionIntegrationTest.java index cfa164f3..2a663ea8 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/permission/PermissionIntegrationTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/permission/PermissionIntegrationTest.java @@ -1,7 +1,7 @@ package com.mapbefine.mapbefine.permission; import static com.mapbefine.mapbefine.oauth.domain.OauthServerType.KAKAO; -import static io.restassured.RestAssured.given; +import static io.restassured.RestAssured.*; import static org.apache.http.HttpHeaders.AUTHORIZATION; import static org.assertj.core.api.Assertions.assertThat; @@ -16,14 +16,14 @@ import com.mapbefine.mapbefine.permission.domain.Permission; import com.mapbefine.mapbefine.permission.domain.PermissionRepository; import com.mapbefine.mapbefine.permission.dto.request.PermissionRequest; -import com.mapbefine.mapbefine.permission.dto.response.PermissionDetailResponse; -import com.mapbefine.mapbefine.permission.dto.response.PermissionResponse; +import com.mapbefine.mapbefine.permission.dto.response.PermissionMemberDetailResponse; +import com.mapbefine.mapbefine.permission.dto.response.PermissionedMemberResponse; +import com.mapbefine.mapbefine.permission.dto.response.TopicAccessDetailResponse; import com.mapbefine.mapbefine.topic.TopicFixture; import com.mapbefine.mapbefine.topic.domain.Topic; import com.mapbefine.mapbefine.topic.domain.TopicRepository; -import io.restassured.common.mapper.TypeRef; -import io.restassured.response.ExtractableResponse; -import io.restassured.response.Response; +import io.restassured.common.mapper.*; +import io.restassured.response.*; import java.time.LocalDateTime; import java.util.List; import org.junit.jupiter.api.BeforeEach; @@ -33,7 +33,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -public class PermissionIntegrationTest extends IntegrationTest { +class PermissionIntegrationTest extends IntegrationTest { @Autowired private MemberRepository memberRepository; @@ -79,7 +79,7 @@ public void setUp() { } @Test - @DisplayName("Topic 을 만든자가 특정 회원에게 권한을 준다.") + @DisplayName("Topic 을 만든 회원이 특정 회원에게 해당 Topic 에 대한 권한을 준다.") void addPermission() { // given Topic topic = topicRepository.save(TopicFixture.createByName("topicName", creator)); @@ -100,7 +100,7 @@ void addPermission() { } @Test - @DisplayName("Topic 을 만든자가 특정 회원에게 권한을 삭제한다.") + @DisplayName("Topic 을 만든 회원이 특정 회원이 가진 해당 Topic 에 대한 권한을 삭제한다.") void deletePermission() { // given Topic topic = topicRepository.save(TopicFixture.createByName("topicName", creator)); @@ -121,8 +121,8 @@ void deletePermission() { } @Test - @DisplayName("Topic 에 권한을 가진 자들을 모두 조회한다.") - void findMemberTopicPermissionAll() { + @DisplayName("Topic 의 접근 정보(권한 회원 목록 및 공개 여부)를 조회한다.") + void findTopicAccessDetailById() { // given Topic topic = topicRepository.save(TopicFixture.createByName("topicName", creator)); Permission permission1 = @@ -138,22 +138,22 @@ void findMemberTopicPermissionAll() { .when().get("/permissions/topics/" + topic.getId()) .then().log().all() .extract(); + TopicAccessDetailResponse actual = response.as(new TypeRef<>() {}); // then - List permissionResponse = response.as(new TypeRef<>() { - }); assertThat(response.statusCode()) .isEqualTo(HttpStatus.OK.value()); - assertThat(permissionResponse) + assertThat(actual.publicity()).isEqualTo(topic.getPublicity()); + assertThat(actual.permissionedMembers()) .hasSize(2) - .extracting(PermissionResponse::memberResponse) + .extracting(PermissionedMemberResponse::memberResponse) .usingRecursiveComparison() .isEqualTo(List.of(MemberResponse.from(user1), MemberResponse.from(user2))); } @Test @DisplayName("Topic 에 권한을 가진 자를 조회한다.") - void findMemberTopicPermissionById() { + void findPermissionById() { // given Topic topic = topicRepository.save(TopicFixture.createByName("topicName", creator)); Permission permission = @@ -166,14 +166,13 @@ void findMemberTopicPermissionById() { .when().get("/permissions/" + permission.getId()) .then().log().all() .extract(); + PermissionMemberDetailResponse actual = response.as(new TypeRef<>() {}); // then - PermissionDetailResponse permissionDetailResponse = response.as( - PermissionDetailResponse.class); assertThat(response.statusCode()) .isEqualTo(HttpStatus.OK.value()); - assertThat(permissionDetailResponse) - .extracting(PermissionDetailResponse::memberDetailResponse) + assertThat(actual) + .extracting(PermissionMemberDetailResponse::memberDetailResponse) .usingRecursiveComparison() .ignoringFieldsOfTypes(LocalDateTime.class) .isEqualTo(MemberDetailResponse.from(user1)); diff --git a/backend/src/test/java/com/mapbefine/mapbefine/permission/application/PermissionQueryServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/permission/application/PermissionQueryServiceTest.java index def49d5f..43afe105 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/permission/application/PermissionQueryServiceTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/permission/application/PermissionQueryServiceTest.java @@ -12,8 +12,9 @@ import com.mapbefine.mapbefine.member.dto.response.MemberResponse; import com.mapbefine.mapbefine.permission.domain.Permission; import com.mapbefine.mapbefine.permission.domain.PermissionRepository; -import com.mapbefine.mapbefine.permission.dto.response.PermissionDetailResponse; -import com.mapbefine.mapbefine.permission.dto.response.PermissionResponse; +import com.mapbefine.mapbefine.permission.dto.response.PermissionMemberDetailResponse; +import com.mapbefine.mapbefine.permission.dto.response.PermissionedMemberResponse; +import com.mapbefine.mapbefine.permission.dto.response.TopicAccessDetailResponse; import com.mapbefine.mapbefine.permission.exception.PermissionException.PermissionNotFoundException; import com.mapbefine.mapbefine.topic.TopicFixture; import com.mapbefine.mapbefine.topic.domain.Topic; @@ -36,9 +37,9 @@ class PermissionQueryServiceTest { private PermissionQueryService permissionQueryService; @Test - @DisplayName("Topic 에 권한이 있는자들을 모두 조회한다.") - // creator 는 권한이 있는자들을 조회할 때 조회되어야 할 것인가?? - void findAllWithPermission() { + @DisplayName("특정 지도에 대한 접근 정보(권한 회원 목록 및 공개 여부)를 모두 조회한다. 권한 회원 중 생성자는 제외한다.") + void findTopicAccessDetailById() { + /// TODO: 2023/09/15 리팩터링 // given Member member1InTopic1 = memberRepository.save( MemberFixture.create("member", "member@naver.com", Role.USER) @@ -62,20 +63,26 @@ void findAllWithPermission() { ); // when - List permissionRespons = permissionQueryService.findAllTopicPermissions(topic1.getId()); - MemberResponse memberResponse1 = MemberResponse.from(member1InTopic1); - MemberResponse memberResponse2 = MemberResponse.from(member2InTopic1); + TopicAccessDetailResponse accessDetailResponse = permissionQueryService.findTopicAccessDetailById(topic1.getId()); + MemberResponse member1Response = MemberResponse.from(member1InTopic1); + MemberResponse member2Response = MemberResponse.from(member2InTopic1); // then - assertThat(permissionRespons).hasSize(2) - .extracting(PermissionResponse::memberResponse) + assertThat(accessDetailResponse.publicity()).isEqualTo(topic1.getPublicity()); + List permissionedMembers = accessDetailResponse.permissionedMembers(); + assertThat(permissionedMembers).hasSize(2) + .extracting(PermissionedMemberResponse::memberResponse) .usingRecursiveComparison() - .isEqualTo(List.of(memberResponse1, memberResponse2)); + .isEqualTo(List.of(member1Response, member2Response)); + assertThat(permissionedMembers) + .extracting(PermissionedMemberResponse::memberResponse) + .map(MemberResponse::id) + .doesNotContain(topic1.getCreator().getId()); } @Test @DisplayName("ID 를 통해서 토픽에 권한이 있는자를 조회한다.") - void findMemberTopicPermissionById() { + void findPermissionById() { // given Member creator = memberRepository.save( MemberFixture.create("member", "member@naver.com", Role.USER) @@ -89,20 +96,20 @@ void findMemberTopicPermissionById() { ).getId(); // when - PermissionDetailResponse permissionDetailResponse = + PermissionMemberDetailResponse permissionMemberDetailResponse = permissionQueryService.findPermissionById(savedId); MemberDetailResponse permissionUserResponse = MemberDetailResponse.from(permissionUser); // then - assertThat(permissionDetailResponse) - .extracting(PermissionDetailResponse::memberDetailResponse) + assertThat(permissionMemberDetailResponse) + .extracting(PermissionMemberDetailResponse::memberDetailResponse) .usingRecursiveComparison() .isEqualTo(permissionUserResponse); } @Test @DisplayName("ID 를 통해서 토픽에 권한이 있는자를 조회하려 할 때, 결과가 존재하지 않을 때 예외가 발생한다.") - void findMemberTopicPermissionById_whenNoneExistsPermission_thenFail() { + void findPermissionById_whenNoneExistsPermission_thenFail() { // given when then assertThatThrownBy(() -> permissionQueryService.findPermissionById(Long.MAX_VALUE)) .isInstanceOf(PermissionNotFoundException.class); diff --git a/backend/src/test/java/com/mapbefine/mapbefine/permission/presentation/PermissionControllerTest.java b/backend/src/test/java/com/mapbefine/mapbefine/permission/presentation/PermissionControllerTest.java index e6885267..e2311f47 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/permission/presentation/PermissionControllerTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/permission/presentation/PermissionControllerTest.java @@ -5,16 +5,14 @@ import static org.mockito.BDDMockito.given; import com.mapbefine.mapbefine.common.RestDocsIntegration; -import com.mapbefine.mapbefine.member.MemberFixture; -import com.mapbefine.mapbefine.member.domain.Member; -import com.mapbefine.mapbefine.member.domain.Role; import com.mapbefine.mapbefine.member.dto.response.MemberDetailResponse; import com.mapbefine.mapbefine.member.dto.response.MemberResponse; -import com.mapbefine.mapbefine.permission.application.PermissionCommandService; import com.mapbefine.mapbefine.permission.application.PermissionQueryService; import com.mapbefine.mapbefine.permission.dto.request.PermissionRequest; -import com.mapbefine.mapbefine.permission.dto.response.PermissionDetailResponse; -import com.mapbefine.mapbefine.permission.dto.response.PermissionResponse; +import com.mapbefine.mapbefine.permission.dto.response.PermissionMemberDetailResponse; +import com.mapbefine.mapbefine.permission.dto.response.PermissionedMemberResponse; +import com.mapbefine.mapbefine.permission.dto.response.TopicAccessDetailResponse; +import com.mapbefine.mapbefine.topic.domain.Publicity; import java.time.LocalDateTime; import java.util.List; import org.junit.jupiter.api.DisplayName; @@ -27,13 +25,10 @@ class PermissionControllerTest extends RestDocsIntegration { @MockBean private PermissionQueryService permissionQueryService; - @MockBean - private PermissionCommandService permissionCommandService; @Test @DisplayName("권한 추가") void addPermission() throws Exception { - Member member = MemberFixture.create("member", "member@naver.com", Role.ADMIN); PermissionRequest request = new PermissionRequest(1L, List.of(1L, 2L, 3L)); mockMvc.perform( @@ -47,8 +42,6 @@ void addPermission() throws Exception { @Test @DisplayName("권한 삭제") void deletePermission() throws Exception { - Member member = MemberFixture.create("member", "member@naver.com", Role.ADMIN); - mockMvc.perform( MockMvcRequestBuilders.delete("/permissions/1") .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) @@ -56,28 +49,15 @@ void deletePermission() throws Exception { } @Test - @DisplayName("권한이 있는 자들 모두 조회") - void findAllTopicPermissions() throws Exception { - List permissionResponses = List.of( - new PermissionResponse( - 1L, - new MemberResponse( - 1L, - "member", - "member@naver.com" - ) - ), - new PermissionResponse( - 1L, - new MemberResponse( - 2L, - "memberr", - "memberr@naver.com" - ) - ) + @DisplayName("특정 토픽 접근 정보 조회(권한 회원 목록, 공개 여부)") + void findTopicAccessDetailByTopicId() throws Exception { + List permissionedMembers = List.of( + new PermissionedMemberResponse(1L, new MemberResponse(1L, "member", "member@naver.com")), + new PermissionedMemberResponse(1L, new MemberResponse(2L, "memberr", "memberr@naver.com")) ); + TopicAccessDetailResponse response = new TopicAccessDetailResponse(Publicity.PUBLIC, permissionedMembers); - given(permissionQueryService.findAllTopicPermissions(any())).willReturn(permissionResponses); + given(permissionQueryService.findTopicAccessDetailById(any())).willReturn(response); mockMvc.perform( MockMvcRequestBuilders.get("/permissions/topics/1") @@ -88,7 +68,7 @@ void findAllTopicPermissions() throws Exception { @Test @DisplayName("권한이 있는 자들 모두 조회") void findPermissionById() throws Exception { - PermissionDetailResponse permissionDetailResponse = new PermissionDetailResponse( + PermissionMemberDetailResponse permissionMemberDetailResponse = new PermissionMemberDetailResponse( 1L, LocalDateTime.now(), new MemberDetailResponse( @@ -100,7 +80,7 @@ void findPermissionById() throws Exception { ) ); - given(permissionQueryService.findPermissionById(any())).willReturn(permissionDetailResponse); + given(permissionQueryService.findPermissionById(any())).willReturn(permissionMemberDetailResponse); mockMvc.perform( MockMvcRequestBuilders.get("/permissions/1") diff --git a/backend/src/test/java/com/mapbefine/mapbefine/pin/PinIntegrationTest.java b/backend/src/test/java/com/mapbefine/mapbefine/pin/PinIntegrationTest.java index c154156b..eed8c54f 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/pin/PinIntegrationTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/pin/PinIntegrationTest.java @@ -18,9 +18,8 @@ import com.mapbefine.mapbefine.topic.TopicFixture; import com.mapbefine.mapbefine.topic.domain.Topic; import com.mapbefine.mapbefine.topic.domain.TopicRepository; -import io.restassured.RestAssured; -import io.restassured.response.ExtractableResponse; -import io.restassured.response.Response; +import io.restassured.*; +import io.restassured.response.*; import java.io.File; import java.util.List; import org.junit.jupiter.api.BeforeEach; diff --git a/backend/src/test/java/com/mapbefine/mapbefine/pin/presentation/PinControllerTest.java b/backend/src/test/java/com/mapbefine/mapbefine/pin/presentation/PinControllerTest.java index b5c9eafc..17dc5a8b 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/pin/presentation/PinControllerTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/pin/presentation/PinControllerTest.java @@ -3,16 +3,13 @@ import static org.apache.http.HttpHeaders.AUTHORIZATION; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; -import com.mapbefine.mapbefine.FileFixture; import com.mapbefine.mapbefine.common.RestDocsIntegration; import com.mapbefine.mapbefine.pin.application.PinCommandService; import com.mapbefine.mapbefine.pin.application.PinQueryService; import com.mapbefine.mapbefine.pin.dto.request.PinCreateRequest; -import com.mapbefine.mapbefine.pin.dto.request.PinImageCreateRequest; import com.mapbefine.mapbefine.pin.dto.request.PinUpdateRequest; import com.mapbefine.mapbefine.pin.dto.response.PinDetailResponse; import com.mapbefine.mapbefine.pin.dto.response.PinImageResponse; diff --git a/backend/src/test/java/com/mapbefine/mapbefine/topic/TopicIntegrationTest.java b/backend/src/test/java/com/mapbefine/mapbefine/topic/TopicIntegrationTest.java index 80c13a9f..6d648853 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/topic/TopicIntegrationTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/topic/TopicIntegrationTest.java @@ -3,7 +3,6 @@ import static org.apache.http.HttpHeaders.AUTHORIZATION; import static org.assertj.core.api.Assertions.assertThat; -import com.mapbefine.mapbefine.FileFixture; import com.mapbefine.mapbefine.bookmark.domain.Bookmark; import com.mapbefine.mapbefine.bookmark.domain.BookmarkRepository; import com.mapbefine.mapbefine.common.IntegrationTest; @@ -21,7 +20,6 @@ import com.mapbefine.mapbefine.topic.domain.Publicity; import com.mapbefine.mapbefine.topic.domain.Topic; import com.mapbefine.mapbefine.topic.domain.TopicRepository; -import com.mapbefine.mapbefine.topic.dto.request.TopicCreateRequest; import com.mapbefine.mapbefine.topic.dto.request.TopicCreateRequestWithOutImage; import com.mapbefine.mapbefine.topic.dto.request.TopicMergeRequest; import com.mapbefine.mapbefine.topic.dto.request.TopicUpdateRequest; @@ -38,7 +36,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.mock.web.MockMultipartFile; class TopicIntegrationTest extends IntegrationTest { @@ -329,7 +326,6 @@ void findTopicDetailsByIds_Success() { // then assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); assertThat(responses).hasSize(2); - assertThat(responses).hasSize(2); } @Test diff --git a/backend/src/test/java/com/mapbefine/mapbefine/topic/presentation/TopicControllerTest.java b/backend/src/test/java/com/mapbefine/mapbefine/topic/presentation/TopicControllerTest.java index da72796f..23dc0945 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/topic/presentation/TopicControllerTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/topic/presentation/TopicControllerTest.java @@ -3,16 +3,13 @@ import static org.apache.http.HttpHeaders.AUTHORIZATION; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; -import com.mapbefine.mapbefine.FileFixture; import com.mapbefine.mapbefine.common.RestDocsIntegration; import com.mapbefine.mapbefine.pin.dto.response.PinResponse; import com.mapbefine.mapbefine.topic.application.TopicCommandService; import com.mapbefine.mapbefine.topic.application.TopicQueryService; import com.mapbefine.mapbefine.topic.domain.PermissionType; import com.mapbefine.mapbefine.topic.domain.Publicity; -import com.mapbefine.mapbefine.topic.dto.request.TopicCreateRequest; import com.mapbefine.mapbefine.topic.dto.request.TopicCreateRequestWithOutImage; import com.mapbefine.mapbefine.topic.dto.request.TopicMergeRequest; import com.mapbefine.mapbefine.topic.dto.request.TopicUpdateRequest; @@ -20,18 +17,11 @@ import com.mapbefine.mapbefine.topic.dto.response.TopicResponse; import java.io.File; import java.time.LocalDateTime; -import java.util.HashMap; import java.util.List; -import java.util.Map; -import java.util.Map.Entry; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; -import org.springframework.mock.web.MockMultipartFile; -import org.springframework.test.web.servlet.request.MockMultipartHttpServletRequestBuilder; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; From f3478094f04415f60225f626ce9803e6a691aee7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A4=80=ED=8C=8D=28junpak=29?= <112045553+junpakPark@users.noreply.github.com> Date: Sun, 17 Sep 2023 12:43:22 +0900 Subject: [PATCH 19/34] =?UTF-8?q?[BE]=20Feature/#388=20refresh=20token=20?= =?UTF-8?q?=EB=B0=8F=20=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84=20(#411)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: redis 의존성 추가 * refactor: OauthService 필드에 final 추가 * feat: refreshToken 엔티티 및 레포지토리 구현 * feat: JwtTokenProvider RefreshToken 발급 구현 * feat: 로그인 시 RefreshToken 발급 기능 구현 * feat: Auth 패키지 커스텀 예외 추가 * refactor: validate 메서드 리팩터링 * chore: refreshToken 만료 시간 추가 * test: Test를 위한 설정 변경 * feat: 액세스 토큰 재발급 및 로그아웃 기능 구현 * chore: Redis 의존성 제거 * test: TestTokenProvider 객체 구현 * refactor: /logout HttpMethod 변경, cookie 관련 cors설정 및 maxAge 설정, * test: DisplayName 추가 * feat: RTR 적용 및 OauthConntroller 제거, OauthService 및 TokenService 역할과 책임 재분배 * refactor : 피드백 반영 * refactor : 매직넘버 상수화 * refactor : 네이밍 수정 * feat: 쿠키 설정 추가 --- backend/src/docs/asciidoc/auth.adoc | 13 ++ backend/src/docs/asciidoc/index.adoc | 2 +- backend/src/docs/asciidoc/oauth.adoc | 9 -- .../auth/application/AuthService.java | 10 +- .../auth/application/TokenService.java | 62 ++++++++ .../auth/domain/token/RefreshToken.java | 27 ++++ .../domain/token/RefreshTokenRepository.java | 12 ++ .../mapbefine/auth/dto/AccessToken.java | 6 + .../mapbefine/auth/dto/LoginTokens.java | 11 ++ .../auth/dto/response/LoginInfoResponse.java | 9 ++ .../auth/exception/AuthErrorCode.java | 9 +- .../auth/infrastructure/JwtTokenProvider.java | 72 +++++++-- .../auth/infrastructure/TokenProvider.java | 16 ++ .../auth/presentation/LoginController.java | 96 ++++++++++++ .../mapbefine/common/config/WebConfig.java | 7 +- .../interceptor/AdminAuthInterceptor.java | 25 ++- .../common/interceptor/AuthInterceptor.java | 38 ++--- .../oauth/application/OauthService.java | 19 +-- .../oauth/dto/LoginInfoResponse.java | 15 -- .../oauth/presentation/OauthController.java | 46 ------ backend/src/main/resources/config | 2 +- .../application/AtlasCommandServiceTest.java | 4 +- .../auth/application/TestTokenProvider.java | 103 ++++++++++++ .../auth/application/TokenServiceTest.java | 146 ++++++++++++++++++ .../infrastructure/JwtTokenProviderTest.java | 6 +- .../presentation/LoginControllerTest.java | 110 +++++++++++++ .../BookmarkCommandServiceTest.java | 12 +- .../presentation/BookmarkControllerTest.java | 4 +- .../common/TestAuthHeaderProvider.java | 14 +- .../common/annotation/ServiceTest.java | 22 +-- .../oauth/application/OauthServiceTest.java | 29 ++-- .../presentation/OauthControllerTest.java | 63 -------- .../application/TopicCommandServiceTest.java | 24 +-- .../topic/domain/TopicStatusTest.java | 4 +- backend/src/test/resources/application.yml | 3 +- 35 files changed, 789 insertions(+), 261 deletions(-) create mode 100644 backend/src/docs/asciidoc/auth.adoc delete mode 100644 backend/src/docs/asciidoc/oauth.adoc create mode 100644 backend/src/main/java/com/mapbefine/mapbefine/auth/application/TokenService.java create mode 100644 backend/src/main/java/com/mapbefine/mapbefine/auth/domain/token/RefreshToken.java create mode 100644 backend/src/main/java/com/mapbefine/mapbefine/auth/domain/token/RefreshTokenRepository.java create mode 100644 backend/src/main/java/com/mapbefine/mapbefine/auth/dto/AccessToken.java create mode 100644 backend/src/main/java/com/mapbefine/mapbefine/auth/dto/LoginTokens.java create mode 100644 backend/src/main/java/com/mapbefine/mapbefine/auth/dto/response/LoginInfoResponse.java create mode 100644 backend/src/main/java/com/mapbefine/mapbefine/auth/infrastructure/TokenProvider.java create mode 100644 backend/src/main/java/com/mapbefine/mapbefine/auth/presentation/LoginController.java delete mode 100644 backend/src/main/java/com/mapbefine/mapbefine/oauth/dto/LoginInfoResponse.java delete mode 100644 backend/src/main/java/com/mapbefine/mapbefine/oauth/presentation/OauthController.java create mode 100644 backend/src/test/java/com/mapbefine/mapbefine/auth/application/TestTokenProvider.java create mode 100644 backend/src/test/java/com/mapbefine/mapbefine/auth/application/TokenServiceTest.java create mode 100644 backend/src/test/java/com/mapbefine/mapbefine/auth/presentation/LoginControllerTest.java diff --git a/backend/src/docs/asciidoc/auth.adoc b/backend/src/docs/asciidoc/auth.adoc new file mode 100644 index 00000000..625cf122 --- /dev/null +++ b/backend/src/docs/asciidoc/auth.adoc @@ -0,0 +1,13 @@ +== 소셜 로그인 + +=== KAKAO 로그인 URL 반환 + +operation::login-controller-test/redirection[snippets='http-request,http-response'] + +=== KAKAO 로그인 + +operation::login-controller-test/login[snippets='http-request,http-response'] + +=== 로그아웃 + +operation::login-controller-test/logout[snippets='http-request,http-response'] diff --git a/backend/src/docs/asciidoc/index.adoc b/backend/src/docs/asciidoc/index.adoc index a90d89df..fc35c312 100644 --- a/backend/src/docs/asciidoc/index.adoc +++ b/backend/src/docs/asciidoc/index.adoc @@ -13,6 +13,6 @@ include::pin.adoc[] include::atlas.adoc[] include::member.adoc[] include::permission.adoc[] -include::oauth.adoc[] +include::auth.adoc[] include::bookmark.adoc[] include::admin.adoc[] diff --git a/backend/src/docs/asciidoc/oauth.adoc b/backend/src/docs/asciidoc/oauth.adoc deleted file mode 100644 index f6aeec23..00000000 --- a/backend/src/docs/asciidoc/oauth.adoc +++ /dev/null @@ -1,9 +0,0 @@ -== 소셜 로그인 - -=== KAKAO 로그인 URL 반환 - -operation::oauth-controller-test/redirection[snippets='http-request,http-response'] - -=== KAKAO 로그인 - -operation::oauth-controller-test/login[snippets='http-request,http-response'] diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/application/AuthService.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/application/AuthService.java index 11b2e343..d35346de 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/auth/application/AuthService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/application/AuthService.java @@ -1,8 +1,11 @@ package com.mapbefine.mapbefine.auth.application; +import static com.mapbefine.mapbefine.auth.exception.AuthException.AuthUnauthorizedException; + import com.mapbefine.mapbefine.auth.domain.AuthMember; import com.mapbefine.mapbefine.auth.domain.member.Admin; import com.mapbefine.mapbefine.auth.domain.member.User; +import com.mapbefine.mapbefine.auth.exception.AuthErrorCode; import com.mapbefine.mapbefine.member.domain.Member; import com.mapbefine.mapbefine.member.domain.MemberRepository; import com.mapbefine.mapbefine.topic.domain.Topic; @@ -22,11 +25,10 @@ public AuthService(MemberRepository memberRepository) { this.memberRepository = memberRepository; } - public boolean isMember(Long memberId) { - if (Objects.isNull(memberId)) { - return false; + public void validateMember(Long memberId) { + if (Objects.isNull(memberId) || !memberRepository.existsById(memberId)) { + throw new AuthUnauthorizedException(AuthErrorCode.ILLEGAL_MEMBER_ID); } - return memberRepository.existsById(memberId); } public AuthMember findAuthMemberByMemberId(Long memberId) { diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/application/TokenService.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/application/TokenService.java new file mode 100644 index 00000000..3344a6f9 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/application/TokenService.java @@ -0,0 +1,62 @@ +package com.mapbefine.mapbefine.auth.application; + +import static com.mapbefine.mapbefine.auth.exception.AuthErrorCode.ILLEGAL_TOKEN; + +import com.mapbefine.mapbefine.auth.domain.token.RefreshToken; +import com.mapbefine.mapbefine.auth.domain.token.RefreshTokenRepository; +import com.mapbefine.mapbefine.auth.dto.LoginTokens; +import com.mapbefine.mapbefine.auth.exception.AuthException.AuthUnauthorizedException; +import com.mapbefine.mapbefine.auth.infrastructure.TokenProvider; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +public class TokenService { + + private final TokenProvider tokenProvider; + private final RefreshTokenRepository refreshTokenRepository; + + public TokenService(TokenProvider tokenProvider, RefreshTokenRepository refreshTokenRepository) { + this.tokenProvider = tokenProvider; + this.refreshTokenRepository = refreshTokenRepository; + } + + public LoginTokens issueTokens(Long memberId) { + String accessToken = tokenProvider.createAccessToken(String.valueOf(memberId)); + String refreshToken = tokenProvider.createRefreshToken(); + + refreshTokenRepository.findByMemberId(memberId) + .ifPresent(refreshTokenRepository::delete); + + refreshTokenRepository.save(new RefreshToken(refreshToken, memberId)); + + return new LoginTokens(accessToken, refreshToken); + } + + public LoginTokens reissueToken(String refreshToken, String accessToken) { + tokenProvider.validateTokensForReissue(refreshToken, accessToken); + Long memberId = findMemberIdByRefreshToken(refreshToken); + + return issueTokens(memberId); + } + + private Long findMemberIdByRefreshToken(String token) { + RefreshToken refreshToken = refreshTokenRepository.findById(token) + .orElseThrow(() -> new AuthUnauthorizedException(ILLEGAL_TOKEN)); + + return refreshToken.getMemberId(); + } + + public void removeRefreshToken(String refreshToken, String accessToken) { + tokenProvider.validateTokensForRemoval(refreshToken, accessToken); + + String payload = tokenProvider.getPayload(accessToken); + Long memberId = Long.valueOf(payload); + + refreshTokenRepository.deleteByMemberId(memberId); + } + + + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/token/RefreshToken.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/token/RefreshToken.java new file mode 100644 index 00000000..e58c45f0 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/token/RefreshToken.java @@ -0,0 +1,27 @@ +package com.mapbefine.mapbefine.auth.domain.token; + +import static lombok.AccessLevel.PROTECTED; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@NoArgsConstructor(access = PROTECTED) +@Getter +public class RefreshToken { + + @Id + private String token; + + @Column(nullable = false, unique = true) + private Long memberId; + + public RefreshToken(String token, Long memberId) { + this.token = token; + this.memberId = memberId; + } + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/token/RefreshTokenRepository.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/token/RefreshTokenRepository.java new file mode 100644 index 00000000..8fab5056 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/token/RefreshTokenRepository.java @@ -0,0 +1,12 @@ +package com.mapbefine.mapbefine.auth.domain.token; + +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface RefreshTokenRepository extends JpaRepository { + + Optional findByMemberId(Long memberId); + + void deleteByMemberId(Long memberId); + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/dto/AccessToken.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/dto/AccessToken.java new file mode 100644 index 00000000..740b62b1 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/dto/AccessToken.java @@ -0,0 +1,6 @@ +package com.mapbefine.mapbefine.auth.dto; + +public record AccessToken( + String accessToken +) { +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/dto/LoginTokens.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/dto/LoginTokens.java new file mode 100644 index 00000000..5fee5e89 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/dto/LoginTokens.java @@ -0,0 +1,11 @@ +package com.mapbefine.mapbefine.auth.dto; + +public record LoginTokens( + String accessToken, + String refreshToken +) { + + public AccessToken toAccessToken() { + return new AccessToken(accessToken); + } +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/dto/response/LoginInfoResponse.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/dto/response/LoginInfoResponse.java new file mode 100644 index 00000000..24cdee73 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/dto/response/LoginInfoResponse.java @@ -0,0 +1,9 @@ +package com.mapbefine.mapbefine.auth.dto.response; + +import com.mapbefine.mapbefine.member.dto.response.MemberDetailResponse; + +public record LoginInfoResponse( + String accessToken, + MemberDetailResponse member +) { +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/exception/AuthErrorCode.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/exception/AuthErrorCode.java index 7cbf7c20..50150b86 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/auth/exception/AuthErrorCode.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/exception/AuthErrorCode.java @@ -4,10 +4,11 @@ @Getter public enum AuthErrorCode { - ILLEGAL_MEMBER_ID("03100", "로그인에 실패하였습니다."), - ILLEGAL_TOKEN("03101", "로그인에 실패하였습니다."), - FORBIDDEN_ADMIN_ACCESS("03102", "로그인에 실패하였습니다."), - BLOCKING_MEMBER_ACCESS("03103", "로그인에 실패하였습니다."), + ILLEGAL_MEMBER_ID("01100", "로그인에 실패하였습니다."), + ILLEGAL_TOKEN("01101", "로그인에 실패하였습니다."), + FORBIDDEN_ADMIN_ACCESS("01102", "로그인에 실패하였습니다."), + BLOCKING_MEMBER_ACCESS("01103", "로그인에 실패하였습니다."), + EXPIRED_TOKEN("01104", "기간이 만료된 토큰입니다.") ; private final String code; diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/infrastructure/JwtTokenProvider.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/infrastructure/JwtTokenProvider.java index af4565f1..2e11a2eb 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/auth/infrastructure/JwtTokenProvider.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/infrastructure/JwtTokenProvider.java @@ -1,6 +1,11 @@ package com.mapbefine.mapbefine.auth.infrastructure; +import static com.mapbefine.mapbefine.auth.exception.AuthErrorCode.EXPIRED_TOKEN; +import static com.mapbefine.mapbefine.auth.exception.AuthErrorCode.ILLEGAL_TOKEN; + +import com.mapbefine.mapbefine.auth.exception.AuthException.AuthUnauthorizedException; import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Jws; import io.jsonwebtoken.JwtException; import io.jsonwebtoken.Jwts; @@ -10,13 +15,36 @@ import org.springframework.stereotype.Component; @Component -public class JwtTokenProvider { - @Value("${security.jwt.token.secret-key}") - private String secretKey; - @Value("${security.jwt.token.expire-length}") - private long validityInMilliseconds; +public class JwtTokenProvider implements TokenProvider { + + private static final String EMPTY = ""; + + private final String secretKey; + private final long accessExpirationTime; + private final long refreshExpirationTime; + + public JwtTokenProvider( + @Value("${security.jwt.token.secret-key}") + String secretKey, + @Value("${security.jwt.token.access-expire-length}") + long accessExpirationTime, + @Value("${security.jwt.token.refresh-expire-length}") + long refreshExpirationTime + ) { + this.secretKey = secretKey; + this.accessExpirationTime = accessExpirationTime; + this.refreshExpirationTime = refreshExpirationTime; + } - public String createToken(String payload) { + public String createAccessToken(String payload) { + return createToken(payload, accessExpirationTime); + } + + public String createRefreshToken() { + return createToken(EMPTY, refreshExpirationTime); + } + + private String createToken(String payload, Long validityInMilliseconds) { Claims claims = Jwts.claims().setSubject(payload); Date now = new Date(); Date validity = new Date(now.getTime() + validityInMilliseconds); @@ -33,14 +61,40 @@ public String getPayload(String token) { return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject(); } - public boolean validateToken(String token) { + public void validateTokensForReissue(String refreshToken, String accessToken) { + boolean canReissueAccessToken = !isExpired(refreshToken) && isExpired(accessToken); + if (canReissueAccessToken) { + return; + } + throw new AuthUnauthorizedException(ILLEGAL_TOKEN); + } + + public void validateTokensForRemoval(String refreshToken, String accessToken) { + boolean canRemoveRefreshToken = !isExpired(refreshToken) && !isExpired(accessToken); + if (canRemoveRefreshToken) { + return; + } + throw new AuthUnauthorizedException(EXPIRED_TOKEN); + } + + public void validateAccessToken(String accessToken) { + if (isExpired(accessToken)) { + throw new AuthUnauthorizedException(EXPIRED_TOKEN); + } + } + + private boolean isExpired(String token) { try { Jws claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token); + Date expiration = claims.getBody().getExpiration(); - return !claims.getBody().getExpiration().before(new Date()); + return expiration.before(new Date()); + } catch (ExpiredJwtException e) { + return true; } catch (JwtException | IllegalArgumentException e) { - return false; + throw new AuthUnauthorizedException(ILLEGAL_TOKEN); } } + } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/infrastructure/TokenProvider.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/infrastructure/TokenProvider.java new file mode 100644 index 00000000..cc9f04cd --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/infrastructure/TokenProvider.java @@ -0,0 +1,16 @@ +package com.mapbefine.mapbefine.auth.infrastructure; + +public interface TokenProvider { + + String createAccessToken(String payload); + + String createRefreshToken(); + + String getPayload(String token); + + void validateTokensForReissue(String refreshToken, String accessToken); + + void validateTokensForRemoval(String refreshToken, String accessToken); + + void validateAccessToken(String accessToken); +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/presentation/LoginController.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/presentation/LoginController.java new file mode 100644 index 00000000..b7ca5d0b --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/presentation/LoginController.java @@ -0,0 +1,96 @@ +package com.mapbefine.mapbefine.auth.presentation; + +import static org.springframework.http.HttpHeaders.SET_COOKIE; + +import com.mapbefine.mapbefine.auth.application.TokenService; +import com.mapbefine.mapbefine.auth.dto.AccessToken; +import com.mapbefine.mapbefine.auth.dto.LoginTokens; +import com.mapbefine.mapbefine.auth.dto.response.LoginInfoResponse; +import com.mapbefine.mapbefine.member.dto.response.MemberDetailResponse; +import com.mapbefine.mapbefine.oauth.application.OauthService; +import com.mapbefine.mapbefine.oauth.domain.OauthServerType; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import org.springframework.http.ResponseCookie; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.CookieValue; +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.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class LoginController { + + public static final int TWO_WEEKS = 14 * 24 * 60 * 60; + + private final TokenService tokenService; + private final OauthService oauthService; + + public LoginController(TokenService tokenService, OauthService oauthService) { + this.tokenService = tokenService; + this.oauthService = oauthService; + } + + @GetMapping("/oauth/{oauthServerType}") + public ResponseEntity redirection( + @PathVariable OauthServerType oauthServerType, + HttpServletResponse response + ) throws IOException { + String redirectUrl = oauthService.getAuthCodeRequestUrl(oauthServerType); + response.sendRedirect(redirectUrl); + + return ResponseEntity.ok().build(); + } + + @GetMapping("/oauth/login/{oauthServerType}") + public ResponseEntity login( + @PathVariable OauthServerType oauthServerType, + @RequestParam String code, + HttpServletResponse response + ) { + MemberDetailResponse memberDetail = oauthService.login(oauthServerType, code); + + LoginTokens loginTokens = tokenService.issueTokens(memberDetail.id()); + ResponseCookie cookie = createCookie(loginTokens.refreshToken()); + response.addHeader(SET_COOKIE, cookie.toString()); + + return ResponseEntity.ok(new LoginInfoResponse(loginTokens.accessToken(), memberDetail)); + } + + private ResponseCookie createCookie(String refreshToken) { + return ResponseCookie.from("refresh-token", refreshToken) + .httpOnly(true) + .maxAge(TWO_WEEKS) + .sameSite("Lax") + .secure(true) + .path("/") + .build(); + } + + @PostMapping("/refresh-token") + public ResponseEntity reissueTokens( + @CookieValue("refresh-token") String refreshToken, + @RequestBody AccessToken request, + HttpServletResponse response + ) { + LoginTokens loginTokens = tokenService.reissueToken(refreshToken, request.accessToken()); + ResponseCookie cookie = createCookie(loginTokens.refreshToken()); + response.addHeader(SET_COOKIE, cookie.toString()); + + return ResponseEntity.ok(loginTokens.toAccessToken()); + } + + @PostMapping("/logout") + public ResponseEntity logout( + @CookieValue("refresh-token") String refreshToken, + @RequestBody AccessToken request + ) { + tokenService.removeRefreshToken(refreshToken, request.accessToken()); + + return ResponseEntity.noContent().build(); + } + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/common/config/WebConfig.java b/backend/src/main/java/com/mapbefine/mapbefine/common/config/WebConfig.java index 635d99ae..2af70d48 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/common/config/WebConfig.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/common/config/WebConfig.java @@ -1,5 +1,8 @@ package com.mapbefine.mapbefine.common.config; +import static org.springframework.http.HttpHeaders.LOCATION; +import static org.springframework.http.HttpHeaders.SET_COOKIE; + import com.mapbefine.mapbefine.common.converter.OauthServerTypeConverter; import org.springframework.context.annotation.Configuration; import org.springframework.format.FormatterRegistry; @@ -13,8 +16,10 @@ public class WebConfig implements WebMvcConfigurer { public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("http://localhost:3000", "https://mapbefine.kro.kr", "https://mapbefine.com") + .allowedHeaders("refresh-token") .allowedMethods("*") - .exposedHeaders("Location"); + .allowCredentials(true) + .exposedHeaders(LOCATION, SET_COOKIE); } @Override diff --git a/backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AdminAuthInterceptor.java b/backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AdminAuthInterceptor.java index 9f5c8424..7a7cb1bb 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AdminAuthInterceptor.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AdminAuthInterceptor.java @@ -5,10 +5,11 @@ import com.mapbefine.mapbefine.auth.exception.AuthErrorCode; import com.mapbefine.mapbefine.auth.exception.AuthException; import com.mapbefine.mapbefine.auth.infrastructure.AuthorizationExtractor; -import com.mapbefine.mapbefine.auth.infrastructure.JwtTokenProvider; +import com.mapbefine.mapbefine.auth.infrastructure.TokenProvider; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.util.Objects; +import org.springframework.lang.NonNull; import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; @@ -18,24 +19,24 @@ public class AdminAuthInterceptor implements HandlerInterceptor { private final AuthorizationExtractor authorizationExtractor; private final AuthService authService; - private final JwtTokenProvider jwtTokenProvider; + private final TokenProvider tokenProvider; public AdminAuthInterceptor( AuthorizationExtractor authorizationExtractor, AuthService authService, - JwtTokenProvider jwtTokenProvider + TokenProvider tokenProvider ) { this.authorizationExtractor = authorizationExtractor; this.authService = authService; - this.jwtTokenProvider = jwtTokenProvider; + this.tokenProvider = tokenProvider; } @Override public boolean preHandle( - HttpServletRequest request, - HttpServletResponse response, - Object handler - ) throws Exception { + @NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull Object handler + ) { if (!(handler instanceof HandlerMethod)) { return true; } @@ -53,11 +54,9 @@ private Long extractMemberIdFromToken(HttpServletRequest request) { if (Objects.isNull(authInfo)) { return null; } - String accessToken = authInfo.accessToken(); - if (jwtTokenProvider.validateToken(accessToken)) { - return Long.parseLong(jwtTokenProvider.getPayload(accessToken)); - } - throw new AuthException.AuthUnauthorizedException(AuthErrorCode.ILLEGAL_TOKEN); + tokenProvider.validateAccessToken(authInfo.accessToken()); + + return Long.parseLong(tokenProvider.getPayload(authInfo.accessToken())); } private void validateAdmin(Long memberId) { diff --git a/backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AuthInterceptor.java b/backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AuthInterceptor.java index 01c98c73..7ee91245 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AuthInterceptor.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AuthInterceptor.java @@ -3,15 +3,13 @@ import com.mapbefine.mapbefine.auth.application.AuthService; import com.mapbefine.mapbefine.auth.domain.AuthMember; import com.mapbefine.mapbefine.auth.dto.AuthInfo; -import com.mapbefine.mapbefine.auth.exception.AuthErrorCode; -import com.mapbefine.mapbefine.auth.exception.AuthException; -import com.mapbefine.mapbefine.auth.exception.AuthException.AuthUnauthorizedException; import com.mapbefine.mapbefine.auth.infrastructure.AuthorizationExtractor; -import com.mapbefine.mapbefine.auth.infrastructure.JwtTokenProvider; +import com.mapbefine.mapbefine.auth.infrastructure.TokenProvider; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.util.Arrays; import java.util.Objects; +import org.springframework.lang.NonNull; import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; @@ -21,24 +19,24 @@ public class AuthInterceptor implements HandlerInterceptor { private final AuthorizationExtractor authorizationExtractor; private final AuthService authService; - private final JwtTokenProvider jwtTokenProvider; + private final TokenProvider tokenProvider; public AuthInterceptor( AuthorizationExtractor authorizationExtractor, AuthService authService, - JwtTokenProvider jwtTokenProvider + TokenProvider tokenProvider ) { this.authorizationExtractor = authorizationExtractor; this.authService = authService; - this.jwtTokenProvider = jwtTokenProvider; + this.tokenProvider = tokenProvider; } @Override public boolean preHandle( - HttpServletRequest request, - HttpServletResponse response, - Object handler - ) throws Exception { + @NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull Object handler + ) { if (!(handler instanceof HandlerMethod handlerMethod)) { return true; } @@ -49,7 +47,7 @@ public boolean preHandle( Long memberId = extractMemberIdFromToken(request); if (isLoginRequired((HandlerMethod) handler)) { - validateMember(memberId); + authService.validateMember(memberId); } request.setAttribute("memberId", memberId); @@ -57,14 +55,6 @@ public boolean preHandle( return true; } - private void validateMember(Long memberId) { - if (authService.isMember(memberId)) { - return; - } - - throw new AuthUnauthorizedException(AuthErrorCode.ILLEGAL_MEMBER_ID); - } - private boolean isAuthMemberNotRequired(HandlerMethod handlerMethod) { return Arrays.stream(handlerMethod.getMethodParameters()) .noneMatch(parameter -> parameter.getParameterType().equals(AuthMember.class)); @@ -81,11 +71,9 @@ private Long extractMemberIdFromToken(HttpServletRequest request) { if (Objects.isNull(authInfo)) { return null; } - String accessToken = authInfo.accessToken(); - if (!jwtTokenProvider.validateToken(accessToken)) { - throw new AuthException.AuthUnauthorizedException(AuthErrorCode.ILLEGAL_TOKEN); - } - return Long.parseLong(jwtTokenProvider.getPayload(accessToken)); + tokenProvider.validateAccessToken(authInfo.accessToken()); + + return Long.parseLong(tokenProvider.getPayload(authInfo.accessToken())); } } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/oauth/application/OauthService.java b/backend/src/main/java/com/mapbefine/mapbefine/oauth/application/OauthService.java index 50681719..e7edd8b7 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/oauth/application/OauthService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/oauth/application/OauthService.java @@ -2,32 +2,28 @@ import com.mapbefine.mapbefine.auth.exception.AuthErrorCode; import com.mapbefine.mapbefine.auth.exception.AuthException.AuthUnauthorizedException; -import com.mapbefine.mapbefine.auth.infrastructure.JwtTokenProvider; import com.mapbefine.mapbefine.member.domain.Member; import com.mapbefine.mapbefine.member.domain.MemberRepository; +import com.mapbefine.mapbefine.member.dto.response.MemberDetailResponse; import com.mapbefine.mapbefine.oauth.domain.AuthCodeRequestUrlProviderComposite; import com.mapbefine.mapbefine.oauth.domain.OauthMember; import com.mapbefine.mapbefine.oauth.domain.OauthMemberClientComposite; import com.mapbefine.mapbefine.oauth.domain.OauthServerType; -import com.mapbefine.mapbefine.oauth.dto.LoginInfoResponse; import org.springframework.stereotype.Service; @Service public class OauthService { - private MemberRepository memberRepository; - private JwtTokenProvider jwtTokenProvider; - private AuthCodeRequestUrlProviderComposite authCodeRequestUrlProviderComposite; - private OauthMemberClientComposite oauthMemberClientComposite; + private final MemberRepository memberRepository; + private final AuthCodeRequestUrlProviderComposite authCodeRequestUrlProviderComposite; + private final OauthMemberClientComposite oauthMemberClientComposite; public OauthService( MemberRepository memberRepository, - JwtTokenProvider jwtTokenProvider, AuthCodeRequestUrlProviderComposite authCodeRequestUrlProviderComposite, OauthMemberClientComposite oauthMemberClientComposite ) { this.memberRepository = memberRepository; - this.jwtTokenProvider = jwtTokenProvider; this.authCodeRequestUrlProviderComposite = authCodeRequestUrlProviderComposite; this.oauthMemberClientComposite = oauthMemberClientComposite; } @@ -36,16 +32,14 @@ public String getAuthCodeRequestUrl(OauthServerType oauthServerType) { return authCodeRequestUrlProviderComposite.provide(oauthServerType); } - public LoginInfoResponse login(OauthServerType oauthServerType, String code) { + public MemberDetailResponse login(OauthServerType oauthServerType, String code) { OauthMember oauthMember = oauthMemberClientComposite.fetch(oauthServerType, code); Member savedMember = memberRepository.findByOauthId(oauthMember.getOauthId()) .orElseGet(() -> register(oauthMember)); validateMemberStatus(savedMember); - String accessToken = jwtTokenProvider.createToken(String.valueOf(savedMember.getId())); - - return LoginInfoResponse.of(accessToken, savedMember); + return MemberDetailResponse.from(savedMember); } private Member register(OauthMember oauthMember) { @@ -57,7 +51,6 @@ private void validateMemberStatus(Member member) { if (member.isNormalStatus()) { return; } - throw new AuthUnauthorizedException(AuthErrorCode.BLOCKING_MEMBER_ACCESS); } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/oauth/dto/LoginInfoResponse.java b/backend/src/main/java/com/mapbefine/mapbefine/oauth/dto/LoginInfoResponse.java deleted file mode 100644 index bdf52394..00000000 --- a/backend/src/main/java/com/mapbefine/mapbefine/oauth/dto/LoginInfoResponse.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.mapbefine.mapbefine.oauth.dto; - -import com.mapbefine.mapbefine.member.domain.Member; -import com.mapbefine.mapbefine.member.dto.response.MemberDetailResponse; - -public record LoginInfoResponse( - String accessToken, - MemberDetailResponse member -) { - - public static LoginInfoResponse of(String accessToken, Member member) { - return new LoginInfoResponse(accessToken, MemberDetailResponse.from(member)); - } - -} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/oauth/presentation/OauthController.java b/backend/src/main/java/com/mapbefine/mapbefine/oauth/presentation/OauthController.java deleted file mode 100644 index 410697c9..00000000 --- a/backend/src/main/java/com/mapbefine/mapbefine/oauth/presentation/OauthController.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.mapbefine.mapbefine.oauth.presentation; - -import com.mapbefine.mapbefine.oauth.application.OauthService; -import com.mapbefine.mapbefine.oauth.domain.OauthServerType; -import com.mapbefine.mapbefine.oauth.dto.LoginInfoResponse; -import jakarta.servlet.http.HttpServletResponse; -import java.io.IOException; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/oauth") -public class OauthController { - - private final OauthService oauthService; - - public OauthController(final OauthService oauthService) { - this.oauthService = oauthService; - } - - @GetMapping("/{oauthServerType}") - public ResponseEntity redirection( - @PathVariable OauthServerType oauthServerType, - HttpServletResponse response - ) throws IOException { - String redirectUrl = oauthService.getAuthCodeRequestUrl(oauthServerType); - response.sendRedirect(redirectUrl); - - return ResponseEntity.ok().build(); - } - - @GetMapping("/login/{oauthServerType}") - public ResponseEntity login( - @PathVariable OauthServerType oauthServerType, - @RequestParam String code - ) { - LoginInfoResponse loginInfo = oauthService.login(oauthServerType, code); - - return ResponseEntity.ok(loginInfo); - } - -} diff --git a/backend/src/main/resources/config b/backend/src/main/resources/config index 0b448b77..5a3930cf 160000 --- a/backend/src/main/resources/config +++ b/backend/src/main/resources/config @@ -1 +1 @@ -Subproject commit 0b448b7781204387184f5ce47a575a5b2d955128 +Subproject commit 5a3930cfc4da1807ecd70125a34703e2696e19e9 diff --git a/backend/src/test/java/com/mapbefine/mapbefine/atlas/application/AtlasCommandServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/atlas/application/AtlasCommandServiceTest.java index 28b4975c..ca10b0b6 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/atlas/application/AtlasCommandServiceTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/atlas/application/AtlasCommandServiceTest.java @@ -84,7 +84,7 @@ void alreadyAdd_Fail() { List actual = actualMember.getAtlantes(); //then - assertThat(actual.size()).isEqualTo(expected.size()); + assertThat(actual).hasSameSizeAs(expected); } @Test @@ -124,7 +124,7 @@ void remove_Success() { List actual = memberAfter.getAtlantes(); //then - assertThat(actual.size()).isEqualTo(expected.size() - 1); + assertThat(actual).hasSize(expected.size() - 1); } } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/auth/application/TestTokenProvider.java b/backend/src/test/java/com/mapbefine/mapbefine/auth/application/TestTokenProvider.java new file mode 100644 index 00000000..9d5f81d7 --- /dev/null +++ b/backend/src/test/java/com/mapbefine/mapbefine/auth/application/TestTokenProvider.java @@ -0,0 +1,103 @@ +package com.mapbefine.mapbefine.auth.application; + +import static com.mapbefine.mapbefine.auth.exception.AuthErrorCode.EXPIRED_TOKEN; +import static com.mapbefine.mapbefine.auth.exception.AuthErrorCode.ILLEGAL_TOKEN; + +import com.mapbefine.mapbefine.auth.exception.AuthException.AuthUnauthorizedException; +import com.mapbefine.mapbefine.auth.infrastructure.TokenProvider; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import java.util.Date; + +public class TestTokenProvider implements TokenProvider { + + private static final String EMPTY = ""; + + private final String secretKey; + private final Date issuedAt; + private final long accessExpirationTime; + private final long refreshExpirationTime; + + public TestTokenProvider( + String secretKey, + Date issuedAt, + long accessExpirationTime, + long refreshExpirationTime + ) { + this.secretKey = secretKey; + this.issuedAt = issuedAt; + this.accessExpirationTime = accessExpirationTime; + this.refreshExpirationTime = refreshExpirationTime; + } + + @Override + public String createAccessToken(String payload) { + return createToken(payload, new Date(), accessExpirationTime); + } + + @Override + public String createRefreshToken() { + return createToken(EMPTY, new Date(), refreshExpirationTime); + } + + public String createExpiredAccessToken(String payload) { + return createToken(payload, issuedAt, accessExpirationTime); + } + + private String createToken(String payload, Date issuedAt, Long validityInMilliseconds) { + Claims claims = Jwts.claims().setSubject(payload); + Date validity = new Date(issuedAt.getTime() + validityInMilliseconds); + + return Jwts.builder() + .setClaims(claims) + .setIssuedAt(issuedAt) + .setExpiration(validity) + .signWith(SignatureAlgorithm.HS256, secretKey) + .compact(); + } + + @Override + public String getPayload(String token) { + return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject(); + } + + @Override + public void validateTokensForReissue(String refreshToken, String accessToken) { + boolean canReissueAccessToken = !isExpired(refreshToken) && isExpired(accessToken); + if (!canReissueAccessToken) { + throw new AuthUnauthorizedException(ILLEGAL_TOKEN); + } + } + + @Override + public void validateTokensForRemoval(String refreshToken, String accessToken) { + boolean canRemoveRefreshToken = !isExpired(refreshToken) && !isExpired(accessToken); + if (!canRemoveRefreshToken) { + throw new AuthUnauthorizedException(EXPIRED_TOKEN); + } + } + + @Override + public void validateAccessToken(String accessToken) { + if (isExpired(accessToken)) { + throw new AuthUnauthorizedException(EXPIRED_TOKEN); + } + } + + private boolean isExpired(String token) { + try { + Jws claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token); + Date expiration = claims.getBody().getExpiration(); + + return expiration.before(new Date()); + } catch (ExpiredJwtException e) { + return true; + } catch (JwtException | IllegalArgumentException e) { + throw new AuthUnauthorizedException(ILLEGAL_TOKEN); + } + } +} diff --git a/backend/src/test/java/com/mapbefine/mapbefine/auth/application/TokenServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/auth/application/TokenServiceTest.java new file mode 100644 index 00000000..10b782f8 --- /dev/null +++ b/backend/src/test/java/com/mapbefine/mapbefine/auth/application/TokenServiceTest.java @@ -0,0 +1,146 @@ +package com.mapbefine.mapbefine.auth.application; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.mapbefine.mapbefine.auth.domain.token.RefreshToken; +import com.mapbefine.mapbefine.auth.domain.token.RefreshTokenRepository; +import com.mapbefine.mapbefine.auth.dto.LoginTokens; +import com.mapbefine.mapbefine.member.MemberFixture; +import com.mapbefine.mapbefine.member.domain.Member; +import com.mapbefine.mapbefine.member.domain.MemberRepository; +import com.mapbefine.mapbefine.member.domain.Role; +import java.util.Calendar; +import java.util.Date; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.test.context.TestPropertySource; + +@DataJpaTest +@TestPropertySource(locations = "classpath:application.yml") +class TokenServiceTest { + + @Autowired + private RefreshTokenRepository refreshTokenRepository; + @Autowired + private MemberRepository memberRepository; + + @Value("${security.jwt.token.secret-key}") + private String secretKey; + @Value("${security.jwt.token.access-expire-length}") + private long accessExpirationTime; + @Value("${security.jwt.token.refresh-expire-length}") + private long refreshExpirationTime; + + private TestTokenProvider testTokenProvider; + private TokenService tokenService; + private Long memberId; + + @BeforeEach + void setUp() { + testTokenProvider = getTestTokenProvider(); + tokenService = new TokenService(testTokenProvider, refreshTokenRepository); + + Member member = memberRepository.save(MemberFixture.create("member", "member@gmail.com", Role.USER)); + memberId = member.getId(); + } + + private TestTokenProvider getTestTokenProvider() { + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.DATE, -1); + Date yesterday = calendar.getTime(); + + return new TestTokenProvider( + secretKey, + yesterday, + accessExpirationTime, + refreshExpirationTime + ); + } + + @Nested + @DisplayName("Token을 발급할 때,") + class IssueTokens { + + @Test + @DisplayName("멤버 Id를 입력하면 정상적으로 토큰을 발행한다.") + void success() { + // given + String payload = String.valueOf(memberId); + + // when + LoginTokens loginTokens = tokenService.issueTokens(memberId); + + // then + String actualAccessToken = loginTokens.accessToken(); + String actualRefreshToken = loginTokens.refreshToken(); + + assertThat(actualAccessToken).isNotNull(); + assertThat(actualRefreshToken).isNotNull(); + assertThat(payload).isEqualTo(testTokenProvider.getPayload(actualAccessToken)); + } + + } + + + @Nested + @DisplayName("Token을 재발급할 때,") + class ReissueToken { + + @Test + @DisplayName("Refresh Token이 유효하고, AccessToken이 만료되었다면 성공한다") + void success() { + // given + String payload = String.valueOf(memberId); + String refreshToken = createRefreshToken(); + String accessToken = testTokenProvider.createExpiredAccessToken(payload); + + // when + LoginTokens loginTokens = tokenService.reissueToken(refreshToken, accessToken); + + // then + String actualAccessToken = loginTokens.accessToken(); + String actualRefreshToken = loginTokens.refreshToken(); + + assertThat(actualAccessToken).isNotNull(); + assertThat(actualRefreshToken).isNotNull(); + assertThat(payload).isEqualTo(testTokenProvider.getPayload(actualAccessToken)); + } + + } + + @Nested + @DisplayName("Refresh Token을 제거할 때,") + class RemoveRefreshToken { + + @Test + @DisplayName("Refresh Token과 AccessToken 모두 유효하다면 성공한다") + void removeRefreshToken_success() { + // given + String refreshToken = createRefreshToken(); + String accessToken = testTokenProvider.createAccessToken(String.valueOf(memberId)); + + // when + tokenService.removeRefreshToken(refreshToken, accessToken); + + // then + Optional optionalRefreshToken = refreshTokenRepository.findById(refreshToken); + + assertThat(optionalRefreshToken).isEmpty(); + } + + } + + private String createRefreshToken() { + String refreshToken = testTokenProvider.createRefreshToken(); + refreshTokenRepository.save(new RefreshToken(refreshToken, memberId)); + return refreshToken; + } + + +} diff --git a/backend/src/test/java/com/mapbefine/mapbefine/auth/infrastructure/JwtTokenProviderTest.java b/backend/src/test/java/com/mapbefine/mapbefine/auth/infrastructure/JwtTokenProviderTest.java index 8081cf84..4de1efec 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/auth/infrastructure/JwtTokenProviderTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/auth/infrastructure/JwtTokenProviderTest.java @@ -16,11 +16,11 @@ class JwtTokenProviderTest { private JwtTokenProvider jwtTokenProvider; @Test - @DisplayName("payload를 받아 JWT 토큰을 생성한다.") - void createToken() { + @DisplayName("payload를 받아 access token을 생성한다.") + void createAccessToken_success() { String payload = "1"; - String token = jwtTokenProvider.createToken(payload); + String token = jwtTokenProvider.createAccessToken(payload); assertThat(jwtTokenProvider.getPayload(token)) .isEqualTo(payload); diff --git a/backend/src/test/java/com/mapbefine/mapbefine/auth/presentation/LoginControllerTest.java b/backend/src/test/java/com/mapbefine/mapbefine/auth/presentation/LoginControllerTest.java new file mode 100644 index 00000000..cf4febe0 --- /dev/null +++ b/backend/src/test/java/com/mapbefine/mapbefine/auth/presentation/LoginControllerTest.java @@ -0,0 +1,110 @@ +package com.mapbefine.mapbefine.auth.presentation; + + +import static com.mapbefine.mapbefine.oauth.domain.OauthServerType.KAKAO; +import static org.apache.http.cookie.SM.COOKIE; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; + +import com.mapbefine.mapbefine.auth.application.TokenService; +import com.mapbefine.mapbefine.auth.dto.AccessToken; +import com.mapbefine.mapbefine.auth.dto.LoginTokens; +import com.mapbefine.mapbefine.common.RestDocsIntegration; +import com.mapbefine.mapbefine.member.dto.response.MemberDetailResponse; +import com.mapbefine.mapbefine.oauth.application.OauthService; +import java.time.LocalDateTime; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + + +class LoginControllerTest extends RestDocsIntegration { + + @MockBean + private TokenService tokenService; + @MockBean + private OauthService oauthService; + + + @Test + @DisplayName("kakao 소셜 로그인 Redirection Url 반환") + void redirection() throws Exception { + // given + given(oauthService.getAuthCodeRequestUrl(KAKAO)).willReturn( + "https://kauth.kakao.com/oauth/authorize?" + + "response_type=code" + + "&client_id={client_id}" + + "&redirect_uri={redirection_uri}" + + "&scope={scope}" + ); + + // when then + mockMvc.perform( + MockMvcRequestBuilders.get("/oauth/kakao") + ).andDo(restDocs.document()); + } + + @Test + @DisplayName("소셜 로그인 성공시 로그인한 유저 정보 반환") + void login() throws Exception { + // given + String code = "auth_code"; + + MemberDetailResponse memberDetailResponse = new MemberDetailResponse( + Long.MAX_VALUE, + "모험가03fcb0d", + "yshert@naver.com", + "https://map-befine-official.github.io/favicon.png", + LocalDateTime.now() + ); + + LoginTokens loginTokens = new LoginTokens( + testAuthHeaderProvider.createResponseAccessTokenById(Long.MAX_VALUE), + testAuthHeaderProvider.createRefreshToken() + ); + + given(oauthService.login(KAKAO, code)).willReturn(memberDetailResponse); + given(tokenService.issueTokens(memberDetailResponse.id())).willReturn(loginTokens); + + // when then + mockMvc.perform( + MockMvcRequestBuilders.get("/oauth/login/kakao") + .param("code", code) + ).andDo(restDocs.document()); + } + + @Test + @DisplayName("만료된 Access Token과 유효한 RefreshToken인 경우, 새로운 토큰들을 발행한다.") + void reissueTokens() throws Exception { + AccessToken expiredAccessToken = new AccessToken("만료된 액세스 토큰"); + String refreshToken = "리프레시 토큰"; + + LoginTokens reissuedTokens = new LoginTokens("재발급된 액세스 토큰", "재발급된 리프레시 토큰"); + + given(tokenService.reissueToken(any(), any())).willReturn(reissuedTokens); + + // then + mockMvc.perform( + MockMvcRequestBuilders.post("/refresh-token") + .header(COOKIE, refreshToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(expiredAccessToken)) + ).andDo(restDocs.document()); + } + + @Test + @DisplayName("유효한 Access Token과 유효한 RefreshToken인 경우, RefreshTokenRepository에서 RefreshTokend을 삭제한다") + void logout() throws Exception { + AccessToken accessToken = new AccessToken("access token"); + + mockMvc.perform( + MockMvcRequestBuilders.post("/logout") + .header(COOKIE, testAuthHeaderProvider.createRefreshToken()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(accessToken)) + ).andDo(restDocs.document()); + + } +} diff --git a/backend/src/test/java/com/mapbefine/mapbefine/bookmark/application/BookmarkCommandServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/bookmark/application/BookmarkCommandServiceTest.java index 4db118c8..1c396822 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/bookmark/application/BookmarkCommandServiceTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/bookmark/application/BookmarkCommandServiceTest.java @@ -40,7 +40,7 @@ class BookmarkCommandServiceTest { @Test @DisplayName("다른 회원의 토픽을 즐겨찾기에 추가할 수 있다.") - public void addTopicInBookmark_Success() { + void addTopicInBookmark_Success() { //given Member creator = MemberFixture.create( "member", @@ -73,7 +73,7 @@ public void addTopicInBookmark_Success() { @Test @DisplayName("권한이 없는 다른 회원의 토픽을 즐겨찾기에 추가할 수 없다.") - public void addTopicInBookmark_Fail1() { + void addTopicInBookmark_Fail1() { //given Member creator = MemberFixture.create( "member", @@ -102,7 +102,7 @@ public void addTopicInBookmark_Fail1() { @Test @DisplayName("즐겨찾기 목록에 있는 토픽을 삭제할 수 있다.") - public void deleteTopicInBookmark_Success() { + void deleteTopicInBookmark_Success() { //given Member creator = MemberFixture.create( "member", @@ -136,7 +136,7 @@ public void deleteTopicInBookmark_Success() { @Test @DisplayName("즐겨찾기 목록에 있는 권한이 없는 토픽은 삭제할 수 없다.") - public void deleteTopicInBookmark_Fail() { + void deleteTopicInBookmark_Fail() { //given Member creator = MemberFixture.create( "member", @@ -167,7 +167,7 @@ public void deleteTopicInBookmark_Fail() { @Test @DisplayName("즐겨찾기 목록에 있는 모든 토픽을 삭제할 수 있다") - public void deleteAllBookmarks_Success() { + void deleteAllBookmarks_Success() { //given Member creatorBefore = memberRepository.save(MemberFixture.create( "member", @@ -197,7 +197,7 @@ public void deleteAllBookmarks_Success() { //then Member creatorAfter = memberRepository.findById(creatorBefore.getId()).get(); - assertThat(creatorAfter.getBookmarks()).hasSize(0); + assertThat(creatorAfter.getBookmarks()).isEmpty(); } } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/bookmark/presentation/BookmarkControllerTest.java b/backend/src/test/java/com/mapbefine/mapbefine/bookmark/presentation/BookmarkControllerTest.java index b65443d9..36ff312e 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/bookmark/presentation/BookmarkControllerTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/bookmark/presentation/BookmarkControllerTest.java @@ -20,7 +20,7 @@ class BookmarkControllerTest extends RestDocsIntegration { @Test @DisplayName("토픽을 회원의 즐겨찾기에 추가") - public void addTopicInBookmark() throws Exception { + void addTopicInBookmark() throws Exception { given(bookmarkCommandService.addTopicInBookmark(any(), any())).willReturn(1L); mockMvc.perform( @@ -32,7 +32,7 @@ public void addTopicInBookmark() throws Exception { @Test @DisplayName("회원의 토픽 즐겨찾기 목록 삭제") - public void deleteTopicInBookmark() throws Exception { + void deleteTopicInBookmark() throws Exception { doNothing().when(bookmarkCommandService).deleteTopicInBookmark(any(), any()); mockMvc.perform( diff --git a/backend/src/test/java/com/mapbefine/mapbefine/common/TestAuthHeaderProvider.java b/backend/src/test/java/com/mapbefine/mapbefine/common/TestAuthHeaderProvider.java index 2af99ea5..3a1d8cc9 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/common/TestAuthHeaderProvider.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/common/TestAuthHeaderProvider.java @@ -17,19 +17,23 @@ public TestAuthHeaderProvider(JwtTokenProvider jwtTokenProvider) { public String createAuthHeader(Member member) { Long memberId = member.getId(); - return TOKEN_TYPE + generateToken(memberId); + return TOKEN_TYPE + generateAccessToken(memberId); } public String createAuthHeaderById(Long memberId) { - return TOKEN_TYPE + generateToken(memberId); + return TOKEN_TYPE + generateAccessToken(memberId); } public String createResponseAccessTokenById(Long id) { - return generateToken(id); + return generateAccessToken(id); } - private String generateToken(Long memberId) { - return jwtTokenProvider.createToken(String.valueOf(memberId)); + public String createRefreshToken() { + return jwtTokenProvider.createRefreshToken(); + } + + private String generateAccessToken(Long memberId) { + return jwtTokenProvider.createAccessToken(String.valueOf(memberId)); } } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/common/annotation/ServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/common/annotation/ServiceTest.java index 33016bbb..f8fa5c7c 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/common/annotation/ServiceTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/common/annotation/ServiceTest.java @@ -8,19 +8,23 @@ import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.context.annotation.FilterType; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) -@Transactional @DataJpaTest( - includeFilters = @Filter( - type = FilterType.ANNOTATION, value = Service.class - ), - excludeFilters = @Filter( - type = FilterType.REGEX, - pattern = "com.mapbefine.mapbefine.oauth.application.*" - ) + includeFilters = { + @Filter(type = FilterType.ANNOTATION, value = Service.class), + }, + excludeFilters = { + @Filter( + type = FilterType.REGEX, + pattern = "com.mapbefine.mapbefine.oauth.application.*" + ), + @Filter( + type = FilterType.REGEX, + pattern = "com.mapbefine.mapbefine.auth.application.*" + ) + } ) public @interface ServiceTest { } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/oauth/application/OauthServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/oauth/application/OauthServiceTest.java index be752de1..4f23f219 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/oauth/application/OauthServiceTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/oauth/application/OauthServiceTest.java @@ -8,13 +8,13 @@ import com.mapbefine.mapbefine.DatabaseCleanup; import com.mapbefine.mapbefine.member.domain.Member; import com.mapbefine.mapbefine.member.domain.MemberRepository; +import com.mapbefine.mapbefine.member.dto.response.MemberDetailResponse; import com.mapbefine.mapbefine.oauth.domain.OauthMember; import com.mapbefine.mapbefine.oauth.domain.OauthServerType; import com.mapbefine.mapbefine.oauth.domain.kakao.KakaoApiClient; import com.mapbefine.mapbefine.oauth.domain.kakao.KakaoOauthProperties; import com.mapbefine.mapbefine.oauth.domain.kakao.dto.KakaoMemberResponse; import com.mapbefine.mapbefine.oauth.domain.kakao.dto.KakaoToken; -import com.mapbefine.mapbefine.oauth.dto.LoginInfoResponse; import java.util.NoSuchElementException; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -60,13 +60,13 @@ class OauthServiceTest { @BeforeEach void setUp() { given(kakaoOauthProperties.clientId()).willReturn("774"); - given(kakaoOauthProperties.scope()).willReturn(new String[] {"profile_nickname", "account_email"}); + given(kakaoOauthProperties.scope()).willReturn(new String[]{"profile_nickname", "account_email"}); given(kakaoOauthProperties.redirectUri()).willReturn("http://localhost:3000/oauth/redirected/kakao"); given(kakaoOauthProperties.clientSecret()).willReturn("4521"); - + given(kakaoToken.tokenType()).willReturn("tokenType"); given(kakaoToken.accessToken()).willReturn("accessToken"); - + given(kakaoMemberResponse.extract()).willReturn(oauthMember); given(kakaoApiClient.fetchToken(any())).willReturn(kakaoToken); @@ -91,37 +91,36 @@ void getAuthCodeRequestUrl_success() { + "&redirect_uri=" + kakaoOauthProperties.redirectUri() + "&scope=" + String.join(",", kakaoOauthProperties.scope())); } - + @Test @DisplayName("Kakao 로 로그인 처음 하는 경우 사용자의 정보를 저장하고 사용자의 로그인 정보를 반환한다.") void loginAndRegister_success() { // when - LoginInfoResponse response = oauthService.login(KAKAO, "auth"); - Member savedMember = memberRepository.findById(response.member().id()) + MemberDetailResponse memberDetailResponse = oauthService.login(KAKAO, "auth"); + Member savedMember = memberRepository.findById(memberDetailResponse.id()) .orElseThrow(NoSuchElementException::new); // then - assertThat(response.accessToken()).isNotNull(); - assertThat(response.member().email()).isEqualTo("yshert@naver.com"); - assertThat(response.member().imageUrl()).isEqualTo( + assertThat(memberDetailResponse.email()).isEqualTo("yshert@naver.com"); + assertThat(memberDetailResponse.imageUrl()).isEqualTo( "https://map-befine-official.github.io/favicon.png" ); - assertThat(savedMember.getId()).isEqualTo(response.member().id()); + assertThat(savedMember.getId()).isEqualTo(memberDetailResponse.id()); } @Test @DisplayName("Kakao 로 로그인 처음이 아닌 경우 사용자의 정보를 저장하지 않고 사용자의 로그인 정보를 반환한다.") void login() { // given - LoginInfoResponse firstLogin = oauthService.login(KAKAO, "auth"); - Member savedMember = memberRepository.findById(firstLogin.member().id()) + MemberDetailResponse firstLogin = oauthService.login(KAKAO, "auth"); + Member savedMember = memberRepository.findById(firstLogin.id()) .orElseThrow(NoSuchElementException::new); // when - LoginInfoResponse secondLogin = oauthService.login(KAKAO, "auth"); + MemberDetailResponse secondLogin = oauthService.login(KAKAO, "auth"); // then - assertThat(savedMember.getId()).isEqualTo(secondLogin.member().id()); + assertThat(savedMember.getId()).isEqualTo(secondLogin.id()); } } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/oauth/presentation/OauthControllerTest.java b/backend/src/test/java/com/mapbefine/mapbefine/oauth/presentation/OauthControllerTest.java index 60fc1b56..e69de29b 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/oauth/presentation/OauthControllerTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/oauth/presentation/OauthControllerTest.java @@ -1,63 +0,0 @@ -package com.mapbefine.mapbefine.oauth.presentation; - -import static com.mapbefine.mapbefine.oauth.domain.OauthServerType.KAKAO; -import static org.mockito.BDDMockito.given; - -import com.mapbefine.mapbefine.common.RestDocsIntegration; -import com.mapbefine.mapbefine.member.dto.response.MemberDetailResponse; -import com.mapbefine.mapbefine.oauth.application.OauthService; -import com.mapbefine.mapbefine.oauth.dto.LoginInfoResponse; -import java.time.LocalDateTime; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; - -class OauthControllerTest extends RestDocsIntegration { - - @MockBean - private OauthService oauthService; - - @Test - @DisplayName("kakao 소셜 로그인 Redirection Url 반환") - void redirection() throws Exception { - // given - given(oauthService.getAuthCodeRequestUrl(KAKAO)).willReturn( - "https://kauth.kakao.com/oauth/authorize?" - + "response_type=code" - + "&client_id={client_id}" - + "&redirect_uri={redirection_uri}" - + "&scope={scope}" - ); - - // when then - mockMvc.perform( - MockMvcRequestBuilders.get("/oauth/kakao") - ).andDo(restDocs.document()); - } - - @Test - @DisplayName("소셜 로그인 성공시 로그인한 회원 정보 반환") - void login() throws Exception { - // given - String code = "auth_code"; - LoginInfoResponse response = new LoginInfoResponse( - testAuthHeaderProvider.createResponseAccessTokenById(Long.MAX_VALUE), - new MemberDetailResponse( - Long.MAX_VALUE, - "모험가03fcb0d", - "yshert@naver.com", - "https://map-befine-official.github.io/favicon.png", - LocalDateTime.now() - ) - ); - given(oauthService.login(KAKAO, code)).willReturn(response); - - // when then - mockMvc.perform( - MockMvcRequestBuilders.get("/oauth/login/kakao") - .param("code", code) - ).andDo(restDocs.document()); - } - -} diff --git a/backend/src/test/java/com/mapbefine/mapbefine/topic/application/TopicCommandServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/topic/application/TopicCommandServiceTest.java index 813ba6e7..02794503 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/topic/application/TopicCommandServiceTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/topic/application/TopicCommandServiceTest.java @@ -73,7 +73,7 @@ void setUp() { @Test @DisplayName("비어있는 토픽을 생성할 수 있다.") - public void saveEmptyTopic_Success() { + void saveEmptyTopic_Success() { //given TopicCreateRequest request = TopicFixture.createPublicAndAllMembersCreateRequestWithPins( @@ -94,7 +94,7 @@ public void saveEmptyTopic_Success() { @Test @DisplayName("Guest는 비어있는 토픽을 생성할 수 없다.") - public void saveEmptyTopic_Fail() { + void saveEmptyTopic_Fail() { //given TopicCreateRequest request = TopicFixture.createPublicAndAllMembersCreateRequestWithPins( @@ -108,7 +108,7 @@ public void saveEmptyTopic_Fail() { @Test @DisplayName("핀을 통해 새로운 토픽을 생성할 수 있다.") - public void saveTopicWithPins_Success() { + void saveTopicWithPins_Success() { //given Topic topic = TopicFixture.createPublicAndAllMembersTopic(member); @@ -139,7 +139,7 @@ public void saveTopicWithPins_Success() { @Test @DisplayName("Guest는 핀을 통해 새로운 토픽을 생성할 수 없다.") - public void saveTopicWithPins_Fail1() { + void saveTopicWithPins_Fail1() { //given Topic publicAndAllMembersTopic = TopicFixture.createPublicAndAllMembersTopic(member); @@ -160,7 +160,7 @@ public void saveTopicWithPins_Fail1() { @Test @DisplayName("권한이 없는 핀을 통해 토픽을 생성할 수 없다.") - public void saveTopicWithPins_Fail2() { + void saveTopicWithPins_Fail2() { //given Member topicOwner = MemberFixture.create( "topicOwner", @@ -187,7 +187,7 @@ public void saveTopicWithPins_Fail2() { @Test @DisplayName("기존의 토픽들을 통해 새로운 토픽을 생성할 수 있다.") - public void merge_Success() { + void merge_Success() { //given Topic topic1 = TopicFixture.createPublicAndAllMembersTopic(member); Topic topic2 = TopicFixture.createPublicAndAllMembersTopic(member); @@ -224,7 +224,7 @@ public void merge_Success() { @Test @DisplayName("Guest는 기존의 토픽들을 통해 새로운 토픽을 생성할 수 없다.") - public void merge_Fail1() { + void merge_Fail1() { //given Topic privateAndGroupOnlyTopic = TopicFixture.createPrivateAndGroupOnlyTopic(member); topicRepository.save(privateAndGroupOnlyTopic); @@ -241,7 +241,7 @@ public void merge_Fail1() { @Test @DisplayName("권한이 없는 토픽들을 통해 새로운 토픽을 생성할 수 없다.") - public void merge_Fail2() { + void merge_Fail2() { //given Member topicOwner = MemberFixture.create( "topicOwner", @@ -297,7 +297,7 @@ void copyPin_Success() { @Test @DisplayName("토픽의 정보를 수정할 수 있다.") - public void updateTopicInfo_Success() { + void updateTopicInfo_Success() { //given Topic topic = TopicFixture.createPrivateAndGroupOnlyTopic(member); topicRepository.save(topic); @@ -326,7 +326,7 @@ public void updateTopicInfo_Success() { @Test @DisplayName("권한이 없는 토픽의 정보를 수정할 수 없다.") - public void updateTopicInfo_Fail() { + void updateTopicInfo_Fail() { //given Member topicOwner = MemberFixture.create( "topicOwner", @@ -353,7 +353,7 @@ public void updateTopicInfo_Fail() { @Test @DisplayName("Admin은 토픽을 삭제할 수 있다.") - public void delete_Success() { + void delete_Success() { //given Member admin = MemberFixture.create( "topicOwner", @@ -380,7 +380,7 @@ public void delete_Success() { @Test @DisplayName("Admin이 아닌 경우, 토픽을 삭제할 수 없다.") - public void delete_Fail() { + void delete_Fail() { //given Topic topic = TopicFixture.createPublicAndAllMembersTopic(member); topicRepository.save(topic); diff --git a/backend/src/test/java/com/mapbefine/mapbefine/topic/domain/TopicStatusTest.java b/backend/src/test/java/com/mapbefine/mapbefine/topic/domain/TopicStatusTest.java index bde0c806..6b31aba7 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/topic/domain/TopicStatusTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/topic/domain/TopicStatusTest.java @@ -76,7 +76,7 @@ void whenPublicAndAllMembers_thenSuccess() { @Test @DisplayName("공개 범위가 null인 경우, 예외가 발생한다.") - public void whenPublicityIsNull_Fail() { + void whenPublicityIsNull_Fail() { //given Publicity publicity = null; PermissionType permissionType = PermissionType.ALL_MEMBERS; @@ -88,7 +88,7 @@ public void whenPublicityIsNull_Fail() { @Test @DisplayName("권한 설정이 null인 경우, 예외가 발생한다.") - public void whenPermissionIsNull_Fail() { + void whenPermissionIsNull_Fail() { //given Publicity publicity = Publicity.PUBLIC; PermissionType permissionType = null; diff --git a/backend/src/test/resources/application.yml b/backend/src/test/resources/application.yml index 283d08bc..4fd6070f 100644 --- a/backend/src/test/resources/application.yml +++ b/backend/src/test/resources/application.yml @@ -38,4 +38,5 @@ security: jwt: token: secret-key: ${TEST_JWT_SECRET_KEY} - expire-length: ${TEST_JWT_EXPIRE_LENGTH} + access-expire-length: ${TEST_JWT_ACCESS_EXPIRE_LENGTH} + refresh-expire-length: ${TEST_JWT_REFRESH_EXPIRE_LENGTH} From 5c52631c6b1aef7febdd1a59cbcd89f510612237 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A4=80=ED=8C=8D=28junpak=29?= <112045553+junpakPark@users.noreply.github.com> Date: Sun, 17 Sep 2023 18:05:50 +0900 Subject: [PATCH 20/34] [BE] Fix/#424 refresh token duplicated (#425) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: RefreshToken Payload 추가 및 CORS 완화 * fix: Refresh Token Header 허용 --- .../mapbefine/auth/infrastructure/JwtTokenProvider.java | 7 ++++--- .../com/mapbefine/mapbefine/common/config/WebConfig.java | 1 - 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/infrastructure/JwtTokenProvider.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/infrastructure/JwtTokenProvider.java index 2e11a2eb..f3ec2a3e 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/auth/infrastructure/JwtTokenProvider.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/infrastructure/JwtTokenProvider.java @@ -11,14 +11,13 @@ import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import java.util.Date; +import java.util.UUID; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @Component public class JwtTokenProvider implements TokenProvider { - private static final String EMPTY = ""; - private final String secretKey; private final long accessExpirationTime; private final long refreshExpirationTime; @@ -41,7 +40,9 @@ public String createAccessToken(String payload) { } public String createRefreshToken() { - return createToken(EMPTY, refreshExpirationTime); + UUID payload = UUID.randomUUID(); + + return createToken(payload.toString(), refreshExpirationTime); } private String createToken(String payload, Long validityInMilliseconds) { diff --git a/backend/src/main/java/com/mapbefine/mapbefine/common/config/WebConfig.java b/backend/src/main/java/com/mapbefine/mapbefine/common/config/WebConfig.java index 2af70d48..a6629c6a 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/common/config/WebConfig.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/common/config/WebConfig.java @@ -18,7 +18,6 @@ public void addCorsMappings(CorsRegistry registry) { .allowedOrigins("http://localhost:3000", "https://mapbefine.kro.kr", "https://mapbefine.com") .allowedHeaders("refresh-token") .allowedMethods("*") - .allowCredentials(true) .exposedHeaders(LOCATION, SET_COOKIE); } From f5e441b0b72b96e1c18a6ec8e0abe0fcc3240256 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A4=80=ED=8C=8D=28junpak=29?= <112045553+junpakPark@users.noreply.github.com> Date: Mon, 18 Sep 2023 13:17:10 +0900 Subject: [PATCH 21/34] =?UTF-8?q?[BE]=20Fix/#426=20Token=20CORS=20?= =?UTF-8?q?=EC=9E=AC=EC=84=A4=EC=A0=95=20(#427)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: RefreshToken Payload 추가 및 CORS 완화 * fix: Refresh Token Header 허용 * fix: CORS 재설정 및 sameSite None --- .../mapbefine/mapbefine/auth/presentation/LoginController.java | 2 +- .../java/com/mapbefine/mapbefine/common/config/WebConfig.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/presentation/LoginController.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/presentation/LoginController.java index b7ca5d0b..58a608e4 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/auth/presentation/LoginController.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/presentation/LoginController.java @@ -64,7 +64,7 @@ private ResponseCookie createCookie(String refreshToken) { return ResponseCookie.from("refresh-token", refreshToken) .httpOnly(true) .maxAge(TWO_WEEKS) - .sameSite("Lax") + .sameSite("None") .secure(true) .path("/") .build(); diff --git a/backend/src/main/java/com/mapbefine/mapbefine/common/config/WebConfig.java b/backend/src/main/java/com/mapbefine/mapbefine/common/config/WebConfig.java index a6629c6a..7b5fafe5 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/common/config/WebConfig.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/common/config/WebConfig.java @@ -1,5 +1,6 @@ package com.mapbefine.mapbefine.common.config; +import static org.springframework.http.HttpHeaders.COOKIE; import static org.springframework.http.HttpHeaders.LOCATION; import static org.springframework.http.HttpHeaders.SET_COOKIE; @@ -16,7 +17,7 @@ public class WebConfig implements WebMvcConfigurer { public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("http://localhost:3000", "https://mapbefine.kro.kr", "https://mapbefine.com") - .allowedHeaders("refresh-token") + .allowedHeaders(COOKIE) .allowedMethods("*") .exposedHeaders(LOCATION, SET_COOKIE); } From 3d54c201ab89c6dedb873c63fc6fcda471b72931 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A4=80=ED=8C=8D=28junpak=29?= <112045553+junpakPark@users.noreply.github.com> Date: Mon, 18 Sep 2023 19:46:10 +0900 Subject: [PATCH 22/34] =?UTF-8?q?[BE]=20HotFix/#426=20Refresh=20Token=20?= =?UTF-8?q?=EC=A4=91=EB=B3=B5=20=EC=A0=80=EC=9E=A5=20=EB=B0=A9=EC=A7=80=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95=20(#431)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: RefreshToken Payload 추가 및 CORS 완화 * fix: Refresh Token Header 허용 * fix: CORS 재설정 및 sameSite None * fix: refreshToken 존재 시 삭제 로직 변경 --- .../mapbefine/mapbefine/auth/application/TokenService.java | 6 +++--- .../mapbefine/auth/domain/token/RefreshTokenRepository.java | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/application/TokenService.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/application/TokenService.java index 3344a6f9..0fb0bd0b 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/auth/application/TokenService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/application/TokenService.java @@ -26,8 +26,9 @@ public LoginTokens issueTokens(Long memberId) { String accessToken = tokenProvider.createAccessToken(String.valueOf(memberId)); String refreshToken = tokenProvider.createRefreshToken(); - refreshTokenRepository.findByMemberId(memberId) - .ifPresent(refreshTokenRepository::delete); + if (refreshTokenRepository.existsByMemberId(memberId)) { + refreshTokenRepository.deleteByMemberId(memberId); + } refreshTokenRepository.save(new RefreshToken(refreshToken, memberId)); @@ -58,5 +59,4 @@ public void removeRefreshToken(String refreshToken, String accessToken) { } - } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/token/RefreshTokenRepository.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/token/RefreshTokenRepository.java index 8fab5056..75838b30 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/token/RefreshTokenRepository.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/token/RefreshTokenRepository.java @@ -1,11 +1,10 @@ package com.mapbefine.mapbefine.auth.domain.token; -import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; public interface RefreshTokenRepository extends JpaRepository { - Optional findByMemberId(Long memberId); + boolean existsByMemberId(Long memberId); void deleteByMemberId(Long memberId); From 74178aa04c349ac3f3fd0cf25355de416d0f5152 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A4=80=ED=8C=8D=28junpak=29?= <112045553+junpakPark@users.noreply.github.com> Date: Mon, 18 Sep 2023 20:11:11 +0900 Subject: [PATCH 23/34] =?UTF-8?q?[BE]=20HotFix/#426=20delete=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=EC=97=90=20clearAutomatically=20=EC=86=8D?= =?UTF-8?q?=EC=84=B1=20=EC=A0=81=EC=9A=A9=20(#432)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: RefreshToken Payload 추가 및 CORS 완화 * fix: Refresh Token Header 허용 * fix: CORS 재설정 및 sameSite None * fix: refreshToken 존재 시 삭제 로직 변경 * fix: delete 메서드에 clearAutomatically 속성 적용 --- .../mapbefine/auth/domain/token/RefreshTokenRepository.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/token/RefreshTokenRepository.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/token/RefreshTokenRepository.java index 75838b30..f9c0698d 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/token/RefreshTokenRepository.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/token/RefreshTokenRepository.java @@ -1,6 +1,7 @@ package com.mapbefine.mapbefine.auth.domain.token; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; public interface RefreshTokenRepository extends JpaRepository { @@ -8,4 +9,7 @@ public interface RefreshTokenRepository extends JpaRepository Date: Mon, 18 Sep 2023 20:41:55 +0900 Subject: [PATCH 24/34] =?UTF-8?q?[BE]=20HotFix/#426=20tokenService=20flush?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20(#433)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: RefreshToken Payload 추가 및 CORS 완화 * fix: Refresh Token Header 허용 * fix: CORS 재설정 및 sameSite None * fix: refreshToken 존재 시 삭제 로직 변경 * fix: delete 메서드에 clearAutomatically 속성 적용 * fix: delete 메서드에 clearAutomatically 속성 제거 및 flush 추가 --- .../com/mapbefine/mapbefine/auth/application/TokenService.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/application/TokenService.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/application/TokenService.java index 0fb0bd0b..6c65921e 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/auth/application/TokenService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/application/TokenService.java @@ -30,6 +30,8 @@ public LoginTokens issueTokens(Long memberId) { refreshTokenRepository.deleteByMemberId(memberId); } + refreshTokenRepository.flush(); + refreshTokenRepository.save(new RefreshToken(refreshToken, memberId)); return new LoginTokens(accessToken, refreshToken); From 2743fabd1b8ada7ad42fadce0e2a36ef1fc9ff49 Mon Sep 17 00:00:00 2001 From: Doy Date: Tue, 19 Sep 2023 15:20:07 +0900 Subject: [PATCH 25/34] =?UTF-8?q?[BE]=20Refactor/#400=20=ED=86=A0=ED=94=BD?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=20=EC=8B=9C=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=20=EC=9D=BC=EC=8B=9C=EB=A5=BC=20=EC=B5=9C=EA=B7=BC?= =?UTF-8?q?=EC=97=90=20=ED=95=80=EC=9D=B4=20=EC=B6=94=EA=B0=80/=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=EB=90=9C=20=EC=9D=BC=EC=8B=9C=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20(#429)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: BaseEntity의 createdAt update 방지 * feat: Topic에 lastPinUpdatedAt 컬럼 추가, EntityListner 적용 - 기존 BaseEntity의 값들은 객체가 영속화될 때 저장된다. - 이에 대해 일관성을 유지해야 한다. (핀 생성 일시, 핀 변경 일시 = 토픽의 최근 핀 변경 일시가 서로 같아야 하므로) - 따라서 lastPinUpdatedAt 컬럼의 업데이트 또한 EntityListener 로 적용한다. * feat: 토픽 조회 DTO의 updatedAt 값 lastPinUpdatedAt 으로 변경 * feat: 토픽 최신순 조회 로직 수정 - Topic에 lastPinUpdatedAt 추가로 인해 로직 수정 가능 * test: 토픽 조회 시 updatedAt 검증 테스트 추가 * chore: 로컬 테스트용 SQL에 테이블 컬럼 추가 변경 반영 * refactor: 토픽 Response Dto에 lastPinUpdatedAt 반영 * fix : 토큰 만료시간 및 redirect uri 수정 --------- Co-authored-by: jaeyeon kim --- .../common/entity/BaseTimeEntity.java | 2 + .../mapbefine/mapbefine/pin/domain/Pin.java | 12 ++++++ .../mapbefine/pin/domain/PinRepository.java | 1 - .../topic/application/TopicQueryService.java | 13 +----- .../mapbefine/topic/domain/Topic.java | 14 ++++++ .../topic/domain/TopicRepository.java | 10 +++-- .../dto/response/TopicDetailResponse.java | 4 +- .../topic/dto/response/TopicResponse.java | 4 +- backend/src/main/resources/config | 2 +- backend/src/main/resources/data-default.sql | 10 ++--- .../common/annotation/ServiceTest.java | 3 ++ .../application/PinCommandServiceTest.java | 43 +++++++++++++++++-- .../mapbefine/topic/TopicIntegrationTest.java | 3 ++ .../application/TopicQueryServiceTest.java | 20 +++++++++ .../mapbefine/topic/domain/TopicTest.java | 19 +++++++- 15 files changed, 129 insertions(+), 31 deletions(-) diff --git a/backend/src/main/java/com/mapbefine/mapbefine/common/entity/BaseTimeEntity.java b/backend/src/main/java/com/mapbefine/mapbefine/common/entity/BaseTimeEntity.java index f8a145e2..ad3ad481 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/common/entity/BaseTimeEntity.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/common/entity/BaseTimeEntity.java @@ -2,6 +2,7 @@ import static lombok.AccessLevel.PROTECTED; +import jakarta.persistence.Column; import jakarta.persistence.EntityListeners; import jakarta.persistence.MappedSuperclass; import java.time.LocalDateTime; @@ -18,6 +19,7 @@ public abstract class BaseTimeEntity { @CreatedDate + @Column(updatable = false) private LocalDateTime createdAt; @LastModifiedDate private LocalDateTime updatedAt; diff --git a/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/Pin.java b/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/Pin.java index 194bcaf1..8081b913 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/Pin.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/Pin.java @@ -17,6 +17,8 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; +import jakarta.persistence.PrePersist; +import jakarta.persistence.PreUpdate; import java.util.ArrayList; import java.util.List; import lombok.Getter; @@ -86,6 +88,16 @@ public static Pin createPinAssociatedWithLocationAndTopicAndMember( return pin; } + @PrePersist + protected void prePersist() { + topic.updateLastPinUpdatedAt(getUpdatedAt()); + } + + @PreUpdate + protected void preUpdate() { + topic.updateLastPinUpdatedAt(getUpdatedAt()); + } + public void updatePinInfo(String name, String description) { pinInfo = PinInfo.of(name, description); } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinRepository.java b/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinRepository.java index 60d29dc9..20ac5a4a 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinRepository.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinRepository.java @@ -26,5 +26,4 @@ public interface PinRepository extends JpaRepository { List findAllByCreatorId(Long creatorId); - List findAllByOrderByUpdatedAtDesc(); } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/application/TopicQueryService.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/application/TopicQueryService.java index de0efdda..5f217e71 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/topic/application/TopicQueryService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/application/TopicQueryService.java @@ -8,8 +8,6 @@ import com.mapbefine.mapbefine.bookmark.domain.Bookmark; import com.mapbefine.mapbefine.member.domain.Member; import com.mapbefine.mapbefine.member.domain.MemberRepository; -import com.mapbefine.mapbefine.pin.domain.Pin; -import com.mapbefine.mapbefine.pin.domain.PinRepository; import com.mapbefine.mapbefine.topic.domain.Topic; import com.mapbefine.mapbefine.topic.domain.TopicRepository; import com.mapbefine.mapbefine.topic.dto.response.TopicDetailResponse; @@ -28,16 +26,13 @@ public class TopicQueryService { private final TopicRepository topicRepository; - private final PinRepository pinRepository; private final MemberRepository memberRepository; public TopicQueryService( TopicRepository topicRepository, - PinRepository pinRepository, MemberRepository memberRepository ) { this.topicRepository = topicRepository; - this.pinRepository = pinRepository; this.memberRepository = memberRepository; } @@ -223,10 +218,8 @@ private List getUserNewestTopicResponse(AuthMember authMember) { List topicsInAtlas = findTopicsInAtlas(member); List topicsInBookMark = findBookMarkedTopics(member); - return pinRepository.findAllByOrderByUpdatedAtDesc() + return topicRepository.findAllByOrderByLastPinUpdatedAtDesc() .stream() - .map(Pin::getTopic) - .distinct() .filter(authMember::canRead) .map(topic -> TopicResponse.from( topic, @@ -237,10 +230,8 @@ private List getUserNewestTopicResponse(AuthMember authMember) { } private List getGuestNewestTopicResponse(AuthMember authMember) { - return pinRepository.findAllByOrderByUpdatedAtDesc() + return topicRepository.findAllByOrderByLastPinUpdatedAtDesc() .stream() - .map(Pin::getTopic) - .distinct() .filter(authMember::canRead) .map(TopicResponse::fromGuestQuery) .toList(); diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/Topic.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/Topic.java index a407bcdb..3483e21c 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/Topic.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/Topic.java @@ -17,6 +17,8 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; +import jakarta.persistence.PrePersist; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import lombok.Getter; @@ -55,6 +57,9 @@ public class Topic extends BaseTimeEntity { @ColumnDefault(value = "false") private boolean isDeleted = false; + @Column(nullable = false) + private LocalDateTime lastPinUpdatedAt; + private Topic( TopicInfo topicInfo, TopicStatus topicStatus, @@ -65,6 +70,11 @@ private Topic( this.creator = creator; } + @PrePersist + protected void prePersist() { + lastPinUpdatedAt = LocalDateTime.now(); + } + public static Topic createTopicAssociatedWithCreator( String name, String description, @@ -90,6 +100,10 @@ public void updateTopicInfo( this.topicInfo = TopicInfo.of(name, description, imageUrl); } + public void updateLastPinUpdatedAt(LocalDateTime lastPinUpdatedAt) { + this.lastPinUpdatedAt = lastPinUpdatedAt; + } + public void updateTopicStatus(Publicity publicity, PermissionType permissionType) { topicStatus.update(publicity, permissionType); } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicRepository.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicRepository.java index e6eb95db..22fe220a 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicRepository.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicRepository.java @@ -12,6 +12,12 @@ public interface TopicRepository extends JpaRepository { List findByIdIn(List ids); + boolean existsById(Long id); + + List findAllByOrderByLastPinUpdatedAtDesc(); + + List findAllByCreatorId(Long creatorId); + @Modifying(clearAutomatically = true) @Query("update Topic t set t.isDeleted = true where t.id = :topicId") void deleteById(@Param("topicId") Long topicId); @@ -20,8 +26,4 @@ public interface TopicRepository extends JpaRepository { @Query("update Topic t set t.isDeleted = true where t.creator.id = :memberId") void deleteAllByMemberId(@Param("memberId") Long memberId); - boolean existsById(Long id); - - List findAllByCreatorId(Long creatorId); - } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/dto/response/TopicDetailResponse.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/dto/response/TopicDetailResponse.java index f46f6dbf..23fefa8a 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/topic/dto/response/TopicDetailResponse.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/dto/response/TopicDetailResponse.java @@ -44,7 +44,7 @@ public static TopicDetailResponse of( topic.countBookmarks(), isBookmarked, canUpdate, - topic.getUpdatedAt(), + topic.getLastPinUpdatedAt(), pinResponses ); } @@ -67,7 +67,7 @@ public static TopicDetailResponse fromGuestQuery(Topic topic) { topic.countBookmarks(), Boolean.FALSE, Boolean.FALSE, - topic.getUpdatedAt(), + topic.getLastPinUpdatedAt(), pinResponses ); } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/dto/response/TopicResponse.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/dto/response/TopicResponse.java index 9cb31309..bd757a43 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/topic/dto/response/TopicResponse.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/dto/response/TopicResponse.java @@ -28,7 +28,7 @@ public static TopicResponse from(Topic topic, Boolean isInAtlas, Boolean isBookm isInAtlas, topic.countBookmarks(), isBookmarked, - topic.getUpdatedAt() + topic.getLastPinUpdatedAt() ); } @@ -44,7 +44,7 @@ public static TopicResponse fromGuestQuery(Topic topic) { Boolean.FALSE, topic.countBookmarks(), Boolean.FALSE, - topic.getUpdatedAt() + topic.getLastPinUpdatedAt() ); } diff --git a/backend/src/main/resources/config b/backend/src/main/resources/config index 5a3930cf..31d9b5a2 160000 --- a/backend/src/main/resources/config +++ b/backend/src/main/resources/config @@ -1 +1 @@ -Subproject commit 5a3930cfc4da1807ecd70125a34703e2696e19e9 +Subproject commit 31d9b5a25e02428dafa205e9d00d99ef7fd9b6a5 diff --git a/backend/src/main/resources/data-default.sql b/backend/src/main/resources/data-default.sql index 75f9969c..8d6887eb 100644 --- a/backend/src/main/resources/data-default.sql +++ b/backend/src/main/resources/data-default.sql @@ -1,16 +1,16 @@ -INSERT INTO member (nick_name, email, image_url, role, +INSERT INTO member (nick_name, email, image_url, role, status, oauth_server_id, oauth_server_type, created_at, updated_at) -VALUES ('dummyMember', 'dummy@gmail.com', 'https://map-befine-official.github.io/favicon.png', 'USER', +VALUES ('dummyMember', 'dummy@gmail.com', 'https://map-befine-official.github.io/favicon.png', 'USER', 'NORMAL', 1L, 'KAKAO', now(), now()); -INSERT INTO topic (name, image_url, description, +INSERT INTO topic (name, image_url, description,가 permission_type, publicity, member_id, - created_at, updated_at) + created_at, updated_at, last_pin_updated_at) VALUES ('dummyTopic', 'https://map-befine-official.github.io/favicon.png', 'description', 'ALL_MEMBERS', 'PUBLIC', 1L, - now(), now()) + now(), now(), now()) ; diff --git a/backend/src/test/java/com/mapbefine/mapbefine/common/annotation/ServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/common/annotation/ServiceTest.java index f8fa5c7c..7993e891 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/common/annotation/ServiceTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/common/annotation/ServiceTest.java @@ -1,5 +1,6 @@ package com.mapbefine.mapbefine.common.annotation; +import com.mapbefine.mapbefine.common.config.JpaConfig; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -7,10 +8,12 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.context.annotation.FilterType; +import org.springframework.context.annotation.Import; import org.springframework.stereotype.Service; @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) +@Import(JpaConfig.class) @DataJpaTest( includeFilters = { @Filter(type = FilterType.ANNOTATION, value = Service.class), diff --git a/backend/src/test/java/com/mapbefine/mapbefine/pin/application/PinCommandServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/pin/application/PinCommandServiceTest.java index 7bd8ca55..22ea4686 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/pin/application/PinCommandServiceTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/pin/application/PinCommandServiceTest.java @@ -133,6 +133,22 @@ void saveIfNotExistLocation_Success() { .containsExactly(location.getId(), savedLocation.getId()); } + @Test + @DisplayName("핀을 추가하면 토픽에 핀의 변경 일시를 새로 반영한다. (모든 일시는 영속화 시점 기준이다.)") + void save_Success_UpdateLastPinAddedAt() { + // given when + long pinId = pinCommandService.save(authMember, List.of(BASE_IMAGE_FILE), createRequest); + Pin pin = pinRepository.findById(pinId) + .orElseGet(Assertions::fail); + + // then + topicRepository.findById(createRequest.topicId()) + .ifPresentOrElse( + topic -> assertThat(topic.getLastPinUpdatedAt()).isEqualTo(pin.getCreatedAt()), + Assertions::fail + ); + } + @Test @DisplayName("권한이 없는 토픽에 핀을 저장하면 예외를 발생시킨다.") void save_FailByForbidden() { @@ -140,15 +156,34 @@ void save_FailByForbidden() { .isInstanceOf(PinForbiddenException.class); } + + @Test + @DisplayName("핀을 변경하면 토픽에 핀의 변경 일시를 새로 반영한다. (모든 일시는 영속화 시점 기준이다.)") + void update_Success_UpdateLastPinsAddedAt() { + // given + long pinId = pinCommandService.save(authMember, List.of(BASE_IMAGE_FILE), createRequest); + + // when + pinCommandService.update(authMember, pinId, new PinUpdateRequest("name", "update")); + Pin pin = pinRepository.findById(pinId) + .orElseGet(Assertions::fail); + pinRepository.flush(); + + // then + topicRepository.findById(createRequest.topicId()) + .ifPresentOrElse( + topic -> assertThat(topic.getLastPinUpdatedAt()).isEqualTo(pin.getUpdatedAt()), + Assertions::fail + ); + } + @Test @DisplayName("권한이 없는 토픽에 핀을 수정하면 예외를 발생시킨다.") void update_FailByForbidden() { long pinId = pinCommandService.save(authMember, List.of(BASE_IMAGE_FILE), createRequest); - assertThatThrownBy(() -> pinCommandService.update( - new Guest(), pinId, new PinUpdateRequest("name", "description")) - ).isInstanceOf(PinForbiddenException.class); - + assertThatThrownBy(() -> pinCommandService.update(new Guest(), pinId, new PinUpdateRequest("name", "update"))) + .isInstanceOf(PinForbiddenException.class); } @Test diff --git a/backend/src/test/java/com/mapbefine/mapbefine/topic/TopicIntegrationTest.java b/backend/src/test/java/com/mapbefine/mapbefine/topic/TopicIntegrationTest.java index 6d648853..d4115af6 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/topic/TopicIntegrationTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/topic/TopicIntegrationTest.java @@ -367,6 +367,8 @@ void findAllBestTopics_Success() { @Test @DisplayName("핀이 추가/수정된 일자 기준 내림차순으로 토픽 목록을 조회할 경우, 200을 반환한다") void findAllByOrderByUpdatedAtDesc_Success() { + topicRepository.deleteAll(); + // given Topic topic1 = topicRepository.save(TopicFixture.createByName("topic1", member)); Topic topic2 = topicRepository.save(TopicFixture.createByName("topic2", member)); @@ -392,6 +394,7 @@ void findAllByOrderByUpdatedAtDesc_Success() { .then().log().all() .extract(); + // then List topicResponses = response.jsonPath().getList(".", TopicResponse.class); assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); assertThat(topicResponses).hasSize(3); diff --git a/backend/src/test/java/com/mapbefine/mapbefine/topic/application/TopicQueryServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/topic/application/TopicQueryServiceTest.java index 5196a7f6..00374715 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/topic/application/TopicQueryServiceTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/topic/application/TopicQueryServiceTest.java @@ -134,6 +134,26 @@ void findDetailById_Success() { assertThat(detail.name()).isEqualTo("아무나 읽을 수 있는 토픽"); } + @Test + @DisplayName("토픽 상세 조회 시 토픽의 변경일자는 핀의 최신 변경 일자이다.") + void findDetailById_Success_lastPinUpdatedAt() { + //given + Topic topic = TopicFixture.createPublicAndAllMembersTopic(member); + Location location = LocationFixture.create(); + Pin pin = PinFixture.create(location, topic, member); + locationRepository.save(location); + topicRepository.save(topic); + pinRepository.save(pin); + + //when + pin.updatePinInfo("updatePin", "updatedAt will be update"); + pinRepository.flush(); + TopicDetailResponse response = topicQueryService.findDetailById(new Admin(member.getId()), topic.getId()); + + //then + assertThat(response.updatedAt()).isEqualTo(pin.getUpdatedAt()); + } + @Test @DisplayName("권한이 없는 토픽을 ID로 조회하면, 예외가 발생한다.") void findDetailById_Fail() { diff --git a/backend/src/test/java/com/mapbefine/mapbefine/topic/domain/TopicTest.java b/backend/src/test/java/com/mapbefine/mapbefine/topic/domain/TopicTest.java index 0d5b02a4..3d98d2dc 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/topic/domain/TopicTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/topic/domain/TopicTest.java @@ -8,7 +8,9 @@ import com.mapbefine.mapbefine.member.domain.Role; import com.mapbefine.mapbefine.pin.PinFixture; import com.mapbefine.mapbefine.pin.domain.Pin; +import java.time.LocalDateTime; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; class TopicTest { @@ -33,6 +35,7 @@ void setUp() { } @Test + @DisplayName("토픽 정보를 변경한다.") void updateTopicInfo() { //given String name = "New Topic"; @@ -54,6 +57,7 @@ void updateTopicInfo() { } @Test + @DisplayName("토픽 상태를 변경한다.") void updateTopicStatus() { //given Publicity publicity = Publicity.PRIVATE; @@ -72,6 +76,7 @@ void updateTopicStatus() { } @Test + @DisplayName("토픽의 핀 개수를 알 수 있다.") void countPins() { //given topic.addPin(pin); @@ -81,15 +86,27 @@ void countPins() { //then assertThat(pinCounts).isEqualTo(2); - // 핀이 처음 생성됐을 때, 연관관계 매핑으로 인해 하나 추가된 후 해당 메서드에서 하나 더 추가됌 } @Test + @DisplayName("토픽에 직접 핀을 추가해도, 조회 전용이므로 핀 변경 일시는 반영되지 않는다.") void addPin() { //when + LocalDateTime beforeAdding = topic.getLastPinUpdatedAt(); topic.addPin(pin); //then assertThat(topic.getPins()).contains(pin); + assertThat(topic.getLastPinUpdatedAt()).isEqualTo(beforeAdding); + } + + @Test + @DisplayName("핀 없이 생성된 토픽의 초기 핀 추가 일시는 토픽 변경 일시와 같다.") + void create() { + // when + Topic emptyPinsTopic = new Topic(); + + //then + assertThat(emptyPinsTopic.getCreatedAt()).isEqualTo(emptyPinsTopic.getLastPinUpdatedAt()); } } From d355eb6efbaf3442c70ddffc9ef5aaebb5915227 Mon Sep 17 00:00:00 2001 From: zun <50602742+cpot5620@users.noreply.github.com> Date: Tue, 19 Sep 2023 16:58:48 +0900 Subject: [PATCH 26/34] =?UTF-8?q?[BE]=20Feature/#422=20=EC=84=B1=EB=8A=A5?= =?UTF-8?q?=20=EC=B8=A1=EC=A0=95=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EB=A1=9C?= =?UTF-8?q?=EA=B9=85=20=EA=B5=AC=ED=98=84=20(#434)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: QueryCounter 객체 구현 * feat: QueryInspector 객체 구현 * feat: LatencyRecorder 객체 구현 * feat: LatencyLoggingFilter 객체 구현 * feat: LatencyRecorder Thread-safe 테스트 구현 * feat: HibernateConfig 구현 * test: 테스트 수정 * style: 개행 추가 * refactor: 수식 표현 방식 수정 --- .../common/config/HibernateConfig.java | 24 ++++++++++ .../common/filter/LatencyLoggingFilter.java | 45 +++++++++++++++++ .../common/filter/LatencyRecorder.java | 23 +++++++++ .../mapbefine/common/filter/QueryCounter.java | 20 ++++++++ .../common/filter/QueryInspector.java | 33 +++++++++++++ .../common/filter/LatencyRecorderTest.java | 48 +++++++++++++++++++ .../common/filter/QueryCounterTest.java | 24 ++++++++++ 7 files changed, 217 insertions(+) create mode 100644 backend/src/main/java/com/mapbefine/mapbefine/common/config/HibernateConfig.java create mode 100644 backend/src/main/java/com/mapbefine/mapbefine/common/filter/LatencyLoggingFilter.java create mode 100644 backend/src/main/java/com/mapbefine/mapbefine/common/filter/LatencyRecorder.java create mode 100644 backend/src/main/java/com/mapbefine/mapbefine/common/filter/QueryCounter.java create mode 100644 backend/src/main/java/com/mapbefine/mapbefine/common/filter/QueryInspector.java create mode 100644 backend/src/test/java/com/mapbefine/mapbefine/common/filter/LatencyRecorderTest.java create mode 100644 backend/src/test/java/com/mapbefine/mapbefine/common/filter/QueryCounterTest.java diff --git a/backend/src/main/java/com/mapbefine/mapbefine/common/config/HibernateConfig.java b/backend/src/main/java/com/mapbefine/mapbefine/common/config/HibernateConfig.java new file mode 100644 index 00000000..8bb66b0c --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/common/config/HibernateConfig.java @@ -0,0 +1,24 @@ +package com.mapbefine.mapbefine.common.config; + +import com.mapbefine.mapbefine.common.filter.QueryInspector; +import org.hibernate.cfg.AvailableSettings; +import org.springframework.boot.autoconfigure.orm.jpa.HibernatePropertiesCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class HibernateConfig { + + private final QueryInspector queryInspector; + + public HibernateConfig(QueryInspector queryInspector) { + this.queryInspector = queryInspector; + } + + @Bean + public HibernatePropertiesCustomizer hibernatePropertiesCustomizer() { + return hibernateProperties -> + hibernateProperties.put(AvailableSettings.STATEMENT_INSPECTOR, queryInspector); + } + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/common/filter/LatencyLoggingFilter.java b/backend/src/main/java/com/mapbefine/mapbefine/common/filter/LatencyLoggingFilter.java new file mode 100644 index 00000000..48fe8fb0 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/common/filter/LatencyLoggingFilter.java @@ -0,0 +1,45 @@ +package com.mapbefine.mapbefine.common.filter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +@Component +public class LatencyLoggingFilter extends OncePerRequestFilter { + + private static final Logger log = LoggerFactory.getLogger(LatencyLoggingFilter.class); + + private final LatencyRecorder latencyRecorder; + private final QueryInspector queryInspector; + + public LatencyLoggingFilter(LatencyRecorder latencyRecorder, QueryInspector queryInspector) { + this.latencyRecorder = latencyRecorder; + this.queryInspector = queryInspector; + } + + @Override + protected void doFilterInternal( + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain + ) throws ServletException, IOException { + latencyRecorder.start(); + + filterChain.doFilter(request, response); + + double latencyForSeconds = latencyRecorder.getLatencyForSeconds(); + int queryCount = queryInspector.getQueryCount(); + String requestURI = request.getRequestURI(); + + log.info("Latency : {}s, Query count : {}, Request URI : {}", latencyForSeconds, queryCount, requestURI); + MDC.clear(); + } + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/common/filter/LatencyRecorder.java b/backend/src/main/java/com/mapbefine/mapbefine/common/filter/LatencyRecorder.java new file mode 100644 index 00000000..204085ff --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/common/filter/LatencyRecorder.java @@ -0,0 +1,23 @@ +package com.mapbefine.mapbefine.common.filter; + +import org.springframework.stereotype.Component; + +@Component +public class LatencyRecorder { + + private final ThreadLocal threadLocal = new ThreadLocal<>(); + + public void start() { + threadLocal.set(System.currentTimeMillis()); + } + + public double getLatencyForSeconds() { + long start = threadLocal.get(); + long end = System.currentTimeMillis(); + + threadLocal.remove(); + + return (end - start) / 1000d; + } + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/common/filter/QueryCounter.java b/backend/src/main/java/com/mapbefine/mapbefine/common/filter/QueryCounter.java new file mode 100644 index 00000000..8ebdc614 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/common/filter/QueryCounter.java @@ -0,0 +1,20 @@ +package com.mapbefine.mapbefine.common.filter; + +import org.springframework.stereotype.Component; +import org.springframework.web.context.annotation.RequestScope; + +@Component +@RequestScope +public class QueryCounter { + + private int count = 0; + + public void increase() { + count++; + } + + public int getCount() { + return count; + } + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/common/filter/QueryInspector.java b/backend/src/main/java/com/mapbefine/mapbefine/common/filter/QueryInspector.java new file mode 100644 index 00000000..fc33ab29 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/common/filter/QueryInspector.java @@ -0,0 +1,33 @@ +package com.mapbefine.mapbefine.common.filter; + +import org.hibernate.resource.jdbc.spi.StatementInspector; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; + +@Component +public class QueryInspector implements StatementInspector { + + private final QueryCounter queryCounter; + + public QueryInspector(QueryCounter queryCounter) { + this.queryCounter = queryCounter; + } + + @Override + public String inspect(String sql) { + if (isInRequestScope()) { + queryCounter.increase(); + } + + return sql; + } + + private boolean isInRequestScope() { + return RequestContextHolder.getRequestAttributes() != null; + } + + public int getQueryCount() { + return queryCounter.getCount(); + } + +} diff --git a/backend/src/test/java/com/mapbefine/mapbefine/common/filter/LatencyRecorderTest.java b/backend/src/test/java/com/mapbefine/mapbefine/common/filter/LatencyRecorderTest.java new file mode 100644 index 00000000..3e19f933 --- /dev/null +++ b/backend/src/test/java/com/mapbefine/mapbefine/common/filter/LatencyRecorderTest.java @@ -0,0 +1,48 @@ +package com.mapbefine.mapbefine.common.filter; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class LatencyRecorderTest { + + private final LatencyRecorder latencyRecorder = new LatencyRecorder(); + + @Test + @DisplayName("실행 시간을 초단위로 측정할 수 있다.") + void getLatencyForSeconds() throws InterruptedException { + //given + latencyRecorder.start(); + + //5초 + Thread.sleep(5000); + + //when + double latencyForSeconds = latencyRecorder.getLatencyForSeconds(); + + //then + assertThat(latencyForSeconds).isGreaterThanOrEqualTo(5) + .isLessThanOrEqualTo(6); + } + + @Test + @DisplayName("다른 쓰레드에 영향을 받지 않는다.") + void getLatencyForSecondsThreadSafe() throws InterruptedException { + //given + Thread thread = new Thread(latencyRecorder::start); + thread.start(); + Thread.sleep(3000); + + latencyRecorder.start(); + Thread.sleep(3000); + + //when + double latencyForSeconds = latencyRecorder.getLatencyForSeconds(); + + //then + assertThat(latencyForSeconds).isGreaterThanOrEqualTo(3) + .isLessThanOrEqualTo(4); + } + +} diff --git a/backend/src/test/java/com/mapbefine/mapbefine/common/filter/QueryCounterTest.java b/backend/src/test/java/com/mapbefine/mapbefine/common/filter/QueryCounterTest.java new file mode 100644 index 00000000..073af887 --- /dev/null +++ b/backend/src/test/java/com/mapbefine/mapbefine/common/filter/QueryCounterTest.java @@ -0,0 +1,24 @@ +package com.mapbefine.mapbefine.common.filter; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class QueryCounterTest { + + @Test + @DisplayName("값을 증가시킬 수 있다.") + void increase_Success() { + //given + QueryCounter queryCounter = new QueryCounter(); + assertThat(queryCounter.getCount()).isZero(); + + //when + queryCounter.increase(); + + //then + assertThat(queryCounter.getCount()).isOne(); + } + +} From cf39b5ed4d70e60082d2dfd5cca12016e5672803 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A4=80=ED=8C=8D=28junpak=29?= <112045553+junpakPark@users.noreply.github.com> Date: Tue, 19 Sep 2023 20:32:28 +0900 Subject: [PATCH 27/34] [BE] HotFix/#424 refresh token duplicated (#441) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: RefreshToken Payload 추가 및 CORS 완화 * fix: Refresh Token Header 허용 * fix: CORS 재설정 및 sameSite None * fix: 디버깅을 위한 에러코드 추가 --- .../com/mapbefine/mapbefine/auth/exception/AuthErrorCode.java | 3 ++- .../mapbefine/auth/infrastructure/JwtTokenProvider.java | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/exception/AuthErrorCode.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/exception/AuthErrorCode.java index 50150b86..31eb8b91 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/auth/exception/AuthErrorCode.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/exception/AuthErrorCode.java @@ -8,7 +8,8 @@ public enum AuthErrorCode { ILLEGAL_TOKEN("01101", "로그인에 실패하였습니다."), FORBIDDEN_ADMIN_ACCESS("01102", "로그인에 실패하였습니다."), BLOCKING_MEMBER_ACCESS("01103", "로그인에 실패하였습니다."), - EXPIRED_TOKEN("01104", "기간이 만료된 토큰입니다.") + EXPIRED_TOKEN("01104", "기간이 만료된 토큰입니다."), + BAD_REQUEST_TOKEN("01005", "잘못된 요청입니다.") ; private final String code; diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/infrastructure/JwtTokenProvider.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/infrastructure/JwtTokenProvider.java index f3ec2a3e..01f64a96 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/auth/infrastructure/JwtTokenProvider.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/infrastructure/JwtTokenProvider.java @@ -1,5 +1,6 @@ package com.mapbefine.mapbefine.auth.infrastructure; +import static com.mapbefine.mapbefine.auth.exception.AuthErrorCode.BAD_REQUEST_TOKEN; import static com.mapbefine.mapbefine.auth.exception.AuthErrorCode.EXPIRED_TOKEN; import static com.mapbefine.mapbefine.auth.exception.AuthErrorCode.ILLEGAL_TOKEN; @@ -75,7 +76,7 @@ public void validateTokensForRemoval(String refreshToken, String accessToken) { if (canRemoveRefreshToken) { return; } - throw new AuthUnauthorizedException(EXPIRED_TOKEN); + throw new AuthUnauthorizedException(BAD_REQUEST_TOKEN); } public void validateAccessToken(String accessToken) { From 48c6a52d9be5a6ca9bb5c4607059b8d3a1e7a60b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A4=80=ED=8C=8D=28junpak=29?= <112045553+junpakPark@users.noreply.github.com> Date: Tue, 19 Sep 2023 20:43:39 +0900 Subject: [PATCH 28/34] =?UTF-8?q?[BE]=20HOTFix/#424=20validateTokensForRei?= =?UTF-8?q?ssue=20=EB=94=94=EB=B2=84=EA=B9=85=EC=9D=84=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20=EC=97=90=EB=9F=AC=EC=BD=94=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#443)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: RefreshToken Payload 추가 및 CORS 완화 * fix: Refresh Token Header 허용 * fix: CORS 재설정 및 sameSite None * fix: 디버깅을 위한 에러코드 추가 * fix: validateTokensForReissue 디버깅을 위한 에러코드 추가 --- .../mapbefine/auth/infrastructure/JwtTokenProvider.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/infrastructure/JwtTokenProvider.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/infrastructure/JwtTokenProvider.java index 01f64a96..36a5d1ad 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/auth/infrastructure/JwtTokenProvider.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/infrastructure/JwtTokenProvider.java @@ -68,7 +68,7 @@ public void validateTokensForReissue(String refreshToken, String accessToken) { if (canReissueAccessToken) { return; } - throw new AuthUnauthorizedException(ILLEGAL_TOKEN); + throw new AuthUnauthorizedException(BAD_REQUEST_TOKEN); } public void validateTokensForRemoval(String refreshToken, String accessToken) { From 445f0dd18dce3bc5799db7b4e67919853d9948c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A4=80=ED=8C=8D=28junpak=29?= <112045553+junpakPark@users.noreply.github.com> Date: Tue, 19 Sep 2023 21:08:35 +0900 Subject: [PATCH 29/34] =?UTF-8?q?fix:=20isExpired=20=EC=9E=84=EC=8B=9C=20l?= =?UTF-8?q?og=20=EC=B2=98=EB=A6=AC=20(#444)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mapbefine/auth/infrastructure/JwtTokenProvider.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/infrastructure/JwtTokenProvider.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/infrastructure/JwtTokenProvider.java index 36a5d1ad..062dbf37 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/auth/infrastructure/JwtTokenProvider.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/infrastructure/JwtTokenProvider.java @@ -13,9 +13,11 @@ import io.jsonwebtoken.SignatureAlgorithm; import java.util.Date; import java.util.UUID; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; +@Slf4j @Component public class JwtTokenProvider implements TokenProvider { @@ -94,6 +96,8 @@ private boolean isExpired(String token) { } catch (ExpiredJwtException e) { return true; } catch (JwtException | IllegalArgumentException e) { + log.warn("source = {}", e.getStackTrace()[0].toString()); + throw new AuthUnauthorizedException(ILLEGAL_TOKEN); } } From 21081b93cbfbbd419576724eb42f410d2dd42a3a Mon Sep 17 00:00:00 2001 From: junpakPark <112045553+junpakPark@users.noreply.github.com> Date: Tue, 19 Sep 2023 22:39:53 +0900 Subject: [PATCH 30/34] =?UTF-8?q?Revert=20"fix:=20isExpired=20=EC=9E=84?= =?UTF-8?q?=EC=8B=9C=20log=20=EC=B2=98=EB=A6=AC=20(#444)"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 445f0dd18dce3bc5799db7b4e67919853d9948c2. --- .../mapbefine/auth/infrastructure/JwtTokenProvider.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/infrastructure/JwtTokenProvider.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/infrastructure/JwtTokenProvider.java index 062dbf37..36a5d1ad 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/auth/infrastructure/JwtTokenProvider.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/infrastructure/JwtTokenProvider.java @@ -13,11 +13,9 @@ import io.jsonwebtoken.SignatureAlgorithm; import java.util.Date; import java.util.UUID; -import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; -@Slf4j @Component public class JwtTokenProvider implements TokenProvider { @@ -96,8 +94,6 @@ private boolean isExpired(String token) { } catch (ExpiredJwtException e) { return true; } catch (JwtException | IllegalArgumentException e) { - log.warn("source = {}", e.getStackTrace()[0].toString()); - throw new AuthUnauthorizedException(ILLEGAL_TOKEN); } } From 954199c938a8f5cd89ceca6fa895dc1b9d601545 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A4=80=ED=8C=8D=28junpak=29?= <112045553+junpakPark@users.noreply.github.com> Date: Wed, 20 Sep 2023 14:03:48 +0900 Subject: [PATCH 31/34] =?UTF-8?q?fix:=20cors=20Credentials=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#458)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/mapbefine/mapbefine/common/config/WebConfig.java | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/main/java/com/mapbefine/mapbefine/common/config/WebConfig.java b/backend/src/main/java/com/mapbefine/mapbefine/common/config/WebConfig.java index 7b5fafe5..6cfddd36 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/common/config/WebConfig.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/common/config/WebConfig.java @@ -19,6 +19,7 @@ public void addCorsMappings(CorsRegistry registry) { .allowedOrigins("http://localhost:3000", "https://mapbefine.kro.kr", "https://mapbefine.com") .allowedHeaders(COOKIE) .allowedMethods("*") + .allowCredentials(true) .exposedHeaders(LOCATION, SET_COOKIE); } From 3776882a4d9129505b392ce7af662efe5dd099df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A4=80=ED=8C=8D=28junpak=29?= <112045553+junpakPark@users.noreply.github.com> Date: Wed, 20 Sep 2023 15:23:41 +0900 Subject: [PATCH 32/34] =?UTF-8?q?[BE]=20Hotfix/cors=20allowHeaders=20?= =?UTF-8?q?=EC=99=80=EC=9D=BC=EB=93=9C=EC=B9=B4=EB=93=9C=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=20(#462)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: cors Credentials 추가 * fix: allowedHeaders 와일드카드 적용 --- .../java/com/mapbefine/mapbefine/common/config/WebConfig.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/src/main/java/com/mapbefine/mapbefine/common/config/WebConfig.java b/backend/src/main/java/com/mapbefine/mapbefine/common/config/WebConfig.java index 6cfddd36..1ffbcace 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/common/config/WebConfig.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/common/config/WebConfig.java @@ -1,6 +1,5 @@ package com.mapbefine.mapbefine.common.config; -import static org.springframework.http.HttpHeaders.COOKIE; import static org.springframework.http.HttpHeaders.LOCATION; import static org.springframework.http.HttpHeaders.SET_COOKIE; @@ -17,7 +16,7 @@ public class WebConfig implements WebMvcConfigurer { public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("http://localhost:3000", "https://mapbefine.kro.kr", "https://mapbefine.com") - .allowedHeaders(COOKIE) + .allowedHeaders("*") .allowedMethods("*") .allowCredentials(true) .exposedHeaders(LOCATION, SET_COOKIE); From ad7bcd4eb916bebc2cdb4e9bcf51cf7154f2914c Mon Sep 17 00:00:00 2001 From: kpeel5839 <89840550+kpeel5839@users.noreply.github.com> Date: Wed, 20 Sep 2023 16:31:41 +0900 Subject: [PATCH 33/34] =?UTF-8?q?[BE]=20=EB=B6=80=ED=95=98=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EB=A5=BC=20=EC=9C=84=ED=95=9C=20Tomcat=20Log?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20(#464)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: yml 변수 적용 확인을 위한 debug 로그 추가 * chore: 톰캣 설정 기본값 추가 * chore: 톰캣 설정 기본값 추가 --------- Co-authored-by: yoondgu --- .../mapbefine/common/config/S3Config.java | 15 +++++++++++++++ .../kakao/KakaoAuthCodeRequestUrlProvider.java | 4 ++++ backend/src/main/resources/config | 2 +- 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/com/mapbefine/mapbefine/common/config/S3Config.java b/backend/src/main/java/com/mapbefine/mapbefine/common/config/S3Config.java index 5d609979..2cb668ab 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/common/config/S3Config.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/common/config/S3Config.java @@ -4,12 +4,23 @@ import com.amazonaws.regions.Regions; import com.amazonaws.services.s3.AmazonS3; import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class S3Config { + private static final Logger log = LoggerFactory.getLogger(S3Config.class); + @Value("${server.tomcat.accept-count}") + private int acceptCount; + @Value("${server.tomcat.max-connections}") + private int maxConnections; + @Value("${server.tomcat.threads.max}") + private int threadsMax; + @Bean public InstanceProfileCredentialsProvider instanceProfileCredentialsProvider() { return InstanceProfileCredentialsProvider.getInstance(); @@ -17,6 +28,10 @@ public InstanceProfileCredentialsProvider instanceProfileCredentialsProvider() { @Bean public AmazonS3 amazonS3() { + log.debug("tomcat acceptCount : {}", acceptCount); + log.debug("tomcat maxConnections : {}", maxConnections); + log.debug("tomcat threadsMax : {}", threadsMax); + return AmazonS3ClientBuilder.standard() .withRegion(Regions.AP_NORTHEAST_2) .withCredentials(instanceProfileCredentialsProvider()) diff --git a/backend/src/main/java/com/mapbefine/mapbefine/oauth/domain/kakao/KakaoAuthCodeRequestUrlProvider.java b/backend/src/main/java/com/mapbefine/mapbefine/oauth/domain/kakao/KakaoAuthCodeRequestUrlProvider.java index a62ab39b..eb2054ce 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/oauth/domain/kakao/KakaoAuthCodeRequestUrlProvider.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/oauth/domain/kakao/KakaoAuthCodeRequestUrlProvider.java @@ -2,16 +2,20 @@ import com.mapbefine.mapbefine.oauth.domain.AuthCodeRequestUrlProvider; import com.mapbefine.mapbefine.oauth.domain.OauthServerType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import org.springframework.web.util.UriComponentsBuilder; @Component public class KakaoAuthCodeRequestUrlProvider implements AuthCodeRequestUrlProvider { + private final Logger log = LoggerFactory.getLogger(KakaoAuthCodeRequestUrlProvider.class); private final KakaoOauthProperties kakaoOauthProperties; public KakaoAuthCodeRequestUrlProvider(KakaoOauthProperties kakaoOauthProperties) { this.kakaoOauthProperties = kakaoOauthProperties; + log.debug("client_id: {}", kakaoOauthProperties.redirectUri()); } @Override diff --git a/backend/src/main/resources/config b/backend/src/main/resources/config index 31d9b5a2..8069c700 160000 --- a/backend/src/main/resources/config +++ b/backend/src/main/resources/config @@ -1 +1 @@ -Subproject commit 31d9b5a25e02428dafa205e9d00d99ef7fd9b6a5 +Subproject commit 8069c700c69d2b46b52c34817d8d4f66fe8d70b1 From 327e80a6643c1070dde4baf1c6a4f41e8a5b5750 Mon Sep 17 00:00:00 2001 From: kpeel5839 <89840550+kpeel5839@users.noreply.github.com> Date: Thu, 21 Sep 2023 10:53:05 +0900 Subject: [PATCH 34/34] =?UTF-8?q?[BE]=20S3=20=EB=A5=BC=20=ED=86=B5?= =?UTF-8?q?=ED=95=9C=20Image=20Upload=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#428)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor : s3 패키지 추가로 인한 에러 Code 수정 * feat : s3 exception 추가 * refactor : image extension 추출 방식 수정 * refactor : S3Client 가 IOException 을 throw 할 수 있도록 작성 * style : 프린트, 주석 제거 * test : imageExtension Test 작성 * refactor : image 가 요청으로 들어오지 않는 경우를 고려해 로직 수정 * test : 이미지가 null 로 들어오는 경우 test 작성 * feat : 병합시에도 S3 Image Upload 가 가능하도록 구현 * refactor : 기본 이미지 URL 변경 * refactor : 기본 이미지의 처리를 TopicInfo -> Image 에서 할 수 있도록 수정 * refactor : 주석 앞에 TODO 추가 * refactor : fromImageFileName -> from 으로 메서드 명 변경 * refactor : getExtension -> findExtension 으로 변경 * refactor : S3 관련 Service 네이밍 수정 * [BE] Fix/#426 Token CORS 재설정 (#427) * fix: RefreshToken Payload 추가 및 CORS 완화 * fix: Refresh Token Header 허용 * fix: CORS 재설정 및 sameSite None * refactor : S3 관련 Service 네이밍 수정 * [BE] HotFix/#426 Refresh Token 중복 저장 방지 로직 수정 (#431) * fix: RefreshToken Payload 추가 및 CORS 완화 * fix: Refresh Token Header 허용 * fix: CORS 재설정 및 sameSite None * fix: refreshToken 존재 시 삭제 로직 변경 * [BE] HotFix/#426 delete 메서드에 clearAutomatically 속성 적용 (#432) * fix: RefreshToken Payload 추가 및 CORS 완화 * fix: Refresh Token Header 허용 * fix: CORS 재설정 및 sameSite None * fix: refreshToken 존재 시 삭제 로직 변경 * fix: delete 메서드에 clearAutomatically 속성 적용 * [BE] HotFix/#426 tokenService flush 추가 (#433) * fix: RefreshToken Payload 추가 및 CORS 완화 * fix: Refresh Token Header 허용 * fix: CORS 재설정 및 sameSite None * fix: refreshToken 존재 시 삭제 로직 변경 * fix: delete 메서드에 clearAutomatically 속성 적용 * fix: delete 메서드에 clearAutomatically 속성 제거 및 flush 추가 * [BE] Refactor/#400 토픽 조회 시 업데이트 일시를 최근에 핀이 추가/변경된 일시로 변경 (#429) * refactor: BaseEntity의 createdAt update 방지 * feat: Topic에 lastPinUpdatedAt 컬럼 추가, EntityListner 적용 - 기존 BaseEntity의 값들은 객체가 영속화될 때 저장된다. - 이에 대해 일관성을 유지해야 한다. (핀 생성 일시, 핀 변경 일시 = 토픽의 최근 핀 변경 일시가 서로 같아야 하므로) - 따라서 lastPinUpdatedAt 컬럼의 업데이트 또한 EntityListener 로 적용한다. * feat: 토픽 조회 DTO의 updatedAt 값 lastPinUpdatedAt 으로 변경 * feat: 토픽 최신순 조회 로직 수정 - Topic에 lastPinUpdatedAt 추가로 인해 로직 수정 가능 * test: 토픽 조회 시 updatedAt 검증 테스트 추가 * chore: 로컬 테스트용 SQL에 테이블 컬럼 추가 변경 반영 * refactor: 토픽 Response Dto에 lastPinUpdatedAt 반영 * fix : 토큰 만료시간 및 redirect uri 수정 --------- Co-authored-by: jaeyeon kim * [BE] Feature/#422 성능 측정을 위한 로깅 구현 (#434) * feat: QueryCounter 객체 구현 * feat: QueryInspector 객체 구현 * feat: LatencyRecorder 객체 구현 * feat: LatencyLoggingFilter 객체 구현 * feat: LatencyRecorder Thread-safe 테스트 구현 * feat: HibernateConfig 구현 * test: 테스트 수정 * style: 개행 추가 * refactor: 수식 표현 방식 수정 * [BE] HotFix/#424 refresh token duplicated (#441) * fix: RefreshToken Payload 추가 및 CORS 완화 * fix: Refresh Token Header 허용 * fix: CORS 재설정 및 sameSite None * fix: 디버깅을 위한 에러코드 추가 * [BE] HOTFix/#424 validateTokensForReissue 디버깅을 위한 에러코드 추가 (#443) * fix: RefreshToken Payload 추가 및 CORS 완화 * fix: Refresh Token Header 허용 * fix: CORS 재설정 및 sameSite None * fix: 디버깅을 위한 에러코드 추가 * fix: validateTokensForReissue 디버깅을 위한 에러코드 추가 * fix: isExpired 임시 log 처리 (#444) * Revert "fix: isExpired 임시 log 처리 (#444)" This reverts commit 445f0dd18dce3bc5799db7b4e67919853d9948c2. * fix: cors Credentials 추가 (#458) * [BE] Hotfix/cors allowHeaders 와일드카드 적용 (#462) * fix: cors Credentials 추가 * fix: allowedHeaders 와일드카드 적용 * [BE] 부하테스트를 위한 Tomcat Log 추가 (#464) * chore: yml 변수 적용 확인을 위한 debug 로그 추가 * chore: 톰캣 설정 기본값 추가 * chore: 톰캣 설정 기본값 추가 --------- Co-authored-by: yoondgu * refactor : s3 패키지 추가로 인한 에러 Code 수정 * feat : s3 exception 추가 * refactor : image extension 추출 방식 수정 * refactor : S3Client 가 IOException 을 throw 할 수 있도록 작성 * style : 프린트, 주석 제거 * test : imageExtension Test 작성 * refactor : image 가 요청으로 들어오지 않는 경우를 고려해 로직 수정 * test : 이미지가 null 로 들어오는 경우 test 작성 * feat : 병합시에도 S3 Image Upload 가 가능하도록 구현 * refactor : 기본 이미지 URL 변경 * refactor : 기본 이미지의 처리를 TopicInfo -> Image 에서 할 수 있도록 수정 * refactor : 주석 앞에 TODO 추가 * refactor : fromImageFileName -> from 으로 메서드 명 변경 * refactor : getExtension -> findExtension 으로 변경 * refactor : S3 관련 Service 네이밍 수정 * refactor : S3 관련 Service 네이밍 수정 * refactor : topic, image errorCode 수정 * refactor : Exception 부분 네이밍 S3 -> Image 로 변경 * refactor : findExtension -> extractExtensio 으로 메서드 네이밍 변경 * refactor : 부정 조건문 제거 * refactor : Illegal Image File Extension 에러 메세지 수정 * refactor : action method consume type 순서 조정 --------- Co-authored-by: 준팍(junpak) <112045553+junpakPark@users.noreply.github.com> Co-authored-by: Doy Co-authored-by: zun <50602742+cpot5620@users.noreply.github.com> --- .../application/ImageService.java} | 4 +- .../application/S3ImageService.java} | 12 +- .../image/domain/ImageExtension.java | 34 +++++ .../{s3 => image}/domain/ImageName.java | 12 +- .../{s3 => image}/domain/S3Client.java | 20 ++- .../{s3 => image}/domain/UploadFile.java | 2 +- .../image/exception/ImageErrorCode.java | 20 +++ .../image/exception/ImageException.java | 16 ++ .../pin/application/PinCommandService.java | 27 +++- .../pin/presentation/PinController.java | 8 +- .../application/TopicCommandService.java | 24 ++- .../mapbefine/topic/domain/TopicInfo.java | 5 +- .../topic/dto/request/TopicCreateRequest.java | 3 +- ...va => TopicCreateRequestWithoutImage.java} | 2 +- .../topic/dto/request/TopicMergeRequest.java | 18 ++- .../TopicMergeRequestWithoutImage.java | 14 ++ .../topic/presentation/TopicController.java | 23 ++- .../com/mapbefine/mapbefine/FileFixture.java | 11 -- .../com/mapbefine/mapbefine/StubFile.java | 63 -------- .../mapbefine/TestS3ServiceImpl.java | 18 --- .../application/AdminCommandServiceTest.java | 5 +- .../mapbefine/image/FileFixture.java | 17 +++ .../mapbefine/image/ImageExtensionTest.java | 35 +++++ .../mapbefine/image/TestImageService.java | 17 +++ .../mapbefine/pin/PinIntegrationTest.java | 20 ++- .../application/PinCommandServiceTest.java | 16 +- .../pin/presentation/PinControllerTest.java | 10 +- .../mapbefine/topic/TopicFixture.java | 22 ++- .../mapbefine/topic/TopicIntegrationTest.java | 140 ++++++++++++++---- .../application/TopicCommandServiceTest.java | 24 +++ .../mapbefine/topic/domain/TopicInfoTest.java | 3 +- .../presentation/TopicControllerTest.java | 34 +++-- 32 files changed, 476 insertions(+), 203 deletions(-) rename backend/src/main/java/com/mapbefine/mapbefine/{s3/application/S3Service.java => image/application/ImageService.java} (66%) rename backend/src/main/java/com/mapbefine/mapbefine/{s3/application/S3ServiceImpl.java => image/application/S3ImageService.java} (74%) create mode 100644 backend/src/main/java/com/mapbefine/mapbefine/image/domain/ImageExtension.java rename backend/src/main/java/com/mapbefine/mapbefine/{s3 => image}/domain/ImageName.java (62%) rename backend/src/main/java/com/mapbefine/mapbefine/{s3 => image}/domain/S3Client.java (58%) rename backend/src/main/java/com/mapbefine/mapbefine/{s3 => image}/domain/UploadFile.java (97%) create mode 100644 backend/src/main/java/com/mapbefine/mapbefine/image/exception/ImageErrorCode.java create mode 100644 backend/src/main/java/com/mapbefine/mapbefine/image/exception/ImageException.java rename backend/src/main/java/com/mapbefine/mapbefine/topic/dto/request/{TopicCreateRequestWithOutImage.java => TopicCreateRequestWithoutImage.java} (88%) create mode 100644 backend/src/main/java/com/mapbefine/mapbefine/topic/dto/request/TopicMergeRequestWithoutImage.java delete mode 100644 backend/src/test/java/com/mapbefine/mapbefine/FileFixture.java delete mode 100644 backend/src/test/java/com/mapbefine/mapbefine/StubFile.java delete mode 100644 backend/src/test/java/com/mapbefine/mapbefine/TestS3ServiceImpl.java create mode 100644 backend/src/test/java/com/mapbefine/mapbefine/image/FileFixture.java create mode 100644 backend/src/test/java/com/mapbefine/mapbefine/image/ImageExtensionTest.java create mode 100644 backend/src/test/java/com/mapbefine/mapbefine/image/TestImageService.java diff --git a/backend/src/main/java/com/mapbefine/mapbefine/s3/application/S3Service.java b/backend/src/main/java/com/mapbefine/mapbefine/image/application/ImageService.java similarity index 66% rename from backend/src/main/java/com/mapbefine/mapbefine/s3/application/S3Service.java rename to backend/src/main/java/com/mapbefine/mapbefine/image/application/ImageService.java index a451f3b4..3a9d0782 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/s3/application/S3Service.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/image/application/ImageService.java @@ -1,10 +1,10 @@ -package com.mapbefine.mapbefine.s3.application; +package com.mapbefine.mapbefine.image.application; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; @Service -public interface S3Service { +public interface ImageService { String upload(MultipartFile multipartFile); diff --git a/backend/src/main/java/com/mapbefine/mapbefine/s3/application/S3ServiceImpl.java b/backend/src/main/java/com/mapbefine/mapbefine/image/application/S3ImageService.java similarity index 74% rename from backend/src/main/java/com/mapbefine/mapbefine/s3/application/S3ServiceImpl.java rename to backend/src/main/java/com/mapbefine/mapbefine/image/application/S3ImageService.java index 55f68f42..e9a9baf5 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/s3/application/S3ServiceImpl.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/image/application/S3ImageService.java @@ -1,7 +1,7 @@ -package com.mapbefine.mapbefine.s3.application; +package com.mapbefine.mapbefine.image.application; -import com.mapbefine.mapbefine.s3.domain.S3Client; -import com.mapbefine.mapbefine.s3.domain.UploadFile; +import com.mapbefine.mapbefine.image.domain.S3Client; +import com.mapbefine.mapbefine.image.domain.UploadFile; import java.io.IOException; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Profile; @@ -10,13 +10,13 @@ @Service @Profile("!test") -public class S3ServiceImpl implements S3Service { +public class S3ImageService implements ImageService { @Value("${prefix.upload.path}") private String prefixUploadPath; private final S3Client s3Client; - public S3ServiceImpl(S3Client s3Client) { + public S3ImageService(S3Client s3Client) { this.s3Client = s3Client; } @@ -31,7 +31,7 @@ public String upload(MultipartFile multipartFile) { } } - private String getUploadPath(final UploadFile uploadFile) { + private String getUploadPath(UploadFile uploadFile) { return String.join( "/", prefixUploadPath, diff --git a/backend/src/main/java/com/mapbefine/mapbefine/image/domain/ImageExtension.java b/backend/src/main/java/com/mapbefine/mapbefine/image/domain/ImageExtension.java new file mode 100644 index 00000000..90134906 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/image/domain/ImageExtension.java @@ -0,0 +1,34 @@ +package com.mapbefine.mapbefine.image.domain; + +import static com.mapbefine.mapbefine.image.exception.ImageErrorCode.ILLEGAL_IMAGE_FILE_EXTENSION; + +import com.mapbefine.mapbefine.image.exception.ImageException.ImageBadRequestException; +import java.util.Arrays; + +public enum ImageExtension { + + JPEG(".jpeg"), + JPG(".jpg"), + JFIF(".jfif"), + PNG(".png"), + SVG(".svg"), + ; + + private final String extension; + + ImageExtension(final String extension) { + this.extension = extension; + } + + public static ImageExtension from(String imageFileName) { + return Arrays.stream(values()) + .filter(imageExtension -> imageFileName.endsWith(imageExtension.getExtension())) + .findFirst() + .orElseThrow(() -> new ImageBadRequestException(ILLEGAL_IMAGE_FILE_EXTENSION)); + } + + public String getExtension() { + return extension; + } + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/s3/domain/ImageName.java b/backend/src/main/java/com/mapbefine/mapbefine/image/domain/ImageName.java similarity index 62% rename from backend/src/main/java/com/mapbefine/mapbefine/s3/domain/ImageName.java rename to backend/src/main/java/com/mapbefine/mapbefine/image/domain/ImageName.java index c53553f6..63a011a5 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/s3/domain/ImageName.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/image/domain/ImageName.java @@ -1,4 +1,4 @@ -package com.mapbefine.mapbefine.s3.domain; +package com.mapbefine.mapbefine.image.domain; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; @@ -6,7 +6,6 @@ public class ImageName { private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSSSSS"); - private static final String EXTENSION_DELIMITER = "."; private final String fileName; @@ -16,15 +15,14 @@ private ImageName(String fileName) { public static ImageName from(String originalFileName) { String fileName = FORMATTER.format(LocalDateTime.now()); - String extension = getExtension(originalFileName); + String extension = extractExtension(originalFileName); return new ImageName(fileName + extension); } - private static String getExtension(String originalFileName) { - return originalFileName.substring( - originalFileName.lastIndexOf(EXTENSION_DELIMITER) - ); + private static String extractExtension(String originalFileName) { + return ImageExtension.from(originalFileName) + .getExtension(); } public String getFileName() { diff --git a/backend/src/main/java/com/mapbefine/mapbefine/s3/domain/S3Client.java b/backend/src/main/java/com/mapbefine/mapbefine/image/domain/S3Client.java similarity index 58% rename from backend/src/main/java/com/mapbefine/mapbefine/s3/domain/S3Client.java rename to backend/src/main/java/com/mapbefine/mapbefine/image/domain/S3Client.java index ab9775b5..595cebe5 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/s3/domain/S3Client.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/image/domain/S3Client.java @@ -1,10 +1,11 @@ -package com.mapbefine.mapbefine.s3.domain; +package com.mapbefine.mapbefine.image.domain; import com.amazonaws.services.s3.AmazonS3; import com.amazonaws.services.s3.model.DeleteObjectRequest; import com.amazonaws.services.s3.model.PutObjectRequest; import java.io.File; import java.io.IOException; +import java.util.Objects; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.web.multipart.MultipartFile; @@ -20,27 +21,32 @@ public S3Client(AmazonS3 amazonS3) { this.amazonS3 = amazonS3; } - public void upload(MultipartFile multipartFile) { + public void upload(MultipartFile multipartFile) throws IOException { File tempFile = null; try { tempFile = File.createTempFile("upload_", ".tmp"); multipartFile.transferTo(tempFile); - amazonS3.putObject(new PutObjectRequest(bucket, multipartFile.getOriginalFilename(), tempFile)); - } catch (IOException e) { // TODO: 2023/09/07 Exception 을 수정 - throw new RuntimeException(e); + amazonS3.putObject(new PutObjectRequest( + bucket, + multipartFile.getOriginalFilename(), + tempFile + )); + } catch (IOException exception) { + throw new IOException(exception); } finally { removeTempFileIfExists(tempFile); } } - private void removeTempFileIfExists(final File tempFile) { - if (tempFile != null && tempFile.exists()) { + private void removeTempFileIfExists(File tempFile) { + if (Objects.nonNull(tempFile) && tempFile.exists()) { tempFile.delete(); } } public void delete(String key) { + // TODO 현재는 일단 기능만 만들어놓고, API 는 만들어놓지 않았습니다 회의를 통해서 결정해야 할 사항이 있는 것 같아서요! amazonS3.deleteObject(new DeleteObjectRequest(bucket, key)); } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/s3/domain/UploadFile.java b/backend/src/main/java/com/mapbefine/mapbefine/image/domain/UploadFile.java similarity index 97% rename from backend/src/main/java/com/mapbefine/mapbefine/s3/domain/UploadFile.java rename to backend/src/main/java/com/mapbefine/mapbefine/image/domain/UploadFile.java index 7166cf1f..b8199f2b 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/s3/domain/UploadFile.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/image/domain/UploadFile.java @@ -1,4 +1,4 @@ -package com.mapbefine.mapbefine.s3.domain; +package com.mapbefine.mapbefine.image.domain; import java.io.ByteArrayInputStream; import java.io.File; diff --git a/backend/src/main/java/com/mapbefine/mapbefine/image/exception/ImageErrorCode.java b/backend/src/main/java/com/mapbefine/mapbefine/image/exception/ImageErrorCode.java new file mode 100644 index 00000000..c92fd970 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/image/exception/ImageErrorCode.java @@ -0,0 +1,20 @@ +package com.mapbefine.mapbefine.image.exception; + +import lombok.Getter; + +@Getter +public enum ImageErrorCode { + + ILLEGAL_IMAGE_FILE_EXTENSION("10000", "지원하지 않는 이미지 파일입니다."), + IMAGE_FILE_IS_NULL("10001", "이미지가 선택되지 않았습니다.") + ; + + private final String code; + private final String message; + + ImageErrorCode(String code, String message) { + this.code = code; + this.message = message; + } + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/image/exception/ImageException.java b/backend/src/main/java/com/mapbefine/mapbefine/image/exception/ImageException.java new file mode 100644 index 00000000..680140d6 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/image/exception/ImageException.java @@ -0,0 +1,16 @@ +package com.mapbefine.mapbefine.image.exception; + +import com.mapbefine.mapbefine.common.exception.BadRequestException; +import com.mapbefine.mapbefine.common.exception.ErrorCode; + +public class ImageException { + + public static class ImageBadRequestException extends BadRequestException { + + public ImageBadRequestException(ImageErrorCode errorCode) { + super(new ErrorCode<>(errorCode.getCode(), errorCode.getMessage())); + } + + } + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/pin/application/PinCommandService.java b/backend/src/main/java/com/mapbefine/mapbefine/pin/application/PinCommandService.java index bb72b26e..b1f5ada8 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/pin/application/PinCommandService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/pin/application/PinCommandService.java @@ -3,6 +3,7 @@ import static com.mapbefine.mapbefine.pin.exception.PinErrorCode.FORBIDDEN_PIN_CREATE_OR_UPDATE; import static com.mapbefine.mapbefine.pin.exception.PinErrorCode.ILLEGAL_PIN_ID; import static com.mapbefine.mapbefine.pin.exception.PinErrorCode.ILLEGAL_PIN_IMAGE_ID; +import static com.mapbefine.mapbefine.image.exception.ImageErrorCode.IMAGE_FILE_IS_NULL; import static com.mapbefine.mapbefine.topic.exception.TopicErrorCode.ILLEGAL_TOPIC_ID; import com.mapbefine.mapbefine.auth.domain.AuthMember; @@ -21,7 +22,8 @@ import com.mapbefine.mapbefine.pin.dto.request.PinUpdateRequest; import com.mapbefine.mapbefine.pin.exception.PinException.PinBadRequestException; import com.mapbefine.mapbefine.pin.exception.PinException.PinForbiddenException; -import com.mapbefine.mapbefine.s3.application.S3Service; +import com.mapbefine.mapbefine.image.application.ImageService; +import com.mapbefine.mapbefine.image.exception.ImageException.ImageBadRequestException; import com.mapbefine.mapbefine.topic.domain.Topic; import com.mapbefine.mapbefine.topic.domain.TopicRepository; import com.mapbefine.mapbefine.topic.exception.TopicException.TopicBadRequestException; @@ -43,7 +45,7 @@ public class PinCommandService { private final TopicRepository topicRepository; private final MemberRepository memberRepository; private final PinImageRepository pinImageRepository; - private final S3Service s3Service; + private final ImageService imageService; public PinCommandService( PinRepository pinRepository, @@ -51,14 +53,14 @@ public PinCommandService( TopicRepository topicRepository, MemberRepository memberRepository, PinImageRepository pinImageRepository, - S3Service s3Service + ImageService imageService ) { this.pinRepository = pinRepository; this.locationRepository = locationRepository; this.topicRepository = topicRepository; this.memberRepository = memberRepository; this.pinImageRepository = pinImageRepository; - this.s3Service = s3Service; + this.imageService = imageService; } public long save( @@ -78,12 +80,21 @@ public long save( member ); - images.forEach(image -> addImageToPin(image, pin)); + addPinImagesToPin(images, pin); + pinRepository.save(pin); return pin.getId(); } + private void addPinImagesToPin(final List images, final Pin pin) { + if (Objects.isNull(images)) { + return; + } + + images.forEach(image -> addImageToPin(image, pin)); + } + private Topic findTopic(Long topicId) { if (Objects.isNull(topicId)) { throw new TopicBadRequestException(ILLEGAL_TOPIC_ID); @@ -153,7 +164,11 @@ public void addImage(AuthMember authMember, PinImageCreateRequest request) { } private void addImageToPin(MultipartFile image, Pin pin) { - String imageUrl = s3Service.upload(image); + if (Objects.isNull(image)) { + throw new ImageBadRequestException(IMAGE_FILE_IS_NULL); + } + + String imageUrl = imageService.upload(image); PinImage.createPinImageAssociatedWithPin(imageUrl, pin); } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/pin/presentation/PinController.java b/backend/src/main/java/com/mapbefine/mapbefine/pin/presentation/PinController.java index 083b46c3..d144d889 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/pin/presentation/PinController.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/pin/presentation/PinController.java @@ -39,10 +39,10 @@ public PinController(PinCommandService pinCommandService, PinQueryService pinQue } @LoginRequired - @PostMapping(consumes = {MediaType.MULTIPART_FORM_DATA_VALUE, MediaType.APPLICATION_JSON_VALUE}) + @PostMapping(consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE}) public ResponseEntity add( AuthMember member, - @RequestPart List images, + @RequestPart(required = false) List images, @RequestPart PinCreateRequest request ) { long savedId = pinCommandService.save(member, images, request); @@ -101,12 +101,12 @@ public ResponseEntity> findAllPinsByMemberId( @LoginRequired @PostMapping( value = "/images", - consumes = {MediaType.MULTIPART_FORM_DATA_VALUE, MediaType.APPLICATION_JSON_VALUE} + consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE} ) public ResponseEntity addImage( AuthMember member, @RequestPart Long pinId, - @RequestPart MultipartFile image + @RequestPart(required = false) MultipartFile image ) { pinCommandService.addImage(member, new PinImageCreateRequest(pinId, image)); diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/application/TopicCommandService.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/application/TopicCommandService.java index 1466c02d..30a7ba89 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/topic/application/TopicCommandService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/application/TopicCommandService.java @@ -14,7 +14,7 @@ import com.mapbefine.mapbefine.pin.domain.PinRepository; import com.mapbefine.mapbefine.pin.exception.PinException.PinBadRequestException; import com.mapbefine.mapbefine.pin.exception.PinException.PinForbiddenException; -import com.mapbefine.mapbefine.s3.application.S3Service; +import com.mapbefine.mapbefine.image.application.ImageService; import com.mapbefine.mapbefine.topic.domain.Topic; import com.mapbefine.mapbefine.topic.domain.TopicRepository; import com.mapbefine.mapbefine.topic.dto.request.TopicCreateRequest; @@ -28,6 +28,7 @@ import java.util.Objects; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; @Transactional @Service @@ -36,25 +37,25 @@ public class TopicCommandService { private final TopicRepository topicRepository; private final PinRepository pinRepository; private final MemberRepository memberRepository; - private final S3Service s3Service; + private final ImageService imageService; public TopicCommandService( TopicRepository topicRepository, PinRepository pinRepository, MemberRepository memberRepository, - S3Service s3Service + ImageService imageService ) { this.topicRepository = topicRepository; this.pinRepository = pinRepository; this.memberRepository = memberRepository; - this.s3Service = s3Service; + this.imageService = imageService; } public Long saveTopic(AuthMember member, TopicCreateRequest request) { Topic topic = convertToTopic(member, request); List pinIds = request.pins(); - if (pinIds.size() > 0) { + if (0 < pinIds.size()) { copyPinsToTopic(member, topic, pinIds); } @@ -65,7 +66,7 @@ public Long saveTopic(AuthMember member, TopicCreateRequest request) { private Topic convertToTopic(AuthMember member, TopicCreateRequest request) { Member creator = findCreatorByAuthMember(member); - String image = s3Service.upload(request.image()); + String image = createImageUrl(request.image()); return Topic.createTopicAssociatedWithCreator( request.name(), @@ -77,6 +78,14 @@ private Topic convertToTopic(AuthMember member, TopicCreateRequest request) { ); } + private String createImageUrl(MultipartFile image) { + if (Objects.isNull(image)) { + return null; + } + + return imageService.upload(image); + } + private Member findCreatorByAuthMember(AuthMember member) { Long memberId = member.getMemberId(); if (Objects.isNull(memberId)) { @@ -137,11 +146,12 @@ public Long merge(AuthMember member, TopicMergeRequest request) { private Topic convertToTopic(AuthMember member, TopicMergeRequest request) { Member creator = findCreatorByAuthMember(member); + String imageUrl = createImageUrl(request.image()); return Topic.createTopicAssociatedWithCreator( request.name(), request.description(), - request.image(), + imageUrl, request.publicity(), request.permissionType(), creator diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicInfo.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicInfo.java index dff5da7e..f7dfe464 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicInfo.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicInfo.java @@ -21,8 +21,9 @@ @Getter public class TopicInfo { - private static final Image DEFAULT_IMAGE = - Image.from("https://map-befine-official.github.io/favicon.png"); + private static final Image DEFAULT_IMAGE = Image.from( + "https://velog.velcdn.com/images/semnil5202/post/37f3bcb9-0b07-4100-85f6-f1d5ad037c14/image.svg" + ); private static final int MAX_DESCRIPTION_LENGTH = 1000; private static final int MAX_NAME_LENGTH = 20; diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/dto/request/TopicCreateRequest.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/dto/request/TopicCreateRequest.java index 211149fc..40efd476 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/topic/dto/request/TopicCreateRequest.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/dto/request/TopicCreateRequest.java @@ -15,7 +15,7 @@ public record TopicCreateRequest( ) { public static TopicCreateRequest of( - TopicCreateRequestWithOutImage request, + TopicCreateRequestWithoutImage request, MultipartFile image ) { return new TopicCreateRequest( @@ -27,4 +27,5 @@ public static TopicCreateRequest of( request.pins() ); } + } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/dto/request/TopicCreateRequestWithOutImage.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/dto/request/TopicCreateRequestWithoutImage.java similarity index 88% rename from backend/src/main/java/com/mapbefine/mapbefine/topic/dto/request/TopicCreateRequestWithOutImage.java rename to backend/src/main/java/com/mapbefine/mapbefine/topic/dto/request/TopicCreateRequestWithoutImage.java index 2f59da22..5cfeb905 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/topic/dto/request/TopicCreateRequestWithOutImage.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/dto/request/TopicCreateRequestWithoutImage.java @@ -4,7 +4,7 @@ import com.mapbefine.mapbefine.topic.domain.Publicity; import java.util.List; -public record TopicCreateRequestWithOutImage( +public record TopicCreateRequestWithoutImage( String name, String description, Publicity publicity, diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/dto/request/TopicMergeRequest.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/dto/request/TopicMergeRequest.java index deb14c79..920bbe2a 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/topic/dto/request/TopicMergeRequest.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/dto/request/TopicMergeRequest.java @@ -3,13 +3,29 @@ import com.mapbefine.mapbefine.topic.domain.PermissionType; import com.mapbefine.mapbefine.topic.domain.Publicity; import java.util.List; +import org.springframework.web.multipart.MultipartFile; public record TopicMergeRequest( String name, - String image, + MultipartFile image, String description, Publicity publicity, PermissionType permissionType, List topics ) { + + public static TopicMergeRequest of( + TopicMergeRequestWithoutImage topicMergeRequestWithoutImage, + MultipartFile image + ) { + return new TopicMergeRequest( + topicMergeRequestWithoutImage.name(), + image, + topicMergeRequestWithoutImage.description(), + topicMergeRequestWithoutImage.publicity(), + topicMergeRequestWithoutImage.permissionType(), + topicMergeRequestWithoutImage.topics() + ); + } + } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/dto/request/TopicMergeRequestWithoutImage.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/dto/request/TopicMergeRequestWithoutImage.java new file mode 100644 index 00000000..a7904b23 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/dto/request/TopicMergeRequestWithoutImage.java @@ -0,0 +1,14 @@ +package com.mapbefine.mapbefine.topic.dto.request; + +import com.mapbefine.mapbefine.topic.domain.PermissionType; +import com.mapbefine.mapbefine.topic.domain.Publicity; +import java.util.List; + +public record TopicMergeRequestWithoutImage( + String name, + String description, + Publicity publicity, + PermissionType permissionType, + List topics +) { +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/presentation/TopicController.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/presentation/TopicController.java index fffae8fb..7bb8d169 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/topic/presentation/TopicController.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/presentation/TopicController.java @@ -5,8 +5,9 @@ import com.mapbefine.mapbefine.topic.application.TopicCommandService; import com.mapbefine.mapbefine.topic.application.TopicQueryService; import com.mapbefine.mapbefine.topic.dto.request.TopicCreateRequest; -import com.mapbefine.mapbefine.topic.dto.request.TopicCreateRequestWithOutImage; +import com.mapbefine.mapbefine.topic.dto.request.TopicCreateRequestWithoutImage; import com.mapbefine.mapbefine.topic.dto.request.TopicMergeRequest; +import com.mapbefine.mapbefine.topic.dto.request.TopicMergeRequestWithoutImage; import com.mapbefine.mapbefine.topic.dto.request.TopicUpdateRequest; import com.mapbefine.mapbefine.topic.dto.response.TopicDetailResponse; import com.mapbefine.mapbefine.topic.dto.response.TopicResponse; @@ -44,12 +45,12 @@ public TopicController( @LoginRequired @PostMapping( value = "/new", - consumes = {MediaType.MULTIPART_FORM_DATA_VALUE, MediaType.APPLICATION_JSON_VALUE} + consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE} ) public ResponseEntity create( AuthMember member, - @RequestPart TopicCreateRequestWithOutImage request, - @RequestPart MultipartFile image + @RequestPart TopicCreateRequestWithoutImage request, + @RequestPart(required = false) MultipartFile image ) { TopicCreateRequest topicCreateRequest = TopicCreateRequest.of(request, image); Long topicId = topicCommandService.saveTopic(member, topicCreateRequest); @@ -59,9 +60,17 @@ public ResponseEntity create( } @LoginRequired - @PostMapping("/merge") - public ResponseEntity mergeAndCreate(AuthMember member, @RequestBody TopicMergeRequest request) { - Long topicId = topicCommandService.merge(member, request); + @PostMapping( + value = "/merge", + consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE} + ) + public ResponseEntity mergeAndCreate( + AuthMember member, + @RequestPart TopicMergeRequestWithoutImage request, + @RequestPart(required = false) MultipartFile image + ) { + TopicMergeRequest topicMergeRequest = TopicMergeRequest.of(request, image); + Long topicId = topicCommandService.merge(member, topicMergeRequest); return ResponseEntity.created(URI.create("/topics/" + topicId)) .build(); diff --git a/backend/src/test/java/com/mapbefine/mapbefine/FileFixture.java b/backend/src/test/java/com/mapbefine/mapbefine/FileFixture.java deleted file mode 100644 index 7acc3fec..00000000 --- a/backend/src/test/java/com/mapbefine/mapbefine/FileFixture.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.mapbefine.mapbefine; - -import org.springframework.web.multipart.MultipartFile; - -public class FileFixture { - - public static MultipartFile createFile() { - return new StubFile(); - } - -} diff --git a/backend/src/test/java/com/mapbefine/mapbefine/StubFile.java b/backend/src/test/java/com/mapbefine/mapbefine/StubFile.java deleted file mode 100644 index f90b4407..00000000 --- a/backend/src/test/java/com/mapbefine/mapbefine/StubFile.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.mapbefine.mapbefine; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.Base64; -import org.springframework.web.multipart.MultipartFile; - -public class StubFile implements MultipartFile { - - private final String fileName; - private final byte[] bytes; - - public StubFile() { - this.fileName = "yyyyMMddHHmmssSSSSSS"; - this.bytes = fileName.getBytes(); - } - - @Override - public String getName() { - return fileName; - } - - @Override - public String getOriginalFilename() { - return fileName; - } - - @Override - public String getContentType() { - return "text/plain"; - } - - @Override - public boolean isEmpty() { - return false; - } - - @Override - public long getSize() { - return 0; - } - - @Override - public byte[] getBytes() throws IOException { - return Base64.getEncoder() - .encode(bytes); - } - - @Override - public InputStream getInputStream() { - return InputStream.nullInputStream(); - } - - @Override - public void transferTo(final File dest) throws IOException, IllegalStateException { - FileOutputStream fileOutputStream = new FileOutputStream(dest); - fileOutputStream.write(this.bytes); - fileOutputStream.close(); - } - -} diff --git a/backend/src/test/java/com/mapbefine/mapbefine/TestS3ServiceImpl.java b/backend/src/test/java/com/mapbefine/mapbefine/TestS3ServiceImpl.java deleted file mode 100644 index b53f80df..00000000 --- a/backend/src/test/java/com/mapbefine/mapbefine/TestS3ServiceImpl.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.mapbefine.mapbefine; - -import com.mapbefine.mapbefine.s3.application.S3Service; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Service; -import org.springframework.web.multipart.MultipartFile; - -@Service -@Profile("test") -public class TestS3ServiceImpl implements S3Service { - - @Override - public String upload(MultipartFile multipartFile) { - System.out.println("TestS3ServiceImple Upload Method Called !!!!!!!!!!!!!!!!!!!!!!!!!!!!"); - return "https://mapbefine.github.io/favicon.png"; - } - -} diff --git a/backend/src/test/java/com/mapbefine/mapbefine/admin/application/AdminCommandServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/admin/application/AdminCommandServiceTest.java index 12e49f77..0f3dfcba 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/admin/application/AdminCommandServiceTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/admin/application/AdminCommandServiceTest.java @@ -194,8 +194,9 @@ void deleteTopicImage_Success() { //then Topic imageDeletedTopic = topicRepository.findById(topic.getId()).get(); - assertThat(imageDeletedTopic.getTopicInfo().getImageUrl()) - .isEqualTo("https://map-befine-official.github.io/favicon.png"); + assertThat(imageDeletedTopic.getTopicInfo().getImageUrl()).isEqualTo( + "https://velog.velcdn.com/images/semnil5202/post/37f3bcb9-0b07-4100-85f6-f1d5ad037c14/image.svg" + ); } @DisplayName("Admin이 아닐 경우, 이미지를 삭제할 수 없다.") diff --git a/backend/src/test/java/com/mapbefine/mapbefine/image/FileFixture.java b/backend/src/test/java/com/mapbefine/mapbefine/image/FileFixture.java new file mode 100644 index 00000000..47e5edcd --- /dev/null +++ b/backend/src/test/java/com/mapbefine/mapbefine/image/FileFixture.java @@ -0,0 +1,17 @@ +package com.mapbefine.mapbefine.image; + +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.multipart.MultipartFile; + +public class FileFixture { + + public static MultipartFile createFile() { + return new MockMultipartFile( + "test", + "test.png", + "img/png", + "image".getBytes() + ); + } + +} diff --git a/backend/src/test/java/com/mapbefine/mapbefine/image/ImageExtensionTest.java b/backend/src/test/java/com/mapbefine/mapbefine/image/ImageExtensionTest.java new file mode 100644 index 00000000..2f9ed9a0 --- /dev/null +++ b/backend/src/test/java/com/mapbefine/mapbefine/image/ImageExtensionTest.java @@ -0,0 +1,35 @@ +package com.mapbefine.mapbefine.image; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.mapbefine.mapbefine.image.domain.ImageExtension; +import com.mapbefine.mapbefine.image.exception.ImageException.ImageBadRequestException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class ImageExtensionTest { + + @DisplayName("fromByImageFileName 를 통해 정상적으로 ImageExtension 을 생성한다.") + @ParameterizedTest + @ValueSource(strings = {"image.jpeg", "image.jpg", "image.jfif", "image.png", "image.svg"}) + void createImageExtensionByFileName_Success(String fileName) { + // given when + String extension = ImageExtension.from(fileName) + .getExtension(); + + // then + assertThat(fileName).contains(extension); + } + + @DisplayName("fromByImageFileName 을 통해 존재하지 않는 확장자라면 예외가 발생한다.") + @ParameterizedTest + @ValueSource(strings = {"image.pppng", "image.jpeeg", "image.gi"}) + void createImageExtensionByFileName_Fail(String fileName) { + // given when then + assertThatThrownBy(() -> ImageExtension.from(fileName)) + .isInstanceOf(ImageBadRequestException.class); + } + +} diff --git a/backend/src/test/java/com/mapbefine/mapbefine/image/TestImageService.java b/backend/src/test/java/com/mapbefine/mapbefine/image/TestImageService.java new file mode 100644 index 00000000..dd33ef61 --- /dev/null +++ b/backend/src/test/java/com/mapbefine/mapbefine/image/TestImageService.java @@ -0,0 +1,17 @@ +package com.mapbefine.mapbefine.image; + +import com.mapbefine.mapbefine.image.application.ImageService; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +@Service +@Profile("test") +public class TestImageService implements ImageService { + + @Override + public String upload(MultipartFile multipartFile) { + return "https://map-befine-official.github.io/favicon.png"; + } + +} diff --git a/backend/src/test/java/com/mapbefine/mapbefine/pin/PinIntegrationTest.java b/backend/src/test/java/com/mapbefine/mapbefine/pin/PinIntegrationTest.java index eed8c54f..031f0da8 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/pin/PinIntegrationTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/pin/PinIntegrationTest.java @@ -114,6 +114,23 @@ private ExtractableResponse createPin(PinCreateRequest request) { .extract(); } + @Test + @DisplayName("Image List 없이 Pin 을 정상적으로 생성한다.") + void addIfNonExistImageList_Success() { + // when + ExtractableResponse response = RestAssured.given() + .log().all() + .header(AUTHORIZATION, authHeader) + .multiPart("request", createRequestDuplicateLocation, MediaType.APPLICATION_JSON_VALUE) + .when().post("/pins") + .then().log().all() + .extract(); + + // then + assertThat(response.header("Location")).isNotBlank(); + assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value()); + } + @Test @DisplayName("Pin을 생성하면 저장된 Pin의 Location 헤더값과 201을 반환한다.") void addIfNotExistDuplicateLocation_Success() { @@ -160,7 +177,6 @@ void findDetail_Success() { ExtractableResponse response = findById(pinId); PinDetailResponse as = response.as(PinDetailResponse.class); - System.out.println(as); // then assertThat(response.jsonPath().getString("name")) @@ -195,8 +211,6 @@ void addImage_Success() { // when ExtractableResponse response = createPinImage(pinId); - System.out.println(response); - // then assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value()); } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/pin/application/PinCommandServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/pin/application/PinCommandServiceTest.java index 22ea4686..020dc74f 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/pin/application/PinCommandServiceTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/pin/application/PinCommandServiceTest.java @@ -3,11 +3,12 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import com.mapbefine.mapbefine.FileFixture; import com.mapbefine.mapbefine.auth.domain.AuthMember; import com.mapbefine.mapbefine.auth.domain.member.Admin; import com.mapbefine.mapbefine.auth.domain.member.Guest; import com.mapbefine.mapbefine.common.annotation.ServiceTest; +import com.mapbefine.mapbefine.image.FileFixture; +import com.mapbefine.mapbefine.image.exception.ImageException.ImageBadRequestException; import com.mapbefine.mapbefine.location.LocationFixture; import com.mapbefine.mapbefine.location.domain.Location; import com.mapbefine.mapbefine.location.domain.LocationRepository; @@ -234,6 +235,19 @@ void addImage_Success() { ); } + @Test + @DisplayName("이미지가 null 인 경우 예외를 발생시킨다.") + void addImage_FailByNull() { + // given + long pinId = pinCommandService.save(authMember, List.of(BASE_IMAGE_FILE), createRequest); + + PinImageCreateRequest imageNullRequest = new PinImageCreateRequest(pinId, null); + + // when then + assertThatThrownBy(() -> pinCommandService.addImage(authMember, imageNullRequest)) + .isInstanceOf(ImageBadRequestException.class); + } + @Test @DisplayName("권한이 없는 토픽의 핀에 핀 이미지를 추가하면 예외를 발생시킨다.") void addImage_FailByForbidden() { diff --git a/backend/src/test/java/com/mapbefine/mapbefine/pin/presentation/PinControllerTest.java b/backend/src/test/java/com/mapbefine/mapbefine/pin/presentation/PinControllerTest.java index 17dc5a8b..a283aff6 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/pin/presentation/PinControllerTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/pin/presentation/PinControllerTest.java @@ -40,7 +40,10 @@ class PinControllerTest extends RestDocsIntegration { @DisplayName("핀 추가") void add() throws Exception { given(pinCommandService.save(any(), any(), any())).willReturn(1L); - File mockFile = new File(getClass().getClassLoader().getResource("test.png").getPath()); + File mockFile = new File(getClass() + .getClassLoader() + .getResource("test.png") + .getPath()); MultiValueMap param = new LinkedMultiValueMap<>(); PinCreateRequest pinCreateRequest = new PinCreateRequest( @@ -159,11 +162,6 @@ void addImage() throws Exception { .getPath(); File mockFile = new File(imageFilePath); -// PinImageCreateRequest pinImageCreateRequest = new PinImageCreateRequest( -// 1L, -// FileFixture.createFile() -// ); - mockMvc.perform( MockMvcRequestBuilders.post("/pins/images") .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) diff --git a/backend/src/test/java/com/mapbefine/mapbefine/topic/TopicFixture.java b/backend/src/test/java/com/mapbefine/mapbefine/topic/TopicFixture.java index a8c4d93c..4dab6572 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/topic/TopicFixture.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/topic/TopicFixture.java @@ -1,7 +1,7 @@ package com.mapbefine.mapbefine.topic; -import com.mapbefine.mapbefine.FileFixture; +import com.mapbefine.mapbefine.image.FileFixture; import com.mapbefine.mapbefine.member.domain.Member; import com.mapbefine.mapbefine.topic.domain.PermissionType; import com.mapbefine.mapbefine.topic.domain.Publicity; @@ -40,7 +40,7 @@ public static Topic createByName(String name, Member member) { return Topic.createTopicAssociatedWithCreator( name, "설명", - null, + IMAGE_URL, Publicity.PUBLIC, PermissionType.ALL_MEMBERS, member @@ -51,7 +51,7 @@ public static Topic createPrivateByName(String name, Member member) { return Topic.createTopicAssociatedWithCreator( name, "설명", - null, + IMAGE_URL, Publicity.PRIVATE, PermissionType.GROUP_ONLY, member @@ -64,7 +64,19 @@ public static TopicCreateRequest createPublicAndAllMembersCreateRequestWithPins( return new TopicCreateRequest( "아무나 읽을 수 있는 토픽", FileFixture.createFile(), -// IMAGE_URL, + "아무나 읽을 수 있는 토픽입니다.", + Publicity.PUBLIC, + PermissionType.ALL_MEMBERS, + pinIds + ); + } + + public static TopicCreateRequest createPublicAndAllMembersAndEmptyImageCreateRequestWithPins( + List pinIds + ) { + return new TopicCreateRequest( + "아무나 읽을 수 있는 토픽", + null, "아무나 읽을 수 있는 토픽입니다.", Publicity.PUBLIC, PermissionType.ALL_MEMBERS, @@ -77,7 +89,7 @@ public static TopicMergeRequest createPublicAndAllMembersMergeRequestWithTopics( ) { return new TopicMergeRequest( "아무나 읽을 수 있는 토픽", - IMAGE_URL, + FileFixture.createFile(), "아무나 읽을 수 있는 토픽입니다.", Publicity.PUBLIC, PermissionType.ALL_MEMBERS, diff --git a/backend/src/test/java/com/mapbefine/mapbefine/topic/TopicIntegrationTest.java b/backend/src/test/java/com/mapbefine/mapbefine/topic/TopicIntegrationTest.java index d4115af6..20152e7f 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/topic/TopicIntegrationTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/topic/TopicIntegrationTest.java @@ -20,13 +20,15 @@ import com.mapbefine.mapbefine.topic.domain.Publicity; import com.mapbefine.mapbefine.topic.domain.Topic; import com.mapbefine.mapbefine.topic.domain.TopicRepository; -import com.mapbefine.mapbefine.topic.dto.request.TopicCreateRequestWithOutImage; +import com.mapbefine.mapbefine.topic.dto.request.TopicCreateRequestWithoutImage; import com.mapbefine.mapbefine.topic.dto.request.TopicMergeRequest; +import com.mapbefine.mapbefine.topic.dto.request.TopicMergeRequestWithoutImage; import com.mapbefine.mapbefine.topic.dto.request.TopicUpdateRequest; import com.mapbefine.mapbefine.topic.dto.response.TopicDetailResponse; import com.mapbefine.mapbefine.topic.dto.response.TopicResponse; -import io.restassured.*; -import io.restassured.response.*; +import io.restassured.RestAssured; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; import java.io.File; import java.util.Collections; import java.util.List; @@ -58,6 +60,7 @@ class TopicIntegrationTest extends IntegrationTest { private Topic topic; private Location location; private String authHeader; + private File mockFile; @BeforeEach void setMember() { @@ -65,13 +68,18 @@ void setMember() { topic = topicRepository.save(TopicFixture.createPublicAndAllMembersTopic(member)); location = locationRepository.save(LocationFixture.create()); authHeader = testAuthHeaderProvider.createAuthHeader(member); - } + mockFile = new File( + getClass().getClassLoader() + .getResource("test.png") + .getPath() + ); + } @Test @DisplayName("Pin 목록 없이 Topic을 생성하면 201을 반환한다") void createNewTopicWithoutPins_Success() { - TopicCreateRequestWithOutImage 준팍의_또간집 = new TopicCreateRequestWithOutImage( + TopicCreateRequestWithoutImage 준팍의_또간집 = new TopicCreateRequestWithoutImage( "준팍의 또간집", "준팍이 2번 이상 간집 ", Publicity.PUBLIC, @@ -87,19 +95,7 @@ void createNewTopicWithoutPins_Success() { assertThat(response.header("Location")).isNotBlank(); } - private ExtractableResponse createNewTopic(TopicCreateRequestWithOutImage request, String authHeader) { - String imageFilePath = getClass().getClassLoader() - .getResource("test.png") - .getPath(); - File mockFile = new File(imageFilePath); - -// MockMultipartFile mockFile = new MockMultipartFile( // 이것은 왜 그런 것일까?? -// "test", -// "test.png", -// "image/png", -// "byteCode".getBytes() -// ); - + private ExtractableResponse createNewTopic(TopicCreateRequestWithoutImage request, String authHeader) { return RestAssured.given() .log().all() .header(AUTHORIZATION, authHeader) @@ -110,10 +106,19 @@ private ExtractableResponse createNewTopic(TopicCreateRequestWithOutIm .extract(); } + private ExtractableResponse createNewTopicExcludeImage(TopicCreateRequestWithoutImage request, String authHeader) { + return RestAssured.given() + .log().all() + .header(AUTHORIZATION, authHeader) + .multiPart("request", request, MediaType.APPLICATION_JSON_VALUE) + .when().post("/topics/new") + .then().log().all() + .extract(); + } + @Test @DisplayName("Pin 목록과 함께 Topic을 생성하면 201을 반환한다") void createNewTopicWithPins_Success() { - PinFixture.create(location, topic, member); List pins = pinRepository.findAll(); @@ -121,7 +126,7 @@ void createNewTopicWithPins_Success() { .map(Pin::getId) .toList(); - TopicCreateRequestWithOutImage 준팍의_또간집 = new TopicCreateRequestWithOutImage( + TopicCreateRequestWithoutImage 준팍의_또간집 = new TopicCreateRequestWithoutImage( "준팍의 또간집", "준팍이 2번 이상 간집 ", Publicity.PUBLIC, @@ -137,18 +142,44 @@ void createNewTopicWithPins_Success() { assertThat(response.header("Location")).isNotBlank(); } + @Test + @DisplayName("Image 없이 Topic 을 생성하더라도 201을 반환한다") + void createNewTopicNonExistsImage_Success() { + PinFixture.create(location, topic, member); + + List pins = pinRepository.findAll(); + List pinIds = pins.stream() + .map(Pin::getId) + .toList(); + + TopicCreateRequestWithoutImage 준팍의_또간집 = new TopicCreateRequestWithoutImage( + "준팍의 또간집", + "준팍이 2번 이상 간집 ", + Publicity.PUBLIC, + PermissionType.ALL_MEMBERS, + pinIds + ); + + // when + ExtractableResponse response = createNewTopicExcludeImage(준팍의_또간집, authHeader); + + // then + assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value()); + assertThat(response.header("Location")).isNotBlank(); + } + @Test @DisplayName("여러개의 토픽을 병합하면 201을 반환한다") void createMergeTopic_Success() { // given - TopicCreateRequestWithOutImage 준팍의_또간집 = new TopicCreateRequestWithOutImage( + TopicCreateRequestWithoutImage 준팍의_또간집 = new TopicCreateRequestWithoutImage( "준팍의 또간집", "준팍이 2번 이상 간집 ", Publicity.PUBLIC, PermissionType.ALL_MEMBERS, Collections.emptyList() ); - TopicCreateRequestWithOutImage 준팍의_또안간집 = new TopicCreateRequestWithOutImage( + TopicCreateRequestWithoutImage 준팍의_또안간집 = new TopicCreateRequestWithoutImage( "준팍의 또안간집", "준팍이 2번 이상 안간집 ", Publicity.PUBLIC, @@ -162,20 +193,65 @@ void createMergeTopic_Success() { .map(Topic::getId) .toList(); - TopicMergeRequest 송파_데이트코스 = new TopicMergeRequest( + TopicMergeRequestWithoutImage 송파_데이트코스 = new TopicMergeRequestWithoutImage( "송파 데이트코스", - "https://map-befine-official.github.io/favicon.png", "맛집과 카페 토픽 합치기", Publicity.PUBLIC, PermissionType.ALL_MEMBERS, topicIds); // when - ExtractableResponse response = RestAssured - .given().log().all() + ExtractableResponse response = RestAssured.given() + .log().all() .header(AUTHORIZATION, authHeader) - .contentType(MediaType.APPLICATION_JSON_VALUE) - .body(송파_데이트코스) + .multiPart("image", mockFile, MediaType.MULTIPART_FORM_DATA_VALUE) + .multiPart("request", 송파_데이트코스, MediaType.APPLICATION_JSON_VALUE) + .when().post("/topics/merge") + .then().log().all() + .extract(); + + // then + assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value()); + assertThat(response.header("Location")).isNotBlank(); + } + + @Test + @DisplayName("여러개의 토픽을 토픽 이미지 없이 병합해도 201을 반환한다") + void createMergeTopicWithoutImage_Success() { + // given + TopicCreateRequestWithoutImage 준팍의_또간집 = new TopicCreateRequestWithoutImage( + "준팍의 또간집", + "준팍이 2번 이상 간집 ", + Publicity.PUBLIC, + PermissionType.ALL_MEMBERS, + Collections.emptyList() + ); + TopicCreateRequestWithoutImage 준팍의_또안간집 = new TopicCreateRequestWithoutImage( + "준팍의 또안간집", + "준팍이 2번 이상 안간집 ", + Publicity.PUBLIC, + PermissionType.ALL_MEMBERS, + Collections.emptyList() + ); + createNewTopic(준팍의_또간집, authHeader); + createNewTopic(준팍의_또안간집, authHeader); + List topics = topicRepository.findAll(); + List topicIds = topics.stream() + .map(Topic::getId) + .toList(); + + TopicMergeRequestWithoutImage 송파_데이트코스 = new TopicMergeRequestWithoutImage( + "송파 데이트코스", + "맛집과 카페 토픽 합치기", + Publicity.PUBLIC, + PermissionType.ALL_MEMBERS, + topicIds); + + // when + ExtractableResponse response = RestAssured.given() + .log().all() + .header(AUTHORIZATION, authHeader) + .multiPart("request", 송파_데이트코스, MediaType.APPLICATION_JSON_VALUE) .when().post("/topics/merge") .then().log().all() .extract(); @@ -189,7 +265,7 @@ void createMergeTopic_Success() { @DisplayName("Topic을 수정하면 200을 반환한다") void updateTopic_Success() { ExtractableResponse newTopic = createNewTopic( - new TopicCreateRequestWithOutImage( + new TopicCreateRequestWithoutImage( "준팍의 또간집", "준팍이 두번 간집", Publicity.PUBLIC, @@ -225,7 +301,7 @@ void updateTopic_Success() { @DisplayName("Topic을 삭제하면 204를 반환한다") void deleteTopic_Success() { ExtractableResponse newTopic = createNewTopic( - new TopicCreateRequestWithOutImage( + new TopicCreateRequestWithoutImage( "준팍의 또간집", "준팍이 두번 간집 ", Publicity.PUBLIC, @@ -269,7 +345,7 @@ void findTopics_Success() { @DisplayName("Topic 상세 정보를 조회하면 200을 반환한다") void findTopicDetail_Success() { //given - TopicCreateRequestWithOutImage request = new TopicCreateRequestWithOutImage( + TopicCreateRequestWithoutImage request = new TopicCreateRequestWithoutImage( "topicName", "description", Publicity.PUBLIC, @@ -297,7 +373,7 @@ void findTopicDetail_Success() { @DisplayName("Topic 상세 정보 여러개를 조회하면 200을 반환한다") void findTopicDetailsByIds_Success() { //given - TopicCreateRequestWithOutImage request = new TopicCreateRequestWithOutImage( + TopicCreateRequestWithoutImage request = new TopicCreateRequestWithoutImage( "topicName", "description", Publicity.PUBLIC, diff --git a/backend/src/test/java/com/mapbefine/mapbefine/topic/application/TopicCommandServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/topic/application/TopicCommandServiceTest.java index 02794503..90459094 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/topic/application/TopicCommandServiceTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/topic/application/TopicCommandServiceTest.java @@ -92,6 +92,30 @@ void saveEmptyTopic_Success() { assertThat(detail.pinCount()).isEqualTo(request.pins().size()); } + @Test + @DisplayName("이미지 없이 토픽을 생성하면 기본 이미지를 반환한다.") + void saveEmptyTopicAndEmptyImage_Success() { + //given + TopicCreateRequest request = + TopicFixture.createPublicAndAllMembersAndEmptyImageCreateRequestWithPins( + Collections.emptyList() + ); + + //when + Long topicId = topicCommandService.saveTopic(user, request); + + //then + TopicDetailResponse detail = topicQueryService.findDetailById(user, topicId); + + assertThat(detail.id()).isEqualTo(topicId); + assertThat(detail.name()).isEqualTo(request.name()); + assertThat(detail.description()).isEqualTo(request.description()); + assertThat(detail.pinCount()).isEqualTo(request.pins().size()); + assertThat(detail.image()).isEqualTo( + "https://velog.velcdn.com/images/semnil5202/post/37f3bcb9-0b07-4100-85f6-f1d5ad037c14/image.svg" + ); + } + @Test @DisplayName("Guest는 비어있는 토픽을 생성할 수 없다.") void saveEmptyTopic_Fail() { diff --git a/backend/src/test/java/com/mapbefine/mapbefine/topic/domain/TopicInfoTest.java b/backend/src/test/java/com/mapbefine/mapbefine/topic/domain/TopicInfoTest.java index a3e15bae..ed026493 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/topic/domain/TopicInfoTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/topic/domain/TopicInfoTest.java @@ -116,7 +116,8 @@ void whenImageNull_thenReturnDefault() { assertThat(topicInfo.getName()).isEqualTo(validName); assertThat(topicInfo.getDescription()).isEqualTo(validDescription); assertThat(topicInfo.getImageUrl()).isEqualTo( - "https://map-befine-official.github.io/favicon.png"); + "https://velog.velcdn.com/images/semnil5202/post/37f3bcb9-0b07-4100-85f6-f1d5ad037c14/image.svg" + ); } } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/topic/presentation/TopicControllerTest.java b/backend/src/test/java/com/mapbefine/mapbefine/topic/presentation/TopicControllerTest.java index 23dc0945..261e558d 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/topic/presentation/TopicControllerTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/topic/presentation/TopicControllerTest.java @@ -10,14 +10,15 @@ import com.mapbefine.mapbefine.topic.application.TopicQueryService; import com.mapbefine.mapbefine.topic.domain.PermissionType; import com.mapbefine.mapbefine.topic.domain.Publicity; -import com.mapbefine.mapbefine.topic.dto.request.TopicCreateRequestWithOutImage; -import com.mapbefine.mapbefine.topic.dto.request.TopicMergeRequest; +import com.mapbefine.mapbefine.topic.dto.request.TopicCreateRequestWithoutImage; +import com.mapbefine.mapbefine.topic.dto.request.TopicMergeRequestWithoutImage; import com.mapbefine.mapbefine.topic.dto.request.TopicUpdateRequest; import com.mapbefine.mapbefine.topic.dto.response.TopicDetailResponse; import com.mapbefine.mapbefine.topic.dto.response.TopicResponse; import java.io.File; import java.time.LocalDateTime; import java.util.List; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.boot.test.mock.mockito.MockBean; @@ -50,11 +51,22 @@ class TopicControllerTest extends RestDocsIntegration { // TODO: 2023/07/25 Imag LocalDateTime.now() )); + private File mockFile; + @MockBean private TopicCommandService topicCommandService; @MockBean private TopicQueryService topicQueryService; + @BeforeEach + void setUp() { + mockFile = new File( + getClass().getClassLoader() + .getResource("test.png") + .getPath() + ); + } + @Test @DisplayName("토픽 새로 생성") @@ -63,7 +75,7 @@ void create() throws Exception { File mockFile = new File(getClass().getClassLoader().getResource("test.png").getPath()); MultiValueMap param = new LinkedMultiValueMap<>(); - TopicCreateRequestWithOutImage request = new TopicCreateRequestWithOutImage( + TopicCreateRequestWithoutImage request = new TopicCreateRequestWithoutImage( "준팍의 안갈집", "준팍이 두번 다시 안갈집", Publicity.PUBLIC, @@ -78,30 +90,34 @@ void create() throws Exception { MockMvcRequestBuilders.post("/topics/new") .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) .contentType(MediaType.MULTIPART_FORM_DATA_VALUE) - .content(objectMapper.writeValueAsString(mockFile)) + .content(objectMapper.writeValueAsString(param)) ).andDo(restDocs.document()); } @Test @DisplayName("토픽 병합 생성") void mergeAndCreate() throws Exception { - given(topicCommandService.merge(any(), any())).willReturn(1L); - TopicMergeRequest topicMergeRequest = new TopicMergeRequest( + MultiValueMap param = new LinkedMultiValueMap<>(); + + + TopicMergeRequestWithoutImage request = new TopicMergeRequestWithoutImage( "준팍의 안갈집", - "https://map-befine-official.github.io/favicon.png", "준팍이 두번 다시 안갈집", Publicity.PUBLIC, PermissionType.ALL_MEMBERS, List.of(1L, 2L, 3L) ); + param.add("image", mockFile); + param.add("request", request); + mockMvc.perform( MockMvcRequestBuilders.post("/topics/merge") .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(topicMergeRequest)) + .contentType(MediaType.MULTIPART_FORM_DATA_VALUE) + .content(objectMapper.writeValueAsString(param)) ).andDo(restDocs.document()); }