Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 관리자 인증 기능을 구현한다 #37

Merged
merged 67 commits into from
Jul 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
443a56f
feat: 관리자 정보를 저장하고 관리하는 Admin 엔티티 생성
eom-tae-in Jul 6, 2024
e554ca1
feat: 관리자 인증 응답 dto 생성
eom-tae-in Jul 6, 2024
0a3a813
feat: 특정 uri를 관리자에게만 허용되도록 설정하기 위해 필요한 기능 구현
eom-tae-in Jul 6, 2024
ab4b26f
feat: 관리자 인증시 발생할 수 있는 예외 클래스 구현
eom-tae-in Jul 6, 2024
6d66b4a
feat: 관리자 repository 구현
eom-tae-in Jul 6, 2024
2c9eef7
feat: 토큰을 생성하고 토큰의 claim을 찾아주는 기능 구현
eom-tae-in Jul 6, 2024
b0ca1fc
feat: Admin Auth 서비스 구현
eom-tae-in Jul 6, 2024
93e4724
feat: Admin Auth 컨트롤러 구현
eom-tae-in Jul 6, 2024
7ead965
feat: 관리자 인증 요청 dto 생성
eom-tae-in Jul 6, 2024
be5a429
feat: 관리자의 역할과 상태를 저장하는 enum 클래스 구현
eom-tae-in Jul 6, 2024
6c929ba
feat: AT가 만료되었을 때 요청하는 유저의 RT를 Argument Resolver를 통해 손쉽게 받을 수 있도록 구현하…
eom-tae-in Jul 6, 2024
55b13a1
feat: 검증된 관리자의 id 값을 argument resolver를 이용해 쉽게 넘겨받기 위해 어노테이션 구현
eom-tae-in Jul 6, 2024
d02be3b
remove: 불필요해진 클래스 제거
eom-tae-in Jul 6, 2024
b62f73d
move: 파일 경로 변
eom-tae-in Jul 6, 2024
b8ddb8d
refactor: 한 바운디드 컨텍스트안에 존재하던 관리자와 유저를 서로 다른 컨텍스트로 분리
eom-tae-in Jul 6, 2024
99a5b5e
refactor: Member 바운디드 컨텍스트 변경으로 인한 코드 수정
eom-tae-in Jul 6, 2024
41788fc
test: AdminAuthController 테스트 작성
eom-tae-in Jul 6, 2024
c072bd6
test: AdminAuthenticationContext 테스트 작성
eom-tae-in Jul 6, 2024
3115b3c
test: AdminAuthenticationExtractor 테스트 작성
eom-tae-in Jul 6, 2024
48b6564
test: AdminAuthService 테스트 작성
eom-tae-in Jul 6, 2024
1ab3e4d
test: 테스트를 위한 AdminFakeRepository 구현
eom-tae-in Jul 6, 2024
c63397c
test: 테스트를 편하게 하기 위해 Fixture 구현
eom-tae-in Jul 6, 2024
fe6d42f
test: AdminJpaRepository 테스트 작성
eom-tae-in Jul 6, 2024
cba1d28
test: AdminLoginValidCheckerInterceptor 테스트 작성
eom-tae-in Jul 6, 2024
2912e1e
test: Admin 테스트 작성
eom-tae-in Jul 6, 2024
2b0b85f
test: Member 바운디드 컨텍스트 변경으로 인한 테스트 코드 변경
eom-tae-in Jul 6, 2024
db2dc41
docs: member와 auth로 나누어져 있던 member 문서를 하나로 합침
eom-tae-in Jul 6, 2024
0af78a7
docs: auth 문서 제거
eom-tae-in Jul 6, 2024
a74eabb
docs: admin api 문서 작성
eom-tae-in Jul 6, 2024
ea9ee8c
docs: 토큰과 쿠키 만료 시간 설정
eom-tae-in Jul 6, 2024
a52a28e
feat: 관리자 정보를 저장하고 관리하는 Admin 엔티티 생성
eom-tae-in Jul 6, 2024
49f6a0a
feat: 관리자 인증 응답 dto 생성
eom-tae-in Jul 6, 2024
dae8193
feat: 특정 uri를 관리자에게만 허용되도록 설정하기 위해 필요한 기능 구현
eom-tae-in Jul 6, 2024
9a28b47
feat: 관리자 인증시 발생할 수 있는 예외 클래스 구현
eom-tae-in Jul 6, 2024
33ea1cd
feat: 관리자 repository 구현
eom-tae-in Jul 6, 2024
b96c120
feat: 토큰을 생성하고 토큰의 claim을 찾아주는 기능 구현
eom-tae-in Jul 6, 2024
56c22ee
feat: Admin Auth 서비스 구현
eom-tae-in Jul 6, 2024
d33bfc5
feat: Admin Auth 컨트롤러 구현
eom-tae-in Jul 6, 2024
4aae7e6
feat: 관리자 인증 요청 dto 생성
eom-tae-in Jul 6, 2024
c491d58
feat: 관리자의 역할과 상태를 저장하는 enum 클래스 구현
eom-tae-in Jul 6, 2024
4c9ebfb
feat: AT가 만료되었을 때 요청하는 유저의 RT를 Argument Resolver를 통해 손쉽게 받을 수 있도록 구현하…
eom-tae-in Jul 6, 2024
551d5d4
feat: 검증된 관리자의 id 값을 argument resolver를 이용해 쉽게 넘겨받기 위해 어노테이션 구현
eom-tae-in Jul 6, 2024
ce994a9
remove: 불필요해진 클래스 제거
eom-tae-in Jul 6, 2024
10cf2e7
move: 파일 경로 변
eom-tae-in Jul 6, 2024
6eb2887
refactor: 한 바운디드 컨텍스트안에 존재하던 관리자와 유저를 서로 다른 컨텍스트로 분리
eom-tae-in Jul 6, 2024
99553e8
refactor: Member 바운디드 컨텍스트 변경으로 인한 코드 수정
eom-tae-in Jul 6, 2024
ab57bc3
test: AdminAuthController 테스트 작성
eom-tae-in Jul 6, 2024
ffc72da
test: AdminAuthenticationContext 테스트 작성
eom-tae-in Jul 6, 2024
dc412d0
test: AdminAuthenticationExtractor 테스트 작성
eom-tae-in Jul 6, 2024
0b72297
test: AdminAuthService 테스트 작성
eom-tae-in Jul 6, 2024
7e5d3bd
test: 테스트를 위한 AdminFakeRepository 구현
eom-tae-in Jul 6, 2024
46fb99a
test: 테스트를 편하게 하기 위해 Fixture 구현
eom-tae-in Jul 6, 2024
213b5a7
test: AdminJpaRepository 테스트 작성
eom-tae-in Jul 6, 2024
782cb53
test: AdminLoginValidCheckerInterceptor 테스트 작성
eom-tae-in Jul 6, 2024
b9aa487
test: Admin 테스트 작성
eom-tae-in Jul 6, 2024
fccd653
test: Member 바운디드 컨텍스트 변경으로 인한 테스트 코드 변경
eom-tae-in Jul 6, 2024
ebbfec8
docs: develop 브랜치와 변황 문제로 인한 문서 수정
eom-tae-in Jul 6, 2024
58160b3
refactor: 클래스명 변경으로 인한 코드 수정
eom-tae-in Jul 6, 2024
2df5b04
test: 프로덕션 코드 수정으로 인한 테스트 수정
eom-tae-in Jul 6, 2024
c08ec60
docs: 쿠키, 토큰 만료시간 설정
eom-tae-in Jul 6, 2024
c78c9ea
docs: develop 브랜치와의 병합 충돌로 인해 발생한 문서 수정
eom-tae-in Jul 6, 2024
b85000f
docs: admin api 문서 작성
eom-tae-in Jul 6, 2024
bd171df
docs: 토큰과 쿠키 만료 시간 설정
eom-tae-in Jul 6, 2024
edc1577
docs: 공백 제거
eom-tae-in Jul 6, 2024
9f21e41
remove: 불필요한 클래스 제거
eom-tae-in Jul 6, 2024
0741feb
refactor: dto에 validation 추가
eom-tae-in Jul 6, 2024
15e5a93
refactor: 불필요한 로직 제거
eom-tae-in Jul 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions src/docs/asciidoc/admin.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
:toc: left
:source-highlighter: highlightjs
:sectlinks:
:toclevels: 2
:sectlinks:
:sectnums:

