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
12 changes: 6 additions & 6 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=your_redis_password

# RabbitMQ
RABBITMQ_HOST=localhost
RABBITMQ_PORT=5672
RABBITMQ_USERNAME=your_username
RABBITMQ_PASSWORD=your_password

# S3
S3_ENDPOINT=http://localhost:9000
S3_REGION=ap-northeast-2
Expand All @@ -28,12 +34,6 @@ JWT_REFRESH_TOKEN_EXPIRATION=1209600000
JWT_COOKIE_SECURE=false
JWT_REFRESH_COOKIE_PATH=/api/v1/auth

# Mail
MAIL_HOST=smtp.gmail.com
MAIL_PORT=587
MAIL_USERNAME=your_email@gmail.com
MAIL_PASSWORD=your_app_password

# Encryption
ENCRYPT_SECRET_KEY=your_encrypt_secret_key
ENCRYPT_HASH_KEY=your_encrypt_hash_key
Expand Down
9 changes: 3 additions & 6 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,14 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.redisson:redisson-spring-boot-starter:4.6.1'

// rabbitMQ
implementation 'org.springframework.boot:spring-boot-starter-amqp'

// jwt
implementation 'io.jsonwebtoken:jjwt-api:0.13.0'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.13.0'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.13.0'

// email
implementation 'org.springframework.boot:spring-boot-starter-mail'

// thymeleaf
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'

// swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.3'

Expand Down
27 changes: 26 additions & 1 deletion docker-compose.prod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ services:
depends_on:
redis:
condition: service_healthy
rabbitmq:
condition: service_healthy
networks:
- ditda-net
healthcheck:
Expand All @@ -28,6 +30,8 @@ services:
depends_on:
redis:
condition: service_healthy
rabbitmq:
condition: service_healthy
networks:
- ditda-net
healthcheck:
Expand Down Expand Up @@ -57,6 +61,25 @@ services:
timeout: 3s
retries: 5

rabbitmq:
image: rabbitmq:4.3.2-management-alpine
container_name: ditda-rabbitmq
restart: unless-stopped
environment:
RABBITMQ_DEFAULT_USER: ${RABBITMQ_USERNAME}
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD}
volumes:
- rabbitmq-data:/var/lib/rabbitmq
networks:
- ditda-net
healthcheck:
test: [ "CMD", "rabbitmq-diagnostics", "-q", "ping" ]
interval: 10s
timeout: 5s
retries: 10
start_period: 30s


nginx:
image: nginx:1.27-alpine
container_name: ditda-nginx
Expand All @@ -82,7 +105,9 @@ services:

networks:
ditda-net:
name: ditda-net
driver: bridge

volumes:
redis-data:
redis-data:
rabbitmq-data:
21 changes: 20 additions & 1 deletion docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,24 @@ services:
volumes:
- redis-data:/data

rabbitmq:
image: rabbitmq:4.3.2-management-alpine
container_name: ditda-rabbitmq
ports:
- "127.0.0.1:5672:5672" # AMQP
- "127.0.0.1:15672:15672" # Admin UI
environment:
RABBITMQ_DEFAULT_USER: ${RABBITMQ_USERNAME}
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD}
healthcheck:
test: [ "CMD", "rabbitmq-diagnostics", "-q", "ping" ]
interval: 10s
timeout: 5s
retries: 10
start_period: 30s
volumes:
- rabbitmq-data:/var/lib/rabbitmq

