Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ dependencies {
//SOLAPI
implementation 'net.nurigo:sdk:4.2.7'

//Kafka
implementation 'org.springframework.kafka:spring-kafka:3.0.7'

// //Querydsl 추가
// implementation 'com.querydsl:querydsl-core:5.0.0'
// implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
Expand Down
25 changes: 25 additions & 0 deletions compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
version: '3'
services:
zookeeper:
image: confluentinc/cp-zookeeper:latest
container_name: zookeeper
ports:
- "2181:2181"
environment:
ZOOKEEPER_CLIENT_PORT: 2181
ZOOKEEPER_TICK_TIME: 2000

kafka:
image: confluentinc/cp-kafka:latest
container_name: kafka
depends_on:
- zookeeper
ports:
- "9092:9092"
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
# 클러스터 내부와 외부에서 접근할 listener 설정 (내부: kafka:29092, 외부: localhost:9092)
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Builder
@Table(name = "USER")
@Table(name = "USER", indexes = {
@Index(name = "idx_email", columnList = "email"),
@Index(name = "idx_refresh_token", columnList = "refresh_token"),
@Index(name = "idx_social_type_id", columnList = "social_type, social_id")
})
@AllArgsConstructor
@Slf4j
public class UserInfo extends BaseTimeEntity {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.EntityGraph;

import com.readyvery.readyverydemo.domain.Order;
import com.readyvery.readyverydemo.domain.Progress;
Expand All @@ -20,6 +21,9 @@ public interface OrdersRepository extends JpaRepository<Order, Long> {

Optional<List<Order>> findAllByUserInfo(UserInfo userInfo);

@EntityGraph(attributePaths = {"store", "cart", "userInfo", "receipt"})
Optional<List<Order>> findAllWithAssociationsByUserInfo(UserInfo userInfo);

Long countByCreatedAtBetweenAndProgressNotAndStore(LocalDateTime localDateTime, LocalDateTime localDateTime1,
Progress progress, Store store);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.UUID;

import org.springframework.stereotype.Component;

Expand Down Expand Up @@ -40,6 +41,7 @@ public String generateRefreshToken() {
return JWT.create()
.withSubject(REFRESH_TOKEN_SUBJECT)
.withExpiresAt(Date.from(expirationTime))
.withJWTId(UUID.randomUUID().toString()) // 고유값 추가로 중복 방지
.sign(jwtConfig.getAlgorithm());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ private void loginSuccess(HttpServletResponse response, CustomOAuth2User oAuth2U
String refreshToken = jwtService.createRefreshToken();

jwtService.sendAccessAndRefreshToken(response, accessToken, refreshToken, role);
// RefreshTokenRepository를 직접 사용하지 말고 반드시 RefreshTokenService를 통해서만 접근하세요. (key-value 방식으로 변경됨)
refreshTokenServiceImpl.saveRefreshTokenInRedis(oAuth2User.getEmail(), refreshToken);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.readyvery.readyverydemo.src.order;

import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;

import com.readyvery.readyverydemo.src.order.dto.OrderUpdateMessage;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class OrderEventProducer {

private final KafkaTemplate<String, OrderUpdateMessage> kafkaTemplate;

public void sendOrderUpdate(OrderUpdateMessage message) {
// 예: "order_updates"라는 토픽 사용
kafkaTemplate.send("order_updates", message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
import com.readyvery.readyverydemo.src.order.dto.HistoryDetailRes;
import com.readyvery.readyverydemo.src.order.dto.HistoryRes;
import com.readyvery.readyverydemo.src.order.dto.OrderMapper;
import com.readyvery.readyverydemo.src.order.dto.OrderUpdateMessage;
import com.readyvery.readyverydemo.src.order.dto.PaySuccess;
import com.readyvery.readyverydemo.src.order.dto.PaymentReq;
import com.readyvery.readyverydemo.src.order.dto.TossCancelReq;
Expand Down Expand Up @@ -97,6 +98,7 @@ public class OrderServiceImpl implements OrderService {
private final PointRepository pointRepository;
private final PointServiceFacade pointServiceFacade;
private final OrderNumberSequenceRepository orderNumberSequenceRepository;
private final OrderEventProducer orderEventProducer;

@Override
public FoodyDetailRes getFoody(Long storeId, Long foodyId, Long inout) {
Expand Down Expand Up @@ -332,6 +334,16 @@ public PaySuccess tossPaymentSuccess(String paymentKey, String orderId, Long amo
if (!Objects.equals(order.getMessage(), TOSSPAYMENT_SUCCESS_MESSAGE)) {
return orderMapper.tosspaymentDtoToPaySuccess(order.getMessage());
}

OrderUpdateMessage message = new OrderUpdateMessage();
message.setOrderId(order.getOrderId());
message.setStoreId(order.getStore().getId()); // 중요!
message.setStatus(order.getProgress().name()); // "ORDER"
message.setMessage("주문 상태가 " + order.getProgress().name() + " 로 변경되었습니다.");

// 4. 메시지 발행
orderEventProducer.sendOrderUpdate(message);

//TODO: 영수증 처리
Receipt receipt = orderMapper.tosspaymentDtoToReceipt(tosspaymentDto, order);
receiptRepository.save(receipt);
Expand Down Expand Up @@ -543,7 +555,7 @@ private void verifyOrderCurrent(Order order) {
}

private List<Order> getOrders(UserInfo user) {
return ordersRepository.findAllByUserInfo(user).orElseThrow(
return ordersRepository.findAllWithAssociationsByUserInfo(user).orElseThrow(
() -> new BusinessLogicException(ExceptionCode.ORDER_NOT_FOUND));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.readyvery.readyverydemo.src.order.dto;

import lombok.Data;

@Data
public class OrderUpdateMessage {
private String orderId; // 주문 식별자
private Long storeId; // 가게의 번호
private String status; // 주문 상태 (ORDER, MAKE, COMPLETE 등)
private String message; // 기타 메시지
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,53 @@

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.concurrent.TimeUnit;
import java.time.LocalDateTime;
import com.readyvery.readyverydemo.src.refreshtoken.fallback.RefreshTokenFallback;
import com.readyvery.readyverydemo.src.refreshtoken.fallback.RefreshTokenFallbackRepository;

@Service
@RequiredArgsConstructor
@Slf4j
public class RefreshTokenServiceImpl implements RefreshTokenService {

private final RefreshTokenRepository refreshTokenRepository;
private final RedisTemplate<String, String> redisTemplate;
private final RefreshTokenFallbackRepository fallbackRepository;
private static final String REFRESH_TOKEN_PREFIX = "refreshToken:";
private static final long REFRESH_TOKEN_TTL = 60L * 60 * 24 * 7;

@Override
public void removeRefreshTokenInRedis(String email) {
RefreshToken refreshToken = refreshTokenRepository.findById(email)
.orElseThrow(() -> new UsernameNotFoundException("해당 이메일이 존재하지 않습니다."));
refreshTokenRepository.delete(refreshToken);
try {
redisTemplate.delete(REFRESH_TOKEN_PREFIX + email);
} catch (Exception e) {
log.error("Redis 삭제 실패, DB fallback 사용: {}", e.getMessage());
fallbackRepository.deleteById(email);
}
}

@Override
public void saveRefreshTokenInRedis(String email, String refreshToken) {
RefreshToken token = RefreshToken.builder()
.id(email)
.refreshToken(refreshToken)
.build();
refreshTokenRepository.save(token);
try {
redisTemplate.opsForValue().set(REFRESH_TOKEN_PREFIX + email, refreshToken, REFRESH_TOKEN_TTL, TimeUnit.SECONDS);
} catch (Exception e) {
log.error("Redis 저장 실패, DB fallback 사용: {}", e.getMessage());
fallbackRepository.save(
new RefreshTokenFallback(email, refreshToken, LocalDateTime.now().plusSeconds(REFRESH_TOKEN_TTL))
);
}
}

public String getRefreshToken(String email) {
try {
return redisTemplate.opsForValue().get(REFRESH_TOKEN_PREFIX + email);
} catch (Exception e) {
log.error("Redis 조회 실패, DB fallback 사용: {}", e.getMessage());
return fallbackRepository.findById(email)
.filter(token -> token.getExpiresAt().isAfter(LocalDateTime.now()))
.map(RefreshTokenFallback::getRefreshToken)
.orElse(null);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.readyvery.readyverydemo.src.refreshtoken.fallback;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import java.time.LocalDateTime;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class RefreshTokenFallback {
@Id
private String userId;
private String refreshToken;
private LocalDateTime expiresAt;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.readyvery.readyverydemo.src.refreshtoken.fallback;

import org.springframework.data.jpa.repository.JpaRepository;

public interface RefreshTokenFallbackRepository extends JpaRepository<RefreshTokenFallback, String> {
}
Loading