== Admin

=== 회원 가입(POST api/admins/auth/sign-up)

==== 요청
include::{snippets}/admin-auth-controller-test/관리자_회원가입/http-request.adoc[]
include::{snippets}/admin-auth-controller-test/관리자_회원가입/request-fields.adoc[]

==== 응답
include::{snippets}/admin-auth-controller-test/관리자_회원가입/http-response.adoc[]
include::{snippets}/admin-auth-controller-test/관리자_회원가입/response-headers.adoc[]
include::{snippets}/admin-auth-controller-test/관리자_회원가입/response-fields.adoc[]


=== 로그인(POST api/admins/auth/login)

==== 요청
include::{snippets}/admin-auth-controller-test/관리자_로그인/http-request.adoc[]
include::{snippets}/admin-auth-controller-test/관리자_로그인/request-fields.adoc[]

==== 응답
include::{snippets}/admin-auth-controller-test/관리자_로그인/http-response.adoc[]
include::{snippets}/admin-auth-controller-test/관리자_로그인/response-headers.adoc[]
include::{snippets}/admin-auth-controller-test/관리자_로그인/response-fields.adoc[]


=== 엑세스 토큰 재발행(POST api/admins/auth/access-token-regeneration)

==== 요청
include::{snippets}/admin-auth-controller-test/관리자_액세스_토큰_재발행/http-request.adoc[]
include::{snippets}/admin-auth-controller-test/관리자_액세스_토큰_재발행/request-headers.adoc[]

