ai 실행환경 설정

This commit is contained in:
djeon 2025-10-24 09:46:18 +09:00
parent d2a92bcc20
commit 869ce3bbd4
68 changed files with 3800 additions and 0 deletions

281
ai/logs/ai-service.log Normal file
View File

@ -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

View File

@ -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

View File

@ -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;
}

View File

@ -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<DiscussionItem> discussions;
/**
* 결정사항 목록
*/
private List<DecisionItem> decisions;
/**
* 보류사항 목록
*/
private List<String> 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;
}
}

View File

@ -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<String> participants;
/**
* 관련도 점수 (0-100)
*/
private Double relevanceScore;
/**
* 공통 키워드 목록
*/
private List<String> commonKeywords;
/**
* 회의록 링크
*/
private String link;
}

View File

@ -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<String> participants;
/**
* 카테고리 (결정사항인 경우: 기술, 일정, 리소스, 정책, 기타)
*/
private String category;
/**
* 원문 발췌 (결정사항인 경우)
*/
private String extractedFrom;
/**
* 배경 설명 (결정사항인 경우)
*/
private String context;
/**
* 제안 유형
*/
public enum SuggestionType {
DISCUSSION, // 논의사항
DECISION // 결정사항
}
}

View File

@ -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;
}
}

View File

@ -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<String> participants, List<String> 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<String> agenda);
/**
* 결정사항 제안 (LLM 기반)
*
* @param transcriptText 현재 회의록 텍스트
* @return 결정사항 제안 JSON
*/
String suggestDecisions(String transcriptText);
}

View File

@ -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);
}

View File

@ -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<ProcessedTranscript> findById(String transcriptId);
/**
* 회의 ID로 조회
*
* @param meetingId 회의 ID
* @return 회의록 (Optional)
*/
Optional<ProcessedTranscript> findByMeetingId(String meetingId);
/**
* 회의 ID 목록으로 조회
*
* @param meetingIds 회의 ID 목록
* @return 회의록 목록
*/
List<ProcessedTranscript> findByMeetingIds(List<String> meetingIds);
/**
* 상태로 조회
*
* @param status 상태
* @return 회의록 목록
*/
List<ProcessedTranscript> findByStatus(String status);
/**
* 회의록 존재 여부 확인
*
* @param meetingId 회의 ID
* @return 존재 여부
*/
boolean existsByMeetingId(String meetingId);
/**
* 회의록 삭제
*
* @param transcriptId 회의록 ID
*/
void delete(String transcriptId);
}

View File

@ -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<RelatedMinutes> 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()
);
}
}

View File

@ -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);
}
}

View File

@ -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<Suggestion> suggestDiscussions(String meetingId, String transcriptText) {
log.info("Suggesting discussions: meetingId={}", meetingId);
// TODO: 회의 안건 조회
List<String> 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<Suggestion> 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()
);
}
}

View File

@ -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<Term> 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()
);
}
}

View File

@ -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"
))
);
}
}

View File

@ -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<ExtractedTodo> 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()
);
}
}

View File

@ -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<String> participants,
List<String> 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<ProcessedTranscript.DiscussionItem> 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<ProcessedTranscript.DecisionItem> 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<String> 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 "{}";
}
}
}

View File

@ -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<RelatedMinutes> findRelatedTranscripts(String meetingId, String transcriptId, int limit);
}

View File

@ -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);
}

View File

@ -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<Suggestion> suggestDiscussions(String meetingId, String transcriptText);
/**
* 결정사항 제안
*
* @param meetingId 회의 ID
* @param transcriptText 현재까지의 회의록 텍스트
* @return 결정사항 제안 목록
*/
List<Suggestion> suggestDecisions(String meetingId, String transcriptText);
}

View File

@ -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<Term> detectTerms(String meetingId, String text, String organizationId);
}

View File

@ -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<String> useCases,
List<RelatedProject> relatedProjects,
List<PastDiscussion> pastDiscussions,
List<Reference> references
) {}
record RelatedProject(String name, String relevance) {}
record PastDiscussion(java.time.LocalDate date, List<String> participants, String summary, String link) {}
record Reference(String title, String type, String link) {}
}

View File

