From 795e6e4baba7946eedca77edab4f2d46ce427d18 Mon Sep 17 00:00:00 2001 From: brothergiven Date: Mon, 10 Nov 2025 17:36:14 +0900 Subject: [PATCH 01/39] =?UTF-8?q?fix:=20=EA=B3=B5=EC=A7=80=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=8B=B9=205?= =?UTF-8?q?=EA=B0=9C=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Gotcha/domain/notification/service/NotificationService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gotcha/src/main/java/Gotcha/domain/notification/service/NotificationService.java b/gotcha/src/main/java/Gotcha/domain/notification/service/NotificationService.java index 4566b62b..f2127a3f 100644 --- a/gotcha/src/main/java/Gotcha/domain/notification/service/NotificationService.java +++ b/gotcha/src/main/java/Gotcha/domain/notification/service/NotificationService.java @@ -21,7 +21,7 @@ public class NotificationService { private final NotificationRepository notificationRepository; - private final Integer NOTIS_PER_PAGE = 10; + private final Integer NOTIS_PER_PAGE = 5; @Transactional(readOnly = true) From fc912ba0a2e72d825c0fae97edb7011b3b486cef Mon Sep 17 00:00:00 2001 From: brothergiven Date: Mon, 10 Nov 2025 19:27:05 +0900 Subject: [PATCH 02/39] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=A0=9C=EC=9E=AC=20=EC=97=94=ED=84=B0=ED=8B=B0=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/gotcha_domain/report/UserReport.java | 5 +- .../gotcha_domain/sanction/SanctionType.java | 7 +++ .../gotcha_domain/sanction/UserSanction.java | 60 +++++++++++++++++++ 3 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 gotcha-domain/src/main/java/gotcha_domain/sanction/SanctionType.java create mode 100644 gotcha-domain/src/main/java/gotcha_domain/sanction/UserSanction.java diff --git a/gotcha-domain/src/main/java/gotcha_domain/report/UserReport.java b/gotcha-domain/src/main/java/gotcha_domain/report/UserReport.java index 67ba76cc..0051e79c 100644 --- a/gotcha-domain/src/main/java/gotcha_domain/report/UserReport.java +++ b/gotcha-domain/src/main/java/gotcha_domain/report/UserReport.java @@ -31,12 +31,14 @@ public class UserReport extends BaseTimeEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + + @NotNull @Enumerated(EnumType.STRING) private UserReportType userReportType; @NotNull - private String detail; + private String detail; // by user @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id") @@ -57,4 +59,5 @@ public UserReport(UserReportType userReportType, String detail, List chatLog, User user) { return new UserReport(userReportType, detail, chatLog, user); } + } diff --git a/gotcha-domain/src/main/java/gotcha_domain/sanction/SanctionType.java b/gotcha-domain/src/main/java/gotcha_domain/sanction/SanctionType.java new file mode 100644 index 00000000..e14c9036 --- /dev/null +++ b/gotcha-domain/src/main/java/gotcha_domain/sanction/SanctionType.java @@ -0,0 +1,7 @@ +package gotcha_domain.sanction; + +public enum SanctionType { + WARNING, // 경고 + TEMP_BAN, // 일시 정지 + PERM_BAN // 영구 정지 +} diff --git a/gotcha-domain/src/main/java/gotcha_domain/sanction/UserSanction.java b/gotcha-domain/src/main/java/gotcha_domain/sanction/UserSanction.java new file mode 100644 index 00000000..a2e5d166 --- /dev/null +++ b/gotcha-domain/src/main/java/gotcha_domain/sanction/UserSanction.java @@ -0,0 +1,60 @@ +package gotcha_domain.sanction; + +import gotcha_common.entity.BaseTimeEntity; +import gotcha_domain.report.UserReport; +import gotcha_domain.user.User; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class UserSanction extends BaseTimeEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + // 제재를 받은 유저 + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + // 조치를 내린 관리자 + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "admin_id", nullable = false) + private User admin; + + @Enumerated(EnumType.STRING) + @Column(name = "sanction_type", nullable = false) + private SanctionType sanctionType; + + // 제재 사유 + @Column(nullable = false, columnDefinition = "TEXT") + private String reason; + + // 제재 만료 일시 + @Column(name = "expiry_date", nullable = false) + private LocalDateTime expiresAt; + + // 근거가 된 신고(nullable) + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_report_id") + private UserReport userReport; + + @Builder + public UserSanction(User user, User admin, SanctionType sanctionType, String reason, LocalDateTime expiresAt, UserReport userReport) { + this.user = user; + this.admin = admin; + this.sanctionType = sanctionType; + this.reason = reason; + this.expiresAt = expiresAt; + this.userReport = userReport; + } + + +} From f32d8c1f06bd6d4b2a2d5b03c7df4699f2e51d57 Mon Sep 17 00:00:00 2001 From: brothergiven Date: Mon, 10 Nov 2025 20:00:18 +0900 Subject: [PATCH 03/39] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=A0=9C=EC=9E=AC=20DTO,=20Repository=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/sanction/dto/SanctionReq.java | 15 +++++++++ .../domain/sanction/dto/SanctionRes.java | 32 +++++++++++++++++++ .../repository/SanctionRepository.java | 8 +++++ 3 files changed, 55 insertions(+) create mode 100644 gotcha/src/main/java/Gotcha/domain/sanction/dto/SanctionReq.java create mode 100644 gotcha/src/main/java/Gotcha/domain/sanction/dto/SanctionRes.java create mode 100644 gotcha/src/main/java/Gotcha/domain/sanction/repository/SanctionRepository.java diff --git a/gotcha/src/main/java/Gotcha/domain/sanction/dto/SanctionReq.java b/gotcha/src/main/java/Gotcha/domain/sanction/dto/SanctionReq.java new file mode 100644 index 00000000..d7c1e7e5 --- /dev/null +++ b/gotcha/src/main/java/Gotcha/domain/sanction/dto/SanctionReq.java @@ -0,0 +1,15 @@ +package Gotcha.domain.sanction.dto; + +import gotcha_domain.sanction.SanctionType; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class SanctionReq { + private Long targetUserId; + private Long adminUserId; + private String reason; + private SanctionType sanctionType; + private Long durationDays; // 제재 기간 (일 단위), 영구 정지인 경우 null +} diff --git a/gotcha/src/main/java/Gotcha/domain/sanction/dto/SanctionRes.java b/gotcha/src/main/java/Gotcha/domain/sanction/dto/SanctionRes.java new file mode 100644 index 00000000..e11b788a --- /dev/null +++ b/gotcha/src/main/java/Gotcha/domain/sanction/dto/SanctionRes.java @@ -0,0 +1,32 @@ +package Gotcha.domain.sanction.dto; + +import gotcha_domain.sanction.SanctionType; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +@Builder +public class SanctionRes { + private Long id; + private String targetUserName; + private String adminUserName; + private SanctionType sanctionType; + private String reason; + private LocalDateTime expiresAt; + private LocalDateTime createdAt; + + public static SanctionRes fromEntity(gotcha_domain.sanction.UserSanction sanction) { + return SanctionRes.builder() + .id(sanction.getId()) + .targetUserName(sanction.getUser().getNickname()) + .adminUserName(sanction.getAdmin().getNickname()) + .sanctionType(sanction.getSanctionType()) + .reason(sanction.getReason()) + .expiresAt(sanction.getExpiresAt()) + .createdAt(sanction.getCreatedAt()) + .build(); + } + +} diff --git a/gotcha/src/main/java/Gotcha/domain/sanction/repository/SanctionRepository.java b/gotcha/src/main/java/Gotcha/domain/sanction/repository/SanctionRepository.java new file mode 100644 index 00000000..1c11ce4e --- /dev/null +++ b/gotcha/src/main/java/Gotcha/domain/sanction/repository/SanctionRepository.java @@ -0,0 +1,8 @@ +package Gotcha.domain.sanction.repository; + +import gotcha_domain.sanction.UserSanction; +import org.springframework.data.jpa.repository.JpaRepository; + + +public interface SanctionRepository extends JpaRepository { +} From 1415a3f2b56d2250cc7d6c8596b4ee00480de702 Mon Sep 17 00:00:00 2001 From: brothergiven Date: Mon, 10 Nov 2025 20:32:49 +0900 Subject: [PATCH 04/39] =?UTF-8?q?feat:=20UserReport=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Gotcha/domain/report/exception/ReportExceptionCode.java | 3 ++- .../java/Gotcha/domain/report/service/UserReportService.java | 5 +++++ .../main/java/Gotcha/domain/sanction/dto/SanctionReq.java | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/gotcha/src/main/java/Gotcha/domain/report/exception/ReportExceptionCode.java b/gotcha/src/main/java/Gotcha/domain/report/exception/ReportExceptionCode.java index 1e8ea27f..0630b82b 100644 --- a/gotcha/src/main/java/Gotcha/domain/report/exception/ReportExceptionCode.java +++ b/gotcha/src/main/java/Gotcha/domain/report/exception/ReportExceptionCode.java @@ -6,7 +6,8 @@ @AllArgsConstructor public enum ReportExceptionCode implements ExceptionCode { - CANNOT_REPORT_SELF(HttpStatus.BAD_REQUEST, "REPORT-400-001", "자기 자신을 신고할 수 없습니다."); + CANNOT_REPORT_SELF(HttpStatus.BAD_REQUEST, "REPORT-400-001", "자기 자신을 신고할 수 없습니다."), + REPORT_NOT_FOUND(HttpStatus.NOT_FOUND, "REPORT-404-001", "신고 내역을 찾을 수 없습니다."); private final HttpStatus status; private final String code; diff --git a/gotcha/src/main/java/Gotcha/domain/report/service/UserReportService.java b/gotcha/src/main/java/Gotcha/domain/report/service/UserReportService.java index 213ffac8..9c689c66 100644 --- a/gotcha/src/main/java/Gotcha/domain/report/service/UserReportService.java +++ b/gotcha/src/main/java/Gotcha/domain/report/service/UserReportService.java @@ -40,4 +40,9 @@ public void reportUser(UserReportReq reportReq, SecurityUserDetails userDetails) userReportRepository.save(userReport); } + public UserReport findUserReportById(Long reportId){ + return userReportRepository.findById(reportId) + .orElseThrow(() -> new CustomException(ReportExceptionCode.REPORT_NOT_FOUND)); + } + } diff --git a/gotcha/src/main/java/Gotcha/domain/sanction/dto/SanctionReq.java b/gotcha/src/main/java/Gotcha/domain/sanction/dto/SanctionReq.java index d7c1e7e5..b1320d7e 100644 --- a/gotcha/src/main/java/Gotcha/domain/sanction/dto/SanctionReq.java +++ b/gotcha/src/main/java/Gotcha/domain/sanction/dto/SanctionReq.java @@ -11,5 +11,6 @@ public class SanctionReq { private Long adminUserId; private String reason; private SanctionType sanctionType; + private Long sourceReportId; // 제재의 근거가 되는 신고 ID (nullable) private Long durationDays; // 제재 기간 (일 단위), 영구 정지인 경우 null } From dc4a2ef84b863ce44f20d95b75228ee1d7b3a220 Mon Sep 17 00:00:00 2001 From: brothergiven Date: Tue, 11 Nov 2025 22:39:07 +0900 Subject: [PATCH 05/39] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A0=80=20=EC=A0=95?= =?UTF-8?q?=EC=A7=80=20=EC=83=81=ED=83=9C=EB=A5=BC=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=ED=95=98=EA=B8=B0=20=EC=9C=84=ED=95=9C=20status=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/gotcha_domain/user/User.java | 14 ++++++++++++++ .../main/java/gotcha_domain/user/UserStatus.java | 5 +++++ 2 files changed, 19 insertions(+) create mode 100644 gotcha-domain/src/main/java/gotcha_domain/user/UserStatus.java diff --git a/gotcha-domain/src/main/java/gotcha_domain/user/User.java b/gotcha-domain/src/main/java/gotcha_domain/user/User.java index fc9be831..07c723f6 100644 --- a/gotcha-domain/src/main/java/gotcha_domain/user/User.java +++ b/gotcha-domain/src/main/java/gotcha_domain/user/User.java @@ -72,6 +72,10 @@ public class User extends BaseTimeEntity { @Column(name = "chat_option", nullable = false) private ChatOption chatOption = ChatOption.ALLOW_ALL; + @Enumerated(EnumType.STRING) + @Column(name = "user_status", nullable = false) + private UserStatus userStatus = UserStatus.ACTIVE; + @Enumerated(EnumType.STRING) @Column(name = "private_chat_option", nullable = false) private PrivateChatOption privateChatOption = PrivateChatOption.ALLOW; @@ -120,6 +124,8 @@ public class User extends BaseTimeEntity { @OneToMany(mappedBy = "user") private Set bugReports = new HashSet<>(); + + @Builder public User(Long id, String email, String password, String nickname, Role role, String uuid){ this.id = id; @@ -139,6 +145,14 @@ public void updateChatSettings(ChatOption chatOption, PrivateChatOption privateC this.privateChatOption = privateChatOption; } + public void incrementWarningCount() { + this.warningCount = (this.warningCount == null) ? 1 : this.warningCount + 1; + } + + public void suspendUser(Long days) { + this.isLocked = true; + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/gotcha-domain/src/main/java/gotcha_domain/user/UserStatus.java b/gotcha-domain/src/main/java/gotcha_domain/user/UserStatus.java new file mode 100644 index 00000000..ce54051c --- /dev/null +++ b/gotcha-domain/src/main/java/gotcha_domain/user/UserStatus.java @@ -0,0 +1,5 @@ +package gotcha_domain.user; + +public enum UserStatus { + ACTIVE, SUSPENDED, BANNED +} From 73657d5570c5584066377de6d38529fdba969be4 Mon Sep 17 00:00:00 2001 From: brothergiven Date: Tue, 11 Nov 2025 22:40:42 +0900 Subject: [PATCH 06/39] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A0=80=20=EC=A0=95?= =?UTF-8?q?=EC=A7=80=20=EC=B2=98=EB=A6=AC=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80(User=20=EC=97=94=ED=84=B0=ED=8B=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/gotcha_domain/sanction/UserSanction.java | 2 +- .../src/main/java/gotcha_domain/user/User.java | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/gotcha-domain/src/main/java/gotcha_domain/sanction/UserSanction.java b/gotcha-domain/src/main/java/gotcha_domain/sanction/UserSanction.java index a2e5d166..d774b3b2 100644 --- a/gotcha-domain/src/main/java/gotcha_domain/sanction/UserSanction.java +++ b/gotcha-domain/src/main/java/gotcha_domain/sanction/UserSanction.java @@ -38,7 +38,7 @@ public class UserSanction extends BaseTimeEntity { private String reason; // 제재 만료 일시 - @Column(name = "expiry_date", nullable = false) + @Column(name = "expiry_date") private LocalDateTime expiresAt; // 근거가 된 신고(nullable) diff --git a/gotcha-domain/src/main/java/gotcha_domain/user/User.java b/gotcha-domain/src/main/java/gotcha_domain/user/User.java index 07c723f6..4f7ac9b7 100644 --- a/gotcha-domain/src/main/java/gotcha_domain/user/User.java +++ b/gotcha-domain/src/main/java/gotcha_domain/user/User.java @@ -57,7 +57,7 @@ public class User extends BaseTimeEntity { @Setter private LocalDateTime lastLogout; - private Boolean isLocked; +// private Boolean isLocked; // UserStatus로 대체 @Setter private int level; @@ -76,6 +76,9 @@ public class User extends BaseTimeEntity { @Column(name = "user_status", nullable = false) private UserStatus userStatus = UserStatus.ACTIVE; + @Column(name = "suspension_end_date") + private LocalDateTime suspensionEndDate; + @Enumerated(EnumType.STRING) @Column(name = "private_chat_option", nullable = false) private PrivateChatOption privateChatOption = PrivateChatOption.ALLOW; @@ -150,7 +153,12 @@ public void incrementWarningCount() { } public void suspendUser(Long days) { - this.isLocked = true; + this.userStatus = UserStatus.SUSPENDED; + this.suspensionEndDate = LocalDateTime.now().plusDays(days); + } + + public void banUser(){ + this.userStatus = UserStatus.BANNED; } @Override From ec124be888359e8bf4141c84a29f1705a2b5d642 Mon Sep 17 00:00:00 2001 From: brothergiven Date: Tue, 11 Nov 2025 22:59:02 +0900 Subject: [PATCH 07/39] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A0=80=20=EC=A0=9C?= =?UTF-8?q?=EC=9E=AC=20=EB=B0=8F=20=EA=B8=B0=EB=A1=9D=20=EC=A0=80=EC=9E=A5?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/gotcha_domain/user/User.java | 5 +- .../sanction/service/SanctionService.java | 75 +++++++++++++++++++ 2 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 gotcha/src/main/java/Gotcha/domain/sanction/service/SanctionService.java diff --git a/gotcha-domain/src/main/java/gotcha_domain/user/User.java b/gotcha-domain/src/main/java/gotcha_domain/user/User.java index 4f7ac9b7..d85e5588 100644 --- a/gotcha-domain/src/main/java/gotcha_domain/user/User.java +++ b/gotcha-domain/src/main/java/gotcha_domain/user/User.java @@ -127,8 +127,6 @@ public class User extends BaseTimeEntity { @OneToMany(mappedBy = "user") private Set bugReports = new HashSet<>(); - - @Builder public User(Long id, String email, String password, String nickname, Role role, String uuid){ this.id = id; @@ -152,13 +150,14 @@ public void incrementWarningCount() { this.warningCount = (this.warningCount == null) ? 1 : this.warningCount + 1; } - public void suspendUser(Long days) { + public void suspendUser(long days) { this.userStatus = UserStatus.SUSPENDED; this.suspensionEndDate = LocalDateTime.now().plusDays(days); } public void banUser(){ this.userStatus = UserStatus.BANNED; + this.suspensionEndDate = null; } @Override diff --git a/gotcha/src/main/java/Gotcha/domain/sanction/service/SanctionService.java b/gotcha/src/main/java/Gotcha/domain/sanction/service/SanctionService.java new file mode 100644 index 00000000..7596127f --- /dev/null +++ b/gotcha/src/main/java/Gotcha/domain/sanction/service/SanctionService.java @@ -0,0 +1,75 @@ +package Gotcha.domain.sanction.service; + +import Gotcha.domain.report.service.UserReportService; +import Gotcha.domain.sanction.dto.SanctionReq; +import Gotcha.domain.sanction.dto.SanctionRes; +import Gotcha.domain.sanction.repository.SanctionRepository; +import gotcha_domain.report.UserReport; +import gotcha_domain.sanction.SanctionType; +import gotcha_domain.sanction.UserSanction; +import gotcha_domain.user.User; +import gotcha_user.service.UserService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; + +@Service +@RequiredArgsConstructor +public class SanctionService { + + private final SanctionRepository sanctionRepository; + private final UserService userService; + private final UserReportService userReportService; + + @Transactional + // 1. 사용자 에게 제재를 + public SanctionRes sanctionUser(SanctionReq sanctionReq) { + // admin User 조회 + User adminUser = userService.findUserByUserId(sanctionReq.getAdminUserId()); + + //1. target User 조회 + User targetUser = userService.findUserByUserId(sanctionReq.getTargetUserId()); + + // 2. source Report 조회 (존재한다면) + UserReport sourceReport = null; + if(sanctionReq.getSourceReportId() != null){ + sourceReport = userReportService.findUserReportById(sanctionReq.getSourceReportId()); + } + + // 3. 제재 유형에 따른 로직 처리 + LocalDateTime expiresAt = null; // 제재 만료 시간 + SanctionType sanctionType = sanctionReq.getSanctionType(); + switch(sanctionType){ + case WARNING: + targetUser.incrementWarningCount(); + // 경고는 만료 시간이 없음 + break; + case TEMP_BAN: + // 임시 정지는 현재 시간에서 지정된 기간만큼 더함 + Long durationDays = sanctionReq.getDurationDays(); + expiresAt = LocalDateTime.now().plusDays(durationDays); + targetUser.suspendUser(durationDays); + break; + case PERM_BAN: + // 영구 정지는 만료 시간이 없음 + targetUser.banUser(); + break; + } + + // 4. 제재 기록 저장 + UserSanction userSanction = UserSanction.builder() + .user(targetUser) + .admin(adminUser) + .sanctionType(sanctionType) + .reason(sanctionReq.getReason()) + .expiresAt(expiresAt) + .userReport(sourceReport) + .build(); + + sanctionRepository.save(userSanction); + + return SanctionRes.fromEntity(userSanction); + } +} From c7b97ab79857884d18d4b98b2a2aeaaa8e62acb4 Mon Sep 17 00:00:00 2001 From: brothergiven Date: Tue, 11 Nov 2025 23:11:52 +0900 Subject: [PATCH 08/39] =?UTF-8?q?fix:=20=EC=A7=81=EC=A0=91=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=ED=95=9C=20=EA=B4=80=EB=A6=AC=EC=9E=90=20=EC=9D=B8?= =?UTF-8?q?=EC=A6=9D=20=EC=BD=94=EB=93=9C=20PreAuthorize=20=EC=95=A0?= =?UTF-8?q?=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=8C=80=EC=B2=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AdminNotificationController.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/gotcha/src/main/java/Gotcha/domain/notification/controller/AdminNotificationController.java b/gotcha/src/main/java/Gotcha/domain/notification/controller/AdminNotificationController.java index 9c06f563..73241172 100644 --- a/gotcha/src/main/java/Gotcha/domain/notification/controller/AdminNotificationController.java +++ b/gotcha/src/main/java/Gotcha/domain/notification/controller/AdminNotificationController.java @@ -11,6 +11,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -27,6 +28,7 @@ public class AdminNotificationController implements AdminNotificationApi { @Override @PostMapping + @PreAuthorize("hasRole('ADMIN')") public ResponseEntity createNotification( @Valid @RequestBody NotificationReq notificationReq, @AuthenticationPrincipal SecurityUserDetails userDetails){ @@ -36,20 +38,22 @@ public ResponseEntity createNotification( @Override @PutMapping("/{notificationId}") + @PreAuthorize("hasRole('ADMIN')") public ResponseEntity updateNotification( @PathVariable(value = "notificationId") Long notificationId, @Valid @RequestBody NotificationReq notificationReq, @AuthenticationPrincipal SecurityUserDetails userDetails){ - adminNotificationService.updateNotification(notificationReq, notificationId, userDetails.getId()); + adminNotificationService.updateNotification(notificationReq, notificationId); return ResponseEntity.ok(SuccessRes.from("공지사항 수정에 성공했습니다.")); } @Override @DeleteMapping("/{notificationId}") + @PreAuthorize("hasRole('ADMIN')") public ResponseEntity deleteNotification( @PathVariable(value = "notificationId") Long notificationId, @AuthenticationPrincipal SecurityUserDetails userDetails){ - adminNotificationService.deleteNotification(notificationId, userDetails.getId()); + adminNotificationService.deleteNotification(notificationId); return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); } } \ No newline at end of file From 12a893c114be2b68772ba7e035a9d81bd75d42a6 Mon Sep 17 00:00:00 2001 From: brothergiven Date: Tue, 11 Nov 2025 23:13:54 +0900 Subject: [PATCH 09/39] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A0=80=20=EC=A0=9C?= =?UTF-8?q?=EC=9E=AC=20=EA=B4=80=EB=A0=A8=20=EC=BB=A8=ED=8A=B8=EB=A1=A4?= =?UTF-8?q?=EB=9F=AC=20=EA=B3=84=EC=B8=B5=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/AdminNotificationService.java | 25 ++++-------- .../controller/SanctionController.java | 40 +++++++++++++++++++ .../domain/sanction/dto/SanctionReq.java | 17 ++++++-- .../sanction/service/SanctionService.java | 7 ++-- 4 files changed, 64 insertions(+), 25 deletions(-) create mode 100644 gotcha/src/main/java/Gotcha/domain/sanction/controller/SanctionController.java diff --git a/gotcha/src/main/java/Gotcha/domain/notification/service/AdminNotificationService.java b/gotcha/src/main/java/Gotcha/domain/notification/service/AdminNotificationService.java index 1d4aae04..3d121614 100644 --- a/gotcha/src/main/java/Gotcha/domain/notification/service/AdminNotificationService.java +++ b/gotcha/src/main/java/Gotcha/domain/notification/service/AdminNotificationService.java @@ -5,11 +5,11 @@ import gotcha_common.exception.CustomException; import gotcha_domain.notification.Notification; import gotcha_domain.notification.NotificationReq; -import gotcha_domain.user.Role; import gotcha_domain.user.User; import gotcha_user.exceptionCode.UserExceptionCode; import gotcha_user.repository.UserRepository; import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -23,7 +23,8 @@ public class AdminNotificationService { @Transactional public void createNotification(NotificationReq notificationReq, Long writerId){ - User writer = validateAdmin(writerId); + User writer = userRepository.findById(writerId) + .orElseThrow(() -> new CustomException(UserExceptionCode.INVALID_USERID)); Notification notification = notificationReq.toEntity(writer); @@ -32,19 +33,15 @@ public void createNotification(NotificationReq notificationReq, Long writerId){ @Transactional - public void updateNotification(NotificationReq notificationReq, Long notificationId, Long userId){ - validateAdmin(userId); - - Notification notification = validateUserNotification(notificationId, userId); + public void updateNotification(NotificationReq notificationReq, Long notificationId){ + Notification notification = validateNotification(notificationId); notification.update(notificationReq); } @Transactional - public void deleteNotification(Long notificationId, Long userId){ - validateAdmin(userId); - - Notification notification = validateUserNotification(notificationId, userId); + public void deleteNotification(Long notificationId){ + Notification notification = validateNotification(notificationId); notificationRepository.delete(notification); } @@ -58,14 +55,6 @@ private Notification validateUserNotification(Long notificationId, Long userId) return notification; } - private User validateAdmin(Long userId){ - User user = userRepository.findById(userId) - .orElseThrow(() -> new CustomException(UserExceptionCode.INVALID_USERID)); - if(!user.getRole().equals(Role.ADMIN)) - throw new CustomException(NotificationExceptionCode.UNAUTHORIZED_ACTION); - return user; - } - private Notification validateNotification(Long notificationId){ return notificationRepository.findById(notificationId) .orElseThrow(() -> new CustomException(NotificationExceptionCode.NOT_FOUND)); diff --git a/gotcha/src/main/java/Gotcha/domain/sanction/controller/SanctionController.java b/gotcha/src/main/java/Gotcha/domain/sanction/controller/SanctionController.java new file mode 100644 index 00000000..c838ff9e --- /dev/null +++ b/gotcha/src/main/java/Gotcha/domain/sanction/controller/SanctionController.java @@ -0,0 +1,40 @@ +package Gotcha.domain.sanction.controller; + +import Gotcha.domain.sanction.dto.SanctionReq; +import Gotcha.domain.sanction.dto.SanctionRes; +import Gotcha.domain.sanction.service.SanctionService; +import gotcha_domain.auth.SecurityUserDetails; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.security.Principal; + +@RestController +@RequestMapping("/api/v1/admin/sanctions") +@RequiredArgsConstructor +public class SanctionController { + + private final SanctionService sanctionService; + + + @PostMapping + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity applySanction( + @Valid @RequestBody SanctionReq sanctionReq, + @AuthenticationPrincipal SecurityUserDetails userDetails) { + + String adminId = userDetails.getUuid(); + + SanctionRes response = sanctionService.sanctionUser(sanctionReq, adminId); + + return ResponseEntity.status(HttpStatus.CREATED).body(response); + } +} diff --git a/gotcha/src/main/java/Gotcha/domain/sanction/dto/SanctionReq.java b/gotcha/src/main/java/Gotcha/domain/sanction/dto/SanctionReq.java index b1320d7e..f14e696b 100644 --- a/gotcha/src/main/java/Gotcha/domain/sanction/dto/SanctionReq.java +++ b/gotcha/src/main/java/Gotcha/domain/sanction/dto/SanctionReq.java @@ -1,16 +1,27 @@ package Gotcha.domain.sanction.dto; import gotcha_domain.sanction.SanctionType; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import lombok.Getter; import lombok.NoArgsConstructor; @Getter @NoArgsConstructor public class SanctionReq { - private Long targetUserId; - private Long adminUserId; + + @NotBlank(message = "제재 대상의 ID는 필수입니다.") + private String targetUserId; + + // adminUserId 필드는 보안을 위해 제거하고, Controller에서 직접 인증 정보를 사용합니다. + + @NotBlank(message = "제재 사유는 필수입니다.") private String reason; + + @NotNull(message = "제재 유형은 필수입니다.") private SanctionType sanctionType; + private Long sourceReportId; // 제재의 근거가 되는 신고 ID (nullable) - private Long durationDays; // 제재 기간 (일 단위), 영구 정지인 경우 null + + private Long durationDays; // 제재 기간 (일 단위), TEMP_BAN일 때 필요 } diff --git a/gotcha/src/main/java/Gotcha/domain/sanction/service/SanctionService.java b/gotcha/src/main/java/Gotcha/domain/sanction/service/SanctionService.java index 7596127f..3e3b483a 100644 --- a/gotcha/src/main/java/Gotcha/domain/sanction/service/SanctionService.java +++ b/gotcha/src/main/java/Gotcha/domain/sanction/service/SanctionService.java @@ -24,13 +24,12 @@ public class SanctionService { private final UserReportService userReportService; @Transactional - // 1. 사용자 에게 제재를 - public SanctionRes sanctionUser(SanctionReq sanctionReq) { + public SanctionRes sanctionUser(SanctionReq sanctionReq, String adminId) { // admin User 조회 - User adminUser = userService.findUserByUserId(sanctionReq.getAdminUserId()); + User adminUser = userService.findUserByUuid(adminId); //1. target User 조회 - User targetUser = userService.findUserByUserId(sanctionReq.getTargetUserId()); + User targetUser = userService.findUserByUuid(sanctionReq.getTargetUserId()); // 2. source Report 조회 (존재한다면) UserReport sourceReport = null; From 37a4bd2c8849ccb245f91851d000918511ad3a28 Mon Sep 17 00:00:00 2001 From: brothergiven Date: Tue, 11 Nov 2025 23:20:20 +0900 Subject: [PATCH 10/39] =?UTF-8?q?docs:=20swagger=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/sanction/api/SanctionApi.java | 87 +++++++++++++++++++ .../controller/SanctionController.java | 3 +- 2 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 gotcha/src/main/java/Gotcha/domain/sanction/api/SanctionApi.java diff --git a/gotcha/src/main/java/Gotcha/domain/sanction/api/SanctionApi.java b/gotcha/src/main/java/Gotcha/domain/sanction/api/SanctionApi.java new file mode 100644 index 00000000..3ffd327b --- /dev/null +++ b/gotcha/src/main/java/Gotcha/domain/sanction/api/SanctionApi.java @@ -0,0 +1,87 @@ +package Gotcha.domain.sanction.api; + +import Gotcha.domain.sanction.dto.SanctionReq; +import Gotcha.domain.sanction.dto.SanctionRes; +import gotcha_domain.auth.SecurityUserDetails; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestBody; + + + +@Tag(name = "[관리자 제재 API]", description = "관리자용 사용자 제재 관련 API") +public interface SanctionApi { + + @Operation(summary = "사용자 제재 적용 API", description = "특정 사용자에게 경고, 임시 정지, 영구 정지 등의 제재를 가합니다.") + @SecurityRequirement(name = "bearerAuth") + @ApiResponses({ + @ApiResponse(responseCode = "201", description = "제재 조치 성공", + content = @Content(mediaType = "application/json", examples = { + @ExampleObject(value = """ + { + "id": 1, + "targetUserName": "제재받은유저", + "adminUserName": "관리자", + "sanctionType": "TEMP_BAN", + "reason": "부적절한 언어 사용", + "expiresAt": "2025-11-18T10:00:00", + "createdAt": "2025-11-11T10:00:00" + } + """) + }) + ), + @ApiResponse(responseCode = "400", description = "필드 검증 오류 / 잘못된 요청", + content = @Content(mediaType = "application/json", examples = { + @ExampleObject(value = """ + { + "status": "BAD_REQUEST", + "message": "필드 검증 오류입니다.", + "fields": { + "reason": "제재 사유는 필수입니다." + } + } + """) + }) + ), + @ApiResponse(responseCode = "401", description = "인증되지 않은 사용자", + content = @Content(mediaType = "application/json", examples = { + @ExampleObject(value = """ + { + "status": "UNAUTHORIZED", + "message": "인증이 필요합니다." + } + """) + }) + ), + @ApiResponse(responseCode = "403", description = "권한 없음", + content = @Content(mediaType = "application/json", examples = { + @ExampleObject(value = """ + { + "status": "FORBIDDEN", + "message": "접근 권한이 없습니다." + } + """) + }) + ), + @ApiResponse(responseCode = "404", description = "존재하지 않는 사용자", + content = @Content(mediaType = "application/json", examples = { + @ExampleObject(value = """ + { + "status": "NOT_FOUND", + "message": "해당 사용자를 찾을 수 없습니다." + } + """) + }) + ) + }) + ResponseEntity applySanction( + @Valid @RequestBody SanctionReq sanctionReq, + SecurityUserDetails userDetails); +} diff --git a/gotcha/src/main/java/Gotcha/domain/sanction/controller/SanctionController.java b/gotcha/src/main/java/Gotcha/domain/sanction/controller/SanctionController.java index c838ff9e..3c2ef519 100644 --- a/gotcha/src/main/java/Gotcha/domain/sanction/controller/SanctionController.java +++ b/gotcha/src/main/java/Gotcha/domain/sanction/controller/SanctionController.java @@ -1,5 +1,6 @@ package Gotcha.domain.sanction.controller; +import Gotcha.domain.sanction.api.SanctionApi; import Gotcha.domain.sanction.dto.SanctionReq; import Gotcha.domain.sanction.dto.SanctionRes; import Gotcha.domain.sanction.service.SanctionService; @@ -20,7 +21,7 @@ @RestController @RequestMapping("/api/v1/admin/sanctions") @RequiredArgsConstructor -public class SanctionController { +public class SanctionController implements SanctionApi { private final SanctionService sanctionService; From beba1f5f9014ab2ccc6d04dc513cdd3f81afae6b Mon Sep 17 00:00:00 2001 From: brothergiven Date: Wed, 12 Nov 2025 22:54:51 +0900 Subject: [PATCH 11/39] =?UTF-8?q?docs:=20Swagger=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/sanction/api/SanctionApi.java | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/gotcha/src/main/java/Gotcha/domain/sanction/api/SanctionApi.java b/gotcha/src/main/java/Gotcha/domain/sanction/api/SanctionApi.java index 3ffd327b..a0550677 100644 --- a/gotcha/src/main/java/Gotcha/domain/sanction/api/SanctionApi.java +++ b/gotcha/src/main/java/Gotcha/domain/sanction/api/SanctionApi.java @@ -37,45 +37,44 @@ public interface SanctionApi { """) }) ), - @ApiResponse(responseCode = "400", description = "필드 검증 오류 / 잘못된 요청", + @ApiResponse(responseCode = "422", description = "필드 검증 오류 ", content = @Content(mediaType = "application/json", examples = { @ExampleObject(value = """ { - "status": "BAD_REQUEST", - "message": "필드 검증 오류입니다.", - "fields": { - "reason": "제재 사유는 필수입니다." - } + "reason": "제재 사유는 필수입니다." } """) }) ), - @ApiResponse(responseCode = "401", description = "인증되지 않은 사용자", + @ApiResponse(responseCode = "403", description = "관리자 권한 없음", content = @Content(mediaType = "application/json", examples = { @ExampleObject(value = """ { - "status": "UNAUTHORIZED", - "message": "인증이 필요합니다." + "code": "AUTH-403-001", + "status": "FORBIDDEN", + "message": "접근 권한이 없습니다." } """) }) ), - @ApiResponse(responseCode = "403", description = "권한 없음", + @ApiResponse(responseCode = "404", description = "존재하지 않는 사용자", content = @Content(mediaType = "application/json", examples = { @ExampleObject(value = """ { - "status": "FORBIDDEN", - "message": "접근 권한이 없습니다." + "code": "USER-404-001", + "status": "NOT_FOUND", + "message": "해당 사용자를 찾을 수 없습니다." } """) }) ), - @ApiResponse(responseCode = "404", description = "존재하지 않는 사용자", + @ApiResponse(responseCode = "404", description = "존재하지 않는 신고 건", content = @Content(mediaType = "application/json", examples = { @ExampleObject(value = """ { + "code": "REPORT-404-001", "status": "NOT_FOUND", - "message": "해당 사용자를 찾을 수 없습니다." + "message": "신고 내역을 찾을 수 없습니다." } """) }) From 4e0439ece498caeb0bd1cf9493ae74ea8a29b9dc Mon Sep 17 00:00:00 2001 From: brothergiven Date: Fri, 14 Nov 2025 01:37:49 +0900 Subject: [PATCH 12/39] =?UTF-8?q?feat:=20=EC=A0=9C=EC=9E=AC=20=EA=B8=B0?= =?UTF-8?q?=EB=A1=9D=EC=97=90=20Read=20=ED=95=84=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/gotcha_domain/sanction/UserSanction.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/gotcha-domain/src/main/java/gotcha_domain/sanction/UserSanction.java b/gotcha-domain/src/main/java/gotcha_domain/sanction/UserSanction.java index d774b3b2..232acdf1 100644 --- a/gotcha-domain/src/main/java/gotcha_domain/sanction/UserSanction.java +++ b/gotcha-domain/src/main/java/gotcha_domain/sanction/UserSanction.java @@ -46,6 +46,9 @@ public class UserSanction extends BaseTimeEntity { @JoinColumn(name = "user_report_id") private UserReport userReport; + @Column(name = "is_read", nullable = false) + private boolean isRead = false; + @Builder public UserSanction(User user, User admin, SanctionType sanctionType, String reason, LocalDateTime expiresAt, UserReport userReport) { this.user = user; @@ -56,5 +59,9 @@ public UserSanction(User user, User admin, SanctionType sanctionType, String rea this.userReport = userReport; } + public void markAsRead() { + this.isRead = true; + } + } From 3fc76c5c90a6cdaa87e560ddba6f5c569337c453 Mon Sep 17 00:00:00 2001 From: brothergiven Date: Fri, 14 Nov 2025 01:38:06 +0900 Subject: [PATCH 13/39] =?UTF-8?q?feat:=20Read=20=ED=95=84=EB=93=9C=20?= =?UTF-8?q?=EA=B8=B0=EC=A4=80=EC=9C=BC=EB=A1=9C=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EB=A1=9C=EC=A7=81=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sanction/repository/SanctionRepository.java | 6 ++++++ .../domain/sanction/service/SanctionService.java | 15 +++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/gotcha/src/main/java/Gotcha/domain/sanction/repository/SanctionRepository.java b/gotcha/src/main/java/Gotcha/domain/sanction/repository/SanctionRepository.java index 1c11ce4e..a456f7f1 100644 --- a/gotcha/src/main/java/Gotcha/domain/sanction/repository/SanctionRepository.java +++ b/gotcha/src/main/java/Gotcha/domain/sanction/repository/SanctionRepository.java @@ -1,8 +1,14 @@ package Gotcha.domain.sanction.repository; +import gotcha_domain.sanction.SanctionType; import gotcha_domain.sanction.UserSanction; +import gotcha_domain.user.User; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface SanctionRepository extends JpaRepository { + Optional findTopByUserAndIsReadIsFalseOrderByCreatedAtDesc(User user); + Optional findTopByUserAndSanctionTypeAndIsReadIsFalseOrderByCreatedAtDesc(User user, SanctionType type); } diff --git a/gotcha/src/main/java/Gotcha/domain/sanction/service/SanctionService.java b/gotcha/src/main/java/Gotcha/domain/sanction/service/SanctionService.java index 3e3b483a..aea3e877 100644 --- a/gotcha/src/main/java/Gotcha/domain/sanction/service/SanctionService.java +++ b/gotcha/src/main/java/Gotcha/domain/sanction/service/SanctionService.java @@ -6,6 +6,7 @@ import Gotcha.domain.sanction.repository.SanctionRepository; import gotcha_domain.report.UserReport; import gotcha_domain.sanction.SanctionType; +import gotcha_domain.sanction.SanctionType; import gotcha_domain.sanction.UserSanction; import gotcha_domain.user.User; import gotcha_user.service.UserService; @@ -14,6 +15,7 @@ import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; +import java.util.Optional; @Service @RequiredArgsConstructor @@ -71,4 +73,17 @@ public SanctionRes sanctionUser(SanctionReq sanctionReq, String adminId) { return SanctionRes.fromEntity(userSanction); } + + public Optional findLatestUnreadSanction(User user) { + return sanctionRepository.findTopByUserAndIsReadIsFalseOrderByCreatedAtDesc(user); + } + + public Optional findLatestUnreadWarning(User user) { + return sanctionRepository.findTopByUserAndSanctionTypeAndIsReadIsFalseOrderByCreatedAtDesc(user, SanctionType.WARNING); + } + + @Transactional + public void markSanctionAsRead(UserSanction sanction) { + sanction.markAsRead(); + } } From 90296d81f6d42f013c7887af8595c0d589e62b5f Mon Sep 17 00:00:00 2001 From: brothergiven Date: Fri, 14 Nov 2025 01:42:25 +0900 Subject: [PATCH 14/39] =?UTF-8?q?feat:=20details=20=ED=95=84=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=EB=90=9C=20UserAccountException=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=EC=9E=91=EC=84=B1(=EC=A0=95=EC=A7=80,=20=EA=B2=BD?= =?UTF-8?q?=EA=B3=A0=20=EC=8B=9C=20=EC=84=B8=EB=B6=80=20=EC=A0=95=EB=B3=B4?= =?UTF-8?q?=20=ED=95=A8=EA=BB=98)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/UserAccountStatusException.java | 20 ++++++++++++++++++ .../UserAccountStatusExceptionHandler.java | 21 +++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 gotcha/src/main/java/Gotcha/domain/auth/exception/UserAccountStatusException.java create mode 100644 gotcha/src/main/java/Gotcha/domain/auth/exception/UserAccountStatusExceptionHandler.java diff --git a/gotcha/src/main/java/Gotcha/domain/auth/exception/UserAccountStatusException.java b/gotcha/src/main/java/Gotcha/domain/auth/exception/UserAccountStatusException.java new file mode 100644 index 00000000..5bd723f2 --- /dev/null +++ b/gotcha/src/main/java/Gotcha/domain/auth/exception/UserAccountStatusException.java @@ -0,0 +1,20 @@ +package Gotcha.domain.auth.exception; + +import gotcha_common.exception.CustomException; +import gotcha_common.exception.exceptionCode.ExceptionCode; +import lombok.Getter; + +import java.util.Map; + +@Getter +public class UserAccountStatusException extends CustomException { + + private final Map details; + + public UserAccountStatusException(ExceptionCode exceptionCode, Map details) { + super(exceptionCode); + this.details = details; + } + +} + diff --git a/gotcha/src/main/java/Gotcha/domain/auth/exception/UserAccountStatusExceptionHandler.java b/gotcha/src/main/java/Gotcha/domain/auth/exception/UserAccountStatusExceptionHandler.java new file mode 100644 index 00000000..f9482bed --- /dev/null +++ b/gotcha/src/main/java/Gotcha/domain/auth/exception/UserAccountStatusExceptionHandler.java @@ -0,0 +1,21 @@ +package Gotcha.domain.auth.exception; + +import gotcha_common.exception.ExceptionRes; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@Slf4j +@RestControllerAdvice +public class UserAccountStatusExceptionHandler { + + @ExceptionHandler(UserAccountStatusException.class) + public ResponseEntity handleAccountStatusException(UserAccountStatusException e) { + log.error("[Account Status Exception] {}: {}", e.getExceptionCode().getMessage(), e.getDetails()); + return ResponseEntity + .status(e.getExceptionCode().getStatus()) + .body(ExceptionRes.from(e.getExceptionCode(), e.getDetails())); + } + +} From c4add8ccc2580118cffea9e4e57aea2b2d507064 Mon Sep 17 00:00:00 2001 From: brothergiven Date: Fri, 14 Nov 2025 01:43:06 +0900 Subject: [PATCH 15/39] =?UTF-8?q?fix:=20ExceptionRes=20DTO=20=EC=88=98?= =?UTF-8?q?=EC=A0=95(=20=20->=20=20)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/gotcha_common/exception/ExceptionRes.java | 4 ++-- .../java/gotcha_common/exception/GlobalExceptionHandler.java | 2 +- .../common/exception/SocketGlobalExceptionHandler.java | 4 +++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/gotcha-common/src/main/java/gotcha_common/exception/ExceptionRes.java b/gotcha-common/src/main/java/gotcha_common/exception/ExceptionRes.java index 05eaac6b..065619cd 100644 --- a/gotcha-common/src/main/java/gotcha_common/exception/ExceptionRes.java +++ b/gotcha-common/src/main/java/gotcha_common/exception/ExceptionRes.java @@ -13,7 +13,7 @@ public record ExceptionRes( String code, HttpStatus status, String message, - Map fields + Map fields ) { public static ExceptionRes from(ExceptionCode error){ return ExceptionRes.builder() @@ -23,7 +23,7 @@ public static ExceptionRes from(ExceptionCode error){ .build(); } - public static ExceptionRes from(ExceptionCode error, Map fields) { + public static ExceptionRes from(ExceptionCode error, Map fields) { return ExceptionRes.builder() .status(error.getStatus()) .code(error.getCode()) diff --git a/gotcha-common/src/main/java/gotcha_common/exception/GlobalExceptionHandler.java b/gotcha-common/src/main/java/gotcha_common/exception/GlobalExceptionHandler.java index 349aab15..63bd1ac6 100644 --- a/gotcha-common/src/main/java/gotcha_common/exception/GlobalExceptionHandler.java +++ b/gotcha-common/src/main/java/gotcha_common/exception/GlobalExceptionHandler.java @@ -94,7 +94,7 @@ public ResponseEntity handleFieldValidationException(FieldValidationException log.warn("[Field Validation Exception] {}: {}", field, message) ); ExceptionCode error = GlobalExceptionCode.FIELD_VALIDATION_ERROR; - return ResponseEntity.status(error.getStatus()).body(ExceptionRes.from(error, e.getFieldErrors())); + return ResponseEntity.status(error.getStatus()).body(ExceptionRes.from(error, new HashMap<>(e.getFieldErrors()))); } } \ No newline at end of file diff --git a/gotcha-socket/src/main/java/socket_server/common/exception/SocketGlobalExceptionHandler.java b/gotcha-socket/src/main/java/socket_server/common/exception/SocketGlobalExceptionHandler.java index 893d6e74..0642bcba 100644 --- a/gotcha-socket/src/main/java/socket_server/common/exception/SocketGlobalExceptionHandler.java +++ b/gotcha-socket/src/main/java/socket_server/common/exception/SocketGlobalExceptionHandler.java @@ -12,6 +12,8 @@ import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.ControllerAdvice; +import java.util.HashMap; + import static gotcha_common.exception.exceptionCode.GlobalExceptionCode.USER_NOT_FOUND; import static socket_server.common.constants.WebSocketConstants.*; @@ -27,7 +29,7 @@ public void handleSocketFieldValidationException( SimpMessageHeaderAccessor accessor ) { log.warn("[WebSocket FieldValidationException] {} - {}", e.getSource(), e.getFieldErrors()); - sendErrorToUser(e.getSource(), accessor, ExceptionRes.from(GlobalExceptionCode.FIELD_VALIDATION_ERROR, e.getFieldErrors())); + sendErrorToUser(e.getSource(), accessor, ExceptionRes.from(GlobalExceptionCode.FIELD_VALIDATION_ERROR, new HashMap<>(e.getFieldErrors()))); } @MessageExceptionHandler(SocketCustomException.class) From 3b356ffb0ed4fee9309190f8aec78dac64403034 Mon Sep 17 00:00:00 2001 From: brothergiven Date: Fri, 14 Nov 2025 01:43:27 +0900 Subject: [PATCH 16/39] =?UTF-8?q?fix:=20TokenDto=20=ED=95=84=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80(=EA=B2=BD=EA=B3=A0/=EC=A0=95=EC=A7=80=20?= =?UTF-8?q?=EB=A9=94=EC=8B=9C=EC=A7=80)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/gotcha_auth/dto/TokenDto.java | 12 +++++++++--- .../src/main/java/gotcha_auth/jwt/JwtHelper.java | 4 ++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/gotcha-auth/src/main/java/gotcha_auth/dto/TokenDto.java b/gotcha-auth/src/main/java/gotcha_auth/dto/TokenDto.java index 603ce077..c839428f 100644 --- a/gotcha-auth/src/main/java/gotcha_auth/dto/TokenDto.java +++ b/gotcha-auth/src/main/java/gotcha_auth/dto/TokenDto.java @@ -1,17 +1,23 @@ package gotcha_auth.dto; +import com.fasterxml.jackson.annotation.JsonInclude; import java.time.LocalDateTime; +@JsonInclude(JsonInclude.Include.NON_NULL) public record TokenDto( String accessToken, String refreshToken, LocalDateTime accessTokenExpiredAt, - boolean autoSignIn + boolean autoSignIn, + Object warningDetails ) { public static TokenDto of(String accessToken, String refreshToken, LocalDateTime accessTokenExpiredAt) { - return new TokenDto(accessToken, refreshToken, accessTokenExpiredAt, false); + return new TokenDto(accessToken, refreshToken, accessTokenExpiredAt, false, null); } public static TokenDto of(String accessToken, String refreshToken, LocalDateTime accessTokenExpiredAt, boolean autoSignIn) { - return new TokenDto(accessToken, refreshToken, accessTokenExpiredAt, autoSignIn); + return new TokenDto(accessToken, refreshToken, accessTokenExpiredAt, autoSignIn, null); + } + public static TokenDto of(String accessToken, String refreshToken, LocalDateTime accessTokenExpiredAt, boolean autoSignIn, Object warningDetails) { + return new TokenDto(accessToken, refreshToken, accessTokenExpiredAt, autoSignIn, warningDetails); } } \ No newline at end of file diff --git a/gotcha-auth/src/main/java/gotcha_auth/jwt/JwtHelper.java b/gotcha-auth/src/main/java/gotcha_auth/jwt/JwtHelper.java index d9a263ab..29ed3ba3 100644 --- a/gotcha-auth/src/main/java/gotcha_auth/jwt/JwtHelper.java +++ b/gotcha-auth/src/main/java/gotcha_auth/jwt/JwtHelper.java @@ -46,7 +46,7 @@ private TokenDto getTokenDto(User user, String uuid, String username, boolean au userService.updateLastLogout(user, accessTokenExpiredAt); refreshTokenService.saveRefreshToken(uuid, refreshToken); - return new TokenDto(accessToken, refreshToken, accessTokenExpiredAt, autoSignIn); + return TokenDto.of(accessToken, refreshToken, accessTokenExpiredAt, autoSignIn); } public TokenDto reissueToken(String refreshToken) { @@ -75,7 +75,7 @@ public TokenDto reissueToken(String refreshToken) { refreshTokenService.deleteRefreshToken(refreshToken); refreshTokenService.saveRefreshToken(uuid, newRefreshToken); - return new TokenDto(newAccessToken, newRefreshToken, newAccessTokenExpiredAt, autoSignIn); + return TokenDto.of(newAccessToken, newRefreshToken, newAccessTokenExpiredAt, autoSignIn); } public void removeToken(String accessToken, String refreshToken, HttpServletResponse response) { From d9c2cc9af8399cbfd27133bbbe811d17677992a7 Mon Sep 17 00:00:00 2001 From: brothergiven Date: Fri, 14 Nov 2025 01:43:51 +0900 Subject: [PATCH 17/39] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=8B=9C=20=EA=B2=BD=EA=B3=A0/=EC=A0=95=EC=A7=80=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EB=A1=9C=EC=A7=81=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/AuthController.java | 4 ++ .../auth/exception/AuthExceptionCode.java | 4 +- .../domain/auth/service/AuthService.java | 42 ++++++++++++++++++- 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/gotcha/src/main/java/Gotcha/domain/auth/controller/AuthController.java b/gotcha/src/main/java/Gotcha/domain/auth/controller/AuthController.java index 443b3da3..fe84320d 100644 --- a/gotcha/src/main/java/Gotcha/domain/auth/controller/AuthController.java +++ b/gotcha/src/main/java/Gotcha/domain/auth/controller/AuthController.java @@ -121,6 +121,10 @@ private ResponseEntity createTokenRes(TokenDto tokenDto, boolean autoSignIn) responseData.put("accessToken", tokenDto.accessToken()); responseData.put("expiredAt", tokenDto.accessTokenExpiredAt()); + if (tokenDto.warningDetails() != null) { + responseData.put("warningDetails", tokenDto.warningDetails()); + } + return ResponseEntity.ok() .header(HttpHeaders.SET_COOKIE, cookieUtil.createCookie(REFRESH_COOKIE_VALUE, diff --git a/gotcha/src/main/java/Gotcha/domain/auth/exception/AuthExceptionCode.java b/gotcha/src/main/java/Gotcha/domain/auth/exception/AuthExceptionCode.java index 55096bc4..9b02c0f4 100644 --- a/gotcha/src/main/java/Gotcha/domain/auth/exception/AuthExceptionCode.java +++ b/gotcha/src/main/java/Gotcha/domain/auth/exception/AuthExceptionCode.java @@ -9,7 +9,9 @@ public enum AuthExceptionCode implements ExceptionCode { INVALID_USERNAME_AND_PASSWORD(HttpStatus.NOT_FOUND, "AUTH-404-001", "아이디 또는 비밀번호가 유효하지 않습니다."), INVALID_GUEST(HttpStatus.BAD_REQUEST, "AUTH-400-001", "게스트가 아닙니다."), - INVALID_USERID(HttpStatus.NOT_FOUND, "AUTH-404-002", "존재하지 않는 사용자입니다."); + INVALID_USERID(HttpStatus.NOT_FOUND, "AUTH-404-002", "존재하지 않는 사용자입니다."), + ACCOUNT_SUSPENDED(HttpStatus.FORBIDDEN, "AUTH-403-002", "계정이 일시 정지되었습니다."), + ACCOUNT_BANNED(HttpStatus.FORBIDDEN, "AUTH-403-003", "계정이 영구 정지되었습니다."); private final HttpStatus status; private final String code; diff --git a/gotcha/src/main/java/Gotcha/domain/auth/service/AuthService.java b/gotcha/src/main/java/Gotcha/domain/auth/service/AuthService.java index 47f4bba8..29f14e86 100644 --- a/gotcha/src/main/java/Gotcha/domain/auth/service/AuthService.java +++ b/gotcha/src/main/java/Gotcha/domain/auth/service/AuthService.java @@ -4,14 +4,19 @@ import Gotcha.domain.auth.dto.SignUpReq; import Gotcha.domain.auth.exception.AuthExceptionCode; import Gotcha.domain.auth.util.RandomNicknameGenerator; +import Gotcha.domain.auth.exception.UserAccountStatusException; +import Gotcha.domain.sanction.service.SanctionService; import gotcha_auth.dto.TokenDto; import gotcha_auth.jwt.JwtHelper; import gotcha_common.exception.CustomException; import gotcha_common.exception.FieldValidationException; import gotcha_common.util.RedisUtil; import gotcha_domain.auth.SecurityUserDetails; +import Gotcha.domain.sanction.dto.SanctionRes; +import gotcha_domain.sanction.UserSanction; import gotcha_domain.user.Role; import gotcha_domain.user.User; +import gotcha_domain.user.UserStatus; import gotcha_user.repository.UserRepository; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; @@ -39,6 +44,7 @@ public class AuthService { private final PasswordEncoder passwordEncoder; private final JwtHelper jwtHelper; private final RedisUtil redisUtil; + private final SanctionService sanctionService; @Transactional public TokenDto guestSignUp(SignUpReq signUpReq, SecurityUserDetails userDetails){ @@ -78,11 +84,45 @@ public TokenDto signIn(SignInReq signInReq){ User user = userRepository.findByEmail(signInReq.email()) .orElseThrow(() -> new CustomException(AuthExceptionCode.INVALID_USERNAME_AND_PASSWORD)); + // 1. 제재/차단 상태 확인 (로그인 차단) + if (user.getUserStatus() == UserStatus.SUSPENDED || user.getUserStatus() == UserStatus.BANNED) { + Optional unreadSanctionOpt = sanctionService.findLatestUnreadSanction(user); + AuthExceptionCode code = user.getUserStatus() == UserStatus.SUSPENDED ? + AuthExceptionCode.ACCOUNT_SUSPENDED : AuthExceptionCode.ACCOUNT_BANNED; + + UserSanction sanction = unreadSanctionOpt.get(); + sanctionService.markSanctionAsRead(sanction); + + Map details = new HashMap<>(); + details.put("reason", sanction.getReason()); + if (sanction.getExpiresAt() != null) { + details.put("expiresAt", sanction.getExpiresAt()); + } + throw new UserAccountStatusException(code, details); + + } + + // 2. 비밀번호 확인 if(!passwordEncoder.matches(signInReq.password(), user.getPassword())){ throw new CustomException(AuthExceptionCode.INVALID_USERNAME_AND_PASSWORD); } - return jwtHelper.createToken(user, signInReq.autoSignIn()); + // 3. 경고 확인 (로그인 성공, 메시지 전달) + Optional unreadWarningOpt = sanctionService.findLatestUnreadWarning(user); + SanctionRes warningDetails = null; + if (unreadWarningOpt.isPresent()) { + UserSanction warning = unreadWarningOpt.get(); + sanctionService.markSanctionAsRead(warning); + warningDetails = SanctionRes.fromEntity(warning); + } + + // 4. 토큰 생성 및 경고 메시지 추가 + TokenDto tokenDto = jwtHelper.createToken(user, signInReq.autoSignIn()); + if (warningDetails != null) { + return TokenDto.of(tokenDto.accessToken(), tokenDto.refreshToken(), tokenDto.accessTokenExpiredAt(), tokenDto.autoSignIn(), warningDetails); + } + + return tokenDto; } public void signOut(String HeaderAccessToken, String refreshToken, HttpServletResponse response) { From ede73c87fc9e239bccae367dc88692437adf97f3 Mon Sep 17 00:00:00 2001 From: brothergiven Date: Fri, 14 Nov 2025 01:43:59 +0900 Subject: [PATCH 18/39] =?UTF-8?q?docs:=20Swagger=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/Gotcha/domain/auth/api/AuthApi.java | 45 ++++++++++++++++++- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/gotcha/src/main/java/Gotcha/domain/auth/api/AuthApi.java b/gotcha/src/main/java/Gotcha/domain/auth/api/AuthApi.java index 6a1cf207..296709a1 100644 --- a/gotcha/src/main/java/Gotcha/domain/auth/api/AuthApi.java +++ b/gotcha/src/main/java/Gotcha/domain/auth/api/AuthApi.java @@ -130,13 +130,54 @@ ResponseEntity guestSignUp(@Valid @RequestBody SignUpReq signUpReq, @Operation(summary = "로그인", description = "로그인 API") @ApiResponses({ - @ApiResponse(responseCode = "200", description = "로그인 성공", + @ApiResponse(responseCode = "200", description = "로그인 성공. 읽지 않은 경고가 있을 경우 warningDetails가 함께 반환됩니다.", content = @Content(mediaType = "application/json", examples = { - @ExampleObject(value = """ + @ExampleObject(name = "일반 로그인 성공", value = """ { "expiredAt": "2025-04-10T06:57:45", "accessToken": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0ZXN0QGdtYWlsLmNvbSIsInJvbGUiOiJVU0VSIiwidXNlcklkIjo1LCJpc3MiOiJnb3RjaGEhIiwiaWF0IjoxNzQ0MjY2NDY1LCJleHAiOjE3NDQyNjgyNjV9.u8RTE1VFsxZjQNB_dsc3ibSKqoHQGbC9-ppbOQUvzVY" } + """), + @ExampleObject(name = "경고 메시지가 있는 로그인 성공", value = """ + { + "accessToken": "Bearer eyJhbGciOiJIxzI1NiJ9...", + "refreshToken": "Bearer eyJhbGciOiJIUzI1NiJ9...", + "accessTokenExpiredAt": "2025-11-13T15:00:00", + "autoSignIn": false, + "warningDetails": { + "id": 1, + "targetUserName": "nickname123", + "adminUserName": "admin", + "sanctionType": "WARNING", + "reason": "부적절한 언어 사용", + "expiresAt": null, + "createdAt": "2025-11-13T14:00:00" + } + } + """) + })), + @ApiResponse(responseCode = "403", description = "계정이 정지됨. details에 상세 정보가 포함됩니다(일시 정지일 경우 만료 기간 포함).", + content = @Content(mediaType = "application/json", examples = { + @ExampleObject(name = "일시 정지", value = """ + { + "code": "AUTH-403-002", + "status": "FORBIDDEN", + "message": "계정이 일시 정지되었습니다.", + "details": { + "reason": "스팸 메시지 발송", + "expiresAt": "2025-12-25T00:00:00" + } + } + """), + @ExampleObject(name = "영구 정지", value = """ + { + "code": "AUTH-403-002", + "status": "FORBIDDEN", + "message": "계정이 영구 정지되었습니다.", + "details": { + "reason": "스팸 메시지 발송" + } + } """) })), @ApiResponse(responseCode = "422", description = "유효성검사 실패", From 4dd5e8156676d2334299d9c23c6fc7c529e64fac Mon Sep 17 00:00:00 2001 From: brothergiven Date: Fri, 14 Nov 2025 01:45:36 +0900 Subject: [PATCH 19/39] =?UTF-8?q?docs:=20=EC=98=A4=ED=83=80=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gotcha/src/main/java/Gotcha/domain/auth/api/AuthApi.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gotcha/src/main/java/Gotcha/domain/auth/api/AuthApi.java b/gotcha/src/main/java/Gotcha/domain/auth/api/AuthApi.java index 296709a1..1046d8fc 100644 --- a/gotcha/src/main/java/Gotcha/domain/auth/api/AuthApi.java +++ b/gotcha/src/main/java/Gotcha/domain/auth/api/AuthApi.java @@ -171,7 +171,7 @@ ResponseEntity guestSignUp(@Valid @RequestBody SignUpReq signUpReq, """), @ExampleObject(name = "영구 정지", value = """ { - "code": "AUTH-403-002", + "code": "AUTH-403-003", "status": "FORBIDDEN", "message": "계정이 영구 정지되었습니다.", "details": { From 8c456c128994d037207f67842e11dac04d34b93e Mon Sep 17 00:00:00 2001 From: LeeSeunghyeon <50231309+Uralauah@users.noreply.github.com> Date: Sun, 16 Nov 2025 16:44:08 +0900 Subject: [PATCH 20/39] =?UTF-8?q?fix:=20Optional=20=EC=98=88=EC=99=B8?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/Gotcha/domain/auth/exception/AuthExceptionCode.java | 3 ++- .../src/main/java/Gotcha/domain/auth/service/AuthService.java | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/gotcha/src/main/java/Gotcha/domain/auth/exception/AuthExceptionCode.java b/gotcha/src/main/java/Gotcha/domain/auth/exception/AuthExceptionCode.java index 9b02c0f4..8fd4ffe5 100644 --- a/gotcha/src/main/java/Gotcha/domain/auth/exception/AuthExceptionCode.java +++ b/gotcha/src/main/java/Gotcha/domain/auth/exception/AuthExceptionCode.java @@ -11,7 +11,8 @@ public enum AuthExceptionCode implements ExceptionCode { INVALID_GUEST(HttpStatus.BAD_REQUEST, "AUTH-400-001", "게스트가 아닙니다."), INVALID_USERID(HttpStatus.NOT_FOUND, "AUTH-404-002", "존재하지 않는 사용자입니다."), ACCOUNT_SUSPENDED(HttpStatus.FORBIDDEN, "AUTH-403-002", "계정이 일시 정지되었습니다."), - ACCOUNT_BANNED(HttpStatus.FORBIDDEN, "AUTH-403-003", "계정이 영구 정지되었습니다."); + ACCOUNT_BANNED(HttpStatus.FORBIDDEN, "AUTH-403-003", "계정이 영구 정지되었습니다."), + SANCTION_NOT_FOUND(HttpStatus.NOT_FOUND, "AUTH-404-003", "제재 내역이 존재하지 않습니다."); private final HttpStatus status; private final String code; diff --git a/gotcha/src/main/java/Gotcha/domain/auth/service/AuthService.java b/gotcha/src/main/java/Gotcha/domain/auth/service/AuthService.java index 29f14e86..24cde05d 100644 --- a/gotcha/src/main/java/Gotcha/domain/auth/service/AuthService.java +++ b/gotcha/src/main/java/Gotcha/domain/auth/service/AuthService.java @@ -86,11 +86,11 @@ public TokenDto signIn(SignInReq signInReq){ // 1. 제재/차단 상태 확인 (로그인 차단) if (user.getUserStatus() == UserStatus.SUSPENDED || user.getUserStatus() == UserStatus.BANNED) { - Optional unreadSanctionOpt = sanctionService.findLatestUnreadSanction(user); AuthExceptionCode code = user.getUserStatus() == UserStatus.SUSPENDED ? AuthExceptionCode.ACCOUNT_SUSPENDED : AuthExceptionCode.ACCOUNT_BANNED; - UserSanction sanction = unreadSanctionOpt.get(); + UserSanction sanction = sanctionService.findLatestUnreadSanction(user) + .orElseThrow(()->new CustomException(AuthExceptionCode.SANCTION_NOT_FOUND)); sanctionService.markSanctionAsRead(sanction); Map details = new HashMap<>(); From 06e557a9be877a5a4a99eb9ca0987f737db43cb6 Mon Sep 17 00:00:00 2001 From: LeeSeunghyeon <50231309+Uralauah@users.noreply.github.com> Date: Sun, 16 Nov 2025 16:45:05 +0900 Subject: [PATCH 21/39] =?UTF-8?q?fix:=20=EB=B6=88=ED=95=84=EC=9A=94=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/Gotcha/domain/auth/service/AuthService.java | 4 ++-- .../Gotcha/domain/sanction/service/SanctionService.java | 6 ------ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/gotcha/src/main/java/Gotcha/domain/auth/service/AuthService.java b/gotcha/src/main/java/Gotcha/domain/auth/service/AuthService.java index 24cde05d..8c97c7b4 100644 --- a/gotcha/src/main/java/Gotcha/domain/auth/service/AuthService.java +++ b/gotcha/src/main/java/Gotcha/domain/auth/service/AuthService.java @@ -91,7 +91,7 @@ public TokenDto signIn(SignInReq signInReq){ UserSanction sanction = sanctionService.findLatestUnreadSanction(user) .orElseThrow(()->new CustomException(AuthExceptionCode.SANCTION_NOT_FOUND)); - sanctionService.markSanctionAsRead(sanction); + sanction.markAsRead(); Map details = new HashMap<>(); details.put("reason", sanction.getReason()); @@ -112,7 +112,7 @@ public TokenDto signIn(SignInReq signInReq){ SanctionRes warningDetails = null; if (unreadWarningOpt.isPresent()) { UserSanction warning = unreadWarningOpt.get(); - sanctionService.markSanctionAsRead(warning); + warning.markAsRead(); warningDetails = SanctionRes.fromEntity(warning); } diff --git a/gotcha/src/main/java/Gotcha/domain/sanction/service/SanctionService.java b/gotcha/src/main/java/Gotcha/domain/sanction/service/SanctionService.java index aea3e877..21fb63a3 100644 --- a/gotcha/src/main/java/Gotcha/domain/sanction/service/SanctionService.java +++ b/gotcha/src/main/java/Gotcha/domain/sanction/service/SanctionService.java @@ -6,7 +6,6 @@ import Gotcha.domain.sanction.repository.SanctionRepository; import gotcha_domain.report.UserReport; import gotcha_domain.sanction.SanctionType; -import gotcha_domain.sanction.SanctionType; import gotcha_domain.sanction.UserSanction; import gotcha_domain.user.User; import gotcha_user.service.UserService; @@ -81,9 +80,4 @@ public Optional findLatestUnreadSanction(User user) { public Optional findLatestUnreadWarning(User user) { return sanctionRepository.findTopByUserAndSanctionTypeAndIsReadIsFalseOrderByCreatedAtDesc(user, SanctionType.WARNING); } - - @Transactional - public void markSanctionAsRead(UserSanction sanction) { - sanction.markAsRead(); - } } From f76e298b9ab0f9f151d10ae0e0c0806f6dd390d1 Mon Sep 17 00:00:00 2001 From: LeeSeunghyeon <50231309+Uralauah@users.noreply.github.com> Date: Sun, 16 Nov 2025 16:45:58 +0900 Subject: [PATCH 22/39] =?UTF-8?q?fix:=20warningCount=20=EA=B8=B0=EB=B3=B8?= =?UTF-8?q?=EA=B0=92=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gotcha-domain/src/main/java/gotcha_domain/user/User.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gotcha-domain/src/main/java/gotcha_domain/user/User.java b/gotcha-domain/src/main/java/gotcha_domain/user/User.java index d85e5588..ffc8067b 100644 --- a/gotcha-domain/src/main/java/gotcha_domain/user/User.java +++ b/gotcha-domain/src/main/java/gotcha_domain/user/User.java @@ -52,7 +52,7 @@ public class User extends BaseTimeEntity { @Enumerated(EnumType.STRING) private Role role; - private Integer warningCount; + private Integer warningCount = 0; @Setter private LocalDateTime lastLogout; @@ -147,7 +147,7 @@ public void updateChatSettings(ChatOption chatOption, PrivateChatOption privateC } public void incrementWarningCount() { - this.warningCount = (this.warningCount == null) ? 1 : this.warningCount + 1; + this.warningCount++; } public void suspendUser(long days) { From 0d744daf5ae9946eb72c282821aa0aea105299e9 Mon Sep 17 00:00:00 2001 From: LeeSeunghyeon <50231309+Uralauah@users.noreply.github.com> Date: Sun, 16 Nov 2025 16:51:24 +0900 Subject: [PATCH 23/39] =?UTF-8?q?fix:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=88=9C=EC=84=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Gotcha/domain/auth/service/AuthService.java | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/gotcha/src/main/java/Gotcha/domain/auth/service/AuthService.java b/gotcha/src/main/java/Gotcha/domain/auth/service/AuthService.java index 8c97c7b4..27f912ab 100644 --- a/gotcha/src/main/java/Gotcha/domain/auth/service/AuthService.java +++ b/gotcha/src/main/java/Gotcha/domain/auth/service/AuthService.java @@ -3,8 +3,9 @@ import Gotcha.domain.auth.dto.SignInReq; import Gotcha.domain.auth.dto.SignUpReq; import Gotcha.domain.auth.exception.AuthExceptionCode; -import Gotcha.domain.auth.util.RandomNicknameGenerator; import Gotcha.domain.auth.exception.UserAccountStatusException; +import Gotcha.domain.auth.util.RandomNicknameGenerator; +import Gotcha.domain.sanction.dto.SanctionRes; import Gotcha.domain.sanction.service.SanctionService; import gotcha_auth.dto.TokenDto; import gotcha_auth.jwt.JwtHelper; @@ -12,7 +13,6 @@ import gotcha_common.exception.FieldValidationException; import gotcha_common.util.RedisUtil; import gotcha_domain.auth.SecurityUserDetails; -import Gotcha.domain.sanction.dto.SanctionRes; import gotcha_domain.sanction.UserSanction; import gotcha_domain.user.Role; import gotcha_domain.user.User; @@ -84,7 +84,12 @@ public TokenDto signIn(SignInReq signInReq){ User user = userRepository.findByEmail(signInReq.email()) .orElseThrow(() -> new CustomException(AuthExceptionCode.INVALID_USERNAME_AND_PASSWORD)); - // 1. 제재/차단 상태 확인 (로그인 차단) + // 1. 비밀번호 확인 + if(!passwordEncoder.matches(signInReq.password(), user.getPassword())){ + throw new CustomException(AuthExceptionCode.INVALID_USERNAME_AND_PASSWORD); + } + + // 2. 제재/차단 상태 확인 (로그인 차단) if (user.getUserStatus() == UserStatus.SUSPENDED || user.getUserStatus() == UserStatus.BANNED) { AuthExceptionCode code = user.getUserStatus() == UserStatus.SUSPENDED ? AuthExceptionCode.ACCOUNT_SUSPENDED : AuthExceptionCode.ACCOUNT_BANNED; @@ -99,12 +104,6 @@ public TokenDto signIn(SignInReq signInReq){ details.put("expiresAt", sanction.getExpiresAt()); } throw new UserAccountStatusException(code, details); - - } - - // 2. 비밀번호 확인 - if(!passwordEncoder.matches(signInReq.password(), user.getPassword())){ - throw new CustomException(AuthExceptionCode.INVALID_USERNAME_AND_PASSWORD); } // 3. 경고 확인 (로그인 성공, 메시지 전달) From c640f7f44f4c04dad6a9ea7c2ff472ad229468f0 Mon Sep 17 00:00:00 2001 From: LeeSeunghyeon <50231309+Uralauah@users.noreply.github.com> Date: Sun, 16 Nov 2025 19:29:37 +0900 Subject: [PATCH 24/39] =?UTF-8?q?fix:=20=ED=95=A8=EC=88=98=20=EC=98=A4?= =?UTF-8?q?=EB=B2=84=EB=A1=9C=EB=94=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/Gotcha/domain/auth/service/AuthService.java | 5 +++-- .../Gotcha/domain/sanction/service/SanctionService.java | 7 ++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/gotcha/src/main/java/Gotcha/domain/auth/service/AuthService.java b/gotcha/src/main/java/Gotcha/domain/auth/service/AuthService.java index 27f912ab..5be16491 100644 --- a/gotcha/src/main/java/Gotcha/domain/auth/service/AuthService.java +++ b/gotcha/src/main/java/Gotcha/domain/auth/service/AuthService.java @@ -13,6 +13,7 @@ import gotcha_common.exception.FieldValidationException; import gotcha_common.util.RedisUtil; import gotcha_domain.auth.SecurityUserDetails; +import gotcha_domain.sanction.SanctionType; import gotcha_domain.sanction.UserSanction; import gotcha_domain.user.Role; import gotcha_domain.user.User; @@ -94,7 +95,7 @@ public TokenDto signIn(SignInReq signInReq){ AuthExceptionCode code = user.getUserStatus() == UserStatus.SUSPENDED ? AuthExceptionCode.ACCOUNT_SUSPENDED : AuthExceptionCode.ACCOUNT_BANNED; - UserSanction sanction = sanctionService.findLatestUnreadSanction(user) + UserSanction sanction = sanctionService.findLatestUnread(user) .orElseThrow(()->new CustomException(AuthExceptionCode.SANCTION_NOT_FOUND)); sanction.markAsRead(); @@ -107,7 +108,7 @@ public TokenDto signIn(SignInReq signInReq){ } // 3. 경고 확인 (로그인 성공, 메시지 전달) - Optional unreadWarningOpt = sanctionService.findLatestUnreadWarning(user); + Optional unreadWarningOpt = sanctionService.findLatestUnread(user, SanctionType.WARNING); SanctionRes warningDetails = null; if (unreadWarningOpt.isPresent()) { UserSanction warning = unreadWarningOpt.get(); diff --git a/gotcha/src/main/java/Gotcha/domain/sanction/service/SanctionService.java b/gotcha/src/main/java/Gotcha/domain/sanction/service/SanctionService.java index 21fb63a3..e90410a1 100644 --- a/gotcha/src/main/java/Gotcha/domain/sanction/service/SanctionService.java +++ b/gotcha/src/main/java/Gotcha/domain/sanction/service/SanctionService.java @@ -73,11 +73,12 @@ public SanctionRes sanctionUser(SanctionReq sanctionReq, String adminId) { return SanctionRes.fromEntity(userSanction); } - public Optional findLatestUnreadSanction(User user) { + public Optional findLatestUnread(User user) { return sanctionRepository.findTopByUserAndIsReadIsFalseOrderByCreatedAtDesc(user); } - public Optional findLatestUnreadWarning(User user) { - return sanctionRepository.findTopByUserAndSanctionTypeAndIsReadIsFalseOrderByCreatedAtDesc(user, SanctionType.WARNING); + public Optional findLatestUnread(User user, SanctionType type) { + return sanctionRepository.findTopByUserAndSanctionTypeAndIsReadIsFalseOrderByCreatedAtDesc(user, type); } + } From 528dc55e50089267efec1cc2a67b7212dd8a4a19 Mon Sep 17 00:00:00 2001 From: LeeSeunghyeon <50231309+Uralauah@users.noreply.github.com> Date: Sun, 16 Nov 2025 19:34:21 +0900 Subject: [PATCH 25/39] =?UTF-8?q?fix:=20=EC=B1=85=EC=9E=84=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gotcha_domain/sanction/SanctionType.java | 25 ++++++++++++++++--- .../sanction/service/SanctionService.java | 21 ++-------------- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/gotcha-domain/src/main/java/gotcha_domain/sanction/SanctionType.java b/gotcha-domain/src/main/java/gotcha_domain/sanction/SanctionType.java index e14c9036..d09ea408 100644 --- a/gotcha-domain/src/main/java/gotcha_domain/sanction/SanctionType.java +++ b/gotcha-domain/src/main/java/gotcha_domain/sanction/SanctionType.java @@ -1,7 +1,26 @@ package gotcha_domain.sanction; +import gotcha_domain.user.User; + public enum SanctionType { - WARNING, // 경고 - TEMP_BAN, // 일시 정지 - PERM_BAN // 영구 정지 + WARNING { + @Override + public void apply(User user, Long days) { + user.incrementWarningCount(); + } + }, + TEMP_BAN { + @Override + public void apply(User user, Long days) { + user.suspendUser(days); + } + }, + PERM_BAN { + @Override + public void apply(User user, Long days) { + user.banUser(); + } + }; + + public abstract void apply(User user, Long days); } diff --git a/gotcha/src/main/java/Gotcha/domain/sanction/service/SanctionService.java b/gotcha/src/main/java/Gotcha/domain/sanction/service/SanctionService.java index e90410a1..8b802313 100644 --- a/gotcha/src/main/java/Gotcha/domain/sanction/service/SanctionService.java +++ b/gotcha/src/main/java/Gotcha/domain/sanction/service/SanctionService.java @@ -13,7 +13,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.time.LocalDateTime; import java.util.Optional; @Service @@ -39,24 +38,8 @@ public SanctionRes sanctionUser(SanctionReq sanctionReq, String adminId) { } // 3. 제재 유형에 따른 로직 처리 - LocalDateTime expiresAt = null; // 제재 만료 시간 SanctionType sanctionType = sanctionReq.getSanctionType(); - switch(sanctionType){ - case WARNING: - targetUser.incrementWarningCount(); - // 경고는 만료 시간이 없음 - break; - case TEMP_BAN: - // 임시 정지는 현재 시간에서 지정된 기간만큼 더함 - Long durationDays = sanctionReq.getDurationDays(); - expiresAt = LocalDateTime.now().plusDays(durationDays); - targetUser.suspendUser(durationDays); - break; - case PERM_BAN: - // 영구 정지는 만료 시간이 없음 - targetUser.banUser(); - break; - } + sanctionType.apply(targetUser, sanctionReq.getDurationDays()); // 4. 제재 기록 저장 UserSanction userSanction = UserSanction.builder() @@ -64,7 +47,7 @@ public SanctionRes sanctionUser(SanctionReq sanctionReq, String adminId) { .admin(adminUser) .sanctionType(sanctionType) .reason(sanctionReq.getReason()) - .expiresAt(expiresAt) + .expiresAt(targetUser.getSuspensionEndDate()) .userReport(sourceReport) .build(); From 43b7b9991f6175701faa7094d5f3ff89772d6ba0 Mon Sep 17 00:00:00 2001 From: LeeSeunghyeon <50231309+Uralauah@users.noreply.github.com> Date: Sun, 16 Nov 2025 19:48:01 +0900 Subject: [PATCH 26/39] =?UTF-8?q?fix:=20=EC=A0=9C=EC=9E=AC=20=EA=B8=B0?= =?UTF-8?q?=EA=B0=84=20=ED=95=84=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gotcha_domain/sanction/UserSanction.java | 21 ++++++++++++------- .../sanction/service/SanctionService.java | 2 +- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/gotcha-domain/src/main/java/gotcha_domain/sanction/UserSanction.java b/gotcha-domain/src/main/java/gotcha_domain/sanction/UserSanction.java index 232acdf1..3bf4c2c0 100644 --- a/gotcha-domain/src/main/java/gotcha_domain/sanction/UserSanction.java +++ b/gotcha-domain/src/main/java/gotcha_domain/sanction/UserSanction.java @@ -3,14 +3,21 @@ import gotcha_common.entity.BaseTimeEntity; import gotcha_domain.report.UserReport; import gotcha_domain.user.User; -import jakarta.persistence.*; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import java.time.LocalDateTime; - @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -38,8 +45,8 @@ public class UserSanction extends BaseTimeEntity { private String reason; // 제재 만료 일시 - @Column(name = "expiry_date") - private LocalDateTime expiresAt; + @Column(name = "expire_duration") + private Long expireDuration; // 근거가 된 신고(nullable) @ManyToOne(fetch = FetchType.LAZY) @@ -50,12 +57,12 @@ public class UserSanction extends BaseTimeEntity { private boolean isRead = false; @Builder - public UserSanction(User user, User admin, SanctionType sanctionType, String reason, LocalDateTime expiresAt, UserReport userReport) { + public UserSanction(User user, User admin, SanctionType sanctionType, String reason, Long expireDuration, UserReport userReport) { this.user = user; this.admin = admin; this.sanctionType = sanctionType; this.reason = reason; - this.expiresAt = expiresAt; + this.expireDuration = expireDuration; this.userReport = userReport; } diff --git a/gotcha/src/main/java/Gotcha/domain/sanction/service/SanctionService.java b/gotcha/src/main/java/Gotcha/domain/sanction/service/SanctionService.java index 8b802313..a6cabd4c 100644 --- a/gotcha/src/main/java/Gotcha/domain/sanction/service/SanctionService.java +++ b/gotcha/src/main/java/Gotcha/domain/sanction/service/SanctionService.java @@ -47,7 +47,7 @@ public SanctionRes sanctionUser(SanctionReq sanctionReq, String adminId) { .admin(adminUser) .sanctionType(sanctionType) .reason(sanctionReq.getReason()) - .expiresAt(targetUser.getSuspensionEndDate()) + .expireDuration(sanctionReq.getDurationDays()) .userReport(sourceReport) .build(); From e21209271c28196eb484dc1ad7936620ff06816e Mon Sep 17 00:00:00 2001 From: LeeSeunghyeon <50231309+Uralauah@users.noreply.github.com> Date: Sun, 16 Nov 2025 19:53:57 +0900 Subject: [PATCH 27/39] =?UTF-8?q?fix:=20=EC=A0=9C=EC=9E=AC=20=EA=B8=B0?= =?UTF-8?q?=EA=B0=84=20=ED=95=84=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/Gotcha/domain/auth/service/AuthService.java | 4 ++-- .../src/main/java/Gotcha/domain/sanction/dto/SanctionRes.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gotcha/src/main/java/Gotcha/domain/auth/service/AuthService.java b/gotcha/src/main/java/Gotcha/domain/auth/service/AuthService.java index 5be16491..df878c8b 100644 --- a/gotcha/src/main/java/Gotcha/domain/auth/service/AuthService.java +++ b/gotcha/src/main/java/Gotcha/domain/auth/service/AuthService.java @@ -101,8 +101,8 @@ public TokenDto signIn(SignInReq signInReq){ Map details = new HashMap<>(); details.put("reason", sanction.getReason()); - if (sanction.getExpiresAt() != null) { - details.put("expiresAt", sanction.getExpiresAt()); + if (sanction.getExpireDuration() != null) { + details.put("expireDuration", sanction.getExpireDuration()); } throw new UserAccountStatusException(code, details); } diff --git a/gotcha/src/main/java/Gotcha/domain/sanction/dto/SanctionRes.java b/gotcha/src/main/java/Gotcha/domain/sanction/dto/SanctionRes.java index e11b788a..86d66d37 100644 --- a/gotcha/src/main/java/Gotcha/domain/sanction/dto/SanctionRes.java +++ b/gotcha/src/main/java/Gotcha/domain/sanction/dto/SanctionRes.java @@ -14,7 +14,7 @@ public class SanctionRes { private String adminUserName; private SanctionType sanctionType; private String reason; - private LocalDateTime expiresAt; + private Long expireDuration; private LocalDateTime createdAt; public static SanctionRes fromEntity(gotcha_domain.sanction.UserSanction sanction) { @@ -24,7 +24,7 @@ public static SanctionRes fromEntity(gotcha_domain.sanction.UserSanction sanctio .adminUserName(sanction.getAdmin().getNickname()) .sanctionType(sanction.getSanctionType()) .reason(sanction.getReason()) - .expiresAt(sanction.getExpiresAt()) + .expireDuration(sanction.getExpireDuration()) .createdAt(sanction.getCreatedAt()) .build(); } From 2a072ba41e36115af556a594841dc79d46a51bf8 Mon Sep 17 00:00:00 2001 From: LeeSeunghyeon <50231309+Uralauah@users.noreply.github.com> Date: Sun, 16 Nov 2025 20:20:07 +0900 Subject: [PATCH 28/39] =?UTF-8?q?feat:=20=EC=8B=A0=EA=B3=A0=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EC=82=AC=EC=9A=A9=EC=9E=90=20uuid=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/Gotcha/domain/report/dto/UserReportRes.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gotcha/src/main/java/Gotcha/domain/report/dto/UserReportRes.java b/gotcha/src/main/java/Gotcha/domain/report/dto/UserReportRes.java index 308c876e..e537a9ed 100644 --- a/gotcha/src/main/java/Gotcha/domain/report/dto/UserReportRes.java +++ b/gotcha/src/main/java/Gotcha/domain/report/dto/UserReportRes.java @@ -9,6 +9,7 @@ public record UserReportRes ( Long userReportId, + String reportedUserUuid, LocalDateTime reportedAt, String nickname, UserReportType reportType, @@ -18,6 +19,7 @@ public record UserReportRes ( public static UserReportRes from(UserReport userReport) { return new UserReportRes( userReport.getId(), + userReport.getUser().getUuid(), userReport.getCreatedAt(), userReport.getUser().getNickname(), userReport.getUserReportType(), From a6c925b24a356406ed66bfee5b9e452df8bac34e Mon Sep 17 00:00:00 2001 From: LeeSeunghyeon <50231309+Uralauah@users.noreply.github.com> Date: Sun, 16 Nov 2025 20:22:08 +0900 Subject: [PATCH 29/39] =?UTF-8?q?docs:=20SanctionReq=20=ED=95=84=EB=93=9C?= =?UTF-8?q?=20=EC=84=A4=EB=AA=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/Gotcha/domain/sanction/dto/SanctionReq.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gotcha/src/main/java/Gotcha/domain/sanction/dto/SanctionReq.java b/gotcha/src/main/java/Gotcha/domain/sanction/dto/SanctionReq.java index f14e696b..f0d47c58 100644 --- a/gotcha/src/main/java/Gotcha/domain/sanction/dto/SanctionReq.java +++ b/gotcha/src/main/java/Gotcha/domain/sanction/dto/SanctionReq.java @@ -10,7 +10,7 @@ @NoArgsConstructor public class SanctionReq { - @NotBlank(message = "제재 대상의 ID는 필수입니다.") + @NotBlank(message = "제재 대상의 UUID는 필수입니다.") private String targetUserId; // adminUserId 필드는 보안을 위해 제거하고, Controller에서 직접 인증 정보를 사용합니다. From 61e758a8ffcfc422b8c43515d5af8090f15ec5fe Mon Sep 17 00:00:00 2001 From: brothergiven Date: Sun, 16 Nov 2025 20:26:19 +0900 Subject: [PATCH 30/39] =?UTF-8?q?fix:=20QnA=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=ED=81=AC=EA=B8=B0=205=EB=A1=9C?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/Gotcha/domain/inquiry/service/InquiryService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gotcha/src/main/java/Gotcha/domain/inquiry/service/InquiryService.java b/gotcha/src/main/java/Gotcha/domain/inquiry/service/InquiryService.java index 2e439072..ca84c6d1 100644 --- a/gotcha/src/main/java/Gotcha/domain/inquiry/service/InquiryService.java +++ b/gotcha/src/main/java/Gotcha/domain/inquiry/service/InquiryService.java @@ -25,7 +25,7 @@ public class InquiryService { private final InquiryRepository inquiryRepository; - private static final Integer INQUIRIES_PER_PAGE = 10; + private static final Integer INQUIRIES_PER_PAGE = 5; @Transactional(readOnly = true) public Page getInquiries(String keyword, Integer page, InquirySortType sort, Boolean isSolved) { From 4faa9164d2f38c3781fdb49f6c7068c3dcb43a3d Mon Sep 17 00:00:00 2001 From: LeeSeunghyeon <50231309+Uralauah@users.noreply.github.com> Date: Sun, 16 Nov 2025 20:46:15 +0900 Subject: [PATCH 31/39] =?UTF-8?q?fix:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=B1=85=EC=9E=84=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/AuthController.java | 4 +- .../domain/auth/service/AuthService.java | 45 +------------------ .../domain/auth/service/SignInUseCase.java | 41 +++++++++++++++++ .../sanction/service/SanctionService.java | 34 ++++++++++++++ 4 files changed, 80 insertions(+), 44 deletions(-) create mode 100644 gotcha/src/main/java/Gotcha/domain/auth/service/SignInUseCase.java diff --git a/gotcha/src/main/java/Gotcha/domain/auth/controller/AuthController.java b/gotcha/src/main/java/Gotcha/domain/auth/controller/AuthController.java index fe84320d..50f52ba0 100644 --- a/gotcha/src/main/java/Gotcha/domain/auth/controller/AuthController.java +++ b/gotcha/src/main/java/Gotcha/domain/auth/controller/AuthController.java @@ -5,6 +5,7 @@ import Gotcha.domain.auth.dto.SignInReq; import Gotcha.domain.auth.dto.SignUpReq; import Gotcha.domain.auth.service.AuthService; +import Gotcha.domain.auth.service.SignInUseCase; import gotcha_domain.auth.SecurityUserDetails; import gotcha_auth.dto.TokenDto; import gotcha_auth.exception.JwtExceptionCode; @@ -39,6 +40,7 @@ @RequestMapping("/api/v1/auth") public class AuthController implements AuthApi { private final AuthService authService; + private final SignInUseCase signInUseCase; private final CookieUtil cookieUtil; private final MailCodeService mailCodeService; private final UserService userService; @@ -62,7 +64,7 @@ public ResponseEntity guestSignUp(@Valid @RequestBody SignUpReq signUpReq, @PostMapping("/sign-in") public ResponseEntity signIn(@Valid @RequestBody SignInReq signInReq) { - TokenDto tokenDto = authService.signIn(signInReq); + TokenDto tokenDto = signInUseCase.execute(signInReq); return createTokenRes(tokenDto, signInReq.autoSignIn()); } diff --git a/gotcha/src/main/java/Gotcha/domain/auth/service/AuthService.java b/gotcha/src/main/java/Gotcha/domain/auth/service/AuthService.java index df878c8b..73bda03e 100644 --- a/gotcha/src/main/java/Gotcha/domain/auth/service/AuthService.java +++ b/gotcha/src/main/java/Gotcha/domain/auth/service/AuthService.java @@ -3,21 +3,15 @@ import Gotcha.domain.auth.dto.SignInReq; import Gotcha.domain.auth.dto.SignUpReq; import Gotcha.domain.auth.exception.AuthExceptionCode; -import Gotcha.domain.auth.exception.UserAccountStatusException; import Gotcha.domain.auth.util.RandomNicknameGenerator; -import Gotcha.domain.sanction.dto.SanctionRes; -import Gotcha.domain.sanction.service.SanctionService; import gotcha_auth.dto.TokenDto; import gotcha_auth.jwt.JwtHelper; import gotcha_common.exception.CustomException; import gotcha_common.exception.FieldValidationException; import gotcha_common.util.RedisUtil; import gotcha_domain.auth.SecurityUserDetails; -import gotcha_domain.sanction.SanctionType; -import gotcha_domain.sanction.UserSanction; import gotcha_domain.user.Role; import gotcha_domain.user.User; -import gotcha_domain.user.UserStatus; import gotcha_user.repository.UserRepository; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; @@ -45,7 +39,6 @@ public class AuthService { private final PasswordEncoder passwordEncoder; private final JwtHelper jwtHelper; private final RedisUtil redisUtil; - private final SanctionService sanctionService; @Transactional public TokenDto guestSignUp(SignUpReq signUpReq, SecurityUserDetails userDetails){ @@ -81,48 +74,14 @@ public TokenDto signUp(SignUpReq signUpReq) { } @Transactional - public TokenDto signIn(SignInReq signInReq){ + public User authenticate(SignInReq signInReq){ User user = userRepository.findByEmail(signInReq.email()) .orElseThrow(() -> new CustomException(AuthExceptionCode.INVALID_USERNAME_AND_PASSWORD)); - // 1. 비밀번호 확인 if(!passwordEncoder.matches(signInReq.password(), user.getPassword())){ throw new CustomException(AuthExceptionCode.INVALID_USERNAME_AND_PASSWORD); } - - // 2. 제재/차단 상태 확인 (로그인 차단) - if (user.getUserStatus() == UserStatus.SUSPENDED || user.getUserStatus() == UserStatus.BANNED) { - AuthExceptionCode code = user.getUserStatus() == UserStatus.SUSPENDED ? - AuthExceptionCode.ACCOUNT_SUSPENDED : AuthExceptionCode.ACCOUNT_BANNED; - - UserSanction sanction = sanctionService.findLatestUnread(user) - .orElseThrow(()->new CustomException(AuthExceptionCode.SANCTION_NOT_FOUND)); - sanction.markAsRead(); - - Map details = new HashMap<>(); - details.put("reason", sanction.getReason()); - if (sanction.getExpireDuration() != null) { - details.put("expireDuration", sanction.getExpireDuration()); - } - throw new UserAccountStatusException(code, details); - } - - // 3. 경고 확인 (로그인 성공, 메시지 전달) - Optional unreadWarningOpt = sanctionService.findLatestUnread(user, SanctionType.WARNING); - SanctionRes warningDetails = null; - if (unreadWarningOpt.isPresent()) { - UserSanction warning = unreadWarningOpt.get(); - warning.markAsRead(); - warningDetails = SanctionRes.fromEntity(warning); - } - - // 4. 토큰 생성 및 경고 메시지 추가 - TokenDto tokenDto = jwtHelper.createToken(user, signInReq.autoSignIn()); - if (warningDetails != null) { - return TokenDto.of(tokenDto.accessToken(), tokenDto.refreshToken(), tokenDto.accessTokenExpiredAt(), tokenDto.autoSignIn(), warningDetails); - } - - return tokenDto; + return user; } public void signOut(String HeaderAccessToken, String refreshToken, HttpServletResponse response) { diff --git a/gotcha/src/main/java/Gotcha/domain/auth/service/SignInUseCase.java b/gotcha/src/main/java/Gotcha/domain/auth/service/SignInUseCase.java new file mode 100644 index 00000000..c32d0519 --- /dev/null +++ b/gotcha/src/main/java/Gotcha/domain/auth/service/SignInUseCase.java @@ -0,0 +1,41 @@ +package Gotcha.domain.auth.service; + +import Gotcha.domain.auth.dto.SignInReq; +import Gotcha.domain.sanction.dto.SanctionRes; +import Gotcha.domain.sanction.service.SanctionService; +import gotcha_auth.dto.TokenDto; +import gotcha_auth.jwt.JwtHelper; +import gotcha_domain.user.User; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class SignInUseCase { + private final AuthService authService; + private final SanctionService sanctionService; + private final JwtHelper jwtHelper; + + @Transactional + public TokenDto execute(SignInReq signInReq) { + // 1. 사용자 인증 + User user = authService.authenticate(signInReq); + + // 2. 제재/차단 상태 확인 + sanctionService.validateLoginAccess(user); + + // 3. 경고 확인 + Optional warningOpt = sanctionService.findAndMarkUnreadWarning(user); + + // 4. 토큰 생성 + TokenDto tokenDto = jwtHelper.createToken(user, signInReq.autoSignIn()); + + // 5. 경고 메시지가 있으면 토큰에 추가하여 반환 + return warningOpt + .map(warning -> TokenDto.of(tokenDto.accessToken(), tokenDto.refreshToken(), tokenDto.accessTokenExpiredAt(), tokenDto.autoSignIn(), warning)) + .orElse(tokenDto); + } +} \ No newline at end of file diff --git a/gotcha/src/main/java/Gotcha/domain/sanction/service/SanctionService.java b/gotcha/src/main/java/Gotcha/domain/sanction/service/SanctionService.java index a6cabd4c..40aa161d 100644 --- a/gotcha/src/main/java/Gotcha/domain/sanction/service/SanctionService.java +++ b/gotcha/src/main/java/Gotcha/domain/sanction/service/SanctionService.java @@ -1,18 +1,24 @@ package Gotcha.domain.sanction.service; +import Gotcha.domain.auth.exception.AuthExceptionCode; +import Gotcha.domain.auth.exception.UserAccountStatusException; import Gotcha.domain.report.service.UserReportService; import Gotcha.domain.sanction.dto.SanctionReq; import Gotcha.domain.sanction.dto.SanctionRes; import Gotcha.domain.sanction.repository.SanctionRepository; +import gotcha_common.exception.CustomException; import gotcha_domain.report.UserReport; import gotcha_domain.sanction.SanctionType; import gotcha_domain.sanction.UserSanction; import gotcha_domain.user.User; +import gotcha_domain.user.UserStatus; import gotcha_user.service.UserService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.HashMap; +import java.util.Map; import java.util.Optional; @Service @@ -56,6 +62,34 @@ public SanctionRes sanctionUser(SanctionReq sanctionReq, String adminId) { return SanctionRes.fromEntity(userSanction); } + @Transactional + public void validateLoginAccess(User user) { + if (user.getUserStatus() == UserStatus.SUSPENDED || user.getUserStatus() == UserStatus.BANNED) { + AuthExceptionCode code = user.getUserStatus() == UserStatus.SUSPENDED ? + AuthExceptionCode.ACCOUNT_SUSPENDED : AuthExceptionCode.ACCOUNT_BANNED; + + UserSanction sanction = findLatestUnread(user) + .orElseThrow(()->new CustomException(AuthExceptionCode.SANCTION_NOT_FOUND)); + sanction.markAsRead(); + + Map details = new HashMap<>(); + details.put("reason", sanction.getReason()); + if (sanction.getExpireDuration() != null) { + details.put("expireDuration", sanction.getExpireDuration()); + } + throw new UserAccountStatusException(code, details); + } + } + + @Transactional + public Optional findAndMarkUnreadWarning(User user) { + return findLatestUnread(user, SanctionType.WARNING) + .map(warning -> { + warning.markAsRead(); + return SanctionRes.fromEntity(warning); + }); + } + public Optional findLatestUnread(User user) { return sanctionRepository.findTopByUserAndIsReadIsFalseOrderByCreatedAtDesc(user); } From e4ebcb337a9d725bcbeec055adf57242f5f71fe0 Mon Sep 17 00:00:00 2001 From: minju Date: Mon, 17 Nov 2025 18:55:16 +0900 Subject: [PATCH 32/39] =?UTF-8?q?docs=20:=20=EC=8A=A4=EC=9B=A8=EA=B1=B0=20?= =?UTF-8?q?=EC=84=A4=EB=AA=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/Gotcha/domain/sanction/api/SanctionApi.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/gotcha/src/main/java/Gotcha/domain/sanction/api/SanctionApi.java b/gotcha/src/main/java/Gotcha/domain/sanction/api/SanctionApi.java index a0550677..cdc30e55 100644 --- a/gotcha/src/main/java/Gotcha/domain/sanction/api/SanctionApi.java +++ b/gotcha/src/main/java/Gotcha/domain/sanction/api/SanctionApi.java @@ -19,7 +19,17 @@ @Tag(name = "[관리자 제재 API]", description = "관리자용 사용자 제재 관련 API") public interface SanctionApi { - @Operation(summary = "사용자 제재 적용 API", description = "특정 사용자에게 경고, 임시 정지, 영구 정지 등의 제재를 가합니다.") + @Operation(summary = "사용자 제재 적용 API", description = "특정 사용자에게 제재(경고, 임시 정지, 영구 정지)를 부여합니다.\n" + + "\n" + + "- targetUserUuid: 제재 대상 사용자의 UUID입니다.\n" + + "- sanctionType: 제재 유형을 나타내는 Enum입니다.\n" + + " • WARNING : 경고 1회를 부여합니다.\n" + + " • TEMP_BAN : 일정 기간 동안 계정을 정지시킵니다. durationDays가 필요합니다.\n" + + " • TEMP_BAN_CANCEL : 일정 기간 동안 정지된 계정을 다시 활성화 시킵니다. 정지 상태가 아니었다면 에러가 반환됩니다.\n" + + " • PERM_BAN : 계정을 영구 정지(영구 차단)합니다.\n" + + "\n" + + "- sourceReportId: 제재의 근거가 되는 신고 ID입니다. 제재가 신고 기반일 경우 사용되며, 없을 수도 있습니다.\n" + + "- durationDays: TEMP_BAN 제재 시 적용되는 정지 기간(일 단위)입니다. WARNING, PERM_BAN에서는 사용되지 않습니다.\n") @SecurityRequirement(name = "bearerAuth") @ApiResponses({ @ApiResponse(responseCode = "201", description = "제재 조치 성공", From fe99db9138b6459c5a20b7b99f029f88955c8349 Mon Sep 17 00:00:00 2001 From: minju Date: Mon, 17 Nov 2025 18:56:01 +0900 Subject: [PATCH 33/39] =?UTF-8?q?style=20:=20=EB=B3=80=EC=88=98=EB=AA=85?= =?UTF-8?q?=20userUuid=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/Gotcha/domain/sanction/dto/SanctionReq.java | 2 +- .../java/Gotcha/domain/sanction/service/SanctionService.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gotcha/src/main/java/Gotcha/domain/sanction/dto/SanctionReq.java b/gotcha/src/main/java/Gotcha/domain/sanction/dto/SanctionReq.java index f0d47c58..4185f426 100644 --- a/gotcha/src/main/java/Gotcha/domain/sanction/dto/SanctionReq.java +++ b/gotcha/src/main/java/Gotcha/domain/sanction/dto/SanctionReq.java @@ -11,7 +11,7 @@ public class SanctionReq { @NotBlank(message = "제재 대상의 UUID는 필수입니다.") - private String targetUserId; + private String targetUserUuId; // adminUserId 필드는 보안을 위해 제거하고, Controller에서 직접 인증 정보를 사용합니다. diff --git a/gotcha/src/main/java/Gotcha/domain/sanction/service/SanctionService.java b/gotcha/src/main/java/Gotcha/domain/sanction/service/SanctionService.java index 40aa161d..61005886 100644 --- a/gotcha/src/main/java/Gotcha/domain/sanction/service/SanctionService.java +++ b/gotcha/src/main/java/Gotcha/domain/sanction/service/SanctionService.java @@ -35,7 +35,7 @@ public SanctionRes sanctionUser(SanctionReq sanctionReq, String adminId) { User adminUser = userService.findUserByUuid(adminId); //1. target User 조회 - User targetUser = userService.findUserByUuid(sanctionReq.getTargetUserId()); + User targetUser = userService.findUserByUuid(sanctionReq.getTargetUserUuId()); // 2. source Report 조회 (존재한다면) UserReport sourceReport = null; From 695deb3f0536710c24a9ba9c2f7386c4285ae879 Mon Sep 17 00:00:00 2001 From: minju Date: Mon, 17 Nov 2025 19:21:53 +0900 Subject: [PATCH 34/39] =?UTF-8?q?add=20:=20=EC=A0=95=EC=A7=80=20=EC=B7=A8?= =?UTF-8?q?=EC=86=8C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gotcha_domain/sanction/SanctionType.java | 6 ++++ .../main/java/gotcha_domain/user/User.java | 6 ++++ .../exception/SanctionExceptionCode.java | 30 +++++++++++++++++++ .../sanction/service/SanctionService.java | 6 ++++ 4 files changed, 48 insertions(+) create mode 100644 gotcha/src/main/java/Gotcha/domain/sanction/exception/SanctionExceptionCode.java diff --git a/gotcha-domain/src/main/java/gotcha_domain/sanction/SanctionType.java b/gotcha-domain/src/main/java/gotcha_domain/sanction/SanctionType.java index d09ea408..569eaf96 100644 --- a/gotcha-domain/src/main/java/gotcha_domain/sanction/SanctionType.java +++ b/gotcha-domain/src/main/java/gotcha_domain/sanction/SanctionType.java @@ -15,6 +15,12 @@ public void apply(User user, Long days) { user.suspendUser(days); } }, + TEMP_BAN_CANCEL { + @Override + public void apply(User user, Long days) { + user.unsuspendUser(); // 정지 취소(즉시 해제) + } + }, PERM_BAN { @Override public void apply(User user, Long days) { diff --git a/gotcha-domain/src/main/java/gotcha_domain/user/User.java b/gotcha-domain/src/main/java/gotcha_domain/user/User.java index 66d326d7..04446594 100644 --- a/gotcha-domain/src/main/java/gotcha_domain/user/User.java +++ b/gotcha-domain/src/main/java/gotcha_domain/user/User.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import gotcha_common.entity.BaseTimeEntity; +import gotcha_common.exception.CustomException; import gotcha_domain.achivement.UserAchievement; import gotcha_domain.friend.Friend; import gotcha_domain.friend.FriendRequest; @@ -155,6 +156,11 @@ public void suspendUser(long days) { this.suspensionEndDate = LocalDateTime.now().plusDays(days); } + public void unsuspendUser() { + this.userStatus = UserStatus.ACTIVE; + this.suspensionEndDate = null; + } + public void banUser(){ this.userStatus = UserStatus.BANNED; this.suspensionEndDate = null; diff --git a/gotcha/src/main/java/Gotcha/domain/sanction/exception/SanctionExceptionCode.java b/gotcha/src/main/java/Gotcha/domain/sanction/exception/SanctionExceptionCode.java new file mode 100644 index 00000000..d1764dac --- /dev/null +++ b/gotcha/src/main/java/Gotcha/domain/sanction/exception/SanctionExceptionCode.java @@ -0,0 +1,30 @@ +package Gotcha.domain.sanction.exception; + +import gotcha_common.exception.exceptionCode.ExceptionCode; +import lombok.AllArgsConstructor; +import org.springframework.http.HttpStatus; + +@AllArgsConstructor +public enum SanctionExceptionCode implements ExceptionCode { + NOT_SUSPENDED_USER(HttpStatus.BAD_REQUEST, "SANCTION-400-001", "해당 유저는 정지 상태가 아닙니다."); + + private final HttpStatus status; + private final String code; + private final String message; + + @Override + public HttpStatus getStatus() { + return status; + } + + @Override + public String getCode() { + return code; + } + + @Override + public String getMessage() { + return message; + } + +} diff --git a/gotcha/src/main/java/Gotcha/domain/sanction/service/SanctionService.java b/gotcha/src/main/java/Gotcha/domain/sanction/service/SanctionService.java index 61005886..ec43d824 100644 --- a/gotcha/src/main/java/Gotcha/domain/sanction/service/SanctionService.java +++ b/gotcha/src/main/java/Gotcha/domain/sanction/service/SanctionService.java @@ -5,6 +5,7 @@ import Gotcha.domain.report.service.UserReportService; import Gotcha.domain.sanction.dto.SanctionReq; import Gotcha.domain.sanction.dto.SanctionRes; +import Gotcha.domain.sanction.exception.SanctionExceptionCode; import Gotcha.domain.sanction.repository.SanctionRepository; import gotcha_common.exception.CustomException; import gotcha_domain.report.UserReport; @@ -45,6 +46,11 @@ public SanctionRes sanctionUser(SanctionReq sanctionReq, String adminId) { // 3. 제재 유형에 따른 로직 처리 SanctionType sanctionType = sanctionReq.getSanctionType(); + if(sanctionType == SanctionType.TEMP_BAN_CANCEL) { + if(targetUser.getUserStatus() != UserStatus.SUSPENDED) { + throw new CustomException(SanctionExceptionCode.NOT_SUSPENDED_USER); + } + } sanctionType.apply(targetUser, sanctionReq.getDurationDays()); // 4. 제재 기록 저장 From 148e3264bce00cac03d5daf2e1088a7ec3a37e78 Mon Sep 17 00:00:00 2001 From: minju Date: Mon, 17 Nov 2025 19:23:55 +0900 Subject: [PATCH 35/39] =?UTF-8?q?docs=20:=20=EC=A0=95=EC=A7=80=20=EC=B7=A8?= =?UTF-8?q?=EC=86=8C=20=EA=B8=B0=EB=8A=A5=20=EC=8A=A4=EC=9B=A8=EA=B1=B0=20?= =?UTF-8?q?=EC=84=A4=EB=AA=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/Gotcha/domain/sanction/api/SanctionApi.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/gotcha/src/main/java/Gotcha/domain/sanction/api/SanctionApi.java b/gotcha/src/main/java/Gotcha/domain/sanction/api/SanctionApi.java index cdc30e55..e2bec06f 100644 --- a/gotcha/src/main/java/Gotcha/domain/sanction/api/SanctionApi.java +++ b/gotcha/src/main/java/Gotcha/domain/sanction/api/SanctionApi.java @@ -88,6 +88,17 @@ public interface SanctionApi { } """) }) + ), + @ApiResponse(responseCode = "400", description = "정지 상태가 아닌 유저에게 정지 취소 시", + content = @Content(mediaType = "application/json", examples = { + @ExampleObject(value = """ + { + "code": "SANCTION-400-001", + "status": "Bad Request", + "message": "해당 유저는 정지 상태가 아닙니다." + } + """) + }) ) }) ResponseEntity applySanction( From dc95a89428abb9bebe12744874bff5c6339d0c14 Mon Sep 17 00:00:00 2001 From: minju Date: Mon, 17 Nov 2025 19:30:44 +0900 Subject: [PATCH 36/39] =?UTF-8?q?add=20:=20=EC=A0=95=EC=A7=80=20=EA=B8=B0?= =?UTF-8?q?=EA=B0=84=20=EB=81=9D=EB=82=AC=EC=9D=84=20=EC=8B=9C,=20?= =?UTF-8?q?=EC=9C=A0=EC=A0=80=20=ED=99=9C=EC=84=B1=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/gotcha_domain/user/User.java | 9 +++++++++ .../Gotcha/domain/auth/service/SignInUseCase.java | 13 ++++++++----- .../domain/sanction/service/SanctionService.java | 8 ++++++++ 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/gotcha-domain/src/main/java/gotcha_domain/user/User.java b/gotcha-domain/src/main/java/gotcha_domain/user/User.java index 04446594..e43d43e3 100644 --- a/gotcha-domain/src/main/java/gotcha_domain/user/User.java +++ b/gotcha-domain/src/main/java/gotcha_domain/user/User.java @@ -166,6 +166,15 @@ public void banUser(){ this.suspensionEndDate = null; } + public boolean isSuspensionExpired() { + return suspensionEndDate != null && suspensionEndDate.isBefore(LocalDateTime.now()); + } + + public void checkSuspensionAndUnsuspend() { + if (isSuspensionExpired()) { + unsuspendUser(); + } + } @Override public boolean equals(Object o) { if (this == o) { diff --git a/gotcha/src/main/java/Gotcha/domain/auth/service/SignInUseCase.java b/gotcha/src/main/java/Gotcha/domain/auth/service/SignInUseCase.java index c32d0519..bf54dc35 100644 --- a/gotcha/src/main/java/Gotcha/domain/auth/service/SignInUseCase.java +++ b/gotcha/src/main/java/Gotcha/domain/auth/service/SignInUseCase.java @@ -24,18 +24,21 @@ public TokenDto execute(SignInReq signInReq) { // 1. 사용자 인증 User user = authService.authenticate(signInReq); - // 2. 제재/차단 상태 확인 + // 2. 정지기간 만료되었는지 확인 + sanctionService.validateSuspendedEndDate(user); + + // 3. 제재/차단 상태 확인 sanctionService.validateLoginAccess(user); - // 3. 경고 확인 + // 4. 경고 확인 Optional warningOpt = sanctionService.findAndMarkUnreadWarning(user); - // 4. 토큰 생성 + // 5. 토큰 생성 TokenDto tokenDto = jwtHelper.createToken(user, signInReq.autoSignIn()); - // 5. 경고 메시지가 있으면 토큰에 추가하여 반환 + // 6. 경고 메시지가 있으면 토큰에 추가하여 반환 return warningOpt .map(warning -> TokenDto.of(tokenDto.accessToken(), tokenDto.refreshToken(), tokenDto.accessTokenExpiredAt(), tokenDto.autoSignIn(), warning)) .orElse(tokenDto); } -} \ No newline at end of file +} diff --git a/gotcha/src/main/java/Gotcha/domain/sanction/service/SanctionService.java b/gotcha/src/main/java/Gotcha/domain/sanction/service/SanctionService.java index ec43d824..270b0a14 100644 --- a/gotcha/src/main/java/Gotcha/domain/sanction/service/SanctionService.java +++ b/gotcha/src/main/java/Gotcha/domain/sanction/service/SanctionService.java @@ -14,6 +14,7 @@ import gotcha_domain.user.User; import gotcha_domain.user.UserStatus; import gotcha_user.service.UserService; +import jakarta.persistence.TableGenerator; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -87,6 +88,13 @@ public void validateLoginAccess(User user) { } } + @Transactional + public void validateSuspendedEndDate(User user) { + if(user.getUserStatus()==UserStatus.SUSPENDED) { + user.checkSuspensionAndUnsuspend(); + } + } + @Transactional public Optional findAndMarkUnreadWarning(User user) { return findLatestUnread(user, SanctionType.WARNING) From f693e4b99f90452c15e8b014c1364bdd0a98fe6a Mon Sep 17 00:00:00 2001 From: minju Date: Mon, 17 Nov 2025 19:55:39 +0900 Subject: [PATCH 37/39] =?UTF-8?q?feat=20:=20=EA=B7=BC=EA=B1=B0=20=EC=82=AC?= =?UTF-8?q?=EC=9C=A0=20=EC=84=9C=EB=B2=84=EC=97=90=EC=84=9C=20=EC=A3=BC?= =?UTF-8?q?=EC=9E=85=EB=90=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/gotcha_domain/report/UserReport.java | 4 +--- .../gotcha_domain/report/UserReportType.java | 19 ++++++++++++++----- .../domain/sanction/api/SanctionApi.java | 6 +++--- .../domain/sanction/dto/SanctionReq.java | 7 ++----- .../sanction/service/SanctionService.java | 10 ++++------ 5 files changed, 24 insertions(+), 22 deletions(-) diff --git a/gotcha-domain/src/main/java/gotcha_domain/report/UserReport.java b/gotcha-domain/src/main/java/gotcha_domain/report/UserReport.java index 0051e79c..38600406 100644 --- a/gotcha-domain/src/main/java/gotcha_domain/report/UserReport.java +++ b/gotcha-domain/src/main/java/gotcha_domain/report/UserReport.java @@ -31,11 +31,9 @@ public class UserReport extends BaseTimeEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - - @NotNull @Enumerated(EnumType.STRING) - private UserReportType userReportType; + public UserReportType userReportType; @NotNull private String detail; // by user diff --git a/gotcha-domain/src/main/java/gotcha_domain/report/UserReportType.java b/gotcha-domain/src/main/java/gotcha_domain/report/UserReportType.java index e02815c2..5fe07a43 100644 --- a/gotcha-domain/src/main/java/gotcha_domain/report/UserReportType.java +++ b/gotcha-domain/src/main/java/gotcha_domain/report/UserReportType.java @@ -1,9 +1,18 @@ package gotcha_domain.report; +import lombok.AllArgsConstructor; + +@AllArgsConstructor public enum UserReportType { - BAD_LANGUAGE, // 욕설 - HATE_SPEECH, // 혐오 발언 - INAPPROPRIATE_NAME, // 불쾌감을 주거나 부적절한 이름 - SPAM, // 도배 - OTHER // 기타 + BAD_LANGUAGE("욕설"), + HATE_SPEECH("혐오 발언"), + INAPPROPRIATE_NAME("불쾌감을 주거나 부적절한 이름"), + SPAM("도배"), + OTHER("기타. 신고 기록을 참조하세요."); + + String reason; + + public String getReason() { + return reason; + } } diff --git a/gotcha/src/main/java/Gotcha/domain/sanction/api/SanctionApi.java b/gotcha/src/main/java/Gotcha/domain/sanction/api/SanctionApi.java index e2bec06f..44a4e438 100644 --- a/gotcha/src/main/java/Gotcha/domain/sanction/api/SanctionApi.java +++ b/gotcha/src/main/java/Gotcha/domain/sanction/api/SanctionApi.java @@ -21,14 +21,14 @@ public interface SanctionApi { @Operation(summary = "사용자 제재 적용 API", description = "특정 사용자에게 제재(경고, 임시 정지, 영구 정지)를 부여합니다.\n" + "\n" - + "- targetUserUuid: 제재 대상 사용자의 UUID입니다.\n" - + "- sanctionType: 제재 유형을 나타내는 Enum입니다.\n" + + "- targetUserUuid: 제재 대상 사용자의 UUID입니다.(필수)\n" + + "- sanctionType: 제재 유형을 나타내는 Enum입니다.(필수)\n" + " • WARNING : 경고 1회를 부여합니다.\n" + " • TEMP_BAN : 일정 기간 동안 계정을 정지시킵니다. durationDays가 필요합니다.\n" + " • TEMP_BAN_CANCEL : 일정 기간 동안 정지된 계정을 다시 활성화 시킵니다. 정지 상태가 아니었다면 에러가 반환됩니다.\n" + " • PERM_BAN : 계정을 영구 정지(영구 차단)합니다.\n" + "\n" - + "- sourceReportId: 제재의 근거가 되는 신고 ID입니다. 제재가 신고 기반일 경우 사용되며, 없을 수도 있습니다.\n" + + "- sourceReportId: 제재의 근거가 되는 신고 ID입니다.(필수)\n" + "- durationDays: TEMP_BAN 제재 시 적용되는 정지 기간(일 단위)입니다. WARNING, PERM_BAN에서는 사용되지 않습니다.\n") @SecurityRequirement(name = "bearerAuth") @ApiResponses({ diff --git a/gotcha/src/main/java/Gotcha/domain/sanction/dto/SanctionReq.java b/gotcha/src/main/java/Gotcha/domain/sanction/dto/SanctionReq.java index 4185f426..a987af1f 100644 --- a/gotcha/src/main/java/Gotcha/domain/sanction/dto/SanctionReq.java +++ b/gotcha/src/main/java/Gotcha/domain/sanction/dto/SanctionReq.java @@ -9,19 +9,16 @@ @Getter @NoArgsConstructor public class SanctionReq { - @NotBlank(message = "제재 대상의 UUID는 필수입니다.") private String targetUserUuId; // adminUserId 필드는 보안을 위해 제거하고, Controller에서 직접 인증 정보를 사용합니다. - @NotBlank(message = "제재 사유는 필수입니다.") - private String reason; - @NotNull(message = "제재 유형은 필수입니다.") private SanctionType sanctionType; - private Long sourceReportId; // 제재의 근거가 되는 신고 ID (nullable) + @NotNull(message = "제재의 근거가 되는 유저 신고 id는 필수입니다.") + private Long sourceReportId; // 제재의 근거가 되는 신고 ID private Long durationDays; // 제재 기간 (일 단위), TEMP_BAN일 때 필요 } diff --git a/gotcha/src/main/java/Gotcha/domain/sanction/service/SanctionService.java b/gotcha/src/main/java/Gotcha/domain/sanction/service/SanctionService.java index 270b0a14..4dd9d48a 100644 --- a/gotcha/src/main/java/Gotcha/domain/sanction/service/SanctionService.java +++ b/gotcha/src/main/java/Gotcha/domain/sanction/service/SanctionService.java @@ -39,11 +39,9 @@ public SanctionRes sanctionUser(SanctionReq sanctionReq, String adminId) { //1. target User 조회 User targetUser = userService.findUserByUuid(sanctionReq.getTargetUserUuId()); - // 2. source Report 조회 (존재한다면) - UserReport sourceReport = null; - if(sanctionReq.getSourceReportId() != null){ - sourceReport = userReportService.findUserReportById(sanctionReq.getSourceReportId()); - } + // 2. source Report 조회 + UserReport sourceReport = userReportService.findUserReportById(sanctionReq.getSourceReportId()); + String reason = sourceReport.getUserReportType().getReason(); // 3. 제재 유형에 따른 로직 처리 SanctionType sanctionType = sanctionReq.getSanctionType(); @@ -59,7 +57,7 @@ public SanctionRes sanctionUser(SanctionReq sanctionReq, String adminId) { .user(targetUser) .admin(adminUser) .sanctionType(sanctionType) - .reason(sanctionReq.getReason()) + .reason(reason) .expireDuration(sanctionReq.getDurationDays()) .userReport(sourceReport) .build(); From e77c26511f2ddd86de6b58834ea9bb18b0d5be95 Mon Sep 17 00:00:00 2001 From: minju Date: Mon, 17 Nov 2025 20:07:27 +0900 Subject: [PATCH 38/39] =?UTF-8?q?docs=20:=20=EC=8A=A4=EC=9B=A8=EA=B1=B0=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EC=84=A4=EB=AA=85=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Gotcha/domain/sanction/api/SanctionApi.java | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/gotcha/src/main/java/Gotcha/domain/sanction/api/SanctionApi.java b/gotcha/src/main/java/Gotcha/domain/sanction/api/SanctionApi.java index 44a4e438..962f1fdf 100644 --- a/gotcha/src/main/java/Gotcha/domain/sanction/api/SanctionApi.java +++ b/gotcha/src/main/java/Gotcha/domain/sanction/api/SanctionApi.java @@ -23,10 +23,10 @@ public interface SanctionApi { + "\n" + "- targetUserUuid: 제재 대상 사용자의 UUID입니다.(필수)\n" + "- sanctionType: 제재 유형을 나타내는 Enum입니다.(필수)\n" - + " • WARNING : 경고 1회를 부여합니다.\n" - + " • TEMP_BAN : 일정 기간 동안 계정을 정지시킵니다. durationDays가 필요합니다.\n" - + " • TEMP_BAN_CANCEL : 일정 기간 동안 정지된 계정을 다시 활성화 시킵니다. 정지 상태가 아니었다면 에러가 반환됩니다.\n" - + " • PERM_BAN : 계정을 영구 정지(영구 차단)합니다.\n" + + " * WARNING : 경고 1회를 부여합니다.\n" + + " * TEMP_BAN : 일정 기간 동안 계정을 정지시킵니다. durationDays가 필요합니다.\n" + + " * TEMP_BAN_CANCEL : 일정 기간 동안 정지된 계정을 다시 활성화 시킵니다. 정지 상태가 아니었다면 에러가 반환됩니다.\n" + + " * PERM_BAN : 계정을 영구 정지(영구 차단)합니다.\n" + "\n" + "- sourceReportId: 제재의 근거가 되는 신고 ID입니다.(필수)\n" + "- durationDays: TEMP_BAN 제재 시 적용되는 정지 기간(일 단위)입니다. WARNING, PERM_BAN에서는 사용되지 않습니다.\n") @@ -47,15 +47,6 @@ public interface SanctionApi { """) }) ), - @ApiResponse(responseCode = "422", description = "필드 검증 오류 ", - content = @Content(mediaType = "application/json", examples = { - @ExampleObject(value = """ - { - "reason": "제재 사유는 필수입니다." - } - """) - }) - ), @ApiResponse(responseCode = "403", description = "관리자 권한 없음", content = @Content(mediaType = "application/json", examples = { @ExampleObject(value = """ From 1c1a74d55489f84d49a878b4a34bac9baac89e52 Mon Sep 17 00:00:00 2001 From: minju Date: Tue, 18 Nov 2025 03:53:32 +0900 Subject: [PATCH 39/39] =?UTF-8?q?fix:=20isSuspensionExpired()=20=EC=A7=81?= =?UTF-8?q?=EB=A0=AC=ED=99=94=20=EB=B0=A9=EC=A7=80=ED=95=98=EC=97=AC=20Red?= =?UTF-8?q?is=20=EC=BA=90=EC=8B=9C=20=EC=97=90=EB=9F=AC=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Jackson이 getter를 필드로 인식해 "suspensionExpired"가 캐시에 저장되던 문제 해결. --- gotcha-domain/src/main/java/gotcha_domain/user/User.java | 1 + 1 file changed, 1 insertion(+) diff --git a/gotcha-domain/src/main/java/gotcha_domain/user/User.java b/gotcha-domain/src/main/java/gotcha_domain/user/User.java index e43d43e3..08fb85d7 100644 --- a/gotcha-domain/src/main/java/gotcha_domain/user/User.java +++ b/gotcha-domain/src/main/java/gotcha_domain/user/User.java @@ -166,6 +166,7 @@ public void banUser(){ this.suspensionEndDate = null; } + @JsonIgnore public boolean isSuspensionExpired() { return suspensionEndDate != null && suspensionEndDate.isBefore(LocalDateTime.now()); }