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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: CI

on:
pull_request:
branches: [main]
branches: [main, develop]

jobs:
test:
Expand Down
7 changes: 4 additions & 3 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,16 +108,17 @@ jobs:
# 6. app๋งŒ ๊ต์ฒด (nginx/certbot์€ ๊ทธ๋Œ€๋กœ ์œ ์ง€ โ€” ๋ฌด์ค‘๋‹จ ๊ฐ€๊นŒ์›€)
sudo docker compose -f "$COMPOSE" up -d --no-deps app

# 7. ํ—ฌ์Šค์ฒดํฌ (Docker healthcheck ์ƒํƒœ polling, ์ตœ๋Œ€ 90์ดˆ)
# 7. ํ—ฌ์Šค์ฒดํฌ (Docker healthcheck ์ƒํƒœ polling, ์ตœ๋Œ€ 180์ดˆ)
# OTel Agent + JPA 16๊ฐœ repo + Tomcat ๋“ฑ์œผ๋กœ ๋ถ€ํŒ…์— ์•ฝ 90์ดˆ ์†Œ์š”๋จ
echo "Waiting for app to become healthy..."
for i in $(seq 1 18); do
for i in $(seq 1 36); do
STATUS=$(sudo docker inspect --format='{{.State.Health.Status}}' catchtable-app 2>/dev/null || echo "starting")
if [ "$STATUS" = "healthy" ]; then
echo "โœ… App is healthy"
sudo docker image prune -f
exit 0
fi
echo " ($i/18) status=$STATUS"
echo " ($i/36) status=$STATUS"
sleep 5
done

Expand Down
13 changes: 9 additions & 4 deletions docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,20 +37,25 @@ services:
KAFKA_INTER_BROKER_LISTENER_NAME: 'PLAINTEXT'

KAFKA_CONTROLLER_QUORUM_VOTERS: '1@127.0.0.1:9093'
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1

volumes:
- kafka-data-dev:/tmp/kafka-logs
networks:
- catchtable-dev-net

networks:
catchtable-dev-net:
driver: bridge

redis:
image: redis:7-alpine
container_name: catchtable-redis
ports:
- "${REDIS_PORT:-6379}:6379"
networks:
- catchtable-dev-net

networks:
catchtable-dev-net:
driver: bridge

