-
Notifications
You must be signed in to change notification settings - Fork 1
feat: 프론트 미연동 API 구현 #119
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: 프론트 미연동 API 구현 #119
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| package com.project.dorumdorum.domain.notice.application.usecase; | ||
|
|
||
| import com.project.dorumdorum.domain.notice.application.dto.response.NoticeResponse; | ||
| import com.project.dorumdorum.domain.notice.domain.entity.Notice; | ||
| import com.project.dorumdorum.domain.notice.domain.repository.NoticeRepository; | ||
| import com.project.dorumdorum.global.exception.RestApiException; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| import static com.project.dorumdorum.global.exception.code.status.CommonErrorStatus._NOT_FOUND; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| public class LoadNoticeDetailUseCase { | ||
|
|
||
| private final NoticeRepository noticeRepository; | ||
|
|
||
| @Transactional(readOnly = true) | ||
| public NoticeResponse execute(String noticeNo) { | ||
| Notice notice = noticeRepository.findById(noticeNo) | ||
| .orElseThrow(() -> new RestApiException(_NOT_FOUND)); | ||
|
|
||
| return new NoticeResponse( | ||
| notice.getNoticeNo(), | ||
| notice.getTitle(), | ||
| notice.getContent(), | ||
| notice.getWrittenDate(), | ||
| notice.getOriginalLink() | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| package com.project.dorumdorum.domain.notice.ui; | ||
|
|
||
| import com.project.dorumdorum.domain.notice.application.dto.response.NoticeResponse; | ||
| import com.project.dorumdorum.domain.notice.application.usecase.LoadNoticeDetailUseCase; | ||
| import com.project.dorumdorum.domain.notice.ui.spec.LoadNoticeDetailApiSpec; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.web.bind.annotation.PathVariable; | ||
| import org.springframework.web.bind.annotation.RestController; | ||
|
|
||
| @RestController | ||
| @RequiredArgsConstructor | ||
| public class LoadNoticeDetailController implements LoadNoticeDetailApiSpec { | ||
|
|
||
| private final LoadNoticeDetailUseCase loadNoticeDetailUseCase; | ||
|
|
||
| @Override | ||
| public ResponseEntity<NoticeResponse> loadNotice(@PathVariable String noticeNo) { | ||
| return ResponseEntity.ok(loadNoticeDetailUseCase.execute(noticeNo)); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| package com.project.dorumdorum.domain.notice.ui.spec; | ||
|
|
||
| import com.project.dorumdorum.domain.notice.application.dto.response.NoticeResponse; | ||
| import io.swagger.v3.oas.annotations.Operation; | ||
| import io.swagger.v3.oas.annotations.tags.Tag; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.web.bind.annotation.GetMapping; | ||
| import org.springframework.web.bind.annotation.PathVariable; | ||
|
|
||
| @Tag(name = "Notice", description = "공지사항 API") | ||
| public interface LoadNoticeDetailApiSpec { | ||
|
|
||
| @Operation(summary = "공지사항 상세 조회", description = "공지사항을 단건 조회합니다.") | ||
| @GetMapping("/api/notices/{noticeNo}") | ||
| ResponseEntity<NoticeResponse> loadNotice(@PathVariable String noticeNo); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| package com.project.dorumdorum.domain.notification.application.dto.request; | ||
|
|
||
| public record NotificationSettingRequest( | ||
| boolean enabled, | ||
| boolean applicants, | ||
| boolean applicantResult, | ||
| boolean chat, | ||
| boolean notice, | ||
| boolean schedule | ||
| ) { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| package com.project.dorumdorum.domain.notification.application.dto.response; | ||
|
|
||
| import com.project.dorumdorum.domain.notification.domain.entity.NotificationSetting; | ||
|
|
||
| public record NotificationSettingResponse( | ||
| boolean enabled, | ||
| boolean applicants, | ||
| boolean applicantResult, | ||
| boolean chat, | ||
| boolean notice, | ||
| boolean schedule | ||
| ) { | ||
| public static NotificationSettingResponse from(NotificationSetting setting) { | ||
| return new NotificationSettingResponse( | ||
| setting.isEnabled(), | ||
| setting.isApplicants(), | ||
| setting.isApplicantResult(), | ||
| setting.isChat(), | ||
| setting.isNotice(), | ||
| setting.isSchedule() | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| package com.project.dorumdorum.domain.notification.application.usecase; | ||
|
|
||
| import com.project.dorumdorum.domain.notification.application.dto.response.NotificationSettingResponse; | ||
| import com.project.dorumdorum.domain.notification.domain.entity.NotificationSetting; | ||
| import com.project.dorumdorum.domain.notification.domain.repository.NotificationSettingRepository; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| public class LoadNotificationSettingUseCase { | ||
|
|
||
| private final NotificationSettingRepository notificationSettingRepository; | ||
|
|
||
| @Transactional(readOnly = true) | ||
| public NotificationSettingResponse execute(String userNo) { | ||
| NotificationSetting setting = notificationSettingRepository.findByUserNo(userNo) | ||
| .orElseGet(() -> NotificationSetting.defaultFor(userNo)); | ||
| return NotificationSettingResponse.from(setting); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| package com.project.dorumdorum.domain.notification.application.usecase; | ||
|
|
||
| import com.project.dorumdorum.domain.notification.application.dto.request.NotificationSettingRequest; | ||
| import com.project.dorumdorum.domain.notification.application.dto.response.NotificationSettingResponse; | ||
| import com.project.dorumdorum.domain.notification.domain.entity.NotificationSetting; | ||
| import com.project.dorumdorum.domain.notification.domain.repository.NotificationSettingRepository; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| public class UpdateNotificationSettingUseCase { | ||
|
|
||
| private final NotificationSettingRepository notificationSettingRepository; | ||
|
|
||
| @Transactional | ||
| public NotificationSettingResponse execute(String userNo, NotificationSettingRequest request) { | ||
| NotificationSetting setting = notificationSettingRepository.findByUserNo(userNo) | ||
| .orElseGet(() -> NotificationSetting.defaultFor(userNo)); | ||
| setting.update(request); | ||
| NotificationSetting saved = notificationSettingRepository.save(setting); | ||
| return NotificationSettingResponse.from(saved); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| package com.project.dorumdorum.domain.notification.domain.entity; | ||
|
|
||
| import com.project.dorumdorum.domain.notification.application.dto.request.NotificationSettingRequest; | ||
| import com.project.dorumdorum.global.common.BaseEntity; | ||
| import io.hypersistence.utils.hibernate.id.Tsid; | ||
| import jakarta.persistence.Column; | ||
| import jakarta.persistence.Entity; | ||
| import jakarta.persistence.Id; | ||
| import jakarta.persistence.Table; | ||
| import lombok.AccessLevel; | ||
| import lombok.AllArgsConstructor; | ||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| @Entity | ||
| @Getter | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| @AllArgsConstructor(access = AccessLevel.PROTECTED) | ||
| @Builder | ||
| @Table(name = "notification_settings") | ||
| public class NotificationSetting extends BaseEntity { | ||
|
|
||
| @Id | ||
| @Tsid | ||
| private String notificationSettingNo; | ||
|
|
||
| @Column(nullable = false, unique = true) | ||
| private String userNo; | ||
|
|
||
| @Column(nullable = false) | ||
| private boolean enabled; | ||
|
|
||
| @Column(nullable = false) | ||
| private boolean applicants; | ||
|
|
||
| @Column(nullable = false) | ||
| private boolean applicantResult; | ||
|
|
||
| @Column(nullable = false) | ||
| private boolean chat; | ||
|
|
||
| @Column(nullable = false) | ||
| private boolean notice; | ||
|
|
||
| @Column(nullable = false) | ||
| private boolean schedule; | ||
|
|
||
| public static NotificationSetting defaultFor(String userNo) { | ||
| return NotificationSetting.builder() | ||
| .userNo(userNo) | ||
| .enabled(true) | ||
| .applicants(true) | ||
| .applicantResult(true) | ||
| .chat(true) | ||
| .notice(true) | ||
| .schedule(false) | ||
| .build(); | ||
| } | ||
|
|
||
| public void update(NotificationSettingRequest request) { | ||
| this.enabled = request.enabled(); | ||
| this.applicants = request.applicants(); | ||
| this.applicantResult = request.applicantResult(); | ||
| this.chat = request.chat(); | ||
| this.notice = request.notice(); | ||
| this.schedule = request.schedule(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,10 +2,17 @@ | |
|
|
||
| import com.project.dorumdorum.domain.notification.domain.entity.Notification; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
| import org.springframework.data.jpa.repository.Modifying; | ||
| import org.springframework.data.jpa.repository.Query; | ||
| import org.springframework.data.repository.query.Param; | ||
|
|
||
| import java.util.Optional; | ||
|
|
||
| public interface NotificationRepository extends JpaRepository<Notification, String>, NotificationQueryRepository { | ||
|
|
||
| Optional<Notification> findByNotificationNoAndRecipientNo(String notificationNo, String recipientNo); | ||
|
|
||
| @Modifying(clearAutomatically = true, flushAutomatically = true) | ||
| @Query("UPDATE Notification n SET n.deletedAt = CURRENT_TIMESTAMP WHERE n.recipientNo = :userNo AND n.deletedAt IS NULL") | ||
| void deleteAllByRecipientNo(@Param("userNo") String userNo); | ||
|
Comment on lines
+15
to
+17
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
rg -n -C 8 \
'NotificationSettingRepository|notification_settings|deleteAllByUserNo|deleteAllByRecipientNo' \
src/main/java src/main/resourcesRepository: DorumDorum/BE Length of output: 22575 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '--- schema notification_settings ---\n'
sed -n '63,90p' src/main/resources/schema.sql
printf '\n--- entity NotificationSetting ---\n'
cat -n src/main/java/com/project/dorumdorum/domain/notification/domain/entity/NotificationSetting.java
printf '\n--- repository NotificationSettingRepository ---\n'
cat -n src/main/java/com/project/dorumdorum/domain/notification/domain/repository/NotificationSettingRepository.java
printf '\n--- DeleteAccountUseCase imports and body ---\n'
cat -n src/main/java/com/project/dorumdorum/domain/user/application/usecase/DeleteAccountUseCase.java | sed -n '1,70p'
printf '\n--- search for foreign key/cascade in JPA config and mapping files ---\n'
rg -n "`@JoinColumn`|`@ManyToOne`|onDelete|foreignKey|cascade" src/main/java src/main/resources --glob '!**/*.jar' | sed -n '1,220p'Repository: DorumDorum/BE Length of output: 242 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- schema notification_settings ---'
sed -n '63,90p' src/main/resources/schema.sql
printf '%s\n' ''
printf '%s\n' '--- entity NotificationSetting ---'
cat -n src/main/java/com/project/dorumdorum/domain/notification/domain/entity/NotificationSetting.java
printf '%s\n' ''
printf '%s\n' '--- repository NotificationSettingRepository ---'
cat -n src/main/java/com/project/dorumdorum/domain/notification/domain/repository/NotificationSettingRepository.java
printf '%s\n' ''
printf '%s\n' '--- DeleteAccountUseCase imports and body ---'
cat -n src/main/java/com/project/dorumdorum/domain/user/application/usecase/DeleteAccountUseCase.java | sed -n '1,70p'
printf '%s\n' ''
printf '%s\n' '--- relational mapping/cascade clues ---'
rg -n -C 3 '`@JoinColumn`|`@ManyToOne`|onDelete|`@JoinColumnOptions`|cascade|`@FieldDefault`|DEFAULT|notification_settings|user_no' src/main/java src/main/resources --glob '!**/*.jar' | sed -n '1,240p'
printf '%s\n' ''
printf '%s\n' '--- deterministic check: delete handlers for notification user owner column ---'
python3 - <<'PY'
from pathlib import Path
import re
files = {
'schema': Path('src/main/resources/schema.sql').read_text(),
'entity': Path('src/main/java/com/project/dorumdorum/domain/notification/domain/entity/NotificationSetting.java').read_text(),
'repo': Path('src/main/java/com/project/dorumdorum/domain/notification/domain/repository/NotificationSettingRepository.java').read_text(),
'delete': Path('src/main/java/com/project/dorumdorum/domain/user/application/usecase/DeleteAccountUseCase.java').read_text(),
}
checks = {
'notices_table_is_user_owned': bool(re.search(r'CREATE TABLE IF NOT EXISTS notification_settings[\s\S]*?user_no', files['schema'])),
'no_db_fk_with_on_delete_cascade_in_schema': not re.search(r'FOREIGN KEY[\s\S]*?notification_settings|ON DELETE CASCADE', files['schema']),
'setting_has_deletion_field_like_deleted_at': bool(re.search(r'(`@Column`|deleted_at|deletedAt)', files['entity'], re.I)),
'repository_has_soft_delete_or_delete_all_by_userNo': bool(re.search(r'deleteAll|DELETE|DELETE_ALL|deletedAt', files['repo'], re.I)),
'delete_account_calls_setting_delete': bool(re.search(r'NotificationSettingRepository|notificationSettingRepository|notification_settings', files['delete'], re.I)),
'jpa_reference_to_notification_setting_entity': bool(re.search(r'Table\(name = "notification_settings"\)|class NotificationSetting', files['entity'])),
}
for name, value in checks.items():
print(f'{name}: {value}')
PYRepository: DorumDorum/BE Length of output: 26625
🤖 Prompt for AI Agents |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| package com.project.dorumdorum.domain.notification.domain.repository; | ||
|
|
||
| import com.project.dorumdorum.domain.notification.domain.entity.NotificationSetting; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
|
|
||
| import java.util.Optional; | ||
|
|
||
| public interface NotificationSettingRepository extends JpaRepository<NotificationSetting, String> { | ||
| Optional<NotificationSetting> findByUserNo(String userNo); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| package com.project.dorumdorum.domain.notification.ui; | ||
|
|
||
| import com.project.dorumdorum.domain.notification.application.dto.request.NotificationSettingRequest; | ||
| import com.project.dorumdorum.domain.notification.application.dto.response.NotificationSettingResponse; | ||
| import com.project.dorumdorum.domain.notification.application.usecase.LoadNotificationSettingUseCase; | ||
| import com.project.dorumdorum.domain.notification.application.usecase.UpdateNotificationSettingUseCase; | ||
| import com.project.dorumdorum.domain.notification.ui.spec.NotificationSettingApiSpec; | ||
| import com.project.dorumdorum.global.annotation.CurrentUser; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.web.bind.annotation.RequestBody; | ||
| import org.springframework.web.bind.annotation.RestController; | ||
|
|
||
| @RestController | ||
| @RequiredArgsConstructor | ||
| public class NotificationSettingController implements NotificationSettingApiSpec { | ||
|
|
||
| private final LoadNotificationSettingUseCase loadNotificationSettingUseCase; | ||
| private final UpdateNotificationSettingUseCase updateNotificationSettingUseCase; | ||
|
|
||
| @Override | ||
| public ResponseEntity<NotificationSettingResponse> load(@CurrentUser String userNo) { | ||
| return ResponseEntity.ok(loadNotificationSettingUseCase.execute(userNo)); | ||
| } | ||
|
|
||
| @Override | ||
| public ResponseEntity<NotificationSettingResponse> update( | ||
| @CurrentUser String userNo, | ||
| @RequestBody NotificationSettingRequest request | ||
| ) { | ||
| return ResponseEntity.ok(updateNotificationSettingUseCase.execute(userNo, request)); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| package com.project.dorumdorum.domain.notification.ui.spec; | ||
|
|
||
| import com.project.dorumdorum.domain.notification.application.dto.request.NotificationSettingRequest; | ||
| import com.project.dorumdorum.domain.notification.application.dto.response.NotificationSettingResponse; | ||
| import io.swagger.v3.oas.annotations.Operation; | ||
| import io.swagger.v3.oas.annotations.Parameter; | ||
| import io.swagger.v3.oas.annotations.tags.Tag; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.web.bind.annotation.GetMapping; | ||
| import org.springframework.web.bind.annotation.PutMapping; | ||
| import org.springframework.web.bind.annotation.RequestBody; | ||
|
|
||
| @Tag(name = "Notification") | ||
| public interface NotificationSettingApiSpec { | ||
|
|
||
| @Operation(summary = "내 알림 설정 조회 API", description = "사용자별 알림 수신 설정을 조회합니다. 저장된 설정이 없으면 기본값을 반환합니다.") | ||
| @GetMapping("/api/users/me/notification-settings") | ||
| ResponseEntity<NotificationSettingResponse> load(@Parameter(hidden = true) String userNo); | ||
|
|
||
| @Operation(summary = "내 알림 설정 저장 API", description = "사용자별 알림 수신 설정을 저장합니다.") | ||
| @PutMapping("/api/users/me/notification-settings") | ||
| ResponseEntity<NotificationSettingResponse> update( | ||
| @Parameter(hidden = true) String userNo, | ||
| @RequestBody NotificationSettingRequest request | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
최초 설정 저장 시 경합 조건(TOCTOU)이 발생할 수 있습니다.
findByUserNo조회와save사이에 동기화 장치가 없습니다.user_no는notification_settings테이블에서 UNIQUE 제약을 가집니다.사용자가 설정을 아직 저장하지 않은 상태에서 두 개의 PUT 요청이 거의 동시에 들어오면, 두 요청 모두
findByUserNo에서 빈 결과를 받습니다. 그 결과 두 요청이 각각 신규 엔티티를 생성해 저장을 시도하고, 유니크 제약 위반으로 한쪽 요청이 예외와 함께 실패합니다.저장 시 제약 위반 예외를 잡아 재조회 후 재적용하는 방식으로 이 경합을 방지하는 것을 제안합니다.
🔧 제안하는 수정
🤖 Prompt for AI Agents