From 61df878e88aa92943048768a69f514b1c3955b6f Mon Sep 17 00:00:00 2001 From: ChoiDongKuen Date: Fri, 19 Jan 2024 11:16:28 +0900 Subject: [PATCH 1/7] release: 0.1.6 (#122) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 모임 삭제 기능 구현 (#102) * refactor: 기획 변경에 따른 회원 카드 등록 API 리팩토링 (#105) * refactor: sql 변경 (#104) * refactor: User 클래스 필드명 변경 (#104) * refactor: API 변경에 따른 코드 리팩토링 (#104) * refactor: 불필요한 getId() 제거 ( 피드백 반영 ) (#104) * feat: 유효성 검증 관련 에러 일괄 처리을 위한 메소드 추가 (#104) * test: API 변경에 따른 통합 테스트 수정 및 단위 테스트 구현 (#104) * refactor: UserMeGetResponse 필드 변경 (#104) * fix: CI 에러 수정 (#104) * refactor: 피드백 반영 (#104) * fix: CI 에러 수정 (#104) * refactor: JWT 에러 응답 리팩토링 및 인가 인증 예최 처리 코드 수정 (#103) * refactor: 기타 코드 리팩토링( 피드백 반영 ) (#101) * chore: JWT 관련 의존성 변경 (#101) * refactor: JWT 및 인증 관련 로직 리팩토링 (#101) * test: 테스트 코드 및 설정 관련 변경(#101) * refactor: ObjectMapper Autowired 로 주입 (#101) * refactor: AuthService 반환 타입 Optional -> User 변경 (#101) * fix: V6_create_users_interests.sql 추가 (#109) (#110) * refactor: 회원 카드 등록시 JWT 정보도 함께 반환하도록 수정 (#114) * chore : application.properties 에 jwt 관련 설정 값 추가 (#113) * refactor : userService 회원 카드 등록 로직 수정 (#113) * feat : UserRegisterResponse 필드 추가(#113) * test: API 변경에 따른 테스트 관련 코드 수정 (#113) * fix: 소셜 로그인 관련 500 에러 수정 및 OAuth 로직 일부 개선 (#112) * fix: OAuthLoginController @RestController 어노테이션 추가(나는 바보..) 및 favicon 관련 임시 컨트롤러 생성 (#111) * refactor : 설정 yml 리팩토링 (#111) * refactor : SecurityConfig 리팩토링 (#111) * refactor : 기타 OAuth 관련 로직 리팩토링(#111) * refactor : Cors 허용 주소 임시 전부 허용 (#111) * fix : SonarCloud 오류 수정 (#111) * fix : SonarCloud 오류 수정 (#111) * feat: 위치 기반 API 구현 및 테스트 (#108) * refactor: 기타 코드 리팩토링( 피드백 반영 ) (#101) * chore: JWT 관련 의존성 변경 (#101) * refactor: JWT 및 인증 관련 로직 리팩토링 (#101) * test: 테스트 코드 및 설정 관련 변경(#101) * refactor: ObjectMapper Autowired 로 주입 (#101) * refactor: AuthService 반환 타입 Optional -> User 변경 (#101) * refactor: application.properties redis.port 변경 (#91) * refactor: 기존 Redis 설정 리팩토링 및 추가 구현 (#91) * feat: 유저 위치 기반 관련 DTO 및 VO 구현 (#91) * feat: 유저 위치 기반 API 구현 (#91) * test: 유저 위치 기반 통합 테스트 관련 클래스 구현 및 테스트 (#91) * test: redis port 변경에 따른 테스트 수정 (#91) * fix: CI 에러 수정 (#91) * fix: sonarCloud 에러 수정 (#91) * refactor: 리뷰 반영 (#108) * build: Gatling 세팅 (#115) * feat: 부하테스트 툴 Gatling을 세팅한다 * feat: 예시 코드를 작성한다 * refactor: Sample 코드의 이름을 변경한다 * feat: 내 정보 조회에 Oauth 정보 추가 (#119) * feat: 내 정보 조회에 Oauth 정보 추가 * test: Oauth 정보 추가에 따른 테스트 변경 * fix: 모임 목록 조회 조건에 활동 지역 조건이 안걸리는 버그 수정 (#121) --------- Co-authored-by: ddingmin Co-authored-by: xb205 <62425964+devxb@users.noreply.github.com> --- build.gradle | 2 + gradle.properties | 4 + gradle/gatling.gradle | 12 ++ src/gatling/java/protocol/Protocol.java | 17 +++ .../java/simulation/SimulationSample.java | 27 ++++ src/gatling/resources/gatling.conf | 127 ++++++++++++++++++ src/gatling/resources/recorder.conf | 56 ++++++++ .../meeting/controller/MeetingController.java | 2 +- .../meeting/service/MeetingService.java | 2 +- .../domain/response/UserMeGetResponse.java | 28 +++- .../java/net/teumteum/integration/Api.java | 8 ++ .../integration/UserIntegrationTest.java | 2 +- 12 files changed, 278 insertions(+), 9 deletions(-) create mode 100644 gradle/gatling.gradle create mode 100644 src/gatling/java/protocol/Protocol.java create mode 100644 src/gatling/java/simulation/SimulationSample.java create mode 100644 src/gatling/resources/gatling.conf create mode 100644 src/gatling/resources/recorder.conf diff --git a/build.gradle b/build.gradle index c577d99c..a75ed0fb 100644 --- a/build.gradle +++ b/build.gradle @@ -4,6 +4,7 @@ plugins { id 'io.spring.dependency-management' id 'org.sonarqube' id 'io.sentry.jvm.gradle' + id "io.gatling.gradle" version "${gatlingVersion}" } apply from: "gradle/spring.gradle" @@ -13,6 +14,7 @@ apply from: "gradle/sonar.gradle" apply from: "gradle/db.gradle" apply from: "gradle/aws.gradle" apply from: "gradle/sentry.gradle" +apply from: "gradle/gatling.gradle" allprojects { diff --git a/gradle.properties b/gradle.properties index 3be9c45d..d6f71211 100644 --- a/gradle.properties +++ b/gradle.properties @@ -23,3 +23,7 @@ sentryVersion=4.1.1 springCloudAwsVersion=3.1.0 ### JWT ### jwtVersion=0.11.5 +### Gatling ### +gatlingVersion=3.9.5.6 +### Data faker ### +datafakerVersion=2.0.2 diff --git a/gradle/gatling.gradle b/gradle/gatling.gradle new file mode 100644 index 00000000..20c8ce93 --- /dev/null +++ b/gradle/gatling.gradle @@ -0,0 +1,12 @@ +gatling { + simulations = { + include "**/*Simulation.java" + } + jvmArgs = ["-server", "-Xms512M", "-Xmx2G"] + systemProperties = ['file.encoding': 'UTF-8'] + includeTestOutput = false +} + +dependencies { + gatlingImplementation "net.datafaker:datafaker:${datafakerVersion}" +} diff --git a/src/gatling/java/protocol/Protocol.java b/src/gatling/java/protocol/Protocol.java new file mode 100644 index 00000000..b131c75a --- /dev/null +++ b/src/gatling/java/protocol/Protocol.java @@ -0,0 +1,17 @@ +package protocol; + +import io.gatling.javaapi.http.HttpDsl; +import io.gatling.javaapi.http.HttpProtocolBuilder; + +public class Protocol { + + private static final String USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:16.0) Gecko/20100101 Firefox/16.0"; + + public static final HttpProtocolBuilder httpProtocol = HttpDsl.http.baseUrl("https://api.teum.org") + .header("Content-Type", "application/json") + .userAgentHeader(USER_AGENT); + + private Protocol() { + throw new UnsupportedOperationException("Cannot invoke constructor \"protocol.Protocol()\""); + } +} diff --git a/src/gatling/java/simulation/SimulationSample.java b/src/gatling/java/simulation/SimulationSample.java new file mode 100644 index 00000000..c5f90cfd --- /dev/null +++ b/src/gatling/java/simulation/SimulationSample.java @@ -0,0 +1,27 @@ +package simulation; + +import static io.gatling.javaapi.core.CoreDsl.rampUsers; +import static io.gatling.javaapi.core.CoreDsl.scenario; +import static io.gatling.javaapi.http.HttpDsl.http; +import static io.gatling.javaapi.http.HttpDsl.status; + +import io.gatling.javaapi.core.ScenarioBuilder; +import io.gatling.javaapi.core.Simulation; +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import protocol.Protocol; + +public class SimulationSample extends Simulation { + + private final ScenarioBuilder scn = scenario(this.getClass().getSimpleName()) + .exec(http("get user") + .get("/users/1") + .check(status().is(200)) + ); + + { + setUp( + scn.injectOpen(rampUsers(10).during(Duration.of(10, ChronoUnit.MINUTES))) + ).protocols(Protocol.httpProtocol); + } +} diff --git a/src/gatling/resources/gatling.conf b/src/gatling/resources/gatling.conf new file mode 100644 index 00000000..6c7684f0 --- /dev/null +++ b/src/gatling/resources/gatling.conf @@ -0,0 +1,127 @@ +######################### +# Gatling Configuration # +######################### + +# This file contains all the settings configurable for Gatling with their default values + +gatling { + core { + #outputDirectoryBaseName = "" # The prefix for each simulation result folder (then suffixed by the report generation timestamp) + #runDescription = "" # The description for this simulation run, displayed in each report + #encoding = "utf-8" # Encoding to use throughout Gatling for file and string manipulation + #simulationClass = "" # The FQCN of the simulation to run (when used in conjunction with noReports, the simulation for which assertions will be validated) + #elFileBodiesCacheMaxCapacity = 200 # Cache size for request body EL templates, set to 0 to disable + #rawFileBodiesCacheMaxCapacity = 200 # Cache size for request body raw files, set to 0 to disable + #rawFileBodiesInMemoryMaxSize = 1000 # Max bite size of raw files to be cached in memory + #pebbleFileBodiesCacheMaxCapacity = 200 # Cache size for request body Pebble templates, set to 0 to disable + #feederAdaptiveLoadModeThreshold = 100 # File size threshold (in MB). Below load eagerly in memory, above use batch mode with default buffer size + #shutdownTimeout = 10000 # Milliseconds to wait for the actor system to shutdown + extract { + regex { + #cacheMaxCapacity = 200 # Cache size for the compiled regexes, set to 0 to disable caching + } + xpath { + #cacheMaxCapacity = 200 # Cache size for the compiled XPath queries, set to 0 to disable caching + } + jsonPath { + #cacheMaxCapacity = 200 # Cache size for the compiled jsonPath queries, set to 0 to disable caching + } + css { + #cacheMaxCapacity = 200 # Cache size for the compiled CSS selectors queries, set to 0 to disable caching + } + } + directory { + #simulations = "" # If set, directory where simulation classes are located + #resources = "" # If set, directory where resources, such as feeder files and request bodies, are located + #reportsOnly = "" # If set, name of report folder to look for in order to generate its report + #binaries = "" # If set, name of the folder where compiles classes are located: Defaults to GATLING_HOME/target. + #results = results # Name of the folder where all reports folder are located + } + } + socket { + #connectTimeout = 10000 # Timeout in millis for establishing a TCP socket + #tcpNoDelay = true + #soKeepAlive = false # if TCP keepalive configured at OS level should be used + #soReuseAddress = false + } + netty { + #useNativeTransport = true # if Netty Linux native transport should be used instead of Java NIO + #useIoUring = false # if io_uring should be used instead of epoll if available + #allocator = "pooled" # switch to unpooled for unpooled ByteBufAllocator + #maxThreadLocalCharBufferSize = 200000 # Netty's default is 16k + } + ssl { + #useOpenSsl = true # if OpenSSL should be used instead of JSSE (only the latter can be debugged with -Djavax.net.debug=ssl) + #useOpenSslFinalizers = false # if OpenSSL contexts should be freed with Finalizer or if using RefCounted is fine + #handshakeTimeout = 10000 # TLS handshake timeout in millis + #useInsecureTrustManager = true # Use an insecure TrustManager that trusts all server certificates + #enabledProtocols = [] # Array of enabled protocols for HTTPS, if empty use Netty's defaults + #enabledCipherSuites = [] # Array of enabled cipher suites for HTTPS, if empty enable all available ciphers + #sessionCacheSize = 0 # SSLSession cache size, set to 0 to use JDK's default + #sessionTimeout = 0 # SSLSession timeout in seconds, set to 0 to use JDK's default (24h) + #enableSni = true # When set to true, enable Server Name indication (SNI) + keyStore { + #type = "" # Type of SSLContext's KeyManagers store, possible values are jks and p12 + #file = "" # Location of SSLContext's KeyManagers store + #password = "" # Password for SSLContext's KeyManagers store + #algorithm = "" # Algorithm used SSLContext's KeyManagers store, typically RSA + } + trustStore { + #type = "" # Type of SSLContext's TrustManagers store, possible values are jks and p12 + #file = "" # Location of SSLContext's TrustManagers store + #password = "" # Password for SSLContext's TrustManagers store + #algorithm = "" # Algorithm used by SSLContext's TrustManagers store, typically RSA + } + } + charting { + #noReports = false # When set to true, don't generate HTML reports + #maxPlotPerSeries = 1000 # Number of points per graph in Gatling reports + #useGroupDurationMetric = false # Switch group timings from cumulated response time to group duration. + indicators { + #lowerBound = 800 # Lower bound for the requests' response time to track in the reports and the console summary + #higherBound = 1200 # Higher bound for the requests' response time to track in the reports and the console summary + #percentile1 = 50 # Value for the 1st percentile to track in the reports, the console summary and Graphite + #percentile2 = 75 # Value for the 2nd percentile to track in the reports, the console summary and Graphite + #percentile3 = 95 # Value for the 3rd percentile to track in the reports, the console summary and Graphite + #percentile4 = 99 # Value for the 4th percentile to track in the reports, the console summary and Graphite + } + } + http { + #fetchedCssCacheMaxCapacity = 200 # Cache size for CSS parsed content, set to 0 to disable + #fetchedHtmlCacheMaxCapacity = 200 # Cache size for HTML parsed content, set to 0 to disable + #perUserCacheMaxCapacity = 200 # Per virtual user cache size, set to 0 to disable + #warmUpUrl = "https://gatling.io" # The URL to use to warm-up the HTTP stack (blank means disabled) + #pooledConnectionIdleTimeout = 60000 # Timeout in millis for a connection to stay idle in the pool + #requestTimeout = 60000 # Timeout in millis for performing an HTTP request + #enableHostnameVerification = false # When set to true, enable hostname verification: SSLEngine.setHttpsEndpointIdentificationAlgorithm("HTTPS") + dns { + #queryTimeout = 5000 # Timeout in millis of each DNS query in millis + #maxQueriesPerResolve = 6 # Maximum allowed number of DNS queries for a given name resolution + } + } + jms { + #replyTimeoutScanPeriod = 1000 # scan period for timed out reply messages + } + data { + #writers = [console, file] # The list of DataWriters to which Gatling write simulation data (currently supported : console, file, graphite) + console { + #light = false # When set to true, displays a light version without detailed request stats + #writePeriod = 5 # Write interval, in seconds + } + file { + #bufferSize = 8192 # FileDataWriter's internal data buffer size, in bytes + } + leak { + #noActivityTimeout = 30 # Period, in seconds, for which Gatling may have no activity before considering a leak may be happening + } + graphite { + #light = false # only send the all* stats + #host = "localhost" # The host where the Carbon server is located + #port = 2003 # The port to which the Carbon server listens to (2003 is default for plaintext, 2004 is default for pickle) + #protocol = "tcp" # The protocol used to send data to Carbon (currently supported : "tcp", "udp") + #rootPathPrefix = "gatling" # The common prefix of all metrics sent to Graphite + #bufferSize = 8192 # Internal data buffer size, in bytes + #writePeriod = 1 # Write period, in seconds + } + } +} diff --git a/src/gatling/resources/recorder.conf b/src/gatling/resources/recorder.conf new file mode 100644 index 00000000..60fbdd11 --- /dev/null +++ b/src/gatling/resources/recorder.conf @@ -0,0 +1,56 @@ +recorder { + core { + # mode = "Proxy" + # encoding = "utf-8" # The encoding used for reading/writing request bodies and the generated simulation + # simulationsFolder = "" # The folder where generated simulation will be generated + # package = "" # The package's name of the generated simulation + # className = "RecordedSimulation" # The name of the generated Simulation class + # thresholdForPauseCreation = 100 # The minimum time, in milliseconds, that must pass between requests to trigger a pause creation + # saveConfig = false # When set to true, the configuration from the Recorder GUI overwrites this configuration + # headless = false # When set to true, run the Recorder in headless mode instead of the GUI + # harFilePath = "" # The path of the HAR file to convert + # format = "java8" # java8|java11|java17|kotlin|scala + } + filters { + # enable = false # If filters are enabled + # allowList = [] # The list of ressources patterns that are part of the Recorder's allow list + # denyList = [] # The list of ressources patterns that are part of the Recorder's deny list + } + http { + # automaticReferer = true # When set to false, write the referer + enable 'disableAutoReferer' in the generated simulation + # followRedirect = true # When set to false, write redirect requests + enable 'disableFollowRedirect' in the generated simulation + # removeCacheHeaders = true # When set to true, removes from the generated requests headers leading to request caching + # inferHtmlResources = true # When set to true, add inferred resources + set 'inferHtmlResources' with the configured blacklist/whitelist in the generated simulation + # checkResponseBodies = false # When set to true, save response bodies as files and add raw checks in the generated simulation + # useSimulationAsPrefix = false # When set to true, use the simulation class name instead of 'request' as a prefix for http(s) requests + # useMethodAndUriAsPostfix = false # When set to true, use the HTTP method and the URI as a postfix for http(s) requests + } + proxy { + # port = 8000 # Local port used by Gatling's Proxy for HTTP/HTTPS + https { + # mode = "SelfSignedCertificate" # The selected "HTTPS mode" (currently supported : "SelfSignedCertificate", "ProvidedKeyStore", "CertificateAuthority") + keyStore { + # path = "" # The path of the custom key store + # password = "" # The password for this key store + # type = "JKS" # The type of the key store (currently supported: "JKS" and "PKCS#12") + } + certificateAuthority { + # certificatePath = "" # The path of the custom certificate + # privateKeyPath = "" # The certificate's private key path + } + } + outgoing { + # host = "" # The outgoing proxy's hostname + # username = "" # The username to use to connect to the outgoing proxy + # password = "" # The password corresponding to the user to use to connect to the outgoing proxy + # port = 0 # The HTTP port to use to connect to the outgoing proxy + # sslPort = 0 # If set, The HTTPS port to use to connect to the outgoing proxy + } + } + netty { + # maxInitialLineLength = 10000 # Maximum length of the initial line of the response (e.g. "HTTP/1.0 200 OK") + # maxHeaderSize = 20000 # Maximum size, in bytes, of each request's headers + # maxChunkSize = 8192 # Maximum length of the content or each chunk + # maxContentLength = 100000000 # Maximum length of the aggregated content of each response + } +} diff --git a/src/main/java/net/teumteum/meeting/controller/MeetingController.java b/src/main/java/net/teumteum/meeting/controller/MeetingController.java index bc087d78..b6539868 100644 --- a/src/main/java/net/teumteum/meeting/controller/MeetingController.java +++ b/src/main/java/net/teumteum/meeting/controller/MeetingController.java @@ -52,7 +52,7 @@ public MeetingResponse getMeetingById(@PathVariable("meetingId") Long meetingId) @GetMapping @ResponseStatus(HttpStatus.OK) - public PageDto getMeetingsOrderByDate( + public PageDto getMeetingsByCondition( Pageable pageable, @RequestParam(value = "isOpen") boolean isOpen, @RequestParam(value = "topic", required = false) Topic topic, diff --git a/src/main/java/net/teumteum/meeting/service/MeetingService.java b/src/main/java/net/teumteum/meeting/service/MeetingService.java index 554ee0f3..2bd005fa 100644 --- a/src/main/java/net/teumteum/meeting/service/MeetingService.java +++ b/src/main/java/net/teumteum/meeting/service/MeetingService.java @@ -83,7 +83,7 @@ public PageDto getMeetingsBySpecification(Pageable pageable, T if (topic != null) { spec = spec.and(MeetingSpecification.withTopic(topic)); } else if (meetingAreaStreet != null) { - spec.and(MeetingSpecification.withAreaStreet(meetingAreaStreet)); + spec = spec.and(MeetingSpecification.withAreaStreet(meetingAreaStreet)); } else if (participantUserId != null) { spec = spec.and(MeetingSpecification.withParticipantUserId(participantUserId)); } else if (searchWord != null) { diff --git a/src/main/java/net/teumteum/user/domain/response/UserMeGetResponse.java b/src/main/java/net/teumteum/user/domain/response/UserMeGetResponse.java index 99d827ef..d9ea035b 100644 --- a/src/main/java/net/teumteum/user/domain/response/UserMeGetResponse.java +++ b/src/main/java/net/teumteum/user/domain/response/UserMeGetResponse.java @@ -18,21 +18,37 @@ public record UserMeGetResponse( String status, String goal, Job job, + String oauthType, List interests ) { public static UserMeGetResponse of(User user) { - return new UserMeGetResponse(user.getId(), user.getName(), user.getBirth(), user.getCharacterId(), - user.getMannerTemperature(), user.getOauth().getAuthenticated(), user.getActivityArea(), user.getMbti(), - user.getStatus().name(), user.getGoal(), Job.of(user), user.getInterests()); + return new UserMeGetResponse( + user.getId(), + user.getName(), + user.getBirth(), + user.getCharacterId(), + user.getMannerTemperature(), + user.getOauth().getAuthenticated(), + user.getActivityArea(), + user.getMbti(), + user.getStatus().name(), + user.getGoal(), + Job.of(user), + user.getOauth().getAuthenticated().name(), + user.getInterests() + ); } - public record Job(String name, boolean certificated, @JsonProperty("class") String jobClass, String detailClass) { public static Job of(User user) { - return new Job(user.getJob().getName(), user.getJob().isCertificated(), user.getJob().getJobClass(), - user.getJob().getDetailJobClass()); + return new Job( + user.getJob().getName(), + user.getJob().isCertificated(), + user.getJob().getJobClass(), + user.getJob().getDetailJobClass() + ); } } } diff --git a/src/test/java/net/teumteum/integration/Api.java b/src/test/java/net/teumteum/integration/Api.java index 1d6f66e8..05d2945b 100644 --- a/src/test/java/net/teumteum/integration/Api.java +++ b/src/test/java/net/teumteum/integration/Api.java @@ -36,6 +36,14 @@ ResponseSpec getUser(String token, Long userId) { .exchange(); } + ResponseSpec getMe(String token) { + return webTestClient + .get() + .uri("/users/me") + .header(HttpHeaders.AUTHORIZATION, token) + .exchange(); + } + ResponseSpec getUsersById(String token, String userIds) { return webTestClient.get() .uri("/users?id=" + userIds) diff --git a/src/test/java/net/teumteum/integration/UserIntegrationTest.java b/src/test/java/net/teumteum/integration/UserIntegrationTest.java index b44f31e9..33c69d8d 100644 --- a/src/test/java/net/teumteum/integration/UserIntegrationTest.java +++ b/src/test/java/net/teumteum/integration/UserIntegrationTest.java @@ -124,7 +124,7 @@ void Return_my_info_if_valid_token_received() { var expected = UserMeGetResponse.of(me); // when - var result = api.getUser(VALID_TOKEN, me.getId()); + var result = api.getMe(VALID_TOKEN); // then Assertions.assertThat(result.expectStatus().isOk() From 4b6b6d998ea3113d72a7979f6860908109ee6d0a Mon Sep 17 00:00:00 2001 From: ChoiDongKuen Date: Fri, 19 Jan 2024 16:44:13 +0900 Subject: [PATCH 2/7] release: 0.1.7 (#125) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 모임 삭제 기능 구현 (#102) * refactor: 기획 변경에 따른 회원 카드 등록 API 리팩토링 (#105) * refactor: sql 변경 (#104) * refactor: User 클래스 필드명 변경 (#104) * refactor: API 변경에 따른 코드 리팩토링 (#104) * refactor: 불필요한 getId() 제거 ( 피드백 반영 ) (#104) * feat: 유효성 검증 관련 에러 일괄 처리을 위한 메소드 추가 (#104) * test: API 변경에 따른 통합 테스트 수정 및 단위 테스트 구현 (#104) * refactor: UserMeGetResponse 필드 변경 (#104) * fix: CI 에러 수정 (#104) * refactor: 피드백 반영 (#104) * fix: CI 에러 수정 (#104) * refactor: JWT 에러 응답 리팩토링 및 인가 인증 예최 처리 코드 수정 (#103) * refactor: 기타 코드 리팩토링( 피드백 반영 ) (#101) * chore: JWT 관련 의존성 변경 (#101) * refactor: JWT 및 인증 관련 로직 리팩토링 (#101) * test: 테스트 코드 및 설정 관련 변경(#101) * refactor: ObjectMapper Autowired 로 주입 (#101) * refactor: AuthService 반환 타입 Optional -> User 변경 (#101) * fix: V6_create_users_interests.sql 추가 (#109) (#110) * refactor: 회원 카드 등록시 JWT 정보도 함께 반환하도록 수정 (#114) * chore : application.properties 에 jwt 관련 설정 값 추가 (#113) * refactor : userService 회원 카드 등록 로직 수정 (#113) * feat : UserRegisterResponse 필드 추가(#113) * test: API 변경에 따른 테스트 관련 코드 수정 (#113) * fix: 소셜 로그인 관련 500 에러 수정 및 OAuth 로직 일부 개선 (#112) * fix: OAuthLoginController @RestController 어노테이션 추가(나는 바보..) 및 favicon 관련 임시 컨트롤러 생성 (#111) * refactor : 설정 yml 리팩토링 (#111) * refactor : SecurityConfig 리팩토링 (#111) * refactor : 기타 OAuth 관련 로직 리팩토링(#111) * refactor : Cors 허용 주소 임시 전부 허용 (#111) * fix : SonarCloud 오류 수정 (#111) * fix : SonarCloud 오류 수정 (#111) * feat: 위치 기반 API 구현 및 테스트 (#108) * refactor: 기타 코드 리팩토링( 피드백 반영 ) (#101) * chore: JWT 관련 의존성 변경 (#101) * refactor: JWT 및 인증 관련 로직 리팩토링 (#101) * test: 테스트 코드 및 설정 관련 변경(#101) * refactor: ObjectMapper Autowired 로 주입 (#101) * refactor: AuthService 반환 타입 Optional -> User 변경 (#101) * refactor: application.properties redis.port 변경 (#91) * refactor: 기존 Redis 설정 리팩토링 및 추가 구현 (#91) * feat: 유저 위치 기반 관련 DTO 및 VO 구현 (#91) * feat: 유저 위치 기반 API 구현 (#91) * test: 유저 위치 기반 통합 테스트 관련 클래스 구현 및 테스트 (#91) * test: redis port 변경에 따른 테스트 수정 (#91) * fix: CI 에러 수정 (#91) * fix: sonarCloud 에러 수정 (#91) * refactor: 리뷰 반영 (#108) * build: Gatling 세팅 (#115) * feat: 부하테스트 툴 Gatling을 세팅한다 * feat: 예시 코드를 작성한다 * refactor: Sample 코드의 이름을 변경한다 * feat: 내 정보 조회에 Oauth 정보 추가 (#119) * feat: 내 정보 조회에 Oauth 정보 추가 * test: Oauth 정보 추가에 따른 테스트 변경 * fix: 모임 목록 조회 조건에 활동 지역 조건이 안걸리는 버그 수정 (#121) * feat: 회원 탈퇴 사유 저장 로직 추가 및 cors 관련 재설정 (#123) * refactor: SecurityConfig Cors 관련 전체 허용으로 변경 및 회원 카드 등록 API permitAll 로 변경 (#117) * refactor: 기존 엔티티 관련 리팩토링 (#117) * feat: WithdrawReason 생성 sql 추가 (#117) * feat: WithdrawReason 관련 도메인 구현 (#117) * feat: 회원 탈퇴 시 탈퇴 사유 기능 추가 (#117) * test: 회원 탈퇴 단위 테스트 (#117) * refactor: 회원 탈퇴 API POST 로 수정 (#117) * refactor: 회원탈퇴 관련 통합 테스트 수정 및 요청 DTO 수정 (#117) * refactor: meeting 엔티티명 다시 복구 - 모든 테이블에 `s` 붙이는 방향 논의 필요 (#117) * feat: withdraw_reason 테이블 이름 변경 (#117) * fix: CI 에러 해결 (#117) --------- Co-authored-by: ddingmin Co-authored-by: xb205 <62425964+devxb@users.noreply.github.com> --- .../core/security/SecurityConfig.java | 4 +- .../net/teumteum/meeting/domain/Meeting.java | 26 +++++++++---- .../teum_teum/service/TeumTeumService.java | 6 ++- .../user/controller/UserController.java | 8 ++-- .../java/net/teumteum/user/domain/User.java | 5 ++- .../teumteum/user/domain/WithdrawReason.java | 26 +++++++++++++ .../user/domain/WithdrawReasonRepository.java | 7 ++++ .../domain/request/UserWithdrawRequest.java | 22 +++++++++++ .../teumteum/user/service/UserService.java | 7 +++- .../migration/V7__create_withdraw_reason.sql | 8 ++++ .../migration/V8__rename_withdraw_reason.sql | 10 +++++ .../java/net/teumteum/integration/Api.java | 9 +++-- .../teumteum/integration/RequestFixture.java | 6 +++ .../integration/UserIntegrationTest.java | 9 +++-- .../user/controller/UserControllerTest.java | 34 +++++++++++++++-- .../unit/user/service/UserServiceTest.java | 38 +++++++++++++++++++ src/test/resources/schema.sql | 9 +++++ 17 files changed, 205 insertions(+), 29 deletions(-) create mode 100644 src/main/java/net/teumteum/user/domain/WithdrawReason.java create mode 100644 src/main/java/net/teumteum/user/domain/WithdrawReasonRepository.java create mode 100644 src/main/java/net/teumteum/user/domain/request/UserWithdrawRequest.java create mode 100644 src/main/resources/db/migration/V7__create_withdraw_reason.sql create mode 100644 src/main/resources/db/migration/V8__rename_withdraw_reason.sql diff --git a/src/main/java/net/teumteum/core/security/SecurityConfig.java b/src/main/java/net/teumteum/core/security/SecurityConfig.java index 17d77fe6..d9032047 100644 --- a/src/main/java/net/teumteum/core/security/SecurityConfig.java +++ b/src/main/java/net/teumteum/core/security/SecurityConfig.java @@ -49,7 +49,7 @@ public WebSecurityCustomizer webSecurityCustomizer() { SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.csrf(AbstractHttpConfigurer::disable).cors(cors -> cors.configurationSource(corsConfigurationSource())) .authorizeHttpRequests(request -> request.requestMatchers(PATTERNS).permitAll() - .requestMatchers(HttpMethod.POST, "/users/registers").permitAll() + .requestMatchers(HttpMethod.POST, "/users").permitAll() .anyRequest().authenticated()).httpBasic(AbstractHttpConfigurer::disable) .formLogin(AbstractHttpConfigurer::disable) .sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(STATELESS)) @@ -64,7 +64,7 @@ SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { @Bean CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); - config.addAllowedOrigin("http://localhost:3000"); + config.addAllowedOrigin("*"); config.addAllowedHeader("*"); config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")); config.addExposedHeader("Authorization"); diff --git a/src/main/java/net/teumteum/meeting/domain/Meeting.java b/src/main/java/net/teumteum/meeting/domain/Meeting.java index 847c6725..59dc9135 100644 --- a/src/main/java/net/teumteum/meeting/domain/Meeting.java +++ b/src/main/java/net/teumteum/meeting/domain/Meeting.java @@ -1,6 +1,21 @@ package net.teumteum.meeting.domain; -import jakarta.persistence.*; +import jakarta.persistence.Column; +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.PrePersist; +import java.time.LocalDateTime; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Set; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -8,16 +23,11 @@ import net.teumteum.core.entity.TimeBaseEntity; import org.springframework.util.Assert; -import java.time.LocalDateTime; -import java.util.HashSet; -import java.util.LinkedHashSet; -import java.util.Set; - -@Entity @Getter @Builder -@NoArgsConstructor @AllArgsConstructor +@Entity(name = "meeting") +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class Meeting extends TimeBaseEntity { @Id diff --git a/src/main/java/net/teumteum/teum_teum/service/TeumTeumService.java b/src/main/java/net/teumteum/teum_teum/service/TeumTeumService.java index 9dd1e21e..1963cd1c 100644 --- a/src/main/java/net/teumteum/teum_teum/service/TeumTeumService.java +++ b/src/main/java/net/teumteum/teum_teum/service/TeumTeumService.java @@ -73,11 +73,13 @@ private UserAroundLocationsResponse getUserAroundLocationsResponse(GeoResults> geoResult : Objects.requireNonNull(geoResults)) { - String userSavedTime = String.valueOf(geoResult.getContent().getName()).split(":")[1]; + String userSavedTime = String.valueOf(geoResult.getContent().getName()).split(":")[5]; long timestamp = Long.parseLong(userSavedTime); if (currentTime - timestamp < LOCATION_EXPIRATION.toMillis()) { - String userDataJson = String.valueOf(geoResult.getContent().getName()).split(":")[0]; + String savedUserLocation = String.valueOf(geoResult.getContent().getName()); + String userDataJson = savedUserLocation.substring(savedUserLocation.lastIndexOf(":") + 1); + UserData userData = null; try { userData = objectMapper.readValue(userDataJson, UserData.class); diff --git a/src/main/java/net/teumteum/user/controller/UserController.java b/src/main/java/net/teumteum/user/controller/UserController.java index 3b14dfd1..8d51bbf2 100644 --- a/src/main/java/net/teumteum/user/controller/UserController.java +++ b/src/main/java/net/teumteum/user/controller/UserController.java @@ -9,6 +9,7 @@ import net.teumteum.core.security.service.SecurityService; import net.teumteum.user.domain.request.UserRegisterRequest; import net.teumteum.user.domain.request.UserUpdateRequest; +import net.teumteum.user.domain.request.UserWithdrawRequest; import net.teumteum.user.domain.response.FriendsResponse; import net.teumteum.user.domain.response.InterestQuestionResponse; import net.teumteum.user.domain.response.UserGetResponse; @@ -20,7 +21,6 @@ import org.springframework.validation.BindingResult; import org.springframework.validation.ObjectError; import org.springframework.web.bind.MethodArgumentNotValidException; -import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -87,10 +87,10 @@ public InterestQuestionResponse getInterestQuestion(@RequestParam("user-id") Lis return userService.getInterestQuestionByUserIds(userIds, balance); } - @DeleteMapping + @PostMapping("/withdraw") @ResponseStatus(HttpStatus.OK) - public void withdraw() { - userService.withdraw(getCurrentUserId()); + public void withdraw(@Valid @RequestBody UserWithdrawRequest request) { + userService.withdraw(request, getCurrentUserId()); } @PostMapping diff --git a/src/main/java/net/teumteum/user/domain/User.java b/src/main/java/net/teumteum/user/domain/User.java index 5943d2fb..d1b87d6b 100644 --- a/src/main/java/net/teumteum/user/domain/User.java +++ b/src/main/java/net/teumteum/user/domain/User.java @@ -15,6 +15,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @@ -24,9 +25,9 @@ import org.springframework.util.Assert; @Getter -@Entity(name = "users") -@NoArgsConstructor @AllArgsConstructor +@Entity(name = "users") +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class User extends TimeBaseEntity { @Id diff --git a/src/main/java/net/teumteum/user/domain/WithdrawReason.java b/src/main/java/net/teumteum/user/domain/WithdrawReason.java new file mode 100644 index 00000000..1f92e102 --- /dev/null +++ b/src/main/java/net/teumteum/user/domain/WithdrawReason.java @@ -0,0 +1,26 @@ +package net.teumteum.user.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import net.teumteum.core.entity.TimeBaseEntity; + +@Getter +@AllArgsConstructor +@Entity(name = "withdraw_reasons") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class WithdrawReason extends TimeBaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "withdraw_reason", nullable = false) + private String reason; +} diff --git a/src/main/java/net/teumteum/user/domain/WithdrawReasonRepository.java b/src/main/java/net/teumteum/user/domain/WithdrawReasonRepository.java new file mode 100644 index 00000000..4ad51e66 --- /dev/null +++ b/src/main/java/net/teumteum/user/domain/WithdrawReasonRepository.java @@ -0,0 +1,7 @@ +package net.teumteum.user.domain; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface WithdrawReasonRepository extends JpaRepository { + +} diff --git a/src/main/java/net/teumteum/user/domain/request/UserWithdrawRequest.java b/src/main/java/net/teumteum/user/domain/request/UserWithdrawRequest.java new file mode 100644 index 00000000..04cab06b --- /dev/null +++ b/src/main/java/net/teumteum/user/domain/request/UserWithdrawRequest.java @@ -0,0 +1,22 @@ +package net.teumteum.user.domain.request; + + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import jakarta.validation.constraints.Size; +import java.util.List; +import net.teumteum.user.domain.WithdrawReason; + +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY) +public record UserWithdrawRequest( + @Size(min = 1, max = 3, message = "탈퇴 사유는 최소 1개, 최대 3개의 입력값입니다.") + List withdrawReasons +) { + + private static final Long IGNORE_ID = null; + + public List toEntity() { + return withdrawReasons.stream() + .map(withdrawReason -> new WithdrawReason(IGNORE_ID, withdrawReason)) + .toList(); + } +} diff --git a/src/main/java/net/teumteum/user/service/UserService.java b/src/main/java/net/teumteum/user/service/UserService.java index 808318bb..df469dec 100644 --- a/src/main/java/net/teumteum/user/service/UserService.java +++ b/src/main/java/net/teumteum/user/service/UserService.java @@ -10,8 +10,10 @@ import net.teumteum.user.domain.InterestQuestion; import net.teumteum.user.domain.User; import net.teumteum.user.domain.UserRepository; +import net.teumteum.user.domain.WithdrawReasonRepository; import net.teumteum.user.domain.request.UserRegisterRequest; import net.teumteum.user.domain.request.UserUpdateRequest; +import net.teumteum.user.domain.request.UserWithdrawRequest; import net.teumteum.user.domain.response.FriendsResponse; import net.teumteum.user.domain.response.InterestQuestionResponse; import net.teumteum.user.domain.response.UserGetResponse; @@ -28,6 +30,7 @@ public class UserService { private final UserRepository userRepository; + private final WithdrawReasonRepository withdrawReasonRepository; private final InterestQuestion interestQuestion; private final RedisService redisService; private final JwtService jwtService; @@ -71,11 +74,13 @@ public void addFriends(Long myId, Long friendId) { } @Transactional - public void withdraw(Long userId) { + public void withdraw(UserWithdrawRequest request, Long userId) { var existUser = getUser(userId); userRepository.delete(existUser); redisService.deleteData(String.valueOf(userId)); + + withdrawReasonRepository.saveAll(request.toEntity()); } @Transactional diff --git a/src/main/resources/db/migration/V7__create_withdraw_reason.sql b/src/main/resources/db/migration/V7__create_withdraw_reason.sql new file mode 100644 index 00000000..f2690253 --- /dev/null +++ b/src/main/resources/db/migration/V7__create_withdraw_reason.sql @@ -0,0 +1,8 @@ +create table if not exists withdraw_reason +( + id bigint not null auto_increment, + withdraw_reason varchar(30) not null, + created_at timestamp(6) not null, + updated_at timestamp(6) not null, + primary key (id) +); diff --git a/src/main/resources/db/migration/V8__rename_withdraw_reason.sql b/src/main/resources/db/migration/V8__rename_withdraw_reason.sql new file mode 100644 index 00000000..a21ba04c --- /dev/null +++ b/src/main/resources/db/migration/V8__rename_withdraw_reason.sql @@ -0,0 +1,10 @@ +drop table withdraw_reason; + +create table if not exists withdraw_reasons +( + id bigint not null auto_increment, + withdraw_reason varchar(30) not null, + created_at timestamp(6) not null, + updated_at timestamp(6) not null, + primary key (id) +); diff --git a/src/test/java/net/teumteum/integration/Api.java b/src/test/java/net/teumteum/integration/Api.java index 05d2945b..2b4c498e 100644 --- a/src/test/java/net/teumteum/integration/Api.java +++ b/src/test/java/net/teumteum/integration/Api.java @@ -6,6 +6,7 @@ import net.teumteum.teum_teum.domain.request.UserLocationRequest; import net.teumteum.user.domain.request.UserRegisterRequest; import net.teumteum.user.domain.request.UserUpdateRequest; +import net.teumteum.user.domain.request.UserWithdrawRequest; import org.springframework.boot.test.context.TestComponent; import org.springframework.context.ApplicationContext; import org.springframework.data.domain.Pageable; @@ -137,10 +138,12 @@ ResponseSpec reissueJwt(String accessToken, String refreshToken) { .exchange(); } - ResponseSpec withdrawUser(String accessToken) { - return webTestClient.delete() - .uri("/users") + ResponseSpec withdrawUser(String accessToken, UserWithdrawRequest request) { + return webTestClient + .post() + .uri("/users/withdraw") .header(HttpHeaders.AUTHORIZATION, accessToken) + .bodyValue(request) .exchange(); } diff --git a/src/test/java/net/teumteum/integration/RequestFixture.java b/src/test/java/net/teumteum/integration/RequestFixture.java index 28a35222..5f8b1dc0 100644 --- a/src/test/java/net/teumteum/integration/RequestFixture.java +++ b/src/test/java/net/teumteum/integration/RequestFixture.java @@ -1,5 +1,6 @@ package net.teumteum.integration; +import java.util.List; import java.util.UUID; import net.teumteum.core.security.Authenticated; import net.teumteum.user.domain.User; @@ -8,9 +9,14 @@ import net.teumteum.user.domain.request.UserRegisterRequest.Terms; import net.teumteum.user.domain.request.UserUpdateRequest; import net.teumteum.user.domain.request.UserUpdateRequest.NewJob; +import net.teumteum.user.domain.request.UserWithdrawRequest; public class RequestFixture { + public static UserWithdrawRequest userWithdrawRequest(List withdrawReasons) { + return new UserWithdrawRequest(withdrawReasons); + } + public static UserUpdateRequest userUpdateRequest(User user) { return new UserUpdateRequest(user.getId(), "new_name", user.getBirth(), user.getCharacterId(), user.getActivityArea(), user.getMbti(), user.getStatus().name(), user.getGoal(), newJob(user), diff --git a/src/test/java/net/teumteum/integration/UserIntegrationTest.java b/src/test/java/net/teumteum/integration/UserIntegrationTest.java index 33c69d8d..5ba74373 100644 --- a/src/test/java/net/teumteum/integration/UserIntegrationTest.java +++ b/src/test/java/net/teumteum/integration/UserIntegrationTest.java @@ -240,9 +240,10 @@ void Withdraw_user_info_api() { loginContext.setUserId(me.getId()); - // when & then + var request = RequestFixture.userWithdrawRequest(List.of("쓰지 않는 앱이에요", "오류가 생겨서 쓸 수 없어요")); - assertThatCode(() -> api.withdrawUser(VALID_TOKEN)) + // when & then + assertThatCode(() -> api.withdrawUser(VALID_TOKEN, request)) .doesNotThrowAnyException(); } @@ -252,8 +253,10 @@ void Return_500_error_if_user_not_exist() { // given repository.clearUserRepository(); + var request = RequestFixture.userWithdrawRequest(List.of("쓰지 않는 앱이에요", "오류가 생겨서 쓸 수 없어요")); + // when - var result = api.withdrawUser(VALID_TOKEN); + var result = api.withdrawUser(VALID_TOKEN, request); // then Assertions.assertThat(result.expectStatus().is5xxServerError() diff --git a/src/test/java/net/teumteum/unit/user/controller/UserControllerTest.java b/src/test/java/net/teumteum/unit/user/controller/UserControllerTest.java index 1b7d893e..61c5ad2d 100644 --- a/src/test/java/net/teumteum/unit/user/controller/UserControllerTest.java +++ b/src/test/java/net/teumteum/unit/user/controller/UserControllerTest.java @@ -12,6 +12,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.List; import net.teumteum.core.security.SecurityConfig; import net.teumteum.core.security.filter.JwtAuthenticationFilter; import net.teumteum.core.security.service.JwtService; @@ -22,6 +24,7 @@ import net.teumteum.user.domain.User; import net.teumteum.user.domain.UserFixture; import net.teumteum.user.domain.request.UserRegisterRequest; +import net.teumteum.user.domain.request.UserWithdrawRequest; import net.teumteum.user.domain.response.UserRegisterResponse; import net.teumteum.user.service.UserService; import org.junit.jupiter.api.BeforeEach; @@ -35,7 +38,6 @@ import org.springframework.context.annotation.FilterType; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.web.servlet.MockMvc; -import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper; @WebMvcTest(value = UserController.class, excludeFilters = {@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = SecurityConfig.class), @@ -48,8 +50,10 @@ public class UserControllerTest { @Autowired - private MockMvc mockMvc; + ObjectMapper objectMapper; + @Autowired + private MockMvc mockMvc; @MockBean private UserService userService; @@ -79,7 +83,7 @@ void Register_user_card_with_201_created() throws Exception { // when & then mockMvc.perform(post("/users") - .content(new ObjectMapper().writeValueAsString(request)) + .content(objectMapper.writeValueAsString(request)) .contentType(APPLICATION_JSON) .with(csrf()) .header(AUTHORIZATION, VALID_ACCESS_TOKEN)) @@ -98,7 +102,7 @@ void Register_user_card_with_400_bad_request() throws Exception { // when // then mockMvc.perform(post("/users") - .content(new ObjectMapper().writeValueAsString(request)) + .content(objectMapper.writeValueAsString(request)) .contentType(APPLICATION_JSON) .with(csrf()) .header(AUTHORIZATION, VALID_ACCESS_TOKEN)) @@ -107,4 +111,26 @@ void Register_user_card_with_400_bad_request() throws Exception { .andExpect(jsonPath("$.message").isNotEmpty()); } } + + @Nested + @DisplayName("회원 탈퇴 API는") + class Withdraw_user_api_unit { + + @Test + @DisplayName("회원 탈퇴 사유와 회원 탈퇴 요청이 들어오면, 탈퇴를 진행하고 200 OK을 반환한다.") + void Withdraw_user_with_200_ok() throws Exception { + // given + UserWithdrawRequest request + = RequestFixture.userWithdrawRequest(List.of("쓰지 않는 앱이에요", "오류가 생겨서 쓸 수 없어요")); + + // when & then + mockMvc.perform(post("/users/withdraw") + .content(new ObjectMapper().writeValueAsString(request)) + .contentType(APPLICATION_JSON) + .with(csrf()) + .header(AUTHORIZATION, VALID_ACCESS_TOKEN)) + .andDo(print()) + .andExpect(status().isOk()); + } + } } diff --git a/src/test/java/net/teumteum/unit/user/service/UserServiceTest.java b/src/test/java/net/teumteum/unit/user/service/UserServiceTest.java index 87ffb254..f3dc6295 100644 --- a/src/test/java/net/teumteum/unit/user/service/UserServiceTest.java +++ b/src/test/java/net/teumteum/unit/user/service/UserServiceTest.java @@ -5,8 +5,15 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import java.util.List; +import java.util.Optional; import net.teumteum.auth.domain.response.TokenResponse; import net.teumteum.core.security.service.JwtService; import net.teumteum.core.security.service.RedisService; @@ -14,7 +21,9 @@ import net.teumteum.user.domain.User; import net.teumteum.user.domain.UserFixture; import net.teumteum.user.domain.UserRepository; +import net.teumteum.user.domain.WithdrawReasonRepository; import net.teumteum.user.domain.request.UserRegisterRequest; +import net.teumteum.user.domain.request.UserWithdrawRequest; import net.teumteum.user.domain.response.UserRegisterResponse; import net.teumteum.user.service.UserService; import org.junit.jupiter.api.BeforeEach; @@ -36,6 +45,9 @@ public class UserServiceTest { @Mock UserRepository userRepository; + @Mock + WithdrawReasonRepository withdrawReasonRepository; + @Mock RedisService redisService; @@ -89,4 +101,30 @@ void If_user_already_exist_register_user_card_fail() { .hasMessage("일치하는 user 가 이미 존재합니다."); } } + + @Nested + @DisplayName("유저 탈퇴 API는") + class Withdraw_user_api_unit { + + @Test + @DisplayName("유효한 유저 회원 탈퇴 요청이 들어오는 경우, 회원을 탈퇴하고 탈퇴 사유 데이터를 저장한다.") + void If_valid_user_withdraw_request_withdraw_user() { + // given + UserWithdrawRequest request + = RequestFixture.userWithdrawRequest(List.of("쓰지 않는 앱이에요", "오류가 생겨서 쓸 수 없어요")); + + given(userRepository.findById(anyLong())) + .willReturn(Optional.ofNullable(user)); + + doNothing().when(userRepository).delete(any()); + + doNothing().when(redisService).deleteData(anyString()); + // when + userService.withdraw(request, user.getId()); + // then + verify(userRepository, times(1)).findById(anyLong()); + verify(redisService, times(1)).deleteData(anyString()); + verify(withdrawReasonRepository, times(1)).saveAll(any()); + } + } } diff --git a/src/test/resources/schema.sql b/src/test/resources/schema.sql index 1304b85a..41f35219 100644 --- a/src/test/resources/schema.sql +++ b/src/test/resources/schema.sql @@ -68,3 +68,12 @@ create table if not exists users_friends friends bigint not null, foreign key (users_id) references users (id) ); + +create table if not exists withdraw_reasons +( + id bigint not null auto_increment, + withdraw_reason varchar(30) not null, + created_at timestamp(6) not null, + updated_at timestamp(6) not null, + primary key (id) +); From 126e9347d434055eb2ed61eb64678af73cdc9ca2 Mon Sep 17 00:00:00 2001 From: ddingmin Date: Sat, 20 Jan 2024 01:24:42 +0900 Subject: [PATCH 3/7] release: 0.1.7 (#128) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 모임 삭제 기능 구현 (#102) * refactor: 기획 변경에 따른 회원 카드 등록 API 리팩토링 (#105) * refactor: sql 변경 (#104) * refactor: User 클래스 필드명 변경 (#104) * refactor: API 변경에 따른 코드 리팩토링 (#104) * refactor: 불필요한 getId() 제거 ( 피드백 반영 ) (#104) * feat: 유효성 검증 관련 에러 일괄 처리을 위한 메소드 추가 (#104) * test: API 변경에 따른 통합 테스트 수정 및 단위 테스트 구현 (#104) * refactor: UserMeGetResponse 필드 변경 (#104) * fix: CI 에러 수정 (#104) * refactor: 피드백 반영 (#104) * fix: CI 에러 수정 (#104) * refactor: JWT 에러 응답 리팩토링 및 인가 인증 예최 처리 코드 수정 (#103) * refactor: 기타 코드 리팩토링( 피드백 반영 ) (#101) * chore: JWT 관련 의존성 변경 (#101) * refactor: JWT 및 인증 관련 로직 리팩토링 (#101) * test: 테스트 코드 및 설정 관련 변경(#101) * refactor: ObjectMapper Autowired 로 주입 (#101) * refactor: AuthService 반환 타입 Optional -> User 변경 (#101) * fix: V6_create_users_interests.sql 추가 (#109) (#110) * refactor: 회원 카드 등록시 JWT 정보도 함께 반환하도록 수정 (#114) * chore : application.properties 에 jwt 관련 설정 값 추가 (#113) * refactor : userService 회원 카드 등록 로직 수정 (#113) * feat : UserRegisterResponse 필드 추가(#113) * test: API 변경에 따른 테스트 관련 코드 수정 (#113) * fix: 소셜 로그인 관련 500 에러 수정 및 OAuth 로직 일부 개선 (#112) * fix: OAuthLoginController @RestController 어노테이션 추가(나는 바보..) 및 favicon 관련 임시 컨트롤러 생성 (#111) * refactor : 설정 yml 리팩토링 (#111) * refactor : SecurityConfig 리팩토링 (#111) * refactor : 기타 OAuth 관련 로직 리팩토링(#111) * refactor : Cors 허용 주소 임시 전부 허용 (#111) * fix : SonarCloud 오류 수정 (#111) * fix : SonarCloud 오류 수정 (#111) * feat: 위치 기반 API 구현 및 테스트 (#108) * refactor: 기타 코드 리팩토링( 피드백 반영 ) (#101) * chore: JWT 관련 의존성 변경 (#101) * refactor: JWT 및 인증 관련 로직 리팩토링 (#101) * test: 테스트 코드 및 설정 관련 변경(#101) * refactor: ObjectMapper Autowired 로 주입 (#101) * refactor: AuthService 반환 타입 Optional -> User 변경 (#101) * refactor: application.properties redis.port 변경 (#91) * refactor: 기존 Redis 설정 리팩토링 및 추가 구현 (#91) * feat: 유저 위치 기반 관련 DTO 및 VO 구현 (#91) * feat: 유저 위치 기반 API 구현 (#91) * test: 유저 위치 기반 통합 테스트 관련 클래스 구현 및 테스트 (#91) * test: redis port 변경에 따른 테스트 수정 (#91) * fix: CI 에러 수정 (#91) * fix: sonarCloud 에러 수정 (#91) * refactor: 리뷰 반영 (#108) * build: Gatling 세팅 (#115) * feat: 부하테스트 툴 Gatling을 세팅한다 * feat: 예시 코드를 작성한다 * refactor: Sample 코드의 이름을 변경한다 * feat: 내 정보 조회에 Oauth 정보 추가 (#119) * feat: 내 정보 조회에 Oauth 정보 추가 * test: Oauth 정보 추가에 따른 테스트 변경 * fix: 모임 목록 조회 조건에 활동 지역 조건이 안걸리는 버그 수정 (#121) * feat: 회원 탈퇴 사유 저장 로직 추가 및 cors 관련 재설정 (#123) * refactor: SecurityConfig Cors 관련 전체 허용으로 변경 및 회원 카드 등록 API permitAll 로 변경 (#117) * refactor: 기존 엔티티 관련 리팩토링 (#117) * feat: WithdrawReason 생성 sql 추가 (#117) * feat: WithdrawReason 관련 도메인 구현 (#117) * feat: 회원 탈퇴 시 탈퇴 사유 기능 추가 (#117) * test: 회원 탈퇴 단위 테스트 (#117) * refactor: 회원 탈퇴 API POST 로 수정 (#117) * refactor: 회원탈퇴 관련 통합 테스트 수정 및 요청 DTO 수정 (#117) * refactor: meeting 엔티티명 다시 복구 - 모든 테이블에 `s` 붙이는 방향 논의 필요 (#117) * feat: withdraw_reason 테이블 이름 변경 (#117) * fix: CI 에러 해결 (#117) * fix: 모임 규격 변경 (#127) --------- Co-authored-by: ChoiDongKuen Co-authored-by: xb205 <62425964+devxb@users.noreply.github.com> --- .../java/net/teumteum/meeting/domain/MeetingArea.java | 9 ++++++++- .../net/teumteum/integration/MeetingIntegrationTest.java | 7 +++---- .../java/net/teumteum/meeting/domain/MeetingFixture.java | 4 ++-- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/main/java/net/teumteum/meeting/domain/MeetingArea.java b/src/main/java/net/teumteum/meeting/domain/MeetingArea.java index 2cc8f4cd..9f5aac8d 100644 --- a/src/main/java/net/teumteum/meeting/domain/MeetingArea.java +++ b/src/main/java/net/teumteum/meeting/domain/MeetingArea.java @@ -6,6 +6,7 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.springframework.util.Assert; @Getter @Builder @@ -24,7 +25,13 @@ public class MeetingArea { private String addressDetail; public static MeetingArea of(String roadName, String addressDetail) { - return new MeetingArea(roadName.split(" ")[1], roadName, addressDetail); + return new MeetingArea(toMainStreet(roadName), roadName, addressDetail); + } + + private static String toMainStreet(String roadName) { + String[] roadNameSplit = roadName.split(" "); + Assert.isTrue(roadNameSplit.length >= 2, "잘못된 도로명 주소입니다. \"" + roadName + "\""); + return roadNameSplit[0] + " " + roadNameSplit[1]; } } diff --git a/src/test/java/net/teumteum/integration/MeetingIntegrationTest.java b/src/test/java/net/teumteum/integration/MeetingIntegrationTest.java index b02e288e..90fc7baf 100644 --- a/src/test/java/net/teumteum/integration/MeetingIntegrationTest.java +++ b/src/test/java/net/teumteum/integration/MeetingIntegrationTest.java @@ -1,5 +1,8 @@ package net.teumteum.integration; +import java.util.Collection; +import java.util.Comparator; +import java.util.stream.Stream; import net.teumteum.core.error.ErrorResponse; import net.teumteum.meeting.domain.Meeting; import net.teumteum.meeting.domain.Topic; @@ -16,10 +19,6 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; -import java.util.Collection; -import java.util.Comparator; -import java.util.stream.Stream; - @DisplayName("미팅 통합테스트의") class MeetingIntegrationTest extends IntegrationTest { diff --git a/src/test/java/net/teumteum/meeting/domain/MeetingFixture.java b/src/test/java/net/teumteum/meeting/domain/MeetingFixture.java index 352e5e61..052c542a 100644 --- a/src/test/java/net/teumteum/meeting/domain/MeetingFixture.java +++ b/src/test/java/net/teumteum/meeting/domain/MeetingFixture.java @@ -160,7 +160,7 @@ public static class MeetingBuilder { private String introduction = "모임에 대한 간단한 설명입니다."; @Builder.Default - private MeetingArea meetingArea = new MeetingArea("강남구", "서울특별시 강남대로 390", "강남역 11번 출구"); + private MeetingArea meetingArea = MeetingArea.of("서울 강남구 강남대로 390", "강남역 11번 출구"); @Builder.Default private int numberOfRecruits = 3; @@ -169,7 +169,7 @@ public static class MeetingBuilder { private LocalDateTime promiseDateTime = LocalDateTime.of(2024, 10, 10, 0, 0); @Builder.Default - private Set imageUrls = new HashSet<>(List.of("https://www.google.com")); + private Set imageUrls = new HashSet<>(List.of("/1/image.jpg", "/2/image.jpg")); } } From 49d361c12f41338b8cbbe3afc95f7813bd1cae5c Mon Sep 17 00:00:00 2001 From: ddingmin Date: Sat, 20 Jan 2024 01:48:21 +0900 Subject: [PATCH 4/7] release: 0.1.9 (#131) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 모임 삭제 기능 구현 (#102) * refactor: 기획 변경에 따른 회원 카드 등록 API 리팩토링 (#105) * refactor: sql 변경 (#104) * refactor: User 클래스 필드명 변경 (#104) * refactor: API 변경에 따른 코드 리팩토링 (#104) * refactor: 불필요한 getId() 제거 ( 피드백 반영 ) (#104) * feat: 유효성 검증 관련 에러 일괄 처리을 위한 메소드 추가 (#104) * test: API 변경에 따른 통합 테스트 수정 및 단위 테스트 구현 (#104) * refactor: UserMeGetResponse 필드 변경 (#104) * fix: CI 에러 수정 (#104) * refactor: 피드백 반영 (#104) * fix: CI 에러 수정 (#104) * refactor: JWT 에러 응답 리팩토링 및 인가 인증 예최 처리 코드 수정 (#103) * refactor: 기타 코드 리팩토링( 피드백 반영 ) (#101) * chore: JWT 관련 의존성 변경 (#101) * refactor: JWT 및 인증 관련 로직 리팩토링 (#101) * test: 테스트 코드 및 설정 관련 변경(#101) * refactor: ObjectMapper Autowired 로 주입 (#101) * refactor: AuthService 반환 타입 Optional -> User 변경 (#101) * fix: V6_create_users_interests.sql 추가 (#109) (#110) * refactor: 회원 카드 등록시 JWT 정보도 함께 반환하도록 수정 (#114) * chore : application.properties 에 jwt 관련 설정 값 추가 (#113) * refactor : userService 회원 카드 등록 로직 수정 (#113) * feat : UserRegisterResponse 필드 추가(#113) * test: API 변경에 따른 테스트 관련 코드 수정 (#113) * fix: 소셜 로그인 관련 500 에러 수정 및 OAuth 로직 일부 개선 (#112) * fix: OAuthLoginController @RestController 어노테이션 추가(나는 바보..) 및 favicon 관련 임시 컨트롤러 생성 (#111) * refactor : 설정 yml 리팩토링 (#111) * refactor : SecurityConfig 리팩토링 (#111) * refactor : 기타 OAuth 관련 로직 리팩토링(#111) * refactor : Cors 허용 주소 임시 전부 허용 (#111) * fix : SonarCloud 오류 수정 (#111) * fix : SonarCloud 오류 수정 (#111) * feat: 위치 기반 API 구현 및 테스트 (#108) * refactor: 기타 코드 리팩토링( 피드백 반영 ) (#101) * chore: JWT 관련 의존성 변경 (#101) * refactor: JWT 및 인증 관련 로직 리팩토링 (#101) * test: 테스트 코드 및 설정 관련 변경(#101) * refactor: ObjectMapper Autowired 로 주입 (#101) * refactor: AuthService 반환 타입 Optional -> User 변경 (#101) * refactor: application.properties redis.port 변경 (#91) * refactor: 기존 Redis 설정 리팩토링 및 추가 구현 (#91) * feat: 유저 위치 기반 관련 DTO 및 VO 구현 (#91) * feat: 유저 위치 기반 API 구현 (#91) * test: 유저 위치 기반 통합 테스트 관련 클래스 구현 및 테스트 (#91) * test: redis port 변경에 따른 테스트 수정 (#91) * fix: CI 에러 수정 (#91) * fix: sonarCloud 에러 수정 (#91) * refactor: 리뷰 반영 (#108) * build: Gatling 세팅 (#115) * feat: 부하테스트 툴 Gatling을 세팅한다 * feat: 예시 코드를 작성한다 * refactor: Sample 코드의 이름을 변경한다 * feat: 내 정보 조회에 Oauth 정보 추가 (#119) * feat: 내 정보 조회에 Oauth 정보 추가 * test: Oauth 정보 추가에 따른 테스트 변경 * fix: 모임 목록 조회 조건에 활동 지역 조건이 안걸리는 버그 수정 (#121) * feat: 회원 탈퇴 사유 저장 로직 추가 및 cors 관련 재설정 (#123) * refactor: SecurityConfig Cors 관련 전체 허용으로 변경 및 회원 카드 등록 API permitAll 로 변경 (#117) * refactor: 기존 엔티티 관련 리팩토링 (#117) * feat: WithdrawReason 생성 sql 추가 (#117) * feat: WithdrawReason 관련 도메인 구현 (#117) * feat: 회원 탈퇴 시 탈퇴 사유 기능 추가 (#117) * test: 회원 탈퇴 단위 테스트 (#117) * refactor: 회원 탈퇴 API POST 로 수정 (#117) * refactor: 회원탈퇴 관련 통합 테스트 수정 및 요청 DTO 수정 (#117) * refactor: meeting 엔티티명 다시 복구 - 모든 테이블에 `s` 붙이는 방향 논의 필요 (#117) * feat: withdraw_reason 테이블 이름 변경 (#117) * fix: CI 에러 해결 (#117) * fix: 모임 규격 변경 (#127) * refactor: 코드 포매팅 적용 (#130) --------- Co-authored-by: ChoiDongKuen Co-authored-by: xb205 <62425964+devxb@users.noreply.github.com> --- .../meeting/domain/MeetingSpecification.java | 12 ++- .../domain/request/CreateMeetingRequest.java | 40 ++++---- .../domain/response/ImageUploadResponse.java | 9 +- .../domain/response/MeetingResponse.java | 58 ++++++------ .../domain/response/MeetingsResponse.java | 67 +++++++------- .../meeting/infra/ImageUploadService.java | 18 ++-- ...PageableHandlerMethodArgumentResolver.java | 20 ++-- .../meeting/domain/MeetingFixture.java | 91 ++++++++++--------- 8 files changed, 166 insertions(+), 149 deletions(-) diff --git a/src/main/java/net/teumteum/meeting/domain/MeetingSpecification.java b/src/main/java/net/teumteum/meeting/domain/MeetingSpecification.java index 2d74be02..fc2bc861 100644 --- a/src/main/java/net/teumteum/meeting/domain/MeetingSpecification.java +++ b/src/main/java/net/teumteum/meeting/domain/MeetingSpecification.java @@ -10,9 +10,11 @@ public class MeetingSpecification { public static Specification withIsOpen(boolean isOpen) { if (isOpen) { - return (root, query, criteriaBuilder) -> criteriaBuilder.greaterThan(root.get("promiseDateTime"), LocalDateTime.now()); + return (root, query, criteriaBuilder) -> criteriaBuilder.greaterThan(root.get("promiseDateTime"), + LocalDateTime.now()); } - return (root, query, criteriaBuilder) -> criteriaBuilder.lessThanOrEqualTo(root.get("promiseDateTime"), LocalDateTime.now()); + return (root, query, criteriaBuilder) -> criteriaBuilder.lessThanOrEqualTo(root.get("promiseDateTime"), + LocalDateTime.now()); } public static Specification withTopic(Topic topic) { @@ -20,7 +22,8 @@ public static Specification withTopic(Topic topic) { } public static Specification withAreaStreet(String meetingAreaStreet) { - return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("meetingArea").get("mainStreet"), meetingAreaStreet); + return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("meetingArea").get("mainStreet"), + meetingAreaStreet); } public static Specification withSearchWordInTitle(String searchWord) { @@ -32,7 +35,8 @@ public static Specification withSearchWordInIntroduction(String searchW } public static Specification withParticipantUserId(Long participantUserId) { - return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.join("participantUserIds"), participantUserId); + return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.join("participantUserIds"), + participantUserId); } } diff --git a/src/main/java/net/teumteum/meeting/domain/request/CreateMeetingRequest.java b/src/main/java/net/teumteum/meeting/domain/request/CreateMeetingRequest.java index 7635fb0a..4da305bf 100644 --- a/src/main/java/net/teumteum/meeting/domain/request/CreateMeetingRequest.java +++ b/src/main/java/net/teumteum/meeting/domain/request/CreateMeetingRequest.java @@ -9,27 +9,29 @@ import java.time.LocalDateTime; public record CreateMeetingRequest( - @NotNull(message = "모임 주제를 입력해주세요.") - Topic topic, - @NotNull(message = "모임 제목을 입력해주세요.") - @Size(min = 2, max = 32, message = "모임 제목은 2자 이상 32자 이하로 입력해주세요.") - String title, - @NotNull(message = "모임 소개를 입력해주세요.") - @Size(min = 10, max = 200, message = "모임 소개는 10자 이상 200자 이하로 입력해주세요.") - String introduction, - @NotNull(message = "약속 시간을 입력해주세요.") - @Future(message = "약속 시간은 현재 시간보다 미래여야 합니다.") - LocalDateTime promiseDateTime, - @NotNull(message = "모집 인원을 입력해주세요.") - int numberOfRecruits, - @Valid - MeetingArea meetingArea + @NotNull(message = "모임 주제를 입력해주세요.") + Topic topic, + @NotNull(message = "모임 제목을 입력해주세요.") + @Size(min = 2, max = 32, message = "모임 제목은 2자 이상 32자 이하로 입력해주세요.") + String title, + @NotNull(message = "모임 소개를 입력해주세요.") + @Size(min = 10, max = 200, message = "모임 소개는 10자 이상 200자 이하로 입력해주세요.") + String introduction, + @NotNull(message = "약속 시간을 입력해주세요.") + @Future(message = "약속 시간은 현재 시간보다 미래여야 합니다.") + LocalDateTime promiseDateTime, + @NotNull(message = "모집 인원을 입력해주세요.") + int numberOfRecruits, + @Valid + MeetingArea meetingArea ) { + public record MeetingArea( - @NotNull(message = "주소를 입력해주세요.") - String address, - @NotNull(message = "상세 주소를 입력해주세요.") - String addressDetail + @NotNull(message = "주소를 입력해주세요.") + String address, + @NotNull(message = "상세 주소를 입력해주세요.") + String addressDetail ) { + } } diff --git a/src/main/java/net/teumteum/meeting/domain/response/ImageUploadResponse.java b/src/main/java/net/teumteum/meeting/domain/response/ImageUploadResponse.java index a3f1d3b2..77141812 100644 --- a/src/main/java/net/teumteum/meeting/domain/response/ImageUploadResponse.java +++ b/src/main/java/net/teumteum/meeting/domain/response/ImageUploadResponse.java @@ -4,9 +4,10 @@ @Builder public record ImageUploadResponse( - String fileName, - String originalFileName, - String contentType, - String filePath + String fileName, + String originalFileName, + String contentType, + String filePath ) { + } diff --git a/src/main/java/net/teumteum/meeting/domain/response/MeetingResponse.java b/src/main/java/net/teumteum/meeting/domain/response/MeetingResponse.java index 56386670..372638b5 100644 --- a/src/main/java/net/teumteum/meeting/domain/response/MeetingResponse.java +++ b/src/main/java/net/teumteum/meeting/domain/response/MeetingResponse.java @@ -8,46 +8,48 @@ import java.util.Set; public record MeetingResponse( - Long id, - Long hostId, - Topic topic, - String title, - String introduction, - Set photoUrls, - LocalDateTime promiseDateTime, - int numberOfRecruits, - MeetingArea meetingArea, - Set participantIds + Long id, + Long hostId, + Topic topic, + String title, + String introduction, + Set photoUrls, + LocalDateTime promiseDateTime, + int numberOfRecruits, + MeetingArea meetingArea, + Set participantIds ) { + public static MeetingResponse of( - Meeting meeting + Meeting meeting ) { return new MeetingResponse( - meeting.getId(), - meeting.getHostUserId(), - meeting.getTopic(), - meeting.getTitle(), - meeting.getIntroduction(), - meeting.getImageUrls(), - meeting.getPromiseDateTime(), - meeting.getNumberOfRecruits(), - MeetingArea.of(meeting), - meeting.getParticipantUserIds() + meeting.getId(), + meeting.getHostUserId(), + meeting.getTopic(), + meeting.getTitle(), + meeting.getIntroduction(), + meeting.getImageUrls(), + meeting.getPromiseDateTime(), + meeting.getNumberOfRecruits(), + MeetingArea.of(meeting), + meeting.getParticipantUserIds() ); } public record MeetingArea( - String mainStreet, - String address, - String addressDetail + String mainStreet, + String address, + String addressDetail ) { + public static MeetingArea of( - Meeting meeting + Meeting meeting ) { return new MeetingArea( - meeting.getMeetingArea().getMainStreet(), - meeting.getMeetingArea().getAddress(), - meeting.getMeetingArea().getAddressDetail() + meeting.getMeetingArea().getMainStreet(), + meeting.getMeetingArea().getAddress(), + meeting.getMeetingArea().getAddressDetail() ); } } diff --git a/src/main/java/net/teumteum/meeting/domain/response/MeetingsResponse.java b/src/main/java/net/teumteum/meeting/domain/response/MeetingsResponse.java index d20bbe9d..31765751 100644 --- a/src/main/java/net/teumteum/meeting/domain/response/MeetingsResponse.java +++ b/src/main/java/net/teumteum/meeting/domain/response/MeetingsResponse.java @@ -8,57 +8,60 @@ import java.util.Set; public record MeetingsResponse( - List meetings + List meetings ) { + public static MeetingsResponse of(List meetings) { return new MeetingsResponse( - meetings.stream() - .map(MeetingResponse::of) - .toList() + meetings.stream() + .map(MeetingResponse::of) + .toList() ); } public record MeetingResponse( - Long id, - Long hostId, - Topic topic, - String title, - String introduction, - Set photoUrls, - LocalDateTime promiseDateTime, - int numberOfRecruits, - MeetingArea meetingArea, - Set participantIds + Long id, + Long hostId, + Topic topic, + String title, + String introduction, + Set photoUrls, + LocalDateTime promiseDateTime, + int numberOfRecruits, + MeetingArea meetingArea, + Set participantIds ) { + public static MeetingResponse of( - Meeting meeting + Meeting meeting ) { return new MeetingResponse( - meeting.getId(), - meeting.getHostUserId(), - meeting.getTopic(), - meeting.getTitle(), - meeting.getIntroduction(), - meeting.getImageUrls(), - meeting.getPromiseDateTime(), - meeting.getNumberOfRecruits(), - MeetingArea.of(meeting), - meeting.getParticipantUserIds() + meeting.getId(), + meeting.getHostUserId(), + meeting.getTopic(), + meeting.getTitle(), + meeting.getIntroduction(), + meeting.getImageUrls(), + meeting.getPromiseDateTime(), + meeting.getNumberOfRecruits(), + MeetingArea.of(meeting), + meeting.getParticipantUserIds() ); } public record MeetingArea( - String mainStreet, - String address, - String addressDetail + String mainStreet, + String address, + String addressDetail ) { + public static MeetingArea of( - Meeting meeting + Meeting meeting ) { return new MeetingArea( - meeting.getMeetingArea().getMainStreet(), - meeting.getMeetingArea().getAddress(), - meeting.getMeetingArea().getAddressDetail() + meeting.getMeetingArea().getMainStreet(), + meeting.getMeetingArea().getAddress(), + meeting.getMeetingArea().getAddressDetail() ); } } diff --git a/src/main/java/net/teumteum/meeting/infra/ImageUploadService.java b/src/main/java/net/teumteum/meeting/infra/ImageUploadService.java index 25f5fca8..b9a194b1 100644 --- a/src/main/java/net/teumteum/meeting/infra/ImageUploadService.java +++ b/src/main/java/net/teumteum/meeting/infra/ImageUploadService.java @@ -26,15 +26,15 @@ public class ImageUploadService implements ImageUpload { @Override public ImageUploadResponse upload(MultipartFile file, String path) { String originalFilename = Optional.ofNullable(file.getOriginalFilename()) - .orElseThrow(() -> new IllegalArgumentException("파일 이름이 없습니다.")); + .orElseThrow(() -> new IllegalArgumentException("파일 이름이 없습니다.")); String fileExtension = originalFilename.substring(originalFilename.lastIndexOf(".")); String fileName = UUID.randomUUID().toString(); String destination = path + "/" + fileName + fileExtension; PutObjectRequest request = PutObjectRequest.builder() - .bucket(bucketName) - .key(destination) - .build(); + .bucket(bucketName) + .key(destination) + .build(); try (var inputStream = file.getInputStream()) { s3Client.putObject(request, RequestBody.fromInputStream(inputStream, file.getSize())); @@ -43,10 +43,10 @@ public ImageUploadResponse upload(MultipartFile file, String path) { } return ImageUploadResponse.builder() - .fileName(fileName) - .originalFileName(originalFilename) - .contentType(file.getContentType()) - .filePath(destination) - .build(); + .fileName(fileName) + .originalFileName(originalFilename) + .contentType(file.getContentType()) + .filePath(destination) + .build(); } } diff --git a/src/test/java/net/teumteum/meeting/config/PageableHandlerMethodArgumentResolver.java b/src/test/java/net/teumteum/meeting/config/PageableHandlerMethodArgumentResolver.java index e354ddf6..705bc0d7 100644 --- a/src/test/java/net/teumteum/meeting/config/PageableHandlerMethodArgumentResolver.java +++ b/src/test/java/net/teumteum/meeting/config/PageableHandlerMethodArgumentResolver.java @@ -1,6 +1,7 @@ package net.teumteum.meeting.config; +import org.jetbrains.annotations.NotNull; import org.springframework.core.MethodParameter; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -25,9 +26,12 @@ public boolean supportsParameter(MethodParameter parameter) { } @Override - public Mono resolveArgument(MethodParameter methodParameter, BindingContext bindingContext, ServerWebExchange serverWebExchange) { - List pageValues = serverWebExchange.getRequest().getQueryParams().getOrDefault("page", List.of(DEFAULT_PAGE)); - List sizeValues = serverWebExchange.getRequest().getQueryParams().getOrDefault("size", List.of(DEFAULT_SIZE)); + public @NotNull Mono resolveArgument(@NotNull MethodParameter methodParameter, @NotNull BindingContext bindingContext, + ServerWebExchange serverWebExchange) { + List pageValues = serverWebExchange.getRequest().getQueryParams() + .getOrDefault("page", List.of(DEFAULT_PAGE)); + List sizeValues = serverWebExchange.getRequest().getQueryParams() + .getOrDefault("size", List.of(DEFAULT_SIZE)); String page = pageValues.get(0); @@ -44,11 +48,11 @@ public Mono resolveArgument(MethodParameter methodParameter, BindingCont } return Mono.just( - PageRequest.of( - Integer.parseInt(page), - Math.min(Integer.parseInt(sizeValues.get(0)), - MAX_SIZE), sort - ) + PageRequest.of( + Integer.parseInt(page), + Math.min(Integer.parseInt(sizeValues.get(0)), + MAX_SIZE), sort + ) ); } } diff --git a/src/test/java/net/teumteum/meeting/domain/MeetingFixture.java b/src/test/java/net/teumteum/meeting/domain/MeetingFixture.java index 052c542a..eb988ef9 100644 --- a/src/test/java/net/teumteum/meeting/domain/MeetingFixture.java +++ b/src/test/java/net/teumteum/meeting/domain/MeetingFixture.java @@ -15,96 +15,96 @@ public static Meeting getDefaultMeeting() { public static Meeting getOpenMeeting() { return newMeetingByBuilder(MeetingBuilder.builder() - .promiseDateTime(LocalDateTime.of(4000, 1, 1, 0, 0)) - .build() + .promiseDateTime(LocalDateTime.of(4000, 1, 1, 0, 0)) + .build() ); } public static Meeting getCloseMeeting() { return newMeetingByBuilder(MeetingBuilder.builder() - .promiseDateTime(LocalDateTime.of(2000, 1, 1, 0, 0)) - .build() + .promiseDateTime(LocalDateTime.of(2000, 1, 1, 0, 0)) + .build() ); } public static Meeting getOpenFullMeeting() { return newMeetingByBuilder(MeetingBuilder.builder() - .promiseDateTime(LocalDateTime.of(4000, 1, 1, 0, 0)) - .numberOfRecruits(3) - .participantUserIds(new HashSet<>(List.of(0L, 1L, 2L))) - .build() + .promiseDateTime(LocalDateTime.of(4000, 1, 1, 0, 0)) + .numberOfRecruits(3) + .participantUserIds(new HashSet<>(List.of(0L, 1L, 2L))) + .build() ); } public static Meeting getOpenMeetingWithTopic(Topic topic) { return newMeetingByBuilder(MeetingBuilder.builder() - .promiseDateTime(LocalDateTime.of(4000, 1, 1, 0, 0)) - .topic(topic) - .build() + .promiseDateTime(LocalDateTime.of(4000, 1, 1, 0, 0)) + .topic(topic) + .build() ); } public static Meeting getCloseMeetingWithTopic(Topic topic) { return newMeetingByBuilder(MeetingBuilder.builder() - .promiseDateTime(LocalDateTime.of(2000, 1, 1, 0, 0)) - .topic(topic) - .build() + .promiseDateTime(LocalDateTime.of(2000, 1, 1, 0, 0)) + .topic(topic) + .build() ); } public static Meeting getOpenMeetingWithMainStreet(String mainStreet) { return newMeetingByBuilder(MeetingBuilder.builder() - .promiseDateTime(LocalDateTime.of(4000, 1, 1, 0, 0)) - .meetingArea(new MeetingArea(mainStreet, "서울특별시", "강남대로 390")) - .build() + .promiseDateTime(LocalDateTime.of(4000, 1, 1, 0, 0)) + .meetingArea(new MeetingArea(mainStreet, "서울특별시", "강남대로 390")) + .build() ); } public static Meeting getOpenMeetingWithParticipantUserId(Long participantUserId) { return newMeetingByBuilder(MeetingBuilder.builder() - .promiseDateTime(LocalDateTime.of(4000, 1, 1, 0, 0)) - .participantUserIds(new HashSet<>(List.of(participantUserId))) - .build() + .promiseDateTime(LocalDateTime.of(4000, 1, 1, 0, 0)) + .participantUserIds(new HashSet<>(List.of(participantUserId))) + .build() ); } public static Meeting getCloseMeetingWithParticipantUserId(Long participantUserId) { return newMeetingByBuilder(MeetingBuilder.builder() - .promiseDateTime(LocalDateTime.of(2000, 1, 1, 0, 0)) - .participantUserIds(new HashSet<>(List.of(participantUserId))) - .build() + .promiseDateTime(LocalDateTime.of(2000, 1, 1, 0, 0)) + .participantUserIds(new HashSet<>(List.of(participantUserId))) + .build() ); } public static Meeting getOpenMeetingWithTitle(String title) { return newMeetingByBuilder(MeetingBuilder.builder() - .promiseDateTime(LocalDateTime.of(4000, 1, 1, 0, 0)) - .title(title) - .build() + .promiseDateTime(LocalDateTime.of(4000, 1, 1, 0, 0)) + .title(title) + .build() ); } public static Meeting getCloseMeetingWithTitle(String title) { return newMeetingByBuilder(MeetingBuilder.builder() - .title(title) - .promiseDateTime(LocalDateTime.of(2000, 1, 1, 0, 0)) - .build() + .title(title) + .promiseDateTime(LocalDateTime.of(2000, 1, 1, 0, 0)) + .build() ); } public static Meeting getOpenMeetingWithIntroduction(String introduction) { return newMeetingByBuilder(MeetingBuilder.builder() - .promiseDateTime(LocalDateTime.of(4000, 1, 1, 0, 0)) - .introduction(introduction) - .build() + .promiseDateTime(LocalDateTime.of(4000, 1, 1, 0, 0)) + .introduction(introduction) + .build() ); } public static Meeting getCloseMeetingWithIntroduction(String introduction) { return newMeetingByBuilder(MeetingBuilder.builder() - .promiseDateTime(LocalDateTime.of(2000, 1, 1, 0, 0)) - .introduction(introduction) - .build() + .promiseDateTime(LocalDateTime.of(2000, 1, 1, 0, 0)) + .introduction(introduction) + .build() ); } @@ -126,21 +126,22 @@ public static Meeting getCloseMeetingWithHostId(Long hostId) { public static Meeting newMeetingByBuilder(MeetingBuilder meetingBuilder) { return new Meeting( - meetingBuilder.id, - meetingBuilder.title, - meetingBuilder.hostUserId, - meetingBuilder.participantUserIds, - meetingBuilder.topic, - meetingBuilder.introduction, - meetingBuilder.meetingArea, - meetingBuilder.numberOfRecruits, - meetingBuilder.promiseDateTime, - meetingBuilder.imageUrls + meetingBuilder.id, + meetingBuilder.title, + meetingBuilder.hostUserId, + meetingBuilder.participantUserIds, + meetingBuilder.topic, + meetingBuilder.introduction, + meetingBuilder.meetingArea, + meetingBuilder.numberOfRecruits, + meetingBuilder.promiseDateTime, + meetingBuilder.imageUrls ); } @Builder public static class MeetingBuilder { + @Builder.Default private Long id = null; From 8738a0bb552209e0599810567ad1a8847a9e4559 Mon Sep 17 00:00:00 2001 From: xb205 <62425964+devxb@users.noreply.github.com> Date: Sun, 21 Jan 2024 12:40:23 +0900 Subject: [PATCH 5/7] =?UTF-8?q?refactor:=20=EC=9C=A0=EC=A0=80=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C,=20=EC=9E=90=EC=8B=A0=EC=A1=B0=ED=9A=8C,=20=EC=9C=A0?= =?UTF-8?q?=EC=A0=80=EB=93=A4=20=EC=A1=B0=ED=9A=8C=20api=EC=9D=91=EB=8B=B5?= =?UTF-8?q?=EC=97=90=20=EC=B9=9C=EA=B5=AC=20=EC=88=98=20=ED=95=84=EB=93=9C?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20(#135)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 유저조회, 자신조회, 유저들 조회 api에 친구 수 필드를 추가한다 * refactor: 자기자신 조회 api응답의 중복 oAuthType 필드를 삭제한다 --- .../teumteum/user/domain/response/UserGetResponse.java | 6 ++++-- .../teumteum/user/domain/response/UserMeGetResponse.java | 8 ++++---- .../user/domain/response/UsersGetByIdResponse.java | 6 ++++-- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/main/java/net/teumteum/user/domain/response/UserGetResponse.java b/src/main/java/net/teumteum/user/domain/response/UserGetResponse.java index 34ee5b5d..4777dd9b 100644 --- a/src/main/java/net/teumteum/user/domain/response/UserGetResponse.java +++ b/src/main/java/net/teumteum/user/domain/response/UserGetResponse.java @@ -17,7 +17,8 @@ public record UserGetResponse( String status, String goal, Job job, - List interests + List interests, + int friends ) { public static UserGetResponse of(User user) { @@ -33,7 +34,8 @@ public static UserGetResponse of(User user) { user.getStatus().name(), user.getGoal(), Job.of(user), - user.getInterests() + user.getInterests(), + user.getFriends().size() ); } diff --git a/src/main/java/net/teumteum/user/domain/response/UserMeGetResponse.java b/src/main/java/net/teumteum/user/domain/response/UserMeGetResponse.java index d9ea035b..a23a8961 100644 --- a/src/main/java/net/teumteum/user/domain/response/UserMeGetResponse.java +++ b/src/main/java/net/teumteum/user/domain/response/UserMeGetResponse.java @@ -18,8 +18,8 @@ public record UserMeGetResponse( String status, String goal, Job job, - String oauthType, - List interests + List interests, + int friends ) { public static UserMeGetResponse of(User user) { @@ -35,8 +35,8 @@ public static UserMeGetResponse of(User user) { user.getStatus().name(), user.getGoal(), Job.of(user), - user.getOauth().getAuthenticated().name(), - user.getInterests() + user.getInterests(), + user.getFriends().size() ); } diff --git a/src/main/java/net/teumteum/user/domain/response/UsersGetByIdResponse.java b/src/main/java/net/teumteum/user/domain/response/UsersGetByIdResponse.java index 40406e2d..db4597af 100644 --- a/src/main/java/net/teumteum/user/domain/response/UsersGetByIdResponse.java +++ b/src/main/java/net/teumteum/user/domain/response/UsersGetByIdResponse.java @@ -28,7 +28,8 @@ public record UserGetResponse( String status, String goal, Job job, - List interests + List interests, + int friends ) { public static UserGetResponse of(User user) { @@ -44,7 +45,8 @@ public static UserGetResponse of(User user) { user.getStatus().name(), user.getGoal(), Job.of(user), - user.getInterests() + user.getInterests(), + user.getFriends().size() ); } From 738a370fd1ea9300bbab194519236b26df2824cb Mon Sep 17 00:00:00 2001 From: xb205 <62425964+devxb@users.noreply.github.com> Date: Sun, 21 Jan 2024 12:43:15 +0900 Subject: [PATCH 6/7] release: 0.2.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 모임 삭제 기능 구현 (#102) * refactor: 기획 변경에 따른 회원 카드 등록 API 리팩토링 (#105) * refactor: sql 변경 (#104) * refactor: User 클래스 필드명 변경 (#104) * refactor: API 변경에 따른 코드 리팩토링 (#104) * refactor: 불필요한 getId() 제거 ( 피드백 반영 ) (#104) * feat: 유효성 검증 관련 에러 일괄 처리을 위한 메소드 추가 (#104) * test: API 변경에 따른 통합 테스트 수정 및 단위 테스트 구현 (#104) * refactor: UserMeGetResponse 필드 변경 (#104) * fix: CI 에러 수정 (#104) * refactor: 피드백 반영 (#104) * fix: CI 에러 수정 (#104) * refactor: JWT 에러 응답 리팩토링 및 인가 인증 예최 처리 코드 수정 (#103) * refactor: 기타 코드 리팩토링( 피드백 반영 ) (#101) * chore: JWT 관련 의존성 변경 (#101) * refactor: JWT 및 인증 관련 로직 리팩토링 (#101) * test: 테스트 코드 및 설정 관련 변경(#101) * refactor: ObjectMapper Autowired 로 주입 (#101) * refactor: AuthService 반환 타입 Optional -> User 변경 (#101) * fix: V6_create_users_interests.sql 추가 (#109) (#110) * refactor: 회원 카드 등록시 JWT 정보도 함께 반환하도록 수정 (#114) * chore : application.properties 에 jwt 관련 설정 값 추가 (#113) * refactor : userService 회원 카드 등록 로직 수정 (#113) * feat : UserRegisterResponse 필드 추가(#113) * test: API 변경에 따른 테스트 관련 코드 수정 (#113) * fix: 소셜 로그인 관련 500 에러 수정 및 OAuth 로직 일부 개선 (#112) * fix: OAuthLoginController @RestController 어노테이션 추가(나는 바보..) 및 favicon 관련 임시 컨트롤러 생성 (#111) * refactor : 설정 yml 리팩토링 (#111) * refactor : SecurityConfig 리팩토링 (#111) * refactor : 기타 OAuth 관련 로직 리팩토링(#111) * refactor : Cors 허용 주소 임시 전부 허용 (#111) * fix : SonarCloud 오류 수정 (#111) * fix : SonarCloud 오류 수정 (#111) * feat: 위치 기반 API 구현 및 테스트 (#108) * refactor: 기타 코드 리팩토링( 피드백 반영 ) (#101) * chore: JWT 관련 의존성 변경 (#101) * refactor: JWT 및 인증 관련 로직 리팩토링 (#101) * test: 테스트 코드 및 설정 관련 변경(#101) * refactor: ObjectMapper Autowired 로 주입 (#101) * refactor: AuthService 반환 타입 Optional -> User 변경 (#101) * refactor: application.properties redis.port 변경 (#91) * refactor: 기존 Redis 설정 리팩토링 및 추가 구현 (#91) * feat: 유저 위치 기반 관련 DTO 및 VO 구현 (#91) * feat: 유저 위치 기반 API 구현 (#91) * test: 유저 위치 기반 통합 테스트 관련 클래스 구현 및 테스트 (#91) * test: redis port 변경에 따른 테스트 수정 (#91) * fix: CI 에러 수정 (#91) * fix: sonarCloud 에러 수정 (#91) * refactor: 리뷰 반영 (#108) * build: Gatling 세팅 (#115) * feat: 부하테스트 툴 Gatling을 세팅한다 * feat: 예시 코드를 작성한다 * refactor: Sample 코드의 이름을 변경한다 * feat: 내 정보 조회에 Oauth 정보 추가 (#119) * feat: 내 정보 조회에 Oauth 정보 추가 * test: Oauth 정보 추가에 따른 테스트 변경 * fix: 모임 목록 조회 조건에 활동 지역 조건이 안걸리는 버그 수정 (#121) * feat: 회원 탈퇴 사유 저장 로직 추가 및 cors 관련 재설정 (#123) * refactor: SecurityConfig Cors 관련 전체 허용으로 변경 및 회원 카드 등록 API permitAll 로 변경 (#117) * refactor: 기존 엔티티 관련 리팩토링 (#117) * feat: WithdrawReason 생성 sql 추가 (#117) * feat: WithdrawReason 관련 도메인 구현 (#117) * feat: 회원 탈퇴 시 탈퇴 사유 기능 추가 (#117) * test: 회원 탈퇴 단위 테스트 (#117) * refactor: 회원 탈퇴 API POST 로 수정 (#117) * refactor: 회원탈퇴 관련 통합 테스트 수정 및 요청 DTO 수정 (#117) * refactor: meeting 엔티티명 다시 복구 - 모든 테이블에 `s` 붙이는 방향 논의 필요 (#117) * feat: withdraw_reason 테이블 이름 변경 (#117) * fix: CI 에러 해결 (#117) * fix: 모임 규격 변경 (#127) * refactor: 코드 포매팅 적용 (#130) * refactor: 유저조회, 자신조회, 유저들 조회 api응답에 친구 수 필드 추가 (#135) * refactor: 유저조회, 자신조회, 유저들 조회 api에 친구 수 필드를 추가한다 * refactor: 자기자신 조회 api응답의 중복 oAuthType 필드를 삭제한다 --------- Co-authored-by: ddingmin Co-authored-by: ChoiDongKuen --- .../teumteum/user/domain/response/UserGetResponse.java | 6 ++++-- .../teumteum/user/domain/response/UserMeGetResponse.java | 9 +++++---- .../user/domain/response/UsersGetByIdResponse.java | 6 ++++-- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/main/java/net/teumteum/user/domain/response/UserGetResponse.java b/src/main/java/net/teumteum/user/domain/response/UserGetResponse.java index 34ee5b5d..4777dd9b 100644 --- a/src/main/java/net/teumteum/user/domain/response/UserGetResponse.java +++ b/src/main/java/net/teumteum/user/domain/response/UserGetResponse.java @@ -17,7 +17,8 @@ public record UserGetResponse( String status, String goal, Job job, - List interests + List interests, + int friends ) { public static UserGetResponse of(User user) { @@ -33,7 +34,8 @@ public static UserGetResponse of(User user) { user.getStatus().name(), user.getGoal(), Job.of(user), - user.getInterests() + user.getInterests(), + user.getFriends().size() ); } diff --git a/src/main/java/net/teumteum/user/domain/response/UserMeGetResponse.java b/src/main/java/net/teumteum/user/domain/response/UserMeGetResponse.java index d9ea035b..c01edc2a 100644 --- a/src/main/java/net/teumteum/user/domain/response/UserMeGetResponse.java +++ b/src/main/java/net/teumteum/user/domain/response/UserMeGetResponse.java @@ -18,8 +18,8 @@ public record UserMeGetResponse( String status, String goal, Job job, - String oauthType, - List interests + List interests, + int friends ) { public static UserMeGetResponse of(User user) { @@ -35,11 +35,12 @@ public static UserMeGetResponse of(User user) { user.getStatus().name(), user.getGoal(), Job.of(user), - user.getOauth().getAuthenticated().name(), - user.getInterests() + user.getInterests(), + user.getFriends().size() ); } + public record Job(String name, boolean certificated, @JsonProperty("class") String jobClass, String detailClass) { public static Job of(User user) { diff --git a/src/main/java/net/teumteum/user/domain/response/UsersGetByIdResponse.java b/src/main/java/net/teumteum/user/domain/response/UsersGetByIdResponse.java index 40406e2d..db4597af 100644 --- a/src/main/java/net/teumteum/user/domain/response/UsersGetByIdResponse.java +++ b/src/main/java/net/teumteum/user/domain/response/UsersGetByIdResponse.java @@ -28,7 +28,8 @@ public record UserGetResponse( String status, String goal, Job job, - List interests + List interests, + int friends ) { public static UserGetResponse of(User user) { @@ -44,7 +45,8 @@ public static UserGetResponse of(User user) { user.getStatus().name(), user.getGoal(), Job.of(user), - user.getInterests() + user.getInterests(), + user.getFriends().size() ); } From 05dcd4c87034b8a1a5bdd0eada7aa0f9b34f96cc Mon Sep 17 00:00:00 2001 From: ChoiDongKuen Date: Mon, 22 Jan 2024 22:53:54 +0900 Subject: [PATCH 7/7] =?UTF-8?q?fix:=20JWT=20=EC=9E=AC=EB=B0=9C=EA=B8=89=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=ED=95=B4=EA=B2=B0=20(#138)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 토큰 재발급 에러 수정 (#132) * fix: 토큰 재발급 에러 수정 (#132) * test: 토큰 재발급 에러 수정에 따른 테스트 수정 (#132) * test: SecurityConfig permitAll url 중복 제거 (#132) --- .../net/teumteum/auth/service/AuthService.java | 3 +-- .../core/security/service/JwtService.java | 9 +++++---- .../core/security/service/RedisService.java | 4 ++-- .../unit/auth/service/AuthServiceTest.java | 17 +++++++++-------- 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/main/java/net/teumteum/auth/service/AuthService.java b/src/main/java/net/teumteum/auth/service/AuthService.java index ee91cce1..a2530fb9 100644 --- a/src/main/java/net/teumteum/auth/service/AuthService.java +++ b/src/main/java/net/teumteum/auth/service/AuthService.java @@ -49,7 +49,6 @@ private void checkRefreshTokenMatch(User user, String refreshToken) { private TokenResponse issueNewToken(User user) { - return new TokenResponse(jwtService.createAccessToken(user.getOauth().getOauthId()), - jwtService.createRefreshToken()); + return jwtService.createServiceToken(user); } } diff --git a/src/main/java/net/teumteum/core/security/service/JwtService.java b/src/main/java/net/teumteum/core/security/service/JwtService.java index fda0e80d..eb939017 100644 --- a/src/main/java/net/teumteum/core/security/service/JwtService.java +++ b/src/main/java/net/teumteum/core/security/service/JwtService.java @@ -18,7 +18,6 @@ import net.teumteum.core.property.JwtProperty; import net.teumteum.user.domain.User; import org.springframework.beans.factory.InitializingBean; -import org.springframework.security.oauth2.jwt.JwtException; import org.springframework.stereotype.Service; import org.springframework.util.ObjectUtils; @@ -60,12 +59,14 @@ public String extractRefreshToken(HttpServletRequest request) { public Long getUserIdFromToken(String token) { try { - return Long.valueOf(getClaims(token).get("id", String.class)); - } catch (Exception exception) { - throw new JwtException("Access Token is not valid"); + Claims claims = getClaims(token); + return claims.get("id", Long.class); + } catch (ExpiredJwtException exception) { + return Long.valueOf(exception.getClaims().get("id").toString()); } } + public TokenResponse createServiceToken(User users) { String accessToken = createAccessToken(users.getId().toString()); String refreshToken = createRefreshToken(); diff --git a/src/main/java/net/teumteum/core/security/service/RedisService.java b/src/main/java/net/teumteum/core/security/service/RedisService.java index 323263ac..5be3a95c 100644 --- a/src/main/java/net/teumteum/core/security/service/RedisService.java +++ b/src/main/java/net/teumteum/core/security/service/RedisService.java @@ -9,10 +9,10 @@ @RequiredArgsConstructor public class RedisService { - private final RedisTemplate redisTemplate; + private final RedisTemplate redisTemplate; public String getData(String key) { - return (String) redisTemplate.opsForValue().get(key); + return redisTemplate.opsForValue().get(key); } public void setData(String key, String value) { diff --git a/src/test/java/net/teumteum/unit/auth/service/AuthServiceTest.java b/src/test/java/net/teumteum/unit/auth/service/AuthServiceTest.java index 5e194698..3163c935 100644 --- a/src/test/java/net/teumteum/unit/auth/service/AuthServiceTest.java +++ b/src/test/java/net/teumteum/unit/auth/service/AuthServiceTest.java @@ -1,6 +1,8 @@ package net.teumteum.unit.auth.service; import static net.teumteum.core.security.Authenticated.네이버; +import static net.teumteum.unit.auth.common.SecurityValue.INVALID_ACCESS_TOKEN; +import static net.teumteum.unit.auth.common.SecurityValue.VALID_REFRESH_TOKEN; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; @@ -55,17 +57,16 @@ void Return_new_jwt_if_access_and_refresh_is_exist() { HttpServletRequest httpServletRequest = mock(HttpServletRequest.class); - given(jwtService.extractAccessToken(any(HttpServletRequest.class))).willReturn("access token"); + given(jwtService.extractAccessToken(any(HttpServletRequest.class))).willReturn(INVALID_ACCESS_TOKEN); - given(jwtService.extractRefreshToken(any(HttpServletRequest.class))).willReturn("refresh token"); + given(jwtService.extractRefreshToken(any(HttpServletRequest.class))).willReturn(VALID_REFRESH_TOKEN); given(jwtService.getUserIdFromToken(anyString())).willReturn(1L); - given(jwtService.createAccessToken(anyString())).willReturn("new access token"); - - given(jwtService.createRefreshToken()).willReturn("new refresh token"); + given(jwtService.createServiceToken(any(User.class))).willReturn( + TokenResponse.builder().accessToken("access token").refreshToken("refresh token").build()); - given(redisService.getData(anyString())).willReturn("refresh token"); + given(redisService.getData(anyString())).willReturn(VALID_REFRESH_TOKEN); given(userConnector.findUserById(anyLong())).willReturn(user); @@ -76,8 +77,8 @@ void Return_new_jwt_if_access_and_refresh_is_exist() { // then assertThat(response).isNotNull(); - assertThat(response.getAccessToken()).isEqualTo("new access token"); - assertThat(response.getRefreshToken()).isEqualTo("new refresh token"); + assertThat(response.getAccessToken()).isEqualTo("access token"); + assertThat(response.getRefreshToken()).isEqualTo("refresh token"); verify(userConnector, times(1)).findUserById(anyLong()); verify(jwtService, times(1)).validateToken(any()); }