diff --git a/.editorconfig b/.editorconfig
deleted file mode 100644
index 855c0c54..00000000
--- a/.editorconfig
+++ /dev/null
@@ -1,18 +0,0 @@
-root = true
-
-[*]
-charset = utf-8
-end_of_line = lf
-indent_size = 4
-indent_style = space
-insert_final_newline = true
-max_line_length = 120
-tab_width = 4
-trim_trailing_whitespace = true
-
-[*.bat]
-end_of_line = crlf
-
-[*.java]
-indent_style = tab
-ij_java_blank_lines_after_class_header = 1
diff --git a/build.gradle b/build.gradle
index 1fc75afe..0bb89395 100644
--- a/build.gradle
+++ b/build.gradle
@@ -52,6 +52,9 @@ dependencies {
// H2
implementation 'com.h2database:h2'
+
+ // Configuration Binding
+ annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"
}
tasks.named('test') {
diff --git a/config/naver-checkstyle-rules.xml b/config/naver-checkstyle-rules.xml
deleted file mode 100644
index dafbb4d1..00000000
--- a/config/naver-checkstyle-rules.xml
+++ /dev/null
@@ -1,433 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/config/naver-intellij-formatter-custom.xml b/config/naver-intellij-formatter-custom.xml
index 26f28954..dc4c4a2e 100644
--- a/config/naver-intellij-formatter-custom.xml
+++ b/config/naver-intellij-formatter-custom.xml
@@ -1,74 +1,75 @@
-
-
-
-
-
-
-
-
-
-
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/java/com/moabam/MoabamServerApplication.java b/src/main/java/com/moabam/MoabamServerApplication.java
index 5390acc3..e2dbce1d 100644
--- a/src/main/java/com/moabam/MoabamServerApplication.java
+++ b/src/main/java/com/moabam/MoabamServerApplication.java
@@ -2,12 +2,13 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
+@ConfigurationPropertiesScan
@SpringBootApplication
public class MoabamServerApplication {
public static void main(String[] args) {
SpringApplication.run(MoabamServerApplication.class, args);
}
-
}
diff --git a/src/main/java/com/moabam/api/application/auth/AuthenticationService.java b/src/main/java/com/moabam/api/application/auth/AuthenticationService.java
new file mode 100644
index 00000000..3688b540
--- /dev/null
+++ b/src/main/java/com/moabam/api/application/auth/AuthenticationService.java
@@ -0,0 +1,24 @@
+package com.moabam.api.application.auth;
+
+import org.springframework.stereotype.Service;
+
+import com.moabam.api.dto.auth.AuthorizationCodeIssue;
+import com.moabam.global.config.OAuthConfig;
+
+import lombok.RequiredArgsConstructor;
+
+@Service
+@RequiredArgsConstructor
+public class AuthenticationService {
+
+ private final OAuthConfig oAuthConfig;
+
+ public String getAuthorizaionCodeUri() {
+ return AuthorizationCodeIssue.builder()
+ .clientId(oAuthConfig.client().clientId())
+ .redirectUri(oAuthConfig.provider().redirectUri())
+ .scope(oAuthConfig.client().scope())
+ .build()
+ .generateQueryParamsWith(oAuthConfig.provider().authorizationUri());
+ }
+}
diff --git a/src/main/java/com/moabam/api/domain/member/Member.java b/src/main/java/com/moabam/api/domain/member/Member.java
new file mode 100644
index 00000000..58440858
--- /dev/null
+++ b/src/main/java/com/moabam/api/domain/member/Member.java
@@ -0,0 +1,90 @@
+package com.moabam.api.domain.member;
+
+import static java.util.Objects.*;
+
+import java.time.LocalDateTime;
+
+import org.hibernate.annotations.ColumnDefault;
+
+import com.moabam.global.common.entity.BaseTimeEntity;
+import com.moabam.global.common.util.BaseImageUrl;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.EnumType;
+import jakarta.persistence.Enumerated;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.AccessLevel;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@Entity
+@Table(name = "members")
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+public class Member extends BaseTimeEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "member_id")
+ private Long id;
+
+ @Column(name = "social_id", nullable = false, unique = true)
+ private String socialId;
+
+ @Column(name = "nickname", nullable = false, unique = true)
+ private String nickname;
+
+ @Column(name = "intro")
+ private String intro;
+
+ @Column(name = "profile_image", nullable = false)
+ private String profileImage;
+
+ @Column(name = "total_certify_count", nullable = false)
+ @ColumnDefault("0")
+ private long totalCertifyCount;
+
+ @Column(name = "report_count", nullable = false)
+ @ColumnDefault("0")
+ private int reportCount;
+
+ @Column(name = "current_night_count", nullable = false)
+ @ColumnDefault("0")
+ private int currentNightCount;
+
+ @Column(name = "current_morning_count", nullable = false)
+ @ColumnDefault("0")
+ private int currentMorningCount;
+
+ @Column(name = "morning_bug", nullable = false)
+ @ColumnDefault("0")
+ private int morningBug;
+
+ @Column(name = "night_bug", nullable = false)
+ @ColumnDefault("0")
+ private int nightBug;
+
+ @Column(name = "golden_bug", nullable = false)
+ @ColumnDefault("0")
+ private int goldenBug;
+
+ @Enumerated(EnumType.STRING)
+ @Column(name = "role", nullable = false)
+ private Role role;
+
+ @Column(name = "deleted_at")
+ private LocalDateTime deletedAt;
+
+ @Builder
+ private Member(String socialId, String nickname, String profileImage) {
+ this.socialId = requireNonNull(socialId);
+ this.nickname = requireNonNull(nickname);
+ this.profileImage = requireNonNullElse(profileImage, BaseImageUrl.PROFILE_URL.getUrl());
+ this.role = Role.USER;
+ }
+}
diff --git a/src/main/java/com/moabam/api/domain/member/Role.java b/src/main/java/com/moabam/api/domain/member/Role.java
new file mode 100644
index 00000000..b168314b
--- /dev/null
+++ b/src/main/java/com/moabam/api/domain/member/Role.java
@@ -0,0 +1,8 @@
+package com.moabam.api.domain.member;
+
+public enum Role {
+
+ USER,
+ @SuppressWarnings("checkstyle:JavadocVariable") BLACK,
+ ADMIN
+}
diff --git a/src/main/java/com/moabam/api/dto/auth/AuthorizationCodeIssue.java b/src/main/java/com/moabam/api/dto/auth/AuthorizationCodeIssue.java
new file mode 100644
index 00000000..80a7ec23
--- /dev/null
+++ b/src/main/java/com/moabam/api/dto/auth/AuthorizationCodeIssue.java
@@ -0,0 +1,45 @@
+package com.moabam.api.dto.auth;
+
+import static com.moabam.global.common.util.OAuthParameterNames.*;
+
+import java.util.List;
+
+import org.springframework.web.util.UriComponentsBuilder;
+
+import com.moabam.global.common.util.GlobalConstant;
+
+import lombok.Builder;
+
+public record AuthorizationCodeIssue(
+ String clientId,
+ String redirectUri,
+ String responseType,
+ List scope,
+ String state
+) {
+
+ @Builder
+ public AuthorizationCodeIssue(String clientId, String redirectUri, String responseType, List scope,
+ String state) {
+ this.clientId = clientId;
+ this.redirectUri = redirectUri;
+ this.responseType = responseType;
+ this.scope = scope;
+ this.state = state;
+ }
+
+ public String generateQueryParamsWith(String baseUrl) {
+ UriComponentsBuilder authorizationCodeUri = UriComponentsBuilder
+ .fromPath(baseUrl)
+ .queryParam(RESPONSE_TYPE, CODE)
+ .queryParam(CLIENT_ID, clientId)
+ .queryParam(REDIRECT_URI, redirectUri);
+
+ if (!scope.isEmpty()) {
+ String scopes = String.join(GlobalConstant.COMMA, scope);
+ authorizationCodeUri.queryParam(SCOPE, scopes);
+ }
+
+ return authorizationCodeUri.toUriString();
+ }
+}
diff --git a/src/main/java/com/moabam/api/presentation/member/MemberController.java b/src/main/java/com/moabam/api/presentation/member/MemberController.java
new file mode 100644
index 00000000..703b415a
--- /dev/null
+++ b/src/main/java/com/moabam/api/presentation/member/MemberController.java
@@ -0,0 +1,29 @@
+package com.moabam.api.presentation.member;
+
+import java.io.IOException;
+
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import com.moabam.api.application.auth.AuthenticationService;
+import com.moabam.global.common.util.GlobalConstant;
+
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+
+@RestController
+@RequestMapping("/members")
+@RequiredArgsConstructor
+public class MemberController {
+
+ private final AuthenticationService authenticationService;
+
+ @GetMapping
+ public void socialLogin(HttpServletResponse httpServletResponse) throws IOException {
+ String authorizationCodeUri = authenticationService.getAuthorizaionCodeUri();
+ httpServletResponse.setContentType(MediaType.APPLICATION_FORM_URLENCODED + GlobalConstant.CHARSET_UTF_8);
+ httpServletResponse.sendRedirect(authorizationCodeUri);
+ }
+}
diff --git a/src/main/java/com/moabam/global/common/util/BaseImageUrl.java b/src/main/java/com/moabam/global/common/util/BaseImageUrl.java
new file mode 100644
index 00000000..8a302328
--- /dev/null
+++ b/src/main/java/com/moabam/global/common/util/BaseImageUrl.java
@@ -0,0 +1,15 @@
+package com.moabam.global.common.util;
+
+import lombok.Getter;
+
+@Getter
+public enum BaseImageUrl {
+
+ PROFILE_URL("/profile/baseUrl");
+
+ private String url;
+
+ BaseImageUrl(String url) {
+ this.url = url;
+ }
+}
diff --git a/src/main/java/com/moabam/global/common/util/GlobalConstant.java b/src/main/java/com/moabam/global/common/util/GlobalConstant.java
new file mode 100644
index 00000000..5b87cd58
--- /dev/null
+++ b/src/main/java/com/moabam/global/common/util/GlobalConstant.java
@@ -0,0 +1,11 @@
+package com.moabam.global.common.util;
+
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class GlobalConstant {
+
+ public static final String COMMA = ",";
+ public static final String CHARSET_UTF_8 = ";charset=UTF-8";
+}
diff --git a/src/main/java/com/moabam/global/common/util/OAuthParameterNames.java b/src/main/java/com/moabam/global/common/util/OAuthParameterNames.java
new file mode 100644
index 00000000..efc65e59
--- /dev/null
+++ b/src/main/java/com/moabam/global/common/util/OAuthParameterNames.java
@@ -0,0 +1,14 @@
+package com.moabam.global.common.util;
+
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class OAuthParameterNames {
+
+ public static final String RESPONSE_TYPE = "response_type";
+ public static final String CODE = "code";
+ public static final String CLIENT_ID = "client_id";
+ public static final String REDIRECT_URI = "redirect_uri";
+ public static final String SCOPE = "scope";
+}
diff --git a/src/main/java/com/moabam/global/config/OAuthConfig.java b/src/main/java/com/moabam/global/config/OAuthConfig.java
new file mode 100644
index 00000000..cff7f45b
--- /dev/null
+++ b/src/main/java/com/moabam/global/config/OAuthConfig.java
@@ -0,0 +1,28 @@
+package com.moabam.global.config;
+
+import java.util.List;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+@ConfigurationProperties(prefix = "oauth2")
+public record OAuthConfig(
+ Provider provider,
+ Client client
+) {
+
+ public record Client(
+ String provider,
+ String clientId,
+ String authorizationGrantType,
+ List scope
+ ) {
+
+ }
+
+ public record Provider(
+ String authorizationUri,
+ String redirectUri
+ ) {
+
+ }
+}
diff --git a/src/main/java/com/moabam/global/error/model/ErrorMessage.java b/src/main/java/com/moabam/global/error/model/ErrorMessage.java
index 662874f6..0ad98085 100644
--- a/src/main/java/com/moabam/global/error/model/ErrorMessage.java
+++ b/src/main/java/com/moabam/global/error/model/ErrorMessage.java
@@ -7,7 +7,8 @@
@RequiredArgsConstructor
public enum ErrorMessage {
- INVALID_REQUEST_FIELD("올바른 요청 정보가 아닙니다.");
+ INVALID_REQUEST_FIELD("올바른 요청 정보가 아닙니다."),
+ LOGIN_FAILED("로그인에 실패했습니다.");
private final String message;
}
diff --git a/src/main/resources/config b/src/main/resources/config
index 8bc59e64..600a2467 160000
--- a/src/main/resources/config
+++ b/src/main/resources/config
@@ -1 +1 @@
-Subproject commit 8bc59e6455ce1220e00acf676849951cbd935373
+Subproject commit 600a2467b779690dd9c24f172425832b0d5eb87b
diff --git a/src/test/java/com/moabam/api/application/auth/AuthenticationServiceTest.java b/src/test/java/com/moabam/api/application/auth/AuthenticationServiceTest.java
new file mode 100644
index 00000000..6f29348e
--- /dev/null
+++ b/src/test/java/com/moabam/api/application/auth/AuthenticationServiceTest.java
@@ -0,0 +1,45 @@
+package com.moabam.api.application.auth;
+
+import static org.assertj.core.api.Assertions.*;
+
+import java.util.List;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import com.moabam.global.config.OAuthConfig;
+
+@ExtendWith(MockitoExtension.class)
+class AuthenticationServiceTest {
+
+ @InjectMocks
+ AuthenticationService authenticationService;
+
+ OAuthConfig oauthConfig;
+
+ @BeforeEach
+ public void initParams() {
+ oauthConfig = new OAuthConfig(
+ new OAuthConfig.Provider("https://test.com/test/test", "http://test:8080/test"),
+ new OAuthConfig.Client("provider", "testtestetsttest", "authorization_code",
+ List.of("profile_nickname", "profile_image"))
+ );
+ ReflectionTestUtils.setField(authenticationService, "oAuthConfig", oauthConfig);
+ }
+
+ @DisplayName("authentication code Url 생성 성공")
+ @Test
+ void authenticationUrl() {
+ // Given + When
+ String authorizaionCodeUri = authenticationService.getAuthorizaionCodeUri();
+
+ // Then
+ assertThat(authorizaionCodeUri).contains(oauthConfig.client().clientId(),
+ String.join(",", oauthConfig.client().scope()), oauthConfig.provider().redirectUri());
+ }
+}
diff --git a/src/test/java/com/moabam/api/domain/member/MemberTest.java b/src/test/java/com/moabam/api/domain/member/MemberTest.java
new file mode 100644
index 00000000..caa56681
--- /dev/null
+++ b/src/test/java/com/moabam/api/domain/member/MemberTest.java
@@ -0,0 +1,63 @@
+package com.moabam.api.domain.member;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import com.moabam.global.common.util.BaseImageUrl;
+
+class MemberTest {
+
+ String socialId = "1";
+ String nickname = "밥세공기";
+ String profileImage = "kakao/profile/url";
+
+ @DisplayName("회원 생성 성공")
+ @Test
+ void create_member_success() {
+ // When + Then
+ assertThatNoException().isThrownBy(() -> Member.builder()
+ .socialId(socialId)
+ .nickname(nickname)
+ .profileImage(profileImage)
+ .build());
+ }
+
+ @DisplayName("프로필 이미지 없이 회원 생성 성공")
+ @Test
+ void create_member_noImage_success() {
+ // When + Then
+ assertThatNoException().isThrownBy(() -> {
+ Member member = Member.builder()
+ .socialId(socialId)
+ .nickname(nickname)
+ .profileImage(null)
+ .build();
+
+ assertAll(
+ () -> assertThat(member.getProfileImage()).isEqualTo(BaseImageUrl.PROFILE_URL.getUrl()),
+ () -> assertThat(member.getRole()).isEqualTo(Role.USER)
+ );
+ });
+ }
+
+ @DisplayName("소셜ID에 따른 회원 생성 실패")
+ @Test
+ void creat_member_failBy_socialId() {
+ // When + Then
+ assertThatThrownBy(Member.builder()
+ .nickname(nickname)::build)
+ .isInstanceOf(NullPointerException.class);
+ }
+
+ @DisplayName("닉네임에 따른 회원 생성 실패")
+ @Test
+ void create_member_failBy_nickname() {
+ // When + Then
+ assertThatThrownBy(Member.builder()
+ .socialId(socialId)::build)
+ .isInstanceOf(NullPointerException.class);
+ }
+}
diff --git a/src/test/java/com/moabam/api/presentation/member/MemberControllerTest.java b/src/test/java/com/moabam/api/presentation/member/MemberControllerTest.java
new file mode 100644
index 00000000..bf09a503
--- /dev/null
+++ b/src/test/java/com/moabam/api/presentation/member/MemberControllerTest.java
@@ -0,0 +1,67 @@
+package com.moabam.api.presentation.member;
+
+import static com.moabam.global.common.util.OAuthParameterNames.*;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.ResultActions;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.moabam.api.application.auth.AuthenticationService;
+import com.moabam.global.common.util.GlobalConstant;
+import com.moabam.global.config.OAuthConfig;
+
+@SpringBootTest(
+ properties = {
+ "oauth2.provider.authorization-uri=https://kauth.kakao.com/oauth/authorize",
+ "oauth2.provider.redirect-uri=http://localhost:8080/members/login/kakao/oauth",
+ "oauth2.client.client-id=testtesttesttesttesttesttesttesttest",
+ "oauth2.client.authorization-grant-type=authorization_code",
+ "oauth2.client.scope=profile_nickname,profile_image"
+ }
+)
+@AutoConfigureMockMvc
+class MemberControllerTest {
+
+ @Autowired
+ MockMvc mockMvc;
+
+ @Autowired
+ ObjectMapper objectMapper;
+
+ @Autowired
+ AuthenticationService authenticationService;
+
+ @Autowired
+ OAuthConfig oAuthConfig;
+
+ @DisplayName("인가 코드 받기 위한 로그인 페이지 요청")
+ @Test
+ void authorization_code_request_success() throws Exception {
+ // given
+ String uri = UriComponentsBuilder
+ .fromPath(oAuthConfig.provider().authorizationUri())
+ .queryParam(RESPONSE_TYPE, "code")
+ .queryParam(CLIENT_ID, oAuthConfig.client().clientId())
+ .queryParam(REDIRECT_URI, oAuthConfig.provider().redirectUri())
+ .queryParam(SCOPE, String.join(",", oAuthConfig.client().scope()))
+ .toUriString();
+
+ // when
+ ResultActions result = mockMvc.perform(get("/members"));
+
+ // then
+ result.andExpect(status().is3xxRedirection())
+ .andExpect(header().string("Content-type",
+ MediaType.APPLICATION_FORM_URLENCODED_VALUE + GlobalConstant.CHARSET_UTF_8))
+ .andExpect(redirectedUrl(uri));
+ }
+}