==== 응답
include::{snippets}/admin-auth-controller-test/관리자_액세스_토큰_재발행/http-response.adoc[]
include::{snippets}/admin-auth-controller-test/관리자_액세스_토큰_재발행/response-fields.adoc[]
25 changes: 0 additions & 25 deletions src/docs/asciidoc/auth.adoc

This file was deleted.

2 changes: 1 addition & 1 deletion src/docs/asciidoc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
:sectlinks:

== atwoz Server API
* link:auth.html[회원 API]
* link:member.html[회원 정보 API]
* link:admin.html[관리자 정보 API]
* link:mission.html[미션 API]
* link:membermissions.html[회원 미션 API]
* link:survey.html[연애고사 API]
Expand Down
14 changes: 13 additions & 1 deletion src/docs/asciidoc/member.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,23 @@

== Member

=== 로그인 (POST api/members/auth/login)

==== 요청
include::{snippets}/member-auth-controller-web-mvc-test/유저_로그인/http-request.adoc[]
include::{snippets}/member-auth-controller-web-mvc-test/유저_로그인/request-fields.adoc[]

==== 응답
include::{snippets}/member-auth-controller-web-mvc-test/유저_로그인/http-response.adoc[]
include::{snippets}/member-auth-controller-web-mvc-test/유저_로그인/request-fields.adoc[]


=== 닉네임 중복 확인 (GET /api/members/nickname/existence)

==== 요청
include::{snippets}/member-controller-web-mvc-test/닉네임_중복_확인/request-headers.adoc[]
include::{snippets}/member-controller-web-mvc-test/닉네임_중복_확인/http-request.adoc[]
include::{snippets}/member-controller-web-mvc-test/닉네임_중복_확인/request-headers.adoc[]
include::{snippets}/member-controller-web-mvc-test/닉네임_중복_확인/path-parameters.adoc[]

==== 응답
include::{snippets}/member-controller-web-mvc-test/닉네임_중복_확인/http-response.adoc[]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.atwoz.admin.application.auth;

import com.atwoz.admin.application.auth.dto.AdminAccessTokenResponse;
import com.atwoz.admin.application.auth.dto.AdminLoginRequest;
import com.atwoz.admin.application.auth.dto.AdminProfileSignUpRequest;
import com.atwoz.admin.application.auth.dto.AdminSignUpRequest;
import com.atwoz.admin.application.auth.dto.AdminTokenResponse;
import com.atwoz.admin.domain.admin.Admin;
import com.atwoz.admin.domain.admin.AdminRepository;
import com.atwoz.admin.domain.admin.AdminTokenProvider;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

토큰을 주는 것은 도메인 보다는 서비스가 낫지 않을까 조심스럽게 의견 드려봅니다..! 다르게 생각한다면 편하게 말씀해주세요!

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 토큰을 암호화하는 부분이 외부라이브러리를 이용해서 인프라 레이어로 두고 이를 인터페이스화시킨 것을 도메인 레이어에서 둬서 응용 계층에서 호출해서 사용하도록 구현하려고 했는데 혹시 서비스 레이어에 두는게 더 좋은 것 같다고 생각한 이유를 알 수 있을까요?

