commit
This commit is contained in:
@@ -5,16 +5,16 @@ import lombok.Getter;
|
||||
@Getter
|
||||
public class BizBaseException extends RuntimeException {
|
||||
|
||||
private final ExceptionRule exceptionRule;
|
||||
private final ErrorRule errorRule;
|
||||
|
||||
public BizBaseException() {
|
||||
super(ExceptionRule.SYSTE_ERROR.getMessage());
|
||||
this.exceptionRule = ExceptionRule.SYSTE_ERROR;
|
||||
this.errorRule = ExceptionRule.SYSTE_ERROR;
|
||||
}
|
||||
|
||||
public BizBaseException(ExceptionRule exceptionRule) {
|
||||
public BizBaseException(ErrorRule exceptionRule) {
|
||||
super(exceptionRule.getMessage());
|
||||
this.exceptionRule = exceptionRule;
|
||||
this.errorRule = exceptionRule;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -10,12 +10,12 @@ import lombok.Getter;
|
||||
@Getter
|
||||
public class BizErrorResponse extends ErrorResponse {
|
||||
|
||||
public BizErrorResponse(ExceptionRule exceptionRule, List<RejectedValue> errors) {
|
||||
public BizErrorResponse(ErrorRule exceptionRule, List<RejectedValue> errors) {
|
||||
super(exceptionRule);
|
||||
this.errors = errors;
|
||||
}
|
||||
|
||||
public static BizErrorResponse valueOf(ExceptionRule exceptionRule) {
|
||||
public static BizErrorResponse valueOf(ErrorRule exceptionRule) {
|
||||
return new BizErrorResponse(exceptionRule, null);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,9 +16,7 @@ public enum ExceptionRule implements ErrorRule {
|
||||
NOT_FOUND(HttpStatus.NOT_FOUND, "요청한 자원을 찾을 수 없습니다."),
|
||||
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버에 오류가 발생했습니다."),
|
||||
CONFLICT(HttpStatus.CONFLICT, "데이터 충돌이 발생했습니다."),
|
||||
UNPROCESSABLE_ENTITY(HttpStatus.UNPROCESSABLE_ENTITY, "요청을 처리할 수 없습니다."),
|
||||
USER_NOT_FOUND(HttpStatus.NOT_FOUND, "사용자를 찾을 수 없습니다."),
|
||||
SCHEDULE_NOT_FOUND(HttpStatus.NOT_FOUND, "스케쥴 정보를 찾을 수 없습니다.");
|
||||
UNPROCESSABLE_ENTITY(HttpStatus.UNPROCESSABLE_ENTITY, "요청을 처리할 수 없습니다.");
|
||||
|
||||
private final HttpStatus status;
|
||||
private String message;
|
||||
|
||||
@@ -9,7 +9,7 @@ public class GlobalExceptionHandler {
|
||||
|
||||
@ExceptionHandler(BizBaseException.class)
|
||||
public BizErrorResponse handleCustomException(BizBaseException e) {
|
||||
return BizErrorResponse.valueOf(e.getExceptionRule());
|
||||
return BizErrorResponse.valueOf(e.getErrorRule());
|
||||
}
|
||||
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.spring.common.validation;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import javax.validation.Constraint;
|
||||
import javax.validation.Payload;
|
||||
|
||||
@Constraint(validatedBy = {EnumValidator.class})
|
||||
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface EnumValid {
|
||||
String message() default "Invalid Enum Value.";
|
||||
Class<?>[] groups() default {};
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
Class<? extends java.lang.Enum<?>> target();
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.spring.common.validation;
|
||||
|
||||
import javax.validation.ConstraintValidator;
|
||||
import javax.validation.ConstraintValidatorContext;
|
||||
|
||||
public class EnumValidator implements ConstraintValidator<EnumValid, Enum<?>> {
|
||||
private EnumValid annotation;
|
||||
|
||||
@Override
|
||||
public void initialize(EnumValid constraintAnnotation) {
|
||||
this.annotation = constraintAnnotation;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid(Enum<?> value, ConstraintValidatorContext context) {
|
||||
if (value == null) return false;
|
||||
Object[] enumValues = this.annotation.target().getEnumConstants();
|
||||
if (enumValues != null) {
|
||||
for (Object enumValue : enumValues) {
|
||||
if (value.toString().equals(enumValue.toString())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,16 +1,11 @@
|
||||
package com.spring.domain.schedule.error;
|
||||
|
||||
import com.spring.common.error.BizBaseException;
|
||||
import com.spring.common.error.ExceptionRule;
|
||||
|
||||
public class ScheduleNotFoundException extends BizBaseException {
|
||||
|
||||
public ScheduleNotFoundException() {
|
||||
super(ExceptionRule.SCHEDULE_NOT_FOUND);
|
||||
}
|
||||
|
||||
public ScheduleNotFoundException(ExceptionRule exceptionRule) {
|
||||
super(exceptionRule);
|
||||
super(ScheduleRule.SCHEDULE_NOT_FOUND);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.spring.domain.schedule.error;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
import com.spring.common.error.ErrorRule;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum ScheduleRule implements ErrorRule {
|
||||
|
||||
SCHEDULE_NOT_FOUND(HttpStatus.NOT_FOUND, "스케쥴 정보를 찾을 수 없습니다.");
|
||||
|
||||
private final HttpStatus status;
|
||||
private String message;
|
||||
|
||||
ScheduleRule(HttpStatus status, String message) {
|
||||
this.status = status;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public ScheduleRule message(final String message) {
|
||||
this.message = message;
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,6 +2,8 @@ package com.spring.domain.user.api;
|
||||
|
||||
import javax.validation.Valid;
|
||||
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
@@ -19,6 +21,11 @@ public class SignApi {
|
||||
|
||||
private final SignUpService signUpService;
|
||||
|
||||
@GetMapping("/conflict/{userId}")
|
||||
public boolean isConflictUserId(@PathVariable String userId) {
|
||||
return signUpService.isConflictUserId(userId);
|
||||
}
|
||||
|
||||
@PostMapping("/sign-up")
|
||||
public void signUp(@RequestBody @Valid SignUpRequest request) {
|
||||
signUpService.signUp(request);
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
package com.spring.domain.user.dto;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public class SignInRequest {
|
||||
|
||||
private final String username;
|
||||
private final String password;
|
||||
|
||||
}
|
||||
@@ -2,7 +2,9 @@ package com.spring.domain.user.dto;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
|
||||
import com.spring.domain.user.entity.AppUser;
|
||||
import com.spring.common.validation.EnumValid;
|
||||
import com.spring.domain.user.entity.AgentUser;
|
||||
import com.spring.domain.user.entity.AgentUserRole;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
@@ -12,21 +14,28 @@ import lombok.NoArgsConstructor;
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class SignUpRequest {
|
||||
|
||||
@NotBlank(message = "로그인ID는 필수값 입니다.")
|
||||
private String loginId;
|
||||
@NotBlank(message = "사용자ID는 필수값 입니다.")
|
||||
private String userId;
|
||||
|
||||
@NotBlank(message = "비밀번호는 필수값 입니다.")
|
||||
private String password;
|
||||
private String userPassword;
|
||||
|
||||
@NotBlank(message = "사용자명은 필수값 입니다.")
|
||||
private String userName;
|
||||
|
||||
@EnumValid(target = AgentUserRole.class, message = "올바른 값을 입력해주세요.")
|
||||
private AgentUserRole userRole;
|
||||
|
||||
public void encodePassword(String password) {
|
||||
this.password = password;
|
||||
this.userPassword = password;
|
||||
}
|
||||
|
||||
public AppUser toEntity() {
|
||||
return AppUser.builder()
|
||||
.loginId(loginId)
|
||||
.password(password)
|
||||
public AgentUser toEntity() {
|
||||
return AgentUser.builder()
|
||||
.userId(userId)
|
||||
.userPassword(userPassword)
|
||||
.userName(userName)
|
||||
.userRole(userRole)
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.spring.domain.user.entity;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.persistence.CascadeType;
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.EnumType;
|
||||
import javax.persistence.Enumerated;
|
||||
import javax.persistence.FetchType;
|
||||
import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.OneToOne;
|
||||
import javax.persistence.Table;
|
||||
|
||||
import org.hibernate.annotations.GenericGenerator;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Entity
|
||||
@Table(name = "AGENT_USER")
|
||||
@Getter
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class AgentUser {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(generator = "uuid2")
|
||||
@GenericGenerator(name="uuid2", strategy = "uuid2")
|
||||
@Column(name = "ID", nullable = false, columnDefinition = "BINARY(16)")
|
||||
private UUID id;
|
||||
|
||||
// @OneToOne(mappedBy = "agentUser", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
// private AgentUserToken agentUserToken;
|
||||
|
||||
@Column(name = "USER_ID", nullable = false, length = 50)
|
||||
private String userId;
|
||||
|
||||
@Column(name = "USER_PASSWORD", nullable = false, length = 128)
|
||||
private String userPassword;
|
||||
|
||||
@Column(name = "USER_NAME", nullable = false, length = 50)
|
||||
private String userName;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "USER_ROLE", nullable = false, length = 50)
|
||||
private AgentUserRole userRole;
|
||||
|
||||
@Builder
|
||||
public AgentUser(String userId, String userPassword, String userName, AgentUserRole userRole) {
|
||||
this.userId = userId;
|
||||
this.userPassword = userPassword;
|
||||
this.userName = userName;
|
||||
this.userRole = userRole;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.spring.domain.user.entity;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum AgentUserRole {
|
||||
|
||||
ROLE_USER("ROLE_USER", "사용자"),
|
||||
ROLE_ADMIN("ROLE_ADMIN", "관리자"),
|
||||
ROLE_ANONYMOUS("ROLE_ANONYMOUS", "익명사용자");
|
||||
|
||||
@JsonValue
|
||||
private final String role;
|
||||
private final String roleName;
|
||||
|
||||
@JsonCreator(mode = JsonCreator.Mode.DELEGATING)
|
||||
public static AgentUserRole fromRole(String role) {
|
||||
return Arrays.stream(values())
|
||||
.filter(value -> value.getRole().equalsIgnoreCase(role))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import java.util.UUID;
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.FetchType;
|
||||
import javax.persistence.ForeignKey;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.JoinColumn;
|
||||
import javax.persistence.MapsId;
|
||||
@@ -12,23 +13,24 @@ import javax.persistence.OneToOne;
|
||||
import javax.persistence.Table;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Entity
|
||||
@Table(name = "MEMBER_REFRESH_TOKEN")
|
||||
@Table(name = "AGENT_USER_TOKEN")
|
||||
@Getter
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class MemberRefreshToken {
|
||||
public class AgentUserToken {
|
||||
|
||||
@Id
|
||||
@Column(name = "MEMBER_ID", nullable = false)
|
||||
private UUID memberId;
|
||||
|
||||
@Column(name = "ID", nullable = false)
|
||||
private UUID id;
|
||||
|
||||
@OneToOne(fetch = FetchType.LAZY)
|
||||
@MapsId
|
||||
@JoinColumn(name = "member_id")
|
||||
private Member member;
|
||||
@JoinColumn(name = "ID", referencedColumnName = "ID", columnDefinition = "BINARY(16)", foreignKey = @ForeignKey(name = "FK_AGENT_USER_TOKEN"))
|
||||
private AgentUser agentUser;
|
||||
|
||||
@Column(name = "REFRESH_TOKEN", nullable = false)
|
||||
private String refreshToken;
|
||||
@@ -36,6 +38,13 @@ public class MemberRefreshToken {
|
||||
@Column(name = "REISSUE_COUNT", nullable = false)
|
||||
private int reissueCount;
|
||||
|
||||
@Builder
|
||||
public AgentUserToken(AgentUser agentUser, String refreshToken, int reissueCount) {
|
||||
this.agentUser = agentUser;
|
||||
this.refreshToken = refreshToken;
|
||||
this.reissueCount = reissueCount;
|
||||
}
|
||||
|
||||
public void updateRefreshToken(String refreshToken) {
|
||||
this.refreshToken = refreshToken;
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package com.spring.domain.user.entity;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.Table;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Entity
|
||||
@Table(name = "MEMBER")
|
||||
@Getter
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class Member {
|
||||
|
||||
@Id
|
||||
@Column(name = "MEMBER_ID", nullable = false)
|
||||
private UUID memberId;
|
||||
|
||||
@Column(name = "LOGIN_ID", nullable = false, length = 50)
|
||||
private String loginId;
|
||||
|
||||
@Column(name = "PASSWORD", nullable = false, length = 128)
|
||||
private String password;
|
||||
|
||||
@Column(name = "MEMBER_NAME", nullable = false, length = 50)
|
||||
private String userName;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.spring.domain.user.error;
|
||||
|
||||
import com.spring.common.error.BizBaseException;
|
||||
|
||||
public class UserIdConflictException extends BizBaseException {
|
||||
|
||||
public UserIdConflictException() {
|
||||
super(UserRule.USER_ID_CONFLICT);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,16 +1,11 @@
|
||||
package com.spring.domain.user.error;
|
||||
|
||||
import com.spring.common.error.BizBaseException;
|
||||
import com.spring.common.error.ExceptionRule;
|
||||
|
||||
public class UserNotFoundException extends BizBaseException {
|
||||
|
||||
public UserNotFoundException() {
|
||||
super(ExceptionRule.USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
public UserNotFoundException(ExceptionRule exceptionRule) {
|
||||
super(exceptionRule);
|
||||
super(UserRule.USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.spring.domain.user.error;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
import com.spring.common.error.ErrorRule;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum UserRule implements ErrorRule {
|
||||
|
||||
USER_NOT_FOUND(HttpStatus.NOT_FOUND, "사용자를 찾을 수 없습니다."),
|
||||
USER_ID_CONFLICT(HttpStatus.CONFLICT, "중복된 아이디 입니다.");
|
||||
|
||||
private final HttpStatus status;
|
||||
private String message;
|
||||
|
||||
UserRule(HttpStatus status, String message) {
|
||||
this.status = status;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public UserRule message(final String message) {
|
||||
this.message = message;
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.spring.domain.user.repository;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import com.spring.domain.user.entity.AgentUser;
|
||||
|
||||
public interface AgentUserRepository extends JpaRepository<AgentUser, UUID> {
|
||||
|
||||
Optional<AgentUser> findByUserId(String userId);
|
||||
|
||||
boolean existsByUserId(String userId);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.spring.domain.user.repository;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import com.spring.domain.user.entity.AgentUserToken;
|
||||
|
||||
public interface AgentUserTokenRepository extends JpaRepository<AgentUserToken, UUID> {
|
||||
|
||||
Optional<AgentUserToken> findByIdAndReissueCountLessThan(UUID id, long count);
|
||||
|
||||
}
|
||||
@@ -5,7 +5,8 @@ import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.spring.domain.user.dto.SignUpRequest;
|
||||
import com.spring.domain.user.repository.AppUserRepository;
|
||||
import com.spring.domain.user.error.UserIdConflictException;
|
||||
import com.spring.domain.user.repository.AgentUserRepository;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@@ -13,13 +14,20 @@ import lombok.RequiredArgsConstructor;
|
||||
@RequiredArgsConstructor
|
||||
public class SignUpService {
|
||||
|
||||
private final AppUserRepository appUserRepository;
|
||||
private final AgentUserRepository agentUserRepository;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public boolean isConflictUserId(String userId) {
|
||||
boolean result = agentUserRepository.existsByUserId(userId);
|
||||
if (result) throw new UserIdConflictException();
|
||||
return result;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void signUp(SignUpRequest request) {
|
||||
request.encodePassword(passwordEncoder.encode(request.getPassword()));
|
||||
appUserRepository.save(request.toEntity());
|
||||
request.encodePassword(passwordEncoder.encode(request.getUserPassword()));
|
||||
agentUserRepository.save(request.toEntity());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ public class QuartzConfig {
|
||||
factory.setDataSource(dataSource);
|
||||
factory.setTransactionManager(transactionManager);
|
||||
factory.setJobFactory(jobFactory);
|
||||
factory.setAutoStartup(true);
|
||||
factory.setAutoStartup(false);
|
||||
factory.setWaitForJobsToCompleteOnShutdown(true);
|
||||
return factory;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.spring.infra.security.config;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum PermittedURI {
|
||||
|
||||
ROOT_URI("/"),
|
||||
H2_CONSOLE_URI("/h2-console/**"),
|
||||
FAVICON_URI("/favicon.ico"),
|
||||
USER_CONFLICT_URI("/api/user/conflict/{userId}"),
|
||||
USER_SIGN_UP("/api/user/sign-up"),
|
||||
USER_SIGN_IN("/sign-in"),
|
||||
USER_SIGN_OUT("/sign-out");
|
||||
|
||||
private final String uri;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.spring.infra.security.config;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
@@ -47,13 +47,6 @@ import com.spring.infra.security.provider.UserAuthenticationProvider;
|
||||
@EnableWebSecurity
|
||||
@EnableMethodSecurity(securedEnabled = true, prePostEnabled = true)
|
||||
public class SecurityConfig {
|
||||
|
||||
private static final String[] PERMITTED_URI = {
|
||||
"/",
|
||||
"/h2-console/**",
|
||||
"/favicon.ico",
|
||||
"/api/user/sign-up"
|
||||
};
|
||||
|
||||
/**
|
||||
* Spring Security의 필터 체인을 구성합니다.
|
||||
@@ -79,13 +72,13 @@ public class SecurityConfig {
|
||||
.httpBasic(HttpBasicConfigurer::disable)
|
||||
.formLogin(FormLoginConfigurer::disable)
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.antMatchers(PERMITTED_URI).permitAll()
|
||||
.antMatchers(Arrays.stream(PermittedURI.values()).map(PermittedURI::getUri).toArray(String[]::new)).permitAll()
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.logout(logout -> logout
|
||||
.logoutUrl("/sign-out")
|
||||
.logoutUrl(PermittedURI.USER_SIGN_OUT.getUri())
|
||||
.addLogoutHandler(new SignOutHandler(tokenService))
|
||||
.logoutSuccessUrl("/")
|
||||
.logoutSuccessUrl(PermittedURI.ROOT_URI.getUri())
|
||||
.invalidateHttpSession(true))
|
||||
.sessionManagement(session -> session
|
||||
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
||||
@@ -95,7 +88,7 @@ public class SecurityConfig {
|
||||
UsernamePasswordAuthenticationFilter.class
|
||||
)
|
||||
.addFilterAfter(
|
||||
new JwtAuthenticationFilter(tokenService, List.of(PERMITTED_URI)),
|
||||
new JwtAuthenticationFilter(tokenService),
|
||||
AuthenticationProcessingFilter.class
|
||||
)
|
||||
.addFilterAfter(
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.spring.infra.security.config;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum SecurityURI {
|
||||
|
||||
REDIRECT_URI("/dashboard");
|
||||
|
||||
private final String uri;
|
||||
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.spring.infra.security.domain;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -7,7 +8,8 @@ import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
|
||||
import com.spring.domain.user.entity.AppUser;
|
||||
import com.spring.domain.user.entity.AgentUser;
|
||||
import com.spring.domain.user.entity.AgentUserRole;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -23,16 +25,16 @@ import lombok.RequiredArgsConstructor;
|
||||
@RequiredArgsConstructor
|
||||
public final class UserPrincipal implements UserDetails {
|
||||
|
||||
private final transient AppUser appUser;
|
||||
private final transient AgentUser agentUser;
|
||||
|
||||
/**
|
||||
* AppUser 객체로부터 UserPrincipal 객체를 생성합니다.
|
||||
* AgentUser 객체로부터 UserPrincipal 객체를 생성합니다.
|
||||
*
|
||||
* @param appUser 변환할 AppUser 객체
|
||||
* @param agentUser 변환할 AgentUser 객체
|
||||
* @return 생성된 UserPrincipal 객체
|
||||
*/
|
||||
public static UserPrincipal valueOf(AppUser appUser) {
|
||||
return new UserPrincipal(appUser);
|
||||
public static UserPrincipal valueOf(AgentUser agentUser) {
|
||||
return new UserPrincipal(agentUser);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,10 +44,11 @@ public final class UserPrincipal implements UserDetails {
|
||||
*/
|
||||
@Override
|
||||
public Collection<? extends GrantedAuthority> getAuthorities() {
|
||||
return appUser.getAppUserRoleMap().stream()
|
||||
.map(role -> role.getAppUserRole().getRoleType())
|
||||
.map(SimpleGrantedAuthority::new)
|
||||
.collect(Collectors.toList());
|
||||
return Arrays.stream(AgentUserRole.values())
|
||||
.filter(role -> Arrays.asList(agentUser.getUserRole()).contains(role))
|
||||
.map(AgentUserRole::getRole)
|
||||
.map(SimpleGrantedAuthority::new)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -96,7 +99,7 @@ public final class UserPrincipal implements UserDetails {
|
||||
*/
|
||||
@Override
|
||||
public String getPassword() {
|
||||
return appUser.getPassword();
|
||||
return agentUser.getUserPassword();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -106,7 +109,7 @@ public final class UserPrincipal implements UserDetails {
|
||||
*/
|
||||
@Override
|
||||
public String getUsername() {
|
||||
return appUser.getLoginId();
|
||||
return agentUser.getUserName();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ public enum SecurityExceptionRule implements ErrorRule {
|
||||
UNSUPPORTED_MEDIA_ERROR(HttpStatus.UNSUPPORTED_MEDIA_TYPE, "지원되지 않는 유형 입니다."),
|
||||
USER_BAD_REQUEST(HttpStatus.BAD_REQUEST, "사용자 정보가 올바르지 않습니다."),
|
||||
USER_UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "사용자 인증에 실패 하였습니다."),
|
||||
USER_NOT_PASSWORD(HttpStatus.UNAUTHORIZED, "비밀번호가 틀립니다."),
|
||||
USER_FORBIDDEN(HttpStatus.FORBIDDEN, "사용자 권한이 없습니다."),
|
||||
JWT_TOKEN_ERROR(HttpStatus.UNAUTHORIZED, "토큰이 잘못되었습니다."),
|
||||
JWT_TOKEN_NOT_FOUND(HttpStatus.UNAUTHORIZED, "토큰 정보가 없습니다."),
|
||||
|
||||
@@ -17,13 +17,14 @@ import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.spring.infra.security.config.PermittedURI;
|
||||
import com.spring.infra.security.dto.SignInRequest;
|
||||
import com.spring.infra.security.error.SecurityAuthException;
|
||||
import com.spring.infra.security.error.SecurityExceptionRule;
|
||||
|
||||
public class AuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter {
|
||||
|
||||
private static final String DEFAULT_LOGIN_REQUEST_URL = "/sign-in";
|
||||
private static final String DEFAULT_LOGIN_REQUEST_URL = PermittedURI.USER_SIGN_IN.getUri();
|
||||
private static final String HTTP_METHOD = "POST";
|
||||
private static final AntPathRequestMatcher DEFAULT_LOGIN_PATH_REQUEST_MATCHER =
|
||||
new AntPathRequestMatcher(DEFAULT_LOGIN_REQUEST_URL, HTTP_METHOD);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.spring.infra.security.filter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
@@ -16,6 +16,7 @@ import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import com.spring.infra.security.config.PermittedURI;
|
||||
import com.spring.infra.security.jwt.JwtTokenRule;
|
||||
import com.spring.infra.security.jwt.JwtTokenService;
|
||||
|
||||
@@ -34,7 +35,6 @@ public final class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
private final AntPathMatcher pathMatcher = new AntPathMatcher();
|
||||
private final JwtTokenService jwtTokenService;
|
||||
private final List<String> permitAllUrls;
|
||||
private static final String EXCEPTION_ATTRIBUTE = "exception";
|
||||
|
||||
/**
|
||||
@@ -54,7 +54,9 @@ public final class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
) throws ServletException, IOException {
|
||||
|
||||
String requestURI = request.getRequestURI();
|
||||
if (permitAllUrls.stream().anyMatch(url -> pathMatcher.match(url, requestURI)) && !"/".equals(requestURI)) {
|
||||
if (Arrays.stream(PermittedURI.values()).map(PermittedURI::getUri).anyMatch(uri -> pathMatcher.match(uri, requestURI)) &&
|
||||
!PermittedURI.ROOT_URI.getUri().equals(requestURI))
|
||||
{
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.HandlerExceptionResolver;
|
||||
|
||||
import com.spring.infra.security.config.PermittedURI;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
@@ -40,7 +42,7 @@ public class SecurityAuthenticationEntryPoint implements AuthenticationEntryPoin
|
||||
} else if (isApiRequest(request)) {
|
||||
handleApiRequest(request, response, authException);
|
||||
} else {
|
||||
response.sendRedirect("/");
|
||||
response.sendRedirect(PermittedURI.ROOT_URI.getUri());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import org.springframework.security.web.savedrequest.SavedRequest;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.spring.infra.security.config.SecurityURI;
|
||||
import com.spring.infra.security.dto.SignResponse;
|
||||
import com.spring.infra.security.jwt.JwtTokenRule;
|
||||
import com.spring.infra.security.jwt.JwtTokenService;
|
||||
@@ -42,7 +43,7 @@ public class SigninSuccessHandler implements AuthenticationSuccessHandler {
|
||||
jwtTokenService.generateRefreshToken(response, authentication);
|
||||
|
||||
SavedRequest savedRequest = requestCache.getRequest(request, response);
|
||||
String targetUrl = (savedRequest != null) ? savedRequest.getRedirectUrl() : "/dashboard";
|
||||
String targetUrl = (savedRequest != null) ? savedRequest.getRedirectUrl() : SecurityURI.REDIRECT_URI.getUri();
|
||||
|
||||
response.setHeader(HttpHeaders.AUTHORIZATION, JwtTokenRule.BEARER_PREFIX.getValue() + accessToken);
|
||||
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||
|
||||
@@ -4,6 +4,7 @@ import java.security.Key;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.persistence.EntityNotFoundException;
|
||||
import javax.servlet.http.Cookie;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
@@ -17,6 +18,11 @@ import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.spring.domain.user.entity.AgentUser;
|
||||
import com.spring.domain.user.entity.AgentUserToken;
|
||||
import com.spring.domain.user.repository.AgentUserRepository;
|
||||
import com.spring.domain.user.repository.AgentUserTokenRepository;
|
||||
import com.spring.infra.security.domain.UserPrincipal;
|
||||
import com.spring.infra.security.error.SecurityAuthException;
|
||||
import com.spring.infra.security.error.SecurityExceptionRule;
|
||||
import com.spring.infra.security.service.UserPrincipalService;
|
||||
@@ -38,6 +44,8 @@ public class JwtTokenService {
|
||||
private final JwtTokenUtil jwtTokenUtil;
|
||||
private final JwtTokenGenerator jwtTokenGenerator;
|
||||
private final UserPrincipalService userPrincipalService;
|
||||
private final AgentUserRepository agentUserRepository;
|
||||
private final AgentUserTokenRepository agentUserTokenRepository;
|
||||
private final Key accessSecretKey;
|
||||
private final Key refreshSecretKey;
|
||||
private final long refreshExpiration;
|
||||
@@ -46,11 +54,15 @@ public class JwtTokenService {
|
||||
JwtTokenUtil jwtTokenUtil,
|
||||
JwtTokenGenerator jwtTokenGenerator,
|
||||
UserPrincipalService userPrincipalService,
|
||||
AgentUserRepository agentUserRepository,
|
||||
AgentUserTokenRepository agentUserTokenRepository,
|
||||
JwtProperties jwtProperties
|
||||
) {
|
||||
this.jwtTokenUtil = jwtTokenUtil;
|
||||
this.jwtTokenGenerator = jwtTokenGenerator;
|
||||
this.userPrincipalService = userPrincipalService;
|
||||
this.agentUserRepository = agentUserRepository;
|
||||
this.agentUserTokenRepository = agentUserTokenRepository;
|
||||
this.accessSecretKey = jwtTokenUtil.getSigningKey(jwtProperties.getAccessToken().getSecret());
|
||||
this.refreshSecretKey = jwtTokenUtil.getSigningKey(jwtProperties.getRefreshToken().getSecret());
|
||||
this.refreshExpiration = jwtProperties.getRefreshToken().getExpiration();
|
||||
@@ -76,8 +88,20 @@ public class JwtTokenService {
|
||||
* @param authentication 인증 정보
|
||||
* @return 생성된 리프레시 토큰
|
||||
*/
|
||||
@Transactional
|
||||
public String generateRefreshToken(HttpServletResponse response, Authentication authentication) {
|
||||
String refreshToken = jwtTokenGenerator.generateRefreshToken(authentication);
|
||||
|
||||
UserPrincipal user = (UserPrincipal) authentication.getPrincipal();
|
||||
AgentUser agentUser = agentUserRepository.findById(user.getAgentUser().getId()).orElseThrow(() -> new EntityNotFoundException("AgentUser not found"));
|
||||
agentUserTokenRepository.findById(agentUser.getId())
|
||||
.ifPresentOrElse(
|
||||
it -> it.updateRefreshToken(refreshToken),
|
||||
() -> agentUserTokenRepository.save(
|
||||
AgentUserToken.builder().agentUser(agentUser).refreshToken(refreshToken).build()
|
||||
)
|
||||
);
|
||||
|
||||
ResponseCookie cookie = setTokenToCookie(JwtTokenRule.REFRESH_PREFIX.getValue(), refreshToken, refreshExpiration);
|
||||
response.addHeader(JwtTokenRule.JWT_ISSUE_HEADER.getValue(), cookie.toString());
|
||||
return refreshToken;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.spring.infra.security.provider;
|
||||
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
@@ -10,6 +9,8 @@ import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.spring.infra.security.error.SecurityAuthException;
|
||||
import com.spring.infra.security.error.SecurityExceptionRule;
|
||||
import com.spring.infra.security.service.UserPrincipalService;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -24,11 +25,11 @@ public class UserAuthenticationProvider implements AuthenticationProvider {
|
||||
@Transactional(readOnly = true)
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
String loginId = authentication.getName();
|
||||
String username = authentication.getName();
|
||||
String password = String.valueOf(authentication.getCredentials());
|
||||
UserDetails user = userPrincipalService.loadUserByUsername(loginId);
|
||||
UserDetails user = userPrincipalService.loadUserByUsername(username);
|
||||
if (isNotMatches(password, user.getPassword())) {
|
||||
throw new BadCredentialsException(loginId);
|
||||
throw new SecurityAuthException(SecurityExceptionRule.USER_NOT_PASSWORD.getMessage());
|
||||
}
|
||||
return new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities());
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.spring.domain.user.entity.AppUser;
|
||||
import com.spring.domain.user.repository.AppUserRepository;
|
||||
import com.spring.domain.user.entity.AgentUser;
|
||||
import com.spring.domain.user.repository.AgentUserRepository;
|
||||
import com.spring.infra.security.domain.UserPrincipal;
|
||||
import com.spring.infra.security.error.SecurityExceptionRule;
|
||||
|
||||
@@ -17,12 +17,12 @@ import lombok.RequiredArgsConstructor;
|
||||
@RequiredArgsConstructor
|
||||
public class UserPrincipalService implements UserDetailsService {
|
||||
|
||||
private final AppUserRepository appUserRepository;
|
||||
private final AgentUserRepository agentUserRepository;
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||
AppUser user = appUserRepository.findByLoginId(username)
|
||||
AgentUser user = agentUserRepository.findByUserId(username)
|
||||
.orElseThrow(() -> new UsernameNotFoundException(SecurityExceptionRule.USER_UNAUTHORIZED.getMessage()));
|
||||
return UserPrincipal.valueOf(user);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
import com.spring.domain.user.entity.AgentUserRole;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/")
|
||||
public class SignController {
|
||||
@@ -20,6 +22,7 @@ public class SignController {
|
||||
public String signIn(Model model) {
|
||||
model.addAttribute("baseUrl", baseUrl);
|
||||
model.addAttribute("timeout", timeout);
|
||||
model.addAttribute("roles", AgentUserRole.values());
|
||||
return "pages/sign/sign-in";
|
||||
}
|
||||
|
||||
|
||||
@@ -14,9 +14,14 @@ const signService = {
|
||||
removeTokens();
|
||||
},
|
||||
|
||||
signUp: async (loginId, password, userName) => {
|
||||
const response = await apiClient.post('/api/user/sign-up', { loginId, password, userName });
|
||||
signUp: async (params) => {
|
||||
const response = await apiClient.post('/api/user/sign-up', params);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
isConflictUserId: async (userId) => {
|
||||
const response = await apiClient.get(`/api/user/conflict/${userId}`);
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import signService from '../../apis/sign-api.js';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
const signupModal = new bootstrap.Modal(document.getElementById('signupModal'));
|
||||
const signInButton = document.getElementById('signIn');
|
||||
const signupButton = document.getElementById('signUp');
|
||||
const signupModal = new bootstrap.Modal(document.getElementById('signupModal'));
|
||||
const signupSubmit = document.getElementById('signupSubmit');
|
||||
const signupForm = document.getElementById('signupForm');
|
||||
|
||||
signInButton.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
signInButton.addEventListener('click', () => {
|
||||
const username = document.getElementById('username').value;
|
||||
const password = document.getElementById('password').value;
|
||||
signService.signIn(username, password).then(response => {
|
||||
@@ -18,20 +16,25 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
});
|
||||
|
||||
signupButton.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
signupButton.addEventListener('click', () => {
|
||||
signupModal.show();
|
||||
});
|
||||
|
||||
signupSubmit.addEventListener('click', (e) => {
|
||||
signupForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const loginId = document.getElementById('loginId').value;
|
||||
const password = document.getElementById('loginPassword').value;
|
||||
const userName = document.getElementById('userName').value;
|
||||
signService.signUp(loginId, password, userName).then(() => {
|
||||
alert(`회원가입이 완료 되었습니다.`);
|
||||
signupModal.hide();
|
||||
});
|
||||
const userId = document.getElementById('userId').value;
|
||||
const isConflict = await signService.isConflictUserId(userId);
|
||||
if (!isConflict) {
|
||||
const params = Object.fromEntries(new FormData(signupForm));
|
||||
signService.signUp(params).then(() => {
|
||||
alert(`회원가입이 완료 되었습니다.`);
|
||||
signupModal.hide();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('signupModal').addEventListener('hidden.bs.modal', () => {
|
||||
signupForm.reset();
|
||||
});
|
||||
|
||||
});
|
||||
@@ -54,36 +54,44 @@
|
||||
<h5 class="modal-title" id="signupModalLabel">회원가입</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="signupForm">
|
||||
<form id="signupForm">
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="bi bi-person"></i></span>
|
||||
<input type="text" class="form-control" id="loginId" placeholder="아이디" required>
|
||||
<input type="text" class="form-control" id="userId" name="userId" placeholder="아이디" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="bi bi-key"></i></span>
|
||||
<input type="password" class="form-control" id="loginPassword" placeholder="비밀번호" required>
|
||||
<input type="password" class="form-control" id="userPassword" name="userPassword" placeholder="비밀번호" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="bi bi-envelope"></i></span>
|
||||
<input type="text" class="form-control" id="userName" placeholder="사용자명" required>
|
||||
<span class="input-group-text"><i class="bi bi-person-vcard"></i></span>
|
||||
<input type="text" class="form-control" id="userName" name="userName" placeholder="사용자명" required>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">
|
||||
<i class="bi bi-x-circle me-2"></i>취소
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" id="signupSubmit">
|
||||
<i class="bi bi-check-circle me-2"></i>가입하기
|
||||
</button>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="bi bi-shield-lock"></i></span>
|
||||
<select id="userRole" name="userRole" class="form-select" required>
|
||||
<option th:each="role : ${roles}" th:value="${role}" th:text="${role.roleName}"></option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">
|
||||
<i class="bi bi-x-circle me-2"></i>취소
|
||||
</button>
|
||||
<button type="submit" class="btn btn-outline-primary" id="signupSubmit">
|
||||
<i class="bi bi-check-circle me-2"></i>가입하기
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user