Skip to content

Commit

Permalink
[FEAT] 봉사 인증 폼 CUD 및 봉사 확인 메시지 전송 기능 구현 (#259)
Browse files Browse the repository at this point in the history
* feat(VolunteerCertification): 봉사 인증 폼 Entity 추가

- Volunteerer: 봉사자 이름과 이메일 저장
- VolunteerTime: 봉사 일자, 시작 시간, 끝 시간 저장

* feat(PostErrorCode): Invalid enum 값에 대한 예외 코드 정의 (PostType, AssistanceType)

* refactor(post.entity.enum): AssistanceType, PostType에 ValueEnum 인터페이스 적용

- assistanceType, postType 변수명 value로 변경
- PostType 열거형 인스턴스 필드 한글에서 영어로 변경

* feat(VolunteerCertificationRequest): 봉사 인증 폼 생성을 위한 Request DTO 추가

- Annotation 기반 검증
- @AssertTrue를 통해 시작, 종료 시간 검증

* feat(VolunteerCertificationRepository): 봉사 인증 폼 저장소 추가

* feat(VolunteerCertificationErrorCode): 봉사 인증 폼 생성 관련 예외 코드 정의

* feat(VolunteerCertificationValidator): 봉사활동 인증 폼 검증 로직 구현

- 매칭에 참여한 Giver와 로그인 한 사용자 검증
- 매칭 상태 VOLUNTEERING_COMPLETED 검증
- PostType 일치 검증
- 봉사 일자 검증
- 봉사 시간 검증
- AssistanceType 일치 검증

* feat(VolunteerCertificationMapper): 봉사활동 인증 폼 도메인 변환 매퍼 구현

- DTO to Volunteerer 객체 변환
- DTO to VolunteerTime 객체 변환

* feat(VolunteerCertificationService): 봉사활동 인증 폼 생성 서비스 구현

* feat(VolunteerCertificationController): 봉사활동 인증 폼 생성 컨트롤러 구현

* fix(VolunteerCertificationService): 봉사활동 인증 폼 생성 시 인증 폼 존재 추가 검증

- VolunteerCertificationErrorCode: 봉사활동 인증 폼이 이미 존재 한다는 에러 코드 추가
- VolunteerCertificationRepository: matchingId 기반 봉사활동 인증 폼 유무 검사

* feat(VolunteerCertificationService): Id 기반 VolunteerCertification 조회 기능 구현

- VolunteerCertificationErrorCode: 봉사활동 인증 폼 없을 시 에러 코드 추가
- VolunteerCertificationRepository: fetch join으로 인증 폼, 매칭, 게시글 조회

* feat: 봉사활동 인증 폼 수정 기능 구현

- VolunteerCertificationController: 봉사 활동 인증 폼 수정 엔드포인트 추가
- VolunteerCertificationService: 봉사 활동 인증 폼 수정 비즈니스 로직 구현
- VolunteerCertificationMapper: UpdateDto에 대해 VolunteerTime 생성
- VolunteerCertificationValidator: 인증 폼 수정을 위한 검증 로직 추가
- VolunteerCertificationUpdateRequest: 인증 폼 수정 요청 DTO 추가, 유효성 검증

* refactor(VolunteerCertification): 불필요한 Volunteerer 객체 제거

- Matching.Giver를 참고 하면서 불필요한 name, email 필드를 갖는 객체 제거
- VolunteerCertification Mapper, Validator에서 volunteerer 관련 필드 제거

* refactor(VolunteerCertificationService): 불필요한 주석 제거

- 하드 코딩 언급 하는 주석 제거

* feat(VolunteerCertification): 인증 폼 매칭 유효성 검증 및 ErrorCode 정의

* refactor: 게시글/매칭 유효성 검증 로직 구현 및 접근제어자 수정

- 게시글 검증: 봉사 일자, 시간, 게시글 타입, 도움 유형
- 매칭 검증: 봉사자 정보, 매칭 상태
- AssistanceTime: 기존 필드 private 접근제어자 추가

* refactor(VolunteerCertificationException): Exception 접미사 컨벤션 통일

* refactor(VolunteerCertificationController): 봉사활동 인증 폼 제출 엔드포인트 /certifications 추가

- 기존: Post /api/v1/matchings/{matching-id}
- 변경: Post /api/v1/matchings/{matching-id}/certifications

* refactor(VolunteerCertificationValidator): 검증 메서드 통합 관리

- 개별적으로 존재하던 Post, Matching 검증 메서드 하나로 통합

* feat(Matching): MatchingStatus Done 검증 및 예외 코드 추가

* feat: 미완료된 봉사활동에 대한 완료 요청 문자 발송 기능 구현

- 수혜자의 봉사 완료 확인이 없는 경우 문자 발송 서비스 구현
- 문자 발송 요청을 위한 컨트롤러 엔드포인트 추가

* feat(CertificationTracking): 봉사 인증 재요청 추적을 위한 엔티티 추가

- 봉사 인증 요청 24시간 제한 관리용 엔티티 정의
- 매칭 상태 및 요청 시간 업데이트 시 검증 관련 예외 코드 추가

* feat(ChatMessageService): 봉사 인증 요청 추적 기능 구현

- CertificationTracking을 이용한 봉사 인증 요청 이력 관리 로직 구현
- 24시간 내 재요청 제한을 위한 검증 및 업데이트 로직 추가
- 요청 이력 조회를 위한 Repository 쿼리 추가

* feat(VolunteerCertificationService): 관리자 인증 폼 삭제 기능 구현

- AdminVolunteerCertificationController: 인증 폼 관리자 관련 컨트롤러
- VolunteerCertificationService: 인증 폼 삭제 관련 로직 구현
  • Loading branch information
injae-348 authored Jan 11, 2025
1 parent d7f2d14 commit f62a4e1
Show file tree
Hide file tree
Showing 37 changed files with 911 additions and 16 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package econo.buddybridge.certification.controller;

import econo.buddybridge.auth.resolver.MemberTokenId;
import econo.buddybridge.certification.service.VolunteerCertificationService;
import econo.buddybridge.member.entity.Role;
import econo.buddybridge.utils.api.ApiResponse;
import econo.buddybridge.utils.api.ApiResponse.CustomBody;
import econo.buddybridge.utils.api.ApiResponseGenerator;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/v1")
@RequiredArgsConstructor
@Tag(name = "관리자 API 봉사활동 인증 폼", description = "봉사활동 인증 폼 관련 관리자 API")
public class AdminVolunteerCertificationController {

private final VolunteerCertificationService volunteerCertificationService;

@DeleteMapping("/certifications/{certification-id}")
public ApiResponse<CustomBody<Void>> deleteVolunteerCertification(
@PathVariable("certification-id") Long volunteerCertificationId,
@Parameter(hidden = true) @MemberTokenId(allowedRoles = {Role.ADMIN}) Long memberId
) {
volunteerCertificationService.deleteVolunteerCertificationForAdmin(volunteerCertificationId);
return ApiResponseGenerator.success(HttpStatus.NO_CONTENT);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package econo.buddybridge.certification.controller;

import econo.buddybridge.auth.resolver.MemberTokenId;
import econo.buddybridge.certification.dto.VolunteerCertificationRequest;
import econo.buddybridge.certification.dto.VolunteerCertificationUpdateRequest;
import econo.buddybridge.certification.service.VolunteerCertificationService;
import econo.buddybridge.utils.api.ApiResponse;
import econo.buddybridge.utils.api.ApiResponse.CustomBody;
import econo.buddybridge.utils.api.ApiResponseGenerator;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/v1/matchings")
@RequiredArgsConstructor
@Tag(name = "봉사활동 인증 폼 API", description = "봉사활동 인증 폼 관련 API")
public class VolunteerCertificationController {

private final VolunteerCertificationService volunteerCertificationService;

@Operation(summary = "봉사활동 인증 폼 작성", description = "봉사활동 인증 폼을 작성합니다.")
@PostMapping("/{matching-id}/certifications")
public ApiResponse<CustomBody<Void>> submitVolunteerCertification(
@PathVariable("matching-id") Long matchingId,
@Valid @RequestBody VolunteerCertificationRequest volunteerCertificationRequest,
@Parameter(hidden = true) @MemberTokenId Long memberId
) {
volunteerCertificationService.submitVolunteerCertification(matchingId, volunteerCertificationRequest, memberId);
return ApiResponseGenerator.success(HttpStatus.CREATED);
}

@Operation(summary = "봉사활동 인증 폼 수정", description = "봉사활동 인증 폼을 수정합니다.")
@PutMapping("/{matching-id}/certifications/{certification-id}")
public ApiResponse<CustomBody<Void>> modifyVolunteerCertification(
@PathVariable("matching-id") Long matchingId,
@PathVariable("certification-id") Long certificationId,
@Valid @RequestBody VolunteerCertificationUpdateRequest volunteerCertificationUpdateRequest,
@Parameter(hidden = true) @MemberTokenId Long memberId
) {
volunteerCertificationService.modifyVolunteerCertification(matchingId, certificationId, volunteerCertificationUpdateRequest, memberId);
return ApiResponseGenerator.success(HttpStatus.OK);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package econo.buddybridge.certification.dto;

import econo.buddybridge.common.validation.EnumTypeValue;
import econo.buddybridge.post.entity.AssistanceType;
import econo.buddybridge.post.entity.PostType;
import jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.time.LocalDate;
import java.time.LocalTime;

public record VolunteerCertificationRequest(
@NotBlank(message = "봉사자 이름은 필수 입니다.")
String volunteerName,

@NotBlank(message = "봉사자 이메일은 필수 입니다.")
String volunteerEmail,

@EnumTypeValue(enumClass = PostType.class)
String postType,

@NotNull(message = "봉사 일자는 필수 입니다.")
LocalDate volunteerDate,

@EnumTypeValue(enumClass = AssistanceType.class)
String assistanceType,

@NotNull(message = "봉사 시작 시간은 필수 입니다.")
LocalTime startTime,

@NotNull(message = "봉사 종료 시간은 필수 입니다.")
LocalTime endTime,

@NotBlank(message = "봉사 내용은 필수 입니다.")
@Size(min = 200, max = 1000, message = "봉사 활동 내용 및 소감은 200자 이상 1000자 이하로 작성해주세요.")
String content
) {

@AssertTrue(message = "봉사 시작 시간은 봉사 종료 시간보다 빨라야 합니다.")
private boolean isStartTimeBeforeEndTime() {
return !startTime.isAfter(endTime);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package econo.buddybridge.certification.dto;

import jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.time.LocalDate;
import java.time.LocalTime;

public record VolunteerCertificationUpdateRequest(
@NotNull(message = "봉사 일자는 필수 입니다.")
LocalDate volunteerDate,

@NotNull(message = "봉사 시작 시간은 필수 입니다.")
LocalTime startTime,

@NotNull(message = "봉사 종료 시간은 필수 입니다.")
LocalTime endTime,

@NotBlank(message = "봉사 내용은 필수 입니다.")
@Size(min = 200, max = 1000, message = "봉사 활동 내용 및 소감은 200자 이상 1000자 이하로 작성해주세요.")
String content
) {

@AssertTrue(message = "봉사 시작 시간은 봉사 종료 시간보다 빨라야 합니다.")
private boolean isStartTimeBeforeEndTime() {
return !startTime.isAfter(endTime);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package econo.buddybridge.certification.entity;

import econo.buddybridge.certification.exception.VolunteerCertificationMatchingMismatchException;
import econo.buddybridge.common.persistence.BaseEntity;
import econo.buddybridge.matching.entity.Matching;
import jakarta.persistence.Column;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@Table(name = "VOLUNTEER_CERTIFICATION")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class VolunteerCertification extends BaseEntity {

@Id
@Column(name = "certification_id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@OneToOne(fetch = FetchType.LAZY)
private Matching matching;

@Embedded
private VolunteerTime volunteerTime;

private String content;

@Builder
private VolunteerCertification(Matching matching, VolunteerTime volunteerTime, String content) {
this.matching = matching;
this.volunteerTime = volunteerTime;
this.content = content;
}

public static VolunteerCertification of(Matching matching, VolunteerTime volunteerTime, String content) {
return VolunteerCertification.builder()
.matching(matching)
.volunteerTime(volunteerTime)
.content(content)
.build();
}

public void updateVolunteerCertification(VolunteerTime volunteerTime, String content) {
this.volunteerTime = volunteerTime;
this.content = content;
}

public void validateMatching(Matching matching) {
if (!this.matching.equals(matching)) {
throw VolunteerCertificationMatchingMismatchException.EXCEPTION;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package econo.buddybridge.certification.entity;

import jakarta.persistence.Embeddable;
import java.time.LocalDate;
import java.time.LocalTime;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Embeddable
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Builder
@AllArgsConstructor
@EqualsAndHashCode
public class VolunteerTime {

private LocalDate volunteerDate;

private LocalTime startTime;

private LocalTime endTime;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package econo.buddybridge.certification.exception;

import econo.buddybridge.common.exception.BusinessException;

public class VolunteerCertificationAllowedOnlyVolunteeringCompletedException extends BusinessException {

public static final BusinessException EXCEPTION = new VolunteerCertificationAllowedOnlyVolunteeringCompletedException();

private VolunteerCertificationAllowedOnlyVolunteeringCompletedException() {
super(VolunteerCertificationErrorCode.VOLUNTEER_CERTIFICATION_ALLOWED_ONLY_VOLUNTEERING_COMPLETED);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package econo.buddybridge.certification.exception;

import econo.buddybridge.common.exception.BusinessException;

public class VolunteerCertificationAlreadyExistsException extends BusinessException {

public static final BusinessException EXCEPTION = new VolunteerCertificationAlreadyExistsException();

private VolunteerCertificationAlreadyExistsException() {
super(VolunteerCertificationErrorCode.VOLUNTEER_CERTIFICATION_ALREADY_EXISTS);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package econo.buddybridge.certification.exception;

import econo.buddybridge.common.exception.BusinessException;

public class VolunteerCertificationAssistanceTimeMismatchException extends BusinessException {

public static final BusinessException EXCEPTION = new VolunteerCertificationAssistanceTimeMismatchException();

private VolunteerCertificationAssistanceTimeMismatchException() {
super(VolunteerCertificationErrorCode.VOLUNTEER_CERTIFICATION_ASSISTANCE_TIME_MISMATCH);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package econo.buddybridge.certification.exception;

import econo.buddybridge.common.exception.BusinessException;

public class VolunteerCertificationAssistanceTypeMismatchException extends BusinessException {

public static final BusinessException EXCEPTION = new VolunteerCertificationAssistanceTypeMismatchException();

private VolunteerCertificationAssistanceTypeMismatchException() {
super(VolunteerCertificationErrorCode.VOLUNTEER_CERTIFICATION_ASSISTANCE_TYPE_MISMATCH);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package econo.buddybridge.certification.exception;

import econo.buddybridge.common.exception.ErrorCode;
import org.springframework.http.HttpStatus;

public enum VolunteerCertificationErrorCode implements ErrorCode {
VOLUNTEER_CERTIFICATION_ALLOWED_ONLY_VOLUNTEERING_COMPLETED("VC001", HttpStatus.BAD_REQUEST, "봉사 인증은 매칭 상태가 봉사 완료(VOLUNTEERING_COMPLETED)인 경우에만 가능합니다."),
VOLUNTEER_CERTIFICATION_VOLUNTEERER_MISMATCH("VC002", HttpStatus.BAD_REQUEST, "봉사자가 일치하지 않습니다. 관리자에게 문의해주세요."),
VOLUNTEER_CERTIFICATION_POST_TYPE_MISMATCH("VC003", HttpStatus.BAD_REQUEST, "게시글 유형이 일치하지 않습니다. 관리자에게 문의해주세요."),
VOLUNTEER_CERTIFICATION_SCHEDULE_DATE_MISMATCH("VC004", HttpStatus.BAD_REQUEST, "봉사 일자가 게시글에 명시된 일자와 일치하지 않습니다."),
VOLUNTEER_CERTIFICATION_ASSISTANCE_TIME_MISMATCH("VC005", HttpStatus.BAD_REQUEST, "봉사 시간이 게시글에 명시된 시간과 일치하지 않습니다."),
VOLUNTEER_CERTIFICATION_ASSISTANCE_TYPE_MISMATCH("VC006", HttpStatus.BAD_REQUEST, "도움 유형이 게시글에 명시된 유형과 일치하지 않습니다."),
VOLUNTEER_CERTIFICATION_ALREADY_EXISTS("VC007", HttpStatus.BAD_REQUEST, "작성한 봉사활동 인증 폼이 존재합니다."),
VOLUNTEER_CERTIFICATION_NOT_FOUND("VC008", HttpStatus.NOT_FOUND, "봉사 인증 폼을 찾을 수 없습니다."),
VOLUNTEER_CERTIFICATION_MATCHING_MISMATCH("VC009", HttpStatus.BAD_REQUEST, "매칭이 일치하지 않습니다. 관리자에게 문의해주세요."),
;

private final String code;
private final HttpStatus httpStatus;
private final String message;

VolunteerCertificationErrorCode(String code, HttpStatus httpStatus, String message) {
this.code = code;
this.httpStatus = httpStatus;
this.message = message;
}

@Override
public String getCode() {
return code;
}

@Override
public HttpStatus getHttpStatus() {
return httpStatus;
}

@Override
public String getMessage() {
return message;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package econo.buddybridge.certification.exception;

import econo.buddybridge.common.exception.BusinessException;

public class VolunteerCertificationMatchingMismatchException extends BusinessException {

public static final BusinessException EXCEPTION = new VolunteerCertificationMatchingMismatchException();

private VolunteerCertificationMatchingMismatchException() {
super(VolunteerCertificationErrorCode.VOLUNTEER_CERTIFICATION_MATCHING_MISMATCH);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package econo.buddybridge.certification.exception;

import econo.buddybridge.common.exception.BusinessException;

public class VolunteerCertificationNotFoundException extends BusinessException {

public static final BusinessException EXCEPTION = new VolunteerCertificationNotFoundException();

private VolunteerCertificationNotFoundException() {
super(VolunteerCertificationErrorCode.VOLUNTEER_CERTIFICATION_NOT_FOUND);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package econo.buddybridge.certification.exception;

import econo.buddybridge.common.exception.BusinessException;

public class VolunteerCertificationPostTypeMismatchException extends BusinessException {

public static final BusinessException EXCEPTION = new VolunteerCertificationPostTypeMismatchException();

private VolunteerCertificationPostTypeMismatchException() {
super(VolunteerCertificationErrorCode.VOLUNTEER_CERTIFICATION_POST_TYPE_MISMATCH);
}
}
Loading

0 comments on commit f62a4e1

Please sign in to comment.