Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
6809405
feat: 프로필 조회 관련 API 뼈대 작성
sapiens2000 Apr 1, 2025
7e33d98
Merge branch 'feature/login' into feature/profile
sapiens2000 Apr 1, 2025
be68bb9
feat: 닉네임 중복 검사 API
sapiens2000 Apr 1, 2025
60c9f6f
Merge branch 'develop' into feature/profile
sapiens2000 Apr 2, 2025
b2ac0c4
feat: 프로필 업데이트 dto 추가
sapiens2000 Apr 2, 2025
e9d1237
refactor: 팔로우 엔티티 칼럼명 변경
sapiens2000 Apr 2, 2025
fb4bad3
refactor: UserFixture User 엔티티와 동기화
sapiens2000 Apr 2, 2025
660f0f1
feat: 프로필 임시 코드 및 팔로우를 위한 id 추출 함수추가
sapiens2000 Apr 2, 2025
99881e7
refactor: dto 대신 of 함수 엔티티에 탑제
sapiens2000 Apr 2, 2025
083b793
feat: Follow 예외 정의
sapiens2000 Apr 2, 2025
7b0a907
feat: Follow 생성, 취소 구현
sapiens2000 Apr 2, 2025
c23360e
test: 팔로우 테스트(서비스 - DB) 구현
sapiens2000 Apr 2, 2025
60b4f5d
feat: 팔로워 수, 팔로잉 수 조회 간단 구현
sapiens2000 Apr 2, 2025
605c365
test: follow 테스트 수정
sapiens2000 Apr 2, 2025
b764d3b
refactor: isPremium 칼럼에 Builder.Default 추가
sapiens2000 Apr 2, 2025
736dd53
fix: 테스트 통과 위한 dto와 수반되는 로직 추가
sapiens2000 Apr 2, 2025
f5ea1c2
fix: 테스트 통과 위해 pathvariable 추가
sapiens2000 Apr 2, 2025
911088c
fix: context 캐시 비활성화
sapiens2000 Apr 2, 2025
8d575aa
fix: fixture 캐싱 안되게 변경
sapiens2000 Apr 2, 2025
50543e4
fix: DirtiesContext 추가해서 다시 테스트
sapiens2000 Apr 2, 2025
6a4517f
fix: fixture 사용안하고 테스트
sapiens2000 Apr 3, 2025
753664a
fix: db 연결 확인 테스트 추가
sapiens2000 Apr 3, 2025
e978a93
chore: mysql 컨테이너 기다리게 수정
sapiens2000 Apr 3, 2025
68f4539
chore: dev db 변수 수정
sapiens2000 Apr 3, 2025
fee21c4
chore: 빌드 시 정보 표기
sapiens2000 Apr 3, 2025
05b3aa5
chore: 빌드 파일 수정
sapiens2000 Apr 3, 2025
ab453ec
chore: 빌드 파일 수정
sapiens2000 Apr 3, 2025
c66c537
chore: 빌드 파일 수정
sapiens2000 Apr 3, 2025
0f82308
chore: 빌드 파일 수정
sapiens2000 Apr 3, 2025
9ad037f
chore: 빌드 파일 수정
sapiens2000 Apr 3, 2025
a4ea8da
refactor: 팔로우 테스트 픽스쳐 활용
sapiens2000 Apr 3, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 37 additions & 9 deletions .github/workflows/dev-build.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: log4u-dev-build
name: dev-build
on:
pull_request:
branches:
Expand All @@ -8,6 +8,7 @@ on:
jobs:
build:
name: Build and analyze
environment: develop-test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -40,25 +41,52 @@ jobs:
docker run --name log4u-mysql \
-e MYSQL_ROOT_PASSWORD=root \
-e MYSQL_DATABASE=log4u \
-e MYSQL_USER=dev \
-e MYSQL_PASSWORD=devcos4-team08 \
-e MYSQL_USER=${{ secrets.DB_USERNAME }} \
-e MYSQL_PASSWORD=${{ secrets.DB_PASSWORD}} \
-d \
-p 3307:3306 \
mysql:8.0.33

- name: Build and analyze
- name: Wait for MySQL to be ready
run: |
for i in {1..10}; do
if docker exec log4u-mysql mysqladmin ping -h "127.0.0.1" --silent; then
echo "MySQL is ready!"
break
fi
echo "Waiting for MySQL to start..."
sleep 1
done
if ! docker exec log4u-mysql mysqladmin ping -h "127.0.0.1" --silent; then
echo "MySQL did not start in time!"
exit 1
fi

