좋아요 시스템 (Like System) - fitpassTeam/fitpass GitHub Wiki
사용자가 헬스장과 게시글에 대해 좋아요를 표현할 수 있는 토글 방식의 좋아요 시스템입니다. 토글 방식으로 구현되어 한 번 클릭하면 좋아요 추가, 다시 클릭하면 좋아요 취소가 되는 직관적인 UX를 제공합니다.
domain/likes/
├── controller/
│ └── LikeController.java # 좋아요 토글 API
├── service/
│ └── LikeService.java # 좋아요 비즈니스 로직
├── entity/
│ └── Like.java # 좋아요 엔티티
├── repository/
│ └── LikeRepository.java # 좋아요 데이터 접근
└── enums/
└── LikeType.java # 좋아요 대상 타입
- 한 번 클릭 → 좋아요 추가
- 다시 클릭 → 좋아요 취소
- 사용자 친화적인 UX 제공
- 헬스장(GYM) 좋아요
- 게시글(POST) 좋아요
- 확장 가능한 구조 (트레이너, 리뷰 등 추가 가능)
- 정규화된 테이블 설계
- 인덱스 최적화 가능
- 집계 쿼리 지원
@Entity(name = "likes")
public class Like extends BaseEntity {
private Long id; // 좋아요 ID
private User user; // 좋아요를 누른 사용자
private LikeType likeType; // 좋아요 대상 타입 (GYM, POST)
private Long targetId; // 대상 객체 ID
private LocalDateTime createdAt; // 좋아요 생성 시간
private LocalDateTime updatedAt; // 마지막 수정 시간
}
public enum LikeType {
POST, // 게시글 좋아요
GYM // 헬스장 좋아요
}
POST /gyms/{gymId}/like
처리 과정:
- 사용자 인증: JWT 토큰 기반 사용자 확인
-
중복 확인:
findByUserAndTargetId(user, gymId)
조회 -
토글 로직:
- 기존 좋아요 없음 → 새로운 좋아요 생성
- 기존 좋아요 있음 → 좋아요 삭제
- 응답: 성공 메시지 반환
POST /posts/{postId}/like
처리 과정:
- 헬스장 좋아요와 동일한 플로우
- LikeType.POST로 구분하여 저장
@Transactional
public void postGymLike(Long userId, Long gymId) {
User user = userRepository.findByIdOrElseThrow(userId);
Optional<Like> likeOptional = likeRepository.findByUserAndTargetId(user, gymId);
if(likeOptional.isEmpty()){
// 좋아요가 없으면 생성
Like like = Like.of(user, LikeType.GYM, gymId);
likeRepository.save(like);
} else {
// 좋아요가 있으면 삭제 (토글)
likeRepository.delete(likeOptional.get());
}
}
@Query("SELECT l.targetId FROM likes l WHERE l.user.id = :userId AND l.likeType = :likeType")
Set<Long> findTargetIdsByUserIdAndLikeType(@Param("userId") Long userId, @Param("likeType") LikeType likeType);
// GymService.getAllGyms() 에서 활용
Set<Long> likedGymIds = (userId != null)
? likeRepository.findTargetIdsByUserIdAndLikeType(userId, LikeType.GYM)
: Collections.emptySet();
return gyms.map(gym -> {
boolean isLiked = likedGymIds.contains(gym.getId());
return GymResponseDto.from(gym, isLiked); // 좋아요 상태 포함
});
- Post 도메인에서 유사한 방식으로 연동 가능
- 사용자별 좋아요한 게시글 목록 조회
Method | Endpoint | 설명 | 권한 |
---|---|---|---|
POST | /gyms/{gymId}/like | 헬스장 좋아요 토글 | USER |
POST | /posts/{postId}/like | 게시글 좋아요 토글 | USER |
POST /gyms/123/like
Authorization: Bearer {JWT_TOKEN}
응답 (성공)
{
"statusCode": 200,
"message": "좋아요가 성공적으로 처리되었습니다.",
"data": null
}
POST /posts/456/like
Authorization: Bearer {JWT_TOKEN}
응답 (성공)
{
"statusCode": 200,
"message": "좋아요가 성공적으로 처리되었습니다.",
"data": null
}
- JWT 토큰 기반 사용자 인증 필수
- 로그인하지 않은 사용자는 좋아요 불가
// 헬스장 존재 확인
@Transactional
public void postGymLike(Long userId, Long gymId) {
User user = userRepository.findByIdOrElseThrow(userId);
Gym gym = gymRepository.findByIdOrElseThrow(gymId); // 존재 확인
// ... 기존 로직 ...
}
-
findByUserAndTargetId()
쿼리로 중복 좋아요 방지 - DB 레벨에서 UNIQUE 제약조건 추가 권장
-- 복합 인덱스 설정으로 조회 성능 향상
CREATE INDEX idx_likes_user_target_type ON likes(user_id, target_id, like_type);
CREATE INDEX idx_likes_target_type ON likes(target_id, like_type);
// 여러 대상의 좋아요 상태를 한 번에 조회
@Query("SELECT l.targetId FROM likes l WHERE l.user.id = :userId AND l.targetId IN :targetIds AND l.likeType = :likeType")
Set<Long> findLikedTargetIds(@Param("userId") Long userId, @Param("targetIds") List<Long> targetIds, @Param("likeType") LikeType likeType);
- 토글 방식의 사용자 친화적 UX
- 별도의 상태 관리 없이 생성/삭제로 처리
- LikeType으로 다양한 대상 지원
- 새로운 좋아요 대상 쉽게 추가 가능
- 정규화된 테이블 구조
- 사용자별 좋아요 목록 빠른 조회
- 헬스장 목록에서 좋아요 상태 표시
- 통계 및 집계 데이터 활용 가능