1. 지원 기능
- 후보자 관련 기능
- 후보자 등록
- 후보자 정보 수정
- 후보자 정보 삭제
2. Candidate Entity 수정
- Sns, Youtube 엔티티와 양방향 매핑으로 수정
- 추후에 total likes, total comments 등 정보를 제공하는 기능도 만들어 보고 싶어서
- 후보 삭제 시 관련된 항목들까지 함께 삭제해야 함으로, cascade all을 적용
- 생성자 추가
- 기본 생성자로 생성하는 것을 방지하기 위해 AccessLevel.PROTECTED로 설정
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Candidate {
@Id @GeneratedValue
@Column(name = "candidate_id")
private Long id;
private int number;
@Column(name = "candidate_name")
private String name;
@Column(name = "candidate_likes")
private int likes;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "city_id")
private City city;
@OneToMany(mappedBy = "candidate", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Sns> snsList = new ArrayList<>();
@OneToMany(mappedBy = "candidate", cascade = CascadeType.ALL)
private List<Youtube> youtubeList = new ArrayList<>();
@Builder
public Candidate(int number, String name, City city) {
this.number = number;
this.name = name;
this.likes = 0;
this.city = city;
}
public void addSns(Sns sns) {
snsList.add(sns);
sns.setCandidate(this);
}
public void addYoutube(Youtube youtube) {
youtubeList.add(youtube);
youtube.setCandidate(this);
}
}
3. Candidate Repository
@Repository
@RequiredArgsConstructor
public class CandidateRepository {
private final EntityManager em;
public void save(Candidate candidate) {
em.persist(candidate);
}
public void remove(Candidate candidate) {
em.remove(candidate);
}
public Candidate findById(Long id) {
return em.find(Candidate.class, id);
}
public List<Candidate> findAllByCity(City city) {
return em.createQuery("select c from Candidate c where c.city = :city")
.setParameter("city", city)
.getResultList();
}
public List<Candidate> findByCityAndNumber(City city, int number) {
return em.createQuery("select c from Candidate c where c.city = :city and c.number = :number")
.setParameter("city", city)
.setParameter("number", number)
.getResultList();
}
}
4. Candidate Service
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class CandidateService {
private final CityRepository cityRepository;
private final CandidateRepository candidateRepository;
@Transactional
public Long join(CandidateRequestDto candidateRequestDto) {
Long cityId = candidateRequestDto.getCityId();
City city = cityRepository.findOne(cityId);
Candidate candidate = candidateRequestDto.toEntity(city);
validateDuplicateCandidate(candidate);
candidateRepository.save(candidate);
return candidate.getId();
}
@Transactional
public void delete(Long candidateId) {
Candidate candidate = candidateRepository.findById(candidateId);
validateCandidateExistence(candidate);
candidateRepository.remove(candidate);
}
public void validateDuplicateCandidate(Candidate candidate) {
List<Candidate> candidateList = candidateRepository.findByCityAndNumber(candidate.getCity(), candidate.getNumber());
if(!candidateList.isEmpty()) {
throw new IllegalStateException("이미 존재하는 후보 번호입니다");
}
}
public void validateCandidateExistence(Candidate candidate) {
List<Candidate> candidateList = candidateRepository.findByCityAndNumber(candidate.getCity(), candidate.getNumber());
if(candidateList.isEmpty()) {
throw new IllegalStateException("후보가 존재하지 않습니다.");
}
}
}
5. Candidate Dto
RequestDto
@Getter @Setter
public class CandidateRequestDto {
private int number;
private String name;
private Long cityId;
@Builder
public CandidateRequestDto(int number, String name, Long cityId) {
this.number = number;
this.name = name;
this.cityId = cityId;
}
public Candidate toEntity(City city) {
return Candidate.builder()
.number(number)
.name(name)
.city(city)
.build();
}
}
ResponseDto
@Getter @Setter
public class CandidateResponseDto {
private int number;
private String name;
@Builder
public CandidateResponseDto(Candidate candidate) {
this.number = candidate.getNumber();
this.name = candidate.getName();
}
}
6. Candidate Service Test
테스트 요구사항
- 후보 추가
- 후보 삭제
- 한 도시에 같은 번호(number)를 가진 후보가 둘 이상 존재할 수 없다
테스트 코드
@SpringBootTest
@Transactional
class CandidateServiceTest {
@Autowired CandidateRepository candidateRepository;
@Autowired CandidateService candidateService;
@Autowired CityService cityService;
@Test
public void 후보_추가() throws Exception {
City city = createCity();
CandidateRequestDto candidateRequestDto1 = new CandidateRequestDto(1, "Jake", city.getId());
CandidateRequestDto candidateRequestDto2 = new CandidateRequestDto(2, "Sam", city.getId());
candidateService.join(candidateRequestDto1);
candidateService.join(candidateRequestDto2);
assertEquals(2, candidateRepository.findAllByCity(city).size());
}
@Test
public void 후보_삭제() throws Exception {
City city = createCity();
CandidateRequestDto candidateRequestDto = new CandidateRequestDto(1, "Jake", city.getId());
Long candidateId = candidateService.join(candidateRequestDto);
candidateService.delete(candidateId);
assertEquals(null, candidateRepository.findById(candidateId));
}
@Test
public void 후보_중복_검증() throws Exception {
City city = createCity();
CandidateRequestDto candidateRequestDto = new CandidateRequestDto(1, "Jake", city.getId());
candidateService.join(candidateRequestDto);
CandidateRequestDto candidateRequestDto2 = new CandidateRequestDto(1, "Sam", city.getId());
try {
candidateService.join(candidateRequestDto2);
} catch (IllegalStateException e) {
return;
}
Assertions.fail();
}
private City createCity() {
District district = new District("서울", "서울특별시 종로구", "서울 종로");
City city = new City(district);
cityService.join(city);
return city;
}
}
테스트 결과