- name: Test and analyze
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
#GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
DB_URL: jdbc:mysql://localhost:3307/log4u
DB_USERNAME: dev
DB_PASSWORD: devcos4-team08
DB_URL: ${{ secrets.DB_URL }}
DB_USERNAME: ${{ secrets.DB_USERNAME }}
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
NAVER_DEV_CLIENT_ID: ${{ secrets.NAVER_DEV_CLIENT_ID }}
NAVER_DEV_CLIENT_SECRET: ${{ secrets.NAVER_DEV_CLIENT_SECRET }}
NAVER_DEV_REDIRECT_URI: ${{ secrets.NAVER_DEV_REDIRECT_URI }}
GOOGLE_DEV_CLIENT_ID: ${{ secrets.GOOGLE_DEV_CLIENT_ID }}
GOOGLE_DEV_CLIENT_SECRET: ${{ secrets.GOOGLE_DEV_CLIENT_SECRET }}r
GOOGLE_DEV_REDIRECT_URI: ${{ secrets.GOOGLE_DEV_REDIRECT_URI }}
KAKAO_DEV_CLIENT_ID: ${{ secrets.KAKAO_DEV_CLIENT_ID }}
KAKAO_DEV_CLIENT_SECRET: ${{ secrets.KAKAO_DEV_CLIENT_SECRET }}
KAKAO_DEV_REDIRECT_URI: ${{ secrets.KAKAO_DEV_REDIRECT_URI }}
JWT_SECRET: ${{ secrets.JWT_SECRET }}
JWT_ACCESS_TOKEN_EXPIRE_TIME_SECONDS: ${{ secrets.JWT_ACCESS_TOKEN_EXPIRE_TIME_SECONDS }}
JWT_REFRESH_TOKEN_EXPIRE_TIME_SECONDS: ${{ secrets.JWT_ACCESS_TOKEN_EXPIRE_TIME_SECONDS }}

# dev 프로필 사용
run: |
chmod +x ./gradlew
# 소나클라우드 임시 비활성화 ./gradlew build jacocoTestReport sonar --info -Pprofile=dev -Dsonar.branch.name=${{ github.ref_name }}
./gradlew build jacocoTestReport -Pprofile=dev
./gradlew build -i jacocoTestReport -Pprofile=dev

- name: Docker MySQL 종료 및 제거
run: |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ private List<VisibilityType> determineAccessibleVisibilities(Long userId, Long t
return List.of(VisibilityType.PUBLIC, VisibilityType.PRIVATE, VisibilityType.FOLLOWER);
}

