Skip to content

Commit 272e1df

Browse files
authored
Merge pull request #229 from CSE-Shaco/develop
refactor: 출석 관리 DB 스키마 및 서비스 구조 재정비(관계 정규화)
2 parents 564e907 + 0b10eb1 commit 272e1df

File tree

5 files changed

+84
-66
lines changed

5 files changed

+84
-66
lines changed

src/main/java/inha/gdgoc/domain/core/attendance/entity/AttendanceRecord.java

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,36 @@
1-
// src/main/java/inha/gdgoc/domain/core/attendance/entity/AttendanceRecord.java
21
package inha.gdgoc.domain.core.attendance.entity;
32

43
import inha.gdgoc.domain.user.entity.User;
54
import jakarta.persistence.*;
65
import lombok.*;
7-
import java.time.LocalDate;
6+
87
import java.time.OffsetDateTime;
98

109
@Entity
11-
@Table(
12-
name = "attendance_records",
13-
uniqueConstraints = {
14-
@UniqueConstraint(name = "uq_attendance", columnNames = {"meeting_date", "user_id"})
15-
},
16-
indexes = {
17-
@Index(name = "idx_attendance_user_date", columnList = "user_id, meeting_date DESC"),
18-
@Index(name = "idx_attendance_date_only", columnList = "meeting_date DESC")
19-
}
20-
)
21-
@Getter @Setter
22-
@NoArgsConstructor @AllArgsConstructor @Builder
10+
@Table(name = "attendance_records", uniqueConstraints = {@UniqueConstraint(name = "uq_attendance", columnNames = {"meeting_id", "user_id"})}, indexes = {@Index(name = "idx_attendance_user_meeting", columnList = "user_id, meeting_id"), @Index(name = "idx_attendance_meeting_only", columnList = "meeting_id")})
11+
@Getter
12+
@Setter
13+
@NoArgsConstructor
14+
@AllArgsConstructor
15+
@Builder
2316
public class AttendanceRecord {
2417

2518
@Id
2619
@GeneratedValue(strategy = GenerationType.IDENTITY)
2720
private Long id;
2821

29-
/** FK → meetings(meeting_date) */
30-
@Column(name = "meeting_date", nullable = false)
31-
private LocalDate meetingDate;
22+
/**
23+
* FK → meetings(id)
24+
*/
25+
@ManyToOne(fetch = FetchType.LAZY, optional = false)
26+
@JoinColumn(name = "meeting_id", nullable = false, foreignKey = @ForeignKey(name = "fk_attendance_meeting"))
27+
private Meeting meeting;
3228

33-
/** FK → users(id) */
29+
/**
30+
* FK → users(id)
31+
*/
3432
@ManyToOne(fetch = FetchType.LAZY, optional = false)
35-
@JoinColumn(name = "user_id", nullable = false,
36-
foreignKey = @ForeignKey(name = "fk_attendance_user"))
33+
@JoinColumn(name = "user_id", nullable = false, foreignKey = @ForeignKey(name = "fk_attendance_user"))
3734
private User user;
3835

3936
@Column(name = "present", nullable = false)
@@ -42,13 +39,15 @@ public class AttendanceRecord {
4239
@Column(name = "updated_at", nullable = false)
4340
private OffsetDateTime updatedAt;
4441

45-
/** 마지막 수정자(선택) */
42+
/**
43+
* 마지막 수정자(선택)
44+
*/
4645
@ManyToOne(fetch = FetchType.LAZY)
47-
@JoinColumn(name = "updated_by",
48-
foreignKey = @ForeignKey(name = "fk_attendance_updated_by"))
46+
@JoinColumn(name = "updated_by", foreignKey = @ForeignKey(name = "fk_attendance_updated_by"))
4947
private User updatedBy;
5048

51-
@PrePersist @PreUpdate
49+
@PrePersist
50+
@PreUpdate
5251
void onUpdate() {
5352
updatedAt = OffsetDateTime.now();
5453
}

src/main/java/inha/gdgoc/domain/core/attendance/entity/Meeting.java

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,26 @@
22

33
import jakarta.persistence.*;
44
import lombok.*;
5+
56
import java.time.LocalDate;
67
import java.time.OffsetDateTime;
78

89
@Entity
9-
@Table(name = "meetings")
10-
@Getter @Setter
11-
@NoArgsConstructor @AllArgsConstructor @Builder
10+
@Table(name = "meetings", uniqueConstraints = {@UniqueConstraint(name = "uq_meeting_date", columnNames = "meeting_date")}, indexes = {@Index(name = "idx_meeting_date", columnList = "meeting_date")})
11+
@Getter
12+
@Setter
13+
@NoArgsConstructor
14+
@AllArgsConstructor
15+
@Builder
1216
public class Meeting {
1317

14-
/** 단일 날짜 = 단일 회의 정책 → 날짜를 PK로 */
1518
@Id
19+
@GeneratedValue(strategy = GenerationType.IDENTITY)
20+
private Long id;
21+
22+
/**
23+
* 도메인 식별자: 날짜는 유니크로 강제
24+
*/
1625
@Column(name = "meeting_date", nullable = false)
1726
private LocalDate meetingDate;
1827

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,36 @@
11
package inha.gdgoc.domain.core.attendance.repository;
22

33
import inha.gdgoc.domain.core.attendance.entity.AttendanceRecord;
4-
import java.time.LocalDate;
5-
import java.util.List;
6-
import org.springframework.data.jpa.repository.*;
4+
import org.springframework.data.jpa.repository.JpaRepository;
5+
import org.springframework.data.jpa.repository.Modifying;
6+
import org.springframework.data.jpa.repository.Query;
77
import org.springframework.data.repository.query.Param;
88
import org.springframework.stereotype.Repository;
99

10+
import java.util.List;
11+
1012
@Repository
1113
public interface AttendanceRecordRepository extends JpaRepository<AttendanceRecord, Long> {
1214

13-
/* 조회: 특정 날짜의 (userId, present) 목록 */
15+
/* 조회: 특정 meetingId의 (userId, present) 목록 */
1416
@Query("""
15-
select ar.user.id, ar.present
16-
from AttendanceRecord ar
17-
where ar.meetingDate = :date
18-
""")
19-
List<Object[]> findPresencePairsByDate(@Param("date") LocalDate date);
17+
select ar.user.id, ar.present
18+
from AttendanceRecord ar
19+
where ar.meeting.id = :meetingId
20+
""")
21+
List<Object[]> findPresencePairsByMeetingId(@Param("meetingId") Long meetingId);
2022

21-
/* 삭제: 해당 날짜의 출석 레코드 전체 삭제 */
22-
void deleteByMeetingDate(LocalDate date);
23+
/* 삭제: 해당 meetingId의 출석 레코드 전체 삭제 */
24+
void deleteByMeeting_Id(Long meetingId);
2325

24-
/* 배치 업서트(ON CONFLICT) — 고성능로 present를 일괄 반영 */
26+
/* 배치 업서트(ON CONFLICT) — meeting_id 기준 */
2527
@Modifying
2628
@Query(value = """
27-
INSERT INTO public.attendance_records (meeting_date, user_id, present, updated_at)
28-
SELECT :date, u, :present, NOW()
29-
FROM unnest(:userIds) AS u
30-
ON CONFLICT (meeting_date, user_id)
31-
DO UPDATE SET present = EXCLUDED.present, updated_at = NOW()
32-
""", nativeQuery = true)
33-
int upsertBatch(@Param("date") LocalDate date,
34-
@Param("userIds") List<Long> userIds,
35-
@Param("present") boolean present);
29+
INSERT INTO public.attendance_records (meeting_id, user_id, present, updated_at)
30+
SELECT :meetingId, u, :present, NOW()
31+
FROM unnest(:userIds) AS u
32+
ON CONFLICT (meeting_id, user_id)
33+
DO UPDATE SET present = EXCLUDED.present, updated_at = NOW()
34+
""", nativeQuery = true)
35+
int upsertBatchByMeetingId(@Param("meetingId") Long meetingId, @Param("userIds") List<Long> userIds, @Param("present") boolean present);
3636
}

src/main/java/inha/gdgoc/domain/core/attendance/repository/MeetingRepository.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
import inha.gdgoc.domain.core.attendance.entity.Meeting;
44
import org.springframework.data.jpa.repository.JpaRepository;
5-
65
import java.time.LocalDate;
6+
import java.util.Optional;
77

8-
public interface MeetingRepository extends JpaRepository<Meeting, LocalDate> {
8+
public interface MeetingRepository extends JpaRepository<Meeting, Long> {
9+
Optional<Meeting> findByMeetingDate(LocalDate meetingDate);
910
}

src/main/java/inha/gdgoc/domain/core/attendance/service/CoreAttendanceService.java

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -41,18 +41,17 @@ public List<String> getDates() {
4141
@Transactional
4242
public void addDate(String date) {
4343
LocalDate d = LocalDate.parse(date);
44-
if (!meetingRepository.existsById(d)) {
45-
meetingRepository.save(Meeting.builder().meetingDate(d).build());
46-
}
44+
meetingRepository.findByMeetingDate(d)
45+
.orElseGet(() -> meetingRepository.save(Meeting.builder().meetingDate(d).build()));
4746
}
4847

4948
@Transactional
5049
public void deleteDate(String date) {
5150
LocalDate d = LocalDate.parse(date);
52-
// FK ON DELETE CASCADE가 걸려있다면 아래 한 줄로 충분
53-
meetingRepository.deleteById(d);
54-
// 만약 FK cascade가 없다면 다음 라인을 활성화
55-
// attendanceRecordRepository.deleteByMeetingDate(d);
51+
meetingRepository.findByMeetingDate(d).ifPresent(m -> {
52+
// FK ON DELETE CASCADE라면 meeting만 지우면 attendance도 함께 삭제됨
53+
meetingRepository.deleteById(m.getId());
54+
});
5655
}
5756

5857
/* ===================== Teams ===================== */
@@ -114,12 +113,8 @@ public long setAttendance(String date, List<Long> userIds, boolean present) {
114113
if (userIds == null || userIds.isEmpty()) return 0L;
115114

116115
LocalDate d = LocalDate.parse(date);
117-
// 회의가 없으면 생성 (idempotent)
118-
if (!meetingRepository.existsById(d)) {
119-
meetingRepository.save(Meeting.builder().meetingDate(d).build());
120-
}
121-
int affected = attendanceRecordRepository.upsertBatch(d, userIds, present);
122-
// batch 결과는 행수에 대한 DB 드라이버 의존이 있어 절대값으로 환산
116+
Long meetingId = ensureMeetingAndGetId(d);
117+
int affected = attendanceRecordRepository.upsertBatchByMeetingId(meetingId, userIds, present);
123118
return Math.max(affected, 0);
124119
}
125120

@@ -219,11 +214,25 @@ public String buildSummaryCsv(String date, TeamType teamOrNull) {
219214

220215
/* ===================== helpers ===================== */
221216

217+
/** date로 meeting을 보장하고 meetingId 반환 */
218+
@Transactional
219+
protected Long ensureMeetingAndGetId(LocalDate date) {
220+
return meetingRepository.findByMeetingDate(date)
221+
.map(Meeting::getId)
222+
.orElseGet(() -> meetingRepository.save(
223+
Meeting.builder().meetingDate(date).build()
224+
).getId());
225+
}
226+
227+
/** 특정 날짜의 출석 맵(userId → present) */
222228
@Transactional(readOnly = true)
223229
protected Map<Long, Boolean> getPresenceMap(LocalDate date) {
224230
Map<Long, Boolean> map = new HashMap<>();
225-
attendanceRecordRepository.findPresencePairsByDate(date).forEach(row -> {
226-
// row[0]=userId(Long/Number), row[1]=present(Boolean)
231+
var meetingOpt = meetingRepository.findByMeetingDate(date);
232+
if (meetingOpt.isEmpty()) return map;
233+
234+
Long meetingId = meetingOpt.get().getId();
235+
attendanceRecordRepository.findPresencePairsByMeetingId(meetingId).forEach(row -> {
227236
Long uid = ((Number) row[0]).longValue();
228237
Boolean present = (Boolean) row[1];
229238
map.put(uid, present != null && present);

0 commit comments

Comments
 (0)