-
Notifications
You must be signed in to change notification settings - Fork 0
Develop #216
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Develop #216
Changes from all commits
dad3be1
a42d19d
e9a0662
4cb7585
d8cd8f1
5c28cec
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| package team.incube.flooding.domain.dormitory.study.entity | ||
|
|
||
| import jakarta.persistence.Column | ||
| import jakarta.persistence.Entity | ||
| import jakarta.persistence.FetchType | ||
| import jakarta.persistence.GeneratedValue | ||
| import jakarta.persistence.GenerationType | ||
| import jakarta.persistence.Id | ||
| import jakarta.persistence.JoinColumn | ||
| import jakarta.persistence.ManyToOne | ||
| import jakarta.persistence.Table | ||
| import jakarta.persistence.UniqueConstraint | ||
| import team.incube.flooding.domain.user.entity.UserJpaEntity | ||
| import java.time.LocalDate | ||
|
|
||
| @Entity | ||
| @Table( | ||
| name = "tb_study_attendance_history", | ||
| uniqueConstraints = [ | ||
| UniqueConstraint(columnNames = ["user_id", "attended_date"]), | ||
| ], | ||
| ) | ||
| class StudyAttendanceHistoryJpaEntity( | ||
| @field:Id | ||
| @field:GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| val id: Long = 0, | ||
| @field:ManyToOne(fetch = FetchType.LAZY) | ||
| @field:JoinColumn(nullable = false, name = "user_id") | ||
| val user: UserJpaEntity, | ||
| @field:Column(name = "attended_date", nullable = false) | ||
| val attendedDate: LocalDate, | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| package team.incube.flooding.domain.dormitory.study.presentation.controller | ||
|
|
||
| import io.swagger.v3.oas.annotations.Operation | ||
| import io.swagger.v3.oas.annotations.responses.ApiResponse | ||
| import io.swagger.v3.oas.annotations.responses.ApiResponses | ||
| import io.swagger.v3.oas.annotations.tags.Tag | ||
| import org.springframework.web.bind.annotation.GetMapping | ||
| import org.springframework.web.bind.annotation.RequestMapping | ||
| import org.springframework.web.bind.annotation.RestController | ||
| import team.incube.flooding.domain.dormitory.study.presentation.data.response.GetStudyAttendanceListResponse | ||
| import team.incube.flooding.domain.dormitory.study.service.GetPublicStudyAttendanceListService | ||
| import team.themoment.sdk.response.CommonApiResponse | ||
|
|
||
| @Tag(name = "자습(공개)", description = "인증 없이 조회 가능한 자습 관련 공개 API") | ||
| @RestController | ||
| @RequestMapping("public/study") | ||
| class PublicStudyController( | ||
| private val getPublicStudyAttendanceListService: GetPublicStudyAttendanceListService, | ||
| ) { | ||
| @Operation( | ||
| summary = "최근 1주일 자습 출석자 목록 조회 (공개)", | ||
| description = | ||
| "인증 없이 오늘을 포함한 최근 7일간 날짜별 자습 출석자 이름 리스트를 조회합니다. " + | ||
| "학번, 유저 ID 등 개인 식별 정보는 포함되지 않으며 이름만 제공합니다. " + | ||
| "스트릭(연속 출석) 계산과 같은 부가 로직은 이 API를 사용하는 클라이언트(서드파티)가 직접 수행해야 합니다.", | ||
| ) | ||
| @ApiResponses( | ||
| ApiResponse(responseCode = "200", description = "조회 성공"), | ||
| ) | ||
| @GetMapping("/attendances") | ||
| fun getAttendances(): CommonApiResponse<List<GetStudyAttendanceListResponse>> = | ||
| CommonApiResponse.success("OK", getPublicStudyAttendanceListService.execute()) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| package team.incube.flooding.domain.dormitory.study.presentation.data.response | ||
|
|
||
| import java.time.LocalDate | ||
|
|
||
| data class GetStudyAttendanceListResponse( | ||
| val date: LocalDate, | ||
| val students: List<String>, | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| package team.incube.flooding.domain.dormitory.study.repository | ||
|
|
||
| import org.springframework.data.jpa.repository.JpaRepository | ||
| import org.springframework.data.jpa.repository.Query | ||
| import org.springframework.data.repository.query.Param | ||
| import team.incube.flooding.domain.dormitory.study.entity.StudyAttendanceHistoryJpaEntity | ||
| import java.time.LocalDate | ||
|
|
||
| interface StudyAttendanceHistoryRepository : JpaRepository<StudyAttendanceHistoryJpaEntity, Long> { | ||
| fun existsByUserIdAndAttendedDate( | ||
| userId: Long, | ||
| attendedDate: LocalDate, | ||
| ): Boolean | ||
|
|
||
| fun deleteByUserIdAndAttendedDate( | ||
| userId: Long, | ||
| attendedDate: LocalDate, | ||
| ) | ||
|
|
||
| @Query( | ||
| "SELECT h FROM StudyAttendanceHistoryJpaEntity h JOIN FETCH h.user " + | ||
| "WHERE h.attendedDate BETWEEN :startDate AND :endDate ORDER BY h.attendedDate ASC", | ||
| ) | ||
| fun findAllByAttendedDateBetweenOrderByAttendedDateAsc( | ||
| @Param("startDate") startDate: LocalDate, | ||
| @Param("endDate") endDate: LocalDate, | ||
| ): List<StudyAttendanceHistoryJpaEntity> | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package team.incube.flooding.domain.dormitory.study.service | ||
|
|
||
| import team.incube.flooding.domain.dormitory.study.presentation.data.response.GetStudyAttendanceListResponse | ||
|
|
||
| interface GetPublicStudyAttendanceListService { | ||
| fun execute(): List<GetStudyAttendanceListResponse> | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -7,17 +7,23 @@ import org.springframework.transaction.annotation.Transactional | |||||||||||||||||||||||||||||
| import team.incube.flooding.domain.dormitory.study.adapter.StudyAttendanceSseEmitterRegistry | ||||||||||||||||||||||||||||||
| import team.incube.flooding.domain.dormitory.study.adapter.StudyRedisAdapter | ||||||||||||||||||||||||||||||
| import team.incube.flooding.domain.dormitory.study.entity.StudyApplicationStatus | ||||||||||||||||||||||||||||||
| import team.incube.flooding.domain.dormitory.study.entity.StudyAttendanceHistoryJpaEntity | ||||||||||||||||||||||||||||||
| import team.incube.flooding.domain.dormitory.study.presentation.data.response.StudyAttendanceEventResponse | ||||||||||||||||||||||||||||||
| import team.incube.flooding.domain.dormitory.study.repository.StudyAttendanceHistoryRepository | ||||||||||||||||||||||||||||||
| import team.incube.flooding.domain.dormitory.study.service.CheckStudyAttendanceService | ||||||||||||||||||||||||||||||
| import team.incube.flooding.domain.user.repository.UserRepository | ||||||||||||||||||||||||||||||
| import team.themoment.sdk.exception.ExpectedException | ||||||||||||||||||||||||||||||
| import java.time.Clock | ||||||||||||||||||||||||||||||
| import java.time.LocalDate | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| @Service | ||||||||||||||||||||||||||||||
| @Transactional(readOnly = true) | ||||||||||||||||||||||||||||||
| @Transactional | ||||||||||||||||||||||||||||||
| class CheckStudyAttendanceServiceImpl( | ||||||||||||||||||||||||||||||
| private val studyRedisAdapter: StudyRedisAdapter, | ||||||||||||||||||||||||||||||
| private val userRepository: UserRepository, | ||||||||||||||||||||||||||||||
| private val sseEmitterRegistry: StudyAttendanceSseEmitterRegistry, | ||||||||||||||||||||||||||||||
| private val studyAttendanceHistoryRepository: StudyAttendanceHistoryRepository, | ||||||||||||||||||||||||||||||
| private val clock: Clock, | ||||||||||||||||||||||||||||||
| ) : CheckStudyAttendanceService { | ||||||||||||||||||||||||||||||
| private val log = LoggerFactory.getLogger(javaClass) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
|
@@ -36,6 +42,12 @@ class CheckStudyAttendanceServiceImpl( | |||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| studyRedisAdapter.checkAttendance(userId) | ||||||||||||||||||||||||||||||
| val today = LocalDate.now(clock) | ||||||||||||||||||||||||||||||
| if (!studyAttendanceHistoryRepository.existsByUserIdAndAttendedDate(userId, today)) { | ||||||||||||||||||||||||||||||
| studyAttendanceHistoryRepository.save( | ||||||||||||||||||||||||||||||
| StudyAttendanceHistoryJpaEntity(user = user, attendedDate = today), | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
Comment on lines
44
to
+50
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 데이터베이스 저장( 안전한 트랜잭션 처리를 위해 데이터베이스 쓰기 작업을 Redis 상태 변경 및 SSE 브로드캐스트보다 먼저 수행하도록 순서를 변경하는 것을 권장합니다.
Suggested change
|
||||||||||||||||||||||||||||||
| log.info("checkAttendance Redis 반영 완료, broadcast 호출 직전: userId={}", userId) | ||||||||||||||||||||||||||||||
| sseEmitterRegistry.broadcast( | ||||||||||||||||||||||||||||||
| StudyAttendanceEventResponse(userId = user.id, name = user.name, studentNumber = user.studentNumber), | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| package team.incube.flooding.domain.dormitory.study.service.impl | ||
|
|
||
| import org.springframework.stereotype.Service | ||
| import org.springframework.transaction.annotation.Transactional | ||
| import team.incube.flooding.domain.dormitory.study.presentation.data.response.GetStudyAttendanceListResponse | ||
| import team.incube.flooding.domain.dormitory.study.repository.StudyAttendanceHistoryRepository | ||
| import team.incube.flooding.domain.dormitory.study.service.GetPublicStudyAttendanceListService | ||
| import java.time.Clock | ||
| import java.time.LocalDate | ||
|
|
||
| @Service | ||
| @Transactional(readOnly = true) | ||
| class GetPublicStudyAttendanceListServiceImpl( | ||
| private val studyAttendanceHistoryRepository: StudyAttendanceHistoryRepository, | ||
| private val clock: Clock, | ||
| ) : GetPublicStudyAttendanceListService { | ||
| override fun execute(): List<GetStudyAttendanceListResponse> { | ||
| val endDate = LocalDate.now(clock) | ||
| val startDate = endDate.minusDays(6) | ||
| val histories = | ||
| studyAttendanceHistoryRepository.findAllByAttendedDateBetweenOrderByAttendedDateAsc(startDate, endDate) | ||
| val namesByDate = histories.groupBy({ it.attendedDate }, { it.user.name }) | ||
| return (0..6).map { i -> | ||
| val date = startDate.plusDays(i.toLong()) | ||
| GetStudyAttendanceListResponse(date = date, students = namesByDate[date] ?: emptyList()) | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -7,16 +7,21 @@ import org.springframework.transaction.annotation.Transactional | |||||||||
| import team.incube.flooding.domain.dormitory.study.adapter.StudyAttendanceSseEmitterRegistry | ||||||||||
| import team.incube.flooding.domain.dormitory.study.adapter.StudyRedisAdapter | ||||||||||
| import team.incube.flooding.domain.dormitory.study.presentation.data.response.StudyAttendanceEventResponse | ||||||||||
| import team.incube.flooding.domain.dormitory.study.repository.StudyAttendanceHistoryRepository | ||||||||||
| import team.incube.flooding.domain.dormitory.study.service.UncheckStudyAttendanceService | ||||||||||
| import team.incube.flooding.domain.user.repository.UserRepository | ||||||||||
| import team.themoment.sdk.exception.ExpectedException | ||||||||||
| import java.time.Clock | ||||||||||
| import java.time.LocalDate | ||||||||||
|
|
||||||||||
| @Service | ||||||||||
| @Transactional | ||||||||||
| class UncheckStudyAttendanceServiceImpl( | ||||||||||
| private val userRepository: UserRepository, | ||||||||||
| private val studyRedisAdapter: StudyRedisAdapter, | ||||||||||
| private val sseEmitterRegistry: StudyAttendanceSseEmitterRegistry, | ||||||||||
| private val studyAttendanceHistoryRepository: StudyAttendanceHistoryRepository, | ||||||||||
| private val clock: Clock, | ||||||||||
| ) : UncheckStudyAttendanceService { | ||||||||||
| private val log = LoggerFactory.getLogger(javaClass) | ||||||||||
|
|
||||||||||
|
|
@@ -31,6 +36,7 @@ class UncheckStudyAttendanceServiceImpl( | |||||||||
| } | ||||||||||
|
|
||||||||||
| studyRedisAdapter.cancelAttendance(userId) | ||||||||||
| studyAttendanceHistoryRepository.deleteByUserIdAndAttendedDate(userId, LocalDate.now(clock)) | ||||||||||
|
Comment on lines
38
to
+39
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 데이터베이스 삭제 작업 중 예외가 발생할 경우 트랜잭션은 롤백되지만, 이미 실행된 Redis 작업( 데이터베이스 작업을 먼저 수행한 후 Redis 상태를 변경하도록 순서를 변경하는 것을 권장합니다.
Suggested change
|
||||||||||
| log.info("cancelAttendance Redis 반영 완료, broadcastCancel 호출 직전: userId={}", userId) | ||||||||||
| sseEmitterRegistry.broadcastCancel( | ||||||||||
| StudyAttendanceEventResponse(userId = user.id, name = user.name, studentNumber = user.studentNumber), | ||||||||||
|
|
||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| package team.incube.flooding.domain.neis.config | ||
|
|
||
| import org.springframework.boot.context.properties.ConfigurationProperties | ||
|
|
||
| @ConfigurationProperties(prefix = "datagsm.neis.timetables") | ||
| data class DgTimetableProperties( | ||
| val baseUrl: String, | ||
| val path: String = "v1/neis/timetables", | ||
| ) |
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Spring Data JPA의 기본
deleteBy...메서드는 대상을 먼저 조회(SELECT)한 후 개별적으로 삭제(DELETE)를 수행하므로 성능상 비효율적입니다. 단일 쿼리로 즉시 삭제할 수 있도록@Modifying과@Query를 사용한 벌크 삭제 쿼리로 변경하는 것을 권장합니다.@Modifying @Query("DELETE FROM StudyAttendanceHistoryJpaEntity h WHERE h.user.id = :userId AND h.attendedDate = :attendedDate") fun deleteByUserIdAndAttendedDate( @Param("userId") userId: Long, @Param("attendedDate") attendedDate: LocalDate, )References