diff --git a/backend/src/main/java/org/example/backend/domain/notification/controller/NotificationController.java b/backend/src/main/java/org/example/backend/domain/notification/controller/NotificationController.java index d120e4bf..2ea10cea 100644 --- a/backend/src/main/java/org/example/backend/domain/notification/controller/NotificationController.java +++ b/backend/src/main/java/org/example/backend/domain/notification/controller/NotificationController.java @@ -1,4 +1,31 @@ package org.example.backend.domain.notification.controller; +import lombok.RequiredArgsConstructor; +import org.example.backend.domain.notification.dto.response.NotificationResponseDTO; +import org.example.backend.domain.notification.service.NotificationService; +import org.example.backend.global.ApiResponse; +import org.example.backend.global.security.auth.CustomSecurityUtil; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.UUID; + +@RestController +@RequestMapping("/api/notifications") +@RequiredArgsConstructor +// 알림 목록 조회 public class NotificationController { + private final NotificationService notificationService; + private final CustomSecurityUtil customSecurityUtil; + + @GetMapping("") + public ApiResponse> getNotifications() { + UUID userId = customSecurityUtil.getUserId(); + + + List notifications = notificationService.getNotificationsByUserId(userId); + return ApiResponse.onSuccess(notifications); + } } diff --git a/backend/src/main/java/org/example/backend/domain/notification/converter/NotificationConverter.java b/backend/src/main/java/org/example/backend/domain/notification/converter/NotificationConverter.java new file mode 100644 index 00000000..32854970 --- /dev/null +++ b/backend/src/main/java/org/example/backend/domain/notification/converter/NotificationConverter.java @@ -0,0 +1,31 @@ +package org.example.backend.domain.notification.converter; + +import lombok.RequiredArgsConstructor; +import org.example.backend.domain.notification.dto.response.NotificationResponseDTO; +import org.example.backend.domain.notification.entity.Notification; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +@Component +@RequiredArgsConstructor +public class NotificationConverter { + + public NotificationResponseDTO toDTO(Notification notification, String className) { + return NotificationResponseDTO.builder() + .notificationId(notification.getId()) + .className(className) + .alarmType(notification.getAlarmType()) + .isRead(notification.isRead()) + .createdAt(notification.getCreatedAt()) + .build(); + } + + public List toResponseDTO(List notifications, Map classNameMap) { + return notifications.stream() + .map(notification -> toDTO(notification, classNameMap.get(notification.getLecture().getId()))) + .toList(); + } +} diff --git a/backend/src/main/java/org/example/backend/domain/notification/dto/response/NotificationResponseDTO.java b/backend/src/main/java/org/example/backend/domain/notification/dto/response/NotificationResponseDTO.java new file mode 100644 index 00000000..9d26f6a3 --- /dev/null +++ b/backend/src/main/java/org/example/backend/domain/notification/dto/response/NotificationResponseDTO.java @@ -0,0 +1,24 @@ +package org.example.backend.domain.notification.dto.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.example.backend.domain.notification.entity.AlarmType; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class NotificationResponseDTO { + private UUID notificationId; + private String className; + private AlarmType alarmType; + @JsonProperty("isRead") + private boolean isRead; + private LocalDateTime createdAt; +} diff --git a/backend/src/main/java/org/example/backend/domain/notification/entity/Notification.java b/backend/src/main/java/org/example/backend/domain/notification/entity/Notification.java index 229315d4..28d54d1f 100644 --- a/backend/src/main/java/org/example/backend/domain/notification/entity/Notification.java +++ b/backend/src/main/java/org/example/backend/domain/notification/entity/Notification.java @@ -1,5 +1,6 @@ package org.example.backend.domain.notification.entity; +import com.fasterxml.jackson.annotation.JsonProperty; import jakarta.persistence.*; import lombok.*; import org.example.backend.domain.lecture.entity.Lecture; @@ -33,6 +34,7 @@ public class Notification extends BaseEntity { @Column(name = "alarm_type", nullable = false) private AlarmType alarmType; + @JsonProperty("isRead") @Column(name = "is_read", nullable = false) private boolean isRead; diff --git a/backend/src/main/java/org/example/backend/domain/notification/repository/NotificationRepository.java b/backend/src/main/java/org/example/backend/domain/notification/repository/NotificationRepository.java index 92f07dcd..3b324036 100644 --- a/backend/src/main/java/org/example/backend/domain/notification/repository/NotificationRepository.java +++ b/backend/src/main/java/org/example/backend/domain/notification/repository/NotificationRepository.java @@ -1,4 +1,14 @@ package org.example.backend.domain.notification.repository; -public interface NotificationRepository { +import org.example.backend.domain.classroom.entity.Classroom; +import org.example.backend.domain.notification.entity.Notification; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.UUID; + +@Repository +public interface NotificationRepository extends JpaRepository { + List findByUserIdOrderByCreatedAtDesc(UUID userId); } diff --git a/backend/src/main/java/org/example/backend/domain/notification/service/NotificationService.java b/backend/src/main/java/org/example/backend/domain/notification/service/NotificationService.java index bc024b20..8fd9e294 100644 --- a/backend/src/main/java/org/example/backend/domain/notification/service/NotificationService.java +++ b/backend/src/main/java/org/example/backend/domain/notification/service/NotificationService.java @@ -1,4 +1,42 @@ package org.example.backend.domain.notification.service; -public class NotificationService { +import lombok.RequiredArgsConstructor; +import org.example.backend.domain.classroom.entity.Classroom; +import org.example.backend.domain.classroom.repository.ClassroomRepository; +import org.example.backend.domain.lecture.entity.Lecture; +import org.example.backend.domain.lecture.repository.LectureRepository; +import org.example.backend.domain.notification.converter.NotificationConverter; +import org.example.backend.domain.notification.dto.response.NotificationResponseDTO; +import org.example.backend.domain.notification.entity.Notification; +import org.example.backend.domain.notification.repository.NotificationRepository; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.UUID; + +@RequiredArgsConstructor +@Service +public class NotificationService implements NotificationServiceImpl{ + private final NotificationRepository notificationRepository; + private final LectureRepository lectureRepository; + private final ClassroomRepository classroomRepository; + private final NotificationConverter notificationConverter;; + + public List getNotificationsByUserId(UUID userId) { + List notificationList = + notificationRepository.findByUserIdOrderByCreatedAtDesc(userId); + + return notificationList.stream() + .map(notification -> { + Lecture lecture = notification.getLecture(); + + String className = null; + if (lecture != null && lecture.getClassroom() != null) { + className = lecture.getClassroom().getClassName(); + } + + return notificationConverter.toDTO(notification, className); + }) + .toList(); + } } diff --git a/backend/src/main/java/org/example/backend/domain/notification/service/NotificationServiceImpl.java b/backend/src/main/java/org/example/backend/domain/notification/service/NotificationServiceImpl.java index 88add3e4..d87d0aa9 100644 --- a/backend/src/main/java/org/example/backend/domain/notification/service/NotificationServiceImpl.java +++ b/backend/src/main/java/org/example/backend/domain/notification/service/NotificationServiceImpl.java @@ -1,4 +1,7 @@ package org.example.backend.domain.notification.service; +import org.springframework.stereotype.Service; + +@Service public interface NotificationServiceImpl { } diff --git a/backend/src/main/java/org/example/backend/domain/notificationSetting/controller/NotificationSettingController.java b/backend/src/main/java/org/example/backend/domain/notificationSetting/controller/NotificationSettingController.java index 9d3b5699..337c89c1 100644 --- a/backend/src/main/java/org/example/backend/domain/notificationSetting/controller/NotificationSettingController.java +++ b/backend/src/main/java/org/example/backend/domain/notificationSetting/controller/NotificationSettingController.java @@ -1,4 +1,35 @@ package org.example.backend.domain.notificationSetting.controller; +import lombok.RequiredArgsConstructor; +import org.example.backend.domain.notificationSetting.dto.request.NotificationSettingPatchRequest; +import org.example.backend.domain.notificationSetting.dto.response.NotificationSettingResponseDTO; +import org.example.backend.domain.notificationSetting.service.NotificationSettingService; +import org.example.backend.global.ApiResponse; +import org.example.backend.global.security.auth.CustomSecurityUtil; +import org.springframework.web.bind.annotation.*; + +import java.util.UUID; + +@RestController +@RequestMapping("/api/notifications/setting") +@RequiredArgsConstructor public class NotificationSettingController { + private final NotificationSettingService notificationSettingService; + private final CustomSecurityUtil customSecurityUtil; + + @GetMapping("") + public ApiResponse getNotiSetting(){ + UUID userId = customSecurityUtil.getUserId(); + + NotificationSettingResponseDTO response = notificationSettingService.getNotiSetting(userId); + return ApiResponse.onSuccess(response); + + } + + @PatchMapping("") + public ApiResponse patchSettings(@RequestBody NotificationSettingPatchRequest req) { + UUID userId = customSecurityUtil.getUserId(); + notificationSettingService.patchSettings(userId, req); + return ApiResponse.onSuccess(null); + } } diff --git a/backend/src/main/java/org/example/backend/domain/notificationSetting/converter/NotificationSettingConverter.java b/backend/src/main/java/org/example/backend/domain/notificationSetting/converter/NotificationSettingConverter.java new file mode 100644 index 00000000..04fc13b6 --- /dev/null +++ b/backend/src/main/java/org/example/backend/domain/notificationSetting/converter/NotificationSettingConverter.java @@ -0,0 +1,16 @@ +package org.example.backend.domain.notificationSetting.converter; + +import org.example.backend.domain.notificationSetting.dto.response.NotificationSettingResponseDTO; +import org.example.backend.domain.notificationSetting.entity.NotificationSetting; + +public class NotificationSettingConverter { + public static NotificationSettingResponseDTO toDTO(NotificationSetting setting) { + return NotificationSettingResponseDTO.builder() + .quizUpload(setting.isQuizUpload()) + .quizAnswerUpload(setting.isQuizAnswerUpload()) + .lectureNoteUpload(setting.isLectureNoteUpload()) + .lectureUpload(setting.isLectureUpload()) + .recordUpload(setting.isRecordUpload()) + .build(); + } +} \ No newline at end of file diff --git a/backend/src/main/java/org/example/backend/domain/notificationSetting/dto/request/NotificationSettingPatchRequest.java b/backend/src/main/java/org/example/backend/domain/notificationSetting/dto/request/NotificationSettingPatchRequest.java new file mode 100644 index 00000000..38735c1a --- /dev/null +++ b/backend/src/main/java/org/example/backend/domain/notificationSetting/dto/request/NotificationSettingPatchRequest.java @@ -0,0 +1,9 @@ +package org.example.backend.domain.notificationSetting.dto.request; + +public record NotificationSettingPatchRequest( + Boolean quizUpload, + Boolean quizAnswerUpload, + Boolean lectureNoteUpload, + Boolean lectureUpload, + Boolean recordUpload +) {} \ No newline at end of file diff --git a/backend/src/main/java/org/example/backend/domain/notificationSetting/dto/response/NotificationSettingResponseDTO.java b/backend/src/main/java/org/example/backend/domain/notificationSetting/dto/response/NotificationSettingResponseDTO.java new file mode 100644 index 00000000..5fb4ae98 --- /dev/null +++ b/backend/src/main/java/org/example/backend/domain/notificationSetting/dto/response/NotificationSettingResponseDTO.java @@ -0,0 +1,19 @@ +package org.example.backend.domain.notificationSetting.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class NotificationSettingResponseDTO { + + private boolean quizUpload; + private boolean quizAnswerUpload; + private boolean lectureNoteUpload; + private boolean lectureUpload; + private boolean recordUpload; +} \ No newline at end of file diff --git a/backend/src/main/java/org/example/backend/domain/notificationSetting/entity/NotificationSetting.java b/backend/src/main/java/org/example/backend/domain/notificationSetting/entity/NotificationSetting.java index 7baf9475..99683732 100644 --- a/backend/src/main/java/org/example/backend/domain/notificationSetting/entity/NotificationSetting.java +++ b/backend/src/main/java/org/example/backend/domain/notificationSetting/entity/NotificationSetting.java @@ -16,6 +16,9 @@ public class NotificationSetting extends BaseEntity { @Column(name = "user_id") private String userId; + @Column(name = "token", nullable = false, unique = true, length = 512) + private String token; + @Column(name = "quiz_upload", nullable = false) @Builder.Default private boolean quizUpload = true; diff --git a/backend/src/main/java/org/example/backend/domain/notificationSetting/repository/NotificationSettingRepository.java b/backend/src/main/java/org/example/backend/domain/notificationSetting/repository/NotificationSettingRepository.java index 14cfe024..fb989cf5 100644 --- a/backend/src/main/java/org/example/backend/domain/notificationSetting/repository/NotificationSettingRepository.java +++ b/backend/src/main/java/org/example/backend/domain/notificationSetting/repository/NotificationSettingRepository.java @@ -1,4 +1,7 @@ package org.example.backend.domain.notificationSetting.repository; -public interface NotificationSettingRepository { +import org.example.backend.domain.notificationSetting.entity.NotificationSetting; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface NotificationSettingRepository extends JpaRepository { } diff --git a/backend/src/main/java/org/example/backend/domain/notificationSetting/service/NotificationSettingService.java b/backend/src/main/java/org/example/backend/domain/notificationSetting/service/NotificationSettingService.java index d47d982a..d263b4fc 100644 --- a/backend/src/main/java/org/example/backend/domain/notificationSetting/service/NotificationSettingService.java +++ b/backend/src/main/java/org/example/backend/domain/notificationSetting/service/NotificationSettingService.java @@ -1,4 +1,44 @@ package org.example.backend.domain.notificationSetting.service; -public class NotificationSettingService { +import lombok.RequiredArgsConstructor; +import org.example.backend.domain.notificationSetting.converter.NotificationSettingConverter; +import org.example.backend.domain.notificationSetting.dto.request.NotificationSettingPatchRequest; +import org.example.backend.domain.notificationSetting.dto.response.NotificationSettingResponseDTO; +import org.example.backend.domain.notificationSetting.entity.NotificationSetting; +import org.example.backend.domain.notificationSetting.repository.NotificationSettingRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class NotificationSettingService implements NotificationSettingServiceImpl{ + private final NotificationSettingRepository notificationSettingRepository; + + @Override + public NotificationSettingResponseDTO getNotiSetting(UUID userId) { + NotificationSetting setting = notificationSettingRepository.findById(userId.toString()) + .orElseGet(() -> NotificationSetting.builder() + .userId(userId.toString()) + .quizUpload(true) + .quizAnswerUpload(true) + .lectureNoteUpload(true) + .lectureUpload(true) + .recordUpload(true) + .build()); + + return NotificationSettingConverter.toDTO(setting); + } + + @Transactional + public void patchSettings(UUID userId, NotificationSettingPatchRequest req) { + NotificationSetting entity = notificationSettingRepository.findById(userId.toString()) + .orElseThrow(() -> new IllegalArgumentException("알림 설정이 존재하지 않습니다.")); + + if (req.quizUpload() != null) entity.setQuizUpload(req.quizUpload()); + if (req.quizAnswerUpload() != null) entity.setQuizAnswerUpload(req.quizAnswerUpload()); + if (req.lectureNoteUpload() != null) entity.setLectureNoteUpload(req.lectureNoteUpload()); + if (req.lectureUpload() != null) entity.setLectureUpload(req.lectureUpload()); + if (req.recordUpload() != null) entity.setRecordUpload(req.recordUpload()); + } } diff --git a/backend/src/main/java/org/example/backend/domain/notificationSetting/service/NotificationSettingServiceImpl.java b/backend/src/main/java/org/example/backend/domain/notificationSetting/service/NotificationSettingServiceImpl.java index 35989150..1f5923a4 100644 --- a/backend/src/main/java/org/example/backend/domain/notificationSetting/service/NotificationSettingServiceImpl.java +++ b/backend/src/main/java/org/example/backend/domain/notificationSetting/service/NotificationSettingServiceImpl.java @@ -1,4 +1,12 @@ package org.example.backend.domain.notificationSetting.service; +import org.example.backend.domain.notificationSetting.dto.request.NotificationSettingPatchRequest; +import org.example.backend.domain.notificationSetting.dto.response.NotificationSettingResponseDTO; + +import java.util.UUID; + public interface NotificationSettingServiceImpl { + NotificationSettingResponseDTO getNotiSetting(UUID userId); + + void patchSettings(UUID userId, NotificationSettingPatchRequest req); } diff --git a/frontend/api/notifications/fetchNotification.ts b/frontend/api/notifications/fetchNotification.ts new file mode 100644 index 00000000..db195cab --- /dev/null +++ b/frontend/api/notifications/fetchNotification.ts @@ -0,0 +1,49 @@ +import { axiosInstance } from "@/api/axiosInstance"; +import axios from "axios"; +import { ENDPOINTS } from "@/constants/endpoints"; +import { ApiResponse } from "@/types/apiResponseTypes"; + +export interface NotificationResponse { + notificationId: string; + className: string | null; + alarmType: string; + isRead: boolean; + createdAt: string; +} + +const alarmTypeLabels: Record = { + quizUpload: "새 퀴즈가 업로드되었습니다", + quizAnswerUpload: "퀴즈 답안이 업로드되었습니다", + lectureNoteUpload: "새 강의자료가 올라왔습니다", + startLecture: "강의가 시작되었습니다", + recordUpload: "녹음 파일이 업로드되었습니다", +}; + +export function getAlarmMessage(alarmType: string) { + return alarmTypeLabels[alarmType] ?? alarmType; +} + +// 알림 목록 조회 API +export async function fetchNotifications() { + try { + const response = await axiosInstance.get< + ApiResponse[]> + >(ENDPOINTS.NOTIFICATIONS.LIST); + + if (response.data.isSuccess && response.data.result) { + // alarmType → alarmMessage 변환 추가 + const mapped = response.data.result.map((n) => ({ + ...n, + alarmMessage: alarmTypeLabels[n.alarmType] ?? n.alarmType, + })); + return { ...response.data, result: mapped }; + } + + return response.data; + } catch (error: unknown) { + if (axios.isAxiosError(error) && error.response) { + return error.response.data as ApiResponse; + } + throw error; + } +} \ No newline at end of file diff --git a/frontend/api/notifications/fetchNotificationSetting.ts b/frontend/api/notifications/fetchNotificationSetting.ts new file mode 100644 index 00000000..32c8bd6b --- /dev/null +++ b/frontend/api/notifications/fetchNotificationSetting.ts @@ -0,0 +1,27 @@ +import { axiosInstance } from "@/api/axiosInstance"; +import axios from "axios"; +import { ENDPOINTS } from "@/constants/endpoints"; +import { ApiResponse } from "@/types/apiResponseTypes"; + +// 알림 설정 응답 타입 +export interface NotificationSettingResponse { + quizUpload: boolean; + quizAnswerUpload: boolean; + lectureNoteUpload: boolean; + lectureUpload: boolean; + recordUpload: boolean; +} + +export async function fetchNotificationSetting() { + try { + const response = await axiosInstance.get< + ApiResponse + >(ENDPOINTS.NOTIFICATIONS.GET_SETTINGS); + return response.data; + } catch (error: unknown) { + if (axios.isAxiosError(error) && error.response) { + return error.response.data as ApiResponse; + } + throw error; + } +} diff --git a/frontend/api/notifications/updateNotificationSetting.ts b/frontend/api/notifications/updateNotificationSetting.ts new file mode 100644 index 00000000..388e4d8f --- /dev/null +++ b/frontend/api/notifications/updateNotificationSetting.ts @@ -0,0 +1,20 @@ +import { axiosInstance } from "@/api/axiosInstance"; +import { ApiResponse } from "@/types/apiResponseTypes"; + +export type NotiSettingKey = + | "quizUpload" + | "quizAnswerUpload" + | "lectureNoteUpload" + | "lectureUpload" + | "recordUpload"; + +export async function updateNotificationSetting( + key: NotiSettingKey, + value: boolean +) { + const res = await axiosInstance.patch>( + "/api/notifications/setting", + { [key]: value } + ); + return res.data; +} \ No newline at end of file diff --git a/frontend/app/teacher/notification/page.module.scss b/frontend/app/teacher/notification/page.module.scss new file mode 100644 index 00000000..a06a4bd1 --- /dev/null +++ b/frontend/app/teacher/notification/page.module.scss @@ -0,0 +1,86 @@ +.container { + display: flex; + flex-direction: column; + align-items: center; + min-height: 100vh; + background-color: #f8f9fa; + width: 100%; + box-sizing: border-box; +} + +.header { + position: relative; + width: 100%; + max-width: 800px; + text-align: center; + font-size: 18px; + font-weight: bold; + color: #333; + padding: 40px 0; + height: auto; +} + +.backButton { + position: absolute; + left: 16px; + top: 50%; + transform: translateY(-50%); + background: none; + border: none; + padding: 0; + cursor: pointer; + color: #333; +} + +.divider { + width: 100%; + max-width: 800px; + border: 0; + border-top: 1px dashed #ccc; + margin-bottom: 15px; +} + +.notificationList { + list-style: none; + padding: 0; + margin: 0; + width: 100%; + max-width: 800px; +} + +.notificationItem { + display: flex; + align-items: center; + justify-content: space-between; + padding: 15px 0; + border-bottom: 1px solid #f0f0f0; + + &:last-child { + border-bottom: none; + } +} + +.title { + display: flex; + justify-content: space-between; + align-items: center; + flex-grow: 1; /* 남은 공간을 모두 차지 */ + font-size: 16px; + color: #555; + padding-right: 20px; /* newIndicator와의 간격 */ +} + +.dateTime { + font-size: 12px; + color: #999; + white-space: nowrap; + margin-left: 10px; +} + +.newIndicator { + width: 5px; + height: 5px; + background-color: #e53e3e; + border-radius: 50%; + flex-shrink: 0; +} \ No newline at end of file diff --git a/frontend/app/teacher/notification/page.tsx b/frontend/app/teacher/notification/page.tsx new file mode 100644 index 00000000..14b96274 --- /dev/null +++ b/frontend/app/teacher/notification/page.tsx @@ -0,0 +1,68 @@ +"use client"; + +import React, { useEffect, useState } from "react"; +import styles from "./page.module.scss"; +import { useRouter } from "next/navigation"; +import { ChevronLeft } from "lucide-react"; +import { fetchNotifications , NotificationResponse, getAlarmMessage } from "@/api/notifications/fetchNotification"; + +export default function TeacherNotificationPage() { + const router = useRouter(); + const [notifications, setNotifications] = useState([]); + + useEffect(() => { + const loadNotifications = async () => { + const res = await fetchNotifications(); + if (res.isSuccess && res.result) { + setNotifications(res.result); + } else { + console.error("알림 조회 실패:", res.message); + } + }; + loadNotifications(); + }, []); + + return ( +
+
+ + 알림 +
+ +
    + {notifications.map((notification) => { + const date = new Date(notification.createdAt); + const formattedDate = date.toLocaleDateString("ko-KR", { + year: "numeric", + month: "2-digit", + day: "2-digit", + }); + const formattedTime = date.toLocaleTimeString("ko-KR", { + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }); + + return ( +
  • +
    + + [{notification.className ?? "알 수 없음"}] {getAlarmMessage(notification.alarmType)} + + + {formattedDate} {formattedTime} + +
    +
    +
  • + ); + })} +
