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 new file mode 100644 index 0000000..042c94d --- /dev/null +++ b/backend/pirocheck/src/main/java/backend/pirocheck/Assignment/controller/AssignmentController.java @@ -0,0 +1,133 @@ +package backend.pirocheck.Assignment.controller; + +import backend.pirocheck.Assignment.dto.request.AssignmentCreateReq; +import backend.pirocheck.Assignment.dto.request.AssignmentItemCreateReq; +import backend.pirocheck.Assignment.dto.request.AssignmentItemUpdateReq; +import backend.pirocheck.Assignment.dto.request.AssignmentUpdateReq; +import backend.pirocheck.Assignment.dto.response.AssignmentWeekRes; +import backend.pirocheck.Assignment.entity.AssignmentStatus; +import backend.pirocheck.Assignment.service.AssignmentService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api") +@RequiredArgsConstructor +@Tag(name = "과제관리", description = "과제 관련 API") +public class AssignmentController { + + private final AssignmentService assignmentService; + + // 과제 결과 확인 API + // 과제 주차별, 요일별 그룹화 JSON + @Operation(summary = "학생별 과제 결과 확인", description = "관리자가 채점한 과제의 결과를 학생들이 확인합니다.") + @ApiResponses( + value = { + @ApiResponse(responseCode = "200", description = "사용자별 과제 조회에 성공하였습니다."), + @ApiResponse(responseCode = "400", description = "잘못된 요청입니다.") + } + ) + @GetMapping("/assignment/{userId}") + public List getGroupedAssignments( + @Parameter(description = "사용자 ID", example = "1") + @PathVariable Long userId + ) { + return assignmentService.search(userId); + } + + // 과제 생성 API + @Operation(summary = "과제 생성 API", description = "관리자가 과제를 생성합니다.") + @ApiResponses( + value = { + @ApiResponse(responseCode = "200", description = "과제 생성에 성공하였습니다."), + @ApiResponse(responseCode = "400", description = "잘못된 요청입니다.") + } + ) + @PostMapping("/admin/assignment/signup") + public String signupAssignment( + @Valid + @RequestBody AssignmentCreateReq assignmentCreateReq + ) { + return assignmentService.createAssignment(assignmentCreateReq); + } + + // 과제 삭제 API + @Operation(summary = "과제 삭제 API", description = "관리자가 과제를 삭제합니다.") + @ApiResponses( + value = { + @ApiResponse(responseCode = "200", description = "과제 삭제에 성공하였습니다."), + @ApiResponse(responseCode = "400", description = "잘못된 요청입니다.") + } + ) + @DeleteMapping("/admin/assignment/{assignmentId}") + public String deleteAssignment( + @Parameter(description = "과제 ID", example = "1") + @PathVariable Long assignmentId + ) { + return assignmentService.deleteAssignment(assignmentId); + } + + // 과제 수정 API + @Operation(summary = "과제 수정 API", description = "관리자가 과제의 잘못된 부분을 수정합니다.") + @ApiResponses( + value = { + @ApiResponse(responseCode = "200", description = "과제 수정에 성공하였습니다."), + @ApiResponse(responseCode = "400", description = "잘못된 요청입니다.") + } + ) + @PutMapping("/admin/assignment/{assignmentId}") + public String updateAssignment( + @Parameter(description = "과제 ID", example = "1") + @PathVariable("assignmentId") Long assignmentId, + @RequestBody AssignmentUpdateReq assignmentUpdateReq + ) { + return assignmentService.updateAssignment(assignmentId, assignmentUpdateReq); + } + + // 사용자별 과제 제출 결과 생성 API + @Operation(summary = "관리자 과제 채점 API", description = "관리자가 사용자들의 과제를 채점한 결과 저장합니다.") + @ApiResponses( + value = { + @ApiResponse(responseCode = "200", description = "사용자의 과제 채점 결과 저장에 성공하였습니다."), + @ApiResponse(responseCode = "400", description = "잘못된 요청입니다.") + } + ) + @PostMapping("/admin/users/{userId}/assignments/{assignmentId}/submission") + public AssignmentStatus submissionAssignment( + @Parameter(description = "사용자 ID", example = "1") + @PathVariable Long userId, + @Parameter(description = "과제 ID", example = "1") + @PathVariable Long assignmentId, + @RequestBody AssignmentItemCreateReq req + ) { + return assignmentService.createAssignmentItem(userId, assignmentId, req); + } + + // 사용자별 과제 제출 여부 수정 API + @Operation(summary = "관리자 과제 채점 내용 수정 API", description = "관리자가 사용자의 과제 결과를 수정하여 저장합니다.") + @ApiResponses( + value = { + @ApiResponse(responseCode = "200", description = "사용자 과제 채점 결과 수정에 성공하였습니다."), + @ApiResponse(responseCode = "400", description = "잘못된 요청입니다.") + } + ) + @PutMapping("/admin/users/{userId}/assignments/{assignmentId}/submission") + public AssignmentStatus updateSubmission( + @Parameter(description = "사용자 ID", example = "1") + @PathVariable Long userId, + @Parameter(description = "과제 ID", example = "1") + @PathVariable Long assignmentId, + @RequestBody AssignmentItemUpdateReq req + ) { + return assignmentService.updateAssignmentItem(userId, assignmentId, req); + } + +} diff --git a/backend/pirocheck/src/main/java/backend/pirocheck/Assignment/dto/request/AssignmentCreateReq.java b/backend/pirocheck/src/main/java/backend/pirocheck/Assignment/dto/request/AssignmentCreateReq.java new file mode 100644 index 0000000..65cbe2f --- /dev/null +++ b/backend/pirocheck/src/main/java/backend/pirocheck/Assignment/dto/request/AssignmentCreateReq.java @@ -0,0 +1,33 @@ +package backend.pirocheck.Assignment.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class AssignmentCreateReq { + + @Schema(description = "과제 주제", example = "Git/HTML/CSS") + @NotNull(message = "과제 주제는 필수입니다.") + private String subject; + + @Schema(description = "과제명", example = "제로초 인강") + @NotNull(message = "과제명은 필수입니다.") + private String assignmentName; + + @Schema(description = "주차", example = "1") + @Positive + private Long week; + + @Schema(description = "요일", example = "화") + @NotBlank(message = "요일을 입력해주세요.") + private String day; + + @Schema(description = "해당 일자 과제 numbering", example = "1") + private Long orderNumber; + +} diff --git a/backend/pirocheck/src/main/java/backend/pirocheck/Assignment/dto/request/AssignmentItemCreateReq.java b/backend/pirocheck/src/main/java/backend/pirocheck/Assignment/dto/request/AssignmentItemCreateReq.java new file mode 100644 index 0000000..62fc718 --- /dev/null +++ b/backend/pirocheck/src/main/java/backend/pirocheck/Assignment/dto/request/AssignmentItemCreateReq.java @@ -0,0 +1,22 @@ +package backend.pirocheck.Assignment.dto.request; + +import backend.pirocheck.Assignment.entity.AssignmentStatus; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Pattern; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class AssignmentItemCreateReq { + + private Long assignmentId; + + private Long userId; + + @Pattern(regexp = "SUCCESS/INSUFFICIENT/FAILURE", message = "status는 SUCCESS, INSUFFICIENT 혹은 FAILURE 여야 합니다.") + @Schema(description = "과제 결과", example = "SUCCESS") + private AssignmentStatus status; +} diff --git a/backend/pirocheck/src/main/java/backend/pirocheck/Assignment/dto/request/AssignmentItemUpdateReq.java b/backend/pirocheck/src/main/java/backend/pirocheck/Assignment/dto/request/AssignmentItemUpdateReq.java new file mode 100644 index 0000000..b917d54 --- /dev/null +++ b/backend/pirocheck/src/main/java/backend/pirocheck/Assignment/dto/request/AssignmentItemUpdateReq.java @@ -0,0 +1,16 @@ +package backend.pirocheck.Assignment.dto.request; + +import backend.pirocheck.Assignment.entity.AssignmentStatus; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Pattern; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class AssignmentItemUpdateReq { + + @Pattern(regexp = "SUCCESS/INSUFFICIENT/FAILURE", message = "status는 SUCCESS, INSUFFICIENT 혹은 FAILURE 여야 합니다.") + @Schema(description = "과제 결과", example = "SUCCESS") + private AssignmentStatus status; +} diff --git a/backend/pirocheck/src/main/java/backend/pirocheck/Assignment/dto/request/AssignmentUpdateReq.java b/backend/pirocheck/src/main/java/backend/pirocheck/Assignment/dto/request/AssignmentUpdateReq.java new file mode 100644 index 0000000..5caa73a --- /dev/null +++ b/backend/pirocheck/src/main/java/backend/pirocheck/Assignment/dto/request/AssignmentUpdateReq.java @@ -0,0 +1,33 @@ +package backend.pirocheck.Assignment.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class AssignmentUpdateReq { + + @Schema(description = "과제 주제", example = "Git/HTML/CSS") + @NotNull(message = "과제 주제는 필수입니다.") + private String subject; + + @Schema(description = "과제명", example = "제로초 인강") + @NotNull(message = "과제명은 필수입니다.") + private String assignmentName; + + @Schema(description = "주차", example = "1") + @Positive + private Long week; + + @Schema(description = "요일", example = "화") + @NotBlank(message = "요일을 입력해주세요.") + private String day; + + @Schema(description = "해당 일자 과제 numbering", example = "1") + private Long orderNumber; + +} diff --git a/backend/pirocheck/src/main/java/backend/pirocheck/assignment/dto/response/AssignmentDayRes.java b/backend/pirocheck/src/main/java/backend/pirocheck/Assignment/dto/response/AssignmentDayRes.java similarity index 81% rename from backend/pirocheck/src/main/java/backend/pirocheck/assignment/dto/response/AssignmentDayRes.java rename to backend/pirocheck/src/main/java/backend/pirocheck/Assignment/dto/response/AssignmentDayRes.java index be325e6..2f46e6a 100644 --- a/backend/pirocheck/src/main/java/backend/pirocheck/assignment/dto/response/AssignmentDayRes.java +++ b/backend/pirocheck/src/main/java/backend/pirocheck/Assignment/dto/response/AssignmentDayRes.java @@ -1,4 +1,4 @@ -package backend.pirocheck.assignment.dto.response; +package backend.pirocheck.Assignment.dto.response; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/backend/pirocheck/src/main/java/backend/pirocheck/assignment/dto/response/AssignmentDetailRes.java b/backend/pirocheck/src/main/java/backend/pirocheck/Assignment/dto/response/AssignmentDetailRes.java similarity index 63% rename from backend/pirocheck/src/main/java/backend/pirocheck/assignment/dto/response/AssignmentDetailRes.java rename to backend/pirocheck/src/main/java/backend/pirocheck/Assignment/dto/response/AssignmentDetailRes.java index 5a40dad..9c0f7f3 100644 --- a/backend/pirocheck/src/main/java/backend/pirocheck/assignment/dto/response/AssignmentDetailRes.java +++ b/backend/pirocheck/src/main/java/backend/pirocheck/Assignment/dto/response/AssignmentDetailRes.java @@ -1,6 +1,6 @@ -package backend.pirocheck.assignment.dto.response; +package backend.pirocheck.Assignment.dto.response; -import backend.pirocheck.assignment.entity.AssignmentStatus; +import backend.pirocheck.Assignment.entity.AssignmentStatus; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/backend/pirocheck/src/main/java/backend/pirocheck/assignment/dto/response/AssignmentRes.java b/backend/pirocheck/src/main/java/backend/pirocheck/Assignment/dto/response/AssignmentRes.java similarity index 66% rename from backend/pirocheck/src/main/java/backend/pirocheck/assignment/dto/response/AssignmentRes.java rename to backend/pirocheck/src/main/java/backend/pirocheck/Assignment/dto/response/AssignmentRes.java index c924848..ef2ad37 100644 --- a/backend/pirocheck/src/main/java/backend/pirocheck/assignment/dto/response/AssignmentRes.java +++ b/backend/pirocheck/src/main/java/backend/pirocheck/Assignment/dto/response/AssignmentRes.java @@ -1,6 +1,6 @@ -package backend.pirocheck.assignment.dto.response; +package backend.pirocheck.Assignment.dto.response; -import backend.pirocheck.assignment.entity.AssignmentStatus; +import backend.pirocheck.Assignment.entity.AssignmentStatus; import lombok.AllArgsConstructor; import lombok.Getter; @@ -11,7 +11,7 @@ public class AssignmentRes { // private Long userId; private String assignmentName; private Long week; - private Long section; + private String day; private Long orderNumber; private AssignmentStatus submitted; diff --git a/backend/pirocheck/src/main/java/backend/pirocheck/assignment/dto/response/AssignmentWeekRes.java b/backend/pirocheck/src/main/java/backend/pirocheck/Assignment/dto/response/AssignmentWeekRes.java similarity index 62% rename from backend/pirocheck/src/main/java/backend/pirocheck/assignment/dto/response/AssignmentWeekRes.java rename to backend/pirocheck/src/main/java/backend/pirocheck/Assignment/dto/response/AssignmentWeekRes.java index 4be2d26..b0d7721 100644 --- a/backend/pirocheck/src/main/java/backend/pirocheck/assignment/dto/response/AssignmentWeekRes.java +++ b/backend/pirocheck/src/main/java/backend/pirocheck/Assignment/dto/response/AssignmentWeekRes.java @@ -1,4 +1,4 @@ -package backend.pirocheck.assignment.dto.response; +package backend.pirocheck.Assignment.dto.response; import lombok.AllArgsConstructor; import lombok.Getter; @@ -9,6 +9,6 @@ @AllArgsConstructor public class AssignmentWeekRes { private Long week; - private String title; // 각 주차 주제 (e.g, Git / HTML / CSS) + private String subject; // 각 주차 주제 (e.g, Git / HTML / CSS) private List days; } diff --git a/backend/pirocheck/src/main/java/backend/pirocheck/Assignment/entity/Assignment.java b/backend/pirocheck/src/main/java/backend/pirocheck/Assignment/entity/Assignment.java new file mode 100644 index 0000000..eede8f9 --- /dev/null +++ b/backend/pirocheck/src/main/java/backend/pirocheck/Assignment/entity/Assignment.java @@ -0,0 +1,68 @@ +package backend.pirocheck.Assignment.entity; + +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.persistence.*; +import lombok.*; + +import java.util.ArrayList; +import java.util.List; + +@Entity +@Getter +@Builder(access = AccessLevel.PRIVATE) +@NoArgsConstructor +@AllArgsConstructor +@Tag(name = "과제 관리", description = "과제 관련 API") +public class Assignment { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + // 전체 주제 + private String subject; + + // 과제명 + private String assignmentName; + + // 주차 + private Long week; + + // 요일 + private String day; + + // 과제 번호 + private Long orderNumber; + + // AssignmentItem 입장에서 "assignment" 필드의 외래 키를 가진 주인 + // assignment를 참조하는 assignmentitem 컬랙션을 가짐 + @OneToMany(mappedBy = "assignment", cascade = CascadeType.ALL, orphanRemoval = true) + @Builder.Default + private List assignments = new ArrayList<>(); + + // 연관관계 편의 메서드 (양방향 시 자주 사용) + public void addAssignmentItem(AssignmentItem assignmentItem) { + this.assignments.add(assignmentItem); + assignmentItem.setAssignment(this); + } + + // 관리자가 생성 + public static Assignment create(String subject, String assignmentName, Long week, String day, Long orderNumber) { + return Assignment.builder() + .subject(subject) + .assignmentName(assignmentName) + .week(week) + .day(day) + .orderNumber(orderNumber) + .build(); + } + + // 과제 내용 업데이트 + public void update(String subject, String assignmentName, Long week, String day, Long orderNumber) { + this.subject = subject; + this.assignmentName = assignmentName; + this.week = week; + this.day = day; + this.orderNumber = orderNumber; + } +} diff --git a/backend/pirocheck/src/main/java/backend/pirocheck/Assignment/entity/AssignmentItem.java b/backend/pirocheck/src/main/java/backend/pirocheck/Assignment/entity/AssignmentItem.java new file mode 100644 index 0000000..6a9dddf --- /dev/null +++ b/backend/pirocheck/src/main/java/backend/pirocheck/Assignment/entity/AssignmentItem.java @@ -0,0 +1,44 @@ +package backend.pirocheck.Assignment.entity; + +import backend.pirocheck.User.entity.User; +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@Setter +@Builder(access = AccessLevel.PRIVATE) +@AllArgsConstructor +@NoArgsConstructor +public class AssignmentItem { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; // 유저별 과제 정보를 저장하는 ID + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "assignment_id") + private Assignment assignment; + + // 과제 결과 + @Enumerated(EnumType.STRING) + @Column(length = 100) + private AssignmentStatus submitted; // 수강생의 과제 제출여부 + + public static AssignmentItem create(User user, Assignment assignment, AssignmentStatus submitted) { + return AssignmentItem.builder() + .assignment(assignment) + .user(user) + .submitted(submitted) + .build(); + } + + public void update(AssignmentStatus submitted) { + + this.submitted = submitted; + + } +} diff --git a/backend/pirocheck/src/main/java/backend/pirocheck/assignment/entity/AssignmentStatus.java b/backend/pirocheck/src/main/java/backend/pirocheck/Assignment/entity/AssignmentStatus.java similarity index 60% rename from backend/pirocheck/src/main/java/backend/pirocheck/assignment/entity/AssignmentStatus.java rename to backend/pirocheck/src/main/java/backend/pirocheck/Assignment/entity/AssignmentStatus.java index dcc9466..290cfea 100644 --- a/backend/pirocheck/src/main/java/backend/pirocheck/assignment/entity/AssignmentStatus.java +++ b/backend/pirocheck/src/main/java/backend/pirocheck/Assignment/entity/AssignmentStatus.java @@ -1,4 +1,4 @@ -package backend.pirocheck.assignment.entity; +package backend.pirocheck.Assignment.entity; public enum AssignmentStatus { SUCCESS, INSUFFICIENT, FAILURE; diff --git a/backend/pirocheck/src/main/java/backend/pirocheck/Assignment/repository/AssignmentItemRepository.java b/backend/pirocheck/src/main/java/backend/pirocheck/Assignment/repository/AssignmentItemRepository.java new file mode 100644 index 0000000..f07c351 --- /dev/null +++ b/backend/pirocheck/src/main/java/backend/pirocheck/Assignment/repository/AssignmentItemRepository.java @@ -0,0 +1,25 @@ +package backend.pirocheck.Assignment.repository; + +import backend.pirocheck.Assignment.entity.Assignment; +import backend.pirocheck.Assignment.entity.AssignmentItem; +import backend.pirocheck.Assignment.entity.AssignmentStatus; +import backend.pirocheck.User.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface AssignmentItemRepository extends JpaRepository { + + // 유저별 과제 목록 조회 + List findByUserId(Long userId); + + // 보증금 + int countByUserAndSubmitted(User user, AssignmentStatus status); + + Optional findByUserAndAssignment(User user, Assignment assignment); + // Optional 처리로 오류 발생 check + +} diff --git a/backend/pirocheck/src/main/java/backend/pirocheck/Assignment/repository/AssignmentRepository.java b/backend/pirocheck/src/main/java/backend/pirocheck/Assignment/repository/AssignmentRepository.java new file mode 100644 index 0000000..408e69c --- /dev/null +++ b/backend/pirocheck/src/main/java/backend/pirocheck/Assignment/repository/AssignmentRepository.java @@ -0,0 +1,9 @@ +package backend.pirocheck.Assignment.repository; + +import backend.pirocheck.Assignment.entity.Assignment; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface AssignmentRepository extends JpaRepository { +} diff --git a/backend/pirocheck/src/main/java/backend/pirocheck/Assignment/service/AssignmentService.java b/backend/pirocheck/src/main/java/backend/pirocheck/Assignment/service/AssignmentService.java new file mode 100644 index 0000000..e589e2c --- /dev/null +++ b/backend/pirocheck/src/main/java/backend/pirocheck/Assignment/service/AssignmentService.java @@ -0,0 +1,161 @@ +package backend.pirocheck.Assignment.service; + +import backend.pirocheck.Assignment.dto.request.AssignmentCreateReq; +import backend.pirocheck.Assignment.dto.request.AssignmentItemCreateReq; +import backend.pirocheck.Assignment.dto.request.AssignmentItemUpdateReq; +import backend.pirocheck.Assignment.dto.request.AssignmentUpdateReq; +import backend.pirocheck.Assignment.dto.response.AssignmentDayRes; +import backend.pirocheck.Assignment.dto.response.AssignmentDetailRes; +import backend.pirocheck.Assignment.dto.response.AssignmentWeekRes; +import backend.pirocheck.Assignment.entity.Assignment; +import backend.pirocheck.Assignment.entity.AssignmentItem; +import backend.pirocheck.Assignment.entity.AssignmentStatus; +import backend.pirocheck.Assignment.repository.AssignmentItemRepository; +import backend.pirocheck.Assignment.repository.AssignmentRepository; +import backend.pirocheck.User.entity.User; +import backend.pirocheck.User.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Slf4j // 로그를 찍기위해 사용 +@Service +@Transactional +@RequiredArgsConstructor +public class AssignmentService { + + private final AssignmentItemRepository assignmentItemRepository; + private final AssignmentRepository assignmentRepository; + private final UserRepository userRepository; + + public List search(Long userId) { + + List assignments = assignmentItemRepository.findByUserId(userId); + // week 기준으로 그룹화 + Map> weekGroup = assignments.stream() + .collect(Collectors.groupingBy(item -> item.getAssignment().getWeek())); // assignmentItems를 week 별로 그룹핑 + + List assignmentResponses = new ArrayList<>(); + + for (Map.Entry> entry : weekGroup.entrySet()) { + Long week = entry.getKey(); // 주차 정보 + List assignmentList = entry.getValue(); // 주차에 해당하는 days의 list + + String subject = assignmentList.get(0).getAssignment().getSubject(); + + // day를 기준으로 그룹핑 + Map> dayGroup = assignmentList.stream() + .collect(Collectors.groupingBy(item -> item.getAssignment().getDay())); + + List assignmentDayResList = new ArrayList<>(); + + for (Map.Entry> dayEntry : dayGroup.entrySet()) { + String day = dayEntry.getKey(); + List dayAssignmentList = dayEntry.getValue(); + + // 세부 과제명과 과제 결과를 리스트 형태로 + List assignmentDetailResList = dayAssignmentList.stream() + .map(assignmentItem -> new AssignmentDetailRes( + assignmentItem.getAssignment().getAssignmentName(), + assignmentItem.getSubmitted() + )) + .toList(); + assignmentDayResList.add(new AssignmentDayRes(day, assignmentDetailResList)); + } + + assignmentResponses.add(new AssignmentWeekRes(week, subject, assignmentDayResList)); + } + + return assignmentResponses; + } + + public String createAssignment(AssignmentCreateReq assignmentCreateReq) { + + Assignment assignment = Assignment.create( + assignmentCreateReq.getSubject(), + assignmentCreateReq.getAssignmentName(), + assignmentCreateReq.getWeek(), + assignmentCreateReq.getDay(), + assignmentCreateReq.getOrderNumber()); + + assignment = assignmentRepository.save(assignment); + + // 전체 유저에게 과제 자동 할당 + List users = userRepository.findAll(); + + for (User user : users) { + + AssignmentItem item = AssignmentItem.create(user, assignment, AssignmentStatus.INSUFFICIENT); + + assignment.addAssignmentItem(item); + user.addAssignmentItem(item); + +// assignmentItemRepository.save(item); +// Cascade 설정이 되어있으므로 assignment = assignmentRepository.save(assignment); 이 코드를 실행할 때 연관된 AssignmentItem도 함께 저장 됨 + } + + return assignment.getAssignmentName(); + } + + // 과제 삭제 + public String deleteAssignment(Long assignmentId) { + assignmentRepository.deleteById(assignmentId); + return "과제가 성공적으로 삭제되었습니다."; + } + + // 과제 수정 + public String updateAssignment(Long assignmentId, AssignmentUpdateReq req) { + Assignment assignment = assignmentRepository.findById(assignmentId) + .orElseThrow(() -> new IllegalArgumentException("조회된 과제가 없습니다.")); + + assignment.update(req.getSubject(), req.getAssignmentName(), req.getWeek(), req.getDay(), req.getOrderNumber()); + assignmentRepository.save(assignment); + + return assignment.getAssignmentName(); + } + + // 과제 채점 결과 저장 + public AssignmentStatus createAssignmentItem(Long userId, Long assignmentId, AssignmentItemCreateReq req) { + log.info("userId 요청 값: {}", userId); + + User user = userRepository.findById(userId) + .orElseThrow(() -> new IllegalArgumentException("조회된 사용자가 없습니다.")); + + Assignment assignment = assignmentRepository.findById(assignmentId) + .orElseThrow(() -> new IllegalArgumentException("조회된 과제가 없습니다.")); + + AssignmentItem assignmentItem = AssignmentItem.create( + user, + assignment, + req.getStatus() + ); + + assignmentItemRepository.save(assignmentItem); + + return assignmentItem.getSubmitted(); + } + + // 과제 채점 결과 수정 + public AssignmentStatus updateAssignmentItem(Long userId, Long assignmentId, AssignmentItemUpdateReq req) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new IllegalArgumentException("조회된 사용자가 없습니다.")); + + Assignment assignment = assignmentRepository.findById(assignmentId) + .orElseThrow(() -> new IllegalArgumentException("조회된 과제가 없습니다.")); + + AssignmentItem assignmentItem = assignmentItemRepository.findByUserAndAssignment(user, assignment) + .orElseThrow(() -> new IllegalArgumentException("해당 유저의 과제 채점 결과가 없습니다.")); + + assignmentItem.update(req.getStatus()); // 상태 업데이트 + + assignmentItemRepository.save(assignmentItem); // 상태 저장 + + return assignmentItem.getSubmitted(); + } +} 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 new file mode 100644 index 0000000..591b212 --- /dev/null +++ b/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/controller/AdminAttendanceController.java @@ -0,0 +1,202 @@ +package backend.pirocheck.Attendance.controller; + +import backend.pirocheck.Attendance.dto.request.UpdateAttendanceStatusReq; +import backend.pirocheck.Attendance.dto.response.ApiResponse; +import backend.pirocheck.Attendance.dto.response.AttendanceCodeResponse; +import backend.pirocheck.Attendance.dto.response.UserAttendanceStatusRes; +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.security.SecurityRequirement; +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") +@Tag(name = "관리자 출석관리", description = "관리자용 출석 관리 API") +public class AdminAttendanceController { + + private final AttendanceService attendanceService; + + // 출석체크 시작 + @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 = "잘못된 요청") + }) + @PostMapping("/admin/attendance/start") + public AttendanceCodeResponse startAttendance() { + try { + AttendanceCode code = attendanceService.generateCodeAndCreateAttendances(); + return AttendanceCodeResponse.from(code); + } catch (IllegalStateException e) { + // 하루 최대 출석 체크 횟수를 초과한 경우 + throw new IllegalStateException(e.getMessage()); + } catch (Exception e) { + throw new RuntimeException("출석 코드 생성 중 오류가 발생했습니다: " + e.getMessage()); + } + } + + // 현재 활성화된 출석코드 조회 + @Operation(summary = "현재 활성화된 출석 코드 조회", description = "현재 활성화된 출석 코드 정보를 조회합니다.") + @ApiResponses(value = { + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "조회 성공"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "활성화된 출석 코드 없음") + }) + @GetMapping("/admin/attendance/active-code") + public AttendanceCodeResponse getActiveCode() { + Optional codeOpt = attendanceService.getActiveAttendanceCode(); + + if (codeOpt.isEmpty()) { + throw new RuntimeException("현재 활성화된 출석코드가 없습니다"); + } + + return AttendanceCodeResponse.from(codeOpt.get()); + } + + // 출석체크 종료 (코드 직접 전달) + @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 = "출석 코드를 찾을 수 없음") + }) + @PutMapping("/admin/attendance/expire") + public String expireAttendance( + @Parameter(description = "만료할 출석 코드", example = "1234") + @RequestParam String code) { + return attendanceService.expireAttendanceCode(code); + } + + // 출석체크 종료 (가장 최근 활성화된 코드 자동 만료) + @Operation(summary = "최근 활성화된 출석 코드 만료", description = "가장 최근 활성화된 출석 코드를 자동으로 만료 처리합니다.") + @ApiResponses(value = { + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "만료 처리 성공"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "활성화된 출석 코드가 없음") + }) + @PutMapping("/admin/attendance/expire-latest") + public String expireLatestAttendance() { + return attendanceService.expireLatestAttendanceCode(); + } + + // 출석 상태 변경 (관리자 전용) + @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 = "출석 기록을 찾을 수 없음") + }) + @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) { + + // 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) { + + // userId 파라미터 검증은 여기서 할 수 있음 (필요 시) + return attendanceService.deleteAttendance(attendanceId); + } + + // 특정 날짜와 차수에 대한 모든 학생의 출석 현황 조회 + @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("/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 = "조회할 차수", 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) { + + 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/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/request/UpdateAttendanceStatusReq.java b/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/dto/request/UpdateAttendanceStatusReq.java new file mode 100644 index 0000000..0bea7c5 --- /dev/null +++ b/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/dto/request/UpdateAttendanceStatusReq.java @@ -0,0 +1,17 @@ +package backend.pirocheck.Attendance.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "출석 상태 수정 요청") +public class UpdateAttendanceStatusReq { + @Schema(description = "변경할 출석 상태", example = "true") + private boolean status; +} \ No newline at end of file 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/AttendanceMarkResponse.java b/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/dto/response/AttendanceMarkResponse.java new file mode 100644 index 0000000..8f3551c --- /dev/null +++ b/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/dto/response/AttendanceMarkResponse.java @@ -0,0 +1,68 @@ +package backend.pirocheck.Attendance.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "출석 체크 응답") +public class AttendanceMarkResponse { + @Schema(description = "응답 메시지", example = "출석이 성공적으로 처리되었습니다") + private String message; + + @Schema(description = "상태 코드 (SUCCESS, ALREADY_MARKED, INVALID_CODE, NO_ACTIVE_SESSION, CODE_EXPIRED, ERROR)", example = "SUCCESS") + private String statusCode; + + // 출석 성공 + public static AttendanceMarkResponse success() { + return AttendanceMarkResponse.builder() + .statusCode("SUCCESS") + .message("출석이 성공적으로 처리되었습니다") + .build(); + } + + // 이미 출석 완료 + public static AttendanceMarkResponse alreadyMarked() { + return AttendanceMarkResponse.builder() + .statusCode("ALREADY_MARKED") + .message("이미 출석처리가 완료되었습니다") + .build(); + } + + // 출석체크 진행중 아님 + public static AttendanceMarkResponse noActiveSession() { + return AttendanceMarkResponse.builder() + .statusCode("NO_ACTIVE_SESSION") + .message("출석 코드가 존재하지 않습니다. 현재 출석 체크가 진행중이 아닙니다") + .build(); + } + + // 잘못된 출석 코드 입력 + public static AttendanceMarkResponse invalidCode() { + return AttendanceMarkResponse.builder() + .statusCode("INVALID_CODE") + .message("잘못된 출석 코드입니다. 다시 확인해주세요") + .build(); + } + + // 출석 코드 만료 + public static AttendanceMarkResponse codeExpired() { + return AttendanceMarkResponse.builder() + .statusCode("CODE_EXPIRED") + .message("출석 코드가 만료되었습니다") + .build(); + } + + // 기타 오류 + public static AttendanceMarkResponse error(String message) { + return AttendanceMarkResponse.builder() + .statusCode("ERROR") + .message(message) + .build(); + } +} 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/dto/response/UserAttendanceStatusRes.java b/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/dto/response/UserAttendanceStatusRes.java new file mode 100644 index 0000000..fcb624c --- /dev/null +++ b/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/dto/response/UserAttendanceStatusRes.java @@ -0,0 +1,34 @@ +package backend.pirocheck.Attendance.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "사용자 출석 상태 응답") +public class UserAttendanceStatusRes { + @Schema(description = "출석 기록 ID", example = "1") + private Long attendanceId; + + @Schema(description = "사용자 ID", example = "1") + private Long userId; + + @Schema(description = "사용자 이름", example = "홍길동") + private String username; + + @Schema(description = "출석 날짜", example = "2023-10-20") + private LocalDate date; + + @Schema(description = "출석 차수", example = "1") + private int order; + + @Schema(description = "출석 상태", example = "true") + private boolean status; +} \ No newline at end of file diff --git a/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/repository/AttendanceRepository.java b/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/repository/AttendanceRepository.java index e06f6e9..783b90b 100644 --- a/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/repository/AttendanceRepository.java +++ b/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/repository/AttendanceRepository.java @@ -17,4 +17,7 @@ public interface AttendanceRepository extends JpaRepository { // 출석 실패 int countByUserAndStatusFalse(User user); + + // 특정 날짜와 차수에 대한 모든 출석 기록 조회 + List findByDateAndOrder(LocalDate date, int order); } 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..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 @@ -3,8 +3,10 @@ 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.dto.response.UserAttendanceStatusRes; import backend.pirocheck.Attendance.entity.Attendance; import backend.pirocheck.Attendance.entity.AttendanceCode; import backend.pirocheck.Attendance.repository.AttendanceCodeRepository; @@ -43,6 +45,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)); @@ -98,7 +105,7 @@ public String expireLatestAttendanceCode() { // 출석코드 만료처리 함수 @Transactional - public String exprireAttendanceCode(String code) { + public String expireAttendanceCode(String code) { Optional codeOpt = attendanceCodeRepository.findByCodeAndDate(code, LocalDate.now()); if (codeOpt.isEmpty()) { @@ -119,25 +126,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 +165,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(); } // 유저의 전체 출석 현황을 조회하는 함수 @@ -189,4 +209,120 @@ public List findByUserIdAndDate(Long userId, LocalDate date) .sorted(Comparator.comparingInt(AttendanceSlotRes::getOrder)) .toList(); } + + // 관리자가 유저의 출석 상태를 변경하는 함수 + @Transactional + public boolean updateAttendanceStatus(Long attendanceId, boolean status) { + Optional attendanceOpt = attendanceRepository.findById(attendanceId); + + if (attendanceOpt.isEmpty()) { + return false; + } + + // 출석 상태 변경 + Attendance attendance = attendanceOpt.get(); + attendance.setStatus(status); + attendanceRepository.save(attendance); + return true; + } + + // 특정 날짜와 차수의 모든 학생 출석 현황 조회 + public List findAllByDateAndOrder(LocalDate date, int order) { + // 해당 날짜와 차수에 대한 모든 출석 기록 조회 + List attendances = attendanceRepository.findByDateAndOrder(date, order); + + // 사용자별로 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()) // 출석 기록 ID 추가 + .build(); + }) + .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/Deposit/service/DepositService.java b/backend/pirocheck/src/main/java/backend/pirocheck/Deposit/service/DepositService.java index ce3da5f..66eafb1 100644 --- a/backend/pirocheck/src/main/java/backend/pirocheck/Deposit/service/DepositService.java +++ b/backend/pirocheck/src/main/java/backend/pirocheck/Deposit/service/DepositService.java @@ -6,8 +6,8 @@ import backend.pirocheck.Deposit.repository.DepositRepository; import backend.pirocheck.User.entity.User; import backend.pirocheck.User.repository.UserRepository; -import backend.pirocheck.assignment.entity.AssignmentStatus; -import backend.pirocheck.assignment.repository.AssignmentRepository; +import backend.pirocheck.Assignment.entity.AssignmentStatus; +import backend.pirocheck.Assignment.repository.AssignmentItemRepository; import backend.pirocheck.Attendance.repository.AttendanceRepository; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; @@ -20,7 +20,7 @@ public class DepositService { private final DepositRepository depositRepository; private final UserRepository userRepository; private final AttendanceRepository attendanceRepository; - private final AssignmentRepository assignmentRepository; + private final AssignmentItemRepository assignmentItemRepository; @Transactional public DepositResDto getDeposit(Long userId) { @@ -34,8 +34,8 @@ public DepositResDto getDeposit(Long userId) { int descentAttendance = failAttendanceCount * 10_000; // 과제 실패 - int failAssignmentCount = assignmentRepository.countByUserAndSubmitted(user, AssignmentStatus.FAILURE); - int weakAssignmentCount = assignmentRepository.countByUserAndSubmitted(user, AssignmentStatus.INSUFFICIENT); + int failAssignmentCount = assignmentItemRepository.countByUserAndSubmitted(user, AssignmentStatus.FAILURE); + int weakAssignmentCount = assignmentItemRepository.countByUserAndSubmitted(user, AssignmentStatus.INSUFFICIENT); int descentAssignment = failAssignmentCount * 20_000 + weakAssignmentCount * 10_000; // 방어권 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 e28c409..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 @@ -1,5 +1,7 @@ package backend.pirocheck.ManageStudents.service; +import backend.pirocheck.Assignment.entity.AssignmentItem; +import backend.pirocheck.Assignment.repository.AssignmentItemRepository; import backend.pirocheck.Deposit.entity.Deposit; import backend.pirocheck.Deposit.repository.DepositRepository; import backend.pirocheck.ManageStudents.dto.response.ManageStudentDetailResDto; @@ -7,8 +9,7 @@ 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 jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -21,17 +22,16 @@ public class ManageStudentsService { private final UserRepository userRepository; private final DepositRepository depositRepository; - private final AssignmentRepository assignmentRepository; + private final AssignmentItemRepository assignmentItemRepository; // 수강생 조회 public List searchMembers(String name) { List users; - if(name == null || name.isBlank()) { + if (name == null || name.isBlank()) { // 검색어가 없으면 맴버 전체 조회 users = userRepository.findByRole(Role.MEMBER); - } - else { + } else { // 이름 검색 users = userRepository.findByNameContainingAndRole(name, Role.MEMBER); } @@ -42,6 +42,7 @@ public List searchMembers(String name) { } // 수강생 상세 조회 + @Transactional public ManageStudentDetailResDto getMemberDetail(Long studentId) { // User 조회 User user = userRepository.findById(studentId) @@ -54,11 +55,11 @@ public ManageStudentDetailResDto getMemberDetail(Long studentId) { } // Assignment 리스트 조회 - List assignments = assignmentRepository.findByUserId(studentId); + List assignments = assignmentItemRepository.findByUserId(studentId); // 과제 제목만 리스트로 변환 List assignmentTitles = assignments.stream() - .map(Assignment::getAssignmentName) + .map(assignment -> assignment.getAssignment().getAssignmentName()) .toList(); // ManageStudentDetailResDto 조립 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/backend/pirocheck/src/main/java/backend/pirocheck/User/entity/User.java b/backend/pirocheck/src/main/java/backend/pirocheck/User/entity/User.java index ad215b0..65b6717 100644 --- a/backend/pirocheck/src/main/java/backend/pirocheck/User/entity/User.java +++ b/backend/pirocheck/src/main/java/backend/pirocheck/User/entity/User.java @@ -1,11 +1,15 @@ package backend.pirocheck.User.entity; +import backend.pirocheck.Assignment.entity.AssignmentItem; import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import java.util.ArrayList; +import java.util.List; + @Entity @Getter @Table(name="users") @@ -30,6 +34,20 @@ public class User { @Enumerated(EnumType.STRING) private Role role; // MEMBER or ADMIN + @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) + private List assignmentItems = new ArrayList<>(); + private Integer generation; + // AssignmentItem 입장에서 "user" 필드의 외래 키를 가진 주인 + // assignment를 참조하는 assignmentitem 컬랙션을 가짐 + @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) + private List assignments = new ArrayList<>(); + + // 연관관계 편의 메서드 (양방향 시 자주 사용) + public void addAssignmentItem(AssignmentItem assignmentItem) { + this.assignments.add(assignmentItem); + assignmentItem.setUser(this); + } + } \ No newline at end of file diff --git a/backend/pirocheck/src/main/java/backend/pirocheck/User/filter/SessionCheckFilter.java b/backend/pirocheck/src/main/java/backend/pirocheck/User/filter/SessionCheckFilter.java index 61a09c4..83dc991 100644 --- a/backend/pirocheck/src/main/java/backend/pirocheck/User/filter/SessionCheckFilter.java +++ b/backend/pirocheck/src/main/java/backend/pirocheck/User/filter/SessionCheckFilter.java @@ -18,9 +18,10 @@ protected void doFilterInternal(HttpServletRequest request, throws ServletException, IOException { String path = request.getRequestURI(); + String method = request.getMethod(); - // 로그인/로그아웃 요청은 세션 체크 제외 - if (path.startsWith("/api/login") || path.startsWith("/api/logout")) { + // CORS preflight 요청(OPTIONS) 또는 로그인/로그아웃 요청은 세션 체크 제외 + if ("OPTIONS".equals(method) || path.startsWith("/api/login") || path.startsWith("/api/logout")) { filterChain.doFilter(request, response); // 다음 필터나 컨트롤러로 넘기는 명령어 return; // 세션 검사 안함 } 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 deleted file mode 100644 index 0c1379f..0000000 --- a/backend/pirocheck/src/main/java/backend/pirocheck/assignment/controller/AssignmentController.java +++ /dev/null @@ -1,36 +0,0 @@ -package backend.pirocheck.assignment.controller; - -import backend.pirocheck.assignment.dto.request.AssignmentReq; -import backend.pirocheck.assignment.dto.response.AssignmentRes; -import backend.pirocheck.assignment.dto.response.AssignmentWeekRes; -import backend.pirocheck.assignment.service.AssignmentService; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@RestController -@RequestMapping("/api/assignment") -@RequiredArgsConstructor -public class AssignmentController { - - private final AssignmentService assignmentService; - - // 과제 결과 확인 API -// @GetMapping("/{userId}") -// public List getAssignment(@PathVariable("userId") Long userId) { -// // 주차별 과제 제목, 요일별 과제 제목, 과제 상태 반환 -// return assignmentService.search(userId); -// } - // 과제 주차별, 요일별 그룹화 JSON - @GetMapping("/grouped/{userId}") - public List getGroupedAssignments(@PathVariable Long userId) { - return assignmentService.search(userId); - } - - // 과제 생성 API - @PostMapping("/signup") - public String signupAssignment(@RequestBody AssignmentReq assignmentReq) { - return assignmentService.create(assignmentReq); - } -} diff --git a/backend/pirocheck/src/main/java/backend/pirocheck/assignment/dto/request/AssignmentReq.java b/backend/pirocheck/src/main/java/backend/pirocheck/assignment/dto/request/AssignmentReq.java deleted file mode 100644 index 3d1cfd6..0000000 --- a/backend/pirocheck/src/main/java/backend/pirocheck/assignment/dto/request/AssignmentReq.java +++ /dev/null @@ -1,16 +0,0 @@ -package backend.pirocheck.assignment.dto.request; - -import backend.pirocheck.assignment.entity.AssignmentStatus; -import lombok.AllArgsConstructor; -import lombok.Getter; - -@Getter -@AllArgsConstructor -public class AssignmentReq { - - private String assignmentName; - private Long week; - private Long section; - private Long orderNumber; - -} diff --git a/backend/pirocheck/src/main/java/backend/pirocheck/assignment/entity/Assignment.java b/backend/pirocheck/src/main/java/backend/pirocheck/assignment/entity/Assignment.java deleted file mode 100644 index 62aa724..0000000 --- a/backend/pirocheck/src/main/java/backend/pirocheck/assignment/entity/Assignment.java +++ /dev/null @@ -1,49 +0,0 @@ -package backend.pirocheck.assignment.entity; - -import backend.pirocheck.User.entity.User; -import jakarta.persistence.*; -import lombok.*; - -@Entity -@Getter -@Builder(access = AccessLevel.PRIVATE) -@NoArgsConstructor -@AllArgsConstructor -public class Assignment { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - // user 정보와 과제 정보를 연결해 저장 - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id") - private User user; // user를 생성했을 때의 해당 user 엔티티 - - // 과제명 - private String assignmentName; - - // 주차 - private Long week; - - // 요일 - private Long section; - - // 과제 번호 - private Long orderNumber; - - // 과제 결과 - @Enumerated(EnumType.STRING) - @Column(length = 100) - private AssignmentStatus submitted; - - // 관리자가 생성 - public static Assignment create(String assignmentName, Long week, Long section, Long orderNumber) { - return Assignment.builder() - .assignmentName(assignmentName) - .week(week) - .section(section) - .orderNumber(orderNumber) - .build(); - } -} diff --git a/backend/pirocheck/src/main/java/backend/pirocheck/assignment/repository/AssignmentRepository.java b/backend/pirocheck/src/main/java/backend/pirocheck/assignment/repository/AssignmentRepository.java deleted file mode 100644 index c0048f9..0000000 --- a/backend/pirocheck/src/main/java/backend/pirocheck/assignment/repository/AssignmentRepository.java +++ /dev/null @@ -1,20 +0,0 @@ -package backend.pirocheck.assignment.repository; - -import backend.pirocheck.User.entity.User; -import backend.pirocheck.assignment.entity.Assignment; -import backend.pirocheck.assignment.entity.AssignmentStatus; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.List; - -@Repository -public interface AssignmentRepository extends JpaRepository { - - // 유저별 과제 목록 조회 - List findByUserId(Long userId); - - // 보증금 - int countByUserAndSubmitted(User user, AssignmentStatus status); - -} diff --git a/backend/pirocheck/src/main/java/backend/pirocheck/assignment/service/AssignmentService.java b/backend/pirocheck/src/main/java/backend/pirocheck/assignment/service/AssignmentService.java deleted file mode 100644 index d98b74b..0000000 --- a/backend/pirocheck/src/main/java/backend/pirocheck/assignment/service/AssignmentService.java +++ /dev/null @@ -1,83 +0,0 @@ -package backend.pirocheck.assignment.service; - -import backend.pirocheck.assignment.dto.request.AssignmentReq; -import backend.pirocheck.assignment.dto.response.AssignmentDayRes; -import backend.pirocheck.assignment.dto.response.AssignmentDetailRes; -import backend.pirocheck.assignment.dto.response.AssignmentRes; -import backend.pirocheck.assignment.dto.response.AssignmentWeekRes; -import backend.pirocheck.assignment.entity.Assignment; -import backend.pirocheck.assignment.repository.AssignmentRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -@Service -@Transactional -@RequiredArgsConstructor -public class AssignmentService { - - private final AssignmentRepository assignmentRepository; - - // 그룹화 되지 않은 JSON 형식 -// public List search(Long userId) { -// // 각 유저별 전체 과제 목록 조회 (userId를 어떻게 넘길 것인가?) search의 인자로 넘긴다... -// List assignments = assignmentRepository.findByUserId(userId); -// -// return assignments.stream() -// .map(assignment -> new AssignmentRes(assignment.getAssignmentName(), assignment.getWeek(), assignment.getSection(), assignment.getOrderNumber(), assignment.getSubmitted())) -// .toList(); -// } - public List search(Long userId) { - - List assignments = assignmentRepository.findByUserId(userId); - // week 기준으로 그룹화 - Map> weekGroup = assignments.stream() - .collect(Collectors.groupingBy(Assignment::getWeek)); // assignments를 week 별로 그룹핑 - - List assignmentResponses = new ArrayList<>(); - - for (Map.Entry> entry : weekGroup.entrySet()) { - Long week = entry.getKey(); - List assignmentList = entry.getValue(); - - // day를 기준으로 그룹핑 - Map> dayGroup = assignments.stream() - .collect(Collectors.groupingBy(Assignment::getSection)); - - List assignmentDayResList = new ArrayList<>(); - - for (Map.Entry> dayEntry : dayGroup.entrySet()) { - Long day = dayEntry.getKey(); - List dayAssignmentList = dayEntry.getValue(); - - // 세부 과제명과 과제 결과를 리스트 형태로 - List assignmentDetailResList = dayAssignmentList.stream() - .map(assignment -> new AssignmentDetailRes( - assignment.getAssignmentName(), - assignment.getSubmitted() - )) - .toList(); - } - } - - return assignmentResponses; - } - - public String create(AssignmentReq assignmentReq) { - - Assignment assignment = Assignment.create( - assignmentReq.getAssignmentName(), - assignmentReq.getWeek(), - assignmentReq.getSection(), - assignmentReq.getOrderNumber()); - - assignment = assignmentRepository.save(assignment); - - return assignment.getAssignmentName(); - } -} diff --git a/backend/pirocheck/src/main/java/backend/pirocheck/config/SwaggerConfig.java b/backend/pirocheck/src/main/java/backend/pirocheck/config/SwaggerConfig.java new file mode 100644 index 0000000..aa87446 --- /dev/null +++ b/backend/pirocheck/src/main/java/backend/pirocheck/config/SwaggerConfig.java @@ -0,0 +1,24 @@ +package backend.pirocheck.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.servers.Server; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SwaggerConfig { + + @Bean + public OpenAPI openAPI() { + return new OpenAPI() + .info(new Info() + .title("PiroCheck API") + .description("피로그래밍 출석체크 시스템 API 문서") + .version("v1.0.0") + .license(new License().name("MIT").url("https://opensource.org/licenses/MIT"))) + .addServersItem(new Server().url("/").description("Default Server URL")); + } +} \ No newline at end of file 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 d149b66..430ff99 100644 --- a/backend/pirocheck/src/main/java/backend/pirocheck/config/WebConfig.java +++ b/backend/pirocheck/src/main/java/backend/pirocheck/config/WebConfig.java @@ -10,7 +10,11 @@ public class WebConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") // 백엔드 API 요청에만 CORS 허용 - .allowedOrigins("http://localhost:5173", "https://www.pirocheck.org") // 프론트 배포 URL + .allowedOrigins( + "http://localhost:5173", + "http://www.pirocheck.org", + "https://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/index.html b/frontend/index.html index 79c4701..eadd9cb 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2,9 +2,9 @@ - + - Vite + React + pirocheck
diff --git a/frontend/public/assets/img/edit.png b/frontend/public/assets/img/edit.png new file mode 100644 index 0000000..639f11c Binary files /dev/null and b/frontend/public/assets/img/edit.png differ diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index ed53cf3..10300fe 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -6,11 +6,15 @@ import Assignment from "./pages/generation/Assignment"; import Deposit from "./pages/generation/Deposit"; import Intro from "./Intro"; import Admin from "./pages/admin/Admin"; -import MagageStudent from "./pages/admin/ManageStudent.jsx"; +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"; function App() { return ( @@ -18,15 +22,94 @@ 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/Login.jsx b/frontend/src/Login.jsx index 58f63b1..4690e4d 100644 --- a/frontend/src/Login.jsx +++ b/frontend/src/Login.jsx @@ -57,7 +57,14 @@ const Login = () => { return (
-
+
{ + if (e.key === "Enter" && name && password) { + handleLogin(); + } + }} + >

