commit
446e4c613d
@ -29,6 +29,8 @@ primary:
|
||||
|
||||
# 성능 최적화 설정
|
||||
extraEnvVars:
|
||||
- name: POSTGRESQL_READ_ONLY_MODE
|
||||
value: "no"
|
||||
- name: POSTGRESQL_SHARED_BUFFERS
|
||||
value: "1GB"
|
||||
- name: POSTGRESQL_EFFECTIVE_CACHE_SIZE
|
||||
|
||||
@ -51,7 +51,7 @@ paths:
|
||||
- JWT 토큰 자동 발급
|
||||
|
||||
**처리 흐름:**
|
||||
1. 중복 사용자 확인 (전화번호 기반)
|
||||
1. 중복 사용자 확인 (이메일/전화번호 기반)
|
||||
2. 비밀번호 해싱 (bcrypt)
|
||||
3. User/Store 데이터베이스 트랜잭션 처리
|
||||
4. JWT 토큰 생성 및 세션 저장 (Redis)
|
||||
@ -114,7 +114,7 @@ paths:
|
||||
summary: 중복 사용자
|
||||
value:
|
||||
code: USER_001
|
||||
message: 이미 가입된 전화번호입니다
|
||||
message: 이미 가입된 이메일입니다
|
||||
timestamp: 2025-10-22T10:30:00Z
|
||||
validationError:
|
||||
summary: 입력 검증 오류
|
||||
@ -140,7 +140,7 @@ paths:
|
||||
**유저스토리:** UFR-USER-020
|
||||
|
||||
**주요 기능:**
|
||||
- 전화번호/비밀번호 인증
|
||||
- 이메일/비밀번호 인증
|
||||
- JWT 토큰 발급
|
||||
- Redis 세션 저장
|
||||
- 최종 로그인 시각 업데이트 (비동기)
|
||||
@ -162,7 +162,7 @@ paths:
|
||||
default:
|
||||
summary: 로그인 요청 예시
|
||||
value:
|
||||
phoneNumber: "01012345678"
|
||||
email: hong@example.com
|
||||
password: "Password123!"
|
||||
responses:
|
||||
'200':
|
||||
@ -191,7 +191,7 @@ paths:
|
||||
summary: 인증 실패
|
||||
value:
|
||||
code: AUTH_001
|
||||
message: 전화번호 또는 비밀번호를 확인해주세요
|
||||
message: 이메일 또는 비밀번호를 확인해주세요
|
||||
timestamp: 2025-10-22T10:30:00Z
|
||||
|
||||
/users/logout:
|
||||
@ -679,14 +679,15 @@ components:
|
||||
LoginRequest:
|
||||
type: object
|
||||
required:
|
||||
- phoneNumber
|
||||
- email
|
||||
- password
|
||||
properties:
|
||||
phoneNumber:
|
||||
email:
|
||||
type: string
|
||||
pattern: '^010\d{8}$'
|
||||
description: 휴대폰 번호
|
||||
example: "01012345678"
|
||||
format: email
|
||||
maxLength: 100
|
||||
description: 이메일 주소
|
||||
example: hong@example.com
|
||||
password:
|
||||
type: string
|
||||
minLength: 8
|
||||
@ -977,7 +978,7 @@ components:
|
||||
message:
|
||||
type: string
|
||||
description: 에러 메시지
|
||||
example: 이미 가입된 전화번호입니다
|
||||
example: 이미 가입된 이메일입니다
|
||||
timestamp:
|
||||
type: string
|
||||
format: date-time
|
||||
|
||||
87
user-service/.run/user-service.run.xml
Normal file
87
user-service/.run/user-service.run.xml
Normal file
@ -0,0 +1,87 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="user-service" type="GradleRunConfiguration" factoryName="Gradle">
|
||||
<ExternalSystemSettings>
|
||||
<option name="env">
|
||||
<map>
|
||||
<!-- Server Configuration -->
|
||||
<entry key="SERVER_PORT" value="8081" />
|
||||
|
||||
<!-- Database Configuration -->
|
||||
<entry key="DB_URL" value="jdbc:postgresql://20.249.125.115:5432/userdb" />
|
||||
<entry key="DB_DRIVER" value="org.postgresql.Driver" />
|
||||
<entry key="DB_HOST" value="20.249.125.115" />
|
||||
<entry key="DB_PORT" value="5432" />
|
||||
<entry key="DB_NAME" value="userdb" />
|
||||
<entry key="DB_USERNAME" value="eventuser" />
|
||||
<entry key="DB_PASSWORD" value="Hi5Jessica!" />
|
||||
<entry key="DB_KIND" value="postgresql" />
|
||||
|
||||
<!-- JPA Configuration -->
|
||||
<entry key="DDL_AUTO" value="update" />
|
||||
<entry key="SHOW_SQL" value="true" />
|
||||
<entry key="JPA_DIALECT" value="org.hibernate.dialect.PostgreSQLDialect" />
|
||||
|
||||
<!-- H2 Console (disabled for production DB) -->
|
||||
<entry key="H2_CONSOLE_ENABLED" value="false" />
|
||||
|
||||
<!-- Redis Configuration -->
|
||||
<entry key="REDIS_ENABLED" value="true" />
|
||||
<entry key="REDIS_HOST" value="20.214.210.71" />
|
||||
<entry key="REDIS_PORT" value="6379" />
|
||||
<entry key="REDIS_PASSWORD" value="Hi5Jessica!" />
|
||||
<entry key="REDIS_DATABASE" value="0" />
|
||||
<entry key="EXCLUDE_REDIS" value="" />
|
||||
|
||||
<!-- Kafka Configuration -->
|
||||
<entry key="KAFKA_BOOTSTRAP_SERVERS" value="4.230.50.63:9092" />
|
||||
<entry key="KAFKA_CONSUMER_GROUP" value="user-service-consumers" />
|
||||
<entry key="EXCLUDE_KAFKA" value="" />
|
||||
|
||||
<!-- JWT Configuration -->
|
||||
<entry key="JWT_SECRET" value="kt-event-marketing-secret-key-for-development-only-please-change-in-production" />
|
||||
<entry key="JWT_ACCESS_TOKEN_VALIDITY" value="604800000" />
|
||||
|
||||
<!-- CORS Configuration -->
|
||||
<entry key="CORS_ALLOWED_ORIGINS" value="http://localhost:*" />
|
||||
|
||||
<!-- Logging Configuration -->
|
||||
<entry key="LOG_LEVEL_APP" value="DEBUG" />
|
||||
<entry key="LOG_LEVEL_WEB" value="INFO" />
|
||||
<entry key="LOG_LEVEL_SQL" value="DEBUG" />
|
||||
<entry key="LOG_LEVEL_SQL_TYPE" value="TRACE" />
|
||||
<entry key="LOG_FILE_PATH" value="logs/user-service.log" />
|
||||
</map>
|
||||
</option>
|
||||
<option name="executionName" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="externalSystemIdString" value="GRADLE" />
|
||||
<option name="scriptParameters" value="" />
|
||||
<option name="taskDescriptions">
|
||||
<list />
|
||||
</option>
|
||||
<option name="taskNames">
|
||||
<list>
|
||||
<option value="user-service:bootRun" />
|
||||
</list>
|
||||
</option>
|
||||
<option name="vmOptions" />
|
||||
</ExternalSystemSettings>
|
||||
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
|
||||
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
||||
<EXTENSION ID="com.intellij.execution.ExternalSystemRunConfigurationJavaExtension">
|
||||
<extension name="net.ashald.envfile">
|
||||
<option name="IS_ENABLED" value="false" />
|
||||
<option name="IS_SUBST" value="false" />
|
||||
<option name="IS_PATH_MACRO_SUPPORTED" value="false" />
|
||||
<option name="IS_IGNORE_MISSING_FILES" value="false" />
|
||||
<option name="IS_ENABLE_EXPERIMENTAL_INTEGRATIONS" value="false" />
|
||||
<ENTRIES>
|
||||
<ENTRY IS_ENABLED="true" PARSER="runconfig" IS_EXECUTABLE="false" />
|
||||
</ENTRIES>
|
||||
</extension>
|
||||
</EXTENSION>
|
||||
<DebugAllEnabled>false</DebugAllEnabled>
|
||||
<RunAsTest>false</RunAsTest>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
@ -7,4 +7,10 @@ dependencies {
|
||||
|
||||
// OpenFeign for external API calls (사업자번호 검증)
|
||||
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
|
||||
|
||||
// H2 Database for development
|
||||
runtimeOnly 'com.h2database:h2'
|
||||
|
||||
// PostgreSQL Database for production
|
||||
runtimeOnly 'org.postgresql:postgresql'
|
||||
}
|
||||
|
||||
@ -0,0 +1,30 @@
|
||||
package com.kt.event.user;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.autoconfigure.domain.EntityScan;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
||||
|
||||
/**
|
||||
* User Service Application
|
||||
*
|
||||
* KT AI 기반 소상공인 이벤트 자동 생성 서비스 - User Service
|
||||
*
|
||||
* @author Backend Developer
|
||||
* @since 1.0
|
||||
*/
|
||||
@SpringBootApplication(scanBasePackages = {
|
||||
"com.kt.event.user",
|
||||
"com.kt.event.common"
|
||||
})
|
||||
@EntityScan(basePackages = {
|
||||
"com.kt.event.user.entity",
|
||||
"com.kt.event.common.entity"
|
||||
})
|
||||
@EnableJpaAuditing
|
||||
public class UserServiceApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(UserServiceApplication.class, args);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
package com.kt.event.user.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
/**
|
||||
* 비동기 처리 설정
|
||||
*
|
||||
* @Async 어노테이션 활성화 및 스레드 풀 설정
|
||||
*
|
||||
* @author Backend Developer
|
||||
* @since 1.0
|
||||
*/
|
||||
@Configuration
|
||||
@EnableAsync
|
||||
public class AsyncConfig {
|
||||
|
||||
@Bean(name = "taskExecutor")
|
||||
public Executor taskExecutor() {
|
||||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||
executor.setCorePoolSize(2);
|
||||
executor.setMaxPoolSize(10);
|
||||
executor.setQueueCapacity(500);
|
||||
executor.setThreadNamePrefix("async-");
|
||||
executor.initialize();
|
||||
return executor;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,59 @@
|
||||
package com.kt.event.user.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
|
||||
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
|
||||
/**
|
||||
* Redis 설정
|
||||
*
|
||||
* Redis 연결 및 템플릿 설정
|
||||
*
|
||||
* @author Backend Developer
|
||||
* @since 1.0
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnProperty(name = "spring.data.redis.enabled", havingValue = "true", matchIfMissing = false)
|
||||
public class RedisConfig {
|
||||
|
||||
@Value("${spring.data.redis.host}")
|
||||
private String host;
|
||||
|
||||
@Value("${spring.data.redis.port}")
|
||||
private int port;
|
||||
|
||||
@Value("${spring.data.redis.password:}")
|
||||
private String password;
|
||||
|
||||
@Value("${spring.data.redis.database:0}")
|
||||
private int database;
|
||||
|
||||
@Bean
|
||||
public RedisConnectionFactory redisConnectionFactory() {
|
||||
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
|
||||
config.setHostName(host);
|
||||
config.setPort(port);
|
||||
config.setDatabase(database);
|
||||
if (password != null && !password.isEmpty()) {
|
||||
config.setPassword(password);
|
||||
}
|
||||
return new LettuceConnectionFactory(config);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RedisTemplate<String, Object> redisTemplate() {
|
||||
RedisTemplate<String, Object> template = new RedisTemplate<>();
|
||||
template.setConnectionFactory(redisConnectionFactory());
|
||||
template.setKeySerializer(new StringRedisSerializer());
|
||||
template.setValueSerializer(new StringRedisSerializer());
|
||||
template.setHashKeySerializer(new StringRedisSerializer());
|
||||
template.setHashValueSerializer(new StringRedisSerializer());
|
||||
return template;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,96 @@
|
||||
package com.kt.event.user.config;
|
||||
|
||||
import com.kt.event.common.security.JwtAuthenticationFilter;
|
||||
import com.kt.event.common.security.JwtTokenProvider;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Spring Security 설정
|
||||
*
|
||||
* JWT 기반 인증 및 API 보안 설정
|
||||
*
|
||||
* @author Backend Developer
|
||||
* @since 1.0
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@RequiredArgsConstructor
|
||||
public class SecurityConfig {
|
||||
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
|
||||
@Value("${cors.allowed-origins:http://localhost:*}")
|
||||
private String allowedOrigins;
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
return http
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
// Public endpoints
|
||||
.requestMatchers("/api/v1/users/register", "/api/v1/users/login").permitAll()
|
||||
// Actuator endpoints
|
||||
.requestMatchers("/actuator/**").permitAll()
|
||||
// Swagger UI endpoints
|
||||
.requestMatchers("/swagger-ui/**", "/swagger-ui.html", "/v3/api-docs/**", "/swagger-resources/**", "/webjars/**").permitAll()
|
||||
// Health check
|
||||
.requestMatchers("/health").permitAll()
|
||||
// All other requests require authentication
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider),
|
||||
UsernamePasswordAuthenticationFilter.class)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CorsConfigurationSource corsConfigurationSource() {
|
||||
CorsConfiguration configuration = new CorsConfiguration();
|
||||
|
||||
// 환경변수에서 허용할 Origin 패턴 설정
|
||||
String[] origins = allowedOrigins.split(",");
|
||||
configuration.setAllowedOriginPatterns(Arrays.asList(origins));
|
||||
|
||||
// 허용할 HTTP 메소드
|
||||
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));
|
||||
|
||||
// 허용할 헤더
|
||||
configuration.setAllowedHeaders(Arrays.asList(
|
||||
"Authorization", "Content-Type", "X-Requested-With", "Accept",
|
||||
"Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers"
|
||||
));
|
||||
|
||||
// 자격 증명 허용
|
||||
configuration.setAllowCredentials(true);
|
||||
|
||||
// Pre-flight 요청 캐시 시간
|
||||
configuration.setMaxAge(3600L);
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", configuration);
|
||||
return source;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,67 @@
|
||||
package com.kt.event.user.config;
|
||||
|
||||
import io.swagger.v3.oas.models.Components;
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Contact;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.models.security.SecurityScheme;
|
||||
import io.swagger.v3.oas.models.servers.Server;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Swagger/OpenAPI 설정
|
||||
*
|
||||
* User Service API 문서화를 위한 설정
|
||||
*
|
||||
* @author Backend Developer
|
||||
* @since 1.0
|
||||
*/
|
||||
@Configuration
|
||||
public class SwaggerConfig {
|
||||
|
||||
@Bean
|
||||
public OpenAPI openAPI() {
|
||||
return new OpenAPI()
|
||||
.info(apiInfo())
|
||||
.addServersItem(new Server()
|
||||
.url("http://localhost:8081")
|
||||
.description("Local Development"))
|
||||
.addServersItem(new Server()
|
||||
.url("{protocol}://{host}:{port}")
|
||||
.description("Custom Server")
|
||||
.variables(new io.swagger.v3.oas.models.servers.ServerVariables()
|
||||
.addServerVariable("protocol", new io.swagger.v3.oas.models.servers.ServerVariable()
|
||||
._default("http")
|
||||
.description("Protocol (http or https)")
|
||||
.addEnumItem("http")
|
||||
.addEnumItem("https"))
|
||||
.addServerVariable("host", new io.swagger.v3.oas.models.servers.ServerVariable()
|
||||
._default("localhost")
|
||||
.description("Server host"))
|
||||
.addServerVariable("port", new io.swagger.v3.oas.models.servers.ServerVariable()
|
||||
._default("8081")
|
||||
.description("Server port"))))
|
||||
.addSecurityItem(new SecurityRequirement().addList("Bearer Authentication"))
|
||||
.components(new Components()
|
||||
.addSecuritySchemes("Bearer Authentication", createAPIKeyScheme()));
|
||||
}
|
||||
|
||||
private Info apiInfo() {
|
||||
return new Info()
|
||||
.title("User Service API")
|
||||
.description("KT AI 기반 소상공인 이벤트 자동 생성 서비스 - User Service API")
|
||||
.version("1.0.0")
|
||||
.contact(new Contact()
|
||||
.name("Digital Garage Team")
|
||||
.email("support@kt-event-marketing.com"));
|
||||
}
|
||||
|
||||
private SecurityScheme createAPIKeyScheme() {
|
||||
return new SecurityScheme()
|
||||
.type(SecurityScheme.Type.HTTP)
|
||||
.bearerFormat("JWT")
|
||||
.scheme("bearer");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,132 @@
|
||||
package com.kt.event.user.controller;
|
||||
|
||||
import com.kt.event.common.security.UserPrincipal;
|
||||
import com.kt.event.user.dto.request.ChangePasswordRequest;
|
||||
import com.kt.event.user.dto.request.LoginRequest;
|
||||
import com.kt.event.user.dto.request.RegisterRequest;
|
||||
import com.kt.event.user.dto.request.UpdateProfileRequest;
|
||||
import com.kt.event.user.dto.response.LoginResponse;
|
||||
import com.kt.event.user.dto.response.LogoutResponse;
|
||||
import com.kt.event.user.dto.response.ProfileResponse;
|
||||
import com.kt.event.user.dto.response.RegisterResponse;
|
||||
import com.kt.event.user.service.AuthenticationService;
|
||||
import com.kt.event.user.service.UserService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* User Controller
|
||||
*
|
||||
* 사용자 인증 및 프로필 관리 API
|
||||
*
|
||||
* @author Backend Developer
|
||||
* @since 1.0
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/users")
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "User", description = "사용자 인증 및 프로필 관리 API")
|
||||
public class UserController {
|
||||
|
||||
private final UserService userService;
|
||||
private final AuthenticationService authenticationService;
|
||||
|
||||
/**
|
||||
* 회원가입
|
||||
*
|
||||
* UFR-USER-010: 회원가입
|
||||
*/
|
||||
@PostMapping("/register")
|
||||
@Operation(summary = "회원가입", description = "소상공인 회원가입 API")
|
||||
public ResponseEntity<RegisterResponse> register(@Valid @RequestBody RegisterRequest request) {
|
||||
log.info("회원가입 요청: phoneNumber={}, email={}", request.getPhoneNumber(), request.getEmail());
|
||||
RegisterResponse response = userService.register(request);
|
||||
log.info("회원가입 성공: userId={}", response.getUserId());
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 로그인
|
||||
*
|
||||
* UFR-USER-020: 로그인
|
||||
*/
|
||||
@PostMapping("/login")
|
||||
@Operation(summary = "로그인", description = "소상공인 로그인 API")
|
||||
public ResponseEntity<LoginResponse> login(@Valid @RequestBody LoginRequest request) {
|
||||
log.info("로그인 요청: email={}", request.getEmail());
|
||||
LoginResponse response = authenticationService.login(request);
|
||||
log.info("로그인 성공: userId={}", response.getUserId());
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 로그아웃
|
||||
*
|
||||
* UFR-USER-040: 로그아웃
|
||||
*/
|
||||
@PostMapping("/logout")
|
||||
@Operation(summary = "로그아웃", description = "로그아웃 API")
|
||||
public ResponseEntity<LogoutResponse> logout(@RequestHeader("Authorization") String authHeader) {
|
||||
String token = authHeader.substring(7); // "Bearer " 제거
|
||||
log.info("로그아웃 요청");
|
||||
LogoutResponse response = authenticationService.logout(token);
|
||||
log.info("로그아웃 성공");
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 프로필 조회
|
||||
*
|
||||
* UFR-USER-030: 프로필 관리
|
||||
*/
|
||||
@GetMapping("/profile")
|
||||
@Operation(summary = "프로필 조회", description = "사용자 프로필 조회 API")
|
||||
public ResponseEntity<ProfileResponse> getProfile(@AuthenticationPrincipal UserPrincipal principal) {
|
||||
Long userId = principal.getUserId();
|
||||
log.info("프로필 조회 요청: userId={}", userId);
|
||||
ProfileResponse response = userService.getProfile(userId);
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 프로필 수정
|
||||
*
|
||||
* UFR-USER-030: 프로필 관리
|
||||
*/
|
||||
@PutMapping("/profile")
|
||||
@Operation(summary = "프로필 수정", description = "사용자 프로필 수정 API")
|
||||
public ResponseEntity<ProfileResponse> updateProfile(
|
||||
@AuthenticationPrincipal UserPrincipal principal,
|
||||
@Valid @RequestBody UpdateProfileRequest request) {
|
||||
Long userId = principal.getUserId();
|
||||
log.info("프로필 수정 요청: userId={}", userId);
|
||||
ProfileResponse response = userService.updateProfile(userId, request);
|
||||
log.info("프로필 수정 성공: userId={}", userId);
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 비밀번호 변경
|
||||
*
|
||||
* UFR-USER-030: 프로필 관리
|
||||
*/
|
||||
@PutMapping("/password")
|
||||
@Operation(summary = "비밀번호 변경", description = "비밀번호 변경 API")
|
||||
public ResponseEntity<Void> changePassword(
|
||||
@AuthenticationPrincipal UserPrincipal principal,
|
||||
@Valid @RequestBody ChangePasswordRequest request) {
|
||||
Long userId = principal.getUserId();
|
||||
log.info("비밀번호 변경 요청: userId={}", userId);
|
||||
userService.changePassword(userId, request);
|
||||
log.info("비밀번호 변경 성공: userId={}", userId);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
package com.kt.event.user.dto.request;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 비밀번호 변경 요청 DTO
|
||||
*
|
||||
* UFR-USER-030: 프로필 관리
|
||||
*
|
||||
* @author Backend Developer
|
||||
* @since 1.0
|
||||
*/
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class ChangePasswordRequest {
|
||||
|
||||
/**
|
||||
* 현재 비밀번호
|
||||
*/
|
||||
@NotBlank(message = "현재 비밀번호는 필수입니다")
|
||||
private String currentPassword;
|
||||
|
||||
/**
|
||||
* 새 비밀번호 (8자 이상)
|
||||
*/
|
||||
@NotBlank(message = "새 비밀번호는 필수입니다")
|
||||
@Size(min = 8, max = 100, message = "새 비밀번호는 8자 이상 100자 이하여야 합니다")
|
||||
private String newPassword;
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
package com.kt.event.user.dto.request;
|
||||
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 로그인 요청 DTO
|
||||
*
|
||||
* UFR-USER-020: 로그인
|
||||
*
|
||||
* @author Backend Developer
|
||||
* @since 1.0
|
||||
*/
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class LoginRequest {
|
||||
|
||||
/**
|
||||
* 이메일 주소
|
||||
*/
|
||||
@NotBlank(message = "이메일은 필수입니다")
|
||||
@Email(message = "이메일 형식이 올바르지 않습니다")
|
||||
@Size(max = 100, message = "이메일은 100자를 초과할 수 없습니다")
|
||||
private String email;
|
||||
|
||||
/**
|
||||
* 비밀번호
|
||||
*/
|
||||
@NotBlank(message = "비밀번호는 필수입니다")
|
||||
private String password;
|
||||
}
|
||||
@ -0,0 +1,80 @@
|
||||
package com.kt.event.user.dto.request;
|
||||
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 회원가입 요청 DTO
|
||||
*
|
||||
* UFR-USER-010: 회원가입
|
||||
*
|
||||
* @author Backend Developer
|
||||
* @since 1.0
|
||||
*/
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class RegisterRequest {
|
||||
|
||||
/**
|
||||
* 사용자 이름 (2자 이상)
|
||||
*/
|
||||
@NotBlank(message = "이름은 필수입니다")
|
||||
@Size(min = 2, max = 50, message = "이름은 2자 이상 50자 이하여야 합니다")
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 전화번호 (010XXXXXXXX 형식)
|
||||
*/
|
||||
@NotBlank(message = "전화번호는 필수입니다")
|
||||
@Pattern(regexp = "^010\\d{8}$", message = "전화번호는 010XXXXXXXX 형식이어야 합니다")
|
||||
private String phoneNumber;
|
||||
|
||||
/**
|
||||
* 이메일 주소
|
||||
*/
|
||||
@NotBlank(message = "이메일은 필수입니다")
|
||||
@Email(message = "이메일 형식이 올바르지 않습니다")
|
||||
@Size(max = 100, message = "이메일은 100자 이하여야 합니다")
|
||||
private String email;
|
||||
|
||||
/**
|
||||
* 비밀번호 (8자 이상)
|
||||
*/
|
||||
@NotBlank(message = "비밀번호는 필수입니다")
|
||||
@Size(min = 8, max = 100, message = "비밀번호는 8자 이상 100자 이하여야 합니다")
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* 매장명
|
||||
*/
|
||||
@NotBlank(message = "매장명은 필수입니다")
|
||||
@Size(max = 100, message = "매장명은 100자 이하여야 합니다")
|
||||
private String storeName;
|
||||
|
||||
/**
|
||||
* 업종
|
||||
*/
|
||||
@Size(max = 50, message = "업종은 50자 이하여야 합니다")
|
||||
private String industry;
|
||||
|
||||
/**
|
||||
* 주소
|
||||
*/
|
||||
@NotBlank(message = "주소는 필수입니다")
|
||||
@Size(max = 255, message = "주소는 255자 이하여야 합니다")
|
||||
private String address;
|
||||
|
||||
/**
|
||||
* 영업시간
|
||||
*/
|
||||
@Size(max = 255, message = "영업시간은 255자 이하여야 합니다")
|
||||
private String businessHours;
|
||||
}
|
||||
@ -0,0 +1,67 @@
|
||||
package com.kt.event.user.dto.request;
|
||||
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 프로필 수정 요청 DTO
|
||||
*
|
||||
* UFR-USER-030: 프로필 관리
|
||||
*
|
||||
* @author Backend Developer
|
||||
* @since 1.0
|
||||
*/
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class UpdateProfileRequest {
|
||||
|
||||
/**
|
||||
* 사용자 이름
|
||||
*/
|
||||
@Size(min = 2, max = 50, message = "이름은 2자 이상 50자 이하여야 합니다")
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 전화번호 (010XXXXXXXX 형식)
|
||||
*/
|
||||
@Pattern(regexp = "^010\\d{8}$", message = "전화번호는 010XXXXXXXX 형식이어야 합니다")
|
||||
private String phoneNumber;
|
||||
|
||||
/**
|
||||
* 이메일 주소
|
||||
*/
|
||||
@Email(message = "이메일 형식이 올바르지 않습니다")
|
||||
@Size(max = 100, message = "이메일은 100자 이하여야 합니다")
|
||||
private String email;
|
||||
|
||||
/**
|
||||
* 매장명
|
||||
*/
|
||||
@Size(max = 100, message = "매장명은 100자 이하여야 합니다")
|
||||
private String storeName;
|
||||
|
||||
/**
|
||||
* 업종
|
||||
*/
|
||||
@Size(max = 50, message = "업종은 50자 이하여야 합니다")
|
||||
private String industry;
|
||||
|
||||
/**
|
||||
* 주소
|
||||
*/
|
||||
@Size(max = 255, message = "주소는 255자 이하여야 합니다")
|
||||
private String address;
|
||||
|
||||
/**
|
||||
* 영업시간
|
||||
*/
|
||||
@Size(max = 255, message = "영업시간은 255자 이하여야 합니다")
|
||||
private String businessHours;
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
package com.kt.event.user.dto.response;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 로그인 응답 DTO
|
||||
*
|
||||
* UFR-USER-020: 로그인
|
||||
*
|
||||
* @author Backend Developer
|
||||
* @since 1.0
|
||||
*/
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class LoginResponse {
|
||||
|
||||
/**
|
||||
* JWT 토큰
|
||||
*/
|
||||
private String token;
|
||||
|
||||
/**
|
||||
* 사용자 ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 사용자 이름
|
||||
*/
|
||||
private String userName;
|
||||
|
||||
/**
|
||||
* 역할
|
||||
*/
|
||||
private String role;
|
||||
|
||||
/**
|
||||
* 이메일
|
||||
*/
|
||||
private String email;
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
package com.kt.event.user.dto.response;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 로그아웃 응답 DTO
|
||||
*
|
||||
* UFR-USER-040: 로그아웃
|
||||
*
|
||||
* @author Backend Developer
|
||||
* @since 1.0
|
||||
*/
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class LogoutResponse {
|
||||
|
||||
/**
|
||||
* 성공 여부
|
||||
*/
|
||||
private boolean success;
|
||||
|
||||
/**
|
||||
* 메시지
|
||||
*/
|
||||
private String message;
|
||||
}
|
||||
@ -0,0 +1,83 @@
|
||||
package com.kt.event.user.dto.response;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 프로필 응답 DTO
|
||||
*
|
||||
* UFR-USER-030: 프로필 관리
|
||||
*
|
||||
* @author Backend Developer
|
||||
* @since 1.0
|
||||
*/
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class ProfileResponse {
|
||||
|
||||
/**
|
||||
* 사용자 ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 사용자 이름
|
||||
*/
|
||||
private String userName;
|
||||
|
||||
/**
|
||||
* 전화번호
|
||||
*/
|
||||
private String phoneNumber;
|
||||
|
||||
/**
|
||||
* 이메일
|
||||
*/
|
||||
private String email;
|
||||
|
||||
/**
|
||||
* 역할
|
||||
*/
|
||||
private String role;
|
||||
|
||||
/**
|
||||
* 매장 ID
|
||||
*/
|
||||
private Long storeId;
|
||||
|
||||
/**
|
||||
* 매장명
|
||||
*/
|
||||
private String storeName;
|
||||
|
||||
/**
|
||||
* 업종
|
||||
*/
|
||||
private String industry;
|
||||
|
||||
/**
|
||||
* 주소
|
||||
*/
|
||||
private String address;
|
||||
|
||||
/**
|
||||
* 영업시간
|
||||
*/
|
||||
private String businessHours;
|
||||
|
||||
/**
|
||||
* 생성일시
|
||||
*/
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
/**
|
||||
* 최종 로그인 일시
|
||||
*/
|
||||
private LocalDateTime lastLoginAt;
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
package com.kt.event.user.dto.response;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 회원가입 응답 DTO
|
||||
*
|
||||
* UFR-USER-010: 회원가입
|
||||
*
|
||||
* @author Backend Developer
|
||||
* @since 1.0
|
||||
*/
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class RegisterResponse {
|
||||
|
||||
/**
|
||||
* JWT 토큰
|
||||
*/
|
||||
private String token;
|
||||
|
||||
/**
|
||||
* 사용자 ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 사용자 이름
|
||||
*/
|
||||
private String userName;
|
||||
|
||||
/**
|
||||
* 매장 ID
|
||||
*/
|
||||
private Long storeId;
|
||||
|
||||
/**
|
||||
* 매장명
|
||||
*/
|
||||
private String storeName;
|
||||
}
|
||||
@ -0,0 +1,93 @@
|
||||
package com.kt.event.user.entity;
|
||||
|
||||
import com.kt.event.common.entity.BaseTimeEntity;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
|
||||
/**
|
||||
* 매장 엔티티
|
||||
*
|
||||
* 소상공인 매장 정보를 저장하는 엔티티
|
||||
*
|
||||
* @author Backend Developer
|
||||
* @since 1.0
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "stores")
|
||||
@Getter
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class Store extends BaseTimeEntity {
|
||||
|
||||
/**
|
||||
* 매장 ID (Primary Key)
|
||||
*/
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "store_id")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 매장명
|
||||
*/
|
||||
@Column(name = "name", nullable = false, length = 100)
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 업종
|
||||
*/
|
||||
@Column(name = "industry", length = 50)
|
||||
private String industry;
|
||||
|
||||
/**
|
||||
* 주소
|
||||
*/
|
||||
@Column(name = "address", nullable = false, length = 255)
|
||||
private String address;
|
||||
|
||||
/**
|
||||
* 영업시간
|
||||
*/
|
||||
@Column(name = "business_hours", length = 255)
|
||||
private String businessHours;
|
||||
|
||||
/**
|
||||
* 사용자 정보 (One-to-One)
|
||||
*/
|
||||
@OneToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "user_id", nullable = false)
|
||||
private User user;
|
||||
|
||||
/**
|
||||
* 사용자 연결 (내부용)
|
||||
*
|
||||
* @param user 사용자
|
||||
*/
|
||||
void setUser(User user) {
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
/**
|
||||
* 매장 정보 수정
|
||||
*
|
||||
* @param name 매장명
|
||||
* @param industry 업종
|
||||
* @param address 주소
|
||||
* @param businessHours 영업시간
|
||||
*/
|
||||
public void updateInfo(String name, String industry, String address, String businessHours) {
|
||||
if (name != null) {
|
||||
this.name = name;
|
||||
}
|
||||
if (industry != null) {
|
||||
this.industry = industry;
|
||||
}
|
||||
if (address != null) {
|
||||
this.address = address;
|
||||
}
|
||||
if (businessHours != null) {
|
||||
this.businessHours = businessHours;
|
||||
}
|
||||
}
|
||||
}
|
||||
174
user-service/src/main/java/com/kt/event/user/entity/User.java
Normal file
174
user-service/src/main/java/com/kt/event/user/entity/User.java
Normal file
@ -0,0 +1,174 @@
|
||||
package com.kt.event.user.entity;
|
||||
|
||||
import com.kt.event.common.entity.BaseTimeEntity;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 사용자 엔티티
|
||||
*
|
||||
* 소상공인 사용자 정보를 저장하는 엔티티
|
||||
*
|
||||
* @author Backend Developer
|
||||
* @since 1.0
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "users", indexes = {
|
||||
@Index(name = "idx_user_phone", columnList = "phone_number", unique = true),
|
||||
@Index(name = "idx_user_email", columnList = "email", unique = true)
|
||||
})
|
||||
@Getter
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class User extends BaseTimeEntity {
|
||||
|
||||
/**
|
||||
* 사용자 ID (Primary Key)
|
||||
*/
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "user_id")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 사용자 이름
|
||||
*/
|
||||
@Column(name = "name", nullable = false, length = 50)
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 전화번호 (로그인 ID로도 사용)
|
||||
*/
|
||||
@Column(name = "phone_number", nullable = false, unique = true, length = 20)
|
||||
private String phoneNumber;
|
||||
|
||||
/**
|
||||
* 이메일 주소
|
||||
*/
|
||||
@Column(name = "email", nullable = false, unique = true, length = 100)
|
||||
private String email;
|
||||
|
||||
/**
|
||||
* 비밀번호 (bcrypt 해시)
|
||||
*/
|
||||
@Column(name = "password_hash", nullable = false, length = 255)
|
||||
private String passwordHash;
|
||||
|
||||
/**
|
||||
* 사용자 역할 (기본값: OWNER)
|
||||
*/
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "role", nullable = false, length = 20)
|
||||
@Builder.Default
|
||||
private UserRole role = UserRole.OWNER;
|
||||
|
||||
/**
|
||||
* 계정 상태 (기본값: ACTIVE)
|
||||
*/
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "status", nullable = false, length = 20)
|
||||
@Builder.Default
|
||||
private UserStatus status = UserStatus.ACTIVE;
|
||||
|
||||
/**
|
||||
* 최종 로그인 일시
|
||||
*/
|
||||
@Column(name = "last_login_at")
|
||||
private LocalDateTime lastLoginAt;
|
||||
|
||||
/**
|
||||
* 매장 정보 (One-to-One)
|
||||
*/
|
||||
@OneToOne(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
|
||||
private Store store;
|
||||
|
||||
/**
|
||||
* 최종 로그인 시각 업데이트
|
||||
*/
|
||||
public void updateLastLoginAt() {
|
||||
this.lastLoginAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 비밀번호 변경
|
||||
*
|
||||
* @param newPasswordHash 새 비밀번호 해시
|
||||
*/
|
||||
public void changePassword(String newPasswordHash) {
|
||||
this.passwordHash = newPasswordHash;
|
||||
}
|
||||
|
||||
/**
|
||||
* 프로필 정보 수정
|
||||
*
|
||||
* @param name 이름
|
||||
* @param email 이메일
|
||||
* @param phoneNumber 전화번호
|
||||
*/
|
||||
public void updateProfile(String name, String email, String phoneNumber) {
|
||||
if (name != null) {
|
||||
this.name = name;
|
||||
}
|
||||
if (email != null) {
|
||||
this.email = email;
|
||||
}
|
||||
if (phoneNumber != null) {
|
||||
this.phoneNumber = phoneNumber;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 매장 정보 연결
|
||||
*
|
||||
* @param store 매장 정보
|
||||
*/
|
||||
public void setStore(Store store) {
|
||||
this.store = store;
|
||||
if (store != null) {
|
||||
store.setUser(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 역할 Enum
|
||||
*/
|
||||
public enum UserRole {
|
||||
/**
|
||||
* 매장 소유주
|
||||
*/
|
||||
OWNER,
|
||||
|
||||
/**
|
||||
* 시스템 관리자
|
||||
*/
|
||||
ADMIN
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 계정 상태 Enum
|
||||
*/
|
||||
public enum UserStatus {
|
||||
/**
|
||||
* 활성 상태
|
||||
*/
|
||||
ACTIVE,
|
||||
|
||||
/**
|
||||
* 비활성 상태
|
||||
*/
|
||||
INACTIVE,
|
||||
|
||||
/**
|
||||
* 잠금 상태 (보안상 이유)
|
||||
*/
|
||||
LOCKED,
|
||||
|
||||
/**
|
||||
* 탈퇴 상태
|
||||
*/
|
||||
WITHDRAWN
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
package com.kt.event.user.exception;
|
||||
|
||||
import com.kt.event.common.exception.ErrorCode;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* User Service 에러 코드
|
||||
*
|
||||
* Common 모듈의 ErrorCode enum을 사용
|
||||
* User Service에서 사용하는 에러 코드만 열거
|
||||
*
|
||||
* @author Backend Developer
|
||||
* @since 1.0
|
||||
*/
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum UserErrorCode {
|
||||
|
||||
// User 관련 에러 - Common ErrorCode 사용
|
||||
USER_DUPLICATE_EMAIL(ErrorCode.USER_001),
|
||||
USER_DUPLICATE_PHONE(ErrorCode.USER_001), // 중복 사용자로 처리
|
||||
USER_NOT_FOUND(ErrorCode.USER_003),
|
||||
|
||||
// Authentication 관련 에러 - Common ErrorCode 사용
|
||||
AUTH_FAILED(ErrorCode.AUTH_001),
|
||||
AUTH_INVALID_TOKEN(ErrorCode.AUTH_002),
|
||||
AUTH_TOKEN_EXPIRED(ErrorCode.AUTH_003),
|
||||
AUTH_UNAUTHORIZED(ErrorCode.AUTH_001),
|
||||
|
||||
// Password 관련 에러 - Common ErrorCode 사용
|
||||
PWD_INVALID_CURRENT(ErrorCode.USER_004),
|
||||
PWD_SAME_AS_CURRENT(ErrorCode.USER_004);
|
||||
|
||||
private final ErrorCode errorCode;
|
||||
|
||||
public String getCode() {
|
||||
return errorCode.getCode();
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return errorCode.getMessage();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
package com.kt.event.user.repository;
|
||||
|
||||
import com.kt.event.user.entity.Store;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 매장 Repository
|
||||
*
|
||||
* 매장 데이터 액세스 인터페이스
|
||||
*
|
||||
* @author Backend Developer
|
||||
* @since 1.0
|
||||
*/
|
||||
@Repository
|
||||
public interface StoreRepository extends JpaRepository<Store, Long> {
|
||||
|
||||
/**
|
||||
* 사용자 ID로 매장 조회
|
||||
*
|
||||
* @param userId 사용자 ID
|
||||
* @return 매장 Optional
|
||||
*/
|
||||
Optional<Store> findByUserId(Long userId);
|
||||
}
|
||||
@ -0,0 +1,65 @@
|
||||
package com.kt.event.user.repository;
|
||||
|
||||
import com.kt.event.user.entity.User;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 사용자 Repository
|
||||
*
|
||||
* 사용자 데이터 액세스 인터페이스
|
||||
*
|
||||
* @author Backend Developer
|
||||
* @since 1.0
|
||||
*/
|
||||
@Repository
|
||||
public interface UserRepository extends JpaRepository<User, Long> {
|
||||
|
||||
/**
|
||||
* 이메일로 사용자 조회
|
||||
*
|
||||
* @param email 이메일
|
||||
* @return 사용자 Optional
|
||||
*/
|
||||
Optional<User> findByEmail(String email);
|
||||
|
||||
/**
|
||||
* 전화번호로 사용자 조회
|
||||
*
|
||||
* @param phoneNumber 전화번호
|
||||
* @return 사용자 Optional
|
||||
*/
|
||||
Optional<User> findByPhoneNumber(String phoneNumber);
|
||||
|
||||
/**
|
||||
* 이메일 존재 여부 확인
|
||||
*
|
||||
* @param email 이메일
|
||||
* @return 존재 여부
|
||||
*/
|
||||
boolean existsByEmail(String email);
|
||||
|
||||
/**
|
||||
* 전화번호 존재 여부 확인
|
||||
*
|
||||
* @param phoneNumber 전화번호
|
||||
* @return 존재 여부
|
||||
*/
|
||||
boolean existsByPhoneNumber(String phoneNumber);
|
||||
|
||||
/**
|
||||
* 최종 로그인 시각 업데이트
|
||||
*
|
||||
* @param userId 사용자 ID
|
||||
* @param lastLoginAt 최종 로그인 시각
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE User u SET u.lastLoginAt = :lastLoginAt WHERE u.id = :userId")
|
||||
void updateLastLoginAt(@Param("userId") Long userId, @Param("lastLoginAt") LocalDateTime lastLoginAt);
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
package com.kt.event.user.service;
|
||||
|
||||
import com.kt.event.user.dto.request.LoginRequest;
|
||||
import com.kt.event.user.dto.response.LoginResponse;
|
||||
import com.kt.event.user.dto.response.LogoutResponse;
|
||||
|
||||
/**
|
||||
* Authentication Service Interface
|
||||
*
|
||||
* 인증 관련 비즈니스 로직 인터페이스
|
||||
*
|
||||
* @author Backend Developer
|
||||
* @since 1.0
|
||||
*/
|
||||
public interface AuthenticationService {
|
||||
|
||||
/**
|
||||
* 로그인
|
||||
*
|
||||
* @param request 로그인 요청
|
||||
* @return 로그인 응답
|
||||
*/
|
||||
LoginResponse login(LoginRequest request);
|
||||
|
||||
/**
|
||||
* 로그아웃
|
||||
*
|
||||
* @param token JWT 토큰
|
||||
* @return 로그아웃 응답
|
||||
*/
|
||||
LogoutResponse logout(String token);
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
package com.kt.event.user.service;
|
||||
|
||||
import com.kt.event.user.dto.request.ChangePasswordRequest;
|
||||
import com.kt.event.user.dto.request.UpdateProfileRequest;
|
||||
import com.kt.event.user.dto.request.RegisterRequest;
|
||||
import com.kt.event.user.dto.response.ProfileResponse;
|
||||
import com.kt.event.user.dto.response.RegisterResponse;
|
||||
|
||||
/**
|
||||
* User Service Interface
|
||||
*
|
||||
* 사용자 관리 비즈니스 로직 인터페이스
|
||||
*
|
||||
* @author Backend Developer
|
||||
* @since 1.0
|
||||
*/
|
||||
public interface UserService {
|
||||
|
||||
/**
|
||||
* 회원가입
|
||||
*
|
||||
* @param request 회원가입 요청
|
||||
* @return 회원가입 응답
|
||||
*/
|
||||
RegisterResponse register(RegisterRequest request);
|
||||
|
||||
/**
|
||||
* 프로필 조회
|
||||
*
|
||||
* @param userId 사용자 ID
|
||||
* @return 프로필 응답
|
||||
*/
|
||||
ProfileResponse getProfile(Long userId);
|
||||
|
||||
/**
|
||||
* 프로필 수정
|
||||
*
|
||||
* @param userId 사용자 ID
|
||||
* @param request 프로필 수정 요청
|
||||
* @return 프로필 응답
|
||||
*/
|
||||
ProfileResponse updateProfile(Long userId, UpdateProfileRequest request);
|
||||
|
||||
/**
|
||||
* 비밀번호 변경
|
||||
*
|
||||
* @param userId 사용자 ID
|
||||
* @param request 비밀번호 변경 요청
|
||||
*/
|
||||
void changePassword(Long userId, ChangePasswordRequest request);
|
||||
|
||||
/**
|
||||
* 최종 로그인 시각 업데이트 (비동기)
|
||||
*
|
||||
* @param userId 사용자 ID
|
||||
*/
|
||||
void updateLastLoginAt(Long userId);
|
||||
}
|
||||
@ -0,0 +1,147 @@
|
||||
package com.kt.event.user.service.impl;
|
||||
|
||||
import com.kt.event.common.exception.BusinessException;
|
||||
import com.kt.event.common.security.JwtTokenProvider;
|
||||
import com.kt.event.user.dto.request.LoginRequest;
|
||||
import com.kt.event.user.dto.response.LoginResponse;
|
||||
import com.kt.event.user.dto.response.LogoutResponse;
|
||||
import com.kt.event.user.entity.User;
|
||||
import com.kt.event.user.exception.UserErrorCode;
|
||||
import com.kt.event.user.repository.UserRepository;
|
||||
import com.kt.event.user.service.AuthenticationService;
|
||||
import com.kt.event.user.service.UserService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Authentication Service 구현체
|
||||
*
|
||||
* 인증 관련 비즈니스 로직 구현
|
||||
*
|
||||
* @author Backend Developer
|
||||
* @since 1.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@Transactional(readOnly = true)
|
||||
public class AuthenticationServiceImpl implements AuthenticationService {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
private final UserService userService;
|
||||
|
||||
@Autowired(required = false)
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
public AuthenticationServiceImpl(UserRepository userRepository,
|
||||
PasswordEncoder passwordEncoder,
|
||||
JwtTokenProvider jwtTokenProvider,
|
||||
UserService userService) {
|
||||
this.userRepository = userRepository;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
this.jwtTokenProvider = jwtTokenProvider;
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 로그인
|
||||
*
|
||||
* UFR-USER-020: 로그인
|
||||
*/
|
||||
@Override
|
||||
@Transactional(readOnly = false)
|
||||
public LoginResponse login(LoginRequest request) {
|
||||
// 1. 사용자 조회 (이메일 기반)
|
||||
User user = userRepository.findByEmail(request.getEmail())
|
||||
.orElseThrow(() -> new BusinessException(UserErrorCode.AUTH_FAILED.getErrorCode()));
|
||||
|
||||
// 2. 비밀번호 검증
|
||||
if (!passwordEncoder.matches(request.getPassword(), user.getPasswordHash())) {
|
||||
throw new BusinessException(UserErrorCode.AUTH_FAILED.getErrorCode());
|
||||
}
|
||||
|
||||
// 3. JWT 토큰 생성
|
||||
String token = jwtTokenProvider.createAccessToken(
|
||||
user.getId(),
|
||||
user.getEmail(),
|
||||
user.getName(),
|
||||
List.of(user.getRole().name())
|
||||
);
|
||||
|
||||
// 4. Redis 세션 저장 (TTL 7일)
|
||||
saveSession(token, user.getId(), user.getRole().name());
|
||||
|
||||
// 5. 최종 로그인 시각 업데이트 (비동기)
|
||||
userService.updateLastLoginAt(user.getId());
|
||||
|
||||
// 6. 응답 반환
|
||||
return LoginResponse.builder()
|
||||
.token(token)
|
||||
.userId(user.getId())
|
||||
.userName(user.getName())
|
||||
.role(user.getRole().name())
|
||||
.email(user.getEmail())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 로그아웃
|
||||
*
|
||||
* UFR-USER-040: 로그아웃
|
||||
*/
|
||||
@Override
|
||||
public LogoutResponse logout(String token) {
|
||||
// 1. JWT 토큰 검증
|
||||
if (!jwtTokenProvider.validateToken(token)) {
|
||||
throw new BusinessException(UserErrorCode.AUTH_INVALID_TOKEN.getErrorCode());
|
||||
}
|
||||
|
||||
// 2. Redis 세션 삭제 (Redis가 활성화된 경우에만)
|
||||
if (redisTemplate != null) {
|
||||
String sessionKey = "user:session:" + token;
|
||||
redisTemplate.delete(sessionKey);
|
||||
|
||||
// 3. JWT Blacklist 추가 (남은 만료 시간만큼 TTL 설정)
|
||||
String blacklistKey = "jwt:blacklist:" + token;
|
||||
long remainingTime = jwtTokenProvider.getExpirationFromToken(token).getTime() - System.currentTimeMillis();
|
||||
if (remainingTime > 0) {
|
||||
redisTemplate.opsForValue().set(blacklistKey, "true", remainingTime, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
log.debug("Redis session and blacklist updated for logout");
|
||||
} else {
|
||||
log.warn("Redis is disabled. Session not cleared from Redis.");
|
||||
}
|
||||
|
||||
// 4. 응답 반환
|
||||
return LogoutResponse.builder()
|
||||
.success(true)
|
||||
.message("안전하게 로그아웃되었습니다")
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Redis 세션 저장 (Redis가 활성화된 경우에만)
|
||||
*
|
||||
* @param token JWT 토큰
|
||||
* @param userId 사용자 ID
|
||||
* @param role 역할
|
||||
*/
|
||||
private void saveSession(String token, Long userId, String role) {
|
||||
if (redisTemplate != null) {
|
||||
String key = "user:session:" + token;
|
||||
String value = userId + ":" + role;
|
||||
redisTemplate.opsForValue().set(key, value, 7, TimeUnit.DAYS);
|
||||
log.debug("Redis session saved: userId={}", userId);
|
||||
} else {
|
||||
log.warn("Redis is disabled. Session not saved to Redis.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,236 @@
|
||||
package com.kt.event.user.service.impl;
|
||||
|
||||
import com.kt.event.common.exception.BusinessException;
|
||||
import com.kt.event.common.security.JwtTokenProvider;
|
||||
import com.kt.event.user.dto.request.ChangePasswordRequest;
|
||||
import com.kt.event.user.dto.request.RegisterRequest;
|
||||
import com.kt.event.user.dto.request.UpdateProfileRequest;
|
||||
import com.kt.event.user.dto.response.ProfileResponse;
|
||||
import com.kt.event.user.dto.response.RegisterResponse;
|
||||
import com.kt.event.user.entity.Store;
|
||||
import com.kt.event.user.entity.User;
|
||||
import com.kt.event.user.exception.UserErrorCode;
|
||||
import com.kt.event.user.repository.StoreRepository;
|
||||
import com.kt.event.user.repository.UserRepository;
|
||||
import com.kt.event.user.service.UserService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* User Service 구현체
|
||||
*
|
||||
* 사용자 관리 비즈니스 로직 구현
|
||||
*
|
||||
* @author Backend Developer
|
||||
* @since 1.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@Transactional(readOnly = true)
|
||||
public class UserServiceImpl implements UserService {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
private final StoreRepository storeRepository;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
|
||||
@Autowired(required = false)
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
public UserServiceImpl(UserRepository userRepository,
|
||||
StoreRepository storeRepository,
|
||||
PasswordEncoder passwordEncoder,
|
||||
JwtTokenProvider jwtTokenProvider) {
|
||||
this.userRepository = userRepository;
|
||||
this.storeRepository = storeRepository;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
this.jwtTokenProvider = jwtTokenProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* 회원가입
|
||||
*
|
||||
* UFR-USER-010: 회원가입
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public RegisterResponse register(RegisterRequest request) {
|
||||
// 1. 이메일 중복 확인
|
||||
if (userRepository.existsByEmail(request.getEmail())) {
|
||||
throw new BusinessException(UserErrorCode.USER_DUPLICATE_EMAIL.getErrorCode());
|
||||
}
|
||||
|
||||
// 2. 전화번호 중복 확인
|
||||
if (userRepository.existsByPhoneNumber(request.getPhoneNumber())) {
|
||||
throw new BusinessException(UserErrorCode.USER_DUPLICATE_PHONE.getErrorCode());
|
||||
}
|
||||
|
||||
// 3. 비밀번호 해싱
|
||||
String passwordHash = passwordEncoder.encode(request.getPassword());
|
||||
|
||||
// 4. User 엔티티 생성 및 저장
|
||||
User user = User.builder()
|
||||
.name(request.getName())
|
||||
.phoneNumber(request.getPhoneNumber())
|
||||
.email(request.getEmail())
|
||||
.passwordHash(passwordHash)
|
||||
.role(User.UserRole.OWNER)
|
||||
.status(User.UserStatus.ACTIVE)
|
||||
.build();
|
||||
|
||||
User savedUser = userRepository.save(user);
|
||||
|
||||
// 5. Store 엔티티 생성 및 저장
|
||||
Store store = Store.builder()
|
||||
.name(request.getStoreName())
|
||||
.industry(request.getIndustry())
|
||||
.address(request.getAddress())
|
||||
.businessHours(request.getBusinessHours())
|
||||
.user(savedUser)
|
||||
.build();
|
||||
|
||||
Store savedStore = storeRepository.save(store);
|
||||
|
||||
// 6. JWT 토큰 생성
|
||||
String token = jwtTokenProvider.createAccessToken(
|
||||
savedUser.getId(),
|
||||
savedUser.getEmail(),
|
||||
savedUser.getName(),
|
||||
List.of(savedUser.getRole().name())
|
||||
);
|
||||
|
||||
// 7. Redis 세션 저장 (TTL 7일)
|
||||
saveSession(token, savedUser.getId(), savedUser.getRole().name());
|
||||
|
||||
// 8. 응답 반환
|
||||
return RegisterResponse.builder()
|
||||
.token(token)
|
||||
.userId(savedUser.getId())
|
||||
.userName(savedUser.getName())
|
||||
.storeId(savedStore.getId())
|
||||
.storeName(savedStore.getName())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 프로필 조회
|
||||
*
|
||||
* UFR-USER-030: 프로필 관리
|
||||
*/
|
||||
@Override
|
||||
public ProfileResponse getProfile(Long userId) {
|
||||
User user = userRepository.findById(userId)
|
||||
.orElseThrow(() -> new BusinessException(UserErrorCode.USER_NOT_FOUND.getErrorCode()));
|
||||
|
||||
Store store = storeRepository.findByUserId(userId)
|
||||
.orElse(null);
|
||||
|
||||
return ProfileResponse.builder()
|
||||
.userId(user.getId())
|
||||
.userName(user.getName())
|
||||
.phoneNumber(user.getPhoneNumber())
|
||||
.email(user.getEmail())
|
||||
.role(user.getRole().name())
|
||||
.storeId(store != null ? store.getId() : null)
|
||||
.storeName(store != null ? store.getName() : null)
|
||||
.industry(store != null ? store.getIndustry() : null)
|
||||
.address(store != null ? store.getAddress() : null)
|
||||
.businessHours(store != null ? store.getBusinessHours() : null)
|
||||
.createdAt(user.getCreatedAt())
|
||||
.lastLoginAt(user.getLastLoginAt())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 프로필 수정
|
||||
*
|
||||
* UFR-USER-030: 프로필 관리
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public ProfileResponse updateProfile(Long userId, UpdateProfileRequest request) {
|
||||
User user = userRepository.findById(userId)
|
||||
.orElseThrow(() -> new BusinessException(UserErrorCode.USER_NOT_FOUND.getErrorCode()));
|
||||
|
||||
// User 정보 수정
|
||||
user.updateProfile(request.getName(), request.getEmail(), request.getPhoneNumber());
|
||||
|
||||
// Store 정보 수정
|
||||
Store store = storeRepository.findByUserId(userId).orElse(null);
|
||||
if (store != null) {
|
||||
store.updateInfo(
|
||||
request.getStoreName(),
|
||||
request.getIndustry(),
|
||||
request.getAddress(),
|
||||
request.getBusinessHours()
|
||||
);
|
||||
}
|
||||
|
||||
return getProfile(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 비밀번호 변경
|
||||
*
|
||||
* UFR-USER-030: 프로필 관리
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public void changePassword(Long userId, ChangePasswordRequest request) {
|
||||
User user = userRepository.findById(userId)
|
||||
.orElseThrow(() -> new BusinessException(UserErrorCode.USER_NOT_FOUND.getErrorCode()));
|
||||
|
||||
// 현재 비밀번호 검증
|
||||
if (!passwordEncoder.matches(request.getCurrentPassword(), user.getPasswordHash())) {
|
||||
throw new BusinessException(UserErrorCode.PWD_INVALID_CURRENT.getErrorCode());
|
||||
}
|
||||
|
||||
// 새 비밀번호가 현재 비밀번호와 동일한지 확인
|
||||
if (passwordEncoder.matches(request.getNewPassword(), user.getPasswordHash())) {
|
||||
throw new BusinessException(UserErrorCode.PWD_SAME_AS_CURRENT.getErrorCode());
|
||||
}
|
||||
|
||||
// 새 비밀번호 해싱 및 저장
|
||||
String newPasswordHash = passwordEncoder.encode(request.getNewPassword());
|
||||
user.changePassword(newPasswordHash);
|
||||
}
|
||||
|
||||
/**
|
||||
* 최종 로그인 시각 업데이트 (비동기)
|
||||
*
|
||||
* UFR-USER-020: 로그인
|
||||
*/
|
||||
@Override
|
||||
@Async
|
||||
@Transactional
|
||||
public void updateLastLoginAt(Long userId) {
|
||||
userRepository.updateLastLoginAt(userId, LocalDateTime.now());
|
||||
}
|
||||
|
||||
/**
|
||||
* Redis 세션 저장 (Redis가 활성화된 경우에만)
|
||||
*
|
||||
* @param token JWT 토큰
|
||||
* @param userId 사용자 ID
|
||||
* @param role 역할
|
||||
*/
|
||||
private void saveSession(String token, Long userId, String role) {
|
||||
if (redisTemplate != null) {
|
||||
String key = "user:session:" + token;
|
||||
String value = userId + ":" + role;
|
||||
redisTemplate.opsForValue().set(key, value, 7, TimeUnit.DAYS);
|
||||
log.debug("Redis session saved: userId={}", userId);
|
||||
} else {
|
||||
log.warn("Redis is disabled. Session not saved to Redis.");
|
||||
}
|
||||
}
|
||||
}
|
||||
123
user-service/src/main/resources/application.yml
Normal file
123
user-service/src/main/resources/application.yml
Normal file
@ -0,0 +1,123 @@
|
||||
spring:
|
||||
application:
|
||||
name: user-service
|
||||
|
||||
# Database Configuration (PostgreSQL)
|
||||
datasource:
|
||||
url: ${DB_URL:jdbc:postgresql://20.249.125.115:5432/userdb}
|
||||
username: ${DB_USERNAME:eventuser}
|
||||
password: ${DB_PASSWORD:Hi5Jessica!}
|
||||
driver-class-name: ${DB_DRIVER:org.postgresql.Driver}
|
||||
hikari:
|
||||
maximum-pool-size: ${DB_POOL_MAX:20}
|
||||
minimum-idle: ${DB_POOL_MIN:5}
|
||||
connection-timeout: ${DB_CONN_TIMEOUT:30000}
|
||||
idle-timeout: ${DB_IDLE_TIMEOUT:600000}
|
||||
max-lifetime: ${DB_MAX_LIFETIME:1800000}
|
||||
leak-detection-threshold: ${DB_LEAK_THRESHOLD:60000}
|
||||
|
||||
# H2 Console (개발용 - PostgreSQL 사용 시 비활성화)
|
||||
h2:
|
||||
console:
|
||||
enabled: ${H2_CONSOLE_ENABLED:false}
|
||||
path: /h2-console
|
||||
|
||||
# JPA Configuration
|
||||
jpa:
|
||||
show-sql: ${SHOW_SQL:true}
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
use_sql_comments: true
|
||||
dialect: ${JPA_DIALECT:org.hibernate.dialect.PostgreSQLDialect}
|
||||
hibernate:
|
||||
ddl-auto: ${DDL_AUTO:update}
|
||||
|
||||
# Auto-configuration exclusions for development without external services
|
||||
autoconfigure:
|
||||
exclude:
|
||||
- ${EXCLUDE_KAFKA:}
|
||||
- ${EXCLUDE_REDIS:}
|
||||
|
||||
# Redis Configuration
|
||||
data:
|
||||
redis:
|
||||
enabled: ${REDIS_ENABLED:true}
|
||||
host: ${REDIS_HOST:20.214.210.71}
|
||||
port: ${REDIS_PORT:6379}
|
||||
password: ${REDIS_PASSWORD:Hi5Jessica!}
|
||||
timeout: ${REDIS_TIMEOUT:2000ms}
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: ${REDIS_POOL_MAX:8}
|
||||
max-idle: ${REDIS_POOL_IDLE:8}
|
||||
min-idle: ${REDIS_POOL_MIN:0}
|
||||
max-wait: ${REDIS_POOL_WAIT:-1ms}
|
||||
database: ${REDIS_DATABASE:0}
|
||||
|
||||
# Kafka Configuration
|
||||
kafka:
|
||||
bootstrap-servers: ${KAFKA_BOOTSTRAP_SERVERS:4.230.50.63:9092}
|
||||
consumer:
|
||||
group-id: ${KAFKA_CONSUMER_GROUP:user-service-consumers}
|
||||
auto-offset-reset: earliest
|
||||
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
|
||||
value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
|
||||
properties:
|
||||
spring.json.trusted.packages: "*"
|
||||
producer:
|
||||
key-serializer: org.apache.kafka.common.serialization.StringSerializer
|
||||
value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
|
||||
|
||||
# JWT Configuration
|
||||
jwt:
|
||||
secret: ${JWT_SECRET:kt-event-marketing-secret-key-for-development-only-please-change-in-production}
|
||||
access-token-validity: ${JWT_ACCESS_TOKEN_VALIDITY:604800000} # 7 days in milliseconds
|
||||
|
||||
# CORS Configuration
|
||||
cors:
|
||||
allowed-origins: ${CORS_ALLOWED_ORIGINS:http://localhost:*}
|
||||
|
||||
# Actuator
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,metrics,prometheus
|
||||
base-path: /actuator
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
show-components: always
|
||||
health:
|
||||
livenessState:
|
||||
enabled: true
|
||||
readinessState:
|
||||
enabled: true
|
||||
|
||||
# OpenAPI Documentation
|
||||
springdoc:
|
||||
api-docs:
|
||||
path: /v3/api-docs
|
||||
swagger-ui:
|
||||
path: /swagger-ui.html
|
||||
tags-sorter: alpha
|
||||
operations-sorter: alpha
|
||||
show-actuator: false
|
||||
|
||||
# Logging
|
||||
logging:
|
||||
level:
|
||||
com.kt.event.user: ${LOG_LEVEL_APP:DEBUG}
|
||||
org.springframework.web: ${LOG_LEVEL_WEB:INFO}
|
||||
org.hibernate.SQL: ${LOG_LEVEL_SQL:DEBUG}
|
||||
org.hibernate.type: ${LOG_LEVEL_SQL_TYPE:TRACE}
|
||||
pattern:
|
||||
console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n"
|
||||
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
|
||||
file:
|
||||
name: ${LOG_FILE_PATH:logs/user-service.log}
|
||||
|
||||
# Server Configuration
|
||||
server:
|
||||
port: ${SERVER_PORT:8081}
|
||||
Loading…
x
Reference in New Issue
Block a user