diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index f0b620f..e969afb 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -40,6 +40,7 @@ jobs: - name: Copy JAR to EC2 run: | scp -o StrictHostKeyChecking=no -i pirocheck.pem backend/pirocheck/build/libs/*.jar ubuntu@${{ secrets.EC2_HOST }}:/home/ubuntu/ + - name: Restart Spring Boot on EC2 run: | @@ -106,4 +107,4 @@ jobs: AWS_S3_BUCKET: www.pirocheck.org AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - SOURCE_DIR: frontend/dist \ No newline at end of file + SOURCE_DIR: frontend/dist diff --git a/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/controller/AttendanceController.java b/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/controller/AttendanceController.java index 2401afb..103135c 100644 --- a/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/controller/AttendanceController.java +++ b/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/controller/AttendanceController.java @@ -2,93 +2,88 @@ import backend.pirocheck.Attendance.dto.request.MarkAttendanceReq; import backend.pirocheck.Attendance.dto.response.ApiResponse; -import backend.pirocheck.Attendance.dto.response.AttendanceCodeResponse; +import backend.pirocheck.Attendance.dto.response.AttendanceMarkResponse; import backend.pirocheck.Attendance.dto.response.AttendanceSlotRes; import backend.pirocheck.Attendance.dto.response.AttendanceStatusRes; -import backend.pirocheck.Attendance.entity.AttendanceCode; import backend.pirocheck.Attendance.service.AttendanceService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.web.bind.annotation.*; import java.time.LocalDate; import java.util.List; -import java.util.Optional; @RestController @RequiredArgsConstructor @RequestMapping("/api/attendance") +@Tag(name = "출석관리", description = "학생용 출석 관련 API") public class AttendanceController { private final AttendanceService attendanceService; // 특정 유저의 출석 정보 + @Operation(summary = "사용자 출석 정보 조회", description = "특정 사용자의 전체 출석 정보를 조회합니다.") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "조회 성공"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "잘못된 요청"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "사용자를 찾을 수 없음") + }) @GetMapping("/user") - public ApiResponse> getAttendanceByUserId(@RequestParam Long userId) { + public ApiResponse> getAttendanceByUserId( + @Parameter(description = "사용자 ID", required = true) + @RequestParam Long userId) { return ApiResponse.success(attendanceService.findByUserId(userId)); } // 특정 유저의 특정 일자 출석 정보 + @Operation(summary = "특정 날짜 출석 정보 조회", description = "특정 사용자의 특정 날짜 출석 정보를 조회합니다.") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "조회 성공"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "잘못된 요청"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "사용자 또는 날짜 정보를 찾을 수 없음") + }) @GetMapping("/user/date") public ApiResponse> getAttendanceByUserIdAndDate( + @Parameter(description = "사용자 ID", required = true) @RequestParam Long userId, + @Parameter(description = "조회할 날짜 (YYYY-MM-DD)", required = true) @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date ) { return ApiResponse.success(attendanceService.findByUserIdAndDate(userId, date)); } - // 출석체크 시작 - @PostMapping("/start") - public ApiResponse postAttendance() { - AttendanceCode code = attendanceService.generateCodeAndCreateAttendances(); - return ApiResponse.success(AttendanceCodeResponse.from(code)); - } - - // 현재 활성화된 출석코드 조회 - @GetMapping("/active-code") - public ApiResponse getActiveCode() { - Optional codeOpt = attendanceService.getActiveAttendanceCode(); - - if (codeOpt.isEmpty()) { - return ApiResponse.error("현재 활성화된 출석코드가 없습니다"); - } - - return ApiResponse.success(AttendanceCodeResponse.from(codeOpt.get())); - } - // 출석코드 비교 + @Operation(summary = "출석 체크", description = "출석 코드를 입력하여 출석을 체크합니다.") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "출석 성공 또는 이미 출석 완료"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "잘못된 출석 코드 또는 출석 체크 진행중이 아님") + }) @PostMapping("/mark") - public ApiResponse markAttendance(@RequestBody MarkAttendanceReq req) { - String result = attendanceService.markAttendance(req.getUserId(), req.getCode()); - - if (result.equals("출석이 성공적으로 처리되었습니다")) { - return ApiResponse.success(result, null); - } else { - return ApiResponse.error(result); - } - } - - // 출석체크 종료 (코드 직접 전달) - @PutMapping("/expire") - public ApiResponse expireAttendance(@RequestParam String code) { - String result = attendanceService.exprireAttendanceCode(code); + public ApiResponse markAttendance( + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "출석 체크 요청", required = true, + content = @Content(schema = @Schema(implementation = MarkAttendanceReq.class))) + @RequestBody MarkAttendanceReq req) { + AttendanceMarkResponse response = attendanceService.markAttendance(req.getUserId(), req.getCode()); - if (result.equals("출석 코드가 성공적으로 만료되었습니다")) { - return ApiResponse.success(result, null); - } else { - return ApiResponse.error(result); - } - } - - // 출석체크 종료 (가장 최근 활성화된 코드 자동 만료) - @PutMapping("/expire-latest") - public ApiResponse expireLatestAttendance() { - String result = attendanceService.expireLatestAttendanceCode(); + // statusCode가 SUCCESS 또는 ALREADY_MARKED인 경우 성공으로 처리 + boolean isSuccess = "SUCCESS".equals(response.getStatusCode()) || + "ALREADY_MARKED".equals(response.getStatusCode()); - if (result.equals("출석 코드가 성공적으로 만료되었습니다")) { - return ApiResponse.success(result, null); + if (isSuccess) { + return ApiResponse.success(response); } else { - return ApiResponse.error(result); + // 그 외의 경우 (NO_ACTIVE_SESSION, CODE_EXPIRED, ERROR)는 오류로 처리 + return ApiResponse.builder() + .success(false) + .message(response.getMessage()) + .data(response) + .build(); } } } diff --git a/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/dto/request/MarkAttendanceReq.java b/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/dto/request/MarkAttendanceReq.java index 779ead3..450728e 100644 --- a/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/dto/request/MarkAttendanceReq.java +++ b/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/dto/request/MarkAttendanceReq.java @@ -1,9 +1,14 @@ package backend.pirocheck.Attendance.dto.request; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; @Getter +@Schema(description = "출석 체크 요청") public class MarkAttendanceReq { + @Schema(description = "사용자 ID", example = "1") private Long userId; + + @Schema(description = "출석 코드", example = "1234") private String code; } diff --git a/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/dto/response/AttendanceCodeResponse.java b/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/dto/response/AttendanceCodeResponse.java index 4fe0b37..ca3dc4b 100644 --- a/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/dto/response/AttendanceCodeResponse.java +++ b/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/dto/response/AttendanceCodeResponse.java @@ -1,6 +1,7 @@ package backend.pirocheck.Attendance.dto.response; import backend.pirocheck.Attendance.entity.AttendanceCode; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -12,10 +13,18 @@ @Builder @NoArgsConstructor @AllArgsConstructor +@Schema(description = "출석 코드 응답") public class AttendanceCodeResponse { + @Schema(description = "출석 코드", example = "1234") private String code; + + @Schema(description = "출석 날짜", example = "2025-06-24") private LocalDate date; + + @Schema(description = "출석 차시 (1, 2, 3)", example = "1") private int order; + + @Schema(description = "만료 여부", example = "false") private boolean isExpired; public static AttendanceCodeResponse from(AttendanceCode attendanceCode) { diff --git a/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/dto/response/AttendanceSlotRes.java b/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/dto/response/AttendanceSlotRes.java index aa1a5cc..8a804bd 100644 --- a/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/dto/response/AttendanceSlotRes.java +++ b/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/dto/response/AttendanceSlotRes.java @@ -1,5 +1,6 @@ package backend.pirocheck.Attendance.dto.response; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; @@ -7,8 +8,11 @@ @Getter @Setter @AllArgsConstructor +@Schema(description = "출석 차시별 상태") public class AttendanceSlotRes { + @Schema(description = "출석 차시 (1, 2, 3)", example = "1") private int order; + + @Schema(description = "출석 여부", example = "true") private boolean status; - } diff --git a/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/dto/response/AttendanceStatusRes.java b/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/dto/response/AttendanceStatusRes.java index 7a0cfa4..6716f8a 100644 --- a/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/dto/response/AttendanceStatusRes.java +++ b/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/dto/response/AttendanceStatusRes.java @@ -1,5 +1,6 @@ package backend.pirocheck.Attendance.dto.response; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.Setter; @@ -8,7 +9,14 @@ @Getter @Setter +@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 slots; } diff --git a/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/service/AttendanceService.java b/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/service/AttendanceService.java index de65f49..ae2d5a8 100644 --- a/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/service/AttendanceService.java +++ b/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/service/AttendanceService.java @@ -3,6 +3,7 @@ import backend.pirocheck.User.entity.Role; import backend.pirocheck.User.entity.User; import backend.pirocheck.User.repository.UserRepository; +import backend.pirocheck.Attendance.dto.response.AttendanceMarkResponse; import backend.pirocheck.Attendance.dto.response.AttendanceSlotRes; import backend.pirocheck.Attendance.dto.response.AttendanceStatusRes; import backend.pirocheck.Attendance.entity.Attendance; @@ -43,6 +44,11 @@ public AttendanceCode generateCodeAndCreateAttendances() { // 오늘 생성된 출석코드 개수 = 현재까지 생성된 차시 수 + 1 (MAX=3) int currentOrder = attendanceCodeRepository.countByDate(today) + 1; + + // 하루 최대 3회 출석 체크만 허용 + if (currentOrder > 3) { + throw new IllegalStateException("하루에 최대 3회까지만 출석 체크를 진행할 수 있습니다."); + } // 1. 출석 코드 생성 String code = String.valueOf(ThreadLocalRandom.current().nextInt(1000, 10000)); @@ -119,25 +125,38 @@ public String exprireAttendanceCode(String code) { // 출석처리 함수 @Transactional - public String markAttendance(Long userId, String inputCode) { - // 1. 출석코드 일치 비교 - Optional validCodeOpt = attendanceCodeRepository.findByCodeAndDate(inputCode, LocalDate.now()); - + public AttendanceMarkResponse markAttendance(Long userId, String inputCode) { + // 오늘 날짜 + LocalDate today = LocalDate.now(); + + // 현재 활성화된 출석 코드가 있는지 확인 + List activeCodes = attendanceCodeRepository.findByDateAndIsExpiredFalse(today); + + // 활성화된 출석 코드가 없는 경우 + if (activeCodes.isEmpty()) { + return AttendanceMarkResponse.noActiveSession(); + } + + // 입력한 출석 코드와 일치하는 코드가 있는지 확인 + Optional validCodeOpt = attendanceCodeRepository.findByCodeAndDate(inputCode, today); + + // 입력한 출석 코드가 존재하지 않는 경우 if (validCodeOpt.isEmpty()) { - return "출석 코드가 존재하지 않습니다. 현재 출석 체크가 진행중이 아닙니다"; + return AttendanceMarkResponse.invalidCode(); } AttendanceCode code = validCodeOpt.get(); + // 입력한 출석 코드가 만료된 경우 if (code.isExpired()) { - return "출석 코드가 만료되었습니다"; + return AttendanceMarkResponse.codeExpired(); } // 2. 해당 유저의 출석 레코드 조회 Optional attendanceOpt = attendanceRepository.findByUserIdAndDateAndOrder(userId, code.getDate(), code.getOrder()); if (attendanceOpt.isEmpty()) { - return "출석 정보를 찾을 수 없습니다"; + return AttendanceMarkResponse.error("출석 정보를 찾을 수 없습니다"); } // 3. 출석 처리 @@ -145,13 +164,13 @@ public String markAttendance(Long userId, String inputCode) { // 이미 출석한 경우 if (attendance.isStatus()) { - return "이미 출석처리가 완료되었습니다"; + return AttendanceMarkResponse.alreadyMarked(); } attendance.setStatus(true); attendanceRepository.save(attendance); - return "출석이 성공적으로 처리되었습니다"; + return AttendanceMarkResponse.success(); } // 유저의 전체 출석 현황을 조회하는 함수 diff --git a/backend/pirocheck/src/main/java/backend/pirocheck/Deposit/entity/Deposit.java b/backend/pirocheck/src/main/java/backend/pirocheck/Deposit/entity/Deposit.java index 139e385..a0431e6 100644 --- a/backend/pirocheck/src/main/java/backend/pirocheck/Deposit/entity/Deposit.java +++ b/backend/pirocheck/src/main/java/backend/pirocheck/Deposit/entity/Deposit.java @@ -2,13 +2,11 @@ import backend.pirocheck.User.entity.User; import jakarta.persistence.*; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.*; @Entity @Getter +@Setter @NoArgsConstructor @AllArgsConstructor @Builder @@ -37,4 +35,12 @@ public void updateAmounts(int descentAssignment, int descentAttendance, int asce this.amount = Math.min(calculateAmount, 120000); // 12만원 넘어가지 않도록 } + // 방어권 업데이트 + public void updateDefence(int newAscentDefence) { + this.ascentDefence = newAscentDefence; + int calculateAmount = 120000 - this.descentAssignment - this.descentAttendance + newAscentDefence; + this.amount = Math.min(calculateAmount, 120000); + + } + } diff --git a/backend/pirocheck/src/main/java/backend/pirocheck/ManageStudents/Controller/ManageStudentsController.java b/backend/pirocheck/src/main/java/backend/pirocheck/ManageStudents/Controller/ManageStudentsController.java new file mode 100644 index 0000000..043b571 --- /dev/null +++ b/backend/pirocheck/src/main/java/backend/pirocheck/ManageStudents/Controller/ManageStudentsController.java @@ -0,0 +1,37 @@ +package backend.pirocheck.ManageStudents.Controller; + +import backend.pirocheck.ManageStudents.dto.request.DefenceUpdateReqDto; +import backend.pirocheck.ManageStudents.dto.response.ManageStudentDetailResDto; +import backend.pirocheck.ManageStudents.dto.response.ManageStudentsListResDto; +import backend.pirocheck.ManageStudents.service.ManageStudentsService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/admin/managestudent") +@RequiredArgsConstructor +public class ManageStudentsController { + + private final ManageStudentsService manageStudentsService; + + // 수강생 리스트 조회 + @GetMapping("") + public List getStudents(@RequestParam(required = false) String name) { + return manageStudentsService.searchMembers(name); + } + + // 수강생 상세 조회 + @GetMapping("/{studentId}") + public ManageStudentDetailResDto getStudentDetail(@PathVariable Long studentId) { + return manageStudentsService.getMemberDetail(studentId); + } + + // 방어권 업데이트 + @PatchMapping("/{studentId}/defence") + public void updateDefence(@PathVariable Long studentId, @RequestBody DefenceUpdateReqDto req) { + manageStudentsService.updateDefence(studentId, req.getDefence()); + } + +} diff --git a/backend/pirocheck/src/main/java/backend/pirocheck/ManageStudents/dto/request/DefenceUpdateReqDto.java b/backend/pirocheck/src/main/java/backend/pirocheck/ManageStudents/dto/request/DefenceUpdateReqDto.java new file mode 100644 index 0000000..78e39f7 --- /dev/null +++ b/backend/pirocheck/src/main/java/backend/pirocheck/ManageStudents/dto/request/DefenceUpdateReqDto.java @@ -0,0 +1,11 @@ +package backend.pirocheck.ManageStudents.dto.request; + + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class DefenceUpdateReqDto { + private int defence; +} diff --git a/backend/pirocheck/src/main/java/backend/pirocheck/ManageStudents/dto/response/ManageStudentDetailResDto.java b/backend/pirocheck/src/main/java/backend/pirocheck/ManageStudents/dto/response/ManageStudentDetailResDto.java new file mode 100644 index 0000000..e5206fc --- /dev/null +++ b/backend/pirocheck/src/main/java/backend/pirocheck/ManageStudents/dto/response/ManageStudentDetailResDto.java @@ -0,0 +1,16 @@ +package backend.pirocheck.ManageStudents.dto.response; + +import lombok.Builder; +import lombok.Getter; + +import java.util.List; + +@Getter +@Builder +public class ManageStudentDetailResDto { + + private String name; + private int deposit; + private int defence; // 방어권 + private List assignmentTitles; // 과제 제목 리스트 +} diff --git a/backend/pirocheck/src/main/java/backend/pirocheck/ManageStudents/dto/response/ManageStudentsListResDto.java b/backend/pirocheck/src/main/java/backend/pirocheck/ManageStudents/dto/response/ManageStudentsListResDto.java new file mode 100644 index 0000000..d5e028f --- /dev/null +++ b/backend/pirocheck/src/main/java/backend/pirocheck/ManageStudents/dto/response/ManageStudentsListResDto.java @@ -0,0 +1,13 @@ +package backend.pirocheck.ManageStudents.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class ManageStudentsListResDto { + + private Long id; + private String name; + +} diff --git a/backend/pirocheck/src/main/java/backend/pirocheck/ManageStudents/service/ManageStudentsService.java b/backend/pirocheck/src/main/java/backend/pirocheck/ManageStudents/service/ManageStudentsService.java new file mode 100644 index 0000000..e28c409 --- /dev/null +++ b/backend/pirocheck/src/main/java/backend/pirocheck/ManageStudents/service/ManageStudentsService.java @@ -0,0 +1,86 @@ +package backend.pirocheck.ManageStudents.service; + +import backend.pirocheck.Deposit.entity.Deposit; +import backend.pirocheck.Deposit.repository.DepositRepository; +import backend.pirocheck.ManageStudents.dto.response.ManageStudentDetailResDto; +import backend.pirocheck.ManageStudents.dto.response.ManageStudentsListResDto; +import backend.pirocheck.User.entity.Role; +import backend.pirocheck.User.entity.User; +import backend.pirocheck.User.repository.UserRepository; +import backend.pirocheck.assignment.entity.Assignment; +import backend.pirocheck.assignment.repository.AssignmentRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class ManageStudentsService { + + private final UserRepository userRepository; + private final DepositRepository depositRepository; + private final AssignmentRepository assignmentRepository; + + // 수강생 조회 + public List searchMembers(String name) { + List users; + + if(name == null || name.isBlank()) { + // 검색어가 없으면 맴버 전체 조회 + users = userRepository.findByRole(Role.MEMBER); + } + else { + // 이름 검색 + users = userRepository.findByNameContainingAndRole(name, Role.MEMBER); + } + + return users.stream() + .map(user -> new ManageStudentsListResDto(user.getId(), user.getName())) + .collect(Collectors.toList()); + } + + // 수강생 상세 조회 + public ManageStudentDetailResDto getMemberDetail(Long studentId) { + // User 조회 + User user = userRepository.findById(studentId) + .orElseThrow(() -> new RuntimeException("해당 맴버가 존재하지 않습니다.")); + + // deposit 조회 + Deposit deposit = depositRepository.findByUser(user); + if (deposit == null) { + throw new RuntimeException("해당 수강생의 보증금 정보가 없습니다."); + } + + // Assignment 리스트 조회 + List assignments = assignmentRepository.findByUserId(studentId); + + // 과제 제목만 리스트로 변환 + List assignmentTitles = assignments.stream() + .map(Assignment::getAssignmentName) + .toList(); + + // ManageStudentDetailResDto 조립 + return ManageStudentDetailResDto.builder() + .name(user.getName()) + .deposit(deposit.getAmount()) + .defence(deposit.getAscentDefence()) + .assignmentTitles(assignmentTitles) + .build(); + } + + // 방어권 업데이트 + public void updateDefence(Long studentId, int defence) { + User user = userRepository.findById(studentId) + .orElseThrow(() -> new RuntimeException("해당 수강생의 보증금 정보가 없습니다.")); + Deposit deposit = depositRepository.findByUser(user); + + // 업데이트 + deposit.updateDefence(defence); + + // 저장 + depositRepository.save(deposit); + + } +} diff --git a/backend/pirocheck/src/main/java/backend/pirocheck/User/exception/GlobalExceptionHandler.java b/backend/pirocheck/src/main/java/backend/pirocheck/User/exception/GlobalExceptionHandler.java index 4fd3755..f26232f 100644 --- a/backend/pirocheck/src/main/java/backend/pirocheck/User/exception/GlobalExceptionHandler.java +++ b/backend/pirocheck/src/main/java/backend/pirocheck/User/exception/GlobalExceptionHandler.java @@ -16,4 +16,12 @@ public ResponseEntity> handleInvalidLoginException(InvalidLoginEx .status(HttpStatus.UNAUTHORIZED) // 401 상태 코드 .body(ApiResponse.error(e.getMessage())); // 에러 메시지 전달 } + + // RuntimeException (유저관리 상세페이지) + @ExceptionHandler(RuntimeException.class) + public ResponseEntity> handleRuntimeException(RuntimeException ex) { + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) // 404 + .body(ApiResponse.error(ex.getMessage())); + } } diff --git a/backend/pirocheck/src/main/java/backend/pirocheck/User/repository/UserRepository.java b/backend/pirocheck/src/main/java/backend/pirocheck/User/repository/UserRepository.java index 9f80d25..8859494 100644 --- a/backend/pirocheck/src/main/java/backend/pirocheck/User/repository/UserRepository.java +++ b/backend/pirocheck/src/main/java/backend/pirocheck/User/repository/UserRepository.java @@ -12,4 +12,7 @@ public interface UserRepository extends JpaRepository { Optional findByName(String name); List findByRole(Role role); + + // 학생 이름으로 검색기능 + List findByNameContainingAndRole(String name, Role role); } diff --git a/backend/pirocheck/src/main/java/backend/pirocheck/assignment/controller/AssignmentController.java b/backend/pirocheck/src/main/java/backend/pirocheck/assignment/controller/AssignmentController.java index 3552b44..0c1379f 100644 --- a/backend/pirocheck/src/main/java/backend/pirocheck/assignment/controller/AssignmentController.java +++ b/backend/pirocheck/src/main/java/backend/pirocheck/assignment/controller/AssignmentController.java @@ -10,7 +10,7 @@ import java.util.List; @RestController -@RequestMapping("/assignment") +@RequestMapping("/api/assignment") @RequiredArgsConstructor public class AssignmentController { diff --git a/backend/pirocheck/src/main/java/backend/pirocheck/config/WebConfig.java b/backend/pirocheck/src/main/java/backend/pirocheck/config/WebConfig.java index a6ae980..1dcff3e 100644 --- a/backend/pirocheck/src/main/java/backend/pirocheck/config/WebConfig.java +++ b/backend/pirocheck/src/main/java/backend/pirocheck/config/WebConfig.java @@ -10,8 +10,9 @@ public class WebConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") // 백엔드 API 요청에만 CORS 허용 - .allowedOrigins("http://pirocheck.org:3000") // 프론트 배포 URL - .allowedMethods("GET", "POST", "PUT", "DELETE") // 허용할 HTTP 메서드 + .allowedOrigins("http://www.pirocheck.org") // 프론트 배포 URL + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // 허용할 HTTP 메서드 + .allowedHeaders("*") .allowCredentials(true); // 세션 쿠키 주고받기 허용 } } diff --git a/backend/pirocheck/src/main/resources/application.yml b/backend/pirocheck/src/main/resources/application.yml index b8b7096..bdef4aa 100644 --- a/backend/pirocheck/src/main/resources/application.yml +++ b/backend/pirocheck/src/main/resources/application.yml @@ -20,4 +20,20 @@ server: secure: false # HTTPS 전용 전송 (Https -> true로 바꿔야 함) same-site: Lax # CSRF 방지 timeout: 30m # 세션 타임아웃 30분 (30 minutes) - address: 0.0.0.0 \ No newline at end of file + address: 0.0.0.0 + +# Swagger 설정 +springdoc: + packages-to-scan: backend.pirocheck + default-consumes-media-type: application/json + default-produces-media-type: application/json + swagger-ui: + path: /swagger-ui.html + disable-swagger-default-url: true + display-request-duration: true + tags-sorter: alpha + operations-sorter: alpha + api-docs: + path: /api-docs + cache: + disabled: true \ No newline at end of file diff --git a/frontend/src/Admin.jsx b/frontend/src/Admin.jsx deleted file mode 100644 index 1eb01d0..0000000 --- a/frontend/src/Admin.jsx +++ /dev/null @@ -1,9 +0,0 @@ -const Admin = () => { - return ( -
-

Admin Page

-

This is the admin page.

-
- ); -}; -export default Admin; diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index db217ff..b11b9d0 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,12 +1,15 @@ import React from "react"; import { BrowserRouter, Routes, Route } from "react-router-dom"; import Login from "./Login"; -import Home from "./Home"; -import Assignment from "./Assignment"; -import Deposit from "./Deposit"; +import Home from "./pages/generation/Home"; +import Assignment from "./pages/generation/Assignment"; +import Deposit from "./pages/generation/Deposit"; import Intro from "./Intro"; -import Admin from "./Admin"; -import Attendance from "./Attendance"; +import Admin from "./pages/admin/Admin"; +import MagageStudent from "./pages/admin/ManageStudent.jsx"; +import ManageTask from "./pages/admin/ManageTask.jsx"; +import AttendanceCode from "./pages/admin/AttendanceCode"; +import Attendance from "./pages/generation/Attendance"; function App() { return ( @@ -19,6 +22,9 @@ function App() { } /> } /> } /> + } /> + } /> + } /> ); diff --git a/frontend/src/api/api.js b/frontend/src/api/api.js new file mode 100644 index 0000000..2cd0305 --- /dev/null +++ b/frontend/src/api/api.js @@ -0,0 +1,19 @@ +import axios from "axios"; + +const api = axios.create({ + baseURL: "http://api.:8080/api", + withCredentials: true, +}); + +// 401 오류 시 로그인 페이지로 리다이렉트 +api.interceptors.response.use( + (response) => response, + (error) => { + if (error.response?.status === 401) { + window.location.href = "/login"; + } + return Promise.reject(error); + } +); + +export default api; diff --git a/frontend/src/api/assignment.js b/frontend/src/api/assignment.js index 4102105..cf1a11a 100644 --- a/frontend/src/api/assignment.js +++ b/frontend/src/api/assignment.js @@ -1,5 +1,6 @@ -import axios from "axios"; +import api from "./api"; export const fetchAssignmentsByUser = async (userId) => { - const res = await axios.get(`/api/assignment/grouped/${userId}`); return res.data; + const res = await api.get(`/assignment/grouped/${userId}`); + return res.data; }; diff --git a/frontend/src/api/user.js b/frontend/src/api/user.js index 3964bac..12e057a 100644 --- a/frontend/src/api/user.js +++ b/frontend/src/api/user.js @@ -1,5 +1,5 @@ export const loginUser = async ({ name, password }) => { - const res = await fetch("/api/login", { + const res = await fetch("http://api.pirocheck.org:8080/api/login", { method: "POST", headers: { "Content-Type": "application/json", diff --git a/frontend/src/pages/admin/Admin.jsx b/frontend/src/pages/admin/Admin.jsx new file mode 100644 index 0000000..fa42daf --- /dev/null +++ b/frontend/src/pages/admin/Admin.jsx @@ -0,0 +1,34 @@ +import { useNavigate } from "react-router-dom"; +import styles from "./Admin.module.css"; + +const Admin = () => { + const navigate = useNavigate(); + return ( +
+
+

PIROCHECK

+ + + + 로고 +
+
+ ); +}; + +export default Admin; diff --git a/frontend/src/pages/admin/Admin.module.css b/frontend/src/pages/admin/Admin.module.css new file mode 100644 index 0000000..760d1d9 --- /dev/null +++ b/frontend/src/pages/admin/Admin.module.css @@ -0,0 +1,50 @@ +.home { + height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + overflow: hidden; + font-family: "Akira Expanded"; +} +.pirocheck { + margin-top: 84px; + font-size: 1.25rem; + margin-bottom: 103px; +} +.home_container { + background-color: var(--background-black); + color: var(--main-green); + font-family: "Cafe24Moyamoya-Regular-v1.0", sans-serif; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); +} +.button { + width: 309px; + height: 81px; + border-radius: 8px; + background-color: var(--border-gray); + color: var(--text-white); + font-family: "Akira Expanded"; + font-weight: 400; + font-size: 16px; + border: none; + padding: 29px 103px; + font-weight: bold; + margin-bottom: 26px; + z-index: 1; + display: flex; + justify-content: center; +} +.button p { + text-align: start; +} +.button:hover { + background-color: var(--card-toggle-green); + color: var(--text-white); +} +.home img { + position: relative; + opacity: 0.8; + bottom: 11%; + z-index: 0; +} diff --git a/frontend/src/pages/admin/AttendanceCode.jsx b/frontend/src/pages/admin/AttendanceCode.jsx new file mode 100644 index 0000000..d184e1d --- /dev/null +++ b/frontend/src/pages/admin/AttendanceCode.jsx @@ -0,0 +1,4 @@ +const AttendanceCode = () => { + return

출석코드 생성

; +}; +export default AttendanceCode; diff --git a/frontend/src/pages/admin/ManageStudent.jsx b/frontend/src/pages/admin/ManageStudent.jsx new file mode 100644 index 0000000..41f63e8 --- /dev/null +++ b/frontend/src/pages/admin/ManageStudent.jsx @@ -0,0 +1,4 @@ +const MagageStudent = () => { + return

수강생 관리

; +}; +export default MagageStudent; diff --git a/frontend/src/pages/admin/ManageTask.jsx b/frontend/src/pages/admin/ManageTask.jsx new file mode 100644 index 0000000..7140e6e --- /dev/null +++ b/frontend/src/pages/admin/ManageTask.jsx @@ -0,0 +1,4 @@ +const MagageTask = () => { + return

과제 관리

; +}; +export default MagageTask; diff --git a/frontend/src/Assignment.jsx b/frontend/src/pages/generation/Assignment.jsx similarity index 64% rename from frontend/src/Assignment.jsx rename to frontend/src/pages/generation/Assignment.jsx index cff140f..d1c0115 100644 --- a/frontend/src/Assignment.jsx +++ b/frontend/src/pages/generation/Assignment.jsx @@ -1,9 +1,10 @@ import React, { useEffect, useState } from "react"; -import WeeklyListBlock from "./components/WeeklyListBlock"; -import Header from "./components/Header"; -import AssignmentInfoBlock from "./components/AssignmentInfoBlock"; +import WeeklyListBlock from "../../components/WeeklyListBlock"; +import Header from "../../components/Header"; +import AssignmentInfoBlock from "../../components/AssignmentInfoBlock"; import styles from "./Assignment.module.css"; -import { mapStatus } from "./utils/AssignmentStatus.js"; +import { mapStatus } from "../../utils/AssignmentStatus.js"; +import { fetchAssignmentsByUser } from "../../api/assignment.js"; const Assignment = () => { const [weeks, setWeeks] = useState([]); @@ -16,18 +17,18 @@ const Assignment = () => { if (!userId) return; fetchAssignmentsByUser(userId) - .then((weekData) => { - const formatted = weekData.map((weekItem) => ({ - label: `${weekItem.week}주차 ${weekItem.title}`, - details: weekItem.days.map((dayItem) => ({ - day: dayItem.day, - subject: weekItem.title, - tasks: dayItem.details.map((task) => ({ - label: task.assignmentName, - status: mapStatus(task.status), + .then((weekData) => { + const formatted = weekData.map((weekItem) => ({ + label: `${weekItem.week}주차 ${weekItem.title}`, + details: weekItem.days.map((dayItem) => ({ + day: dayItem.day, + subject: weekItem.title, + tasks: dayItem.details.map((task) => ({ + label: task.assignmentName, + status: mapStatus(task.status), + })), })), - })), - })); + })); setWeeks(formatted); @@ -63,4 +64,3 @@ const Assignment = () => { }; export default Assignment; - diff --git a/frontend/src/Assignment.module.css b/frontend/src/pages/generation/Assignment.module.css similarity index 100% rename from frontend/src/Assignment.module.css rename to frontend/src/pages/generation/Assignment.module.css diff --git a/frontend/src/Attendance.jsx b/frontend/src/pages/generation/Attendance.jsx similarity index 89% rename from frontend/src/Attendance.jsx rename to frontend/src/pages/generation/Attendance.jsx index cff814f..dce08b1 100644 --- a/frontend/src/Attendance.jsx +++ b/frontend/src/pages/generation/Attendance.jsx @@ -1,9 +1,9 @@ import { useEffect, useState } from "react"; -import Header from "./components/Header"; -import InputBlock from "./components/InputBlock"; -import AttendanceWeekInfo from "./components/AttendanceWeekInfo"; +import Header from "../../components/Header"; +import InputBlock from "../../components/InputBlock"; +import AttendanceWeekInfo from "../../components/AttendanceWeekInfo"; import styles from "./Attendance.module.css"; -import axios from "axios"; +import api from "../../api/api"; const Attendance = () => { const [attendanceCode, setAttendanceCode] = useState([""]); @@ -88,8 +88,9 @@ const Attendance = () => { if (!userId) return; // 유저 전체 출석 데이터 불러오기 - const res = await axios.get(`/api/attendance/user`, { + const res = await api.get(`/attendance/user`, { params: { userId }, + withCredentials: true, // 세션 기반 인증 요청처리 }); const rawData = res.data.data; const weekly = processWeeklyAttendance(rawData); @@ -107,8 +108,9 @@ const Attendance = () => { if (!userId) return; const today = new Date().toISOString().split("T")[0]; // YYYY-MM-DD - const res = await axios.get(`/api/attendance/user/date`, { + const res = await api.get(`/attendance/user/date`, { params: { userId, date: today }, + withCredentials: true, // 세션 기반 인증 요청처리 }); const slots = res.data.data?.[0]?.slots || []; @@ -149,10 +151,18 @@ const Attendance = () => { if (!userId) return; // 유저가 입력한 출석 코드 서버에 전달(서버에서 출석코드 체크) - const res = await axios.post("/api/attendance/mark", { - userId, - code: attendanceCode[0], - }); + + const res = await axios.post( + "/api/attendance/mark", + + { + userId, + code: attendanceCode[0], + }, + { + withCredentials: true, // 세션 기반 인증 요청처리 + } + ); if (res.data.success) { alert("출석이 성공적으로 처리되었습니다!"); diff --git a/frontend/src/Attendance.module.css b/frontend/src/pages/generation/Attendance.module.css similarity index 100% rename from frontend/src/Attendance.module.css rename to frontend/src/pages/generation/Attendance.module.css diff --git a/frontend/src/Deposit.jsx b/frontend/src/pages/generation/Deposit.jsx similarity index 90% rename from frontend/src/Deposit.jsx rename to frontend/src/pages/generation/Deposit.jsx index ce2198c..8b9c17b 100644 --- a/frontend/src/Deposit.jsx +++ b/frontend/src/pages/generation/Deposit.jsx @@ -1,18 +1,20 @@ -import Header from "./components/Header"; +import Header from "../../components/Header"; import styles from "./Deposit.module.css"; import axios from "axios"; import { useEffect, useState } from "react"; +import api from "../../api/api"; const Deposit = () => { const [deposit, setDeposit] = useState(null); + useEffect(() => { const user = JSON.parse(localStorage.getItem("user")); const userId = user?.id; if (!userId) return; - axios - .get(`/api/deposit/${userId}`) + api + .get(`/deposit/${userId}`) .then((res) => setDeposit(res.data)) .catch((err) => { alert("보증금 정보를 불러오지 못했습니다."); diff --git a/frontend/src/Deposit.module.css b/frontend/src/pages/generation/Deposit.module.css similarity index 100% rename from frontend/src/Deposit.module.css rename to frontend/src/pages/generation/Deposit.module.css diff --git a/frontend/src/Home.jsx b/frontend/src/pages/generation/Home.jsx similarity index 96% rename from frontend/src/Home.jsx rename to frontend/src/pages/generation/Home.jsx index f1fe4fa..df86a12 100644 --- a/frontend/src/Home.jsx +++ b/frontend/src/pages/generation/Home.jsx @@ -1,4 +1,3 @@ -import React from "react"; import { useNavigate } from "react-router-dom"; import styles from "./Home.module.css"; diff --git a/frontend/src/Home.module.css b/frontend/src/pages/generation/Home.module.css similarity index 85% rename from frontend/src/Home.module.css rename to frontend/src/pages/generation/Home.module.css index 9cdf1de..436f3ed 100644 --- a/frontend/src/Home.module.css +++ b/frontend/src/pages/generation/Home.module.css @@ -5,10 +5,10 @@ align-items: center; justify-content: flex-start; overflow: hidden; - font-family: 'Akira Expanded'; + font-family: "Akira Expanded"; } .pirocheck { - margin-top: 43px; + margin-top: 84px; font-size: 1.25rem; margin-bottom: 103px; } @@ -24,10 +24,10 @@ border-radius: 8px; background-color: var(--border-gray); color: var(--text-white); - font-family: 'Akira Expanded'; + font-family: "Akira Expanded"; font-weight: 400; font-size: 16px; - border: 1px var(--background-black) solid; + border: none; padding: 15px; font-weight: bold; margin-bottom: 26px; @@ -43,6 +43,6 @@ .home img { position: relative; opacity: 0.8; - top: -92px; + bottom: -2%; z-index: 0; }