Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
e565b10
cors 주석
xihxxn May 31, 2026
667c684
connect api.piroin.com subdomain for HTTPS
xihxxn May 31, 2026
c2220b0
Merge pull request #153 from pirogramming/#82
xihxxn Jun 1, 2026
85da707
[Security] ADMIN 전용 API 엔드포인트 접근 제어 추가
xihxxn Jun 1, 2026
cf8e9db
[Security] formLogin/httpBasic 비활성화 및 ADMIN 전용 엔드포인트 접근 제어 추가
xihxxn Jun 1, 2026
df14f0f
Merge pull request #154 from pirogramming/#82
xihxxn Jun 1, 2026
409695a
[Feat] Header 반응형 구현
plumbestie Jun 1, 2026
1e6b6df
[Feat] Curriculum 반응형 구현
plumbestie Jun 1, 2026
abbae78
[Feat] Login 반응형 구현
plumbestie Jun 1, 2026
923701a
[Feat] Onboarding 반응형 구현
plumbestie Jun 1, 2026
b73821f
[Feat] PiroCheck(메인, 출석, 과제) 반응형 구현
plumbestie Jun 1, 2026
6e07aed
fix: resolveParentComment()에 삭제/소속 질문/2depth 초과 검증 추가
kkw610 Jun 2, 2026
bc74ccc
Merge pull request #157 from pirogramming/fix/#156
kkw610 Jun 2, 2026
78656bd
fix: V6 마이그레이션 - question_like, understanding_response 유니크 제약 추가
kkw610 Jun 2, 2026
f6b7e26
Merge pull request #159 from pirogramming/fix/#158
kkw610 Jun 2, 2026
8a6cd8c
refactor: 질문 목록 좋아요 여부 조회를 N+1에서 배치 단건 쿼리로 개선
kkw610 Jun 2, 2026
66204ee
Merge pull request #162 from pirogramming/refactor/#161
kkw610 Jun 2, 2026
74ae712
feat: 에러메세지 띄우기
Jun 2, 2026
e1326e1
Merge pull request #163 from pirogramming/feat/160
lilyyang0077 Jun 2, 2026
59f8013
feat: 세션 있는 날에만 출첵 되도록 수정
Jun 2, 2026
7e62bd2
Merge branch 'develop' into Feat/#155
plumbestie Jun 2, 2026
f9ae938
[Feat] PiroCheck(보증금, StudentDetail&List) 반응형 구현
plumbestie Jun 2, 2026
f323fb9
Merge pull request #165 from pirogramming/Feat/#155
plumbestie Jun 2, 2026
e98c74d
fix: 나의 전체 출석현황 요일별로 dto 수정
Jun 4, 2026
ab5320b
Merge pull request #166 from pirogramming/feat/164
lilyyang0077 Jun 4, 2026
f3ce682
feat: 하루 상세 출석 & 전체 출석 프론트 수정
Jun 4, 2026
7d50c04
Merge pull request #168 from pirogramming/feat/167
lilyyang0077 Jun 4, 2026
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.example.Piroin.project.domain.attendance.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;

import java.time.LocalDate;
import java.util.List;