PIROCHECK

{ placeholder: "비밀번호", }, ]} - values={[name, password]} // InputBlock props 수정에 따라 추가 + values={[name, password]} onChange={handleChange} />
diff --git a/frontend/src/api/adminassignment.js b/frontend/src/api/adminassignment.js new file mode 100644 index 0000000..6642f7e --- /dev/null +++ b/frontend/src/api/adminassignment.js @@ -0,0 +1,22 @@ +// src/api/assignmentAdmin.js +import api from "./api"; + +// 학생 정보 불러오기 +export const fetchStudentInfo = (studentId) => + api.get(`/admin/users/${studentId}`); + +// 주차별 과제 데이터 불러오기 +export const fetchStudentAssignments = (userId) => + api.get(`/api/assignment/${userId}`); // ← 수정됨 + +// 과제 상태 수정 (PUT) +export const updateAssignmentStatus = (userId, assignmentId, status) => + api.put(`/api/admin/users/${userId}/assignments/${assignmentId}/submission`, { + status, + }); + +// 과제 상태 등록 (POST) +export const submitAssignmentStatus = (userId, assignmentId, status) => + api.post(`/api/admin/users/${userId}/assignments/${assignmentId}/submission`, { + status, + }); 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 9ba4ee2..1950026 100644 --- a/frontend/src/api/api.js +++ b/frontend/src/api/api.js @@ -2,6 +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 new file mode 100644 index 0000000..3239a66 --- /dev/null +++ b/frontend/src/api/students.js @@ -0,0 +1,19 @@ +import api from "./api"; + +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 5c3501d..50af6bf 100644 --- a/frontend/src/components/Header.jsx +++ b/frontend/src/components/Header.jsx @@ -10,11 +10,16 @@ const Header = () => { if (path.includes("assignment")) title = "ASSIGNMENT\nCHECK"; else if (path.includes("deposit")) title = "DEPOSIT"; else if (path.includes("attendance")) title = "ATTENDANCE\nCHECK"; - else if (path.includes("magagestudent")) title = "수강생관리"; - else if (path.includes("magagetask")) title = "과제 관리"; + else if (path.includes("managestudent")) title = "수강생 관리"; + else if (path.includes("managetask")) title = "과제 관리"; else if (path.includes("attendancecode")) title = "출석코드 생성"; - const showRightDeposit = !path.includes("deposit"); + const showRightDeposit = + !path.includes("deposit") && + !path.includes("managestudent") && + !path.includes("managetask") && + !path.includes("attendancecode"); + const showRightMagageStudent = path.includes("attendancecode"); return ( @@ -45,9 +50,7 @@ const Header = () => { height={30} /> - ) : ( -
- )} + ) : null} {showRightMagageStudent ? ( +
+
+ + setTopic(e.target.value)} + /> + + setDay(e.target.value)} + /> + + {taskList.map((task, i) => ( + handleTaskChange(i, e.target.value)} + /> + ))} + +
+
+ +
+
+
+ ); +}; + +export default TaskModal; diff --git a/frontend/src/components/componentsCss/Header.css b/frontend/src/components/componentsCss/Header.css index 1c8b660..d36c784 100644 --- a/frontend/src/components/componentsCss/Header.css +++ b/frontend/src/components/componentsCss/Header.css @@ -5,9 +5,10 @@ display: flex; align-items: center; width: 390px; - justify-content: space-between; + justify-content: flex-start; padding: 1rem; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + gap: 100px; + box-shadow: 0 2px 4px #0003; margin: 10px; } diff --git a/frontend/src/pages/admin/Admin.jsx b/frontend/src/pages/admin/Admin.jsx index fa42daf..ddcbf85 100644 --- a/frontend/src/pages/admin/Admin.jsx +++ b/frontend/src/pages/admin/Admin.jsx @@ -9,13 +9,13 @@ const Admin = () => {