import com.atwoz.admin.exception.exceptions.AdminNotFoundException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@RequiredArgsConstructor
@Service
@Transactional
public class AdminAuthService {

private static final String ID = "id";

private final AdminRepository adminRepository;
private final AdminTokenProvider adminTokenProvider;

public AdminTokenResponse signUp(final AdminSignUpRequest adminSignUpRequest) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dto로의 변환은 레이어드 아키텍처에서 컨트롤러에서 해주는 것이 책임이라고 생각하는데 어떻게 생각하시나요?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

응용레이어에서 AT와 RT를 만들어서 컨트롤러로 넘겨주기 때문에 이를 컨트롤러에서 받기 위한 DTO를 만들어서 사용했습니다. 응답 바디에 담기는 DTO 타입은 저도 컨트롤러에서 하는게 맞다고 생각합니다! 하지만 지금은 응용 레이어에서 컨트롤러로 넘겨주기 위한 DTO이기 때문에 큰 문제가 없어보이는데 어떻게 생각하시나요? 컨트롤러에서 응답 바디에 넘겨주는 DTO의 타입은 AdminAccessTokenResponse입니다!

AdminProfileSignUpRequest adminProfileSignUpRequest = adminSignUpRequest.adminProfileSignUpRequest();
Admin admin = Admin.createWith(
adminSignUpRequest.email(),
adminSignUpRequest.password(),
adminSignUpRequest.confirmPassword(),
adminProfileSignUpRequest.name(),
adminProfileSignUpRequest.phoneNumber()
);
Admin savedAdmin = adminRepository.save(admin);

return createAdminTokenResponse(savedAdmin.getId());
}

private AdminTokenResponse createAdminTokenResponse(final Long id) {
return new AdminTokenResponse(
adminTokenProvider.createAccessToken(id),
adminTokenProvider.createRefreshToken(id)
);
}

public AdminTokenResponse login(final AdminLoginRequest adminLoginRequest) {
Admin foundAdmin = findAdminByEmail(adminLoginRequest.email());
foundAdmin.validatePassword(adminLoginRequest.password());

return createAdminTokenResponse(foundAdmin.getId());
}

public AdminAccessTokenResponse reGenerateAccessToken(final String refreshToken) {
Admin foundAdmin = findAdminById(adminTokenProvider.extract(refreshToken, ID, Long.class));

return new AdminAccessTokenResponse(adminTokenProvider.createAccessToken(foundAdmin.getId()));
}

private Admin findAdminByEmail(final String email) {
return adminRepository.findAdminByEmail(email)
.orElseThrow(AdminNotFoundException::new);
}

