본문 바로가기
Projects/청하-청년을 위한 커뮤니티 서비스

[청하] 30. 정책 찜하기 기능 구현

by Lpromotion 2024. 10. 21.

앞서 정책 API를 호출하여 데이터베이스에 저장하고, 리스트 조회와 상세 조회를 구현했다.

상세 조회 화면에서 찜하기 버튼을 두고, 정책 찜하기 API를 호출할 수 있도록 구현했다.

 

찜하기 기능을 위해 고려해야할 사항

  1. 현재 정책 업데이트 방식
    • 기존 데이터를 모두 삭제하고 외부 API를 호출해 업데이트된 데이터를 새로 저장.
    • 따라서 원본 정책이 사라지면 정책 정보를 불러올 수 없음.
  2. 찜하기 테이블에 정책 정보 저장의 부담
    • 정책 정보를 찜하기 테이블에 저장하기에는 자원이 많이 소모됨.
  3. 외래키 제약 조건 문제
    • 현재 업데이트 방식을 고려하면 찜하기 테이블의 policyId 에 외래키 제약 조건을 설정할 수 없음.

 

해결 방안

  1. 정책 찜하기 테이블
    • 외래키 제약 조건을 설정하기 않고 정책 찜하기 테이블에 정책 id를 저장.
    • isActive 컬럼을 추가. (정책의 활성화 상태를 저장)
    • 비활성화 되는 경우를 대비해 정책 찜하기 호출 시 최소한의 정보(정책 제목)를 함께 저장.
  2. 찜한 정책 조회 로직
    • 사용자가 찜한 모든 정책을 조회하고, 각 정책의 policyId를 정책 테이블에서 조회
    • 사용자가 찜한 정책이 정책 테이블에 존재하는 경우
      • isActive가 false이면 true로 변경 (삭제되었다가 다시 활성화 된 경우)
      • 해당하는 정책 정보를 리턴
    • 사용자가 찜한 정책이 정책 테이블에 존재하지 않는 경우
      • isActive가 true이면 false로 변경
      • 최소한의 정보(정책 찜하기 테이블에 저장된) 리턴

위 방법 사용 시

  • 사용자는 비활성화된 정책도 확인할 수 있고, 어떤 정책이 비활성화 되었는지 알 수 있음.
  • isActive가 false인 정책에 대한 추가적인 안내가 필요. (비활성화 되었다는 메시지 등으로 찜하기 해제 유도?)

 

1. 프로젝트 구조

src/main/java/com/example/withpeace/
│
├── domain/                   # 도메인 모델 (엔티티)
│   ├── YouthPolicy.java      # 청년 정책 엔티티
│   └── FavoritePolicy.java   # 청년 정책 찜하기 엔티티
│
├── repository/                       # 데이터 접근 계층
│   ├── YouthPolicyRepository.java    # 청년 정책 레포지토리
│   └── FavoritePolicyRepository.java # 정책 찜하기 레포지토리
│
├── controller/               # 컨트롤러 계층
│   └── PolicyController.java # 정책 관련 API 엔드포인트
│
└── service/                  # 비즈니스 로직 계층
    └── PolicyService.java    # 정책 관련 비즈니스 로직

 

2. 도메인 엔티티 구현

FavoritePolicy - 정책 찜하기 엔티티 클래스

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@DynamicUpdate
@Table(name = "favorite_policies")
public class FavoritePolicy {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id", nullable = false, unique = true)
    private Long id;

    @Column(name = "policy_id", nullable = false, unique = true)
    private String policyId;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id", nullable = false)
    @OnDelete(action = OnDeleteAction.CASCADE)
    private User user;

    @Column(name = "title", nullable = false)
    private String title;

    @Column(name = "is_active", nullable = false)
    private boolean isActive;

    @Column(name = "create_date", nullable = false)
    private LocalDateTime createDate;

    @Builder
    public FavoritePolicy(String policyId, User user, String title) {
        this.policyId = policyId;
        this.user = user;
        this.title = title;
        this.isActive = true;
        this.createDate = LocalDateTime.now();
    }

    public void setIsActive(boolean isActive) { this.isActive = isActive; }
}

`policyId` 에는 정책 ID 값을 저장하되, 외래키 제약 조건을 설정하지 않았다. 정책 리프레시 기능 때문이다. (주기적으로 정책 데이터를 초기화하고 다시 저장함.)

 

`title` 컬럼을 이용해 정책 제목을 저장한다. 이를 통해 정책이 삭제되더라도 기본적인 정보를 유지할 수 있다.

 

`isActive` 컬럼을 이용해, 이후 구현할 “내가 찜한 정책 조회” 시 해당 정책을 찾을 수 없는 경우 isActive 값을 false로 설정하여 데이터를 우선 보존하고 해당 정책에 대해 추가적인 처리를 할 수 있도록 했다.

 

3. 레포지토리 구현

FavoritePolicyRepository - 정책 찜하기 레포지토리

public interface FavoritePolicyRepository extends JpaRepository<FavoritePolicy, Long> {

    FavoritePolicy findByUserAndPolicyId(User user, String policyId);
}

`findByUserAndPolicyId` 는 사용자와 정책 ID로 찜한 정책을 찾는다.

 

4. 컨트롤러 구현

YouthPolicyController

@RestController
@RequiredArgsConstructor
@RequestMapping("api/v1/policies")
@Slf4j
public class YouthPolicyController {
    private final YouthPolicyService youthPolicyService;
    // ... (기존 코드 생략)
    
    // 정책 찜하기
    @PostMapping("/{policyId}/favorites")
    public ResponseDto<?> registerFavoritePolicy(@UserId Long userId, @PathVariable String policyId) {
        youthPolicyService.favoritePolicy(userId, policyId);
        return ResponseDto.ok(true);
    }

}

`registerFavoritePolicy`는 사용자 ID와 정책 ID를 파라미터로 받아 정책 찜하기 요청을 처리한다.

 

5. 서비스 구현

YouthPolicyService

@Service
@RequiredArgsConstructor
@Slf4j
public class YouthPolicyService {
    // ... (기존 코드 생략)

    @Transactional
    public void favoritePolicy(Long userId, String policyId) {
        User user = getUserById(userId);
        YouthPolicy policy = getPolicyById(policyId);

        try{
            // 찜하기 되어있는지 확인
            FavoritePolicy favoritePolicy = favoritePolicyRepository.findByUserAndPolicyId(user, policyId);
            // 찜하기 되어있지 않은 경우 찜하기 처리 수행
            if(favoritePolicy == null) {
                favoritePolicyRepository.save(FavoritePolicy.builder()
                        .policyId(policy.getId())
                        .user(user)
                        .title(policy.getTitle())
                        .build());
            }
        } catch (Exception e) {
            throw new CommonException(ErrorCode.YOUTH_POLICY_ERROR);
        }
    }

}
  1. 사용자와 정책을 조회한다. (존재 여부 확인)
  2. `findByUserAndPolicyId`를 통해 사용자가 해당 정책을 찜하기 했는지 확인한다.
  3. 찜하기 되어있지 않은 경우에만 새로운 FavoritePolicy 엔티티를 생성하고 저장한다.

안드로이드 팀원의 요청에 따라 찜하기 되어있을 경우에도 true를 반환하도록 구현했다. (Optimistic UI 적용해보기 위함이라고 함.)

 

6. API 응답 예시

요청 URL

[POST] http://cheongha.site/api/v1/policies/R2024080625806/favorites

 

Response Body

{
  "data": true
  "error": null
}
반응형

댓글