Skip to content
Open
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: 2 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,12 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'com.jcraft:jsch:0.1.55'
implementation 'org.springframework.boot:spring-boot-starter-mail'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'


implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'

// jwt
implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
Expand Down
13 changes: 13 additions & 0 deletions src/main/java/io/oopy/coding/common/config/RedisConfig.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package io.oopy.coding.common.config;

import io.oopy.coding.common.redis.organizationcert.OrganizationCertification;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
@EnableRedisRepositories
Expand All @@ -24,4 +28,13 @@ public RedisConnectionFactory redisConnectionFactory() {
LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder().build();
return new LettuceConnectionFactory(config, clientConfig);
}

@Bean
public RedisTemplate<String, OrganizationCertification> redisTemplate() {
RedisTemplate<String, OrganizationCertification> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
redisTemplate.setKeySerializer(new StringRedisSerializer()); // Key: String
redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(OrganizationCertification.class)); // Value: 직렬화에 사용할 Object 사용하기
return redisTemplate;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ public class SecurityConfig {
"/api/v1/auth/login/**", "/api/v1/auth/signup",
"/api/v1/contents/get", "api/v1/comments/get",
"/api/v1/profile/**",
"/login/oauth2/**", "/api/v1/feed/title", "/api/v1/feed/body", "/api/v1/feed/nickname"
"/login/oauth2/**", "/api/v1/feed/title", "/api/v1/feed/body",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이게 뭐하는 api인지는 모르겠지만, /api/vi/feed/title, /api/v1/feed/body보다 QueryParam으로 받는 게 낫지 않을까요?
예를 들어, /api/v1/feed?type=title이라던가. 그러면 api도 하나로 줄일 수 있을 것 같아서요.

"/api/v1/email-cert/cert/**", "/api/v1/feed/nickname"
};

@Bean
Expand Down
41 changes: 41 additions & 0 deletions src/main/java/io/oopy/coding/common/constant/EmailConstant.java

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

와, 이걸 이렇게 써버리네요 ㅋㅋㅋㅋㅋㅋ 재밌는 거 배워갑니다.

Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package io.oopy.coding.common.constant;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class EmailConstant {
public static final Integer EMAIL_EXPIRED_PERIOD_SECONDS = 600; // 임시 10분
public static final String CERT_TITLE = String.format("[%s] 조직 이메일 인증", GlobalConstant.PROJECT_NAME);
public static final String CERT_CONTENT = "<head>\n" +
"<meta charset=\"UTF-8\">\n" +
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n" +
"<style>\n" +
" .link-button {\n" +
" display: inline-block;\n" +
" padding: 10px 20px;\n" +
" background-color: #3498db;\n" +
" color: #fff;\n" +
" text-align: center;\n" +
" text-decoration: none;\n" +
" border-radius: 5px;\n" +
" border: none;\n" +
" cursor: pointer;\n" +
" transition: background-color 0.3s;\n" +
" cursor: pointer\n"+
" }\n" +
" .text-color {\n" +
" color: #fff;\n" +
" }\n" +
"\n" +
" .link-button:hover {\n" +
" background-color: #2980b9;\n" +
" }\n" +
"</style>\n" +
"</head>\n" +
"<body>\n" +
"<a href=%s/api/v1/email-cert/cert/%s/%s/%s target=\"_blank\" class=\"link-button\"><div class=\"text-color\">인증하기</div></a>\n" +
"\n" +
"</body>\n" +
"</html>";
}

@psychology50 psychology50 Dec 26, 2023

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

근데 상수면 ENUM 쓰는 게 좋을 것 같은데, 전부 class를 사용하신 이유가 있는 건지 궁금해요

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package io.oopy.coding.common.constant;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class GlobalConstant {
public static final String PROJECT_NAME = "팔만코딩경";
}
22 changes: 22 additions & 0 deletions src/main/java/io/oopy/coding/common/email/dto/EmailSend.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package io.oopy.coding.common.email.dto;

import lombok.*;