@ -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<ExtractedTodo> extractTodos(String meetingId, String minutesContent, String userId);
}

View File

@ -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<String> participants,
java.util.List<String> agenda
);
/**
* 회의록 조회
*
* @param transcriptId 회의록 ID
* @return 처리된 회의록
*/
ProcessedTranscript getTranscript(String transcriptId);
/**
* 회의 ID로 회의록 조회
*
* @param meetingId 회의 ID
* @return 처리된 회의록
*/
ProcessedTranscript getTranscriptByMeetingId(String meetingId);
}

View File

@ -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;
}
}

View File

@ -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");
}
}

View File

@ -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<ApiResponse<TermExplanationResponse>> 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));
}
}

View File

@ -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<ApiResponse<RelatedTranscriptsResponse>> findRelatedTranscripts(
@PathVariable String meetingId,
@RequestParam String transcriptId,
@RequestParam(defaultValue = "5") int limit) {
log.info("관련 회의록 조회 요청 - meetingId: {}, transcriptId: {}, limit: {}", meetingId, transcriptId, limit);
List<RelatedMinutes> 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));
}
}

View File

@ -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<ApiResponse<SectionSummaryResponse>> 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));
}
}

View File

@ -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<ApiResponse<DiscussionSuggestionResponse>> suggestDiscussion(
@Valid @RequestBody DiscussionSuggestionRequest request) {
log.info("논의사항 제안 요청 - meetingId: {}", request.getMeetingId());
List<Suggestion> 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<ApiResponse<DecisionSuggestionResponse>> suggestDecision(
@Valid @RequestBody DecisionSuggestionRequest request) {
log.info("결정사항 제안 요청 - meetingId: {}", request.getMeetingId());
List<Suggestion> 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));
}
}

View File

@ -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<ApiResponse<TermDetectionResponse>> detectTerms(
@Valid @RequestBody TermDetectionRequest request) {
log.info("전문용어 감지 요청 - meetingId: {}, organizationId: {}",
request.getMeetingId(), request.getOrganizationId());
List<Term> terms = termDetectionUseCase.detectTerms(
request.getMeetingId(),
request.getText(),
request.getOrganizationId()
);
List<DetectedTermDto> 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<HighlightInfoDto> 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));
}
}

View File

@ -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<ApiResponse<TodoExtractionResponse>> extractTodos(
@RequestHeader("X-User-Id") String userId,
@Valid @RequestBody TodoExtractionRequest request) {
log.info("Todo 추출 요청 - meetingId: {}, userId: {}", request.getMeetingId(), userId);
List<ExtractedTodo> 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));
}
}

View File

@ -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<ApiResponse<TranscriptProcessResponse>> 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();
}
}

View File

@ -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;
}

View File

@ -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<String> participants;
/**
* 신뢰도 점수 (0-1)
*/
private Double confidence;
/**
* 원문 발췌
*/
private String extractedFrom;
/**
* 결정 배경
*/
private String context;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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<String> participants;
/**
* 회의 안건 목록
*/
private List<String> agenda;
/**
* 이전 회의록 내용
*/
private String previousContent;
}

View File

@ -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<String> participants;
/**
* 논의 요약
*/
private String summary;
/**
* 회의록 링크
*/
private String link;
}

View File

@ -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<DiscussionSuggestionDto> discussionTopics;
/**
* 결정사항 제안 목록
*/
private List<DecisionSuggestionDto> decisions;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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<String> participants;
/**
* 관련도 점수 (0-100%)
*/
private Double relevanceScore;
/**
* 공통 키워드 목록
*/
private List<String> commonKeywords;
/**
* 회의록 링크
*/
private String link;
}

View File

@ -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;
}

View File

@ -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<DiscussionItemDto> discussions;
/**
* 결정사항 목록
*/
private List<DecisionItemDto> decisions;
/**
* 보류사항 목록
*/
private List<String> pendingItems;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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<DecisionSuggestionDto> suggestions;
/**
* 제안 개수
*/
private Integer totalCount;
/**
* 생성 시각
*/
private LocalDateTime timestamp;
}

View File

@ -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<DiscussionSuggestionDto> suggestions;
/**
* 제안 개수
*/
private Integer totalCount;
/**
* 생성 시각
*/
private LocalDateTime timestamp;
}