+
+ ); +} \ No newline at end of file diff --git a/frontend/app/teacher/setting/page.module.scss b/frontend/app/teacher/setting/page.module.scss index e69de29b..9bc57795 100644 --- a/frontend/app/teacher/setting/page.module.scss +++ b/frontend/app/teacher/setting/page.module.scss @@ -0,0 +1,188 @@ +.container { + display: flex; + flex-direction: column; + align-items: center; + height: 100vh; + background-color: #f8f9fa; + padding: 16px; +} + +.header { + position: relative; + width: 100%; + max-width: 800px; + padding: 40px 0; + text-align: center; + font-size: 18px; + font-weight: bold; + color: #333; +} + +.backButton { + position: absolute; + left: 16px; + top: 50%; + transform: translateY(-50%); + + background: none; + border: none; + padding: 0; + cursor: pointer; + + color: #333; +} + +.profileCard { + display: flex; + align-items: center; + width: 100%; + max-width: 800px; + background-color: #4a90e2; + border-radius: 12px; + padding: 16px; + color: white; + margin-bottom: 24px; + cursor: pointer; +} + +.avatar { + width: 50px; + height: 50px; + border-radius: 50%; + margin-right: 16px; +} + +.profileInfo { + flex-grow: 1; +} + +.name { + font-size: 16px; + font-weight: bold; + margin: 0; +} + +.role { + font-size: 14px; + margin: 4px 0 0 0; + opacity: 0.9; +} + +.menuList { + width: 100%; + max-width: 800px; + background-color: white; + border-radius: 12px; + overflow: hidden; +} + +.menuItem { + padding: 16px; + cursor: pointer; + font-size: 16px; + border-bottom: 1px solid #f1f1f1; + + &:last-child { + border-bottom: none; + } +} + +.menuItemWithArrow { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px; + cursor: pointer; + font-size: 16px; + border-bottom: 1px solid #f1f1f1; +} + +.arrow { + transition: transform 0.2s ease-in-out; +} + +.arrow.open { + transform: rotate(180deg); +} + +.toggleMenu { + padding: 10px 16px; + background-color: #f9f9f9; + border-bottom: 1px solid #f1f1f1; + display: block; + align-items: center; + justify-content: space-between; + font-size: 14px; +} + +.toggleMenu label { + margin-right: 10px; +} + +.toggleRow { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; // 항목 간 간격 +} + +.toggleSwitch { + position: relative; + display: inline-block; + width: 44px; + height: 24px; + + input { + opacity: 0; + width: 0; + height: 0; + } +} + +.slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + border-radius: 24px; + transition: 0.3s; + + &::before { + position: absolute; + content: ""; + height: 20px; + width: 20px; + left: 2px; + bottom: 2px; + background-color: white; + border-radius: 50%; + transition: 0.3s; + } +} + +.toggleSwitch input:checked + .slider { + background-color: #4a90e2; +} + +.toggleSwitch input:checked + .slider::before { + transform: translateX(20px); +} + + +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.slideDown { + animation: slideDown 0.3s ease-out forwards; +} \ No newline at end of file diff --git a/frontend/app/teacher/setting/page.tsx b/frontend/app/teacher/setting/page.tsx index 116269ac..4981cf8e 100644 --- a/frontend/app/teacher/setting/page.tsx +++ b/frontend/app/teacher/setting/page.tsx @@ -1,3 +1,214 @@ -export default function TeacherSettingPage() { - return
설정
; +"use client"; + +import React, { useState, useEffect } from 'react'; +import styles from './page.module.scss'; +import Image from "next/image"; +import { useRouter } from 'next/navigation'; +import { ChevronDown, ChevronRight, ChevronLeft } from 'lucide-react'; +import { ChangeEvent } from "react"; +import { fetchNotificationSetting } from '@/api/notifications/fetchNotificationSetting'; +import { updateNotificationSetting } from '@/api/notifications/updateNotificationSetting'; +import { getProfile } from '@/api/users/getProfile'; +import { GetProfileResult } from '@/types/users/getProfileTypes'; +import { IMAGES } from '@/constants/images'; + +interface NotiSetting { + quizUpload: boolean; + quizAnswerUpload: boolean; + lectureNoteUpload: boolean; + lectureUpload: boolean; + recordUpload: boolean; } + + +export default function TeacherSettingPage() { + const router = useRouter(); + const [isNotificationOpen, setIsNotificationOpen] = useState(false); + + const toggleNotification = () => { + setIsNotificationOpen(!isNotificationOpen); + }; + + const [notiSetting, setNotiSetting] = useState({ + quizUpload: false, + quizAnswerUpload: false, + lectureNoteUpload: false, + lectureUpload: false, + recordUpload: false, + }); + + const [userProfile, setUserProfile] = useState(null); + const [isLoadingProfile, setIsLoadingProfile] = useState(true); + + useEffect(() => { + const fetchUserProfile = async () => { + try { + setIsLoadingProfile(true); + const response = await getProfile(); + if (response.isSuccess && response.result) { + setUserProfile(response.result); + } + } catch (error) { + console.error("Failed to fetch user profile:", error); + } finally { + setIsLoadingProfile(false); + } + }; + + fetchUserProfile(); + }, []); + + const handleToggleChange = async (e: ChangeEvent) => { + const { name, checked } = e.target as HTMLInputElement; + + // 타입 안전을 위해 key 좁히기 + type NotiSettingKey = keyof NotiSetting; + const key = name as NotiSettingKey; + + setNotiSetting(prev => ({ ...prev, [key]: checked })); + + try { + const res = await updateNotificationSetting(key, checked); + if (!res.isSuccess) { + setNotiSetting(prev => ({ ...prev, [key]: !checked })); + console.error('알림 설정 저장 실패:', res.message); + alert(res.message ?? '알림 설정 저장에 실패했습니다.'); + } + } catch (err) { + // 4) 네트워크/예외 발생 시 롤백 + setNotiSetting(prev => ({ ...prev, [key]: !checked })); + console.error('알림 설정 저장 에러:', err); + alert('네트워크 오류로 알림 설정 저장에 실패했습니다.'); + } + }; + + useEffect(() => { + const fetchSetting = async () => { + try { + const res = await fetchNotificationSetting(); + if (res.isSuccess && res.result) { + setNotiSetting(res.result); + } else { + console.error('알림 설정 조회 실패', res.message); + } + } catch (error) { + console.error('알림 설정 요청 중 에러 발생', error); + } + }; + + fetchSetting(); + }, []); + + + return ( +
+
+ + 프로필 +
+
+ 프로필 사진 +
+

+ {isLoadingProfile ? "로딩 중..." : userProfile?.name ?? "사용자"} +

+

+ {isLoadingProfile ? "" : userProfile?.organization ?? "기관"} +

+
+ +
+ +
+
+ 알림 설정 + +
+ + {isNotificationOpen && ( +
+ +
+ 퀴즈 업로드 알림 + +
+ +
+ 퀴즈 답안 업로드 알림 + +
+ +
+ 강의노트 업로드 알림 + +
+ +
+ 강의 업로드 알림 + +
+ +
+ 녹음 업로드 알림 + +
+ +
+ )} + + +
+ 로그아웃 +
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/components/Header/Teacher/TeacherHeader.tsx b/frontend/components/Header/Teacher/TeacherHeader.tsx index 5481c656..a6d7bc89 100644 --- a/frontend/components/Header/Teacher/TeacherHeader.tsx +++ b/frontend/components/Header/Teacher/TeacherHeader.tsx @@ -85,6 +85,10 @@ const TeacherHeader: React.FC = ({ mode }) => { }; }, []); + const handleNotificationClick = () => { + router.push(ROUTES.teacherNotification); + } + const handleSettingsClick = () => { router.push(ROUTES.teacherSetting); setIsDropdownOpen(false); @@ -163,7 +167,7 @@ const TeacherHeader: React.FC = ({ mode }) => { )}
- +