@Getter
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class EmailSend {

private String targetEmail;
@Setter
private String title;
@Setter
private String content;

public static EmailSend of(String targetEmail, String title, String content) {
return new EmailSend(targetEmail,
title,
content
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.oopy.coding.common.email.service;

import io.oopy.coding.common.email.dto.EmailSend;
import jakarta.mail.Message;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import lombok.RequiredArgsConstructor;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;

/**
* email db를 만들 가능성이 있으므로, service
*/
@Service
@RequiredArgsConstructor
public class EmailService {

private final JavaMailSender javaMailSender;
private static final String MAIL_CHARACTER_SET = "utf-8";
private static final String MAIL_SUB_TYPE = "html";

public void sendMail(EmailSend emailSend) throws MessagingException {
MimeMessage message = javaMailSender.createMimeMessage();
message.addRecipients(Message.RecipientType.TO, emailSend.getTargetEmail());
message.setSubject(emailSend.getTitle());
message.setText(emailSend.getContent(), MAIL_CHARACTER_SET, MAIL_SUB_TYPE);
javaMailSender.send(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package io.oopy.coding.common.redis.organizationcert;

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import lombok.*;

import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;

@Getter
@NoArgsConstructor
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@EqualsAndHashCode
public class OrganizationCertification {

private String id;
private String certToken;
private String userEmail;
private String organizationCode;
private String organizationName;
@JsonSerialize(using = LocalDateTimeSerializer.class)
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
private LocalDateTime expiredAt;
private Boolean expired;

@Builder
private OrganizationCertification(String certToken, String id, String userEmail,
String organizationCode, String organizationName, long ttl) {
this.certToken = certToken;
this.id = id;
this.userEmail = userEmail;
this.organizationCode = organizationCode;
this.organizationName = organizationName;
this.expiredAt = LocalDateTime.now().plus(ttl, ChronoUnit.SECONDS);
}

public static OrganizationCertification of(String certToken, String id, String userEmail,
String organizationCode, String organizationName, long ttl) {
return new OrganizationCertification(certToken, id, userEmail,
organizationCode, organizationName, ttl);
}

public boolean isExpired() {
return this.expiredAt.isAfter(LocalDateTime.now());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package io.oopy.coding.common.redis.organizationcert;

import org.springframework.data.repository.CrudRepository;

public interface OrganizationCertificationRepository extends CrudRepository<OrganizationCertification, String> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package io.oopy.coding.common.redis.organizationcert;

import io.oopy.coding.common.redis.organizationcert.dto.OrganizationCertificationDto;
import io.oopy.coding.common.redis.organizationcert.dto.OrganizationCertKey;
import io.oopy.coding.common.redis.organizationcert.dto.OrganizationKey;
import io.oopy.coding.organization.service.OrganizationService;
import io.oopy.coding.organization.service.dto.OrganizationDto;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.ListOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ObjectUtils;

import java.time.ZoneId;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

/**
* 조직 인증 관련 서비스. Redis를 사용
*/
@Service
@Slf4j
public class OrganizationCertificationService {

private final ListOperations<String, OrganizationCertification> listOperations;
private final RedisTemplate<String, OrganizationCertification> redisTemplate;
private final OrganizationService organizationService;
private static final long EXPIRED_TIME = 600;

public OrganizationCertificationService(RedisTemplate<String, OrganizationCertification> redisTemplate,
OrganizationService organizationService) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이건 궁금한 건데, @Autowired 안 붙여줘도 자동으로 Bean 주입이 되나요???

this.listOperations = redisTemplate.opsForList();
this.redisTemplate = redisTemplate;
this.organizationService = organizationService;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OrganizationService가 Redis Service를 참조하는 게 아니라,
Redis Service가 OrganizationService를 컴포지션하고 있네요..?
나중에 의존 관계가 헷갈리지 않을까..살짝 우려되는 부분

}

/**
* 인증 진행중인 조직 정보 리스트 조회
* @param organizationCertKey
* @return
*/
@Transactional(readOnly = true)
public List<OrganizationCertification> getOrganizationCertifications(OrganizationKey organizationCertKey) {
List<OrganizationCertification> organizationCertifications = listOperations.range(organizationCertKey.getKey(), 0, -1);
if (ObjectUtils.isEmpty(organizationCertifications))
return new ArrayList<>();
List<OrganizationCertification> filteredOrganizationCertifications = organizationCertifications.stream()
.filter(v -> v.isExpired())
.collect(Collectors.toList());
return ObjectUtils.isEmpty(filteredOrganizationCertifications) ?
new ArrayList<>() :
filteredOrganizationCertifications;
}

/**
* 조직 인증 등록. 만료시간 : 600초
* @param register
*/
@Transactional
public void register(OrganizationCertificationDto.Register register) {
if(organizationService.isUserAlreadyRegistered(
OrganizationDto.Cert.of(register.getOrganizationCode(),
Long.parseLong(register.userId()),
register.userEmail()
))) {
throw new RuntimeException("이미 인증된 조직입니다.");
}
Optional<OrganizationCertification> checkOrganizationCertification = getOrganizationCertification(register);
if (checkOrganizationCertification.isPresent()) {
throw new RuntimeException("인증 진행중입니다.");
}
OrganizationCertification organizationCertification = OrganizationCertification.of(register.token(),
register.userId(),
register.userEmail(),
register.code(),
register.organizationName(),
EXPIRED_TIME
);
listOperations.rightPush(register.getKey(), organizationCertification);
ZoneId zoneId = ZoneId.systemDefault();
redisTemplate.expireAt(register.getKey(),
organizationCertification.getExpiredAt().atZone(zoneId).toInstant());
}

/**
* 조직 인증 삭제
* @param organizationCertKey
*/
@Transactional
public void delete(OrganizationCertKey organizationCertKey) {
Optional<OrganizationCertification> organizationCertification = getOrganizationCertification(organizationCertKey);
if (organizationCertification.isEmpty()) {
throw new RuntimeException("인증 내역 없음.");
}
listOperations.remove(organizationCertKey.getKey(), 0, organizationCertification.get());
}

/**
* 진행 중인 조직 정보 리스트 중에, 특정 조직 하나를 뽑아낸다.
* @param cert
* @return
*/
public Optional<OrganizationCertification> getOrganizationCertification(OrganizationCertKey cert) {
List<OrganizationCertification> organizationCertificationList = getOrganizationCertifications(cert);
if (ObjectUtils.isEmpty(organizationCertificationList)) {
return Optional.empty();
}
Optional<OrganizationCertification> organizationCertification = organizationCertificationList.stream()
.filter(v -> v.getOrganizationCode() != null &&
v.getOrganizationCode().equals(cert.getOrganizationCode()))
.findFirst();
return organizationCertification;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package io.oopy.coding.common.redis.organizationcert.dto;

public interface OrganizationCertKey extends OrganizationKey{
String getOrganizationCode();
}
Loading