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
11 changes: 11 additions & 0 deletions .github/workflows/cicd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,17 @@ jobs:
name: Test
runs-on: ubuntu-latest

services:
redis:
image: redis:7-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5

steps:
- name: Checkout
uses: actions/checkout@v4
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
package com.project.dorumdorum.domain.calendar.application.dto.response;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.project.dorumdorum.domain.calendar.domain.entity.CalendarEventType;

import java.time.LocalDate;
import java.time.LocalTime;

public record CalendarEventResponse(
LocalDate date,
String title
String title,
String content,
@JsonFormat(pattern = "HH:mm") LocalTime time,
CalendarEventType type
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
@Mapper(componentModel = "spring")
public interface CalendarEventMapper {
@Mapping(target = "date", source = "eventDate")
@Mapping(target = "time", source = "eventTime")
@Mapping(target = "type", source = "eventType")
CalendarEventResponse toResponse(CalendarEvent event);
List<CalendarEventResponse> toResponseList(List<CalendarEvent> events);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@
import jakarta.persistence.Table;
import lombok.*;

import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;

import javax.annotation.Nullable;
import java.time.LocalDate;
import java.time.LocalTime;

@Entity
@Getter
Expand All @@ -27,4 +32,14 @@ public class CalendarEvent extends BaseEntity {

@Column(nullable = false)
private String title;

@Column(nullable = false)
private String content;

@Column(nullable = false)
private LocalTime eventTime;

@Enumerated(EnumType.STRING)
@Column(nullable = false)
private CalendarEventType eventType;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.project.dorumdorum.domain.calendar.domain.entity;

public enum CalendarEventType {
CHECK, // 점호
CLEAN, // 청소
NOTICE, // 공지
EVENT // 행사
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,7 @@ public List<FindRoomsResponse> searchByCursor(
}

public FindRoomsResponse findMyRoom(String userNo) {
return roomRepository.findMyRoom(userNo)
.orElseThrow(() -> new RestApiException(ROOM_NOT_FOUND));
return roomRepository.findMyRoom(userNo).orElse(null);
}

public List<FindRoomsResponse> findLikedRooms(String userNo) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.project.dorumdorum.domain.user.application.dto.request;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;

public record ResetPasswordRequest(
@Email @NotBlank String email,
@NotBlank @Size(min = 8, message = "비밀번호는 8자 이상이어야 합니다.") @Pattern(regexp = "^[A-Za-z0-9!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>\\/?]+$", message = "비밀번호는 영문, 숫자, 특수문자만 사용할 수 있습니다.") String newPassword,
@NotBlank String newPasswordCheck
) {
Comment thread
KoungQ marked this conversation as resolved.
public boolean isPasswordMatch() {
return newPassword.equals(newPasswordCheck);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
Expand All @@ -15,7 +17,7 @@ public record SignUpRequest(
@NotBlank String name,
@NotBlank String nickname,
@Email @NotBlank String email,
@NotBlank String password,
@NotBlank @Size(min = 8, message = "비밀번호는 8자 이상이어야 합니다.") @Pattern(regexp = "^[A-Za-z0-9!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>\\/?]+$", message = "비밀번호는 영문, 숫자, 특수문자만 사용할 수 있습니다.") String password,
@NotBlank String passwordCheck,
@NotNull Gender gender,
@NotBlank String studentNo,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.project.dorumdorum.domain.user.application.usecase;

import com.project.dorumdorum.domain.user.application.dto.request.ResetPasswordRequest;
import com.project.dorumdorum.domain.user.domain.entity.User;
import com.project.dorumdorum.domain.user.domain.repository.PasswordResetVerifiedRepository;
import com.project.dorumdorum.domain.user.domain.service.UserService;
import com.project.dorumdorum.global.exception.RestApiException;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import static com.project.dorumdorum.global.exception.code.status.AuthErrorStatus.FAILED_EMAIL_VERIFICATION;
import static com.project.dorumdorum.global.exception.code.status.UserErrorStatus._PASSWORD_NOT_MATCHES;

@Service
@RequiredArgsConstructor
public class ResetPasswordUseCase {

private final UserService userService;
private final PasswordResetVerifiedRepository passwordResetVerifiedRepository;
private final PasswordEncoder passwordEncoder;

/**
* 비밀번호 재설정
* - 비밀번호 일치 여부 확인
* - 이메일 인증 완료 여부 확인
* - 비밀번호 업데이트
*/
@Transactional
public void execute(ResetPasswordRequest request) {
if (!request.isPasswordMatch()) {
throw new RestApiException(_PASSWORD_NOT_MATCHES);
}

if (!passwordResetVerifiedRepository.existsByEmail(request.email())) {
throw new RestApiException(FAILED_EMAIL_VERIFICATION);
}

User user = userService.findByEmail(request.email());
user.updatePassword(passwordEncoder.encode(request.newPassword()));
Comment thread
KoungQ marked this conversation as resolved.
passwordResetVerifiedRepository.delete(request.email());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.project.dorumdorum.domain.user.application.usecase;

import com.project.dorumdorum.domain.user.domain.service.EmailVerificationService;
import com.project.dorumdorum.domain.user.domain.service.UserService;
import com.project.dorumdorum.global.exception.RestApiException;
import com.project.dorumdorum.global.ratelimit.RateLimited;
import com.project.dorumdorum.global.util.SecureRandomGenerator;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import static com.project.dorumdorum.global.exception.code.status.AuthErrorStatus.INVALID_EMAIL_DOMAIN;

@Service
@RequiredArgsConstructor
public class SendPasswordResetEmailUseCase {

private final UserService userService;
private final EmailVerificationService emailVerificationService;
private final SecureRandomGenerator secureRandomGenerator;

/**
* 비밀번호 재설정 인증 코드 발송
* - 허용된 대학 이메일 도메인인지 검증
* - 가입된 이메일인 경우에만 실제로 코드를 발송 (미가입 이메일도 동일한 200 응답 반환)
*/
@RateLimited(tag = "password-reset-email", key = "#email")
public void send(String email) {
if (!emailVerificationService.isAllowedUniversityEmail(email)) {
throw new RestApiException(INVALID_EMAIL_DOMAIN);
}

if (!userService.isAlreadyRegistered(email)) {
return;
}

String code = secureRandomGenerator.generate();
emailVerificationService.sendCode(email, code);
Comment thread
KoungQ marked this conversation as resolved.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.project.dorumdorum.domain.user.domain.service.EmailVerificationService;
import com.project.dorumdorum.domain.user.domain.service.UserService;
import com.project.dorumdorum.global.exception.RestApiException;
import com.project.dorumdorum.global.ratelimit.RateLimited;
import com.project.dorumdorum.global.util.SecureRandomGenerator;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
Expand All @@ -24,6 +25,7 @@ public class SendVerificationEmailUseCase {
* - 허용된 대학 이메일 도메인인지 검증
* - 인증 코드를 생성해 메일로 발송
*/
@RateLimited(tag = "verification-email", key = "#email")
public void send(String email) {
if (userService.isAlreadyRegistered(email)) {
throw new RestApiException(DUPLICATE_EMAIL);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

import com.project.dorumdorum.domain.user.application.dto.request.SignUpRequest;
import com.project.dorumdorum.domain.user.domain.entity.User;
import com.project.dorumdorum.domain.user.domain.repository.EmailVerifiedRepository;
import com.project.dorumdorum.domain.user.domain.service.UserService;
import com.project.dorumdorum.global.exception.RestApiException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import static com.project.dorumdorum.global.exception.code.status.UserErrorStatus.DUPLICATE_SIGN_UP_INFO;
import static com.project.dorumdorum.global.exception.code.status.UserErrorStatus.EMAIL_NOT_VERIFIED;
import static com.project.dorumdorum.global.exception.code.status.UserErrorStatus._PASSWORD_NOT_MATCHES;

@Service
Expand All @@ -17,15 +19,21 @@
public class SignUpUseCase {

private final UserService userService;
private final EmailVerifiedRepository emailVerifiedRepository;

/**
* 회원가입 처리
* - 이메일 인증 완료 여부를 검증
* - 비밀번호 확인 여부를 검증
* - 중복 이메일 가입을 차단
* - 사용자를 저장하고 생성된 사용자 번호를 반환
*/
public String execute(SignUpRequest request) {
if(!request.isCheckedPassword()) {
if (!emailVerifiedRepository.existsByEmail(request.email())) {
throw new RestApiException(EMAIL_NOT_VERIFIED);
}

if (!request.isCheckedPassword()) {
throw new RestApiException(_PASSWORD_NOT_MATCHES);
}

Expand All @@ -35,6 +43,7 @@ public String execute(SignUpRequest request) {
}

User savedUser = userService.save(request);
emailVerifiedRepository.delete(request.email());
return savedUser.getUserNo();
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.project.dorumdorum.domain.user.application.usecase;

import com.project.dorumdorum.domain.user.domain.repository.EmailVerifiedRepository;
import com.project.dorumdorum.domain.user.domain.service.EmailVerificationService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
Expand All @@ -9,13 +10,15 @@
public class VerifyEmailUseCase {

private final EmailVerificationService emailVerificationService;
private final EmailVerifiedRepository emailVerifiedRepository;

/**
* 이메일 인증 코드 검증
* - 이메일과 인증 코드의 일치 여부를 확인
* - 유효하지 않으면 예외를 발생
* - 검증 성공 시 회원가입 가능 상태를 Redis에 저장
*/
public void execute(String email, String code) {
emailVerificationService.verifyCode(email, code);
emailVerifiedRepository.save(email);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.project.dorumdorum.domain.user.application.usecase;

import com.project.dorumdorum.domain.user.domain.repository.PasswordResetVerifiedRepository;
import com.project.dorumdorum.domain.user.domain.service.EmailVerificationService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class VerifyPasswordResetCodeUseCase {

private final PasswordResetVerifiedRepository passwordResetVerifiedRepository;
private final EmailVerificationService emailVerificationService;

/**
* 비밀번호 재설정 인증 코드 검증
* - 인증 코드 일치 여부 확인
* - 검증 성공 시 비밀번호 재설정 가능 상태를 Redis에 저장
*/
public void execute(String email, String code) {
emailVerificationService.verifyCode(email, code);
passwordResetVerifiedRepository.save(email);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,8 @@ public void updateFirebaseToken(String firebaseToken) {
this.firebaseToken = firebaseToken;
}

public void updatePassword(String encodedPassword) {
this.password = encodedPassword;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.project.dorumdorum.domain.user.domain.repository;

public interface EmailVerifiedRepository {

void save(String email);
boolean existsByEmail(String email);
void delete(String email);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.project.dorumdorum.domain.user.domain.repository;

import java.util.Optional;

public interface PasswordResetCodeRepository {

void save(String email, String code);
Optional<String> findByEmail(String email);
void delete(String email);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.project.dorumdorum.domain.user.domain.repository;

public interface PasswordResetVerifiedRepository {

void save(String email);
boolean existsByEmail(String email);
void delete(String email);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.project.dorumdorum.domain.user.infra.repository;

import com.project.dorumdorum.domain.user.domain.repository.EmailVerifiedRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Repository;

import java.time.Duration;

@Repository
@RequiredArgsConstructor
public class RedisEmailVerifiedRepository implements EmailVerifiedRepository {

private final RedisTemplate<String, String> redisTemplate;
private static final String EMAIL_VERIFIED = "EMAIL_VERIFIED:";
private static final String VERIFIED_VALUE = "verified";

@Override
public void save(String email) {
redisTemplate.opsForValue().set(EMAIL_VERIFIED + email, VERIFIED_VALUE, Duration.ofMinutes(30));
}
Comment thread
KoungQ marked this conversation as resolved.

@Override
public boolean existsByEmail(String email) {
return Boolean.TRUE.equals(redisTemplate.hasKey(EMAIL_VERIFIED + email));
}

@Override
public void delete(String email) {
redisTemplate.delete(EMAIL_VERIFIED + email);
}
}
Loading
Loading