
/api/auth/login POST/api/votes GET요구사항 상세
- 투표 목록 조회 API 구현
- GET
/api/votes
- request
없음- response
- 성공시 : HttpStatusCode 200으로 응답.
[ { "id": 1, //투표 주제 번호 "title": "점심 메뉴 선택", //투표 주제명 "status": "OPEN", //상태 "author": "너구리", //사용자 닉네임 "authorId": "delilah", //사용자 로그인 ID "createdAt": "2025-11-22T12:07:35", //투표주제 생성일시 }, { ... } ]- 실패시 : HttpStatusCode와 오류메시지는 상황에 맞게 반환
votes, users테이블을 전체 조회하여 반환합니다.votes테이블의상태컬럼의 데이터는OPEN, CLOSED두가지 상태가 존재합니다.
- 엔티티를 선언할 때,
상태는 enum을 갖도록 합니다.@Enumerated사용
/api/votes POST요구사항 상세
- 투표 주제 생성 API 구현
- POST
/api/votes
- request
- payload
{ "title": "점심뭐먹지?", //투표 주제명 "candidates": [ //후보 리스트 "한식", "일식", "중식", "양식" ], "authorId": "delilah" //작성자 로그인ID }- response
- 성공시 : HttpStatusCode 201 Created로 응답. 투표 주제 번호 반환
3 // 생성 완료된 투표 주제 id- 실패시 : HttpStatusCode와 오류메시지는 상황에 맞게 반환
votes, candidates테이블에 데이터 저장합니다.
/api/votes/{voteId} GET요구사항 상세
- 투표 상세 조회 API 구현
- GET
/api/votes/{voteId}
- request
- path parameter : voteId (투표 주제 번호)
- response
- 성공시 : HttpStatusCode 200으로 응답. 결과 응답
{ "id": 1, //투표 주제 번호 "title": "점심 뭐먹?", // 주제명 "status": "OPEN", //상태 "author": "너구리", //사용자 닉네임 "authorId": "delilah", //사용자 로그인 ID "createdAt": "2025-11-22T10:55:20", //투표 생성 일자 //후보 리스트 "candidates": [ //후보 번호, 후보명, 득표 수 { "id": 1, "name": "한식", "voteCount": 5 }, { "id": 2, "name": "중식", "voteCount": 3 }, { "id": 3, "name": "일식", "voteCount": 2 }, { "id": 4, "name": "양식", "voteCount": 1 } ], "totalVotes": 11 //해당 투표 주제의 총 득표수 }- 실패시 : HttpStatusCode와 오류메시지는 상황에 맞게 반환
- 투표 수 집계가 필요합니다.
- 후보별 득표 현황을 확인 할 수 있도록 처리합니다.
- MySQL Query 보다는 집계를 위해 필요한 테이블을 조회 후 Stream API 를 이용해서 집계해보세요!
/api/votes/{voteId}/vote-record GET요구사항 상세
- 투표 기록 조회 API 구현
- GET
/api/votes/{voteId}/vote-record
- request
- path parameter : voteId (투표 주제 번호)
- request parameter : voterId (투표 기록 조회를 할 대상의 사용자 로그인ID)
- 예시
/api/votes/1/vote-record?voterId=delilah- response
- 성공시 : HttpStatusCode 200으로 응답. 결과 리스트 응답
- 해당 로그인ID로 투표 기록이 있을 경우
{ "voteId":4, //투표 주제 번호 "candidateId":9, //투표 후보 번호 "voterId":"delilah", //투표 진행한 사용자의 로그인ID "votedAt":"2025-11-22T13:43:38" //투표일시 }- 투표기록이 없을 경우 null로 반환
- 투표 이력이 있는 사용자인지 조회합니다.
- 클라이언트에서는 해당 API 호출을 통해 얻은 응답으로
- 이미 투표한 이력이 있다면 투표를 변경할 수 있는 화면을 노출하고,
- 투표 이력이 없다면 첫 투표를 할 수 있는 화면을 노출합니다.
/api/votes/{voteId}/vote POST요구사항 상세
- 투표 API
- POST
/api/votes/{voteId}/vote- request
- path parameter : voteId (투표 주제 번호)
- payload
{ "candidateId": 4, //투표 후보 번호 "voterId": "delilah" //투표한 사람의 로그인ID }- response
- 성공시 : HttpStatusCode 200으로 응답. 결과 응답
{ "voteId": 1, //투표 주제 번호 "candidateId": 4, //투표 후보 번호 "voterId": "delilah", //투표한 사람의 로그인ID "votedAt": "2025-11-22T12:20:35.472445" //투표일시 }- 실패시 : HttpStatusCode와 오류메시지는 상황에 맞게 반환
- 이미 종료된 투표(votes.status == ‘CLOSED’) 라면 오류 메시지 발생시킵니다.
- 투표 내용을
vote_records테이블에 저장합니다.- 선택한 투표 후보 변경이 가능합니다.
- 따라서 이미 저장된 투표내용이 있다면 insert가 아닌 update를 해야합니다.
/api/votes/{voteId}/close PATCH요구사항 상세
- 투표 종료 API 구현
- PATCH
/api/votes/{voteId}/close
- request
- path parameter : voteId (투표 주제 번호)
- payload
{ "authorId": "delilah" //투표 종료한 사용자의 로그인ID }- response
- 성공시 : HttpStatusCode 200으로 응답. 아래 예시 구조로 응답
{ "id": 1, //투표 주제번호 "status": "CLOSED" //상태 }- 실패시 : 실패시 : HttpStatusCode와 오류메시지는 상황에 맞게 반환
votes테이블의상태를 ‘CLOSED’로 변경합니다.- 투표를 생성한 사용자의 로그인ID로 요청이 들어올 경우에만, 투표 종료가 가능합니다.
- 투표를 생성한 유저가 아니라면 오류메시지를 반환합니다.
상세 코드는 GitHub 참조 (live-voting)


(발생 현상)
로그인 성공 후 투표 목록 조회(http://localhost:3000/votes) 페이지로 이어진다. 그런데 위 이미지와 같이 제공된 프론트 프레임이 아니라 오류가 있다는 안내문이 보인다. Network를 확인했으나 HTTP 응답 코드는 200 OK로 정상 동작하는 상태.
(원인 조사)
프론트 코드가 워낙 세세하게 나눠져있어 프론트 쪽을 조사하기엔 어려움이 느껴졌다. 그래서 과제 코드를 한 번씩 검수하셨을 튜터님께 직진해서 도움을 요청했다.
공통 응답에서 아래와 같이 응답을 보내고 있는데
{
"data": [
{
"id": Long,
"title": String,
...
}
]
}
프론트 코드에선 data라는 형태를 응답으로 받고 있지 않으니 응답 형태 불일치로 이런 이슈가 발생한 것이다.
왜 공통 응답으로 구현했냐고 물어본다면... 전역 예외처리를 하는 김에 공통 응답도 시도해보고 싶었다. 결론적으론 시키는 것만 잘 해내자! 라는 교훈을 얻었지만...다음 개인적인 프로젝트나 요구사항에 공통 응답이 들어있는 과제가 나온다면 다시 도전하기로 했다.
(해결 방법)
공통 응답형태ResponseEntity<CommonResponse>를 모두 각 해당되는 응답 DtoResponseEntity<VoteToResponse>를 반환하도록 수정해서 해결했다.

(발생 현상)
투표 선택지를 이미 양식으로 투표한 상황에서 새로운 선택지(ex. 일식)로 '투표하기' 요청을 보내면 투표 결과가 바뀌어야 한다. 그러나 요청은 정상적으로 들어갔으나 막상 투표 결과에서는 예전 결과 그대로 나타나고 있었다.
(원인 조사)
원인은 짐작가는게 있었다.
영속성 컨텍스트와 변경 감지(Dirty-Checking)
코드를 작성하면서 헷갈린 부분이라 찾아봤었는데, 연관관계에 있는 컬럼 내용이 변경될 경우(여기에선 Candidate가 해당된다) 이를 save()를 사용해서 다시 저장하지 않아도 Vote와 VoteRecord, Candidate DB 테이블에 변경 사항이 반영되는가?
투표하기의 비즈니스 로직은 아래와 같이 구성되어 있는 상태다.
public VotedResponse voteTo(Long voteId, VoteToRequest request) {
Vote vote = findVote(voteId);
Users user = findUser(request.getVoterId());
Candidate candidate = candidateService.findCandidate(request.getCandidateId());
if(voteRecordRepository.existsByVoteAndUser(vote, user)) {
VoteRecord updateVoteRecord = voteRecordRepository.findByVoteAndUser(vote, user).orElseThrow(
() -> new BusinessException(ErrorCode.USER_NOT_FOUND));
updateVoteRecord.setCandidate(candidate);
String message = user.getNickname() + "님이 " + vote.getTitle() + " 투표에 다시 참여하셨습니다.";
simpMessagingTemplate.convertAndSend("/topic/announcements", message);
System.out.println(message);
return VotedResponse.from(updateVoteRecord);
}
VoteRecord voteRecord = new VoteRecord(user, vote, candidate);
VoteRecord savedRecord = voteRecordRepository.save(voteRecord);
String message = user.getNickname() + "님이 " + vote.getTitle() + " 투표에 참여하셨습니다.";
simpMessagingTemplate.convertAndSend("/topic/announcements", message);
return VotedResponse.from(savedRecord);
}
영속성 컨텍스트에서 관리되는 엔티티의 내용이 변경되면 이를 감지하고 자동으로 UPDATE 쿼리를 처리해주는 Dirty-Checking 기능이 있기 때문에, 굳이 .save()를 사용하지 않아도 된다는 검색 결과를 봤다.
그래서 save를 사용하지 않고 Setter 메서드로 VoteRecord 엔티티의 Candidate만 변경해주고 응답을 반환했더니 UPDATE가 제대로 이뤄지지 않은 것이다.
(해결방법)
Setter 메서드 아래에 voteRecordRepository.save(updateVoteRecord);를 추가해 변경사항을 저장해줬더니 간단히 해결되었다.
그러나 여전히 왜 더티 체킹이 동작하지 않은 것인지는 개념과 동작 원리를 확인할 필요가 있다. 이 내용은 과제를 제출한 뒤 여유있게 살펴보고자 한다.
⬇️

가입된 멤버를 목록에서 선택한 뒤 로그인 버튼을 눌러 로그인 요청 처리에 성공하면 투표 목록 페이지로 이어진다.
투표 목록 조회

종료 버튼은 투표를 생성한 작성자에게만 활성화된다.
투표하기 & 투표 상세 조회

선택지를 누르고 투표하기를 누르명 아래와 같이 현재 투표 현황(결과)이 나타난다.

Websocket+STOMP 메시지 처리
3번에서 투표하기가 처리되면 다른 사용자에게 메시지가 나타나게 된다.