if (followRepository.existsByFollowerIdAndFollowingId(userId, targetUserId)) {
if (followRepository.existsByInitiatorIdAndTargetId(userId, targetUserId)) {
return List.of(VisibilityType.PUBLIC, VisibilityType.FOLLOWER);
}

Expand All @@ -186,7 +186,7 @@ private void validateDiaryAccess(Diary diary, Long userId) {

if (diary.getVisibility() == VisibilityType.FOLLOWER) {
if (!diary.getUserId().equals(userId)
&& !followRepository.existsByFollowerIdAndFollowingId(userId, diary.getUserId())) {
&& !followRepository.existsByInitiatorIdAndTargetId(userId, diary.getUserId())) {
throw new NotFoundDiaryException();
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.example.log4u.domain.follow.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import com.example.log4u.common.oauth2.dto.CustomOAuth2User;
import com.example.log4u.domain.follow.service.FollowService;

import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;

@Tag(name = "팔로우 API")
@Controller
@RequiredArgsConstructor
@RequestMapping("/users")
public class FollowController {
private final FollowService followService;

@PostMapping("/{nickname}/follow")
public ResponseEntity<Void> createFollow(
@AuthenticationPrincipal CustomOAuth2User customOAuth2User,
@PathVariable("nickname") String nickname
) {
followService.createFollow(customOAuth2User.getUserId(), nickname);
return ResponseEntity.ok().build();
}

@DeleteMapping("/{nickname}/follow")
public ResponseEntity<Void> deleteFollow(
@AuthenticationPrincipal CustomOAuth2User customOAuth2User,
@PathVariable("nickname") String nickname
) {
followService.deleteFollow(customOAuth2User.getUserId(), nickname);
return ResponseEntity.ok().build();
}

}
17 changes: 14 additions & 3 deletions src/main/java/com/example/log4u/domain/follow/entitiy/Follow.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Entity(name = "follow")
@Getter
@Builder
@AllArgsConstructor
Expand All @@ -22,7 +22,18 @@ public class Follow extends BaseEntity {
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private Long followerId;
// 팔로우를 누른 주체
private Long initiatorId;

private Long followingId;
// 팔로우를 받은 대상
private Long targetId;

// dto 넣기 애매해서 엔티티 내부에 static 함수 구현
public static Follow of(Long initiatorId, Long targetId) {
return new Follow(
null,
initiatorId,
targetId
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.example.log4u.domain.follow.exception;

import org.springframework.http.HttpStatus;

import com.example.log4u.common.exception.base.ErrorCode;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum FollowErrorCode implements ErrorCode {
FOLLOW_NOT_FOUND(HttpStatus.NOT_FOUND, "팔로우 정보가 존재하지 않습니다."),
FOLLOWER_NOT_FOUND(HttpStatus.NOT_FOUND, "팔로워 정보가 존재하지 않습니다.");

private final HttpStatus httpStatus;
private final String message;

@Override
public HttpStatus getHttpStatus() {
return httpStatus;
}

@Override
public String getErrorMessage() {
return message;
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.example.log4u.domain.follow.exception;

import com.example.log4u.common.exception.base.ServiceException;

public class FollowNotFoundException extends ServiceException {
public FollowNotFoundException() {
super(FollowErrorCode.FOLLOW_NOT_FOUND);
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
package com.example.log4u.domain.follow.repository;

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

import com.example.log4u.domain.follow.entitiy.Follow;

@Repository
public interface FollowRepository extends JpaRepository<Follow, Long> {
boolean existsByInitiatorIdAndTargetId(Long initiatorId, Long targetId);

boolean existsByFollowerIdAndFollowingId(Long followerId, Long followingId);
void deleteByInitiatorIdAndTargetId(Long initiatorId, Long targetId);

// 기능 구현 초기용, 데이터 쌓이면 개선 필요
Long countByInitiatorId(Long initiatorId);

Long countByTargetId(Long targetId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.example.log4u.domain.follow.service;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.example.log4u.domain.follow.entitiy.Follow;
import com.example.log4u.domain.follow.exception.FollowNotFoundException;
import com.example.log4u.domain.follow.repository.FollowRepository;
import com.example.log4u.domain.user.service.UserService;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class FollowService {
private final FollowRepository followRepository;
private final UserService userService;

@Transactional
public void createFollow(Long initiatorId, String nickname) {
validateTargetUser(nickname);
followRepository.save(Follow.of(
initiatorId,
userService.getUserIdByNickname(nickname)));
}

@Transactional
public void deleteFollow(Long userId, String nickname) {
Long targetId = userService.getUserIdByNickname(nickname);
validateFollow(userId, targetId);
followRepository.deleteByInitiatorIdAndTargetId(userId, targetId);
}

private void validateTargetUser(String nickname) {
// USER NOT FOUND EXCEPTION
userService.getUserByNickname(nickname);
}

private void validateFollow(Long userId, Long targetId) {
if (!followRepository.existsByInitiatorIdAndTargetId(userId, targetId)) {
throw new FollowNotFoundException();
}
}

/**
* 나를 팔로우 하는 사람 수 조회
* */
@Transactional(readOnly = true)
public Long getFollowerCount(Long userId) {
return followRepository.countByTargetId(userId);
}

/**
* 내가 팔로우하는 사람 수 조회
* */
@Transactional(readOnly = true)
public Long getFollowingCount(Long userId) {
return followRepository.countByInitiatorId(userId);
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
package com.example.log4u.domain.user.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.example.log4u.common.oauth2.dto.CustomOAuth2User;
import com.example.log4u.domain.user.dto.NicknameValidationResponseDto;
import com.example.log4u.domain.user.dto.UserProfileResponseDto;
import com.example.log4u.domain.user.dto.UserProfileUpdateRequestDto;
import com.example.log4u.domain.user.service.UserService;

import lombok.extern.slf4j.Slf4j;

Expand All @@ -14,11 +21,54 @@
@Slf4j
public class UserController {

private final UserService userService;

public UserController(UserService userService) {
this.userService = userService;
}

@GetMapping("")
public String modifyUserProfile(
@AuthenticationPrincipal CustomOAuth2User customOAuth2User
) {
log.info("테스트 GET DATA user = " + customOAuth2User.getUserId());
return "test";
}

@GetMapping("/me")
public ResponseEntity<UserProfileResponseDto> getMyProfile(
@AuthenticationPrincipal CustomOAuth2User customOAuth2User
) {
UserProfileResponseDto userProfileResponseDto =
userService.getMyProfile(customOAuth2User.getUserId());
return ResponseEntity.ok(userProfileResponseDto);
}

@GetMapping("/{nickname}")
public ResponseEntity<UserProfileResponseDto> getUserProfile(
@AuthenticationPrincipal CustomOAuth2User customOAuth2User,
@PathVariable String nickname
) {
UserProfileResponseDto userProfileResponseDto =
userService.getUserProfile(nickname);
return ResponseEntity.ok(userProfileResponseDto);
}

@PutMapping("/me")
public ResponseEntity<UserProfileResponseDto> updateMyProfile(
@AuthenticationPrincipal CustomOAuth2User customOAuth2User,
UserProfileUpdateRequestDto userProfileUpdateRequestDto
) {
UserProfileResponseDto userProfileResponseDto =
userService.updateMyProfile(customOAuth2User.getUserId(), userProfileUpdateRequestDto);
return ResponseEntity.ok(userProfileResponseDto);
}

@GetMapping("/validation/{nickname}")
public ResponseEntity<NicknameValidationResponseDto> validateNickname(
@AuthenticationPrincipal CustomOAuth2User customOAuth2User,
@PathVariable String nickname
) {
return ResponseEntity.ok(userService.validateNickname(nickname));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.example.log4u.domain.user.dto;

public record NicknameValidationResponseDto(Boolean available) {
}
Loading