Skip to content

Commit ec857ff

Browse files
authored
[feat] 팔로우 기능 구현 (#326)
1 parent 2afed4f commit ec857ff

File tree

12 files changed

+1251
-2
lines changed

12 files changed

+1251
-2
lines changed

src/main/java/com/back/domain/artist/entity/ArtistProfile.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,9 +217,10 @@ public void addSales(Long amount) {
217217
* 팔로워 수 감소
218218
*/
219219
public void decreaseFollowerCount() {
220-
if (this.followerCount > 0) {
221-
this.followerCount--;
220+
if (this.followerCount <= 0) {
221+
throw new ServiceException("400", "팔로워 수는 0 이하로 감소할 수 없습니다.");
222222
}
223+
this.followerCount--;
223224
}
224225

225226
/**
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
package com.back.domain.follow.controller;
2+
3+
import com.back.domain.follow.dto.response.FollowResponse;
4+
import com.back.domain.follow.dto.response.FollowerListResponse;
5+
import com.back.domain.follow.dto.response.FollowingListResponse;
6+
import com.back.domain.follow.service.FollowService;
7+
import com.back.global.rsData.RsData;
8+
import com.back.global.security.auth.CustomUserDetails;
9+
import io.swagger.v3.oas.annotations.Operation;
10+
import io.swagger.v3.oas.annotations.Parameter;
11+
import io.swagger.v3.oas.annotations.tags.Tag;
12+
import lombok.RequiredArgsConstructor;
13+
import lombok.extern.slf4j.Slf4j;
14+
import org.springframework.http.ResponseEntity;
15+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
16+
import org.springframework.web.bind.annotation.*;
17+
18+
import java.util.List;
19+
20+
@Slf4j
21+
@RestController
22+
@RequestMapping("/api/follows")
23+
@RequiredArgsConstructor
24+
@Tag(name = "팔로우", description = "작가 팔로우 관련 API")
25+
public class FollowController {
26+
27+
private final FollowService followService;
28+
29+
/**
30+
* 작가 팔로우
31+
*/
32+
@PostMapping("/artists/{artistId}")
33+
@Operation(
34+
summary = "작가 팔로우",
35+
description = "특정 작가를 팔로우합니다. 이미 팔로우 중인 경우 오류를 반환합니다."
36+
)
37+
public ResponseEntity<RsData<FollowResponse>> followArtist(
38+
@AuthenticationPrincipal CustomUserDetails userDetails,
39+
@Parameter(description = "작가 프로필 ID", example = "1", required = true)
40+
@PathVariable Long artistId) {
41+
42+
FollowResponse response = followService.followArtist(userDetails.getUserId(), artistId);
43+
44+
return ResponseEntity.ok(
45+
RsData.of("200", "팔로우 성공", response)
46+
);
47+
}
48+
49+
/**
50+
* 작가 언팔로우
51+
*/
52+
@DeleteMapping("/artists/{artistId}")
53+
@Operation(
54+
summary = "작가 언팔로우",
55+
description = "특정 작가를 언팔로우합니다."
56+
)
57+
public ResponseEntity<RsData<Void>> unfollowArtist(
58+
@AuthenticationPrincipal CustomUserDetails userDetails,
59+
@Parameter(description = "작가 프로필 ID", example = "1", required = true)
60+
@PathVariable Long artistId) {
61+
62+
followService.unfollowArtist(userDetails.getUserId(), artistId);
63+
64+
return ResponseEntity.ok(
65+
RsData.of("200", "언팔로우 성공")
66+
);
67+
}
68+
69+
/**
70+
* 팔로우 상태 확인
71+
*/
72+
@GetMapping("/artists/{artistId}/status")
73+
@Operation(
74+
summary = "팔로우 상태 확인",
75+
description = "현재 로그인한 사용자가 특정 작가를 팔로우 중인지 확인합니다."
76+
)
77+
public ResponseEntity<RsData<Boolean>> isFollowing(
78+
@AuthenticationPrincipal CustomUserDetails userDetails,
79+
@Parameter(description = "작가 프로필 ID", example = "1", required = true)
80+
@PathVariable Long artistId) {
81+
82+
boolean isFollowing = followService.isFollowing(userDetails.getUserId(), artistId);
83+
84+
return ResponseEntity.ok(
85+
RsData.of("200", "팔로우 상태 조회 성공", isFollowing)
86+
);
87+
}
88+
89+
/**
90+
* 내가 팔로우하는 작가 목록 조회
91+
*/
92+
@GetMapping("/following")
93+
@Operation(
94+
summary = "내가 팔로우하는 작가 목록",
95+
description = "현재 로그인한 사용자가 팔로우하는 작가 목록을 조회합니다."
96+
)
97+
public ResponseEntity<RsData<List<FollowingListResponse>>> getFollowingList(
98+
@AuthenticationPrincipal CustomUserDetails userDetails) {
99+
100+
List<FollowingListResponse> response = followService.getMyFollowingList(userDetails.getUserId());
101+
102+
return ResponseEntity.ok(
103+
RsData.of("200", "팔로잉 목록 조회 성공", response)
104+
);
105+
}
106+
107+
/**
108+
* 내 팔로워 목록 조회 (작가 전용)
109+
*/
110+
@GetMapping("/followers")
111+
@Operation(
112+
summary = "내 팔로워 목록 조회 (작가 전용)",
113+
description = "작가 본인의 팔로워 목록을 조회합니다. 작가가 아니거나 다른 작가의 팔로워는 조회할 수 없습니다."
114+
)
115+
public ResponseEntity<RsData<List<FollowerListResponse>>> getMyFollowerList(
116+
@AuthenticationPrincipal CustomUserDetails userDetails) {
117+
118+
List<FollowerListResponse> response = followService.getMyFollowerList(userDetails.getUserId());
119+
120+
return ResponseEntity.ok(
121+
RsData.of("200", "팔로워 목록 조회 성공", response)
122+
);
123+
}
124+
125+
/**
126+
* 내가 팔로우하는 작가 수 조회
127+
*/
128+
@GetMapping("/following/count")
129+
@Operation(
130+
summary = "팔로잉 수 조회",
131+
description = "현재 로그인한 사용자가 팔로우하는 작가의 수를 조회합니다."
132+
)
133+
public ResponseEntity<RsData<Long>> getFollowingCount(
134+
@AuthenticationPrincipal CustomUserDetails userDetails) {
135+
136+
long count = followService.getFollowingCount(userDetails.getUserId());
137+
138+
return ResponseEntity.ok(
139+
RsData.of("200", "팔로잉 수 조회 성공", count)
140+
);
141+
}
142+
143+
/**
144+
* 특정 작가의 팔로워 수 조회 (공개)
145+
*/
146+
@GetMapping("/artists/{artistId}/followers/count")
147+
@Operation(
148+
summary = "작가의 팔로워 수 조회",
149+
description = "특정 작가의 팔로워 수를 조회합니다. (공개 API)"
150+
)
151+
public ResponseEntity<RsData<Long>> getFollowerCount(
152+
@Parameter(description = "작가 프로필 ID", example = "1", required = true)
153+
@PathVariable Long artistId) {
154+
155+
long count = followService.getFollowerCount(artistId);
156+
157+
return ResponseEntity.ok(
158+
RsData.of("200", "팔로워 수 조회 성공", count)
159+
);
160+
}
161+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.back.domain.follow.dto.response;
2+
3+
import com.back.domain.artist.entity.ArtistProfile;
4+
import com.back.domain.follow.entity.Follow;
5+
6+
import java.time.LocalDateTime;
7+
8+
/**
9+
* 팔로우 응답 DTO - 팔로우/언팔로우 성공 시 반환
10+
*/
11+
public record FollowResponse(
12+
Long followId,
13+
Long artistId,
14+
String artistName,
15+
String profileImageUrl,
16+
Integer followerCount,
17+
LocalDateTime followedAt,
18+
boolean isFollowing
19+
) {
20+
/**
21+
* Follow 엔티티 -> DTO 변환
22+
*/
23+
public static FollowResponse from(Follow follow, ArtistProfile artist) {
24+
return new FollowResponse(
25+
follow.getId(),
26+
artist.getId(),
27+
artist.getArtistName(),
28+
artist.getProfileImageUrl(),
29+
artist.getFollowerCount(),
30+
follow.getCreateDate(),
31+
true
32+
);
33+
}
34+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.back.domain.follow.dto.response;
2+
3+
import com.back.domain.follow.entity.Follow;
4+
import com.back.domain.user.entity.User;
5+
6+
import java.time.LocalDateTime;
7+
8+
public record FollowerListResponse(
9+
Long followId,
10+
Long userId,
11+
String userName,
12+
String profileImageUrl,
13+
String grade,
14+
LocalDateTime followedAt
15+
) {
16+
/**
17+
* Follow 엔티티 -> DTO 변환
18+
*/
19+
public static FollowerListResponse from(Follow follow) {
20+
User follower = follow.getFollower();
21+
22+
return new FollowerListResponse(
23+
follow.getId(),
24+
follower.getId(),
25+
follower.getName(),
26+
follower.getProfileImageUrl(),
27+
follower.getGrade().name(),
28+
follow.getCreateDate()
29+
);
30+
}
31+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.back.domain.follow.dto.response;
2+
3+
import com.back.domain.artist.entity.ArtistProfile;
4+
import com.back.domain.follow.entity.Follow;
5+
6+
import java.time.LocalDateTime;
7+
8+
/**
9+
* 팔로잉 목록 응답 DTO
10+
*/
11+
public record FollowingListResponse(
12+
Long followId,
13+
Long artistId,
14+
String artistName,
15+
String profileImageUrl,
16+
Integer followerCount,
17+
LocalDateTime followedAt
18+
) {
19+
/**
20+
* Follow 엔티티 -> DTO 변환
21+
*/
22+
public static FollowingListResponse from(Follow follow) {
23+
ArtistProfile artist = follow.getFollowingArtist();
24+
25+
return new FollowingListResponse(
26+
follow.getId(),
27+
artist.getId(),
28+
artist.getArtistName(),
29+
artist.getProfileImageUrl(),
30+
artist.getFollowerCount(),
31+
follow.getCreateDate()
32+
);
33+
}
34+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package com.back.domain.follow.entity;
2+
3+
import com.back.domain.artist.entity.ArtistProfile;
4+
import com.back.domain.user.entity.User;
5+
import com.back.global.exception.ServiceException;
6+
import com.back.global.jpa.entity.BaseEntity;
7+
import jakarta.persistence.*;
8+
import lombok.AccessLevel;
9+
import lombok.Builder;
10+
import lombok.Getter;
11+
import lombok.NoArgsConstructor;
12+
13+
import java.time.LocalDateTime;
14+
15+
/**
16+
* 팔로우 엔티티
17+
*/
18+
@Getter
19+
@Entity
20+
@Table(
21+
name = "follows",
22+
uniqueConstraints = {
23+
@UniqueConstraint(
24+
name = "uk_follower_artist",
25+
columnNames = {"follower_id", "following_artist_id"}
26+
)
27+
}
28+
)
29+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
30+
public class Follow extends BaseEntity {
31+
32+
@ManyToOne(fetch = FetchType.LAZY)
33+
@JoinColumn(name = "follower_id", nullable = false)
34+
private User follower;
35+
36+
@ManyToOne(fetch = FetchType.LAZY)
37+
@JoinColumn(name = "following_artist_id", nullable = false)
38+
private ArtistProfile followingArtist;
39+
40+
@Builder
41+
public Follow(User follower, ArtistProfile followingArtist) {
42+
this.follower = follower;
43+
this.followingArtist = followingArtist;
44+
}
45+
46+
// ===== 정적 팩토리 메서드 ===== //
47+
48+
/**
49+
* 팔로우 관계 생성
50+
*/
51+
public static Follow create(User follower, ArtistProfile artist) {
52+
validateFollowCreation(follower, artist);
53+
54+
return Follow.builder()
55+
.follower(follower)
56+
.followingArtist(artist)
57+
.build();
58+
}
59+
60+
// ===== 검증 메서드 ===== //
61+
62+
/**
63+
* 팔로우 생성 유효성 검증
64+
*/
65+
private static void validateFollowCreation(User follower, ArtistProfile artist) {
66+
if (follower == null) {
67+
throw new ServiceException("400", "팔로우하는 사용자 정보가 없습니다.");
68+
}
69+
70+
if (artist == null) {
71+
throw new ServiceException("400", "팔로우 대상 작가 정보가 없습니다.");
72+
}
73+
74+
// 자기 자신을 팔로우하는 것 방지
75+
if (follower.getId().equals(artist.getUser().getId())) {
76+
throw new ServiceException("400", "자기 자신을 팔로우할 수 없습니다.");
77+
}
78+
}
79+
80+
/**
81+
* 팔로우한 사용자 본인인지 확인
82+
*/
83+
public boolean isFollowedBy(Long userId) {
84+
return this.follower.getId().equals(userId);
85+
}
86+
87+
/**
88+
* 팔로우한 사용자가 아닌 경우 예외 발생
89+
*/
90+
public void validateFollower(Long userId) {
91+
if (!isFollowedBy(userId)) {
92+
throw new ServiceException("403", "본인이 팔로우한 관계만 삭제할 수 있습니다.");
93+
}
94+
}
95+
96+
/**
97+
* 팔로우 시작 시간 조회 (BaseEntity의 createDate 사용)
98+
*/
99+
public LocalDateTime getFollowedAt() {
100+
return this.getCreateDate();
101+
}
102+
}

0 commit comments

Comments
 (0)