From a307f6bccee05ff6eeb29f5da0d8401867f62817 Mon Sep 17 00:00:00 2001 From: DooHyoJeong Date: Fri, 17 Oct 2025 11:23:53 +0900 Subject: [PATCH 1/6] =?UTF-8?q?chore[infra]=20:=20kafka=20=EC=A3=BC?= =?UTF-8?q?=EC=84=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- infra/main.tf | 43 ++++++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/infra/main.tf b/infra/main.tf index 8d9fb65..02a5197 100644 --- a/infra/main.tf +++ b/infra/main.tf @@ -419,25 +419,30 @@ docker run -d \ -c 'ollama serve & sleep 5 && ollama pull daynice/kure-v1:567m && wait' echo "${var.github_access_token_1}" | docker login ghcr.io -u ${var.github_access_token_1_owner} --password-stdin -# zookeeper 설치 -docker run -d \ - --name zookeeper \ - --network common \ - -p 2181:2181 \ - -e ALLOW_ANONYMOUS_LOGIN=yes \ - bitnami/zookeeper:latest - -# kafka -docker run -d \ - --name kafka \ - --network common \ - -p 9092:9092 \ - -e KAFKA_BROKER_ID=1 \ - -e KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181 \ - -e KAFKA_LISTENER_SECURITY_PROTOCOL_MAP=PLAINTEXT:PLAINTEXT \ - -e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://kafka:9092 \ - -e KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR=1 \ - confluentinc/cp-kafka:7.6.0 +# # zookeeper 설치 (카프카용, 필요시 주석 해제) +# docker run -d \ +# --name zookeeper \ +# --restart unless-stopped \ +# --network common \ +# -p 2181:2181 \ +# -e ZOOKEEPER_CLIENT_PORT=2181 \ +# -e ZOOKEEPER_TICK_TIME=2000 \ +# confluentinc/cp-zookeeper:7.8.0 +# +# # kafka +# docker run -d \ +# --name kafka \ +# --restart unless-stopped \ +# --network common \ +# -p 9092:9092 \ +# -v kafka-data:/var/lib/kafka/data \ +# -e KAFKA_BROKER_ID=1 \ +# -e KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181 \ +# -e KAFKA_LISTENER_SECURITY_PROTOCOL_MAP=PLAINTEXT:PLAINTEXT \ +# -e KAFKA_LISTENERS=PLAINTEXT://0.0.0.0:9092 \ +# -e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://kafka:9092 \ +# -e KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR=1 \ +# confluentinc/cp-kafka:7.8.0 From 082e4e8984ec6e76407dc03463bc4c4bed3b6ade Mon Sep 17 00:00:00 2001 From: DooHyoJeong Date: Fri, 17 Oct 2025 11:24:10 +0900 Subject: [PATCH 2/6] =?UTF-8?q?chore[infra]=20:=20.test=EC=9A=A9=20env=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/CI-CD_Pipeline.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI-CD_Pipeline.yml b/.github/workflows/CI-CD_Pipeline.yml index 3c89809..4999811 100644 --- a/.github/workflows/CI-CD_Pipeline.yml +++ b/.github/workflows/CI-CD_Pipeline.yml @@ -463,7 +463,7 @@ jobs: # prod.env 복원 install -d -m 700 /home/ec2-user/configs - printf "%s" "${{ secrets.PROD_ENV_BASE64 }}" | base64 -d > /home/ec2-user/configs/prod.env + printf "%s" "${{ secrets.PROD_TEST_ENV_BASE64 }}" | base64 -d > /home/ec2-user/configs/prod.env chmod 600 /home/ec2-user/configs/prod.env test -s /home/ec2-user/configs/prod.env || { echo "prod.env empty"; exit 1; } From abf1b8740c463ef6a74016ac2a354932877885e2 Mon Sep 17 00:00:00 2001 From: GarakChoi Date: Thu, 16 Oct 2025 04:05:48 +0900 Subject: [PATCH 3/6] work # Conflicts: # backend/src/main/java/com/ai/lawyer/domain/chatbot/service/ChatBotService.java --- .../chatbot/service/ChatBotService.java | 94 +++++++++++++++---- 1 file changed, 75 insertions(+), 19 deletions(-) diff --git a/backend/src/main/java/com/ai/lawyer/domain/chatbot/service/ChatBotService.java b/backend/src/main/java/com/ai/lawyer/domain/chatbot/service/ChatBotService.java index 97481bd..40d7b8e 100644 --- a/backend/src/main/java/com/ai/lawyer/domain/chatbot/service/ChatBotService.java +++ b/backend/src/main/java/com/ai/lawyer/domain/chatbot/service/ChatBotService.java @@ -6,6 +6,10 @@ import com.ai.lawyer.domain.chatbot.dto.ChatDto.ChatResponse; import com.ai.lawyer.domain.chatbot.entity.History; import com.ai.lawyer.domain.chatbot.repository.HistoryRepository; +import com.ai.lawyer.infrastructure.kafka.dto.ChatPostProcessEvent; +import com.ai.lawyer.infrastructure.kafka.dto.DocumentDto; +import com.ai.lawyer.domain.member.entity.Member; +import com.ai.lawyer.domain.member.repositories.MemberRepository; import com.ai.lawyer.global.qdrant.service.QdrantService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -20,9 +24,12 @@ import org.springframework.ai.chat.prompt.PromptTemplate; import org.springframework.ai.document.Document; import org.springframework.beans.factory.annotation.Value; +import org.springframework.kafka.core.KafkaTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; import java.util.HashMap; import java.util.List; @@ -37,48 +44,78 @@ public class ChatBotService { private final ChatClient chatClient; private final QdrantService qdrantService; private final HistoryService historyService; - private final AsyncPostChatProcessingService asyncPostChatProcessingService; - + private final MemberRepository memberRepository; private final HistoryRepository historyRepository; private final ChatMemoryRepository chatMemoryRepository; + // KafkaTemplate 주입 + private final KafkaTemplate kafkaTemplate; + @Value("${custom.ai.system-message}") private String systemMessageTemplate; + // Kafka 토픽 이름 -> 추후 application.yml로 이동 고려 + private static final String POST_PROCESSING_TOPIC = "chat-post-processing"; + // 핵심 로직 // 멤버 조회 -> 벡터 검색 -> 프롬프트 생성 -> LLM 호출 (스트림) -> Kafka 이벤트 발행 -> 응답 반환 @Transactional public Flux sendMessage(Long memberId, ChatRequest chatRequestDto, Long roomId) { + return Mono.fromCallable(() -> { - // 벡터 검색 (판례, 법령) - List similarCaseDocuments = qdrantService.searchDocument(chatRequestDto.getMessage(), "type", "판례"); - List similarLawDocuments = qdrantService.searchDocument(chatRequestDto.getMessage(), "type", "법령"); + // 벡터 검색 (판례, 법령) (블로킹) + List similarCaseDocuments = qdrantService.searchDocument(chatRequestDto.getMessage(), "type", "판례"); + List similarLawDocuments = qdrantService.searchDocument(chatRequestDto.getMessage(), "type", "법령"); - String caseContext = formatting(similarCaseDocuments); - String lawContext = formatting(similarLawDocuments); + String caseContext = formatting(similarCaseDocuments); + String lawContext = formatting(similarLawDocuments); - // 채팅방 조회 또는 생성 - History history = getOrCreateRoom(memberId, roomId); + // 채팅방 조회 또는 생성 (블로킹) + History history = getOrCreateRoom(memberId, roomId); - // 메시지 기억 관리 - ChatMemory chatMemory = saveChatMemory(chatRequestDto, history); + // 메시지 기억 관리 (User 메시지 추가) + ChatMemory chatMemory = saveChatMemory(chatRequestDto, history); - // 프롬프트 생성 - Prompt prompt = getPrompt(caseContext, lawContext, chatMemory, history); + // 프롬프트 생성 + Prompt prompt = getPrompt(caseContext, lawContext, chatMemory, history); - return chatClient.prompt(prompt) + // 준비된 데이터를 담은 컨텍스트 객체 반환 + return new PreparedChatContext(prompt, history, similarCaseDocuments, similarLawDocuments); + }) + .subscribeOn(Schedulers.boundedElastic()) // 블로킹 작업을 별도 스레드에서 실행 + .flatMapMany(context -> { + // LLM 스트리밍 호출 및 클라이언트에게 즉시 응답 + return chatClient.prompt(context.prompt) .stream() .content() .collectList() .map(fullResponseList -> String.join("", fullResponseList)) - .doOnNext(fullResponse -> asyncPostChatProcessingService.processHandlerTasks(history.getHistoryId(), chatRequestDto.getMessage(), fullResponse, similarCaseDocuments, similarLawDocuments)) - .map(fullResponse -> createChatResponse(history, fullResponse, similarCaseDocuments, similarLawDocuments)) + .doOnNext(fullResponse -> { + + // Document를 DTO로 변환 + List caseDtos = context.similarCaseDocuments.stream().map(DocumentDto::from).collect(Collectors.toList()); + List lawDtos = context.similarLawDocuments.stream().map(DocumentDto::from).collect(Collectors.toList()); + + // Kafka로 보낼 이벤트 객체 + ChatPostProcessEvent event = new ChatPostProcessEvent( + context.history.getHistoryId(), + chatRequestDto.getMessage(), + fullResponse, + caseDtos, + lawDtos + ); + + // Kafka 이벤트 발행 + kafkaTemplate.send(POST_PROCESSING_TOPIC, event); + + }) + .map(fullResponse -> createChatResponse(context.history, fullResponse, context.similarCaseDocuments, context.similarLawDocuments)) .flux() .onErrorResume(throwable -> { - log.error("스트리밍 처리 중 에러 발생 (historyId: {})", history.getHistoryId(), throwable); - return Flux.just(handleError(history)); + log.error("스트리밍 처리 중 에러 발생 (historyId: {})", context.history.getHistoryId(), throwable); + return Flux.just(handleError(context.history)); }); - + }); } private ChatResponse createChatResponse(History history, String fullResponse, List cases, List laws) { @@ -148,4 +185,23 @@ private ChatResponse handleError(History history) { .build(); } + /** + * 블로킹 작업에서 준비된 데이터를 담는 컨텍스트 클래스 + * 리액티브 체인에서 데이터를 전달하기 위한 내부 클래스 + */ + private static class PreparedChatContext { + final Prompt prompt; + final History history; + final List similarCaseDocuments; + final List similarLawDocuments; + + PreparedChatContext(Prompt prompt, History history, + List similarCaseDocuments, + List similarLawDocuments) { + this.prompt = prompt; + this.history = history; + this.similarCaseDocuments = similarCaseDocuments; + this.similarLawDocuments = similarLawDocuments; + } + } } \ No newline at end of file From 77f4a90e049ee5be02141a1c113062ea0e2c693d Mon Sep 17 00:00:00 2001 From: GarakChoi Date: Fri, 17 Oct 2025 12:04:46 +0900 Subject: [PATCH 4/6] =?UTF-8?q?Fix[poll]:=EC=A4=91=EB=B3=B5=ED=88=AC?= =?UTF-8?q?=ED=91=9C=EC=98=A4=EB=A5=98=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/poll/service/PollServiceImpl.java | 82 ++++++++++--------- 1 file changed, 43 insertions(+), 39 deletions(-) diff --git a/backend/src/main/java/com/ai/lawyer/domain/poll/service/PollServiceImpl.java b/backend/src/main/java/com/ai/lawyer/domain/poll/service/PollServiceImpl.java index fe468f0..3236c73 100644 --- a/backend/src/main/java/com/ai/lawyer/domain/poll/service/PollServiceImpl.java +++ b/backend/src/main/java/com/ai/lawyer/domain/poll/service/PollServiceImpl.java @@ -21,6 +21,7 @@ import java.time.LocalDateTime; import com.ai.lawyer.global.util.AuthUtil; +import org.springframework.dao.DataIntegrityViolationException; @Service @Transactional @@ -106,61 +107,64 @@ public PollVoteDto vote(Long pollId, Long pollItemsId, Long memberId) { } PollOptions pollOptions = pollOptionsRepository.findById(pollItemsId) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "투표 항목을 찾을 수 없습니다.")); + if (pollOptions.getPoll() == null || !pollOptions.getPoll().getPollId().equals(pollId)) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "투표 항목이 해당 투표에 속하지 않습니다."); + } + Member member = AuthUtil.getMemberOrThrow(memberId); - // USER 또는 ADMIN만 투표 가능 + // enum 비교로 안전하게 역할 검사 (Role 타입 이름은 실제 프로젝트에 맞춰 조정) if (!(member.getRole().name().equals("USER") || member.getRole().name().equals("ADMIN"))) { throw new ResponseStatusException(HttpStatus.FORBIDDEN, "투표 권한이 없습니다."); } 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(); - } + // 1) 기존 투표가 있는지 조회 + var existingOpt = pollVoteRepository.findByMemberIdAndPoll_PollId(memberId, pollId); + if (existingOpt.isPresent()) { + return handleExistingVote(existingOpt.get(), pollOptions, pollId, pollItemsId, memberId); } - // 기존 투표 내역이 없으면 정상 투표 - PollVote pollVote = PollVote.builder() + // 2) 신규 투표 생성 + PollVote newVote = 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) + PollVote saved = pollVoteRepository.save(newVote); + return buildPollVote(saved, pollId, pollItemsId, memberId, "투표가 완료되었습니다."); + } catch (DataIntegrityViolationException e) { + // 동시성(경합)으로 인해 이미 다른 쓰레드가 투표를 만들어 중복 제약에 걸린 경우 복구 처리 log.warn("중복 투표 시도 감지 - memberId: {}, pollId: {}", memberId, pollId, e); + var existingAfterOpt = pollVoteRepository.findByMemberIdAndPoll_PollId(memberId, pollId); + if (existingAfterOpt.isPresent()) { + return handleExistingVote(existingAfterOpt.get(), pollOptions, pollId, pollItemsId, memberId); + } throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "이미 투표하셨습니다. 중복 투표는 불가능합니다."); } } + private PollVoteDto handleExistingVote(PollVote existing, PollOptions targetOption, + Long pollId, Long pollItemsId, Long memberId) { + // 동일 항목이면 idempotent 응답 + if (existing.getPollOptions().getPollItemsId().equals(pollItemsId)) { + return buildPollVote(existing, pollId, pollItemsId, memberId, "이미 해당 항목에 투표하셨습니다."); + } + existing.setPollOptions(targetOption); + PollVote saved = pollVoteRepository.save(existing); + return buildPollVote(saved, pollId, pollItemsId, memberId, "투표 항목을 변경하였습니다."); + } + + private PollVoteDto buildPollVote(PollVote vote, Long pollId, Long pollItemsId, Long memberId, String message) { + Long voteCount = pollVoteRepository.countByPollOptionId(pollItemsId); + return PollVoteDto.builder() + .pollVoteId(vote != null ? vote.getPollVoteId() : null) + .pollId(pollId) + .pollItemsId(pollItemsId) + .memberId(memberId) + .voteCount(voteCount) + .message(message) + .build(); + } + @Override public PollVoteDto voteByIndex(Long pollId, int index, Long memberId) { List options = getPollOptions(pollId); From ed1157a83e4d659f8c0fb28500066492b6a51247 Mon Sep 17 00:00:00 2001 From: GarakChoi Date: Fri, 17 Oct 2025 12:09:27 +0900 Subject: [PATCH 5/6] =?UTF-8?q?Fix[poll]:=EC=A4=91=EB=B3=B5=ED=88=AC?= =?UTF-8?q?=ED=91=9C=EC=98=A4=EB=A5=98=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chatbot/service/ChatBotService.java | 94 ++++--------------- 1 file changed, 19 insertions(+), 75 deletions(-) diff --git a/backend/src/main/java/com/ai/lawyer/domain/chatbot/service/ChatBotService.java b/backend/src/main/java/com/ai/lawyer/domain/chatbot/service/ChatBotService.java index 40d7b8e..97481bd 100644 --- a/backend/src/main/java/com/ai/lawyer/domain/chatbot/service/ChatBotService.java +++ b/backend/src/main/java/com/ai/lawyer/domain/chatbot/service/ChatBotService.java @@ -6,10 +6,6 @@ import com.ai.lawyer.domain.chatbot.dto.ChatDto.ChatResponse; import com.ai.lawyer.domain.chatbot.entity.History; import com.ai.lawyer.domain.chatbot.repository.HistoryRepository; -import com.ai.lawyer.infrastructure.kafka.dto.ChatPostProcessEvent; -import com.ai.lawyer.infrastructure.kafka.dto.DocumentDto; -import com.ai.lawyer.domain.member.entity.Member; -import com.ai.lawyer.domain.member.repositories.MemberRepository; import com.ai.lawyer.global.qdrant.service.QdrantService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -24,12 +20,9 @@ import org.springframework.ai.chat.prompt.PromptTemplate; import org.springframework.ai.document.Document; import org.springframework.beans.factory.annotation.Value; -import org.springframework.kafka.core.KafkaTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.core.scheduler.Schedulers; import java.util.HashMap; import java.util.List; @@ -44,78 +37,48 @@ public class ChatBotService { private final ChatClient chatClient; private final QdrantService qdrantService; private final HistoryService historyService; - private final MemberRepository memberRepository; + private final AsyncPostChatProcessingService asyncPostChatProcessingService; + private final HistoryRepository historyRepository; private final ChatMemoryRepository chatMemoryRepository; - // KafkaTemplate 주입 - private final KafkaTemplate kafkaTemplate; - @Value("${custom.ai.system-message}") private String systemMessageTemplate; - // Kafka 토픽 이름 -> 추후 application.yml로 이동 고려 - private static final String POST_PROCESSING_TOPIC = "chat-post-processing"; - // 핵심 로직 // 멤버 조회 -> 벡터 검색 -> 프롬프트 생성 -> LLM 호출 (스트림) -> Kafka 이벤트 발행 -> 응답 반환 @Transactional public Flux sendMessage(Long memberId, ChatRequest chatRequestDto, Long roomId) { - return Mono.fromCallable(() -> { - // 벡터 검색 (판례, 법령) (블로킹) - List similarCaseDocuments = qdrantService.searchDocument(chatRequestDto.getMessage(), "type", "판례"); - List similarLawDocuments = qdrantService.searchDocument(chatRequestDto.getMessage(), "type", "법령"); + // 벡터 검색 (판례, 법령) + List similarCaseDocuments = qdrantService.searchDocument(chatRequestDto.getMessage(), "type", "판례"); + List similarLawDocuments = qdrantService.searchDocument(chatRequestDto.getMessage(), "type", "법령"); - String caseContext = formatting(similarCaseDocuments); - String lawContext = formatting(similarLawDocuments); + String caseContext = formatting(similarCaseDocuments); + String lawContext = formatting(similarLawDocuments); - // 채팅방 조회 또는 생성 (블로킹) - History history = getOrCreateRoom(memberId, roomId); + // 채팅방 조회 또는 생성 + History history = getOrCreateRoom(memberId, roomId); - // 메시지 기억 관리 (User 메시지 추가) - ChatMemory chatMemory = saveChatMemory(chatRequestDto, history); + // 메시지 기억 관리 + ChatMemory chatMemory = saveChatMemory(chatRequestDto, history); - // 프롬프트 생성 - Prompt prompt = getPrompt(caseContext, lawContext, chatMemory, history); + // 프롬프트 생성 + Prompt prompt = getPrompt(caseContext, lawContext, chatMemory, history); - // 준비된 데이터를 담은 컨텍스트 객체 반환 - return new PreparedChatContext(prompt, history, similarCaseDocuments, similarLawDocuments); - }) - .subscribeOn(Schedulers.boundedElastic()) // 블로킹 작업을 별도 스레드에서 실행 - .flatMapMany(context -> { - // LLM 스트리밍 호출 및 클라이언트에게 즉시 응답 - return chatClient.prompt(context.prompt) + return chatClient.prompt(prompt) .stream() .content() .collectList() .map(fullResponseList -> String.join("", fullResponseList)) - .doOnNext(fullResponse -> { - - // Document를 DTO로 변환 - List caseDtos = context.similarCaseDocuments.stream().map(DocumentDto::from).collect(Collectors.toList()); - List lawDtos = context.similarLawDocuments.stream().map(DocumentDto::from).collect(Collectors.toList()); - - // Kafka로 보낼 이벤트 객체 - ChatPostProcessEvent event = new ChatPostProcessEvent( - context.history.getHistoryId(), - chatRequestDto.getMessage(), - fullResponse, - caseDtos, - lawDtos - ); - - // Kafka 이벤트 발행 - kafkaTemplate.send(POST_PROCESSING_TOPIC, event); - - }) - .map(fullResponse -> createChatResponse(context.history, fullResponse, context.similarCaseDocuments, context.similarLawDocuments)) + .doOnNext(fullResponse -> asyncPostChatProcessingService.processHandlerTasks(history.getHistoryId(), chatRequestDto.getMessage(), fullResponse, similarCaseDocuments, similarLawDocuments)) + .map(fullResponse -> createChatResponse(history, fullResponse, similarCaseDocuments, similarLawDocuments)) .flux() .onErrorResume(throwable -> { - log.error("스트리밍 처리 중 에러 발생 (historyId: {})", context.history.getHistoryId(), throwable); - return Flux.just(handleError(context.history)); + log.error("스트리밍 처리 중 에러 발생 (historyId: {})", history.getHistoryId(), throwable); + return Flux.just(handleError(history)); }); - }); + } private ChatResponse createChatResponse(History history, String fullResponse, List cases, List laws) { @@ -185,23 +148,4 @@ private ChatResponse handleError(History history) { .build(); } - /** - * 블로킹 작업에서 준비된 데이터를 담는 컨텍스트 클래스 - * 리액티브 체인에서 데이터를 전달하기 위한 내부 클래스 - */ - private static class PreparedChatContext { - final Prompt prompt; - final History history; - final List similarCaseDocuments; - final List similarLawDocuments; - - PreparedChatContext(Prompt prompt, History history, - List similarCaseDocuments, - List similarLawDocuments) { - this.prompt = prompt; - this.history = history; - this.similarCaseDocuments = similarCaseDocuments; - this.similarLawDocuments = similarLawDocuments; - } - } } \ No newline at end of file From 5b840776f194e262ac93512bc277571880e01d7b Mon Sep 17 00:00:00 2001 From: GarakChoi Date: Fri, 17 Oct 2025 12:10:39 +0900 Subject: [PATCH 6/6] =?UTF-8?q?Fix[poll]:=EC=A4=91=EB=B3=B5=ED=88=AC?= =?UTF-8?q?=ED=91=9C=EC=98=A4=EB=A5=98=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/ai/lawyer/domain/poll/service/PollServiceImpl.java | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/main/java/com/ai/lawyer/domain/poll/service/PollServiceImpl.java b/backend/src/main/java/com/ai/lawyer/domain/poll/service/PollServiceImpl.java index 3236c73..1cda1b8 100644 --- a/backend/src/main/java/com/ai/lawyer/domain/poll/service/PollServiceImpl.java +++ b/backend/src/main/java/com/ai/lawyer/domain/poll/service/PollServiceImpl.java @@ -112,7 +112,6 @@ public PollVoteDto vote(Long pollId, Long pollItemsId, Long memberId) { } Member member = AuthUtil.getMemberOrThrow(memberId); - // enum 비교로 안전하게 역할 검사 (Role 타입 이름은 실제 프로젝트에 맞춰 조정) if (!(member.getRole().name().equals("USER") || member.getRole().name().equals("ADMIN"))) { throw new ResponseStatusException(HttpStatus.FORBIDDEN, "투표 권한이 없습니다."); }