diff --git a/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/controller/AdminAttendanceController.java b/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/controller/AdminAttendanceController.java index 5b75a14..591b212 100644 --- a/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/controller/AdminAttendanceController.java +++ b/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/controller/AdminAttendanceController.java @@ -23,7 +23,7 @@ @RestController @RequiredArgsConstructor -@RequestMapping("/api/admin/attendance") +@RequestMapping("/api") @Tag(name = "관리자 출석관리", description = "관리자용 출석 관리 API") public class AdminAttendanceController { @@ -31,127 +31,172 @@ public class AdminAttendanceController { // 출석체크 시작 @Operation(summary = "출석 체크 시작", description = "새로운 출석 코드를 생성하고 출석 체크를 시작합니다.") - @ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse( - responseCode = "200", - description = "출석 코드 생성 성공", - content = @Content(schema = @Schema(implementation = AttendanceCodeResponse.class)) - ), + @ApiResponses(value = { + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "출석 코드 생성 성공"), @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "잘못된 요청") }) - @PostMapping("/start") - public ApiResponse startAttendance() { + @PostMapping("/admin/attendance/start") + public AttendanceCodeResponse startAttendance() { try { AttendanceCode code = attendanceService.generateCodeAndCreateAttendances(); - return ApiResponse.success(AttendanceCodeResponse.from(code)); + return AttendanceCodeResponse.from(code); } catch (IllegalStateException e) { // 하루 최대 출석 체크 횟수를 초과한 경우 - return ApiResponse.error(e.getMessage()); + throw new IllegalStateException(e.getMessage()); } catch (Exception e) { - return ApiResponse.error("출석 코드 생성 중 오류가 발생했습니다: " + e.getMessage()); + throw new RuntimeException("출석 코드 생성 중 오류가 발생했습니다: " + e.getMessage()); } } // 현재 활성화된 출석코드 조회 @Operation(summary = "현재 활성화된 출석 코드 조회", description = "현재 활성화된 출석 코드 정보를 조회합니다.") - @ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse( - responseCode = "200", - description = "조회 성공", - content = @Content(schema = @Schema(implementation = AttendanceCodeResponse.class)) - ), + @ApiResponses(value = { + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "조회 성공"), @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "활성화된 출석 코드 없음") }) - @GetMapping("/active-code") - public ApiResponse getActiveCode() { + @GetMapping("/admin/attendance/active-code") + public AttendanceCodeResponse getActiveCode() { Optional codeOpt = attendanceService.getActiveAttendanceCode(); if (codeOpt.isEmpty()) { - return ApiResponse.error("현재 활성화된 출석코드가 없습니다"); + throw new RuntimeException("현재 활성화된 출석코드가 없습니다"); } - return ApiResponse.success(AttendanceCodeResponse.from(codeOpt.get())); + return AttendanceCodeResponse.from(codeOpt.get()); } // 출석체크 종료 (코드 직접 전달) @Operation(summary = "특정 출석 코드 만료", description = "특정 출석 코드를 만료 처리합니다.") - @ApiResponses({ + @ApiResponses(value = { @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 = "출석 코드를 찾을 수 없음") }) - @PutMapping("/expire") - public ApiResponse expireAttendance( - @Parameter(description = "만료할 출석 코드", required = true) + @PutMapping("/admin/attendance/expire") + public String expireAttendance( + @Parameter(description = "만료할 출석 코드", example = "1234") @RequestParam String code) { - String result = attendanceService.expireAttendanceCode(code); - - if (result.equals("출석 코드가 성공적으로 만료되었습니다")) { - return ApiResponse.success(result, null); - } else { - return ApiResponse.error(result); - } + return attendanceService.expireAttendanceCode(code); } // 출석체크 종료 (가장 최근 활성화된 코드 자동 만료) @Operation(summary = "최근 활성화된 출석 코드 만료", description = "가장 최근 활성화된 출석 코드를 자동으로 만료 처리합니다.") - @ApiResponses({ + @ApiResponses(value = { @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "만료 처리 성공"), @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "활성화된 출석 코드가 없음") }) - @PutMapping("/expire-latest") - public ApiResponse expireLatestAttendance() { - String result = attendanceService.expireLatestAttendanceCode(); - - if (result.equals("출석 코드가 성공적으로 만료되었습니다")) { - return ApiResponse.success(result, null); - } else { - return ApiResponse.error(result); - } + @PutMapping("/admin/attendance/expire-latest") + public String expireLatestAttendance() { + return attendanceService.expireLatestAttendanceCode(); } // 출석 상태 변경 (관리자 전용) @Operation(summary = "출석 상태 변경", description = "관리자가 특정 사용자의 출석 상태를 변경합니다.") - @ApiResponses({ + @ApiResponses(value = { @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 = "출석 기록을 찾을 수 없음") }) - @PutMapping("/status") - public ApiResponse updateAttendanceStatus( - @io.swagger.v3.oas.annotations.parameters.RequestBody( - description = "출석 상태 변경 요청", - required = true, - content = @Content(schema = @Schema(implementation = UpdateAttendanceStatusReq.class)) - ) + @PutMapping("/admin/users/{userId}/attendance/{attendanceId}/status") + public boolean updateAttendanceStatus( + @Parameter(description = "사용자 ID", example = "1") + @PathVariable Long userId, + @Parameter(description = "출석 ID", example = "1") + @PathVariable Long attendanceId, @RequestBody UpdateAttendanceStatusReq req) { - boolean result = attendanceService.updateAttendanceStatus( - req.getAttendanceId(), - req.isStatus() - ); + // userId 파라미터 검증은 여기서 할 수 있음 (필요 시) + return attendanceService.updateAttendanceStatus(attendanceId, req.isStatus()); + } + + // 출석 기록 삭제 (관리자 전용) + @Operation(summary = "출석 기록 삭제", description = "관리자가 특정 사용자의 출석 기록을 삭제합니다.") + @ApiResponses(value = { + @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 = "출석 기록을 찾을 수 없음") + }) + @DeleteMapping("/admin/users/{userId}/attendance/{attendanceId}") + public boolean deleteAttendance( + @Parameter(description = "사용자 ID", example = "1") + @PathVariable Long userId, + @Parameter(description = "출석 ID", example = "1") + @PathVariable Long attendanceId) { - if (result) { - return ApiResponse.success("출석 상태가 성공적으로 변경되었습니다", null); - } else { - return ApiResponse.error("출석 상태 변경에 실패했습니다. 출석 기록을 찾을 수 없습니다."); - } + // userId 파라미터 검증은 여기서 할 수 있음 (필요 시) + return attendanceService.deleteAttendance(attendanceId); } // 특정 날짜와 차수에 대한 모든 학생의 출석 현황 조회 @Operation(summary = "특정 날짜와 차수의 출석 현황 조회", description = "특정 날짜와 차수에 대한 모든 학생의 출석 현황을 조회합니다.") - @ApiResponses({ + @ApiResponses(value = { @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "조회 성공"), @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "잘못된 요청") }) - @GetMapping("/list") - public ApiResponse> getAllAttendanceByDateAndOrder( - @Parameter(description = "조회할 날짜 (YYYY-MM-DD)", required = true) + @GetMapping("/admin/attendance/list") + public List getAllAttendanceByDateAndOrder( + @Parameter(description = "조회할 날짜 (YYYY-MM-DD)", example = "2023-08-01") + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date, + @Parameter(description = "조회할 차수", example = "1") + @RequestParam int order) { + return attendanceService.findAllByDateAndOrder(date, order); + } + + // 특정 사용자의 특정 날짜와 차수 출석 기록 조회 + @Operation(summary = "특정 사용자의 특정 날짜와 차수 출석 조회", description = "특정 사용자의 특정 날짜와 차수 출석 기록을 조회합니다.") + @ApiResponses(value = { + @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("/admin/users/{userId}/attendance") + public UserAttendanceStatusRes getUserAttendanceByDateAndOrder( + @Parameter(description = "사용자 ID", example = "1") + @PathVariable Long userId, + @Parameter(description = "조회할 날짜 (YYYY-MM-DD)", example = "2023-08-01") @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date, - @Parameter(description = "조회할 차수", required = true) + @Parameter(description = "조회할 차수", example = "1") @RequestParam int order) { + return attendanceService.findByUserIdAndDateAndOrder(userId, date, order); + } + + // 특정 출석 ID로 출석 기록 조회 + @Operation(summary = "특정 출석 기록 조회", description = "특정 학생의 특정 출석 기록을 ID로 조회합니다.") + @ApiResponses(value = { + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "조회 성공"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "출석 기록을 찾을 수 없음") + }) + @GetMapping("/admin/users/{userId}/attendance/{attendanceId}") + public UserAttendanceStatusRes getAttendanceById( + @Parameter(description = "사용자 ID", example = "1") + @PathVariable Long userId, + @Parameter(description = "출석 ID", example = "1") + @PathVariable Long attendanceId) { - List attendances = attendanceService.findAllByDateAndOrder(date, order); - return ApiResponse.success(attendances); + UserAttendanceStatusRes attendance = attendanceService.findById(attendanceId); + + if (attendance == null) { + throw new RuntimeException("출석 기록을 찾을 수 없습니다"); + } + + // 요청된 userId와 조회된 출석 기록의 userId가 일치하는지 확인 + if (!attendance.getUserId().equals(userId)) { + throw new RuntimeException("요청된 사용자 ID와 출석 기록의 사용자 ID가 일치하지 않습니다"); + } + + return attendance; + } + + // 학생용 출석 현황 조회 + @Operation(summary = "학생별 출석 현황 조회", description = "특정 학생의 출석 현황을 조회합니다.") + @ApiResponses(value = { + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "조회 성공"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "잘못된 요청") + }) + @GetMapping("/attendance/{userId}") + public List getUserAttendances( + @Parameter(description = "사용자 ID", example = "1") + @PathVariable Long userId) { + return attendanceService.findAllByUserId(userId); } } \ No newline at end of file diff --git a/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/dto/request/UpdateAttendanceStatusReq.java b/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/dto/request/UpdateAttendanceStatusReq.java index 2c6973c..0bea7c5 100644 --- a/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/dto/request/UpdateAttendanceStatusReq.java +++ b/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/dto/request/UpdateAttendanceStatusReq.java @@ -12,9 +12,6 @@ @AllArgsConstructor @Schema(description = "출석 상태 수정 요청") public class UpdateAttendanceStatusReq { - @Schema(description = "출석 기록 ID", example = "1") - private Long attendanceId; - @Schema(description = "변경할 출석 상태", example = "true") private boolean status; } \ No newline at end of file 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 4a1adb8..ae0dec4 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 @@ -247,4 +247,82 @@ public List findAllByDateAndOrder(LocalDate date, int o .sorted(Comparator.comparing(UserAttendanceStatusRes::getUsername)) .toList(); } + + // 특정 학생의 모든 출석 현황 조회 + public List findAllByUserId(Long userId) { + // 해당 사용자의 모든 출석 기록 조회 + List attendances = attendanceRepository.findByUserId(userId); + + // DTO 변환 + return attendances.stream() + .map(attendance -> { + User user = attendance.getUser(); + return UserAttendanceStatusRes.builder() + .userId(user.getId()) + .username(user.getName()) + .date(attendance.getDate()) + .order(attendance.getOrder()) + .status(attendance.isStatus()) + .attendanceId(attendance.getId()) + .build(); + }) + .sorted(Comparator.comparing(UserAttendanceStatusRes::getDate).reversed() + .thenComparing(UserAttendanceStatusRes::getOrder)) + .toList(); + } + + // 특정 사용자의 특정 출석 기록 삭제 + @Transactional + public boolean deleteAttendance(Long attendanceId) { + Optional attendanceOpt = attendanceRepository.findById(attendanceId); + + if (attendanceOpt.isEmpty()) { + return false; + } + + attendanceRepository.delete(attendanceOpt.get()); + return true; + } + + // 특정 사용자의 특정 날짜와 차수 출석 기록 조회 + public UserAttendanceStatusRes findByUserIdAndDateAndOrder(Long userId, LocalDate date, int order) { + Optional attendanceOpt = attendanceRepository.findByUserIdAndDateAndOrder(userId, date, order); + + if (attendanceOpt.isEmpty()) { + return null; + } + + Attendance attendance = attendanceOpt.get(); + User user = attendance.getUser(); + + return UserAttendanceStatusRes.builder() + .userId(user.getId()) + .username(user.getName()) + .date(attendance.getDate()) + .order(attendance.getOrder()) + .status(attendance.isStatus()) + .attendanceId(attendance.getId()) + .build(); + } + + // 특정 출석 ID로 출석 기록 조회 + public UserAttendanceStatusRes findById(Long attendanceId) { + Optional attendanceOpt = attendanceRepository.findById(attendanceId); + + if (attendanceOpt.isEmpty()) { + return null; + } + + Attendance attendance = attendanceOpt.get(); + User user = attendance.getUser(); + + return UserAttendanceStatusRes.builder() + .userId(user.getId()) + .username(user.getName()) + .date(attendance.getDate()) + .order(attendance.getOrder()) + .status(attendance.isStatus()) + .attendanceId(attendance.getId()) + .build(); + } } diff --git a/backend/pirocheck/src/main/java/backend/pirocheck/Deposit/controller/DepositController.java b/backend/pirocheck/src/main/java/backend/pirocheck/Deposit/controller/DepositController.java index f4a5f63..e4ea069 100644 --- a/backend/pirocheck/src/main/java/backend/pirocheck/Deposit/controller/DepositController.java +++ b/backend/pirocheck/src/main/java/backend/pirocheck/Deposit/controller/DepositController.java @@ -3,12 +3,15 @@ import backend.pirocheck.Deposit.dto.DepositResDto; import backend.pirocheck.Deposit.service.DepositService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +@Tag(name = "보증금 관리", description = "수강생 보증금/차감/방어권 관련 API") @RestController @RequestMapping("/api/deposit") @RequiredArgsConstructor @@ -16,6 +19,7 @@ public class DepositController { private final DepositService depositService; + @Operation(summary = "보증금 조회", description = "해당 유저의 현재 보증금, 차감 내역, 방어권 금액을 반환합니다.") @GetMapping("/{userId}") public DepositResDto getDeposit(@PathVariable Long userId) { return depositService.getDeposit(userId); diff --git a/backend/pirocheck/src/main/java/backend/pirocheck/Deposit/dto/DepositResDto.java b/backend/pirocheck/src/main/java/backend/pirocheck/Deposit/dto/DepositResDto.java index d05dcdc..a784fcb 100644 --- a/backend/pirocheck/src/main/java/backend/pirocheck/Deposit/dto/DepositResDto.java +++ b/backend/pirocheck/src/main/java/backend/pirocheck/Deposit/dto/DepositResDto.java @@ -1,6 +1,7 @@ package backend.pirocheck.Deposit.dto; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -11,9 +12,16 @@ @NoArgsConstructor @AllArgsConstructor public class DepositResDto { + @Schema(description = "현재 보증금 잔액", example = "110000") private int amount; + + @Schema(description = "과제 차감 총액", example = "10000") private int descentAssignment; + + @Schema(description = "출석 차감 총액", example = "10000") private int descentAttendance; + + @Schema(description = "방어권 보상 총액", example = "10000") private int ascentDefence; } 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 a0431e6..e7d7086 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 @@ -32,14 +32,14 @@ public void updateAmounts(int descentAssignment, int descentAttendance, int asce this.ascentDefence = ascentDefence; int calculateAmount = 120000 - descentAssignment - descentAttendance + ascentDefence; - this.amount = Math.min(calculateAmount, 120000); // 12만원 넘어가지 않도록 + this.amount = Math.max(0, Math.min(calculateAmount, 120000)); // 0 이상 12만원 이하 } // 방어권 업데이트 public void updateDefence(int newAscentDefence) { this.ascentDefence = newAscentDefence; int calculateAmount = 120000 - this.descentAssignment - this.descentAttendance + newAscentDefence; - this.amount = Math.min(calculateAmount, 120000); + this.amount = Math.max(0, Math.min(calculateAmount, 120000)); // 0 이상 12만원 이하 } 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 index 043b571..eb12560 100644 --- a/backend/pirocheck/src/main/java/backend/pirocheck/ManageStudents/Controller/ManageStudentsController.java +++ b/backend/pirocheck/src/main/java/backend/pirocheck/ManageStudents/Controller/ManageStudentsController.java @@ -4,11 +4,14 @@ import backend.pirocheck.ManageStudents.dto.response.ManageStudentDetailResDto; import backend.pirocheck.ManageStudents.dto.response.ManageStudentsListResDto; import backend.pirocheck.ManageStudents.service.ManageStudentsService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; import java.util.List; +@Tag(name = "수강생 관리", description = "관리자가 수강생을 조회하고 방어권을 수정하는 API") @RestController @RequestMapping("/api/admin/managestudent") @RequiredArgsConstructor @@ -17,18 +20,21 @@ public class ManageStudentsController { private final ManageStudentsService manageStudentsService; // 수강생 리스트 조회 + @Operation(summary = "수강생 리스트 조회", description = "이름에 따라 수강생 리스트를 검색합니다.") @GetMapping("") public List getStudents(@RequestParam(required = false) String name) { return manageStudentsService.searchMembers(name); } // 수강생 상세 조회 + @Operation(summary = "수강생 상세 조회", description = "studentId로 해당 수강생의 보증금, 방어권, 과제 정보를 조회합니다.") @GetMapping("/{studentId}") public ManageStudentDetailResDto getStudentDetail(@PathVariable Long studentId) { return manageStudentsService.getMemberDetail(studentId); } // 방어권 업데이트 + @Operation(summary = "방어권 수정", description = "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 index 78e39f7..f719e0e 100644 --- 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 @@ -1,11 +1,15 @@ package backend.pirocheck.ManageStudents.dto.request; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.Setter; @Getter @Setter public class DefenceUpdateReqDto { + + @Schema(description = "방어권 금액", example = "20000") 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 index e5206fc..3df2558 100644 --- 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 @@ -1,5 +1,6 @@ package backend.pirocheck.ManageStudents.dto.response; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; import lombok.Getter; @@ -9,8 +10,15 @@ @Builder public class ManageStudentDetailResDto { + @Schema(description = "수강생 이름", example = "김피로") private String name; + + @Schema(description = "현재 보증금 잔액", example = "11000") private int deposit; + + @Schema(description = "방어권 금액", example = "10000") private int defence; // 방어권 + + @Schema(description = "과제 제목 리스트", example = "[\"제로초 인강\", \"토스 클론\"]") private List assignmentTitles; // 과제 제목 리스트 } 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 index ed60489..209c7fd 100644 --- a/backend/pirocheck/src/main/java/backend/pirocheck/ManageStudents/service/ManageStudentsService.java +++ b/backend/pirocheck/src/main/java/backend/pirocheck/ManageStudents/service/ManageStudentsService.java @@ -9,6 +9,7 @@ import backend.pirocheck.User.entity.Role; import backend.pirocheck.User.entity.User; import backend.pirocheck.User.repository.UserRepository; +import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -41,6 +42,7 @@ public List searchMembers(String name) { } // 수강생 상세 조회 + @Transactional public ManageStudentDetailResDto getMemberDetail(Long studentId) { // User 조회 User user = userRepository.findById(studentId) diff --git a/backend/pirocheck/src/main/java/backend/pirocheck/User/controller/UserController.java b/backend/pirocheck/src/main/java/backend/pirocheck/User/controller/UserController.java index 9f8239d..758c6fb 100644 --- a/backend/pirocheck/src/main/java/backend/pirocheck/User/controller/UserController.java +++ b/backend/pirocheck/src/main/java/backend/pirocheck/User/controller/UserController.java @@ -5,8 +5,11 @@ import backend.pirocheck.User.entity.User; import backend.pirocheck.User.repository.UserRepository; import backend.pirocheck.User.service.UserService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; @@ -14,6 +17,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +@Tag(name = "유저 인증", description = "로그인 / 로그아웃 API") @RestController @RequestMapping("/api") @RequiredArgsConstructor @@ -22,8 +26,9 @@ public class UserController { private final UserService userService; // 로그인 + @Operation(summary = "로그인", description = "사용자 이름과 비밀번호로 로그인하고 세션을 생성합니다.") @PostMapping("/login") - public ResponseEntity login(@RequestBody LoginRequest request, HttpSession session) { + public ResponseEntity login(@RequestBody @Valid LoginRequest request, HttpSession session) { User user = userService.login(request.getName(), request.getPassword()); //세션에 로그인 정보 저장 @@ -34,6 +39,7 @@ public ResponseEntity login(@RequestBody LoginRequest request, Ht } // 로그아웃 + @Operation(summary = "로그아웃", description = "세션을 종료하여 로그아웃합니다.") @PostMapping("/logout") public ResponseEntity logout(HttpSession session) { session.invalidate(); // 세션 종료 (메모리에서 삭제) diff --git a/backend/pirocheck/src/main/java/backend/pirocheck/User/dto/request/LoginRequest.java b/backend/pirocheck/src/main/java/backend/pirocheck/User/dto/request/LoginRequest.java index a530d8a..aa1216a 100644 --- a/backend/pirocheck/src/main/java/backend/pirocheck/User/dto/request/LoginRequest.java +++ b/backend/pirocheck/src/main/java/backend/pirocheck/User/dto/request/LoginRequest.java @@ -1,5 +1,7 @@ package backend.pirocheck.User.dto.request; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; import lombok.Getter; import lombok.Setter; @@ -7,6 +9,11 @@ @Setter public class LoginRequest { + @Schema(description = "사용자 이름", example = "김피로") + @NotBlank(message = "이름을 입력해주세요.") private String name; + + @Schema(description = "비밀번호", example = "qwer1234!") + @NotBlank(message = "비밀번호를 입력해주세요.") private String password; } diff --git a/backend/pirocheck/src/main/java/backend/pirocheck/User/dto/response/LoginResponse.java b/backend/pirocheck/src/main/java/backend/pirocheck/User/dto/response/LoginResponse.java index ec9d65a..0f6e292 100644 --- a/backend/pirocheck/src/main/java/backend/pirocheck/User/dto/response/LoginResponse.java +++ b/backend/pirocheck/src/main/java/backend/pirocheck/User/dto/response/LoginResponse.java @@ -2,13 +2,19 @@ import backend.pirocheck.User.entity.User; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; @Getter public class LoginResponse { + @Schema(description = "유저 고유 ID", example = "1") private Long id; + + @Schema(description = "유저 이름", example = "김피로") private String name; + + @Schema(description = "유저 권한", example = "MEMBER") private String role; public LoginResponse(User user) { diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 5d442d1..10300fe 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -6,11 +6,13 @@ import Assignment from "./pages/generation/Assignment"; import Deposit from "./pages/generation/Deposit"; import Intro from "./Intro"; import Admin from "./pages/admin/Admin"; +import DetailManageStudent from "./pages/admin/DetailManageStudent.jsx"; import ManageStudent from "./pages/admin/ManageStudent.jsx"; import ManageTask from "./pages/admin/ManageTask.jsx"; import AttendanceCode from "./pages/admin/AttendanceCode"; import Attendance from "./pages/generation/Attendance"; import AdminStudentAttendance from "./pages/admin/AdminStudentAttendance"; +import AdminStudentAssignment from "./pages/admin/AdminStudentAssignment.jsx"; import RequireAuth from "./components/RequireAuth"; import RequireAdmin from "./components/RequireAdmin"; @@ -68,6 +70,14 @@ function App() { } /> + + + + } + /> } /> + + + + } + /> ); diff --git a/frontend/src/Intro.module.css b/frontend/src/Intro.module.css index 491aef3..ba814f5 100644 --- a/frontend/src/Intro.module.css +++ b/frontend/src/Intro.module.css @@ -13,7 +13,7 @@ content: ""; position: absolute; inset: 0; - background-image: url("./assets/img/logo.svg"); + background-image: url("/assets/img/logo.svg"); background-repeat: no-repeat; background-size: contain; background-position: center; @@ -27,7 +27,7 @@ .intro_container { background-color: var(--background-black); color: var(--main-green); - font-family: 'Akira Expanded'; + font-family: "Akira Expanded"; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); display: flex; justify-content: center; diff --git a/frontend/src/api/adminattendance.js b/frontend/src/api/adminattendance.js new file mode 100644 index 0000000..4506980 --- /dev/null +++ b/frontend/src/api/adminattendance.js @@ -0,0 +1,26 @@ +import api from "./api"; + +// api/attendanceApi.js + +export const getStudentBasicInfo = async (studentId) => { + try { + const res = await api.get(`/admin/managestudent/${studentId}`); + return res.data; + } catch (error) { + console.error("학생 기본 정보 불러오기 실패:", error); + throw error; + } +}; + +export const getStudentAttendance = async (studentId) => { + try { + const res = await api.get("/admin/attendance/user", { + params: { userId: studentId }, + withCredentials: true, + }); + return res.data; + } catch (error) { + console.error("학생 출석 정보 불러오기 실패:", error); + throw error; + } +}; diff --git a/frontend/src/api/api.js b/frontend/src/api/api.js index 60d0af3..1950026 100644 --- a/frontend/src/api/api.js +++ b/frontend/src/api/api.js @@ -2,10 +2,8 @@ import axios from "axios"; const api = axios.create({ baseURL: "http://api.pirocheck.org:8080/api", - // 수정 필요한지 재검 필요함 // "http://api.pirocheck.org:8080/api" - withCredentials: true, }); diff --git a/frontend/src/api/assignment.js b/frontend/src/api/assignment.js index cf1a11a..0c31de0 100644 --- a/frontend/src/api/assignment.js +++ b/frontend/src/api/assignment.js @@ -1,6 +1,31 @@ import api from "./api"; - +/* export const fetchAssignmentsByUser = async (userId) => { const res = await api.get(`/assignment/grouped/${userId}`); return res.data; }; +*/ +export const fetchAssignmentsByUser = async (userId) => { + try { + const res = await api.get(`/assignment/${userId}`); + return res.data; // 백엔드가 반환하는 JSON 그대로 + } catch (err) { + console.error("과제 데이터 불러오기 실패:", err); + throw err; + } +}; + + +export const submitAssignmentStatus = async (userId, assignmentId, status) => { + return api.post(`/admin/users/${userId}/assignments/${assignmentId}/submission`, { + assignmentId, + userId, + status, + }); +}; + +export const updateAssignmentStatus = async (userId, assignmentId, status) => { + return api.put(`/admin/users/${userId}/assignments/${assignmentId}/submission`, { + status, + }); +}; diff --git a/frontend/src/api/students.js b/frontend/src/api/students.js index 271ee4f..3239a66 100644 --- a/frontend/src/api/students.js +++ b/frontend/src/api/students.js @@ -4,5 +4,16 @@ export const getStudentsByName = async (name) => { const res = await api.get(`/admin/managestudent`, { params: { name }, }); + console.log("💬 getStudentsByName 응답:", res.data); return res.data; // [{ id: ..., name: ... }] }; + +export const getStudentDetail = async (studentId) => { + try { + const res = await api.get(`/admin/managestudent/${studentId}`); + return res.data; + } catch (error) { + console.error("학생 상세 정보 불러오기 실패:", error); + throw error; + } +}; diff --git a/frontend/src/components/Header.jsx b/frontend/src/components/Header.jsx index 1a38ba6..50af6bf 100644 --- a/frontend/src/components/Header.jsx +++ b/frontend/src/components/Header.jsx @@ -50,9 +50,7 @@ const Header = () => { height={30} /> - ) : ( -
- )} + ) : null} {showRightMagageStudent ? ( +
+ ))} + + + + ))} + + ))} + + + ); +}; + +export default AdminStudentAssignment; diff --git a/frontend/src/pages/admin/AdminStudentAssignment.module.css b/frontend/src/pages/admin/AdminStudentAssignment.module.css new file mode 100644 index 0000000..04e3e68 --- /dev/null +++ b/frontend/src/pages/admin/AdminStudentAssignment.module.css @@ -0,0 +1,112 @@ +.container { + display: flex; + flex-direction: column; + padding: 20px; + font-family: "Inter", sans-serif; + color: white; + background-color: #1e1e1e; +} + +/* 과제 개요 카드 (상단 형광 카드) */ +.info { + background-color: #045e07; + border-radius: 12px; + padding: 16px; + margin-bottom: 24px; +} + +/* 주차별 목록 */ +.weekList { + display: flex; + flex-direction: column; + gap: 24px; +} + +/* 주차 구간 */ +.weekBlock { + border-left: 4px solid #00c851; + background-color: #2d2d2d; + border-radius: 10px; + padding: 16px; +} + +.weekTitle { + font-size: 18px; + font-weight: bold; + color: #00ff99; + margin-bottom: 12px; +} + +/* 요일별 카드 */ +.dayCard { + background-color: #3a3a3a; + padding: 12px 16px; + border-radius: 10px; + margin-bottom: 16px; +} + +.dayLabel { + font-size: 16px; + font-weight: 600; + margin-bottom: 8px; +} + +/* 개별 과제 */ +.taskList { + display: flex; + flex-direction: column; + gap: 10px; + margin-bottom: 12px; +} + +.taskRow { + display: flex; + align-items: center; + gap: 10px; + background-color: #505050; + border-radius: 6px; + padding: 8px 12px; +} + +.taskLabel { + flex: 1; + font-size: 14px; + color: white; +} + +/* 드롭다운 */ +.taskRow select { + padding: 6px 8px; + border-radius: 6px; + background-color: #2a2a2a; + color: white; + border: 1px solid #777; +} + +/* save 버튼 */ +.saveButton { + padding: 4px 10px; + border-radius: 6px; + background-color: #00c851; + color: white; + font-weight: bold; + border: none; + cursor: pointer; +} + +.saveButton:disabled { + background-color: gray; + cursor: not-allowed; +} + +/* submit 버튼 */ +.submitBtn { + margin-top: 8px; + padding: 6px 12px; + border-radius: 8px; + background-color: #1fa067; + color: white; + font-weight: bold; + border: none; + cursor: pointer; +} diff --git a/frontend/src/pages/admin/AdminStudentAttendance.jsx b/frontend/src/pages/admin/AdminStudentAttendance.jsx index 827248b..6eed5d1 100644 --- a/frontend/src/pages/admin/AdminStudentAttendance.jsx +++ b/frontend/src/pages/admin/AdminStudentAttendance.jsx @@ -5,6 +5,7 @@ import DailyAttendanceCard from "../../components/AdminDailyAttendanceCard"; import api from "../../api/api"; import styles from "./AdminStudentAttendance.module.css"; import AdminWeeklyAttendanceList from "../../components/AdminWeeklyAttendanceList"; +import { getStudentBasicInfo, getStudentAttendance } from "../../api/adminattendance"; const AdminStudentAttendance = () => { const { studentId } = useParams(); @@ -13,22 +14,20 @@ const AdminStudentAttendance = () => { const [selectedDate, setSelectedDate] = useState(null); useEffect(() => { - // 1. 학생 정보 가져오기 - api.get(`/admin/users/${studentId}`).then((res) => { - setStudentInfo(res.data.data); - }); + const fetchData = async () => { + try { + const studentRes = await getStudentBasicInfo(studentId); + setStudentInfo(studentRes.data); - // 2. 주차별 출석 데이터 가공 - api - .get("/admin/attendance/user", { - params: { userId: studentId }, - withCredentials: true, - }) - .then((res) => { - const raw = res.data.data; - const processed = processWeeklyAttendance(raw); + const attendanceRes = await getStudentAttendance(studentId); + const processed = processWeeklyAttendance(attendanceRes.data); setAttendanceData(processed); - }); + } catch (err) { + console.error("데이터 불러오기 실패:", err); + } + }; + + fetchData(); }, [studentId]); /* diff --git a/frontend/src/pages/admin/AttendanceCode.jsx b/frontend/src/pages/admin/AttendanceCode.jsx index 8806d0e..879f86f 100644 --- a/frontend/src/pages/admin/AttendanceCode.jsx +++ b/frontend/src/pages/admin/AttendanceCode.jsx @@ -1,11 +1,31 @@ -import { useState } from "react"; import api from "../../api/api"; import Header from "../../components/Header"; import style from "./AttendanceCode.module.css"; +import { useState, useEffect } from "react"; const AttendanceCode = () => { const [code, setCode] = useState(""); + useEffect(() => { + const expireIfNeeded = async () => { + try { + const res = await api.get("admin/attendance/active-code"); + const activeCode = res.data.data.code; + + await api.put("admin/attendance/expire", null, { + params: { code: activeCode }, + }); + + console.log("기존 출석코드 자동 만료됨"); + } catch (error) { + if (error.response?.status !== 404) { + return; + } + } + }; + + expireIfNeeded(); + }, []); // 출석코드 생성 const generateCode = async () => { try { diff --git a/frontend/src/pages/admin/DetailManageStudent.jsx b/frontend/src/pages/admin/DetailManageStudent.jsx new file mode 100644 index 0000000..e6b20bb --- /dev/null +++ b/frontend/src/pages/admin/DetailManageStudent.jsx @@ -0,0 +1,78 @@ +import { useParams } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; +import { useEffect, useState } from "react"; +import Header from "../../components/Header"; +import style from "./DetailManageStudent.module.css"; +import { getStudentDetail } from "../../api/students"; + +const weekData = [ + { week: "1주차", title: "Git/HTML/CSS" }, + { week: "2주차", title: "JavaScript/웹 개론" }, + { week: "3주차", title: "Django CRUD/DB 개론" }, + { week: "4주차", title: "Django ORM/Ajax" }, + { week: "5주차", title: "배포/아이디어 기획" }, +]; + +const DetailManageStudent = () => { + const { studentId } = useParams(); + const numericId = Number(studentId); + const [student, setStudent] = useState(null); + const navigate = useNavigate(); + + useEffect(() => { + const fetchStudent = async () => { + try { + const data = await getStudentDetail(numericId); + setStudent(data); + } catch (err) { + console.error("학생 상세 정보 불러오기 실패:", err); + } + }; + + fetchStudent(); + }, [numericId]); + + if (!student) return
loading...
; + + console.log("studentId from URL:", studentId); + console.log("numericId:", numericId); + + return ( +
+
+
+
+

{student.name}

+
+ 잔여 보증금: {student.deposit}원 +
+
+ 보증금 방어권: {student.defence} +
+
+ {student && ( + + )} + {student && ( +
+ {weekData.map((week, index) => ( + + ))} +
+ )} +
+
+ ); +}; +export default DetailManageStudent; diff --git a/frontend/src/pages/admin/DetailManageStudent.module.css b/frontend/src/pages/admin/DetailManageStudent.module.css new file mode 100644 index 0000000..d69fe20 --- /dev/null +++ b/frontend/src/pages/admin/DetailManageStudent.module.css @@ -0,0 +1,68 @@ +.managestudent_wrapper { + display: flex; + justify-content: center; + flex-direction: column; + align-items: center; +} +.under_header { + display: flex; + flex-direction: column; + align-items: center; +} +.student_card { + /* height: 150px; */ + display: flex; + background: #49ff24; + flex-direction: column; + border-radius: 9px; + padding: 15px 20px; + width: 300px; + gap: 15px; +} +.student_name { + font-size: 25px; + font-weight: 700; +} +.deposit_container, +.defence_container { + display: flex; + justify-content: space-between; + font-size: 20px; + font-weight: 500; +} +.attendance_btn { + border: 1px solid #49ff24; + border-radius: 9px; + width: 300px; + height: 40px; + display: flex; + align-items: center; + justify-content: space-between; + color: #49ff24; + padding: 24px 15px; + margin-block: 30px; + font-size: 18px; +} +.assignment_list { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.assignment_button { + width: 309px; + height: 47px; + border-radius: 14px; + background-color: var(--border-gray); + color: var(--text-white); + font-weight: 400; + font-size: 16px; + font-family: "Noto Sans KR", sans-serif; + border: 1px var(--background-black) solid; + padding: 10px; + margin-top: 11px; +} +.assignment_button:focus { + border: 1px var(--main-green) solid; +} diff --git a/frontend/src/pages/admin/ManageStudent.jsx b/frontend/src/pages/admin/ManageStudent.jsx index 252175f..841c6bd 100644 --- a/frontend/src/pages/admin/ManageStudent.jsx +++ b/frontend/src/pages/admin/ManageStudent.jsx @@ -1,4 +1,5 @@ import { useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; import { getStudentsByName } from "../../api/students"; import Header from "../../components/Header"; import InputBlock from "../../components/InputBlock"; @@ -8,6 +9,7 @@ const ManageStudent = () => { const [studentName, setStudentName] = useState([""]); const [page, setPage] = useState(1); const [students, setStudents] = useState([]); // 서버 데이터 저장 + const navigate = useNavigate(); const studentsPerPage = 6; @@ -53,11 +55,18 @@ const ManageStudent = () => { onChange={handleChange} />
- {paginatedStudents.map((student, index) => ( - - ))} + {paginatedStudents.map((student, index) => { + console.log("student to show:", student); + return ( + + ); + })}
{students.length > studentsPerPage && ( diff --git a/frontend/src/pages/admin/ManageStudent.module.css b/frontend/src/pages/admin/ManageStudent.module.css index 931680a..3a900c7 100644 --- a/frontend/src/pages/admin/ManageStudent.module.css +++ b/frontend/src/pages/admin/ManageStudent.module.css @@ -25,6 +25,9 @@ text-align: left; font-size: 16px; width: 100%; + display: flex; + align-items: center; + justify-content: space-between; } .student_button:hover { border: 1px solid #39ff14; diff --git a/frontend/src/pages/generation/Attendance.jsx b/frontend/src/pages/generation/Attendance.jsx index dce08b1..a81d189 100644 --- a/frontend/src/pages/generation/Attendance.jsx +++ b/frontend/src/pages/generation/Attendance.jsx @@ -152,7 +152,7 @@ const Attendance = () => { // 유저가 입력한 출석 코드 서버에 전달(서버에서 출석코드 체크) - const res = await axios.post( + const res = await api.post( "/api/attendance/mark", {