Skip to content

Commit ed1abf3

Browse files
authored
[EC-82] BE/feat:유고 결석 신청 api (#68)
* [EC-82] chore: WIP * [EC-82] chore: 파일업로드 5MB 제한 * [EC-82] chore: AWS S3 Config * [EC-82] feat: Server Custom Exception추가 * [EC-82] feat: S3 업로드를 위한 서비스 구현 * [EC-82] chore: WIP * [EC-82] feat: 유고결석 신청 내역, 첨부파일 저장 로직 * [EC-82] feat: 저장된 유고결석 신청 내역 응답 형태 구현 * [EC-82] refactor: 첨부파일 url 타입 변경 - url 길이 때문에 TEXT 타입으로 변경 * [EC-82] feat: 수강중인 교육과정 유무 유효성 체크 - 유효성 체크 추가 - 코드 정리
1 parent cfb9d25 commit ed1abf3

File tree

14 files changed

+334
-11
lines changed

14 files changed

+334
-11
lines changed

api/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ dependencies {
4141
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
4242
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
4343
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
44-
44+
implementation 'software.amazon.awssdk:s3:2.20.12'
4545

4646
}
4747

api/src/main/java/org/example/educheck/domain/absenceattendance/controller/AbsenceAttendanceController.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
package org.example.educheck.domain.absenceattendance.controller;
22

33
import lombok.RequiredArgsConstructor;
4+
import org.example.educheck.domain.absenceattendance.dto.request.CreateAbsenceAttendacneRequestDto;
45
import org.example.educheck.domain.absenceattendance.dto.request.ProcessAbsenceAttendanceRequestDto;
6+
import org.example.educheck.domain.absenceattendance.dto.response.CreateAbsenceAttendacneReponseDto;
57
import org.example.educheck.domain.absenceattendance.service.AbsenceAttendanceService;
68
import org.example.educheck.domain.member.entity.Member;
79
import org.example.educheck.global.common.dto.ApiResponse;
810
import org.springframework.http.HttpStatus;
911
import org.springframework.http.ResponseEntity;
1012
import org.springframework.security.core.annotation.AuthenticationPrincipal;
1113
import org.springframework.web.bind.annotation.*;
14+
import org.springframework.web.multipart.MultipartFile;
1215

1316
@RestController
1417
@RequiredArgsConstructor
@@ -26,5 +29,20 @@ public ResponseEntity<ApiResponse<Void>> processAbsenceAttendanceService(@PathVa
2629

2730
return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.ok("유고 결석 처리 성공", "OK", null));
2831
}
32+
33+
34+
@PostMapping
35+
public ResponseEntity<ApiResponse<CreateAbsenceAttendacneReponseDto>> applyAttendanceAbsence(@AuthenticationPrincipal Member member,
36+
@PathVariable Long courseId,
37+
@RequestPart(value = "data") CreateAbsenceAttendacneRequestDto requestDto,
38+
@RequestPart(value = "files", required = false) MultipartFile[] files
39+
40+
) {
41+
return ResponseEntity.status(HttpStatus.CREATED)
42+
.body(ApiResponse.ok("유고 결석 신청 성공",
43+
"CREATED",
44+
absenceAttendanceService.createAbsenceAttendance(member, courseId, requestDto, files)))
45+
;
46+
}
2947

