fix: build

This commit is contained in:
unknown 2025-06-11 13:17:30 +09:00
parent f6d4380dc7
commit 38af15a3fd
8 changed files with 282 additions and 79 deletions

View File

@ -38,7 +38,7 @@ public class WeatherApiDataProvider implements WeatherDataProvider {
* @return 날씨 데이터 * @return 날씨 데이터
*/ */
@Override @Override
public WeatherData getCurrentWeather(String location) { public WeatherApiResponse getCurrentWeather(String location) {
try { try {
log.debug("날씨 정보 조회 시작: location={}", location); log.debug("날씨 정보 조회 시작: location={}", location);

View File

@ -10,7 +10,7 @@ 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; import jakarta.validation.Valid;
/** /**
* AI 마케팅 추천을 위한 REST API 컨트롤러 * AI 마케팅 추천을 위한 REST API 컨트롤러

View File

@ -2,6 +2,7 @@ package com.won.smarketing.recommend.presentation.dto;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
@ -14,6 +15,7 @@ import java.time.LocalDateTime;
@Data @Data
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@Builder
@Schema(description = "AI 마케팅 팁 생성 응답") @Schema(description = "AI 마케팅 팁 생성 응답")
public class MarketingTipResponse { public class MarketingTipResponse {

View File

@ -18,12 +18,6 @@ subprojects {
apply plugin: 'org.springframework.boot' apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management' apply plugin: 'io.spring.dependency-management'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
configurations { configurations {
compileOnly { compileOnly {
extendsFrom annotationProcessor extendsFrom annotationProcessor
@ -32,6 +26,7 @@ subprojects {
dependencies { dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-validation'

View File

@ -2,6 +2,7 @@ package com.won.smarketing.common.security;
import io.jsonwebtoken.*; import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys; import io.jsonwebtoken.security.Keys;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@ -18,6 +19,13 @@ import java.util.Date;
public class JwtTokenProvider { public class JwtTokenProvider {
private final SecretKey secretKey; private final SecretKey secretKey;
/**
* -- GETTER --
* 액세스 토큰 유효시간 반환
*
* @return 액세스 토큰 유효시간 (밀리초)
*/
@Getter
private final long accessTokenValidityTime; private final long accessTokenValidityTime;
private final long refreshTokenValidityTime; private final long refreshTokenValidityTime;
@ -29,8 +37,8 @@ public class JwtTokenProvider {
* @param refreshTokenValidityTime 리프레시 토큰 유효시간 (밀리초) * @param refreshTokenValidityTime 리프레시 토큰 유효시간 (밀리초)
*/ */
public JwtTokenProvider(@Value("${jwt.secret}") String secret, public JwtTokenProvider(@Value("${jwt.secret}") String secret,
@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()); this.secretKey = Keys.hmacShaKeyFor(secret.getBytes());
this.accessTokenValidityTime = accessTokenValidityTime; this.accessTokenValidityTime = accessTokenValidityTime;
this.refreshTokenValidityTime = refreshTokenValidityTime; this.refreshTokenValidityTime = refreshTokenValidityTime;
@ -47,9 +55,9 @@ public class JwtTokenProvider {
Date expiryDate = new Date(now.getTime() + accessTokenValidityTime); Date expiryDate = new Date(now.getTime() + accessTokenValidityTime);
return Jwts.builder() return Jwts.builder()
.setSubject(userId) .subject(userId)
.setIssuedAt(now) .issuedAt(now)
.setExpiration(expiryDate) .expiration(expiryDate)
.signWith(secretKey) .signWith(secretKey)
.compact(); .compact();
} }
@ -65,9 +73,9 @@ public class JwtTokenProvider {
Date expiryDate = new Date(now.getTime() + refreshTokenValidityTime); Date expiryDate = new Date(now.getTime() + refreshTokenValidityTime);
return Jwts.builder() return Jwts.builder()
.setSubject(userId) .subject(userId)
.setIssuedAt(now) .issuedAt(now)
.setExpiration(expiryDate) .expiration(expiryDate)
.signWith(secretKey) .signWith(secretKey)
.compact(); .compact();
} }
@ -79,11 +87,11 @@ public class JwtTokenProvider {
* @return 사용자 ID * @return 사용자 ID
*/ */
public String getUserIdFromToken(String token) { public String getUserIdFromToken(String token) {
Claims claims = Jwts.parserBuilder() Claims claims = Jwts.parser()
.setSigningKey(secretKey) .verifyWith(secretKey)
.build() .build()
.parseClaimsJws(token) .parseSignedClaims(token)
.getBody(); .getPayload();
return claims.getSubject(); return claims.getSubject();
} }
@ -96,10 +104,10 @@ public class JwtTokenProvider {
*/ */
public boolean validateToken(String token) { public boolean validateToken(String token) {
try { try {
Jwts.parserBuilder() Jwts.parser()
.setSigningKey(secretKey) .verifyWith(secretKey)
.build() .build()
.parseClaimsJws(token); .parseSignedClaims(token);
return true; return true;
} catch (SecurityException ex) { } catch (SecurityException ex) {
log.error("Invalid JWT signature: {}", ex.getMessage()); log.error("Invalid JWT signature: {}", ex.getMessage());
@ -115,12 +123,4 @@ public class JwtTokenProvider {
return false; return false;
} }
/**
* 액세스 토큰 유효시간 반환
*
* @return 액세스 토큰 유효시간 (밀리초)
*/
public long getAccessTokenValidityTime() {
return accessTokenValidityTime;
}
} }

View File

@ -6,7 +6,7 @@ import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import javax.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
/** /**
* 로그아웃 요청 DTO * 로그아웃 요청 DTO

View File

@ -4,26 +4,79 @@ import jakarta.persistence.*;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstruct import lombok.NoArgsConstructor;
재시도 import org.springframework.data.annotation.CreatedDate;
Y import org.springframework.data.annotation.LastModifiedDate;
계속 import org.springframework.data.jpa.domain.support.AuditingEntityListener;
편집
Member 서비스 모든 클래스 구현 import java.time.LocalDateTime;
코드버전 2
/** /**
* 사용자 ID로 회원 조회 * 회원 엔티티
* * 회원의 기본 정보와 사업자 정보를 관리
* @param userId 사용자 ID */
* @return 회원 정보 (Optional) @Entity
*/ @Table(name = "members")
Optional<Member> findByUserId(String userId); @Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@EntityListeners(AuditingEntityListener.class)
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "member_id")
private Long id;
@Column(name = "user_id", nullable = false, unique = true, length = 50)
private String userId;
@Column(name = "password", nullable = false, length = 100)
private String password;
@Column(name = "name", nullable = false, length = 50)
private String name;
@Column(name = "business_number", length = 12)
private String businessNumber;
@Column(name = "email", nullable = false, unique = true, length = 100)
private String email;
@CreatedDate
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
@Column(name = "updated_at")
private LocalDateTime updatedAt;
/** /**
* 사용자 ID 존재 여부 확인 * 회원 정보 업데이트
* *
* @param userId 사용자 ID * @param name 이름
* @return 존재 여부 * @param email 이메일
* @param businessNumber 사업자번호
*/
public void updateProfile(String name, String email, String businessNumber) {
if (name != null && !name.trim().isEmpty()) {
this.name = name;
}
if (email != null && !email.trim().isEmpty()) {
this.email = email;
}
if (businessNumber != null && !businessNumber.trim().isEmpty()) {
this.businessNumber = businessNumber;
}
}
Member 인증 서비스 구현체 Controllers /**
코드 * 패스워드 변경
*
* @param encodedPassword 암호화된 패스워드
*/
public void changePassword(String encodedPassword) {
this.password = encodedPassword;
}
}

View File

@ -2,21 +2,174 @@ 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. import com.won.smarketing.common.security.JwtTokenProvider;
재시도 import com.won.smarketing.member.dto.LoginRequest;
Y import com.won.smarketing.member.dto.LoginResponse;
계속 import com.won.smarketing.member.dto.TokenResponse;
편집 import com.won.smarketing.member.entity.Member;
Member 인증 서비스 구현체 Controllers import com.won.smarketing.member.repository.MemberRepository;
코드버전 2 import lombok.RequiredArgsConstructor;
// 새로운 리프레시 토큰을 Redis에 저장 import lombok.extern.slf4j.Slf4j;
redisTemplate.opsForValue().set( import org.springframework.data.redis.core.RedisTemplate;
REFRESH_TOKEN_PREFIX + userId, import org.springframework.security.crypto.password.PasswordEncoder;
newRefreshToken, import org.springframework.stereotype.Service;
7, import org.springframework.transaction.annotation.Transactional;
TimeUnit.DAYS
);
// 기존 리프레시 토큰을 import java.util.concurrent.TimeUnit;
Store 서비스 Entity DTO 클래스들
코드 /**
* 인증 서비스 구현체
* 로그인, 로그아웃, 토큰 갱신 기능 구현
*/
@Slf4j
@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:";
private static final String BLACKLIST_PREFIX = "blacklist:";
/**
* 로그인
*
* @param request 로그인 요청 정보
* @return 로그인 응답 정보 (토큰 포함)
*/
@Override
@Transactional
public LoginResponse login(LoginRequest request) {
log.info("로그인 시도: {}", request.getUserId());
// 회원 조회
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);
}
// 토큰 생성
String accessToken = jwtTokenProvider.generateAccessToken(member.getUserId());
String refreshToken = jwtTokenProvider.generateRefreshToken(member.getUserId());
// 리프레시 토큰을 Redis에 저장 (7일)
redisTemplate.opsForValue().set(
REFRESH_TOKEN_PREFIX + member.getUserId(),
refreshToken,
7,
TimeUnit.DAYS
);
log.info("로그인 성공: {}", request.getUserId());
return LoginResponse.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.expiresIn(jwtTokenProvider.getAccessTokenValidityTime() / 1000)
.userInfo(LoginResponse.UserInfo.builder()
.userId(member.getUserId())
.name(member.getName())
.email(member.getEmail())
.build())
.build();
}
/**
* 로그아웃
*
* @param refreshToken 리프레시 토큰
*/
@Override
@Transactional
public void logout(String refreshToken) {
try {
if (jwtTokenProvider.validateToken(refreshToken)) {
String userId = jwtTokenProvider.getUserIdFromToken(refreshToken);
// Redis에서 리프레시 토큰 삭제
redisTemplate.delete(REFRESH_TOKEN_PREFIX + userId);
// 리프레시 토큰을 블랙리스트에 추가
redisTemplate.opsForValue().set(
BLACKLIST_PREFIX + refreshToken,
"logout",
7,
TimeUnit.DAYS
);
log.info("로그아웃 완료: {}", userId);
}
} catch (Exception ex) {
log.warn("로그아웃 처리 중 오류 발생: {}", ex.getMessage());
// 로그아웃은 실패해도 클라이언트에게는 성공으로 응답
}
}
/**
* 토큰 갱신
*
* @param refreshToken 리프레시 토큰
* @return 새로운 토큰 정보
*/
@Override
@Transactional
public TokenResponse refresh(String refreshToken) {
// 토큰 유효성 검증
if (!jwtTokenProvider.validateToken(refreshToken)) {
throw new BusinessException(ErrorCode.INVALID_TOKEN);
}
// 블랙리스트 확인
if (redisTemplate.hasKey(BLACKLIST_PREFIX + refreshToken)) {
throw new BusinessException(ErrorCode.INVALID_TOKEN);
}
String userId = jwtTokenProvider.getUserIdFromToken(refreshToken);
// Redis에 저장된 리프레시 토큰과 비교
String storedRefreshToken = redisTemplate.opsForValue().get(REFRESH_TOKEN_PREFIX + userId);
if (!refreshToken.equals(storedRefreshToken)) {
throw new BusinessException(ErrorCode.INVALID_TOKEN);
}
// 회원 존재 확인
if (!memberRepository.existsByUserId(userId)) {
throw new BusinessException(ErrorCode.MEMBER_NOT_FOUND);
}
// 새로운 토큰 생성
String newAccessToken = jwtTokenProvider.generateAccessToken(userId);
String newRefreshToken = jwtTokenProvider.generateRefreshToken(userId);
// 새로운 리프레시 토큰을 Redis에 저장
redisTemplate.opsForValue().set(
REFRESH_TOKEN_PREFIX + userId,
newRefreshToken,
7,
TimeUnit.DAYS
);
// 기존 리프레시 토큰을 블랙리스트에 추가
redisTemplate.opsForValue().set(
BLACKLIST_PREFIX + refreshToken,
"refreshed",
7,
TimeUnit.DAYS
);
log.info("토큰 갱신 완료: {}", userId);
return TokenResponse.builder()
.accessToken(newAccessToken)
.refreshToken(newRefreshToken)
.expiresIn(jwtTokenProvider.getAccessTokenValidityTime() / 1000)
.build();
}
}