volumes:
catchtable-data:
Expand Down
7 changes: 6 additions & 1 deletion nginx/nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ http {
keepalive_timeout 65;
client_max_body_size 10m;

# Docker ๋‚ด์žฅ DNS โ€” app ์ปจํ…Œ์ด๋„ˆ ์žฌ์ƒ์„ฑ ์‹œ nginx ์žฌ์‹œ์ž‘ ์—†์ด ์ƒˆ IP ์ž๋™ ์ธ์‹
resolver 127.0.0.11 valid=10s ipv6=off;

# โ”€โ”€ HTTP (80) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# Let's Encrypt ์ธ์ฆ + HTTPS ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ
server {
Expand Down Expand Up @@ -59,7 +62,9 @@ http {
}

location / {
proxy_pass http://app:8080;
# ๋ณ€์ˆ˜ ์‚ฌ์šฉ ์‹œ nginx๊ฐ€ ๋งค ์š”์ฒญ๋งˆ๋‹ค resolver๋กœ DNS ์žฌํ•ด์„
set $backend_app "app:8080";
proxy_pass http://$backend_app;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ public enum ErrorCode implements ResponseCode {
REMAIN_NOT_FOUND(HttpStatus.NOT_FOUND, "์กด์žฌํ•˜์ง€ ์•Š๋Š” ์˜ˆ์•ฝ ์‹œ๊ฐ„๋Œ€์ž…๋‹ˆ๋‹ค."),
REMAIN_EXHAUSTED(HttpStatus.BAD_REQUEST, "ํ•ด๋‹น ์‹œ๊ฐ„๋Œ€์˜ ์˜ˆ์•ฝ์ด ๋งˆ๊ฐ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."),
OPTIMISTIC_LOCK_CONFLICT(HttpStatus.CONFLICT, "์ด๋ฏธ ๋‹ค๋ฅธ ์‚ฌ์šฉ์ž๊ฐ€ ์˜ˆ์•ฝํ•˜์—ฌ ๋งˆ๊ฐ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”."),
LOCK_TIMEOUT(HttpStatus.CONFLICT, "์˜ˆ์•ฝ ์š”์ฒญ์ด ๋งŽ์•„ ์ฒ˜๋ฆฌํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”."),

// Review
REVIEW_NOT_FOUND(HttpStatus.NOT_FOUND, "์กด์žฌํ•˜์ง€ ์•Š๋Š” ๋ฆฌ๋ทฐ์ž…๋‹ˆ๋‹ค."),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.catchtable.global.lock;

import com.catchtable.global.exception.CustomException;
import com.catchtable.global.exception.ErrorCode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

@Slf4j
@Component
@RequiredArgsConstructor
public class DistributedLockExecutor {

private final RedissonClient redissonClient;

/**
* ๋ถ„์‚ฐ ๋ฝ์„ ์žก์€ ๋’ค task๋ฅผ ์‹คํ–‰ํ•˜๊ณ , ๋๋‚˜๋ฉด ๋ฝ์„ ํ•ด์ œํ•œ๋‹ค.
*
* @param lockKey ๋ฝ ํ‚ค. ๊ฐ™์€ ํ‚ค๋ผ๋ฆฌ๋งŒ ์ง๋ ฌํ™”๋œ๋‹ค.
* @param waitSeconds ๋ฝ์„ ์žก์œผ๋ ค๊ณ  ๊ธฐ๋‹ค๋ฆฌ๋Š” ์ตœ๋Œ€ ์‹œ๊ฐ„(์ดˆ).
* @param leaseSeconds ๋ฝ์„ ์žก์€ ๋’ค ์ž๋™์œผ๋กœ ํ’€๋ฆฌ๋Š” ์‹œ๊ฐ„(์ดˆ). ์ž‘์—…์ด ์ฃฝ์–ด๋„ ์ด ์‹œ๊ฐ„ ํ›„ ํ•ด์ œ๋œ๋‹ค.
* @param task ๋ฝ ์•ˆ์—์„œ ์‹คํ–‰ํ•  ์ž‘์—….
*/
public <T> T executeWithLock(String lockKey, long waitSeconds, long leaseSeconds, Supplier<T> task) {
RLock lock = redissonClient.getLock(lockKey);
boolean acquired = false;
try {
acquired = lock.tryLock(waitSeconds, leaseSeconds, TimeUnit.SECONDS);
if (!acquired) {
log.warn("๋ถ„์‚ฐ ๋ฝ ํš๋“ ์‹คํŒจ (๋Œ€๊ธฐ {}์ดˆ ์ดˆ๊ณผ): {}", waitSeconds, lockKey);
throw new CustomException(ErrorCode.LOCK_TIMEOUT);
}
return task.get();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new CustomException(ErrorCode.LOCK_TIMEOUT);
} finally {
if (acquired && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
package com.catchtable.notification.event;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.NoArgsConstructor;

@Getter
@RequiredArgsConstructor
@NoArgsConstructor
@AllArgsConstructor
public class ReservationCanceledEvent {
private final Long reservationId;
private final Long userId;
private final String storeName;
private final String remainDate;
private final String remainTime;
private Long reservationId;
private Long userId;
private String storeName;
private String remainDate;
private String remainTime;
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
package com.catchtable.notification.event;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.NoArgsConstructor;

@Getter
@RequiredArgsConstructor
@NoArgsConstructor
@AllArgsConstructor
public class ReservationChangedEvent {
private final Long newReservationId;
private final Long userId;
private final String storeName;
private final String oldRemainDate;
private final String oldRemainTime;
private final String newRemainDate;
private final String newRemainTime;
private Long newReservationId;
private Long userId;
private String storeName;
private String oldRemainDate;
private String oldRemainTime;
private String newRemainDate;
private String newRemainTime;
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
package com.catchtable.notification.event;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.NoArgsConstructor;

@Getter
@RequiredArgsConstructor
@NoArgsConstructor
@AllArgsConstructor
public class ReservationConfirmedEvent {
private final Long reservationId;
private final Long userId;
private final String storeName;
private final String remainDate;
private final String remainTime;
private Long reservationId;
private Long userId;
private String storeName;
private String remainDate;
private String remainTime;
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
package com.catchtable.notification.event;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.NoArgsConstructor;

@Getter
@RequiredArgsConstructor
@NoArgsConstructor
@AllArgsConstructor
public class ReservationReminderEvent {
private final Long reservationId;
private final Long userId;
private final String storeName;
private final String remainDate;
private final String remainTime;
private Long reservationId;
private Long userId;
private String storeName;
private String remainDate;
private String remainTime;
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
package com.catchtable.notification.event;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.NoArgsConstructor;

@Getter
@RequiredArgsConstructor
@NoArgsConstructor
@AllArgsConstructor
public class ReservationVisitedEvent {
private final Long reservationId;
private final Long userId;
private final String storeName;
private final String remainDate;
private final String remainTime;
private Long reservationId;
private Long userId;
private String storeName;
private String remainDate;
private String remainTime;
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package com.catchtable.notification.event;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.NoArgsConstructor;

@Getter
@RequiredArgsConstructor
@NoArgsConstructor
@AllArgsConstructor
public class VacancyEvent {
private final Long remainId;
}
private Long remainId;
}
Original file line number Diff line number Diff line change
@@ -1,50 +1,50 @@
package com.catchtable.notification.listener;

import com.catchtable.global.exception.CustomException;
import com.catchtable.global.exception.ErrorCode;
import com.catchtable.notification.entity.NotificationType;
import com.catchtable.notification.event.ReservationCanceledEvent;
import com.catchtable.notification.service.NotificationService;
import com.catchtable.user.entity.User;
import com.catchtable.user.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;

@Slf4j
@Component
@RequiredArgsConstructor
public class ReservationCanceledNotificationListener {

private final NotificationService notificationService;
private final UserRepository userRepository;

@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void handleReservationCanceledEvent(ReservationCanceledEvent event) {
User user = userRepository.findById(event.getUserId())
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));

String title = "์˜ˆ์•ฝ์ด ์ทจ์†Œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.";
String content = String.format("'%s' ๋งค์žฅ %s %s ์˜ˆ์•ฝ์ด ์ทจ์†Œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.",
event.getStoreName(),
event.getRemainDate(),
event.getRemainTime());

notificationService.createNotification(
user,
NotificationType.RESERVATION_CANCELED,
title,
content,
event.getReservationId()
);

log.info("[์˜ˆ์•ฝ ์ทจ์†Œ ์•Œ๋ฆผ] userId: {}, reservationId: {} ์•Œ๋ฆผ ์ƒ์„ฑ ์™„๋ฃŒ", event.getUserId(), event.getReservationId());
}
}
//package com.catchtable.notification.listener;
//
//import com.catchtable.global.exception.CustomException;
//import com.catchtable.global.exception.ErrorCode;
//import com.catchtable.notification.entity.NotificationType;
//import com.catchtable.notification.event.ReservationCanceledEvent;
//import com.catchtable.notification.service.NotificationService;
//import com.catchtable.user.entity.User;
//import com.catchtable.user.repository.UserRepository;
//import lombok.RequiredArgsConstructor;
//import lombok.extern.slf4j.Slf4j;
//import org.springframework.scheduling.annotation.Async;
//import org.springframework.stereotype.Component;
//import org.springframework.transaction.annotation.Propagation;
//import org.springframework.transaction.annotation.Transactional;
//import org.springframework.transaction.event.TransactionPhase;
//import org.springframework.transaction.event.TransactionalEventListener;
//
//@Slf4j
//@Component
//@RequiredArgsConstructor
//public class ReservationCanceledNotificationListener {
//
// private final NotificationService notificationService;
// private final UserRepository userRepository;
//
// @Async
// @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
// @Transactional(propagation = Propagation.REQUIRES_NEW)
// public void handleReservationCanceledEvent(ReservationCanceledEvent event) {
// User user = userRepository.findById(event.getUserId())
// .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));
//
// String title = "์˜ˆ์•ฝ์ด ์ทจ์†Œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.";
// String content = String.format("'%s' ๋งค์žฅ %s %s ์˜ˆ์•ฝ์ด ์ทจ์†Œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.",
// event.getStoreName(),
// event.getRemainDate(),
// event.getRemainTime());
//
// notificationService.createNotification(
// user,
// NotificationType.RESERVATION_CANCELED,
// title,
// content,
// event.getReservationId()
// );
//
// log.info("[์˜ˆ์•ฝ ์ทจ์†Œ ์•Œ๋ฆผ] userId: {}, reservationId: {} ์•Œ๋ฆผ ์ƒ์„ฑ ์™„๋ฃŒ", event.getUserId(), event.getReservationId());
// }
//}
Loading
Loading