diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml
index e20e2aae..1ca5a619 100644
--- a/.github/workflows/github-actions.yml
+++ b/.github/workflows/github-actions.yml
@@ -29,7 +29,7 @@ jobs:
echo "${{ secrets.APPLICATION_PROD }}" > ./application.yml
# 환경별 yml 파일 생성(2) - dev
- - name: Bake application-dev.yml
+ - name: Make application-dev.yml
if: contains(github.ref, 'deploy')
run: |
cd ./src/main/resources
diff --git a/README.md b/README.md
index 354f4603..7e289d12 100644
--- a/README.md
+++ b/README.md
@@ -18,7 +18,7 @@ EFUB 4기 SWS 3팀 "SongPin" 프로젝트 백엔드 레포지토리입니다.
-
+
@@ -82,7 +82,7 @@ EFUB 4기 SWS 3팀 "SongPin" 프로젝트 백엔드 레포지토리입니다.
| :---: | :---: | :---: | :---: |
| | | | |
| [@seohyun-lee](https://github.com/seohyun-lee) | [@jud1thDev](https://github.com/jud1thDev) | [@crHwang0822](https://github.com/crHwang0822) | [@gkdudans](https://github.com/gkdudans) |
-| [배포] 서버 배포 및 CI/CD 구축
[Place] 장소 검색, 장소 상세정보 조회 기능
[Map] 지도 마커 표시 목적 장소 좌표들 가져오기 기능 (기간&장르 필터링, 유저별, 플레이리스트별)
[Follow] 유저 검색, 유저의 팔로잉/팔로워 목록 조회, 타 유저를 팔로우, 팔로잉 취소/팔로워 삭제 기능
[Alarm] 알림 목록 조회, 구독 기능
[Statistics] 서비스의 종합 통계, 장르별 통계 조회 기능 | [Pin] 핀 생성, 조회, 수정, 삭제 기능
[Spotify] 핀 생성 시 Spotify API를 활용한 노래 검색 기능
[Song] 노래 상세정보 조회, 해당 노래에 대한 전체 핀 목록/내 핀 목록 조회, 노래 검색 기능
[Feed] 타 유저/내 핀 피드 조회, 내 핀 피드 검색, 내 핀피드 캘린더 기능 | [Member] 회원가입, 회원 탈퇴, 프로필 조회, 프로필 편집, Redis를 활용한 비밀번호 재설정 메일 전송 및 비밀번호 변경 기능
[Auth] 스프링 시큐리티, JWT, Redis를 활용한 토큰 (재)발급/인증/인가 구현, 로그인/로그아웃 기능 | [Playlist, PlaylistPin] 플레이리스트 생성, 핀 담기, 메인, 검색, 상세정보 조회, 편집, 삭제, 내 플레이리스트 목록/타 유저 플레이리스트 목록 조회 기능
[Bookmark] 북마크 생성, 취소, 내 북마크 목록 조회 기능
[Home] 최근 생성된 핀 & 장소 조회 기능 |
+| [배포] 서버 배포 및 CI/CD 구축
[Place] 장소 검색, 장소 상세정보 조회 기능
[Map] 지도 마커 표시 목적 장소 좌표들 가져오기 기능 (기간&장르 필터링, 유저별, 플레이리스트별)
[Follow] 유저 검색, 유저의 팔로잉/팔로워 목록 조회, 타 유저를 팔로우, 팔로잉 취소/팔로워 삭제 기능
[Alarm] SSE 알림 구독, 알림 목록 조회 기능
[Statistics] 서비스의 종합 통계, 장르별 통계 조회 기능 | [Pin] 핀 생성, 조회, 수정, 삭제 기능
[Spotify] 핀 생성 시 Spotify API를 활용한 노래 검색 기능
[Song] 노래 상세정보 조회, 해당 노래에 대한 전체 핀 목록/내 핀 목록 조회, 노래 검색 기능
[Feed] 타 유저/내 핀 피드 조회, 내 핀 피드 검색, 내 핀피드 캘린더 기능 | [Member] 회원가입, 회원 탈퇴, 프로필 조회, 프로필 편집, Redis를 활용한 비밀번호 재설정 메일 전송 및 비밀번호 변경 기능
[Auth] 스프링 시큐리티, JWT, Redis를 활용한 토큰 (재)발급/인증/인가 구현, 로그인/로그아웃 기능 | [Playlist, PlaylistPin] 플레이리스트 생성, 핀 담기, 메인, 검색, 상세정보 조회, 편집, 삭제, 내 플레이리스트 목록/타 유저 플레이리스트 목록 조회 기능
[Bookmark] 북마크 생성, 취소, 내 북마크 목록 조회 기능
[Home] 최근 생성된 핀 & 장소 조회 기능 |
diff --git a/src/main/java/sws/songpin/domain/alarm/service/EmitterService.java b/src/main/java/sws/songpin/domain/alarm/service/EmitterService.java
index 71323700..eff68ad5 100644
--- a/src/main/java/sws/songpin/domain/alarm/service/EmitterService.java
+++ b/src/main/java/sws/songpin/domain/alarm/service/EmitterService.java
@@ -3,7 +3,6 @@
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import sws.songpin.domain.alarm.dto.ssedata.AlarmDefaultDataDto;
import sws.songpin.domain.alarm.repository.AlarmRepository;
@@ -12,10 +11,10 @@
import sws.songpin.domain.member.service.MemberService;
import java.io.IOException;
+import java.util.Optional;
@Slf4j
@Service
-@Transactional
@RequiredArgsConstructor
public class EmitterService {
private final EmitterRepository emitterRepository;
@@ -26,7 +25,12 @@ public class EmitterService {
public SseEmitter subscribe() {
Member member = memberService.getCurrentMember();
- SseEmitter emitter = registerEmitter(member.getMemberId());
+ Long memberId = member.getMemberId();
+
+ // 이미 존재하는 Emitter가 있는지 확인
+ SseEmitter emitter = Optional.ofNullable(emitterRepository.get(memberId))
+ .orElseGet(() -> registerEmitter(memberId));
+
sendToClientIfNewAlarmExists(member);
return emitter;
}
diff --git a/src/main/java/sws/songpin/domain/bookmark/repository/BookmarkRepository.java b/src/main/java/sws/songpin/domain/bookmark/repository/BookmarkRepository.java
index fa0c78f9..b857480f 100644
--- a/src/main/java/sws/songpin/domain/bookmark/repository/BookmarkRepository.java
+++ b/src/main/java/sws/songpin/domain/bookmark/repository/BookmarkRepository.java
@@ -11,7 +11,8 @@
import java.util.Optional;
public interface BookmarkRepository extends JpaRepository {
- Optional findByPlaylistAndMember(Playlist playlist, Member member);
+ boolean existsByPlaylistAndMember(Playlist playlist, Member member);
+ List findAllByPlaylistAndMember(Playlist playlist, Member member);
@Query("SELECT b FROM Bookmark b WHERE b.member = :member ORDER BY b.bookmarkId DESC")
List findAllByMember(Member member);
void deleteAllByMember(Member member);
diff --git a/src/main/java/sws/songpin/domain/bookmark/service/BookmarkService.java b/src/main/java/sws/songpin/domain/bookmark/service/BookmarkService.java
index baea3efd..c59e4275 100644
--- a/src/main/java/sws/songpin/domain/bookmark/service/BookmarkService.java
+++ b/src/main/java/sws/songpin/domain/bookmark/service/BookmarkService.java
@@ -37,18 +37,17 @@ public boolean changeBookmark(BookmarkRequestDto requestDto) {
if (!member.equals(playlist.getCreator()) && playlist.getVisibility() == Visibility.PRIVATE) {
throw new CustomException(ErrorCode.UNAUTHORIZED_REQUEST);
}
- Optional bookmark = getBookmarkByPlaylistAndMember(playlist, member);
- if(bookmark.isPresent()){
- bookmark.ifPresent(bookmarkRepository::delete);
- return false;
- }
- else{
+ List bookmarks = bookmarkRepository.findAllByPlaylistAndMember(playlist, member);
+ if (bookmarks.isEmpty()) {
Bookmark newBookmark = Bookmark.builder()
.member(member)
.playlist(playlist)
.build();
bookmarkRepository.save(newBookmark);
return true;
+ } else { // 북마크가 존재하면 삭제
+ bookmarkRepository.deleteAll(bookmarks);
+ return false;
}
}
@@ -65,11 +64,6 @@ public BookmarkListResponseDto getAllBookmarks() {
return BookmarkListResponseDto.from(bookmarkList);
}
- @Transactional(readOnly = true)
- public Optional getBookmarkByPlaylistAndMember(Playlist playlist, Member member) {
- return bookmarkRepository.findByPlaylistAndMember(playlist, member);
- }
-
public void deleteAllBookmarksOfMember(Member member){
bookmarkRepository.deleteAllByMember(member);
}
diff --git a/src/main/java/sws/songpin/domain/follow/repository/FollowRepository.java b/src/main/java/sws/songpin/domain/follow/repository/FollowRepository.java
index d7d34b0a..98356fc1 100644
--- a/src/main/java/sws/songpin/domain/follow/repository/FollowRepository.java
+++ b/src/main/java/sws/songpin/domain/follow/repository/FollowRepository.java
@@ -12,7 +12,7 @@ public interface FollowRepository extends JpaRepository {
List findAllByFollower(Member follower);
Long countByFollowing(Member following);
Long countByFollower(Member follower);
- Optional findByFollowerAndFollowing(Member follower, Member following);
+ List findAllByFollowerAndFollowing(Member follower, Member following);
void deleteAllByFollower(Member member);
void deleteAllByFollowing(Member member);
}
\ No newline at end of file
diff --git a/src/main/java/sws/songpin/domain/follow/service/FollowService.java b/src/main/java/sws/songpin/domain/follow/service/FollowService.java
index efda66bf..9a59b2b6 100644
--- a/src/main/java/sws/songpin/domain/follow/service/FollowService.java
+++ b/src/main/java/sws/songpin/domain/follow/service/FollowService.java
@@ -1,6 +1,7 @@
package sws.songpin.domain.follow.service;
import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import sws.songpin.domain.alarm.service.AlarmService;
@@ -20,6 +21,7 @@
import java.util.Optional;
import java.util.stream.Collectors;
+@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
@@ -36,14 +38,15 @@ public boolean createOrDeleteFollow(FollowRequestDto requestDto) {
throw new CustomException(ErrorCode.FOLLOW_BAD_REQUEST);
}
- Optional followOptional = followRepository.findByFollowerAndFollowing(currentMember, targetMember);
- if (followOptional.isPresent()) { // 팔로우가 존재하면 삭제
- followRepository.delete(followOptional.get());
- return false;
- } else { // 팔로우 추가
+ // 1개가 존재하기를 기대하지만, 멀티 스레드 이슈로 여러개가 입력될 수 있음
+ List follows = followRepository.findAllByFollowerAndFollowing(currentMember, targetMember);
+ if (follows.isEmpty()) { // 팔로우 추가
followRepository.save(FollowRequestDto.toEntity(currentMember, targetMember));
alarmService.createFollowAlarm(currentMember, targetMember);
return true;
+ } else { // 팔로우가 존재하면 삭제
+ followRepository.deleteAll(follows);
+ return false;
}
}
@@ -55,33 +58,38 @@ public void deleteFollower(FollowRequestDto requestDto){
throw new CustomException(ErrorCode.FOLLOW_BAD_REQUEST);
}
- Optional followOptional = followRepository.findByFollowerAndFollowing(targetMember, currentMember);
- if (followOptional.isPresent()) { // 팔로우가 존재하면 삭제
- followRepository.delete(followOptional.get());
- } else {
+ // 1개가 존재하기를 기대하지만, 멀티 스레드 이슈로 여러개가 입력될 수 있음
+ List follows = followRepository.findAllByFollowerAndFollowing(currentMember, targetMember);
+ if (follows.isEmpty()) {
throw new CustomException(ErrorCode.FOLLOW_NOT_FOUND);
+ } else { // 팔로우가 존재하면 삭제
+ followRepository.deleteAll(follows);
}
}
+ @Transactional(readOnly = true)
public Boolean checkIfFollowing(Member targetMember){
Member currentMember = memberService.getCurrentMember();
return checkIfFollowExists(currentMember, targetMember);
}
+ @Transactional(readOnly = true)
public Boolean checkIfFollower(Member targetMember) {
Member currentMember = memberService.getCurrentMember();
return checkIfFollowExists(targetMember, currentMember);
}
+ @Transactional(readOnly = true)
public Boolean checkIfFollowExists(Member follower, Member following) {
if (follower.equals(following)) {
return null;
}
- Optional followOptional = followRepository.findByFollowerAndFollowing(follower, following);
- if (followOptional.isPresent()) {
- return true;
- } else {
+ // 1개가 존재하기를 기대하지만, 멀티 스레드 이슈로 여러개가 입력될 수 있음
+ List follows = followRepository.findAllByFollowerAndFollowing(follower, following);
+ if (follows.isEmpty()) {
return false;
+ } else {
+ return true;
}
}
diff --git a/src/main/java/sws/songpin/domain/genre/service/GenreService.java b/src/main/java/sws/songpin/domain/genre/service/GenreService.java
index 586a6ac8..af812634 100644
--- a/src/main/java/sws/songpin/domain/genre/service/GenreService.java
+++ b/src/main/java/sws/songpin/domain/genre/service/GenreService.java
@@ -11,7 +11,6 @@
import sws.songpin.global.exception.ErrorCode;
@Service
-@Transactional
@RequiredArgsConstructor
public class GenreService {
private final GenreRepository genreRepository;
@@ -29,6 +28,7 @@ public void initGenres() {
}
}
+ @Transactional(readOnly = true)
public Genre getGenreByGenreName(GenreName genreName) {
return genreRepository.findByGenreName(genreName)
.orElseThrow(() -> new CustomException(ErrorCode.GENRE_NOT_FOUND));
diff --git a/src/main/java/sws/songpin/domain/member/service/HomeService.java b/src/main/java/sws/songpin/domain/member/service/HomeService.java
index c4dbe8a1..338b9784 100644
--- a/src/main/java/sws/songpin/domain/member/service/HomeService.java
+++ b/src/main/java/sws/songpin/domain/member/service/HomeService.java
@@ -16,7 +16,6 @@
import java.util.stream.Collectors;
@Service
-@Transactional
@RequiredArgsConstructor
public class HomeService {
private final MemberService memberService;
@@ -30,7 +29,7 @@ public HomeResponseDto getHome() {
List places = placeRepository.findTop3ByOrderByPlaceIdDesc();
// pinList
List pinList = pins.stream()
- .map(pin -> PinBasicUnitDto.from(pin, pin.getCreator().equals(currentMember)))
+ .map(pin -> PinBasicUnitDto.from(pin, pin.getSong(), pin.getPlace(), pin.getGenre().getGenreName(), pin.getCreator().equals(currentMember)))
.collect(Collectors.toList());
// placeList
List placeList = places.stream()
diff --git a/src/main/java/sws/songpin/domain/member/service/MemberService.java b/src/main/java/sws/songpin/domain/member/service/MemberService.java
index ed7550bc..63051c07 100644
--- a/src/main/java/sws/songpin/domain/member/service/MemberService.java
+++ b/src/main/java/sws/songpin/domain/member/service/MemberService.java
@@ -1,7 +1,6 @@
package sws.songpin.domain.member.service;
import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.security.core.Authentication;
@@ -20,19 +19,16 @@
import static sws.songpin.global.common.EscapeSpecialCharactersService.escapeSpecialCharacters;
-@Slf4j
@Service
-@Transactional
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
// 유저 검색
- @Transactional(readOnly = true)
public MemberSearchResponseDto searchMembers(String keyword, Pageable pageable) {
// 키워드의 이스케이프 처리
String escapedWord = escapeSpecialCharacters(keyword);
- Page memberPage = memberRepository.findAllByHandleContainingOrNicknameContaining(escapedWord, pageable);
+ Page memberPage = getSearchedMemberPage(escapedWord, pageable);
Long currentMemberId = getCurrentMember().getMemberId();
// Page를 Page로 변환
@@ -43,6 +39,11 @@ public MemberSearchResponseDto searchMembers(String keyword, Pageable pageable)
return MemberSearchResponseDto.from(memberUnitDtoPage);
}
+ @Transactional(readOnly = true)
+ public Page getSearchedMemberPage(String escapedWord, Pageable pageable) {
+ return memberRepository.findAllByHandleContainingOrNicknameContaining(escapedWord, pageable);
+ }
+
@Transactional(readOnly = true)
public Member getCurrentMember() throws CustomException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
@@ -103,6 +104,7 @@ public boolean checkMemberExistsByHandle(String handle){
return memberRepository.existsByHandle(handle);
}
+ @Transactional
public Member saveMember(Member member){
return memberRepository.save(member);
}
diff --git a/src/main/java/sws/songpin/domain/pin/dto/response/PinBasicUnitDto.java b/src/main/java/sws/songpin/domain/pin/dto/response/PinBasicUnitDto.java
index a3fc150b..d4e4e7cb 100644
--- a/src/main/java/sws/songpin/domain/pin/dto/response/PinBasicUnitDto.java
+++ b/src/main/java/sws/songpin/domain/pin/dto/response/PinBasicUnitDto.java
@@ -2,7 +2,9 @@
import sws.songpin.domain.genre.entity.GenreName;
import sws.songpin.domain.pin.entity.Pin;
+import sws.songpin.domain.place.entity.Place;
import sws.songpin.domain.song.dto.response.SongInfoDto;
+import sws.songpin.domain.song.entity.Song;
import java.time.LocalDate;
@@ -18,15 +20,15 @@ public record PinBasicUnitDto(
Boolean isMine
) {
- public static PinBasicUnitDto from(Pin pin, Boolean isMine) {
+ public static PinBasicUnitDto from(Pin pin, Song song, Place place, GenreName genreName, boolean isMine) {
return new PinBasicUnitDto(
pin.getPinId(),
- SongInfoDto.from(pin.getSong()),
+ SongInfoDto.from(song),
pin.getListenedDate(),
- pin.getPlace().getPlaceName(),
- pin.getPlace().getLatitude( ),
- pin.getPlace().getLongitude(),
- pin.getGenre().getGenreName(),
+ place.getPlaceName(),
+ place.getLatitude(),
+ place.getLongitude(),
+ genreName,
isMine
);
}
diff --git a/src/main/java/sws/songpin/domain/pin/dto/response/PinExistingInfoResponseDto.java b/src/main/java/sws/songpin/domain/pin/dto/response/PinExistingInfoResponseDto.java
index 38f15e65..ad36440b 100644
--- a/src/main/java/sws/songpin/domain/pin/dto/response/PinExistingInfoResponseDto.java
+++ b/src/main/java/sws/songpin/domain/pin/dto/response/PinExistingInfoResponseDto.java
@@ -16,14 +16,14 @@ public record PinExistingInfoResponseDto(
String memo,
Visibility visibility
) {
- public static PinExistingInfoResponseDto from(Pin pin) {
+ public static PinExistingInfoResponseDto from(Pin pin, GenreName genreName) {
return new PinExistingInfoResponseDto(
pin.getSong().getImgPath(),
pin.getSong().getTitle(),
pin.getSong().getArtist(),
pin.getListenedDate(),
pin.getPlace().getPlaceName(),
- pin.getGenre().getGenreName(),
+ genreName,
pin.getMemo(),
pin.getVisibility()
);
diff --git a/src/main/java/sws/songpin/domain/pin/dto/response/PinFeedUnitDto.java b/src/main/java/sws/songpin/domain/pin/dto/response/PinFeedUnitDto.java
index 69811682..2a28d3c6 100644
--- a/src/main/java/sws/songpin/domain/pin/dto/response/PinFeedUnitDto.java
+++ b/src/main/java/sws/songpin/domain/pin/dto/response/PinFeedUnitDto.java
@@ -19,7 +19,7 @@ public record PinFeedUnitDto(
Boolean isMine
) {
- public static PinFeedUnitDto from(Pin pin, Boolean isMine) {
+ public static PinFeedUnitDto from(Pin pin, GenreName genreName, Boolean isMine) {
return new PinFeedUnitDto(
pin.getPinId(),
SongInfoDto.from(pin.getSong()),
@@ -27,7 +27,7 @@ public static PinFeedUnitDto from(Pin pin, Boolean isMine) {
pin.getPlace().getPlaceName(),
pin.getPlace().getLatitude(),
pin.getPlace().getLongitude(),
- pin.getGenre().getGenreName(),
+ genreName,
pin.getMemo(),
pin.getVisibility(),
isMine
diff --git a/src/main/java/sws/songpin/domain/pin/repository/PinRepository.java b/src/main/java/sws/songpin/domain/pin/repository/PinRepository.java
index ce9d84ce..f5acdeb3 100644
--- a/src/main/java/sws/songpin/domain/pin/repository/PinRepository.java
+++ b/src/main/java/sws/songpin/domain/pin/repository/PinRepository.java
@@ -36,8 +36,8 @@ public interface PinRepository extends JpaRepository {
@Query("SELECT p FROM Pin p WHERE p.creator = :creator AND YEAR(p.listenedDate) = :year AND MONTH(p.listenedDate) = :month")
List findAllByCreatorAndDate(@Param("creator") Member creator, @Param("year") int year, @Param("month") int month);
- @Query("SELECT COUNT(p) FROM Pin p WHERE YEAR(p.listenedDate) = :currentYear")
- long countByListenedDateYear(@Param("currentYear") int currentYear);
+ @Query("SELECT COUNT(p) FROM Pin p WHERE YEAR(p.createdTime) = :currentYear")
+ long countByCreatedTimeYear(@Param("currentYear") int currentYear);
@Query("SELECT p.genre.genreName, COUNT(p) FROM Pin p GROUP BY p.genre ORDER BY COUNT(p) DESC")
List