From 869ce3bbd42c940bb501ba610b93cf278b3776d9 Mon Sep 17 00:00:00 2001 From: djeon Date: Fri, 24 Oct 2025 09:46:18 +0900 Subject: [PATCH] =?UTF-8?q?ai=20=EC=8B=A4=ED=96=89=ED=99=98=EA=B2=BD=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ai/logs/ai-service.log | 281 ++++++++++++++++++ .../com/unicorn/hgzero/ai/AiApplication.java | 1 + .../hgzero/ai/biz/domain/ExtractedTodo.java | 44 +++ .../ai/biz/domain/ProcessedTranscript.java | 86 ++++++ .../hgzero/ai/biz/domain/RelatedMinutes.java | 55 ++++ .../hgzero/ai/biz/domain/Suggestion.java | 87 ++++++ .../unicorn/hgzero/ai/biz/domain/Term.java | 54 ++++ .../hgzero/ai/biz/gateway/LlmGateway.java | 64 ++++ .../hgzero/ai/biz/gateway/SearchGateway.java | 39 +++ .../ai/biz/gateway/TranscriptGateway.java | 68 +++++ .../RelatedTranscriptSearchService.java | 48 +++ .../ai/biz/service/SectionSummaryService.java | 28 ++ .../ai/biz/service/SuggestionService.java | 69 +++++ .../ai/biz/service/TermDetectionService.java | 42 +++ .../biz/service/TermExplanationService.java | 54 ++++ .../ai/biz/service/TodoExtractionService.java | 47 +++ .../biz/service/TranscriptProcessService.java | 180 +++++++++++ .../RelatedTranscriptSearchUseCase.java | 22 ++ .../ai/biz/usecase/SectionSummaryUseCase.java | 18 ++ .../ai/biz/usecase/SuggestionUseCase.java | 30 ++ .../ai/biz/usecase/TermDetectionUseCase.java | 22 ++ .../biz/usecase/TermExplanationUseCase.java | 37 +++ .../ai/biz/usecase/TodoExtractionUseCase.java | 22 ++ .../biz/usecase/TranscriptProcessUseCase.java | 48 +++ .../ai/infra/config/SecurityConfig.java | 84 ++++++ .../hgzero/ai/infra/config/SwaggerConfig.java | 63 ++++ .../controller/ExplanationController.java | 74 +++++ .../infra/controller/RelationController.java | 63 ++++ .../infra/controller/SectionController.java | 51 ++++ .../controller/SuggestionController.java | 99 ++++++ .../ai/infra/controller/TermController.java | 80 +++++ .../ai/infra/controller/TodoController.java | 65 ++++ .../controller/TranscriptController.java | 86 ++++++ .../ai/infra/dto/common/DecisionItemDto.java | 32 ++ .../dto/common/DecisionSuggestionDto.java | 59 ++++ .../ai/infra/dto/common/DetectedTermDto.java | 42 +++ .../infra/dto/common/DiscussionItemDto.java | 32 ++ .../dto/common/DiscussionSuggestionDto.java | 47 +++ .../ai/infra/dto/common/ErrorResponseDto.java | 34 +++ .../ai/infra/dto/common/ExtractedTodoDto.java | 44 +++ .../ai/infra/dto/common/HighlightInfoDto.java | 37 +++ .../infra/dto/common/MeetingContextDto.java | 39 +++ .../infra/dto/common/PastDiscussionDto.java | 40 +++ .../dto/common/RealtimeSuggestionsDto.java | 29 ++ .../ai/infra/dto/common/ReferenceDto.java | 32 ++ .../infra/dto/common/RelatedProjectDto.java | 27 ++ .../dto/common/RelatedTranscriptDto.java | 55 ++++ .../ai/infra/dto/common/TextPositionDto.java | 27 ++ .../dto/common/TranscriptContentDto.java | 39 +++ .../request/DecisionSuggestionRequest.java | 31 ++ .../request/DiscussionSuggestionRequest.java | 31 ++ .../dto/request/SectionSummaryRequest.java | 30 ++ .../dto/request/TermDetectionRequest.java | 36 +++ .../dto/request/TodoExtractionRequest.java | 36 +++ .../dto/request/TranscriptProcessRequest.java | 48 +++ .../response/DecisionSuggestionResponse.java | 36 +++ .../DiscussionSuggestionResponse.java | 36 +++ .../response/RelatedTranscriptsResponse.java | 30 ++ .../dto/response/SectionSummaryResponse.java | 29 ++ .../dto/response/TermDetectionResponse.java | 36 +++ .../dto/response/TermExplanationResponse.java | 57 ++++ .../dto/response/TodoExtractionResponse.java | 41 +++ .../response/TranscriptProcessResponse.java | 51 ++++ .../infra/gateway/TranscriptGatewayImpl.java | 80 +++++ .../entity/ProcessedTranscriptEntity.java | 179 +++++++++++ .../ProcessedTranscriptJpaRepository.java | 41 +++ .../hgzero/ai/infra/llm/OpenAiLlmGateway.java | 146 +++++++++ .../ai/infra/search/AzureAiSearchGateway.java | 100 +++++++ 68 files changed, 3800 insertions(+) create mode 100644 ai/logs/ai-service.log create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/biz/domain/ExtractedTodo.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/biz/domain/ProcessedTranscript.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/biz/domain/RelatedMinutes.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/biz/domain/Suggestion.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/biz/domain/Term.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/biz/gateway/LlmGateway.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/biz/gateway/SearchGateway.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/biz/gateway/TranscriptGateway.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/biz/service/RelatedTranscriptSearchService.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/biz/service/SectionSummaryService.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/biz/service/SuggestionService.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/biz/service/TermDetectionService.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/biz/service/TermExplanationService.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/biz/service/TodoExtractionService.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/biz/service/TranscriptProcessService.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/biz/usecase/RelatedTranscriptSearchUseCase.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/biz/usecase/SectionSummaryUseCase.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/biz/usecase/SuggestionUseCase.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/biz/usecase/TermDetectionUseCase.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/biz/usecase/TermExplanationUseCase.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/biz/usecase/TodoExtractionUseCase.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/biz/usecase/TranscriptProcessUseCase.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/infra/config/SecurityConfig.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/infra/config/SwaggerConfig.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/infra/controller/ExplanationController.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/infra/controller/RelationController.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/infra/controller/SectionController.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/infra/controller/SuggestionController.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/infra/controller/TermController.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/infra/controller/TodoController.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/infra/controller/TranscriptController.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/DecisionItemDto.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/DecisionSuggestionDto.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/DetectedTermDto.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/DiscussionItemDto.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/DiscussionSuggestionDto.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/ErrorResponseDto.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/ExtractedTodoDto.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/HighlightInfoDto.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/MeetingContextDto.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/PastDiscussionDto.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/RealtimeSuggestionsDto.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/ReferenceDto.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/RelatedProjectDto.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/RelatedTranscriptDto.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/TextPositionDto.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/TranscriptContentDto.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/request/DecisionSuggestionRequest.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/request/DiscussionSuggestionRequest.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/request/SectionSummaryRequest.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/request/TermDetectionRequest.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/request/TodoExtractionRequest.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/request/TranscriptProcessRequest.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/response/DecisionSuggestionResponse.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/response/DiscussionSuggestionResponse.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/response/RelatedTranscriptsResponse.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/response/SectionSummaryResponse.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/response/TermDetectionResponse.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/response/TermExplanationResponse.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/response/TodoExtractionResponse.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/response/TranscriptProcessResponse.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/infra/gateway/TranscriptGatewayImpl.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/infra/gateway/entity/ProcessedTranscriptEntity.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/infra/gateway/repository/ProcessedTranscriptJpaRepository.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/infra/llm/OpenAiLlmGateway.java create mode 100644 ai/src/main/java/com/unicorn/hgzero/ai/infra/search/AzureAiSearchGateway.java diff --git a/ai/logs/ai-service.log b/ai/logs/ai-service.log new file mode 100644 index 0000000..b6155d9 --- /dev/null +++ b/ai/logs/ai-service.log @@ -0,0 +1,281 @@ +2025-10-24 09:23:35 [main] INFO com.unicorn.hgzero.ai.AiApplication - Starting AiApplication using Java 21.0.8 with PID 92971 (/Users/daewoong/home/workspace/HGZero/ai/build/classes/java/main started by daewoong in /Users/daewoong/home/workspace/HGZero/ai) +2025-10-24 09:23:35 [main] DEBUG com.unicorn.hgzero.ai.AiApplication - Running with Spring Boot v3.3.0, Spring v6.1.8 +2025-10-24 09:23:35 [main] INFO com.unicorn.hgzero.ai.AiApplication - No active profile set, falling back to 1 default profile: "default" +2025-10-24 09:23:36 [main] WARN o.s.b.w.s.c.AnnotationConfigServletWebServerApplicationContext - Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.support.BeanDefinitionOverrideException: Invalid bean definition with name 'jpaAuditingHandler' defined in null: Cannot register bean definition [Root bean: class [org.springframework.data.auditing.AuditingHandler]; scope=; abstract=false; lazyInit=null; autowireMode=2; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=from; initMethodNames=null; destroyMethodNames=null] for bean 'jpaAuditingHandler' since there is already [Root bean: class [org.springframework.data.auditing.AuditingHandler]; scope=; abstract=false; lazyInit=null; autowireMode=2; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=from; initMethodNames=null; destroyMethodNames=null] bound. +2025-10-24 09:23:36 [main] INFO o.s.b.a.l.ConditionEvaluationReportLogger - + +Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled. +2025-10-24 09:23:36 [main] ERROR o.s.b.d.LoggingFailureAnalysisReporter - + +*************************** +APPLICATION FAILED TO START +*************************** + +Description: + +The bean 'jpaAuditingHandler' could not be registered. A bean with that name has already been defined and overriding is disabled. + +Action: + +Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true + +2025-10-24 09:42:56 [main] INFO com.unicorn.hgzero.ai.AiApplication - Starting AiApplication using Java 21.0.8 with PID 93771 (/Users/daewoong/home/workspace/HGZero/ai/build/classes/java/main started by daewoong in /Users/daewoong/home/workspace/HGZero/ai) +2025-10-24 09:42:56 [main] DEBUG com.unicorn.hgzero.ai.AiApplication - Running with Spring Boot v3.3.5, Spring v6.1.14 +2025-10-24 09:42:56 [main] INFO com.unicorn.hgzero.ai.AiApplication - No active profile set, falling back to 1 default profile: "default" +2025-10-24 09:42:56 [main] WARN o.s.b.w.s.c.AnnotationConfigServletWebServerApplicationContext - Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.support.BeanDefinitionOverrideException: Invalid bean definition with name 'jpaAuditingHandler' defined in null: Cannot register bean definition [Root bean: class [org.springframework.data.auditing.AuditingHandler]; scope=; abstract=false; lazyInit=null; autowireMode=2; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=from; initMethodNames=null; destroyMethodNames=null] for bean 'jpaAuditingHandler' since there is already [Root bean: class [org.springframework.data.auditing.AuditingHandler]; scope=; abstract=false; lazyInit=null; autowireMode=2; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=from; initMethodNames=null; destroyMethodNames=null] bound. +2025-10-24 09:42:56 [main] INFO o.s.b.a.l.ConditionEvaluationReportLogger - + +Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled. +2025-10-24 09:42:56 [main] ERROR o.s.b.d.LoggingFailureAnalysisReporter - + +*************************** +APPLICATION FAILED TO START +*************************** + +Description: + +The bean 'jpaAuditingHandler' could not be registered. A bean with that name has already been defined and overriding is disabled. + +Action: + +Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true + +2025-10-24 09:43:58 [main] INFO com.unicorn.hgzero.ai.AiApplication - Starting AiApplication using Java 21.0.8 with PID 93809 (/Users/daewoong/home/workspace/HGZero/ai/build/classes/java/main started by daewoong in /Users/daewoong/home/workspace/HGZero/ai) +2025-10-24 09:43:58 [main] DEBUG com.unicorn.hgzero.ai.AiApplication - Running with Spring Boot v3.3.5, Spring v6.1.14 +2025-10-24 09:43:58 [main] INFO com.unicorn.hgzero.ai.AiApplication - No active profile set, falling back to 1 default profile: "default" +2025-10-24 09:43:58 [main] INFO o.s.d.r.c.RepositoryConfigurationDelegate - Multiple Spring Data modules found, entering strict repository configuration mode +2025-10-24 09:43:58 [main] INFO o.s.d.r.c.RepositoryConfigurationDelegate - Bootstrapping Spring Data JPA repositories in DEFAULT mode. +2025-10-24 09:43:58 [main] INFO o.s.d.r.c.RepositoryConfigurationDelegate - Finished Spring Data repository scanning in 65 ms. Found 1 JPA repository interface. +2025-10-24 09:43:58 [main] INFO o.s.d.r.c.RepositoryConfigurationDelegate - Multiple Spring Data modules found, entering strict repository configuration mode +2025-10-24 09:43:58 [main] INFO o.s.d.r.c.RepositoryConfigurationDelegate - Bootstrapping Spring Data Redis repositories in DEFAULT mode. +2025-10-24 09:43:58 [main] INFO o.s.d.r.c.RepositoryConfigurationExtensionSupport - Spring Data Redis - Could not safely identify store assignment for repository candidate interface com.unicorn.hgzero.ai.infra.gateway.repository.ProcessedTranscriptJpaRepository; If you want this repository to be a Redis repository, consider annotating your entities with one of these annotations: org.springframework.data.redis.core.RedisHash (preferred), or consider extending one of the following types with your repository: org.springframework.data.keyvalue.repository.KeyValueRepository +2025-10-24 09:43:58 [main] INFO o.s.d.r.c.RepositoryConfigurationDelegate - Finished Spring Data repository scanning in 3 ms. Found 0 Redis repository interfaces. +2025-10-24 09:43:59 [main] INFO o.s.b.w.e.tomcat.TomcatWebServer - Tomcat initialized with port 8083 (http) +2025-10-24 09:43:59 [main] INFO o.a.catalina.core.StandardService - Starting service [Tomcat] +2025-10-24 09:43:59 [main] INFO o.a.catalina.core.StandardEngine - Starting Servlet engine: [Apache Tomcat/10.1.31] +2025-10-24 09:43:59 [main] INFO o.a.c.c.C.[Tomcat].[localhost].[/] - Initializing Spring embedded WebApplicationContext +2025-10-24 09:43:59 [main] INFO o.s.b.w.s.c.ServletWebServerApplicationContext - Root WebApplicationContext: initialization completed in 990 ms +2025-10-24 09:43:59 [main] INFO o.h.jpa.internal.util.LogHelper - HHH000204: Processing PersistenceUnitInfo [name: default] +2025-10-24 09:43:59 [main] INFO org.hibernate.Version - HHH000412: Hibernate ORM core version 6.5.3.Final +2025-10-24 09:43:59 [main] INFO o.h.c.i.RegionFactoryInitiator - HHH000026: Second-level cache disabled +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration boolean -> org.hibernate.type.BasicTypeReference@24c84e65 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration boolean -> org.hibernate.type.BasicTypeReference@24c84e65 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Boolean -> org.hibernate.type.BasicTypeReference@24c84e65 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration numeric_boolean -> org.hibernate.type.BasicTypeReference@7337bd2e +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration org.hibernate.type.NumericBooleanConverter -> org.hibernate.type.BasicTypeReference@7337bd2e +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration true_false -> org.hibernate.type.BasicTypeReference@4604e051 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration org.hibernate.type.TrueFalseConverter -> org.hibernate.type.BasicTypeReference@4604e051 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration yes_no -> org.hibernate.type.BasicTypeReference@4535bdc6 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration org.hibernate.type.YesNoConverter -> org.hibernate.type.BasicTypeReference@4535bdc6 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration byte -> org.hibernate.type.BasicTypeReference@23e86863 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration byte -> org.hibernate.type.BasicTypeReference@23e86863 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Byte -> org.hibernate.type.BasicTypeReference@23e86863 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration binary -> org.hibernate.type.BasicTypeReference@6df87ffd +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration byte[] -> org.hibernate.type.BasicTypeReference@6df87ffd +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration [B -> org.hibernate.type.BasicTypeReference@6df87ffd +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration binary_wrapper -> org.hibernate.type.BasicTypeReference@c1f0c7b +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration wrapper-binary -> org.hibernate.type.BasicTypeReference@c1f0c7b +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration image -> org.hibernate.type.BasicTypeReference@642c5bb3 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration blob -> org.hibernate.type.BasicTypeReference@4e79c25 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.sql.Blob -> org.hibernate.type.BasicTypeReference@4e79c25 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_blob -> org.hibernate.type.BasicTypeReference@2ace1cd3 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_blob_wrapper -> org.hibernate.type.BasicTypeReference@5e46a125 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration short -> org.hibernate.type.BasicTypeReference@5831989d +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration short -> org.hibernate.type.BasicTypeReference@5831989d +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Short -> org.hibernate.type.BasicTypeReference@5831989d +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration integer -> org.hibernate.type.BasicTypeReference@608f310a +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration int -> org.hibernate.type.BasicTypeReference@608f310a +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Integer -> org.hibernate.type.BasicTypeReference@608f310a +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration long -> org.hibernate.type.BasicTypeReference@3a7d914c +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration long -> org.hibernate.type.BasicTypeReference@3a7d914c +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Long -> org.hibernate.type.BasicTypeReference@3a7d914c +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration float -> org.hibernate.type.BasicTypeReference@515940af +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration float -> org.hibernate.type.BasicTypeReference@515940af +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Float -> org.hibernate.type.BasicTypeReference@515940af +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration double -> org.hibernate.type.BasicTypeReference@5f8df69 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration double -> org.hibernate.type.BasicTypeReference@5f8df69 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Double -> org.hibernate.type.BasicTypeReference@5f8df69 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration big_integer -> org.hibernate.type.BasicTypeReference@1ce6a9bd +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.math.BigInteger -> org.hibernate.type.BasicTypeReference@1ce6a9bd +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration big_decimal -> org.hibernate.type.BasicTypeReference@4a47bc9c +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.math.BigDecimal -> org.hibernate.type.BasicTypeReference@4a47bc9c +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration character -> org.hibernate.type.BasicTypeReference@5100c143 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration char -> org.hibernate.type.BasicTypeReference@5100c143 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Character -> org.hibernate.type.BasicTypeReference@5100c143 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration character_nchar -> org.hibernate.type.BasicTypeReference@12404f9d +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration string -> org.hibernate.type.BasicTypeReference@3b42b729 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.String -> org.hibernate.type.BasicTypeReference@3b42b729 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration nstring -> org.hibernate.type.BasicTypeReference@4c164f81 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration characters -> org.hibernate.type.BasicTypeReference@1bcb8599 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration char[] -> org.hibernate.type.BasicTypeReference@1bcb8599 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration [C -> org.hibernate.type.BasicTypeReference@1bcb8599 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration wrapper-characters -> org.hibernate.type.BasicTypeReference@b671dda +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration text -> org.hibernate.type.BasicTypeReference@25b20860 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration ntext -> org.hibernate.type.BasicTypeReference@5ba63110 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration clob -> org.hibernate.type.BasicTypeReference@1c0680b0 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.sql.Clob -> org.hibernate.type.BasicTypeReference@1c0680b0 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration nclob -> org.hibernate.type.BasicTypeReference@2f3cd727 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.sql.NClob -> org.hibernate.type.BasicTypeReference@2f3cd727 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_clob -> org.hibernate.type.BasicTypeReference@1af82ba8 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_clob_char_array -> org.hibernate.type.BasicTypeReference@703cb756 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_clob_character_array -> org.hibernate.type.BasicTypeReference@5897aae1 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_nclob -> org.hibernate.type.BasicTypeReference@11dbcb3b +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_nclob_character_array -> org.hibernate.type.BasicTypeReference@4aa517c3 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_nclob_char_array -> org.hibernate.type.BasicTypeReference@5f369fc6 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration Duration -> org.hibernate.type.BasicTypeReference@3a13f663 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.Duration -> org.hibernate.type.BasicTypeReference@3a13f663 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration LocalDateTime -> org.hibernate.type.BasicTypeReference@75de7009 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.LocalDateTime -> org.hibernate.type.BasicTypeReference@75de7009 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration LocalDate -> org.hibernate.type.BasicTypeReference@17a77a7e +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.LocalDate -> org.hibernate.type.BasicTypeReference@17a77a7e +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration LocalTime -> org.hibernate.type.BasicTypeReference@7c840fe3 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.LocalTime -> org.hibernate.type.BasicTypeReference@7c840fe3 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration OffsetDateTime -> org.hibernate.type.BasicTypeReference@59014efe +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.OffsetDateTime -> org.hibernate.type.BasicTypeReference@59014efe +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration OffsetDateTimeWithTimezone -> org.hibernate.type.BasicTypeReference@5f5923ef +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration OffsetDateTimeWithoutTimezone -> org.hibernate.type.BasicTypeReference@7381d6f0 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration OffsetTime -> org.hibernate.type.BasicTypeReference@2f262474 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.OffsetTime -> org.hibernate.type.BasicTypeReference@2f262474 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration OffsetTimeUtc -> org.hibernate.type.BasicTypeReference@7c03f9d0 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration OffsetTimeWithTimezone -> org.hibernate.type.BasicTypeReference@6ad3fbe4 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration OffsetTimeWithoutTimezone -> org.hibernate.type.BasicTypeReference@17189618 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration ZonedDateTime -> org.hibernate.type.BasicTypeReference@983050b +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.ZonedDateTime -> org.hibernate.type.BasicTypeReference@983050b +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration ZonedDateTimeWithTimezone -> org.hibernate.type.BasicTypeReference@6aadb092 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration ZonedDateTimeWithoutTimezone -> org.hibernate.type.BasicTypeReference@1f547af8 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration date -> org.hibernate.type.BasicTypeReference@4caf875c +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.sql.Date -> org.hibernate.type.BasicTypeReference@4caf875c +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration time -> org.hibernate.type.BasicTypeReference@5d15789f +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.sql.Time -> org.hibernate.type.BasicTypeReference@5d15789f +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration timestamp -> org.hibernate.type.BasicTypeReference@5abb7a8f +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.sql.Timestamp -> org.hibernate.type.BasicTypeReference@5abb7a8f +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.util.Date -> org.hibernate.type.BasicTypeReference@5abb7a8f +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration calendar -> org.hibernate.type.BasicTypeReference@6684589a +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.util.Calendar -> org.hibernate.type.BasicTypeReference@6684589a +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.util.GregorianCalendar -> org.hibernate.type.BasicTypeReference@6684589a +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration calendar_date -> org.hibernate.type.BasicTypeReference@5621a671 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration calendar_time -> org.hibernate.type.BasicTypeReference@2006fdaa +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration instant -> org.hibernate.type.BasicTypeReference@21688427 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.Instant -> org.hibernate.type.BasicTypeReference@21688427 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration uuid -> org.hibernate.type.BasicTypeReference@656c5818 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.util.UUID -> org.hibernate.type.BasicTypeReference@656c5818 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration pg-uuid -> org.hibernate.type.BasicTypeReference@656c5818 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration uuid-binary -> org.hibernate.type.BasicTypeReference@3e2578ea +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration uuid-char -> org.hibernate.type.BasicTypeReference@29592929 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration class -> org.hibernate.type.BasicTypeReference@4cf5d999 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Class -> org.hibernate.type.BasicTypeReference@4cf5d999 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration currency -> org.hibernate.type.BasicTypeReference@4bdef487 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration Currency -> org.hibernate.type.BasicTypeReference@4bdef487 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.util.Currency -> org.hibernate.type.BasicTypeReference@4bdef487 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration locale -> org.hibernate.type.BasicTypeReference@5ea9373e +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.util.Locale -> org.hibernate.type.BasicTypeReference@5ea9373e +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration serializable -> org.hibernate.type.BasicTypeReference@3e595da3 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.io.Serializable -> org.hibernate.type.BasicTypeReference@3e595da3 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration timezone -> org.hibernate.type.BasicTypeReference@5c0272e0 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.util.TimeZone -> org.hibernate.type.BasicTypeReference@5c0272e0 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration ZoneOffset -> org.hibernate.type.BasicTypeReference@60c4cf2b +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.ZoneOffset -> org.hibernate.type.BasicTypeReference@60c4cf2b +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration url -> org.hibernate.type.BasicTypeReference@774304ca +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.net.URL -> org.hibernate.type.BasicTypeReference@774304ca +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration vector -> org.hibernate.type.BasicTypeReference@303fbc4 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration row_version -> org.hibernate.type.BasicTypeReference@4cd90c36 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration object -> org.hibernate.type.JavaObjectType@3dbbed3e +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Object -> org.hibernate.type.JavaObjectType@3dbbed3e +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration null -> org.hibernate.type.NullType@64540344 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_date -> org.hibernate.type.BasicTypeReference@b2d8dcd +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_time -> org.hibernate.type.BasicTypeReference@1397b141 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_timestamp -> org.hibernate.type.BasicTypeReference@579dde54 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_calendar -> org.hibernate.type.BasicTypeReference@30b9728f +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_calendar_date -> org.hibernate.type.BasicTypeReference@6b899971 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_calendar_time -> org.hibernate.type.BasicTypeReference@453a30f8 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_binary -> org.hibernate.type.BasicTypeReference@7cec3975 +2025-10-24 09:43:59 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_serializable -> org.hibernate.type.BasicTypeReference@73a116d +2025-10-24 09:43:59 [main] INFO o.s.o.j.p.SpringPersistenceUnitInfo - No LoadTimeWeaver setup: ignoring JPA class transformer +2025-10-24 09:43:59 [main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting... +2025-10-24 09:43:59 [main] INFO com.zaxxer.hikari.pool.HikariPool - HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@720c0996 +2025-10-24 09:43:59 [main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed. +2025-10-24 09:44:00 [main] DEBUG o.h.t.d.sql.spi.DdlTypeRegistry - addDescriptor(2003, org.hibernate.type.descriptor.sql.internal.ArrayDdlTypeImpl@15549dd7) replaced previous registration(org.hibernate.type.descriptor.sql.internal.ArrayDdlTypeImpl@1e75af65) +2025-10-24 09:44:00 [main] DEBUG o.h.t.d.sql.spi.DdlTypeRegistry - addDescriptor(6, org.hibernate.type.descriptor.sql.internal.CapacityDependentDdlType@25a2c4dc) replaced previous registration(org.hibernate.type.descriptor.sql.internal.DdlTypeImpl@29d81c22) +2025-10-24 09:44:00 [main] DEBUG o.h.t.d.jdbc.spi.JdbcTypeRegistry - addDescriptor(2004, BlobTypeDescriptor(BLOB_BINDING)) replaced previous registration(BlobTypeDescriptor(DEFAULT)) +2025-10-24 09:44:00 [main] DEBUG o.h.t.d.jdbc.spi.JdbcTypeRegistry - addDescriptor(2005, ClobTypeDescriptor(CLOB_BINDING)) replaced previous registration(ClobTypeDescriptor(DEFAULT)) +2025-10-24 09:44:00 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration JAVA_OBJECT -> org.hibernate.type.JavaObjectType@35e357b +2025-10-24 09:44:00 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Object -> org.hibernate.type.JavaObjectType@35e357b +2025-10-24 09:44:00 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Type registration key [java.lang.Object] overrode previous entry : `org.hibernate.type.JavaObjectType@3dbbed3e` +2025-10-24 09:44:00 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration org.hibernate.type.DurationType -> basicType@1(java.time.Duration,3015) +2025-10-24 09:44:00 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration Duration -> basicType@1(java.time.Duration,3015) +2025-10-24 09:44:00 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.Duration -> basicType@1(java.time.Duration,3015) +2025-10-24 09:44:00 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration org.hibernate.type.OffsetDateTimeType -> basicType@2(java.time.OffsetDateTime,3003) +2025-10-24 09:44:00 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration OffsetDateTime -> basicType@2(java.time.OffsetDateTime,3003) +2025-10-24 09:44:00 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.OffsetDateTime -> basicType@2(java.time.OffsetDateTime,3003) +2025-10-24 09:44:00 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration org.hibernate.type.ZonedDateTimeType -> basicType@3(java.time.ZonedDateTime,3003) +2025-10-24 09:44:00 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration ZonedDateTime -> basicType@3(java.time.ZonedDateTime,3003) +2025-10-24 09:44:00 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.ZonedDateTime -> basicType@3(java.time.ZonedDateTime,3003) +2025-10-24 09:44:00 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration org.hibernate.type.OffsetTimeType -> basicType@4(java.time.OffsetTime,3007) +2025-10-24 09:44:00 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration OffsetTime -> basicType@4(java.time.OffsetTime,3007) +2025-10-24 09:44:00 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.OffsetTime -> basicType@4(java.time.OffsetTime,3007) +2025-10-24 09:44:00 [main] DEBUG o.h.type.spi.TypeConfiguration$Scope - Scoping TypeConfiguration [org.hibernate.type.spi.TypeConfiguration@755009f2] to MetadataBuildingContext [org.hibernate.boot.internal.MetadataBuildingContextRootImpl@1756a471] +2025-10-24 09:44:00 [main] INFO o.h.e.t.j.p.i.JtaPlatformInitiator - HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration) +2025-10-24 09:44:00 [main] DEBUG o.h.type.spi.TypeConfiguration$Scope - Scoping TypeConfiguration [org.hibernate.type.spi.TypeConfiguration@755009f2] to SessionFactoryImplementor [org.hibernate.internal.SessionFactoryImpl@139da216] +2025-10-24 09:44:00 [main] DEBUG org.hibernate.SQL - + create table processed_transcripts ( + transcript_id varchar(50) not null, + created_at timestamp(6) not null, + updated_at timestamp(6) not null, + decisions TEXT, + discussions TEXT, + meeting_id varchar(50) not null, + pending_items TEXT, + status varchar(20) not null, + summary TEXT, + primary key (transcript_id) + ) +2025-10-24 09:44:00 [main] TRACE o.h.type.spi.TypeConfiguration$Scope - Handling #sessionFactoryCreated from [org.hibernate.internal.SessionFactoryImpl@139da216] for TypeConfiguration +2025-10-24 09:44:00 [main] INFO o.s.o.j.LocalContainerEntityManagerFactoryBean - Initialized JPA EntityManagerFactory for persistence unit 'default' +2025-10-24 09:44:01 [main] WARN o.s.b.a.o.j.JpaBaseConfiguration$JpaWebConfiguration - spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning +2025-10-24 09:44:01 [main] WARN o.s.b.a.s.s.UserDetailsServiceAutoConfiguration - + +Using generated security password: 95eb4232-3294-428d-b23f-4e7e714862aa + +This generated password is for development use only. Your security configuration must be updated before running your application in production. + +2025-10-24 09:44:01 [main] INFO o.s.s.c.a.a.c.InitializeUserDetailsBeanManagerConfigurer$InitializeUserDetailsManagerConfigurer - Global AuthenticationManager configured with UserDetailsService bean with name inMemoryUserDetailsManager +2025-10-24 09:44:01 [main] ERROR i.n.r.d.DnsServerAddressStreamProviders - Unable to load io.netty.resolver.dns.macos.MacOSDnsServerAddressStreamProvider, fallback to system defaults. This may result in incorrect DNS resolutions on MacOS. Check whether you have a dependency on 'io.netty:netty-resolver-dns-native-macos'. Use DEBUG level to see the full stack: java.lang.UnsatisfiedLinkError: failed to load the required native library +2025-10-24 09:44:01 [main] INFO o.s.b.a.e.web.EndpointLinksResolver - Exposing 3 endpoints beneath base path '/actuator' +2025-10-24 09:44:01 [main] DEBUG o.s.s.web.DefaultSecurityFilterChain - Will secure any request with filters: DisableEncodeUrlFilter, WebAsyncManagerIntegrationFilter, SecurityContextHolderFilter, HeaderWriterFilter, CorsFilter, LogoutFilter, JwtAuthenticationFilter, RequestCacheAwareFilter, SecurityContextHolderAwareRequestFilter, AnonymousAuthenticationFilter, SessionManagementFilter, ExceptionTranslationFilter, AuthorizationFilter +2025-10-24 09:44:01 [main] INFO o.s.b.w.e.tomcat.TomcatWebServer - Tomcat started on port 8083 (http) with context path '/' +2025-10-24 09:44:01 [main] INFO com.unicorn.hgzero.ai.AiApplication - Started AiApplication in 3.911 seconds (process running for 4.067) +2025-10-24 09:45:34 [http-nio-8083-exec-1] INFO o.a.c.c.C.[Tomcat].[localhost].[/] - Initializing Spring DispatcherServlet 'dispatcherServlet' +2025-10-24 09:45:34 [http-nio-8083-exec-1] INFO o.s.web.servlet.DispatcherServlet - Initializing Servlet 'dispatcherServlet' +2025-10-24 09:45:34 [http-nio-8083-exec-1] INFO o.s.web.servlet.DispatcherServlet - Completed initialization in 3 ms +2025-10-24 09:45:34 [http-nio-8083-exec-1] DEBUG o.s.security.web.FilterChainProxy - Securing GET /swagger-ui/index.html +2025-10-24 09:45:34 [http-nio-8083-exec-1] DEBUG o.s.s.w.a.AnonymousAuthenticationFilter - Set SecurityContextHolder to anonymous SecurityContext +2025-10-24 09:45:34 [http-nio-8083-exec-1] DEBUG o.s.security.web.FilterChainProxy - Secured GET /swagger-ui/index.html +2025-10-24 09:45:34 [http-nio-8083-exec-2] DEBUG o.s.security.web.FilterChainProxy - Securing GET /swagger-ui/swagger-ui.css +2025-10-24 09:45:34 [http-nio-8083-exec-2] DEBUG o.s.s.w.a.AnonymousAuthenticationFilter - Set SecurityContextHolder to anonymous SecurityContext +2025-10-24 09:45:34 [http-nio-8083-exec-2] DEBUG o.s.security.web.FilterChainProxy - Secured GET /swagger-ui/swagger-ui.css +2025-10-24 09:45:34 [http-nio-8083-exec-3] DEBUG o.s.security.web.FilterChainProxy - Securing GET /swagger-ui/index.css +2025-10-24 09:45:34 [http-nio-8083-exec-4] DEBUG o.s.security.web.FilterChainProxy - Securing GET /swagger-ui/swagger-ui-bundle.js +2025-10-24 09:45:34 [http-nio-8083-exec-4] DEBUG o.s.s.w.a.AnonymousAuthenticationFilter - Set SecurityContextHolder to anonymous SecurityContext +2025-10-24 09:45:34 [http-nio-8083-exec-5] DEBUG o.s.security.web.FilterChainProxy - Securing GET /swagger-ui/swagger-ui-standalone-preset.js +2025-10-24 09:45:34 [http-nio-8083-exec-4] DEBUG o.s.security.web.FilterChainProxy - Secured GET /swagger-ui/swagger-ui-bundle.js +2025-10-24 09:45:34 [http-nio-8083-exec-5] DEBUG o.s.s.w.a.AnonymousAuthenticationFilter - Set SecurityContextHolder to anonymous SecurityContext +2025-10-24 09:45:34 [http-nio-8083-exec-6] DEBUG o.s.security.web.FilterChainProxy - Securing GET /swagger-ui/swagger-initializer.js +2025-10-24 09:45:34 [http-nio-8083-exec-5] DEBUG o.s.security.web.FilterChainProxy - Secured GET /swagger-ui/swagger-ui-standalone-preset.js +2025-10-24 09:45:34 [http-nio-8083-exec-6] DEBUG o.s.s.w.a.AnonymousAuthenticationFilter - Set SecurityContextHolder to anonymous SecurityContext +2025-10-24 09:45:34 [http-nio-8083-exec-6] DEBUG o.s.security.web.FilterChainProxy - Secured GET /swagger-ui/swagger-initializer.js +2025-10-24 09:45:34 [http-nio-8083-exec-3] DEBUG o.s.s.w.a.AnonymousAuthenticationFilter - Set SecurityContextHolder to anonymous SecurityContext +2025-10-24 09:45:34 [http-nio-8083-exec-3] DEBUG o.s.security.web.FilterChainProxy - Secured GET /swagger-ui/index.css +2025-10-24 09:45:34 [http-nio-8083-exec-8] DEBUG o.s.security.web.FilterChainProxy - Securing GET /swagger-ui/favicon-32x32.png +2025-10-24 09:45:34 [http-nio-8083-exec-8] DEBUG o.s.s.w.a.AnonymousAuthenticationFilter - Set SecurityContextHolder to anonymous SecurityContext +2025-10-24 09:45:34 [http-nio-8083-exec-7] DEBUG o.s.security.web.FilterChainProxy - Securing GET /v3/api-docs/swagger-config +2025-10-24 09:45:34 [http-nio-8083-exec-8] DEBUG o.s.security.web.FilterChainProxy - Secured GET /swagger-ui/favicon-32x32.png +2025-10-24 09:45:34 [http-nio-8083-exec-7] DEBUG o.s.s.w.a.AnonymousAuthenticationFilter - Set SecurityContextHolder to anonymous SecurityContext +2025-10-24 09:45:34 [http-nio-8083-exec-7] DEBUG o.s.security.web.FilterChainProxy - Secured GET /v3/api-docs/swagger-config +2025-10-24 09:45:34 [http-nio-8083-exec-7] INFO c.u.hgzero.common.aop.LoggingAspect - [Controller] org.springdoc.webmvc.ui.SwaggerConfigResource.openapiJson 호출 - 파라미터: [SecurityContextHolderAwareRequestWrapper[ org.springframework.security.web.header.HeaderWriterFilter$HeaderWriterRequest@4e36d653]] +2025-10-24 09:45:34 [http-nio-8083-exec-7] INFO c.u.hgzero.common.aop.LoggingAspect - [Controller] org.springdoc.webmvc.ui.SwaggerConfigResource.openapiJson 완료 - 실행시간: 0ms +2025-10-24 09:45:34 [http-nio-8083-exec-9] DEBUG o.s.security.web.FilterChainProxy - Securing GET /v3/api-docs +2025-10-24 09:45:34 [http-nio-8083-exec-9] DEBUG o.s.s.w.a.AnonymousAuthenticationFilter - Set SecurityContextHolder to anonymous SecurityContext +2025-10-24 09:45:34 [http-nio-8083-exec-9] DEBUG o.s.security.web.FilterChainProxy - Secured GET /v3/api-docs +2025-10-24 09:45:34 [http-nio-8083-exec-9] INFO c.u.hgzero.common.aop.LoggingAspect - [Controller] org.springdoc.webmvc.api.OpenApiWebMvcResource.openapiJson 호출 - 파라미터: [SecurityContextHolderAwareRequestWrapper[ org.springframework.security.web.header.HeaderWriterFilter$HeaderWriterRequest@668a5138], /v3/api-docs, ko_KR] +2025-10-24 09:45:35 [http-nio-8083-exec-9] INFO o.s.api.AbstractOpenApiResource - Init duration for springdoc-openapi is: 229 ms +2025-10-24 09:45:35 [http-nio-8083-exec-9] INFO c.u.hgzero.common.aop.LoggingAspect - [Controller] org.springdoc.webmvc.api.OpenApiWebMvcResource.openapiJson 완료 - 실행시간: 239ms diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/AiApplication.java b/ai/src/main/java/com/unicorn/hgzero/ai/AiApplication.java index 67fb76a..fa7fcd8 100644 --- a/ai/src/main/java/com/unicorn/hgzero/ai/AiApplication.java +++ b/ai/src/main/java/com/unicorn/hgzero/ai/AiApplication.java @@ -3,6 +3,7 @@ package com.unicorn.hgzero.ai; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.ComponentScan; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; /** * AI Service Application diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/biz/domain/ExtractedTodo.java b/ai/src/main/java/com/unicorn/hgzero/ai/biz/domain/ExtractedTodo.java new file mode 100644 index 0000000..38fc380 --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/biz/domain/ExtractedTodo.java @@ -0,0 +1,44 @@ +package com.unicorn.hgzero.ai.biz.domain; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; + +/** + * 추출된 Todo 도메인 모델 + * AI가 회의록에서 추출한 Todo 정보 + */ +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ExtractedTodo { + + /** + * Todo 내용 + */ + private String content; + + /** + * 담당자 + */ + private String assignee; + + /** + * 마감일 + */ + private LocalDate dueDate; + + /** + * 우선순위 (HIGH, MEDIUM, LOW) + */ + private String priority; + + /** + * 관련 회의록 섹션 + */ + private String sectionReference; +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/biz/domain/ProcessedTranscript.java b/ai/src/main/java/com/unicorn/hgzero/ai/biz/domain/ProcessedTranscript.java new file mode 100644 index 0000000..0273a4f --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/biz/domain/ProcessedTranscript.java @@ -0,0 +1,86 @@ +package com.unicorn.hgzero.ai.biz.domain; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 처리된 회의록 도메인 모델 + * AI가 처리한 회의록 정보를 담는 도메인 객체 + */ +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ProcessedTranscript { + + /** + * 회의록 ID + */ + private String transcriptId; + + /** + * 회의 ID + */ + private String meetingId; + + /** + * 전체 요약 + */ + private String summary; + + /** + * 논의사항 목록 + */ + private List discussions; + + /** + * 결정사항 목록 + */ + private List decisions; + + /** + * 보류사항 목록 + */ + private List pendingItems; + + /** + * 생성 시간 + */ + private LocalDateTime createdAt; + + /** + * 상태 (DRAFT, COMPLETED) + */ + private String status; + + /** + * 논의사항 아이템 + */ + @Getter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class DiscussionItem { + private String topic; + private String speaker; + private String content; + } + + /** + * 결정사항 아이템 + */ + @Getter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class DecisionItem { + private String content; + private String decisionMaker; + private String category; + } +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/biz/domain/RelatedMinutes.java b/ai/src/main/java/com/unicorn/hgzero/ai/biz/domain/RelatedMinutes.java new file mode 100644 index 0000000..21bc0b7 --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/biz/domain/RelatedMinutes.java @@ -0,0 +1,55 @@ +package com.unicorn.hgzero.ai.biz.domain; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; +import java.util.List; + +/** + * 관련 회의록 도메인 모델 + * RAG 검색으로 찾은 관련 회의록 정보 + */ +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RelatedMinutes { + + /** + * 회의록 ID + */ + private String transcriptId; + + /** + * 회의 제목 + */ + private String title; + + /** + * 회의 날짜 + */ + private LocalDate date; + + /** + * 참석자 목록 + */ + private List participants; + + /** + * 관련도 점수 (0-100) + */ + private Double relevanceScore; + + /** + * 공통 키워드 목록 + */ + private List commonKeywords; + + /** + * 회의록 링크 + */ + private String link; +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/biz/domain/Suggestion.java b/ai/src/main/java/com/unicorn/hgzero/ai/biz/domain/Suggestion.java new file mode 100644 index 0000000..fe60235 --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/biz/domain/Suggestion.java @@ -0,0 +1,87 @@ +package com.unicorn.hgzero.ai.biz.domain; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 제안사항 도메인 모델 + * AI가 제안하는 논의사항 또는 결정사항 + */ +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Suggestion { + + /** + * 제안 ID + */ + private String id; + + /** + * 제안 유형 (DISCUSSION, DECISION) + */ + private SuggestionType type; + + /** + * 제안 내용 + */ + private String content; + + /** + * 우선순위 (HIGH, MEDIUM, LOW) + */ + private String priority; + + /** + * 제안 이유 + */ + private String reason; + + /** + * 신뢰도 점수 (0-1) + */ + private Double confidence; + + /** + * 관련 안건 + */ + private String relatedAgenda; + + /** + * 예상 소요 시간 (분) + */ + private Integer estimatedTime; + + /** + * 참여자 목록 (결정사항인 경우) + */ + private List participants; + + /** + * 카테고리 (결정사항인 경우: 기술, 일정, 리소스, 정책, 기타) + */ + private String category; + + /** + * 원문 발췌 (결정사항인 경우) + */ + private String extractedFrom; + + /** + * 배경 설명 (결정사항인 경우) + */ + private String context; + + /** + * 제안 유형 + */ + public enum SuggestionType { + DISCUSSION, // 논의사항 + DECISION // 결정사항 + } +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/biz/domain/Term.java b/ai/src/main/java/com/unicorn/hgzero/ai/biz/domain/Term.java new file mode 100644 index 0000000..157b639 --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/biz/domain/Term.java @@ -0,0 +1,54 @@ +package com.unicorn.hgzero.ai.biz.domain; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * 전문용어 도메인 모델 + * 회의록에서 감지된 전문용어 정보 + */ +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Term { + + /** + * 용어명 + */ + private String term; + + /** + * 텍스트 위치 정보 + */ + private TextPosition position; + + /** + * 신뢰도 점수 (0-1) + */ + private Double confidence; + + /** + * 용어 카테고리 (기술, 업무, 도메인) + */ + private String category; + + /** + * 하이라이트 여부 + */ + private Boolean highlight; + + /** + * 텍스트 위치 + */ + @Getter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class TextPosition { + private Integer line; + private Integer offset; + } +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/biz/gateway/LlmGateway.java b/ai/src/main/java/com/unicorn/hgzero/ai/biz/gateway/LlmGateway.java new file mode 100644 index 0000000..ffcb5bc --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/biz/gateway/LlmGateway.java @@ -0,0 +1,64 @@ +package com.unicorn.hgzero.ai.biz.gateway; + +import java.util.List; + +/** + * LLM Gateway 인터페이스 + * OpenAI API 연동을 추상화 + */ +public interface LlmGateway { + + /** + * 회의록 자동 작성 (LLM 기반) + * + * @param transcriptText STT 변환 텍스트 + * @param title 회의 제목 + * @param participants 참석자 목록 + * @param agenda 회의 안건 + * @return LLM 생성 회의록 (JSON 형식) + */ + String generateTranscript(String transcriptText, String title, List participants, List agenda); + + /** + * Todo 추출 (LLM 기반) + * + * @param minutesContent 회의록 내용 + * @return 추출된 Todo JSON + */ + String extractTodos(String minutesContent); + + /** + * 섹션 요약 생성 (LLM 기반) + * + * @param sectionContent 섹션 내용 + * @param meetingContext 회의 맥락 + * @return 생성된 요약 (2-3문장) + */ + String generateSummary(String sectionContent, String meetingContext); + + /** + * 전문용어 감지 (LLM 기반) + * + * @param text 분석할 텍스트 + * @param organizationId 조직 ID + * @return 감지된 용어 JSON + */ + String detectTerms(String text, String organizationId); + + /** + * 논의사항 제안 (LLM 기반) + * + * @param transcriptText 현재 회의록 텍스트 + * @param agenda 회의 안건 + * @return 논의사항 제안 JSON + */ + String suggestDiscussions(String transcriptText, List agenda); + + /** + * 결정사항 제안 (LLM 기반) + * + * @param transcriptText 현재 회의록 텍스트 + * @return 결정사항 제안 JSON + */ + String suggestDecisions(String transcriptText); +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/biz/gateway/SearchGateway.java b/ai/src/main/java/com/unicorn/hgzero/ai/biz/gateway/SearchGateway.java new file mode 100644 index 0000000..e3eacde --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/biz/gateway/SearchGateway.java @@ -0,0 +1,39 @@ +package com.unicorn.hgzero.ai.biz.gateway; + +import java.util.List; + +/** + * RAG 검색 Gateway 인터페이스 + * Azure AI Search 연동을 추상화 + */ +public interface SearchGateway { + + /** + * 관련 회의록 검색 (벡터 유사도 기반) + * + * @param meetingId 회의 ID + * @param transcriptId 회의록 ID + * @param limit 최대 개수 + * @return 관련 회의록 JSON + */ + String searchRelatedTranscripts(String meetingId, String transcriptId, int limit); + + /** + * 용어 설명을 위한 문서 검색 + * + * @param term 용어명 + * @param meetingId 회의 ID + * @param context 맥락 + * @return 관련 문서 JSON + */ + String searchTermExplanation(String term, String meetingId, String context); + + /** + * 회의록 인덱싱 (벡터 임베딩 저장) + * + * @param transcriptId 회의록 ID + * @param content 회의록 내용 + * @param metadata 메타데이터 + */ + void indexTranscript(String transcriptId, String content, String metadata); +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/biz/gateway/TranscriptGateway.java b/ai/src/main/java/com/unicorn/hgzero/ai/biz/gateway/TranscriptGateway.java new file mode 100644 index 0000000..6dc80a5 --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/biz/gateway/TranscriptGateway.java @@ -0,0 +1,68 @@ +package com.unicorn.hgzero.ai.biz.gateway; + +import com.unicorn.hgzero.ai.biz.domain.ProcessedTranscript; + +import java.util.List; +import java.util.Optional; + +/** + * 회의록 데이터 Gateway 인터페이스 + * 회의록 영속성 관리를 추상화 + */ +public interface TranscriptGateway { + + /** + * 회의록 저장 + * + * @param transcript 처리된 회의록 + * @return 저장된 회의록 + */ + ProcessedTranscript save(ProcessedTranscript transcript); + + /** + * 회의록 ID로 조회 + * + * @param transcriptId 회의록 ID + * @return 회의록 (Optional) + */ + Optional findById(String transcriptId); + + /** + * 회의 ID로 조회 + * + * @param meetingId 회의 ID + * @return 회의록 (Optional) + */ + Optional findByMeetingId(String meetingId); + + /** + * 회의 ID 목록으로 조회 + * + * @param meetingIds 회의 ID 목록 + * @return 회의록 목록 + */ + List findByMeetingIds(List meetingIds); + + /** + * 상태로 조회 + * + * @param status 상태 + * @return 회의록 목록 + */ + List findByStatus(String status); + + /** + * 회의록 존재 여부 확인 + * + * @param meetingId 회의 ID + * @return 존재 여부 + */ + boolean existsByMeetingId(String meetingId); + + /** + * 회의록 삭제 + * + * @param transcriptId 회의록 ID + */ + void delete(String transcriptId); +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/biz/service/RelatedTranscriptSearchService.java b/ai/src/main/java/com/unicorn/hgzero/ai/biz/service/RelatedTranscriptSearchService.java new file mode 100644 index 0000000..31e4a55 --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/biz/service/RelatedTranscriptSearchService.java @@ -0,0 +1,48 @@ +package com.unicorn.hgzero.ai.biz.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.unicorn.hgzero.ai.biz.domain.RelatedMinutes; +import com.unicorn.hgzero.ai.biz.gateway.SearchGateway; +import com.unicorn.hgzero.ai.biz.usecase.RelatedTranscriptSearchUseCase; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.util.List; + +/** + * 관련 회의록 검색 Service + * RAG 기반 벡터 유사도 검색 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class RelatedTranscriptSearchService implements RelatedTranscriptSearchUseCase { + + private final SearchGateway searchGateway; + private final ObjectMapper objectMapper; + + @Override + public List findRelatedTranscripts(String meetingId, String transcriptId, int limit) { + log.info("Searching related transcripts: meetingId={}, transcriptId={}, limit={}", + meetingId, transcriptId, limit); + + // RAG 검색 + String searchResult = searchGateway.searchRelatedTranscripts(meetingId, transcriptId, limit); + + // TODO: JSON 파싱 및 RelatedMinutes 리스트 생성 + // 현재는 mock 데이터 반환 + return List.of( + RelatedMinutes.builder() + .transcriptId("aa0e8400-e29b-41d4-a716-446655440005") + .title("프로젝트 X 주간 회의") + .date(LocalDate.of(2025, 1, 15)) + .participants(List.of("김철수", "이영희")) + .relevanceScore(85.5) + .commonKeywords(List.of("MSA", "API Gateway", "Spring Boot")) + .link("/transcripts/aa0e8400-e29b-41d4-a716-446655440005") + .build() + ); + } +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/biz/service/SectionSummaryService.java b/ai/src/main/java/com/unicorn/hgzero/ai/biz/service/SectionSummaryService.java new file mode 100644 index 0000000..4bfe020 --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/biz/service/SectionSummaryService.java @@ -0,0 +1,28 @@ +package com.unicorn.hgzero.ai.biz.service; + +import com.unicorn.hgzero.ai.biz.gateway.LlmGateway; +import com.unicorn.hgzero.ai.biz.usecase.SectionSummaryUseCase; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * 섹션 AI 요약 재생성 Service + * LLM 기반 섹션 요약 생성 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class SectionSummaryService implements SectionSummaryUseCase { + + private final LlmGateway llmGateway; + + @Override + public String regenerateSummary(String sectionId, String sectionContent, String meetingId) { + log.info("Regenerating section summary: sectionId={}, meetingId={}", sectionId, meetingId); + + // LLM을 통한 요약 생성 + String meetingContext = meetingId != null ? "회의 ID: " + meetingId : ""; + return llmGateway.generateSummary(sectionContent, meetingContext); + } +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/biz/service/SuggestionService.java b/ai/src/main/java/com/unicorn/hgzero/ai/biz/service/SuggestionService.java new file mode 100644 index 0000000..36f817a --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/biz/service/SuggestionService.java @@ -0,0 +1,69 @@ +package com.unicorn.hgzero.ai.biz.service; + +import com.unicorn.hgzero.ai.biz.domain.Suggestion; +import com.unicorn.hgzero.ai.biz.gateway.LlmGateway; +import com.unicorn.hgzero.ai.biz.usecase.SuggestionUseCase; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 논의사항/결정사항 제안 Service + * LLM 기반 실시간 회의 제안 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class SuggestionService implements SuggestionUseCase { + + private final LlmGateway llmGateway; + + @Override + public List suggestDiscussions(String meetingId, String transcriptText) { + log.info("Suggesting discussions: meetingId={}", meetingId); + + // TODO: 회의 안건 조회 + List agenda = List.of(); + + // LLM을 통한 논의사항 제안 + String llmResponse = llmGateway.suggestDiscussions(transcriptText, agenda); + + // TODO: JSON 파싱 및 Suggestion 리스트 생성 + return List.of( + Suggestion.builder() + .id("sugg-001") + .type(Suggestion.SuggestionType.DISCUSSION) + .content("보안 요구사항 검토") + .priority("HIGH") + .reason("안건에 포함되어 있으나 아직 논의되지 않음") + .confidence(0.9) + .relatedAgenda("프로젝트 개요") + .estimatedTime(15) + .build() + ); + } + + @Override + public List suggestDecisions(String meetingId, String transcriptText) { + log.info("Suggesting decisions: meetingId={}", meetingId); + + // LLM을 통한 결정사항 제안 + String llmResponse = llmGateway.suggestDecisions(transcriptText); + + // TODO: JSON 파싱 및 Suggestion 리스트 생성 + return List.of( + Suggestion.builder() + .id("dec-001") + .type(Suggestion.SuggestionType.DECISION) + .content("React로 프론트엔드 개발") + .category("기술") + .participants(List.of("김철수", "이영희")) + .confidence(0.85) + .extractedFrom("프론트엔드는 React로 개발하기로 했습니다") + .context("팀원 대부분이 React 경험이 있어 개발 속도가 빠를 것으로 예상") + .build() + ); + } +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/biz/service/TermDetectionService.java b/ai/src/main/java/com/unicorn/hgzero/ai/biz/service/TermDetectionService.java new file mode 100644 index 0000000..d56659e --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/biz/service/TermDetectionService.java @@ -0,0 +1,42 @@ +package com.unicorn.hgzero.ai.biz.service; + +import com.unicorn.hgzero.ai.biz.domain.Term; +import com.unicorn.hgzero.ai.biz.gateway.LlmGateway; +import com.unicorn.hgzero.ai.biz.usecase.TermDetectionUseCase; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 전문용어 감지 Service + * LLM 기반 전문용어 자동 감지 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class TermDetectionService implements TermDetectionUseCase { + + private final LlmGateway llmGateway; + + @Override + public List detectTerms(String meetingId, String text, String organizationId) { + log.info("Detecting terms: meetingId={}, organizationId={}", meetingId, organizationId); + + // LLM을 통한 전문용어 감지 + String llmResponse = llmGateway.detectTerms(text, organizationId); + + // TODO: JSON 파싱 및 Term 리스트 생성 + // 현재는 mock 데이터 반환 + return List.of( + Term.builder() + .term("MSA") + .position(Term.TextPosition.builder().line(5).offset(42).build()) + .confidence(0.92) + .category("기술") + .highlight(true) + .build() + ); + } +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/biz/service/TermExplanationService.java b/ai/src/main/java/com/unicorn/hgzero/ai/biz/service/TermExplanationService.java new file mode 100644 index 0000000..0784a6d --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/biz/service/TermExplanationService.java @@ -0,0 +1,54 @@ +package com.unicorn.hgzero.ai.biz.service; + +import com.unicorn.hgzero.ai.biz.gateway.SearchGateway; +import com.unicorn.hgzero.ai.biz.usecase.TermExplanationUseCase; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.util.List; + +/** + * 전문용어 설명 Service + * RAG 기반 맥락적 용어 설명 생성 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class TermExplanationService implements TermExplanationUseCase { + + private final SearchGateway searchGateway; + + @Override + public TermExplanationResult explainTerm(String term, String meetingId, String context) { + log.info("Explaining term: term={}, meetingId={}", term, meetingId); + + // RAG 검색 + String searchResult = searchGateway.searchTermExplanation(term, meetingId, context); + + // TODO: JSON 파싱 및 TermExplanationResult 생성 + // 현재는 mock 데이터 반환 + return new TermExplanationResult( + "MSA", + "Microservices Architecture의 약자", + "이번 프로젝트에서는 확장성과 독립 배포를 위해 MSA를 적용하기로 결정", + List.of( + "2024년 프로젝트 X에서 주문/결제/배송 서비스를 독립적으로 구성", + "서비스별 독립 배포로 배포 시간 70% 단축" + ), + List.of(new RelatedProject("프로젝트 X", "동일한 MSA 아키텍처 적용")), + List.of(new PastDiscussion( + LocalDate.of(2024, 12, 15), + List.of("김철수", "이영희"), + "MSA 아키텍처의 장단점을 비교하고 적용 방안을 논의", + "/transcripts/bb0e8400-e29b-41d4-a716-446655440006" + )), + List.of(new Reference( + "MSA 아키텍처 가이드", + "위키", + "https://wiki.example.com/msa-guide" + )) + ); + } +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/biz/service/TodoExtractionService.java b/ai/src/main/java/com/unicorn/hgzero/ai/biz/service/TodoExtractionService.java new file mode 100644 index 0000000..149ab1a --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/biz/service/TodoExtractionService.java @@ -0,0 +1,47 @@ +package com.unicorn.hgzero.ai.biz.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.unicorn.hgzero.ai.biz.domain.ExtractedTodo; +import com.unicorn.hgzero.ai.biz.gateway.LlmGateway; +import com.unicorn.hgzero.ai.biz.usecase.TodoExtractionUseCase; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; + +/** + * Todo 자동 추출 Service + * LLM 기반 액션 아이템 추출 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class TodoExtractionService implements TodoExtractionUseCase { + + private final LlmGateway llmGateway; + private final ObjectMapper objectMapper; + + @Override + public List extractTodos(String meetingId, String minutesContent, String userId) { + log.info("Extracting todos from minutes: meetingId={}, userId={}", meetingId, userId); + + // LLM을 통한 Todo 추출 + String llmResponse = llmGateway.extractTodos(minutesContent); + + // TODO: JSON 파싱 및 ExtractedTodo 리스트 생성 + // 현재는 mock 데이터 반환 + return List.of( + ExtractedTodo.builder() + .content("API 설계서 작성") + .assignee("박민수") + .dueDate(LocalDate.of(2025, 1, 30)) + .priority("HIGH") + .sectionReference("결정사항 #3") + .build() + ); + } +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/biz/service/TranscriptProcessService.java b/ai/src/main/java/com/unicorn/hgzero/ai/biz/service/TranscriptProcessService.java new file mode 100644 index 0000000..86cae79 --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/biz/service/TranscriptProcessService.java @@ -0,0 +1,180 @@ +package com.unicorn.hgzero.ai.biz.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.unicorn.hgzero.ai.biz.domain.ProcessedTranscript; +import com.unicorn.hgzero.ai.biz.gateway.LlmGateway; +import com.unicorn.hgzero.ai.biz.gateway.SearchGateway; +import com.unicorn.hgzero.ai.biz.gateway.TranscriptGateway; +import com.unicorn.hgzero.ai.biz.usecase.TranscriptProcessUseCase; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +/** + * 회의록 자동 작성 Service + * LLM 기반 회의록 생성 및 저장 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class TranscriptProcessService implements TranscriptProcessUseCase { + + private final LlmGateway llmGateway; + private final SearchGateway searchGateway; + private final TranscriptGateway transcriptGateway; + private final ObjectMapper objectMapper; + + @Override + @Transactional + public ProcessedTranscript processTranscript( + String meetingId, + String transcriptText, + String userId, + String userName, + String title, + List participants, + List agenda + ) { + log.info("Processing transcript for meeting: meetingId={}, userId={}", meetingId, userId); + + // 1. LLM을 통한 회의록 자동 생성 + String llmResponse = llmGateway.generateTranscript(transcriptText, title, participants, agenda); + log.debug("LLM response received: length={}", llmResponse.length()); + + // 2. LLM 응답 파싱 + ProcessedTranscript processedTranscript = parseTranscriptFromLlm(llmResponse, meetingId); + + // 3. 회의록 저장 + ProcessedTranscript saved = transcriptGateway.save(processedTranscript); + log.info("Transcript saved: transcriptId={}, meetingId={}", saved.getTranscriptId(), meetingId); + + // 4. RAG 인덱싱 (비동기 처리 고려) + indexTranscriptForSearch(saved); + + return saved; + } + + @Override + @Transactional(readOnly = true) + public ProcessedTranscript getTranscript(String transcriptId) { + log.debug("Retrieving transcript: transcriptId={}", transcriptId); + return transcriptGateway.findById(transcriptId) + .orElseThrow(() -> new IllegalArgumentException("Transcript not found: " + transcriptId)); + } + + @Override + @Transactional(readOnly = true) + public ProcessedTranscript getTranscriptByMeetingId(String meetingId) { + log.debug("Retrieving transcript by meetingId: {}", meetingId); + return transcriptGateway.findByMeetingId(meetingId) + .orElseThrow(() -> new IllegalArgumentException("Transcript not found for meeting: " + meetingId)); + } + + /** + * LLM 응답을 ProcessedTranscript 도메인으로 파싱 + */ + private ProcessedTranscript parseTranscriptFromLlm(String llmResponse, String meetingId) { + try { + JsonNode root = objectMapper.readTree(llmResponse); + + // Discussions 파싱 + List discussions = new ArrayList<>(); + if (root.has("discussions")) { + root.get("discussions").forEach(node -> { + discussions.add(ProcessedTranscript.DiscussionItem.builder() + .topic(node.get("topic").asText()) + .speaker(node.get("speaker").asText()) + .content(node.get("content").asText()) + .build()); + }); + } + + // Decisions 파싱 + List decisions = new ArrayList<>(); + if (root.has("decisions")) { + root.get("decisions").forEach(node -> { + decisions.add(ProcessedTranscript.DecisionItem.builder() + .content(node.get("content").asText()) + .decisionMaker(node.get("decisionMaker").asText()) + .category(node.get("category").asText()) + .build()); + }); + } + + // Pending items 파싱 + List pendingItems = new ArrayList<>(); + if (root.has("pendingItems")) { + root.get("pendingItems").forEach(node -> pendingItems.add(node.asText())); + } + + return ProcessedTranscript.builder() + .transcriptId(UUID.randomUUID().toString()) + .meetingId(meetingId) + .summary(root.has("summary") ? root.get("summary").asText() : "") + .discussions(discussions) + .decisions(decisions) + .pendingItems(pendingItems) + .createdAt(LocalDateTime.now()) + .status("DRAFT") + .build(); + + } catch (JsonProcessingException e) { + log.error("Failed to parse LLM response: {}", llmResponse, e); + throw new RuntimeException("Failed to parse transcript from LLM", e); + } + } + + /** + * RAG 검색을 위한 회의록 인덱싱 + */ + private void indexTranscriptForSearch(ProcessedTranscript transcript) { + try { + String content = buildSearchableContent(transcript); + String metadata = buildMetadata(transcript); + + searchGateway.indexTranscript(transcript.getTranscriptId(), content, metadata); + log.debug("Transcript indexed for search: transcriptId={}", transcript.getTranscriptId()); + + } catch (Exception e) { + log.error("Failed to index transcript for search: transcriptId={}", + transcript.getTranscriptId(), e); + // 인덱싱 실패는 치명적이지 않으므로 예외를 전파하지 않음 + } + } + + private String buildSearchableContent(ProcessedTranscript transcript) { + StringBuilder content = new StringBuilder(); + content.append(transcript.getSummary()).append("\n\n"); + + if (transcript.getDiscussions() != null) { + transcript.getDiscussions().forEach(d -> + content.append(d.getTopic()).append(": ").append(d.getContent()).append("\n") + ); + } + + if (transcript.getDecisions() != null) { + transcript.getDecisions().forEach(d -> + content.append("결정: ").append(d.getContent()).append("\n") + ); + } + + return content.toString(); + } + + private String buildMetadata(ProcessedTranscript transcript) { + try { + return objectMapper.writeValueAsString(transcript); + } catch (JsonProcessingException e) { + log.warn("Failed to serialize transcript metadata", e); + return "{}"; + } + } +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/biz/usecase/RelatedTranscriptSearchUseCase.java b/ai/src/main/java/com/unicorn/hgzero/ai/biz/usecase/RelatedTranscriptSearchUseCase.java new file mode 100644 index 0000000..fa9f4aa --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/biz/usecase/RelatedTranscriptSearchUseCase.java @@ -0,0 +1,22 @@ +package com.unicorn.hgzero.ai.biz.usecase; + +import com.unicorn.hgzero.ai.biz.domain.RelatedMinutes; + +import java.util.List; + +/** + * 관련 회의록 검색 UseCase + * RAG 기반 벡터 유사도 검색으로 관련 회의록 조회 + */ +public interface RelatedTranscriptSearchUseCase { + + /** + * 관련 회의록 검색 + * + * @param meetingId 회의 ID + * @param transcriptId 회의록 ID + * @param limit 반환할 최대 개수 + * @return 관련 회의록 목록 + */ + List findRelatedTranscripts(String meetingId, String transcriptId, int limit); +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/biz/usecase/SectionSummaryUseCase.java b/ai/src/main/java/com/unicorn/hgzero/ai/biz/usecase/SectionSummaryUseCase.java new file mode 100644 index 0000000..5ec26ac --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/biz/usecase/SectionSummaryUseCase.java @@ -0,0 +1,18 @@ +package com.unicorn.hgzero.ai.biz.usecase; + +/** + * 섹션 AI 요약 재생성 UseCase + * 사용자가 작성한 섹션 내용을 기반으로 AI 요약 재생성 + */ +public interface SectionSummaryUseCase { + + /** + * 섹션 요약 재생성 + * + * @param sectionId 섹션 ID + * @param sectionContent 섹션 내용 (Markdown 형식) + * @param meetingId 회의 ID (선택적, 맥락 이해용) + * @return 생성된 AI 요약 (2-3문장) + */ + String regenerateSummary(String sectionId, String sectionContent, String meetingId); +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/biz/usecase/SuggestionUseCase.java b/ai/src/main/java/com/unicorn/hgzero/ai/biz/usecase/SuggestionUseCase.java new file mode 100644 index 0000000..2b30bf7 --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/biz/usecase/SuggestionUseCase.java @@ -0,0 +1,30 @@ +package com.unicorn.hgzero.ai.biz.usecase; + +import com.unicorn.hgzero.ai.biz.domain.Suggestion; + +import java.util.List; + +/** + * 논의사항/결정사항 제안 UseCase + * AI 기반 실시간 회의 제안 기능 + */ +public interface SuggestionUseCase { + + /** + * 논의사항 제안 + * + * @param meetingId 회의 ID + * @param transcriptText 현재까지의 회의록 텍스트 + * @return 논의사항 제안 목록 + */ + List suggestDiscussions(String meetingId, String transcriptText); + + /** + * 결정사항 제안 + * + * @param meetingId 회의 ID + * @param transcriptText 현재까지의 회의록 텍스트 + * @return 결정사항 제안 목록 + */ + List suggestDecisions(String meetingId, String transcriptText); +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/biz/usecase/TermDetectionUseCase.java b/ai/src/main/java/com/unicorn/hgzero/ai/biz/usecase/TermDetectionUseCase.java new file mode 100644 index 0000000..52a98dc --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/biz/usecase/TermDetectionUseCase.java @@ -0,0 +1,22 @@ +package com.unicorn.hgzero.ai.biz.usecase; + +import com.unicorn.hgzero.ai.biz.domain.Term; + +import java.util.List; + +/** + * 전문용어 감지 UseCase + * 회의록 텍스트에서 전문용어를 자동으로 감지 + */ +public interface TermDetectionUseCase { + + /** + * 전문용어 감지 + * + * @param meetingId 회의 ID + * @param text 분석할 회의록 텍스트 + * @param organizationId 조직 ID + * @return 감지된 전문용어 목록 + */ + List detectTerms(String meetingId, String text, String organizationId); +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/biz/usecase/TermExplanationUseCase.java b/ai/src/main/java/com/unicorn/hgzero/ai/biz/usecase/TermExplanationUseCase.java new file mode 100644 index 0000000..dd0fedf --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/biz/usecase/TermExplanationUseCase.java @@ -0,0 +1,37 @@ +package com.unicorn.hgzero.ai.biz.usecase; + +import java.util.List; + +/** + * 전문용어 설명 UseCase + * RAG 기반 맥락적 용어 설명 생성 + */ +public interface TermExplanationUseCase { + + /** + * 용어 설명 생성 + * + * @param term 용어명 + * @param meetingId 회의 ID + * @param context 현재 회의 맥락 (선택) + * @return 용어 설명 결과 + */ + TermExplanationResult explainTerm(String term, String meetingId, String context); + + /** + * 용어 설명 결과 + */ + record TermExplanationResult( + String term, + String basicDefinition, + String contextualMeaning, + List useCases, + List relatedProjects, + List pastDiscussions, + List references + ) {} + + record RelatedProject(String name, String relevance) {} + record PastDiscussion(java.time.LocalDate date, List participants, String summary, String link) {} + record Reference(String title, String type, String link) {} +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/biz/usecase/TodoExtractionUseCase.java b/ai/src/main/java/com/unicorn/hgzero/ai/biz/usecase/TodoExtractionUseCase.java new file mode 100644 index 0000000..a1c0e31 --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/biz/usecase/TodoExtractionUseCase.java @@ -0,0 +1,22 @@ +package com.unicorn.hgzero.ai.biz.usecase; + +import com.unicorn.hgzero.ai.biz.domain.ExtractedTodo; + +import java.util.List; + +/** + * Todo 자동 추출 UseCase + * 회의록에서 액션 아이템을 자동으로 추출하고 담당자 식별 + */ +public interface TodoExtractionUseCase { + + /** + * 회의록에서 Todo 추출 + * + * @param meetingId 회의 ID + * @param minutesContent 회의록 전체 내용 (Markdown 형식) + * @param userId 요청자 ID + * @return 추출된 Todo 목록 + */ + List extractTodos(String meetingId, String minutesContent, String userId); +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/biz/usecase/TranscriptProcessUseCase.java b/ai/src/main/java/com/unicorn/hgzero/ai/biz/usecase/TranscriptProcessUseCase.java new file mode 100644 index 0000000..930588f --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/biz/usecase/TranscriptProcessUseCase.java @@ -0,0 +1,48 @@ +package com.unicorn.hgzero.ai.biz.usecase; + +import com.unicorn.hgzero.ai.biz.domain.ProcessedTranscript; + +/** + * 회의록 자동 작성 UseCase + * STT에서 변환된 텍스트를 받아 LLM 기반으로 회의록 자동 작성 + */ +public interface TranscriptProcessUseCase { + + /** + * 회의록 자동 작성 + * + * @param meetingId 회의 ID + * @param transcriptText STT에서 변환된 텍스트 + * @param userId 사용자 ID + * @param userName 사용자 이름 + * @param title 회의 제목 + * @param participants 참석자 목록 + * @param agenda 회의 안건 + * @return 처리된 회의록 + */ + ProcessedTranscript processTranscript( + String meetingId, + String transcriptText, + String userId, + String userName, + String title, + java.util.List participants, + java.util.List agenda + ); + + /** + * 회의록 조회 + * + * @param transcriptId 회의록 ID + * @return 처리된 회의록 + */ + ProcessedTranscript getTranscript(String transcriptId); + + /** + * 회의 ID로 회의록 조회 + * + * @param meetingId 회의 ID + * @return 처리된 회의록 + */ + ProcessedTranscript getTranscriptByMeetingId(String meetingId); +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/infra/config/SecurityConfig.java b/ai/src/main/java/com/unicorn/hgzero/ai/infra/config/SecurityConfig.java new file mode 100644 index 0000000..ef749df --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/infra/config/SecurityConfig.java @@ -0,0 +1,84 @@ +package com.unicorn.hgzero.ai.infra.config; + +import com.unicorn.hgzero.common.security.JwtTokenProvider; +import com.unicorn.hgzero.common.security.filter.JwtAuthenticationFilter; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.Arrays; + +/** + * Spring Security 설정 + * JWT 기반 인증 및 API 보안 설정 + */ +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfig { + + private final JwtTokenProvider jwtTokenProvider; + + @Value("${cors.allowed-origins:http://localhost:3000,http://localhost:8080,http://localhost:8081,http://localhost:8082,http://localhost:8083,http://localhost:8084}") + private String allowedOrigins; + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + return http + .csrf(AbstractHttpConfigurer::disable) + .cors(cors -> cors.configurationSource(corsConfigurationSource())) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> auth + // Actuator endpoints + .requestMatchers("/actuator/**").permitAll() + // Swagger UI endpoints - context path와 상관없이 접근 가능하도록 설정 + .requestMatchers("/swagger-ui/**", "/swagger-ui.html", "/v3/api-docs/**", "/swagger-resources/**", "/webjars/**").permitAll() + // Health check + .requestMatchers("/health").permitAll() + // All other requests require authentication + .anyRequest().authenticated() + ) + .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), + UsernamePasswordAuthenticationFilter.class) + .build(); + } + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + + // 환경변수에서 허용할 Origin 패턴 설정 + String[] origins = allowedOrigins.split(","); + configuration.setAllowedOriginPatterns(Arrays.asList(origins)); + + // 허용할 HTTP 메소드 + configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")); + + // 허용할 헤더 + configuration.setAllowedHeaders(Arrays.asList( + "Authorization", "Content-Type", "X-Requested-With", "Accept", + "Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers", + "X-User-Id", "X-User-Name" + )); + + // 자격 증명 허용 + configuration.setAllowCredentials(true); + + // Pre-flight 요청 캐시 시간 + configuration.setMaxAge(3600L); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/infra/config/SwaggerConfig.java b/ai/src/main/java/com/unicorn/hgzero/ai/infra/config/SwaggerConfig.java new file mode 100644 index 0000000..cf0680c --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/infra/config/SwaggerConfig.java @@ -0,0 +1,63 @@ +package com.unicorn.hgzero.ai.infra.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.servers.Server; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Swagger/OpenAPI 설정 + * AI Service API 문서화를 위한 설정 + */ +@Configuration +public class SwaggerConfig { + + @Bean + public OpenAPI openAPI() { + return new OpenAPI() + .info(apiInfo()) + .addServersItem(new Server() + .url("http://localhost:8083") + .description("Local Development")) + .addServersItem(new Server() + .url("{protocol}://{host}:{port}") + .description("Custom Server") + .variables(new io.swagger.v3.oas.models.servers.ServerVariables() + .addServerVariable("protocol", new io.swagger.v3.oas.models.servers.ServerVariable() + ._default("http") + .description("Protocol (http or https)") + .addEnumItem("http") + .addEnumItem("https")) + .addServerVariable("host", new io.swagger.v3.oas.models.servers.ServerVariable() + ._default("localhost") + .description("Server host")) + .addServerVariable("port", new io.swagger.v3.oas.models.servers.ServerVariable() + ._default("8083") + .description("Server port")))) + .addSecurityItem(new SecurityRequirement().addList("Bearer Authentication")) + .components(new Components() + .addSecuritySchemes("Bearer Authentication", createAPIKeyScheme())); + } + + private Info apiInfo() { + return new Info() + .title("AI Service API") + .description("AI 기반 회의록 자동 작성 및 분석 서비스 API") + .version("1.0.0") + .contact(new Contact() + .name("HGZero Development Team") + .email("dev@hgzero.com")); + } + + private SecurityScheme createAPIKeyScheme() { + return new SecurityScheme() + .type(SecurityScheme.Type.HTTP) + .bearerFormat("JWT") + .scheme("bearer"); + } +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/infra/controller/ExplanationController.java b/ai/src/main/java/com/unicorn/hgzero/ai/infra/controller/ExplanationController.java new file mode 100644 index 0000000..af76535 --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/infra/controller/ExplanationController.java @@ -0,0 +1,74 @@ +package com.unicorn.hgzero.ai.infra.controller; + +import com.unicorn.hgzero.ai.biz.usecase.TermExplanationUseCase; +import com.unicorn.hgzero.ai.infra.dto.response.TermExplanationResponse; +import com.unicorn.hgzero.ai.infra.dto.common.*; +import com.unicorn.hgzero.common.dto.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.stream.Collectors; + +/** + * 전문용어 설명 Controller + * GET /api/terms/{term}/explain + */ +@RestController +@RequestMapping("/api/terms") +@RequiredArgsConstructor +@Slf4j +@Tag(name = "Term", description = "전문용어 감지 및 설명 API") +public class ExplanationController { + + private final TermExplanationUseCase termExplanationUseCase; + + @GetMapping("/{term}/explain") + @Operation(summary = "맥락 기반 용어 설명", description = "전문용어에 대한 맥락 기반 설명을 생성합니다") + public ResponseEntity> explainTerm( + @PathVariable String term, + @RequestParam String meetingId, + @RequestParam(required = false) String context) { + + log.info("용어 설명 요청 - term: {}, meetingId: {}", term, meetingId); + + TermExplanationUseCase.TermExplanationResult result = termExplanationUseCase.explainTerm( + term, + meetingId, + context + ); + + TermExplanationResponse response = TermExplanationResponse.builder() + .term(result.term()) + .basicDefinition(result.basicDefinition()) + .contextualMeaning(result.contextualMeaning()) + .useCases(result.useCases()) + .relatedProjects(result.relatedProjects().stream() + .map(p -> RelatedProjectDto.builder() + .name(p.name()) + .relevance(p.relevance()) + .build()) + .collect(Collectors.toList())) + .pastDiscussions(result.pastDiscussions().stream() + .map(d -> PastDiscussionDto.builder() + .date(d.date()) + .participants(d.participants()) + .summary(d.summary()) + .link(d.link()) + .build()) + .collect(Collectors.toList())) + .references(result.references().stream() + .map(r -> ReferenceDto.builder() + .title(r.title()) + .type(r.type()) + .link(r.link()) + .build()) + .collect(Collectors.toList())) + .build(); + + return ResponseEntity.ok(ApiResponse.success(response)); + } +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/infra/controller/RelationController.java b/ai/src/main/java/com/unicorn/hgzero/ai/infra/controller/RelationController.java new file mode 100644 index 0000000..619cf11 --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/infra/controller/RelationController.java @@ -0,0 +1,63 @@ +package com.unicorn.hgzero.ai.infra.controller; + +import com.unicorn.hgzero.ai.biz.domain.RelatedMinutes; +import com.unicorn.hgzero.ai.biz.usecase.RelatedTranscriptSearchUseCase; +import com.unicorn.hgzero.ai.infra.dto.response.RelatedTranscriptsResponse; +import com.unicorn.hgzero.ai.infra.dto.common.RelatedTranscriptDto; +import com.unicorn.hgzero.common.dto.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * 관련 회의록 조회 Controller + * GET /api/transcripts/{meetingId}/related + */ +@RestController +@RequestMapping("/api/transcripts") +@RequiredArgsConstructor +@Slf4j +@Tag(name = "Relation", description = "관련 회의록 조회 API") +public class RelationController { + + private final RelatedTranscriptSearchUseCase relatedTranscriptSearchUseCase; + + @GetMapping("/{meetingId}/related") + @Operation(summary = "관련 회의록 조회", description = "벡터 유사도 검색을 통해 관련된 회의록을 찾아 반환합니다") + public ResponseEntity> findRelatedTranscripts( + @PathVariable String meetingId, + @RequestParam String transcriptId, + @RequestParam(defaultValue = "5") int limit) { + + log.info("관련 회의록 조회 요청 - meetingId: {}, transcriptId: {}, limit: {}", meetingId, transcriptId, limit); + + List relatedMinutes = relatedTranscriptSearchUseCase.findRelatedTranscripts( + meetingId, + transcriptId, + limit + ); + + RelatedTranscriptsResponse response = RelatedTranscriptsResponse.builder() + .relatedTranscripts(relatedMinutes.stream() + .map(r -> RelatedTranscriptDto.builder() + .transcriptId(r.getTranscriptId()) + .title(r.getTitle()) + .date(r.getDate()) + .participants(r.getParticipants()) + .relevanceScore(r.getRelevanceScore()) + .commonKeywords(r.getCommonKeywords()) + .link(r.getLink()) + .build()) + .collect(Collectors.toList())) + .totalCount(relatedMinutes.size()) + .build(); + + return ResponseEntity.ok(ApiResponse.success(response)); + } +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/infra/controller/SectionController.java b/ai/src/main/java/com/unicorn/hgzero/ai/infra/controller/SectionController.java new file mode 100644 index 0000000..4e3f286 --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/infra/controller/SectionController.java @@ -0,0 +1,51 @@ +package com.unicorn.hgzero.ai.infra.controller; + +import com.unicorn.hgzero.ai.biz.usecase.SectionSummaryUseCase; +import com.unicorn.hgzero.ai.infra.dto.request.SectionSummaryRequest; +import com.unicorn.hgzero.ai.infra.dto.response.SectionSummaryResponse; +import com.unicorn.hgzero.common.dto.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; + +/** + * 섹션 AI 요약 재생성 Controller + * POST /api/sections/{sectionId}/regenerate-summary + */ +@RestController +@RequestMapping("/api/sections") +@RequiredArgsConstructor +@Slf4j +@Tag(name = "Section", description = "섹션 AI 요약 재생성 API") +public class SectionController { + + private final SectionSummaryUseCase sectionSummaryUseCase; + + @PostMapping("/{sectionId}/regenerate-summary") + @Operation(summary = "섹션 AI 요약 재생성", description = "사용자가 작성한 섹션 내용을 기반으로 AI 요약을 재생성합니다") + public ResponseEntity> regenerateSummary( + @PathVariable String sectionId, + @Valid @RequestBody SectionSummaryRequest request) { + + log.info("섹션 요약 재생성 요청 - sectionId: {}, meetingId: {}", sectionId, request.getMeetingId()); + + String summary = sectionSummaryUseCase.regenerateSummary( + sectionId, + request.getSectionContent(), + request.getMeetingId() + ); + + SectionSummaryResponse response = SectionSummaryResponse.builder() + .summary(summary) + .generatedAt(LocalDateTime.now()) + .build(); + + return ResponseEntity.ok(ApiResponse.success(response)); + } +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/infra/controller/SuggestionController.java b/ai/src/main/java/com/unicorn/hgzero/ai/infra/controller/SuggestionController.java new file mode 100644 index 0000000..debd1cb --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/infra/controller/SuggestionController.java @@ -0,0 +1,99 @@ +package com.unicorn.hgzero.ai.infra.controller; + +import com.unicorn.hgzero.ai.biz.domain.Suggestion; +import com.unicorn.hgzero.ai.biz.usecase.SuggestionUseCase; +import com.unicorn.hgzero.ai.infra.dto.request.DiscussionSuggestionRequest; +import com.unicorn.hgzero.ai.infra.dto.request.DecisionSuggestionRequest; +import com.unicorn.hgzero.ai.infra.dto.response.DiscussionSuggestionResponse; +import com.unicorn.hgzero.ai.infra.dto.response.DecisionSuggestionResponse; +import com.unicorn.hgzero.ai.infra.dto.common.DiscussionSuggestionDto; +import com.unicorn.hgzero.ai.infra.dto.common.DecisionSuggestionDto; +import com.unicorn.hgzero.common.dto.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 논의사항/결정사항 제안 Controller + * POST /api/suggestions/discussion + * POST /api/suggestions/decision + */ +@RestController +@RequestMapping("/api/suggestions") +@RequiredArgsConstructor +@Slf4j +@Tag(name = "Suggestion", description = "논의사항/결정사항 제안 API") +public class SuggestionController { + + private final SuggestionUseCase suggestionUseCase; + + @PostMapping("/discussion") + @Operation(summary = "논의사항 제안", description = "현재 회의 진행 상황을 분석하여 추가로 논의하면 좋을 주제를 제안합니다") + public ResponseEntity> suggestDiscussion( + @Valid @RequestBody DiscussionSuggestionRequest request) { + + log.info("논의사항 제안 요청 - meetingId: {}", request.getMeetingId()); + + List suggestions = suggestionUseCase.suggestDiscussions( + request.getMeetingId(), + request.getTranscriptText() + ); + + DiscussionSuggestionResponse response = DiscussionSuggestionResponse.builder() + .suggestions(suggestions.stream() + .map(s -> DiscussionSuggestionDto.builder() + .id(s.getId()) + .topic(s.getContent()) + .reason(s.getReason()) + .priority(s.getPriority()) + .relatedAgenda(s.getRelatedAgenda()) + .estimatedTime(s.getEstimatedTime()) + .build()) + .collect(Collectors.toList())) + .totalCount(suggestions.size()) + .timestamp(LocalDateTime.now()) + .build(); + + return ResponseEntity.ok(ApiResponse.success(response)); + } + + @PostMapping("/decision") + @Operation(summary = "결정사항 제안", description = "회의록 텍스트에서 결정사항 패턴을 감지하여 제안합니다") + public ResponseEntity> suggestDecision( + @Valid @RequestBody DecisionSuggestionRequest request) { + + log.info("결정사항 제안 요청 - meetingId: {}", request.getMeetingId()); + + List suggestions = suggestionUseCase.suggestDecisions( + request.getMeetingId(), + request.getTranscriptText() + ); + + DecisionSuggestionResponse response = DecisionSuggestionResponse.builder() + .suggestions(suggestions.stream() + .map(s -> DecisionSuggestionDto.builder() + .id(s.getId()) + .content(s.getContent()) + .category(s.getCategory()) + .decisionMaker("") // TODO: Extract from suggestion + .participants(s.getParticipants()) + .confidence(s.getConfidence()) + .extractedFrom(s.getExtractedFrom()) + .context(s.getContext()) + .build()) + .collect(Collectors.toList())) + .totalCount(suggestions.size()) + .timestamp(LocalDateTime.now()) + .build(); + + return ResponseEntity.ok(ApiResponse.success(response)); + } +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/infra/controller/TermController.java b/ai/src/main/java/com/unicorn/hgzero/ai/infra/controller/TermController.java new file mode 100644 index 0000000..553a9bf --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/infra/controller/TermController.java @@ -0,0 +1,80 @@ +package com.unicorn.hgzero.ai.infra.controller; + +import com.unicorn.hgzero.ai.biz.domain.Term; +import com.unicorn.hgzero.ai.biz.usecase.TermDetectionUseCase; +import com.unicorn.hgzero.ai.infra.dto.request.TermDetectionRequest; +import com.unicorn.hgzero.ai.infra.dto.response.TermDetectionResponse; +import com.unicorn.hgzero.ai.infra.dto.common.DetectedTermDto; +import com.unicorn.hgzero.ai.infra.dto.common.HighlightInfoDto; +import com.unicorn.hgzero.ai.infra.dto.common.TextPositionDto; +import com.unicorn.hgzero.common.dto.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * 전문용어 감지 Controller + * POST /api/terms/detect + */ +@RestController +@RequestMapping("/api/terms") +@RequiredArgsConstructor +@Slf4j +@Tag(name = "Term", description = "전문용어 감지 및 설명 API") +public class TermController { + + private final TermDetectionUseCase termDetectionUseCase; + + @PostMapping("/detect") + @Operation(summary = "전문용어 감지", description = "회의록 텍스트에서 전문용어를 자동으로 감지합니다") + public ResponseEntity> detectTerms( + @Valid @RequestBody TermDetectionRequest request) { + + log.info("전문용어 감지 요청 - meetingId: {}, organizationId: {}", + request.getMeetingId(), request.getOrganizationId()); + + List terms = termDetectionUseCase.detectTerms( + request.getMeetingId(), + request.getText(), + request.getOrganizationId() + ); + + List detectedTerms = terms.stream() + .map(t -> DetectedTermDto.builder() + .term(t.getTerm()) + .position(t.getPosition() != null ? TextPositionDto.builder() + .line(t.getPosition().getLine()) + .offset(t.getPosition().getOffset()) + .build() : null) + .confidence(t.getConfidence()) + .category(t.getCategory()) + .highlight(t.getHighlight()) + .build()) + .collect(Collectors.toList()); + + List highlightInfo = detectedTerms.stream() + .filter(t -> Boolean.TRUE.equals(t.getHighlight())) + .map(t -> HighlightInfoDto.builder() + .term(t.getTerm()) + .position(t.getPosition()) + .style("background-color: yellow") + .tooltip("용어 설명 로딩 중...") + .build()) + .collect(Collectors.toList()); + + TermDetectionResponse response = TermDetectionResponse.builder() + .detectedTerms(detectedTerms) + .totalCount(detectedTerms.size()) + .highlightInfo(highlightInfo) + .build(); + + return ResponseEntity.ok(ApiResponse.success(response)); + } +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/infra/controller/TodoController.java b/ai/src/main/java/com/unicorn/hgzero/ai/infra/controller/TodoController.java new file mode 100644 index 0000000..0ca0ac8 --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/infra/controller/TodoController.java @@ -0,0 +1,65 @@ +package com.unicorn.hgzero.ai.infra.controller; + +import com.unicorn.hgzero.ai.biz.domain.ExtractedTodo; +import com.unicorn.hgzero.ai.biz.usecase.TodoExtractionUseCase; +import com.unicorn.hgzero.ai.infra.dto.request.TodoExtractionRequest; +import com.unicorn.hgzero.ai.infra.dto.response.TodoExtractionResponse; +import com.unicorn.hgzero.ai.infra.dto.common.ExtractedTodoDto; +import com.unicorn.hgzero.common.dto.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Todo 자동 추출 Controller + * POST /api/todos/extract + */ +@RestController +@RequestMapping("/api/todos") +@RequiredArgsConstructor +@Slf4j +@Tag(name = "Todo", description = "Todo 자동 추출 API") +public class TodoController { + + private final TodoExtractionUseCase todoExtractionUseCase; + + @PostMapping("/extract") + @Operation(summary = "Todo 자동 추출", description = "회의록에서 액션 아이템을 자동으로 추출하고 담당자를 식별합니다") + public ResponseEntity> extractTodos( + @RequestHeader("X-User-Id") String userId, + @Valid @RequestBody TodoExtractionRequest request) { + + log.info("Todo 추출 요청 - meetingId: {}, userId: {}", request.getMeetingId(), userId); + + List todos = todoExtractionUseCase.extractTodos( + request.getMeetingId(), + request.getMinutesContent(), + request.getUserId() != null ? request.getUserId() : userId + ); + + TodoExtractionResponse response = TodoExtractionResponse.builder() + .meetingId(request.getMeetingId()) + .todos(todos.stream() + .map(t -> ExtractedTodoDto.builder() + .content(t.getContent()) + .assignee(t.getAssignee()) + .dueDate(t.getDueDate()) + .priority(t.getPriority()) + .sectionReference(t.getSectionReference()) + .build()) + .collect(Collectors.toList())) + .totalCount(todos.size()) + .extractedAt(LocalDateTime.now()) + .build(); + + return ResponseEntity.ok(ApiResponse.success(response)); + } +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/infra/controller/TranscriptController.java b/ai/src/main/java/com/unicorn/hgzero/ai/infra/controller/TranscriptController.java new file mode 100644 index 0000000..ab03938 --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/infra/controller/TranscriptController.java @@ -0,0 +1,86 @@ +package com.unicorn.hgzero.ai.infra.controller; + +import com.unicorn.hgzero.ai.biz.domain.ProcessedTranscript; +import com.unicorn.hgzero.ai.biz.usecase.TranscriptProcessUseCase; +import com.unicorn.hgzero.ai.infra.dto.request.TranscriptProcessRequest; +import com.unicorn.hgzero.ai.infra.dto.response.TranscriptProcessResponse; +import com.unicorn.hgzero.ai.infra.dto.common.*; +import com.unicorn.hgzero.common.dto.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.stream.Collectors; + +/** + * 회의록 자동 작성 Controller + * POST /api/transcripts/process + */ +@RestController +@RequestMapping("/api/transcripts") +@RequiredArgsConstructor +@Slf4j +@Tag(name = "Transcript", description = "회의록 자동 작성 API") +public class TranscriptController { + + private final TranscriptProcessUseCase transcriptProcessUseCase; + + @PostMapping("/process") + @Operation(summary = "회의록 자동 작성", description = "STT에서 변환된 텍스트를 받아 LLM 기반으로 회의록을 자동 작성합니다") + public ResponseEntity> processTranscript( + @RequestHeader("X-User-Id") String userId, + @RequestHeader("X-User-Name") String userName, + @Valid @RequestBody TranscriptProcessRequest request) { + + log.info("회의록 자동 작성 요청 - meetingId: {}, userId: {}", request.getMeetingId(), userId); + + ProcessedTranscript result = transcriptProcessUseCase.processTranscript( + request.getMeetingId(), + request.getTranscriptText(), + request.getUserId() != null ? request.getUserId() : userId, + request.getUserName() != null ? request.getUserName() : userName, + request.getContext() != null ? request.getContext().getTitle() : "", + request.getContext() != null ? request.getContext().getParticipants() : null, + request.getContext() != null ? request.getContext().getAgenda() : null + ); + + TranscriptProcessResponse response = mapToResponse(result); + return ResponseEntity.ok(ApiResponse.success(response)); + } + + private TranscriptProcessResponse mapToResponse(ProcessedTranscript domain) { + return TranscriptProcessResponse.builder() + .transcriptId(domain.getTranscriptId()) + .meetingId(domain.getMeetingId()) + .content(mapContent(domain)) + .suggestions(null) // TODO: 실시간 제안 기능 구현 시 추가 + .createdAt(domain.getCreatedAt()) + .status(domain.getStatus()) + .build(); + } + + private TranscriptContentDto mapContent(ProcessedTranscript domain) { + return TranscriptContentDto.builder() + .summary(domain.getSummary()) + .discussions(domain.getDiscussions().stream() + .map(d -> DiscussionItemDto.builder() + .topic(d.getTopic()) + .speaker(d.getSpeaker()) + .content(d.getContent()) + .build()) + .collect(Collectors.toList())) + .decisions(domain.getDecisions().stream() + .map(d -> DecisionItemDto.builder() + .content(d.getContent()) + .decisionMaker(d.getDecisionMaker()) + .category(d.getCategory()) + .build()) + .collect(Collectors.toList())) + .pendingItems(domain.getPendingItems()) + .build(); + } +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/DecisionItemDto.java b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/DecisionItemDto.java new file mode 100644 index 0000000..f48dd99 --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/DecisionItemDto.java @@ -0,0 +1,32 @@ +package com.unicorn.hgzero.ai.infra.dto.common; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * 결정사항 아이템 DTO + * 결정 내용, 결정자, 카테고리 포함 + */ +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DecisionItemDto { + + /** + * 결정 내용 + */ + private String content; + + /** + * 결정자 + */ + private String decisionMaker; + + /** + * 결정 카테고리 (기술, 일정, 리소스, 정책, 기타) + */ + private String category; +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/DecisionSuggestionDto.java b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/DecisionSuggestionDto.java new file mode 100644 index 0000000..5de073e --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/DecisionSuggestionDto.java @@ -0,0 +1,59 @@ +package com.unicorn.hgzero.ai.infra.dto.common; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 결정사항 제안 DTO + * AI가 감지한 결정사항 패턴 정보 + */ +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DecisionSuggestionDto { + + /** + * 제안 ID + */ + private String id; + + /** + * 결정 내용 + */ + private String content; + + /** + * 결정 카테고리 (기술, 일정, 리소스, 정책, 기타) + */ + private String category; + + /** + * 결정자 + */ + private String decisionMaker; + + /** + * 참여자 목록 + */ + private List participants; + + /** + * 신뢰도 점수 (0-1) + */ + private Double confidence; + + /** + * 원문 발췌 + */ + private String extractedFrom; + + /** + * 결정 배경 + */ + private String context; +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/DetectedTermDto.java b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/DetectedTermDto.java new file mode 100644 index 0000000..7ab602c --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/DetectedTermDto.java @@ -0,0 +1,42 @@ +package com.unicorn.hgzero.ai.infra.dto.common; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * 감지된 전문용어 DTO + * 회의록에서 감지된 전문용어 정보 + */ +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DetectedTermDto { + + /** + * 용어명 + */ + private String term; + + /** + * 텍스트 위치 정보 + */ + private TextPositionDto position; + + /** + * 신뢰도 점수 (0-1) + */ + private Double confidence; + + /** + * 용어 카테고리 (기술, 업무, 도메인) + */ + private String category; + + /** + * 하이라이트 여부 + */ + private Boolean highlight; +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/DiscussionItemDto.java b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/DiscussionItemDto.java new file mode 100644 index 0000000..dcdd98f --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/DiscussionItemDto.java @@ -0,0 +1,32 @@ +package com.unicorn.hgzero.ai.infra.dto.common; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * 논의사항 아이템 DTO + * 논의 주제, 발언자, 논의 내용 포함 + */ +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DiscussionItemDto { + + /** + * 논의 주제 + */ + private String topic; + + /** + * 발언자 + */ + private String speaker; + + /** + * 논의 내용 + */ + private String content; +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/DiscussionSuggestionDto.java b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/DiscussionSuggestionDto.java new file mode 100644 index 0000000..48ff93e --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/DiscussionSuggestionDto.java @@ -0,0 +1,47 @@ +package com.unicorn.hgzero.ai.infra.dto.common; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * 논의사항 제안 DTO + * AI가 추천하는 논의 주제 정보 + */ +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DiscussionSuggestionDto { + + /** + * 제안 ID + */ + private String id; + + /** + * 논의 주제 + */ + private String topic; + + /** + * 제안 이유 + */ + private String reason; + + /** + * 우선순위 (HIGH, MEDIUM, LOW) + */ + private String priority; + + /** + * 관련 안건 + */ + private String relatedAgenda; + + /** + * 예상 소요 시간 (분) + */ + private Integer estimatedTime; +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/ErrorResponseDto.java b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/ErrorResponseDto.java new file mode 100644 index 0000000..86d2de2 --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/ErrorResponseDto.java @@ -0,0 +1,34 @@ +package com.unicorn.hgzero.ai.infra.dto.common; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 에러 응답 DTO + * API 에러 발생 시 통일된 형식으로 에러 정보 반환 + */ +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ErrorResponseDto { + + /** + * 에러 코드 + */ + private String error; + + /** + * 에러 메시지 + */ + private String message; + + /** + * 에러 발생 시각 + */ + private LocalDateTime timestamp; +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/ExtractedTodoDto.java b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/ExtractedTodoDto.java new file mode 100644 index 0000000..2d779cd --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/ExtractedTodoDto.java @@ -0,0 +1,44 @@ +package com.unicorn.hgzero.ai.infra.dto.common; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; + +/** + * 추출된 Todo DTO + * AI가 회의록에서 추출한 Todo 정보 + */ +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ExtractedTodoDto { + + /** + * Todo 내용 + */ + private String content; + + /** + * 담당자 + */ + private String assignee; + + /** + * 마감일 + */ + private LocalDate dueDate; + + /** + * 우선순위 (HIGH, MEDIUM, LOW) + */ + private String priority; + + /** + * 관련 회의록 섹션 + */ + private String sectionReference; +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/HighlightInfoDto.java b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/HighlightInfoDto.java new file mode 100644 index 0000000..1a7a4ff --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/HighlightInfoDto.java @@ -0,0 +1,37 @@ +package com.unicorn.hgzero.ai.infra.dto.common; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * 하이라이트 정보 DTO + * 용어 하이라이트 스타일과 툴팁 정보 + */ +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class HighlightInfoDto { + + /** + * 용어명 + */ + private String term; + + /** + * 텍스트 위치 정보 + */ + private TextPositionDto position; + + /** + * 하이라이트 스타일 + */ + private String style; + + /** + * 툴팁 텍스트 + */ + private String tooltip; +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/MeetingContextDto.java b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/MeetingContextDto.java new file mode 100644 index 0000000..62a6950 --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/MeetingContextDto.java @@ -0,0 +1,39 @@ +package com.unicorn.hgzero.ai.infra.dto.common; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 회의 맥락 정보 DTO + * 회의 제목, 참석자, 안건, 이전 회의록 등의 맥락 정보를 전달 + */ +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MeetingContextDto { + + /** + * 회의 제목 + */ + private String title; + + /** + * 참석자 목록 + */ + private List participants; + + /** + * 회의 안건 목록 + */ + private List agenda; + + /** + * 이전 회의록 내용 + */ + private String previousContent; +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/PastDiscussionDto.java b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/PastDiscussionDto.java new file mode 100644 index 0000000..4a199b4 --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/PastDiscussionDto.java @@ -0,0 +1,40 @@ +package com.unicorn.hgzero.ai.infra.dto.common; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; +import java.util.List; + +/** + * 과거 논의 DTO + * 전문용어 관련 과거 논의 정보 + */ +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PastDiscussionDto { + + /** + * 논의 날짜 + */ + private LocalDate date; + + /** + * 참석자 목록 + */ + private List participants; + + /** + * 논의 요약 + */ + private String summary; + + /** + * 회의록 링크 + */ + private String link; +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/RealtimeSuggestionsDto.java b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/RealtimeSuggestionsDto.java new file mode 100644 index 0000000..d34e26f --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/RealtimeSuggestionsDto.java @@ -0,0 +1,29 @@ +package com.unicorn.hgzero.ai.infra.dto.common; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 실시간 추천사항 DTO + * 논의 주제와 결정사항 제안을 포함 + */ +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RealtimeSuggestionsDto { + + /** + * 논의 주제 제안 목록 + */ + private List discussionTopics; + + /** + * 결정사항 제안 목록 + */ + private List decisions; +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/ReferenceDto.java b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/ReferenceDto.java new file mode 100644 index 0000000..429251a --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/ReferenceDto.java @@ -0,0 +1,32 @@ +package com.unicorn.hgzero.ai.infra.dto.common; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * 참조 문서 DTO + * 전문용어 관련 참조 문서 정보 + */ +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ReferenceDto { + + /** + * 문서 제목 + */ + private String title; + + /** + * 문서 유형 (위키, 매뉴얼, 회의록, 보고서) + */ + private String type; + + /** + * 문서 URL + */ + private String link; +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/RelatedProjectDto.java b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/RelatedProjectDto.java new file mode 100644 index 0000000..af0ee93 --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/RelatedProjectDto.java @@ -0,0 +1,27 @@ +package com.unicorn.hgzero.ai.infra.dto.common; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * 관련 프로젝트 DTO + * 전문용어와 관련된 프로젝트 정보 + */ +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RelatedProjectDto { + + /** + * 프로젝트명 + */ + private String name; + + /** + * 연관성 설명 + */ + private String relevance; +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/RelatedTranscriptDto.java b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/RelatedTranscriptDto.java new file mode 100644 index 0000000..ec43658 --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/RelatedTranscriptDto.java @@ -0,0 +1,55 @@ +package com.unicorn.hgzero.ai.infra.dto.common; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; +import java.util.List; + +/** + * 관련 회의록 DTO + * RAG 검색으로 찾은 관련 회의록 정보 + */ +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RelatedTranscriptDto { + + /** + * 회의록 ID + */ + private String transcriptId; + + /** + * 회의 제목 + */ + private String title; + + /** + * 회의 날짜 + */ + private LocalDate date; + + /** + * 참석자 목록 + */ + private List participants; + + /** + * 관련도 점수 (0-100%) + */ + private Double relevanceScore; + + /** + * 공통 키워드 목록 + */ + private List commonKeywords; + + /** + * 회의록 링크 + */ + private String link; +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/TextPositionDto.java b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/TextPositionDto.java new file mode 100644 index 0000000..68ad0f0 --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/TextPositionDto.java @@ -0,0 +1,27 @@ +package com.unicorn.hgzero.ai.infra.dto.common; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * 텍스트 위치 정보 DTO + * 줄 번호와 오프셋 정보를 포함 + */ +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TextPositionDto { + + /** + * 줄 번호 + */ + private Integer line; + + /** + * 시작 오프셋 + */ + private Integer offset; +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/TranscriptContentDto.java b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/TranscriptContentDto.java new file mode 100644 index 0000000..2b68db8 --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/common/TranscriptContentDto.java @@ -0,0 +1,39 @@ +package com.unicorn.hgzero.ai.infra.dto.common; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 회의록 내용 DTO + * 전체 요약, 논의사항, 결정사항, 보류사항 포함 + */ +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TranscriptContentDto { + + /** + * 전체 요약 + */ + private String summary; + + /** + * 논의사항 목록 + */ + private List discussions; + + /** + * 결정사항 목록 + */ + private List decisions; + + /** + * 보류사항 목록 + */ + private List pendingItems; +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/request/DecisionSuggestionRequest.java b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/request/DecisionSuggestionRequest.java new file mode 100644 index 0000000..cb2edda --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/request/DecisionSuggestionRequest.java @@ -0,0 +1,31 @@ +package com.unicorn.hgzero.ai.infra.dto.request; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import jakarta.validation.constraints.NotBlank; + +/** + * 결정사항 제안 요청 DTO + * 회의록 텍스트에서 결정사항 패턴을 감지하여 제안 요청 + */ +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DecisionSuggestionRequest { + + /** + * 회의 ID (필수) + */ + @NotBlank(message = "회의 ID는 필수입니다") + private String meetingId; + + /** + * 현재까지의 회의록 텍스트 (필수) + */ + @NotBlank(message = "회의록 텍스트는 필수입니다") + private String transcriptText; +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/request/DiscussionSuggestionRequest.java b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/request/DiscussionSuggestionRequest.java new file mode 100644 index 0000000..247e9d0 --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/request/DiscussionSuggestionRequest.java @@ -0,0 +1,31 @@ +package com.unicorn.hgzero.ai.infra.dto.request; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import jakarta.validation.constraints.NotBlank; + +/** + * 논의사항 제안 요청 DTO + * 현재 회의 진행 상황을 분석하여 추가 논의 주제 제안 요청 + */ +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DiscussionSuggestionRequest { + + /** + * 회의 ID (필수) + */ + @NotBlank(message = "회의 ID는 필수입니다") + private String meetingId; + + /** + * 현재까지의 회의록 텍스트 (필수) + */ + @NotBlank(message = "회의록 텍스트는 필수입니다") + private String transcriptText; +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/request/SectionSummaryRequest.java b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/request/SectionSummaryRequest.java new file mode 100644 index 0000000..1ab9ecf --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/request/SectionSummaryRequest.java @@ -0,0 +1,30 @@ +package com.unicorn.hgzero.ai.infra.dto.request; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import jakarta.validation.constraints.NotBlank; + +/** + * 섹션 AI 요약 재생성 요청 DTO + * 사용자가 작성한 섹션 내용을 기반으로 AI 요약 재생성 요청 + */ +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SectionSummaryRequest { + + /** + * 사용자가 작성/수정한 섹션 내용 (필수, Markdown 형식) + */ + @NotBlank(message = "섹션 내용은 필수입니다") + private String sectionContent; + + /** + * 회의 ID (맥락 이해용, 선택적) + */ + private String meetingId; +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/request/TermDetectionRequest.java b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/request/TermDetectionRequest.java new file mode 100644 index 0000000..63e4b88 --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/request/TermDetectionRequest.java @@ -0,0 +1,36 @@ +package com.unicorn.hgzero.ai.infra.dto.request; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import jakarta.validation.constraints.NotBlank; + +/** + * 전문용어 감지 요청 DTO + * 회의록 텍스트에서 전문용어를 자동으로 감지 요청 + */ +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TermDetectionRequest { + + /** + * 회의 ID (필수) + */ + @NotBlank(message = "회의 ID는 필수입니다") + private String meetingId; + + /** + * 분석할 회의록 텍스트 (필수) + */ + @NotBlank(message = "분석할 텍스트는 필수입니다") + private String text; + + /** + * 조직 ID + */ + private String organizationId; +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/request/TodoExtractionRequest.java b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/request/TodoExtractionRequest.java new file mode 100644 index 0000000..f462e64 --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/request/TodoExtractionRequest.java @@ -0,0 +1,36 @@ +package com.unicorn.hgzero.ai.infra.dto.request; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import jakarta.validation.constraints.NotBlank; + +/** + * Todo 자동 추출 요청 DTO + * 회의록에서 액션 아이템을 자동으로 추출하고 담당자 식별 요청 + */ +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TodoExtractionRequest { + + /** + * 회의 ID (필수) + */ + @NotBlank(message = "회의 ID는 필수입니다") + private String meetingId; + + /** + * 요청자 ID + */ + private String userId; + + /** + * 회의록 전체 내용 (필수, Markdown 형식) + */ + @NotBlank(message = "회의록 내용은 필수입니다") + private String minutesContent; +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/request/TranscriptProcessRequest.java b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/request/TranscriptProcessRequest.java new file mode 100644 index 0000000..399ff78 --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/request/TranscriptProcessRequest.java @@ -0,0 +1,48 @@ +package com.unicorn.hgzero.ai.infra.dto.request; + +import com.unicorn.hgzero.ai.infra.dto.common.MeetingContextDto; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +/** + * 회의록 자동 작성 요청 DTO + * STT에서 변환된 텍스트를 받아 LLM 기반 회의록 자동 작성 요청 + */ +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TranscriptProcessRequest { + + /** + * 회의 ID (필수) + */ + @NotBlank(message = "회의 ID는 필수입니다") + private String meetingId; + + /** + * STT에서 변환된 텍스트 (필수) + */ + @NotBlank(message = "회의록 텍스트는 필수입니다") + private String transcriptText; + + /** + * 사용자 ID + */ + private String userId; + + /** + * 사용자 이름 + */ + private String userName; + + /** + * 회의 맥락 정보 + */ + private MeetingContextDto context; +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/response/DecisionSuggestionResponse.java b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/response/DecisionSuggestionResponse.java new file mode 100644 index 0000000..b2fed9c --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/response/DecisionSuggestionResponse.java @@ -0,0 +1,36 @@ +package com.unicorn.hgzero.ai.infra.dto.response; + +import com.unicorn.hgzero.ai.infra.dto.common.DecisionSuggestionDto; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 결정사항 제안 응답 DTO + * AI가 감지한 결정사항 패턴 목록 반환 + */ +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DecisionSuggestionResponse { + + /** + * 결정사항 제안 목록 + */ + private List suggestions; + + /** + * 제안 개수 + */ + private Integer totalCount; + + /** + * 생성 시각 + */ + private LocalDateTime timestamp; +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/response/DiscussionSuggestionResponse.java b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/response/DiscussionSuggestionResponse.java new file mode 100644 index 0000000..96bcc82 --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/response/DiscussionSuggestionResponse.java @@ -0,0 +1,36 @@ +package com.unicorn.hgzero.ai.infra.dto.response; + +import com.unicorn.hgzero.ai.infra.dto.common.DiscussionSuggestionDto; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 논의사항 제안 응답 DTO + * AI가 제안하는 추가 논의 주제 목록 반환 + */ +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DiscussionSuggestionResponse { + + /** + * 논의사항 제안 목록 + */ + private List suggestions; + + /** + * 제안 개수 + */ + private Integer totalCount; + + /** + * 생성 시각 + */ + private LocalDateTime timestamp; +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/response/RelatedTranscriptsResponse.java b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/response/RelatedTranscriptsResponse.java new file mode 100644 index 0000000..dd83a32 --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/response/RelatedTranscriptsResponse.java @@ -0,0 +1,30 @@ +package com.unicorn.hgzero.ai.infra.dto.response; + +import com.unicorn.hgzero.ai.infra.dto.common.RelatedTranscriptDto; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 관련 회의록 조회 응답 DTO + * RAG 검색으로 찾은 관련 회의록 목록 반환 + */ +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RelatedTranscriptsResponse { + + /** + * 관련 회의록 목록 + */ + private List relatedTranscripts; + + /** + * 관련 회의록 개수 + */ + private Integer totalCount; +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/response/SectionSummaryResponse.java b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/response/SectionSummaryResponse.java new file mode 100644 index 0000000..75b1d8f --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/response/SectionSummaryResponse.java @@ -0,0 +1,29 @@ +package com.unicorn.hgzero.ai.infra.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 섹션 요약 응답 DTO + * AI가 생성한 섹션 요약 반환 + */ +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SectionSummaryResponse { + + /** + * 생성된 AI 요약 (2-3문장) + */ + private String summary; + + /** + * 생성 시간 + */ + private LocalDateTime generatedAt; +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/response/TermDetectionResponse.java b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/response/TermDetectionResponse.java new file mode 100644 index 0000000..c6a8125 --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/response/TermDetectionResponse.java @@ -0,0 +1,36 @@ +package com.unicorn.hgzero.ai.infra.dto.response; + +import com.unicorn.hgzero.ai.infra.dto.common.DetectedTermDto; +import com.unicorn.hgzero.ai.infra.dto.common.HighlightInfoDto; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 전문용어 감지 응답 DTO + * 감지된 전문용어 목록과 하이라이트 정보 반환 + */ +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TermDetectionResponse { + + /** + * 감지된 용어 목록 + */ + private List detectedTerms; + + /** + * 감지된 용어 개수 + */ + private Integer totalCount; + + /** + * 하이라이트 정보 목록 + */ + private List highlightInfo; +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/response/TermExplanationResponse.java b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/response/TermExplanationResponse.java new file mode 100644 index 0000000..5cba7b8 --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/response/TermExplanationResponse.java @@ -0,0 +1,57 @@ +package com.unicorn.hgzero.ai.infra.dto.response; + +import com.unicorn.hgzero.ai.infra.dto.common.PastDiscussionDto; +import com.unicorn.hgzero.ai.infra.dto.common.ReferenceDto; +import com.unicorn.hgzero.ai.infra.dto.common.RelatedProjectDto; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 전문용어 설명 응답 DTO + * RAG 기반 맥락적 용어 설명 반환 + */ +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TermExplanationResponse { + + /** + * 용어명 + */ + private String term; + + /** + * 간단한 정의 + */ + private String basicDefinition; + + /** + * 현재 회의 맥락에서의 의미 + */ + private String contextualMeaning; + + /** + * 실제 사용 사례 목록 + */ + private List useCases; + + /** + * 관련 프로젝트 목록 + */ + private List relatedProjects; + + /** + * 과거 논의 목록 + */ + private List pastDiscussions; + + /** + * 참조 문서 목록 + */ + private List references; +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/response/TodoExtractionResponse.java b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/response/TodoExtractionResponse.java new file mode 100644 index 0000000..fa7ad52 --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/response/TodoExtractionResponse.java @@ -0,0 +1,41 @@ +package com.unicorn.hgzero.ai.infra.dto.response; + +import com.unicorn.hgzero.ai.infra.dto.common.ExtractedTodoDto; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * Todo 추출 응답 DTO + * AI가 추출한 Todo 목록 반환 + */ +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TodoExtractionResponse { + + /** + * 회의 ID + */ + private String meetingId; + + /** + * 추출된 Todo 목록 + */ + private List todos; + + /** + * 추출된 Todo 개수 + */ + private Integer totalCount; + + /** + * 추출 시간 + */ + private LocalDateTime extractedAt; +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/response/TranscriptProcessResponse.java b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/response/TranscriptProcessResponse.java new file mode 100644 index 0000000..a95d264 --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/infra/dto/response/TranscriptProcessResponse.java @@ -0,0 +1,51 @@ +package com.unicorn.hgzero.ai.infra.dto.response; + +import com.unicorn.hgzero.ai.infra.dto.common.RealtimeSuggestionsDto; +import com.unicorn.hgzero.ai.infra.dto.common.TranscriptContentDto; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 회의록 자동 작성 응답 DTO + * LLM 기반으로 생성된 회의록 정보 반환 + */ +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TranscriptProcessResponse { + + /** + * 생성된 회의록 ID + */ + private String transcriptId; + + /** + * 회의 ID + */ + private String meetingId; + + /** + * 회의록 내용 + */ + private TranscriptContentDto content; + + /** + * 실시간 추천사항 + */ + private RealtimeSuggestionsDto suggestions; + + /** + * 생성 시간 + */ + private LocalDateTime createdAt; + + /** + * 회의록 상태 (DRAFT, COMPLETED) + */ + private String status; +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/infra/gateway/TranscriptGatewayImpl.java b/ai/src/main/java/com/unicorn/hgzero/ai/infra/gateway/TranscriptGatewayImpl.java new file mode 100644 index 0000000..abd89e2 --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/infra/gateway/TranscriptGatewayImpl.java @@ -0,0 +1,80 @@ +package com.unicorn.hgzero.ai.infra.gateway; + +import com.unicorn.hgzero.ai.biz.domain.ProcessedTranscript; +import com.unicorn.hgzero.ai.biz.gateway.TranscriptGateway; +import com.unicorn.hgzero.ai.infra.gateway.entity.ProcessedTranscriptEntity; +import com.unicorn.hgzero.ai.infra.gateway.repository.ProcessedTranscriptJpaRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * 회의록 Gateway 구현체 + * JPA Repository를 사용한 회의록 영속성 관리 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class TranscriptGatewayImpl implements TranscriptGateway { + + private final ProcessedTranscriptJpaRepository repository; + + @Override + public ProcessedTranscript save(ProcessedTranscript transcript) { + log.debug("Saving transcript: transcriptId={}, meetingId={}", + transcript.getTranscriptId(), transcript.getMeetingId()); + + ProcessedTranscriptEntity entity = ProcessedTranscriptEntity.fromDomain(transcript); + ProcessedTranscriptEntity saved = repository.save(entity); + + log.info("Transcript saved successfully: transcriptId={}", saved.getTranscriptId()); + return saved.toDomain(); + } + + @Override + public Optional findById(String transcriptId) { + log.debug("Finding transcript by id: {}", transcriptId); + return repository.findById(transcriptId) + .map(ProcessedTranscriptEntity::toDomain); + } + + @Override + public Optional findByMeetingId(String meetingId) { + log.debug("Finding transcript by meetingId: {}", meetingId); + return repository.findByMeetingId(meetingId) + .map(ProcessedTranscriptEntity::toDomain); + } + + @Override + public List findByMeetingIds(List meetingIds) { + log.debug("Finding transcripts by meetingIds: count={}", meetingIds.size()); + return repository.findByMeetingIdIn(meetingIds).stream() + .map(ProcessedTranscriptEntity::toDomain) + .collect(Collectors.toList()); + } + + @Override + public List findByStatus(String status) { + log.debug("Finding transcripts by status: {}", status); + return repository.findByStatus(status).stream() + .map(ProcessedTranscriptEntity::toDomain) + .collect(Collectors.toList()); + } + + @Override + public boolean existsByMeetingId(String meetingId) { + log.debug("Checking transcript existence by meetingId: {}", meetingId); + return repository.existsByMeetingId(meetingId); + } + + @Override + public void delete(String transcriptId) { + log.debug("Deleting transcript: {}", transcriptId); + repository.deleteById(transcriptId); + log.info("Transcript deleted successfully: {}", transcriptId); + } +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/infra/gateway/entity/ProcessedTranscriptEntity.java b/ai/src/main/java/com/unicorn/hgzero/ai/infra/gateway/entity/ProcessedTranscriptEntity.java new file mode 100644 index 0000000..31c19af --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/infra/gateway/entity/ProcessedTranscriptEntity.java @@ -0,0 +1,179 @@ +package com.unicorn.hgzero.ai.infra.gateway.entity; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.unicorn.hgzero.ai.biz.domain.ProcessedTranscript; +import com.unicorn.hgzero.common.entity.BaseTimeEntity; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * 처리된 회의록 Entity + * AI가 처리한 회의록 정보를 데이터베이스에 영속화 + */ +@Slf4j +@Entity +@Table(name = "processed_transcripts") +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ProcessedTranscriptEntity extends BaseTimeEntity { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + @Id + @Column(name = "transcript_id", length = 50) + private String transcriptId; + + @Column(name = "meeting_id", length = 50, nullable = false) + private String meetingId; + + @Column(name = "summary", columnDefinition = "TEXT") + private String summary; + + /** + * 논의사항 목록 (JSON 형식) + */ + @Column(name = "discussions", columnDefinition = "TEXT") + private String discussions; + + /** + * 결정사항 목록 (JSON 형식) + */ + @Column(name = "decisions", columnDefinition = "TEXT") + private String decisions; + + /** + * 보류사항 목록 (콤마 구분) + */ + @Column(name = "pending_items", columnDefinition = "TEXT") + private String pendingItems; + + @Column(name = "status", length = 20, nullable = false) + @Builder.Default + private String status = "DRAFT"; + + /** + * Entity를 Domain 모델로 변환 + */ + public ProcessedTranscript toDomain() { + return ProcessedTranscript.builder() + .transcriptId(this.transcriptId) + .meetingId(this.meetingId) + .summary(this.summary) + .discussions(parseDiscussions(this.discussions)) + .decisions(parseDecisions(this.decisions)) + .pendingItems(parsePendingItems(this.pendingItems)) + .createdAt(this.getCreatedAt()) + .status(this.status) + .build(); + } + + /** + * Domain 모델에서 Entity로 변환 + */ + public static ProcessedTranscriptEntity fromDomain(ProcessedTranscript domain) { + return ProcessedTranscriptEntity.builder() + .transcriptId(domain.getTranscriptId()) + .meetingId(domain.getMeetingId()) + .summary(domain.getSummary()) + .discussions(formatDiscussions(domain.getDiscussions())) + .decisions(formatDecisions(domain.getDecisions())) + .pendingItems(formatPendingItems(domain.getPendingItems())) + .status(domain.getStatus()) + .build(); + } + + /** + * 상태 업데이트 + */ + public void updateStatus(String status) { + this.status = status; + } + + /** + * 요약 업데이트 + */ + public void updateSummary(String summary) { + this.summary = summary; + } + + // ======================================== + // Private Helper Methods - JSON 변환 + // ======================================== + + private static List parseDiscussions(String json) { + if (json == null || json.isEmpty()) { + return new ArrayList<>(); + } + try { + return objectMapper.readValue(json, + new TypeReference>() {}); + } catch (JsonProcessingException e) { + log.error("Failed to parse discussions JSON: {}", json, e); + return new ArrayList<>(); + } + } + + private static String formatDiscussions(List discussions) { + if (discussions == null || discussions.isEmpty()) { + return ""; + } + try { + return objectMapper.writeValueAsString(discussions); + } catch (JsonProcessingException e) { + log.error("Failed to format discussions to JSON", e); + return ""; + } + } + + private static List parseDecisions(String json) { + if (json == null || json.isEmpty()) { + return new ArrayList<>(); + } + try { + return objectMapper.readValue(json, + new TypeReference>() {}); + } catch (JsonProcessingException e) { + log.error("Failed to parse decisions JSON: {}", json, e); + return new ArrayList<>(); + } + } + + private static String formatDecisions(List decisions) { + if (decisions == null || decisions.isEmpty()) { + return ""; + } + try { + return objectMapper.writeValueAsString(decisions); + } catch (JsonProcessingException e) { + log.error("Failed to format decisions to JSON", e); + return ""; + } + } + + private static List parsePendingItems(String items) { + if (items == null || items.isEmpty()) { + return new ArrayList<>(); + } + return Arrays.asList(items.split(",")); + } + + private static String formatPendingItems(List items) { + if (items == null || items.isEmpty()) { + return ""; + } + return String.join(",", items); + } +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/infra/gateway/repository/ProcessedTranscriptJpaRepository.java b/ai/src/main/java/com/unicorn/hgzero/ai/infra/gateway/repository/ProcessedTranscriptJpaRepository.java new file mode 100644 index 0000000..4541b8b --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/infra/gateway/repository/ProcessedTranscriptJpaRepository.java @@ -0,0 +1,41 @@ +package com.unicorn.hgzero.ai.infra.gateway.repository; + +import com.unicorn.hgzero.ai.infra.gateway.entity.ProcessedTranscriptEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +/** + * 처리된 회의록 JPA Repository + * 회의록 데이터 영속성 관리 + */ +@Repository +public interface ProcessedTranscriptJpaRepository extends JpaRepository { + + /** + * 회의 ID로 회의록 조회 + */ + Optional findByMeetingId(String meetingId); + + /** + * 회의 ID 목록으로 회의록 목록 조회 + */ + List findByMeetingIdIn(List meetingIds); + + /** + * 상태로 회의록 목록 조회 + */ + List findByStatus(String status); + + /** + * 회의 ID와 상태로 회의록 조회 + */ + Optional findByMeetingIdAndStatus(String meetingId, String status); + + /** + * 회의 ID로 회의록 존재 여부 확인 + */ + boolean existsByMeetingId(String meetingId); +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/infra/llm/OpenAiLlmGateway.java b/ai/src/main/java/com/unicorn/hgzero/ai/infra/llm/OpenAiLlmGateway.java new file mode 100644 index 0000000..dc6de6c --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/infra/llm/OpenAiLlmGateway.java @@ -0,0 +1,146 @@ +package com.unicorn.hgzero.ai.infra.llm; + +import com.unicorn.hgzero.ai.biz.gateway.LlmGateway; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * OpenAI LLM Gateway 구현체 + * OpenAI API를 사용한 LLM 연동 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class OpenAiLlmGateway implements LlmGateway { + + // TODO: OpenAI API 클라이언트 주입 + // private final OpenAiClient openAiClient; + + @Override + public String generateTranscript(String transcriptText, String title, List participants, List agenda) { + log.info("Generating transcript using OpenAI: title={}", title); + + // TODO: OpenAI API 호출 + // 1. 프롬프트 구성 (회의록 자동 작성 프롬프트) + // 2. GPT-4 호출 + // 3. 응답 JSON 파싱 + // 4. 반환 + + // 임시 mock 응답 + return """ + { + "summary": "회의록 자동 생성 요약", + "discussions": [ + { + "topic": "프로젝트 진행 상황", + "speaker": "김철수", + "content": "현재 80% 진행 중" + } + ], + "decisions": [ + { + "content": "React로 프론트엔드 개발", + "decisionMaker": "이영희", + "category": "기술" + } + ], + "pendingItems": ["추가 예산 검토", "외주 업체 선정"] + } + """; + } + + @Override + public String extractTodos(String minutesContent) { + log.info("Extracting todos using OpenAI"); + + // TODO: OpenAI API 호출 (Todo 추출 프롬프트) + return """ + { + "todos": [ + { + "content": "API 설계서 작성", + "assignee": "박민수", + "dueDate": "2025-01-30", + "priority": "HIGH", + "sectionReference": "결정사항 #3" + } + ] + } + """; + } + + @Override + public String generateSummary(String sectionContent, String meetingContext) { + log.info("Generating section summary using OpenAI"); + + // TODO: OpenAI API 호출 (섹션 요약 프롬프트) + return "AI 기반 회의록 자동화 서비스로 결정. 타겟은 중소기업 및 스타트업이며, 주요 기능은 음성인식, AI 요약, Todo 추출입니다."; + } + + @Override + public String detectTerms(String text, String organizationId) { + log.info("Detecting terms using OpenAI: organizationId={}", organizationId); + + // TODO: OpenAI API 호출 (전문용어 감지 프롬프트) + return """ + { + "terms": [ + { + "term": "MSA", + "position": {"line": 5, "offset": 42}, + "confidence": 0.92, + "category": "기술", + "highlight": true + } + ] + } + """; + } + + @Override + public String suggestDiscussions(String transcriptText, List agenda) { + log.info("Suggesting discussions using OpenAI"); + + // TODO: OpenAI API 호출 (논의사항 제안 프롬프트) + return """ + { + "suggestions": [ + { + "id": "sugg-001", + "topic": "보안 요구사항 검토", + "reason": "안건에 포함되어 있으나 아직 논의되지 않음", + "priority": "HIGH", + "relatedAgenda": "프로젝트 개요", + "estimatedTime": 15 + } + ] + } + """; + } + + @Override + public String suggestDecisions(String transcriptText) { + log.info("Suggesting decisions using OpenAI"); + + // TODO: OpenAI API 호출 (결정사항 제안 프롬프트) + return """ + { + "suggestions": [ + { + "id": "dec-001", + "content": "React로 프론트엔드 개발", + "category": "기술", + "decisionMaker": "김철수", + "participants": ["김철수", "이영희"], + "confidence": 0.85, + "extractedFrom": "프론트엔드는 React로 개발하기로 했습니다", + "context": "팀원 대부분이 React 경험이 있어 개발 속도가 빠를 것으로 예상" + } + ] + } + """; + } +} diff --git a/ai/src/main/java/com/unicorn/hgzero/ai/infra/search/AzureAiSearchGateway.java b/ai/src/main/java/com/unicorn/hgzero/ai/infra/search/AzureAiSearchGateway.java new file mode 100644 index 0000000..82775f5 --- /dev/null +++ b/ai/src/main/java/com/unicorn/hgzero/ai/infra/search/AzureAiSearchGateway.java @@ -0,0 +1,100 @@ +package com.unicorn.hgzero.ai.infra.search; + +import com.unicorn.hgzero.ai.biz.gateway.SearchGateway; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * Azure AI Search Gateway 구현체 + * RAG 기반 벡터 검색 기능 제공 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class AzureAiSearchGateway implements SearchGateway { + + // TODO: Azure AI Search 클라이언트 주입 + // private final SearchClient searchClient; + + @Override + public String searchRelatedTranscripts(String meetingId, String transcriptId, int limit) { + log.info("Searching related transcripts: meetingId={}, transcriptId={}, limit={}", + meetingId, transcriptId, limit); + + // TODO: Azure AI Search 벡터 검색 + // 1. 회의록 내용으로 임베딩 생성 + // 2. 벡터 유사도 검색 + // 3. 상위 N개 결과 반환 + + // 임시 mock 응답 + return """ + { + "relatedTranscripts": [ + { + "transcriptId": "aa0e8400-e29b-41d4-a716-446655440005", + "title": "프로젝트 X 주간 회의", + "date": "2025-01-15", + "participants": ["김철수", "이영희"], + "relevanceScore": 85.5, + "commonKeywords": ["MSA", "API Gateway", "Spring Boot"], + "link": "/transcripts/aa0e8400-e29b-41d4-a716-446655440005" + } + ] + } + """; + } + + @Override + public String searchTermExplanation(String term, String meetingId, String context) { + log.info("Searching term explanation: term={}, meetingId={}", term, meetingId); + + // TODO: Azure AI Search 문서 검색 + // 1. 용어와 맥락으로 검색 쿼리 구성 + // 2. 과거 회의록, 위키, 매뉴얼 검색 + // 3. 관련 문서 반환 + + // 임시 mock 응답 + return """ + { + "term": "MSA", + "basicDefinition": "Microservices Architecture의 약자", + "contextualMeaning": "이번 프로젝트에서는 확장성과 독립 배포를 위해 MSA를 적용하기로 결정", + "useCases": [ + "2024년 프로젝트 X에서 주문/결제/배송 서비스를 독립적으로 구성", + "서비스별 독립 배포로 배포 시간 70% 단축" + ], + "relatedProjects": [ + {"name": "프로젝트 X", "relevance": "동일한 MSA 아키텍처 적용"} + ], + "pastDiscussions": [ + { + "date": "2024-12-15", + "participants": ["김철수", "이영희"], + "summary": "MSA 아키텍처의 장단점을 비교하고 적용 방안을 논의", + "link": "/transcripts/bb0e8400-e29b-41d4-a716-446655440006" + } + ], + "references": [ + { + "title": "MSA 아키텍처 가이드", + "type": "위키", + "link": "https://wiki.example.com/msa-guide" + } + ] + } + """; + } + + @Override + public void indexTranscript(String transcriptId, String content, String metadata) { + log.info("Indexing transcript: transcriptId={}", transcriptId); + + // TODO: Azure AI Search 인덱싱 + // 1. 회의록 내용 임베딩 생성 + // 2. 벡터와 메타데이터를 인덱스에 저장 + // 3. 검색 가능 상태로 만들기 + + log.debug("Transcript indexed successfully: {}", transcriptId); + } +}