mirror of
https://github.com/won-ktds/smarketing-backend.git
synced 2026-01-21 11:06:23 +00:00
add: init project
This commit is contained in:
parent
b68c7c5fa1
commit
e6ef3f0671
@ -1,8 +0,0 @@
|
|||||||
tasks.getByName('bootJar') {
|
|
||||||
enabled = false
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.getByName('jar') {
|
|
||||||
enabled = true
|
|
||||||
archiveClassifier = ''
|
|
||||||
}
|
|
||||||
@ -1,10 +1,16 @@
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation project(':common')
|
implementation project(':common')
|
||||||
|
implementation 'com.mysql:mysql-connector-j'
|
||||||
|
|
||||||
// HTTP Client for external API
|
// HTTP Client for external API
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-webflux'
|
implementation 'org.springframework.boot:spring-boot-starter-webflux'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//external:
|
||||||
|
// ai:
|
||||||
|
// api-url: ${EXTERNAL_AI_URL:https://api.openai.com/v1}
|
||||||
|
// api-key: ${EXTERNAL_AI_KEY:your-api-key}
|
||||||
|
|
||||||
bootJar {
|
bootJar {
|
||||||
archiveFileName = "ai-recommend-service.jar"
|
archiveFileName = "ai-recommend-service.jar"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,11 +6,11 @@ import com.won.smarketing.recommend.presentation.dto.MarketingTipRequest;
|
|||||||
import com.won.smarketing.recommend.presentation.dto.MarketingTipResponse;
|
import com.won.smarketing.recommend.presentation.dto.MarketingTipResponse;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.validation.Valid;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import javax.validation.Valid;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AI 마케팅 추천을 위한 REST API 컨트롤러
|
* AI 마케팅 추천을 위한 REST API 컨트롤러
|
||||||
|
|||||||
@ -19,6 +19,12 @@ spring:
|
|||||||
dialect: org.hibernate.dialect.PostgreSQLDialect
|
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||||
format_sql: true
|
format_sql: true
|
||||||
|
|
||||||
|
ai:
|
||||||
|
service:
|
||||||
|
url: ${AI_SERVICE_URL:http://localhost:8080/ai}
|
||||||
|
timeout: ${AI_SERVICE_TIMEOUT:30000}
|
||||||
|
|
||||||
|
|
||||||
external:
|
external:
|
||||||
claude-ai:
|
claude-ai:
|
||||||
api-key: ${CLAUDE_AI_API_KEY:your-claude-api-key}
|
api-key: ${CLAUDE_AI_API_KEY:your-claude-api-key}
|
||||||
|
|||||||
52
build.gradle
52
build.gradle
@ -1,53 +1,5 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id 'org.springframework.boot' version '3.4.0' apply false
|
|
||||||
id 'io.spring.dependency-management' version '1.1.4' apply false
|
|
||||||
id 'java'
|
id 'java'
|
||||||
}
|
id 'org.springframework.boot' version '3.4.0'
|
||||||
|
id 'io.spring.dependency-management' version '1.1.4'
|
||||||
subprojects {
|
|
||||||
apply plugin: 'java'
|
|
||||||
apply plugin: 'org.springframework.boot'
|
|
||||||
apply plugin: 'io.spring.dependency-management'
|
|
||||||
|
|
||||||
group = 'com.won.smarketing'
|
|
||||||
version = '0.0.1-SNAPSHOT'
|
|
||||||
sourceCompatibility = '21'
|
|
||||||
|
|
||||||
repositories {
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
// Spring Boot Starters
|
|
||||||
implementation 'org.springframework.boot:spring-boot-starter'
|
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
|
|
||||||
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'
|
|
||||||
|
|
||||||
// Database
|
|
||||||
runtimeOnly 'org.postgresql:postgresql'
|
|
||||||
|
|
||||||
// Swagger
|
|
||||||
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.4.0'
|
|
||||||
|
|
||||||
// JWT
|
|
||||||
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
|
|
||||||
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
|
|
||||||
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
|
|
||||||
|
|
||||||
// Lombok
|
|
||||||
compileOnly 'org.projectlombok:lombok'
|
|
||||||
annotationProcessor 'org.projectlombok:lombok'
|
|
||||||
|
|
||||||
// Test
|
|
||||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
|
||||||
testImplementation 'org.springframework.security:spring-security-test'
|
|
||||||
}
|
|
||||||
|
|
||||||
test {
|
|
||||||
useJUnitPlatform()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
21
common/build.gradle
Normal file
21
common/build.gradle
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
dependencies {
|
||||||
|
api 'org.springframework.boot:spring-boot-starter-web'
|
||||||
|
api 'org.springframework.boot:spring-boot-starter-data-jpa'
|
||||||
|
api 'org.springframework.boot:spring-boot-starter-security'
|
||||||
|
api 'org.springframework.boot:spring-boot-starter-validation'
|
||||||
|
api 'org.springframework.boot:spring-boot-starter-data-redis'
|
||||||
|
api 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'
|
||||||
|
api 'io.jsonwebtoken:jjwt-api:0.12.3'
|
||||||
|
api 'io.jsonwebtoken:jjwt-impl:0.12.3'
|
||||||
|
api 'io.jsonwebtoken:jjwt-jackson:0.12.3'
|
||||||
|
api 'org.projectlombok:lombok'
|
||||||
|
}
|
||||||
|
|
||||||
|
jar {
|
||||||
|
enabled = true
|
||||||
|
archiveClassifier = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
bootJar {
|
||||||
|
enabled = false
|
||||||
|
}
|
||||||
@ -20,7 +20,7 @@ import java.util.Arrays;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Spring Security 설정 클래스
|
* Spring Security 설정 클래스
|
||||||
* 인증, 인가, CORS 등 보안 관련 설정
|
* JWT 기반 인증 및 CORS 설정
|
||||||
*/
|
*/
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
@ -30,17 +30,7 @@ public class SecurityConfig {
|
|||||||
private final JwtAuthenticationFilter jwtAuthenticationFilter;
|
private final JwtAuthenticationFilter jwtAuthenticationFilter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 패스워드 인코더 Bean 설정
|
* Spring Security 필터 체인 설정
|
||||||
*
|
|
||||||
* @return BCrypt 패스워드 인코더
|
|
||||||
*/
|
|
||||||
@Bean
|
|
||||||
public PasswordEncoder passwordEncoder() {
|
|
||||||
return new BCryptPasswordEncoder();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Security Filter Chain 설정
|
|
||||||
*
|
*
|
||||||
* @param http HttpSecurity 객체
|
* @param http HttpSecurity 객체
|
||||||
* @return SecurityFilterChain
|
* @return SecurityFilterChain
|
||||||
@ -49,42 +39,42 @@ public class SecurityConfig {
|
|||||||
@Bean
|
@Bean
|
||||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||||
http
|
http
|
||||||
.csrf(AbstractHttpConfigurer::disable)
|
.csrf(AbstractHttpConfigurer::disable)
|
||||||
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
||||||
.sessionManagement(session ->
|
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||||
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
.authorizeHttpRequests(auth -> auth
|
||||||
.authorizeHttpRequests(auth -> auth
|
.requestMatchers("/api/auth/**", "/api/member/register", "/api/member/check-duplicate/**",
|
||||||
.requestMatchers(
|
"/api/member/validate-password", "/swagger-ui/**", "/v3/api-docs/**",
|
||||||
"/api/member/register",
|
"/swagger-resources/**", "/webjars/**").permitAll()
|
||||||
"/api/member/check-duplicate",
|
.anyRequest().authenticated()
|
||||||
"/api/member/validate-password",
|
)
|
||||||
"/api/auth/login",
|
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||||
"/swagger-ui/**",
|
|
||||||
"/swagger-ui.html",
|
|
||||||
"/api-docs/**",
|
|
||||||
"/actuator/**"
|
|
||||||
).permitAll()
|
|
||||||
.anyRequest().authenticated()
|
|
||||||
)
|
|
||||||
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
|
||||||
|
|
||||||
return http.build();
|
return http.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 패스워드 인코더 빈 등록
|
||||||
|
*
|
||||||
|
* @return BCryptPasswordEncoder
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public PasswordEncoder passwordEncoder() {
|
||||||
|
return new BCryptPasswordEncoder();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CORS 설정
|
* CORS 설정
|
||||||
*
|
*
|
||||||
* @return CORS 설정 소스
|
* @return CorsConfigurationSource
|
||||||
*/
|
*/
|
||||||
@Bean
|
@Bean
|
||||||
public CorsConfigurationSource corsConfigurationSource() {
|
public CorsConfigurationSource corsConfigurationSource() {
|
||||||
CorsConfiguration configuration = new CorsConfiguration();
|
CorsConfiguration configuration = new CorsConfiguration();
|
||||||
configuration.setAllowedOriginPatterns(Arrays.asList("*"));
|
configuration.setAllowedOriginPatterns(Arrays.asList("*"));
|
||||||
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
|
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
|
||||||
configuration.setAllowedHeaders(Arrays.asList("authorization", "content-type", "x-auth-token"));
|
configuration.setAllowedHeaders(Arrays.asList("*"));
|
||||||
configuration.setExposedHeaders(Arrays.asList("x-auth-token"));
|
|
||||||
configuration.setAllowCredentials(true);
|
configuration.setAllowCredentials(true);
|
||||||
configuration.setMaxAge(3600L);
|
|
||||||
|
|
||||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||||
source.registerCorsConfiguration("/**", configuration);
|
source.registerCorsConfiguration("/**", configuration);
|
||||||
|
|||||||
@ -9,8 +9,8 @@ import org.springframework.context.annotation.Bean;
|
|||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Swagger 설정 클래스
|
* Swagger OpenAPI 설정 클래스
|
||||||
* API 문서화를 위한 OpenAPI 설정
|
* API 문서화 및 JWT 인증 설정
|
||||||
*/
|
*/
|
||||||
@Configuration
|
@Configuration
|
||||||
public class SwaggerConfig {
|
public class SwaggerConfig {
|
||||||
@ -18,24 +18,26 @@ public class SwaggerConfig {
|
|||||||
/**
|
/**
|
||||||
* OpenAPI 설정
|
* OpenAPI 설정
|
||||||
*
|
*
|
||||||
* @return OpenAPI 인스턴스
|
* @return OpenAPI 객체
|
||||||
*/
|
*/
|
||||||
@Bean
|
@Bean
|
||||||
public OpenAPI openAPI() {
|
public OpenAPI openAPI() {
|
||||||
String securitySchemeName = "bearerAuth";
|
String jwtSchemeName = "jwtAuth";
|
||||||
|
SecurityRequirement securityRequirement = new SecurityRequirement().addList(jwtSchemeName);
|
||||||
|
|
||||||
|
Components components = new Components()
|
||||||
|
.addSecuritySchemes(jwtSchemeName, new SecurityScheme()
|
||||||
|
.name(jwtSchemeName)
|
||||||
|
.type(SecurityScheme.Type.HTTP)
|
||||||
|
.scheme("bearer")
|
||||||
|
.bearerFormat("JWT"));
|
||||||
|
|
||||||
return new OpenAPI()
|
return new OpenAPI()
|
||||||
.info(new Info()
|
.info(new Info()
|
||||||
.title("AI 마케팅 서비스 API")
|
.title("스마케팅 API")
|
||||||
.description("소상공인을 위한 맞춤형 AI 마케팅 솔루션 API 문서")
|
.description("소상공인을 위한 AI 마케팅 서비스 API")
|
||||||
.version("v1.0.0"))
|
.version("1.0.0"))
|
||||||
.addSecurityItem(new SecurityRequirement().addList(securitySchemeName))
|
.addSecurityItem(securityRequirement)
|
||||||
.components(new Components()
|
.components(components);
|
||||||
.addSecuritySchemes(securitySchemeName,
|
|
||||||
new SecurityScheme()
|
|
||||||
.name(securitySchemeName)
|
|
||||||
.type(SecurityScheme.Type.HTTP)
|
|
||||||
.scheme("bearer")
|
|
||||||
.bearerFormat("JWT")));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,22 +9,22 @@ import lombok.NoArgsConstructor;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 페이지네이션 응답 DTO
|
* 페이징 응답 DTO
|
||||||
* 페이지 단위 조회 결과를 담는 공통 형식
|
* 페이징된 데이터 응답에 사용되는 공통 형식
|
||||||
*
|
*
|
||||||
* @param <T> 페이지 내용의 데이터 타입
|
* @param <T> 응답 데이터 타입
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@Builder
|
@Builder
|
||||||
@Schema(description = "페이지네이션 응답")
|
@Schema(description = "페이징 응답")
|
||||||
public class PageResponse<T> {
|
public class PageResponse<T> {
|
||||||
|
|
||||||
@Schema(description = "페이지 내용")
|
@Schema(description = "페이지 컨텐츠", example = "[...]")
|
||||||
private List<T> content;
|
private List<T> content;
|
||||||
|
|
||||||
@Schema(description = "현재 페이지 번호", example = "0")
|
@Schema(description = "페이지 번호 (0부터 시작)", example = "0")
|
||||||
private int pageNumber;
|
private int pageNumber;
|
||||||
|
|
||||||
@Schema(description = "페이지 크기", example = "20")
|
@Schema(description = "페이지 크기", example = "20")
|
||||||
@ -41,4 +41,28 @@ public class PageResponse<T> {
|
|||||||
|
|
||||||
@Schema(description = "마지막 페이지 여부", example = "false")
|
@Schema(description = "마지막 페이지 여부", example = "false")
|
||||||
private boolean last;
|
private boolean last;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 성공적인 페이징 응답 생성
|
||||||
|
*
|
||||||
|
* @param content 페이지 컨텐츠
|
||||||
|
* @param pageNumber 페이지 번호
|
||||||
|
* @param pageSize 페이지 크기
|
||||||
|
* @param totalElements 전체 요소 수
|
||||||
|
* @param <T> 데이터 타입
|
||||||
|
* @return 페이징 응답
|
||||||
|
*/
|
||||||
|
public static <T> PageResponse<T> of(List<T> content, int pageNumber, int pageSize, long totalElements) {
|
||||||
|
int totalPages = (int) Math.ceil((double) totalElements / pageSize);
|
||||||
|
|
||||||
|
return PageResponse.<T>builder()
|
||||||
|
.content(content)
|
||||||
|
.pageNumber(pageNumber)
|
||||||
|
.pageSize(pageSize)
|
||||||
|
.totalElements(totalElements)
|
||||||
|
.totalPages(totalPages)
|
||||||
|
.first(pageNumber == 0)
|
||||||
|
.last(pageNumber >= totalPages - 1)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,21 +2,18 @@ package com.won.smarketing.common.exception;
|
|||||||
|
|
||||||
import com.won.smarketing.common.dto.ApiResponse;
|
import com.won.smarketing.common.dto.ApiResponse;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.validation.BindException;
|
import org.springframework.validation.FieldError;
|
||||||
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
|
||||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||||
import org.springframework.web.bind.MissingServletRequestParameterException;
|
|
||||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
|
|
||||||
|
|
||||||
import java.nio.file.AccessDeniedException;
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 전역 예외 처리 핸들러
|
* 전역 예외 처리기
|
||||||
* 애플리케이션에서 발생하는 모든 예외를 처리하여 일관된 응답 형식 제공
|
* 애플리케이션 전반의 예외를 통일된 형식으로 처리
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RestControllerAdvice
|
@RestControllerAdvice
|
||||||
@ -31,121 +28,52 @@ public class GlobalExceptionHandler {
|
|||||||
@ExceptionHandler(BusinessException.class)
|
@ExceptionHandler(BusinessException.class)
|
||||||
public ResponseEntity<ApiResponse<Void>> handleBusinessException(BusinessException ex) {
|
public ResponseEntity<ApiResponse<Void>> handleBusinessException(BusinessException ex) {
|
||||||
log.warn("Business exception occurred: {}", ex.getMessage());
|
log.warn("Business exception occurred: {}", ex.getMessage());
|
||||||
ErrorCode errorCode = ex.getErrorCode();
|
|
||||||
return ResponseEntity
|
return ResponseEntity
|
||||||
.status(errorCode.getHttpStatus())
|
.status(ex.getErrorCode().getHttpStatus())
|
||||||
.body(ApiResponse.error(errorCode.getHttpStatus().value(), ex.getMessage()));
|
.body(ApiResponse.error(
|
||||||
|
ex.getErrorCode().getHttpStatus().value(),
|
||||||
|
ex.getMessage()
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 유효성 검증 예외 처리 (@Valid 애노테이션)
|
* 입력값 검증 예외 처리
|
||||||
*
|
*
|
||||||
* @param ex 유효성 검증 예외
|
* @param ex 입력값 검증 예외
|
||||||
* @return 오류 응답
|
* @return 오류 응답
|
||||||
*/
|
*/
|
||||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||||
public ResponseEntity<ApiResponse<Void>> handleValidationException(MethodArgumentNotValidException ex) {
|
public ResponseEntity<ApiResponse<Map<String, String>>> handleValidationException(
|
||||||
|
MethodArgumentNotValidException ex) {
|
||||||
log.warn("Validation exception occurred: {}", ex.getMessage());
|
log.warn("Validation exception occurred: {}", ex.getMessage());
|
||||||
String errorMessage = ex.getBindingResult()
|
|
||||||
.getFieldErrors()
|
|
||||||
.stream()
|
|
||||||
.findFirst()
|
|
||||||
.map(error -> error.getDefaultMessage())
|
|
||||||
.orElse("유효성 검증에 실패했습니다.");
|
|
||||||
|
|
||||||
return ResponseEntity
|
Map<String, String> errors = new HashMap<>();
|
||||||
.status(HttpStatus.BAD_REQUEST)
|
ex.getBindingResult().getAllErrors().forEach(error -> {
|
||||||
.body(ApiResponse.error(HttpStatus.BAD_REQUEST.value(), errorMessage));
|
String fieldName = ((FieldError) error).getField();
|
||||||
|
String errorMessage = error.getDefaultMessage();
|
||||||
|
errors.put(fieldName, errorMessage);
|
||||||
|
});
|
||||||
|
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(ApiResponse.<Map<String, String>>builder()
|
||||||
|
.status(400)
|
||||||
|
.message("입력값 검증에 실패했습니다.")
|
||||||
|
.data(errors)
|
||||||
|
.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 바인딩 예외 처리
|
* 일반적인 예외 처리
|
||||||
*
|
|
||||||
* @param ex 바인딩 예외
|
|
||||||
* @return 오류 응답
|
|
||||||
*/
|
|
||||||
@ExceptionHandler(BindException.class)
|
|
||||||
public ResponseEntity<ApiResponse<Void>> handleBindException(BindException ex) {
|
|
||||||
log.warn("Bind exception occurred: {}", ex.getMessage());
|
|
||||||
String errorMessage = ex.getBindingResult()
|
|
||||||
.getFieldErrors()
|
|
||||||
.stream()
|
|
||||||
.findFirst()
|
|
||||||
.map(error -> error.getDefaultMessage())
|
|
||||||
.orElse("요청 데이터 바인딩에 실패했습니다.");
|
|
||||||
|
|
||||||
return ResponseEntity
|
|
||||||
.status(HttpStatus.BAD_REQUEST)
|
|
||||||
.body(ApiResponse.error(HttpStatus.BAD_REQUEST.value(), errorMessage));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 타입 불일치 예외 처리
|
|
||||||
*
|
|
||||||
* @param ex 타입 불일치 예외
|
|
||||||
* @return 오류 응답
|
|
||||||
*/
|
|
||||||
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
|
|
||||||
public ResponseEntity<ApiResponse<Void>> handleTypeMismatchException(MethodArgumentTypeMismatchException ex) {
|
|
||||||
log.warn("Type mismatch exception occurred: {}", ex.getMessage());
|
|
||||||
return ResponseEntity
|
|
||||||
.status(HttpStatus.BAD_REQUEST)
|
|
||||||
.body(ApiResponse.error(HttpStatus.BAD_REQUEST.value(), "잘못된 타입의 값입니다."));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 필수 파라미터 누락 예외 처리
|
|
||||||
*
|
|
||||||
* @param ex 필수 파라미터 누락 예외
|
|
||||||
* @return 오류 응답
|
|
||||||
*/
|
|
||||||
@ExceptionHandler(MissingServletRequestParameterException.class)
|
|
||||||
public ResponseEntity<ApiResponse<Void>> handleMissingParameterException(MissingServletRequestParameterException ex) {
|
|
||||||
log.warn("Missing parameter exception occurred: {}", ex.getMessage());
|
|
||||||
return ResponseEntity
|
|
||||||
.status(HttpStatus.BAD_REQUEST)
|
|
||||||
.body(ApiResponse.error(HttpStatus.BAD_REQUEST.value(), "필수 요청 파라미터가 누락되었습니다: " + ex.getParameterName()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* HTTP 메서드 불일치 예외 처리
|
|
||||||
*
|
|
||||||
* @param ex HTTP 메서드 불일치 예외
|
|
||||||
* @return 오류 응답
|
|
||||||
*/
|
|
||||||
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
|
|
||||||
public ResponseEntity<ApiResponse<Void>> handleMethodNotSupportedException(HttpRequestMethodNotSupportedException ex) {
|
|
||||||
log.warn("Method not supported exception occurred: {}", ex.getMessage());
|
|
||||||
return ResponseEntity
|
|
||||||
.status(HttpStatus.METHOD_NOT_ALLOWED)
|
|
||||||
.body(ApiResponse.error(HttpStatus.METHOD_NOT_ALLOWED.value(), "허용되지 않은 HTTP 메서드입니다."));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 접근 거부 예외 처리
|
|
||||||
*
|
|
||||||
* @param ex 접근 거부 예외
|
|
||||||
* @return 오류 응답
|
|
||||||
*/
|
|
||||||
@ExceptionHandler(AccessDeniedException.class)
|
|
||||||
public ResponseEntity<ApiResponse<Void>> handleAccessDeniedException(AccessDeniedException ex) {
|
|
||||||
log.warn("Access denied exception occurred: {}", ex.getMessage());
|
|
||||||
return ResponseEntity
|
|
||||||
.status(HttpStatus.FORBIDDEN)
|
|
||||||
.body(ApiResponse.error(HttpStatus.FORBIDDEN.value(), "접근이 거부되었습니다."));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 기타 모든 예외 처리
|
|
||||||
*
|
*
|
||||||
* @param ex 예외
|
* @param ex 예외
|
||||||
* @return 오류 응답
|
* @return 오류 응답
|
||||||
*/
|
*/
|
||||||
@ExceptionHandler(Exception.class)
|
@ExceptionHandler(Exception.class)
|
||||||
public ResponseEntity<ApiResponse<Void>> handleGenericException(Exception ex) {
|
public ResponseEntity<ApiResponse<Void>> handleException(Exception ex) {
|
||||||
log.error("Unexpected exception occurred", ex);
|
log.error("Unexpected exception occurred", ex);
|
||||||
return ResponseEntity
|
|
||||||
.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
return ResponseEntity.internalServerError()
|
||||||
.body(ApiResponse.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), "서버 내부 오류가 발생했습니다."));
|
.body(ApiResponse.error(500, "서버 내부 오류가 발생했습니다."));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,8 +7,8 @@ import jakarta.servlet.http.HttpServletResponse;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
import org.springframework.web.filter.OncePerRequestFilter;
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
@ -18,7 +18,7 @@ import java.util.Collections;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* JWT 인증 필터
|
* JWT 인증 필터
|
||||||
* 요청 헤더에서 JWT 토큰을 추출하여 인증 처리
|
* HTTP 요청에서 JWT 토큰을 추출하고 인증 처리
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
@ -30,44 +30,53 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
private static final String BEARER_PREFIX = "Bearer ";
|
private static final String BEARER_PREFIX = "Bearer ";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JWT 토큰 인증 처리
|
* JWT 토큰 기반 인증 필터링
|
||||||
*
|
*
|
||||||
* @param request HTTP 요청
|
* @param request HTTP 요청
|
||||||
* @param response HTTP 응답
|
* @param response HTTP 응답
|
||||||
* @param filterChain 필터 체인
|
* @param filterChain 필터 체인
|
||||||
* @throws ServletException 서블릿 예외
|
* @throws ServletException 서블릿 예외
|
||||||
* @throws IOException I/O 예외
|
* @throws IOException IO 예외
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
|
protected void doFilterInternal(HttpServletRequest request,
|
||||||
|
HttpServletResponse response,
|
||||||
FilterChain filterChain) throws ServletException, IOException {
|
FilterChain filterChain) throws ServletException, IOException {
|
||||||
|
|
||||||
// 요청 헤더에서 JWT 토큰 추출
|
try {
|
||||||
String token = resolveToken(request);
|
String jwt = getJwtFromRequest(request);
|
||||||
|
|
||||||
// 토큰이 있고 유효한 경우 인증 정보 설정
|
if (StringUtils.hasText(jwt) && jwtTokenProvider.validateToken(jwt)) {
|
||||||
if (StringUtils.hasText(token) && jwtTokenProvider.validateToken(token)) {
|
String userId = jwtTokenProvider.getUserIdFromToken(jwt);
|
||||||
String userId = jwtTokenProvider.getUserIdFromToken(token);
|
|
||||||
Authentication authentication = new UsernamePasswordAuthenticationToken(
|
// 사용자 인증 정보 설정
|
||||||
userId, null, Collections.emptyList());
|
UsernamePasswordAuthenticationToken authentication =
|
||||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
new UsernamePasswordAuthenticationToken(userId, null, Collections.emptyList());
|
||||||
log.debug("Security context에 '{}' 인증 정보를 저장했습니다.", userId);
|
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||||
|
|
||||||
|
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||||
|
log.debug("User '{}' authenticated successfully", userId);
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.error("Could not set user authentication in security context", ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
filterChain.doFilter(request, response);
|
filterChain.doFilter(request, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 요청 헤더에서 JWT 토큰 추출
|
* HTTP 요청에서 JWT 토큰 추출
|
||||||
*
|
*
|
||||||
* @param request HTTP 요청
|
* @param request HTTP 요청
|
||||||
* @return JWT 토큰 (Bearer 접두사 제거)
|
* @return JWT 토큰 (Bearer 접두사 제거된)
|
||||||
*/
|
*/
|
||||||
private String resolveToken(HttpServletRequest request) {
|
private String getJwtFromRequest(HttpServletRequest request) {
|
||||||
String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
|
String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
|
||||||
|
|
||||||
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
|
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
|
||||||
return bearerToken.substring(BEARER_PREFIX.length());
|
return bearerToken.substring(BEARER_PREFIX.length());
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,6 @@
|
|||||||
package com.won.smarketing.common.security;
|
package com.won.smarketing.common.security;
|
||||||
|
|
||||||
import com.won.smarketing.common.exception.BusinessException;
|
|
||||||
import com.won.smarketing.common.exception.ErrorCode;
|
|
||||||
import io.jsonwebtoken.*;
|
import io.jsonwebtoken.*;
|
||||||
import io.jsonwebtoken.io.Decoders;
|
|
||||||
import io.jsonwebtoken.security.Keys;
|
import io.jsonwebtoken.security.Keys;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
@ -13,67 +10,65 @@ import javax.crypto.SecretKey;
|
|||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JWT 토큰 생성 및 검증 유틸리티
|
* JWT 토큰 생성 및 검증을 담당하는 클래스
|
||||||
* Access Token과 Refresh Token 관리
|
* 액세스 토큰과 리프레시 토큰의 생성, 검증, 파싱 기능 제공
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
public class JwtTokenProvider {
|
public class JwtTokenProvider {
|
||||||
|
|
||||||
private final SecretKey key;
|
private final SecretKey secretKey;
|
||||||
private final long accessTokenValidityTime;
|
private final long accessTokenValidityTime;
|
||||||
private final long refreshTokenValidityTime;
|
private final long refreshTokenValidityTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JWT 토큰 프로바이더 생성자
|
* JWT 토큰 프로바이더 생성자
|
||||||
*
|
*
|
||||||
* @param secretKey JWT 서명에 사용할 비밀키
|
* @param secret JWT 서명에 사용할 비밀키
|
||||||
* @param accessTokenValidityTime Access Token 유효 시간 (밀리초)
|
* @param accessTokenValidityTime 액세스 토큰 유효시간 (밀리초)
|
||||||
* @param refreshTokenValidityTime Refresh Token 유효 시간 (밀리초)
|
* @param refreshTokenValidityTime 리프레시 토큰 유효시간 (밀리초)
|
||||||
*/
|
*/
|
||||||
public JwtTokenProvider(
|
public JwtTokenProvider(@Value("${jwt.secret}") String secret,
|
||||||
@Value("${jwt.secret-key}") String secretKey,
|
@Value("${jwt.access-token-validity}") long accessTokenValidityTime,
|
||||||
@Value("${jwt.access-token-validity}") long accessTokenValidityTime,
|
@Value("${jwt.refresh-token-validity}") long refreshTokenValidityTime) {
|
||||||
@Value("${jwt.refresh-token-validity}") long refreshTokenValidityTime) {
|
this.secretKey = Keys.hmacShaKeyFor(secret.getBytes());
|
||||||
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
|
|
||||||
this.key = Keys.hmacShaKeyFor(keyBytes);
|
|
||||||
this.accessTokenValidityTime = accessTokenValidityTime;
|
this.accessTokenValidityTime = accessTokenValidityTime;
|
||||||
this.refreshTokenValidityTime = refreshTokenValidityTime;
|
this.refreshTokenValidityTime = refreshTokenValidityTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Access Token 생성
|
* 액세스 토큰 생성
|
||||||
*
|
*
|
||||||
* @param userId 사용자 ID
|
* @param userId 사용자 ID
|
||||||
* @return Access Token
|
* @return 생성된 액세스 토큰
|
||||||
*/
|
*/
|
||||||
public String generateAccessToken(String userId) {
|
public String generateAccessToken(String userId) {
|
||||||
long now = System.currentTimeMillis();
|
Date now = new Date();
|
||||||
Date validity = new Date(now + accessTokenValidityTime);
|
Date expiryDate = new Date(now.getTime() + accessTokenValidityTime);
|
||||||
|
|
||||||
return Jwts.builder()
|
return Jwts.builder()
|
||||||
.setSubject(userId)
|
.setSubject(userId)
|
||||||
.setIssuedAt(new Date(now))
|
.setIssuedAt(now)
|
||||||
.setExpiration(validity)
|
.setExpiration(expiryDate)
|
||||||
.signWith(key, SignatureAlgorithm.HS256)
|
.signWith(secretKey)
|
||||||
.compact();
|
.compact();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Refresh Token 생성
|
* 리프레시 토큰 생성
|
||||||
*
|
*
|
||||||
* @param userId 사용자 ID
|
* @param userId 사용자 ID
|
||||||
* @return Refresh Token
|
* @return 생성된 리프레시 토큰
|
||||||
*/
|
*/
|
||||||
public String generateRefreshToken(String userId) {
|
public String generateRefreshToken(String userId) {
|
||||||
long now = System.currentTimeMillis();
|
Date now = new Date();
|
||||||
Date validity = new Date(now + refreshTokenValidityTime);
|
Date expiryDate = new Date(now.getTime() + refreshTokenValidityTime);
|
||||||
|
|
||||||
return Jwts.builder()
|
return Jwts.builder()
|
||||||
.setSubject(userId)
|
.setSubject(userId)
|
||||||
.setIssuedAt(new Date(now))
|
.setIssuedAt(now)
|
||||||
.setExpiration(validity)
|
.setExpiration(expiryDate)
|
||||||
.signWith(key, SignatureAlgorithm.HS256)
|
.signWith(secretKey)
|
||||||
.compact();
|
.compact();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,67 +79,48 @@ public class JwtTokenProvider {
|
|||||||
* @return 사용자 ID
|
* @return 사용자 ID
|
||||||
*/
|
*/
|
||||||
public String getUserIdFromToken(String token) {
|
public String getUserIdFromToken(String token) {
|
||||||
Claims claims = parseClaims(token);
|
Claims claims = Jwts.parserBuilder()
|
||||||
|
.setSigningKey(secretKey)
|
||||||
|
.build()
|
||||||
|
.parseClaimsJws(token)
|
||||||
|
.getBody();
|
||||||
|
|
||||||
return claims.getSubject();
|
return claims.getSubject();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 토큰 유효성 검증
|
* 토큰 유효성 검증
|
||||||
*
|
*
|
||||||
* @param token JWT 토큰
|
* @param token 검증할 토큰
|
||||||
* @return 유효성 여부
|
* @return 유효성 여부
|
||||||
*/
|
*/
|
||||||
public boolean validateToken(String token) {
|
public boolean validateToken(String token) {
|
||||||
try {
|
try {
|
||||||
parseClaims(token);
|
Jwts.parserBuilder()
|
||||||
|
.setSigningKey(secretKey)
|
||||||
|
.build()
|
||||||
|
.parseClaimsJws(token);
|
||||||
return true;
|
return true;
|
||||||
} catch (ExpiredJwtException e) {
|
} catch (SecurityException ex) {
|
||||||
log.warn("Expired JWT token: {}", e.getMessage());
|
log.error("Invalid JWT signature: {}", ex.getMessage());
|
||||||
throw new BusinessException(ErrorCode.TOKEN_EXPIRED);
|
} catch (MalformedJwtException ex) {
|
||||||
} catch (UnsupportedJwtException e) {
|
log.error("Invalid JWT token: {}", ex.getMessage());
|
||||||
log.warn("Unsupported JWT token: {}", e.getMessage());
|
} catch (ExpiredJwtException ex) {
|
||||||
throw new BusinessException(ErrorCode.INVALID_TOKEN);
|
log.error("Expired JWT token: {}", ex.getMessage());
|
||||||
} catch (MalformedJwtException e) {
|
} catch (UnsupportedJwtException ex) {
|
||||||
log.warn("Malformed JWT token: {}", e.getMessage());
|
log.error("Unsupported JWT token: {}", ex.getMessage());
|
||||||
throw new BusinessException(ErrorCode.INVALID_TOKEN);
|
} catch (IllegalArgumentException ex) {
|
||||||
} catch (SecurityException e) {
|
log.error("JWT claims string is empty: {}", ex.getMessage());
|
||||||
log.warn("Invalid JWT signature: {}", e.getMessage());
|
|
||||||
throw new BusinessException(ErrorCode.INVALID_TOKEN);
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
log.warn("JWT token compact of handler are invalid: {}", e.getMessage());
|
|
||||||
throw new BusinessException(ErrorCode.INVALID_TOKEN);
|
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 토큰에서 Claims 추출
|
* 액세스 토큰 유효시간 반환
|
||||||
*
|
*
|
||||||
* @param token JWT 토큰
|
* @return 액세스 토큰 유효시간 (밀리초)
|
||||||
* @return Claims
|
|
||||||
*/
|
|
||||||
private Claims parseClaims(String token) {
|
|
||||||
return Jwts.parserBuilder()
|
|
||||||
.setSigningKey(key)
|
|
||||||
.build()
|
|
||||||
.parseClaimsJws(token)
|
|
||||||
.getBody();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Access Token 유효 시간 반환
|
|
||||||
*
|
|
||||||
* @return Access Token 유효 시간 (밀리초)
|
|
||||||
*/
|
*/
|
||||||
public long getAccessTokenValidityTime() {
|
public long getAccessTokenValidityTime() {
|
||||||
return accessTokenValidityTime;
|
return accessTokenValidityTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Refresh Token 유효 시간 반환
|
|
||||||
*
|
|
||||||
* @return Refresh Token 유효 시간 (밀리초)
|
|
||||||
*/
|
|
||||||
public long getRefreshTokenValidityTime() {
|
|
||||||
return refreshTokenValidityTime;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
7
gradle/wrapper/gradle-wrapper.properties
vendored
7
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,7 +0,0 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
|
||||||
distributionPath=wrapper/dists
|
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
|
||||||
networkTimeout=10000
|
|
||||||
validateDistributionUrl=true
|
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
|
||||||
zipStorePath=wrapper/dists
|
|
||||||
251
gradlew
vendored
251
gradlew
vendored
@ -1,251 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
#
|
|
||||||
# Copyright © 2015-2021 the original authors.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
#
|
|
||||||
|
|
||||||
##############################################################################
|
|
||||||
#
|
|
||||||
# Gradle start up script for POSIX generated by Gradle.
|
|
||||||
#
|
|
||||||
# Important for running:
|
|
||||||
#
|
|
||||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
|
||||||
# noncompliant, but you have some other compliant shell such as ksh or
|
|
||||||
# bash, then to run this script, type that shell name before the whole
|
|
||||||
# command line, like:
|
|
||||||
#
|
|
||||||
# ksh Gradle
|
|
||||||
#
|
|
||||||
# Busybox and similar reduced shells will NOT work, because this script
|
|
||||||
# requires all of these POSIX shell features:
|
|
||||||
# * functions;
|
|
||||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
|
||||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
|
||||||
# * compound commands having a testable exit status, especially «case»;
|
|
||||||
# * various built-in commands including «command», «set», and «ulimit».
|
|
||||||
#
|
|
||||||
# Important for patching:
|
|
||||||
#
|
|
||||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
|
||||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
|
||||||
#
|
|
||||||
# The "traditional" practice of packing multiple parameters into a
|
|
||||||
# space-separated string is a well documented source of bugs and security
|
|
||||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
|
||||||
# options in "$@", and eventually passing that to Java.
|
|
||||||
#
|
|
||||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
|
||||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
|
||||||
# see the in-line comments for details.
|
|
||||||
#
|
|
||||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
|
||||||
# Darwin, MinGW, and NonStop.
|
|
||||||
#
|
|
||||||
# (3) This script is generated from the Groovy template
|
|
||||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
|
||||||
# within the Gradle project.
|
|
||||||
#
|
|
||||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
|
||||||
#
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
# Attempt to set APP_HOME
|
|
||||||
|
|
||||||
# Resolve links: $0 may be a link
|
|
||||||
app_path=$0
|
|
||||||
|
|
||||||
# Need this for daisy-chained symlinks.
|
|
||||||
while
|
|
||||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
|
||||||
[ -h "$app_path" ]
|
|
||||||
do
|
|
||||||
ls=$( ls -ld "$app_path" )
|
|
||||||
link=${ls#*' -> '}
|
|
||||||
case $link in #(
|
|
||||||
/*) app_path=$link ;; #(
|
|
||||||
*) app_path=$APP_HOME$link ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
# This is normally unused
|
|
||||||
# shellcheck disable=SC2034
|
|
||||||
APP_BASE_NAME=${0##*/}
|
|
||||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
|
||||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
|
||||||
MAX_FD=maximum
|
|
||||||
|
|
||||||
warn () {
|
|
||||||
echo "$*"
|
|
||||||
} >&2
|
|
||||||
|
|
||||||
die () {
|
|
||||||
echo
|
|
||||||
echo "$*"
|
|
||||||
echo
|
|
||||||
exit 1
|
|
||||||
} >&2
|
|
||||||
|
|
||||||
# OS specific support (must be 'true' or 'false').
|
|
||||||
cygwin=false
|
|
||||||
msys=false
|
|
||||||
darwin=false
|
|
||||||
nonstop=false
|
|
||||||
case "$( uname )" in #(
|
|
||||||
CYGWIN* ) cygwin=true ;; #(
|
|
||||||
Darwin* ) darwin=true ;; #(
|
|
||||||
MSYS* | MINGW* ) msys=true ;; #(
|
|
||||||
NONSTOP* ) nonstop=true ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
|
||||||
|
|
||||||
|
|
||||||
# Determine the Java command to use to start the JVM.
|
|
||||||
if [ -n "$JAVA_HOME" ] ; then
|
|
||||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
|
||||||
# IBM's JDK on AIX uses strange locations for the executables
|
|
||||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
|
||||||
else
|
|
||||||
JAVACMD=$JAVA_HOME/bin/java
|
|
||||||
fi
|
|
||||||
if [ ! -x "$JAVACMD" ] ; then
|
|
||||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
|
||||||
|
|
||||||
Please set the JAVA_HOME variable in your environment to match the
|
|
||||||
location of your Java installation."
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
JAVACMD=java
|
|
||||||
if ! command -v java >/dev/null 2>&1
|
|
||||||
then
|
|
||||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
|
||||||
|
|
||||||
Please set the JAVA_HOME variable in your environment to match the
|
|
||||||
location of your Java installation."
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Increase the maximum file descriptors if we can.
|
|
||||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
|
||||||
case $MAX_FD in #(
|
|
||||||
max*)
|
|
||||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
|
||||||
# shellcheck disable=SC2039,SC3045
|
|
||||||
MAX_FD=$( ulimit -H -n ) ||
|
|
||||||
warn "Could not query maximum file descriptor limit"
|
|
||||||
esac
|
|
||||||
case $MAX_FD in #(
|
|
||||||
'' | soft) :;; #(
|
|
||||||
*)
|
|
||||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
|
||||||
# shellcheck disable=SC2039,SC3045
|
|
||||||
ulimit -n "$MAX_FD" ||
|
|
||||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Collect all arguments for the java command, stacking in reverse order:
|
|
||||||
# * args from the command line
|
|
||||||
# * the main class name
|
|
||||||
# * -classpath
|
|
||||||
# * -D...appname settings
|
|
||||||
# * --module-path (only if needed)
|
|
||||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
|
||||||
|
|
||||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
|
||||||
if "$cygwin" || "$msys" ; then
|
|
||||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
|
||||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
|
||||||
|
|
||||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
|
||||||
|
|
||||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
|
||||||
for arg do
|
|
||||||
if
|
|
||||||
case $arg in #(
|
|
||||||
-*) false ;; # don't mess with options #(
|
|
||||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
|
||||||
[ -e "$t" ] ;; #(
|
|
||||||
*) false ;;
|
|
||||||
esac
|
|
||||||
then
|
|
||||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
|
||||||
fi
|
|
||||||
# Roll the args list around exactly as many times as the number of
|
|
||||||
# args, so each arg winds up back in the position where it started, but
|
|
||||||
# possibly modified.
|
|
||||||
#
|
|
||||||
# NB: a `for` loop captures its iteration list before it begins, so
|
|
||||||
# changing the positional parameters here affects neither the number of
|
|
||||||
# iterations, nor the values presented in `arg`.
|
|
||||||
shift # remove old arg
|
|
||||||
set -- "$@" "$arg" # push replacement arg
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
|
||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
|
||||||
|
|
||||||
# Collect all arguments for the java command:
|
|
||||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
|
||||||
# and any embedded shellness will be escaped.
|
|
||||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
|
||||||
# treated as '${Hostname}' itself on the command line.
|
|
||||||
|
|
||||||
set -- \
|
|
||||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
|
||||||
-classpath "$CLASSPATH" \
|
|
||||||
org.gradle.wrapper.GradleWrapperMain \
|
|
||||||
"$@"
|
|
||||||
|
|
||||||
# Stop when "xargs" is not available.
|
|
||||||
if ! command -v xargs >/dev/null 2>&1
|
|
||||||
then
|
|
||||||
die "xargs is not available"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Use "xargs" to parse quoted args.
|
|
||||||
#
|
|
||||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
|
||||||
#
|
|
||||||
# In Bash we could simply go:
|
|
||||||
#
|
|
||||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
|
||||||
# set -- "${ARGS[@]}" "$@"
|
|
||||||
#
|
|
||||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
|
||||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
|
||||||
# character that might be a shell metacharacter, then use eval to reverse
|
|
||||||
# that process (while maintaining the separation between arguments), and wrap
|
|
||||||
# the whole thing up as a single "set" statement.
|
|
||||||
#
|
|
||||||
# This will of course break if any of these variables contains a newline or
|
|
||||||
# an unmatched quote.
|
|
||||||
#
|
|
||||||
|
|
||||||
eval "set -- $(
|
|
||||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
|
||||||
xargs -n1 |
|
|
||||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
|
||||||
tr '\n' ' '
|
|
||||||
)" '"$@"'
|
|
||||||
|
|
||||||
exec "$JAVACMD" "$@"
|
|
||||||
94
gradlew.bat
vendored
94
gradlew.bat
vendored
@ -1,94 +0,0 @@
|
|||||||
@rem
|
|
||||||
@rem Copyright 2015 the original author or authors.
|
|
||||||
@rem
|
|
||||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
@rem you may not use this file except in compliance with the License.
|
|
||||||
@rem You may obtain a copy of the License at
|
|
||||||
@rem
|
|
||||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
@rem
|
|
||||||
@rem Unless required by applicable law or agreed to in writing, software
|
|
||||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
@rem See the License for the specific language governing permissions and
|
|
||||||
@rem limitations under the License.
|
|
||||||
@rem
|
|
||||||
@rem SPDX-License-Identifier: Apache-2.0
|
|
||||||
@rem
|
|
||||||
|
|
||||||
@if "%DEBUG%"=="" @echo off
|
|
||||||
@rem ##########################################################################
|
|
||||||
@rem
|
|
||||||
@rem Gradle startup script for Windows
|
|
||||||
@rem
|
|
||||||
@rem ##########################################################################
|
|
||||||
|
|
||||||
@rem Set local scope for the variables with windows NT shell
|
|
||||||
if "%OS%"=="Windows_NT" setlocal
|
|
||||||
|
|
||||||
set DIRNAME=%~dp0
|
|
||||||
if "%DIRNAME%"=="" set DIRNAME=.
|
|
||||||
@rem This is normally unused
|
|
||||||
set APP_BASE_NAME=%~n0
|
|
||||||
set APP_HOME=%DIRNAME%
|
|
||||||
|
|
||||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
|
||||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
|
||||||
|
|
||||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
|
||||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
|
||||||
|
|
||||||
@rem Find java.exe
|
|
||||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
|
||||||
|
|
||||||
set JAVA_EXE=java.exe
|
|
||||||
%JAVA_EXE% -version >NUL 2>&1
|
|
||||||
if %ERRORLEVEL% equ 0 goto execute
|
|
||||||
|
|
||||||
echo. 1>&2
|
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
|
||||||
echo. 1>&2
|
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
|
||||||
echo location of your Java installation. 1>&2
|
|
||||||
|
|
||||||
goto fail
|
|
||||||
|
|
||||||
:findJavaFromJavaHome
|
|
||||||
set JAVA_HOME=%JAVA_HOME:"=%
|
|
||||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
|
||||||
|
|
||||||
if exist "%JAVA_EXE%" goto execute
|
|
||||||
|
|
||||||
echo. 1>&2
|
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
|
||||||
echo. 1>&2
|
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
|
||||||
echo location of your Java installation. 1>&2
|
|
||||||
|
|
||||||
goto fail
|
|
||||||
|
|
||||||
:execute
|
|
||||||
@rem Setup the command line
|
|
||||||
|
|
||||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
|
||||||
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
|
||||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
|
||||||
|
|
||||||
:end
|
|
||||||
@rem End local scope for the variables with windows NT shell
|
|
||||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
|
||||||
|
|
||||||
:fail
|
|
||||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
|
||||||
rem the _cmd.exe /c_ return code!
|
|
||||||
set EXIT_CODE=%ERRORLEVEL%
|
|
||||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
|
||||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
|
||||||
exit /b %EXIT_CODE%
|
|
||||||
|
|
||||||
:mainEnd
|
|
||||||
if "%OS%"=="Windows_NT" endlocal
|
|
||||||
|
|
||||||
:omega
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation project(':common')
|
implementation project(':common')
|
||||||
|
implementation 'com.mysql:mysql-connector-j'
|
||||||
// HTTP Client for external AI API
|
// HTTP Client for external AI API
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-webflux'
|
implementation 'org.springframework.boot:spring-boot-starter-webflux'
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,6 +18,10 @@ spring:
|
|||||||
hibernate:
|
hibernate:
|
||||||
dialect: org.hibernate.dialect.PostgreSQLDialect
|
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||||
format_sql: true
|
format_sql: true
|
||||||
|
ai:
|
||||||
|
service:
|
||||||
|
url: ${AI_SERVICE_URL:http://localhost:8080/ai}
|
||||||
|
timeout: ${AI_SERVICE_TIMEOUT:30000}
|
||||||
|
|
||||||
external:
|
external:
|
||||||
claude-ai:
|
claude-ai:
|
||||||
|
|||||||
@ -1,7 +1,4 @@
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation project(':common')
|
implementation project(':common')
|
||||||
}
|
implementation 'com.mysql:mysql-connector-j'
|
||||||
|
|
||||||
bootJar {
|
|
||||||
archiveFileName = "member-service.jar"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,13 @@
|
|||||||
|
package com.won.smarketing.member.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JPA 설정 클래스
|
||||||
|
* JPA Auditing 기능 활성화
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@EnableJpaAuditing
|
||||||
|
public class JpaConfig {
|
||||||
|
}
|
||||||
@ -0,0 +1,53 @@
|
|||||||
|
package com.won.smarketing.member.config;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
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.lettuce.LettuceConnectionFactory;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redis 설정 클래스
|
||||||
|
* Redis 연결 및 RedisTemplate 설정
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class RedisConfig {
|
||||||
|
|
||||||
|
@Value("${spring.data.redis.host}")
|
||||||
|
private String redisHost;
|
||||||
|
|
||||||
|
@Value("${spring.data.redis.port}")
|
||||||
|
private int redisPort;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redis 연결 팩토리 설정
|
||||||
|
*
|
||||||
|
* @return RedisConnectionFactory
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public RedisConnectionFactory redisConnectionFactory() {
|
||||||
|
return new LettuceConnectionFactory(redisHost, redisPort);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RedisTemplate 설정
|
||||||
|
*
|
||||||
|
* @return RedisTemplate<String, String>
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public RedisTemplate<String, String> redisTemplate() {
|
||||||
|
RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
|
||||||
|
redisTemplate.setConnectionFactory(redisConnectionFactory());
|
||||||
|
|
||||||
|
// 직렬화 설정
|
||||||
|
redisTemplate.setKeySerializer(new StringRedisSerializer());
|
||||||
|
redisTemplate.setValueSerializer(new StringRedisSerializer());
|
||||||
|
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
|
||||||
|
redisTemplate.setHashValueSerializer(new StringRedisSerializer());
|
||||||
|
|
||||||
|
return redisTemplate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -1,25 +1,21 @@
|
|||||||
package com.won.smarketing.member.controller;
|
package com.won.smarketing.member.controller;
|
||||||
|
|
||||||
import com.won.smarketing.common.dto.ApiResponse;
|
import com.won.smarketing.common.dto.ApiResponse;
|
||||||
import com.won.smarketing.member.dto.LoginRequest;
|
import com.won.smarketing.member.dto.*;
|
||||||
import com.won.smarketing.member.dto.LoginResponse;
|
|
||||||
import com.won.smarketing.member.dto.LogoutRequest;
|
|
||||||
import com.won.smarketing.member.dto.TokenRefreshRequest;
|
|
||||||
import com.won.smarketing.member.dto.TokenResponse;
|
|
||||||
import com.won.smarketing.member.service.AuthService;
|
import com.won.smarketing.member.service.AuthService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.validation.Valid;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 인증/인가를 위한 REST API 컨트롤러
|
* 인증을 위한 REST API 컨트롤러
|
||||||
* 로그인, 로그아웃, 토큰 갱신 기능 제공
|
* 로그인, 로그아웃, 토큰 갱신 기능 제공
|
||||||
*/
|
*/
|
||||||
@Tag(name = "인증/인가", description = "로그인, 로그아웃, 토큰 관리 API")
|
@Tag(name = "인증 관리", description = "로그인, 로그아웃, 토큰 관리 API")
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/auth")
|
@RequestMapping("/api/auth")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@ -28,12 +24,12 @@ public class AuthController {
|
|||||||
private final AuthService authService;
|
private final AuthService authService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 로그인 인증
|
* 로그인
|
||||||
*
|
*
|
||||||
* @param request 로그인 요청 정보
|
* @param request 로그인 요청 정보
|
||||||
* @return JWT 토큰 정보
|
* @return 로그인 성공 응답 (토큰 포함)
|
||||||
*/
|
*/
|
||||||
@Operation(summary = "로그인", description = "사용자 인증 후 JWT 토큰을 발급합니다.")
|
@Operation(summary = "로그인", description = "사용자 ID와 패스워드로 로그인합니다.")
|
||||||
@PostMapping("/login")
|
@PostMapping("/login")
|
||||||
public ResponseEntity<ApiResponse<LoginResponse>> login(@Valid @RequestBody LoginRequest request) {
|
public ResponseEntity<ApiResponse<LoginResponse>> login(@Valid @RequestBody LoginRequest request) {
|
||||||
LoginResponse response = authService.login(request);
|
LoginResponse response = authService.login(request);
|
||||||
@ -41,12 +37,12 @@ public class AuthController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 로그아웃 처리
|
* 로그아웃
|
||||||
*
|
*
|
||||||
* @param request 로그아웃 요청 정보
|
* @param request 로그아웃 요청 정보
|
||||||
* @return 로그아웃 성공 응답
|
* @return 로그아웃 성공 응답
|
||||||
*/
|
*/
|
||||||
@Operation(summary = "로그아웃", description = "사용자를 로그아웃하고 토큰을 무효화합니다.")
|
@Operation(summary = "로그아웃", description = "리프레시 토큰을 무효화하여 로그아웃합니다.")
|
||||||
@PostMapping("/logout")
|
@PostMapping("/logout")
|
||||||
public ResponseEntity<ApiResponse<Void>> logout(@Valid @RequestBody LogoutRequest request) {
|
public ResponseEntity<ApiResponse<Void>> logout(@Valid @RequestBody LogoutRequest request) {
|
||||||
authService.logout(request.getRefreshToken());
|
authService.logout(request.getRefreshToken());
|
||||||
@ -57,9 +53,9 @@ public class AuthController {
|
|||||||
* 토큰 갱신
|
* 토큰 갱신
|
||||||
*
|
*
|
||||||
* @param request 토큰 갱신 요청 정보
|
* @param request 토큰 갱신 요청 정보
|
||||||
* @return 새로운 JWT 토큰 정보
|
* @return 새로운 토큰 정보
|
||||||
*/
|
*/
|
||||||
@Operation(summary = "토큰 갱신", description = "Refresh Token을 사용하여 새로운 Access Token을 발급합니다.")
|
@Operation(summary = "토큰 갱신", description = "리프레시 토큰을 사용하여 새로운 액세스 토큰을 발급받습니다.")
|
||||||
@PostMapping("/refresh")
|
@PostMapping("/refresh")
|
||||||
public ResponseEntity<ApiResponse<TokenResponse>> refresh(@Valid @RequestBody TokenRefreshRequest request) {
|
public ResponseEntity<ApiResponse<TokenResponse>> refresh(@Valid @RequestBody TokenRefreshRequest request) {
|
||||||
TokenResponse response = authService.refresh(request.getRefreshToken());
|
TokenResponse response = authService.refresh(request.getRefreshToken());
|
||||||
|
|||||||
@ -9,15 +9,15 @@ import com.won.smarketing.member.service.MemberService;
|
|||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.validation.Valid;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 회원 관리를 위한 REST API 컨트롤러
|
* 회원 관리를 위한 REST API 컨트롤러
|
||||||
* 회원가입, ID 중복 확인, 패스워드 유효성 검증 기능 제공
|
* 회원가입, 중복 확인, 패스워드 검증 기능 제공
|
||||||
*/
|
*/
|
||||||
@Tag(name = "회원 관리", description = "회원가입 및 회원 정보 관리 API")
|
@Tag(name = "회원 관리", description = "회원가입 및 회원 정보 관리 API")
|
||||||
@RestController
|
@RestController
|
||||||
@ -28,10 +28,10 @@ public class MemberController {
|
|||||||
private final MemberService memberService;
|
private final MemberService memberService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 회원가입 처리
|
* 회원가입
|
||||||
*
|
*
|
||||||
* @param request 회원가입 요청 정보
|
* @param request 회원가입 요청 정보
|
||||||
* @return 회원가입 성공/실패 응답
|
* @return 회원가입 성공 응답
|
||||||
*/
|
*/
|
||||||
@Operation(summary = "회원가입", description = "새로운 회원을 등록합니다.")
|
@Operation(summary = "회원가입", description = "새로운 회원을 등록합니다.")
|
||||||
@PostMapping("/register")
|
@PostMapping("/register")
|
||||||
@ -41,34 +41,80 @@ public class MemberController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ID 중복 확인
|
* 사용자 ID 중복 확인
|
||||||
*
|
*
|
||||||
* @param userId 확인할 사용자 ID
|
* @param userId 확인할 사용자 ID
|
||||||
* @return 중복 여부 응답
|
* @return 중복 확인 결과
|
||||||
*/
|
*/
|
||||||
@Operation(summary = "ID 중복 확인", description = "사용자 ID의 중복 여부를 확인합니다.")
|
@Operation(summary = "사용자 ID 중복 확인", description = "사용자 ID의 중복 여부를 확인합니다.")
|
||||||
@GetMapping("/check-duplicate")
|
@GetMapping("/check-duplicate/user-id")
|
||||||
public ResponseEntity<ApiResponse<DuplicateCheckResponse>> checkDuplicate(
|
public ResponseEntity<ApiResponse<DuplicateCheckResponse>> checkUserIdDuplicate(
|
||||||
@Parameter(description = "확인할 사용자 ID", required = true)
|
@Parameter(description = "확인할 사용자 ID", required = true)
|
||||||
@RequestParam String userId) {
|
@RequestParam String userId) {
|
||||||
|
|
||||||
boolean isDuplicate = memberService.checkDuplicate(userId);
|
boolean isDuplicate = memberService.checkDuplicate(userId);
|
||||||
DuplicateCheckResponse response = DuplicateCheckResponse.builder()
|
DuplicateCheckResponse response = isDuplicate
|
||||||
.isDuplicate(isDuplicate)
|
? DuplicateCheckResponse.duplicate("이미 사용 중인 사용자 ID입니다.")
|
||||||
.message(isDuplicate ? "이미 사용 중인 ID입니다." : "사용 가능한 ID입니다.")
|
: DuplicateCheckResponse.available("사용 가능한 사용자 ID입니다.");
|
||||||
.build();
|
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이메일 중복 확인
|
||||||
|
*
|
||||||
|
* @param email 확인할 이메일
|
||||||
|
* @return 중복 확인 결과
|
||||||
|
*/
|
||||||
|
@Operation(summary = "이메일 중복 확인", description = "이메일의 중복 여부를 확인합니다.")
|
||||||
|
@GetMapping("/check-duplicate/email")
|
||||||
|
public ResponseEntity<ApiResponse<DuplicateCheckResponse>> checkEmailDuplicate(
|
||||||
|
@Parameter(description = "확인할 이메일", required = true)
|
||||||
|
@RequestParam String email) {
|
||||||
|
|
||||||
|
boolean isDuplicate = memberService.checkEmailDuplicate(email);
|
||||||
|
DuplicateCheckResponse response = isDuplicate
|
||||||
|
? DuplicateCheckResponse.duplicate("이미 사용 중인 이메일입니다.")
|
||||||
|
: DuplicateCheckResponse.available("사용 가능한 이메일입니다.");
|
||||||
|
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사업자번호 중복 확인
|
||||||
|
*
|
||||||
|
* @param businessNumber 확인할 사업자번호
|
||||||
|
* @return 중복 확인 결과
|
||||||
|
*/
|
||||||
|
@Operation(summary = "사업자번호 중복 확인", description = "사업자번호의 중복 여부를 확인합니다.")
|
||||||
|
@GetMapping("/check-duplicate/business-number")
|
||||||
|
public ResponseEntity<ApiResponse<DuplicateCheckResponse>> checkBusinessNumberDuplicate(
|
||||||
|
@Parameter(description = "확인할 사업자번호", required = true)
|
||||||
|
@RequestParam String businessNumber) {
|
||||||
|
|
||||||
|
boolean isDuplicate = memberService.checkBusinessNumberDuplicate(businessNumber);
|
||||||
|
DuplicateCheckResponse response = isDuplicate
|
||||||
|
? DuplicateCheckResponse.duplicate("이미 등록된 사업자번호입니다.")
|
||||||
|
: DuplicateCheckResponse.available("사용 가능한 사업자번호입니다.");
|
||||||
|
|
||||||
return ResponseEntity.ok(ApiResponse.success(response));
|
return ResponseEntity.ok(ApiResponse.success(response));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 패스워드 유효성 검증
|
* 패스워드 유효성 검증
|
||||||
*
|
*
|
||||||
* @param request 패스워드 유효성 검증 요청
|
* @param request 패스워드 검증 요청
|
||||||
* @return 유효성 검증 결과
|
* @return 패스워드 검증 결과
|
||||||
*/
|
*/
|
||||||
@Operation(summary = "패스워드 유효성 검증", description = "패스워드가 보안 규칙을 만족하는지 확인합니다.")
|
@Operation(summary = "패스워드 검증", description = "패스워드가 규칙을 만족하는지 확인합니다.")
|
||||||
@PostMapping("/validate-password")
|
@PostMapping("/validate-password")
|
||||||
public ResponseEntity<ApiResponse<ValidationResponse>> validatePassword(@Valid @RequestBody PasswordValidationRequest request) {
|
public ResponseEntity<ApiResponse<ValidationResponse>> validatePassword(
|
||||||
|
@Valid @RequestBody PasswordValidationRequest request) {
|
||||||
|
|
||||||
ValidationResponse response = memberService.validatePassword(request.getPassword());
|
ValidationResponse response = memberService.validatePassword(request.getPassword());
|
||||||
return ResponseEntity.ok(ApiResponse.success(response));
|
return ResponseEntity.ok(ApiResponse.success(response));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -7,19 +7,48 @@ import lombok.Data;
|
|||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ID 중복 확인 응답 DTO
|
* 중복 확인 응답 DTO
|
||||||
* 사용자 ID 중복 여부 확인 결과
|
* 사용자 ID, 이메일 등의 중복 확인 결과를 전달합니다.
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@Builder
|
@Builder
|
||||||
@Schema(description = "ID 중복 확인 응답")
|
@Schema(description = "중복 확인 응답")
|
||||||
public class DuplicateCheckResponse {
|
public class DuplicateCheckResponse {
|
||||||
|
|
||||||
@Schema(description = "중복 여부", example = "false")
|
@Schema(description = "중복 여부", example = "false")
|
||||||
private boolean isDuplicate;
|
private boolean isDuplicate;
|
||||||
|
|
||||||
@Schema(description = "확인 결과 메시지", example = "사용 가능한 ID입니다.")
|
@Schema(description = "메시지", example = "사용 가능한 ID입니다.")
|
||||||
private String message;
|
private String message;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 중복된 경우의 응답 생성
|
||||||
|
*
|
||||||
|
* @param message 메시지
|
||||||
|
* @return 중복 응답
|
||||||
|
*/
|
||||||
|
public static DuplicateCheckResponse duplicate(String message) {
|
||||||
|
return DuplicateCheckResponse.builder()
|
||||||
|
.isDuplicate(true)
|
||||||
|
.message(message)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사용 가능한 경우의 응답 생성
|
||||||
|
*
|
||||||
|
* @param message 메시지
|
||||||
|
* @return 사용 가능 응답
|
||||||
|
*/
|
||||||
|
public static DuplicateCheckResponse available(String message) {
|
||||||
|
return DuplicateCheckResponse.builder()
|
||||||
|
.isDuplicate(false)
|
||||||
|
.message(message)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,29 +1,26 @@
|
|||||||
package com.won.smarketing.member.dto;
|
package com.won.smarketing.member.dto;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
import javax.validation.constraints.NotBlank;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 로그인 요청 DTO
|
* 로그인 요청 DTO
|
||||||
* 로그인 시 필요한 사용자 ID와 패스워드 정보
|
* 로그인 시 필요한 정보를 전달합니다.
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@Builder
|
@Schema(description = "로그인 요청")
|
||||||
@Schema(description = "로그인 요청 정보")
|
|
||||||
public class LoginRequest {
|
public class LoginRequest {
|
||||||
|
|
||||||
@Schema(description = "사용자 ID", example = "testuser", required = true)
|
@Schema(description = "사용자 ID", example = "user123", required = true)
|
||||||
@NotBlank(message = "사용자 ID는 필수입니다.")
|
@NotBlank(message = "사용자 ID는 필수입니다")
|
||||||
private String userId;
|
private String userId;
|
||||||
|
|
||||||
@Schema(description = "패스워드", example = "password123!", required = true)
|
@Schema(description = "패스워드", example = "password123!", required = true)
|
||||||
@NotBlank(message = "패스워드는 필수입니다.")
|
@NotBlank(message = "패스워드는 필수입니다")
|
||||||
private String password;
|
private String password;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,21 +8,40 @@ import lombok.NoArgsConstructor;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 로그인 응답 DTO
|
* 로그인 응답 DTO
|
||||||
* 로그인 성공 시 반환되는 JWT 토큰 정보
|
* 로그인 성공 시 토큰 정보를 전달합니다.
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@Builder
|
@Builder
|
||||||
@Schema(description = "로그인 응답 정보")
|
@Schema(description = "로그인 응답")
|
||||||
public class LoginResponse {
|
public class LoginResponse {
|
||||||
|
|
||||||
@Schema(description = "Access Token", example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...")
|
@Schema(description = "액세스 토큰", example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...")
|
||||||
private String accessToken;
|
private String accessToken;
|
||||||
|
|
||||||
@Schema(description = "Refresh Token", example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...")
|
@Schema(description = "리프레시 토큰", example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...")
|
||||||
private String refreshToken;
|
private String refreshToken;
|
||||||
|
|
||||||
@Schema(description = "토큰 만료 시간 (밀리초)", example = "900000")
|
@Schema(description = "토큰 만료 시간 (초)", example = "3600")
|
||||||
private long expiresIn;
|
private long expiresIn;
|
||||||
|
|
||||||
|
@Schema(description = "사용자 정보")
|
||||||
|
private UserInfo userInfo;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
@Schema(description = "사용자 정보")
|
||||||
|
public static class UserInfo {
|
||||||
|
@Schema(description = "사용자 ID", example = "user123")
|
||||||
|
private String userId;
|
||||||
|
|
||||||
|
@Schema(description = "이름", example = "홍길동")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Schema(description = "이메일", example = "user@example.com")
|
||||||
|
private String email;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,25 +1,22 @@
|
|||||||
package com.won.smarketing.member.dto;
|
package com.won.smarketing.member.dto;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
import javax.validation.constraints.NotBlank;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 패스워드 유효성 검증 요청 DTO
|
* 패스워드 검증 요청 DTO
|
||||||
* 패스워드 보안 규칙 확인을 위한 요청 정보
|
* 패스워드 규칙 검증을 위한 요청 정보를 전달합니다.
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@Builder
|
@Schema(description = "패스워드 검증 요청")
|
||||||
@Schema(description = "패스워드 유효성 검증 요청")
|
|
||||||
public class PasswordValidationRequest {
|
public class PasswordValidationRequest {
|
||||||
|
|
||||||
@Schema(description = "검증할 패스워드", example = "password123!", required = true)
|
@Schema(description = "검증할 패스워드", example = "password123!", required = true)
|
||||||
@NotBlank(message = "패스워드는 필수입니다.")
|
@NotBlank(message = "패스워드는 필수입니다")
|
||||||
private String password;
|
private String password;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,49 +1,49 @@
|
|||||||
package com.won.smarketing.member.dto;
|
package com.won.smarketing.member.dto;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
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.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
import javax.validation.constraints.Email;
|
|
||||||
import javax.validation.constraints.NotBlank;
|
|
||||||
import javax.validation.constraints.Pattern;
|
|
||||||
import javax.validation.constraints.Size;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 회원가입 요청 DTO
|
* 회원가입 요청 DTO
|
||||||
* 회원가입 시 필요한 정보를 담는 데이터 전송 객체
|
* 회원가입 시 필요한 정보를 전달합니다.
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@Builder
|
@Schema(description = "회원가입 요청")
|
||||||
@Schema(description = "회원가입 요청 정보")
|
|
||||||
public class RegisterRequest {
|
public class RegisterRequest {
|
||||||
|
|
||||||
@Schema(description = "사용자 ID", example = "testuser", required = true)
|
@Schema(description = "사용자 ID", example = "user123", required = true)
|
||||||
@NotBlank(message = "사용자 ID는 필수입니다.")
|
@NotBlank(message = "사용자 ID는 필수입니다")
|
||||||
@Size(min = 4, max = 20, message = "사용자 ID는 4자 이상 20자 이하여야 합니다.")
|
@Size(min = 4, max = 20, message = "사용자 ID는 4-20자 사이여야 합니다")
|
||||||
@Pattern(regexp = "^[a-zA-Z0-9]+$", message = "사용자 ID는 영문과 숫자만 가능합니다.")
|
@Pattern(regexp = "^[a-zA-Z0-9]+$", message = "사용자 ID는 영문과 숫자만 사용 가능합니다")
|
||||||
private String userId;
|
private String userId;
|
||||||
|
|
||||||
@Schema(description = "패스워드", example = "password123!", required = true)
|
@Schema(description = "패스워드", example = "password123!", required = true)
|
||||||
@NotBlank(message = "패스워드는 필수입니다.")
|
@NotBlank(message = "패스워드는 필수입니다")
|
||||||
|
@Size(min = 8, max = 20, message = "패스워드는 8-20자 사이여야 합니다")
|
||||||
|
@Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]+$",
|
||||||
|
message = "패스워드는 영문, 숫자, 특수문자를 포함해야 합니다")
|
||||||
private String password;
|
private String password;
|
||||||
|
|
||||||
@Schema(description = "이름", example = "홍길동", required = true)
|
@Schema(description = "이름", example = "홍길동", required = true)
|
||||||
@NotBlank(message = "이름은 필수입니다.")
|
@NotBlank(message = "이름은 필수입니다")
|
||||||
@Size(max = 100, message = "이름은 100자 이하여야 합니다.")
|
@Size(max = 50, message = "이름은 50자 이하여야 합니다")
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
@Schema(description = "사업자 번호", example = "123-45-67890", required = true)
|
@Schema(description = "사업자등록번호", example = "123-45-67890")
|
||||||
@NotBlank(message = "사업자 번호는 필수입니다.")
|
@Pattern(regexp = "^\\d{3}-\\d{2}-\\d{5}$", message = "사업자등록번호 형식이 올바르지 않습니다 (000-00-00000)")
|
||||||
@Pattern(regexp = "^\\d{3}-\\d{2}-\\d{5}$", message = "사업자 번호 형식이 올바르지 않습니다.")
|
|
||||||
private String businessNumber;
|
private String businessNumber;
|
||||||
|
|
||||||
@Schema(description = "이메일", example = "test@example.com", required = true)
|
@Schema(description = "이메일", example = "user@example.com", required = true)
|
||||||
@NotBlank(message = "이메일은 필수입니다.")
|
@NotBlank(message = "이메일은 필수입니다")
|
||||||
@Email(message = "올바른 이메일 형식이 아닙니다.")
|
@Email(message = "이메일 형식이 올바르지 않습니다")
|
||||||
|
@Size(max = 100, message = "이메일은 100자 이하여야 합니다")
|
||||||
private String email;
|
private String email;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,25 +1,22 @@
|
|||||||
package com.won.smarketing.member.dto;
|
package com.won.smarketing.member.dto;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
import javax.validation.constraints.NotBlank;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 토큰 갱신 요청 DTO
|
* 토큰 갱신 요청 DTO
|
||||||
* Refresh Token을 사용한 토큰 갱신 요청 정보
|
* 리프레시 토큰을 사용한 액세스 토큰 갱신 요청 정보를 전달합니다.
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@Builder
|
|
||||||
@Schema(description = "토큰 갱신 요청")
|
@Schema(description = "토큰 갱신 요청")
|
||||||
public class TokenRefreshRequest {
|
public class TokenRefreshRequest {
|
||||||
|
|
||||||
@Schema(description = "Refresh Token", example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", required = true)
|
@Schema(description = "리프레시 토큰", example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", required = true)
|
||||||
@NotBlank(message = "Refresh Token은 필수입니다.")
|
@NotBlank(message = "리프레시 토큰은 필수입니다")
|
||||||
private String refreshToken;
|
private String refreshToken;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,21 +8,21 @@ import lombok.NoArgsConstructor;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 토큰 응답 DTO
|
* 토큰 응답 DTO
|
||||||
* 토큰 갱신 시 반환되는 새로운 JWT 토큰 정보
|
* 토큰 갱신 시 새로운 토큰 정보를 전달합니다.
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@Builder
|
@Builder
|
||||||
@Schema(description = "토큰 응답 정보")
|
@Schema(description = "토큰 응답")
|
||||||
public class TokenResponse {
|
public class TokenResponse {
|
||||||
|
|
||||||
@Schema(description = "새로운 Access Token", example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...")
|
@Schema(description = "새로운 액세스 토큰", example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...")
|
||||||
private String accessToken;
|
private String accessToken;
|
||||||
|
|
||||||
@Schema(description = "새로운 Refresh Token", example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...")
|
@Schema(description = "새로운 리프레시 토큰", example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...")
|
||||||
private String refreshToken;
|
private String refreshToken;
|
||||||
|
|
||||||
@Schema(description = "토큰 만료 시간 (밀리초)", example = "900000")
|
@Schema(description = "토큰 만료 시간 (초)", example = "3600")
|
||||||
private long expiresIn;
|
private long expiresIn;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,22 +9,50 @@ import lombok.NoArgsConstructor;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 유효성 검증 응답 DTO
|
* 검증 응답 DTO
|
||||||
* 패스워드 유효성 검증 결과 정보
|
* 패스워드 등의 검증 결과를 전달합니다.
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@Builder
|
@Builder
|
||||||
@Schema(description = "유효성 검증 응답")
|
@Schema(description = "검증 응답")
|
||||||
public class ValidationResponse {
|
public class ValidationResponse {
|
||||||
|
|
||||||
@Schema(description = "유효성 여부", example = "true")
|
@Schema(description = "유효성 여부", example = "true")
|
||||||
private boolean isValid;
|
private boolean isValid;
|
||||||
|
|
||||||
@Schema(description = "검증 결과 메시지", example = "유효한 패스워드입니다.")
|
@Schema(description = "메시지", example = "사용 가능한 패스워드입니다.")
|
||||||
private String message;
|
private String message;
|
||||||
|
|
||||||
@Schema(description = "오류 목록")
|
@Schema(description = "오류 목록", example = "[\"영문이 포함되어야 합니다\", \"숫자가 포함되어야 합니다\"]")
|
||||||
private List<String> errors;
|
private List<String> errors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 유효한 경우의 응답 생성
|
||||||
|
*
|
||||||
|
* @param message 메시지
|
||||||
|
* @return 유효 응답
|
||||||
|
*/
|
||||||
|
public static ValidationResponse valid(String message) {
|
||||||
|
return ValidationResponse.builder()
|
||||||
|
.isValid(true)
|
||||||
|
.message(message)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 유효하지 않은 경우의 응답 생성
|
||||||
|
*
|
||||||
|
* @param message 메시지
|
||||||
|
* @param errors 오류 목록
|
||||||
|
* @return 무효 응답
|
||||||
|
*/
|
||||||
|
public static ValidationResponse invalid(String message, List<String> errors) {
|
||||||
|
return ValidationResponse.builder()
|
||||||
|
.isValid(false)
|
||||||
|
.message(message)
|
||||||
|
.errors(errors)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,87 +1,29 @@
|
|||||||
package com.won.smarketing.member.entity;
|
package com.won.smarketing.member.entity;
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import lombok.*;
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
import java.time.LocalDateTime;
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstruct
|
||||||
/**
|
재시도
|
||||||
* 회원 정보를 나타내는 엔티티
|
Y
|
||||||
* 사용자 ID, 패스워드, 이름, 사업자 번호, 이메일 정보 저장
|
계속
|
||||||
*/
|
편집
|
||||||
@Entity
|
Member 서비스 모든 클래스 구현
|
||||||
@Table(name = "members")
|
코드 ∙ 버전 2
|
||||||
@Getter
|
/**
|
||||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
* 사용자 ID로 회원 조회
|
||||||
@AllArgsConstructor
|
*
|
||||||
@Builder
|
* @param userId 사용자 ID
|
||||||
public class Member {
|
* @return 회원 정보 (Optional)
|
||||||
|
*/
|
||||||
|
Optional<Member> findByUserId(String userId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 회원 고유 식별자
|
* 사용자 ID 존재 여부 확인
|
||||||
*/
|
*
|
||||||
@Id
|
* @param userId 사용자 ID
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
* @return 존재 여부
|
||||||
private Long id;
|
|
||||||
|
|
||||||
/**
|
Member 인증 서비스 구현체 및 Controllers
|
||||||
* 사용자 ID (로그인용)
|
코드
|
||||||
*/
|
|
||||||
@Column(name = "user_id", unique = true, nullable = false, length = 50)
|
|
||||||
private String userId;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 암호화된 패스워드
|
|
||||||
*/
|
|
||||||
@Column(name = "password", nullable = false)
|
|
||||||
private String password;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 회원 이름
|
|
||||||
*/
|
|
||||||
@Column(name = "name", nullable = false, length = 100)
|
|
||||||
private String name;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 사업자 번호
|
|
||||||
*/
|
|
||||||
@Column(name = "business_number", unique = true, nullable = false, length = 20)
|
|
||||||
private String businessNumber;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 이메일 주소
|
|
||||||
*/
|
|
||||||
@Column(name = "email", unique = true, nullable = false)
|
|
||||||
private String email;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 회원 생성 시각
|
|
||||||
*/
|
|
||||||
@Column(name = "created_at", nullable = false, updatable = false)
|
|
||||||
private LocalDateTime createdAt;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 회원 정보 수정 시각
|
|
||||||
*/
|
|
||||||
@Column(name = "updated_at", nullable = false)
|
|
||||||
private LocalDateTime updatedAt;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 엔티티 저장 전 실행되는 메서드
|
|
||||||
* 생성 시각과 수정 시각을 현재 시각으로 설정
|
|
||||||
*/
|
|
||||||
@PrePersist
|
|
||||||
protected void onCreate() {
|
|
||||||
createdAt = LocalDateTime.now();
|
|
||||||
updatedAt = LocalDateTime.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 엔티티 업데이트 전 실행되는 메서드
|
|
||||||
* 수정 시각을 현재 시각으로 갱신
|
|
||||||
*/
|
|
||||||
@PreUpdate
|
|
||||||
protected void onUpdate() {
|
|
||||||
updatedAt = LocalDateTime.now();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -17,7 +17,7 @@ public interface MemberRepository extends JpaRepository<Member, Long> {
|
|||||||
* 사용자 ID로 회원 조회
|
* 사용자 ID로 회원 조회
|
||||||
*
|
*
|
||||||
* @param userId 사용자 ID
|
* @param userId 사용자 ID
|
||||||
* @return 회원 정보
|
* @return 회원 정보 (Optional)
|
||||||
*/
|
*/
|
||||||
Optional<Member> findByUserId(String userId);
|
Optional<Member> findByUserId(String userId);
|
||||||
|
|
||||||
@ -38,9 +38,9 @@ public interface MemberRepository extends JpaRepository<Member, Long> {
|
|||||||
boolean existsByEmail(String email);
|
boolean existsByEmail(String email);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 사업자 번호 존재 여부 확인
|
* 사업자번호 존재 여부 확인
|
||||||
*
|
*
|
||||||
* @param businessNumber 사업자 번호
|
* @param businessNumber 사업자번호
|
||||||
* @return 존재 여부
|
* @return 존재 여부
|
||||||
*/
|
*/
|
||||||
boolean existsByBusinessNumber(String businessNumber);
|
boolean existsByBusinessNumber(String businessNumber);
|
||||||
|
|||||||
@ -5,31 +5,31 @@ import com.won.smarketing.member.dto.LoginResponse;
|
|||||||
import com.won.smarketing.member.dto.TokenResponse;
|
import com.won.smarketing.member.dto.TokenResponse;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 인증/인가 서비스 인터페이스
|
* 인증 서비스 인터페이스
|
||||||
* 로그인, 로그아웃, 토큰 갱신 기능 정의
|
* 로그인, 로그아웃, 토큰 갱신 관련 비즈니스 로직 정의
|
||||||
*/
|
*/
|
||||||
public interface AuthService {
|
public interface AuthService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 로그인 인증 처리
|
* 로그인
|
||||||
*
|
*
|
||||||
* @param request 로그인 요청 정보
|
* @param request 로그인 요청 정보
|
||||||
* @return JWT 토큰 정보
|
* @return 로그인 응답 정보 (토큰 포함)
|
||||||
*/
|
*/
|
||||||
LoginResponse login(LoginRequest request);
|
LoginResponse login(LoginRequest request);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 로그아웃 처리
|
* 로그아웃
|
||||||
*
|
*
|
||||||
* @param refreshToken 무효화할 Refresh Token
|
* @param refreshToken 리프레시 토큰
|
||||||
*/
|
*/
|
||||||
void logout(String refreshToken);
|
void logout(String refreshToken);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 토큰 갱신 처리
|
* 토큰 갱신
|
||||||
*
|
*
|
||||||
* @param refreshToken 갱신에 사용할 Refresh Token
|
* @param refreshToken 리프레시 토큰
|
||||||
* @return 새로운 JWT 토큰 정보
|
* @return 새로운 토큰 정보
|
||||||
*/
|
*/
|
||||||
TokenResponse refresh(String refreshToken);
|
TokenResponse refresh(String refreshToken);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,130 +2,21 @@ package com.won.smarketing.member.service;
|
|||||||
|
|
||||||
import com.won.smarketing.common.exception.BusinessException;
|
import com.won.smarketing.common.exception.BusinessException;
|
||||||
import com.won.smarketing.common.exception.ErrorCode;
|
import com.won.smarketing.common.exception.ErrorCode;
|
||||||
import com.won.smarketing.common.security.JwtTokenProvider;
|
import com.
|
||||||
import com.won.smarketing.member.dto.LoginRequest;
|
재시도
|
||||||
import com.won.smarketing.member.dto.LoginResponse;
|
Y
|
||||||
import com.won.smarketing.member.dto.TokenResponse;
|
계속
|
||||||
import com.won.smarketing.member.entity.Member;
|
편집
|
||||||
import com.won.smarketing.member.repository.MemberRepository;
|
Member 인증 서비스 구현체 및 Controllers
|
||||||
import lombok.RequiredArgsConstructor;
|
코드 ∙ 버전 2
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
// 새로운 리프레시 토큰을 Redis에 저장
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
redisTemplate.opsForValue().set(
|
||||||
import org.springframework.stereotype.Service;
|
REFRESH_TOKEN_PREFIX + userId,
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
newRefreshToken,
|
||||||
|
7,
|
||||||
|
TimeUnit.DAYS
|
||||||
|
);
|
||||||
|
|
||||||
import java.util.concurrent.TimeUnit;
|
// 기존 리프레시 토큰을
|
||||||
|
Store 서비스 Entity 및 DTO 클래스들
|
||||||
/**
|
코드
|
||||||
* 인증/인가 서비스 구현체
|
|
||||||
* 로그인, 로그아웃, 토큰 갱신 기능 구현
|
|
||||||
*/
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
@Transactional(readOnly = true)
|
|
||||||
public class AuthServiceImpl implements AuthService {
|
|
||||||
|
|
||||||
private final MemberRepository memberRepository;
|
|
||||||
private final PasswordEncoder passwordEncoder;
|
|
||||||
private final JwtTokenProvider jwtTokenProvider;
|
|
||||||
private final RedisTemplate<String, String> redisTemplate;
|
|
||||||
|
|
||||||
private static final String REFRESH_TOKEN_PREFIX = "refresh_token:";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 로그인 인증 처리
|
|
||||||
*
|
|
||||||
* @param request 로그인 요청 정보
|
|
||||||
* @return JWT 토큰 정보
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
@Transactional
|
|
||||||
public LoginResponse login(LoginRequest request) {
|
|
||||||
// 사용자 조회
|
|
||||||
Member member = memberRepository.findByUserId(request.getUserId())
|
|
||||||
.orElseThrow(() -> new BusinessException(ErrorCode.MEMBER_NOT_FOUND));
|
|
||||||
|
|
||||||
// 패스워드 검증
|
|
||||||
if (!passwordEncoder.matches(request.getPassword(), member.getPassword())) {
|
|
||||||
throw new BusinessException(ErrorCode.INVALID_PASSWORD);
|
|
||||||
}
|
|
||||||
|
|
||||||
// JWT 토큰 생성
|
|
||||||
String accessToken = jwtTokenProvider.generateAccessToken(member.getUserId());
|
|
||||||
String refreshToken = jwtTokenProvider.generateRefreshToken(member.getUserId());
|
|
||||||
long expiresIn = jwtTokenProvider.getAccessTokenValidityTime();
|
|
||||||
|
|
||||||
// Refresh Token을 Redis에 저장
|
|
||||||
String refreshTokenKey = REFRESH_TOKEN_PREFIX + member.getUserId();
|
|
||||||
redisTemplate.opsForValue().set(refreshTokenKey, refreshToken,
|
|
||||||
jwtTokenProvider.getRefreshTokenValidityTime(), TimeUnit.MILLISECONDS);
|
|
||||||
|
|
||||||
return LoginResponse.builder()
|
|
||||||
.accessToken(accessToken)
|
|
||||||
.refreshToken(refreshToken)
|
|
||||||
.expiresIn(expiresIn)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 로그아웃 처리
|
|
||||||
*
|
|
||||||
* @param refreshToken 무효화할 Refresh Token
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
@Transactional
|
|
||||||
public void logout(String refreshToken) {
|
|
||||||
// 토큰에서 사용자 ID 추출
|
|
||||||
String userId = jwtTokenProvider.getUserIdFromToken(refreshToken);
|
|
||||||
|
|
||||||
// Redis에서 Refresh Token 삭제
|
|
||||||
String refreshTokenKey = REFRESH_TOKEN_PREFIX + userId;
|
|
||||||
redisTemplate.delete(refreshTokenKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 토큰 갱신 처리
|
|
||||||
*
|
|
||||||
* @param refreshToken 갱신에 사용할 Refresh Token
|
|
||||||
* @return 새로운 JWT 토큰 정보
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
@Transactional
|
|
||||||
public TokenResponse refresh(String refreshToken) {
|
|
||||||
// Refresh Token 유효성 검증
|
|
||||||
if (!jwtTokenProvider.validateToken(refreshToken)) {
|
|
||||||
throw new BusinessException(ErrorCode.INVALID_TOKEN);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 토큰에서 사용자 ID 추출
|
|
||||||
String userId = jwtTokenProvider.getUserIdFromToken(refreshToken);
|
|
||||||
|
|
||||||
// Redis에서 저장된 Refresh Token 확인
|
|
||||||
String refreshTokenKey = REFRESH_TOKEN_PREFIX + userId;
|
|
||||||
String storedRefreshToken = redisTemplate.opsForValue().get(refreshTokenKey);
|
|
||||||
|
|
||||||
if (storedRefreshToken == null || !storedRefreshToken.equals(refreshToken)) {
|
|
||||||
throw new BusinessException(ErrorCode.INVALID_TOKEN);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 사용자 존재 여부 확인
|
|
||||||
Member member = memberRepository.findByUserId(userId)
|
|
||||||
.orElseThrow(() -> new BusinessException(ErrorCode.MEMBER_NOT_FOUND));
|
|
||||||
|
|
||||||
// 새로운 토큰 생성
|
|
||||||
String newAccessToken = jwtTokenProvider.generateAccessToken(member.getUserId());
|
|
||||||
String newRefreshToken = jwtTokenProvider.generateRefreshToken(member.getUserId());
|
|
||||||
long expiresIn = jwtTokenProvider.getAccessTokenValidityTime();
|
|
||||||
|
|
||||||
// 기존 Refresh Token 삭제 후 새로운 토큰 저장
|
|
||||||
redisTemplate.delete(refreshTokenKey);
|
|
||||||
redisTemplate.opsForValue().set(refreshTokenKey, newRefreshToken,
|
|
||||||
jwtTokenProvider.getRefreshTokenValidityTime(), TimeUnit.MILLISECONDS);
|
|
||||||
|
|
||||||
return TokenResponse.builder()
|
|
||||||
.accessToken(newAccessToken)
|
|
||||||
.refreshToken(newRefreshToken)
|
|
||||||
.expiresIn(expiresIn)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -4,13 +4,13 @@ import com.won.smarketing.member.dto.RegisterRequest;
|
|||||||
import com.won.smarketing.member.dto.ValidationResponse;
|
import com.won.smarketing.member.dto.ValidationResponse;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 회원 관리 서비스 인터페이스
|
* 회원 서비스 인터페이스
|
||||||
* 회원가입, 중복 확인, 패스워드 유효성 검증 기능 정의
|
* 회원 관리 관련 비즈니스 로직 정의
|
||||||
*/
|
*/
|
||||||
public interface MemberService {
|
public interface MemberService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 회원가입 처리
|
* 회원 등록
|
||||||
*
|
*
|
||||||
* @param request 회원가입 요청 정보
|
* @param request 회원가입 요청 정보
|
||||||
*/
|
*/
|
||||||
@ -20,15 +20,31 @@ public interface MemberService {
|
|||||||
* 사용자 ID 중복 확인
|
* 사용자 ID 중복 확인
|
||||||
*
|
*
|
||||||
* @param userId 확인할 사용자 ID
|
* @param userId 확인할 사용자 ID
|
||||||
* @return 중복 여부 (true: 중복, false: 사용 가능)
|
* @return 중복 여부
|
||||||
*/
|
*/
|
||||||
boolean checkDuplicate(String userId);
|
boolean checkDuplicate(String userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이메일 중복 확인
|
||||||
|
*
|
||||||
|
* @param email 확인할 이메일
|
||||||
|
* @return 중복 여부
|
||||||
|
*/
|
||||||
|
boolean checkEmailDuplicate(String email);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사업자번호 중복 확인
|
||||||
|
*
|
||||||
|
* @param businessNumber 확인할 사업자번호
|
||||||
|
* @return 중복 여부
|
||||||
|
*/
|
||||||
|
boolean checkBusinessNumberDuplicate(String businessNumber);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 패스워드 유효성 검증
|
* 패스워드 유효성 검증
|
||||||
*
|
*
|
||||||
* @param password 검증할 패스워드
|
* @param password 검증할 패스워드
|
||||||
* @return 유효성 검증 결과
|
* @return 검증 결과
|
||||||
*/
|
*/
|
||||||
ValidationResponse validatePassword(String password);
|
ValidationResponse validatePassword(String password);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import com.won.smarketing.member.dto.ValidationResponse;
|
|||||||
import com.won.smarketing.member.entity.Member;
|
import com.won.smarketing.member.entity.Member;
|
||||||
import com.won.smarketing.member.repository.MemberRepository;
|
import com.won.smarketing.member.repository.MemberRepository;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
@ -16,9 +17,10 @@ import java.util.List;
|
|||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 회원 관리 서비스 구현체
|
* 회원 서비스 구현체
|
||||||
* 회원가입, 중복 확인, 패스워드 유효성 검증 기능 구현
|
* 회원 등록, 중복 확인, 패스워드 검증 기능 구현
|
||||||
*/
|
*/
|
||||||
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
@ -27,89 +29,118 @@ public class MemberServiceImpl implements MemberService {
|
|||||||
private final MemberRepository memberRepository;
|
private final MemberRepository memberRepository;
|
||||||
private final PasswordEncoder passwordEncoder;
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
// 패스워드 정규식: 영문, 숫자, 특수문자 각각 최소 1개 포함, 8자 이상
|
// 패스워드 검증 패턴
|
||||||
private static final Pattern PASSWORD_PATTERN = Pattern.compile(
|
private static final Pattern LETTER_PATTERN = Pattern.compile(".*[a-zA-Z].*");
|
||||||
"^(?=.*[a-zA-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,}$"
|
private static final Pattern DIGIT_PATTERN = Pattern.compile(".*\\d.*");
|
||||||
);
|
private static final Pattern SPECIAL_CHAR_PATTERN = Pattern.compile(".*[@$!%*?&].*");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 회원가입 처리
|
* 회원 등록
|
||||||
*
|
*
|
||||||
* @param request 회원가입 요청 정보
|
* @param request 회원가입 요청 정보
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public void register(RegisterRequest request) {
|
public void register(RegisterRequest request) {
|
||||||
// 중복 ID 확인
|
log.info("회원 등록 시작: {}", request.getUserId());
|
||||||
|
|
||||||
|
// 중복 확인
|
||||||
if (memberRepository.existsByUserId(request.getUserId())) {
|
if (memberRepository.existsByUserId(request.getUserId())) {
|
||||||
throw new BusinessException(ErrorCode.DUPLICATE_MEMBER_ID);
|
throw new BusinessException(ErrorCode.DUPLICATE_MEMBER_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 이메일 중복 확인
|
|
||||||
if (memberRepository.existsByEmail(request.getEmail())) {
|
if (memberRepository.existsByEmail(request.getEmail())) {
|
||||||
throw new BusinessException(ErrorCode.DUPLICATE_EMAIL);
|
throw new BusinessException(ErrorCode.DUPLICATE_EMAIL);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 사업자 번호 중복 확인
|
if (request.getBusinessNumber() != null &&
|
||||||
if (memberRepository.existsByBusinessNumber(request.getBusinessNumber())) {
|
memberRepository.existsByBusinessNumber(request.getBusinessNumber())) {
|
||||||
throw new BusinessException(ErrorCode.DUPLICATE_BUSINESS_NUMBER);
|
throw new BusinessException(ErrorCode.DUPLICATE_BUSINESS_NUMBER);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 패스워드 암호화
|
|
||||||
String encodedPassword = passwordEncoder.encode(request.getPassword());
|
|
||||||
|
|
||||||
// 회원 엔티티 생성 및 저장
|
// 회원 엔티티 생성 및 저장
|
||||||
Member member = Member.builder()
|
Member member = Member.builder()
|
||||||
.userId(request.getUserId())
|
.userId(request.getUserId())
|
||||||
.password(encodedPassword)
|
.password(passwordEncoder.encode(request.getPassword()))
|
||||||
.name(request.getName())
|
.name(request.getName())
|
||||||
.businessNumber(request.getBusinessNumber())
|
.businessNumber(request.getBusinessNumber())
|
||||||
.email(request.getEmail())
|
.email(request.getEmail())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
memberRepository.save(member);
|
memberRepository.save(member);
|
||||||
|
log.info("회원 등록 완료: {}", request.getUserId());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 사용자 ID 중복 확인
|
* 사용자 ID 중복 확인
|
||||||
*
|
*
|
||||||
* @param userId 확인할 사용자 ID
|
* @param userId 확인할 사용자 ID
|
||||||
* @return 중복 여부 (true: 중복, false: 사용 가능)
|
* @return 중복 여부
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public boolean checkDuplicate(String userId) {
|
public boolean checkDuplicate(String userId) {
|
||||||
return memberRepository.existsByUserId(userId);
|
return memberRepository.existsByUserId(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이메일 중복 확인
|
||||||
|
*
|
||||||
|
* @param email 확인할 이메일
|
||||||
|
* @return 중복 여부
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean checkEmailDuplicate(String email) {
|
||||||
|
return memberRepository.existsByEmail(email);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사업자번호 중복 확인
|
||||||
|
*
|
||||||
|
* @param businessNumber 확인할 사업자번호
|
||||||
|
* @return 중복 여부
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean checkBusinessNumberDuplicate(String businessNumber) {
|
||||||
|
if (businessNumber == null || businessNumber.trim().isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return memberRepository.existsByBusinessNumber(businessNumber);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 패스워드 유효성 검증
|
* 패스워드 유효성 검증
|
||||||
*
|
*
|
||||||
* @param password 검증할 패스워드
|
* @param password 검증할 패스워드
|
||||||
* @return 유효성 검증 결과
|
* @return 검증 결과
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public ValidationResponse validatePassword(String password) {
|
public ValidationResponse validatePassword(String password) {
|
||||||
List<String> errors = new ArrayList<>();
|
List<String> errors = new ArrayList<>();
|
||||||
boolean isValid = true;
|
|
||||||
|
|
||||||
// 길이 검증 (8자 이상)
|
// 길이 검증
|
||||||
if (password.length() < 8) {
|
if (password.length() < 8 || password.length() > 20) {
|
||||||
errors.add("패스워드는 8자 이상이어야 합니다.");
|
errors.add("패스워드는 8-20자 사이여야 합니다");
|
||||||
isValid = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 패턴 검증 (영문, 숫자, 특수문자 포함)
|
// 영문 포함 여부
|
||||||
if (!PASSWORD_PATTERN.matcher(password).matches()) {
|
if (!LETTER_PATTERN.matcher(password).matches()) {
|
||||||
errors.add("패스워드는 영문, 숫자, 특수문자를 각각 최소 1개씩 포함해야 합니다.");
|
errors.add("영문이 포함되어야 합니다");
|
||||||
isValid = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String message = isValid ? "유효한 패스워드입니다." : "패스워드가 보안 규칙을 만족하지 않습니다.";
|
// 숫자 포함 여부
|
||||||
|
if (!DIGIT_PATTERN.matcher(password).matches()) {
|
||||||
|
errors.add("숫자가 포함되어야 합니다");
|
||||||
|
}
|
||||||
|
|
||||||
return ValidationResponse.builder()
|
// 특수문자 포함 여부
|
||||||
.isValid(isValid)
|
if (!SPECIAL_CHAR_PATTERN.matcher(password).matches()) {
|
||||||
.message(message)
|
errors.add("특수문자(@$!%*?&)가 포함되어야 합니다");
|
||||||
.errors(errors)
|
}
|
||||||
.build();
|
|
||||||
|
if (errors.isEmpty()) {
|
||||||
|
return ValidationResponse.valid("사용 가능한 패스워드입니다.");
|
||||||
|
} else {
|
||||||
|
return ValidationResponse.invalid("패스워드 규칙을 확인해 주세요.", errors);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,42 +1,33 @@
|
|||||||
server:
|
server:
|
||||||
port: ${SERVER_PORT:8081}
|
port: ${MEMBER_PORT:8081}
|
||||||
servlet:
|
|
||||||
context-path: /
|
|
||||||
|
|
||||||
spring:
|
spring:
|
||||||
application:
|
application:
|
||||||
name: member-service
|
name: member-service
|
||||||
datasource:
|
datasource:
|
||||||
url: jdbc:postgresql://${POSTGRES_HOST:localhost}:${POSTGRES_PORT:5432}/${POSTGRES_DB:memberdb}
|
url: ${DB_URL:jdbc:mysql://localhost:3306/smarketing_member}
|
||||||
username: ${POSTGRES_USER:postgres}
|
username: ${DB_USERNAME:root}
|
||||||
password: ${POSTGRES_PASSWORD:postgres}
|
password: ${DB_PASSWORD:password}
|
||||||
|
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||||
jpa:
|
jpa:
|
||||||
hibernate:
|
hibernate:
|
||||||
ddl-auto: ${JPA_DDL_AUTO:update}
|
ddl-auto: ${DDL_AUTO:update}
|
||||||
show-sql: ${JPA_SHOW_SQL:true}
|
show-sql: ${SHOW_SQL:true}
|
||||||
properties:
|
properties:
|
||||||
hibernate:
|
hibernate:
|
||||||
dialect: org.hibernate.dialect.PostgreSQLDialect
|
dialect: org.hibernate.dialect.MySQLDialect
|
||||||
format_sql: true
|
format_sql: true
|
||||||
data:
|
data:
|
||||||
redis:
|
redis:
|
||||||
host: ${REDIS_HOST:localhost}
|
host: ${REDIS_HOST:localhost}
|
||||||
port: ${REDIS_PORT:6379}
|
port: ${REDIS_PORT:6379}
|
||||||
password: ${REDIS_PASSWORD:}
|
password: ${REDIS_PASSWORD:}
|
||||||
timeout: 2000ms
|
|
||||||
|
|
||||||
jwt:
|
jwt:
|
||||||
secret-key: ${JWT_SECRET_KEY:mySecretKeyForJWTTokenGenerationThatShouldBeVeryLongAndSecure}
|
secret: ${JWT_SECRET:mySecretKeyForJWTTokenGenerationAndValidation123456789}
|
||||||
access-token-validity: ${JWT_ACCESS_TOKEN_VALIDITY:900000}
|
access-token-validity: ${JWT_ACCESS_VALIDITY:3600000}
|
||||||
refresh-token-validity: ${JWT_REFRESH_TOKEN_VALIDITY:86400000}
|
refresh-token-validity: ${JWT_REFRESH_VALIDITY:604800000}
|
||||||
|
|
||||||
springdoc:
|
|
||||||
swagger-ui:
|
|
||||||
path: /swagger-ui.html
|
|
||||||
operations-sorter: method
|
|
||||||
api-docs:
|
|
||||||
path: /api-docs
|
|
||||||
|
|
||||||
logging:
|
logging:
|
||||||
level:
|
level:
|
||||||
com.won.smarketing.member: ${LOG_LEVEL:DEBUG}
|
com.won.smarketing: ${LOG_LEVEL:DEBUG}
|
||||||
|
|||||||
@ -29,3 +29,4 @@ public class ErrorResponseDto {
|
|||||||
@Schema(description = "요청 경로", example = "/api/recommendation/marketing-tips")
|
@Schema(description = "요청 경로", example = "/api/recommendation/marketing-tips")
|
||||||
private String path;
|
private String path;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1,5 +1,4 @@
|
|||||||
rootProject.name = 'smarketing'
|
rootProject.name = 'smarketing'
|
||||||
|
|
||||||
include 'common'
|
include 'common'
|
||||||
include 'member'
|
include 'member'
|
||||||
include 'store'
|
include 'store'
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation project(':common')
|
implementation project(':common')
|
||||||
|
implementation 'com.mysql:mysql-connector-j'
|
||||||
}
|
}
|
||||||
|
|
||||||
bootJar {
|
bootJar {
|
||||||
|
|||||||
@ -0,0 +1,28 @@
|
|||||||
|
package com.won.smarketing.store.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JPA 설정 클래스
|
||||||
|
* JPA Auditing 기능 활성화
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@EnableJpaAuditing
|
||||||
|
public class JpaConfig {
|
||||||
|
}리는 50자 이하여야 합니다")
|
||||||
|
private String category;
|
||||||
|
|
||||||
|
@Schema(description = "가격", example = "4500", required = true)
|
||||||
|
@NotNull(message = "가격은 필수입니다")
|
||||||
|
@Min(value = 0, message = "가격은 0원 이상이어야 합니다")
|
||||||
|
private Integer price;
|
||||||
|
|
||||||
|
@Schema(description = "메뉴 설명", example = "진한 맛의 아메리카노")
|
||||||
|
@Size(max = 500, message = "메뉴 설명은 500자 이하여야 합니다")
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@Schema(description = "이미지 URL", example = "https://example.com/americano.jpg")
|
||||||
|
@Size(max = 500, message = "이미지 URL은 500자 이하여야 합니다")
|
||||||
|
private String image;
|
||||||
|
}
|
||||||
@ -1,49 +1,49 @@
|
|||||||
package com.won.smarketing.store.dto;
|
package com.won.smarketing.store.dto;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.Min;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
import javax.validation.constraints.Min;
|
|
||||||
import javax.validation.constraints.NotBlank;
|
|
||||||
import javax.validation.constraints.NotNull;
|
|
||||||
import javax.validation.constraints.Size;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 메뉴 등록 요청 DTO
|
* 메뉴 등록 요청 DTO
|
||||||
* 메뉴 등록 시 필요한 정보를 담는 데이터 전송 객체
|
* 메뉴 등록 시 필요한 정보를 전달합니다.
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@Builder
|
@Schema(description = "메뉴 등록 요청")
|
||||||
@Schema(description = "메뉴 등록 요청 정보")
|
|
||||||
public class MenuCreateRequest {
|
public class MenuCreateRequest {
|
||||||
|
|
||||||
@Schema(description = "매장 ID", example = "1", required = true)
|
@Schema(description = "매장 ID", example = "1", required = true)
|
||||||
@NotNull(message = "매장 ID는 필수입니다.")
|
@NotNull(message = "매장 ID는 필수입니다")
|
||||||
private Long storeId;
|
private Long storeId;
|
||||||
|
|
||||||
@Schema(description = "메뉴명", example = "아메리카노", required = true)
|
@Schema(description = "메뉴명", example = "아메리카노", required = true)
|
||||||
@NotBlank(message = "메뉴명은 필수입니다.")
|
@NotBlank(message = "메뉴명은 필수입니다")
|
||||||
@Size(max = 200, message = "메뉴명은 200자 이하여야 합니다.")
|
@Size(max = 100, message = "메뉴명은 100자 이하여야 합니다")
|
||||||
private String menuName;
|
private String menuName;
|
||||||
|
|
||||||
@Schema(description = "메뉴 카테고리", example = "커피", required = true)
|
@Schema(description = "카테고리", example = "커피")
|
||||||
@NotBlank(message = "카테고리는 필수입니다.")
|
@Size(max = 50, message = "카테고리는 50자 이하여야 합니다")
|
||||||
@Size(max = 100, message = "카테고리는 100자 이하여야 합니다.")
|
|
||||||
private String category;
|
private String category;
|
||||||
|
|
||||||
@Schema(description = "가격", example = "4500", required = true)
|
@Schema(description = "가격", example = "4500")
|
||||||
@NotNull(message = "가격은 필수입니다.")
|
@Min(value = 0, message = "가격은 0원 이상이어야 합니다")
|
||||||
@Min(value = 0, message = "가격은 0 이상이어야 합니다.")
|
|
||||||
private Integer price;
|
private Integer price;
|
||||||
|
|
||||||
@Schema(description = "메뉴 설명", example = "진한 원두의 깊은 맛")
|
@Schema(description = "메뉴 설명", example = "진한 맛의 아메리카노")
|
||||||
|
@Size(max = 500, message = "메뉴 설명은 500자 이하여야 합니다")
|
||||||
private String description;
|
private String description;
|
||||||
|
|
||||||
@Schema(description = "메뉴 이미지 URL", example = "https://example.com/americano.jpg")
|
@Schema(description = "이미지 URL", example = "https://example.com/americano.jpg")
|
||||||
|
@Size(max = 500, message = "이미지 URL은 500자 이하여야 합니다")
|
||||||
private String image;
|
private String image;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -9,37 +9,41 @@ import lombok.NoArgsConstructor;
|
|||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 메뉴 정보 응답 DTO
|
* 메뉴 응답 DTO
|
||||||
* 메뉴 정보 조회/등록/수정 시 반환되는 데이터
|
* 메뉴 정보를 클라이언트에게 전달합니다.
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@Builder
|
@Builder
|
||||||
@Schema(description = "메뉴 정보 응답")
|
@Schema(description = "메뉴 응답")
|
||||||
public class MenuResponse {
|
public class MenuResponse {
|
||||||
|
|
||||||
@Schema(description = "메뉴 ID", example = "1")
|
@Schema(description = "메뉴 ID", example = "1")
|
||||||
private Long menuId;
|
private Long menuId;
|
||||||
|
|
||||||
|
@Schema(description = "매장 ID", example = "1")
|
||||||
|
private Long storeId;
|
||||||
|
|
||||||
@Schema(description = "메뉴명", example = "아메리카노")
|
@Schema(description = "메뉴명", example = "아메리카노")
|
||||||
private String menuName;
|
private String menuName;
|
||||||
|
|
||||||
@Schema(description = "메뉴 카테고리", example = "커피")
|
@Schema(description = "카테고리", example = "커피")
|
||||||
private String category;
|
private String category;
|
||||||
|
|
||||||
@Schema(description = "가격", example = "4500")
|
@Schema(description = "가격", example = "4500")
|
||||||
private Integer price;
|
private Integer price;
|
||||||
|
|
||||||
@Schema(description = "메뉴 설명", example = "진한 원두의 깊은 맛")
|
@Schema(description = "메뉴 설명", example = "진한 맛의 아메리카노")
|
||||||
private String description;
|
private String description;
|
||||||
|
|
||||||
@Schema(description = "메뉴 이미지 URL", example = "https://example.com/americano.jpg")
|
@Schema(description = "이미지 URL", example = "https://example.com/americano.jpg")
|
||||||
private String image;
|
private String image;
|
||||||
|
|
||||||
@Schema(description = "등록 시각")
|
@Schema(description = "등록일시", example = "2024-01-15T10:30:00")
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
@Schema(description = "수정 시각")
|
@Schema(description = "수정일시", example = "2024-01-15T10:30:00")
|
||||||
private LocalDateTime updatedAt;
|
private LocalDateTime updatedAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,22 +9,28 @@ import lombok.NoArgsConstructor;
|
|||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 매출 정보 응답 DTO
|
* 매출 응답 DTO
|
||||||
* 오늘 매출, 월간 매출, 전일 대비 매출 정보
|
* 매출 정보를 클라이언트에게 전달합니다.
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@Builder
|
@Builder
|
||||||
@Schema(description = "매출 정보 응답")
|
@Schema(description = "매출 응답")
|
||||||
public class SalesResponse {
|
public class SalesResponse {
|
||||||
|
|
||||||
@Schema(description = "오늘 매출", example = "150000")
|
@Schema(description = "오늘 매출", example = "150000")
|
||||||
private BigDecimal todaySales;
|
private BigDecimal todaySales;
|
||||||
|
|
||||||
@Schema(description = "이번 달 매출", example = "3200000")
|
@Schema(description = "월간 매출", example = "4500000")
|
||||||
private BigDecimal monthSales;
|
private BigDecimal monthSales;
|
||||||
|
|
||||||
@Schema(description = "전일 대비 매출 변화량", example = "25000")
|
@Schema(description = "전일 대비 매출 변화", example = "25000")
|
||||||
private BigDecimal previousDayComparison;
|
private BigDecimal previousDayComparison;
|
||||||
|
|
||||||
|
@Schema(description = "전일 대비 매출 변화율 (%)", example = "15.5")
|
||||||
|
private BigDecimal previousDayChangeRate;
|
||||||
|
|
||||||
|
@Schema(description = "목표 매출 대비 달성율 (%)", example = "85.2")
|
||||||
|
private BigDecimal goalAchievementRate;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,79 +1,58 @@
|
|||||||
package com.won.smarketing.store.dto;
|
package com.won.smarketing.store.dto;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
import javax.validation.constraints.NotBlank;
|
|
||||||
import javax.validation.constraints.NotNull;
|
|
||||||
import javax.validation.constraints.Pattern;
|
|
||||||
import javax.validation.constraints.Size;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 매장 등록 요청 DTO
|
* 매장 등록 요청 DTO
|
||||||
* 매장 등록 시 필요한 정보를 담는 데이터 전송 객체
|
* 매장 등록 시 필요한 정보를 전달합니다.
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@Builder
|
@Schema(description = "매장 등록 요청")
|
||||||
@Schema(description = "매장 등록 요청 정보")
|
|
||||||
public class StoreCreateRequest {
|
public class StoreCreateRequest {
|
||||||
|
|
||||||
@Schema(description = "매장 소유자 사용자 ID", example = "testuser", required = true)
|
|
||||||
@NotBlank(message = "사용자 ID는 필수입니다.")
|
|
||||||
private String userId;
|
|
||||||
|
|
||||||
@Schema(description = "매장명", example = "맛있는 카페", required = true)
|
@Schema(description = "매장명", example = "맛있는 카페", required = true)
|
||||||
@NotBlank(message = "매장명은 필수입니다.")
|
@NotBlank(message = "매장명은 필수입니다")
|
||||||
@Size(max = 200, message = "매장명은 200자 이하여야 합니다.")
|
@Size(max = 100, message = "매장명은 100자 이하여야 합니다")
|
||||||
private String storeName;
|
private String storeName;
|
||||||
|
|
||||||
@Schema(description = "매장 이미지 URL", example = "https://example.com/store.jpg")
|
@Schema(description = "업종", example = "카페")
|
||||||
private String storeImage;
|
@Size(max = 50, message = "업종은 50자 이하여야 합니다")
|
||||||
|
|
||||||
@Schema(description = "업종", example = "카페", required = true)
|
|
||||||
@NotBlank(message = "업종은 필수입니다.")
|
|
||||||
@Size(max = 100, message = "업종은 100자 이하여야 합니다.")
|
|
||||||
private String businessType;
|
private String businessType;
|
||||||
|
|
||||||
@Schema(description = "매장 주소", example = "서울시 강남구 테헤란로 123", required = true)
|
@Schema(description = "주소", example = "서울시 강남구 테헤란로 123", required = true)
|
||||||
@NotBlank(message = "주소는 필수입니다.")
|
@NotBlank(message = "주소는 필수입니다")
|
||||||
@Size(max = 500, message = "주소는 500자 이하여야 합니다.")
|
@Size(max = 200, message = "주소는 200자 이하여야 합니다")
|
||||||
private String address;
|
private String address;
|
||||||
|
|
||||||
@Schema(description = "매장 전화번호", example = "02-1234-5678", required = true)
|
@Schema(description = "전화번호", example = "02-1234-5678")
|
||||||
@NotBlank(message = "전화번호는 필수입니다.")
|
@Size(max = 20, message = "전화번호는 20자 이하여야 합니다")
|
||||||
@Pattern(regexp = "^\\d{2,3}-\\d{3,4}-\\d{4}$", message = "올바른 전화번호 형식이 아닙니다.")
|
|
||||||
private String phoneNumber;
|
private String phoneNumber;
|
||||||
|
|
||||||
@Schema(description = "사업자 번호", example = "123-45-67890", required = true)
|
@Schema(description = "영업시간", example = "09:00 - 22:00")
|
||||||
@NotBlank(message = "사업자 번호는 필수입니다.")
|
@Size(max = 100, message = "영업시간은 100자 이하여야 합니다")
|
||||||
@Pattern(regexp = "^\\d{3}-\\d{2}-\\d{5}$", message = "사업자 번호 형식이 올바르지 않습니다.")
|
private String businessHours;
|
||||||
private String businessNumber;
|
|
||||||
|
|
||||||
@Schema(description = "인스타그램 계정", example = "@mycafe")
|
@Schema(description = "휴무일", example = "매주 일요일")
|
||||||
@Size(max = 100, message = "인스타그램 계정은 100자 이하여야 합니다.")
|
@Size(max = 100, message = "휴무일은 100자 이하여야 합니다")
|
||||||
private String instaAccount;
|
|
||||||
|
|
||||||
@Schema(description = "네이버 블로그 계정", example = "mycafe_blog")
|
|
||||||
@Size(max = 100, message = "네이버 블로그 계정은 100자 이하여야 합니다.")
|
|
||||||
private String naverBlogAccount;
|
|
||||||
|
|
||||||
@Schema(description = "오픈 시간", example = "09:00")
|
|
||||||
@Pattern(regexp = "^\\d{2}:\\d{2}$", message = "올바른 시간 형식이 아닙니다. (HH:MM)")
|
|
||||||
private String openTime;
|
|
||||||
|
|
||||||
@Schema(description = "마감 시간", example = "22:00")
|
|
||||||
@Pattern(regexp = "^\\d{2}:\\d{2}$", message = "올바른 시간 형식이 아닙니다. (HH:MM)")
|
|
||||||
private String closeTime;
|
|
||||||
|
|
||||||
@Schema(description = "휴무일", example = "매주 월요일")
|
|
||||||
@Size(max = 100, message = "휴무일은 100자 이하여야 합니다.")
|
|
||||||
private String closedDays;
|
private String closedDays;
|
||||||
|
|
||||||
@Schema(description = "좌석 수", example = "20")
|
@Schema(description = "좌석 수", example = "20")
|
||||||
private Integer seatCount;
|
private Integer seatCount;
|
||||||
|
|
||||||
|
@Schema(description = "SNS 계정 정보", example = "인스타그램: @mystore")
|
||||||
|
@Size(max = 500, message = "SNS 계정 정보는 500자 이하여야 합니다")
|
||||||
|
private String snsAccounts;
|
||||||
|
|
||||||
|
@Schema(description = "매장 설명", example = "따뜻한 분위기의 동네 카페입니다.")
|
||||||
|
@Size(max = 1000, message = "매장 설명은 1000자 이하여야 합니다")
|
||||||
|
private String description;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -9,14 +9,14 @@ import lombok.NoArgsConstructor;
|
|||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 매장 정보 응답 DTO
|
* 매장 응답 DTO
|
||||||
* 매장 정보 조회/등록/수정 시 반환되는 데이터
|
* 매장 정보를 클라이언트에게 전달합니다.
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@Builder
|
@Builder
|
||||||
@Schema(description = "매장 정보 응답")
|
@Schema(description = "매장 응답")
|
||||||
public class StoreResponse {
|
public class StoreResponse {
|
||||||
|
|
||||||
@Schema(description = "매장 ID", example = "1")
|
@Schema(description = "매장 ID", example = "1")
|
||||||
@ -25,42 +25,34 @@ public class StoreResponse {
|
|||||||
@Schema(description = "매장명", example = "맛있는 카페")
|
@Schema(description = "매장명", example = "맛있는 카페")
|
||||||
private String storeName;
|
private String storeName;
|
||||||
|
|
||||||
@Schema(description = "매장 이미지 URL", example = "https://example.com/store.jpg")
|
|
||||||
private String storeImage;
|
|
||||||
|
|
||||||
@Schema(description = "업종", example = "카페")
|
@Schema(description = "업종", example = "카페")
|
||||||
private String businessType;
|
private String businessType;
|
||||||
|
|
||||||
@Schema(description = "매장 주소", example = "서울시 강남구 테헤란로 123")
|
@Schema(description = "주소", example = "서울시 강남구 테헤란로 123")
|
||||||
private String address;
|
private String address;
|
||||||
|
|
||||||
@Schema(description = "매장 전화번호", example = "02-1234-5678")
|
@Schema(description = "전화번호", example = "02-1234-5678")
|
||||||
private String phoneNumber;
|
private String phoneNumber;
|
||||||
|
|
||||||
@Schema(description = "사업자 번호", example = "123-45-67890")
|
@Schema(description = "영업시간", example = "09:00 - 22:00")
|
||||||
private String businessNumber;
|
private String businessHours;
|
||||||
|
|
||||||
@Schema(description = "인스타그램 계정", example = "@mycafe")
|
@Schema(description = "휴무일", example = "매주 일요일")
|
||||||
private String instaAccount;
|
|
||||||
|
|
||||||
@Schema(description = "네이버 블로그 계정", example = "mycafe_blog")
|
|
||||||
private String naverBlogAccount;
|
|
||||||
|
|
||||||
@Schema(description = "오픈 시간", example = "09:00")
|
|
||||||
private String openTime;
|
|
||||||
|
|
||||||
@Schema(description = "마감 시간", example = "22:00")
|
|
||||||
private String closeTime;
|
|
||||||
|
|
||||||
@Schema(description = "휴무일", example = "매주 월요일")
|
|
||||||
private String closedDays;
|
private String closedDays;
|
||||||
|
|
||||||
@Schema(description = "좌석 수", example = "20")
|
@Schema(description = "좌석 수", example = "20")
|
||||||
private Integer seatCount;
|
private Integer seatCount;
|
||||||
|
|
||||||
@Schema(description = "등록 시각")
|
@Schema(description = "SNS 계정 정보", example = "인스타그램: @mystore")
|
||||||
|
private String snsAccounts;
|
||||||
|
|
||||||
|
@Schema(description = "매장 설명", example = "따뜻한 분위기의 동네 카페입니다.")
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@Schema(description = "등록일시", example = "2024-01-15T10:30:00")
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
@Schema(description = "수정 시각")
|
@Schema(description = "수정일시", example = "2024-01-15T10:30:00")
|
||||||
private LocalDateTime updatedAt;
|
private LocalDateTime updatedAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,60 +1,55 @@
|
|||||||
package com.won.smarketing.store.dto;
|
package com.won.smarketing.store.dto;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
import javax.validation.constraints.Pattern;
|
|
||||||
import javax.validation.constraints.Size;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 매장 수정 요청 DTO
|
* 매장 수정 요청 DTO
|
||||||
* 매장 정보 수정 시 필요한 정보를 담는 데이터 전송 객체
|
* 매장 정보 수정 시 필요한 정보를 전달합니다.
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@Builder
|
@Schema(description = "매장 수정 요청")
|
||||||
@Schema(description = "매장 수정 요청 정보")
|
|
||||||
public class StoreUpdateRequest {
|
public class StoreUpdateRequest {
|
||||||
|
|
||||||
@Schema(description = "매장명", example = "맛있는 카페")
|
@Schema(description = "매장명", example = "맛있는 카페")
|
||||||
@Size(max = 200, message = "매장명은 200자 이하여야 합니다.")
|
@Size(max = 100, message = "매장명은 100자 이하여야 합니다")
|
||||||
private String storeName;
|
private String storeName;
|
||||||
|
|
||||||
@Schema(description = "매장 이미지 URL", example = "https://example.com/store.jpg")
|
@Schema(description = "업종", example = "카페")
|
||||||
private String storeImage;
|
@Size(max = 50, message = "업종은 50자 이하여야 합니다")
|
||||||
|
private String businessType;
|
||||||
|
|
||||||
@Schema(description = "매장 주소", example = "서울시 강남구 테헤란로 123")
|
@Schema(description = "주소", example = "서울시 강남구 테헤란로 123")
|
||||||
@Size(max = 500, message = "주소는 500자 이하여야 합니다.")
|
@Size(max = 200, message = "주소는 200자 이하여야 합니다")
|
||||||
private String address;
|
private String address;
|
||||||
|
|
||||||
@Schema(description = "매장 전화번호", example = "02-1234-5678")
|
@Schema(description = "전화번호", example = "02-1234-5678")
|
||||||
@Pattern(regexp = "^\\d{2,3}-\\d{3,4}-\\d{4}$", message = "올바른 전화번호 형식이 아닙니다.")
|
@Size(max = 20, message = "전화번호는 20자 이하여야 합니다")
|
||||||
private String phoneNumber;
|
private String phoneNumber;
|
||||||
|
|
||||||
@Schema(description = "인스타그램 계정", example = "@mycafe")
|
@Schema(description = "영업시간", example = "09:00 - 22:00")
|
||||||
@Size(max = 100, message = "인스타그램 계정은 100자 이하여야 합니다.")
|
@Size(max = 100, message = "영업시간은 100자 이하여야 합니다")
|
||||||
private String instaAccount;
|
private String businessHours;
|
||||||
|
|
||||||
@Schema(description = "네이버 블로그 계정", example = "mycafe_blog")
|
@Schema(description = "휴무일", example = "매주 일요일")
|
||||||
@Size(max = 100, message = "네이버 블로그 계정은 100자 이하여야 합니다.")
|
@Size(max = 100, message = "휴무일은 100자 이하여야 합니다")
|
||||||
private String naverBlogAccount;
|
|
||||||
|
|
||||||
@Schema(description = "오픈 시간", example = "09:00")
|
|
||||||
@Pattern(regexp = "^\\d{2}:\\d{2}$", message = "올바른 시간 형식이 아닙니다. (HH:MM)")
|
|
||||||
private String openTime;
|
|
||||||
|
|
||||||
@Schema(description = "마감 시간", example = "22:00")
|
|
||||||
@Pattern(regexp = "^\\d{2}:\\d{2}$", message = "올바른 시간 형식이 아닙니다. (HH:MM)")
|
|
||||||
private String closeTime;
|
|
||||||
|
|
||||||
@Schema(description = "휴무일", example = "매주 월요일")
|
|
||||||
@Size(max = 100, message = "휴무일은 100자 이하여야 합니다.")
|
|
||||||
private String closedDays;
|
private String closedDays;
|
||||||
|
|
||||||
@Schema(description = "좌석 수", example = "20")
|
@Schema(description = "좌석 수", example = "20")
|
||||||
private Integer seatCount;
|
private Integer seatCount;
|
||||||
|
|
||||||
|
@Schema(description = "SNS 계정 정보", example = "인스타그램: @mystore")
|
||||||
|
@Size(max = 500, message = "SNS 계정 정보는 500자 이하여야 합니다")
|
||||||
|
private String snsAccounts;
|
||||||
|
|
||||||
|
@Schema(description = "매장 설명", example = "따뜻한 분위기의 동네 카페입니다.")
|
||||||
|
@Size(max = 1000, message = "매장 설명은 1000자 이하여야 합니다")
|
||||||
|
private String description;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,98 +1,62 @@
|
|||||||
package com.won.smarketing.store.entity;
|
package com.won.smarketing.store.entity;
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import lombok.*;
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.springframework.data.annotation.CreatedDate;
|
||||||
|
import org.springframework.data.annotation.LastModifiedDate;
|
||||||
|
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 메뉴 정보를 나타내는 엔티티
|
* 메뉴 엔티티
|
||||||
* 메뉴명, 카테고리, 가격, 설명, 이미지 정보 저장
|
* 매장의 메뉴 정보를 관리
|
||||||
*/
|
*/
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "menus")
|
@Table(name = "menus")
|
||||||
@Getter
|
@Getter
|
||||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@Builder
|
@Builder
|
||||||
|
@EntityListeners(AuditingEntityListener.class)
|
||||||
public class Menu {
|
public class Menu {
|
||||||
|
|
||||||
/**
|
|
||||||
* 메뉴 고유 식별자
|
|
||||||
*/
|
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
@Column(name = "menu_id")
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
/**
|
|
||||||
* 매장 ID
|
|
||||||
*/
|
|
||||||
@Column(name = "store_id", nullable = false)
|
@Column(name = "store_id", nullable = false)
|
||||||
private Long storeId;
|
private Long storeId;
|
||||||
|
|
||||||
/**
|
@Column(name = "menu_name", nullable = false, length = 100)
|
||||||
* 메뉴명
|
|
||||||
*/
|
|
||||||
@Column(name = "menu_name", nullable = false, length = 200)
|
|
||||||
private String menuName;
|
private String menuName;
|
||||||
|
|
||||||
/**
|
@Column(name = "category", length = 50)
|
||||||
* 메뉴 카테고리
|
|
||||||
*/
|
|
||||||
@Column(name = "category", nullable = false, length = 100)
|
|
||||||
private String category;
|
private String category;
|
||||||
|
|
||||||
/**
|
|
||||||
* 가격
|
|
||||||
*/
|
|
||||||
@Column(name = "price", nullable = false)
|
@Column(name = "price", nullable = false)
|
||||||
private Integer price;
|
private Integer price;
|
||||||
|
|
||||||
/**
|
@Column(name = "description", length = 500)
|
||||||
* 메뉴 설명
|
|
||||||
*/
|
|
||||||
@Column(name = "description", columnDefinition = "TEXT")
|
|
||||||
private String description;
|
private String description;
|
||||||
|
|
||||||
/**
|
@Column(name = "image_url", length = 500)
|
||||||
* 메뉴 이미지 URL
|
|
||||||
*/
|
|
||||||
@Column(name = "image", length = 500)
|
|
||||||
private String image;
|
private String image;
|
||||||
|
|
||||||
/**
|
@CreatedDate
|
||||||
* 메뉴 등록 시각
|
|
||||||
*/
|
|
||||||
@Column(name = "created_at", nullable = false, updatable = false)
|
@Column(name = "created_at", nullable = false, updatable = false)
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
/**
|
@LastModifiedDate
|
||||||
* 메뉴 정보 수정 시각
|
@Column(name = "updated_at")
|
||||||
*/
|
|
||||||
@Column(name = "updated_at", nullable = false)
|
|
||||||
private LocalDateTime updatedAt;
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 엔티티 저장 전 실행되는 메서드
|
* 메뉴 정보 업데이트
|
||||||
* 생성 시각과 수정 시각을 현재 시각으로 설정
|
|
||||||
*/
|
|
||||||
@PrePersist
|
|
||||||
protected void onCreate() {
|
|
||||||
createdAt = LocalDateTime.now();
|
|
||||||
updatedAt = LocalDateTime.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 엔티티 업데이트 전 실행되는 메서드
|
|
||||||
* 수정 시각을 현재 시각으로 갱신
|
|
||||||
*/
|
|
||||||
@PreUpdate
|
|
||||||
protected void onUpdate() {
|
|
||||||
updatedAt = LocalDateTime.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 메뉴 정보 업데이트 메서드
|
|
||||||
*
|
*
|
||||||
* @param menuName 메뉴명
|
* @param menuName 메뉴명
|
||||||
* @param category 카테고리
|
* @param category 카테고리
|
||||||
@ -100,10 +64,17 @@ public class Menu {
|
|||||||
* @param description 설명
|
* @param description 설명
|
||||||
* @param image 이미지 URL
|
* @param image 이미지 URL
|
||||||
*/
|
*/
|
||||||
public void updateMenuInfo(String menuName, String category, Integer price, String description, String image) {
|
public void updateMenu(String menuName, String category, Integer price,
|
||||||
this.menuName = menuName;
|
String description, String image) {
|
||||||
this.category = category;
|
if (menuName != null && !menuName.trim().isEmpty()) {
|
||||||
this.price = price;
|
this.menuName = menuName;
|
||||||
|
}
|
||||||
|
if (category != null && !category.trim().isEmpty()) {
|
||||||
|
this.category = category;
|
||||||
|
}
|
||||||
|
if (price != null && price > 0) {
|
||||||
|
this.price = price;
|
||||||
|
}
|
||||||
this.description = description;
|
this.description = description;
|
||||||
this.image = image;
|
this.image = image;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -59,3 +59,4 @@ public class Sales {
|
|||||||
createdAt = LocalDateTime.now();
|
createdAt = LocalDateTime.now();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,164 +1,103 @@
|
|||||||
package com.won.smarketing.store.entity;
|
package com.won.smarketing.store.entity;
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import lombok.*;
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.springframework.data.annotation.CreatedDate;
|
||||||
|
import org.springframework.data.annotation.LastModifiedDate;
|
||||||
|
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.LocalTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 매장 정보를 나타내는 엔티티
|
* 매장 엔티티
|
||||||
* 매장의 기본 정보, 운영 정보, SNS 계정 정보 저장
|
* 매장의 기본 정보와 운영 정보를 관리
|
||||||
*/
|
*/
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "stores")
|
@Table(name = "stores")
|
||||||
@Getter
|
@Getter
|
||||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@Builder
|
@Builder
|
||||||
|
@EntityListeners(AuditingEntityListener.class)
|
||||||
public class Store {
|
public class Store {
|
||||||
|
|
||||||
/**
|
|
||||||
* 매장 고유 식별자
|
|
||||||
*/
|
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
@Column(name = "store_id")
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
/**
|
@Column(name = "member_id", nullable = false)
|
||||||
* 매장 소유자 사용자 ID
|
private Long memberId;
|
||||||
*/
|
|
||||||
@Column(name = "user_id", unique = true, nullable = false, length = 50)
|
|
||||||
private String userId;
|
|
||||||
|
|
||||||
/**
|
@Column(name = "store_name", nullable = false, length = 100)
|
||||||
* 매장명
|
|
||||||
*/
|
|
||||||
@Column(name = "store_name", nullable = false, length = 200)
|
|
||||||
private String storeName;
|
private String storeName;
|
||||||
|
|
||||||
/**
|
@Column(name = "business_type", length = 50)
|
||||||
* 매장 이미지 URL
|
|
||||||
*/
|
|
||||||
@Column(name = "store_image", length = 500)
|
|
||||||
private String storeImage;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 업종
|
|
||||||
*/
|
|
||||||
@Column(name = "business_type", nullable = false, length = 100)
|
|
||||||
private String businessType;
|
private String businessType;
|
||||||
|
|
||||||
/**
|
@Column(name = "address", nullable = false, length = 200)
|
||||||
* 매장 주소
|
|
||||||
*/
|
|
||||||
@Column(name = "address", nullable = false, length = 500)
|
|
||||||
private String address;
|
private String address;
|
||||||
|
|
||||||
/**
|
@Column(name = "phone_number", length = 20)
|
||||||
* 매장 전화번호
|
|
||||||
*/
|
|
||||||
@Column(name = "phone_number", nullable = false, length = 20)
|
|
||||||
private String phoneNumber;
|
private String phoneNumber;
|
||||||
|
|
||||||
/**
|
@Column(name = "business_hours", length = 100)
|
||||||
* 사업자 번호
|
private String businessHours;
|
||||||
*/
|
|
||||||
@Column(name = "business_number", nullable = false, length = 20)
|
|
||||||
private String businessNumber;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 인스타그램 계정
|
|
||||||
*/
|
|
||||||
@Column(name = "insta_account", length = 100)
|
|
||||||
private String instaAccount;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 네이버 블로그 계정
|
|
||||||
*/
|
|
||||||
@Column(name = "naver_blog_account", length = 100)
|
|
||||||
private String naverBlogAccount;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 오픈 시간
|
|
||||||
*/
|
|
||||||
@Column(name = "open_time", length = 10)
|
|
||||||
private String openTime;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 마감 시간
|
|
||||||
*/
|
|
||||||
@Column(name = "close_time", length = 10)
|
|
||||||
private String closeTime;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 휴무일
|
|
||||||
*/
|
|
||||||
@Column(name = "closed_days", length = 100)
|
@Column(name = "closed_days", length = 100)
|
||||||
private String closedDays;
|
private String closedDays;
|
||||||
|
|
||||||
/**
|
|
||||||
* 좌석 수
|
|
||||||
*/
|
|
||||||
@Column(name = "seat_count")
|
@Column(name = "seat_count")
|
||||||
private Integer seatCount;
|
private Integer seatCount;
|
||||||
|
|
||||||
/**
|
@Column(name = "sns_accounts", length = 500)
|
||||||
* 매장 등록 시각
|
private String snsAccounts;
|
||||||
*/
|
|
||||||
|
@Column(name = "description", length = 1000)
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@CreatedDate
|
||||||
@Column(name = "created_at", nullable = false, updatable = false)
|
@Column(name = "created_at", nullable = false, updatable = false)
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
/**
|
@LastModifiedDate
|
||||||
* 매장 정보 수정 시각
|
@Column(name = "updated_at")
|
||||||
*/
|
|
||||||
@Column(name = "updated_at", nullable = false)
|
|
||||||
private LocalDateTime updatedAt;
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 엔티티 저장 전 실행되는 메서드
|
* 매장 정보 업데이트
|
||||||
* 생성 시각과 수정 시각을 현재 시각으로 설정
|
|
||||||
*/
|
|
||||||
@PrePersist
|
|
||||||
protected void onCreate() {
|
|
||||||
createdAt = LocalDateTime.now();
|
|
||||||
updatedAt = LocalDateTime.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 엔티티 업데이트 전 실행되는 메서드
|
|
||||||
* 수정 시각을 현재 시각으로 갱신
|
|
||||||
*/
|
|
||||||
@PreUpdate
|
|
||||||
protected void onUpdate() {
|
|
||||||
updatedAt = LocalDateTime.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 매장 정보 업데이트 메서드
|
|
||||||
*
|
*
|
||||||
* @param storeName 매장명
|
* @param storeName 매장명
|
||||||
* @param storeImage 매장 이미지
|
* @param businessType 업종
|
||||||
* @param address 주소
|
* @param address 주소
|
||||||
* @param phoneNumber 전화번호
|
* @param phoneNumber 전화번호
|
||||||
* @param instaAccount 인스타그램 계정
|
* @param businessHours 영업시간
|
||||||
* @param naverBlogAccount 네이버 블로그 계정
|
|
||||||
* @param openTime 오픈 시간
|
|
||||||
* @param closeTime 마감 시간
|
|
||||||
* @param closedDays 휴무일
|
* @param closedDays 휴무일
|
||||||
* @param seatCount 좌석 수
|
* @param seatCount 좌석 수
|
||||||
|
* @param snsAccounts SNS 계정 정보
|
||||||
|
* @param description 설명
|
||||||
*/
|
*/
|
||||||
public void updateStoreInfo(String storeName, String storeImage, String address, String phoneNumber,
|
public void updateStore(String storeName, String businessType, String address,
|
||||||
String instaAccount, String naverBlogAccount, String openTime, String closeTime,
|
String phoneNumber, String businessHours, String closedDays,
|
||||||
String closedDays, Integer seatCount) {
|
Integer seatCount, String snsAccounts, String description) {
|
||||||
this.storeName = storeName;
|
if (storeName != null && !storeName.trim().isEmpty()) {
|
||||||
this.storeImage = storeImage;
|
this.storeName = storeName;
|
||||||
this.address = address;
|
}
|
||||||
|
if (businessType != null && !businessType.trim().isEmpty()) {
|
||||||
|
this.businessType = businessType;
|
||||||
|
}
|
||||||
|
if (address != null && !address.trim().isEmpty()) {
|
||||||
|
this.address = address;
|
||||||
|
}
|
||||||
this.phoneNumber = phoneNumber;
|
this.phoneNumber = phoneNumber;
|
||||||
this.instaAccount = instaAccount;
|
this.businessHours = businessHours;
|
||||||
this.naverBlogAccount = naverBlogAccount;
|
|
||||||
this.openTime = openTime;
|
|
||||||
this.closeTime = closeTime;
|
|
||||||
this.closedDays = closedDays;
|
this.closedDays = closedDays;
|
||||||
this.seatCount = seatCount;
|
this.seatCount = seatCount;
|
||||||
|
this.snsAccounts = snsAccounts;
|
||||||
|
this.description = description;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,10 +14,29 @@ import java.util.Optional;
|
|||||||
public interface StoreRepository extends JpaRepository<Store, Long> {
|
public interface StoreRepository extends JpaRepository<Store, Long> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 사용자 ID로 매장 조회
|
* 회원 ID로 매장 조회
|
||||||
*
|
*
|
||||||
* @param userId 사용자 ID
|
* @param memberId 회원 ID
|
||||||
* @return 매장 정보
|
* @return 매장 정보 (Optional)
|
||||||
*/
|
*/
|
||||||
Optional<Store> findByUserId(String userId);
|
Optional<Store> findByMemberId(Long memberId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 회원의 매장 존재 여부 확인
|
||||||
|
*
|
||||||
|
* @param memberId 회원 ID
|
||||||
|
* @return 존재 여부
|
||||||
|
*/
|
||||||
|
boolean existsByMemberId(Long memberId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장명으로 매장 조회
|
||||||
|
*
|
||||||
|
* @param storeName 매장명
|
||||||
|
* @return 매장 목록
|
||||||
|
*/
|
||||||
|
Optional<Store> findByStoreName(String storeName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -7,13 +7,13 @@ import com.won.smarketing.store.dto.MenuUpdateRequest;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 메뉴 관리 서비스 인터페이스
|
* 메뉴 서비스 인터페이스
|
||||||
* 메뉴 등록, 조회, 수정, 삭제 기능 정의
|
* 메뉴 관리 관련 비즈니스 로직 정의
|
||||||
*/
|
*/
|
||||||
public interface MenuService {
|
public interface MenuService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 메뉴 정보 등록
|
* 메뉴 등록
|
||||||
*
|
*
|
||||||
* @param request 메뉴 등록 요청 정보
|
* @param request 메뉴 등록 요청 정보
|
||||||
* @return 등록된 메뉴 정보
|
* @return 등록된 메뉴 정보
|
||||||
@ -31,7 +31,7 @@ public interface MenuService {
|
|||||||
/**
|
/**
|
||||||
* 메뉴 정보 수정
|
* 메뉴 정보 수정
|
||||||
*
|
*
|
||||||
* @param menuId 수정할 메뉴 ID
|
* @param menuId 메뉴 ID
|
||||||
* @param request 메뉴 수정 요청 정보
|
* @param request 메뉴 수정 요청 정보
|
||||||
* @return 수정된 메뉴 정보
|
* @return 수정된 메뉴 정보
|
||||||
*/
|
*/
|
||||||
@ -40,7 +40,7 @@ public interface MenuService {
|
|||||||
/**
|
/**
|
||||||
* 메뉴 삭제
|
* 메뉴 삭제
|
||||||
*
|
*
|
||||||
* @param menuId 삭제할 메뉴 ID
|
* @param menuId 메뉴 ID
|
||||||
*/
|
*/
|
||||||
void deleteMenu(Long menuId);
|
void deleteMenu(Long menuId);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,15 +3,15 @@ package com.won.smarketing.store.service;
|
|||||||
import com.won.smarketing.store.dto.SalesResponse;
|
import com.won.smarketing.store.dto.SalesResponse;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 매출 관리 서비스 인터페이스
|
* 매출 서비스 인터페이스
|
||||||
* 매출 조회 기능 정의
|
* 매출 조회 관련 비즈니스 로직 정의
|
||||||
*/
|
*/
|
||||||
public interface SalesService {
|
public interface SalesService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 매출 정보 조회
|
* 매출 정보 조회
|
||||||
*
|
*
|
||||||
* @return 매출 정보 (오늘, 월간, 전일 대비)
|
* @return 매출 정보
|
||||||
*/
|
*/
|
||||||
SalesResponse getSales();
|
SalesResponse getSales();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,42 +1,60 @@
|
|||||||
package com.won.smarketing.store.service;
|
package com.won.smarketing.store.service;
|
||||||
|
|
||||||
import com.won.smarketing.store.dto.SalesResponse;
|
import com.won.smarketing.store.dto.SalesResponse;
|
||||||
import com.won.smarketing.store.repository.SalesRepository;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 매출 관리 서비스 구현체
|
* 매출 서비스 구현체
|
||||||
* 매출 조회 기능 구현
|
* 매출 조회 기능 구현 (현재는 Mock 데이터)
|
||||||
*/
|
*/
|
||||||
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public class SalesServiceImpl implements SalesService {
|
public class SalesServiceImpl implements SalesService {
|
||||||
|
|
||||||
private final SalesRepository salesRepository;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 매출 정보 조회
|
* 매출 정보 조회
|
||||||
|
* 현재는 Mock 데이터를 반환 (실제로는 매출 데이터 조회 로직 필요)
|
||||||
*
|
*
|
||||||
* @return 매출 정보 (오늘, 월간, 전일 대비)
|
* @return 매출 정보
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public SalesResponse getSales() {
|
public SalesResponse getSales() {
|
||||||
// TODO: 현재는 더미 데이터 반환, 실제로는 현재 로그인한 사용자의 매장 ID를 사용해야 함
|
log.info("매출 정보 조회");
|
||||||
Long storeId = 1L; // 임시로 설정
|
|
||||||
|
|
||||||
BigDecimal todaySales = salesRepository.findTodaySalesByStoreId(storeId);
|
// Mock 데이터 (실제로는 데이터베이스에서 조회)
|
||||||
BigDecimal monthSales = salesRepository.findMonthSalesByStoreId(storeId);
|
BigDecimal todaySales = new BigDecimal("150000");
|
||||||
BigDecimal previousDayComparison = salesRepository.findPreviousDayComparisonByStoreId(storeId);
|
BigDecimal monthSales = new BigDecimal("4500000");
|
||||||
|
BigDecimal yesterdaySales = new BigDecimal("125000");
|
||||||
|
BigDecimal targetSales = new BigDecimal("176000");
|
||||||
|
|
||||||
|
// 전일 대비 변화 계산
|
||||||
|
BigDecimal previousDayComparison = todaySales.subtract(yesterdaySales);
|
||||||
|
BigDecimal previousDayChangeRate = yesterdaySales.compareTo(BigDecimal.ZERO) > 0
|
||||||
|
? previousDayComparison.divide(yesterdaySales, 4, RoundingMode.HALF_UP)
|
||||||
|
.multiply(new BigDecimal("100"))
|
||||||
|
: BigDecimal.ZERO;
|
||||||
|
|
||||||
|
// 목표 대비 달성률 계산
|
||||||
|
BigDecimal goalAchievementRate = targetSales.compareTo(BigDecimal.ZERO) > 0
|
||||||
|
? todaySales.divide(targetSales, 4, RoundingMode.HALF_UP)
|
||||||
|
.multiply(new BigDecimal("100"))
|
||||||
|
: BigDecimal.ZERO;
|
||||||
|
|
||||||
return SalesResponse.builder()
|
return SalesResponse.builder()
|
||||||
.todaySales(todaySales != null ? todaySales : BigDecimal.ZERO)
|
.todaySales(todaySales)
|
||||||
.monthSales(monthSales != null ? monthSales : BigDecimal.ZERO)
|
.monthSales(monthSales)
|
||||||
.previousDayComparison(previousDayComparison != null ? previousDayComparison : BigDecimal.ZERO)
|
.previousDayComparison(previousDayComparison)
|
||||||
|
.previousDayChangeRate(previousDayChangeRate)
|
||||||
|
.goalAchievementRate(goalAchievementRate)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,13 +5,13 @@ import com.won.smarketing.store.dto.StoreResponse;
|
|||||||
import com.won.smarketing.store.dto.StoreUpdateRequest;
|
import com.won.smarketing.store.dto.StoreUpdateRequest;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 매장 관리 서비스 인터페이스
|
* 매장 서비스 인터페이스
|
||||||
* 매장 등록, 조회, 수정 기능 정의
|
* 매장 관리 관련 비즈니스 로직 정의
|
||||||
*/
|
*/
|
||||||
public interface StoreService {
|
public interface StoreService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 매장 정보 등록
|
* 매장 등록
|
||||||
*
|
*
|
||||||
* @param request 매장 등록 요청 정보
|
* @param request 매장 등록 요청 정보
|
||||||
* @return 등록된 매장 정보
|
* @return 등록된 매장 정보
|
||||||
@ -19,9 +19,16 @@ public interface StoreService {
|
|||||||
StoreResponse register(StoreCreateRequest request);
|
StoreResponse register(StoreCreateRequest request);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 매장 정보 조회
|
* 매장 정보 조회 (현재 로그인 사용자)
|
||||||
*
|
*
|
||||||
* @param storeId 조회할 매장 ID
|
* @return 매장 정보
|
||||||
|
*/
|
||||||
|
StoreResponse getMyStore();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장 정보 조회 (매장 ID)
|
||||||
|
*
|
||||||
|
* @param storeId 매장 ID
|
||||||
* @return 매장 정보
|
* @return 매장 정보
|
||||||
*/
|
*/
|
||||||
StoreResponse getStore(String storeId);
|
StoreResponse getStore(String storeId);
|
||||||
@ -29,7 +36,7 @@ public interface StoreService {
|
|||||||
/**
|
/**
|
||||||
* 매장 정보 수정
|
* 매장 정보 수정
|
||||||
*
|
*
|
||||||
* @param storeId 수정할 매장 ID
|
* @param storeId 매장 ID
|
||||||
* @param request 매장 수정 요청 정보
|
* @param request 매장 수정 요청 정보
|
||||||
* @return 수정된 매장 정보
|
* @return 수정된 매장 정보
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -8,13 +8,16 @@ import com.won.smarketing.store.dto.StoreUpdateRequest;
|
|||||||
import com.won.smarketing.store.entity.Store;
|
import com.won.smarketing.store.entity.Store;
|
||||||
import com.won.smarketing.store.repository.StoreRepository;
|
import com.won.smarketing.store.repository.StoreRepository;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 매장 관리 서비스 구현체
|
* 매장 서비스 구현체
|
||||||
* 매장 등록, 조회, 수정 기능 구현
|
* 매장 등록, 조회, 수정 기능 구현
|
||||||
*/
|
*/
|
||||||
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
@ -23,7 +26,7 @@ public class StoreServiceImpl implements StoreService {
|
|||||||
private final StoreRepository storeRepository;
|
private final StoreRepository storeRepository;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 매장 정보 등록
|
* 매장 등록
|
||||||
*
|
*
|
||||||
* @param request 매장 등록 요청 정보
|
* @param request 매장 등록 요청 정보
|
||||||
* @return 등록된 매장 정보
|
* @return 등록된 매장 정보
|
||||||
@ -31,50 +34,75 @@ public class StoreServiceImpl implements StoreService {
|
|||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public StoreResponse register(StoreCreateRequest request) {
|
public StoreResponse register(StoreCreateRequest request) {
|
||||||
// 사용자별 매장 중복 등록 확인
|
String currentUserId = getCurrentUserId();
|
||||||
if (storeRepository.findByUserId(request.getUserId()).isPresent()) {
|
Long memberId = Long.valueOf(currentUserId); // 실제로는 Member ID 조회 필요
|
||||||
|
|
||||||
|
log.info("매장 등록 시작: {} (회원: {})", request.getStoreName(), memberId);
|
||||||
|
|
||||||
|
// 회원당 하나의 매장만 등록 가능
|
||||||
|
if (storeRepository.existsByMemberId(memberId)) {
|
||||||
throw new BusinessException(ErrorCode.STORE_ALREADY_EXISTS);
|
throw new BusinessException(ErrorCode.STORE_ALREADY_EXISTS);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 매장 엔티티 생성 및 저장
|
// 매장 엔티티 생성 및 저장
|
||||||
Store store = Store.builder()
|
Store store = Store.builder()
|
||||||
.userId(request.getUserId())
|
.memberId(memberId)
|
||||||
.storeName(request.getStoreName())
|
.storeName(request.getStoreName())
|
||||||
.storeImage(request.getStoreImage())
|
|
||||||
.businessType(request.getBusinessType())
|
.businessType(request.getBusinessType())
|
||||||
.address(request.getAddress())
|
.address(request.getAddress())
|
||||||
.phoneNumber(request.getPhoneNumber())
|
.phoneNumber(request.getPhoneNumber())
|
||||||
.businessNumber(request.getBusinessNumber())
|
.businessHours(request.getBusinessHours())
|
||||||
.instaAccount(request.getInstaAccount())
|
|
||||||
.naverBlogAccount(request.getNaverBlogAccount())
|
|
||||||
.openTime(request.getOpenTime())
|
|
||||||
.closeTime(request.getCloseTime())
|
|
||||||
.closedDays(request.getClosedDays())
|
.closedDays(request.getClosedDays())
|
||||||
.seatCount(request.getSeatCount())
|
.seatCount(request.getSeatCount())
|
||||||
|
.snsAccounts(request.getSnsAccounts())
|
||||||
|
.description(request.getDescription())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
Store savedStore = storeRepository.save(store);
|
Store savedStore = storeRepository.save(store);
|
||||||
|
log.info("매장 등록 완료: {} (ID: {})", savedStore.getStoreName(), savedStore.getId());
|
||||||
|
|
||||||
return toStoreResponse(savedStore);
|
return toStoreResponse(savedStore);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 매장 정보 조회
|
* 매장 정보 조회 (현재 로그인 사용자)
|
||||||
*
|
*
|
||||||
* @param storeId 조회할 매장 ID
|
|
||||||
* @return 매장 정보
|
* @return 매장 정보
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public StoreResponse getStore(String storeId) {
|
public StoreResponse getMyStore() {
|
||||||
Store store = storeRepository.findByUserId(storeId)
|
String currentUserId = getCurrentUserId();
|
||||||
|
Long memberId = Long.valueOf(currentUserId);
|
||||||
|
|
||||||
|
Store store = storeRepository.findByMemberId(memberId)
|
||||||
.orElseThrow(() -> new BusinessException(ErrorCode.STORE_NOT_FOUND));
|
.orElseThrow(() -> new BusinessException(ErrorCode.STORE_NOT_FOUND));
|
||||||
|
|
||||||
return toStoreResponse(store);
|
return toStoreResponse(store);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장 정보 조회 (매장 ID)
|
||||||
|
*
|
||||||
|
* @param storeId 매장 ID
|
||||||
|
* @return 매장 정보
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public StoreResponse getStore(String storeId) {
|
||||||
|
try {
|
||||||
|
Long id = Long.valueOf(storeId);
|
||||||
|
Store store = storeRepository.findById(id)
|
||||||
|
.orElseThrow(() -> new BusinessException(ErrorCode.STORE_NOT_FOUND));
|
||||||
|
|
||||||
|
return toStoreResponse(store);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 매장 정보 수정
|
* 매장 정보 수정
|
||||||
*
|
*
|
||||||
* @param storeId 수정할 매장 ID
|
* @param storeId 매장 ID
|
||||||
* @param request 매장 수정 요청 정보
|
* @param request 매장 수정 요청 정보
|
||||||
* @return 수정된 매장 정보
|
* @return 수정된 매장 정보
|
||||||
*/
|
*/
|
||||||
@ -85,20 +113,21 @@ public class StoreServiceImpl implements StoreService {
|
|||||||
.orElseThrow(() -> new BusinessException(ErrorCode.STORE_NOT_FOUND));
|
.orElseThrow(() -> new BusinessException(ErrorCode.STORE_NOT_FOUND));
|
||||||
|
|
||||||
// 매장 정보 업데이트
|
// 매장 정보 업데이트
|
||||||
store.updateStoreInfo(
|
store.updateStore(
|
||||||
request.getStoreName(),
|
request.getStoreName(),
|
||||||
request.getStoreImage(),
|
request.getBusinessType(),
|
||||||
request.getAddress(),
|
request.getAddress(),
|
||||||
request.getPhoneNumber(),
|
request.getPhoneNumber(),
|
||||||
request.getInstaAccount(),
|
request.getBusinessHours(),
|
||||||
request.getNaverBlogAccount(),
|
|
||||||
request.getOpenTime(),
|
|
||||||
request.getCloseTime(),
|
|
||||||
request.getClosedDays(),
|
request.getClosedDays(),
|
||||||
request.getSeatCount()
|
request.getSeatCount(),
|
||||||
|
request.getSnsAccounts(),
|
||||||
|
request.getDescription()
|
||||||
);
|
);
|
||||||
|
|
||||||
Store updatedStore = storeRepository.save(store);
|
Store updatedStore = storeRepository.save(store);
|
||||||
|
log.info("매장 정보 수정 완료: {} (ID: {})", updatedStore.getStoreName(), updatedStore.getId());
|
||||||
|
|
||||||
return toStoreResponse(updatedStore);
|
return toStoreResponse(updatedStore);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,19 +141,25 @@ public class StoreServiceImpl implements StoreService {
|
|||||||
return StoreResponse.builder()
|
return StoreResponse.builder()
|
||||||
.storeId(store.getId())
|
.storeId(store.getId())
|
||||||
.storeName(store.getStoreName())
|
.storeName(store.getStoreName())
|
||||||
.storeImage(store.getStoreImage())
|
|
||||||
.businessType(store.getBusinessType())
|
.businessType(store.getBusinessType())
|
||||||
.address(store.getAddress())
|
.address(store.getAddress())
|
||||||
.phoneNumber(store.getPhoneNumber())
|
.phoneNumber(store.getPhoneNumber())
|
||||||
.businessNumber(store.getBusinessNumber())
|
.businessHours(store.getBusinessHours())
|
||||||
.instaAccount(store.getInstaAccount())
|
|
||||||
.naverBlogAccount(store.getNaverBlogAccount())
|
|
||||||
.openTime(store.getOpenTime())
|
|
||||||
.closeTime(store.getCloseTime())
|
|
||||||
.closedDays(store.getClosedDays())
|
.closedDays(store.getClosedDays())
|
||||||
.seatCount(store.getSeatCount())
|
.seatCount(store.getSeatCount())
|
||||||
|
.snsAccounts(store.getSnsAccounts())
|
||||||
|
.description(store.getDescription())
|
||||||
.createdAt(store.getCreatedAt())
|
.createdAt(store.getCreatedAt())
|
||||||
.updatedAt(store.getUpdatedAt())
|
.updatedAt(store.getUpdatedAt())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 현재 로그인된 사용자 ID 조회
|
||||||
|
*
|
||||||
|
* @return 사용자 ID
|
||||||
|
*/
|
||||||
|
private String getCurrentUserId() {
|
||||||
|
return SecurityContextHolder.getContext().getAuthentication().getName();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user