admin/front 분리

This commit is contained in:
2026-07-20 17:49:24 +09:00
parent 9269278e40
commit ed41ef24cb
673 changed files with 16782 additions and 11277 deletions
+13
View File
@@ -0,0 +1,13 @@
FROM amazoncorretto:21
RUN yum -y install curl && yum clean all
WORKDIR /api
COPY build/libs/api-front.jar api-front.jar
ENV JAVA_OPTS=""
EXPOSE 8112
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar /api/api-front.jar"]
+22
View File
@@ -0,0 +1,22 @@
plugins {
id 'org.springframework.boot'
}
dependencies {
implementation project(':01-api-core')
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.0'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
bootJar {
archiveFileName = 'api-front.jar'
}
@@ -0,0 +1,15 @@
package com.alist.api.front;
import com.alist.api.core.config.ApiCoreConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Import;
@Import(ApiCoreConfig.class)
@SpringBootApplication
public class ApiFrontApplication {
public static void main(String[] args) {
SpringApplication.run(ApiFrontApplication.class, args);
}
}
@@ -0,0 +1,37 @@
package com.alist.api.front.config;
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 org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class OpenApiConfig {
private static final String SECURITY_SCHEME_NAME = "bearerAuth";
@Value("${api.version}")
private String apiVersion;
@Bean
public OpenAPI openAPI() {
return new OpenAPI()
.info(new Info()
.title("A*List Front API")
.version(apiVersion)
.description("A*List Front API 입니다.!"))
.addSecurityItem(new SecurityRequirement().addList(SECURITY_SCHEME_NAME))
.components(new Components()
.addSecuritySchemes(SECURITY_SCHEME_NAME,
new SecurityScheme()
.name("Authorization")
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
)
);
}
}
@@ -0,0 +1,97 @@
package com.alist.api.front.config;
import com.alist.api.core.common.jwt.JwtAccessDeniedHandler;
import com.alist.api.core.common.jwt.JwtAuthenticationEntryPoint;
import com.alist.api.core.common.jwt.JwtAuthenticationFilter;
import com.alist.api.core.common.jwt.JwtTokenProvider;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Slf4j
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Value("${swagger.login.id}")
private String swaggerLoginId;
@Value("${swagger.login.password}")
private String swaggerLoginPassword;
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider) {
return new JwtAuthenticationFilter(
jwtTokenProvider,
"USER",
"accessToken"
);
}
@Bean
@Order(1)
public SecurityFilterChain swaggerFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html")
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.httpBasic(Customizer.withDefaults())
.csrf(csrf -> csrf.disable());
return http.build();
}
@Bean
@Order(2)
public SecurityFilterChain apiFilterChain(HttpSecurity http, JwtAuthenticationFilter jwtAuthenticationFilter) throws Exception {
http
.cors(Customizer.withDefaults())
.csrf(csrf -> csrf.disable())
.exceptionHandling(ex -> ex
.authenticationEntryPoint(new JwtAuthenticationEntryPoint())
.accessDeniedHandler(new JwtAccessDeniedHandler())
)
.authorizeHttpRequests(auth -> auth
.requestMatchers(
"/"
, "/actuator/health"
, "/sso/**"
, "/auth/**"
, "/user/add"
, "/user/migration/list"
, "/file/**"
).permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public UserDetailsService userDetailsService() {
UserDetails alist = User.builder()
.username(swaggerLoginId)
.password(passwordEncoder().encode(swaggerLoginPassword))
.roles("ADMIN")
.build();
return new InMemoryUserDetailsManager(alist);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
@@ -0,0 +1,87 @@
package com.alist.api.front.modules.auth;
import com.alist.api.core.common.response.ApiResponse;
import com.alist.api.core.common.response.ApiResponseCode;
import com.alist.api.front.modules.auth.form.AuthApiKeyLoginForm;
import com.alist.api.front.modules.auth.form.AuthTokenForm;
import com.alist.api.front.modules.auth.service.AuthService;
import com.alist.api.front.modules.auth.vo.AuthAccessVo;
import com.alist.api.front.modules.auth.vo.AuthApiKeyLoginVo;
import com.alist.api.front.modules.auth.vo.AuthRefreshVo;
import com.alist.api.front.modules.auth.vo.AuthTokenVo;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "A-2. 엑세스 토큰 발급", description = "SSO 기반 Access Token 발급 및 갱신 API")
@RestController
@RequestMapping("/auth")
public class AuthController {
private final AuthService authService;
public AuthController(AuthService authService) {
this.authService = authService;
}
@Operation(summary = "SSO 기반 토큰 발급", description = "유효한 SSO 세션을 확인한 뒤 Access Token과 Refresh Token 쿠키를 발급합니다.")
@PostMapping("/access")
public ResponseEntity<ApiResponse<AuthAccessVo>> authAccess(
HttpServletRequest request,
HttpServletResponse response
) {
AuthAccessVo authAccessVo = authService.selectAuthAccess(request, response);
if (authAccessVo.getResultCode() == 401) {
return ApiResponse.entity(authAccessVo, ApiResponseCode.CODE_401);
}
return ApiResponse.entity(authAccessVo, ApiResponseCode.CODE_2001, "Access Token");
}
@Operation(summary = "외부 API Key 로그인", description = "외부 연동용으로 Access Token과 Refresh Token을 JSON으로 반환합니다.")
@PostMapping("/apiKeyLogin")
public ResponseEntity<ApiResponse<AuthApiKeyLoginVo>> authApiKeyLogin(
@Valid @RequestBody AuthApiKeyLoginForm authApiKeyLoginForm
) {
AuthApiKeyLoginVo authApiKeyLoginVo = authService.selectAuthApiKeyLogin(authApiKeyLoginForm);
if (authApiKeyLoginVo.getResultCode() == 2003) {
return ApiResponse.entity(authApiKeyLoginVo, ApiResponseCode.CODE_2003);
}
return ApiResponse.entity(authApiKeyLoginVo, ApiResponseCode.CODE_2001, "Access Token");
}
@Operation(summary = "Access Token 재발급", description = "유효한 Refresh Token 쿠키를 검증하고 새 Access Token과 Refresh Token 쿠키를 발급합니다.")
@PostMapping("/refresh")
public ResponseEntity<ApiResponse<AuthRefreshVo>> authRefresh(
@CookieValue(name = "refreshToken", required = false) String refreshToken,
HttpServletResponse response
) {
AuthRefreshVo authRefreshVo = authService.selectAuthRefresh(refreshToken, response);
if (authRefreshVo.getResultCode() == 401) {
return ApiResponse.entity(authRefreshVo, ApiResponseCode.CODE_401);
}
return ApiResponse.entity(authRefreshVo, ApiResponseCode.CODE_200);
}
@Operation(summary = "테스트 Access Token 발급", description = "개발 테스트 전용 Access Token을 반환합니다. 실서비스 인증에는 사용하지 않습니다.")
@PostMapping("/token")
public ResponseEntity<ApiResponse<AuthTokenVo>> authToken(
@Valid @RequestBody AuthTokenForm authTokenForm
) {
AuthTokenVo authTokenVo = authService.selectAuthToken(authTokenForm);
return ApiResponse.entity(authTokenVo, ApiResponseCode.CODE_2001, "테스트 Access Token");
}
}
@@ -0,0 +1,15 @@
package com.alist.api.front.modules.auth.form;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.Setter;
@Schema(description = "외부 API Key 로그인 요청")
@Getter
@Setter
public class AuthApiKeyLoginForm {
@Schema(description = "발급된 사용자 API Key", example = "YjeWJr9-N-XaJ7ix2M2spnfjOyG9nTmRlY9t6sKw4_M", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "API Key는 필수입니다.")
private String userApiKey;
}
@@ -0,0 +1,15 @@
package com.alist.api.front.modules.auth.form;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.Setter;
@Schema(description = "테스트 Access Token 요청")
@Getter
@Setter
public class AuthTokenForm {
@Schema(description = "사용자명", example = "test", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "사용자명은 필수입니다.")
private String id;
}
@@ -0,0 +1,208 @@
package com.alist.api.front.modules.auth.service;
import com.alist.api.core.common.jwt.JwtTokenProvider;
import com.alist.api.core.common.utils.SessionUtil;
import com.alist.api.core.modules.bespoke.testUserToken.dto.TestUserTokenApiKeyLoginViewDto;
import com.alist.api.core.modules.bespoke.testUserToken.dto.TestUserTokenRefreshViewDto;
import com.alist.api.core.modules.bespoke.testUserToken.service.TestUserTokenAuthService;
import com.alist.api.core.modules.bespoke.testUserToken.vo.TestUserTokenApiKeyLoginViewVo;
import com.alist.api.core.modules.bespoke.testUserToken.vo.TestUserTokenRefreshViewVo;
import com.alist.api.core.modules.standard.testUser.dto.TestUserModifyDto;
import com.alist.api.core.modules.standard.testUser.service.TestUserService;
import com.alist.api.core.modules.standard.testUserToken.dto.TestUserTokenModifyDto;
import com.alist.api.core.modules.standard.testUserToken.service.TestUserTokenService;
import com.alist.api.front.modules.auth.form.AuthApiKeyLoginForm;
import com.alist.api.front.modules.auth.form.AuthTokenForm;
import com.alist.api.front.modules.auth.vo.AuthAccessVo;
import com.alist.api.front.modules.auth.vo.AuthApiKeyLoginVo;
import com.alist.api.front.modules.auth.vo.AuthRefreshVo;
import com.alist.api.front.modules.auth.vo.AuthTokenVo;
import com.alist.api.front.modules.sso.service.SsoService;
import com.alist.api.front.modules.sso.vo.SsoLoginCheckedVo;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
@Service
public class AuthService {
private static final List<String> FRONT_USER_TYPES = List.of("T", "S");
private static final String USER_SCOPE = "USER";
private static final String REFRESH_TOKEN_TYPE = "REFRESH";
private static final String ACCESS_TOKEN_COOKIE_NAME = "accessToken";
private static final String REFRESH_TOKEN_COOKIE_NAME = "refreshToken";
@Value("${cookie.secure}")
private boolean cookieSecure;
@Value("${cookie.domain:}")
private String cookieDomain;
@Value("${cookie.same-site:Lax}")
private String cookieSameSite;
@Value("${jwt.access-token-validity-seconds}")
private long accessTokenValiditySeconds;
@Value("${jwt.refresh-token-validity-seconds}")
private long refreshTokenValiditySeconds;
private final SsoService ssoService;
private final TestUserTokenAuthService testUserTokenAuthService;
private final TestUserTokenService testUserTokenService;
private final TestUserService testUserService;
private final JwtTokenProvider jwtTokenProvider;
public AuthService(
SsoService ssoService,
TestUserTokenAuthService testUserTokenAuthService,
TestUserTokenService testUserTokenService,
TestUserService testUserService,
JwtTokenProvider jwtTokenProvider
) {
this.ssoService = ssoService;
this.testUserTokenAuthService = testUserTokenAuthService;
this.testUserTokenService = testUserTokenService;
this.testUserService = testUserService;
this.jwtTokenProvider = jwtTokenProvider;
}
@Transactional
public AuthAccessVo selectAuthAccess(HttpServletRequest request, HttpServletResponse response) {
SsoLoginCheckedVo ssoLoginCheckedVo = ssoService.selectSsoLoginChecked(request, response);
if (!ssoLoginCheckedVo.isLoggedIn() || ssoLoginCheckedVo.getUserTokenIdx() == null) {
AuthAccessVo authAccessVo = new AuthAccessVo();
authAccessVo.setAccessToken(false);
authAccessVo.setResultCode(401);
return authAccessVo;
}
issueTokenCookie(ssoLoginCheckedVo.getUserTokenIdx(), ssoLoginCheckedVo.getUserRole(), response);
updateLastLoginAt(ssoLoginCheckedVo.getUserIdx());
AuthAccessVo authAccessVo = new AuthAccessVo();
authAccessVo.setAccessToken(true);
authAccessVo.setUserIdx(ssoLoginCheckedVo.getUserIdx());
authAccessVo.setUserId(ssoLoginCheckedVo.getUserId());
authAccessVo.setUserRole(ssoLoginCheckedVo.getUserRole());
authAccessVo.setUserType(ssoLoginCheckedVo.getUserType());
authAccessVo.setResultCode(2001);
return authAccessVo;
}
@Transactional
public AuthApiKeyLoginVo selectAuthApiKeyLogin(AuthApiKeyLoginForm authApiKeyLoginForm) {
TestUserTokenApiKeyLoginViewDto testUserTokenApiKeyLoginViewDto = new TestUserTokenApiKeyLoginViewDto();
testUserTokenApiKeyLoginViewDto.setUserApiKey(authApiKeyLoginForm.getUserApiKey().trim());
testUserTokenApiKeyLoginViewDto.setUserTypes(FRONT_USER_TYPES);
TestUserTokenApiKeyLoginViewVo testUserTokenApiKeyLoginViewVo = testUserTokenAuthService.testUserTokenApiKeyLoginView(testUserTokenApiKeyLoginViewDto);
if (testUserTokenApiKeyLoginViewVo == null) {
AuthApiKeyLoginVo authApiKeyLoginVo = new AuthApiKeyLoginVo();
authApiKeyLoginVo.setResultCode(2003);
return authApiKeyLoginVo;
}
String accessToken = jwtTokenProvider.createAccessToken(testUserTokenApiKeyLoginViewVo.getUserTokenIdx(), testUserTokenApiKeyLoginViewVo.getUserRole());
String refreshToken = jwtTokenProvider.createRefreshToken(testUserTokenApiKeyLoginViewVo.getUserTokenIdx());
updateRefreshToken(testUserTokenApiKeyLoginViewVo.getUserTokenIdx(), refreshToken);
updateLastLoginAt(testUserTokenApiKeyLoginViewVo.getUserIdx());
AuthApiKeyLoginVo authApiKeyLoginVo = new AuthApiKeyLoginVo();
authApiKeyLoginVo.setId(testUserTokenApiKeyLoginViewVo.getId());
authApiKeyLoginVo.setAccessToken(accessToken);
authApiKeyLoginVo.setRefreshToken(refreshToken);
authApiKeyLoginVo.setResultCode(2001);
return authApiKeyLoginVo;
}
@Transactional
public AuthRefreshVo selectAuthRefresh(String refreshToken, HttpServletResponse response) {
AuthRefreshVo authRefreshVo = new AuthRefreshVo();
if (refreshToken == null
|| !jwtTokenProvider.validateToken(refreshToken)
|| !USER_SCOPE.equals(jwtTokenProvider.getScope(refreshToken))
|| !REFRESH_TOKEN_TYPE.equals(jwtTokenProvider.getTokenType(refreshToken))) {
authRefreshVo.setRefreshed(false);
authRefreshVo.setResultCode(401);
return authRefreshVo;
}
Integer userTokenIdx;
try {
userTokenIdx = Integer.valueOf(jwtTokenProvider.getUserTokenIdx(refreshToken));
} catch (NumberFormatException e) {
authRefreshVo.setRefreshed(false);
authRefreshVo.setResultCode(401);
return authRefreshVo;
}
TestUserTokenRefreshViewDto testUserTokenRefreshViewDto = new TestUserTokenRefreshViewDto();
testUserTokenRefreshViewDto.setUserTokenIdx(userTokenIdx);
testUserTokenRefreshViewDto.setUserTypes(FRONT_USER_TYPES);
TestUserTokenRefreshViewVo testUserTokenRefreshViewVo = testUserTokenAuthService.testUserTokenRefreshView(testUserTokenRefreshViewDto);
if (testUserTokenRefreshViewVo == null || !refreshToken.equals(testUserTokenRefreshViewVo.getRefreshToken())) {
authRefreshVo.setRefreshed(false);
authRefreshVo.setResultCode(401);
return authRefreshVo;
}
issueTokenCookie(testUserTokenRefreshViewVo.getUserTokenIdx(), testUserTokenRefreshViewVo.getUserRole(), response);
authRefreshVo.setRefreshed(true);
authRefreshVo.setResultCode(200);
return authRefreshVo;
}
@Transactional(readOnly = true)
public AuthTokenVo selectAuthToken(AuthTokenForm authTokenForm) {
AuthTokenVo authTokenVo = new AuthTokenVo();
authTokenVo.setAccessToken(jwtTokenProvider.createToken(authTokenForm.getId().trim()));
return authTokenVo;
}
private void issueTokenCookie(Integer userTokenIdx, String userRole, HttpServletResponse response) {
String accessToken = jwtTokenProvider.createAccessToken(userTokenIdx, userRole);
String refreshToken = jwtTokenProvider.createRefreshToken(userTokenIdx);
updateRefreshToken(userTokenIdx, refreshToken);
SessionUtil.addTokenCookie(response, ACCESS_TOKEN_COOKIE_NAME, accessToken, cookieDomain, cookieSecure, cookieSameSite, accessTokenValiditySeconds);
SessionUtil.addTokenCookie(response, REFRESH_TOKEN_COOKIE_NAME, refreshToken, cookieDomain, cookieSecure, cookieSameSite, refreshTokenValiditySeconds);
}
private void updateRefreshToken(Integer userTokenIdx, String refreshToken) {
TestUserTokenModifyDto testUserTokenModifyDto = new TestUserTokenModifyDto();
testUserTokenModifyDto.setUserTokenIdx(userTokenIdx);
testUserTokenModifyDto.setRefreshToken(refreshToken);
testUserTokenModifyDto.setExpiresAt(LocalDateTime.now().plusSeconds(refreshTokenValiditySeconds));
testUserTokenService.updateTestUserTokenModify(testUserTokenModifyDto);
}
private void updateLastLoginAt(Integer userIdx) {
TestUserModifyDto testUserModifyDto = new TestUserModifyDto();
testUserModifyDto.setUserIdx(userIdx);
testUserModifyDto.setLastLoginAt(LocalDateTime.now());
testUserService.updateTestUserModify(testUserModifyDto);
}
}
@@ -0,0 +1,32 @@
package com.alist.api.front.modules.auth.vo;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
@Schema(description = "SSO 기반 토큰 발급 응답")
@Getter
@Setter
public class AuthAccessVo {
@JsonProperty("isAccessToken")
@Schema(description = "Access Token 발급 여부", example = "true")
private boolean accessToken;
@Schema(description = "회원 PK", example = "1")
private Integer userIdx;
@Schema(description = "회원 아이디", example = "teacher01")
private String userId;
@Schema(description = "회원 권한", example = "TEACHER")
private String userRole;
@Schema(description = "회원 유형", example = "T")
private String userType;
@JsonIgnore
@Schema(hidden = true)
private int resultCode;
}
@@ -0,0 +1,24 @@
package com.alist.api.front.modules.auth.vo;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
@Schema(description = "외부 API Key 로그인 응답")
@Getter
@Setter
public class AuthApiKeyLoginVo {
@Schema(description = "회원 아이디", example = "teacher01")
private String id;
@Schema(description = "외부 API용 Access Token")
private String accessToken;
@Schema(description = "외부 API용 Refresh Token")
private String refreshToken;
@JsonIgnore
@Schema(hidden = true)
private int resultCode;
}
@@ -0,0 +1,18 @@
package com.alist.api.front.modules.auth.vo;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
@Schema(description = "Access Token 재발급 응답")
@Getter
@Setter
public class AuthRefreshVo {
@Schema(description = "토큰 재발급 여부", example = "true")
private boolean refreshed;
@JsonIgnore
@Schema(hidden = true)
private int resultCode;
}
@@ -0,0 +1,13 @@
package com.alist.api.front.modules.auth.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
@Schema(description = "테스트 Access Token 응답")
@Getter
@Setter
public class AuthTokenVo {
@Schema(description = "테스트용 Access Token")
private String accessToken;
}
@@ -0,0 +1,78 @@
package com.alist.api.front.modules.file;
import com.alist.api.core.common.response.ApiResponse;
import com.alist.api.core.common.response.ApiResponseCode;
import com.alist.api.front.modules.file.form.SunEditorUploadForm;
import com.alist.api.front.modules.file.service.FileService;
import com.alist.api.front.modules.file.vo.FileUploadVo;
import com.alist.api.front.modules.file.vo.SunEditorUploadVo;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.core.io.Resource;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
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.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
@Tag(name = "D-1. 파일 업로드", description = "공통 파일 저장, 보기, 다운로드 API")
@RestController
@RequestMapping("/file")
public class FileController {
private final FileService fileService;
public FileController(FileService fileService) {
this.fileService = fileService;
}
@Operation(summary = "단순 파일 업로드", description = "DB 기록 없이 파일을 저장하고 업로드 경로를 반환합니다.")
@PostMapping(
value = "/upload"
, consumes = MediaType.MULTIPART_FORM_DATA_VALUE
)
public ResponseEntity<ApiResponse<FileUploadVo>> fileUpload(
@Parameter(description = "저장 폴더 키", example = "notice", required = true) @RequestParam String folder
, @Parameter(description = "업로드 파일", required = true) @RequestPart("file") MultipartFile multipartFile
) {
FileUploadVo fileUploadVo = fileService.insertFileUpload(folder, multipartFile);
return ApiResponse.entity(fileUploadVo, ApiResponseCode.CODE_200, "파일 업로드");
}
@Operation(
summary = "SunEditor 이미지 업로드"
, description = "여러 이미지를 저장하고 SunEditor result 배열 형식으로 공개 파일 URL을 반환합니다."
, requestBody = @RequestBody(content = @Content(mediaType = MediaType.MULTIPART_FORM_DATA_VALUE, schema = @Schema(implementation = SunEditorUploadForm.class)))
)
@PostMapping(
value = "/suneditor/upload"
, consumes = MediaType.MULTIPART_FORM_DATA_VALUE
)
public ResponseEntity<SunEditorUploadVo> fileSunEditorUpload(
@Parameter(description = "저장 폴더 키", example = "editor") @RequestParam(value = "folder", defaultValue = "editor") String folder
, MultipartHttpServletRequest multipartHttpServletRequest
) {
return ResponseEntity.ok(fileService.insertFileSunEditorUpload(folder, multipartHttpServletRequest));
}
@Operation(summary = "파일 보기", description = "uploadPath 기준으로 파일을 inline 응답합니다.")
@GetMapping("/view")
public ResponseEntity<Resource> fileView(@Parameter(description = "업로드 파일 경로", example = "/uploads/notice/2026/07/20/a1b2c3d4.png", required = true) @RequestParam String path) {
return fileService.selectFileView(path);
}
@Operation(summary = "파일 다운로드", description = "uploadPath 기준으로 파일을 attachment 응답합니다.")
@GetMapping("/download")
public ResponseEntity<Resource> fileDownload(@Parameter(description = "업로드 파일 경로", example = "/uploads/notice/2026/07/20/a1b2c3d4.png", required = true) @RequestParam String path) {
return fileService.selectFileDownload(path);
}
}
@@ -0,0 +1,17 @@
package com.alist.api.front.modules.file.form;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
@Schema(description = "SunEditor 이미지 업로드 요청")
@Getter
@Setter
public class SunEditorUploadForm {
@ArraySchema(schema = @Schema(type = "string", format = "binary", description = "업로드 이미지 파일"))
private List<MultipartFile> files;
}
@@ -0,0 +1,58 @@
package com.alist.api.front.modules.file.service;
import com.alist.api.core.common.file.service.FileStorageService;
import com.alist.api.core.common.file.vo.FileStorageUploadVo;
import com.alist.api.front.modules.file.vo.FileUploadVo;
import com.alist.api.front.modules.file.vo.SunEditorUploadVo;
import org.springframework.core.io.Resource;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import java.util.ArrayList;
import java.util.List;
@Service
public class FileService {
private final FileStorageService fileStorageService;
public FileService(FileStorageService fileStorageService) {
this.fileStorageService = fileStorageService;
}
public FileUploadVo insertFileUpload(String folder, MultipartFile multipartFile) {
FileStorageUploadVo fileStorageUploadVo = fileStorageService.saveFileUpload(folder, multipartFile);
FileUploadVo fileUploadVo = new FileUploadVo();
fileUploadVo.setUploadPath(fileStorageUploadVo.getUploadPath());
fileUploadVo.setOriginalFileName(fileStorageUploadVo.getOriginalFileName());
fileUploadVo.setStoredFileName(fileStorageUploadVo.getStoredFileName());
fileUploadVo.setFileExtension(fileStorageUploadVo.getFileExtension());
fileUploadVo.setContentType(fileStorageUploadVo.getContentType());
fileUploadVo.setFileSize(fileStorageUploadVo.getFileSize());
fileUploadVo.setWidth(fileStorageUploadVo.getWidth());
fileUploadVo.setHeight(fileStorageUploadVo.getHeight());
return fileUploadVo;
}
public SunEditorUploadVo insertFileSunEditorUpload(String folder, MultipartHttpServletRequest multipartHttpServletRequest) {
List<SunEditorUploadVo.Item> sunEditorUploadItemList = new ArrayList<>();
for (MultipartFile multipartFile : multipartHttpServletRequest.getFileMap().values()) {
FileStorageUploadVo fileStorageUploadVo = fileStorageService.saveFileUpload(folder, multipartFile);
String fileViewUrl = fileStorageService.buildFileViewUrl(fileStorageUploadVo.getUploadPath());
sunEditorUploadItemList.add(new SunEditorUploadVo.Item(fileViewUrl, fileStorageUploadVo.getOriginalFileName(), fileStorageUploadVo.getFileSize()));
}
return new SunEditorUploadVo(sunEditorUploadItemList);
}
public ResponseEntity<Resource> selectFileView(String path) {
return fileStorageService.loadFileResource(path, true);
}
public ResponseEntity<Resource> selectFileDownload(String path) {
return fileStorageService.loadFileResource(path, false);
}
}
@@ -0,0 +1,34 @@
package com.alist.api.front.modules.file.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
@Schema(description = "파일 업로드 응답")
@Getter
@Setter
public class FileUploadVo {
@Schema(description = "업로드 파일 접근 경로", example = "/uploads/notice/2026/07/20/a1b2c3d4.png")
private String uploadPath;
@Schema(description = "원본 파일명", example = "sample.png")
private String originalFileName;
@Schema(description = "저장 파일명", example = "a1b2c3d4.png")
private String storedFileName;
@Schema(description = "파일 확장자", example = "png")
private String fileExtension;
@Schema(description = "Content-Type", example = "image/png")
private String contentType;
@Schema(description = "파일 크기(byte)", example = "102400")
private Long fileSize;
@Schema(description = "이미지 너비(px)", example = "800")
private Integer width;
@Schema(description = "이미지 높이(px)", example = "600")
private Integer height;
}
@@ -0,0 +1,39 @@
package com.alist.api.front.modules.file.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
@Schema(description = "SunEditor 이미지 업로드 응답")
@Getter
@Setter
public class SunEditorUploadVo {
@Schema(description = "업로드 결과 목록")
private List<Item> result;
public SunEditorUploadVo(List<Item> result) {
this.result = result;
}
@Schema(description = "SunEditor 업로드 파일 항목")
@Getter
@Setter
public static class Item {
@Schema(description = "업로드 파일 URL", example = "https://file-alist.pjt.kr/uploads/editor/2026/07/20/a1b2c3d4.png")
private String url;
@Schema(description = "원본 파일명", example = "sample.png")
private String name;
@Schema(description = "파일 크기(byte)", example = "102400")
private Long size;
public Item(String url, String name, Long size) {
this.url = url;
this.name = name;
this.size = size;
}
}
}
@@ -0,0 +1,15 @@
package com.alist.api.front.modules.main;
import io.swagger.v3.oas.annotations.Hidden;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Hidden
@Controller
public class MainController {
@GetMapping(value = "/")
public String index() {
return "redirect:/swagger-ui/index.html";
}
}
@@ -0,0 +1,106 @@
package com.alist.api.front.modules.sso;
import com.alist.api.core.common.response.ApiResponse;
import com.alist.api.core.common.response.ApiResponseCode;
import com.alist.api.core.common.utils.SessionUtil;
import com.alist.api.front.modules.sso.form.SsoExchangeForm;
import com.alist.api.front.modules.sso.form.SsoLoginForm;
import com.alist.api.front.modules.sso.service.SsoService;
import com.alist.api.front.modules.sso.vo.SsoExchangeVo;
import com.alist.api.front.modules.sso.vo.SsoLoginCheckedVo;
import com.alist.api.front.modules.sso.vo.SsoLoginVo;
import com.alist.api.front.modules.sso.vo.SsoLogoutVo;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.net.URI;
@Tag(name = "A-1. SSO 인증", description = "공통 로그인, 인가 코드 발급, 코드 교환, 로그아웃 API")
@RestController
@RequestMapping("/sso")
public class SsoController {
@Value("${sso.cookie.name}")
private String ssoCookieName;
private final SsoService ssoService;
public SsoController(SsoService ssoService) {
this.ssoService = ssoService;
}
@Operation(summary = "SSO 로그인", description = "아이디와 비밀번호를 검증하고 공통 SSO HttpOnly 쿠키를 발급합니다.")
@PostMapping("/login")
public ResponseEntity<ApiResponse<SsoLoginVo>> ssoLogin(
@Valid @RequestBody SsoLoginForm ssoLoginForm,
HttpServletResponse response
) {
SsoLoginVo ssoLoginVo = ssoService.selectSsoLogin(ssoLoginForm, response);
if (ssoLoginVo.getResultCode() == 2003) {
return ApiResponse.entity(ssoLoginVo, ApiResponseCode.CODE_2003);
}
return ApiResponse.entity(ssoLoginVo, ApiResponseCode.CODE_2001, "SSO 로그인");
}
@Operation(summary = "SSO 로그인 상태 확인", description = "SSO 세션과 Access/Refresh Token 쿠키 존재 여부를 조회합니다.")
@GetMapping("/loginChecked")
public ResponseEntity<ApiResponse<SsoLoginCheckedVo>> ssoLoginChecked(
HttpServletRequest request,
HttpServletResponse response
) {
SsoLoginCheckedVo ssoLoginCheckedVo = ssoService.selectSsoLoginChecked(request, response);
return ApiResponse.entity(ssoLoginCheckedVo, ApiResponseCode.CODE_200);
}
@Operation(summary = "SSO 인가 코드 발급", description = "SSO 세션을 확인하고 등록된 서비스의 redirect URI로 1회용 인가 코드를 전달합니다.")
@GetMapping("/authorize")
public ResponseEntity<Void> ssoAuthorize(
@RequestParam String clientId,
@RequestParam String redirectUri,
@RequestParam(required = false) String state,
HttpServletRequest request
) {
String ssoSessionId = SessionUtil.resolveSsoCookieValue(request, ssoCookieName);
String redirectUrl = ssoService.selectSsoAuthorize(clientId, redirectUri, state, ssoSessionId);
return ResponseEntity.status(302).location(URI.create(redirectUrl)).build();
}
@Operation(summary = "SSO 인가 코드 교환", description = "1회용 인가 코드를 검증하고 서비스 세션 생성에 필요한 사용자 정보를 반환합니다.")
@PostMapping("/exchange")
public ResponseEntity<ApiResponse<SsoExchangeVo>> ssoExchange(
@Valid @RequestBody SsoExchangeForm ssoExchangeForm,
HttpServletResponse response
) {
SsoExchangeVo ssoExchangeVo = ssoService.selectSsoExchange(ssoExchangeForm, response);
return ApiResponse.entity(ssoExchangeVo, ApiResponseCode.CODE_2001, "SSO code 교환");
}
@Operation(summary = "SSO 로그아웃", description = "SSO 세션과 USER Refresh Token을 무효화하고 관련 쿠키를 삭제합니다.")
@PostMapping("/logout")
public ResponseEntity<ApiResponse<SsoLogoutVo>> ssoLogout(
HttpServletRequest request,
HttpServletResponse response,
@CookieValue(name = "refreshToken", required = false) String refreshToken
) {
String ssoSessionId = SessionUtil.resolveSsoCookieValue(request, ssoCookieName);
SsoLogoutVo ssoLogoutVo = ssoService.updateSsoLogout(ssoSessionId, refreshToken, response);
return ApiResponse.entity(ssoLogoutVo, ApiResponseCode.CODE_200);
}
}
@@ -0,0 +1,19 @@
package com.alist.api.front.modules.sso.form;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.Setter;
@Schema(description = "SSO 인가 코드 교환 요청")
@Getter
@Setter
public class SsoExchangeForm {
@Schema(description = "SSO 인가 코드", example = "CODE_123e4567-e89b-12d3-a456-426614174000", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "인가 코드는 필수입니다.")
private String code;
@Schema(description = "SSO 클라이언트 ID", example = "alist-main", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "클라이언트 ID는 필수입니다.")
private String clientId;
}
@@ -0,0 +1,19 @@
package com.alist.api.front.modules.sso.form;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.Setter;
@Schema(description = "SSO 로그인 요청")
@Getter
@Setter
public class SsoLoginForm {
@Schema(description = "회원 아이디", example = "teacher01", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "아이디는 필수입니다.")
private String id;
@Schema(description = "비밀번호", example = "password1234", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "비밀번호는 필수입니다.")
private String password;
}
@@ -0,0 +1,311 @@
package com.alist.api.front.modules.sso.service;
import com.alist.api.core.common.jwt.JwtTokenProvider;
import com.alist.api.core.common.utils.SessionUtil;
import com.alist.api.core.modules.bespoke.testUser.dto.TestUserSsoLoginViewDto;
import com.alist.api.core.modules.bespoke.testUser.service.TestUserSsoService;
import com.alist.api.core.modules.bespoke.testUser.vo.TestUserSsoLoginViewVo;
import com.alist.api.core.modules.standard.ssoClient.dto.SsoClientAuthorizeViewDto;
import com.alist.api.core.modules.standard.ssoClient.service.SsoClientService;
import com.alist.api.core.modules.standard.ssoClient.vo.SsoClientAuthorizeViewVo;
import com.alist.api.core.modules.standard.testUser.dto.TestUserModifyDto;
import com.alist.api.core.modules.standard.testUser.service.TestUserService;
import com.alist.api.core.modules.standard.testUserToken.dto.TestUserTokenModifyDto;
import com.alist.api.core.modules.standard.testUserToken.service.TestUserTokenService;
import com.alist.api.front.modules.sso.form.SsoExchangeForm;
import com.alist.api.front.modules.sso.form.SsoLoginForm;
import com.alist.api.front.modules.sso.vo.SsoExchangeVo;
import com.alist.api.front.modules.sso.vo.SsoLoginCheckedVo;
import com.alist.api.front.modules.sso.vo.SsoLoginVo;
import com.alist.api.front.modules.sso.vo.SsoLogoutVo;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.UUID;
@Service
public class SsoService {
private static final String SSO_SESSION_KEY_PREFIX = "alist:sso:session:";
private static final String SSO_USER_KEY_PREFIX = "alist:sso:userIdx:";
private static final String SSO_CODE_KEY_PREFIX = "alist:sso:code:";
private static final String USER_SCOPE = "USER";
private static final String REFRESH_TOKEN_TYPE = "REFRESH";
@Value("${cookie.secure}")
private boolean cookieSecure;
@Value("${cookie.domain:}")
private String cookieDomain;
@Value("${cookie.same-site:Lax}")
private String cookieSameSite;
@Value("${sso.session.ttl-seconds}")
private long ssoSessionTtlSeconds;
@Value("${sso.code.ttl-seconds}")
private long ssoCodeTtlSeconds;
@Value("${sso.cookie.name}")
private String ssoCookieName;
private final TestUserSsoService testUserSsoService;
private final SsoClientService ssoClientService;
private final TestUserService testUserService;
private final TestUserTokenService testUserTokenService;
private final StringRedisTemplate stringRedisTemplate;
private final PasswordEncoder passwordEncoder;
private final JwtTokenProvider jwtTokenProvider;
public SsoService(
TestUserSsoService testUserSsoService,
SsoClientService ssoClientService,
TestUserService testUserService,
TestUserTokenService testUserTokenService,
StringRedisTemplate stringRedisTemplate,
PasswordEncoder passwordEncoder,
JwtTokenProvider jwtTokenProvider
) {
this.testUserSsoService = testUserSsoService;
this.ssoClientService = ssoClientService;
this.testUserService = testUserService;
this.testUserTokenService = testUserTokenService;
this.stringRedisTemplate = stringRedisTemplate;
this.passwordEncoder = passwordEncoder;
this.jwtTokenProvider = jwtTokenProvider;
}
@Transactional
public SsoLoginVo selectSsoLogin(SsoLoginForm ssoLoginForm, HttpServletResponse response) {
TestUserSsoLoginViewDto testUserSsoLoginViewDto = new TestUserSsoLoginViewDto();
testUserSsoLoginViewDto.setId(ssoLoginForm.getId().trim());
TestUserSsoLoginViewVo testUserSsoLoginViewVo = testUserSsoService.testUserSsoLoginView(testUserSsoLoginViewDto);
if (testUserSsoLoginViewVo == null
|| !passwordEncoder.matches(ssoLoginForm.getPassword(), testUserSsoLoginViewVo.getPassword())) {
SsoLoginVo ssoLoginVo = new SsoLoginVo();
ssoLoginVo.setLogin(false);
ssoLoginVo.setResultCode(2003);
return ssoLoginVo;
}
String ssoSessionId = "SSO_" + UUID.randomUUID();
String ssoSessionKey = SSO_SESSION_KEY_PREFIX + ssoSessionId;
String ssoUserKey = SSO_USER_KEY_PREFIX + testUserSsoLoginViewVo.getUserIdx();
String previousSsoSessionId = stringRedisTemplate.opsForValue().get(ssoUserKey);
if (previousSsoSessionId != null && !previousSsoSessionId.isBlank()) {
stringRedisTemplate.delete(SSO_SESSION_KEY_PREFIX + previousSsoSessionId);
}
stringRedisTemplate.opsForHash().put(ssoSessionKey, "userIdx", String.valueOf(testUserSsoLoginViewVo.getUserIdx()));
stringRedisTemplate.opsForHash().put(ssoSessionKey, "userTokenIdx", String.valueOf(testUserSsoLoginViewVo.getUserTokenIdx()));
stringRedisTemplate.opsForHash().put(ssoSessionKey, "userId", testUserSsoLoginViewVo.getId());
stringRedisTemplate.opsForHash().put(ssoSessionKey, "userRole", testUserSsoLoginViewVo.getUserRole());
stringRedisTemplate.opsForHash().put(ssoSessionKey, "userType", testUserSsoLoginViewVo.getUserType());
stringRedisTemplate.expire(ssoSessionKey, java.time.Duration.ofSeconds(ssoSessionTtlSeconds));
stringRedisTemplate.opsForValue().set(ssoUserKey, ssoSessionId, java.time.Duration.ofSeconds(ssoSessionTtlSeconds));
TestUserModifyDto testUserModifyDto = new TestUserModifyDto();
testUserModifyDto.setUserIdx(testUserSsoLoginViewVo.getUserIdx());
testUserModifyDto.setLastLoginAt(LocalDateTime.now());
testUserService.updateTestUserModify(testUserModifyDto);
SessionUtil.addTokenCookie(response, ssoCookieName, ssoSessionId, cookieDomain, cookieSecure, cookieSameSite, ssoSessionTtlSeconds);
SsoLoginVo ssoLoginVo = new SsoLoginVo();
ssoLoginVo.setLogin(true);
ssoLoginVo.setId(testUserSsoLoginViewVo.getId());
ssoLoginVo.setUserIdx(testUserSsoLoginViewVo.getUserIdx());
ssoLoginVo.setUserRole(testUserSsoLoginViewVo.getUserRole());
ssoLoginVo.setUserType(testUserSsoLoginViewVo.getUserType());
ssoLoginVo.setResultCode(2001);
return ssoLoginVo;
}
@Transactional(readOnly = true)
public SsoLoginCheckedVo selectSsoLoginChecked(HttpServletRequest request, HttpServletResponse response) {
String ssoSessionId = SessionUtil.resolveSsoCookieValue(request, ssoCookieName);
String accessToken = SessionUtil.resolveSsoCookieValue(request, "accessToken");
String refreshToken = SessionUtil.resolveSsoCookieValue(request, "refreshToken");
SsoLoginCheckedVo ssoLoginCheckedVo = new SsoLoginCheckedVo();
ssoLoginCheckedVo.setAccessToken(accessToken != null && !accessToken.isBlank());
ssoLoginCheckedVo.setRefreshToken(refreshToken != null && !refreshToken.isBlank());
if (ssoSessionId == null || ssoSessionId.isBlank()) {
ssoLoginCheckedVo.setLoggedIn(false);
return ssoLoginCheckedVo;
}
String ssoSessionKey = SSO_SESSION_KEY_PREFIX + ssoSessionId;
String userIdx = (String) stringRedisTemplate.opsForHash().get(ssoSessionKey, "userIdx");
if (userIdx == null || userIdx.isBlank()) {
ssoLoginCheckedVo.setLoggedIn(false);
return ssoLoginCheckedVo;
}
String latestSsoSessionId = stringRedisTemplate.opsForValue().get(SSO_USER_KEY_PREFIX + userIdx);
if (!ssoSessionId.equals(latestSsoSessionId)) {
ssoLoginCheckedVo.setLoggedIn(false);
return ssoLoginCheckedVo;
}
refreshSsoSessionTtl(ssoSessionId, userIdx);
SessionUtil.addTokenCookie(response, ssoCookieName, ssoSessionId, cookieDomain, cookieSecure, cookieSameSite, ssoSessionTtlSeconds);
ssoLoginCheckedVo.setLoggedIn(true);
ssoLoginCheckedVo.setSsoSessionId(ssoSessionId);
ssoLoginCheckedVo.setUserIdx(Integer.valueOf(userIdx));
ssoLoginCheckedVo.setUserTokenIdx(Integer.valueOf((String) stringRedisTemplate.opsForHash().get(ssoSessionKey, "userTokenIdx")));
ssoLoginCheckedVo.setUserId((String) stringRedisTemplate.opsForHash().get(ssoSessionKey, "userId"));
ssoLoginCheckedVo.setUserRole((String) stringRedisTemplate.opsForHash().get(ssoSessionKey, "userRole"));
ssoLoginCheckedVo.setUserType((String) stringRedisTemplate.opsForHash().get(ssoSessionKey, "userType"));
return ssoLoginCheckedVo;
}
@Transactional(readOnly = true)
public String selectSsoAuthorize(String clientId, String redirectUri, String state, String ssoSessionId) {
if (ssoSessionId == null || ssoSessionId.isBlank()) {
return "/login";
}
String ssoSessionKey = SSO_SESSION_KEY_PREFIX + ssoSessionId;
String userIdx = (String) stringRedisTemplate.opsForHash().get(ssoSessionKey, "userIdx");
if (userIdx == null || userIdx.isBlank()) {
return "/login";
}
String latestSsoSessionId = stringRedisTemplate.opsForValue().get(SSO_USER_KEY_PREFIX + userIdx);
if (!ssoSessionId.equals(latestSsoSessionId)) {
return "/login";
}
SsoClientAuthorizeViewDto ssoClientAuthorizeViewDto = new SsoClientAuthorizeViewDto();
ssoClientAuthorizeViewDto.setClientId(clientId.trim());
ssoClientAuthorizeViewDto.setRedirectUri(redirectUri.trim());
SsoClientAuthorizeViewVo ssoClientAuthorizeViewVo = ssoClientService.selectSsoClientAuthorizeView(ssoClientAuthorizeViewDto);
if (ssoClientAuthorizeViewVo == null) {
throw new IllegalArgumentException("허용되지 않은 clientId 또는 redirectUri 입니다.");
}
refreshSsoSessionTtl(ssoSessionId, userIdx);
String code = "CODE_" + UUID.randomUUID();
String ssoCodeKey = SSO_CODE_KEY_PREFIX + code;
stringRedisTemplate.opsForHash().put(ssoCodeKey, "ssoSessionId", ssoSessionId);
stringRedisTemplate.opsForHash().put(ssoCodeKey, "clientId", ssoClientAuthorizeViewVo.getClientId());
stringRedisTemplate.opsForHash().put(ssoCodeKey, "redirectUri", ssoClientAuthorizeViewVo.getRedirectUri());
stringRedisTemplate.expire(ssoCodeKey, java.time.Duration.ofSeconds(ssoCodeTtlSeconds));
URI uri = URI.create(ssoClientAuthorizeViewVo.getRedirectUri());
StringBuilder redirectUrl = new StringBuilder(uri.getScheme() + "://" + uri.getAuthority() + uri.getPath());
String encodedCode = URLEncoder.encode(code, StandardCharsets.UTF_8);
if (uri.getQuery() == null || uri.getQuery().isBlank()) {
redirectUrl.append("?code=").append(encodedCode);
} else {
redirectUrl.append("?").append(uri.getQuery()).append("&code=").append(encodedCode);
}
if (state != null && !state.isBlank()) {
redirectUrl.append("&state=").append(URLEncoder.encode(state, StandardCharsets.UTF_8));
}
return redirectUrl.toString();
}
@Transactional(readOnly = true)
public SsoExchangeVo selectSsoExchange(SsoExchangeForm ssoExchangeForm, HttpServletResponse response) {
String ssoCodeKey = SSO_CODE_KEY_PREFIX + ssoExchangeForm.getCode().trim();
String ssoSessionId = (String) stringRedisTemplate.opsForHash().get(ssoCodeKey, "ssoSessionId");
String clientId = (String) stringRedisTemplate.opsForHash().get(ssoCodeKey, "clientId");
if (ssoSessionId == null || !ssoExchangeForm.getClientId().trim().equals(clientId)) {
throw new IllegalArgumentException("유효하지 않은 code 입니다.");
}
String ssoSessionKey = SSO_SESSION_KEY_PREFIX + ssoSessionId;
String userIdx = (String) stringRedisTemplate.opsForHash().get(ssoSessionKey, "userIdx");
String latestSsoSessionId = stringRedisTemplate.opsForValue().get(SSO_USER_KEY_PREFIX + userIdx);
if (!ssoSessionId.equals(latestSsoSessionId)) {
throw new IllegalArgumentException("다른 곳에서 다시 로그인되어 세션이 만료되었습니다.");
}
refreshSsoSessionTtl(ssoSessionId, userIdx);
stringRedisTemplate.delete(ssoCodeKey);
SessionUtil.addTokenCookie(response, ssoCookieName, ssoSessionId, cookieDomain, cookieSecure, cookieSameSite, ssoSessionTtlSeconds);
SsoExchangeVo ssoExchangeVo = new SsoExchangeVo();
ssoExchangeVo.setSsoSessionId(ssoSessionId);
ssoExchangeVo.setUserIdx(Integer.valueOf(userIdx));
ssoExchangeVo.setUserTokenIdx(Integer.valueOf((String) stringRedisTemplate.opsForHash().get(ssoSessionKey, "userTokenIdx")));
ssoExchangeVo.setUserId((String) stringRedisTemplate.opsForHash().get(ssoSessionKey, "userId"));
ssoExchangeVo.setUserRole((String) stringRedisTemplate.opsForHash().get(ssoSessionKey, "userRole"));
ssoExchangeVo.setUserType((String) stringRedisTemplate.opsForHash().get(ssoSessionKey, "userType"));
return ssoExchangeVo;
}
@Transactional
public SsoLogoutVo updateSsoLogout(String ssoSessionId, String refreshToken, HttpServletResponse response) {
if (ssoSessionId != null && !ssoSessionId.isBlank()) {
String ssoSessionKey = SSO_SESSION_KEY_PREFIX + ssoSessionId;
String userIdx = (String) stringRedisTemplate.opsForHash().get(ssoSessionKey, "userIdx");
stringRedisTemplate.delete(ssoSessionKey);
if (userIdx != null && !userIdx.isBlank()) {
stringRedisTemplate.delete(SSO_USER_KEY_PREFIX + userIdx);
}
}
if (refreshToken != null
&& jwtTokenProvider.validateToken(refreshToken)
&& USER_SCOPE.equals(jwtTokenProvider.getScope(refreshToken))
&& REFRESH_TOKEN_TYPE.equals(jwtTokenProvider.getTokenType(refreshToken))) {
try {
TestUserTokenModifyDto testUserTokenModifyDto = new TestUserTokenModifyDto();
testUserTokenModifyDto.setUserTokenIdx(Integer.valueOf(jwtTokenProvider.getUserTokenIdx(refreshToken)));
testUserTokenModifyDto.setRefreshToken("");
testUserTokenService.updateTestUserTokenModify(testUserTokenModifyDto);
} catch (NumberFormatException ignored) {
}
}
SessionUtil.expireCookie(response, ssoCookieName, cookieDomain, cookieSecure, cookieSameSite);
SessionUtil.expireCookie(response, "accessToken", cookieDomain, cookieSecure, cookieSameSite);
SessionUtil.expireCookie(response, "refreshToken", cookieDomain, cookieSecure, cookieSameSite);
SsoLogoutVo ssoLogoutVo = new SsoLogoutVo();
ssoLogoutVo.setLoggedOut(true);
return ssoLogoutVo;
}
private void refreshSsoSessionTtl(String ssoSessionId, String userIdx) {
stringRedisTemplate.expire(SSO_SESSION_KEY_PREFIX + ssoSessionId, java.time.Duration.ofSeconds(ssoSessionTtlSeconds));
stringRedisTemplate.expire(SSO_USER_KEY_PREFIX + userIdx, java.time.Duration.ofSeconds(ssoSessionTtlSeconds));
}
}
@@ -0,0 +1,28 @@
package com.alist.api.front.modules.sso.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
@Schema(description = "SSO 인가 코드 교환 응답")
@Getter
@Setter
public class SsoExchangeVo {
@Schema(description = "SSO 세션 ID")
private String ssoSessionId;
@Schema(description = "회원 PK", example = "1")
private Integer userIdx;
@Schema(description = "회원 토큰 PK", example = "1")
private Integer userTokenIdx;
@Schema(description = "회원 아이디", example = "teacher01")
private String userId;
@Schema(description = "회원 권한", example = "TEACHER")
private String userRole;
@Schema(description = "회원 유형", example = "T")
private String userType;
}
@@ -0,0 +1,40 @@
package com.alist.api.front.modules.sso.vo;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
@Schema(description = "SSO 로그인 상태 확인 응답")
@Getter
@Setter
public class SsoLoginCheckedVo {
@JsonProperty("isAccessToken")
@Schema(description = "Access Token 쿠키 존재 여부", example = "true")
private boolean accessToken;
@JsonProperty("isRefreshToken")
@Schema(description = "Refresh Token 쿠키 존재 여부", example = "true")
private boolean refreshToken;
@Schema(description = "SSO 로그인 상태", example = "true")
private boolean loggedIn;
@Schema(description = "SSO 세션 ID")
private String ssoSessionId;
@Schema(description = "회원 아이디", example = "teacher01")
private String userId;
@Schema(description = "회원 PK", example = "1")
private Integer userIdx;
@Schema(description = "회원 토큰 PK", example = "1")
private Integer userTokenIdx;
@Schema(description = "회원 권한", example = "TEACHER")
private String userRole;
@Schema(description = "회원 유형", example = "T")
private String userType;
}
@@ -0,0 +1,30 @@
package com.alist.api.front.modules.sso.vo;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
@Schema(description = "SSO 로그인 응답")
@Getter
@Setter
public class SsoLoginVo {
@Schema(description = "로그인 성공 여부", example = "true")
private boolean login;
@Schema(description = "회원 아이디", example = "teacher01")
private String id;
@Schema(description = "회원 PK", example = "1")
private Integer userIdx;
@Schema(description = "회원 권한", example = "TEACHER")
private String userRole;
@Schema(description = "회원 유형", example = "T")
private String userType;
@JsonIgnore
@Schema(hidden = true)
private int resultCode;
}
@@ -0,0 +1,13 @@
package com.alist.api.front.modules.sso.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
@Schema(description = "SSO 로그아웃 응답")
@Getter
@Setter
public class SsoLogoutVo {
@Schema(description = "로그아웃 완료 여부", example = "true")
private boolean loggedOut;
}
@@ -0,0 +1,85 @@
package com.alist.api.front.modules.user;
import com.alist.api.core.common.response.ApiResponse;
import com.alist.api.core.common.response.ApiResponseCode;
import com.alist.api.front.modules.user.form.*;
import com.alist.api.front.modules.user.service.UserService;
import com.alist.api.front.modules.user.vo.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@Tag(name = "A-3. 사용자 관리", description = "사용자 회원가입 및 개인정보 관리 API")
@RestController
@RequestMapping("/user")
public class UserController {
private final UserService userService;
public UserController(UserService userService) { this.userService = userService; }
@Operation(summary = "사용자 회원가입", description = "새 사용자와 사용자 API Key를 등록합니다.")
@PostMapping("/add")
public ResponseEntity<ApiResponse<UserAddVo>> userAdd(@Valid @RequestBody UserAddForm userAddForm) {
UserAddVo userAddVo = userService.insertUserAdd(userAddForm);
if (userAddVo.getResultCode() == 2004) return ApiResponse.entity(userAddVo, ApiResponseCode.CODE_2004, "아이디");
return ApiResponse.entity(userAddVo, ApiResponseCode.CODE_2002, "사용자");
}
@Operation(summary = "이전 회원 정보 조회", description = "ALIST와 ELTOWN의 이전 회원 정보를 조회합니다.")
@PostMapping("/migration/list")
public ResponseEntity<ApiResponse<UserMigrationListVo>> userMigrationList(@Valid @RequestBody UserMigrationListForm userMigrationListForm) {
UserMigrationListVo userMigrationListVo = userService.selectUserMigrationList(userMigrationListForm);
if (userMigrationListVo.getAlistUserList().isEmpty() && userMigrationListVo.getEltownUserList().isEmpty()) return ApiResponse.entity(userMigrationListVo, ApiResponseCode.CODE_2003);
return ApiResponse.entity(userMigrationListVo, ApiResponseCode.CODE_2001, "이전 회원");
}
@Operation(summary = "사용자 개인정보 조회", description = "로그인한 사용자의 개인정보를 조회합니다.")
@GetMapping("/profile/view")
public ResponseEntity<ApiResponse<UserProfileViewVo>> userProfileView() {
UserProfileViewVo userProfileViewVo = userService.selectUserProfileView();
if (userProfileViewVo == null) return ApiResponse.entity(new UserProfileViewVo(), ApiResponseCode.CODE_2003);
return ApiResponse.entity(userProfileViewVo, ApiResponseCode.CODE_2001, "개인정보");
}
@Operation(summary = "사용자 비밀번호 확인", description = "로그인한 사용자의 현재 비밀번호를 확인합니다.")
@PostMapping("/password/check")
public ResponseEntity<ApiResponse<UserPasswordCheckVo>> userPasswordCheck(@Valid @RequestBody UserPasswordCheckForm userPasswordCheckForm) {
UserPasswordCheckVo userPasswordCheckVo = userService.selectUserPasswordCheck(userPasswordCheckForm);
if (!userPasswordCheckVo.isPasswordMatched()) return ApiResponse.entity(userPasswordCheckVo, userPasswordCheckVo.getResultCode() == 4003 ? ApiResponseCode.CODE_4001 : ApiResponseCode.CODE_2003);
return ApiResponse.entity(userPasswordCheckVo, ApiResponseCode.CODE_200);
}
@Operation(summary = "사용자 비밀번호 수정", description = "현재 비밀번호 확인 후 새 비밀번호로 변경합니다.")
@PatchMapping("/password/modify")
public ResponseEntity<ApiResponse<UserPasswordModifyVo>> userPasswordModify(@Valid @RequestBody UserPasswordModifyForm userPasswordModifyForm) {
UserPasswordModifyVo userPasswordModifyVo = userService.updateUserPasswordModify(userPasswordModifyForm);
if (!userPasswordModifyVo.isPasswordChanged()) return ApiResponse.entity(userPasswordModifyVo, userPasswordModifyVo.getResultCode() == 4001 || userPasswordModifyVo.getResultCode() == 4003 ? ApiResponseCode.CODE_4001 : ApiResponseCode.CODE_2003);
return ApiResponse.entity(userPasswordModifyVo, ApiResponseCode.CODE_2005, "비밀번호 변경");
}
@Operation(summary = "사용자 개인정보 수정", description = "현재 비밀번호 확인 후 이메일과 휴대폰 번호를 수정합니다.")
@PatchMapping("/profile/modify")
public ResponseEntity<ApiResponse<UserProfileModifyVo>> userProfileModify(@Valid @RequestBody UserProfileModifyForm userProfileModifyForm) {
UserProfileModifyVo userProfileModifyVo = userService.updateUserProfileModify(userProfileModifyForm);
if (!userProfileModifyVo.isProfileUpdated()) return ApiResponse.entity(userProfileModifyVo, userProfileModifyVo.getResultCode() == 4001 || userProfileModifyVo.getResultCode() == 4003 ? ApiResponseCode.CODE_4001 : ApiResponseCode.CODE_2003);
return ApiResponse.entity(userProfileModifyVo, ApiResponseCode.CODE_2005, "개인정보 수정");
}
@Operation(summary = "사용자 탈퇴 상태 변경", description = "탈퇴 요청(P) 또는 철회(N)를 처리합니다.")
@PatchMapping("/withdraw/modify")
public ResponseEntity<ApiResponse<UserWithdrawModifyVo>> userWithdrawModify(@Valid @RequestBody UserWithdrawModifyForm userWithdrawModifyForm) {
UserWithdrawModifyVo userWithdrawModifyVo = userService.updateUserWithdrawModify(userWithdrawModifyForm);
if (!userWithdrawModifyVo.isWithdrawStatusUpdated()) return ApiResponse.entity(userWithdrawModifyVo, userWithdrawModifyVo.getResultCode() == 4003 ? ApiResponseCode.CODE_4001 : ApiResponseCode.CODE_2003);
return ApiResponse.entity(userWithdrawModifyVo, ApiResponseCode.CODE_2005, "탈퇴 상태 변경");
}
@Operation(summary = "사용자 휴면 상태 변경", description = "휴면 처리(Y) 또는 해지(N)를 처리합니다.")
@PatchMapping("/dormant/modify")
public ResponseEntity<ApiResponse<UserDormantModifyVo>> userDormantModify(@Valid @RequestBody UserDormantModifyForm userDormantModifyForm) {
UserDormantModifyVo userDormantModifyVo = userService.updateUserDormantModify(userDormantModifyForm);
if (!userDormantModifyVo.isDormantStatusUpdated()) return ApiResponse.entity(userDormantModifyVo, userDormantModifyVo.getResultCode() == 4003 ? ApiResponseCode.CODE_4001 : ApiResponseCode.CODE_2003);
return ApiResponse.entity(userDormantModifyVo, ApiResponseCode.CODE_2005, "휴면 상태 변경");
}
}
@@ -0,0 +1,22 @@
package com.alist.api.front.modules.user.form;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.Setter;
@Schema(description = "사용자 회원가입 요청")
@Getter
@Setter
public class UserAddForm {
@Schema(description = "사용자 아이디", example = "teacher02", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "아이디를 입력해주세요.") private String id;
@Schema(description = "비밀번호", example = "pass1234", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "비밀번호를 입력해주세요.") @Size(min = 8, max = 64) @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)\\S+$", message = "비밀번호는 영문과 숫자를 포함하고 공백이 없어야 합니다.") private String password;
@Schema(description = "회원 권한", example = "I", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank @Pattern(regexp = "^(CCM|SCM|I|R|E)$") private String userRole;
@Schema(description = "회원 유형", example = "T", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank @Pattern(regexp = "^(S|T)$") private String userType;
}
@@ -0,0 +1,12 @@
package com.alist.api.front.modules.user.form;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.Getter;
import lombok.Setter;
@Schema(description = "사용자 휴면 상태 변경 요청") @Getter @Setter
public class UserDormantModifyForm {
@Schema(description = "휴면 여부", example = "Y", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank @Pattern(regexp = "^[YN]$") private String dormantYn;
}
@@ -0,0 +1,12 @@
package com.alist.api.front.modules.user.form;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.Setter;
@Schema(description = "이전 회원 조회 요청") @Getter @Setter
public class UserMigrationListForm {
@Schema(description = "이전 회원 아이디", example = "test", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank private String id;
@Schema(description = "이전 회원 비밀번호", example = "pass1234", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank private String password;
}
@@ -0,0 +1,11 @@
package com.alist.api.front.modules.user.form;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.Setter;
@Schema(description = "사용자 비밀번호 확인 요청") @Getter @Setter
public class UserPasswordCheckForm {
@Schema(description = "현재 비밀번호", example = "pass1234", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank private String password;
}
@@ -0,0 +1,14 @@
package com.alist.api.front.modules.user.form;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.Setter;
@Schema(description = "사용자 비밀번호 수정 요청") @Getter @Setter
public class UserPasswordModifyForm {
@Schema(description = "현재 비밀번호", example = "pass1234", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank private String currentPassword;
@Schema(description = "새 비밀번호", example = "newpass1234", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank @Size(min = 8, max = 64) @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)\\S+$") private String newPassword;
}
@@ -0,0 +1,14 @@
package com.alist.api.front.modules.user.form;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.Setter;
@Schema(description = "사용자 개인정보 수정 요청") @Getter @Setter
public class UserProfileModifyForm {
@Schema(description = "현재 비밀번호", example = "pass1234", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank private String currentPassword;
@Schema(description = "이메일", example = "user@example.com") @Email private String email;
@Schema(description = "휴대폰 번호", example = "01012345678") private String hp;
}
@@ -0,0 +1,12 @@
package com.alist.api.front.modules.user.form;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.Getter;
import lombok.Setter;
@Schema(description = "사용자 탈퇴 상태 변경 요청") @Getter @Setter
public class UserWithdrawModifyForm {
@Schema(description = "탈퇴 상태(P: 요청, N: 철회)", example = "P", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank @Pattern(regexp = "^[PN]$") private String withdrawStatus;
}
@@ -0,0 +1,177 @@
package com.alist.api.front.modules.user.service;
import com.alist.api.core.common.utils.ApiKeyGenerator;
import com.alist.api.core.common.utils.SecurityUtil;
import com.alist.api.core.modules.bespoke.testUser.dto.TestUserUserPasswordViewDto;
import com.alist.api.core.modules.bespoke.testUser.dto.TestUserUserProfileViewDto;
import com.alist.api.core.modules.bespoke.testUser.service.TestUserUserService;
import com.alist.api.core.modules.bespoke.testUser.vo.TestUserUserPasswordViewVo;
import com.alist.api.core.modules.bespoke.testUser.vo.TestUserUserProfileViewVo;
import com.alist.api.core.modules.migration.alist.user.dto.AlistUserMigrationDto;
import com.alist.api.core.modules.migration.alist.user.service.AlistUserMigrationService;
import com.alist.api.core.modules.migration.alist.user.vo.AlistUserMigrationVo;
import com.alist.api.core.modules.migration.eltown.user.dto.EltownUserMigrationDto;
import com.alist.api.core.modules.migration.eltown.user.service.EltownUserMigrationService;
import com.alist.api.core.modules.migration.eltown.user.vo.EltownUserMigrationVo;
import com.alist.api.core.modules.standard.testUser.dto.TestUserAddDto;
import com.alist.api.core.modules.standard.testUser.dto.TestUserModifyDto;
import com.alist.api.core.modules.standard.testUser.service.TestUserService;
import com.alist.api.core.modules.standard.testUserToken.dto.TestUserTokenAddDto;
import com.alist.api.core.modules.standard.testUserToken.service.TestUserTokenService;
import com.alist.api.front.modules.user.form.*;
import com.alist.api.front.modules.user.vo.*;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
@Service
public class UserService {
private final TestUserService testUserService;
private final TestUserTokenService testUserTokenService;
private final TestUserUserService testUserUserService;
private final AlistUserMigrationService alistUserMigrationService;
private final EltownUserMigrationService eltownUserMigrationService;
private final PasswordEncoder passwordEncoder;
public UserService(TestUserService testUserService, TestUserTokenService testUserTokenService, TestUserUserService testUserUserService, AlistUserMigrationService alistUserMigrationService, EltownUserMigrationService eltownUserMigrationService, PasswordEncoder passwordEncoder) {
this.testUserService = testUserService;
this.testUserTokenService = testUserTokenService;
this.testUserUserService = testUserUserService;
this.alistUserMigrationService = alistUserMigrationService;
this.eltownUserMigrationService = eltownUserMigrationService;
this.passwordEncoder = passwordEncoder;
}
@Transactional
public UserAddVo insertUserAdd(UserAddForm userAddForm) {
UserAddVo userAddVo = new UserAddVo();
userAddVo.setId(userAddForm.getId().trim());
userAddVo.setUserRole(userAddForm.getUserRole().trim());
userAddVo.setUserType(userAddForm.getUserType().trim());
if (testUserService.selectTestUserDuplicateCount(userAddVo.getId()) > 0) {
userAddVo.setResultCode(2004);
return userAddVo;
}
TestUserAddDto testUserAddDto = new TestUserAddDto();
testUserAddDto.setId(userAddVo.getId());
testUserAddDto.setPassword(userAddForm.getPassword());
var testUserAddVo = testUserService.insertTestUserAdd(testUserAddDto);
if (!testUserAddVo.isAdded()) {
userAddVo.setResultCode(testUserAddVo.getResultCode());
return userAddVo;
}
for (int retryCount = 0; retryCount < 5; retryCount++) {
TestUserTokenAddDto testUserTokenAddDto = new TestUserTokenAddDto();
testUserTokenAddDto.setUserIdx(testUserAddVo.getUserIdx());
testUserTokenAddDto.setUserApiKey(ApiKeyGenerator.userApiKeyProc());
testUserTokenAddDto.setUserRole(userAddVo.getUserRole());
testUserTokenAddDto.setUserType(userAddVo.getUserType());
var testUserTokenAddVo = testUserTokenService.insertTestUserTokenAdd(testUserTokenAddDto);
if (testUserTokenAddVo.isAdded()) {
userAddVo.setUserIdx(testUserAddVo.getUserIdx());
userAddVo.setResultCode(2002);
return userAddVo;
}
}
throw new IllegalStateException("사용자 API Key 생성에 실패했습니다.");
}
@Transactional(readOnly = true)
public UserProfileViewVo selectUserProfileView() {
Integer userTokenIdx = SecurityUtil.getLoginUserTokenIdx();
if (userTokenIdx == null) return null;
TestUserUserProfileViewDto testUserUserProfileViewDto = new TestUserUserProfileViewDto();
testUserUserProfileViewDto.setUserTokenIdx(userTokenIdx);
TestUserUserProfileViewVo testUserUserProfileViewVo = testUserUserService.testUserUserProfileView(testUserUserProfileViewDto);
if (testUserUserProfileViewVo == null) return null;
UserProfileViewVo userProfileViewVo = new UserProfileViewVo();
userProfileViewVo.setUserIdx(testUserUserProfileViewVo.getUserIdx());
userProfileViewVo.setId(testUserUserProfileViewVo.getId());
userProfileViewVo.setEmail(testUserUserProfileViewVo.getEmail());
userProfileViewVo.setHp(testUserUserProfileViewVo.getHp());
userProfileViewVo.setUserRole(testUserUserProfileViewVo.getUserRole());
userProfileViewVo.setUserType(testUserUserProfileViewVo.getUserType());
return userProfileViewVo;
}
@Transactional(readOnly = true)
public UserPasswordCheckVo selectUserPasswordCheck(UserPasswordCheckForm userPasswordCheckForm) {
return passwordCheck(userPasswordCheckForm.getPassword());
}
@Transactional
public UserPasswordModifyVo updateUserPasswordModify(UserPasswordModifyForm userPasswordModifyForm) {
UserPasswordCheckVo userPasswordCheckVo = passwordCheck(userPasswordModifyForm.getCurrentPassword());
UserPasswordModifyVo userPasswordModifyVo = new UserPasswordModifyVo();
if (!userPasswordCheckVo.isPasswordMatched()) { userPasswordModifyVo.setResultCode(userPasswordCheckVo.getResultCode()); return userPasswordModifyVo; }
TestUserUserPasswordViewDto testUserUserPasswordViewDto = new TestUserUserPasswordViewDto();
testUserUserPasswordViewDto.setUserTokenIdx(SecurityUtil.getLoginUserTokenIdx());
TestUserUserPasswordViewVo testUserUserPasswordViewVo = testUserUserService.testUserUserPasswordView(testUserUserPasswordViewDto);
TestUserModifyDto testUserModifyDto = new TestUserModifyDto();
testUserModifyDto.setUserIdx(testUserUserPasswordViewVo.getUserIdx());
testUserModifyDto.setPassword(userPasswordModifyForm.getNewPassword());
var testUserModifyVo = testUserService.updateTestUserModify(testUserModifyDto);
userPasswordModifyVo.setPasswordChanged(testUserModifyVo.isUpdated()); userPasswordModifyVo.setResultCode(testUserModifyVo.getResultCode()); return userPasswordModifyVo;
}
private UserPasswordCheckVo passwordCheck(String password) {
UserPasswordCheckVo userPasswordCheckVo = new UserPasswordCheckVo(); Integer userTokenIdx = SecurityUtil.getLoginUserTokenIdx();
if (userTokenIdx == null || password == null) { userPasswordCheckVo.setResultCode(4003); return userPasswordCheckVo; }
TestUserUserPasswordViewDto testUserUserPasswordViewDto = new TestUserUserPasswordViewDto(); testUserUserPasswordViewDto.setUserTokenIdx(userTokenIdx);
TestUserUserPasswordViewVo testUserUserPasswordViewVo = testUserUserService.testUserUserPasswordView(testUserUserPasswordViewDto);
if (testUserUserPasswordViewVo == null) { userPasswordCheckVo.setResultCode(2003); return userPasswordCheckVo; }
userPasswordCheckVo.setPasswordMatched(passwordEncoder.matches(password, testUserUserPasswordViewVo.getPassword())); userPasswordCheckVo.setResultCode(userPasswordCheckVo.isPasswordMatched() ? 200 : 4001); return userPasswordCheckVo;
}
@Transactional(readOnly = true)
public UserMigrationListVo selectUserMigrationList(UserMigrationListForm userMigrationListForm) {
AlistUserMigrationDto alistUserMigrationDto = new AlistUserMigrationDto();
alistUserMigrationDto.setId(userMigrationListForm.getId().trim());
alistUserMigrationDto.setPassword(userMigrationListForm.getPassword());
EltownUserMigrationDto eltownUserMigrationDto = new EltownUserMigrationDto();
eltownUserMigrationDto.setId(userMigrationListForm.getId().trim());
eltownUserMigrationDto.setPassword(userMigrationListForm.getPassword());
UserMigrationListVo userMigrationListVo = new UserMigrationListVo();
List<UserMigrationItemVo> alistUserList = new ArrayList<>();
for (AlistUserMigrationVo sourceVo : alistUserMigrationService.selectAlistUserMigrationList(alistUserMigrationDto)) { UserMigrationItemVo itemVo = new UserMigrationItemVo(); itemVo.setId(sourceVo.getId()); itemVo.setName(sourceVo.getName()); itemVo.setHp(sourceVo.getHp()); itemVo.setEmail(sourceVo.getEmail()); itemVo.setType(sourceVo.getType()); itemVo.setJoinDate(sourceVo.getJoinDate()); alistUserList.add(itemVo); }
List<UserMigrationItemVo> eltownUserList = new ArrayList<>();
for (EltownUserMigrationVo sourceVo : eltownUserMigrationService.selectEltownUserMigrationList(eltownUserMigrationDto)) { UserMigrationItemVo itemVo = new UserMigrationItemVo(); itemVo.setId(sourceVo.getId()); itemVo.setName(sourceVo.getName()); itemVo.setHp(sourceVo.getHp()); itemVo.setEmail(sourceVo.getEmail()); itemVo.setType(sourceVo.getType()); itemVo.setJoinDate(sourceVo.getJoinDate()); eltownUserList.add(itemVo); }
userMigrationListVo.setAlistUserList(alistUserList); userMigrationListVo.setEltownUserList(eltownUserList); return userMigrationListVo;
}
@Transactional
public UserProfileModifyVo updateUserProfileModify(UserProfileModifyForm userProfileModifyForm) {
UserPasswordCheckVo userPasswordCheckVo = passwordCheck(userProfileModifyForm.getCurrentPassword()); UserProfileModifyVo userProfileModifyVo = new UserProfileModifyVo();
if (!userPasswordCheckVo.isPasswordMatched()) { userProfileModifyVo.setResultCode(userPasswordCheckVo.getResultCode()); return userProfileModifyVo; }
if (userProfileModifyForm.getEmail() == null && userProfileModifyForm.getHp() == null) { userProfileModifyVo.setResultCode(4001); return userProfileModifyVo; }
TestUserUserPasswordViewDto dto = new TestUserUserPasswordViewDto(); dto.setUserTokenIdx(SecurityUtil.getLoginUserTokenIdx()); TestUserUserPasswordViewVo passwordVo = testUserUserService.testUserUserPasswordView(dto);
TestUserModifyDto testUserModifyDto = new TestUserModifyDto(); testUserModifyDto.setUserIdx(passwordVo.getUserIdx()); testUserModifyDto.setEmail(userProfileModifyForm.getEmail()); testUserModifyDto.setHp(userProfileModifyForm.getHp());
var coreVo = testUserService.updateTestUserModify(testUserModifyDto); userProfileModifyVo.setProfileUpdated(coreVo.isUpdated()); userProfileModifyVo.setResultCode(coreVo.getResultCode()); return userProfileModifyVo;
}
@Transactional
public UserWithdrawModifyVo updateUserWithdrawModify(UserWithdrawModifyForm userWithdrawModifyForm) {
UserWithdrawModifyVo userWithdrawModifyVo = new UserWithdrawModifyVo(); Integer userTokenIdx = SecurityUtil.getLoginUserTokenIdx(); if (userTokenIdx == null) { userWithdrawModifyVo.setResultCode(4003); return userWithdrawModifyVo; }
TestUserUserPasswordViewDto dto = new TestUserUserPasswordViewDto(); dto.setUserTokenIdx(userTokenIdx); TestUserUserPasswordViewVo passwordVo = testUserUserService.testUserUserPasswordView(dto); if (passwordVo == null) { userWithdrawModifyVo.setResultCode(2003); return userWithdrawModifyVo; }
TestUserModifyDto testUserModifyDto = new TestUserModifyDto(); testUserModifyDto.setUserIdx(passwordVo.getUserIdx()); testUserModifyDto.setWithdrawStatus(userWithdrawModifyForm.getWithdrawStatus()); var coreVo = testUserService.updateTestUserModify(testUserModifyDto); userWithdrawModifyVo.setWithdrawStatusUpdated(coreVo.isUpdated()); userWithdrawModifyVo.setResultCode(coreVo.getResultCode()); return userWithdrawModifyVo;
}
@Transactional
public UserDormantModifyVo updateUserDormantModify(UserDormantModifyForm userDormantModifyForm) {
UserDormantModifyVo userDormantModifyVo = new UserDormantModifyVo(); Integer userTokenIdx = SecurityUtil.getLoginUserTokenIdx(); if (userTokenIdx == null) { userDormantModifyVo.setResultCode(4003); return userDormantModifyVo; }
TestUserUserPasswordViewDto dto = new TestUserUserPasswordViewDto(); dto.setUserTokenIdx(userTokenIdx); TestUserUserPasswordViewVo passwordVo = testUserUserService.testUserUserPasswordView(dto); if (passwordVo == null) { userDormantModifyVo.setResultCode(2003); return userDormantModifyVo; }
TestUserModifyDto testUserModifyDto = new TestUserModifyDto(); testUserModifyDto.setUserIdx(passwordVo.getUserIdx()); testUserModifyDto.setDormantYn(userDormantModifyForm.getDormantYn()); var coreVo = testUserService.updateTestUserModify(testUserModifyDto); userDormantModifyVo.setDormantStatusUpdated(coreVo.isUpdated()); userDormantModifyVo.setResultCode(coreVo.getResultCode()); return userDormantModifyVo;
}
}
@@ -0,0 +1,15 @@
package com.alist.api.front.modules.user.vo;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
@Schema(description = "사용자 회원가입 응답") @Getter @Setter
public class UserAddVo {
@Schema(description = "회원 PK", example = "2") private Integer userIdx;
@Schema(description = "회원 아이디", example = "teacher02") private String id;
@Schema(description = "회원 권한", example = "I") private String userRole;
@Schema(description = "회원 유형", example = "T") private String userType;
@JsonIgnore @Schema(hidden = true) private int resultCode;
}
@@ -0,0 +1,12 @@
package com.alist.api.front.modules.user.vo;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
@Schema(description = "사용자 휴면 상태 변경 응답") @Getter @Setter
public class UserDormantModifyVo {
@Schema(description = "휴면 상태 변경 여부", example = "true") private boolean dormantStatusUpdated;
@JsonIgnore @Schema(hidden = true) private int resultCode;
}
@@ -0,0 +1,15 @@
package com.alist.api.front.modules.user.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
@Schema(description = "이전 회원 항목") @Getter @Setter
public class UserMigrationItemVo {
@Schema(description = "회원 아이디", example = "test") private String id;
@Schema(description = "회원 이름", example = "홍길동") private String name;
@Schema(description = "휴대폰 번호", example = "01012345678") private String hp;
@Schema(description = "이메일", example = "user@example.com") private String email;
@Schema(description = "회원 구분", example = "교사") private String type;
@Schema(description = "가입일", example = "2026-05-19") private String joinDate;
}
@@ -0,0 +1,13 @@
package com.alist.api.front.modules.user.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
@Schema(description = "이전 회원 조회 응답") @Getter @Setter
public class UserMigrationListVo {
@Schema(description = "ALIST 이전 회원 목록") private List<UserMigrationItemVo> alistUserList;
@Schema(description = "ELTOWN 이전 회원 목록") private List<UserMigrationItemVo> eltownUserList;
}
@@ -0,0 +1,12 @@
package com.alist.api.front.modules.user.vo;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
@Schema(description = "사용자 비밀번호 확인 응답") @Getter @Setter
public class UserPasswordCheckVo {
@Schema(description = "비밀번호 일치 여부", example = "true") private boolean passwordMatched;
@JsonIgnore @Schema(hidden = true) private int resultCode;
}
@@ -0,0 +1,12 @@
package com.alist.api.front.modules.user.vo;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
@Schema(description = "사용자 비밀번호 수정 응답") @Getter @Setter
public class UserPasswordModifyVo {
@Schema(description = "비밀번호 수정 여부", example = "true") private boolean passwordChanged;
@JsonIgnore @Schema(hidden = true) private int resultCode;
}
@@ -0,0 +1,12 @@
package com.alist.api.front.modules.user.vo;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
@Schema(description = "사용자 개인정보 수정 응답") @Getter @Setter
public class UserProfileModifyVo {
@Schema(description = "개인정보 수정 여부", example = "true") private boolean profileUpdated;
@JsonIgnore @Schema(hidden = true) private int resultCode;
}
@@ -0,0 +1,15 @@
package com.alist.api.front.modules.user.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
@Schema(description = "사용자 개인정보 조회 응답") @Getter @Setter
public class UserProfileViewVo {
@Schema(description = "회원 PK", example = "1") private Integer userIdx;
@Schema(description = "회원 아이디", example = "test") private String id;
@Schema(description = "이메일", example = "user@example.com") private String email;
@Schema(description = "휴대폰 번호", example = "01012345678") private String hp;
@Schema(description = "회원 권한", example = "CCM") private String userRole;
@Schema(description = "회원 유형", example = "T") private String userType;
}
@@ -0,0 +1,12 @@
package com.alist.api.front.modules.user.vo;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
@Schema(description = "사용자 탈퇴 상태 변경 응답") @Getter @Setter
public class UserWithdrawModifyVo {
@Schema(description = "탈퇴 상태 변경 여부", example = "true") private boolean withdrawStatusUpdated;
@JsonIgnore @Schema(hidden = true) private int resultCode;
}
@@ -0,0 +1,119 @@
spring:
application:
name: api
datasource:
url: jdbc:log4jdbc:mariadb://121.160.234.222:3000/ALISTLMS?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Seoul&connectionCollation=utf8mb4_uca1400_ai_ci
username: alist_dev
password: 1qaz2wsx!@
driver-class-name: net.sf.log4jdbc.sql.jdbcapi.DriverSpy
hikari:
maximum-pool-size: 10
minimum-idle: 5
connection-timeout: 10000
idle-timeout: 600000
data:
redis:
host: 121.160.234.222
port: 3001
password: 1qaz2wsx!@
servlet:
multipart:
max-file-size: -1
max-request-size: -1
migration:
datasource:
alist:
jdbc-url: jdbc:log4jdbc:sqlserver://bigfuntnp.co.kr:51433;databaseName=alist_dev;encrypt=false;trustServerCertificate=true
username: bigfuntnp
password: password12!@
driver-class-name: net.sf.log4jdbc.sql.jdbcapi.DriverSpy
maximum-pool-size: 5
minimum-idle: 1
connection-timeout: 10000
eltown:
jdbc-url: jdbc:log4jdbc:sqlserver://bigfuntnp.co.kr:51433;databaseName=eltown;encrypt=false;trustServerCertificate=true
username: bigfuntnp
password: password12!@
driver-class-name: net.sf.log4jdbc.sql.jdbcapi.DriverSpy
maximum-pool-size: 5
minimum-idle: 1
connection-timeout: 10000
logging:
config: classpath:logback-local.xml
jwt:
secret: "F9k3s!29dkF#1lP0X9QZx8eW!2m@0AbD"
access-token-validity-seconds: 3600
refresh-token-validity-seconds: 2592000
cookie:
secure: false # (HTTP) -> false
domain: # (HTTP) -> 비워두세요
name: ALIST_SSO
same-site: Lax # (HTTP) -> Lax
swagger:
login:
id: alist
password: "1qaz2wsx!@"
tus-file:
upload:
tus-endpoint: https://file-alist.pjt.kr/tus/file/
public-base-url: https://file-alist.pjt.kr
tmp-root: /srv/project/alist/uploads/tmp
final-root: /srv/project/alist/uploads
interrupt-seconds: 30
auth-cache:
ttl-seconds: 20
# types:
# notice:
# folder: notice 저장폴더
# max-size: 20MB 최대 용량
# image-only: false
# resize:
# enabled: true
# width: 800 고정넓이
# height: 600 고정높이
# max-width: 1200 최대 넓이
file:
upload:
view:
file-domain: http://localhost:8110
root-path: ${tus-file.upload.final-root}
max-size: 10MB
allowed-extensions: [jpg, jpeg, png, gif, pdf, hwp, hwpx, doc, docx, xls, xlsx, ppt, pptx, txt, csv, zip]
types:
profile:
folder: profile
max-size: 5MB
image-only: true
resize:
enabled: true
width: 400
height: 400
notice:
folder: notice
max-size: 20MB
image-only: false
resize:
enabled: true
max-width: 1200
springdoc:
api-docs:
enabled: true
swagger-ui:
enabled: true
sso:
session:
ttl-seconds: 7200
code:
ttl-seconds: 300
cookie:
name: ALIST_SSO
@@ -0,0 +1,121 @@
spring:
application:
name: api
main:
banner-mode: log
datasource:
url: jdbc:log4jdbc:mariadb://${DB_HOST}:${DB_PORT}/${DB_NAME}?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Seoul&connectionCollation=utf8mb4_uca1400_ai_ci
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
driver-class-name: net.sf.log4jdbc.sql.jdbcapi.DriverSpy
hikari:
maximum-pool-size: 10
minimum-idle: 5
connection-timeout: 10000
idle-timeout: 600000
data:
redis:
host: ${REDIS_HOST}
port: ${REDIS_PORT}
password: ${REDIS_PASSWORD}
servlet:
multipart:
max-file-size: -1
max-request-size: -1
migration:
datasource:
alist:
jdbc-url: jdbc:log4jdbc:sqlserver://${ALIST_DB_HOST}:${ALIST_DB_PORT};databaseName=${ALIST_DB_NAME};encrypt=false;trustServerCertificate=true
username: ${ALIST_DB_USERNAME}
password: ${ALIST_DB_PASSWORD}
driver-class-name: net.sf.log4jdbc.sql.jdbcapi.DriverSpy
maximum-pool-size: 5
minimum-idle: 1
connection-timeout: 10000
eltown:
jdbc-url: jdbc:log4jdbc:sqlserver://${ELTOWN_DB_HOST}:${ELTOWN_DB_PORT};databaseName=${ELTOWN_DB_NAME};encrypt=false;trustServerCertificate=true
username: ${ELTOWN_DB_USERNAME}
password: ${ELTOWN_DB_PASSWORD}
driver-class-name: net.sf.log4jdbc.sql.jdbcapi.DriverSpy
maximum-pool-size: 5
minimum-idle: 1
connection-timeout: 10000
logging:
config: classpath:logback-pjt.xml
jwt:
secret: ${JWT_SECRET}
access-token-validity-seconds: 3600
refresh-token-validity-seconds: 2592000
cookie:
secure: true # 프로젝트 환경 (HTTPS)
domain: api-alist.pjt.kr
name: ALIST_SSO
same-site: None
swagger:
login:
id: ${SWAGGER_ID}
password: ${SWAGGER_PASSWORD}
tus-file:
upload:
tus-endpoint: https://file-alist.pjt.kr/tus/file/
public-base-url: https://file-alist.pjt.kr
tmp-root: /srv/project/alist/uploads/tmp
final-root: /srv/project/alist/uploads
interrupt-seconds: 30
auth-cache:
ttl-seconds: 20
# types:
# notice:
# folder: notice 저장폴더
# max-size: 20MB 최대 용량
# image-only: false
# resize:
# enabled: true
# width: 800 고정넓이
# height: 600 고정높이
# max-width: 1200 최대 넓이
file:
upload:
view:
file-domain: https://file-alist.pjt.kr
root-path: ${tus-file.upload.final-root}
max-size: 10MB
allowed-extensions: [jpg, jpeg, png, gif, pdf, hwp, hwpx, doc, docx, xls, xlsx, ppt, pptx, txt, csv, zip]
types:
profile:
folder: profile
max-size: 5MB
image-only: true
resize:
enabled: true
width: 400
height: 400
notice:
folder: notice
max-size: 20MB
image-only: false
resize:
enabled: true
max-width: 1200
springdoc:
api-docs:
enabled: true
swagger-ui:
enabled: true
sso:
session:
ttl-seconds: 7200
code:
ttl-seconds: 300
cookie:
name: ALIST_SSO
@@ -0,0 +1,47 @@
server:
port: 8112
forward-headers-strategy: framework
spring:
application:
name: api-front
springdoc:
swagger-ui:
path: /swagger-ui.html
tags-sorter: alpha # 태그 알파벳 순 정렬
operations-sorter: alpha # API 알파벳 순 정렬
api-docs:
path: /v3/api-docs
# API version rule: MAJOR.MINOR.PATCH
# MAJOR: 기존 API와 호환되지 않는 변경
# - 요청/응답 필드명 변경 또는 삭제
# - URL, HTTP Method, 인증 방식 변경
# - 기존 클라이언트가 수정 없이는 사용할 수 없는 변경
#
# MINOR: 기존 API와 호환되는 기능 추가
# - 신규 API 추가
# - 응답 필드 추가
# - 선택 요청 파라미터 추가
# - 기존 동작을 깨지 않는 기능 확장
#
# PATCH: 호환성에 영향 없는 수정
# - 버그 수정
# - 문서/Swagger 설명 수정
# - 내부 로직 개선
# - 성능 개선
api:
version: v1.0.1
management:
endpoints:
web:
exposure:
include: health,info,metrics
endpoint:
health:
show-details: always
health:
db:
enabled: false
@@ -0,0 +1,18 @@
q p ,,
db \ / `7MMF' db mm
;MM:o=--*--=o MM MM
,V^MM. / \ MM `7MM ,pP"Ybd mmMMmm
,M `MM d b MM MM 8I `" MM
AbmmmqMA MM , MM `YMMMa. MM
A' VML MM ,M MM L. I8 MM
.AMA. .AMMA. .JMMmmmmMMM .JMML.M9mmmP' `Mbmo
:: ALIST API ::
Application : ${spring.application.name}
Profile : ${spring.profiles.active}
Spring Boot : ${spring-boot.version}
Java : ${java.version}
Port : ${server.port}
PID : ${PID}
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 로그 디렉토리 -->
<property name="LOG_PATH" value="/logs" />
<!-- Spring Boot가 제공하는 %clr / 색상 변환기 등록 -->
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<!-- 콘솔 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%clr(%d{HH:mm:ss.SSS}){faint} %clr(%5p) %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n</pattern>
</encoder>
</appender>
<!-- 콘솔 appender: Boot 기본 제공 -->
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
<!-- ==== 로컬(local) ==== -->
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
<!-- springframework -->
<logger name="org.springframework" level="INFO"/>
<!-- project -->
<logger name="com.alist.api" level="DEBUG"/>
<!-- MyBatis SQL/파라미터 보고 싶으면 DEBUG -->
<logger name="org.mybatis" level="DEBUG"/>
<logger name="org.apache.ibatis" level="DEBUG"/>
<!-- log4jdbc -->
<logger name="jdbc.sqltiming" level="DEBUG"/>
<logger name="jdbc.resultsettable" level="DEBUG"/>
<logger name="jdbc.audit" level="OFF"/>
<logger name="jdbc.resultset" level="OFF"/>
<logger name="jdbc.sqlonly" level="OFF"/>
</configuration>
@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 로그 디렉토리 -->
<property name="LOG_PATH" value="/logs" />
<property name="LOG_FILE_NAME" value="api-front" />
<!-- 파일 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 현재 로그는 고정 위치 (tail 편함) -->
<file>${LOG_PATH}/${LOG_FILE_NAME}.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 롤링 파일은 날짜 폴더(yyyy/MM) 하위로 저장 -->
<fileNamePattern>
${LOG_PATH}/%d{yyyy/MM}/${LOG_FILE_NAME}.log-%d{yyyy-MM-dd}.%i
</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>50MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<maxHistory>180</maxHistory>
<cleanHistoryOnStart>true</cleanHistoryOnStart>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} | %-5level | %-15thread | %-40logger{40} | %msg%n</pattern>
</encoder>
</appender>
<!-- ==== 빅펀개발(pjt) ==== -->
<!-- 기본 -->
<root level="INFO">
<appender-ref ref="FILE"/>
</root>
<!-- springframework -->
<logger name="org.springframework" level="WARN"/>
<!-- project -->
<logger name="com.alist.api.front" level="INFO"/>
<!-- MyBatis -->
<logger name="org.mybatis" level="WARN"/>
<logger name="org.apache.ibatis" level="WARN"/>
<!-- log4jdbc -->
<logger name="jdbc.sqltiming" level="INFO"/>
<logger name="jdbc.resultsettable" level="OFF"/>
<logger name="jdbc.audit" level="OFF"/>
<logger name="jdbc.resultset" level="OFF"/>
<logger name="jdbc.sqlonly" level="OFF"/>
</configuration>
@@ -0,0 +1,2 @@
User-agent: *
Disallow: /