From 2859d050796d028e8dfad36a88f2994b10542192 Mon Sep 17 00:00:00 2001 From: sewon Date: Tue, 26 Aug 2025 21:55:10 +0900 Subject: [PATCH 01/12] =?UTF-8?q?:sparkles:=20(#303)=20=EC=95=8C=EB=A6=BC?= =?UTF-8?q?=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20api=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20-=20title=20=EC=9D=B4=EB=9E=91=20contents=20?= =?UTF-8?q?=EC=B6=94=ED=9B=84=20=EC=B6=94=EA=B0=80=20=EC=98=88=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/NotificationController.java | 27 ++++++++++++++++++ .../converter/NotificationConverter.java | 28 +++++++++++++++++++ .../dto/response/NotificationResponseDTO.java | 21 ++++++++++++++ .../repository/NotificationRepository.java | 12 +++++++- .../service/NotificationService.java | 25 ++++++++++++++++- .../service/NotificationServiceImpl.java | 3 ++ 6 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 backend/src/main/java/org/example/backend/domain/notification/converter/NotificationConverter.java create mode 100644 backend/src/main/java/org/example/backend/domain/notification/dto/response/NotificationResponseDTO.java 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..e8901a8a --- /dev/null +++ b/backend/src/main/java/org/example/backend/domain/notification/converter/NotificationConverter.java @@ -0,0 +1,28 @@ +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; + +@Component +@RequiredArgsConstructor +public class NotificationConverter { + + public NotificationResponseDTO toDTO(Notification notification) { + return NotificationResponseDTO.builder() + .notificationId(notification.getId()) + .alarmType(notification.getAlarmType()) + .isRead(notification.isRead()) + .createdAt(notification.getCreatedAt()) + .build(); + } + + public List toResponseDTO(List notifications) { + return notifications.stream() + .map(this::toDTO) + .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..afdc9dab --- /dev/null +++ b/backend/src/main/java/org/example/backend/domain/notification/dto/response/NotificationResponseDTO.java @@ -0,0 +1,21 @@ +package org.example.backend.domain.notification.dto.response; + +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 AlarmType alarmType; + private boolean isRead; + private LocalDateTime createdAt; +} 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..14f8b4e1 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,27 @@ package org.example.backend.domain.notification.service; -public class NotificationService { +import lombok.RequiredArgsConstructor; +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 NotificationConverter notificationConverter; + + public List getNotificationsByUserId(UUID userId) { + + List notificationList = notificationRepository.findByUserIdOrderByCreatedAtDesc(userId); + return notificationList.stream() + .map(notificationConverter::toDTO) + .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 { } From c1ec1b655816f6368017ccef95cffa5f5fa70d29 Mon Sep 17 00:00:00 2001 From: sewon Date: Wed, 27 Aug 2025 21:45:29 +0900 Subject: [PATCH 02/12] =?UTF-8?q?:sparkles:=20(#303)=20=EC=95=8C=EB=A6=BC?= =?UTF-8?q?=20=EC=84=A4=EC=A0=95=20=EC=A1=B0=ED=9A=8C=20api=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NotificationSettingController.java | 25 ++++++++++++++++ .../NotificationSettingConverter.java | 16 ++++++++++ .../NotificationSettingResponseDTO.java | 19 ++++++++++++ .../entity/NotificationSetting.java | 3 ++ .../NotificationSettingRepository.java | 5 +++- .../service/NotificationSettingService.java | 29 ++++++++++++++++++- .../NotificationSettingServiceImpl.java | 5 ++++ 7 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 backend/src/main/java/org/example/backend/domain/notificationSetting/converter/NotificationSettingConverter.java create mode 100644 backend/src/main/java/org/example/backend/domain/notificationSetting/dto/response/NotificationSettingResponseDTO.java 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..2354fc1f 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,29 @@ package org.example.backend.domain.notificationSetting.controller; +import lombok.RequiredArgsConstructor; +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.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +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); + + } } 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/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..94de4f23 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,31 @@ 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.response.NotificationSettingResponseDTO; +import org.example.backend.domain.notificationSetting.entity.NotificationSetting; +import org.example.backend.domain.notificationSetting.repository.NotificationSettingRepository; +import org.springframework.stereotype.Service; + +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); + } } 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..4125d9ea 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,9 @@ package org.example.backend.domain.notificationSetting.service; +import org.example.backend.domain.notificationSetting.dto.response.NotificationSettingResponseDTO; + +import java.util.UUID; + public interface NotificationSettingServiceImpl { + NotificationSettingResponseDTO getNotiSetting(UUID userId); } From eae2af787078318a6f29173fc1ce63362f5f1b9e Mon Sep 17 00:00:00 2001 From: sewon Date: Fri, 15 Aug 2025 21:34:36 +0900 Subject: [PATCH 03/12] =?UTF-8?q?=E2=9C=A8=20(#281)=20=EC=84=A0=EC=83=9D?= =?UTF-8?q?=EB=8B=98=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=EC=9D=B4=EB=9E=91=20=EC=95=8C=EB=A6=BC=20=EB=B6=80=EB=B6=84=20?= =?UTF-8?q?=EB=A7=88=ED=81=AC=EC=97=85=20=ED=96=88=EC=8A=B5=EB=8B=88?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/teacher/notification/page.module.scss | 86 +++++++++ frontend/app/teacher/notification/page.tsx | 77 ++++++++ frontend/app/teacher/setting/page.module.scss | 181 ++++++++++++++++++ frontend/app/teacher/setting/page.tsx | 69 ++++++- .../Header/Teacher/TeacherHeader.tsx | 6 +- frontend/constants/routes.ts | 1 + 6 files changed, 417 insertions(+), 3 deletions(-) create mode 100644 frontend/app/teacher/notification/page.module.scss create mode 100644 frontend/app/teacher/notification/page.tsx 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..0e8abb87 --- /dev/null +++ b/frontend/app/teacher/notification/page.tsx @@ -0,0 +1,77 @@ +"use client"; + +import React from 'react'; +import styles from './page.module.scss'; +import { useRouter } from 'next/navigation'; +import { ChevronLeft } from 'lucide-react'; + +interface Notification { + id: number; + title: string; + date: string; + time: string; + isNew?: boolean; +} + +const notifications: Notification[] = [ + { + id: 1, + title: "[자료구조] 1차시 강의를 시작합니다", + date: "2025.03.25", + time: "16:20:00", + isNew: true, + }, + { + id: 2, + title: "[자료구조] 새 강의자료가 올라왔습니다", + date: "2025.03.25", + time: "16:20:00", + }, + { + id: 3, + title: "[자료구조] 새 강의자료가 올라왔습니다", + date: "2025.03.25", + time: "16:20:00", + }, + { + id: 4, + title: "[자료구조] 새 강의자료가 올라왔습니다", + date: "2025.03.25", + time: "16:20:00", + }, + { + id: 5, + title: "[자료구조] 1차시 강의를 시작합니다", + date: "2025.03.25", + time: "16:20:00", + isNew: true, + }, +]; + +export default function TeacherNotificationPage() { + const router = useRouter(); + + return ( +
+
+ + 알림
+
    + {notifications.map((notification) => ( +
  • +
    + {notification.title} + + + {notification.date} {notification.time} + +
    + {notification.isNew &&
    } +
  • + ))} +
+
+ ); +} \ 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..1d88ff3d 100644 --- a/frontend/app/teacher/setting/page.module.scss +++ b/frontend/app/teacher/setting/page.module.scss @@ -0,0 +1,181 @@ +.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: flex; + align-items: center; + justify-content: space-between; + font-size: 14px; +} + +.toggleMenu label { + margin-right: 10px; +} + +.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..ad23f438 100644 --- a/frontend/app/teacher/setting/page.tsx +++ b/frontend/app/teacher/setting/page.tsx @@ -1,3 +1,68 @@ +"use client"; + +import React, { useState } from 'react'; +import styles from './page.module.scss'; +import { useRouter } from 'next/navigation'; +import { ChevronDown, ChevronRight, ChevronLeft } from 'lucide-react'; + export default function TeacherSettingPage() { - return
설정
; -} + const router = useRouter(); + const [isNotificationOpen, setIsNotificationOpen] = useState(false); + const [isNotificationEnabled, setIsNotificationEnabled] = useState(false); + + const toggleNotification = () => { + setIsNotificationOpen(!isNotificationOpen); + }; + + const handleToggleChange = (event: React.ChangeEvent) => { + setIsNotificationEnabled(event.target.checked); + }; + + return ( +
+
+ + 프로필 +
+
+ 프로필 사진 +
+

손아현

+

학생

+
+ +
+ +
+
+ 알림 설정 + +
+ + {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 }) => { )}
- +
Date: Sat, 6 Sep 2025 17:11:45 +0900 Subject: [PATCH 04/12] =?UTF-8?q?:sparkles:=20(#303)=20=ED=94=84=EB=A1=A0?= =?UTF-8?q?=ED=8A=B8=20=EB=A7=88=ED=81=AC=EC=97=85=20=ED=95=A9=EC=B9=98?= =?UTF-8?q?=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notifications/fetchNotificationSetting.ts | 27 +++ frontend/app/teacher/setting/page.module.scss | 27 ++- frontend/app/teacher/setting/page.tsx | 217 +++++++++++++----- 3 files changed, 202 insertions(+), 69 deletions(-) create mode 100644 frontend/api/notifications/fetchNotificationSetting.ts 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/app/teacher/setting/page.module.scss b/frontend/app/teacher/setting/page.module.scss index 1d88ff3d..9bc57795 100644 --- a/frontend/app/teacher/setting/page.module.scss +++ b/frontend/app/teacher/setting/page.module.scss @@ -8,7 +8,7 @@ } .header { - position: relative; + position: relative; width: 100%; max-width: 800px; padding: 40px 0; @@ -20,15 +20,15 @@ .backButton { position: absolute; - left: 16px; + left: 16px; top: 50%; - transform: translateY(-50%); - + transform: translateY(-50%); + background: none; border: none; padding: 0; cursor: pointer; - + color: #333; } @@ -41,7 +41,7 @@ border-radius: 12px; padding: 16px; color: white; - margin-bottom: 24px; + margin-bottom: 24px; cursor: pointer; } @@ -102,14 +102,14 @@ } .arrow.open { - transform: rotate(180deg); + transform: rotate(180deg); } .toggleMenu { padding: 10px 16px; background-color: #f9f9f9; border-bottom: 1px solid #f1f1f1; - display: flex; + display: block; align-items: center; justify-content: space-between; font-size: 14px; @@ -119,12 +119,19 @@ 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; - + height: 24px; + input { opacity: 0; width: 0; diff --git a/frontend/app/teacher/setting/page.tsx b/frontend/app/teacher/setting/page.tsx index ad23f438..6d593c01 100644 --- a/frontend/app/teacher/setting/page.tsx +++ b/frontend/app/teacher/setting/page.tsx @@ -1,68 +1,167 @@ "use client"; -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import styles from './page.module.scss'; import { useRouter } from 'next/navigation'; -import { ChevronDown, ChevronRight, ChevronLeft } from 'lucide-react'; +import { ChevronDown, ChevronRight, ChevronLeft } from 'lucide-react'; +import { ChangeEvent } from "react"; +import { fetchNotificationSetting } from '@/api/notifications/fetchNotificationSetting'; + + +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 [isNotificationEnabled, setIsNotificationEnabled] = useState(false); - - const toggleNotification = () => { - setIsNotificationOpen(!isNotificationOpen); - }; - - const handleToggleChange = (event: React.ChangeEvent) => { - setIsNotificationEnabled(event.target.checked); - }; - - return ( -
-
- - 프로필 -
-
- 프로필 사진 -
-

손아현

-

학생

-
- -
+ const router = useRouter(); + const [isNotificationOpen, setIsNotificationOpen] = useState(false); -
-
- 알림 설정 - -
- - {isNotificationOpen && ( -
- - -
- )} - -
- 로그아웃 + const toggleNotification = () => { + setIsNotificationOpen(!isNotificationOpen); + }; + + const [notiSetting, setNotiSetting] = useState({ + quizUpload: false, + quizAnswerUpload: false, + lectureNoteUpload: false, + lectureUpload: false, + recordUpload: false, + }); + + const handleToggleChange = (e: ChangeEvent) => { + const { name, checked } = e.target; + setNotiSetting(prev => ({ + ...prev, + [name]: checked, + })); + }; + + 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 ( +
+
+ + 프로필 +
+
+ 프로필 사진 +
+

손아현

+

학생

+
+ +
+ +
+
+ 알림 설정 + +
+ + {isNotificationOpen && ( +
+ +
+ 퀴즈 업로드 알림 + +
+ +
+ 퀴즈 답안 업로드 알림 + +
+ +
+ 강의노트 업로드 알림 + +
+ +
+ 강의 업로드 알림 + +
+ +
+ 녹음 업로드 알림 + +
+ +
+ )} + + +
+ 로그아웃 +
+
-
-
- ); + ); } \ No newline at end of file From 2ddc375e24d13d8a37a750f22fa86a531b301912 Mon Sep 17 00:00:00 2001 From: sewon Date: Sat, 6 Sep 2025 17:48:32 +0900 Subject: [PATCH 05/12] =?UTF-8?q?:sparkles:=20(#303)=20=EC=95=8C=EB=A6=BC?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20=ED=94=84=EB=A1=A0=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../updateNotificationSetting.ts | 21 ++++++++++++ frontend/app/teacher/setting/page.tsx | 34 +++++++++++++++---- 2 files changed, 48 insertions(+), 7 deletions(-) create mode 100644 frontend/api/notifications/updateNotificationSetting.ts diff --git a/frontend/api/notifications/updateNotificationSetting.ts b/frontend/api/notifications/updateNotificationSetting.ts new file mode 100644 index 00000000..20a5cc22 --- /dev/null +++ b/frontend/api/notifications/updateNotificationSetting.ts @@ -0,0 +1,21 @@ +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 +) { + // 서버가 PATCH + partial body를 받도록 구현했다고 가정 + const res = await axiosInstance.patch>( + "/api/notifications/settings", + { [key]: value } + ); + return res.data; +} \ No newline at end of file diff --git a/frontend/app/teacher/setting/page.tsx b/frontend/app/teacher/setting/page.tsx index 6d593c01..0a403fa3 100644 --- a/frontend/app/teacher/setting/page.tsx +++ b/frontend/app/teacher/setting/page.tsx @@ -6,7 +6,7 @@ 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'; interface NotiSetting { quizUpload: boolean; @@ -33,12 +33,32 @@ export default function TeacherSettingPage() { recordUpload: false, }); - const handleToggleChange = (e: ChangeEvent) => { - const { name, checked } = e.target; - setNotiSetting(prev => ({ - ...prev, - [name]: checked, - })); + + const handleToggleChange = async (e: ChangeEvent) => { + const { name, checked } = e.target as HTMLInputElement; + + // 타입 안전을 위해 key 좁히기 + type NotiSettingKey = keyof NotiSetting; + const key = name as NotiSettingKey; + + // 1) 옵티미스틱 UI 반영 + setNotiSetting(prev => ({ ...prev, [key]: checked })); + + try { + // 2) 서버 반영 + const res = await updateNotificationSetting(key, checked); + if (!res.isSuccess) { + // 3) 서버에서 실패 응답이면 롤백 + 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(() => { From 01018e087f87a61c4347fe3397350426b1ca2437 Mon Sep 17 00:00:00 2001 From: sewon Date: Sun, 14 Sep 2025 15:37:47 +0900 Subject: [PATCH 06/12] =?UTF-8?q?:sparkles:=20(#303)=20=EC=95=8C=EB=A6=BC?= =?UTF-8?q?=20=ED=97=88=EC=9A=A9=20=EA=B0=92=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=B0=B1=EC=95=A4=EB=93=9C-Patch=20api=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/NotificationSettingController.java | 12 +++++++++--- .../request/NotificationSettingPatchRequest.java | 9 +++++++++ .../service/NotificationSettingService.java | 15 ++++++++++++++- .../service/NotificationSettingServiceImpl.java | 3 +++ 4 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 backend/src/main/java/org/example/backend/domain/notificationSetting/dto/request/NotificationSettingPatchRequest.java 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 2354fc1f..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,13 +1,12 @@ 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.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import java.util.UUID; @@ -26,4 +25,11 @@ public ApiResponse getNotiSetting(){ 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/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/service/NotificationSettingService.java b/backend/src/main/java/org/example/backend/domain/notificationSetting/service/NotificationSettingService.java index 94de4f23..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 @@ -2,11 +2,12 @@ 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 @@ -28,4 +29,16 @@ public NotificationSettingResponseDTO getNotiSetting(UUID userId) { 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 4125d9ea..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,9 +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); } From f822f8f161cc24335242924db142179290211401 Mon Sep 17 00:00:00 2001 From: sewon Date: Sun, 14 Sep 2025 16:00:06 +0900 Subject: [PATCH 07/12] =?UTF-8?q?:sparkles:=20(#303)=20=EC=95=8C=EB=A6=BC?= =?UTF-8?q?=20=ED=97=88=EC=9A=A9=20=EA=B0=92=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=ED=94=84=EB=A1=A0=ED=8A=B8=20api=20=EC=97=B0=EA=B2=B0=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/api/notifications/updateNotificationSetting.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/api/notifications/updateNotificationSetting.ts b/frontend/api/notifications/updateNotificationSetting.ts index 20a5cc22..388e4d8f 100644 --- a/frontend/api/notifications/updateNotificationSetting.ts +++ b/frontend/api/notifications/updateNotificationSetting.ts @@ -12,9 +12,8 @@ export async function updateNotificationSetting( key: NotiSettingKey, value: boolean ) { - // 서버가 PATCH + partial body를 받도록 구현했다고 가정 const res = await axiosInstance.patch>( - "/api/notifications/settings", + "/api/notifications/setting", { [key]: value } ); return res.data; From 662bf812bfa86ee69cfefb96c8583cc1f8a80501 Mon Sep 17 00:00:00 2001 From: sewon Date: Sun, 14 Sep 2025 16:00:16 +0900 Subject: [PATCH 08/12] =?UTF-8?q?:sparkles:=20(#303)=20=EC=95=8C=EB=A6=BC?= =?UTF-8?q?=20=ED=97=88=EC=9A=A9=20=EA=B0=92=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=ED=94=84=EB=A1=A0=ED=8A=B8=20api=20=EC=97=B0=EA=B2=B0=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/app/teacher/setting/page.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/frontend/app/teacher/setting/page.tsx b/frontend/app/teacher/setting/page.tsx index 0a403fa3..c27157ff 100644 --- a/frontend/app/teacher/setting/page.tsx +++ b/frontend/app/teacher/setting/page.tsx @@ -41,14 +41,11 @@ export default function TeacherSettingPage() { type NotiSettingKey = keyof NotiSetting; const key = name as NotiSettingKey; - // 1) 옵티미스틱 UI 반영 setNotiSetting(prev => ({ ...prev, [key]: checked })); try { - // 2) 서버 반영 const res = await updateNotificationSetting(key, checked); if (!res.isSuccess) { - // 3) 서버에서 실패 응답이면 롤백 setNotiSetting(prev => ({ ...prev, [key]: !checked })); console.error('알림 설정 저장 실패:', res.message); alert(res.message ?? '알림 설정 저장에 실패했습니다.'); From af8a0db5e71d5c81b3f0a3426e46a6b64fece5ae Mon Sep 17 00:00:00 2001 From: sewon Date: Sun, 14 Sep 2025 16:27:44 +0900 Subject: [PATCH 09/12] =?UTF-8?q?:sparkles:=20(#303)=20=EC=95=8C=EB=A6=BC?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=20className=20=ED=95=84=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../converter/NotificationConverter.java | 9 +++++--- .../dto/response/NotificationResponseDTO.java | 1 + .../service/NotificationService.java | 23 +++++++++++++++---- 3 files changed, 26 insertions(+), 7 deletions(-) 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 index e8901a8a..32854970 100644 --- 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 @@ -6,23 +6,26 @@ 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) { + 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) { + public List toResponseDTO(List notifications, Map classNameMap) { return notifications.stream() - .map(this::toDTO) + .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 index afdc9dab..28e68dbf 100644 --- 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 @@ -15,6 +15,7 @@ @NoArgsConstructor public class NotificationResponseDTO { private UUID notificationId; + private String className; private AlarmType alarmType; private boolean isRead; private LocalDateTime createdAt; 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 14f8b4e1..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,6 +1,10 @@ package org.example.backend.domain.notification.service; 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; @@ -14,14 +18,25 @@ @Service public class NotificationService implements NotificationServiceImpl{ private final NotificationRepository notificationRepository; - private final NotificationConverter notificationConverter; + private final LectureRepository lectureRepository; + private final ClassroomRepository classroomRepository; + private final NotificationConverter notificationConverter;; public List getNotificationsByUserId(UUID userId) { + List notificationList = + notificationRepository.findByUserIdOrderByCreatedAtDesc(userId); - List notificationList = notificationRepository.findByUserIdOrderByCreatedAtDesc(userId); return notificationList.stream() - .map(notificationConverter::toDTO) + .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(); } - } From dee494981b1e443dc4ffd4d3bdf61cc967e126a8 Mon Sep 17 00:00:00 2001 From: sewon Date: Sun, 14 Sep 2025 17:09:13 +0900 Subject: [PATCH 10/12] =?UTF-8?q?:sparkles:=20(#303)=20=EC=95=8C=EB=A6=BC?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=20=ED=95=84=EB=93=9C=20=EC=9D=B4=EB=A6=84?= =?UTF-8?q?=20@JsonProperty=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification/dto/response/NotificationResponseDTO.java | 2 ++ .../backend/domain/notification/entity/Notification.java | 2 ++ 2 files changed, 4 insertions(+) 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 index 28e68dbf..9d26f6a3 100644 --- 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 @@ -1,5 +1,6 @@ package org.example.backend.domain.notification.dto.response; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -17,6 +18,7 @@ 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; From f18c74f02d42cfcf27224f129a66fb850c5b3556 Mon Sep 17 00:00:00 2001 From: sewon Date: Sun, 14 Sep 2025 17:10:32 +0900 Subject: [PATCH 11/12] =?UTF-8?q?:sparkles:=20(#303)=20=EC=95=8C=EB=A6=BC?= =?UTF-8?q?=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20api=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/notifications/fetchNotification.ts | 49 +++++++ frontend/app/teacher/notification/page.tsx | 125 ++++++++---------- 2 files changed, 107 insertions(+), 67 deletions(-) create mode 100644 frontend/api/notifications/fetchNotification.ts 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/app/teacher/notification/page.tsx b/frontend/app/teacher/notification/page.tsx index 0e8abb87..14b96274 100644 --- a/frontend/app/teacher/notification/page.tsx +++ b/frontend/app/teacher/notification/page.tsx @@ -1,77 +1,68 @@ "use client"; -import React from 'react'; -import styles from './page.module.scss'; -import { useRouter } from 'next/navigation'; -import { ChevronLeft } from 'lucide-react'; - -interface Notification { - id: number; - title: string; - date: string; - time: string; - isNew?: boolean; -} - -const notifications: Notification[] = [ - { - id: 1, - title: "[자료구조] 1차시 강의를 시작합니다", - date: "2025.03.25", - time: "16:20:00", - isNew: true, - }, - { - id: 2, - title: "[자료구조] 새 강의자료가 올라왔습니다", - date: "2025.03.25", - time: "16:20:00", - }, - { - id: 3, - title: "[자료구조] 새 강의자료가 올라왔습니다", - date: "2025.03.25", - time: "16:20:00", - }, - { - id: 4, - title: "[자료구조] 새 강의자료가 올라왔습니다", - date: "2025.03.25", - time: "16:20:00", - }, - { - id: 5, - title: "[자료구조] 1차시 강의를 시작합니다", - date: "2025.03.25", - time: "16:20:00", - isNew: true, - }, -]; +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) => ( -
  • -
    - {notification.title} +
    +
    + + 알림 +
    + +
      + {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", + }); - - {notification.date} {notification.time} - -
    - {notification.isNew &&
    } -
  • - ))} -
