Skip to content

Commit 117c1b0

Browse files
authored
Merge pull request #350 from prgrms-web-devcourse-final-project/develop
배포
2 parents 81fb2e8 + e58093a commit 117c1b0

File tree

4 files changed

+69
-56
lines changed

4 files changed

+69
-56
lines changed

backend/src/main/java/com/ai/lawyer/domain/chatbot/controller/ChatBotController.java

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,39 +8,40 @@
88
import io.swagger.v3.oas.annotations.tags.Tag;
99
import lombok.RequiredArgsConstructor;
1010
import lombok.extern.slf4j.Slf4j;
11-
import org.springframework.http.ResponseEntity;
12-
import org.springframework.stereotype.Controller;
13-
import org.springframework.web.bind.annotation.PathVariable;
14-
import org.springframework.web.bind.annotation.PostMapping;
15-
import org.springframework.web.bind.annotation.RequestBody;
16-
import org.springframework.web.bind.annotation.RequestMapping;
11+
import org.springframework.web.bind.annotation.*;
1712
import reactor.core.publisher.Flux;
1813

1914
@Slf4j
2015
@Tag(name = "ChatBot API", description = "챗봇 관련 API")
21-
@Controller
16+
@RestController
2217
@RequiredArgsConstructor
2318
@RequestMapping("/api/chat")
2419
public class ChatBotController {
2520

2621
private final ChatBotService chatBotService;
2722

2823
@Operation(summary = "01. 새로운 채팅", description = "첫 메시지 전송으로 새로운 채팅방을 생성하고 챗봇과 대화를 시작")
29-
@PostMapping("/message")
30-
public ResponseEntity<Flux<ChatResponse>> postNewMessage(@RequestBody ChatRequest chatRequest) {
24+
@PostMapping(value = "/message", produces = "application/stream+json")
25+
public Flux<ChatResponse> postNewMessage(@RequestBody ChatRequest chatRequest) {
26+
// SecurityContext에서 memberId를 미리 추출 (컨트롤러 진입 시점)
3127
Long memberId = AuthUtil.getAuthenticatedMemberId();
32-
log.debug("새로운 채팅 요청: memberId={}", memberId);
33-
return ResponseEntity.ok(chatBotService.sendMessage(memberId, chatRequest, null));
28+
log.info("새로운 채팅 요청: memberId={}", memberId);
29+
30+
// memberId를 Flux에 전달 (SecurityContext 전파 문제 방지)
31+
return chatBotService.sendMessage(memberId, chatRequest, null);
3432
}
3533

3634
@Operation(summary = "02. 기존 채팅", description = "기존 채팅방에 메시지를 보내고 챗봇과 대화를 이어감")
37-
@PostMapping("{roomId}/message")
38-
public ResponseEntity<Flux<ChatResponse>> postMessage(
35+
@PostMapping(value = "{roomId}/message", produces = "application/stream+json")
36+
public Flux<ChatResponse> postMessage(
3937
@RequestBody ChatRequest chatRequest,
4038
@PathVariable(value = "roomId", required = false) Long roomId) {
39+
// SecurityContext에서 memberId를 미리 추출 (컨트롤러 진입 시점)
4140
Long memberId = AuthUtil.getAuthenticatedMemberId();
42-
log.debug("기존 채팅 요청: memberId={}, roomId={}", memberId, roomId);
43-
return ResponseEntity.ok(chatBotService.sendMessage(memberId, chatRequest, roomId));
41+
log.info("기존 채팅 요청: memberId={}, roomId={}", memberId, roomId);
42+
43+
// memberId를 Flux에 전달 (SecurityContext 전파 문제 방지)
44+
return chatBotService.sendMessage(memberId, chatRequest, roomId);
4445
}
4546

4647
}

backend/src/main/java/com/ai/lawyer/domain/poll/entity/PollVote.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
package com.ai.lawyer.domain.poll.entity;
22

3-
import com.ai.lawyer.domain.member.entity.Member;
43
import jakarta.persistence.*;
54
import lombok.*;
65

76
@Entity
8-
@Table(name = "poll_vote")
7+
@Table(name = "poll_vote",
8+
uniqueConstraints = @UniqueConstraint(
9+
name = "uk_poll_vote_member_poll",
10+
columnNames = {"member_id", "poll_id"}
11+
)
12+
)
913
@Data
1014
@NoArgsConstructor
1115
@AllArgsConstructor

backend/src/main/java/com/ai/lawyer/domain/poll/service/PollServiceImpl.java

Lines changed: 46 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -111,47 +111,54 @@ public PollVoteDto vote(Long pollId, Long pollItemsId, Long memberId) {
111111
if (!(member.getRole().name().equals("USER") || member.getRole().name().equals("ADMIN"))) {
112112
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "투표 권한이 없습니다.");
113113
}
114-
// 기존 투표 내역 조회
115-
var existingVoteOpt = pollVoteRepository.findByMemberIdAndPoll_PollId(memberId, pollId);
116-
if (existingVoteOpt.isPresent()) {
117-
PollVote existingVote = existingVoteOpt.get();
118-
if (existingVote.getPollOptions().getPollItemsId().equals(pollItemsId)) {
119-
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "이미 투표하셨습니다.");
120-
} else {
121-
pollVoteRepository.deleteByMemberIdAndPoll_PollId(memberId, pollId);
122-
PollVote pollVote = PollVote.builder()
123-
.poll(poll)
124-
.pollOptions(pollOptions)
125-
.memberId(memberId)
126-
.build();
127-
PollVote savedVote = pollVoteRepository.save(pollVote);
128-
Long voteCount = pollVoteRepository.countByPollOptionId(pollItemsId);
129-
return PollVoteDto.builder()
130-
.pollVoteId(savedVote.getPollVoteId())
131-
.pollId(pollId)
132-
.pollItemsId(pollItemsId)
133-
.memberId(memberId)
134-
.voteCount(voteCount)
135-
.message("투표 항목을 변경하였습니다.")
136-
.build();
114+
115+
try {
116+
// 기존 투표 내역 조회
117+
var existingVoteOpt = pollVoteRepository.findByMemberIdAndPoll_PollId(memberId, pollId);
118+
if (existingVoteOpt.isPresent()) {
119+
PollVote existingVote = existingVoteOpt.get();
120+
if (existingVote.getPollOptions().getPollItemsId().equals(pollItemsId)) {
121+
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "이미 투표하셨습니다.");
122+
} else {
123+
pollVoteRepository.deleteByMemberIdAndPoll_PollId(memberId, pollId);
124+
PollVote pollVote = PollVote.builder()
125+
.poll(poll)
126+
.pollOptions(pollOptions)
127+
.memberId(memberId)
128+
.build();
129+
PollVote savedVote = pollVoteRepository.save(pollVote);
130+
Long voteCount = pollVoteRepository.countByPollOptionId(pollItemsId);
131+
return PollVoteDto.builder()
132+
.pollVoteId(savedVote.getPollVoteId())
133+
.pollId(pollId)
134+
.pollItemsId(pollItemsId)
135+
.memberId(memberId)
136+
.voteCount(voteCount)
137+
.message("투표 항목을 변경하였습니다.")
138+
.build();
139+
}
137140
}
141+
// 기존 투표 내역이 없으면 정상 투표
142+
PollVote pollVote = PollVote.builder()
143+
.poll(poll)
144+
.pollOptions(pollOptions)
145+
.memberId(memberId)
146+
.build();
147+
PollVote savedVote = pollVoteRepository.save(pollVote);
148+
Long voteCount = pollVoteRepository.countByPollOptionId(pollItemsId);
149+
return PollVoteDto.builder()
150+
.pollVoteId(savedVote.getPollVoteId())
151+
.pollId(pollId)
152+
.pollItemsId(pollItemsId)
153+
.memberId(memberId)
154+
.voteCount(voteCount)
155+
.message("투표가 완료되었습니다.")
156+
.build();
157+
} catch (org.springframework.dao.DataIntegrityViolationException e) {
158+
// 동시성 문제로 인한 중복 투표 시도 (unique constraint violation)
159+
log.warn("중복 투표 시도 감지 - memberId: {}, pollId: {}", memberId, pollId, e);
160+
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "이미 투표하셨습니다. 중복 투표는 불가능합니다.");
138161
}
139-
// 기존 투표 내역이 없으면 정상 투표
140-
PollVote pollVote = PollVote.builder()
141-
.poll(poll)
142-
.pollOptions(pollOptions)
143-
.memberId(memberId)
144-
.build();
145-
PollVote savedVote = pollVoteRepository.save(pollVote);
146-
Long voteCount = pollVoteRepository.countByPollOptionId(pollItemsId);
147-
return PollVoteDto.builder()
148-
.pollVoteId(savedVote.getPollVoteId())
149-
.pollId(pollId)
150-
.pollItemsId(pollItemsId)
151-
.memberId(memberId)
152-
.voteCount(voteCount)
153-
.message("투표가 완료되었습니다.")
154-
.build();
155162
}
156163

157164
@Override

backend/src/main/java/com/ai/lawyer/global/security/SecurityConfig.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ public class SecurityConfig {
5757
"/api/law/**", // 법령 (공개)
5858
"/api/law-word/**", // 법률 용어 (공개)
5959
"/api/home/**", // 홈 (공개)
60+
"/api/chat/**", // 챗봇
6061
"/h2-console/**", // H2 콘솔 (개발용)
6162
"/actuator/health", "/actuator/health/**", "/actuator/info", // Spring Actuator
6263
"/api/actuator/health", "/api/actuator/health/**", "/api/actuator/info",

0 commit comments

Comments
 (0)