private Admin findAdminById(final Long id) {
return adminRepository.findAdminById(id)
.orElseThrow(AdminNotFoundException::new);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.atwoz.admin.application.auth.dto;

public record AdminAccessTokenResponse(
String accessToken
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.atwoz.admin.application.auth.dto;

import jakarta.validation.constraints.NotBlank;

public record AdminLoginRequest(
@NotBlank(message = "이메일을 입력해주세요")
String email,

@NotBlank(message = "비밀번호를 입력해주세요")
String password
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.atwoz.admin.application.auth.dto;

import jakarta.validation.constraints.NotBlank;

public record AdminProfileSignUpRequest(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기도 validation 추가 해주세요!

@NotBlank(message = "이름을 입력해주세요.")
String name,

@NotBlank(message = "전화번호를 입력해주세요.")
String phoneNumber
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.atwoz.admin.application.auth.dto;

import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;

public record AdminSignUpRequest(
@NotBlank(message = "이메일을 입력해주세요")
String email,

@NotBlank(message = "비밀번호를 입력해주세요")
String password,

@NotBlank(message = "비밀번호 확인을 위해 다시한번 입력해주세요")
String confirmPassword,

@Valid
@NotNull(message = "관리자 프로필 정보를 입력해주세요")
AdminProfileSignUpRequest adminProfileSignUpRequest
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.atwoz.admin.application.auth.dto;

public record AdminTokenResponse(
String accessToken,
String refreshToken
) {
}
40 changes: 40 additions & 0 deletions src/main/java/com/atwoz/admin/config/AdminAuthConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.atwoz.admin.config;

import com.atwoz.admin.ui.auth.interceptor.AdminLoginValidCheckerInterceptor;
import com.atwoz.admin.ui.auth.support.resolver.AdminAuthArgumentResolver;
import com.atwoz.admin.ui.auth.support.resolver.AdminRefreshTokenExtractionArgumentResolver;
import com.atwoz.global.config.interceptor.PathMatcherInterceptor;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import static com.atwoz.global.config.interceptor.support.HttpMethod.OPTIONS;

@RequiredArgsConstructor
@Configuration
public class AdminAuthConfig implements WebMvcConfigurer {

private final AdminAuthArgumentResolver adminAuthArgumentResolver;
private final AdminRefreshTokenExtractionArgumentResolver adminRefreshTokenExtractionArgumentResolver;
private final AdminLoginValidCheckerInterceptor adminLoginValidCheckerInterceptor;

@Override
public void addInterceptors(final InterceptorRegistry registry) {
registry.addInterceptor(adminLoginValidCheckerInterceptor());
}

private HandlerInterceptor adminLoginValidCheckerInterceptor() {
return new PathMatcherInterceptor(adminLoginValidCheckerInterceptor)
.excludePathPattern("/**", OPTIONS);
}

@Override
public void addArgumentResolvers(final List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(adminAuthArgumentResolver);
resolvers.add(adminRefreshTokenExtractionArgumentResolver);
}
}
87 changes: 87 additions & 0 deletions src/main/java/com/atwoz/admin/domain/admin/Admin.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package com.atwoz.admin.domain.admin;

import com.atwoz.admin.domain.admin.vo.AdminStatus;
import com.atwoz.admin.domain.admin.vo.Authority;
import com.atwoz.admin.domain.admin.vo.Department;
import com.atwoz.admin.exception.exceptions.InvalidPasswordException;
import com.atwoz.admin.exception.exceptions.PasswordMismatchException;
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 lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Builder
@EqualsAndHashCode(of = "id", callSuper = false)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Admin {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(unique = true, nullable = false)
private String email;

@Column(nullable = false)
private String password;

@Column(nullable = false)
private String name;

@Column(nullable = false)
private String phoneNumber;

@Enumerated(value = EnumType.STRING)
@Column(nullable = false)
private Authority authority;

@Enumerated(value = EnumType.STRING)
@Column(nullable = false)
private Department department;

@Enumerated(value = EnumType.STRING)
@Column(nullable = false)
private AdminStatus adminStatus;

public static Admin createWith(final String email,
final String password,
final String confirmPassword,
final String name,
final String phoneNumber) {
validatePasswordsMatch(password, confirmPassword);
return Admin.builder()
.email(email)
.password(password)
.name(name)
.phoneNumber(phoneNumber)
.authority(Authority.ADMIN)
.department(Department.OPERATION)
.adminStatus(AdminStatus.PENDING)
.build();
}

private static void validatePasswordsMatch(final String password,
final String confirmPassword) {
if (!password.equals(confirmPassword)) {
throw new PasswordMismatchException();
}
}

public void validatePassword(final String password) {
if (!password.equals(this.password)) {
throw new InvalidPasswordException();
}
}
}
12 changes: 12 additions & 0 deletions src/main/java/com/atwoz/admin/domain/admin/AdminRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.atwoz.admin.domain.admin;

import java.util.Optional;

public interface AdminRepository {

Admin save(Admin admin);

Optional<Admin> findAdminById(Long id);

Optional<Admin> findAdminByEmail(String email);
}
10 changes: 10 additions & 0 deletions src/main/java/com/atwoz/admin/domain/admin/AdminTokenProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.atwoz.admin.domain.admin;

public interface AdminTokenProvider {

String createAccessToken(Long id);

String createRefreshToken(Long id);

<T> T extract(String token, String claimName, Class<T> classType);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제네릭을 사용하신 이유가 따로 있을까요?!

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

클레임에는 문자열(이메일), 숫자(id)등이 들어값니다. 그래서 추출할 때 만약 문자열 값과 숫자 값을 모두 추출할 일이 있으면 해당 메서드를 여러개 만들어야 해서 중복을 최소한해보고자 제네릭을 이용해서 구현해보았습니다.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.atwoz.admin.domain.admin.vo;

public enum AdminStatus {

AVAILABLE,
UNAVAILABLE,
PENDING
}
Loading
Loading