PIROCHECK

diff --git a/frontend/src/pages/admin/AdminStudentAssignment.jsx b/frontend/src/pages/admin/AdminStudentAssignment.jsx new file mode 100644 index 0000000..de0ff08 --- /dev/null +++ b/frontend/src/pages/admin/AdminStudentAssignment.jsx @@ -0,0 +1,161 @@ +import React, { useEffect, useState } from "react"; +import { useParams } from "react-router-dom"; +import AdminStudentHeader from "../../components/AdminStudentHeader"; +import WeeklyOpenBlock from "../../components/WeeklyOpenBlock"; +import AssignmentInfoBlock from "../../components/AssignmentInfoBlock"; +import api from "../../api/api"; +import styles from "./AdminStudentAssignment.module.css"; +import { + submitAssignmentStatus, + updateAssignmentStatus, +} from "../../api/assignment"; + +const AdminStudentAssignment = () => { + const { studentId, week } = useParams(); + const [studentInfo, setStudentInfo] = useState(null); + const [weeks, setWeeks] = useState([]); + const [highlightCard, setHighlightCard] = useState(null); + const [selectedWeekLabel, setSelectedWeekLabel] = useState(null); + + useEffect(() => { + // 기존 (오류 발생) api.get(`/admin/users/${userId}`).then((res) => { + api.get(`/admin/users/${studentId}`).then((res) => { + setStudentInfo(res.data.data); + }); + + api + .get(`/admin/managestudent/${studentId}`, { + params: { userId: studentId }, + withCredentials: true, + }) + .then((res) => { + const formatted = res.data.data.map((weekItem) => ({ + week: weekItem.week, + label: `${weekItem.week}주차 ${weekItem.subject}`, + days: weekItem.days.map((dayItem) => ({ + day: dayItem.day, + subject: weekItem.subject, + tasks: dayItem.details.map((task) => ({ + id: task.id, + label: task.assignmentName, + status: task.status, + modified: false, + })), + })), + })); + + setWeeks(formatted); + + const matched = formatted.find((w) => String(w.week) === String(week)); + if (matched) { + setSelectedWeekLabel(matched.label); + if (matched.days.length > 0) { + setHighlightCard({ + weekLabel: matched.label, + day: matched.days[0].day, + tasks: matched.days[0].tasks, + }); + } + } + }); + }, [studentId, week]); + + const handleStatusChange = (weekIdx, dayIdx, taskIdx, newStatus) => { + const updated = [...weeks]; + const task = updated[weekIdx].days[dayIdx].tasks[taskIdx]; + task.status = newStatus; + task.modified = true; + setWeeks(updated); + }; + /* + const handleSave = async (taskId, status) => { + await api.put("/admin/assignment/status", { + assignmentId: taskId, + status, + }); + }; +*/ + const handleSave = async (taskId, status) => { + const userId = parseInt(studentId); // 문자열일 수 있으니 숫자로 변환 + + try { + // PUT 요청 시도 (기존 과제 수정) + await updateAssignmentStatus(userId, taskId, status); + alert("과제 상태가 수정되었습니다."); + } catch (err) { + console.warn("PUT 실패, POST 시도"); + try { + // 없으면 POST 요청 (새 과제 등록) + await submitAssignmentStatus(userId, taskId, status); + alert("과제 상태가 등록되었습니다."); + } catch (err) { + alert("상태 저장 실패"); + console.error(err); + } + } + }; + + return ( +
+ window.history.back()} + /> + + {highlightCard && ( +
+ +
+ )} + +
+ {weeks.map((weekItem, weekIdx) => ( +
+

{weekItem.label}

+ {weekItem.days.map((dayItem, dayIdx) => ( +
+

+ {dayItem.day}   {dayItem.subject} +

+
+ {dayItem.tasks.map((task, taskIdx) => ( +
+ {task.label} + + +
+ ))} +
+ +
+ ))} +
+ ))} +
+
+ ); +}; + +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 79e0002..879f86f 100644 --- a/frontend/src/pages/admin/AttendanceCode.jsx +++ b/frontend/src/pages/admin/AttendanceCode.jsx @@ -1,15 +1,35 @@ -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 { - const res = await api.post("/attendance/start"); + const res = await api.post("admin/attendance/start"); const newCode = res.data.data.code; setCode(newCode); } catch (error) { @@ -19,10 +39,12 @@ const AttendanceCode = () => { } }; - // 출석코드 만료 + // 출석코드 만료 (직접 코드 전달 방식) const expireCode = async () => { try { - const res = await api.put("/attendance/expire-latest"); + const res = await api.put("admin/attendance/expire", null, { + params: { code }, + }); alert(res.data.message || "출석코드가 만료되었습니다"); setCode(""); } catch (error) { diff --git a/frontend/src/pages/admin/AttendanceCode.module.css b/frontend/src/pages/admin/AttendanceCode.module.css index 09977f1..344ba58 100644 --- a/frontend/src/pages/admin/AttendanceCode.module.css +++ b/frontend/src/pages/admin/AttendanceCode.module.css @@ -36,4 +36,5 @@ justify-content: center; align-items: center; margin-top: 60px; + gap: 20px; } diff --git a/frontend/src/pages/admin/DetailManageStudent.jsx b/frontend/src/pages/admin/DetailManageStudent.jsx new file mode 100644 index 0000000..632aa59 --- /dev/null +++ b/frontend/src/pages/admin/DetailManageStudent.jsx @@ -0,0 +1,79 @@ +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); + console.log("API 응답 데이터:", data); // 확인 포인트 + 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 41f63e8..841c6bd 100644 --- a/frontend/src/pages/admin/ManageStudent.jsx +++ b/frontend/src/pages/admin/ManageStudent.jsx @@ -1,4 +1,95 @@ -const MagageStudent = () => { - return