-
+ return ( +
  • +
    + + [{notification.className ?? "알 수 없음"}] {getAlarmMessage(notification.alarmType)} + + + {formattedDate} {formattedTime} + +
    +
    +
  • + ); + })} + +
    ); } \ No newline at end of file From 777401e5459211a2e5802e71f4d7df6efb71232e Mon Sep 17 00:00:00 2001 From: sewon Date: Tue, 23 Sep 2025 17:23:12 +0900 Subject: [PATCH 12/12] =?UTF-8?q?(#303)=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EC=83=81=EC=9E=90=20=EB=B6=80=EB=B6=84=20api=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/app/teacher/setting/page.tsx | 44 ++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/frontend/app/teacher/setting/page.tsx b/frontend/app/teacher/setting/page.tsx index c27157ff..4981cf8e 100644 --- a/frontend/app/teacher/setting/page.tsx +++ b/frontend/app/teacher/setting/page.tsx @@ -2,11 +2,15 @@ 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; @@ -33,6 +37,26 @@ export default function TeacherSettingPage() { 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; @@ -80,27 +104,33 @@ export default function TeacherSettingPage() {
    프로필
    - 프로필 사진
    -

    손아현

    -

    학생

    +

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

    +

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

    - +
    알림 설정 - +
    {isNotificationOpen && (