minio:
image: minio/minio@sha256:a1ea29fa28355559ef137d71fc570e508a214ec84ff8083e39bc5428980b015e #RELEASE.2025-04-22T22-12-26Z
container_name: ditda-minio
Expand All @@ -67,4 +85,5 @@ services:
volumes:
mysql-data:
redis-data:
minio-data:
minio-data:
rabbitmq-data:
10 changes: 5 additions & 5 deletions scripts/deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -70,18 +70,18 @@ REQUIRED_VARS=(
"REDIS_HOST"
"REDIS_PORT"
"REDIS_PASSWORD"
# RabbitMQ
"RABBITMQ_HOST"
"RABBITMQ_PORT"
"RABBITMQ_USERNAME"
"RABBITMQ_PASSWORD"
# JWT
"JWT_SECRET_KEY"
"JWT_ACCESS_TOKEN_EXPIRATION"
"JWT_REFRESH_TOKEN_EXPIRATION"
"JWT_REFRESH_TOKEN_HASH_SECRET"
"JWT_COOKIE_SECURE"
"JWT_REFRESH_COOKIE_PATH"
# Mail
"MAIL_HOST"
"MAIL_PORT"
"MAIL_USERNAME"
"MAIL_PASSWORD"
# S3
"S3_PUBLIC_BUCKET"
"S3_PRIVATE_BUCKET"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ public enum AuthErrorCode implements BaseErrorCode {
EMAIL_VERIFICATION_COOLDOWN(HttpStatus.TOO_MANY_REQUESTS, "AUTH_429_01", "잠시 후 다시 시도해주세요."),
EMAIL_CODE_EXPIRED(HttpStatus.BAD_REQUEST, "AUTH_400_01", "인증번호가 만료되었거나 발급되지 않았습니다."),
EMAIL_CODE_INVALID(HttpStatus.BAD_REQUEST, "AUTH_400_02", "인증번호가 일치하지 않습니다."),
EMAIL_NOT_VERIFIED(HttpStatus.BAD_REQUEST, "AUTH_400_03", "이메일 인증이 완료되지 않았습니다.");
EMAIL_NOT_VERIFIED(HttpStatus.BAD_REQUEST, "AUTH_400_03", "이메일 인증이 완료되지 않았습니다."),
EMAIL_VERIFICATION_SEND_FAILED(HttpStatus.SERVICE_UNAVAILABLE, "AUTH_503_01", "인증 메일 발송에 실패했습니다. 잠시 후 다시 시도해주세요.");

private final HttpStatus httpStatus;
private final String code;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,31 @@

import java.util.Map;

import org.springframework.amqp.AmqpException;
import org.springframework.stereotype.Component;

import ditda.backend.global.notification.EmailSender;
import ditda.backend.domain.auth.exception.AuthErrorCode;
import ditda.backend.global.apipayload.exception.GeneralException;
import ditda.backend.global.notification.MailMessage;
import ditda.backend.global.notification.MailPublisher;
import ditda.backend.global.notification.NotificationType;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
@RequiredArgsConstructor
public class EmailVerificationMailer {

private final EmailSender emailSender;
private final MailPublisher mailPublisher;

public void sendVerificationCode(String to, String code) {
emailSender.sendAsync(to, NotificationType.EMAIL_VERIFICATION, Map.of("code", code));
NotificationType type = NotificationType.EMAIL_VERIFICATION;
try {
mailPublisher.publish(new MailMessage(type.name(), to, Map.of("code", code)));
} catch (AmqpException e) {
log.error("인증 메일 발행 실패.", e);
throw new GeneralException(AuthErrorCode.EMAIL_VERIFICATION_SEND_FAILED);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ private void registerAdminRefundRequest(ApplicationDeadlineClosedEvent event, Lo
"instructorName", event.instructorName(),
"instructorEmail", event.instructorEmail(),
"refundAmount", event.refundAmount(),
"isCancelled", event.cancelled()
"reason", event.cancelled() ? "APPLICATION_CANCELLED" : "APPLICATION_SHORTFALL"
),
mailScheduledAt
));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ private void registerAdminRefundRequest(
"instructorName", event.instructorName(),
"instructorEmail", event.instructorEmail(),
"refundAmount", event.refundAmount(),
"isCancelled", isCancelled
"reason", isCancelled ? "FIRST_DRAFT_ZERO" : "FIRST_DRAFT_SHORTFALL"
),
mailScheduledAt
));
Expand Down
35 changes: 35 additions & 0 deletions src/main/java/ditda/backend/global/config/RabbitConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package ditda.backend.global.config;

import org.springframework.amqp.support.converter.JacksonJsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.boot.amqp.autoconfigure.RabbitTemplateCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Configuration
public class RabbitConfig {

@Bean
public RabbitTemplateCustomizer rabbitTemplateCustomizer() {
return template -> {
template.setConfirmCallback((correlationData, ack, cause) -> {
if (!ack) {
log.error("RabbitMQ publish nack. correlationData={}, cause={}", correlationData, cause);
}
});

template.setReturnsCallback(returned -> {
log.error("RabbitMQ unroutable message. replyText={}, exchange={}, routingKey={}",
returned.getReplyText(), returned.getExchange(), returned.getRoutingKey());
});
};
}

@Bean
public MessageConverter messageConverter() {
return new JacksonJsonMessageConverter();
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package ditda.backend.global.notification;

import java.util.Map;

public record MailMessage(
String type,
String to,
Map<String, Object> variables
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package ditda.backend.global.notification;

import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;

import lombok.RequiredArgsConstructor;

@Component
@RequiredArgsConstructor
public class MailPublisher {

private final RabbitTemplate rabbitTemplate;

public void publish(MailMessage message) {
rabbitTemplate.convertAndSend(MailRabbitConfig.MAIL_EXCHANGE, MailRabbitConfig.MAIL_ROUTING_KEY, message);
}

public void publish(MailMessage message, CorrelationData correlationData) {
rabbitTemplate.convertAndSend(
MailRabbitConfig.MAIL_EXCHANGE,
MailRabbitConfig.MAIL_ROUTING_KEY,
message,
correlationData
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package ditda.backend.global.notification;

import org.springframework.amqp.core.DirectExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MailRabbitConfig {

public static final String MAIL_EXCHANGE = "mail.exchange";
public static final String MAIL_ROUTING_KEY = "mail.send";

@Bean
DirectExchange mailExchange() {
return new DirectExchange(MAIL_EXCHANGE);
}
}
Loading