수강생 관리

; +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"; +import style from "./ManageStudent.module.css"; + +const ManageStudent = () => { + const [studentName, setStudentName] = useState([""]); + const [page, setPage] = useState(1); + const [students, setStudents] = useState([]); // 서버 데이터 저장 + const navigate = useNavigate(); + + const studentsPerPage = 6; + + useEffect(() => { + const fetchStudents = async () => { + try { + const name = studentName[0] || ""; + const data = await getStudentsByName(name); + setStudents(data); + } catch (err) { + console.error("수강생 불러오기 실패:", err); + } + }; + + fetchStudents(); + }, [studentName]); + + const handleChange = (index, value) => { + const newNames = [...studentName]; + newNames[index] = value; + setStudentName(newNames); + setPage(1); // 검색 시 페이지 초기화 + }; + + const totalPages = Math.ceil(students.length / studentsPerPage); + const paginatedStudents = students.slice( + (page - 1) * studentsPerPage, + page * studentsPerPage + ); + + return ( +
+
+
+ +
+ {paginatedStudents.map((student, index) => { + console.log("student to show:", student); + return ( + + ); + })} +
+ + {students.length > studentsPerPage && ( +
+ + + {page} / {totalPages} + + +
+ )} +
+
+ ); }; -export default MagageStudent; +export default ManageStudent; diff --git a/frontend/src/pages/admin/ManageStudent.module.css b/frontend/src/pages/admin/ManageStudent.module.css new file mode 100644 index 0000000..3a900c7 --- /dev/null +++ b/frontend/src/pages/admin/ManageStudent.module.css @@ -0,0 +1,53 @@ +.managestudent_wrapper { + display: flex; + justify-content: center; + flex-direction: column; + align-items: center; +} +.student_list { + margin-top: 60px; + display: flex; + flex-direction: column; + gap: 20px; + width: 100%; +} +.under_header { + display: flex; + flex-direction: column; + align-items: center; +} +.student_button { + background-color: #333; + color: white; + padding: 15px; + border-radius: 8px; + border: 1px solid var(--fill-gray); + text-align: left; + font-size: 16px; + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; +} +.student_button:hover { + border: 1px solid #39ff14; +} +.pagination { + margin-top: 60px; + display: flex; + justify-content: center; + align-items: center; + gap: 12px; +} +.pagination button { + background-color: #111; + color: #39ff14; + padding: 8px 12px; + border: 1px solid #39ff14; + border-radius: 6px; + cursor: pointer; +} +.pagination button:disabled { + opacity: 0.5; + cursor: not-allowed; +} diff --git a/frontend/src/pages/admin/ManageTask.jsx b/frontend/src/pages/admin/ManageTask.jsx index 7140e6e..82075dc 100644 --- a/frontend/src/pages/admin/ManageTask.jsx +++ b/frontend/src/pages/admin/ManageTask.jsx @@ -1,4 +1,52 @@ -const MagageTask = () => { - return

과제 관리

; +import { useState } from "react"; +import Header from "../../components/Header"; +import style from "./ManageTask.module.css"; +import TaskModal from "../../components/TaskModal"; + +const weekData = [ + { week: "1주차", title: "Comming soon~", tasks: [] }, + { week: "2주차", title: "Comming soon~", tasks: [] }, + { week: "3주차", title: "Comming soon~", tasks: [] }, + { week: "4주차", title: "Comming soon~", tasks: [] }, + { week: "5주차", title: "Comming soon~", tasks: [] }, +]; + +const ManageTask = () => { + const [selectedWeekIndex, setSelectedWeekIndex] = useState(null); + const [showModal, setShowModal] = useState(false); + + const handleEditClick = (index) => { + setSelectedWeekIndex(index); + setShowModal(true); + }; + + const closeModal = () => setShowModal(false); + + return ( +
+
+
+ {weekData.map((week, index) => ( +
+ + edit handleEditClick(index)} + /> +
+ ))} +
+ {showModal && ( + + )} +
+ ); }; -export default MagageTask; +export default ManageTask; diff --git a/frontend/src/pages/admin/ManageTask.module.css b/frontend/src/pages/admin/ManageTask.module.css new file mode 100644 index 0000000..36906ff --- /dev/null +++ b/frontend/src/pages/admin/ManageTask.module.css @@ -0,0 +1,106 @@ +.managetask_wrapper { + display: flex; + justify-content: center; + flex-direction: column; + align-items: center; +} +.week_container { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; +} +.week_block { + display: flex; + align-items: center; + margin: 10px 0; + width: 100%; + justify-content: center; +} +.week_button { + background-color: #444; + color: white; + padding: 14px 16px; + border: none; + border-radius: 8px; + min-width: 250px; + margin-right: 8px; + font-size: 16px; + display: flex; + justify-content: flex-start; +} +.edit_icon { + width: 20px; + height: 20px; + cursor: pointer; +} +.modal_overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.6); + display: flex; + justify-content: center; + align-items: center; +} +.modal { + background-color: #333; + border: 1px solid #4fff24; + border-radius: 12px; + padding: 20px; + width: 300px; + color: white; + display: flex; + flex-direction: column; + gap: 10px; + align-items: center; +} +.modal_header { + display: flex; + justify-content: space-between; + align-items: center; + position: relative; +} +.close_button { + background: none; + border: none; + color: white; + font-size: 20px; + cursor: pointer; + position: absolute; + left: 130px; +} +.modal_body { + width: 90%; +} +.modal_body label { + margin-block: 10px; + display: block; +} +.modal_body input { + width: 100%; + padding: 6px; + margin-bottom: 8px; + background-color: #555; + border: none; + border-radius: 4px; + color: white; +} +.add_button { + background: none; + border: none; + font-size: 24px; + color: white; + cursor: pointer; +} +.save_button { + margin-top: 10px; + padding: 8px 16px; + background-color: #666; + border: none; + border-radius: 6px; + color: white; + cursor: not-allowed; +} diff --git a/frontend/src/pages/generation/Assignment.jsx b/frontend/src/pages/generation/Assignment.jsx index d1c0115..095d5fc 100644 --- a/frontend/src/pages/generation/Assignment.jsx +++ b/frontend/src/pages/generation/Assignment.jsx @@ -19,10 +19,10 @@ const Assignment = () => { fetchAssignmentsByUser(userId) .then((weekData) => { const formatted = weekData.map((weekItem) => ({ - label: `${weekItem.week}주차 ${weekItem.title}`, + label: `${weekItem.week}주차 ${weekItem.subject}`, details: weekItem.days.map((dayItem) => ({ day: dayItem.day, - subject: weekItem.title, + subject: weekItem.subject, tasks: dayItem.details.map((task) => ({ label: task.assignmentName, status: mapStatus(task.status), 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", { diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 6b46dee..26f670c 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -7,7 +7,7 @@ export default defineConfig({ server: { proxy: { "/api": { - target: "http://localhost:8080", + target: "http://api.pirocheck.org:8080/api", changeOrigin: true, }, },