Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,39 +8,40 @@
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;

@Slf4j
@Tag(name = "ChatBot API", description = "챗봇 관련 API")
@Controller
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/chat")
public class ChatBotController {

private final ChatBotService chatBotService;

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

// memberId를 Flux에 전달 (SecurityContext 전파 문제 방지)
return chatBotService.sendMessage(memberId, chatRequest, null);
}

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

// memberId를 Flux에 전달 (SecurityContext 전파 문제 방지)
return chatBotService.sendMessage(memberId, chatRequest, roomId);
}

}
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package com.ai.lawyer.domain.poll.entity;

import com.ai.lawyer.domain.member.entity.Member;
import jakarta.persistence.*;
import lombok.*;

@Entity
@Table(name = "poll_vote")
@Table(name = "poll_vote",
uniqueConstraints = @UniqueConstraint(
name = "uk_poll_vote_member_poll",
columnNames = {"member_id", "poll_id"}
)
)
@Data
@NoArgsConstructor
@AllArgsConstructor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,47 +111,54 @@ public PollVoteDto vote(Long pollId, Long pollItemsId, Long memberId) {
if (!(member.getRole().name().equals("USER") || member.getRole().name().equals("ADMIN"))) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "투표 권한이 없습니다.");
}
// 기존 투표 내역 조회
var existingVoteOpt = pollVoteRepository.findByMemberIdAndPoll_PollId(memberId, pollId);
if (existingVoteOpt.isPresent()) {
PollVote existingVote = existingVoteOpt.get();
if (existingVote.getPollOptions().getPollItemsId().equals(pollItemsId)) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "이미 투표하셨습니다.");
} else {
pollVoteRepository.deleteByMemberIdAndPoll_PollId(memberId, pollId);
PollVote pollVote = PollVote.builder()
.poll(poll)
.pollOptions(pollOptions)
.memberId(memberId)
.build();
PollVote savedVote = pollVoteRepository.save(pollVote);
Long voteCount = pollVoteRepository.countByPollOptionId(pollItemsId);
return PollVoteDto.builder()
.pollVoteId(savedVote.getPollVoteId())
.pollId(pollId)
.pollItemsId(pollItemsId)
.memberId(memberId)
.voteCount(voteCount)
.message("투표 항목을 변경하였습니다.")
.build();

try {
// 기존 투표 내역 조회
var existingVoteOpt = pollVoteRepository.findByMemberIdAndPoll_PollId(memberId, pollId);
if (existingVoteOpt.isPresent()) {
PollVote existingVote = existingVoteOpt.get();
if (existingVote.getPollOptions().getPollItemsId().equals(pollItemsId)) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "이미 투표하셨습니다.");
} else {
pollVoteRepository.deleteByMemberIdAndPoll_PollId(memberId, pollId);
PollVote pollVote = PollVote.builder()
.poll(poll)
.pollOptions(pollOptions)
.memberId(memberId)
.build();
PollVote savedVote = pollVoteRepository.save(pollVote);
Long voteCount = pollVoteRepository.countByPollOptionId(pollItemsId);
return PollVoteDto.builder()
.pollVoteId(savedVote.getPollVoteId())
.pollId(pollId)
.pollItemsId(pollItemsId)
.memberId(memberId)
.voteCount(voteCount)
.message("투표 항목을 변경하였습니다.")
.build();
}
}
// 기존 투표 내역이 없으면 정상 투표
PollVote pollVote = PollVote.builder()
.poll(poll)
.pollOptions(pollOptions)
.memberId(memberId)
.build();
PollVote savedVote = pollVoteRepository.save(pollVote);
Long voteCount = pollVoteRepository.countByPollOptionId(pollItemsId);
return PollVoteDto.builder()
.pollVoteId(savedVote.getPollVoteId())
.pollId(pollId)
.pollItemsId(pollItemsId)
.memberId(memberId)
.voteCount(voteCount)
.message("투표가 완료되었습니다.")
.build();
} catch (org.springframework.dao.DataIntegrityViolationException e) {
// 동시성 문제로 인한 중복 투표 시도 (unique constraint violation)
log.warn("중복 투표 시도 감지 - memberId: {}, pollId: {}", memberId, pollId, e);
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "이미 투표하셨습니다. 중복 투표는 불가능합니다.");
}
// 기존 투표 내역이 없으면 정상 투표
PollVote pollVote = PollVote.builder()
.poll(poll)
.pollOptions(pollOptions)
.memberId(memberId)
.build();
PollVote savedVote = pollVoteRepository.save(pollVote);
Long voteCount = pollVoteRepository.countByPollOptionId(pollItemsId);
return PollVoteDto.builder()
.pollVoteId(savedVote.getPollVoteId())
.pollId(pollId)
.pollItemsId(pollItemsId)
.memberId(memberId)
.voteCount(voteCount)
.message("투표가 완료되었습니다.")
.build();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ public class SecurityConfig {
"/api/law/**", // 법령 (공개)
"/api/law-word/**", // 법률 용어 (공개)
"/api/home/**", // 홈 (공개)
"/api/chat/**", // 챗봇
"/h2-console/**", // H2 콘솔 (개발용)
"/actuator/health", "/actuator/health/**", "/actuator/info", // Spring Actuator
"/api/actuator/health", "/api/actuator/health/**", "/api/actuator/info",
Expand Down