[알림 기능 찾아 삼만리 Day 6 링크]
더보기
package com.project.cheerha.domain.notice.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Entity
@Getter
@NoArgsConstructor
@Table(
name = "email_job_opening_mapping",
uniqueConstraints = {
@UniqueConstraint(columnNames = {"email", "jobOpeningUrl"})
})
public class EmailJobOpeningMapping {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String email;
@Column(nullable = false)
private String jobOpeningUrl;
/**
* 이메일과 채용 공고 URL을 받아 EmailJobOpeningMapping 객체 생성
* @param email 사용자의 이메일 주소
* @param jobOpeningUrl 채용 공고 URL
* @return 생성된 EmailJobOpeningMapping 객체
*/
public static EmailJobOpeningMapping toEntity(
String email,
String jobOpeningUrl
) {
EmailJobOpeningMapping emailJobOpeningMapping = new EmailJobOpeningMapping();
emailJobOpeningMapping.email = email;
emailJobOpeningMapping.jobOpeningUrl = jobOpeningUrl;
return emailJobOpeningMapping;
}
}
더보기
package com.project.cheerha.domain.notice.repository;
import com.project.cheerha.domain.notice.entity.EmailJobOpeningMapping;
import org.springframework.data.jpa.repository.JpaRepository;
public interface EmailJobOpeningMappingRepository extends JpaRepository<EmailJobOpeningMapping, Long> {
}
더보기
package com.project.cheerha.domain.notice.repository;
import com.project.cheerha.domain.notice.UserDto;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Map;
public interface EmailRepositoryQuery {
/**
* 조회 시간으로 모든 채용 공고 정보를 조회
*
* @param referenceTime 조회 시간
* @return 채용 공고 키워드와 연관된 URL 목록을 매핑한 맵
* Key: KeywordId (키워드 식별자)
* Value: 해당 키워드로 매칭되는 URL 목록
*/
Map<Long, List<String>> findAllJobOpeningKeywords(ZonedDateTime referenceTime);
/**
* 모든 사용자의 이메일 및 등록한 키워드를 조회
* @return 사용자 이메일과 선택한 키워드를 포함한 UserDto 리스트
*/
List<UserDto> findAllUserKeywords();
}
package com.project.cheerha.domain.notice.repository;
import static com.querydsl.core.group.GroupBy.groupBy;
import static com.querydsl.core.group.GroupBy.list;
import static com.project.cheerha.domain.keyword.entity.QUserKeyword.*;
import static com.project.cheerha.domain.user.entity.QUser.*;
import static com.project.cheerha.domain.jobopening.entity.QJobOpening.*;
import static com.project.cheerha.domain.keyword.entity.QJobOpeningKeyword.*;
import com.project.cheerha.domain.notice.QUserDto;
import com.project.cheerha.domain.notice.UserDto;
import com.querydsl.jpa.impl.JPAQueryFactory;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
@Repository
@RequiredArgsConstructor
public class EmailRepositoryQueryImpl implements EmailRepositoryQuery {
private final JPAQueryFactory queryFactory;
@Override
public Map<Long, List<String>> findAllJobOpeningKeywords(
ZonedDateTime referenceTime
) {
return queryFactory
.from(jobOpeningKeyword)
.join(jobOpeningKeyword.jobOpening, jobOpening)
.where(jobOpening.createdAt.after(referenceTime))
// 조회 시간 이후 생성된 채용 공고
.transform(
groupBy(jobOpeningKeyword.keyword.id)
// 키워드 ID별로 그룹화
.as(list(jobOpening.jobOpeningUrl))
// URL 목록을 그룹에 매핑
);
}
@Override
public List<UserDto> findAllUserKeywords() {
return queryFactory
.select(
new QUserDto(
userKeyword.keyword.id,
// 사용자가 선택한 키워드 ID
user.email
// 사용자 이메일
)
).from(userKeyword)
.join(userKeyword.user, user)
// 사용자와 키워드 조인
.fetch();
}
}
더보기
package com.project.cheerha.domain.notice.service;
import com.project.cheerha.domain.notice.UserDto;
import com.project.cheerha.domain.notice.repository.EmailRepositoryQuery;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class DataFetchService {
private final EmailRepositoryQuery repositoryQuery;
public Map<Long, List<String>> findKeywordIdToUrlList(ZonedDateTime referenceTime) {
return repositoryQuery.findAllJobOpeningKeywords(referenceTime);
}
public List<UserDto> findUserDtoList() {
return repositoryQuery.findAllUserKeywords();
}
}
더보기
package com.project.cheerha.domain.notice.service;
import com.project.cheerha.domain.notice.UserDto;
import com.project.cheerha.domain.notice.entity.EmailJobOpeningMapping;
import com.project.cheerha.domain.notice.repository.EmailJobOpeningMappingRepository;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class MappingService {
private final EmailJobOpeningMappingRepository mappingRepository;
/**
* 이메일과 채용 공고 URL을 매핑하여 저장
*
* @param userDtoList 사용자 정보 리스트
* @param jobOpeningKeywordMap 채용 공고 URL 목록을 키워드 ID로 매핑한 맵
*/
@Transactional
public void saveEmailJobOpeningMappings(
List<UserDto> userDtoList,
Map<Long, List<String>> jobOpeningKeywordMap
) {
// 이메일과 채용 공고 URL을 매핑할 맵
// key: 사용자 이메일
// value: 채용 공고 URL Set
Map<String, Set<String>> emailUrlMap = new HashMap<>();
// (1) 사용자 정보 리스트 순회
// (2) 사용자 이메일과 매칭된 채용 공고 URL 수집
for (UserDto dto : userDtoList) {
List<String> matchingUrlList = jobOpeningKeywordMap.get(dto.keywordId());
// 매칭된 URL이 존재하면 해당 이메일에 URL 추가
if (matchingUrlList != null && !matchingUrlList.isEmpty()) {
emailUrlMap.computeIfAbsent(
dto.email(), // 이메일을 key로 사용
email -> new HashSet<>()
).addAll(matchingUrlList); // URL 목록을 Set에 추가
}
}
// 이메일-채용 공고 URL 매핑 저장
emailUrlMap.forEach((email, urlSet) -> {
for (String jobOpeningUrl : urlSet) {
// EmailJobOpeningMapping 객체 생성
EmailJobOpeningMapping mapping = EmailJobOpeningMapping.toEntity(email, jobOpeningUrl);
// 데이터베이스에 저장
mappingRepository.save(mapping);
}
});
}
}
머릿속에 떠오른 대로 묘사 실력이 따라오지 못할 때 글 쓰다가 스트레스를 엄청나게 받듯이, 머릿속으로 그린 대로 로직(logic)을 막 분리하다가 하루가 끝났다. '처음부터 로직을 여러 개로 나눌걸' 후회가 밀려오면서도 어떻게든 알림 기능을 구현해 놓아서 그나마 구조를 개선하는 데 더 집중할 수 있나, 온갖 생각이 마음속을 헤집어서 하루 종일 더 큰 부담감에 짓눌렸다. 내일은 꼭 실시간 알림 기능까지 어떻게든 구현하고 싶다.
할 수 있겠지, 나?
Day 10에서 계속…….