Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
@@ -1,77 +1,45 @@
package com.example.ReMap.domain.auth.entity;


import com.example.ReMap.domain.member.Member;
import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDateTime;


@Entity
@Table(name = "refresh_token")
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
public class RefreshToken {

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

@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", nullable = false)
// Member 삭제 시 토큰도 함께 삭제
@OneToOne(fetch = FetchType.LAZY, optional = false, cascade = CascadeType.REMOVE)
@JoinColumn(name = "member_id", nullable = false, unique = true)
private Member member;

@Column(name = "refresh_token", nullable = false)
@Column(name = "refresh_token", nullable = false, length = 512)
private String refreshToken;

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

@Column(name = "updated_at")
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;

// 기본 생성자
public RefreshToken() {}

// 생성자
public RefreshToken(Member member, String refreshToken, LocalDateTime createdAt, LocalDateTime updatedAt) {
this.member = member;
this.refreshToken = refreshToken;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
}

// Getter / Setter
public Long getId() {
return id;
}

public Member getMember() {
return member;
}

public void setMember(Member member) {
this.member = member;
}

public String getRefreshToken() {
return refreshToken;
}

public void setRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}

public LocalDateTime getCreatedAt() {
return createdAt;
}

public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}

public LocalDateTime getUpdatedAt() {
return updatedAt;
@PrePersist
public void prePersist() {
LocalDateTime now = LocalDateTime.now();
this.createdAt = now;
this.updatedAt = now;
}

public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
@PreUpdate
public void preUpdate() {
this.updatedAt = LocalDateTime.now();
}
}
2 changes: 2 additions & 0 deletions src/main/java/com/example/ReMap/domain/cast/Cast.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.example.ReMap.domain.cast;

import com.example.ReMap.domain.member.Member;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.*;

import java.time.LocalDateTime;
Expand All @@ -20,6 +21,7 @@ public class Cast {

@ManyToOne
@JoinColumn(name = "member_id")
@JsonIgnore
private Member member;

// 생성자, getter, setter
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/com/example/ReMap/domain/cast/CastRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.example.ReMap.domain.cast;


import org.springframework.data.jpa.repository.JpaRepository;

/**
* Cast(게시글) 전용 JPA 레포지토리
* → JpaRepository 를 상속하면 findById, save, delete 등 기본 메서드가 자동 생성됩니다.
*/
public interface CastRepository extends JpaRepository<Cast, Long> {
// 필요하면 여기에 커스텀 쿼리 메서드 추가
}
33 changes: 28 additions & 5 deletions src/main/java/com/example/ReMap/domain/comment/Comment.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,50 @@

import com.example.ReMap.domain.cast.Cast;
import com.example.ReMap.domain.member.Member;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.CreationTimestamp;

import java.time.LocalDateTime;

@Entity
@Table(name = "comment")
@Getter
@Setter // 전체 Setter 허용
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)

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

/** VARCHAR(255) 에 맞춤 */
@Column(length = 255, nullable = false)
private String content;

/** 생성 시각 – DB 기본값 대신 애플리케이션에서 채움 */
@CreationTimestamp
@Column(name = "created_at")
private LocalDateTime createdAt;

@ManyToOne
/* ---------- 연관 관계 ---------- */

/** 작성자 – DDL이 NULL 허용 → optional = true */
@ManyToOne(fetch = FetchType.LAZY, optional = true)
@JoinColumn(name = "member_id")
@JsonIgnore
private Member member;

@ManyToOne
/** 대상 캐스트 – DDL이 NULL 허용 → optional = true */
@ManyToOne(fetch = FetchType.LAZY, optional = true)
@JoinColumn(name = "cast_id")
private Cast cast;
}

/* ---------- 도메인 메서드 ---------- */