3048
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package org.example.educheck.domain.absenceattendance.dto.request;
2+
3+
import jakarta.validation.constraints.NotEmpty;
4+
import lombok.Getter;
5+
import lombok.NoArgsConstructor;
6+
import lombok.Setter;
7+
8+
import java.time.LocalDate;
9+
10+
@NoArgsConstructor
11+
@Setter
12+
@Getter
13+
public class CreateAbsenceAttendacneRequestDto {
14+
15+
@NotEmpty
16+
private String resean;
17+
@NotEmpty
18+
private LocalDate startDate;
19+
@NotEmpty
20+
private LocalDate endDate;
21+
@NotEmpty
22+
private String category;
23+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package org.example.educheck.domain.absenceattendance.dto.response;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Builder;
5+
import lombok.Getter;
6+
import lombok.NoArgsConstructor;
7+
import org.example.educheck.domain.absenceattendance.entity.AbsenceAttendance;
8+
9+
import java.time.LocalDate;
10+
import java.time.LocalDateTime;
11+
12+
@NoArgsConstructor
13+
@AllArgsConstructor
14+
@Builder
15+
@Getter
16+
public class CreateAbsenceAttendacneReponseDto {
17+
18+
private Long absenceAttendanceId;
19+
private String resean;
20+
private LocalDate startDate;
21+
private LocalDate endDate;
22+
private String category;
23+
// 생성 시 밑에 2개는 항시 의미 없는 데이터인데 줘야 할까요?
24+
private Character isApprove;
25+
private LocalDateTime approveDate;
26+
27+
public static CreateAbsenceAttendacneReponseDto from(AbsenceAttendance absenceAttendance) {
28+
return CreateAbsenceAttendacneReponseDto.builder()
29+
.absenceAttendanceId(absenceAttendance.getId())
30+
.resean(absenceAttendance.getReason())
31+
.startDate(absenceAttendance.getStartTime())
32+
.endDate(absenceAttendance.getEndTime())
33+
.category(absenceAttendance.getCategory())
34+
.isApprove(absenceAttendance.getIsApprove())
35+
.approveDate(absenceAttendance.getApproveDate())
36+
.build();
37+
}
38+
39+
40+
}

api/src/main/java/org/example/educheck/domain/absenceattendance/entity/AbsenceAttendance.java

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
package org.example.educheck.domain.absenceattendance.entity;
22

33
import jakarta.persistence.*;
4-
import lombok.AccessLevel;
5-
import lombok.Getter;
6-
import lombok.NoArgsConstructor;
7-
import lombok.Setter;
4+
import lombok.*;
85
import org.example.educheck.domain.course.entity.Course;
96
import org.example.educheck.domain.member.staff.entity.Staff;
107
import org.example.educheck.domain.member.student.entity.Student;
118

9+
import java.time.LocalDate;
1210
import java.time.LocalDateTime;
1311

1412
/**
@@ -22,7 +20,7 @@ public class AbsenceAttendance {
2220

2321
@Id
2422
@GeneratedValue(strategy = GenerationType.IDENTITY)
25-
private long id;
23+
private Long id;
2624

2725
@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
2826
@JoinColumn(name = "approver_id")
@@ -36,9 +34,23 @@ public class AbsenceAttendance {
3634
@JoinColumn(name = "student_id")
3735
private Student student;
3836

39-
private LocalDateTime startTime;
40-
private LocalDateTime endTime;
37+
private LocalDate startTime;
38+
private LocalDate endTime;
4139
private Character isApprove;
4240
private LocalDateTime approveDate;
4341
private String reason;
42+
private String category;
43+
44+
@Builder
45+
public AbsenceAttendance(Staff staff, Course course, Student student, LocalDate startTime, LocalDate endTime, Character isApprove, LocalDateTime approveDate, String reason, String category) {
46+
this.staff = staff;
47+
this.course = course;
48+
this.student = student;
49+
this.startTime = startTime;
50+
this.endTime = endTime;
51+
this.isApprove = isApprove;
52+
this.approveDate = approveDate;
53+
this.reason = reason;
54+
this.category = category;
55+
}
4456
}

api/src/main/java/org/example/educheck/domain/absenceattendance/service/AbsenceAttendanceService.java

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,30 @@
22

33
import lombok.RequiredArgsConstructor;
44
import lombok.extern.slf4j.Slf4j;
5+
import org.example.educheck.domain.absenceattendance.dto.request.CreateAbsenceAttendacneRequestDto;
56
import org.example.educheck.domain.absenceattendance.dto.request.ProcessAbsenceAttendanceRequestDto;
7+
import org.example.educheck.domain.absenceattendance.dto.response.CreateAbsenceAttendacneReponseDto;
68
import org.example.educheck.domain.absenceattendance.entity.AbsenceAttendance;
79
import org.example.educheck.domain.absenceattendance.repository.AbsenceAttendanceRepository;
10+
import org.example.educheck.domain.absenceattendanceattachmentfile.entity.AbsenceAttendanceAttachmentFile;
11+
import org.example.educheck.domain.absenceattendanceattachmentfile.repository.AbsenceAttendanceAttachmentFileRepository;
12+
import org.example.educheck.domain.course.repository.CourseRepository;
813
import org.example.educheck.domain.member.entity.Member;
914
import org.example.educheck.domain.member.repository.StaffRepository;
1015
import org.example.educheck.domain.member.staff.entity.Staff;
16+
import org.example.educheck.domain.registration.entity.Registration;
17+
import org.example.educheck.domain.registration.repository.RegistrationRepository;
1118
import org.example.educheck.global.common.exception.custom.common.ResourceMismatchException;
1219
import org.example.educheck.global.common.exception.custom.common.ResourceNotFoundException;
20+
import org.example.educheck.global.common.s3.S3Service;
1321
import org.springframework.security.access.prepost.PreAuthorize;
1422
import org.springframework.stereotype.Service;
1523
import org.springframework.transaction.annotation.Transactional;
24+
import org.springframework.web.multipart.MultipartFile;
1625

1726
import java.time.LocalDateTime;
27+
import java.util.List;
28+
import java.util.Map;
1829

1930
@Service
2031
@RequiredArgsConstructor
@@ -23,6 +34,10 @@
2334
public class AbsenceAttendanceService {
2435
private final AbsenceAttendanceRepository absenceAttendanceRepository;
2536
private final StaffRepository staffRepository;
37+
private final S3Service s3Service;
38+
private final CourseRepository courseRepository;
39+
private final AbsenceAttendanceAttachmentFileRepository absenceAttendanceAttachmentFileRepository;
40+
private final RegistrationRepository registrationRepository;
2641

2742
@Transactional
2843
@PreAuthorize("hasAnyAuthority('MIDDLE_ADMIN')")
@@ -50,4 +65,60 @@ public void processAbsenceAttendanceService(Long courseId, Long absesnceAttendan
5065
.toUpperCase().charAt(0)
5166
);
5267
}
68+
69+
@Transactional
70+
public CreateAbsenceAttendacneReponseDto createAbsenceAttendance(Member member, Long courseId, CreateAbsenceAttendacneRequestDto requestDto, MultipartFile[] files) {
71+
72+
validateRegistrationCourse(member, courseId);
73+
74+
AbsenceAttendance absenceAttendance = AbsenceAttendance.builder()
75+
.course(courseRepository.findById(courseId)
76+
.orElseThrow(() -> new ResourceNotFoundException("해당 교육 과정을 찾을 수 없습니다.")))
77+
.student(member.getStudent())
78+
.startTime(requestDto.getStartDate())
79+
.endTime(requestDto.getEndDate())
80+
.reason(requestDto.getResean())
81+
.category(requestDto.getCategory())
82+
.build();
83+
84+
AbsenceAttendance savedAbsenceAttendance = absenceAttendanceRepository.save(absenceAttendance);
85+
86+
saveAttachementFiles(files, savedAbsenceAttendance);
87+
88+
return CreateAbsenceAttendacneReponseDto.from(savedAbsenceAttendance);
89+
}
90+
91+
private void saveAttachementFiles(MultipartFile[] files, AbsenceAttendance savedAbsenceAttendance) {
92+
if (files != null && files.length > 0) {
93+
List<Map<String, String>> uploadedResults = s3Service.uploadFiles(files);
94+
for (Map<String, String> result : uploadedResults) {
95+
for (MultipartFile file : files) {
96+
97+
String originalName = file.getOriginalFilename();
98+
String mimeType = file.getContentType();
99+
100+
AbsenceAttendanceAttachmentFile attachmentFile = AbsenceAttendanceAttachmentFile.builder()
101+
.absenceAttendance(savedAbsenceAttendance)
102+
.url(result.get("fileUrl"))
103+
.s3Key(result.get("s3Key"))
104+
.originalName(originalName)
105+
.mime(mimeType)
106+
.build();
107+
108+
log.info(attachmentFile.getUrl());
109+
110+
absenceAttendanceAttachmentFileRepository.save(attachmentFile);
111+
}
112+
}
113+
}
114+
}
115+
116+
private void validateRegistrationCourse(Member member, Long courseId) {
117+
Registration registration = registrationRepository.findByStudentIdAndCourseId(member.getStudent().getId(), courseId)
118+
.orElseThrow(ResourceNotFoundException::new);
119+
120+
if (registration == null) {
121+
throw new ResourceMismatchException();
122+
}
123+
}
53124
}

api/src/main/java/org/example/educheck/domain/absenceattendanceattachmentfile/entity/AbsenceAttendanceAttachmentFile.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@
22

33
import jakarta.persistence.*;
44
import lombok.AccessLevel;
5+
import lombok.Builder;
56
import lombok.Getter;
67
import lombok.NoArgsConstructor;
78
import org.example.educheck.domain.absenceattendance.entity.AbsenceAttendance;
9+
import org.example.educheck.global.common.entity.BaseTimeEntity;
810

911
@Getter
1012
@Entity(name = "absence_attendacne_attachment_file")
1113
@NoArgsConstructor(access = AccessLevel.PROTECTED)
12-
public class AbsenceAttendanceAttachmentFile {
14+
public class AbsenceAttendanceAttachmentFile extends BaseTimeEntity {
1315

1416
@Id
1517
@GeneratedValue(strategy = GenerationType.IDENTITY)
@@ -19,9 +21,20 @@ public class AbsenceAttendanceAttachmentFile {
1921
@JoinColumn(name = "absence_attendance_id")
2022
private AbsenceAttendance absenceAttendance;
2123

24+
@Column(columnDefinition = "TEXT")
2225
private String url;
2326
private String mime;
2427
private String originalName;
28+
//버킷 내 고유 식별자, 전체 경로 포함
2529
private String s3Key;
2630

31+
@Builder
32+
public AbsenceAttendanceAttachmentFile(AbsenceAttendance absenceAttendance, String url, String originalName, String s3Key, String mime) {
33+
this.absenceAttendance = absenceAttendance;
34+
this.url = url;
35+
this.originalName = originalName;
36+
this.s3Key = s3Key;
37+
this.mime = mime;
38+
}
39+
2740
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package org.example.educheck.domain.absenceattendanceattachmentfile.repository;
2+
3+
import org.example.educheck.domain.absenceattendanceattachmentfile.entity.AbsenceAttendanceAttachmentFile;
4+
import org.springframework.data.jpa.repository.JpaRepository;
5+
6+
public interface AbsenceAttendanceAttachmentFileRepository extends JpaRepository<AbsenceAttendanceAttachmentFile, Long> {
7+
}

api/src/main/java/org/example/educheck/domain/registration/repository/RegistrationRepository.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@
88

99
public interface RegistrationRepository extends JpaRepository<Registration, Long> {
1010
Optional<Registration> findByStudentIdAndStatus(Long studentId, Status status);
11-
11+
12+
Optional<Registration> findByStudentIdAndCourseId(Long id, Long courseId);
1213
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package org.example.educheck.global.common.config;
2+
3+
import org.springframework.beans.factory.annotation.Value;
4+
import org.springframework.context.annotation.Bean;
5+
import org.springframework.context.annotation.Configuration;
6+
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
7+
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
8+
import software.amazon.awssdk.regions.Region;
9+
import software.amazon.awssdk.services.s3.S3Client;
10+
11+
@Configuration
12+
public class AwsS3Config {
13+
14+
@Value("${REGION}")
15+
private String REGION;
16+
@Value("${ACCESS_KEY}")
17+
private String ACCESS_KEY;
18+
@Value("${SECRET_KEY}")
19+
private String SECRET_KEY;
20+
21+
@Bean
22+
public S3Client s3Client() {
23+
StaticCredentialsProvider credential = StaticCredentialsProvider.create(
24+
AwsBasicCredentials.create(ACCESS_KEY, SECRET_KEY)
25+
);
26+
27+
return S3Client.builder()
28+
.region(Region.of(REGION))
29+
.credentialsProvider(credential)
30+
.build();
31+
}
32+
}

0 commit comments

Comments
 (0)