Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
public enum ChatErrorCode implements ErrorCode {

CANNOT_CREATE_CHAT_ROOM_WITH_SELF("CHAT-E001", HttpStatus.BAD_REQUEST, "자기 자신과는 채팅방을 생성할 수 없습니다."),
CHAT_ROOM_NOT_FOUND("CHAT-E002", HttpStatus.NOT_FOUND, "채팅방을 찾을 수 없습니다.");
CHAT_ROOM_NOT_FOUND("CHAT-E002", HttpStatus.NOT_FOUND, "채팅방을 찾을 수 없습니다."),
BLOCKED_CHAT_PARTNER("CHAT-E003", HttpStatus.FORBIDDEN, "차단된 상대방에게는 메시지를 보낼 수 없습니다."),
REPORT_MAIL_SEND_FAILED("CHAT-E004", HttpStatus.INTERNAL_SERVER_ERROR, "신고 접수 메일 발송에 실패했습니다.");

private final String code;
private final HttpStatus httpStatus;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,21 @@
import jakarta.validation.constraints.Min;
import java.time.OffsetDateTime;
import lombok.RequiredArgsConstructor;
import org.sopt.buddys.domain.chat.controller.swagger.BlockChatPartnerSwagger;
import org.sopt.buddys.domain.chat.controller.swagger.CreateChatRoomSwagger;
import org.sopt.buddys.domain.chat.controller.swagger.GetChatMessagesSwagger;
import org.sopt.buddys.domain.chat.controller.swagger.GetChatRoomsSwagger;
import org.sopt.buddys.domain.chat.controller.swagger.GetChatRoomSwagger;
import org.sopt.buddys.domain.chat.controller.swagger.ReportChatPartnerSwagger;
import org.sopt.buddys.domain.chat.dto.request.CreateChatRoomRequest;
import org.sopt.buddys.domain.chat.dto.request.ReportChatPartnerRequest;
import org.sopt.buddys.domain.chat.dto.response.ChatMessageListResponse;
import org.sopt.buddys.domain.chat.dto.response.ChatRoomListResponse;
import org.sopt.buddys.domain.chat.dto.response.ChatRoomResponse;
import org.sopt.buddys.domain.chat.service.ChatMessageService;
import org.sopt.buddys.domain.chat.service.ChatRoomService;
import org.sopt.buddys.domain.chat.service.ChatUserBlockService;
import org.sopt.buddys.domain.chat.service.ChatUserReportService;
import org.sopt.buddys.domain.chat.util.ChatTimeConverter;
import org.sopt.buddys.global.common.code.GlobalSuccessCode;
import org.sopt.buddys.global.response.BaseResponse;
Expand All @@ -40,6 +45,8 @@ public class ChatRoomController {

private final ChatRoomService chatRoomService;
private final ChatMessageService chatMessageService;
private final ChatUserBlockService chatUserBlockService;
private final ChatUserReportService chatUserReportService;

@CreateChatRoomSwagger
@PostMapping
Expand Down Expand Up @@ -119,4 +126,32 @@ public BaseResponse<ChatMessageListResponse> getMessages(
)
);
}

@BlockChatPartnerSwagger
@PostMapping("/{chatRoomId}/block")
public BaseResponse<Void> blockChatPartner(
@Parameter(hidden = true)
@LoginUser Long userId,
@Parameter(description = "채팅방 ID", example = "1")
@PathVariable Long chatRoomId
) {

chatUserBlockService.blockChatPartner(userId, chatRoomId);
return BaseResponse.success(GlobalSuccessCode.OK);
}