View File

@ -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<RelatedTranscriptDto> relatedTranscripts;
/**
* 관련 회의록 개수
*/
private Integer totalCount;
}

View File

@ -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;
}

View File

@ -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<DetectedTermDto> detectedTerms;
/**
* 감지된 용어 개수
*/
private Integer totalCount;
/**
* 하이라이트 정보 목록
*/
private List<HighlightInfoDto> highlightInfo;
}

View File

@ -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<String> useCases;
/**
* 관련 프로젝트 목록
*/
private List<RelatedProjectDto> relatedProjects;
/**
* 과거 논의 목록
*/
private List<PastDiscussionDto> pastDiscussions;
/**
* 참조 문서 목록
*/
private List<ReferenceDto> references;
}

View File

@ -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<ExtractedTodoDto> todos;
/**
* 추출된 Todo 개수
*/
private Integer totalCount;
/**
* 추출 시간
*/
private LocalDateTime extractedAt;
}

View File

@ -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;
}

View File

@ -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<ProcessedTranscript> findById(String transcriptId) {
log.debug("Finding transcript by id: {}", transcriptId);
return repository.findById(transcriptId)
.map(ProcessedTranscriptEntity::toDomain);
}
@Override
public Optional<ProcessedTranscript> findByMeetingId(String meetingId) {
log.debug("Finding transcript by meetingId: {}", meetingId);
return repository.findByMeetingId(meetingId)
.map(ProcessedTranscriptEntity::toDomain);
}
@Override
public List<ProcessedTranscript> findByMeetingIds(List<String> meetingIds) {
log.debug("Finding transcripts by meetingIds: count={}", meetingIds.size());
return repository.findByMeetingIdIn(meetingIds).stream()
.map(ProcessedTranscriptEntity::toDomain)
.collect(Collectors.toList());
}
@Override
public List<ProcessedTranscript> 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);
}
}

View File

@ -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<ProcessedTranscript.DiscussionItem> parseDiscussions(String json) {
if (json == null || json.isEmpty()) {
return new ArrayList<>();
}
try {
return objectMapper.readValue(json,
new TypeReference<List<ProcessedTranscript.DiscussionItem>>() {});
} catch (JsonProcessingException e) {
log.error("Failed to parse discussions JSON: {}", json, e);
return new ArrayList<>();
}
}
private static String formatDiscussions(List<ProcessedTranscript.DiscussionItem> 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<ProcessedTranscript.DecisionItem> parseDecisions(String json) {
if (json == null || json.isEmpty()) {
return new ArrayList<>();
}
try {
return objectMapper.readValue(json,
new TypeReference<List<ProcessedTranscript.DecisionItem>>() {});
} catch (JsonProcessingException e) {
log.error("Failed to parse decisions JSON: {}", json, e);
return new ArrayList<>();
}
}
private static String formatDecisions(List<ProcessedTranscript.DecisionItem> 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<String> parsePendingItems(String items) {
if (items == null || items.isEmpty()) {
return new ArrayList<>();
}
return Arrays.asList(items.split(","));
}
private static String formatPendingItems(List<String> items) {
if (items == null || items.isEmpty()) {
return "";
}
return String.join(",", items);
}
}

View File

@ -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<ProcessedTranscriptEntity, String> {
/**
* 회의 ID로 회의록 조회
*/
Optional<ProcessedTranscriptEntity> findByMeetingId(String meetingId);
/**
* 회의 ID 목록으로 회의록 목록 조회
*/
List<ProcessedTranscriptEntity> findByMeetingIdIn(List<String> meetingIds);
/**
* 상태로 회의록 목록 조회
*/
List<ProcessedTranscriptEntity> findByStatus(String status);
/**
* 회의 ID와 상태로 회의록 조회
*/
Optional<ProcessedTranscriptEntity> findByMeetingIdAndStatus(String meetingId, String status);
/**
* 회의 ID로 회의록 존재 여부 확인
*/
boolean existsByMeetingId(String meetingId);
}

View File

@ -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<String> participants, List<String> 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<String> 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 경험이 있어 개발 속도가 빠를 것으로 예상"
}
]
}
""";
}
}

View File

@ -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);
}
}