diff --git a/build.gradle b/build.gradle index e63edb00..9125452e 100644 --- a/build.gradle +++ b/build.gradle @@ -62,6 +62,8 @@ dependencies { implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.1' // Redis implementation 'org.springframework.boot:spring-boot-starter-data-redis' + // OAuth2 + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' } clean { @@ -72,8 +74,12 @@ tasks.register("preScript", Exec) { environment "DEV_NAME", System.getenv("DEV_NAME") environment "CREDENTIAL_NAME", System.getenv("CREDENTIAL_NAME") environment "CREDENTIAL_PW", System.getenv("CREDENTIAL_PW") - if (System.properties['os.name'].toLowerCase().contains('win')) { - commandLine "C:\\Program Files\\Git\\bin\\bash.exe", "--login", "-c", "./prepare-build.sh" + + if (System.getProperty('os.name').toLowerCase().contains('win')) { + // For windows + commandLine 'C:\\Program Files\\Git\\bin\\bash.exe', '-c', './prepare-build.sh' + } else { + commandLine "sh", "prepare-build.sh" } } @@ -81,7 +87,7 @@ tasks.named("processResources") { dependsOn("preScript") } -tasks.named("bootRun") { +tasks.named("bootRun"){ dependsOn("preScript") } diff --git a/src/main/java/com/gamzabat/algohub/auth/controller/OAuth2Controller.java b/src/main/java/com/gamzabat/algohub/auth/controller/OAuth2Controller.java new file mode 100644 index 00000000..ae948f68 --- /dev/null +++ b/src/main/java/com/gamzabat/algohub/auth/controller/OAuth2Controller.java @@ -0,0 +1,31 @@ +package com.gamzabat.algohub.auth.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.gamzabat.algohub.auth.service.OAuth2Service; +import com.gamzabat.algohub.feature.user.dto.TokenResponse; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/api") +@Tag(name = "oauth 소셜 로그인 관련 API", description = "code를 사용해 Github 소셜 로그인하는 API") +public class OAuth2Controller { + private final OAuth2Service oAuth2Service; + + @PostMapping("/oauth/github/sign-in") + @Operation(summary = "Github 소셜 로그인 API", description = "Github 소셜 로그인 후 발급된 code를 전달해 로그인하는 API") + public ResponseEntity signIn(@RequestParam String code) { + TokenResponse response = oAuth2Service.githubSignIn(code); + return ResponseEntity.ok(response); + } +} diff --git a/src/main/java/com/gamzabat/algohub/auth/dto/GithubEmailDto.java b/src/main/java/com/gamzabat/algohub/auth/dto/GithubEmailDto.java new file mode 100644 index 00000000..fd99598e --- /dev/null +++ b/src/main/java/com/gamzabat/algohub/auth/dto/GithubEmailDto.java @@ -0,0 +1,8 @@ +package com.gamzabat.algohub.auth.dto; + +import lombok.Getter; + +@Getter +public class GithubEmailDto { + private String email; +} diff --git a/src/main/java/com/gamzabat/algohub/auth/dto/GithubUserDto.java b/src/main/java/com/gamzabat/algohub/auth/dto/GithubUserDto.java new file mode 100644 index 00000000..71f93728 --- /dev/null +++ b/src/main/java/com/gamzabat/algohub/auth/dto/GithubUserDto.java @@ -0,0 +1,11 @@ +package com.gamzabat.algohub.auth.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class GithubUserDto { + private String login; + private String email; +} diff --git a/src/main/java/com/gamzabat/algohub/auth/dto/OAuthAccessTokenDto.java b/src/main/java/com/gamzabat/algohub/auth/dto/OAuthAccessTokenDto.java new file mode 100644 index 00000000..fa86c4be --- /dev/null +++ b/src/main/java/com/gamzabat/algohub/auth/dto/OAuthAccessTokenDto.java @@ -0,0 +1,13 @@ +package com.gamzabat.algohub.auth.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class OAuthAccessTokenDto { + @JsonProperty("access_token") + private String accessToken; +} diff --git a/src/main/java/com/gamzabat/algohub/auth/exception/GithubApiException.java b/src/main/java/com/gamzabat/algohub/auth/exception/GithubApiException.java new file mode 100644 index 00000000..15017ca6 --- /dev/null +++ b/src/main/java/com/gamzabat/algohub/auth/exception/GithubApiException.java @@ -0,0 +1,10 @@ +package com.gamzabat.algohub.auth.exception; + +import lombok.Getter; + +@Getter +public class GithubApiException extends RuntimeException { + public GithubApiException(String message) { + super(message); + } +} diff --git a/src/main/java/com/gamzabat/algohub/auth/service/OAuth2Service.java b/src/main/java/com/gamzabat/algohub/auth/service/OAuth2Service.java new file mode 100644 index 00000000..91bc9217 --- /dev/null +++ b/src/main/java/com/gamzabat/algohub/auth/service/OAuth2Service.java @@ -0,0 +1,164 @@ +package com.gamzabat.algohub.auth.service; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import com.gamzabat.algohub.auth.dto.GithubEmailDto; +import com.gamzabat.algohub.auth.dto.GithubUserDto; +import com.gamzabat.algohub.auth.dto.OAuthAccessTokenDto; +import com.gamzabat.algohub.auth.exception.GithubApiException; +import com.gamzabat.algohub.common.jwt.TokenProvider; +import com.gamzabat.algohub.common.jwt.dto.JwtDTO; +import com.gamzabat.algohub.constants.ApiConstants; +import com.gamzabat.algohub.enums.Role; +import com.gamzabat.algohub.feature.user.domain.User; +import com.gamzabat.algohub.feature.user.dto.TokenResponse; +import com.gamzabat.algohub.feature.user.repository.UserRepository; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +@RequiredArgsConstructor +public class OAuth2Service { + private final TokenProvider tokenProvider; + @Value("${github.client_id}") + private String clientId; + @Value("${github.client_secret}") + private String secretKey; + private final RestTemplate restTemplate; + private final UserRepository userRepository; + + @Transactional + public TokenResponse githubSignIn(String code) { + String accessToken = getGithubAccessToken(code); + GithubUserDto githubUser = getGithubUserInfo(accessToken); + syncWithUser(githubUser); + Authentication authentication = createGithubAuthentication(githubUser); + JwtDTO tokens = tokenProvider.generateTokens(authentication); + return new TokenResponse(tokens.getAccessToken(), tokens.getRefreshToken()); + } + + private void syncWithUser(GithubUserDto githubUser) { + Optional optionalUser = userRepository.findByEmail(githubUser.getEmail()); + if (optionalUser.isEmpty()) { + register(githubUser); + return; + } + User user = optionalUser.get(); + user.editGithubName(githubUser.getLogin()); + } + + private void register(GithubUserDto githubUser) { + User newUser = userRepository.save(User.builder() + .email(githubUser.getEmail()) + .password(null) + .nickname(null) + .bjNickname(null) + .role(Role.USER) + .build()); + newUser.editNickname(createTemporaryNickname(githubUser.getLogin(), newUser.getId())); + newUser.editGithubName(githubUser.getLogin()); + } + + private String createTemporaryNickname(String login, Long id) { + return login + "@" + id; + } + + private String getGithubAccessToken(String code) { + HttpEntity response = restTemplate.exchange( + ApiConstants.GITHUB_ACCESS_TOKEN_URL, + HttpMethod.POST, + createAccessTokenRequest(code), + OAuthAccessTokenDto.class); + + if (response.getBody() == null) { + log.error("failed to request GitHub access token : GitHub API response body is null."); + throw new GithubApiException("Github access token 응답에 실패했습니다."); + } + + return response.getBody().getAccessToken(); + } + + private GithubUserDto getGithubUserInfo(String accessToken) { + ResponseEntity response = restTemplate.exchange( + ApiConstants.GITHUB_USER_URL, + HttpMethod.GET, + createAuthorizationHeader(accessToken), + GithubUserDto.class); + + GithubUserDto user = response.getBody(); + if (user == null) { + log.error("failed to request GitHub user info : GitHub API response body is null."); + throw new GithubApiException("Github 사용자 정보를 가져오는데 실패했습니다."); + } + + if (user.getEmail() == null || user.getEmail().isEmpty()) { + user.setEmail(getEmail(accessToken)); + } + + return user; + } + + private String getEmail(String accessToken) { + ResponseEntity> emails = restTemplate.exchange( + ApiConstants.GITHUB_EMAIL_URL, + HttpMethod.GET, + createAuthorizationHeader(accessToken), + new ParameterizedTypeReference<>() { + } + ); + + if (emails.getBody() == null) { + log.error("failed to request GitHub user email : GitHub API response body is null."); + throw new GithubApiException("Github 유저의 이메일을 가져오는데 실패했습니다."); + } + + return emails.getBody().getFirst().getEmail(); + } + + private Authentication createGithubAuthentication(GithubUserDto githubUser) { + OAuth2User oAuth2User = new DefaultOAuth2User( + Collections.singleton(new SimpleGrantedAuthority(Role.USER.toString())), + Map.of("email", githubUser.getEmail()), + "email" + ); + + return new OAuth2AuthenticationToken(oAuth2User, oAuth2User.getAuthorities(), "github"); + } + + private HttpEntity> createAccessTokenRequest(String code) { + LinkedMultiValueMap params = new LinkedMultiValueMap<>(); + params.add("client_id", clientId); + params.add("client_secret", secretKey); + params.add("code", code); + + return new HttpEntity<>(params, new HttpHeaders()); + } + + private HttpEntity> createAuthorizationHeader(String accessToken) { + HttpHeaders headers = new HttpHeaders(); + headers.add("Authorization", "Bearer " + accessToken); + return new HttpEntity<>(headers); + } +} diff --git a/src/main/java/com/gamzabat/algohub/common/jwt/JwtAuthenticationFilter.java b/src/main/java/com/gamzabat/algohub/common/jwt/JwtAuthenticationFilter.java index 62921c89..14fef478 100644 --- a/src/main/java/com/gamzabat/algohub/common/jwt/JwtAuthenticationFilter.java +++ b/src/main/java/com/gamzabat/algohub/common/jwt/JwtAuthenticationFilter.java @@ -30,7 +30,8 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { "/api/auth/reissue-token", "/api/users/check-email", "/api/users/check-nickname", - "/api/users/check-baekjoon-nickname"); + "/api/users/check-baekjoon-nickname", + "/api/oauth/github/sign-in"); @Override protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { diff --git a/src/main/java/com/gamzabat/algohub/common/jwt/TokenProvider.java b/src/main/java/com/gamzabat/algohub/common/jwt/TokenProvider.java index 86d225a5..5900a4dd 100644 --- a/src/main/java/com/gamzabat/algohub/common/jwt/TokenProvider.java +++ b/src/main/java/com/gamzabat/algohub/common/jwt/TokenProvider.java @@ -164,8 +164,8 @@ public String resolveToken(HttpServletRequest request) { throw new JwtRequestException(HttpStatus.BAD_REQUEST.value(), "BAD_REQUEST", "유효한 형태의 토큰이 존재하지 않습니다."); } - private Claims getClaims(String expiredToken) { - String token = expiredToken.replace("Bearer", "").trim(); + private Claims getClaims(String inputToken) { + String token = inputToken.replace("Bearer", "").trim(); return parseClaims(token); } diff --git a/src/main/java/com/gamzabat/algohub/config/SwaggerConfig.java b/src/main/java/com/gamzabat/algohub/config/SwaggerConfig.java index b0cbc4a8..48ccc5eb 100644 --- a/src/main/java/com/gamzabat/algohub/config/SwaggerConfig.java +++ b/src/main/java/com/gamzabat/algohub/config/SwaggerConfig.java @@ -1,18 +1,30 @@ package com.gamzabat.algohub.config; import java.util.Collections; +import java.util.List; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import com.gamzabat.algohub.constants.ApiConstants; + import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.security.SecurityRequirement; import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.servers.Server; @Configuration public class SwaggerConfig { + + @Value("${server.port}") + private String serverPort; + + @Value("${spring.profiles.active:dev}") // 기본값을 local로 설정 + private String activeProfile; + @Bean public OpenAPI openAPI() { SecurityScheme scheme = new SecurityScheme() @@ -20,12 +32,32 @@ public OpenAPI openAPI() { .in(SecurityScheme.In.HEADER).name("Authorization"); SecurityRequirement requirement = new SecurityRequirement().addList("bearerAuth"); + List servers = createServers(); + return new OpenAPI() .components(new Components().addSecuritySchemes("bearerAuth", scheme)) .security(Collections.singletonList(requirement)) .info(new Info() .title("AlgoHub API 명세서") .description("AlgoHub API 명세서 입니다.") - .version("1.0.0")); + .version("1.0.0")) + .servers(servers); + + } + + private List createServers() { + Server prodServer = new Server() + .description("Algohub API") + .url(ApiConstants.SERVER_HTTPS_ENDPOINT); + + if ("prod".equalsIgnoreCase(activeProfile)) { + return Collections.singletonList(prodServer); + } + + Server localServer = new Server() + .description("Algohub API for LOCAL") + .url("http://localhost:" + serverPort); + + return List.of(localServer, prodServer); } } diff --git a/src/main/java/com/gamzabat/algohub/constants/ApiConstants.java b/src/main/java/com/gamzabat/algohub/constants/ApiConstants.java index 20dd72ed..3dd1b2b0 100644 --- a/src/main/java/com/gamzabat/algohub/constants/ApiConstants.java +++ b/src/main/java/com/gamzabat/algohub/constants/ApiConstants.java @@ -4,6 +4,11 @@ public final class ApiConstants { public static final String SOLVED_AC_PROBLEM_API_URL = "https://solved.ac/api/v3/problem/lookup?problemIds="; public static final String BOJ_USER_PROFILE_URL = "https://www.acmicpc.net/user/"; public static final String BOJ_PROBLEM_URL = "www.acmicpc.net"; + public static final String SERVER_HTTPS_ENDPOINT = "https://api.algohub.kr"; + + public static final String GITHUB_ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token"; + public static final String GITHUB_USER_URL = "https://api.github.com/user"; + public static final String GITHUB_EMAIL_URL = "https://api.github.com/user/emails"; private ApiConstants() { throw new RuntimeException("Can not instantiate : ApiConstants"); diff --git a/src/main/java/com/gamzabat/algohub/exception/CustomExceptionHandler.java b/src/main/java/com/gamzabat/algohub/exception/CustomExceptionHandler.java index 18e565d1..ce2ae0a5 100644 --- a/src/main/java/com/gamzabat/algohub/exception/CustomExceptionHandler.java +++ b/src/main/java/com/gamzabat/algohub/exception/CustomExceptionHandler.java @@ -5,6 +5,7 @@ import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; +import com.gamzabat.algohub.auth.exception.GithubApiException; import com.gamzabat.algohub.common.jwt.exception.ExpiredTokenException; import com.gamzabat.algohub.common.jwt.exception.TokenException; import com.gamzabat.algohub.feature.comment.exception.CommentValidationException; @@ -183,4 +184,10 @@ protected ResponseEntity handler(CheckPasswordFormException e) { return ResponseEntity.status(e.getCode()) .body(new ErrorResponse(e.getCode(), e.getErrors(), null)); } + + @ExceptionHandler(GithubApiException.class) + protected ResponseEntity handler(GithubApiException e) { + return ResponseEntity.internalServerError() + .body(new ErrorResponse(HttpStatus.SERVICE_UNAVAILABLE.value(), e.getMessage(), null)); + } } diff --git a/src/main/java/com/gamzabat/algohub/feature/group/studygroup/controller/StudyGroupController.java b/src/main/java/com/gamzabat/algohub/feature/group/studygroup/controller/StudyGroupController.java index e172febc..53fe19b4 100644 --- a/src/main/java/com/gamzabat/algohub/feature/group/studygroup/controller/StudyGroupController.java +++ b/src/main/java/com/gamzabat/algohub/feature/group/studygroup/controller/StudyGroupController.java @@ -23,12 +23,14 @@ import com.gamzabat.algohub.feature.group.studygroup.dto.CreateGroupRequest; import com.gamzabat.algohub.feature.group.studygroup.dto.EditGroupRequest; import com.gamzabat.algohub.feature.group.studygroup.dto.EditGroupVisibilityRequest; +import com.gamzabat.algohub.feature.group.studygroup.dto.GetGroupIdResponse; import com.gamzabat.algohub.feature.group.studygroup.dto.GetGroupMemberResponse; import com.gamzabat.algohub.feature.group.studygroup.dto.GetGroupResponse; import com.gamzabat.algohub.feature.group.studygroup.dto.GetGroupSettingResponse; import com.gamzabat.algohub.feature.group.studygroup.dto.GetStudyGroupListsResponse; import com.gamzabat.algohub.feature.group.studygroup.dto.GetStudyGroupWithCodeResponse; import com.gamzabat.algohub.feature.group.studygroup.dto.GroupCodeResponse; +import com.gamzabat.algohub.feature.group.studygroup.dto.GroupRoleResponse; import com.gamzabat.algohub.feature.group.studygroup.dto.UpdateBookmarkResponse; import com.gamzabat.algohub.feature.group.studygroup.dto.UpdateGroupMemberRoleRequest; import com.gamzabat.algohub.feature.group.studygroup.service.StudyGroupService; @@ -59,9 +61,9 @@ public ResponseEntity createGroup(@AuthedUser User user, @PostMapping(value = "/groups/{code}/join") @Operation(summary = "그룹 코드를 사용한 그룹 참여 API") - public ResponseEntity joinGroupWithCode(@AuthedUser User user, @PathVariable String code) { - studyGroupService.joinGroupWithCode(user, code); - return ResponseEntity.ok().build(); + public ResponseEntity joinGroupWithCode(@AuthedUser User user, @PathVariable String code) { + GetGroupIdResponse groupId = studyGroupService.joinGroupWithCode(user, code); + return ResponseEntity.ok().body(groupId); } @GetMapping(value = "/users/me/groups") @@ -163,8 +165,8 @@ public ResponseEntity updateMemberRole(@AuthedUser User user, @GetMapping(value = "/groups/{groupId}/role") @Operation(summary = "스터디 그룹 내 유저의 Role 조회 API", description = "특정 스터디 그룹 내에서 유저의 Role을 조회하는 API") - public ResponseEntity getGroupRole(@AuthedUser User user, @PathVariable Long groupId) { - String response = studyGroupService.getRoleInGroup(user, groupId); + public ResponseEntity getGroupRole(@AuthedUser User user, @PathVariable Long groupId) { + GroupRoleResponse response = studyGroupService.getRoleInGroup(user, groupId); return ResponseEntity.ok().body(response); } diff --git a/src/main/java/com/gamzabat/algohub/feature/group/studygroup/dto/GetGroupIdResponse.java b/src/main/java/com/gamzabat/algohub/feature/group/studygroup/dto/GetGroupIdResponse.java new file mode 100644 index 00000000..18632c1b --- /dev/null +++ b/src/main/java/com/gamzabat/algohub/feature/group/studygroup/dto/GetGroupIdResponse.java @@ -0,0 +1,4 @@ +package com.gamzabat.algohub.feature.group.studygroup.dto; + +public record GetGroupIdResponse(Long groupId) { +} diff --git a/src/main/java/com/gamzabat/algohub/feature/group/studygroup/dto/GroupRoleResponse.java b/src/main/java/com/gamzabat/algohub/feature/group/studygroup/dto/GroupRoleResponse.java new file mode 100644 index 00000000..0b68bed0 --- /dev/null +++ b/src/main/java/com/gamzabat/algohub/feature/group/studygroup/dto/GroupRoleResponse.java @@ -0,0 +1,4 @@ +package com.gamzabat.algohub.feature.group.studygroup.dto; + +public record GroupRoleResponse(String role) { +} diff --git a/src/main/java/com/gamzabat/algohub/feature/group/studygroup/service/StudyGroupService.java b/src/main/java/com/gamzabat/algohub/feature/group/studygroup/service/StudyGroupService.java index 83a9ba65..5af1af37 100644 --- a/src/main/java/com/gamzabat/algohub/feature/group/studygroup/service/StudyGroupService.java +++ b/src/main/java/com/gamzabat/algohub/feature/group/studygroup/service/StudyGroupService.java @@ -31,6 +31,7 @@ import com.gamzabat.algohub.feature.group.studygroup.dto.CreateGroupRequest; import com.gamzabat.algohub.feature.group.studygroup.dto.EditGroupRequest; import com.gamzabat.algohub.feature.group.studygroup.dto.EditGroupVisibilityRequest; +import com.gamzabat.algohub.feature.group.studygroup.dto.GetGroupIdResponse; import com.gamzabat.algohub.feature.group.studygroup.dto.GetGroupMemberResponse; import com.gamzabat.algohub.feature.group.studygroup.dto.GetGroupResponse; import com.gamzabat.algohub.feature.group.studygroup.dto.GetGroupSettingResponse; @@ -38,6 +39,7 @@ import com.gamzabat.algohub.feature.group.studygroup.dto.GetStudyGroupResponse; import com.gamzabat.algohub.feature.group.studygroup.dto.GetStudyGroupWithCodeResponse; import com.gamzabat.algohub.feature.group.studygroup.dto.GroupCodeResponse; +import com.gamzabat.algohub.feature.group.studygroup.dto.GroupRoleResponse; import com.gamzabat.algohub.feature.group.studygroup.dto.UpdateBookmarkResponse; import com.gamzabat.algohub.feature.group.studygroup.dto.UpdateGroupMemberRoleRequest; import com.gamzabat.algohub.feature.group.studygroup.etc.RoleOfGroupMember; @@ -124,7 +126,7 @@ public GroupCodeResponse createGroup(User user, CreateGroupRequest request, Mult } @Transactional - public void joinGroupWithCode(User user, String code) { + public GetGroupIdResponse joinGroupWithCode(User user, String code) { StudyGroup studyGroup = groupRepository.findByGroupCode(code) .orElseThrow(() -> new StudyGroupValidationException(HttpStatus.NOT_FOUND.value(), "존재하지 않는 그룹 입니다.")); @@ -152,8 +154,10 @@ public void joinGroupWithCode(User user, String code) { ); sendNewMemberNotification(studyGroup, member); - log.info("success to join study group"); + + return new GetGroupIdResponse(studyGroup.getId()); + } @Transactional @@ -531,14 +535,14 @@ public void updateGroupMemberRole(User user, Long groupId, UpdateGroupMemberRole } @Transactional(readOnly = true) - public String getRoleInGroup(User user, Long groupId) { + public GroupRoleResponse getRoleInGroup(User user, Long groupId) { StudyGroup group = groupRepository.findById(groupId) .orElseThrow(() -> new CannotFoundGroupException("존재하지 않는 그룹입니다.")); GroupMember member = groupMemberRepository.findByUserAndStudyGroup(user, group) .orElseThrow(() -> new GroupMemberValidationException(HttpStatus.NOT_FOUND.value(), "참여하지 않은 그룹입니다.")); - return member.getRole().getValue(); + return new GroupRoleResponse(member.getRole().getValue()); } @Transactional diff --git a/src/main/java/com/gamzabat/algohub/feature/user/domain/User.java b/src/main/java/com/gamzabat/algohub/feature/user/domain/User.java index 00f3b106..4c0daa8d 100644 --- a/src/main/java/com/gamzabat/algohub/feature/user/domain/User.java +++ b/src/main/java/com/gamzabat/algohub/feature/user/domain/User.java @@ -41,6 +41,7 @@ public class User { private String description = ""; private LocalDateTime deletedAt; + private String githubName; @Enumerated(EnumType.STRING) private Role role; @@ -54,6 +55,7 @@ public User(String email, String password, String nickname, String bjNickname, S this.profileImage = profileImage; this.role = role; this.deletedAt = null; + this.githubName = null; } public void editDescription(String description) { @@ -75,4 +77,8 @@ public void editProfileImage(String profileImage) { public void editPassword(String password) { this.password = password; } + + public void editGithubName(String githubName) { + this.githubName = githubName; + } } diff --git a/src/main/java/com/gamzabat/algohub/feature/user/service/UserService.java b/src/main/java/com/gamzabat/algohub/feature/user/service/UserService.java index 108f7c35..db8eb941 100644 --- a/src/main/java/com/gamzabat/algohub/feature/user/service/UserService.java +++ b/src/main/java/com/gamzabat/algohub/feature/user/service/UserService.java @@ -232,7 +232,7 @@ public void checkEmailDuplication(String email) { public void checkNickname(String nickname) { if (isInvalidNicknameForm(nickname)) throw new CheckNicknameValidationException(HttpStatus.BAD_REQUEST.value(), - "닉네임은 3글자 이상, 16글자 이하이며 특수문자 불가입니다."); + "닉네임은 영문과 숫자로 구성된 3~16글자여야 합니다."); if (userRepository.existsByNickname(nickname)) throw new CheckNicknameValidationException(HttpStatus.CONFLICT.value(), "이미 사용 중인 닉네임입니다."); @@ -251,7 +251,7 @@ public UserInfoResponse otherUserInfo(String userNickname) { } private boolean isInvalidNicknameForm(String nickname) { - String regex = "[^a-zA-Z0-9가-힣ㄱ-ㅎㅏ-ㅣ]"; + String regex = "[^a-zA-Z0-9]"; return nickname.length() < 3 || nickname.length() > 16 || Pattern.compile(regex).matcher(nickname).find(); } diff --git a/src/test/java/com/gamzabat/algohub/feature/studygroup/controller/StudyGroupControllerTest.java b/src/test/java/com/gamzabat/algohub/feature/studygroup/controller/StudyGroupControllerTest.java index d2bb4c46..dcdf9fd3 100644 --- a/src/test/java/com/gamzabat/algohub/feature/studygroup/controller/StudyGroupControllerTest.java +++ b/src/test/java/com/gamzabat/algohub/feature/studygroup/controller/StudyGroupControllerTest.java @@ -42,10 +42,12 @@ import com.gamzabat.algohub.feature.group.studygroup.dto.CreateGroupRequest; import com.gamzabat.algohub.feature.group.studygroup.dto.EditGroupRequest; import com.gamzabat.algohub.feature.group.studygroup.dto.EditGroupVisibilityRequest; +import com.gamzabat.algohub.feature.group.studygroup.dto.GetGroupIdResponse; import com.gamzabat.algohub.feature.group.studygroup.dto.GetGroupMemberResponse; import com.gamzabat.algohub.feature.group.studygroup.dto.GetStudyGroupListsResponse; import com.gamzabat.algohub.feature.group.studygroup.dto.GetStudyGroupResponse; import com.gamzabat.algohub.feature.group.studygroup.dto.GroupCodeResponse; +import com.gamzabat.algohub.feature.group.studygroup.dto.GroupRoleResponse; import com.gamzabat.algohub.feature.group.studygroup.dto.UpdateBookmarkResponse; import com.gamzabat.algohub.feature.group.studygroup.dto.UpdateGroupMemberRoleRequest; import com.gamzabat.algohub.feature.group.studygroup.etc.RoleOfGroupMember; @@ -162,12 +164,14 @@ void createGroupFailed_1(String name, LocalDate startDate, LocalDate endDate, St @DisplayName("그룹 코드를 사용해 그룹 참여 성공") void joinGroupWithCode() throws Exception { // given - doNothing().when(studyGroupService).joinGroupWithCode(any(User.class), anyString()); + GetGroupIdResponse response = new GetGroupIdResponse(12321L); + when(studyGroupService.joinGroupWithCode(any(User.class), anyString())).thenReturn(response); // when, then mockMvc.perform(post("/api/groups/{code}/join", code) .header("Authorization", token) .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()); + .andExpect(status().isOk()) + .andExpect(content().json(objectMapper.writeValueAsString(response))); verify(studyGroupService, times(1)).joinGroupWithCode(user, code); } @@ -778,12 +782,13 @@ void updateGroupMemberRoleFailed_4() throws Exception { @DisplayName("그룹 내 회원 Role 조회 성공") void getRoleInGroup() throws Exception { // given - when(studyGroupService.getRoleInGroup(user, groupId)).thenReturn(RoleOfGroupMember.OWNER.getValue()); + when(studyGroupService.getRoleInGroup(user, groupId)).thenReturn( + new GroupRoleResponse(RoleOfGroupMember.OWNER.getValue())); // when, then mockMvc.perform(get("/api/groups/{groupId}/role", groupId) .header("Authorization", token)) .andExpect(status().isOk()) - .andExpect(content().string(RoleOfGroupMember.OWNER.getValue())); + .andExpect(jsonPath("$.role").value(RoleOfGroupMember.OWNER.getValue())); verify(studyGroupService, times(1)).getRoleInGroup(user, groupId); } diff --git a/src/test/java/com/gamzabat/algohub/feature/user/service/UserServiceTest.java b/src/test/java/com/gamzabat/algohub/feature/user/service/UserServiceTest.java index f49c8d89..9b5fc04b 100644 --- a/src/test/java/com/gamzabat/algohub/feature/user/service/UserServiceTest.java +++ b/src/test/java/com/gamzabat/algohub/feature/user/service/UserServiceTest.java @@ -357,7 +357,7 @@ void checkNickname_1() { } @ParameterizedTest - @ValueSource(strings = {"***asdf", "16글자가초과된nickname입니다", "ab"}) + @ValueSource(strings = {"***asdf", "16asdfasdfnicknameasdf", "ab"}) @DisplayName("닉네임 중복 검사 : 잘못된 형식의 닉네임") void checkNickname_2(String invalidNickname) { // given @@ -365,7 +365,7 @@ void checkNickname_2(String invalidNickname) { assertThatThrownBy(() -> userService.checkNickname(invalidNickname)) .isInstanceOf(CheckNicknameValidationException.class) .hasFieldOrPropertyWithValue("code", HttpStatus.BAD_REQUEST.value()) - .hasFieldOrPropertyWithValue("error", "닉네임은 3글자 이상, 16글자 이하이며 특수문자 불가입니다."); + .hasFieldOrPropertyWithValue("error", "닉네임은 영문과 숫자로 구성된 3~16글자여야 합니다."); } @Test diff --git a/src/test/java/com/gamzabat/algohub/service/StudyGroupServiceTest.java b/src/test/java/com/gamzabat/algohub/service/StudyGroupServiceTest.java index 7cfc4bf3..59a042ec 100644 --- a/src/test/java/com/gamzabat/algohub/service/StudyGroupServiceTest.java +++ b/src/test/java/com/gamzabat/algohub/service/StudyGroupServiceTest.java @@ -40,6 +40,7 @@ import com.gamzabat.algohub.feature.group.studygroup.dto.GetGroupSettingResponse; import com.gamzabat.algohub.feature.group.studygroup.dto.GetStudyGroupListsResponse; import com.gamzabat.algohub.feature.group.studygroup.dto.GetStudyGroupResponse; +import com.gamzabat.algohub.feature.group.studygroup.dto.GroupRoleResponse; import com.gamzabat.algohub.feature.group.studygroup.dto.UpdateBookmarkResponse; import com.gamzabat.algohub.feature.group.studygroup.dto.UpdateGroupMemberRoleRequest; import com.gamzabat.algohub.feature.group.studygroup.etc.RoleOfGroupMember; @@ -674,9 +675,9 @@ void getRoleInGroup() { when(studyGroupRepository.findById(groupId)).thenReturn(Optional.of(group)); when(groupMemberRepository.findByUserAndStudyGroup(user, group)).thenReturn(Optional.ofNullable(groupMember1)); // when - String result = studyGroupService.getRoleInGroup(user, groupId); + GroupRoleResponse result = studyGroupService.getRoleInGroup(user, groupId); // then - assertThat(result).isEqualTo(RoleOfGroupMember.OWNER.getValue()); + assertThat(result.role()).isEqualTo(RoleOfGroupMember.OWNER.getValue()); } @Test