@ReportChatPartnerSwagger
@PostMapping("/{chatRoomId}/report")
public BaseResponse<Void> reportChatPartner(
@Parameter(hidden = true)
@LoginUser Long userId,
@Parameter(description = "채팅방 ID", example = "1")
@PathVariable Long chatRoomId,
@RequestBody(required = false) @Valid ReportChatPartnerRequest request
) {

String reason = request != null ? request.reason() : null;
Comment thread
haerxeong marked this conversation as resolved.
chatUserReportService.reportChatPartner(userId, chatRoomId, reason);
return BaseResponse.success(GlobalSuccessCode.OK);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package org.sopt.buddys.domain.chat.controller.swagger;

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.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.sopt.buddys.global.response.BaseResponse;
import org.sopt.buddys.global.swagger.CommonErrorResponses;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Operation(
summary = "채팅 상대방 차단",
description = """
해당 채팅방의 상대방을 차단합니다.

- 차단 후에는 양쪽 모두 해당 상대방과 서로 메시지를 보낼 수 없습니다.
- 기존 채팅 내역은 삭제되지 않고 그대로 유지됩니다.
- 이미 차단한 상대방을 다시 차단해도 오류 없이 처리됩니다(멱등).
"""
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "차단 성공"),
@ApiResponse(
responseCode = "403",
description = "채팅방 접근 권한 없음",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = BaseResponse.class),
examples = @ExampleObject(value = """
{
"success": false,
"code": "GLB-E003",
"message": "접근 권한이 없습니다.",
"data": null
}
""")
)
),
@ApiResponse(
responseCode = "404",
description = "채팅방을 찾을 수 없음",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = BaseResponse.class),
examples = @ExampleObject(value = """
{
"success": false,
"code": "CHAT-E002",
"message": "채팅방을 찾을 수 없습니다.",
"data": null
}
""")
)
)
})
@CommonErrorResponses
public @interface BlockChatPartnerSwagger {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package org.sopt.buddys.domain.chat.controller.swagger;

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.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.sopt.buddys.global.response.BaseResponse;
import org.sopt.buddys.global.swagger.CommonErrorResponses;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Operation(
summary = "채팅 상대방 신고",
description = """
해당 채팅방의 상대방을 신고합니다.

- 신고 사유(reason)는 선택 입력이며, 생략하면 사유 없이 즉시 접수됩니다.
- 신고 접수 시 신고자·신고 대상자 정보(및 입력된 사유)를 포함한 메일이 운영팀으로 발송됩니다.
"""
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "신고 접수 성공"),
@ApiResponse(
responseCode = "403",
description = "채팅방 접근 권한 없음",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = BaseResponse.class),
examples = @ExampleObject(value = """
{
"success": false,
"code": "GLB-E003",
"message": "접근 권한이 없습니다.",
"data": null
}
""")
)
),
@ApiResponse(
responseCode = "404",
description = "채팅방을 찾을 수 없음",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = BaseResponse.class),
examples = @ExampleObject(value = """
{
"success": false,
"code": "CHAT-E002",
"message": "채팅방을 찾을 수 없습니다.",
"data": null
}
""")
)
),
@ApiResponse(
responseCode = "500",
description = "신고 접수 메일 발송 실패",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = BaseResponse.class),
examples = @ExampleObject(value = """
{
"success": false,
"code": "CHAT-E004",
"message": "신고 접수 메일 발송에 실패했습니다.",
"data": null
}
""")
)
Comment thread
haerxeong marked this conversation as resolved.
Outdated
)
})
@CommonErrorResponses
public @interface ReportChatPartnerSwagger {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.sopt.buddys.domain.chat.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Size;

public record ReportChatPartnerRequest(
@Schema(
description = "신고 사유. 현재는 별도 사유 입력 없이 신고할 수 있어 생략 가능합니다.",
example = "부적절한 언행",
nullable = true
)
@Size(max = 500)
String reason
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package org.sopt.buddys.domain.chat.entity;

import jakarta.persistence.Column;
import jakarta.persistence.EmbeddedId;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.MapsId;
import jakarta.persistence.PrePersist;
import jakarta.persistence.Table;
import java.time.LocalDateTime;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.sopt.buddys.domain.chat.util.ChatTimeConverter;
import org.sopt.buddys.domain.user.entity.User;

@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "chat_user_block")
public class ChatUserBlock {

@EmbeddedId
private ChatUserBlockId id;

@MapsId("blockerId")
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "blocker_id", nullable = false)
private User blocker;

@MapsId("blockedId")
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "blocked_id", nullable = false)
private User blocked;

@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;

public ChatUserBlock(User blocker, User blocked) {
this.blocker = blocker;
this.blocked = blocked;
this.id = new ChatUserBlockId(blocker.getId(), blocked.getId());
}

@PrePersist
private void prePersist() {
if (createdAt == null) {
createdAt = ChatTimeConverter.now();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.sopt.buddys.domain.chat.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import java.io.Serializable;
import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Embeddable
@EqualsAndHashCode
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ChatUserBlockId implements Serializable {

@Column(name = "blocker_id")
private Long blockerId;

@Column(name = "blocked_id")
private Long blockedId;

public ChatUserBlockId(Long blockerId, Long blockedId) {
this.blockerId = blockerId;
this.blockedId = blockedId;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package org.sopt.buddys.domain.chat.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
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 jakarta.persistence.PrePersist;
import jakarta.persistence.Table;
import java.time.LocalDateTime;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.sopt.buddys.domain.chat.util.ChatTimeConverter;
import org.sopt.buddys.domain.user.entity.User;

@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "chat_user_report")
public class ChatUserReport {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "chat_room_id", nullable = false)
private ChatRoom chatRoom;

@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "reporter_id", nullable = false)
private User reporter;

@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "reported_id", nullable = false)
private User reported;

@Column(length = 500)
private String reason;

@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;

public ChatUserReport(ChatRoom chatRoom, User reporter, User reported, String reason) {
this.chatRoom = chatRoom;
this.reporter = reporter;
this.reported = reported;
this.reason = reason;
}

@PrePersist
private void prePersist() {
if (createdAt == null) {
createdAt = ChatTimeConverter.now();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,17 @@ Optional<Long> findParticipantLastReadMessageId(
@Param("userId") Long userId
);

@Query("""
select participantMember.user.id
from ChatRoomMember participantMember
where participantMember.chatRoom.id = :chatRoomId
and participantMember.user.id <> :userId
""")
Optional<Long> findOtherMemberUserId(
@Param("chatRoomId") Long chatRoomId,
@Param("userId") Long userId
);

@Query("""
select myMember.lastReadMessageId
from ChatRoomMember myMember
Expand Down
Loading
Loading