diff --git a/src/main/java/com/first/flash/FlashApplication.java b/src/main/java/com/first/flash/FlashApplication.java index 55548eba..5ecde314 100644 --- a/src/main/java/com/first/flash/FlashApplication.java +++ b/src/main/java/com/first/flash/FlashApplication.java @@ -4,10 +4,12 @@ import java.util.TimeZone; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication @EnableScheduling +@EnableJpaAuditing public class FlashApplication { public static void main(String[] args) { diff --git a/src/main/java/com/first/flash/account/auth/application/AuthService.java b/src/main/java/com/first/flash/account/auth/application/AuthService.java index 51eda7d1..bb204ed5 100644 --- a/src/main/java/com/first/flash/account/auth/application/AuthService.java +++ b/src/main/java/com/first/flash/account/auth/application/AuthService.java @@ -33,7 +33,7 @@ public class AuthService { public LoginResponseDto login(final LoginRequestDto request) { log.info("로그인 요청: {}", request); SocialInfo socialInfo = socialService.getSocialInfo(request.provider(), request.token()); - Optional foundMember = memberRepository.findByEmail(socialInfo.socialId()); + Optional foundMember = memberRepository.findBySocialId(socialInfo.socialId()); Member member = saveOrGetMember(foundMember, socialInfo); String accessToken = tokenManager.createAccessToken(member.getId()); return new LoginResponseDto(accessToken, member.isCompleteRegistration()); diff --git a/src/main/java/com/first/flash/account/member/application/BlockService.java b/src/main/java/com/first/flash/account/member/application/BlockService.java new file mode 100644 index 00000000..e48bac2a --- /dev/null +++ b/src/main/java/com/first/flash/account/member/application/BlockService.java @@ -0,0 +1,31 @@ +package com.first.flash.account.member.application; + +import com.first.flash.account.member.application.dto.MemberBlockResponse; +import com.first.flash.account.member.domain.Member; +import com.first.flash.account.member.domain.MemberBlock; +import com.first.flash.account.member.infrastructure.BlockRepository; +import com.first.flash.global.util.AuthUtil; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class BlockService { + + private final BlockRepository blockRepository; + private final MemberService memberService; + + @Transactional + public MemberBlockResponse blockMember(final UUID blockedUser) { + UUID blockerId = AuthUtil.getId(); + Member blocker = memberService.findById(blockerId); + Member blocked = memberService.findById(blockedUser); + MemberBlock.blockMember(blocker, blocked); + MemberBlock blockResult = blockRepository.save(MemberBlock.blockMember(blocker, blocked)); + blockRepository.save(blockResult); + return MemberBlockResponse.toDto(blocked); + } +} diff --git a/src/main/java/com/first/flash/account/member/application/ReportService.java b/src/main/java/com/first/flash/account/member/application/ReportService.java new file mode 100644 index 00000000..67892ac8 --- /dev/null +++ b/src/main/java/com/first/flash/account/member/application/ReportService.java @@ -0,0 +1,32 @@ +package com.first.flash.account.member.application; + +import com.first.flash.account.member.application.dto.MemberReportRequest; +import com.first.flash.account.member.application.dto.MemberReportResponse; +import com.first.flash.account.member.domain.Member; +import com.first.flash.account.member.domain.MemberReport; +import com.first.flash.account.member.infrastructure.ReportRepository; +import com.first.flash.global.util.AuthUtil; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ReportService { + + private final ReportRepository reportRepository; + private final MemberService memberService; + + @Transactional + public MemberReportResponse reportMember(final Long reportedContentId, + final MemberReportRequest request) { + UUID reporterId = AuthUtil.getId(); + Member reporter = memberService.findById(reporterId); + MemberReport memberReport = MemberReport.reportContent(request.reason(), reporter, + reportedContentId); + reportRepository.save(memberReport); + return MemberReportResponse.toDto(reportedContentId, request.reason()); + } +} diff --git a/src/main/java/com/first/flash/account/member/application/dto/MemberBlockResponse.java b/src/main/java/com/first/flash/account/member/application/dto/MemberBlockResponse.java new file mode 100644 index 00000000..11d2e710 --- /dev/null +++ b/src/main/java/com/first/flash/account/member/application/dto/MemberBlockResponse.java @@ -0,0 +1,11 @@ +package com.first.flash.account.member.application.dto; + +import com.first.flash.account.member.domain.Member; +import java.util.UUID; + +public record MemberBlockResponse(UUID blockedMemberId, String blockedMemberName) { + + public static MemberBlockResponse toDto(final Member blockedMember) { + return new MemberBlockResponse(blockedMember.getId(), blockedMember.getNickName()); + } +} diff --git a/src/main/java/com/first/flash/account/member/application/dto/MemberReportRequest.java b/src/main/java/com/first/flash/account/member/application/dto/MemberReportRequest.java new file mode 100644 index 00000000..90ff2035 --- /dev/null +++ b/src/main/java/com/first/flash/account/member/application/dto/MemberReportRequest.java @@ -0,0 +1,7 @@ +package com.first.flash.account.member.application.dto; + +import jakarta.validation.constraints.NotEmpty; + +public record MemberReportRequest(@NotEmpty(message = "신고 사유는 필수입니다.") String reason) { + +} diff --git a/src/main/java/com/first/flash/account/member/application/dto/MemberReportResponse.java b/src/main/java/com/first/flash/account/member/application/dto/MemberReportResponse.java new file mode 100644 index 00000000..94e916dc --- /dev/null +++ b/src/main/java/com/first/flash/account/member/application/dto/MemberReportResponse.java @@ -0,0 +1,8 @@ +package com.first.flash.account.member.application.dto; + +public record MemberReportResponse(Long reportedContentId, String reason) { + + public static MemberReportResponse toDto(final Long reportedContentId, final String reason) { + return new MemberReportResponse(reportedContentId, reason); + } +} diff --git a/src/main/java/com/first/flash/account/member/domain/MemberBlock.java b/src/main/java/com/first/flash/account/member/domain/MemberBlock.java new file mode 100644 index 00000000..114d4bb0 --- /dev/null +++ b/src/main/java/com/first/flash/account/member/domain/MemberBlock.java @@ -0,0 +1,40 @@ +package com.first.flash.account.member.domain; + +import com.first.flash.global.domain.BaseEntity; +import jakarta.persistence.CascadeType; +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 lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +public class MemberBlock extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.REMOVE) + @JoinColumn(name = "blockerId") + private Member blocker; + + @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.REMOVE) + @JoinColumn(name = "blockedId") + private Member blocked; + + private MemberBlock(final Member blocker, final Member blocked) { + this.blocker = blocker; + this.blocked = blocked; + } + + public static MemberBlock blockMember(final Member blocker, final Member blocked) { + return new MemberBlock(blocker, blocked); + } +} diff --git a/src/main/java/com/first/flash/account/member/domain/MemberReport.java b/src/main/java/com/first/flash/account/member/domain/MemberReport.java new file mode 100644 index 00000000..be06c4cd --- /dev/null +++ b/src/main/java/com/first/flash/account/member/domain/MemberReport.java @@ -0,0 +1,39 @@ +package com.first.flash.account.member.domain; + +import com.first.flash.global.domain.BaseEntity; +import jakarta.persistence.CascadeType; +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 lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +public class MemberReport extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private String reason; + @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.REMOVE) + @JoinColumn(name = "reporterId") + private Member reporter; + private Long reportedContentId; + + private MemberReport(final String reason, final Member reporter, final Long reportedContentId) { + this.reason = reason; + this.reporter = reporter; + this.reportedContentId = reportedContentId; + } + + public static MemberReport reportContent(final String reason, final Member reporter, + final Long reportedContentId) { + return new MemberReport(reason, reporter, reportedContentId); + } +} diff --git a/src/main/java/com/first/flash/account/member/domain/MemberRepository.java b/src/main/java/com/first/flash/account/member/domain/MemberRepository.java index 43691cdd..31358388 100644 --- a/src/main/java/com/first/flash/account/member/domain/MemberRepository.java +++ b/src/main/java/com/first/flash/account/member/domain/MemberRepository.java @@ -7,6 +7,6 @@ public interface MemberRepository { Member save(final Member member); Optional findById(final UUID id); - Optional findByEmail(final String email); + Optional findBySocialId(final String socialId); boolean existsByNickName(final String nickName); } diff --git a/src/main/java/com/first/flash/account/member/infrastructure/BlockRepository.java b/src/main/java/com/first/flash/account/member/infrastructure/BlockRepository.java new file mode 100644 index 00000000..f3604319 --- /dev/null +++ b/src/main/java/com/first/flash/account/member/infrastructure/BlockRepository.java @@ -0,0 +1,11 @@ +package com.first.flash.account.member.infrastructure; + +import com.first.flash.account.member.domain.MemberBlock; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface BlockRepository extends JpaRepository { + + MemberBlock save(final MemberBlock memberBlock); +} diff --git a/src/main/java/com/first/flash/account/member/infrastructure/MemberJpaRepository.java b/src/main/java/com/first/flash/account/member/infrastructure/MemberJpaRepository.java index 546cda91..3a0d825a 100644 --- a/src/main/java/com/first/flash/account/member/infrastructure/MemberJpaRepository.java +++ b/src/main/java/com/first/flash/account/member/infrastructure/MemberJpaRepository.java @@ -11,7 +11,7 @@ public interface MemberJpaRepository extends JpaRepository { Optional findById(final UUID id); - Optional findMemberByEmail(final String email); + Optional findMemberBySocialId(final String email); boolean existsByNickName(final String nickName); } diff --git a/src/main/java/com/first/flash/account/member/infrastructure/MemberRepositoryImpl.java b/src/main/java/com/first/flash/account/member/infrastructure/MemberRepositoryImpl.java index c3d90a16..21a68b3e 100644 --- a/src/main/java/com/first/flash/account/member/infrastructure/MemberRepositoryImpl.java +++ b/src/main/java/com/first/flash/account/member/infrastructure/MemberRepositoryImpl.java @@ -24,8 +24,8 @@ public Optional findById(final UUID id) { } @Override - public Optional findByEmail(final String email) { - return jpaRepository.findMemberByEmail(email); + public Optional findBySocialId(final String socialId) { + return jpaRepository.findMemberBySocialId(socialId); } @Override diff --git a/src/main/java/com/first/flash/account/member/infrastructure/ReportRepository.java b/src/main/java/com/first/flash/account/member/infrastructure/ReportRepository.java new file mode 100644 index 00000000..368f760e --- /dev/null +++ b/src/main/java/com/first/flash/account/member/infrastructure/ReportRepository.java @@ -0,0 +1,9 @@ +package com.first.flash.account.member.infrastructure; + +import com.first.flash.account.member.domain.MemberReport; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ReportRepository extends JpaRepository { + + MemberReport save(final MemberReport memberReport); +} diff --git a/src/main/java/com/first/flash/account/member/ui/MemberController.java b/src/main/java/com/first/flash/account/member/ui/MemberController.java index bab7f4c9..3757bb44 100644 --- a/src/main/java/com/first/flash/account/member/ui/MemberController.java +++ b/src/main/java/com/first/flash/account/member/ui/MemberController.java @@ -1,11 +1,16 @@ package com.first.flash.account.member.ui; +import com.first.flash.account.member.application.BlockService; import com.first.flash.account.member.application.MemberService; +import com.first.flash.account.member.application.ReportService; import com.first.flash.account.member.application.dto.ConfirmNickNameRequest; import com.first.flash.account.member.application.dto.ConfirmNickNameResponse; +import com.first.flash.account.member.application.dto.MemberBlockResponse; import com.first.flash.account.member.application.dto.MemberCompleteRegistrationRequest; import com.first.flash.account.member.application.dto.MemberCompleteRegistrationResponse; import com.first.flash.account.member.application.dto.MemberInfoResponse; +import com.first.flash.account.member.application.dto.MemberReportRequest; +import com.first.flash.account.member.application.dto.MemberReportResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.ExampleObject; @@ -14,10 +19,13 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; +import java.util.UUID; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +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; @@ -29,6 +37,8 @@ public class MemberController { private final MemberService memberService; + private final BlockService blockService; + private final ReportService reportService; @Operation(summary = "내 정보 조회", description = "특정 회원 정보 조회") @ApiResponses(value = { @@ -81,4 +91,30 @@ public ResponseEntity confirmNickName( @Valid @RequestBody final ConfirmNickNameRequest request) { return ResponseEntity.ok(memberService.confirmNickName(request)); } + + @Operation(summary = "유저 차단", description = "특정 유저 차단") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "차단 성공", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = MemberBlockResponse.class))), + @ApiResponse(responseCode = "404", description = "리소스를 찾을 수 없음", + content = @Content(mediaType = "application/json", examples = { + @ExampleObject(name = "차단하려는 유저를 찾을 수 없음", value = "{\"error\": \"사용자를 찾을 수 없습니다!\"}"), + })) + }) + @PostMapping("/blocks/{blockedUserId}") + public ResponseEntity blockMember(@PathVariable final UUID blockedUserId) { + return ResponseEntity.ok(blockService.blockMember(blockedUserId)); + } + + @Operation(summary = "컨텐츠 신고", description = "특정 컨텐츠 신고") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "신고 성공", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = MemberReportRequest.class))) + }) + @PostMapping("/reports/{reportedContentId}") + public ResponseEntity reportMember( + @PathVariable final Long reportedContentId, + @RequestBody @Valid final MemberReportRequest request) { + return ResponseEntity.ok(reportService.reportMember(reportedContentId, request)); + } } diff --git a/src/main/java/com/first/flash/global/domain/BaseEntity.java b/src/main/java/com/first/flash/global/domain/BaseEntity.java new file mode 100644 index 00000000..3e530c01 --- /dev/null +++ b/src/main/java/com/first/flash/global/domain/BaseEntity.java @@ -0,0 +1,28 @@ +package com.first.flash.global.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public abstract class BaseEntity { + + @CreatedDate + @Column(updatable = false, nullable = false) + private LocalDateTime createdAt; + + @LastModifiedDate + @Column(nullable = false) + private LocalDateTime updatedAt; +}