@Getter
@Setter
@Schema(description = "요일별 출석 상태")
public class AttendanceDayStatusRes {

@Schema(description = "출석 날짜", example = "2026-06-23")
private LocalDate date;

@Schema(description = "요일", example = "TUESDAY")
private String day;

@Schema(description = "출석 차시별 상태 목록")
private List<AttendanceSlotRes> slots;
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,12 @@

@Getter
@Setter
@Schema(description = "사용자 출석 상태")
@Schema(description = "사용자 주차별 출석 상태")
public class AttendanceStatusRes {
@Schema(description = "출석 날짜", example = "2025-06-24")
private LocalDate date;

@Schema(description = "주차", example = "1")
private int week;

@Schema(description = "출석 차시별 상태 목록")
private List<AttendanceSlotRes> slots;
}
@Schema(description = "요일별 출석 상태 목록")
private List<AttendanceDayStatusRes> days;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.example.Piroin.project.domain.attendance.repository;

import com.example.Piroin.project.domain.attendance.entity.AttendanceCode;
import com.example.Piroin.project.domain.curriculum.entity.StudySession;
import com.example.Piroin.project.domain.user.entity.User;
import com.example.Piroin.project.domain.attendance.entity.Attendance;
import org.springframework.data.jpa.repository.JpaRepository;
Expand All @@ -20,7 +21,6 @@ public interface AttendanceRepository extends JpaRepository<Attendance, Long> {
// 연관관계 필드명이 attendanceCode 라면 내부 ID인 Id를 조합하여 명명
Optional<Attendance> findByUserIdAndAttendanceCodeId(Long userId, Long attendanceCodeId);

//List<Attendance> findByUserIdAndStudySessionSessionDate(Integer userId, LocalDate date);

int countByUserAndStatusFalse(User user);

Expand Down Expand Up @@ -54,6 +54,7 @@ Optional<Attendance> findByUserIdAndAttendanceCodeId(

List<Attendance> findByAttendanceCodeId(Integer id);


// 특정 날짜에 발급된 출석 코드의 개수를 세는 메서드
//long countByAttendanceDate(String attendanceDate);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package com.example.Piroin.project.domain.attendance.service;

import com.example.Piroin.project.domain.assignment.repository.AssignmentRepository;
import com.example.Piroin.project.domain.curriculum.entity.StudySession;
import com.example.Piroin.project.domain.curriculum.exception.CurriculumException;
import com.example.Piroin.project.domain.curriculum.exception.code.CurriculumErrorCode;
import com.example.Piroin.project.domain.curriculum.repository.CurriculumRepository;
import com.example.Piroin.project.domain.deposit.entity.Deposit;
import com.example.Piroin.project.domain.deposit.repository.DepositRepository;
Expand All @@ -23,13 +26,11 @@
import com.example.Piroin.project.domain.assignment.repository.AssignmentItemRepository;
import com.example.Piroin.project.domain.attendance.dto.UpdateUserStatusReq;
import com.example.Piroin.project.domain.curriculum.enums.SessionDayPart;
import com.example.Piroin.project.domain.attendance.dto.AttendanceDayStatusRes;


import java.time.LocalDate;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;

Expand All @@ -41,27 +42,29 @@ public class AttendanceService {
private final AttendanceCodeRepository attendanceCodeRepository;
private final UserRepository userRepository;
private final DepositService depositService;

private final CurriculumRepository curriculumRepository;

private final AssignmentItemRepository assignmentItemRepository;



// 1. 출석 시작 코드 (출석코드 생성 함수)
@Transactional
public AttendanceCode generateCodeAndCreateAttendances(LocalDate date) { // [수정] 세션 ID 대신 날짜를 직접 받음

// 1. [삭제] 더 이상 세션을 조회해서 날짜를 파싱할 필요가 없습니다. (curriculumRepository 조회 제거)
// 1-1) 해당 날짜에 커리큘럼이 있는지 확인
if (!curriculumRepository.existsBySessionDate(date)) {
throw new CurriculumException(
CurriculumErrorCode.ATTENDANCE_DATE_NOT_AVAILABLE
);
}

// 2. 해당 날짜에 생성된 출석 코드 개수 조회
// 1-2) 해당 날짜에 생성된 출석 코드 개수 조회.
long codeCountOfDay = attendanceCodeRepository.countByAttendanceDate(date);

if (codeCountOfDay >= 3) {
throw new IllegalStateException("하루에 최대 3회까지만 출석 코드를 생성할 수 있습니다.");
}

// 3. 기존 활성화된 코드들 만료 처리
// 1-3) 기존 활성화된 코드들 만료 처리
List<AttendanceCode> activeCodes = attendanceCodeRepository.findByIsExpiredFalse();
for (AttendanceCode activeCode : activeCodes) {
activeCode.expire();
Expand All @@ -79,11 +82,11 @@ public AttendanceCode generateCodeAndCreateAttendances(LocalDate date) { // [수
}


// 4. 4자리 랜덤 코드 생성 및 차수(Order) 계산
// 1-4) 4자리 랜덤 코드 생성 및 차수(Order) 계산
String code = String.valueOf(ThreadLocalRandom.current().nextInt(1000, 10000));
String attendanceOrder = String.valueOf(codeCountOfDay + 1); // 1회차, 2회차, 3회차

// 5. 새로운 AttendanceCode 생성 및 저장
// 1-5) 새로운 AttendanceCode 생성 및 저장
AttendanceCode attendanceCode = AttendanceCode.builder()
.attendanceDate(date) // [수정] 파라미터로 받은 날짜 주입
.attendanceOrder(attendanceOrder)
Expand All @@ -93,11 +96,10 @@ public AttendanceCode generateCodeAndCreateAttendances(LocalDate date) { // [수

attendanceCodeRepository.save(attendanceCode);

// 6. 모든 MEMBER 유저에 대해 '현재 생성된 출석 코드' 기준 초기 출석 데이터 생성
// 1-6) 모든 MEMBER 유저에 대해 '현재 생성된 출석 코드' 기준 초기 출석 데이터 생성
List<User> users = userRepository.findByRole(Role.MEMBER);

for (User user : users) {
// [확인] 이미 완벽하게 studySession 대신 attendanceCode를 주입하도록 잘 짜두셨습니다!
Attendance attendance = Attendance.builder()
.user(user)
.attendanceCode(attendanceCode)
Expand Down Expand Up @@ -233,103 +235,92 @@ public List<AttendanceSlotRes> findByUserIdAndDate(Integer userId, LocalDate dat
.toList();
}

// 6. 유저의 전체 출석 현황을 날짜별로 묶어서 조회하는 함수
// 6. 나의 전체 출석 현황 조회 서비스
public List<AttendanceStatusRes> findByUserId(Integer userId) {

List<Attendance> attendances =
attendanceRepository.findByUserId(Long.valueOf(userId));

// LocalDate 기준으로 그룹화
Map<LocalDate, List<Attendance>> grouped = attendances.stream()
.collect(Collectors.groupingBy(
attendance -> attendance.getAttendanceCode().getAttendanceDate()
));
// 날짜별 그룹화
Map<LocalDate, List<Attendance>> dateGrouped =
attendances.stream()
.collect(Collectors.groupingBy(
attendance ->
attendance.getAttendanceCode().getAttendanceDate()
));

// 주차별 그룹화
Map<Integer, List<AttendanceDayStatusRes>> weekGrouped =
new HashMap<>();

for (Map.Entry<LocalDate, List<Attendance>> entry : dateGrouped.entrySet()) {

LocalDate date = entry.getKey();

StudySession studySession =
curriculumRepository
.findFirstBySessionDate(date)
.orElseThrow(() ->
new RuntimeException("세션이 존재하지 않습니다.")
);
Comment on lines +260 to +265

int week = studySession.getWeek().intValue();

List<AttendanceSlotRes> slots =
entry.getValue().stream()
.map(attendance ->
new AttendanceSlotRes(
attendance.getAttendanceCode().getId(),
attendance.getStatus()
)
)
.sorted(
Comparator.comparing(
AttendanceSlotRes::getAttendanceCodeId
)
)
.toList();

AttendanceDayStatusRes dayRes = new AttendanceDayStatusRes();
dayRes.setDate(date);
dayRes.setDay(date.getDayOfWeek().toString());
dayRes.setSlots(slots);

return grouped.entrySet().stream()

weekGrouped
.computeIfAbsent(week, k -> new ArrayList<>())
.add(dayRes);
}

return weekGrouped.entrySet().stream()
.map(entry -> {

LocalDate date = entry.getKey();
AttendanceStatusRes dto =
new AttendanceStatusRes();

List<AttendanceSlotRes> slots = entry.getValue().stream()
.map(attendance -> new AttendanceSlotRes(
attendance.getAttendanceCode().getId(),
attendance.getStatus()
))
.sorted(Comparator.comparing(AttendanceSlotRes::getAttendanceCodeId))
.toList();
dto.setWeek(entry.getKey());

AttendanceStatusRes dto = new AttendanceStatusRes();
dto.setDate(date);
dto.setSlots(slots);
dto.setDays(
entry.getValue().stream()
.sorted(
Comparator.comparing(
AttendanceDayStatusRes::getDate
)
)
.toList()
);

return dto;
})
.sorted(Comparator.comparing(AttendanceStatusRes::getDate).reversed())
.sorted(
Comparator.comparing(
AttendanceStatusRes::getWeek
)
)
.toList();
}
//
// // 6. 유저 상태 변경 (관리자)
// // 컨트롤러 부분은 출석만 받는데 여기는 출석&과제 둘 다 받아서 추후에 수정 예정
// @Transactional
// public boolean updateUserStatus(Integer userId, UpdateUserStatusReq req) {
// boolean updated = false;
//
// // 출석 상태 변경 코드
// if (req.getAttendanceId() != null && req.getAttendanceStatus() != null) {
// Attendance attendance = attendanceRepository.findById(req.getAttendanceId())
// .orElseThrow(() -> new IllegalArgumentException("출석 기록을 찾을 수 없습니다."));
//
// if (!attendance.getUser().getId().equals(userId)) {
// throw new IllegalArgumentException("요청된 사용자와 출석 기록의 사용자가 일치하지 않습니다.");
// }
//
// attendance.updateStatus(req.getAttendanceStatus());
// updated = true;
// }
//
// // 과제 상태 변경 코드
// if (req.getAssignmentItemId() != null && req.getAssignmentStatus() != null) {
// AssignmentItem assignmentItem = assignmentItemRepository.findById(Math.toIntExact(req.getAssignmentItemId()))
// .orElseThrow(() -> new IllegalArgumentException("과제 기록을 찾을 수 없습니다."));
//
// if (!assignmentItem.getUser().getId().equals(userId)) {
// throw new IllegalArgumentException("요청된 사용자와 과제 기록의 사용자가 일치하지 않습니다.");
// }
//
// assignmentItem.updateSubmitted(req.getAssignmentStatus());
// updated = true;
// }
//
// // 출석 변경 → 보증금 재계산 (과제 변경도 포함이 되어 있나..?)
// if (updated) {
// depositService.recalculateDeposit(Long.valueOf(userId));
// }
//
// return updated;
// }


}

/*
// 관리자가 유저의 출석 상태를 변경하는 함수(나중에 과제까지 같이 변경되도록 수정할 것)
@Transactional
public boolean updateAttendanceStatus(Long attendanceId, boolean status) {
Optional<Attendance> attendanceOpt = attendanceRepository.findById(attendanceId);

if (attendanceOpt.isEmpty()) {
return false;
}

// 출석 상태 변경
Attendance attendance = attendanceOpt.get();
attendance.setStatus(status);
attendanceRepository.save(attendance);

// 출석 변경 → 보증금 재계산
depositService.recalculateDeposit(attendance.getUser().getId());

return true;
}
}

*/
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ public enum CurriculumErrorCode {
HttpStatus.BAD_REQUEST,
"CURRICULUM405",
"해당 주차/요일의 세션이 존재하지 않습니다. 세션을 먼저 생성해주세요."
),

ATTENDANCE_DATE_NOT_AVAILABLE(
HttpStatus.BAD_REQUEST,
"CURRICULUM406",
"해당 날짜는 세션 진행일이 아닙니다. 세션 일정 또는 커리큘럼을 확인해주세요."
);

private final HttpStatus status;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import java.time.LocalDate;
import java.util.List;
import java.util.Optional;

/*
StudySession(세션) DB 접근 인터페이스
Expand All @@ -25,6 +26,11 @@ public interface CurriculumRepository extends JpaRepository<StudySession, Long>

List<StudySession> findByWeekOrderBySessionDateAsc(Long week);

boolean existsBySessionDate(LocalDate sessionDate);

Optional<StudySession> findFirstBySessionDate(LocalDate sessionDate);


// @Query("""
// SELECT s
// FROM StudySession s
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
import com.example.Piroin.project.domain.question.entity.QuestionLike;
import com.example.Piroin.project.domain.user.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.List;
import java.util.Optional;

public interface QuestionLikeRepository extends JpaRepository<QuestionLike, Long> {
Expand All @@ -19,4 +22,12 @@ public interface QuestionLikeRepository extends JpaRepository<QuestionLike, Long
용도: 질문 상세 응답에 is_liked 필드 포함 시
*/
boolean existsByQuestionAndUser(Question question, User user);

// 특정 유저가 좋아요를 누른 질문 ID 목록을 한 번에 조회
// 용도: 질문 목록 조회 시 N+1 방지
@Query("SELECT ql.question.id FROM QuestionLike ql WHERE ql.question.id IN :questionIds AND ql.user = :user")
List<Long> findLikedQuestionIdsByQuestionIdsAndUser(
@Param("questionIds") List<Long> questionIds,
@Param("user") User user
);
}
Loading
Loading