From 9fcc10cf7d148551d776dc2ea5ad4a88e5c65ef6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8A=A4=ED=94=8C=EB=A6=BF?= Date: Thu, 17 Aug 2023 21:42:22 +0900 Subject: [PATCH 01/10] =?UTF-8?q?Refactor:=20=ED=98=84=EC=9E=AC=20main=20?= =?UTF-8?q?=EC=97=90=20=EB=8C=80=ED=95=9C=20=EC=A0=84=EC=B2=B4=EC=A0=81?= =?UTF-8?q?=EC=9D=B8=20suggestion=20=EC=BD=94=EB=93=9C=20(#286)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 로그인, 액세스 토큰 재발급 기능 리팩토링 빠른 진행을 PR 대신 주석으로 리뷰내용을 작성하고 suggestion 코드 작성 * refactor: VotingSongRepositoryTest conflict 해결 * refactor: 리팩토링 반영 완료 --- .../shook/auth/application/AuthService.java | 22 +- .../auth/application/GoogleInfoProvider.java | 37 ++-- .../dto/GoogleAccessTokenRequest.java | 18 -- ...e.java => ReissueAccessTokenResponse.java} | 2 +- .../dto/{TokenInfo.java => TokenPair.java} | 2 +- ...java => AccessTokenReissueController.java} | 17 +- .../shook/shook/auth/ui/AuthController.java | 11 +- .../auth/ui/interceptor/TokenInterceptor.java | 2 +- .../member/application/MemberService.java | 20 +- .../member/ui/MemberExceptionHandler.java | 1 - .../shook/song/application/SongService.java | 7 +- .../KillingPartCommentService.java | 11 +- .../killingpart/KillingPartLikeService.java | 6 +- .../application/VotingSongPartService.java | 19 +- .../application/VotingSongService.java | 27 ++- .../dto/VotingSongSwipeResponse.java | 13 +- .../repository/VotingSongRepository.java | 5 +- .../voting_song/ui/VotingSongController.java | 4 +- backend/src/main/resources/shook-security | 2 +- .../auth/application/AuthServiceTest.java | 13 +- .../application/GoogleInfoProviderTest.java | 22 -- ... => AccessTokenReissueControllerTest.java} | 8 +- .../shook/auth/ui/AuthControllerTest.java | 10 +- .../member/application/MemberServiceTest.java | 36 +--- .../application/VotingSongServiceTest.java | 4 +- .../repository/VotingSongRepositoryTest.java | 193 +++++++++++------- 26 files changed, 236 insertions(+), 276 deletions(-) delete mode 100644 backend/src/main/java/shook/shook/auth/application/dto/GoogleAccessTokenRequest.java rename backend/src/main/java/shook/shook/auth/application/dto/{TokenReissueResponse.java => ReissueAccessTokenResponse.java} (87%) rename backend/src/main/java/shook/shook/auth/application/dto/{TokenInfo.java => TokenPair.java} (88%) rename backend/src/main/java/shook/shook/auth/ui/{TokenController.java => AccessTokenReissueController.java} (52%) rename backend/src/test/java/shook/shook/auth/ui/{TokenControllerTest.java => AccessTokenReissueControllerTest.java} (89%) diff --git a/backend/src/main/java/shook/shook/auth/application/AuthService.java b/backend/src/main/java/shook/shook/auth/application/AuthService.java index 6824b817e..ef25d06c4 100644 --- a/backend/src/main/java/shook/shook/auth/application/AuthService.java +++ b/backend/src/main/java/shook/shook/auth/application/AuthService.java @@ -5,10 +5,9 @@ import org.springframework.stereotype.Service; import shook.shook.auth.application.dto.GoogleAccessTokenResponse; import shook.shook.auth.application.dto.GoogleMemberInfoResponse; -import shook.shook.auth.application.dto.TokenInfo; -import shook.shook.auth.application.dto.TokenReissueResponse; +import shook.shook.auth.application.dto.ReissueAccessTokenResponse; +import shook.shook.auth.application.dto.TokenPair; import shook.shook.member.application.MemberService; -import shook.shook.member.domain.Email; import shook.shook.member.domain.Member; import shook.shook.member.domain.Nickname; @@ -20,30 +19,31 @@ public class AuthService { private final GoogleInfoProvider googleInfoProvider; private final TokenProvider tokenProvider; - public TokenInfo login(final String accessCode) { + public TokenPair login(final String authorizationCode) { final GoogleAccessTokenResponse accessTokenResponse = - googleInfoProvider.getAccessToken(accessCode); + googleInfoProvider.getAccessToken(authorizationCode); final GoogleMemberInfoResponse memberInfo = googleInfoProvider .getMemberInfo(accessTokenResponse.getAccessToken()); final String userEmail = memberInfo.getEmail(); - final Member member = memberService.findByEmail(new Email(userEmail)) + + final Member member = memberService.findByEmail(userEmail) .orElseGet(() -> memberService.register(userEmail)); - final long memberId = member.getId(); + final Long memberId = member.getId(); final String nickname = member.getNickname(); final String accessToken = tokenProvider.createAccessToken(memberId, nickname); final String refreshToken = tokenProvider.createRefreshToken(memberId, nickname); - return new TokenInfo(accessToken, refreshToken); + return new TokenPair(accessToken, refreshToken); } - public TokenReissueResponse reissueToken(final String refreshToken) { + public ReissueAccessTokenResponse reissueAccessTokenByRefreshToken(final String refreshToken) { final Claims claims = tokenProvider.parseClaims(refreshToken); final Long memberId = claims.get("memberId", Long.class); final String nickname = claims.get("nickname", String.class); - memberService.findByIdAndNickname(memberId, new Nickname(nickname)); + memberService.findByIdAndNicknameThrowIfNotExist(memberId, new Nickname(nickname)); final String accessToken = tokenProvider.createAccessToken(memberId, nickname); - return new TokenReissueResponse(accessToken); + return new ReissueAccessTokenResponse(accessToken); } } diff --git a/backend/src/main/java/shook/shook/auth/application/GoogleInfoProvider.java b/backend/src/main/java/shook/shook/auth/application/GoogleInfoProvider.java index d9b911bc7..b31dc552c 100644 --- a/backend/src/main/java/shook/shook/auth/application/GoogleInfoProvider.java +++ b/backend/src/main/java/shook/shook/auth/application/GoogleInfoProvider.java @@ -1,16 +1,17 @@ package shook.shook.auth.application; +import java.util.HashMap; import java.util.Objects; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.HttpServerErrorException; import org.springframework.web.client.RestTemplate; -import shook.shook.auth.application.dto.GoogleAccessTokenRequest; import shook.shook.auth.application.dto.GoogleAccessTokenResponse; import shook.shook.auth.application.dto.GoogleMemberInfoResponse; import shook.shook.auth.exception.OAuthException; @@ -46,18 +47,14 @@ public GoogleMemberInfoResponse getMemberInfo(final String accessToken) { headers.set(AUTHORIZATION_HEADER, TOKEN_PREFIX + accessToken); final HttpEntity request = new HttpEntity<>(headers); - final GoogleMemberInfoResponse responseEntity = restTemplate.exchange( + final ResponseEntity response = restTemplate.exchange( GOOGLE_MEMBER_INFO_URL, HttpMethod.GET, request, GoogleMemberInfoResponse.class - ).getBody(); + ); - if (!Objects.requireNonNull(responseEntity).isVerifiedEmail()) { - throw new OAuthException.InvalidEmailException(); - } - - return responseEntity; + return response.getBody(); } catch (HttpClientErrorException e) { throw new OAuthException.InvalidAccessTokenException(); } catch (HttpServerErrorException e) { @@ -67,19 +64,19 @@ public GoogleMemberInfoResponse getMemberInfo(final String accessToken) { public GoogleAccessTokenResponse getAccessToken(final String authorizationCode) { try { - final GoogleAccessTokenRequest googleAccessTokenRequest = new GoogleAccessTokenRequest( - authorizationCode, - GOOGLE_CLIENT_ID, - GOOGLE_CLIENT_SECRET, - LOGIN_REDIRECT_URL, - GRANT_TYPE); - final HttpEntity request = new HttpEntity<>( - googleAccessTokenRequest); - - return Objects.requireNonNull(restTemplate.postForEntity( + final HashMap params = new HashMap<>(); + params.put("code", authorizationCode); + params.put("client_id", GOOGLE_CLIENT_ID); + params.put("client_secret", GOOGLE_CLIENT_SECRET); + params.put("redirect_uri", LOGIN_REDIRECT_URL); + params.put("grant_type", GRANT_TYPE); + + final ResponseEntity response = restTemplate.postForEntity( GOOGLE_ACCESS_TOKEN_URL, - request, - GoogleAccessTokenResponse.class).getBody()); + params, + GoogleAccessTokenResponse.class); + + return Objects.requireNonNull(response.getBody()); } catch (HttpClientErrorException e) { throw new OAuthException.InvalidAuthorizationCodeException(); diff --git a/backend/src/main/java/shook/shook/auth/application/dto/GoogleAccessTokenRequest.java b/backend/src/main/java/shook/shook/auth/application/dto/GoogleAccessTokenRequest.java deleted file mode 100644 index a6e6d32fe..000000000 --- a/backend/src/main/java/shook/shook/auth/application/dto/GoogleAccessTokenRequest.java +++ /dev/null @@ -1,18 +0,0 @@ -package shook.shook.auth.application.dto; - -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@NoArgsConstructor(access = AccessLevel.PRIVATE) -@AllArgsConstructor -@Getter -public class GoogleAccessTokenRequest { - - private String code; - private String clientId; - private String clientSecret; - private String redirectUri; - private String grantType; -} diff --git a/backend/src/main/java/shook/shook/auth/application/dto/TokenReissueResponse.java b/backend/src/main/java/shook/shook/auth/application/dto/ReissueAccessTokenResponse.java similarity index 87% rename from backend/src/main/java/shook/shook/auth/application/dto/TokenReissueResponse.java rename to backend/src/main/java/shook/shook/auth/application/dto/ReissueAccessTokenResponse.java index 201e5e49e..76b3bdab4 100644 --- a/backend/src/main/java/shook/shook/auth/application/dto/TokenReissueResponse.java +++ b/backend/src/main/java/shook/shook/auth/application/dto/ReissueAccessTokenResponse.java @@ -8,7 +8,7 @@ @AllArgsConstructor @RequiredArgsConstructor(access = AccessLevel.PRIVATE) @Getter -public class TokenReissueResponse { +public class ReissueAccessTokenResponse { private String accessToken; } diff --git a/backend/src/main/java/shook/shook/auth/application/dto/TokenInfo.java b/backend/src/main/java/shook/shook/auth/application/dto/TokenPair.java similarity index 88% rename from backend/src/main/java/shook/shook/auth/application/dto/TokenInfo.java rename to backend/src/main/java/shook/shook/auth/application/dto/TokenPair.java index 3459b774e..79693cd83 100644 --- a/backend/src/main/java/shook/shook/auth/application/dto/TokenInfo.java +++ b/backend/src/main/java/shook/shook/auth/application/dto/TokenPair.java @@ -5,7 +5,7 @@ @AllArgsConstructor @Getter -public class TokenInfo { +public class TokenPair { private String accessToken; private String refreshToken; diff --git a/backend/src/main/java/shook/shook/auth/ui/TokenController.java b/backend/src/main/java/shook/shook/auth/ui/AccessTokenReissueController.java similarity index 52% rename from backend/src/main/java/shook/shook/auth/ui/TokenController.java rename to backend/src/main/java/shook/shook/auth/ui/AccessTokenReissueController.java index fe090bd19..02833a112 100644 --- a/backend/src/main/java/shook/shook/auth/ui/TokenController.java +++ b/backend/src/main/java/shook/shook/auth/ui/AccessTokenReissueController.java @@ -6,23 +6,28 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import shook.shook.auth.application.AuthService; -import shook.shook.auth.application.dto.TokenReissueResponse; +import shook.shook.auth.application.dto.ReissueAccessTokenResponse; import shook.shook.auth.exception.AuthorizationException; @RequiredArgsConstructor @RestController -public class TokenController { +public class AccessTokenReissueController { + + private static final String EMPTY_REFRESH_TOKEN = "none"; + private static final String REFRESH_TOKEN_KEY = "refreshToken"; private final AuthService authService; @GetMapping("/reissue") - public ResponseEntity reissue( - @CookieValue(value = "refreshToken", defaultValue = "none") final String refreshToken + public ResponseEntity reissueAccessToken( + @CookieValue(value = REFRESH_TOKEN_KEY, defaultValue = EMPTY_REFRESH_TOKEN) final String refreshToken ) { - if (refreshToken.equals("none")) { + if (refreshToken.equals(EMPTY_REFRESH_TOKEN)) { throw new AuthorizationException.RefreshTokenNotFoundException(); } - final TokenReissueResponse response = authService.reissueToken(refreshToken); + final ReissueAccessTokenResponse response = + authService.reissueAccessTokenByRefreshToken(refreshToken); + return ResponseEntity.ok(response); } } diff --git a/backend/src/main/java/shook/shook/auth/ui/AuthController.java b/backend/src/main/java/shook/shook/auth/ui/AuthController.java index 39fa640f3..fe8d5ad01 100644 --- a/backend/src/main/java/shook/shook/auth/ui/AuthController.java +++ b/backend/src/main/java/shook/shook/auth/ui/AuthController.java @@ -8,7 +8,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import shook.shook.auth.application.AuthService; -import shook.shook.auth.application.dto.TokenInfo; +import shook.shook.auth.application.dto.TokenPair; import shook.shook.auth.ui.dto.LoginResponse; @RequiredArgsConstructor @@ -20,14 +20,13 @@ public class AuthController { @GetMapping("/login/google") public ResponseEntity googleLogin( - @RequestParam("code") final String accessCode, + @RequestParam("code") final String authorizationCode, final HttpServletResponse response ) { - final TokenInfo tokenInfo = authService.login(accessCode); - final Cookie cookie = cookieProvider.createRefreshTokenCookie( - tokenInfo.getRefreshToken()); + final TokenPair tokenPair = authService.login(authorizationCode); + final Cookie cookie = cookieProvider.createRefreshTokenCookie(tokenPair.getRefreshToken()); response.addCookie(cookie); - final LoginResponse loginResponse = new LoginResponse(tokenInfo.getAccessToken()); + final LoginResponse loginResponse = new LoginResponse(tokenPair.getAccessToken()); return ResponseEntity.ok(loginResponse); } } diff --git a/backend/src/main/java/shook/shook/auth/ui/interceptor/TokenInterceptor.java b/backend/src/main/java/shook/shook/auth/ui/interceptor/TokenInterceptor.java index 17c6067ff..5042841f8 100644 --- a/backend/src/main/java/shook/shook/auth/ui/interceptor/TokenInterceptor.java +++ b/backend/src/main/java/shook/shook/auth/ui/interceptor/TokenInterceptor.java @@ -39,7 +39,7 @@ public boolean preHandle( final Claims claims = tokenProvider.parseClaims(token); final Long memberId = claims.get("memberId", Long.class); final String nickname = claims.get("nickname", String.class); - memberService.findByIdAndNickname(memberId, new Nickname(nickname)); + memberService.findByIdAndNicknameThrowIfNotExist(memberId, new Nickname(nickname)); authContext.setAuthenticatedMember(memberId); diff --git a/backend/src/main/java/shook/shook/member/application/MemberService.java b/backend/src/main/java/shook/shook/member/application/MemberService.java index d2f05b4b4..137f8d42c 100644 --- a/backend/src/main/java/shook/shook/member/application/MemberService.java +++ b/backend/src/main/java/shook/shook/member/application/MemberService.java @@ -22,26 +22,20 @@ public class MemberService { @Transactional public Member register(final String email) { - findByEmail(new Email(email)) - .ifPresent(member -> { - throw new MemberException.ExistMemberException(); - }); + findByEmail(email).ifPresent(member -> { + throw new MemberException.ExistMemberException(); + }); final String nickname = email.split(EMAIL_SPILT_DELIMITER)[NICKNAME_INDEX]; final Member newMember = new Member(email, nickname); return memberRepository.save(newMember); } - public Optional findByEmail(final Email email) { - return memberRepository.findByEmail(email); + public Optional findByEmail(final String email) { + return memberRepository.findByEmail(new Email(email)); } - - public Member findByIdAndNickname(final Long id, final Nickname nickname) { + + public Member findByIdAndNicknameThrowIfNotExist(final Long id, final Nickname nickname) { return memberRepository.findByIdAndNickname(id, nickname) .orElseThrow(MemberException.MemberNotExistException::new); } - - public Member findById(final Long id) { - return memberRepository.findById(id) - .orElseThrow(MemberException.MemberNotExistException::new); - } } diff --git a/backend/src/main/java/shook/shook/member/ui/MemberExceptionHandler.java b/backend/src/main/java/shook/shook/member/ui/MemberExceptionHandler.java index 2f642f700..e7ea34bc8 100644 --- a/backend/src/main/java/shook/shook/member/ui/MemberExceptionHandler.java +++ b/backend/src/main/java/shook/shook/member/ui/MemberExceptionHandler.java @@ -17,5 +17,4 @@ public ResponseEntity handleMemberException(final MemberException return ResponseEntity.badRequest().body(ErrorResponse.from(e)); } - } diff --git a/backend/src/main/java/shook/shook/song/application/SongService.java b/backend/src/main/java/shook/shook/song/application/SongService.java index 6c29eeec3..8a637289a 100644 --- a/backend/src/main/java/shook/shook/song/application/SongService.java +++ b/backend/src/main/java/shook/shook/song/application/SongService.java @@ -57,9 +57,10 @@ private List sortByHighestLikeCountAndId( ) { return songWithLikeCounts.stream() .sorted( - Comparator.comparing(SongTotalLikeCountDto::getTotalLikeCount, - Comparator.reverseOrder()) - .thenComparing(dto -> dto.getSong().getId(), Comparator.reverseOrder()) + Comparator.comparing( + SongTotalLikeCountDto::getTotalLikeCount, + Comparator.reverseOrder() + ).thenComparing(dto -> dto.getSong().getId(), Comparator.reverseOrder()) ).toList(); } diff --git a/backend/src/main/java/shook/shook/song/application/killingpart/KillingPartCommentService.java b/backend/src/main/java/shook/shook/song/application/killingpart/KillingPartCommentService.java index 2b5fc9191..a4aa6cb27 100644 --- a/backend/src/main/java/shook/shook/song/application/killingpart/KillingPartCommentService.java +++ b/backend/src/main/java/shook/shook/song/application/killingpart/KillingPartCommentService.java @@ -6,7 +6,7 @@ import org.springframework.transaction.annotation.Transactional; import shook.shook.member.domain.Member; import shook.shook.member.domain.repository.MemberRepository; -import shook.shook.member.exception.MemberException.MemberNotExistException; +import shook.shook.member.exception.MemberException; import shook.shook.song.application.killingpart.dto.KillingPartCommentRegisterRequest; import shook.shook.song.application.killingpart.dto.KillingPartCommentResponse; import shook.shook.song.domain.killingpart.KillingPart; @@ -25,10 +25,13 @@ public class KillingPartCommentService { private final MemberRepository memberRepository; @Transactional - public void register(final long partId, final KillingPartCommentRegisterRequest request, - final Long memberId) { + public void register( + final Long partId, + final KillingPartCommentRegisterRequest request, + final Long memberId + ) { final Member member = memberRepository.findById(memberId) - .orElseThrow(MemberNotExistException::new); + .orElseThrow(MemberException.MemberNotExistException::new); final KillingPart killingPart = killingPartRepository.findById(partId) .orElseThrow(KillingPartException.PartNotExistException::new); diff --git a/backend/src/main/java/shook/shook/song/application/killingpart/KillingPartLikeService.java b/backend/src/main/java/shook/shook/song/application/killingpart/KillingPartLikeService.java index d611a0986..5ec2bd243 100644 --- a/backend/src/main/java/shook/shook/song/application/killingpart/KillingPartLikeService.java +++ b/backend/src/main/java/shook/shook/song/application/killingpart/KillingPartLikeService.java @@ -46,9 +46,9 @@ private void create(final KillingPart killingPart, final Member member) { return; } - final KillingPartLike likeOnKillingPart = likeRepository - .findByKillingPartAndMember(killingPart, member) - .orElseGet(() -> createNewLike(killingPart, member)); + final KillingPartLike likeOnKillingPart = + likeRepository.findByKillingPartAndMember(killingPart, member) + .orElseGet(() -> createNewLike(killingPart, member)); killingPart.like(likeOnKillingPart); } diff --git a/backend/src/main/java/shook/shook/voting_song/application/VotingSongPartService.java b/backend/src/main/java/shook/shook/voting_song/application/VotingSongPartService.java index aa624cb00..2ab69d7d1 100644 --- a/backend/src/main/java/shook/shook/voting_song/application/VotingSongPartService.java +++ b/backend/src/main/java/shook/shook/voting_song/application/VotingSongPartService.java @@ -11,7 +11,7 @@ import shook.shook.voting_song.domain.repository.VoteRepository; import shook.shook.voting_song.domain.repository.VotingSongPartRepository; import shook.shook.voting_song.domain.repository.VotingSongRepository; -import shook.shook.voting_song.exception.VotingSongException.VotingSongNotExistException; +import shook.shook.voting_song.exception.VotingSongException; import shook.shook.voting_song.exception.VotingSongPartException; @RequiredArgsConstructor @@ -24,12 +24,9 @@ public class VotingSongPartService { private final VoteRepository voteRepository; @Transactional - public void register( - final Long votingSongId, - final VotingSongPartRegisterRequest request - ) { + public void register(final Long votingSongId, final VotingSongPartRegisterRequest request) { final VotingSong votingSong = votingSongRepository.findById(votingSongId) - .orElseThrow(VotingSongNotExistException::new); + .orElseThrow(VotingSongException.VotingSongNotExistException::new); final int startSecond = request.getStartSecond(); final PartLength partLength = PartLength.findBySecond(request.getLength()); @@ -44,20 +41,14 @@ public void register( voteToExistPart(votingSong, votingSongPart); } - private void addPartAndVote( - final VotingSong votingSong, - final VotingSongPart votingSongPart - ) { + private void addPartAndVote(final VotingSong votingSong, final VotingSongPart votingSongPart) { votingSong.addPart(votingSongPart); votingSongPartRepository.save(votingSongPart); voteToPart(votingSongPart); } - private void voteToExistPart( - final VotingSong votingSong, - final VotingSongPart votingSongPart - ) { + private void voteToExistPart(final VotingSong votingSong, final VotingSongPart votingSongPart) { final VotingSongPart existPart = votingSong.getSameLengthPartStartAt(votingSongPart) .orElseThrow(VotingSongPartException.PartNotExistException::new); diff --git a/backend/src/main/java/shook/shook/voting_song/application/VotingSongService.java b/backend/src/main/java/shook/shook/voting_song/application/VotingSongService.java index df4b86bd7..3c1e24cea 100644 --- a/backend/src/main/java/shook/shook/voting_song/application/VotingSongService.java +++ b/backend/src/main/java/shook/shook/voting_song/application/VotingSongService.java @@ -1,10 +1,8 @@ package shook.shook.voting_song.application; -import java.util.Collections; +import java.util.Comparator; import java.util.List; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import shook.shook.voting_song.application.dto.VotingSongRegisterRequest; @@ -29,26 +27,25 @@ public void register(final VotingSongRegisterRequest request) { votingSongRepository.save(request.getVotingSong()); } - public VotingSongSwipeResponse findByIdForSwipe(final Long id) { + public VotingSongSwipeResponse findAllForSwipeById(final Long id) { final VotingSong votingSong = votingSongRepository.findById(id) .orElseThrow(VotingSongException.VotingSongNotExistException::new); - final List beforeVotingSongs = votingSongRepository.findByIdLessThanOrderByIdDesc( - id, PageRequest.of(0, BEFORE_SONG_COUNT) - ); - Collections.reverse(beforeVotingSongs); + final long startId = Math.max(0, id - BEFORE_SONG_COUNT); + final long endId = id + AFTER_SONG_COUNT; - final List afterVotingSongs = votingSongRepository.findByIdGreaterThanOrderByIdAsc( - id, PageRequest.of(0, AFTER_SONG_COUNT) - ); + final List songsForSwipe = + votingSongRepository.findByIdGreaterThanEqualAndIdLessThanEqual(startId, endId) + .stream() + .sorted(Comparator.comparing(VotingSong::getId)) + .toList(); - return VotingSongSwipeResponse.of(votingSong, beforeVotingSongs, afterVotingSongs); + return VotingSongSwipeResponse.of(songsForSwipe, votingSong); } public List findAll() { - final Sort ascendingSort = Sort.by("id").ascending(); - - return votingSongRepository.findAll(ascendingSort).stream() + return votingSongRepository.findAll().stream() + .sorted(Comparator.comparing(VotingSong::getId)) .map(VotingSongResponse::from) .toList(); } diff --git a/backend/src/main/java/shook/shook/voting_song/application/dto/VotingSongSwipeResponse.java b/backend/src/main/java/shook/shook/voting_song/application/dto/VotingSongSwipeResponse.java index a6fd87185..527f79373 100644 --- a/backend/src/main/java/shook/shook/voting_song/application/dto/VotingSongSwipeResponse.java +++ b/backend/src/main/java/shook/shook/voting_song/application/dto/VotingSongSwipeResponse.java @@ -17,15 +17,18 @@ public class VotingSongSwipeResponse { private List nextSongs; public static VotingSongSwipeResponse of( - final VotingSong currentSong, - final List prevSongs, - final List nextSongs + final List songs, + final VotingSong currentSong ) { + final int votingSongIndex = songs.indexOf(currentSong); + final List beforeSongs = songs.subList(0, votingSongIndex); + final List afterSongs = songs.subList(votingSongIndex + 1, songs.size()); + final VotingSongResponse currentResponse = VotingSongResponse.from(currentSong); - final List prevResponses = prevSongs.stream() + final List prevResponses = beforeSongs.stream() .map(VotingSongResponse::from) .toList(); - final List nextResponses = nextSongs.stream() + final List nextResponses = afterSongs.stream() .map(VotingSongResponse::from) .toList(); diff --git a/backend/src/main/java/shook/shook/voting_song/domain/repository/VotingSongRepository.java b/backend/src/main/java/shook/shook/voting_song/domain/repository/VotingSongRepository.java index e1439ffe9..0ec518b00 100644 --- a/backend/src/main/java/shook/shook/voting_song/domain/repository/VotingSongRepository.java +++ b/backend/src/main/java/shook/shook/voting_song/domain/repository/VotingSongRepository.java @@ -2,7 +2,6 @@ import java.util.List; import java.util.Optional; -import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import shook.shook.song.domain.SongTitle; @@ -13,7 +12,5 @@ public interface VotingSongRepository extends JpaRepository { Optional findByTitle(final SongTitle title); - List findByIdLessThanOrderByIdDesc(final Long id, final Pageable page); - - List findByIdGreaterThanOrderByIdAsc(final Long id, final Pageable page); + List findByIdGreaterThanEqualAndIdLessThanEqual(final Long start, final Long end); } diff --git a/backend/src/main/java/shook/shook/voting_song/ui/VotingSongController.java b/backend/src/main/java/shook/shook/voting_song/ui/VotingSongController.java index 97930107d..20eb8ff54 100644 --- a/backend/src/main/java/shook/shook/voting_song/ui/VotingSongController.java +++ b/backend/src/main/java/shook/shook/voting_song/ui/VotingSongController.java @@ -26,11 +26,11 @@ public ResponseEntity> findAll() { } @GetMapping("/{voting_song_id}") - public ResponseEntity findByIdForSwipe( + public ResponseEntity findAllForSwipeById( @PathVariable("voting_song_id") final Long votingSongId ) { final VotingSongSwipeResponse swipeResponse = - votingSongService.findByIdForSwipe(votingSongId); + votingSongService.findAllForSwipeById(votingSongId); return ResponseEntity.ok(swipeResponse); } diff --git a/backend/src/main/resources/shook-security b/backend/src/main/resources/shook-security index e2ecf8351..1ff04c570 160000 --- a/backend/src/main/resources/shook-security +++ b/backend/src/main/resources/shook-security @@ -1 +1 @@ -Subproject commit e2ecf83511254975066a9739387b87ef73e68506 +Subproject commit 1ff04c570bbfa1f7a10e2ccca70aa3e676719174 diff --git a/backend/src/test/java/shook/shook/auth/application/AuthServiceTest.java b/backend/src/test/java/shook/shook/auth/application/AuthServiceTest.java index 35b33b2a0..0f2d105d8 100644 --- a/backend/src/test/java/shook/shook/auth/application/AuthServiceTest.java +++ b/backend/src/test/java/shook/shook/auth/application/AuthServiceTest.java @@ -14,8 +14,8 @@ import org.springframework.boot.test.mock.mockito.MockBean; import shook.shook.auth.application.dto.GoogleAccessTokenResponse; import shook.shook.auth.application.dto.GoogleMemberInfoResponse; -import shook.shook.auth.application.dto.TokenInfo; -import shook.shook.auth.application.dto.TokenReissueResponse; +import shook.shook.auth.application.dto.ReissueAccessTokenResponse; +import shook.shook.auth.application.dto.TokenPair; import shook.shook.auth.exception.TokenException; import shook.shook.member.application.MemberService; import shook.shook.member.domain.Member; @@ -69,7 +69,7 @@ void success_login() { .thenReturn(memberInfoResponse); //when - final TokenInfo result = authService.login("accessCode"); + final TokenPair result = authService.login("accessCode"); //then assertThat(result.getAccessToken()).isNotNull(); @@ -85,7 +85,8 @@ void success_reissue() { savedMember.getNickname()); //when - final TokenReissueResponse result = authService.reissueToken(refreshToken); + final ReissueAccessTokenResponse result = authService.reissueAccessTokenByRefreshToken( + refreshToken); //then final String accessToken = tokenProvider.createAccessToken( @@ -110,7 +111,7 @@ void fail_reissue_invalid_refreshToken() { //when //then - assertThatThrownBy(() -> authService.reissueToken(refreshToken)) + assertThatThrownBy(() -> authService.reissueAccessTokenByRefreshToken(refreshToken)) .isInstanceOf(TokenException.NotIssuedTokenException.class); } @@ -129,7 +130,7 @@ void fail_reissue_expired_refreshToken() { //when //then - assertThatThrownBy(() -> authService.reissueToken(refreshToken)) + assertThatThrownBy(() -> authService.reissueAccessTokenByRefreshToken(refreshToken)) .isInstanceOf(TokenException.ExpiredTokenException.class); } } diff --git a/backend/src/test/java/shook/shook/auth/application/GoogleInfoProviderTest.java b/backend/src/test/java/shook/shook/auth/application/GoogleInfoProviderTest.java index 9a7ca1071..aa4288ec4 100644 --- a/backend/src/test/java/shook/shook/auth/application/GoogleInfoProviderTest.java +++ b/backend/src/test/java/shook/shook/auth/application/GoogleInfoProviderTest.java @@ -4,7 +4,6 @@ import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; import static org.springframework.test.web.client.response.MockRestResponseCreators.withBadRequest; import static org.springframework.test.web.client.response.MockRestResponseCreators.withServerError; -import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -12,7 +11,6 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.autoconfigure.web.client.AutoConfigureWebClient; import org.springframework.boot.test.autoconfigure.web.client.RestClientTest; -import org.springframework.http.MediaType; import org.springframework.test.web.client.MockRestServiceServer; import shook.shook.auth.exception.OAuthException; @@ -60,26 +58,6 @@ void fail_request_memberInfo_InvalidAccessToken() { .isInstanceOf(OAuthException.InvalidAccessTokenException.class); } - @DisplayName("이메일이 유효하지 않으면 예외를 던진다.") - @Test - void fail_request_InvalidEmail() { - //given - final String response = """ - { - "email": "shook@wooteco.com", - "verified_email": "false" - } - """; - mockServer - .expect(requestTo(MEMBER_INFO_URL)) - .andRespond(withSuccess(response, MediaType.APPLICATION_JSON)); - - //when - //then - assertThatThrownBy(() -> googleInfoProvider.getMemberInfo("code")) - .isInstanceOf(OAuthException.InvalidEmailException.class); - } - @DisplayName("accessToken을 요청할 때 구글 서버에러가 발생하면 예외를 던진다.") @Test void fail_access_token_request_google_server_error() { diff --git a/backend/src/test/java/shook/shook/auth/ui/TokenControllerTest.java b/backend/src/test/java/shook/shook/auth/ui/AccessTokenReissueControllerTest.java similarity index 89% rename from backend/src/test/java/shook/shook/auth/ui/TokenControllerTest.java rename to backend/src/test/java/shook/shook/auth/ui/AccessTokenReissueControllerTest.java index 1339242c3..1201cd21f 100644 --- a/backend/src/test/java/shook/shook/auth/ui/TokenControllerTest.java +++ b/backend/src/test/java/shook/shook/auth/ui/AccessTokenReissueControllerTest.java @@ -13,12 +13,12 @@ import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.http.HttpStatus; import shook.shook.auth.application.TokenProvider; -import shook.shook.auth.application.dto.TokenReissueResponse; +import shook.shook.auth.application.dto.ReissueAccessTokenResponse; import shook.shook.member.domain.Member; import shook.shook.member.domain.repository.MemberRepository; @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) -class TokenControllerTest { +class AccessTokenReissueControllerTest { @LocalServerPort private int port; @@ -51,11 +51,11 @@ void success_reissue_accessToken() { savedMember.getNickname()); //when - final TokenReissueResponse response = RestAssured.given().log().all() + final ReissueAccessTokenResponse response = RestAssured.given().log().all() .cookie("refreshToken", refreshToken) .when().log().all().get("/reissue") .then().statusCode(HttpStatus.OK.value()) - .extract().body().as(TokenReissueResponse.class); + .extract().body().as(ReissueAccessTokenResponse.class); //then final String accessToken = tokenProvider.createAccessToken( diff --git a/backend/src/test/java/shook/shook/auth/ui/AuthControllerTest.java b/backend/src/test/java/shook/shook/auth/ui/AuthControllerTest.java index 147f9134d..a1228e5e5 100644 --- a/backend/src/test/java/shook/shook/auth/ui/AuthControllerTest.java +++ b/backend/src/test/java/shook/shook/auth/ui/AuthControllerTest.java @@ -18,7 +18,7 @@ import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.http.HttpStatus; import shook.shook.auth.application.AuthService; -import shook.shook.auth.application.dto.TokenInfo; +import shook.shook.auth.application.dto.TokenPair; import shook.shook.auth.ui.dto.LoginResponse; @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @@ -42,11 +42,9 @@ void setUp() { @Test void login_success() { //given - final TokenInfo tokenInfo = new TokenInfo( - "asdfafdv2", - "asdfsg5"); + final TokenPair tokenPair = new TokenPair("asdfafdv2", "asdfsg5"); - when(authService.login(any(String.class))).thenReturn(tokenInfo); + when(authService.login(any(String.class))).thenReturn(tokenPair); //when final ExtractableResponse response = RestAssured.given().log().all() @@ -55,7 +53,7 @@ void login_success() { .extract(); //then - final LoginResponse expectResponseBody = new LoginResponse(tokenInfo.getAccessToken()); + final LoginResponse expectResponseBody = new LoginResponse(tokenPair.getAccessToken()); final LoginResponse responseBody = response.body().as(LoginResponse.class); final String cookie = response.header("Set-Cookie"); diff --git a/backend/src/test/java/shook/shook/member/application/MemberServiceTest.java b/backend/src/test/java/shook/shook/member/application/MemberServiceTest.java index c7aa63a04..0fb9f6a8b 100644 --- a/backend/src/test/java/shook/shook/member/application/MemberServiceTest.java +++ b/backend/src/test/java/shook/shook/member/application/MemberServiceTest.java @@ -7,12 +7,10 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import shook.shook.member.domain.Email; import shook.shook.member.domain.Member; import shook.shook.member.domain.Nickname; import shook.shook.member.domain.repository.MemberRepository; import shook.shook.member.exception.MemberException; -import shook.shook.member.exception.MemberException.MemberNotExistException; import shook.shook.support.UsingJpaTest; class MemberServiceTest extends UsingJpaTest { @@ -63,8 +61,7 @@ void register_fail_alreadyExistMember() { void findByEmail() { //given //when - final Member result = memberService.findByEmail( - new Email(savedMember.getEmail())).get(); + final Member result = memberService.findByEmail(savedMember.getEmail()).get(); //then assertThat(result.getId()).isEqualTo(savedMember.getId()); @@ -72,35 +69,12 @@ void findByEmail() { assertThat(result.getNickname()).isEqualTo(savedMember.getNickname()); } - @DisplayName("회원을 id로 조회한다.") - @Test - void success_findById() { - //given - //when - final Member result = memberService.findById(savedMember.getId()); - - //then - assertThat(result.getId()).isEqualTo(savedMember.getId()); - assertThat(result.getEmail()).isEqualTo(savedMember.getEmail()); - assertThat(result.getNickname()).isEqualTo(savedMember.getNickname()); - } - - @DisplayName("회원을 id로 조회할 때 존재하지 않으면 예외를 던진다.") - @Test - void fail_findById() { - //given - //when - //then - assertThatThrownBy(() -> memberService.findById(Long.MAX_VALUE)) - .isInstanceOf(MemberNotExistException.class); - } - @DisplayName("회원을 id와 nickname으로 조회한다.") @Test void success_findByIdAndNickname() { //given //when - final Member result = memberService.findByIdAndNickname( + final Member result = memberService.findByIdAndNicknameThrowIfNotExist( savedMember.getId(), new Nickname(savedMember.getNickname())); @@ -115,7 +89,7 @@ void fail_findByIdAndNickname_wrong_nickname() { //when //then assertThatThrownBy( - () -> memberService.findByIdAndNickname(savedMember.getId(), + () -> memberService.findByIdAndNicknameThrowIfNotExist(savedMember.getId(), new Nickname(savedMember.getNickname() + "none"))) .isInstanceOf(MemberException.MemberNotExistException.class); } @@ -127,7 +101,7 @@ void fail_findByIdAndNickname_wrong_memberId() { //when //then assertThatThrownBy( - () -> memberService.findByIdAndNickname(savedMember.getId() + 1, + () -> memberService.findByIdAndNicknameThrowIfNotExist(savedMember.getId() + 1, new Nickname(savedMember.getNickname()))) .isInstanceOf(MemberException.MemberNotExistException.class); } @@ -139,7 +113,7 @@ void fail_findByIdAndNickname_wrong_memberId_and_nickname() { //when //then assertThatThrownBy( - () -> memberService.findByIdAndNickname(savedMember.getId() + 1, + () -> memberService.findByIdAndNicknameThrowIfNotExist(savedMember.getId() + 1, new Nickname(savedMember.getNickname() + "none"))) .isInstanceOf(MemberException.MemberNotExistException.class); } diff --git a/backend/src/test/java/shook/shook/voting_song/application/VotingSongServiceTest.java b/backend/src/test/java/shook/shook/voting_song/application/VotingSongServiceTest.java index 2585a1aad..10609cf93 100644 --- a/backend/src/test/java/shook/shook/voting_song/application/VotingSongServiceTest.java +++ b/backend/src/test/java/shook/shook/voting_song/application/VotingSongServiceTest.java @@ -135,7 +135,7 @@ void success() { // when final VotingSongSwipeResponse swipeResponse = - votingSongService.findByIdForSwipe(standardSong.getId()); + votingSongService.findAllForSwipeById(standardSong.getId()); // then final VotingSongResponse expectedCurrent = VotingSongResponse.from(standardSong); @@ -167,7 +167,7 @@ void notExistVotingSong() { // when // then - assertThatThrownBy(() -> votingSongService.findByIdForSwipe(notExistId)) + assertThatThrownBy(() -> votingSongService.findAllForSwipeById(notExistId)) .isInstanceOf(VotingSongException.VotingSongNotExistException.class); } } diff --git a/backend/src/test/java/shook/shook/voting_song/domain/repository/VotingSongRepositoryTest.java b/backend/src/test/java/shook/shook/voting_song/domain/repository/VotingSongRepositoryTest.java index 3db9ac724..b057b7046 100644 --- a/backend/src/test/java/shook/shook/voting_song/domain/repository/VotingSongRepositoryTest.java +++ b/backend/src/test/java/shook/shook/voting_song/domain/repository/VotingSongRepositoryTest.java @@ -7,7 +7,6 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.PageRequest; import shook.shook.support.UsingJpaTest; import shook.shook.voting_song.domain.VotingSong; @@ -20,9 +19,9 @@ class VotingSongRepositoryTest extends UsingJpaTest { @Nested class findSongsLessThanSongId { - @DisplayName("찾으려는 파트 수집 중인 노래 아이디보다 작은 아이디를 갖는 노래 4개를 조회한다. (이전 노래가 4개보다 많을 때)") + @DisplayName("앞뒤로 노래들이 충분히 존재할 때") @Test - void findFourSongsBeforeSongId() { + void enough() { // given final VotingSong firstSong = votingSongRepository.save( new VotingSong("제목1", "비디오ID는 11글자", "이미지URL", "가수", 30) @@ -42,24 +41,48 @@ void findFourSongsBeforeSongId() { final VotingSong standardSong = votingSongRepository.save( new VotingSong("제목5", "비디오ID는 11글자", "이미지URL", "가수", 30) ); - - // when - final List beforeVotingSongs = votingSongRepository.findByIdLessThanOrderByIdDesc( - standardSong.getId(), - PageRequest.of(0, 4) + final VotingSong seventhSong = votingSongRepository.save( + new VotingSong("제목7", "비디오ID는 11글자", "이미지URL", "가수", 30) + ); + final VotingSong eighthSong = votingSongRepository.save( + new VotingSong("제목8", "비디오ID는 11글자", "이미지URL", "가수", 30) + ); + final VotingSong ninthSong = votingSongRepository.save( + new VotingSong("제목9", "비디오ID는 11글자", "이미지URL", "가수", 30) + ); + final VotingSong tenthSong = votingSongRepository.save( + new VotingSong("제목10", "비디오ID는 11글자", "이미지URL", "가수", 30) ); + final VotingSong eleventhSong = votingSongRepository.save( + new VotingSong("제목11", "비디오ID는 11글자", "이미지URL", "가수", 30) + ); + // when + final List beforeVotingSongs = + votingSongRepository.findByIdGreaterThanEqualAndIdLessThanEqual( + standardSong.getId() - 4, standardSong.getId() + 4 + ); // then final List expected = - List.of(fifthSong, fourthSong, thirdSong, secondSong); + List.of( + secondSong, + thirdSong, + fourthSong, + fifthSong, + standardSong, + seventhSong, + eighthSong, + ninthSong, + tenthSong + ); assertThat(beforeVotingSongs).usingRecursiveComparison() .isEqualTo(expected); } - @DisplayName("찾으려는 파트 수집 중인 노래 아이디보다 작은 아이디를 갖는 노래 4개를 조회한다. (이전 노래가 1개 이상 ~ 4개일 때)") + @DisplayName("이전 노래가 4개보다 적을 때") @Test - void findSongsSizeLessThanFourBeforeSongId() { + void prevSongNotEnough() { // given final VotingSong firstSong = votingSongRepository.save( new VotingSong("제목1", "비디오ID는 11글자", "이미지URL", "가수", 30) @@ -70,48 +93,51 @@ void findSongsSizeLessThanFourBeforeSongId() { final VotingSong standardSong = votingSongRepository.save( new VotingSong("제목3", "비디오ID는 11글자", "이미지URL", "가수", 30) ); - - // when - final List beforeVotingSongs = votingSongRepository.findByIdLessThanOrderByIdDesc( - standardSong.getId(), - PageRequest.of(0, 4) + final VotingSong fourthSong = votingSongRepository.save( + new VotingSong("제목4", "비디오ID는 11글자", "이미지URL", "가수", 30) + ); + final VotingSong fifthSong = votingSongRepository.save( + new VotingSong("제목5", "비디오ID는 11글자", "이미지URL", "가수", 30) + ); + final VotingSong sixthSong = votingSongRepository.save( + new VotingSong("제목6", "비디오ID는 11글자", "이미지URL", "가수", 30) + ); + final VotingSong seventhSong = votingSongRepository.save( + new VotingSong("제목7", "비디오ID는 11글자", "이미지URL", "가수", 30) + ); + final VotingSong eighthSong = votingSongRepository.save( + new VotingSong("제목8", "비디오ID는 11글자", "이미지URL", "가수", 30) ); + final VotingSong ninthSong = votingSongRepository.save( + new VotingSong("제목9", "비디오ID는 11글자", "이미지URL", "가수", 30) + ); + // when + final List beforeVotingSongs = + votingSongRepository.findByIdGreaterThanEqualAndIdLessThanEqual( + standardSong.getId() - 4, standardSong.getId() + 4 + ); // then - final List expected = List.of(secondSong, firstSong); + final List expected = + List.of( + firstSong, + secondSong, + standardSong, + fourthSong, + fifthSong, + sixthSong, + seventhSong + ); assertThat(beforeVotingSongs).usingRecursiveComparison() .isEqualTo(expected); } - @DisplayName("찾으려는 파트 수집 중인 노래 아이디보다 작은 아이디를 갖는 노래 4개를 조회한다. (이전 노래가 없을 때)") + @DisplayName("다음 노래가 4개보다 적을 때") @Test - void findEmptySongsBeforeSongId() { + void nextSongNotEnough() { // given - final VotingSong standardSong = votingSongRepository.save( - new VotingSong("제목1", "비디오ID는 11글자", "이미지URL", "가수", 30) - ); - - // when - final List beforeVotingSongs = votingSongRepository.findByIdLessThanOrderByIdDesc( - standardSong.getId(), - PageRequest.of(0, 4) - ); - - // then - assertThat(beforeVotingSongs).isEmpty(); - } - } - - @DisplayName("특정 파트 수집 중인 노래 id 를 기준으로 id가 큰 노래를 조회한다.") - @Nested - class findSongsGreaterThanSongId { - - @DisplayName("찾으려는 파트 수집 중인 노래 아이디보다 큰 아이디를 갖는 노래 4개를 조회한다. (이후 노래가 4개보다 많을 때)") - @Test - void findFourSongsBeforeSongId() { - // given - final VotingSong standardSong = votingSongRepository.save( + final VotingSong firstSong = votingSongRepository.save( new VotingSong("제목1", "비디오ID는 11글자", "이미지URL", "가수", 30) ); final VotingSong secondSong = votingSongRepository.save( @@ -127,28 +153,44 @@ void findFourSongsBeforeSongId() { new VotingSong("제목5", "비디오ID는 11글자", "이미지URL", "가수", 30) ); final VotingSong sixthSong = votingSongRepository.save( - new VotingSong("제목5", "비디오ID는 11글자", "이미지URL", "가수", 30) + new VotingSong("제목6", "비디오ID는 11글자", "이미지URL", "가수", 30) ); - - // when - final List beforeVotingSongs = votingSongRepository.findByIdGreaterThanOrderByIdAsc( - standardSong.getId(), - PageRequest.of(0, 4) + final VotingSong standardSong = votingSongRepository.save( + new VotingSong("제목7", "비디오ID는 11글자", "이미지URL", "가수", 30) + ); + final VotingSong eighthSong = votingSongRepository.save( + new VotingSong("제목8", "비디오ID는 11글자", "이미지URL", "가수", 30) + ); + final VotingSong ninthSong = votingSongRepository.save( + new VotingSong("제목9", "비디오ID는 11글자", "이미지URL", "가수", 30) ); + // when + final List beforeVotingSongs = + votingSongRepository.findByIdGreaterThanEqualAndIdLessThanEqual( + standardSong.getId() - 4, standardSong.getId() + 4 + ); // then final List expected = - List.of(secondSong, thirdSong, fourthSong, fifthSong); + List.of( + thirdSong, + fourthSong, + fifthSong, + sixthSong, + standardSong, + eighthSong, + ninthSong + ); assertThat(beforeVotingSongs).usingRecursiveComparison() .isEqualTo(expected); } - @DisplayName("찾으려는 파트 수집 중인 노래 아이디보다 큰 아이디를 갖는 노래 4개를 조회한다. (이후 노래가 1개 이상 ~ 4개일 때)") + @DisplayName("이전 노래, 다음 노래 모두 4개보다 적을 때") @Test - void findSongsSizeLessThanFourAfterSongId() { + void bothNotEnough() { // given - final VotingSong standardSong = votingSongRepository.save( + final VotingSong firstSong = votingSongRepository.save( new VotingSong("제목1", "비디오ID는 11글자", "이미지URL", "가수", 30) ); final VotingSong secondSong = votingSongRepository.save( @@ -157,36 +199,35 @@ void findSongsSizeLessThanFourAfterSongId() { final VotingSong thirdSong = votingSongRepository.save( new VotingSong("제목3", "비디오ID는 11글자", "이미지URL", "가수", 30) ); + final VotingSong standardSong = votingSongRepository.save( + new VotingSong("제목4", "비디오ID는 11글자", "이미지URL", "가수", 30) + ); + final VotingSong fifthSong = votingSongRepository.save( + new VotingSong("제목5", "비디오ID는 11글자", "이미지URL", "가수", 30) + ); + final VotingSong sixthSong = votingSongRepository.save( + new VotingSong("제목6", "비디오ID는 11글자", "이미지URL", "가수", 30) + ); // when - final List beforeVotingSongs = votingSongRepository.findByIdGreaterThanOrderByIdAsc( - standardSong.getId(), - PageRequest.of(0, 4) - ); + final List beforeVotingSongs = + votingSongRepository.findByIdGreaterThanEqualAndIdLessThanEqual( + standardSong.getId() - 4, standardSong.getId() + 4 + ); // then - final List expected = List.of(secondSong, thirdSong); + final List expected = + List.of( + firstSong, + secondSong, + thirdSong, + standardSong, + fifthSong, + sixthSong + ); assertThat(beforeVotingSongs).usingRecursiveComparison() .isEqualTo(expected); } - - @DisplayName("찾으려는 파트 수집 중인 노래 아이디보다 큰 아이디를 갖는 노래 4개를 조회한다. (이후 노래가 없을 때)") - @Test - void findEmptySongsAfterSongId() { - // given - final VotingSong standardSong = votingSongRepository.save( - new VotingSong("제목1", "비디오ID는 11글자", "이미지URL", "가수", 30) - ); - - // when - final List afterVotingSongs = votingSongRepository.findByIdGreaterThanOrderByIdAsc( - standardSong.getId(), - PageRequest.of(0, 4) - ); - - // then - assertThat(afterVotingSongs).isEmpty(); - } } } From 1ef54b8c5966c6e3afccad194390ecf11b027578 Mon Sep 17 00:00:00 2001 From: SeokHwan An <70303795+seokhwan-an@users.noreply.github.com> Date: Thu, 17 Aug 2023 22:09:55 +0900 Subject: [PATCH 02/10] =?UTF-8?q?TEST/#304=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=EA=B3=BC=20=EA=B4=80=EB=A0=A8=EB=90=9C=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20(#306)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: 로그인 관련 테스트 수정 및 추가 * refactor: submodule sync 맞추기 --- backend/src/main/resources/shook-security | 2 +- .../auth/application/AuthServiceTest.java | 10 ++++++++-- .../auth/application/TokenProviderTest.java | 20 +++++++++++++++++-- .../ui/AccessTokenReissueControllerTest.java | 11 +++++++++- .../shook/shook/auth/ui/AuthContextTest.java | 2 +- .../shook/auth/ui/CookieProviderTest.java | 2 +- .../auth/ui/interceptor/PathMethodTest.java | 3 +-- 7 files changed, 40 insertions(+), 10 deletions(-) diff --git a/backend/src/main/resources/shook-security b/backend/src/main/resources/shook-security index 1ff04c570..e2ecf8351 160000 --- a/backend/src/main/resources/shook-security +++ b/backend/src/main/resources/shook-security @@ -1 +1 @@ -Subproject commit 1ff04c570bbfa1f7a10e2ccca70aa3e676719174 +Subproject commit e2ecf83511254975066a9739387b87ef73e68506 diff --git a/backend/src/test/java/shook/shook/auth/application/AuthServiceTest.java b/backend/src/test/java/shook/shook/auth/application/AuthServiceTest.java index 0f2d105d8..4c112efec 100644 --- a/backend/src/test/java/shook/shook/auth/application/AuthServiceTest.java +++ b/backend/src/test/java/shook/shook/auth/application/AuthServiceTest.java @@ -72,8 +72,14 @@ void success_login() { final TokenPair result = authService.login("accessCode"); //then - assertThat(result.getAccessToken()).isNotNull(); - assertThat(result.getRefreshToken()).isNotNull(); + + final String accessToken = tokenProvider.createAccessToken(savedMember.getId(), + savedMember.getNickname()); + final String refreshToken = tokenProvider.createRefreshToken(savedMember.getId(), + savedMember.getNickname()); + + assertThat(result.getAccessToken()).isEqualTo(accessToken); + assertThat(result.getRefreshToken()).isEqualTo(refreshToken); } @DisplayName("올바른 refresh 토큰이 들어오면 access 토큰을 재발급해준다.") diff --git a/backend/src/test/java/shook/shook/auth/application/TokenProviderTest.java b/backend/src/test/java/shook/shook/auth/application/TokenProviderTest.java index 03211e2ae..e8c1a5965 100644 --- a/backend/src/test/java/shook/shook/auth/application/TokenProviderTest.java +++ b/backend/src/test/java/shook/shook/auth/application/TokenProviderTest.java @@ -53,10 +53,10 @@ void createRefreshToken() { .isEqualTo(REFRESH_TOKEN_VALID_TIME); } - @DisplayName("잘못 만들어진 token을 parsing하면 에러를 발생한다.") + @DisplayName("망가진 형식의 token을 parsing하면 에러를 발생한다.") @Test void parsing_fail_malformed_token() { - // given + //given final String inValidToken = "asdfsev.asefsbd.23dfvs"; //when @@ -65,6 +65,22 @@ void parsing_fail_malformed_token() { .isInstanceOf(TokenException.NotIssuedTokenException.class); } + @DisplayName("다른 출처의 token을 parsing하면 예외를 발생한다.") + @Test + void parsing_fail_different_secretkey() { + //given + final TokenProvider differentTokenProvider = new TokenProvider( + 10000, + 10000, + "asdfksnlxcnvporfsdg8xjcvlk323d"); + final String inValidToken = differentTokenProvider.createAccessToken(1, "shook"); + + //when + //then + assertThatThrownBy(() -> tokenProvider.parseClaims(inValidToken)) + .isInstanceOf(TokenException.NotIssuedTokenException.class); + } + @DisplayName("기한이 만료된 token을 parsing하면 에러를 발생한다.") @Test void parsing_fail_expired_token() { diff --git a/backend/src/test/java/shook/shook/auth/ui/AccessTokenReissueControllerTest.java b/backend/src/test/java/shook/shook/auth/ui/AccessTokenReissueControllerTest.java index 1201cd21f..5b136a4cd 100644 --- a/backend/src/test/java/shook/shook/auth/ui/AccessTokenReissueControllerTest.java +++ b/backend/src/test/java/shook/shook/auth/ui/AccessTokenReissueControllerTest.java @@ -65,5 +65,14 @@ void success_reissue_accessToken() { assertThat(response.getAccessToken()).isEqualTo(accessToken); } - // TODO: 2023/08/11 예외 상태코드가 정해지면 쿠키가 없는 경우 테스트 코드 추가하기 + @DisplayName("refreshToken이 없이 accessToken을 재발급 받으려면 예외를 던잔디.") + @Test + void fail_reissue_accessToken() { + //given + //when + //then + RestAssured.given().log().all() + .when().log().all().get("/reissue") + .then().statusCode(HttpStatus.UNAUTHORIZED.value()); + } } diff --git a/backend/src/test/java/shook/shook/auth/ui/AuthContextTest.java b/backend/src/test/java/shook/shook/auth/ui/AuthContextTest.java index 9a2b40ec4..a7e4257d1 100644 --- a/backend/src/test/java/shook/shook/auth/ui/AuthContextTest.java +++ b/backend/src/test/java/shook/shook/auth/ui/AuthContextTest.java @@ -38,7 +38,7 @@ void success_authContext_memberId_get() { assertThat(result).isEqualTo(1L); } - @DisplayName("authContext의 memberStatus가 Member이면 false를 반환한다.") + @DisplayName("authContext의 memberStatus가 Member이면 비회원검사에서 false를 반환한다.") @Test void return_false_memberStatus_is_member() { //given diff --git a/backend/src/test/java/shook/shook/auth/ui/CookieProviderTest.java b/backend/src/test/java/shook/shook/auth/ui/CookieProviderTest.java index b553df142..782af6adf 100644 --- a/backend/src/test/java/shook/shook/auth/ui/CookieProviderTest.java +++ b/backend/src/test/java/shook/shook/auth/ui/CookieProviderTest.java @@ -10,7 +10,7 @@ class CookieProviderTest { private final CookieProvider cookieProvider = new CookieProvider(640000); - @DisplayName("refreshToken이 주어지면 올바른 Cookie를 생성한다.") + @DisplayName("refreshToken이 주어지면 Cookie를 생성한다.") @Test void success_create_cookie() { //given diff --git a/backend/src/test/java/shook/shook/auth/ui/interceptor/PathMethodTest.java b/backend/src/test/java/shook/shook/auth/ui/interceptor/PathMethodTest.java index 775203dc4..b60c216c2 100644 --- a/backend/src/test/java/shook/shook/auth/ui/interceptor/PathMethodTest.java +++ b/backend/src/test/java/shook/shook/auth/ui/interceptor/PathMethodTest.java @@ -9,7 +9,6 @@ class PathMethodTest { - @DisplayName("요청 메소드 이름이 인자로 들어오는 경우 요소의 이름과 비교하여 대소문자 구분없이 같은 true를 반환한다.") @ValueSource(strings = {"post", "POST", "Post"}) @ParameterizedTest @@ -24,7 +23,7 @@ void return_ture_match_name(String requestMethod) { assertThat(result).isTrue(); } - @DisplayName("요청 메소드 이름이 인자로 들어오는 경우 요소의 이름과 비교하여 대소문자 구분없이 같은 true를 반환한다.") + @DisplayName("요청 메소드 이름이 인자로 들어오는 경우 요소의 이름과 비교하여 대소문자 구분없이 같은 false를 반환한다.") @ValueSource(strings = {"post", "POST", "Post"}) @ParameterizedTest void return_false_nonMatch_name(String requestMethod) { From fa86b1ed9508184e3a6746fb6dcf43ac24ffe71d Mon Sep 17 00:00:00 2001 From: SeokHwan An <70303795+seokhwan-an@users.noreply.github.com> Date: Thu, 17 Aug 2023 22:33:28 +0900 Subject: [PATCH 03/10] =?UTF-8?q?REFACTOR/#305=20=EC=BF=A0=ED=82=A4=20path?= =?UTF-8?q?=20=EC=84=A4=EC=A0=95=20=EC=88=98=EC=A0=95=20(#307)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 쿠키 path 설정 수정 * refactor: 테스트 실패 수정 --- backend/src/main/java/shook/shook/auth/ui/CookieProvider.java | 2 +- .../src/test/java/shook/shook/auth/ui/AuthControllerTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/shook/shook/auth/ui/CookieProvider.java b/backend/src/main/java/shook/shook/auth/ui/CookieProvider.java index 1b0609713..77ee9a196 100644 --- a/backend/src/main/java/shook/shook/auth/ui/CookieProvider.java +++ b/backend/src/main/java/shook/shook/auth/ui/CookieProvider.java @@ -16,7 +16,7 @@ public CookieProvider(@Value("${cookie.valid-time}") final int cookieAge) { public Cookie createRefreshTokenCookie(final String refreshToken) { final Cookie cookie = new Cookie("refreshToken", refreshToken); cookie.setMaxAge(cookieAge); - cookie.setPath("/reissue"); + cookie.setPath("/api/reissue"); cookie.setHttpOnly(true); cookie.setSecure(true); return cookie; diff --git a/backend/src/test/java/shook/shook/auth/ui/AuthControllerTest.java b/backend/src/test/java/shook/shook/auth/ui/AuthControllerTest.java index a1228e5e5..a318174d5 100644 --- a/backend/src/test/java/shook/shook/auth/ui/AuthControllerTest.java +++ b/backend/src/test/java/shook/shook/auth/ui/AuthControllerTest.java @@ -62,7 +62,7 @@ void login_success() { expectResponseBody.getAccessToken()), () -> assertThat(cookie.contains("refreshToken=asdfsg5")).isTrue(), () -> assertThat(cookie.contains("Max-Age=" + cookieAge)).isTrue(), - () -> assertThat(cookie.contains("Path=/reissue")).isTrue(), + () -> assertThat(cookie.contains("Path=/api/reissue")).isTrue(), () -> assertThat(cookie.contains("HttpOnly")).isTrue() ); } From ebb211afb8117819d7eb6bdc5e477870bc413e75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A4=EC=A0=95=EB=AF=BC?= Date: Thu, 17 Aug 2023 23:36:21 +0900 Subject: [PATCH 04/10] =?UTF-8?q?feat:=20=EB=AA=A8=EB=B0=94=EC=9D=BC?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=9D=B8=ED=92=8B=20=ED=8F=AC=EC=BB=A4?= =?UTF-8?q?=EC=8A=A4=EC=8B=9C=20=ED=99=95=EB=8C=80=EB=90=98=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95=20(#303)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/public/index.html | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/public/index.html b/frontend/public/index.html index 033f257ba..38bb635a3 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -1,8 +1,11 @@ - + - + S-HOOK From e09993913801bd7b7d7c143bcffc18f880ffdb97 Mon Sep 17 00:00:00 2001 From: SeokHwan An <70303795+seokhwan-an@users.noreply.github.com> Date: Fri, 18 Aug 2023 00:15:48 +0900 Subject: [PATCH 05/10] =?UTF-8?q?Data/#311=20=EA=B0=9C=EB=B0=9C=20?= =?UTF-8?q?=EC=84=9C=EB=B2=84=20=EC=B4=88=EA=B8=B0=20=EB=8D=94=EB=AF=B8?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=B6=94=EA=B0=80=20(#312)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 더미 데이터 추가 * refactor: submodule sync 맞추기 --- backend/src/main/resources/dev/data.sql | 179 ++++++++++------------ backend/src/main/resources/shook-security | 2 +- 2 files changed, 81 insertions(+), 100 deletions(-) diff --git a/backend/src/main/resources/dev/data.sql b/backend/src/main/resources/dev/data.sql index cb45ac6d4..97e5ddb97 100644 --- a/backend/src/main/resources/dev/data.sql +++ b/backend/src/main/resources/dev/data.sql @@ -1,112 +1,93 @@ TRUNCATE TABLE song; INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) -VALUES ('Super Shy', 'NewJeans', 200, 'ArmDp-zijuc', - 'http://i.maniadb.com/images/album/999/999126_1_f.jpg', now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) -VALUES ('Seven (feat. Latto) - Clean Ver.', '정국', 186, 'UUSbUBYqU_8', - 'http://i.maniadb.com/images/album/1000/000246_1_f.jpg', now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) -VALUES ('퀸카 (Queencard)', '(여자)아이들', 162, 'VOcb6ZHxSjc', - 'http://i.maniadb.com/images/album/992/992813_1_f.jpg', now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) -VALUES ('헤어지자 말해요', '박재정', 244, 'SrQzxD8UFdM', - 'http://i.maniadb.com/images/album/988/988164_1_f.jpg', now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) -VALUES ('I AM', 'IVE (아이브)', 208, 'cU0JrSAyy7o', '', now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) -VALUES ('이브, 프시케 그리고 푸른 수염의 아내', 'LE SSERAFIM (르세라핌)', 186, - 'Ii8L0qEvfC8', 'http://i.maniadb.com/images/album/989/989969_1_f.jpg', +VALUES ('취중고백', '김민석 (멜로망스)', 258, 'FCrMKhrFH7A', + 'https://cdnimg.melon.co.kr/cm2/album/images/108/16/959/10816959_20211217144957_500.jpg?c1818ddc493cb2bbb4d268431e6de7b5/melon/resize/282/quality/80/optimize', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 1, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 1, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 1, 3, now()); + INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) -VALUES ('Spicy', 'aespa', 198, '1kfmWl3o8TE', - 'http://i.maniadb.com/images/album/990/990970_1_f.jpg', now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) -VALUES ('Steal The Show (From "엘리멘탈")', 'Lauv', 194, 'kUMds6XKtfY', - '', now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) -VALUES ('사랑은 늘 도망가', '임영웅', 273, 'pBEAzM2TRmE', - 'http://i.maniadb.com/images/album/918/918794_1_f.jpg', now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) -VALUES ('ISTJ', 'NCT DREAM', 186, 'es60T3k-tyM', - 'http://i.maniadb.com/images/album/998/998384_1_f.jpg', now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) -VALUES ('모래 알갱이', '임영웅', 221, '3_wOZrzmQ1o', - 'http://i.maniadb.com/images/album/995/995218_1_f.jpg', now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) -VALUES ('UNFORGIVEN (feat. Nile Rodgers)', 'LE SSERAFIM (르세라핌)', 181, - 'fzSDGXyGTjg', - 'http://i.maniadb.com/images/album/989/989969_1_f.jpg', now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) -VALUES ('Kitsch', 'IVE (아이브)', 195, 'r572qh2__-U', - 'http://i.maniadb.com/images/album/984/984148_1_f.jpg', now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) -VALUES ('우리들의 블루스', '임영웅', 207, 'epz-aL5RaLQ', - 'http://i.maniadb.com/images/album/930/930879_1_f.jpg', now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) -VALUES ('Candy', 'NCT DREAM', 220, 'QuaVFoBLQeg', - 'http://i.maniadb.com/images/album/978/978389_1_f.jpg', now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) -VALUES ('Hype boy', 'NewJeans', 180, 'T--6HBX2K4g', - 'http://i.maniadb.com/images/album/946/946945_1_f.jpg', now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) -VALUES ('다시 만날 수 있을까', '임영웅', 275, 'VPDRLgfqfSs', '', now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) -VALUES ('Broken Melodies', 'NCT DREAM', 227, 'EPsh2192sTU', - 'http://i.maniadb.com/images/album/998/998384_1_f.jpg', now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) -VALUES ('Still With You', '정국', 239, 'BksBNbTIoPE', '', now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) -VALUES ('무지개', '임영웅', 198, 'o8e0Qd2H1qc', '', now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) -VALUES ('Ditto', 'NewJeans', 187, 'haCpjUXIhrI', - 'http://i.maniadb.com/images/album/978/978654_1_f.jpg', now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) -VALUES ('London Boy', '임영웅', 289, 'ZRDuScdwEbE', '', now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) -VALUES ('이제 나만 믿어요', '임영웅', 274, 'y1KXYmMuZZA', - 'http://i.maniadb.com/images/album_t/80/794/794139_1_f.jpg', now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) -VALUES ('아버지', '임영웅', 240, 'dbaiMJOnaB4', '', now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) -VALUES ('Polaroid', '임영웅', 209, 'PVDxs6GUXSI', - 'http://i.maniadb.com/images/album_t/80/972/972891_1_f.jpg', now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) -VALUES ('Dynamite', '방탄소년단', 198, 'KhZ5DCd7m6s', - 'http://i.maniadb.com/images/album_t/80/816/816954_1_f.jpg', now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) -VALUES ('손오공', '세븐틴 (SEVENTEEN)', 200, 'tFPbzfU5XL4', - 'http://i.maniadb.com/images/album_t/80/988/988481_1_f.jpg', now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) -VALUES ('인생찬가', '임영웅', 235, 'cXHduPVrcDQ', '', now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) -VALUES ('A bientot', '임영웅', 258, 'sZDDLUB8wQE', '', now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) -VALUES ('손이 참 곱던 그대', '임영웅', 197, 'OpZIaI-J0uk', '', now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) -VALUES ('사랑해 진짜', '임영웅', 241, 'qkledxNCNfY', '', now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) -VALUES ('꽃', '지수', 174, '6zM48_rBFbY', - 'http://i.maniadb.com/images/album_t/80/984/984469_1_f.jpg', now()); +VALUES ('해요 (2022)', '#안녕', 238, 'P6gV_t70KAk', 'https://cdnimg.melon.co.kr/cm2/album/images/109/75/276/10975276_20220603165713_500.jpg?690c69f1d7581bed46767533175728ff/melon/resize/282/quality/80/optimize', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 2, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 2, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 2, 3, now()); + INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) -VALUES ('연애편지', '임영웅', 217, 'gSQFZvUuQ3s', '', now()); +VALUES ('TOMBOY', '(여자)아이들', 174, '0wezH4MAncY', 'https://cdnimg.melon.co.kr/cm2/album/images/108/90/384/10890384_20220314111504_500.jpg?4b9dba7aeba43a4e0042eedb6b9865c1/melon/resize/282/quality/80/optimize', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 3, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 3, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 3, 3, now()); + INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) -VALUES ('New jeans', 'New jeans', 109, 'G8GEpK7YDl4', - 'http://i.maniadb.com/images/album_t/80/999/999126_1_f.jpg', now()); +VALUES ('다정히 내 이름을 부르면', '경서예지, 전건호', 263, 'b_6EfFZyBxY', 'https://cdnimg.melon.co.kr/cm2/album/images/106/10/525/10610525_20210518143433_500.jpg?e8c5aa44ff6608c13fa48eb6a20e81af/melon/resize/282/quality/80/optimize', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 4, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 4, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 4, 3, now()); + INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) -VALUES ('OMG', 'New jeans', 215, 'jT0Lh-N3TSg', '', now()); +VALUES ('That''s Hilarious', 'Charlie Puth', 146, 'F3KMndbOhIcㅍ', 'https://cdnimg.melon.co.kr/cm2/album/images/108/44/485/10844485_20221006154824_500.jpg?b752b5ed8fad66b79e2705840630dd94/melon/resize/282/quality/80/optimize', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 5, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 5, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 5, 3, now()); + INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) -VALUES ('Butter', '방탄소년단', 165, 'Uz0PppyT7Cc', - 'http://i.maniadb.com/images/album_t/80/856/856717_1_f.jpg', now()); +VALUES ('Heaven(2023)', '임재현', 279, 'fPLXgfcyoMc', 'https://cdnimg.melon.co.kr/cm2/album/images/111/54/876/11154876_20230121133335_500.jpg?0ae26bb599c92ddd436282395563596e/melon/resize/282/quality/80/optimize', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 6, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 6, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 6, 3, now()); + INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) -VALUES ('파랑 (Blue Wave)', 'NCT DREAM', 191, 'ZkLK4hUqqas', - 'http://i.maniadb.com/images/album_t/80/998/998384_1_f.jpg', now()); +VALUES ('당신을 만나', '김호중, 송가인', 238, 'kn_j1Ipw4DM', 'https://cdnimg.melon.co.kr/cm2/album/images/111/54/876/11154876_20230121133335_500.jpg?0ae26bb599c92ddd436282395563596e/melon/resize/282/quality/80/optimize', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 7, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 7, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 7, 3, now()); + INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) -VALUES ('Take Two', '방탄소년단', 230, '3UE-vpej_VI', - 'http://i.maniadb.com/images/album_t/80/997/997196_1_f.jpg', now()); +VALUES ('잘 지내자, 우리 (여름날 우리 X 로이킴)', '로이킴', 258, 'MbSAeRQl0Xw', 'https://cdnimg.melon.co.kr/cm2/album/images/111/54/876/11154876_20230121133335_500.jpg?0ae26bb599c92ddd436282395563596e/melon/resize/282/quality/80/optimize', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 8, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 8, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 8, 3, now()); + INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) -VALUES ('Like We Just Met', 'NCT DREAM', 210, 'eA9pwL-8wJw', - 'http://i.maniadb.com/images/album_t/80/998/998384_1_f.jpg', now()); +VALUES ('빛이 나는 너에게', '던 (DAWN)', 175, 'wkr3S0hIXLk', 'https://cdnimg.melon.co.kr/cm2/album/images/111/54/876/11154876_20230121133335_500.jpg?0ae26bb599c92ddd436282395563596e/melon/resize/282/quality/80/optimize', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 9, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 9, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 9, 3, now()); + INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) -VALUES ('Yogurt Shake', 'NCT DREAM', 218, 'IUs7tOzHVJw', - 'http://i.maniadb.com/images/album_t/80/998/998384_1_f.jpg', now()); +VALUES ('파랑 (Blue Wave)', 'NCT DREAM', 189, 'NhgoqtRhb4g', 'https://cdnimg.melon.co.kr/cm2/album/images/111/54/876/11154876_20230121133335_500.jpg?0ae26bb599c92ddd436282395563596e/melon/resize/282/quality/80/optimize', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 10, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 10, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 10, 3, now()); diff --git a/backend/src/main/resources/shook-security b/backend/src/main/resources/shook-security index e2ecf8351..faac40305 160000 --- a/backend/src/main/resources/shook-security +++ b/backend/src/main/resources/shook-security @@ -1 +1 @@ -Subproject commit e2ecf83511254975066a9739387b87ef73e68506 +Subproject commit faac40305721e615226f2f55bc4c97c776b583ee From a1b7c76223cededb6d3c6f97635d9483f3c21e1a Mon Sep 17 00:00:00 2001 From: splitCoding Date: Fri, 18 Aug 2023 00:46:09 +0900 Subject: [PATCH 06/10] =?UTF-8?q?fix:=20dev=20allow-origin=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/main/resources/shook-security | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/resources/shook-security b/backend/src/main/resources/shook-security index faac40305..7c1145606 160000 --- a/backend/src/main/resources/shook-security +++ b/backend/src/main/resources/shook-security @@ -1 +1 @@ -Subproject commit faac40305721e615226f2f55bc4c97c776b583ee +Subproject commit 7c11456068491df7b7822f6d374f8dfc70b99dd0 From 5aaadabf3e37574dda020ac7bea7c8ca7cc49516 Mon Sep 17 00:00:00 2001 From: SeokHwan An <70303795+seokhwan-an@users.noreply.github.com> Date: Fri, 18 Aug 2023 01:03:20 +0900 Subject: [PATCH 07/10] =?UTF-8?q?Data/#311=20=ED=82=AC=EB=A7=81=ED=8C=8C?= =?UTF-8?q?=ED=8A=B8=20=EC=88=98=EC=A7=91=EC=A4=91=EC=9D=B8=20=EB=85=B8?= =?UTF-8?q?=EB=9E=98=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?(#313)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 킬링파트 수집중인 노래 추가 * refactor: submodule sync 맞추기 --- backend/src/main/resources/dev/data.sql | 17 +++++++++++++++++ backend/src/main/resources/shook-security | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/backend/src/main/resources/dev/data.sql b/backend/src/main/resources/dev/data.sql index 97e5ddb97..fbf4fbccd 100644 --- a/backend/src/main/resources/dev/data.sql +++ b/backend/src/main/resources/dev/data.sql @@ -1,9 +1,26 @@ TRUNCATE TABLE song; +TRUNCATE TABLE voting_song; + +insert into voting_song (title, singer, length, video_id, album_cover_url, created_at) +values ('달빛소년', '체리필터 (cherryfilter)', 241, 'MENOHM2a8Oo', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/015/027/236/15027236_1319188420285_1_600x600.JPG/dims/resize/Q_80,0', now()); + +insert into voting_song (title, singer, length, video_id, album_cover_url, created_at) +values ('Happy Day', '체리필터 (cherryfilter)', 216, '6CFs-4if788', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/041/719/050/41719050_1319703431175_1_600x600.JPG/dims/resize/Q_80,0', now()); + +insert into voting_song (title, singer, length, video_id, album_cover_url, created_at) +values ('낭만 고양이', '체리필터 (cherryfilter)', 228, 'Nh5Ld4EpXJs', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/015/026/138/15026138_1406191371950_1_600x600.JPG/dims/resize/Q_80,0', now()); + +insert into voting_song (title, singer, length, video_id, album_cover_url, created_at) +values ('오리날다 (영화 ''권순분여사 납치사건'' CF 테마송)', '체리필터 (cherryfilter)', 246, 'ivbdRCDCOig', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/015/027/236/15027236_1319188420285_1_600x600.JPG/dims/resize/Q_80,0', now()); + +insert into voting_song (title, singer, length, video_id, album_cover_url, created_at) +values ('피아니시모 (Pianissimo)', '체리필터 (cherryfilter)', 229, 'VxnWkErj2TE', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/079/949/993/79949993_1398756703913_1_600x600.JPG/dims/resize/Q_80,0', now()); INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) VALUES ('취중고백', '김민석 (멜로망스)', 258, 'FCrMKhrFH7A', 'https://cdnimg.melon.co.kr/cm2/album/images/108/16/959/10816959_20211217144957_500.jpg?c1818ddc493cb2bbb4d268431e6de7b5/melon/resize/282/quality/80/optimize', now()); + INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (5, 'SHORT', 1, 10, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) diff --git a/backend/src/main/resources/shook-security b/backend/src/main/resources/shook-security index 7c1145606..faac40305 160000 --- a/backend/src/main/resources/shook-security +++ b/backend/src/main/resources/shook-security @@ -1 +1 @@ -Subproject commit 7c11456068491df7b7822f6d374f8dfc70b99dd0 +Subproject commit faac40305721e615226f2f55bc4c97c776b583ee From dc2f8dd8b2b09b93c34bbc8f923cb98343bd5864 Mon Sep 17 00:00:00 2001 From: SeokHwan An <70303795+seokhwan-an@users.noreply.github.com> Date: Fri, 18 Aug 2023 01:11:03 +0900 Subject: [PATCH 08/10] =?UTF-8?q?refactor:=20submodule=20sync=20=EB=A7=9E?= =?UTF-8?q?=EC=B6=94=EA=B8=B0=20(#314)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/main/resources/shook-security | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/resources/shook-security b/backend/src/main/resources/shook-security index faac40305..7c1145606 160000 --- a/backend/src/main/resources/shook-security +++ b/backend/src/main/resources/shook-security @@ -1 +1 @@ -Subproject commit faac40305721e615226f2f55bc4c97c776b583ee +Subproject commit 7c11456068491df7b7822f6d374f8dfc70b99dd0 From 68b26f16f3e6985f176064f916e1b960baf9d250 Mon Sep 17 00:00:00 2001 From: ukkodeveloper Date: Fri, 18 Aug 2023 02:02:33 +0900 Subject: [PATCH 09/10] =?UTF-8?q?Feat/#279=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84=20=EB=B0=8F=20=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=EC=99=80=20=EC=88=98=EC=A7=91=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=20=EC=A0=81=EC=9A=A9=20(#3?= =?UTF-8?q?15)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: AuthPage 구현 Co-authored-by: 윤정민 * refactor: login redirect path 상수화 * feat: auth 전역 상태 provider 구현 * refactor: CommentForm 컴포넌트 분리 * feat: avatar 컴포넌트 생성 * feat: 기본 아바타 svg 추가 * style: todo 주석 추가 * feat: AuthProvider 수정 * feat: 로그인 여부에 따라 CommentForm 조건부 렌더 * feat: 임시 로그인 * feat: authProvider accessToken을 상태로 갖도록 변경 * feat: LoginLink 컴포넌트 추가 * feat: accessToken 요청 시에 header에 credentials 추가 * feat: Header 프로필 이미지 로그인에 따라 조건부 렌더링 * feat: 로그인 상태에서 header의 icon을 누르면 My-Page로 이동 * refactor: parseJWT 분리 * feat: AuthLayout 구현 및 마이페이지에 적용 * refactor: LoginLink 삭제 후 googleAuthUrl 문자열 변수로 적용 * feat: 로그인 테스트 버튼 추가 * fix: google auth 수정 * fix: googleAuth url 문자열에서 api 제거 * fix: googleAuthUrl api 문자열 제거 정규표현식 수정 * fix: googleAuthUrl 수정 * style: 사용하지 않은 코드 제거 및 googleAuth 아이디 변경 * feat: 임시 refresh token 받는 버튼 추가 * feat: MSW votingSongs 목 데이터 추가 * fix: voting 노래들 캐러셀 스타일 수정 * feat: msw json 모킹 파일 api 따른 수정 및 voting 추가 * refactor: popularSong, votingSong 타입 분리 * feat: 캐러셀 투표중인 아이템 메인 페이지에 렌더 * feat: Header 내 테스트용 버튼 제거 * refactor: 킬링파트 등록 페이지 최신 api 적용 * chore: 에러 발생하는 storybook 제거 및 타입 수정 * fix: killing part 타입 변경으로 인한 수정 * fix: voting-parts post api 변경 * refactor: videoId를 provider로 제공하도록 변경 * feat: loginModal 컴포넌트 구현 * feat: 노래 수집 페이지에서 로그인 상황에 따라 분기 적용 * fix: google search params에 code 없는 경우 재요청 * feat: logout 기능 추가 * feat: accessToken없을 시에 oauth 페이지로 이동 * fix: 로그인 실패시 등록 모달이 생기지 않도록 수정 * fix: 로그인 하지 않을 경우 google auth 페이지로 가도록 변경 * refactor: 킬링파트 등록 버튼 "수집"에서 "등록"으로 변경 --------- Co-authored-by: 윤정민 --- frontend/src/assets/icon/avatar-default.svg | 3 + .../features/auth/components/AuthProvider.tsx | 55 ++++++ .../features/auth/components/LoginModal.tsx | 70 ++++++++ .../features/auth/constants/googleAuthUrl.ts | 7 + frontend/src/features/auth/utils/parseJWT.ts | 15 ++ .../features/comments/components/Comment.tsx | 15 +- .../comments/components/CommentForm.tsx | 161 ++++++++++++++++++ .../comments/components/CommentList.tsx | 132 +------------- .../songs/components/CarouselItem.tsx | 90 ++++++++++ .../songs/components/CollectionCarousel.tsx | 3 - .../components/IntervalInput.stories.tsx | 2 +- .../songs/components/KillingPartInfo.tsx | 6 +- .../KillingPartToggleGroup.stories.tsx | 26 --- .../components/KillingPartTrack.stories.tsx | 60 ------- .../KillingPartTrackList.stories.tsx | 61 ------- .../components/PopularSongItem.stories.tsx | 4 +- .../songs/components/PopularSongItem.tsx | 8 +- .../songs/components/VoteInterface.tsx | 77 ++++++--- .../components/VoteInterfaceProvider.tsx | 3 + .../src/features/songs/remotes/killingPart.ts | 9 +- .../songs/remotes/usePostKillingPart.ts | 9 +- .../src/features/songs/types/Song.type.ts | 15 ++ frontend/src/index.tsx | 18 +- frontend/src/mocks/fixtures/popularSongs.json | 80 ++++----- frontend/src/mocks/fixtures/songs.json | 45 ++++- frontend/src/mocks/fixtures/votingSongs.json | 42 +++++ frontend/src/mocks/handlers/songsHandlers.ts | 7 +- frontend/src/pages/AuthPage.tsx | 41 +++++ frontend/src/pages/MainPage.tsx | 32 ++-- frontend/src/pages/MyPage.tsx | 8 + frontend/src/pages/PartCollectingPage.tsx | 23 +-- frontend/src/router.tsx | 15 ++ frontend/src/shared/components/Avatar.tsx | 17 ++ .../shared/components/Layout/AuthLayout.tsx | 37 ++++ .../src/shared/components/Layout/Header.tsx | 22 +++ frontend/src/shared/constants/path.ts | 2 + frontend/src/shared/remotes/index.ts | 21 ++- frontend/src/shared/types/killingPart.ts | 6 - frontend/src/shared/types/song.ts | 26 ++- 39 files changed, 839 insertions(+), 434 deletions(-) create mode 100644 frontend/src/assets/icon/avatar-default.svg create mode 100644 frontend/src/features/auth/components/AuthProvider.tsx create mode 100644 frontend/src/features/auth/components/LoginModal.tsx create mode 100644 frontend/src/features/auth/constants/googleAuthUrl.ts create mode 100644 frontend/src/features/auth/utils/parseJWT.ts create mode 100644 frontend/src/features/comments/components/CommentForm.tsx create mode 100644 frontend/src/features/songs/components/CarouselItem.tsx delete mode 100644 frontend/src/features/songs/components/KillingPartToggleGroup.stories.tsx delete mode 100644 frontend/src/features/songs/components/KillingPartTrack.stories.tsx delete mode 100644 frontend/src/features/songs/components/KillingPartTrackList.stories.tsx create mode 100644 frontend/src/features/songs/types/Song.type.ts create mode 100644 frontend/src/mocks/fixtures/votingSongs.json create mode 100644 frontend/src/pages/AuthPage.tsx create mode 100644 frontend/src/pages/MyPage.tsx create mode 100644 frontend/src/shared/components/Avatar.tsx create mode 100644 frontend/src/shared/components/Layout/AuthLayout.tsx diff --git a/frontend/src/assets/icon/avatar-default.svg b/frontend/src/assets/icon/avatar-default.svg new file mode 100644 index 000000000..8f5cd6cba --- /dev/null +++ b/frontend/src/assets/icon/avatar-default.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/features/auth/components/AuthProvider.tsx b/frontend/src/features/auth/components/AuthProvider.tsx new file mode 100644 index 000000000..6d8644965 --- /dev/null +++ b/frontend/src/features/auth/components/AuthProvider.tsx @@ -0,0 +1,55 @@ +import { createContext, useContext, useMemo, useState } from 'react'; +import parseJWT from '../utils/parseJWT'; + +interface User { + memberId: number; + nickname: string; +} + +interface AuthContextProps { + user: User | null; + login: (accessToken: string) => void; + logout: () => void; +} + +export const useAuthContext = () => { + const contextValue = useContext(AuthContext); + + if (contextValue === null) throw new Error('AuthContext가 null입니다.'); + + return contextValue; +}; + +const AuthContext = createContext(null); + +const AuthProvider = ({ children }: { children: React.ReactElement[] }) => { + const [accessToken, setAccessToken] = useState(localStorage.getItem('userToken') || ''); + + // TODO: 예외처리? + const user: User | null = useMemo(() => { + if (!accessToken) { + return null; + } + + const { memberId, nickname } = parseJWT(accessToken); + + return { + memberId, + nickname, + }; + }, [accessToken]); + + const login = (userToken: string) => { + localStorage.setItem('userToken', userToken); + setAccessToken(userToken); + }; + + const logout = () => { + localStorage.removeItem('userToken'); + setAccessToken(''); + }; + + return {children}; +}; + +export default AuthProvider; diff --git a/frontend/src/features/auth/components/LoginModal.tsx b/frontend/src/features/auth/components/LoginModal.tsx new file mode 100644 index 000000000..d107f46a6 --- /dev/null +++ b/frontend/src/features/auth/components/LoginModal.tsx @@ -0,0 +1,70 @@ +import { styled } from 'styled-components'; +import Modal from '@/shared/components/Modal/Modal'; +import googleAuthUrl from '../constants/googleAuthUrl'; + +interface LoginModalProps { + isOpen: boolean; + closeModal: () => void; + message: string; +} + +const LoginModal = ({ isOpen, closeModal, message }: LoginModalProps) => { + const linkToAuth = () => { + window.location.href = googleAuthUrl; + }; + + return ( + + 로그인이 필요합니다 + {message} + + + 닫기 + + + 로그인하러 가기 + + + + ); +}; + +export default LoginModal; + +const ModalTitle = styled.h3``; + +const ModalContent = styled.div` + padding: 16px 0; + + font-size: 16px; + color: #b5b3bc; + text-align: center; + white-space: pre-line; +`; + +const Button = styled.button` + cursor: pointer; + + height: 36px; + + color: ${({ theme: { color } }) => color.white}; + + border: none; + border-radius: 10px; +`; + +const ConfirmButton = styled(Button)` + flex: 1; + background-color: ${({ theme: { color } }) => color.secondary}; +`; + +const LoginButton = styled(Button)` + flex: 1.5; + background-color: ${({ theme: { color } }) => color.primary}; +`; + +const ButtonContainer = styled.div` + display: flex; + gap: 16px; + width: 100%; +`; diff --git a/frontend/src/features/auth/constants/googleAuthUrl.ts b/frontend/src/features/auth/constants/googleAuthUrl.ts new file mode 100644 index 000000000..16c5162dd --- /dev/null +++ b/frontend/src/features/auth/constants/googleAuthUrl.ts @@ -0,0 +1,7 @@ +import ROUTE_PATH from '@/shared/constants/path'; + +const redirectUrl = `${process.env.BASE_URL}${ROUTE_PATH.LOGIN_REDIRECT}`?.replace(/api\/?/, ''); + +const googleAuthUrl = `https://accounts.google.com/o/oauth2/v2/auth?scope=email&response_type=code&redirect_uri=${redirectUrl}&client_id=405219607197-qfpt1e3v1bm25ebvadt5bvttskse5vpg.apps.googleusercontent.com`; + +export default googleAuthUrl; diff --git a/frontend/src/features/auth/utils/parseJWT.ts b/frontend/src/features/auth/utils/parseJWT.ts new file mode 100644 index 000000000..c279376be --- /dev/null +++ b/frontend/src/features/auth/utils/parseJWT.ts @@ -0,0 +1,15 @@ +const parseJWT = (token: string) => { + const payloadUrl = token.split('.')[1]; + const base64 = payloadUrl.replace(/-/g, '+').replace(/_/g, '/'); + const jsonPayload = decodeURIComponent( + window + .atob(base64) + .split('') + .map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)) + .join('') + ); + + return JSON.parse(jsonPayload); +}; + +export default parseJWT; diff --git a/frontend/src/features/comments/components/Comment.tsx b/frontend/src/features/comments/components/Comment.tsx index e558cb07a..444d118fd 100644 --- a/frontend/src/features/comments/components/Comment.tsx +++ b/frontend/src/features/comments/components/Comment.tsx @@ -1,5 +1,6 @@ import styled from 'styled-components'; import shookshook from '@/assets/icon/shookshook.svg'; +import Avatar from '@/shared/components/Avatar'; import Spacing from '@/shared/components/Spacing'; interface CommentProps { @@ -20,9 +21,7 @@ const Comment = ({ content, createdAt }: CommentProps) => { return ( - - 익명 프로필 - + 익명 @@ -47,16 +46,6 @@ const Flex = styled.div` width: 100%; `; -const Profile = styled.div` - overflow: hidden; - - width: 40px; - height: 40px; - - background-color: white; - border-radius: 100%; -`; - const Box = styled.div` flex: 1; `; diff --git a/frontend/src/features/comments/components/CommentForm.tsx b/frontend/src/features/comments/components/CommentForm.tsx new file mode 100644 index 000000000..fd32e39d1 --- /dev/null +++ b/frontend/src/features/comments/components/CommentForm.tsx @@ -0,0 +1,161 @@ +import { useState } from 'react'; +import { Link } from 'react-router-dom'; +import { css, styled } from 'styled-components'; +import defaultAvatar from '@/assets/icon/avatar-default.svg'; +import shookshook from '@/assets/icon/shookshook.svg'; +import { useAuthContext } from '@/features/auth/components/AuthProvider'; +import googleAuthUrl from '@/features/auth/constants/googleAuthUrl'; +import Avatar from '@/shared/components/Avatar'; +import useToastContext from '@/shared/components/Toast/hooks/useToastContext'; +import { useMutation } from '@/shared/hooks/useMutation'; +import fetcher from '@/shared/remotes'; + +interface CommentFormProps { + getComment: () => Promise; + songId: string; + partId: number; +} + +const CommentForm = ({ getComment, songId, partId }: CommentFormProps) => { + const [newComment, setNewComment] = useState(''); + const { user } = useAuthContext(); + + const isLoggedIn = !!user; + + const { mutateData } = useMutation(() => + fetcher(`/songs/${songId}/parts/${partId}/comments`, 'POST', { content: newComment.trim() }) + ); + + const { showToast } = useToastContext(); + + const resetNewComment = () => setNewComment(''); + + const changeNewComment: React.ChangeEventHandler = ({ + currentTarget: { value }, + }) => setNewComment(value); + + const submitNewComment: React.FormEventHandler = async (event) => { + event.preventDefault(); + + await mutateData(); + + showToast('댓글이 등록되었습니다.'); + resetNewComment(); + getComment(); + }; + + return ( + + + {isLoggedIn ? ( + + ) : ( + + )} + {isLoggedIn ? ( + + ) : ( + + + + )} + + {isLoggedIn && ( + + + 취소 + + + 댓글 + + + )} + + ); +}; + +export default CommentForm; + +const LoginLink = styled(Link)` + flex: 1; +`; + +const Flex = styled.div` + display: flex; + gap: 14px; + align-items: flex-start; +`; + +const Container = styled.form` + bottom: 0; + + width: 100%; + padding: 16px; + + background-color: ${({ theme }) => theme.color.black}; + border-top: 1px solid ${({ theme }) => theme.color.white}; +`; + +const Input = styled.input` + flex: 1; + width: 100%; + margin: 0; + padding: 0; + + font-size: 14px; + + background-color: transparent; + border: none; + border-bottom: 1px solid white; + outline: none; + -webkit-box-shadow: none; + box-shadow: none; +`; + +const FlexEnd = styled.div` + display: flex; + gap: 10px; + justify-content: flex-end; +`; + +const buttonBase = css` + width: 50px; + height: 36px; + font-size: 14px; + border-radius: 10px; +`; + +const Cancel = styled.button` + ${buttonBase} + + &:hover, + &:focus { + background-color: ${({ theme }) => theme.color.secondary}; + } +`; + +const Submit = styled.button` + ${buttonBase} + background-color: ${({ theme }) => theme.color.primary}; + + &:hover, + &:focus { + background-color: #de5484; + } + + &:disabled { + background-color: ${({ theme }) => theme.color.secondary}; + } +`; diff --git a/frontend/src/features/comments/components/CommentList.tsx b/frontend/src/features/comments/components/CommentList.tsx index 6c6cb9e77..cb68cb9f9 100644 --- a/frontend/src/features/comments/components/CommentList.tsx +++ b/frontend/src/features/comments/components/CommentList.tsx @@ -1,16 +1,14 @@ -import { useEffect, useState } from 'react'; -import { styled, css } from 'styled-components'; +import { useEffect } from 'react'; +import { styled } from 'styled-components'; import cancelIcon from '@/assets/icon/cancel.svg'; -import shookshook from '@/assets/icon/shookshook.svg'; import BottomSheet from '@/shared/components/BottomSheet/BottomSheet'; import useModal from '@/shared/components/Modal/hooks/useModal'; import Spacing from '@/shared/components/Spacing'; import SRHeading from '@/shared/components/SRHeading'; -import useToastContext from '@/shared/components/Toast/hooks/useToastContext'; import useFetch from '@/shared/hooks/useFetch'; -import { useMutation } from '@/shared/hooks/useMutation'; import fetcher from '@/shared/remotes'; import Comment from './Comment'; +import CommentForm from './CommentForm'; interface Comment { id: number; @@ -24,34 +22,11 @@ interface CommentListProps { } const CommentList = ({ songId, partId }: CommentListProps) => { - const [newComment, setNewComment] = useState(''); const { isOpen, openModal, closeModal } = useModal(false); - const { data: comments, fetchData: getComment } = useFetch(() => fetcher(`/songs/${songId}/parts/${partId}/comments`, 'GET') ); - const { mutateData } = useMutation(() => - fetcher(`/songs/${songId}/parts/${partId}/comments`, 'POST', { content: newComment.trim() }) - ); - const { showToast } = useToastContext(); - - const resetNewComment = () => setNewComment(''); - - const changeNewComment: React.ChangeEventHandler = ({ - currentTarget: { value }, - }) => setNewComment(value); - - const submitNewComment: React.FormEventHandler = async (event) => { - event.preventDefault(); - - await mutateData(); - - showToast('댓글이 등록되었습니다.'); - resetNewComment(); - getComment(); - }; - useEffect(() => { getComment(); }, [partId]); @@ -86,28 +61,7 @@ const CommentList = ({ songId, partId }: CommentListProps) => { ))} - - - - 익명 프로필 - - - - - - 취소 - - - 댓글 - - - + ); @@ -115,74 +69,6 @@ const CommentList = ({ songId, partId }: CommentListProps) => { export default CommentList; -const Flex = styled.div` - display: flex; - gap: 14px; - align-items: flex-start; -`; - -const Profile = styled.div` - overflow: hidden; - - width: 40px; - height: 40px; - - background-color: white; - border-radius: 100%; -`; - -const Input = styled.input` - flex: 1; - - margin: 0; - padding: 0; - - font-size: 14px; - - background-color: transparent; - border: none; - border-bottom: 1px solid white; - outline: none; - -webkit-box-shadow: none; - box-shadow: none; -`; - -const FlexEnd = styled.div` - display: flex; - gap: 10px; - justify-content: flex-end; -`; - -const buttonBase = css` - width: 50px; - height: 36px; - font-size: 14px; - border-radius: 10px; -`; - -const Cancel = styled.button` - ${buttonBase} - - &:hover, - &:focus { - background-color: ${({ theme }) => theme.color.secondary}; - } -`; - -const Submit = styled.button` - ${buttonBase} - background-color: ${({ theme }) => theme.color.primary}; - - &:hover, - &:focus { - background-color: #de5484; - } - - &:disabled { - background-color: ${({ theme }) => theme.color.secondary}; - } -`; - const Comments = styled.ol` overflow-y: scroll; display: flex; @@ -208,16 +94,6 @@ const CommentTitle = styled.p` font-size: 20px; `; -const CommentForm = styled.form` - bottom: 0; - - width: 100%; - padding: 16px; - - background-color: ${({ theme }) => theme.color.black}; - border-top: 1px solid ${({ theme }) => theme.color.white}; -`; - const CommentsTitle = styled.p` padding-left: 16px; `; diff --git a/frontend/src/features/songs/components/CarouselItem.tsx b/frontend/src/features/songs/components/CarouselItem.tsx new file mode 100644 index 000000000..e3c2a7274 --- /dev/null +++ b/frontend/src/features/songs/components/CarouselItem.tsx @@ -0,0 +1,90 @@ +import { Link } from 'react-router-dom'; +import { styled } from 'styled-components'; +import emptyPlay from '@/assets/icon/empty-play.svg'; +import Spacing from '@/shared/components/Spacing'; +import ROUTE_PATH from '@/shared/constants/path'; +import { toMinSecText } from '@/shared/utils/convertTime'; +import type { VotingSong } from '../types/Song.type'; + +interface CarouselItemProps { + votingSong: VotingSong; +} + +const CarouselItem = ({ votingSong }: CarouselItemProps) => { + const { id, singer, title, videoLength, albumCoverUrl } = votingSong; + + return ( + + + + + + {title} + {singer} + + + {toMinSecText(videoLength)} + + + + + ); +}; + +export default CarouselItem; + +const Wrapper = styled.li` + width: 100%; + min-width: 350px; +`; + +const CollectingLink = styled(Link)` + display: flex; + justify-content: center; + + padding: 10px; +`; + +const Album = styled.img` + max-width: 120px; + background-color: white; +`; + +const Contents = styled.div` + display: flex; + flex-direction: column; + align-items: start; + + width: 150px; + white-space: nowrap; + justify-content: space-evenly; +`; + +const Title = styled.p` + overflow: hidden; + margin-left: 0; + font-size: 18px; + font-weight: 700; + max-width: 150px; + + text-overflow: ellipsis; +`; + +const Singer = styled.p` + overflow: hidden; + margin-left: 0; + font-size: 14px; + max-width: 150px; + + text-overflow: ellipsis; +`; + +const PlayingTime = styled.div` + display: flex; + column-gap: 8px; + border-radius: 20px; +`; + +const PlayingTimeText = styled.p` + padding-top: 2px; +`; diff --git a/frontend/src/features/songs/components/CollectionCarousel.tsx b/frontend/src/features/songs/components/CollectionCarousel.tsx index a19d925d9..1a85b149d 100644 --- a/frontend/src/features/songs/components/CollectionCarousel.tsx +++ b/frontend/src/features/songs/components/CollectionCarousel.tsx @@ -24,9 +24,6 @@ const CollectionCarousel = ({ children }: CarouselProps) => { const nextIndex = (currentIndex + 1) % numberOfItems; const itemWidth = carouselRef.current.scrollWidth / numberOfItems; carouselRef.current.scrollLeft = nextIndex * itemWidth; - - console.log('offsetWidth', carouselRef.current.offsetWidth); - console.log('scrollWidth', carouselRef.current.scrollWidth); } }, 2000); diff --git a/frontend/src/features/songs/components/IntervalInput.stories.tsx b/frontend/src/features/songs/components/IntervalInput.stories.tsx index 3a051b8f0..3e9c6f78f 100644 --- a/frontend/src/features/songs/components/IntervalInput.stories.tsx +++ b/frontend/src/features/songs/components/IntervalInput.stories.tsx @@ -10,7 +10,7 @@ const meta = { decorators: [ (Story) => ( - + diff --git a/frontend/src/features/songs/components/KillingPartInfo.tsx b/frontend/src/features/songs/components/KillingPartInfo.tsx index b8efb046c..896caf5a8 100644 --- a/frontend/src/features/songs/components/KillingPartInfo.tsx +++ b/frontend/src/features/songs/components/KillingPartInfo.tsx @@ -17,7 +17,7 @@ const KillingPartInfo = ({ killingPart }: KillingPartInfoProps) => { if (!killingPart) return; - const { voteCount, start, end, partVideoUrl } = killingPart; + const { likeCount, start, end, partVideoUrl } = killingPart; const shareUrl = () => { copyClipboard(partVideoUrl); @@ -42,9 +42,9 @@ const KillingPartInfo = ({ killingPart }: KillingPartInfoProps) => { - + - {voteCount}votes + {likeCount}votes diff --git a/frontend/src/features/songs/components/KillingPartToggleGroup.stories.tsx b/frontend/src/features/songs/components/KillingPartToggleGroup.stories.tsx deleted file mode 100644 index 51cdda72f..000000000 --- a/frontend/src/features/songs/components/KillingPartToggleGroup.stories.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { VideoPlayerProvider } from '@/features/youtube/components/VideoPlayerProvider'; -import KillingPartToggleGroup from './KillingPartToggleGroup'; -import { VoteInterfaceProvider } from './VoteInterfaceProvider'; -import type { Meta, StoryObj } from '@storybook/react'; - -const meta: Meta = { - component: KillingPartToggleGroup, - title: 'KillingPartToggleGroup', - decorators: [ - (Story) => ( - - - - - - ), - ], -}; - -export default meta; - -type Story = StoryObj; - -export const Default: Story = { - render: () => , -}; diff --git a/frontend/src/features/songs/components/KillingPartTrack.stories.tsx b/frontend/src/features/songs/components/KillingPartTrack.stories.tsx deleted file mode 100644 index 12a0e00b5..000000000 --- a/frontend/src/features/songs/components/KillingPartTrack.stories.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { useState } from 'react'; -import { VideoPlayerProvider } from '@/features/youtube/components/VideoPlayerProvider'; -import ToastProvider from '@/shared/components/Toast/ToastProvider'; -import KillingPartTrack from './KillingPartTrack'; -import type { KillingPart } from '@/shared/types/song'; -import type { Meta, StoryObj } from '@storybook/react'; - -const meta = { - component: KillingPartTrack, - title: 'KillingPartTrack', - decorators: [ - (Story) => { - return ( - - - - - - ); - }, - ], -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -const killingPart: KillingPart = { - exist: true, - id: 1, - rank: 1, - voteCount: 0, - start: 70, - end: 80, - partVideoUrl: 'https://youtu.be/ArmDp-zijuc?start=105&end=115', - likeCount: 12, -}; - -const KillingPartTrackWithHook = () => { - const [nowPlayingTrack, setNowPlayingTrack] = useState(-1); - - const changePlayingTrack: React.ChangeEventHandler = ({ currentTarget }) => { - const newTrack = Number(currentTarget.value); - - setNowPlayingTrack(newTrack); - }; - - const isPlaying = killingPart.rank === nowPlayingTrack; - - return ( - - ); -}; - -export const Default: Story = { - render: () => , -}; diff --git a/frontend/src/features/songs/components/KillingPartTrackList.stories.tsx b/frontend/src/features/songs/components/KillingPartTrackList.stories.tsx deleted file mode 100644 index 208488b97..000000000 --- a/frontend/src/features/songs/components/KillingPartTrackList.stories.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { VideoPlayerProvider } from '@/features/youtube/components/VideoPlayerProvider'; -import ToastProvider from '@/shared/components/Toast/ToastProvider'; -import KillingPartTrackList from './KillingPartTrackList'; -import type { KillingPart } from '@/shared/types/song'; -import type { Meta, StoryObj } from '@storybook/react'; - -const meta = { - component: KillingPartTrackList, - title: 'KillingPartTrackList', - decorators: [ - (Story) => { - return ( - - - - - - ); - }, - ], -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -const killingPart: KillingPart = { - exist: true, - id: 1, - rank: 1, - voteCount: 0, - start: 70, - end: 80, - partVideoUrl: 'https://youtu.be/ArmDp-zijuc?start=105&end=115', - likeCount: 12, -}; - -const killingPart2: KillingPart = { - exist: true, - id: 2, - rank: 2, - voteCount: 0, - start: 70, - end: 80, - partVideoUrl: 'https://youtu.be/ArmDp-zijuc?start=105&end=115', - likeCount: 12, -}; - -const killingPart3: KillingPart = { - exist: true, - id: 3, - rank: 3, - voteCount: 0, - start: 70, - end: 80, - partVideoUrl: 'https://youtu.be/ArmDp-zijuc?start=105&end=115', - likeCount: 12, -}; - -export const Default: Story = { - render: () => , -}; diff --git a/frontend/src/features/songs/components/PopularSongItem.stories.tsx b/frontend/src/features/songs/components/PopularSongItem.stories.tsx index 011f689ae..6720f0a84 100644 --- a/frontend/src/features/songs/components/PopularSongItem.stories.tsx +++ b/frontend/src/features/songs/components/PopularSongItem.stories.tsx @@ -10,7 +10,7 @@ export default meta; type Story = StoryObj; -const { title, singer, albumCoverUrl, totalVoteCount } = popularSongs[0]; +const { title, singer, albumCoverUrl, totalLikeCount } = popularSongs[0]; export const Default: Story = { args: { @@ -18,6 +18,6 @@ export const Default: Story = { title, singer, albumCoverUrl, - totalVoteCount, + totalLikeCount, }, }; diff --git a/frontend/src/features/songs/components/PopularSongItem.tsx b/frontend/src/features/songs/components/PopularSongItem.tsx index b62586a0b..c3289d4c6 100644 --- a/frontend/src/features/songs/components/PopularSongItem.tsx +++ b/frontend/src/features/songs/components/PopularSongItem.tsx @@ -6,18 +6,18 @@ interface CardProps { title: string; singer: string; albumCoverUrl: string; - totalVoteCount: number; + totalLikeCount: number; } -const PopularSongItem = ({ rank, albumCoverUrl, title, singer, totalVoteCount }: CardProps) => { +const PopularSongItem = ({ rank, albumCoverUrl, title, singer, totalLikeCount }: CardProps) => { return ( {rank} {title} {singer} - - {new Intl.NumberFormat('ko-KR').format(totalVoteCount)} votes + + {new Intl.NumberFormat('ko-KR').format(totalLikeCount)} likes ); diff --git a/frontend/src/features/songs/components/VoteInterface.tsx b/frontend/src/features/songs/components/VoteInterface.tsx index db3c32ce6..c052599fc 100644 --- a/frontend/src/features/songs/components/VoteInterface.tsx +++ b/frontend/src/features/songs/components/VoteInterface.tsx @@ -1,4 +1,7 @@ import { styled } from 'styled-components'; +import { useAuthContext } from '@/features/auth/components/AuthProvider'; +import LoginModal from '@/features/auth/components/LoginModal'; +import googleAuthUrl from '@/features/auth/constants/googleAuthUrl'; import useVoteInterfaceContext from '@/features/songs/hooks/useVoteInterfaceContext'; import VideoSlider from '@/features/youtube/components/VideoSlider'; import useVideoPlayerContext from '@/features/youtube/hooks/useVideoPlayerContext'; @@ -13,25 +16,29 @@ import KillingPartToggleGroup from './KillingPartToggleGroup'; const VoteInterface = () => { const { showToast } = useToastContext(); - const { interval, partStartTime, songId } = useVoteInterfaceContext(); + const { interval, partStartTime, songId, songVideoId } = useVoteInterfaceContext(); const { videoPlayer } = useVideoPlayerContext(); - const { killingPartPostResponse, createKillingPart } = usePostKillingPart(); + + const { error, createKillingPart } = usePostKillingPart(); + const { user } = useAuthContext(); const { isOpen, openModal, closeModal } = useModal(); + const isLoggedIn = !!user; + const voteTimeText = toPlayingTimeText(partStartTime, partStartTime + interval); const submitKillingPart = async () => { videoPlayer?.pauseVideo(); - await createKillingPart(songId, { startSecond: partStartTime, length: interval }); - + if (error) { + window.location.href = googleAuthUrl; + return; + } openModal(); }; const copyPartVideoUrl = async () => { - if (!killingPartPostResponse?.partVideoUrl) return; - - await copyClipboard(killingPartPostResponse?.partVideoUrl); + await copyClipboard(`https://www.youtube.com/watch?v=${songVideoId}`); closeModal(); showToast('클립보드에 영상링크가 복사되었습니다.'); }; @@ -44,25 +51,43 @@ const VoteInterface = () => { - - 투표 - - - - 킬링파트 투표를 완료했습니다. - - {voteTimeText} - 파트를 공유해 보세요😀 - - - - 확인 - - - 공유하기 - - - + {isLoggedIn ? ( + + 등록 + + ) : ( + { + openModal(); + }} + > + 등록 + + )} + {isLoggedIn ? ( + + 킬링파트 등록을 완료했습니다. + + {voteTimeText} + 파트를 공유해 보세요😀 + + + + 확인 + + + 공유하기 + + + + ) : ( + + )} ); }; diff --git a/frontend/src/features/songs/components/VoteInterfaceProvider.tsx b/frontend/src/features/songs/components/VoteInterfaceProvider.tsx index 42a32704d..ebb8cdd60 100644 --- a/frontend/src/features/songs/components/VoteInterfaceProvider.tsx +++ b/frontend/src/features/songs/components/VoteInterfaceProvider.tsx @@ -16,12 +16,14 @@ export const VoteInterfaceContext = createContext) => { const [interval, setInterval] = useState(10); const [partStartTime, setPartStartTime] = useState(0); @@ -74,6 +76,7 @@ export const VoteInterfaceProvider = ({ interval, videoLength, songId, + songVideoId, updatePartStartTime, updateKillingPartInterval, }} diff --git a/frontend/src/features/songs/remotes/killingPart.ts b/frontend/src/features/songs/remotes/killingPart.ts index 1bfa4fb48..1f78e8437 100644 --- a/frontend/src/features/songs/remotes/killingPart.ts +++ b/frontend/src/features/songs/remotes/killingPart.ts @@ -1,9 +1,6 @@ import fetcher from '@/shared/remotes'; -import type { KillingPartPostRequest, KillingPartPostResponse } from '@/shared/types/killingPart'; +import type { KillingPartPostRequest } from '@/shared/types/killingPart'; -export const postKillingPart = async ( - songId: number, - body: KillingPartPostRequest -): Promise => { - return await fetcher(`/songs/${songId}/parts`, 'POST', body); +export const postKillingPart = async (songId: number, body: KillingPartPostRequest) => { + return await fetcher(`/voting-songs/${songId}/parts`, 'POST', body); }; diff --git a/frontend/src/features/songs/remotes/usePostKillingPart.ts b/frontend/src/features/songs/remotes/usePostKillingPart.ts index 332c13b82..8bf631d37 100644 --- a/frontend/src/features/songs/remotes/usePostKillingPart.ts +++ b/frontend/src/features/songs/remotes/usePostKillingPart.ts @@ -2,12 +2,7 @@ import { postKillingPart } from '@/features/songs/remotes/killingPart'; import { useMutation } from '@/shared/hooks/useMutation'; export const usePostKillingPart = () => { - const { - data: killingPartPostResponse, - isLoading, - error, - mutateData: createKillingPart, - } = useMutation(postKillingPart); + const { isLoading, error, mutateData: createKillingPart } = useMutation(postKillingPart); - return { killingPartPostResponse, isLoading, error, createKillingPart }; + return { isLoading, error, createKillingPart }; }; diff --git a/frontend/src/features/songs/types/Song.type.ts b/frontend/src/features/songs/types/Song.type.ts new file mode 100644 index 000000000..0e99bce69 --- /dev/null +++ b/frontend/src/features/songs/types/Song.type.ts @@ -0,0 +1,15 @@ +export interface PopularSong { + id: number; + title: string; + singer: string; + albumCoverUrl: string; + totalLikeCount: number; +} + +export interface VotingSong { + id: number; + title: string; + singer: string; + videoLength: number; + albumCoverUrl: string; +} diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index b82ef18a8..0b437743f 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -1,8 +1,9 @@ -import React from 'react'; +// import React from 'react'; import { createRoot } from 'react-dom/client'; import { RouterProvider } from 'react-router-dom'; import { ThemeProvider } from 'styled-components'; import GlobalStyles from '@/shared/styles/GlobalStyles'; +import AuthProvider from './features/auth/components/AuthProvider'; import router from './router'; import ToastProvider from './shared/components/Toast/ToastProvider'; import theme from './shared/styles/theme'; @@ -12,24 +13,33 @@ async function main() { const { worker } = await import('./mocks/browser'); await worker.start({ + onUnhandledRequest: 'bypass', serviceWorker: { url: '/mockServiceWorker.js', }, }); } + // TODO: 웹 사이트 진입 시에 자동 로그인 (token 확인) + const root = createRoot(document.getElementById('root') as HTMLElement); root.render( - + // + - + - + + // ); } +const App = () => { + return ; +}; + main(); diff --git a/frontend/src/mocks/fixtures/popularSongs.json b/frontend/src/mocks/fixtures/popularSongs.json index 6dadc2d6e..0645eaee9 100644 --- a/frontend/src/mocks/fixtures/popularSongs.json +++ b/frontend/src/mocks/fixtures/popularSongs.json @@ -4,279 +4,279 @@ "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", - "totalVoteCount": 258200 + "totalLikeCount": 258200 }, { "id": 2, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", - "totalVoteCount": 121312 + "totalLikeCount": 121312 }, { "id": 3, "title": "노래 길이가 좀 많이 길어요. 노래 길이가 좀 많이 길어요. 노래 길이가 좀 많이 길어요. 노래 길이가 좀 많이 길어요.", "singer": "가수 이름이 좀 길어요. 가수 이름이 좀 길어요. 가수 이름이 좀 길어요. 가수 이름이 좀 길어요. 가수 이름이 좀 길어요.", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", - "totalVoteCount": 9000 + "totalLikeCount": 9000 }, { "id": 4, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", - "totalVoteCount": 8000 + "totalLikeCount": 8000 }, { "id": 5, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", - "totalVoteCount": 1 + "totalLikeCount": 1 }, { "id": 6, "title": "Seven (feat. Latto) - Clean Ver.", "singer": "정국", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/86/070/11286070_20230713181059_500.jpg/melon/resize/120/quality/80/optimize", - "totalVoteCount": 1 + "totalLikeCount": 1 }, { "id": 7, "title": "노래 길이가 좀 많이 길어요. 노래 길이가 좀 많이 길어요. 노래 길이가 좀 많이 길어요. 노래 길이가 좀 많이 길어요.", "singer": "가수 이름이 좀 길어요. 가수 이름이 좀 길어요. 가수 이름이 좀 길어요. 가수 이름이 좀 길어요. 가수 이름이 좀 길어요.", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/86/070/11286070_20230713181059_500.jpg/melon/resize/120/quality/80/optimize", - "totalVoteCount": 1 + "totalLikeCount": 1 }, { "id": 8, "title": "Seven (feat. Latto) - Clean Ver.", "singer": "정국", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/86/070/11286070_20230713181059_500.jpg/melon/resize/120/quality/80/optimize", - "totalVoteCount": 1 + "totalLikeCount": 1 }, { "id": 9, "title": "Seven (feat. Latto) - Clean Ver.", "singer": "정국", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/86/070/11286070_20230713181059_500.jpg/melon/resize/120/quality/80/optimize", - "totalVoteCount": 1 + "totalLikeCount": 1 }, { "id": 10, "title": "Seven (feat. Latto) - Clean Ver.", "singer": "정국", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/86/070/11286070_20230713181059_500.jpg/melon/resize/120/quality/80/optimize", - "totalVoteCount": 1 + "totalLikeCount": 1 }, { "id": 11, "title": "I AM", "singer": "IVE (아이브)", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/11/297/11211297_20230410151046_500.jpg/melon/resize/120/quality/80/optimize", - "totalVoteCount": 1 + "totalLikeCount": 1 }, { "id": 12, "title": "I AM", "singer": "IVE (아이브)", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/11/297/11211297_20230410151046_500.jpg/melon/resize/120/quality/80/optimize", - "totalVoteCount": 1 + "totalLikeCount": 1 }, { "id": 13, "title": "I AM", "singer": "IVE (아이브)", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/11/297/11211297_20230410151046_500.jpg/melon/resize/120/quality/80/optimize", - "totalVoteCount": 1 + "totalLikeCount": 1 }, { "id": 14, "title": "I AM", "singer": "IVE (아이브)", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/11/297/11211297_20230410151046_500.jpg/melon/resize/120/quality/80/optimize", - "totalVoteCount": 1 + "totalLikeCount": 1 }, { "id": 15, "title": "I AM", "singer": "IVE (아이브)", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/11/297/11211297_20230410151046_500.jpg/melon/resize/120/quality/80/optimize", - "totalVoteCount": 1 + "totalLikeCount": 1 }, { "id": 16, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", - "totalVoteCount": 1 + "totalLikeCount": 1 }, { "id": 17, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", - "totalVoteCount": 1 + "totalLikeCount": 1 }, { "id": 18, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", - "totalVoteCount": 1 + "totalLikeCount": 1 }, { "id": 19, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", - "totalVoteCount": 1 + "totalLikeCount": 1 }, { "id": 20, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", - "totalVoteCount": 1 + "totalLikeCount": 1 }, { "id": 21, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", - "totalVoteCount": 1 + "totalLikeCount": 1 }, { "id": 22, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", - "totalVoteCount": 1 + "totalLikeCount": 1 }, { "id": 23, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", - "totalVoteCount": 1 + "totalLikeCount": 1 }, { "id": 24, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", - "totalVoteCount": 1 + "totalLikeCount": 1 }, { "id": 25, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", - "totalVoteCount": 1 + "totalLikeCount": 1 }, { "id": 26, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", - "totalVoteCount": 1 + "totalLikeCount": 1 }, { "id": 27, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", - "totalVoteCount": 1 + "totalLikeCount": 1 }, { "id": 28, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", - "totalVoteCount": 1 + "totalLikeCount": 1 }, { "id": 29, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", - "totalVoteCount": 1 + "totalLikeCount": 1 }, { "id": 30, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", - "totalVoteCount": 1 + "totalLikeCount": 1 }, { "id": 31, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", - "totalVoteCount": 1 + "totalLikeCount": 1 }, { "id": 32, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", - "totalVoteCount": 1 + "totalLikeCount": 1 }, { "id": 33, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", - "totalVoteCount": 1 + "totalLikeCount": 1 }, { "id": 34, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", - "totalVoteCount": 1 + "totalLikeCount": 1 }, { "id": 35, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", - "totalVoteCount": 1 + "totalLikeCount": 1 }, { "id": 36, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", - "totalVoteCount": 1 + "totalLikeCount": 1 }, { "id": 37, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", - "totalVoteCount": 1 + "totalLikeCount": 1 }, { "id": 38, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", - "totalVoteCount": 1 + "totalLikeCount": 1 }, { "id": 39, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", - "totalVoteCount": 1 + "totalLikeCount": 1 }, { "id": 40, "title": "Super Shy", "singer": "New Jeans", "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", - "totalVoteCount": 1 + "totalLikeCount": 1 } ] diff --git a/frontend/src/mocks/fixtures/songs.json b/frontend/src/mocks/fixtures/songs.json index 2ebe8df0f..f05e717ba 100644 --- a/frontend/src/mocks/fixtures/songs.json +++ b/frontend/src/mocks/fixtures/songs.json @@ -8,28 +8,37 @@ "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", "killingParts": [ { + "id": 1, "exist": true, "rank": 1, "voteCount": 10, "start": 5, "end": 15, - "partVideoUrl": "https://www.youtube.com/embed/UUSbUBYqU_8?start=5&end=15" + "partVideoUrl": "https://www.youtube.com/embed/UUSbUBYqU_8?start=5&end=15", + "likeCount": 111, + "likeStatus": false }, { + "id": 2, "exist": true, "rank": 2, "voteCount": 2, "start": 20, "end": 30, - "partVideoUrl": "https://www.youtube.com/embed/UUSbUBYqU_8?start=20&end=30" + "partVideoUrl": "https://www.youtube.com/embed/UUSbUBYqU_8?start=20&end=30", + "likeCount": 11, + "likeStatus": true }, { + "id": 3, "exist": true, "rank": 3, "voteCount": 1, "start": 120, "end": 130, - "partVideoUrl": "https://www.youtube.com/embed/UUSbUBYqU_8?start=120&end=130" + "partVideoUrl": "https://www.youtube.com/embed/UUSbUBYqU_8?start=120&end=130", + "likeCount": 1, + "likeStatus": false } ] }, @@ -42,28 +51,37 @@ "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", "killingParts": [ { + "id": 1, "exist": true, "rank": 1, "voteCount": 10, "start": 130, "end": 140, - "partVideoUrl": "https://www.youtube.com/embed/ArmDp-zijuc?start=130&end=140" + "partVideoUrl": "https://www.youtube.com/embed/ArmDp-zijuc?start=130&end=140", + "likeCount": 111, + "likeStatus": false }, { + "id": 2, "exist": true, "rank": 2, "voteCount": 2, "start": 52, "end": 62, - "partVideoUrl": "https://www.youtube.com/embed/ArmDp-zijuc?start=52&end=62" + "partVideoUrl": "https://www.youtube.com/embed/ArmDp-zijuc?start=52&end=62", + "likeCount": 11, + "likeStatus": true }, { + "id": 3, "exist": true, "rank": 3, "voteCount": 1, "start": 123, "end": 133, - "partVideoUrl": "https://www.youtube.com/embed/ArmDp-zijuc?start=123&end=133" + "partVideoUrl": "https://www.youtube.com/embed/ArmDp-zijuc?start=123&end=133", + "likeCount": 11, + "likeStatus": true } ] }, @@ -77,28 +95,37 @@ "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize", "killingParts": [ { + "id": 1, "exist": true, "rank": 1, "voteCount": 10, "start": 0, "end": 10, - "partVideoUrl": "https://www.youtube.com/embed/VOcb6ZHxSjc?start=0&end=10" + "partVideoUrl": "https://www.youtube.com/embed/VOcb6ZHxSjc?start=0&end=10", + "likeCount": 11, + "likeStatus": true }, { + "id": 2, "exist": true, "rank": 2, "voteCount": 2, "start": 40, "end": 50, - "partVideoUrl": "https://www.youtube.com/embed/VOcb6ZHxSjc?start=40&end=50" + "partVideoUrl": "https://www.youtube.com/embed/VOcb6ZHxSjc?start=40&end=50", + "likeCount": 111, + "likeStatus": false }, { + "id": 3, "exist": true, "rank": 3, "voteCount": 1, "start": 24, "end": 34, - "partVideoUrl": "https://www.youtube.com/embed/VOcb6ZHxSjc?start=24&end=34" + "partVideoUrl": "https://www.youtube.com/embed/VOcb6ZHxSjc?start=24&end=34", + "likeCount": 1, + "likeStatus": true } ] } diff --git a/frontend/src/mocks/fixtures/votingSongs.json b/frontend/src/mocks/fixtures/votingSongs.json new file mode 100644 index 000000000..14975c6b6 --- /dev/null +++ b/frontend/src/mocks/fixtures/votingSongs.json @@ -0,0 +1,42 @@ +[ + { + "id": 1, + "title": "HYPE BBBBBBBBBBBBBBOOOOOOYYYYYYYY", + "singer": "명탐정 코난1", + "videoLength": 181, + "songVideoUrl": "https://youtu.be/UUSbUBYqU_8", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize" + }, + { + "id": 2, + "title": "아침에 눈을 뜨면...2", + "singer": "명탐정 코난2", + "videoLength": 182, + "songVideoUrl": "https://youtu.be/UUSbUBYqU_8", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize" + }, + { + "id": 3, + "title": "아침에 눈을 뜨면...3", + "singer": "명탐정 코난3", + "videoLength": 183, + "songVideoUrl": "https://youtu.be/UUSbUBYqU_8", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize" + }, + { + "id": 4, + "title": "아침에 눈을 뜨면...4", + "singer": "명탐정 코난4", + "videoLength": 184, + "songVideoUrl": "https://youtu.be/UUSbUBYqU_8", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize" + }, + { + "id": 5, + "title": "아침에 눈을 뜨면...5", + "singer": "명탐정 코난5", + "videoLength": 185, + "songVideoUrl": "https://youtu.be/UUSbUBYqU_8", + "albumCoverUrl": "https://cdnimg.melon.co.kr/cm2/album/images/112/81/456/11281456_20230706180841_500.jpg/melon/resize/120/quality/80/optimize" + } +] diff --git a/frontend/src/mocks/handlers/songsHandlers.ts b/frontend/src/mocks/handlers/songsHandlers.ts index 161c73e7e..a48392420 100644 --- a/frontend/src/mocks/handlers/songsHandlers.ts +++ b/frontend/src/mocks/handlers/songsHandlers.ts @@ -1,12 +1,13 @@ import { rest } from 'msw'; import popularSongs from '../fixtures/popularSongs.json'; import songs from '../fixtures/songs.json'; +import votingSongs from '../fixtures/votingSongs.json'; import type { KillingPartPostRequest } from '@/shared/types/killingPart'; const { BASE_URL } = process.env; export const songsHandlers = [ - rest.get(`${BASE_URL}/songs/high-voted`, (req, res, ctx) => { + rest.get(`${BASE_URL}/songs/high-liked`, (req, res, ctx) => { return res(ctx.json(popularSongs)); }), @@ -61,4 +62,8 @@ export const songsHandlers = [ return res(ctx.status(200), ctx.json(response)); }), + + rest.get(`${BASE_URL}/voting-songs`, (req, res, ctx) => { + return res(ctx.json(votingSongs)); + }), ]; diff --git a/frontend/src/pages/AuthPage.tsx b/frontend/src/pages/AuthPage.tsx new file mode 100644 index 000000000..b659015ac --- /dev/null +++ b/frontend/src/pages/AuthPage.tsx @@ -0,0 +1,41 @@ +import { useEffect } from 'react'; +import { Navigate, useSearchParams } from 'react-router-dom'; +import { useAuthContext } from '@/features/auth/components/AuthProvider'; +import googleAuthUrl from '@/features/auth/constants/googleAuthUrl'; + +interface AccessTokenResponse { + accessToken: string; +} + +const AuthPage = () => { + const [searchParams] = useSearchParams(); + const { login } = useAuthContext(); + + // TODO: 예외처리 + const getAccessToken = async () => { + const code = searchParams.get('code'); + if (!code) { + localStorage.removeItem('userToken'); + window.location.href = googleAuthUrl; + return; + } + + const response = await fetch(`${process.env.BASE_URL}/login/google?code=${code}`, { + method: 'get', + credentials: 'include', + }); + + const data = (await response.json()) as AccessTokenResponse; + const { accessToken } = data; + + login(accessToken); + }; + + useEffect(() => { + getAccessToken(); + }, []); + + return ; +}; + +export default AuthPage; diff --git a/frontend/src/pages/MainPage.tsx b/frontend/src/pages/MainPage.tsx index 18f8096a6..16c64c33c 100644 --- a/frontend/src/pages/MainPage.tsx +++ b/frontend/src/pages/MainPage.tsx @@ -1,43 +1,47 @@ import { Link } from 'react-router-dom'; import { styled } from 'styled-components'; +import CarouselItem from '@/features/songs/components/CarouselItem'; +import CollectionCarousel from '@/features/songs/components/CollectionCarousel'; import PopularSongItem from '@/features/songs/components/PopularSongItem'; import Spacing from '@/shared/components/Spacing'; import SRHeading from '@/shared/components/SRHeading'; import ROUTE_PATH from '@/shared/constants/path'; import useFetch from '@/shared/hooks/useFetch'; import fetcher from '@/shared/remotes'; - -interface PopularSong { - id: number; - title: string; - singer: string; - albumCoverUrl: string; - totalVoteCount: number; -} +import type { PopularSong, VotingSong } from '@/features/songs/types/Song.type'; const MainPage = () => { - const { data: popularSongs } = useFetch(() => fetcher('/songs/high-voted', 'GET')); + const { data: popularSongs } = useFetch(() => fetcher('/songs/high-liked', 'GET')); + const { data: votingSongs } = useFetch(() => fetcher('/voting-songs', 'GET')); - if (!popularSongs) return null; + if (!popularSongs || !votingSongs) return null; return ( <> shook 메인 페이지 - 킬링파트 투표 많은순 + 현재 수집중인 노래 + + + {votingSongs.map((votingSong) => { + return ; + })} + + 킬링파트 좋아요 많은순 + - {popularSongs.map(({ id, albumCoverUrl, title, singer, totalVoteCount }, i) => ( + {popularSongs.map(({ id, albumCoverUrl, title, singer, totalLikeCount }, i) => (
  • diff --git a/frontend/src/pages/MyPage.tsx b/frontend/src/pages/MyPage.tsx new file mode 100644 index 000000000..c90711eac --- /dev/null +++ b/frontend/src/pages/MyPage.tsx @@ -0,0 +1,8 @@ +import { useParams } from 'react-router-dom'; + +const MyPage = () => { + const { id: memberId } = useParams(); + return <>MyPage: {memberId}; +}; + +export default MyPage; diff --git a/frontend/src/pages/PartCollectingPage.tsx b/frontend/src/pages/PartCollectingPage.tsx index 39da67434..3e9b50d90 100644 --- a/frontend/src/pages/PartCollectingPage.tsx +++ b/frontend/src/pages/PartCollectingPage.tsx @@ -3,23 +3,24 @@ import { styled } from 'styled-components'; import Thumbnail from '@/features/songs/components/Thumbnail'; import VoteInterface from '@/features/songs/components/VoteInterface'; import { VoteInterfaceProvider } from '@/features/songs/components/VoteInterfaceProvider'; -import { useGetSongDetail } from '@/features/songs/remotes/useGetSongDetail'; import { VideoPlayerProvider } from '@/features/youtube/components/VideoPlayerProvider'; import Youtube from '@/features/youtube/components/Youtube'; +import useFetch from '@/shared/hooks/useFetch'; +import fetcher from '@/shared/remotes'; +import type { VotingSongList } from '@/shared/types/song'; const PartCollectingPage = () => { - const { id: songIdParam } = useParams(); - const { songDetail } = useGetSongDetail(Number(songIdParam)); + const { id: songId } = useParams(); + const { data: votingSongs } = useFetch(() => + fetcher(`/voting-songs/${songId}`, 'GET') + ); - if (!songDetail) return; - const { id, title, singer, videoLength, songVideoUrl, albumCoverUrl } = songDetail; - // TODO: videoId 자체가 응답값으로 오도록 API 협의 - // TODO: Jacket img src API 추가 협의 - const videoId = songVideoUrl.replace('https://youtu.be/', ''); + if (!votingSongs) return; + const { id, title, singer, videoLength, songVideoId, albumCoverUrl } = votingSongs.currentSong; return ( - 킬링파트 투표 🔖 + 킬링파트 수집 @@ -28,8 +29,8 @@ const PartCollectingPage = () => { - - + + diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index f87615f25..00732e941 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -1,8 +1,11 @@ import { createBrowserRouter } from 'react-router-dom'; import { VideoPlayerProvider } from './features/youtube/components/VideoPlayerProvider'; +import AuthPage from './pages/AuthPage'; import MainPage from './pages/MainPage'; +import MyPage from './pages/MyPage'; import PartCollectingPage from './pages/PartCollectingPage'; import SongDetailPage from './pages/SongDetailPage'; +import AuthLayout from './shared/components/Layout/AuthLayout'; import Layout from './shared/components/Layout/Layout'; import ROUTE_PATH from './shared/constants/path'; @@ -27,8 +30,20 @@ const router = createBrowserRouter([ ), }, + { + path: `${ROUTE_PATH.MY_PAGE}/:id`, + element: ( + + + + ), + }, ], }, + { + path: `${ROUTE_PATH.LOGIN_REDIRECT}`, + element: , + }, ]); export default router; diff --git a/frontend/src/shared/components/Avatar.tsx b/frontend/src/shared/components/Avatar.tsx new file mode 100644 index 000000000..f3a376451 --- /dev/null +++ b/frontend/src/shared/components/Avatar.tsx @@ -0,0 +1,17 @@ +import { styled } from 'styled-components'; +import type { ImgHTMLAttributes } from 'react'; + +interface AvatarProps extends ImgHTMLAttributes {} + +const Avatar = ({ src, alt = '', ...props }: AvatarProps) => { + return {alt}; +}; + +export default Avatar; + +const Img = styled.img` + width: 40px; + height: 40px; + + border-radius: 50%; +`; diff --git a/frontend/src/shared/components/Layout/AuthLayout.tsx b/frontend/src/shared/components/Layout/AuthLayout.tsx new file mode 100644 index 000000000..bfc4788de --- /dev/null +++ b/frontend/src/shared/components/Layout/AuthLayout.tsx @@ -0,0 +1,37 @@ +import { useParams } from 'react-router-dom'; +import googleAuthUrl from '@/features/auth/constants/googleAuthUrl'; +import parseJWT from '@/features/auth/utils/parseJWT'; + +const isValidToken = (accessToken: string, id: number) => { + if (!accessToken) return false; + + const { memberId, nickname } = parseJWT(accessToken); + + // TODO: memberId와 url param Id 가 다를 때 다르게 처리해줄 것. + if ( + !memberId || + !nickname || + typeof memberId !== 'number' || + typeof nickname !== 'string' || + memberId !== id + ) { + return false; + } + + return true; +}; + +const AuthLayout = ({ children }: { children: React.ReactElement }) => { + const accessToken = localStorage.getItem('userToken'); + const { id } = useParams(); + + if (!accessToken || !isValidToken(accessToken, Number(id))) { + localStorage.removeItem('userToken'); + window.location.href = `${googleAuthUrl}`; + return; + } + + return <>{children}; +}; + +export default AuthLayout; diff --git a/frontend/src/shared/components/Layout/Header.tsx b/frontend/src/shared/components/Layout/Header.tsx index 122ccaeee..18d629b61 100644 --- a/frontend/src/shared/components/Layout/Header.tsx +++ b/frontend/src/shared/components/Layout/Header.tsx @@ -1,14 +1,30 @@ import { Link } from 'react-router-dom'; import { styled } from 'styled-components'; +import defaultAvatar from '@/assets/icon/avatar-default.svg'; import logo from '@/assets/icon/shook-logo.svg'; +import shookshook from '@/assets/icon/shookshook.svg'; +import { useAuthContext } from '@/features/auth/components/AuthProvider'; +import googleAuthUrl from '@/features/auth/constants/googleAuthUrl'; import ROUTE_PATH from '@/shared/constants/path'; +import Avatar from '../Avatar'; const Header = () => { + const { user } = useAuthContext(); + return ( ); }; @@ -18,6 +34,7 @@ export default Header; const Container = styled.header` display: flex; align-items: center; + justify-content: space-between; width: 100%; height: ${({ theme }) => theme.headerHeight.desktop}; @@ -48,3 +65,8 @@ const Logo = styled.img` height: 40px; } `; + +const ProfileAvatar = styled(Avatar)` + width: 28px; + height: 28px; +`; diff --git a/frontend/src/shared/constants/path.ts b/frontend/src/shared/constants/path.ts index 822bbf340..c0dcc63be 100644 --- a/frontend/src/shared/constants/path.ts +++ b/frontend/src/shared/constants/path.ts @@ -2,6 +2,8 @@ const ROUTE_PATH = { ROOT: '/', COLLECT: 'collect', SONG_DETAIL: 'songs', + LOGIN_REDIRECT: '/login/redirect', + MY_PAGE: 'my-page', } as const; export default ROUTE_PATH; diff --git a/frontend/src/shared/remotes/index.ts b/frontend/src/shared/remotes/index.ts index 86f1c709c..b1168aa42 100644 --- a/frontend/src/shared/remotes/index.ts +++ b/frontend/src/shared/remotes/index.ts @@ -1,3 +1,5 @@ +import googleAuthUrl from '@/features/auth/constants/googleAuthUrl'; + export interface ErrorResponse { message: string; } @@ -5,11 +7,19 @@ export interface ErrorResponse { const { BASE_URL } = process.env; const fetcher = async (url: string, method: string, body?: unknown): Promise => { + const accessToken = localStorage.getItem('userToken'); + + const headers: Record = { + 'Content-type': 'application/json', + }; + + if (accessToken) { + headers['Authorization'] = `Bearer ${accessToken}`; + } + const options: RequestInit = { method, - headers: { - 'Content-type': 'application/json', - }, + headers, }; if (body) { @@ -22,6 +32,11 @@ const fetcher = async (url: string, method: string, body?: unknown): Promise< throw new Error(`서버문제로 HTTP 통신에 실패했습니다.`); } + if (response.status === 401) { + localStorage.removeItem('userToken'); + window.location.href = googleAuthUrl; + } + if (!response.ok) { const errorResponse: ErrorResponse = await response.json(); diff --git a/frontend/src/shared/types/killingPart.ts b/frontend/src/shared/types/killingPart.ts index 9b8666771..14aced5a6 100644 --- a/frontend/src/shared/types/killingPart.ts +++ b/frontend/src/shared/types/killingPart.ts @@ -6,9 +6,3 @@ export interface KillingPartPostRequest { startSecond: number; length: KillingPartInterval; } - -export interface KillingPartPostResponse { - rank: number; - voteCount: number; - partVideoUrl: PartVideoUrl; -} diff --git a/frontend/src/shared/types/song.ts b/frontend/src/shared/types/song.ts index 7e93e15b0..7363797fd 100644 --- a/frontend/src/shared/types/song.ts +++ b/frontend/src/shared/types/song.ts @@ -1,5 +1,3 @@ -import type { PartVideoUrl } from './killingPart'; - type VideoUrl = `https://www.youtube.com/embed/${string}`; export interface SongDetail { @@ -14,11 +12,27 @@ export interface SongDetail { export interface KillingPart { id: number; - exist: true; rank: number; + likeCount: number; start: number; end: number; - partVideoUrl: PartVideoUrl; - voteCount: number; - likeCount: number; + partVideoUrl: string; + partLength: number; + likeStatus: boolean; +} + +export interface SongVoting { + id: number; + title: string; + singer: string; + videoLength: number; + songVideoId: string; + albumCoverUrl: string; + killingParts: KillingPart[]; +} + +export interface VotingSongList { + prevSongs: SongVoting[]; + currentSong: SongVoting; + nextSongs: SongVoting[]; } From c44a1a76d05b83b44e9d6fd41a0fcf30f2210d49 Mon Sep 17 00:00:00 2001 From: SeokHwan An <70303795+seokhwan-an@users.noreply.github.com> Date: Fri, 18 Aug 2023 02:04:38 +0900 Subject: [PATCH 10/10] =?UTF-8?q?feat:=20=EC=B6=94=EA=B0=80=EB=A1=9C=20?= =?UTF-8?q?=ED=95=84=EC=9A=94=ED=95=9C=20=EB=85=B8=EB=9E=98=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=B6=94=EA=B0=80=20(#316)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/main/resources/dev/data.sql | 471 ++++++++++++++++++++++++ 1 file changed, 471 insertions(+) diff --git a/backend/src/main/resources/dev/data.sql b/backend/src/main/resources/dev/data.sql index fbf4fbccd..57a61a2b4 100644 --- a/backend/src/main/resources/dev/data.sql +++ b/backend/src/main/resources/dev/data.sql @@ -108,3 +108,474 @@ INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (10, 'STANDARD', 10, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 'LONG', 10, 3, now()); + + +INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) +VALUES ('Ling Ling', '검정치마', 230, 'gjQwwWjxPaQ', + 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/082/976/703/82976703_1663118461097_1_600x600.JPG/dims/resize/Q_80,0', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 11, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 11, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 11, 3, now()); + +INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) +VALUES ('고백', '델리 스파이스 (Deli Spice)', 323, 'BYyVDi8BpZw', + 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/015/027/552/15027552_1368610256849_1_600x600.JPG/dims/resize/Q_80,0', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 12, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 12, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 12, 3, now()); + +INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) +VALUES ('Polaroid', 'ENHYPEN', 184, 'vRdZVDWs3BI', + 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/082/472/258/82472258_1641790812739_1_600x600.JPG/dims/resize/Q_80,0', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 13, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 13, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 13, 3, now()); + +INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) +VALUES ('사랑앓이', 'FTISLAND', 218, 'gnLwCb8Cz7I', + 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/049/974/430/49974430_1317964170310_1_600x600.JPG/dims/resize/Q_80,0', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 14, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 14, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 14, 3, now()); + +INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) +VALUES ('맞네', 'LUCY', 276, 'BRs0GGCT4bU', + 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/082/427/599/82427599_1638854125897_1_600x600.JPG/dims/resize/Q_80,0', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 15, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 15, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 15, 3, now()); + +INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) +VALUES ('Madeleine Love', 'CHEEZE (치즈)', 218, 'EHTagN5HJKQ', + 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/080/580/341/80580341_1431423374354_1_600x600.JPG/dims/resize/Q_80,0', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 16, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 16, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 16, 3, now()); + +INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) +VALUES ('26', '윤하 (YOUNHA)', 199, 'eUqwF1-jjwQ', + 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/341/257/81341257_1578293989428_1_600x600.JPG/dims/resize/Q_80,0', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 17, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 17, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 17, 3, now()); + +INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) +VALUES ('하늘 위로', 'IZ*ONE (아이즈원)', 192, 'P1jdwGsV4lk', + 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 18, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 18, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 18, 3, now()); + +INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) +VALUES ('Super Shy', 'NewJeans', 200, 'ArmDp-zijuc', + 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 19, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 19, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 19, 3, now()); + +INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) +VALUES ('Seven (feat. Latto) - Clean Ver.', '정국', 186, 'UUSbUBYqU_8', + 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 20, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 20, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 20, 3, now()); + +INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) +VALUES ('퀸카 (Queencard)', '(여자)아이들', 162, 'VOcb6ZHxSjc', + 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 21, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 21, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 21, 3, now()); + +INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) +VALUES ('헤어지자 말해요', '박재정', 244, 'SrQzxD8UFdM', + 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 22, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 22, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 22, 3, now()); + +INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) +VALUES ('I AM', 'IVE (아이브)', 208, 'cU0JrSAyy7o', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 23, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 23, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 23, 3, now()); + +INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) +VALUES ('이브, 프시케 그리고 푸른 수염의 아내', 'LE SSERAFIM (르세라핌)', 186, + 'Ii8L0qEvfC8', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', + now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 24, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 24, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 24, 3, now()); + +INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) +VALUES ('Spicy', 'aespa', 198, '1kfmWl3o8TE', + 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 25, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 25, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 25, 3, now()); + +INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) +VALUES ('Steal The Show (From "엘리멘탈")', 'Lauv', 194, 'kUMds6XKtfY', + 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 26, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 26, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 26, 3, now()); + +INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) +VALUES ('사랑은 늘 도망가', '임영웅', 273, 'pBEAzM2TRmE', + 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 27, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 27, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 27, 3, now()); + +INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) +VALUES ('ISTJ', 'NCT DREAM', 186, 'es60T3k-tyM', + 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 28, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 28, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 28, 3, now()); + +INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) +VALUES ('모래 알갱이', '임영웅', 221, '3_wOZrzmQ1o', + 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 29, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 29, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 29, 3, now()); + +INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) +VALUES ('UNFORGIVEN (feat. Nile Rodgers)', 'LE SSERAFIM (르세라핌)', 181, + 'fzSDGXyGTjg', + 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 30, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 30, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 30, 3, now()); + +INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) +VALUES ('Kitsch', 'IVE (아이브)', 195, 'r572qh2__-U', + 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 31, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 31, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 31, 3, now()); + +INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) +VALUES ('우리들의 블루스', '임영웅', 207, 'epz-aL5RaLQ', + 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 32, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 32, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 32, 3, now()); + +INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) +VALUES ('Candy', 'NCT DREAM', 220, 'QuaVFoBLQeg', + 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 33, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 33, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 33, 3, now()); + +INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) +VALUES ('Hype boy', 'NewJeans', 180, 'T--6HBX2K4g', + 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 34, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 34, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 34, 3, now()); + +INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) +VALUES ('다시 만날 수 있을까', '임영웅', 275, 'VPDRLgfqfSs', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 35, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 35, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 35, 3, now()); + +INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) +VALUES ('Broken Melodies', 'NCT DREAM', 227, 'EPsh2192sTU', + 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 36, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 36, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 36, 3, now()); + +INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) +VALUES ('Still With You', '정국', 239, 'BksBNbTIoPE', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 37, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 37, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 37, 3, now()); + +INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) +VALUES ('무지개', '임영웅', 198, 'o8e0Qd2H1qc', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 38, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 38, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 38, 3, now()); + +INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) +VALUES ('Ditto', 'NewJeans', 187, 'haCpjUXIhrI', + 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 39, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 39, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 39, 3, now()); + +INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) +VALUES ('London Boy', '임영웅', 289, 'ZRDuScdwEbE', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 40, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 40, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 40, 3, now()); + +INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) +VALUES ('이제 나만 믿어요', '임영웅', 274, 'y1KXYmMuZZA', + 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 41, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 41, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 41, 3, now()); + +INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) +VALUES ('아버지', '임영웅', 240, 'dbaiMJOnaB4', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 42, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 42, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 42, 3, now()); + +INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) +VALUES ('Polaroid', '임영웅', 209, 'PVDxs6GUXSI', + 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 43, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 43, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 43, 3, now()); + +INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) +VALUES ('Dynamite', '방탄소년단', 198, 'KhZ5DCd7m6s', + 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 44, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 44, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 44, 3, now()); + +INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) +VALUES ('손오공', '세븐틴 (SEVENTEEN)', 200, 'tFPbzfU5XL4', + 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 45, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 45, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 45, 3, now()); + +INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) +VALUES ('인생찬가', '임영웅', 235, 'cXHduPVrcDQ', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 46, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 46, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 46, 3, now()); + +INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) +VALUES ('A bientot', '임영웅', 258, 'sZDDLUB8wQE', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 47, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 47, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 47, 3, now()); + +INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) +VALUES ('손이 참 곱던 그대', '임영웅', 197, 'OpZIaI-J0uk', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 48, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 48, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 48, 3, now()); + +INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) +VALUES ('사랑해 진짜', '임영웅', 241, 'qkledxNCNfY', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 49, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 49, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 49, 3, now()); + +INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) +VALUES ('꽃', '지수', 174, '6zM48_rBFbY', + 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 50, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 50, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 50, 3, now()); + +INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) +VALUES ('연애편지', '임영웅', 217, 'gSQFZvUuQ3s', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 51, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 51, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 51, 3, now()); + +INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) +VALUES ('New jeans', 'New jeans', 109, 'G8GEpK7YDl4', + 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 52, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 52, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 52, 3, now()); + +INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) +VALUES ('OMG', 'New jeans', 215, 'jT0Lh-N3TSg', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 53, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 53, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 53, 3, now()); + +INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) +VALUES ('Butter', '방탄소년단', 165, 'Uz0PppyT7Cc', + 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 54, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 54, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 54, 3, now()); + +INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) +VALUES ('파랑 (Blue Wave)', 'NCT DREAM', 191, 'ZkLK4hUqqas', + 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 55, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 55, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 55, 3, now()); + +INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) +VALUES ('Take Two', '방탄소년단', 230, '3UE-vpej_VI', + 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 56, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 56, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 56, 3, now()); + +INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) +VALUES ('Like We Just Met', 'NCT DREAM', 210, 'eA9pwL-8wJw', + 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 57, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 57, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 57, 3, now()); + +INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at) +VALUES ('Yogurt Shake', 'NCT DREAM', 218, 'IUs7tOzHVJw', + 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (5, 'SHORT', 58, 10, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (10, 'STANDARD', 58, 5, now()); +INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) +VALUES (100, 'LONG', 58, 3, now());