From 38af15a3fd3fb75fed262d29d845b8a1dc131421 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 11 Jun 2025 13:17:30 +0900 Subject: [PATCH] fix: build --- .../external/WeatherApiDataProvider.java | 2 +- .../controller/RecommendationController.java | 2 +- .../dto/MarketingTipResponse.java | 2 + build.gradle | 7 +- .../common/security/JwtTokenProvider.java | 60 +++--- .../smarketing/member/dto/LogoutRequest.java | 2 +- .../won/smarketing/member/entity/Member.java | 97 +++++++-- .../member/service/AuthServiceImpl.java | 189 ++++++++++++++++-- 8 files changed, 282 insertions(+), 79 deletions(-) diff --git a/ai-recommend/src/main/java/com/won/smarketing/recommend/infrastructure/external/WeatherApiDataProvider.java b/ai-recommend/src/main/java/com/won/smarketing/recommend/infrastructure/external/WeatherApiDataProvider.java index 3fcf9dd..4896c5a 100644 --- a/ai-recommend/src/main/java/com/won/smarketing/recommend/infrastructure/external/WeatherApiDataProvider.java +++ b/ai-recommend/src/main/java/com/won/smarketing/recommend/infrastructure/external/WeatherApiDataProvider.java @@ -38,7 +38,7 @@ public class WeatherApiDataProvider implements WeatherDataProvider { * @return 날씨 데이터 */ @Override - public WeatherData getCurrentWeather(String location) { + public WeatherApiResponse getCurrentWeather(String location) { try { log.debug("날씨 정보 조회 시작: location={}", location); diff --git a/ai-recommend/src/main/java/com/won/smarketing/recommend/presentation/controller/RecommendationController.java b/ai-recommend/src/main/java/com/won/smarketing/recommend/presentation/controller/RecommendationController.java index fedc727..e929efb 100644 --- a/ai-recommend/src/main/java/com/won/smarketing/recommend/presentation/controller/RecommendationController.java +++ b/ai-recommend/src/main/java/com/won/smarketing/recommend/presentation/controller/RecommendationController.java @@ -10,7 +10,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import javax.validation.Valid; +import jakarta.validation.Valid; /** * AI 마케팅 추천을 위한 REST API 컨트롤러 diff --git a/ai-recommend/src/main/java/com/won/smarketing/recommend/presentation/dto/MarketingTipResponse.java b/ai-recommend/src/main/java/com/won/smarketing/recommend/presentation/dto/MarketingTipResponse.java index 26e2331..ca1ffe0 100644 --- a/ai-recommend/src/main/java/com/won/smarketing/recommend/presentation/dto/MarketingTipResponse.java +++ b/ai-recommend/src/main/java/com/won/smarketing/recommend/presentation/dto/MarketingTipResponse.java @@ -2,6 +2,7 @@ package com.won.smarketing.recommend.presentation.dto; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; @@ -14,6 +15,7 @@ import java.time.LocalDateTime; @Data @NoArgsConstructor @AllArgsConstructor +@Builder @Schema(description = "AI 마케팅 팁 생성 응답") public class MarketingTipResponse { diff --git a/build.gradle b/build.gradle index f60005e..976019c 100644 --- a/build.gradle +++ b/build.gradle @@ -18,12 +18,6 @@ subprojects { apply plugin: 'org.springframework.boot' apply plugin: 'io.spring.dependency-management' - java { - toolchain { - languageVersion = JavaLanguageVersion.of(21) - } - } - configurations { compileOnly { extendsFrom annotationProcessor @@ -32,6 +26,7 @@ subprojects { dependencies { 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-security' implementation 'org.springframework.boot:spring-boot-starter-validation' diff --git a/common/src/main/java/com/won/smarketing/common/security/JwtTokenProvider.java b/common/src/main/java/com/won/smarketing/common/security/JwtTokenProvider.java index bd3966b..d88bc8e 100644 --- a/common/src/main/java/com/won/smarketing/common/security/JwtTokenProvider.java +++ b/common/src/main/java/com/won/smarketing/common/security/JwtTokenProvider.java @@ -2,6 +2,7 @@ package com.won.smarketing.common.security; import io.jsonwebtoken.*; import io.jsonwebtoken.security.Keys; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @@ -18,19 +19,26 @@ import java.util.Date; public class JwtTokenProvider { private final SecretKey secretKey; + /** + * -- GETTER -- + * 액세스 토큰 유효시간 반환 + * + * @return 액세스 토큰 유효시간 (밀리초) + */ + @Getter private final long accessTokenValidityTime; private final long refreshTokenValidityTime; /** * JWT 토큰 프로바이더 생성자 - * + * * @param secret JWT 서명에 사용할 비밀키 * @param accessTokenValidityTime 액세스 토큰 유효시간 (밀리초) * @param refreshTokenValidityTime 리프레시 토큰 유효시간 (밀리초) */ public JwtTokenProvider(@Value("${jwt.secret}") String secret, - @Value("${jwt.access-token-validity}") long accessTokenValidityTime, - @Value("${jwt.refresh-token-validity}") long refreshTokenValidityTime) { + @Value("${jwt.access-token-validity}") long accessTokenValidityTime, + @Value("${jwt.refresh-token-validity}") long refreshTokenValidityTime) { this.secretKey = Keys.hmacShaKeyFor(secret.getBytes()); this.accessTokenValidityTime = accessTokenValidityTime; this.refreshTokenValidityTime = refreshTokenValidityTime; @@ -38,7 +46,7 @@ public class JwtTokenProvider { /** * 액세스 토큰 생성 - * + * * @param userId 사용자 ID * @return 생성된 액세스 토큰 */ @@ -47,16 +55,16 @@ public class JwtTokenProvider { Date expiryDate = new Date(now.getTime() + accessTokenValidityTime); return Jwts.builder() - .setSubject(userId) - .setIssuedAt(now) - .setExpiration(expiryDate) + .subject(userId) + .issuedAt(now) + .expiration(expiryDate) .signWith(secretKey) .compact(); } /** * 리프레시 토큰 생성 - * + * * @param userId 사용자 ID * @return 생성된 리프레시 토큰 */ @@ -65,41 +73,41 @@ public class JwtTokenProvider { Date expiryDate = new Date(now.getTime() + refreshTokenValidityTime); return Jwts.builder() - .setSubject(userId) - .setIssuedAt(now) - .setExpiration(expiryDate) + .subject(userId) + .issuedAt(now) + .expiration(expiryDate) .signWith(secretKey) .compact(); } /** * 토큰에서 사용자 ID 추출 - * + * * @param token JWT 토큰 * @return 사용자 ID */ public String getUserIdFromToken(String token) { - Claims claims = Jwts.parserBuilder() - .setSigningKey(secretKey) + Claims claims = Jwts.parser() + .verifyWith(secretKey) .build() - .parseClaimsJws(token) - .getBody(); - + .parseSignedClaims(token) + .getPayload(); + return claims.getSubject(); } /** * 토큰 유효성 검증 - * + * * @param token 검증할 토큰 * @return 유효성 여부 */ public boolean validateToken(String token) { try { - Jwts.parserBuilder() - .setSigningKey(secretKey) + Jwts.parser() + .verifyWith(secretKey) .build() - .parseClaimsJws(token); + .parseSignedClaims(token); return true; } catch (SecurityException ex) { log.error("Invalid JWT signature: {}", ex.getMessage()); @@ -115,12 +123,4 @@ public class JwtTokenProvider { return false; } - /** - * 액세스 토큰 유효시간 반환 - * - * @return 액세스 토큰 유효시간 (밀리초) - */ - public long getAccessTokenValidityTime() { - return accessTokenValidityTime; - } -} +} \ No newline at end of file diff --git a/member/src/main/java/com/won/smarketing/member/dto/LogoutRequest.java b/member/src/main/java/com/won/smarketing/member/dto/LogoutRequest.java index d53f388..99008bf 100644 --- a/member/src/main/java/com/won/smarketing/member/dto/LogoutRequest.java +++ b/member/src/main/java/com/won/smarketing/member/dto/LogoutRequest.java @@ -6,7 +6,7 @@ import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; /** * 로그아웃 요청 DTO diff --git a/member/src/main/java/com/won/smarketing/member/entity/Member.java b/member/src/main/java/com/won/smarketing/member/entity/Member.java index 55c5c7c..cb76902 100644 --- a/member/src/main/java/com/won/smarketing/member/entity/Member.java +++ b/member/src/main/java/com/won/smarketing/member/entity/Member.java @@ -4,26 +4,79 @@ import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstruct -재시도 -Y -계속 -편집 -Member 서비스 모든 클래스 구현 -코드 ∙ 버전 2 - /** - * 사용자 ID로 회원 조회 - * - * @param userId 사용자 ID - * @return 회원 정보 (Optional) - */ - Optional findByUserId(String userId); - +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; + +/** + * 회원 엔티티 + * 회원의 기본 정보와 사업자 정보를 관리 + */ +@Entity +@Table(name = "members") +@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 - * @return 존재 여부 - -Member 인증 서비스 구현체 및 Controllers -코드 \ No newline at end of file + * 회원 정보 업데이트 + * + * @param name 이름 + * @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; + } + } + + /** + * 패스워드 변경 + * + * @param encodedPassword 암호화된 패스워드 + */ + public void changePassword(String encodedPassword) { + this.password = encodedPassword; + } +} diff --git a/member/src/main/java/com/won/smarketing/member/service/AuthServiceImpl.java b/member/src/main/java/com/won/smarketing/member/service/AuthServiceImpl.java index d75a828..c01646f 100644 --- a/member/src/main/java/com/won/smarketing/member/service/AuthServiceImpl.java +++ b/member/src/main/java/com/won/smarketing/member/service/AuthServiceImpl.java @@ -2,21 +2,174 @@ package com.won.smarketing.member.service; import com.won.smarketing.common.exception.BusinessException; import com.won.smarketing.common.exception.ErrorCode; -import com. -재시도 -Y -계속 -편집 -Member 인증 서비스 구현체 및 Controllers -코드 ∙ 버전 2 - // 새로운 리프레시 토큰을 Redis에 저장 - redisTemplate.opsForValue().set( - REFRESH_TOKEN_PREFIX + userId, - newRefreshToken, - 7, - TimeUnit.DAYS - ); - - // 기존 리프레시 토큰을 -Store 서비스 Entity 및 DTO 클래스들 -코드 \ No newline at end of file +import com.won.smarketing.common.security.JwtTokenProvider; +import com.won.smarketing.member.dto.LoginRequest; +import com.won.smarketing.member.dto.LoginResponse; +import com.won.smarketing.member.dto.TokenResponse; +import com.won.smarketing.member.entity.Member; +import com.won.smarketing.member.repository.MemberRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.concurrent.TimeUnit; + +/** + * 인증 서비스 구현체 + * 로그인, 로그아웃, 토큰 갱신 기능 구현 + */ +@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 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(); + } +} \ No newline at end of file