public void changeContent(String newContent) {
this.content = newContent;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.example.ReMap.domain.comment;

import com.example.ReMap.domain.comment.CommentService;
import com.example.ReMap.domain.comment.dto.CommentResponse;
import com.example.ReMap.domain.comment.dto.CreateCommentRequest;
import com.example.ReMap.domain.comment.dto.UpdateCommentRequest;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.security.Principal;
import java.util.List;

@RestController
@RequestMapping("/casts/{castId}/comments")
@RequiredArgsConstructor
public class CommentController {

private final CommentService commentService;

/* ------------------------------------ 1. 댓글 등록 ------------------------------------ */
@PostMapping
public ResponseEntity<CommentResponse> create(
@PathVariable Long castId,
@RequestBody @Valid CreateCommentRequest req,
Principal principal // 로그인 사용자 정보(아이디 문자열)
) {
Long memberId = Long.valueOf(principal.getName()); // "1" → 1L 등으로 변환
CommentResponse resp = commentService.create(castId, memberId, req.getContent());
return ResponseEntity.status(HttpStatus.CREATED).body(resp);
}

/* ------------------------------------ 2. 댓글 목록 ------------------------------------ */
@GetMapping
public List<CommentResponse> list(@PathVariable Long castId) {
return commentService.list(castId);
}

/* ------------------------------------ 3. 댓글 수정 ------------------------------------ */
@PutMapping("/{commentId}")
public CommentResponse update(
@PathVariable Long castId, // 경로 일관성 확보용
@PathVariable Long commentId,
@RequestBody @Valid UpdateCommentRequest req,
Principal principal
) {
Long memberId = Long.valueOf(principal.getName());
return commentService.update(commentId, memberId, req.getContent());
}

/* ------------------------------------ 4. 댓글 삭제 ------------------------------------ */
@DeleteMapping("/{commentId}")
public ResponseEntity<Void> delete(
@PathVariable Long castId,
@PathVariable Long commentId,
Principal principal
) {
Long memberId = Long.valueOf(principal.getName());
commentService.delete(commentId, memberId);
return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.example.ReMap.domain.comment;


import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

/**
* 댓글 레포지토리
*
* • findByCastIdOrderByCreatedAtAsc() : 특정 Cast(게시글)의 댓글을
* 작성 시간순으로 가져옵니다.
* • @EntityGraph("member") : 댓글과 작성자를 한 번에 로딩해서
* N+1 쿼리 문제를 예방합니다.
*/
public interface CommentRepository extends JpaRepository<Comment, Long> {

@EntityGraph(attributePaths = "member") // 댓글 조회 + Member 한 번에 가져오기
List<Comment> findByCastIdOrderByCreatedAtAsc(Long castId);
}
91 changes: 91 additions & 0 deletions src/main/java/com/example/ReMap/domain/comment/CommentService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package com.example.ReMap.domain.comment;


import com.example.ReMap.domain.cast.Cast;
import com.example.ReMap.domain.comment.Comment;
import com.example.ReMap.domain.comment.CommentRepository;
import com.example.ReMap.domain.member.Member;
import com.example.ReMap.domain.member.MemberRepository;
import com.example.ReMap.domain.comment.dto.CommentResponse;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
@RequiredArgsConstructor // 의존성 주입용 생성자 자동
@Transactional // 메서드 기본 read-write
public class CommentService {

private final CommentRepository commentRepo;
private final com.example.ReMap.domain.cast.CastRepository castRepo;
private final MemberRepository memberRepo;

/* ---------- CREATE ---------- */

public CommentResponse create(Long castId, Long memberId, String content) {

Cast cast = castRepo.findById(castId)
.orElseThrow(() -> new IllegalArgumentException("Cast not found : " + castId));

Member member = memberRepo.findById(memberId)
.orElseThrow(() -> new IllegalArgumentException("Member not found : " + memberId));

// 댓글 엔티티 생성
Comment comment = Comment.builder()
.content(content)
.build();

// FK 세팅 (엔티티 @Setter 덕분에 가능)
comment.setCast(cast);
comment.setMember(member);

// 저장
commentRepo.save(comment);

return CommentResponse.from(comment);
}

/* ---------- READ (목록) ---------- */
@Transactional(Transactional.TxType.SUPPORTS) // read-only 트랜잭션
public List<CommentResponse> list(Long castId) {
return commentRepo.findByCastIdOrderByCreatedAtAsc(castId)
.stream()
.map(CommentResponse::from)
.toList();
}

/* ---------- UPDATE ---------- */

public CommentResponse update(Long commentId, Long requesterId, String newContent) {

Comment comment = commentRepo.findById(commentId)
.orElseThrow(() -> new IllegalArgumentException("Comment not found : " + commentId));

// 작성자 권한 확인
if (!requesterId.equals(comment.getMember() != null ? comment.getMember().getId() : null)) {
throw new AccessDeniedException("본인 댓글만 수정할 수 있습니다.");
}

comment.changeContent(newContent); // 엔티티 내부에서 필드 변경

return CommentResponse.from(comment);
}

/* ---------- DELETE ---------- */

public void delete(Long commentId, Long requesterId) {

Comment comment = commentRepo.findById(commentId)
.orElseThrow(() -> new IllegalArgumentException("Comment not found : " + commentId));

if (!requesterId.equals(comment.getMember() != null ? comment.getMember().getId() : null)) {
throw new AccessDeniedException("본인 댓글만 삭제할 수 있습니다.");
}

commentRepo.delete(comment); // 하드 삭제
// → 소프트 삭제가 필요하면 Comment에 deleted 필드를 추가해 플래그만 변경
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.example.ReMap.domain.comment.dto;


import com.example.ReMap.domain.comment.Comment;
import lombok.Builder;
import lombok.Getter;

import java.time.LocalDateTime;

/** 댓글 단건 응답 DTO */
@Getter
@Builder
public class CommentResponse {

private Long id; // 댓글 ID
private Long memberId; // 작성자 ID
private String nickname; // 작성자 닉네임
private String content; // 댓글 본문
private LocalDateTime createdAt;

/** Comment 엔티티 → DTO 변환 */
public static CommentResponse from(Comment c) {
return CommentResponse.builder()
.id(c.getId())
.memberId(c.getMember() != null ? c.getMember().getId() : null)
.nickname(c.getMember() != null ? c.getMember().getNickname() : "탈퇴한 회원")
.content(c.getContent())
.createdAt(c.getCreatedAt())
.build();
}
}
Loading