From fca10ad383829090f708d8d50e09d4e7a76ae7b0 Mon Sep 17 00:00:00 2001 From: GarakChoi Date: Mon, 13 Oct 2025 14:26:19 +0900 Subject: [PATCH 01/13] fix[post]:myposts rollback --- .../domain/post/controller/PostController.java | 10 +++++++++- .../ai/lawyer/domain/post/service/PostService.java | 3 ++- .../lawyer/domain/post/service/PostServiceImpl.java | 12 ++++++++++-- .../domain/post/controller/PostControllerTest.java | 2 +- .../lawyer/domain/post/service/PostServiceTest.java | 4 ++-- 5 files changed, 24 insertions(+), 7 deletions(-) diff --git a/backend/src/main/java/com/ai/lawyer/domain/post/controller/PostController.java b/backend/src/main/java/com/ai/lawyer/domain/post/controller/PostController.java index de478aa..38e00a6 100644 --- a/backend/src/main/java/com/ai/lawyer/domain/post/controller/PostController.java +++ b/backend/src/main/java/com/ai/lawyer/domain/post/controller/PostController.java @@ -131,13 +131,21 @@ public ResponseEntity> getMyPostById(@PathVariable Long pos @Operation(summary = "본인 게시글 전체 조회") @GetMapping("/my") + public ResponseEntity>> getMyPosts() { + Long memberId = AuthUtil.getAuthenticatedMemberId(); + List posts = postService.getMyPosts(memberId); + return ResponseEntity.ok(new ApiResponse<>(200, "본인 게시글 전체 조회 성공", posts)); + } + + @Operation(summary = "본인 게시글 전체 패이징 조회") + @GetMapping("/mypaged") public ResponseEntity>> getMyPosts( @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size ) { Pageable pageable = PageRequest.of(page, size, org.springframework.data.domain.Sort.by("updatedAt").descending()); Long memberId = AuthUtil.getAuthenticatedMemberId(); - Page response = postService.getMyPosts(pageable, memberId); + Page response = postService.getMyPostspaged(pageable, memberId); return ResponseEntity.ok(new ApiResponse<>(200, "본인 게시글 전체 조회 성공", response)); } diff --git a/backend/src/main/java/com/ai/lawyer/domain/post/service/PostService.java b/backend/src/main/java/com/ai/lawyer/domain/post/service/PostService.java index 2544fcf..65860de 100644 --- a/backend/src/main/java/com/ai/lawyer/domain/post/service/PostService.java +++ b/backend/src/main/java/com/ai/lawyer/domain/post/service/PostService.java @@ -30,7 +30,8 @@ public interface PostService { // ===== 본인 게시글 관련 ===== PostDto getMyPostById(Long postId, Long requesterMemberId); - Page getMyPosts(Pageable pageable, Long requesterMemberId); + List getMyPosts(Long requesterMemberId); + Page getMyPostspaged(Pageable pageable, Long requesterMemberId); // ===== 페이징 관련 ===== Page getPostsPaged(Pageable pageable, Long memberId); diff --git a/backend/src/main/java/com/ai/lawyer/domain/post/service/PostServiceImpl.java b/backend/src/main/java/com/ai/lawyer/domain/post/service/PostServiceImpl.java index 7b69ae3..2024b2e 100644 --- a/backend/src/main/java/com/ai/lawyer/domain/post/service/PostServiceImpl.java +++ b/backend/src/main/java/com/ai/lawyer/domain/post/service/PostServiceImpl.java @@ -142,7 +142,6 @@ public PostDto updatePost(Long postId, PostUpdateDto postUpdateDto) { public void deletePost(Long postId) { Post post = postRepository.findById(postId) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "삭제할 게시글을 찾을 수 없습니다.")); - // Poll도 명시적으로 삭제 (JPA cascade/orphanRemoval이 있으면 생략 가능) postRepository.delete(post); } @@ -165,8 +164,17 @@ public PostDto getMyPostById(Long postId, Long requesterMemberId) { return convertToDto(post, requesterMemberId); } + public List getMyPosts(Long requesterMemberId) { + Member member = AuthUtil.getMemberOrThrow(requesterMemberId); + List posts = postRepository.findByMember(member); + return posts.stream() + .sorted(Comparator.comparing(Post::getUpdatedAt, Comparator.nullsLast(Comparator.naturalOrder())).reversed()) + .map(post -> convertToDto(post, requesterMemberId)) + .collect(Collectors.toList()); + } + @Override - public Page getMyPosts(Pageable pageable, Long requesterMemberId) { + public Page getMyPostspaged(Pageable pageable, Long requesterMemberId) { Member member = AuthUtil.getMemberOrThrow(requesterMemberId); Page posts = postRepository.findByMember(member, pageable); return posts.map(post -> convertToDto(post, requesterMemberId)); diff --git a/backend/src/test/java/com/ai/lawyer/domain/post/controller/PostControllerTest.java b/backend/src/test/java/com/ai/lawyer/domain/post/controller/PostControllerTest.java index 0098955..d7a163f 100644 --- a/backend/src/test/java/com/ai/lawyer/domain/post/controller/PostControllerTest.java +++ b/backend/src/test/java/com/ai/lawyer/domain/post/controller/PostControllerTest.java @@ -219,7 +219,7 @@ void t10_paged() throws Exception { com.ai.lawyer.domain.post.dto.PostDto.builder().postId(1L).postName("테스트 제목").build() ); org.springframework.data.domain.PageImpl page = new org.springframework.data.domain.PageImpl<>(posts, pageable, 1); - Mockito.when(postService.getMyPosts(Mockito.any(), Mockito.anyLong())).thenReturn(page); + Mockito.when(postService.getMyPostspaged(Mockito.any(), Mockito.anyLong())).thenReturn(page); mockMvc.perform(get("/api/posts/my") .param("page", "0") .param("size", "10") diff --git a/backend/src/test/java/com/ai/lawyer/domain/post/service/PostServiceTest.java b/backend/src/test/java/com/ai/lawyer/domain/post/service/PostServiceTest.java index d8315ed..1341f1e 100644 --- a/backend/src/test/java/com/ai/lawyer/domain/post/service/PostServiceTest.java +++ b/backend/src/test/java/com/ai/lawyer/domain/post/service/PostServiceTest.java @@ -116,8 +116,8 @@ void t10_paged() { java.util.List postList = java.util.List.of(new PostDto()); org.springframework.data.domain.Pageable pageable = org.springframework.data.domain.PageRequest.of(0, 10); org.springframework.data.domain.PageImpl page = new org.springframework.data.domain.PageImpl<>(postList, pageable, 1); - Mockito.when(postService.getMyPosts(Mockito.eq(pageable), Mockito.eq(1L))).thenReturn(page); - var result = postService.getMyPosts(pageable, 1L); + Mockito.when(postService.getMyPostspaged(Mockito.eq(pageable), Mockito.eq(1L))).thenReturn(page); + var result = postService.getMyPostspaged(pageable, 1L); assertThat(result.getContent()).hasSize(1); assertThat(result.getTotalElements()).isEqualTo(1); assertThat(result.getTotalPages()).isEqualTo(1); From e189d1ff055388aced4097c9913d1f4b5c76d81f Mon Sep 17 00:00:00 2001 From: GarakChoi Date: Mon, 13 Oct 2025 14:34:51 +0900 Subject: [PATCH 02/13] fix[post]:myposts rollback --- .../ai/lawyer/domain/post/controller/PostControllerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/test/java/com/ai/lawyer/domain/post/controller/PostControllerTest.java b/backend/src/test/java/com/ai/lawyer/domain/post/controller/PostControllerTest.java index d7a163f..3c2011f 100644 --- a/backend/src/test/java/com/ai/lawyer/domain/post/controller/PostControllerTest.java +++ b/backend/src/test/java/com/ai/lawyer/domain/post/controller/PostControllerTest.java @@ -220,7 +220,7 @@ void t10_paged() throws Exception { ); org.springframework.data.domain.PageImpl page = new org.springframework.data.domain.PageImpl<>(posts, pageable, 1); Mockito.when(postService.getMyPostspaged(Mockito.any(), Mockito.anyLong())).thenReturn(page); - mockMvc.perform(get("/api/posts/my") + mockMvc.perform(get("/api/posts/mypaged") .param("page", "0") .param("size", "10") .cookie(new Cookie("accessToken", "valid-access-token"))) From 18aa050170cea8cc1b2f207eab004a1211913c46 Mon Sep 17 00:00:00 2001 From: yongho9064 Date: Mon, 13 Oct 2025 15:22:51 +0900 Subject: [PATCH 03/13] =?UTF-8?q?feat[chat]:=20kafka=20=EB=B9=84=EB=8F=99?= =?UTF-8?q?=EA=B8=B0=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(=EC=B1=84=ED=8C=85=20=EA=B8=B0=EB=A1=9D,?= =?UTF-8?q?=20=ED=82=A4=EC=9B=8C=EB=93=9C=20=EC=B6=94=EC=B6=9C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chatbot/service/ChatBotService.java | 41 +++++- .../consumer/ChatPostProcessingConsumer.java | 134 ++++++++++++++++++ .../kafka/dto/ChatPostProcessEvent.java | 18 +++ .../lawyer/domain/kafka/dto/DocumentDto.java | 20 +++ 4 files changed, 206 insertions(+), 7 deletions(-) create mode 100644 backend/src/main/java/com/ai/lawyer/domain/kafka/consumer/ChatPostProcessingConsumer.java create mode 100644 backend/src/main/java/com/ai/lawyer/domain/kafka/dto/ChatPostProcessEvent.java create mode 100644 backend/src/main/java/com/ai/lawyer/domain/kafka/dto/DocumentDto.java 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 928e8d6..7231c9d 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,8 @@ 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.domain.kafka.dto.ChatPostProcessEvent; +import com.ai.lawyer.domain.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; @@ -22,6 +24,7 @@ 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; @@ -39,18 +42,21 @@ 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 호출 (스트림) -> (비동기 후처리) -> 응답 반환 + // 멤버 조회 -> 벡터 검색 -> 프롬프트 생성 -> LLM 호출 (스트림) -> Kafka 이벤트 발행 -> 응답 반환 @Transactional public Flux sendMessage(Long memberId, ChatRequest chatRequestDto, Long roomId) { @@ -79,10 +85,31 @@ public Flux sendMessage(Long memberId, ChatRequest chatRequestDto, .content() .collectList() .map(fullResponseList -> String.join("", fullResponseList)) - .doOnNext(fullResponse -> asyncPostChatProcessingService.processHandlerTasks(history.getHistoryId(), chatRequestDto.getMessage(), fullResponse, similarCaseDocuments, similarLawDocuments)) // 비동기 후처리 + .doOnNext(fullResponse -> { + + // Document를 DTO로 변환 + List caseDtos = similarCaseDocuments.stream().map(DocumentDto::from).collect(Collectors.toList()); + List lawDtos = similarLawDocuments.stream().map(DocumentDto::from).collect(Collectors.toList()); + + // Kafka로 보낼 이벤트 객체 + ChatPostProcessEvent event = new ChatPostProcessEvent( + history.getHistoryId(), + chatRequestDto.getMessage(), + fullResponse, + caseDtos, + lawDtos + ); + + // Kafka 이벤트 발행 + kafkaTemplate.send(POST_PROCESSING_TOPIC, event); + + }) .map(fullResponse -> createChatResponse(history, fullResponse, similarCaseDocuments, similarLawDocuments)) .flux() - .onErrorResume(throwable -> Flux.just(handleError(history))); + .onErrorResume(throwable -> { + log.error("스트리밍 처리 중 에러 발생 (historyId: {})", history.getHistoryId(), throwable); + return Flux.just(handleError(history)); + }); } private ChatResponse createChatResponse(History history, String fullResponse, List cases, List laws) { @@ -111,7 +138,7 @@ private ChatMemory saveChatMemory(ChatRequest chatRequestDto, History history) { .maxMessages(10) .chatMemoryRepository(chatMemoryRepository) .build(); - // 사용자 메시지를 메모리에 추가 -> ai 답변은 비동기 후처리에서 추가 + // 사용자 메시지를 메모리에 추가 -> ai 답변은 Consumer에서 추가 chatMemory.add(String.valueOf(history.getHistoryId()), new UserMessage(chatRequestDto.getMessage())); return chatMemory; } diff --git a/backend/src/main/java/com/ai/lawyer/domain/kafka/consumer/ChatPostProcessingConsumer.java b/backend/src/main/java/com/ai/lawyer/domain/kafka/consumer/ChatPostProcessingConsumer.java new file mode 100644 index 0000000..cc9f271 --- /dev/null +++ b/backend/src/main/java/com/ai/lawyer/domain/kafka/consumer/ChatPostProcessingConsumer.java @@ -0,0 +1,134 @@ +package com.ai.lawyer.domain.kafka.consumer; + +import com.ai.lawyer.domain.chatbot.dto.ExtractionDto.KeywordExtractionDto; +import com.ai.lawyer.domain.chatbot.dto.ExtractionDto.TitleExtractionDto; +import com.ai.lawyer.domain.chatbot.entity.*; +import com.ai.lawyer.domain.chatbot.repository.*; +import com.ai.lawyer.domain.chatbot.service.KeywordService; +import com.ai.lawyer.domain.kafka.dto.ChatPostProcessEvent; +import com.ai.lawyer.domain.kafka.dto.DocumentDto; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.chat.memory.ChatMemory; +import org.springframework.ai.chat.memory.ChatMemoryRepository; +import org.springframework.ai.chat.memory.MessageWindowChatMemory; +import org.springframework.ai.chat.messages.AssistantMessage; +import org.springframework.ai.chat.messages.MessageType; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ChatPostProcessingConsumer { + + private final KeywordService keywordService; + private final HistoryRepository historyRepository; + private final ChatRepository chatRepository; + private final KeywordRankRepository keywordRankRepository; + private final ChatMemoryRepository chatMemoryRepository; + private final ChatPrecedentRepository chatPrecedentRepository; + private final ChatLawRepository chatLawRepository; + + @Value("${custom.ai.title-extraction}") + private String titleExtraction; + @Value("${custom.ai.keyword-extraction}") + private String keywordExtraction; + + @KafkaListener(topics = "chat-post-processing", groupId = "chat-processing-group") + @Transactional + public void consume(ChatPostProcessEvent event) { + try { + History history = historyRepository.findById(event.getHistoryId()) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 채팅방입니다. historyId: " + event.getHistoryId())); + + // 1. 메시지 기억 저장 (Assistant 응답) + ChatMemory chatMemory = MessageWindowChatMemory.builder() + .maxMessages(10) + .chatMemoryRepository(chatMemoryRepository) + .build(); + + chatMemory.add(String.valueOf(history.getHistoryId()), new AssistantMessage(event.getChatResponse())); + chatMemoryRepository.saveAll(String.valueOf(history.getHistoryId()), chatMemory.get(String.valueOf(history.getHistoryId()))); + + // 2. 채팅방 제목 설정 / 및 필터 + setHistoryTitle(event.getUserMessage(), history, event.getChatResponse()); + + // 3. 채팅 기록 저장 + saveChatWithDocuments(history, MessageType.USER, event.getUserMessage(), event.getSimilarCaseDocuments(), event.getSimilarLawDocuments()); + saveChatWithDocuments(history, MessageType.ASSISTANT, event.getChatResponse(), event.getSimilarCaseDocuments(), event.getSimilarLawDocuments()); + + // 4. 키워드 추출 및 랭킹 업데이트 + if (!event.getChatResponse().contains("해당 질문은 법률")) { + extractAndUpdateKeywordRanks(event.getUserMessage()); + } + } catch (Exception e) { + log.error("Kafka 이벤트 처리 중 에러 발생 (historyId: {}): ", event.getHistoryId(), e); + } + } + + private void setHistoryTitle(String userMessage, History history, String fullResponse) { + String targetText = fullResponse.contains("해당 질문은 법률") ? userMessage : fullResponse; + TitleExtractionDto titleDto = keywordService.keywordExtract(targetText, titleExtraction, TitleExtractionDto.class); + history.setTitle(titleDto.getTitle()); + historyRepository.save(history); + } + + private void extractAndUpdateKeywordRanks(String message) { + KeywordExtractionDto keywordResponse = keywordService.keywordExtract(message, keywordExtraction, KeywordExtractionDto.class); + if (keywordResponse == null || keywordResponse.getKeyword() == null) { + return; + } + + KeywordRank keywordRank = keywordRankRepository.findByKeyword(keywordResponse.getKeyword()); + + if (keywordRank == null) { + keywordRank = KeywordRank.builder() + .keyword(keywordResponse.getKeyword()) + .score(1L) + .build(); + } else { + keywordRank.setScore(keywordRank.getScore() + 1); + } + keywordRankRepository.save(keywordRank); + } + + private void saveChatWithDocuments(History history, MessageType type, String message, List similarCaseDocuments, List similarLawDocuments) { + Chat chat = chatRepository.save(Chat.builder() + .historyId(history) + .type(type) + .message(message) + .build()); + + // Ai 메시지가 저장될 때 관련 문서 저장 + if (type == MessageType.ASSISTANT) { + if (similarCaseDocuments != null && !similarCaseDocuments.isEmpty()) { + List chatPrecedents = similarCaseDocuments.stream() + .map(doc -> ChatPrecedent.builder() + .chatId(chat) + .precedentContent(doc.getText()) + .caseNumber(doc.getMetadata().get("caseNumber").toString()) + .caseName(doc.getMetadata().get("caseName").toString()) + .build()) + .collect(Collectors.toList()); + chatPrecedentRepository.saveAll(chatPrecedents); + } + + if (similarLawDocuments != null && !similarLawDocuments.isEmpty()) { + List chatLaws = similarLawDocuments.stream() + .map(doc -> ChatLaw.builder() + .chatId(chat) + .content(doc.getText()) + .lawName(doc.getMetadata().get("lawName").toString()) + .build()) + .collect(Collectors.toList()); + chatLawRepository.saveAll(chatLaws); + } + } + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/ai/lawyer/domain/kafka/dto/ChatPostProcessEvent.java b/backend/src/main/java/com/ai/lawyer/domain/kafka/dto/ChatPostProcessEvent.java new file mode 100644 index 0000000..812a0cb --- /dev/null +++ b/backend/src/main/java/com/ai/lawyer/domain/kafka/dto/ChatPostProcessEvent.java @@ -0,0 +1,18 @@ +package com.ai.lawyer.domain.kafka.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ChatPostProcessEvent { + private Long historyId; + private String userMessage; + private String chatResponse; + private List similarCaseDocuments; + private List similarLawDocuments; +} diff --git a/backend/src/main/java/com/ai/lawyer/domain/kafka/dto/DocumentDto.java b/backend/src/main/java/com/ai/lawyer/domain/kafka/dto/DocumentDto.java new file mode 100644 index 0000000..88d984e --- /dev/null +++ b/backend/src/main/java/com/ai/lawyer/domain/kafka/dto/DocumentDto.java @@ -0,0 +1,20 @@ +package com.ai.lawyer.domain.kafka.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.ai.document.Document; + +import java.util.Map; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class DocumentDto { + private String text; + private Map metadata; + + public static DocumentDto from(Document document) { + return new DocumentDto(document.getText(), document.getMetadata()); + } +} \ No newline at end of file From a1b342b7a6aea0d13c22a3a09bf46d587a18948e Mon Sep 17 00:00:00 2001 From: yongho9064 Date: Mon, 13 Oct 2025 15:23:45 +0900 Subject: [PATCH 04/13] =?UTF-8?q?chore[prompt]:=20=ED=82=A4=EC=9B=8C?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EC=B6=9C=20=ED=94=84=EB=A1=AC=ED=94=84?= =?UTF-8?q?=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/main/resources/system-prompt.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/resources/system-prompt.yml b/backend/src/main/resources/system-prompt.yml index 91139cf..b48819a 100644 --- a/backend/src/main/resources/system-prompt.yml +++ b/backend/src/main/resources/system-prompt.yml @@ -40,7 +40,7 @@ custom: [ { "id": 1, - "keyword": "폭행" + "keyword": "층간소음 폭행" "socre": 1 } ] From 59bc9c5dc5b8c5db4bb5ae1dc56b3ea615c5d2ac Mon Sep 17 00:00:00 2001 From: yongho9064 Date: Mon, 13 Oct 2025 15:24:15 +0900 Subject: [PATCH 05/13] =?UTF-8?q?docker:=20kafka=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/docker-compose.yml | 40 ++++++++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml index 6726a09..60c3af7 100644 --- a/backend/docker-compose.yml +++ b/backend/docker-compose.yml @@ -17,14 +17,14 @@ services: volumes: - mysql-data:/var/lib/mysql - ./sql:/sql # 호스트 ./sql 폴더를 컨테이너 /sql에 마운트 - command: > + command: > --character-set-server=utf8mb4 --collation-server=utf8mb4_0900_ai_ci --default-time-zone=Asia/Seoul --skip-log-bin --lower-case-table-names=1 healthcheck: - test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-p${DEV_DATASOURCE_PASSWORD}"] + test: [ "CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-p${DEV_DATASOURCE_PASSWORD}" ] interval: 10s timeout: 5s retries: 10 @@ -40,7 +40,7 @@ services: command: > redis-server --appendonly yes healthcheck: - test: ["CMD", "redis-cli", "ping"] + test: [ "CMD", "redis-cli", "ping" ] interval: 10s timeout: 5s retries: 10 @@ -78,8 +78,40 @@ services: timeout: 5s retries: 10 + zookeeper: + image: confluentinc/cp-zookeeper:7.4.4 + container_name: zookeeper + restart: unless-stopped + ports: + - "2181:2181" + environment: + ZOOKEEPER_CLIENT_PORT: 2181 + ZOOKEEPER_TICK_TIME: 2000 + + kafka: + image: confluentinc/cp-kafka:7.4.4 + container_name: kafka + restart: unless-stopped + depends_on: + - zookeeper + ports: + - "9092:9092" + environment: + KAFKA_BROKER_ID: 1 + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT + KAFKA_LISTENERS: INTERNAL://0.0.0.0:29092,EXTERNAL://0.0.0.0:9092 + KAFKA_ADVERTISED_LISTENERS: INTERNAL://kafka:29092,EXTERNAL://localhost:9092 + KAFKA_INTER_BROKER_LISTENER_NAME: INTERNAL + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 + volumes: + - kafka-data:/var/lib/kafka/data + volumes: mysql-data: redis-data: qdrant-data: - ollama-data: \ No newline at end of file + ollama-data: + kafka-data: \ No newline at end of file From 2f86406adfd8500e4becf195738f18de5148ecb4 Mon Sep 17 00:00:00 2001 From: yongho9064 Date: Mon, 13 Oct 2025 15:24:53 +0900 Subject: [PATCH 06/13] =?UTF-8?q?chore[chat]:=20AsyncPostChatProcessingSer?= =?UTF-8?q?vice.java=20=EB=B9=84=ED=99=9C=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chatbot/service/AsyncPostChatProcessingService.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/backend/src/main/java/com/ai/lawyer/domain/chatbot/service/AsyncPostChatProcessingService.java b/backend/src/main/java/com/ai/lawyer/domain/chatbot/service/AsyncPostChatProcessingService.java index 419371a..df6efef 100644 --- a/backend/src/main/java/com/ai/lawyer/domain/chatbot/service/AsyncPostChatProcessingService.java +++ b/backend/src/main/java/com/ai/lawyer/domain/chatbot/service/AsyncPostChatProcessingService.java @@ -13,13 +13,14 @@ import org.springframework.ai.chat.messages.MessageType; import org.springframework.ai.document.Document; import org.springframework.beans.factory.annotation.Value; -import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.stream.Collectors; +// 더이상 사용 안함 +// 테스트 용도로 남겨둠 @Slf4j @Service @RequiredArgsConstructor @@ -38,7 +39,7 @@ public class AsyncPostChatProcessingService { @Value("{$custom.ai.keyword-extraction}") private String keywordExtraction; - @Async + //@Async @Transactional public void processHandlerTasks(Long historyId, String userMessage, String fullResponse, List similarCaseDocuments, List similarLawDocuments) { try { @@ -74,7 +75,7 @@ private void setHistoryTitle(String userMessage, History history, String fullRes String targetText = fullResponse.contains("해당 질문은 법률") ? userMessage : fullResponse; TitleExtractionDto titleDto = keywordService.keywordExtract(targetText, titleExtraction, TitleExtractionDto.class); history.setTitle(titleDto.getTitle()); - historyRepository.save(history); // @Transactional 어노테이션으로 인해 메소드 종료 시 자동 저장되지만, 명시적으로 호출할 수도 있습니다. + historyRepository.save(history); } private void extractAndUpdateKeywordRanks(String message) { From af4c135973ab81ad769012201bc6952a90895bfc Mon Sep 17 00:00:00 2001 From: yongho9064 Date: Mon, 13 Oct 2025 15:25:22 +0900 Subject: [PATCH 07/13] =?UTF-8?q?config:=20kafka=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/build.gradle | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/build.gradle b/backend/build.gradle index ee05421..ece81c8 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -41,6 +41,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' implementation group: 'org.springframework.boot', name: 'spring-boot-starter-mail', version: '3.0.5' implementation 'org.springframework.boot:spring-boot-starter-batch' + implementation 'org.springframework.kafka:spring-kafka' + testImplementation 'org.springframework.kafka:spring-kafka-test' // API Documentation (문서화) implementation 'org.apache.commons:commons-lang3:3.18.0' From 9161ed7fce9e9e5c4aca928302c559a27f020081 Mon Sep 17 00:00:00 2001 From: yongho9064 Date: Mon, 13 Oct 2025 15:25:56 +0900 Subject: [PATCH 08/13] =?UTF-8?q?config:=20kafka=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/application-dev.yml | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/backend/src/main/resources/application-dev.yml b/backend/src/main/resources/application-dev.yml index 9320def..ff9d6f6 100644 --- a/backend/src/main/resources/application-dev.yml +++ b/backend/src/main/resources/application-dev.yml @@ -17,12 +17,32 @@ spring: password: ${DEV_REDIS_PASSWORD} embedded: false + kafka: + bootstrap-servers: localhost:9092 + producer: + key-serializer: org.apache.kafka.common.serialization.StringSerializer + value-serializer: org.springframework.kafka.support.serializer.JsonSerializer + consumer: + group-id: chat-processing-group # 컨슈머 그룹 ID + key-deserializer: org.apache.kafka.common.serialization.StringDeserializer + value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer + properties: + spring.json.trusted.packages: "*" + batch: job: enabled: false # 최소 한번 시작 jdbc: initialize-schema: always + task: + scheduling: + shutdown: + await-termination: true + await-termination-period: 60s + lifecycle: + timeout-per-shutdown-phase: 60s + datasource-meta: driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://localhost:3306/meta_db?useSSL=false&useUnicode=true&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true&createDatabaseIfNotExist=true From 0d0cdec6d5fa349301eee0886609790d0b5f7814 Mon Sep 17 00:00:00 2001 From: asowjdan Date: Mon, 13 Oct 2025 19:44:21 +0900 Subject: [PATCH 09/13] =?UTF-8?q?fix[member]:=EC=BF=A0=ED=82=A4=20?= =?UTF-8?q?=EC=9C=A0=ED=9A=A8=EC=8B=9C=EA=B0=84=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20=EC=BF=A0=ED=82=A4=20=EC=83=9D=EC=84=B1=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/.env.default | 2 ++ .../com/ai/lawyer/global/jwt/CookieUtil.java | 28 ++++++++++++++++--- .../src/main/resources/application-dev.yml | 2 ++ .../src/main/resources/application-prod.yml | 2 ++ backend/src/main/resources/application.yml | 2 ++ .../ai/lawyer/global/jwt/CookieUtilTest.java | 12 ++++---- 6 files changed, 38 insertions(+), 10 deletions(-) diff --git a/backend/.env.default b/backend/.env.default index b476653..687dd51 100644 --- a/backend/.env.default +++ b/backend/.env.default @@ -77,3 +77,5 @@ CUSTOM_CORS_ALLOWED_ORIGINS=NEED_TO_SET CUSTOM_OAUTH2_REDIRECT_URL=NEED_TO_SET CUSTOM_OAUTH2_FAILURE_URL=NEED_TO_SET CUSTOM_FRONTEND_URL=NEED_TO_SET +PROD_COOKIE_DOMAIN=NEED_TO_SET +DEV_COOKIE_DOMAIN=NEED_TO_SET diff --git a/backend/src/main/java/com/ai/lawyer/global/jwt/CookieUtil.java b/backend/src/main/java/com/ai/lawyer/global/jwt/CookieUtil.java index a498ae6..8de82fd 100644 --- a/backend/src/main/java/com/ai/lawyer/global/jwt/CookieUtil.java +++ b/backend/src/main/java/com/ai/lawyer/global/jwt/CookieUtil.java @@ -3,11 +3,14 @@ import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.ResponseCookie; import org.springframework.stereotype.Component; import java.time.Duration; +@Slf4j @Component public class CookieUtil { @@ -18,7 +21,7 @@ public class CookieUtil { // 쿠키 만료 시간 상수 (초 단위) private static final int MINUTES_PER_HOUR = 60; private static final int HOURS_PER_DAY = 24; - private static final int ACCESS_TOKEN_EXPIRE_TIME = 5 * 60; // 5분 (300초) + private static final int ACCESS_TOKEN_EXPIRE_TIME = 60 * 60; // 5분 (300초) private static final int REFRESH_TOKEN_EXPIRE_TIME = 7 * HOURS_PER_DAY * MINUTES_PER_HOUR * 60; // 7일 // 쿠키 보안 설정 상수 @@ -28,6 +31,9 @@ public class CookieUtil { private static final String SAME_SITE = "Lax"; // Lax: 같은 사이트 요청에서 쿠키 전송 허용 private static final int COOKIE_EXPIRE_IMMEDIATELY = 0; + @Value("${custom.cookie.domain:}") + private String cookieDomain; + public void setTokenCookies(HttpServletResponse response, String accessToken, String refreshToken) { setAccessTokenCookie(response, accessToken); setRefreshTokenCookie(response, refreshToken); @@ -52,13 +58,27 @@ public void clearTokenCookies(HttpServletResponse response) { * ResponseCookie를 생성합니다 (SameSite 지원). */ private ResponseCookie createResponseCookie(String name, String value, int maxAge) { - return ResponseCookie.from(name, value) + log.debug("=== 쿠키 생성 중: name={}, cookieDomain='{}', isEmpty={}", + name, cookieDomain, cookieDomain == null || cookieDomain.isEmpty()); + + ResponseCookie.ResponseCookieBuilder builder = ResponseCookie.from(name, value) .httpOnly(HTTP_ONLY) .secure(SECURE_IN_PRODUCTION) .path(COOKIE_PATH) .maxAge(Duration.ofSeconds(maxAge)) - .sameSite(SAME_SITE) - .build(); + .sameSite(SAME_SITE); + + // 도메인이 설정되어 있으면 추가 + if (cookieDomain != null && !cookieDomain.isEmpty()) { + log.debug("쿠키 도메인 설정: {}", cookieDomain); + builder.domain(cookieDomain); + } else { + log.debug("쿠키 도메인 설정 안 함 (빈 값 또는 null)"); + } + + ResponseCookie cookie = builder.build(); + log.debug("생성된 쿠키: {}", cookie); + return cookie; } /** diff --git a/backend/src/main/resources/application-dev.yml b/backend/src/main/resources/application-dev.yml index ff9d6f6..efdd512 100644 --- a/backend/src/main/resources/application-dev.yml +++ b/backend/src/main/resources/application-dev.yml @@ -70,3 +70,5 @@ custom: failure-url: ${DEV_OAUTH2_FAILURE_REDIRECT_URL} frontend: url: ${DEV_FRONTEND_URL} + cookie: + domain: ${DEV_COOKIE_DOMAIN} diff --git a/backend/src/main/resources/application-prod.yml b/backend/src/main/resources/application-prod.yml index f9e31f0..0a5af98 100644 --- a/backend/src/main/resources/application-prod.yml +++ b/backend/src/main/resources/application-prod.yml @@ -63,6 +63,8 @@ custom: failure-url: ${PROD_OAUTH2_FAILURE_REDIRECT_URL} frontend: url: ${PROD_FRONTEND_URL} + cookie: + domain: ${PROD_COOKIE_DOMAIN:.trybalaw.com} # 운영환경: 모든 서브도메인에서 쿠키 공유 sentry: dsn: ${PROD_SENTRY_DSN} diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index fb9187f..1e2e258 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -155,3 +155,5 @@ custom: failure-url: ${CUSTOM_OAUTH2_FAILURE_URL} frontend: url: ${CUSTOM_FRONTEND_URL} + cookie: + domain: diff --git a/backend/src/test/java/com/ai/lawyer/global/jwt/CookieUtilTest.java b/backend/src/test/java/com/ai/lawyer/global/jwt/CookieUtilTest.java index d5ce7b0..739b637 100644 --- a/backend/src/test/java/com/ai/lawyer/global/jwt/CookieUtilTest.java +++ b/backend/src/test/java/com/ai/lawyer/global/jwt/CookieUtilTest.java @@ -70,7 +70,7 @@ void setTokenCookies_Success() { assertThat(accessCookieHeader).contains(ACCESS_TOKEN_NAME + "=" + ACCESS_TOKEN); assertThat(accessCookieHeader).contains("HttpOnly"); assertThat(accessCookieHeader).contains("Path=/"); - assertThat(accessCookieHeader).contains("Max-Age=300"); // 5분 = 300초 + assertThat(accessCookieHeader).contains("Max-Age=3600"); // 1시간 = 3600초 assertThat(accessCookieHeader).contains("SameSite=Lax"); log.info("액세스 토큰 쿠키 검증 완료: {}", accessCookieHeader); @@ -102,7 +102,7 @@ void setAccessTokenCookie_Success() { String cookieHeader = headerCaptor.getValue(); assertThat(cookieHeader).contains(ACCESS_TOKEN_NAME + "=" + ACCESS_TOKEN); assertThat(cookieHeader).contains("HttpOnly"); - assertThat(cookieHeader).contains("Max-Age=300"); + assertThat(cookieHeader).contains("Max-Age=3600"); assertThat(cookieHeader).contains("SameSite=Lax"); log.info("=== 액세스 토큰 단독 쿠키 설정 테스트 완료 ==="); } @@ -304,11 +304,11 @@ void cookiePathAttribute_Accessibility() { } @Test - @DisplayName("토큰 만료 시간 확인 - 액세스 5분, 리프레시 7일") + @DisplayName("토큰 만료 시간 확인 - 액세스 1시간, 리프레시 7일") void cookieMaxAgeAttribute_ExpiryTime() { // given log.info("=== 토큰 만료 시간 테스트 시작 ==="); - log.info("액세스 토큰 만료: 5분 (300초)"); + log.info("액세스 토큰 만료: 1시간 (3600초)"); log.info("리프레시 토큰 만료: 7일 (604800초)"); // when @@ -321,8 +321,8 @@ void cookieMaxAgeAttribute_ExpiryTime() { var setCookieHeaders = headerCaptor.getAllValues(); String accessHeader = setCookieHeaders.getFirst(); - assertThat(accessHeader).contains("Max-Age=300"); - log.info("액세스 토큰 만료 시간: 300초 (5분)"); + assertThat(accessHeader).contains("Max-Age=3600"); + log.info("액세스 토큰 만료 시간: 3600초 (1시간)"); String refreshHeader = setCookieHeaders.get(1); assertThat(refreshHeader).contains("Max-Age=604800"); From 2d3fb31bc53e75fdf8e35115c673d4b2eeabbfb0 Mon Sep 17 00:00:00 2001 From: DooHyoJeong Date: Fri, 10 Oct 2025 10:41:41 +0900 Subject: [PATCH 10/13] =?UTF-8?q?chore[infra]:=20actuator=20=ED=99=95?= =?UTF-8?q?=EC=9D=B8=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- infra/.gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/infra/.gitignore b/infra/.gitignore index 6d796f5..99d8fcf 100644 --- a/infra/.gitignore +++ b/infra/.gitignore @@ -5,4 +5,5 @@ terraform.tfstate terraform.tfstate.backup .terraform.tfstate.lock.info secrets.tf -init/sql \ No newline at end of file +init/sql +init/qdrant \ No newline at end of file From 893d02da726d0039b9e9ef24a6a822252ca53b33 Mon Sep 17 00:00:00 2001 From: DooHyoJeong Date: Fri, 10 Oct 2025 17:19:02 +0900 Subject: [PATCH 11/13] =?UTF-8?q?chore[infra]:=20ai=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- infra/main.tf | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/infra/main.tf b/infra/main.tf index 75b2b0b..06909cd 100644 --- a/infra/main.tf +++ b/infra/main.tf @@ -317,6 +317,7 @@ mkdir -p /home/ec2-user/app/init/sql/dev aws s3 cp s3://${var.prefix}-s3-bucket-1/init.sql /home/ec2-user/app/init/sql/init.sql aws s3 cp s3://${var.prefix}-s3-bucket-1/lawData-dev.sql /home/ec2-user/app/init/sql/dev/lawData-dev.sql aws s3 cp s3://${var.prefix}-s3-bucket-1/precedentData-dev.sql /home/ec2-user/app/init/sql/dev/precedentData-dev.sql +aws s3 cp s3://${var.prefix}-s3-bucket-1/legal_cases.snapshot /home/ec2-user/app/init/qdrant/snapshot/legal_cases.snapshot # MySQL 설정 폴더 생성 및 UTF8 설정 mkdir -p /dockerProjects/mysql/volumes/etc/mysql/conf.d @@ -325,6 +326,7 @@ cat < /dockerProjects/mysql/volumes/etc/mysql/conf.d/charset.cnf [mysqld] character-set-server = utf8mb4 collation-server = utf8mb4_general_ci +lower_case_table_names=1 [client] default-character-set = utf8mb4 @@ -370,12 +372,17 @@ docker exec -i mysql mysql -uroot -p${var.password_1} ${var.app_1_db_name} < /ho docker exec -i mysql mysql -uroot -p${var.password_1} ${var.app_1_db_name} < /home/ec2-user/app/init/sql/dev/precedentData-dev.sql # Qdrant 설치 +mkdir -p /qdrant/snapshots/legal_cases +aws s3 cp s3://${var.prefix}-s3-bucket-1/legal_cases.snapshot /qdrant/snapshots/legal_cases/legal_cases.snapshot + docker run -d \ --name qdrant \ --restart unless-stopped \ --network common \ -p 6333:6333 \ -p 6334:6334 \ + -v /qdrant/storage:/qdrant/storage \ + -v /qdrant/snapshots:/qdrant/snapshots \ qdrant/qdrant # Qdrant healthcheck 대기 @@ -440,7 +447,7 @@ resource "aws_instance" "ec2_1" { # 사용할 AMI ID ami = data.aws_ami.latest_amazon_linux.id # EC2 인스턴스 유형 - instance_type = "t3.micro" + instance_type = "t3.small" # 사용할 서브넷 ID subnet_id = aws_subnet.subnet_2.id # 적용할 보안 그룹 ID From dde199bc0d3311a0065dd2e1c9ee2a7c583be127 Mon Sep 17 00:00:00 2001 From: DooHyoJeong Date: Mon, 13 Oct 2025 20:25:44 +0900 Subject: [PATCH 12/13] =?UTF-8?q?chore[infra]:=20=EB=B0=B0=ED=8F=AC=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- infra/main.tf | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/infra/main.tf b/infra/main.tf index 06909cd..c7dfdfb 100644 --- a/infra/main.tf +++ b/infra/main.tf @@ -165,14 +165,14 @@ resource "aws_s3_object" "init_data_sql" { resource "aws_s3_object" "law_data_sql" { bucket = aws_s3_bucket.s3_bucket_1.bucket - key = "lawData-dev.sql" - source = "${path.module}/init/sql/dev/lawData-dev.sql" + key = "lawData.sql" + source = "${path.module}/init/sql/prod/lawData.sql" } resource "aws_s3_object" "precedent_data_sql" { bucket = aws_s3_bucket.s3_bucket_1.bucket - key = "precedentData-dev.sql" - source = "${path.module}/init/sql/dev/precedentData-dev.sql" + key = "precedentData.sql" + source = "${path.module}/init/sql/prod/precedentData.sql" } # EC2 설정 시작 @@ -311,12 +311,12 @@ docker run -d \ # SQL 폴더 생성 -mkdir -p /home/ec2-user/app/init/sql/dev +mkdir -p /home/ec2-user/app/init/sql/prod # S3에서 SQL 파일 다운로드 aws s3 cp s3://${var.prefix}-s3-bucket-1/init.sql /home/ec2-user/app/init/sql/init.sql -aws s3 cp s3://${var.prefix}-s3-bucket-1/lawData-dev.sql /home/ec2-user/app/init/sql/dev/lawData-dev.sql -aws s3 cp s3://${var.prefix}-s3-bucket-1/precedentData-dev.sql /home/ec2-user/app/init/sql/dev/precedentData-dev.sql +aws s3 cp s3://${var.prefix}-s3-bucket-1/lawData.sql /home/ec2-user/app/init/sql/prod/lawData.sql +aws s3 cp s3://${var.prefix}-s3-bucket-1/precedentData.sql /home/ec2-user/app/init/sql/prod/precedentData.sql aws s3 cp s3://${var.prefix}-s3-bucket-1/legal_cases.snapshot /home/ec2-user/app/init/qdrant/snapshot/legal_cases.snapshot # MySQL 설정 폴더 생성 및 UTF8 설정 @@ -327,6 +327,12 @@ cat < /dockerProjects/mysql/volumes/etc/mysql/conf.d/charset.cnf character-set-server = utf8mb4 collation-server = utf8mb4_general_ci lower_case_table_names=1 +innodb_buffer_pool_size = 1G +innodb_flush_log_at_trx_commit = 2 +innodb_flush_method = O_DIRECT +tmp_table_size = 256M +max_heap_table_size = 256M +max_connections = 100 [client] default-character-set = utf8mb4 @@ -368,8 +374,8 @@ docker exec mysql mysql -uroot -p${var.password_1} -e " FLUSH PRIVILEGES; " -docker exec -i mysql mysql -uroot -p${var.password_1} ${var.app_1_db_name} < /home/ec2-user/app/init/sql/dev/lawData-dev.sql -docker exec -i mysql mysql -uroot -p${var.password_1} ${var.app_1_db_name} < /home/ec2-user/app/init/sql/dev/precedentData-dev.sql +docker exec -i mysql mysql -uroot -p${var.password_1} ${var.app_1_db_name} < /home/ec2-user/app/init/sql/prod/lawData.sql +docker exec -i mysql mysql -uroot -p${var.password_1} ${var.app_1_db_name} < /home/ec2-user/app/init/sql/prod/precedentData.sql # Qdrant 설치 mkdir -p /qdrant/snapshots/legal_cases From 1a432dbf1264c838f017c4aa163126b74a98dfcc Mon Sep 17 00:00:00 2001 From: DooHyoJeong Date: Mon, 13 Oct 2025 20:30:03 +0900 Subject: [PATCH 13/13] =?UTF-8?q?chore[infra]:=20=EB=B0=B0=ED=8F=AC=20?= =?UTF-8?q?=EC=B9=B4=ED=91=B8=EC=B9=B4=20=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/application-prod.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/backend/src/main/resources/application-prod.yml b/backend/src/main/resources/application-prod.yml index f9e31f0..6d934ed 100644 --- a/backend/src/main/resources/application-prod.yml +++ b/backend/src/main/resources/application-prod.yml @@ -33,6 +33,24 @@ spring: port: ${PROD_REDIS_PORT} password: ${PROD_REDIS_PASSWORD} embedded: false + kafka: + bootstrap-servers: kafka:9092 + producer: + key-serializer: org.apache.kafka.common.serialization.StringSerializer + value-serializer: org.springframework.kafka.support.serializer.JsonSerializer + consumer: + group-id: chat-processing-group # 컨슈머 그룹 ID + key-deserializer: org.apache.kafka.common.serialization.StringDeserializer + value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer + properties: + spring.json.trusted.packages: "*" + task: + scheduling: + shutdown: + await-termination: true + await-termination-period: 60s + lifecycle: + timeout-per-shutdown-phase: 60s security: oauth2: