mirror of
https://github.com/hwanny1128/HGZero.git
synced 2025-12-06 17:16:25 +00:00
Merge branch 'main' of https://github.com/hwanny1128/HGZero into feat/meeting
This commit is contained in:
commit
6db366ac86
11
.gitignore
vendored
11
.gitignore
vendored
@ -1,8 +1,12 @@
|
|||||||
# Build outputs
|
# Build outputs
|
||||||
build/
|
build/*
|
||||||
|
build/*/*
|
||||||
|
build/*/*/*
|
||||||
**/build/
|
**/build/
|
||||||
.gradle/
|
.gradle/
|
||||||
**/.gradle/
|
**/.gradle/
|
||||||
|
.vscode/
|
||||||
|
**/.vscode/
|
||||||
|
|
||||||
# Serena
|
# Serena
|
||||||
serena/
|
serena/
|
||||||
@ -37,3 +41,8 @@ examples/
|
|||||||
|
|
||||||
# Claude settings
|
# Claude settings
|
||||||
.claude/settings.local.json
|
.claude/settings.local.json
|
||||||
|
|
||||||
|
# Backup files
|
||||||
|
design/*/*backup.md
|
||||||
|
design/*backup.md
|
||||||
|
backup/
|
||||||
|
|||||||
482
ai/logs/ai.log
482
ai/logs/ai.log
@ -296,3 +296,485 @@ This generated password is for development use only. Your security configuration
|
|||||||
2025-10-23 16:27:13 [SpringApplicationShutdownHook] INFO o.s.o.j.LocalContainerEntityManagerFactoryBean - Closing JPA EntityManagerFactory for persistence unit 'default'
|
2025-10-23 16:27:13 [SpringApplicationShutdownHook] INFO o.s.o.j.LocalContainerEntityManagerFactoryBean - Closing JPA EntityManagerFactory for persistence unit 'default'
|
||||||
2025-10-23 16:27:13 [SpringApplicationShutdownHook] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown initiated...
|
2025-10-23 16:27:13 [SpringApplicationShutdownHook] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown initiated...
|
||||||
2025-10-23 16:27:13 [SpringApplicationShutdownHook] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown completed.
|
2025-10-23 16:27:13 [SpringApplicationShutdownHook] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown completed.
|
||||||
|
2025-10-23 17:10:12 [main] INFO com.unicorn.hgzero.ai.AiApplication - Starting AiApplication using Java 23.0.2 with PID 43825 (/Users/jominseo/HGZero/ai/build/classes/java/main started by jominseo in /Users/jominseo/HGZero/ai)
|
||||||
|
2025-10-23 17:10:12 [main] DEBUG com.unicorn.hgzero.ai.AiApplication - Running with Spring Boot v3.3.0, Spring v6.1.8
|
||||||
|
2025-10-23 17:10:12 [main] INFO com.unicorn.hgzero.ai.AiApplication - The following 1 profile is active: "dev"
|
||||||
|
2025-10-23 17:10:12 [main] INFO o.s.d.r.c.RepositoryConfigurationDelegate - Multiple Spring Data modules found, entering strict repository configuration mode
|
||||||
|
2025-10-23 17:10:12 [main] INFO o.s.d.r.c.RepositoryConfigurationDelegate - Bootstrapping Spring Data JPA repositories in DEFAULT mode.
|
||||||
|
2025-10-23 17:10:12 [main] INFO o.s.d.r.c.RepositoryConfigurationDelegate - Finished Spring Data repository scanning in 3 ms. Found 0 JPA repository interfaces.
|
||||||
|
2025-10-23 17:10:12 [main] INFO o.s.d.r.c.RepositoryConfigurationDelegate - Multiple Spring Data modules found, entering strict repository configuration mode
|
||||||
|
2025-10-23 17:10:12 [main] INFO o.s.d.r.c.RepositoryConfigurationDelegate - Bootstrapping Spring Data Redis repositories in DEFAULT mode.
|
||||||
|
2025-10-23 17:10:12 [main] INFO o.s.d.r.c.RepositoryConfigurationDelegate - Finished Spring Data repository scanning in 0 ms. Found 0 Redis repository interfaces.
|
||||||
|
2025-10-23 17:10:13 [main] INFO o.s.b.w.e.tomcat.TomcatWebServer - Tomcat initialized with port 8083 (http)
|
||||||
|
2025-10-23 17:10:13 [main] INFO o.a.catalina.core.StandardService - Starting service [Tomcat]
|
||||||
|
2025-10-23 17:10:13 [main] INFO o.a.catalina.core.StandardEngine - Starting Servlet engine: [Apache Tomcat/10.1.24]
|
||||||
|
2025-10-23 17:10:13 [main] INFO o.a.c.c.C.[Tomcat].[localhost].[/] - Initializing Spring embedded WebApplicationContext
|
||||||
|
2025-10-23 17:10:13 [main] INFO o.s.b.w.s.c.ServletWebServerApplicationContext - Root WebApplicationContext: initialization completed in 668 ms
|
||||||
|
2025-10-23 17:10:13 [main] INFO o.h.jpa.internal.util.LogHelper - HHH000204: Processing PersistenceUnitInfo [name: default]
|
||||||
|
2025-10-23 17:10:13 [main] INFO org.hibernate.Version - HHH000412: Hibernate ORM core version 6.5.2.Final
|
||||||
|
2025-10-23 17:10:13 [main] INFO o.h.c.i.RegionFactoryInitiator - HHH000026: Second-level cache disabled
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration boolean -> org.hibernate.type.BasicTypeReference@66716959
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration boolean -> org.hibernate.type.BasicTypeReference@66716959
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Boolean -> org.hibernate.type.BasicTypeReference@66716959
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration numeric_boolean -> org.hibernate.type.BasicTypeReference@34e07e65
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration org.hibernate.type.NumericBooleanConverter -> org.hibernate.type.BasicTypeReference@34e07e65
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration true_false -> org.hibernate.type.BasicTypeReference@7ca0166c
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration org.hibernate.type.TrueFalseConverter -> org.hibernate.type.BasicTypeReference@7ca0166c
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration yes_no -> org.hibernate.type.BasicTypeReference@1dcad16f
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration org.hibernate.type.YesNoConverter -> org.hibernate.type.BasicTypeReference@1dcad16f
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration byte -> org.hibernate.type.BasicTypeReference@701c482e
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration byte -> org.hibernate.type.BasicTypeReference@701c482e
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Byte -> org.hibernate.type.BasicTypeReference@701c482e
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration binary -> org.hibernate.type.BasicTypeReference@4738131e
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration byte[] -> org.hibernate.type.BasicTypeReference@4738131e
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration [B -> org.hibernate.type.BasicTypeReference@4738131e
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration binary_wrapper -> org.hibernate.type.BasicTypeReference@3b576ee3
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration wrapper-binary -> org.hibernate.type.BasicTypeReference@3b576ee3
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration image -> org.hibernate.type.BasicTypeReference@705d914f
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration blob -> org.hibernate.type.BasicTypeReference@6212ea52
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.sql.Blob -> org.hibernate.type.BasicTypeReference@6212ea52
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_blob -> org.hibernate.type.BasicTypeReference@65b5b5ed
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_blob_wrapper -> org.hibernate.type.BasicTypeReference@6595ffce
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration short -> org.hibernate.type.BasicTypeReference@795eddda
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration short -> org.hibernate.type.BasicTypeReference@795eddda
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Short -> org.hibernate.type.BasicTypeReference@795eddda
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration integer -> org.hibernate.type.BasicTypeReference@c6bf8d9
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration int -> org.hibernate.type.BasicTypeReference@c6bf8d9
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Integer -> org.hibernate.type.BasicTypeReference@c6bf8d9
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration long -> org.hibernate.type.BasicTypeReference@44392e64
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration long -> org.hibernate.type.BasicTypeReference@44392e64
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Long -> org.hibernate.type.BasicTypeReference@44392e64
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration float -> org.hibernate.type.BasicTypeReference@e18d2a2
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration float -> org.hibernate.type.BasicTypeReference@e18d2a2
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Float -> org.hibernate.type.BasicTypeReference@e18d2a2
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration double -> org.hibernate.type.BasicTypeReference@1a77eb6
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration double -> org.hibernate.type.BasicTypeReference@1a77eb6
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Double -> org.hibernate.type.BasicTypeReference@1a77eb6
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration big_integer -> org.hibernate.type.BasicTypeReference@52d9f36b
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.math.BigInteger -> org.hibernate.type.BasicTypeReference@52d9f36b
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration big_decimal -> org.hibernate.type.BasicTypeReference@5f9ebd5a
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.math.BigDecimal -> org.hibernate.type.BasicTypeReference@5f9ebd5a
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration character -> org.hibernate.type.BasicTypeReference@175bf9c9
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration char -> org.hibernate.type.BasicTypeReference@175bf9c9
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Character -> org.hibernate.type.BasicTypeReference@175bf9c9
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration character_nchar -> org.hibernate.type.BasicTypeReference@2db3675a
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration string -> org.hibernate.type.BasicTypeReference@306c9b2c
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.String -> org.hibernate.type.BasicTypeReference@306c9b2c
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration nstring -> org.hibernate.type.BasicTypeReference@1ab28416
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration characters -> org.hibernate.type.BasicTypeReference@52efb338
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration char[] -> org.hibernate.type.BasicTypeReference@52efb338
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration [C -> org.hibernate.type.BasicTypeReference@52efb338
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration wrapper-characters -> org.hibernate.type.BasicTypeReference@64508788
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration text -> org.hibernate.type.BasicTypeReference@30b1c5d5
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration ntext -> org.hibernate.type.BasicTypeReference@3e2d65e1
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration clob -> org.hibernate.type.BasicTypeReference@1174676f
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.sql.Clob -> org.hibernate.type.BasicTypeReference@1174676f
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration nclob -> org.hibernate.type.BasicTypeReference@71f8ce0e
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.sql.NClob -> org.hibernate.type.BasicTypeReference@71f8ce0e
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_clob -> org.hibernate.type.BasicTypeReference@4fd92289
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_clob_char_array -> org.hibernate.type.BasicTypeReference@1a8e44fe
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_clob_character_array -> org.hibernate.type.BasicTypeReference@287317df
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_nclob -> org.hibernate.type.BasicTypeReference@1fcc3461
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_nclob_character_array -> org.hibernate.type.BasicTypeReference@1987807b
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_nclob_char_array -> org.hibernate.type.BasicTypeReference@71469e01
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration Duration -> org.hibernate.type.BasicTypeReference@41bbb219
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.Duration -> org.hibernate.type.BasicTypeReference@41bbb219
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration LocalDateTime -> org.hibernate.type.BasicTypeReference@3f2ae973
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.LocalDateTime -> org.hibernate.type.BasicTypeReference@3f2ae973
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration LocalDate -> org.hibernate.type.BasicTypeReference@1a8b22b5
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.LocalDate -> org.hibernate.type.BasicTypeReference@1a8b22b5
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration LocalTime -> org.hibernate.type.BasicTypeReference@5f781173
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.LocalTime -> org.hibernate.type.BasicTypeReference@5f781173
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration OffsetDateTime -> org.hibernate.type.BasicTypeReference@43cf5bff
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.OffsetDateTime -> org.hibernate.type.BasicTypeReference@43cf5bff
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration OffsetDateTimeWithTimezone -> org.hibernate.type.BasicTypeReference@2b464384
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration OffsetDateTimeWithoutTimezone -> org.hibernate.type.BasicTypeReference@681b42d3
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration OffsetTime -> org.hibernate.type.BasicTypeReference@77f7352a
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.OffsetTime -> org.hibernate.type.BasicTypeReference@77f7352a
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration OffsetTimeUtc -> org.hibernate.type.BasicTypeReference@4ede8888
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration OffsetTimeWithTimezone -> org.hibernate.type.BasicTypeReference@571db8b4
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration OffsetTimeWithoutTimezone -> org.hibernate.type.BasicTypeReference@65a2755e
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration ZonedDateTime -> org.hibernate.type.BasicTypeReference@2b3242a5
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.ZonedDateTime -> org.hibernate.type.BasicTypeReference@2b3242a5
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration ZonedDateTimeWithTimezone -> org.hibernate.type.BasicTypeReference@11120583
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration ZonedDateTimeWithoutTimezone -> org.hibernate.type.BasicTypeReference@2bf0c70d
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration date -> org.hibernate.type.BasicTypeReference@5d8e4fa8
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.sql.Date -> org.hibernate.type.BasicTypeReference@5d8e4fa8
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration time -> org.hibernate.type.BasicTypeReference@649009d6
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.sql.Time -> org.hibernate.type.BasicTypeReference@649009d6
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration timestamp -> org.hibernate.type.BasicTypeReference@652f26da
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.sql.Timestamp -> org.hibernate.type.BasicTypeReference@652f26da
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.util.Date -> org.hibernate.type.BasicTypeReference@652f26da
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration calendar -> org.hibernate.type.BasicTypeReference@484a5ddd
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.util.Calendar -> org.hibernate.type.BasicTypeReference@484a5ddd
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.util.GregorianCalendar -> org.hibernate.type.BasicTypeReference@484a5ddd
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration calendar_date -> org.hibernate.type.BasicTypeReference@6796a873
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration calendar_time -> org.hibernate.type.BasicTypeReference@3acc3ee
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration instant -> org.hibernate.type.BasicTypeReference@1f293cb7
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.Instant -> org.hibernate.type.BasicTypeReference@1f293cb7
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration uuid -> org.hibernate.type.BasicTypeReference@5972e3a
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.util.UUID -> org.hibernate.type.BasicTypeReference@5972e3a
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration pg-uuid -> org.hibernate.type.BasicTypeReference@5972e3a
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration uuid-binary -> org.hibernate.type.BasicTypeReference@5790cbcb
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration uuid-char -> org.hibernate.type.BasicTypeReference@32c6d164
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration class -> org.hibernate.type.BasicTypeReference@645c9f0f
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Class -> org.hibernate.type.BasicTypeReference@645c9f0f
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration currency -> org.hibernate.type.BasicTypeReference@58068b40
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration Currency -> org.hibernate.type.BasicTypeReference@58068b40
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.util.Currency -> org.hibernate.type.BasicTypeReference@58068b40
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration locale -> org.hibernate.type.BasicTypeReference@999cd18
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.util.Locale -> org.hibernate.type.BasicTypeReference@999cd18
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration serializable -> org.hibernate.type.BasicTypeReference@dd060be
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.io.Serializable -> org.hibernate.type.BasicTypeReference@dd060be
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration timezone -> org.hibernate.type.BasicTypeReference@df432ec
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.util.TimeZone -> org.hibernate.type.BasicTypeReference@df432ec
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration ZoneOffset -> org.hibernate.type.BasicTypeReference@6144e499
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.ZoneOffset -> org.hibernate.type.BasicTypeReference@6144e499
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration url -> org.hibernate.type.BasicTypeReference@26f204a4
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.net.URL -> org.hibernate.type.BasicTypeReference@26f204a4
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration vector -> org.hibernate.type.BasicTypeReference@28295554
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration row_version -> org.hibernate.type.BasicTypeReference@4e671ef
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration object -> org.hibernate.type.JavaObjectType@2aac6fa7
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Object -> org.hibernate.type.JavaObjectType@2aac6fa7
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration null -> org.hibernate.type.NullType@2358443e
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_date -> org.hibernate.type.BasicTypeReference@25e796fe
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_time -> org.hibernate.type.BasicTypeReference@29ba63f0
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_timestamp -> org.hibernate.type.BasicTypeReference@4822ab4d
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_calendar -> org.hibernate.type.BasicTypeReference@516b84d1
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_calendar_date -> org.hibernate.type.BasicTypeReference@1ad1f167
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_calendar_time -> org.hibernate.type.BasicTypeReference@608eb42e
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_binary -> org.hibernate.type.BasicTypeReference@3d2b13b1
|
||||||
|
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_serializable -> org.hibernate.type.BasicTypeReference@30eb55c9
|
||||||
|
2025-10-23 17:10:13 [main] INFO o.s.o.j.p.SpringPersistenceUnitInfo - No LoadTimeWeaver setup: ignoring JPA class transformer
|
||||||
|
2025-10-23 17:10:13 [main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
|
||||||
|
2025-10-23 17:10:14 [main] WARN o.h.e.j.e.i.JdbcEnvironmentInitiator - HHH000342: Could not obtain connection to query metadata
|
||||||
|
java.lang.NullPointerException: Cannot invoke "org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(java.sql.SQLException, String)" because the return value of "org.hibernate.resource.transaction.backend.jdbc.internal.JdbcIsolationDelegate.sqlExceptionHelper()" is null
|
||||||
|
at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcIsolationDelegate.delegateWork(JdbcIsolationDelegate.java:116)
|
||||||
|
at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.getJdbcEnvironmentUsingJdbcMetadata(JdbcEnvironmentInitiator.java:290)
|
||||||
|
at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.initiateService(JdbcEnvironmentInitiator.java:123)
|
||||||
|
at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.initiateService(JdbcEnvironmentInitiator.java:77)
|
||||||
|
at org.hibernate.boot.registry.internal.StandardServiceRegistryImpl.initiateService(StandardServiceRegistryImpl.java:130)
|
||||||
|
at org.hibernate.service.internal.AbstractServiceRegistryImpl.createService(AbstractServiceRegistryImpl.java:263)
|
||||||
|
at org.hibernate.service.internal.AbstractServiceRegistryImpl.initializeService(AbstractServiceRegistryImpl.java:238)
|
||||||
|
at org.hibernate.service.internal.AbstractServiceRegistryImpl.getService(AbstractServiceRegistryImpl.java:215)
|
||||||
|
at org.hibernate.boot.model.relational.Database.<init>(Database.java:45)
|
||||||
|
at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.getDatabase(InFlightMetadataCollectorImpl.java:221)
|
||||||
|
at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.<init>(InFlightMetadataCollectorImpl.java:189)
|
||||||
|
at org.hibernate.boot.model.process.spi.MetadataBuildingProcess.complete(MetadataBuildingProcess.java:171)
|
||||||
|
at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.metadata(EntityManagerFactoryBuilderImpl.java:1431)
|
||||||
|
at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:1502)
|
||||||
|
at org.springframework.orm.jpa.vendor.SpringHibernateJpaPersistenceProvider.createContainerEntityManagerFactory(SpringHibernateJpaPersistenceProvider.java:75)
|
||||||
|
at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.createNativeEntityManagerFactory(LocalContainerEntityManagerFactoryBean.java:390)
|
||||||
|
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.buildNativeEntityManagerFactory(AbstractEntityManagerFactoryBean.java:409)
|
||||||
|
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.afterPropertiesSet(AbstractEntityManagerFactoryBean.java:396)
|
||||||
|
at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.afterPropertiesSet(LocalContainerEntityManagerFactoryBean.java:366)
|
||||||
|
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1835)
|
||||||
|
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1784)
|
||||||
|
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:600)
|
||||||
|
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:522)
|
||||||
|
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:337)
|
||||||
|
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
|
||||||
|
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:335)
|
||||||
|
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:205)
|
||||||
|
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:952)
|
||||||
|
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:624)
|
||||||
|
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146)
|
||||||
|
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:754)
|
||||||
|
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:456)
|
||||||
|
at org.springframework.boot.SpringApplication.run(SpringApplication.java:335)
|
||||||
|
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1363)
|
||||||
|
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1352)
|
||||||
|
at com.unicorn.hgzero.ai.AiApplication.main(AiApplication.java:20)
|
||||||
|
2025-10-23 17:10:14 [main] ERROR o.s.o.j.LocalContainerEntityManagerFactoryBean - Failed to initialize JPA EntityManagerFactory: Unable to create requested service [org.hibernate.engine.jdbc.env.spi.JdbcEnvironment] due to: Unable to determine Dialect without JDBC metadata (please set 'jakarta.persistence.jdbc.url' for common cases or 'hibernate.dialect' when a custom Dialect implementation must be provided)
|
||||||
|
2025-10-23 17:10:14 [main] WARN o.s.b.w.s.c.AnnotationConfigServletWebServerApplicationContext - Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]: Unable to create requested service [org.hibernate.engine.jdbc.env.spi.JdbcEnvironment] due to: Unable to determine Dialect without JDBC metadata (please set 'jakarta.persistence.jdbc.url' for common cases or 'hibernate.dialect' when a custom Dialect implementation must be provided)
|
||||||
|
2025-10-23 17:10:14 [main] INFO o.a.catalina.core.StandardService - Stopping service [Tomcat]
|
||||||
|
2025-10-23 17:10:14 [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-23 17:10:14 [main] ERROR o.s.boot.SpringApplication - Application run failed
|
||||||
|
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]: Unable to create requested service [org.hibernate.engine.jdbc.env.spi.JdbcEnvironment] due to: Unable to determine Dialect without JDBC metadata (please set 'jakarta.persistence.jdbc.url' for common cases or 'hibernate.dialect' when a custom Dialect implementation must be provided)
|
||||||
|
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1788)
|
||||||
|
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:600)
|
||||||
|
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:522)
|
||||||
|
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:337)
|
||||||
|
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
|
||||||
|
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:335)
|
||||||
|
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:205)
|
||||||
|
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:952)
|
||||||
|
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:624)
|
||||||
|
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146)
|
||||||
|
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:754)
|
||||||
|
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:456)
|
||||||
|
at org.springframework.boot.SpringApplication.run(SpringApplication.java:335)
|
||||||
|
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1363)
|
||||||
|
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1352)
|
||||||
|
at com.unicorn.hgzero.ai.AiApplication.main(AiApplication.java:20)
|
||||||
|
Caused by: org.hibernate.service.spi.ServiceException: Unable to create requested service [org.hibernate.engine.jdbc.env.spi.JdbcEnvironment] due to: Unable to determine Dialect without JDBC metadata (please set 'jakarta.persistence.jdbc.url' for common cases or 'hibernate.dialect' when a custom Dialect implementation must be provided)
|
||||||
|
at org.hibernate.service.internal.AbstractServiceRegistryImpl.createService(AbstractServiceRegistryImpl.java:276)
|
||||||
|
at org.hibernate.service.internal.AbstractServiceRegistryImpl.initializeService(AbstractServiceRegistryImpl.java:238)
|
||||||
|
at org.hibernate.service.internal.AbstractServiceRegistryImpl.getService(AbstractServiceRegistryImpl.java:215)
|
||||||
|
at org.hibernate.boot.model.relational.Database.<init>(Database.java:45)
|
||||||
|
at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.getDatabase(InFlightMetadataCollectorImpl.java:221)
|
||||||
|
at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.<init>(InFlightMetadataCollectorImpl.java:189)
|
||||||
|
at org.hibernate.boot.model.process.spi.MetadataBuildingProcess.complete(MetadataBuildingProcess.java:171)
|
||||||
|
at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.metadata(EntityManagerFactoryBuilderImpl.java:1431)
|
||||||
|
at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:1502)
|
||||||
|
at org.springframework.orm.jpa.vendor.SpringHibernateJpaPersistenceProvider.createContainerEntityManagerFactory(SpringHibernateJpaPersistenceProvider.java:75)
|
||||||
|
at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.createNativeEntityManagerFactory(LocalContainerEntityManagerFactoryBean.java:390)
|
||||||
|
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.buildNativeEntityManagerFactory(AbstractEntityManagerFactoryBean.java:409)
|
||||||
|
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.afterPropertiesSet(AbstractEntityManagerFactoryBean.java:396)
|
||||||
|
at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.afterPropertiesSet(LocalContainerEntityManagerFactoryBean.java:366)
|
||||||
|
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1835)
|
||||||
|
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1784)
|
||||||
|
... 15 common frames omitted
|
||||||
|
Caused by: org.hibernate.HibernateException: Unable to determine Dialect without JDBC metadata (please set 'jakarta.persistence.jdbc.url' for common cases or 'hibernate.dialect' when a custom Dialect implementation must be provided)
|
||||||
|
at org.hibernate.engine.jdbc.dialect.internal.DialectFactoryImpl.determineDialect(DialectFactoryImpl.java:191)
|
||||||
|
at org.hibernate.engine.jdbc.dialect.internal.DialectFactoryImpl.buildDialect(DialectFactoryImpl.java:87)
|
||||||
|
at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.getJdbcEnvironmentWithDefaults(JdbcEnvironmentInitiator.java:152)
|
||||||
|
at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.getJdbcEnvironmentUsingJdbcMetadata(JdbcEnvironmentInitiator.java:362)
|
||||||
|
at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.initiateService(JdbcEnvironmentInitiator.java:123)
|
||||||
|
at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.initiateService(JdbcEnvironmentInitiator.java:77)
|
||||||
|
at org.hibernate.boot.registry.internal.StandardServiceRegistryImpl.initiateService(StandardServiceRegistryImpl.java:130)
|
||||||
|
at org.hibernate.service.internal.AbstractServiceRegistryImpl.createService(AbstractServiceRegistryImpl.java:263)
|
||||||
|
... 30 common frames omitted
|
||||||
|
2025-10-23 17:38:09 [main] INFO com.unicorn.hgzero.ai.AiApplication - Starting AiApplication using Java 23.0.2 with PID 49971 (/Users/jominseo/HGZero/ai/build/classes/java/main started by jominseo in /Users/jominseo/HGZero/ai)
|
||||||
|
2025-10-23 17:38:09 [main] DEBUG com.unicorn.hgzero.ai.AiApplication - Running with Spring Boot v3.3.0, Spring v6.1.8
|
||||||
|
2025-10-23 17:38:09 [main] INFO com.unicorn.hgzero.ai.AiApplication - The following 1 profile is active: "dev"
|
||||||
|
2025-10-23 17:38:09 [main] INFO o.s.d.r.c.RepositoryConfigurationDelegate - Multiple Spring Data modules found, entering strict repository configuration mode
|
||||||
|
2025-10-23 17:38:09 [main] INFO o.s.d.r.c.RepositoryConfigurationDelegate - Bootstrapping Spring Data JPA repositories in DEFAULT mode.
|
||||||
|
2025-10-23 17:38:09 [main] INFO o.s.d.r.c.RepositoryConfigurationDelegate - Finished Spring Data repository scanning in 4 ms. Found 0 JPA repository interfaces.
|
||||||
|
2025-10-23 17:38:09 [main] INFO o.s.d.r.c.RepositoryConfigurationDelegate - Multiple Spring Data modules found, entering strict repository configuration mode
|
||||||
|
2025-10-23 17:38:09 [main] INFO o.s.d.r.c.RepositoryConfigurationDelegate - Bootstrapping Spring Data Redis repositories in DEFAULT mode.
|
||||||
|
2025-10-23 17:38:09 [main] INFO o.s.d.r.c.RepositoryConfigurationDelegate - Finished Spring Data repository scanning in 0 ms. Found 0 Redis repository interfaces.
|
||||||
|
2025-10-23 17:38:09 [main] INFO o.s.b.w.e.tomcat.TomcatWebServer - Tomcat initialized with port 8083 (http)
|
||||||
|
2025-10-23 17:38:09 [main] INFO o.a.catalina.core.StandardService - Starting service [Tomcat]
|
||||||
|
2025-10-23 17:38:09 [main] INFO o.a.catalina.core.StandardEngine - Starting Servlet engine: [Apache Tomcat/10.1.24]
|
||||||
|
2025-10-23 17:38:09 [main] INFO o.a.c.c.C.[Tomcat].[localhost].[/] - Initializing Spring embedded WebApplicationContext
|
||||||
|
2025-10-23 17:38:09 [main] INFO o.s.b.w.s.c.ServletWebServerApplicationContext - Root WebApplicationContext: initialization completed in 679 ms
|
||||||
|
2025-10-23 17:38:10 [main] INFO o.h.jpa.internal.util.LogHelper - HHH000204: Processing PersistenceUnitInfo [name: default]
|
||||||
|
2025-10-23 17:38:10 [main] INFO org.hibernate.Version - HHH000412: Hibernate ORM core version 6.5.2.Final
|
||||||
|
2025-10-23 17:38:10 [main] INFO o.h.c.i.RegionFactoryInitiator - HHH000026: Second-level cache disabled
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration boolean -> org.hibernate.type.BasicTypeReference@306c9b2c
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration boolean -> org.hibernate.type.BasicTypeReference@306c9b2c
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Boolean -> org.hibernate.type.BasicTypeReference@306c9b2c
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration numeric_boolean -> org.hibernate.type.BasicTypeReference@1ab28416
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration org.hibernate.type.NumericBooleanConverter -> org.hibernate.type.BasicTypeReference@1ab28416
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration true_false -> org.hibernate.type.BasicTypeReference@52efb338
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration org.hibernate.type.TrueFalseConverter -> org.hibernate.type.BasicTypeReference@52efb338
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration yes_no -> org.hibernate.type.BasicTypeReference@64508788
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration org.hibernate.type.YesNoConverter -> org.hibernate.type.BasicTypeReference@64508788
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration byte -> org.hibernate.type.BasicTypeReference@30b1c5d5
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration byte -> org.hibernate.type.BasicTypeReference@30b1c5d5
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Byte -> org.hibernate.type.BasicTypeReference@30b1c5d5
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration binary -> org.hibernate.type.BasicTypeReference@3e2d65e1
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration byte[] -> org.hibernate.type.BasicTypeReference@3e2d65e1
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration [B -> org.hibernate.type.BasicTypeReference@3e2d65e1
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration binary_wrapper -> org.hibernate.type.BasicTypeReference@1174676f
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration wrapper-binary -> org.hibernate.type.BasicTypeReference@1174676f
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration image -> org.hibernate.type.BasicTypeReference@71f8ce0e
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration blob -> org.hibernate.type.BasicTypeReference@4fd92289
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.sql.Blob -> org.hibernate.type.BasicTypeReference@4fd92289
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_blob -> org.hibernate.type.BasicTypeReference@1a8e44fe
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_blob_wrapper -> org.hibernate.type.BasicTypeReference@287317df
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration short -> org.hibernate.type.BasicTypeReference@1fcc3461
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration short -> org.hibernate.type.BasicTypeReference@1fcc3461
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Short -> org.hibernate.type.BasicTypeReference@1fcc3461
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration integer -> org.hibernate.type.BasicTypeReference@1987807b
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration int -> org.hibernate.type.BasicTypeReference@1987807b
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Integer -> org.hibernate.type.BasicTypeReference@1987807b
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration long -> org.hibernate.type.BasicTypeReference@71469e01
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration long -> org.hibernate.type.BasicTypeReference@71469e01
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Long -> org.hibernate.type.BasicTypeReference@71469e01
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration float -> org.hibernate.type.BasicTypeReference@41bbb219
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration float -> org.hibernate.type.BasicTypeReference@41bbb219
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Float -> org.hibernate.type.BasicTypeReference@41bbb219
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration double -> org.hibernate.type.BasicTypeReference@3f2ae973
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration double -> org.hibernate.type.BasicTypeReference@3f2ae973
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Double -> org.hibernate.type.BasicTypeReference@3f2ae973
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration big_integer -> org.hibernate.type.BasicTypeReference@1a8b22b5
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.math.BigInteger -> org.hibernate.type.BasicTypeReference@1a8b22b5
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration big_decimal -> org.hibernate.type.BasicTypeReference@5f781173
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.math.BigDecimal -> org.hibernate.type.BasicTypeReference@5f781173
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration character -> org.hibernate.type.BasicTypeReference@43cf5bff
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration char -> org.hibernate.type.BasicTypeReference@43cf5bff
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Character -> org.hibernate.type.BasicTypeReference@43cf5bff
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration character_nchar -> org.hibernate.type.BasicTypeReference@2b464384
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration string -> org.hibernate.type.BasicTypeReference@681b42d3
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.String -> org.hibernate.type.BasicTypeReference@681b42d3
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration nstring -> org.hibernate.type.BasicTypeReference@77f7352a
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration characters -> org.hibernate.type.BasicTypeReference@4ede8888
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration char[] -> org.hibernate.type.BasicTypeReference@4ede8888
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration [C -> org.hibernate.type.BasicTypeReference@4ede8888
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration wrapper-characters -> org.hibernate.type.BasicTypeReference@571db8b4
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration text -> org.hibernate.type.BasicTypeReference@65a2755e
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration ntext -> org.hibernate.type.BasicTypeReference@2b3242a5
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration clob -> org.hibernate.type.BasicTypeReference@11120583
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.sql.Clob -> org.hibernate.type.BasicTypeReference@11120583
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration nclob -> org.hibernate.type.BasicTypeReference@2bf0c70d
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.sql.NClob -> org.hibernate.type.BasicTypeReference@2bf0c70d
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_clob -> org.hibernate.type.BasicTypeReference@5d8e4fa8
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_clob_char_array -> org.hibernate.type.BasicTypeReference@649009d6
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_clob_character_array -> org.hibernate.type.BasicTypeReference@652f26da
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_nclob -> org.hibernate.type.BasicTypeReference@484a5ddd
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_nclob_character_array -> org.hibernate.type.BasicTypeReference@6796a873
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_nclob_char_array -> org.hibernate.type.BasicTypeReference@3acc3ee
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration Duration -> org.hibernate.type.BasicTypeReference@1f293cb7
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.Duration -> org.hibernate.type.BasicTypeReference@1f293cb7
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration LocalDateTime -> org.hibernate.type.BasicTypeReference@5972e3a
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.LocalDateTime -> org.hibernate.type.BasicTypeReference@5972e3a
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration LocalDate -> org.hibernate.type.BasicTypeReference@5790cbcb
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.LocalDate -> org.hibernate.type.BasicTypeReference@5790cbcb
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration LocalTime -> org.hibernate.type.BasicTypeReference@32c6d164
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.LocalTime -> org.hibernate.type.BasicTypeReference@32c6d164
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration OffsetDateTime -> org.hibernate.type.BasicTypeReference@645c9f0f
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.OffsetDateTime -> org.hibernate.type.BasicTypeReference@645c9f0f
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration OffsetDateTimeWithTimezone -> org.hibernate.type.BasicTypeReference@58068b40
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration OffsetDateTimeWithoutTimezone -> org.hibernate.type.BasicTypeReference@999cd18
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration OffsetTime -> org.hibernate.type.BasicTypeReference@dd060be
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.OffsetTime -> org.hibernate.type.BasicTypeReference@dd060be
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration OffsetTimeUtc -> org.hibernate.type.BasicTypeReference@df432ec
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration OffsetTimeWithTimezone -> org.hibernate.type.BasicTypeReference@6144e499
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration OffsetTimeWithoutTimezone -> org.hibernate.type.BasicTypeReference@26f204a4
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration ZonedDateTime -> org.hibernate.type.BasicTypeReference@28295554
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.ZonedDateTime -> org.hibernate.type.BasicTypeReference@28295554
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration ZonedDateTimeWithTimezone -> org.hibernate.type.BasicTypeReference@4e671ef
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration ZonedDateTimeWithoutTimezone -> org.hibernate.type.BasicTypeReference@42403dc6
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration date -> org.hibernate.type.BasicTypeReference@74a1d60e
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.sql.Date -> org.hibernate.type.BasicTypeReference@74a1d60e
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration time -> org.hibernate.type.BasicTypeReference@16c0be3b
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.sql.Time -> org.hibernate.type.BasicTypeReference@16c0be3b
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration timestamp -> org.hibernate.type.BasicTypeReference@219edc05
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.sql.Timestamp -> org.hibernate.type.BasicTypeReference@219edc05
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.util.Date -> org.hibernate.type.BasicTypeReference@219edc05
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration calendar -> org.hibernate.type.BasicTypeReference@62f37bfd
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.util.Calendar -> org.hibernate.type.BasicTypeReference@62f37bfd
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.util.GregorianCalendar -> org.hibernate.type.BasicTypeReference@62f37bfd
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration calendar_date -> org.hibernate.type.BasicTypeReference@1818d00b
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration calendar_time -> org.hibernate.type.BasicTypeReference@b3a8455
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration instant -> org.hibernate.type.BasicTypeReference@5c930fc3
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.Instant -> org.hibernate.type.BasicTypeReference@5c930fc3
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration uuid -> org.hibernate.type.BasicTypeReference@25c6ab3f
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.util.UUID -> org.hibernate.type.BasicTypeReference@25c6ab3f
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration pg-uuid -> org.hibernate.type.BasicTypeReference@25c6ab3f
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration uuid-binary -> org.hibernate.type.BasicTypeReference@7b80af04
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration uuid-char -> org.hibernate.type.BasicTypeReference@2447940d
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration class -> org.hibernate.type.BasicTypeReference@60ee7a51
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Class -> org.hibernate.type.BasicTypeReference@60ee7a51
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration currency -> org.hibernate.type.BasicTypeReference@70e1aa20
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration Currency -> org.hibernate.type.BasicTypeReference@70e1aa20
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.util.Currency -> org.hibernate.type.BasicTypeReference@70e1aa20
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration locale -> org.hibernate.type.BasicTypeReference@e67d3b7
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.util.Locale -> org.hibernate.type.BasicTypeReference@e67d3b7
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration serializable -> org.hibernate.type.BasicTypeReference@1618c98a
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.io.Serializable -> org.hibernate.type.BasicTypeReference@1618c98a
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration timezone -> org.hibernate.type.BasicTypeReference@5b715ea
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.util.TimeZone -> org.hibernate.type.BasicTypeReference@5b715ea
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration ZoneOffset -> org.hibernate.type.BasicTypeReference@787a0fd6
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.ZoneOffset -> org.hibernate.type.BasicTypeReference@787a0fd6
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration url -> org.hibernate.type.BasicTypeReference@48b09105
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.net.URL -> org.hibernate.type.BasicTypeReference@48b09105
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration vector -> org.hibernate.type.BasicTypeReference@18b45500
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration row_version -> org.hibernate.type.BasicTypeReference@25110bb9
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration object -> org.hibernate.type.JavaObjectType@30eb55c9
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Object -> org.hibernate.type.JavaObjectType@30eb55c9
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration null -> org.hibernate.type.NullType@5badeda0
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_date -> org.hibernate.type.BasicTypeReference@56a9a7b5
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_time -> org.hibernate.type.BasicTypeReference@338270ea
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_timestamp -> org.hibernate.type.BasicTypeReference@7f64bd7
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_calendar -> org.hibernate.type.BasicTypeReference@1c79d093
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_calendar_date -> org.hibernate.type.BasicTypeReference@746fd19b
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_calendar_time -> org.hibernate.type.BasicTypeReference@54caeadc
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_binary -> org.hibernate.type.BasicTypeReference@61d7bb61
|
||||||
|
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_serializable -> org.hibernate.type.BasicTypeReference@33f81280
|
||||||
|
2025-10-23 17:38:10 [main] INFO o.s.o.j.p.SpringPersistenceUnitInfo - No LoadTimeWeaver setup: ignoring JPA class transformer
|
||||||
|
2025-10-23 17:38:10 [main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
|
||||||
|
2025-10-23 17:38:11 [main] WARN o.h.e.j.e.i.JdbcEnvironmentInitiator - HHH000342: Could not obtain connection to query metadata
|
||||||
|
java.lang.NullPointerException: Cannot invoke "org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(java.sql.SQLException, String)" because the return value of "org.hibernate.resource.transaction.backend.jdbc.internal.JdbcIsolationDelegate.sqlExceptionHelper()" is null
|
||||||
|
at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcIsolationDelegate.delegateWork(JdbcIsolationDelegate.java:116)
|
||||||
|
at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.getJdbcEnvironmentUsingJdbcMetadata(JdbcEnvironmentInitiator.java:290)
|
||||||
|
at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.initiateService(JdbcEnvironmentInitiator.java:123)
|
||||||
|
at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.initiateService(JdbcEnvironmentInitiator.java:77)
|
||||||
|
at org.hibernate.boot.registry.internal.StandardServiceRegistryImpl.initiateService(StandardServiceRegistryImpl.java:130)
|
||||||
|
at org.hibernate.service.internal.AbstractServiceRegistryImpl.createService(AbstractServiceRegistryImpl.java:263)
|
||||||
|
at org.hibernate.service.internal.AbstractServiceRegistryImpl.initializeService(AbstractServiceRegistryImpl.java:238)
|
||||||
|
at org.hibernate.service.internal.AbstractServiceRegistryImpl.getService(AbstractServiceRegistryImpl.java:215)
|
||||||
|
at org.hibernate.boot.model.relational.Database.<init>(Database.java:45)
|
||||||
|
at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.getDatabase(InFlightMetadataCollectorImpl.java:221)
|
||||||
|
at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.<init>(InFlightMetadataCollectorImpl.java:189)
|
||||||
|
at org.hibernate.boot.model.process.spi.MetadataBuildingProcess.complete(MetadataBuildingProcess.java:171)
|
||||||
|
at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.metadata(EntityManagerFactoryBuilderImpl.java:1431)
|
||||||
|
at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:1502)
|
||||||
|
at org.springframework.orm.jpa.vendor.SpringHibernateJpaPersistenceProvider.createContainerEntityManagerFactory(SpringHibernateJpaPersistenceProvider.java:75)
|
||||||
|
at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.createNativeEntityManagerFactory(LocalContainerEntityManagerFactoryBean.java:390)
|
||||||
|
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.buildNativeEntityManagerFactory(AbstractEntityManagerFactoryBean.java:409)
|
||||||
|
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.afterPropertiesSet(AbstractEntityManagerFactoryBean.java:396)
|
||||||
|
at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.afterPropertiesSet(LocalContainerEntityManagerFactoryBean.java:366)
|
||||||
|
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1835)
|
||||||
|
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1784)
|
||||||
|
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:600)
|
||||||
|
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:522)
|
||||||
|
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:337)
|
||||||
|
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
|
||||||
|
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:335)
|
||||||
|
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:205)
|
||||||
|
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:952)
|
||||||
|
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:624)
|
||||||
|
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146)
|
||||||
|
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:754)
|
||||||
|
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:456)
|
||||||
|
at org.springframework.boot.SpringApplication.run(SpringApplication.java:335)
|
||||||
|
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1363)
|
||||||
|
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1352)
|
||||||
|
at com.unicorn.hgzero.ai.AiApplication.main(AiApplication.java:20)
|
||||||
|
2025-10-23 17:38:11 [main] ERROR o.s.o.j.LocalContainerEntityManagerFactoryBean - Failed to initialize JPA EntityManagerFactory: Unable to create requested service [org.hibernate.engine.jdbc.env.spi.JdbcEnvironment] due to: Unable to determine Dialect without JDBC metadata (please set 'jakarta.persistence.jdbc.url' for common cases or 'hibernate.dialect' when a custom Dialect implementation must be provided)
|
||||||
|
2025-10-23 17:38:11 [main] WARN o.s.b.w.s.c.AnnotationConfigServletWebServerApplicationContext - Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]: Unable to create requested service [org.hibernate.engine.jdbc.env.spi.JdbcEnvironment] due to: Unable to determine Dialect without JDBC metadata (please set 'jakarta.persistence.jdbc.url' for common cases or 'hibernate.dialect' when a custom Dialect implementation must be provided)
|
||||||
|
2025-10-23 17:38:11 [main] INFO o.a.catalina.core.StandardService - Stopping service [Tomcat]
|
||||||
|
2025-10-23 17:38:11 [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-23 17:38:11 [main] ERROR o.s.boot.SpringApplication - Application run failed
|
||||||
|
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]: Unable to create requested service [org.hibernate.engine.jdbc.env.spi.JdbcEnvironment] due to: Unable to determine Dialect without JDBC metadata (please set 'jakarta.persistence.jdbc.url' for common cases or 'hibernate.dialect' when a custom Dialect implementation must be provided)
|
||||||
|
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1788)
|
||||||
|
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:600)
|
||||||
|
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:522)
|
||||||
|
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:337)
|
||||||
|
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
|
||||||
|
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:335)
|
||||||
|
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:205)
|
||||||
|
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:952)
|
||||||
|
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:624)
|
||||||
|
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146)
|
||||||
|
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:754)
|
||||||
|
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:456)
|
||||||
|
at org.springframework.boot.SpringApplication.run(SpringApplication.java:335)
|
||||||
|
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1363)
|
||||||
|
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1352)
|
||||||
|
at com.unicorn.hgzero.ai.AiApplication.main(AiApplication.java:20)
|
||||||
|
Caused by: org.hibernate.service.spi.ServiceException: Unable to create requested service [org.hibernate.engine.jdbc.env.spi.JdbcEnvironment] due to: Unable to determine Dialect without JDBC metadata (please set 'jakarta.persistence.jdbc.url' for common cases or 'hibernate.dialect' when a custom Dialect implementation must be provided)
|
||||||
|
at org.hibernate.service.internal.AbstractServiceRegistryImpl.createService(AbstractServiceRegistryImpl.java:276)
|
||||||
|
at org.hibernate.service.internal.AbstractServiceRegistryImpl.initializeService(AbstractServiceRegistryImpl.java:238)
|
||||||
|
at org.hibernate.service.internal.AbstractServiceRegistryImpl.getService(AbstractServiceRegistryImpl.java:215)
|
||||||
|
at org.hibernate.boot.model.relational.Database.<init>(Database.java:45)
|
||||||
|
at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.getDatabase(InFlightMetadataCollectorImpl.java:221)
|
||||||
|
at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.<init>(InFlightMetadataCollectorImpl.java:189)
|
||||||
|
at org.hibernate.boot.model.process.spi.MetadataBuildingProcess.complete(MetadataBuildingProcess.java:171)
|
||||||
|
at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.metadata(EntityManagerFactoryBuilderImpl.java:1431)
|
||||||
|
at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:1502)
|
||||||
|
at org.springframework.orm.jpa.vendor.SpringHibernateJpaPersistenceProvider.createContainerEntityManagerFactory(SpringHibernateJpaPersistenceProvider.java:75)
|
||||||
|
at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.createNativeEntityManagerFactory(LocalContainerEntityManagerFactoryBean.java:390)
|
||||||
|
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.buildNativeEntityManagerFactory(AbstractEntityManagerFactoryBean.java:409)
|
||||||
|
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.afterPropertiesSet(AbstractEntityManagerFactoryBean.java:396)
|
||||||
|
at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.afterPropertiesSet(LocalContainerEntityManagerFactoryBean.java:366)
|
||||||
|
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1835)
|
||||||
|
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1784)
|
||||||
|
... 15 common frames omitted
|
||||||
|
Caused by: org.hibernate.HibernateException: Unable to determine Dialect without JDBC metadata (please set 'jakarta.persistence.jdbc.url' for common cases or 'hibernate.dialect' when a custom Dialect implementation must be provided)
|
||||||
|
at org.hibernate.engine.jdbc.dialect.internal.DialectFactoryImpl.determineDialect(DialectFactoryImpl.java:191)
|
||||||
|
at org.hibernate.engine.jdbc.dialect.internal.DialectFactoryImpl.buildDialect(DialectFactoryImpl.java:87)
|
||||||
|
at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.getJdbcEnvironmentWithDefaults(JdbcEnvironmentInitiator.java:152)
|
||||||
|
at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.getJdbcEnvironmentUsingJdbcMetadata(JdbcEnvironmentInitiator.java:362)
|
||||||
|
at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.initiateService(JdbcEnvironmentInitiator.java:123)
|
||||||
|
at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.initiateService(JdbcEnvironmentInitiator.java:77)
|
||||||
|
at org.hibernate.boot.registry.internal.StandardServiceRegistryImpl.initiateService(StandardServiceRegistryImpl.java:130)
|
||||||
|
at org.hibernate.service.internal.AbstractServiceRegistryImpl.createService(AbstractServiceRegistryImpl.java:263)
|
||||||
|
... 30 common frames omitted
|
||||||
|
|||||||
545
backup/v1.4.16/07-회의종료.html
Normal file
545
backup/v1.4.16/07-회의종료.html
Normal file
@ -0,0 +1,545 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>회의 종료 - 회의록 서비스</title>
|
||||||
|
<link rel="stylesheet" href="common.css">
|
||||||
|
<style>
|
||||||
|
/* 페이지 특화 스타일 */
|
||||||
|
.page-header {
|
||||||
|
text-align: center;
|
||||||
|
padding: var(--space-lg) 0;
|
||||||
|
background: var(--primary-light);
|
||||||
|
margin: calc(var(--space-md) * -1) calc(var(--space-md) * -1) var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header h1 {
|
||||||
|
font-size: var(--font-h2);
|
||||||
|
color: var(--primary);
|
||||||
|
margin-bottom: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header .meeting-title {
|
||||||
|
font-size: var(--font-body);
|
||||||
|
color: var(--gray-700);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 통계 카드 그리드 - 정보 표시용 (인터랙션 없음) */
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
margin-bottom: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
min-height: 80px;
|
||||||
|
padding: var(--space-md);
|
||||||
|
background: var(--gray-50);
|
||||||
|
border: 1px solid var(--gray-200);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: var(--font-h1);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
color: var(--gray-900);
|
||||||
|
margin-bottom: var(--space-xs);
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: var(--font-small);
|
||||||
|
color: var(--gray-600);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 키워드 클라우드 */
|
||||||
|
.keyword-cloud {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
padding: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyword-tag {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: var(--primary-light);
|
||||||
|
color: var(--primary-dark);
|
||||||
|
border-radius: 16px;
|
||||||
|
font-size: var(--font-small);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 안건 카드 */
|
||||||
|
.agenda-card {
|
||||||
|
background: var(--white);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
margin-bottom: var(--space-md);
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--gray-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agenda-header {
|
||||||
|
padding: var(--space-md);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background: var(--gray-50);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agenda-header:hover {
|
||||||
|
background: var(--gray-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agenda-title {
|
||||||
|
font-size: var(--font-h4);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
color: var(--gray-900);
|
||||||
|
margin-bottom: var(--space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-summary-short {
|
||||||
|
background: var(--gray-100);
|
||||||
|
border-left: 4px solid var(--primary);
|
||||||
|
padding: var(--space-sm) var(--space-md);
|
||||||
|
margin-top: var(--space-sm);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-size: var(--font-small);
|
||||||
|
color: var(--gray-700);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-summary-short .lock-icon {
|
||||||
|
color: var(--gray-500);
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
color: var(--gray-500);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agenda-card.expanded .expand-icon {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agenda-content {
|
||||||
|
display: none;
|
||||||
|
padding: var(--space-md);
|
||||||
|
border-top: 1px solid var(--gray-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agenda-card.expanded .agenda-content {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agenda-section {
|
||||||
|
margin-bottom: var(--space-lg);
|
||||||
|
padding-bottom: var(--space-md);
|
||||||
|
border-bottom: 1px solid var(--gray-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agenda-section:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agenda-section-title {
|
||||||
|
font-size: var(--font-small);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
color: var(--gray-900);
|
||||||
|
margin-bottom: var(--space-sm);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agenda-section-title::before {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
width: 4px;
|
||||||
|
height: 16px;
|
||||||
|
background: var(--primary);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agenda-section-content {
|
||||||
|
font-size: var(--font-body);
|
||||||
|
color: var(--gray-700);
|
||||||
|
line-height: 1.6;
|
||||||
|
padding-left: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agenda-section-content ul {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agenda-section-content li {
|
||||||
|
margin-bottom: var(--space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 하단 액션 바 - 3개 버튼 배치 */
|
||||||
|
.action-bar {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: var(--white);
|
||||||
|
padding: var(--space-md);
|
||||||
|
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-bar .btn {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-bar .btn-primary {
|
||||||
|
flex: 2; /* 바로 최종 확정 버튼 강조 */
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.stats-grid {
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-bar {
|
||||||
|
position: static;
|
||||||
|
box-shadow: none;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-bar .btn {
|
||||||
|
flex: 0 1 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-bar .btn-primary {
|
||||||
|
flex: 0 1 250px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.readonly-notice {
|
||||||
|
background: var(--warning-light);
|
||||||
|
border: 1px solid var(--warning);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
padding: var(--space-md);
|
||||||
|
margin-bottom: var(--space-lg);
|
||||||
|
font-size: var(--font-small);
|
||||||
|
color: var(--warning-dark);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="page">
|
||||||
|
<div class="container has-action-bar">
|
||||||
|
<!-- 페이지 헤더 -->
|
||||||
|
<div class="page-header">
|
||||||
|
<h1>✅ 회의가 종료되었습니다</h1>
|
||||||
|
<p class="meeting-title">2025년 1분기 제품 기획 회의</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 읽기 전용 안내 -->
|
||||||
|
<div class="readonly-notice">
|
||||||
|
🔒 이 화면은 <strong>확인 전용</strong>입니다. 내용을 수정하려면 "회의록 수정" 버튼을 클릭하세요.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 통계 카드 그리드 -->
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-value" id="durationValue">0</div>
|
||||||
|
<div class="stat-label">회의 시간 (분)</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-value" id="participantsValue">0</div>
|
||||||
|
<div class="stat-label">참석자</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-value" id="agendasValue">0</div>
|
||||||
|
<div class="stat-label">안건</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-value" id="todosValue">0</div>
|
||||||
|
<div class="stat-label">Todo</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 주요 키워드 -->
|
||||||
|
<div class="card mb-md">
|
||||||
|
<h3 class="card-title">주요 키워드</h3>
|
||||||
|
<div class="keyword-cloud">
|
||||||
|
<span class="keyword-tag">신제품 기획</span>
|
||||||
|
<span class="keyword-tag">예산 편성</span>
|
||||||
|
<span class="keyword-tag">일정 조율</span>
|
||||||
|
<span class="keyword-tag">시장 조사</span>
|
||||||
|
<span class="keyword-tag">UI/UX</span>
|
||||||
|
<span class="keyword-tag">개발 스펙</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- 안건별 AI 요약 -->
|
||||||
|
<div class="card mb-md">
|
||||||
|
<h3 class="card-title">안건별 AI 요약</h3>
|
||||||
|
<div id="agendaList"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 하단 액션 바 (3가지 선택 옵션) -->
|
||||||
|
<div class="action-bar">
|
||||||
|
<button class="btn btn-ghost" onclick="navigateTo('02-대시보드.html')">
|
||||||
|
대시보드
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-secondary" onclick="navigateTo('11-회의록수정.html')">
|
||||||
|
회의록 수정
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary" onclick="confirmMeetingDirectly()">
|
||||||
|
바로 최종 확정
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="common.js"></script>
|
||||||
|
<script>
|
||||||
|
// 샘플 안건 데이터
|
||||||
|
const SAMPLE_AGENDAS = [
|
||||||
|
{
|
||||||
|
id: 'agenda-1',
|
||||||
|
title: '1. 신제품 기획 방향성',
|
||||||
|
aiSummaryShort: '타겟 고객을 20-30대로 설정, UI/UX 개선 집중',
|
||||||
|
details: {
|
||||||
|
discussion: '신제품의 주요 타겟 고객층을 20-30대 직장인으로 설정하고, 기존 제품 대비 UI/UX를 대폭 개선하기로 함',
|
||||||
|
opinions: [
|
||||||
|
{ speaker: '김민준', opinion: '타겟 고객층을 명확히 설정하여 마케팅 전략 수립 필요' },
|
||||||
|
{ speaker: '박서연', opinion: 'UI/UX 개선에 AI 기술 적용 검토' }
|
||||||
|
],
|
||||||
|
decisions: ['타겟 고객: 20-30대 직장인', 'UI/UX 개선을 최우선 과제로 설정'],
|
||||||
|
pending: []
|
||||||
|
},
|
||||||
|
todos: [
|
||||||
|
{
|
||||||
|
title: '시장 조사 보고서 작성',
|
||||||
|
assignee: SAMPLE_MEETINGS[0].participants[0],
|
||||||
|
dueDate: '2025-11-01',
|
||||||
|
priority: 'high'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'UI/UX 개선안 초안 작성',
|
||||||
|
assignee: SAMPLE_MEETINGS[0].participants[1],
|
||||||
|
dueDate: '2025-11-05',
|
||||||
|
priority: 'medium'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'agenda-2',
|
||||||
|
title: '2. 예산 편성 및 일정',
|
||||||
|
aiSummaryShort: '총 예산 5억, 개발 기간 6개월 확정',
|
||||||
|
details: {
|
||||||
|
discussion: '신제품 개발을 위한 총 예산을 5억원으로 책정하고, 개발 기간은 6개월로 확정함',
|
||||||
|
opinions: [
|
||||||
|
{ speaker: '이준호', opinion: '개발 기간 6개월은 타이트하므로 우선순위 명확화 필요' },
|
||||||
|
{ speaker: '최유진', opinion: '예산 배분은 개발 60%, 마케팅 40%로 제안' }
|
||||||
|
],
|
||||||
|
decisions: ['총 예산: 5억원', '개발 기간: 6개월', '예산 배분: 개발 60%, 마케팅 40%'],
|
||||||
|
pending: ['세부 일정 확정은 다음 회의에서 논의']
|
||||||
|
},
|
||||||
|
todos: [
|
||||||
|
{
|
||||||
|
title: '세부 개발 일정 수립',
|
||||||
|
assignee: SAMPLE_MEETINGS[0].participants[2],
|
||||||
|
dueDate: '2025-10-28',
|
||||||
|
priority: 'high'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'agenda-3',
|
||||||
|
title: '3. 기술 스택 및 개발 방향',
|
||||||
|
aiSummaryShort: 'React 기반 프론트엔드, AI 챗봇 기능 추가',
|
||||||
|
details: {
|
||||||
|
discussion: '프론트엔드는 React 기반으로 개발하고, 고객 지원을 위한 AI 챗봇 기능을 추가하기로 함',
|
||||||
|
opinions: [
|
||||||
|
{ speaker: '박서연', opinion: 'AI 챗봇은 GPT-4 기반으로 개발 제안' },
|
||||||
|
{ speaker: '이준호', opinion: 'React 외에 Next.js 도입 검토 필요' }
|
||||||
|
],
|
||||||
|
decisions: ['프론트엔드: React 기반', 'AI 챗봇 기능 추가', 'Next.js 도입 검토'],
|
||||||
|
pending: ['AI 챗봇 학습 데이터 확보 방안']
|
||||||
|
},
|
||||||
|
todos: [
|
||||||
|
{
|
||||||
|
title: 'AI 챗봇 프로토타입 개발',
|
||||||
|
assignee: SAMPLE_MEETINGS[0].participants[1],
|
||||||
|
dueDate: '2025-11-10',
|
||||||
|
priority: 'medium'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Next.js 도입 검토 보고서',
|
||||||
|
assignee: SAMPLE_MEETINGS[0].participants[3],
|
||||||
|
dueDate: '2025-11-03',
|
||||||
|
priority: 'low'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// 페이지 초기화
|
||||||
|
function initPage() {
|
||||||
|
// 통계 카운트 애니메이션
|
||||||
|
animateCounter('durationValue', 90);
|
||||||
|
animateCounter('participantsValue', 4);
|
||||||
|
animateCounter('agendasValue', SAMPLE_AGENDAS.length);
|
||||||
|
|
||||||
|
// Todo 전체 개수 계산
|
||||||
|
const totalTodos = SAMPLE_AGENDAS.reduce((sum, agenda) => sum + (agenda.todos?.length || 0), 0);
|
||||||
|
animateCounter('todosValue', totalTodos);
|
||||||
|
|
||||||
|
// 안건 리스트 렌더링
|
||||||
|
renderAgendaList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 카운터 애니메이션
|
||||||
|
function animateCounter(elementId, target) {
|
||||||
|
const element = $(`#${elementId}`);
|
||||||
|
let current = 0;
|
||||||
|
const increment = target / 30;
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
current += increment;
|
||||||
|
if (current >= target) {
|
||||||
|
element.textContent = target;
|
||||||
|
clearInterval(timer);
|
||||||
|
} else {
|
||||||
|
element.textContent = Math.floor(current);
|
||||||
|
}
|
||||||
|
}, 30);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 안건 리스트 렌더링
|
||||||
|
function renderAgendaList() {
|
||||||
|
const container = $('#agendaList');
|
||||||
|
|
||||||
|
SAMPLE_AGENDAS.forEach(agenda => {
|
||||||
|
// 안건 카드
|
||||||
|
const card = createElement('div', {
|
||||||
|
className: 'agenda-card',
|
||||||
|
id: `agenda-${agenda.id}`,
|
||||||
|
onclick: `toggleAgenda('${agenda.id}')`
|
||||||
|
});
|
||||||
|
|
||||||
|
// 헤더
|
||||||
|
const header = createElement('div', { className: 'agenda-header' }, `
|
||||||
|
<div>
|
||||||
|
<div class="agenda-title">${agenda.title}</div>
|
||||||
|
<div class="ai-summary-short">
|
||||||
|
<span class="lock-icon">🔒</span>
|
||||||
|
<span>${agenda.aiSummaryShort}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="expand-icon">▼</div>
|
||||||
|
`);
|
||||||
|
card.appendChild(header);
|
||||||
|
|
||||||
|
// 상세 내용
|
||||||
|
const content = createElement('div', { className: 'agenda-content' });
|
||||||
|
|
||||||
|
// 논의 주제
|
||||||
|
if (agenda.details.discussion) {
|
||||||
|
content.appendChild(createElement('div', { className: 'agenda-section' }, `
|
||||||
|
<div class="agenda-section-title">논의 주제</div>
|
||||||
|
<div class="agenda-section-content">${agenda.details.discussion}</div>
|
||||||
|
`));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 결정 사항
|
||||||
|
if (agenda.details.decisions && agenda.details.decisions.length > 0) {
|
||||||
|
const decisionsHtml = agenda.details.decisions.map(d => `<li>✓ ${d}</li>`).join('');
|
||||||
|
content.appendChild(createElement('div', { className: 'agenda-section' }, `
|
||||||
|
<div class="agenda-section-title">결정 사항</div>
|
||||||
|
<div class="agenda-section-content"><ul>${decisionsHtml}</ul></div>
|
||||||
|
`));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 보류 사항
|
||||||
|
if (agenda.details.pending && agenda.details.pending.length > 0) {
|
||||||
|
const pendingHtml = agenda.details.pending.map(p => `<li>⏸ ${p}</li>`).join('');
|
||||||
|
content.appendChild(createElement('div', { className: 'agenda-section' }, `
|
||||||
|
<div class="agenda-section-title">보류 사항</div>
|
||||||
|
<div class="agenda-section-content"><ul>${pendingHtml}</ul></div>
|
||||||
|
`));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Todo 목록 - 제목만 간단히 표시
|
||||||
|
if (agenda.todos && agenda.todos.length > 0) {
|
||||||
|
const todosSection = createElement('div', { className: 'agenda-section' }, `
|
||||||
|
<div class="agenda-section-title">Todo 자동 추출 결과</div>
|
||||||
|
`);
|
||||||
|
|
||||||
|
const todoList = createElement('ul', {
|
||||||
|
style: 'list-style: none; padding: 0; margin: 0;'
|
||||||
|
});
|
||||||
|
|
||||||
|
agenda.todos.forEach(todo => {
|
||||||
|
const todoItem = createElement('li', {
|
||||||
|
style: 'display: flex; align-items: center; gap: var(--space-sm); padding: var(--space-sm) 0; font-size: var(--font-body); color: var(--gray-700);'
|
||||||
|
}, `
|
||||||
|
<span style="color: var(--gray-500);">•</span>
|
||||||
|
<span>${todo.title}</span>
|
||||||
|
`);
|
||||||
|
todoList.appendChild(todoItem);
|
||||||
|
});
|
||||||
|
|
||||||
|
todosSection.appendChild(todoList);
|
||||||
|
|
||||||
|
// 안내 문구 추가
|
||||||
|
const notice = createElement('p', {
|
||||||
|
style: 'font-size: var(--font-small); color: var(--gray-500); margin-top: var(--space-md); padding-top: var(--space-sm); border-top: 1px solid var(--gray-200);'
|
||||||
|
}, '💡 담당자 및 마감일은 회의록 수정 화면에서 지정할 수 있습니다.');
|
||||||
|
todosSection.appendChild(notice);
|
||||||
|
|
||||||
|
content.appendChild(todosSection);
|
||||||
|
}
|
||||||
|
|
||||||
|
card.appendChild(content);
|
||||||
|
container.appendChild(card);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 안건 카드 확장/축소
|
||||||
|
function toggleAgenda(agendaId) {
|
||||||
|
const card = $(`#agenda-${agendaId}`);
|
||||||
|
card.classList.toggle('expanded');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 바로 최종 확정
|
||||||
|
function confirmMeetingDirectly() {
|
||||||
|
if (confirm('AI가 정리한 내용 그대로 최종 확정하시겠습니까?\n\n모든 안건이 자동으로 검증 완료 처리되며, 참석자에게 확정 알림이 발송됩니다.')) {
|
||||||
|
showToast('회의록이 최종 확정되었습니다', 'success');
|
||||||
|
setTimeout(() => {
|
||||||
|
navigateTo('02-대시보드.html');
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 페이지 로드 시 초기화
|
||||||
|
initPage();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
File diff suppressed because one or more lines are too long
@ -36,6 +36,12 @@ public enum ErrorCode {
|
|||||||
OPERATION_NOT_ALLOWED(HttpStatus.BAD_REQUEST, "B004", "허용되지 않은 작업입니다."),
|
OPERATION_NOT_ALLOWED(HttpStatus.BAD_REQUEST, "B004", "허용되지 않은 작업입니다."),
|
||||||
BUSINESS_RULE_VIOLATION(HttpStatus.BAD_REQUEST, "B005", "비즈니스 규칙 위반입니다."),
|
BUSINESS_RULE_VIOLATION(HttpStatus.BAD_REQUEST, "B005", "비즈니스 규칙 위반입니다."),
|
||||||
|
|
||||||
|
// 회의 관련 에러
|
||||||
|
MEETING_TIME_CONFLICT(HttpStatus.CONFLICT, "M001", "해당 시간대에 이미 예약된 회의가 있습니다."),
|
||||||
|
INVALID_MEETING_TIME(HttpStatus.BAD_REQUEST, "M002", "회의 시작 시간이 종료 시간보다 늦습니다."),
|
||||||
|
MEETING_NOT_FOUND(HttpStatus.NOT_FOUND, "M003", "회의를 찾을 수 없습니다."),
|
||||||
|
INVALID_MEETING_STATUS(HttpStatus.BAD_REQUEST, "M004", "유효하지 않은 회의 상태입니다."),
|
||||||
|
|
||||||
// 외부 시스템 에러 (4xxx)
|
// 외부 시스템 에러 (4xxx)
|
||||||
EXTERNAL_API_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "E001", "외부 API 호출 중 오류가 발생했습니다."),
|
EXTERNAL_API_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "E001", "외부 API 호출 중 오류가 발생했습니다."),
|
||||||
DATABASE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "E002", "데이터베이스 오류가 발생했습니다."),
|
DATABASE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "E002", "데이터베이스 오류가 발생했습니다."),
|
||||||
|
|||||||
@ -7,14 +7,12 @@ info:
|
|||||||
**핵심 기능:**
|
**핵심 기능:**
|
||||||
- 음성 녹음 시작/중지 관리
|
- 음성 녹음 시작/중지 관리
|
||||||
- 실시간 음성-텍스트 변환 (스트리밍)
|
- 실시간 음성-텍스트 변환 (스트리밍)
|
||||||
- 배치 음성-텍스트 변환
|
|
||||||
- 화자 식별 및 관리
|
|
||||||
- Azure Speech Service 통합
|
- Azure Speech Service 통합
|
||||||
|
|
||||||
**차별화 포인트:**
|
**차별화 포인트:**
|
||||||
- 기본 기능 (Hygiene Factor) - 경쟁사 대부분 제공
|
- 기본 기능 (Hygiene Factor) - 경쟁사 대부분 제공
|
||||||
- 실시간 스트리밍 처리로 즉각적인 자막 제공
|
- 실시간 스트리밍 처리로 즉각적인 자막 제공
|
||||||
- 화자 자동 식별 (90% 이상 정확도)
|
- **단순화**: 배치 처리 및 화자 식별 제거, 실시간 전용 기능
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
contact:
|
contact:
|
||||||
name: STT Service Team
|
name: STT Service Team
|
||||||
@ -25,7 +23,7 @@ servers:
|
|||||||
description: Production Server
|
description: Production Server
|
||||||
- url: https://dev-api.example.com/stt/v1
|
- url: https://dev-api.example.com/stt/v1
|
||||||
description: Development Server
|
description: Development Server
|
||||||
- url: http://localhost:8083/api/v1
|
- url: http://localhost:8084/api/v1
|
||||||
description: Local Development Server
|
description: Local Development Server
|
||||||
|
|
||||||
tags:
|
tags:
|
||||||
@ -33,8 +31,6 @@ tags:
|
|||||||
description: 음성 녹음 관리 API
|
description: 음성 녹음 관리 API
|
||||||
- name: Transcription
|
- name: Transcription
|
||||||
description: 음성-텍스트 변환 API
|
description: 음성-텍스트 변환 API
|
||||||
- name: Speaker
|
|
||||||
description: 화자 식별 및 관리 API
|
|
||||||
|
|
||||||
paths:
|
paths:
|
||||||
/recordings/prepare:
|
/recordings/prepare:
|
||||||
@ -50,7 +46,7 @@ paths:
|
|||||||
2. DB에 녹음 정보 생성
|
2. DB에 녹음 정보 생성
|
||||||
3. Azure Speech 인식기 초기화
|
3. Azure Speech 인식기 초기화
|
||||||
4. Blob Storage 저장 경로 생성
|
4. Blob Storage 저장 경로 생성
|
||||||
5. RecordingStarted 이벤트 발행
|
5. RecordingStarted 이벤트 발행 (Kafka)
|
||||||
operationId: prepareRecording
|
operationId: prepareRecording
|
||||||
x-user-story: UFR-STT-010
|
x-user-story: UFR-STT-010
|
||||||
x-controller: RecordingController
|
x-controller: RecordingController
|
||||||
@ -243,15 +239,14 @@ paths:
|
|||||||
**처리 흐름:**
|
**처리 흐름:**
|
||||||
1. 음성 데이터 스트림 수신
|
1. 음성 데이터 스트림 수신
|
||||||
2. Azure Speech Service 실시간 인식
|
2. Azure Speech Service 실시간 인식
|
||||||
3. 화자 식별
|
3. 신뢰도 검증 (70% threshold)
|
||||||
4. 신뢰도 검증 (70% threshold)
|
4. DB에 세그먼트 저장
|
||||||
5. DB에 세그먼트 저장
|
5. TranscriptSegmentReady 이벤트 발행 (Kafka)
|
||||||
6. TranscriptSegmentReady 이벤트 발행
|
6. WebSocket으로 실시간 자막 전송
|
||||||
7. WebSocket으로 실시간 자막 전송
|
|
||||||
|
|
||||||
**성능:**
|
**성능:**
|
||||||
- 실시간 인식 지연: < 1초
|
- 실시간 인식 지연: < 1초
|
||||||
- 처리 시간: 1-4초
|
- 처리 시간: 1-3초
|
||||||
operationId: streamTranscription
|
operationId: streamTranscription
|
||||||
x-user-story: UFR-STT-020
|
x-user-story: UFR-STT-020
|
||||||
x-controller: TranscriptController
|
x-controller: TranscriptController
|
||||||
@ -277,8 +272,6 @@ paths:
|
|||||||
transcriptId: "TRS-SEG-001"
|
transcriptId: "TRS-SEG-001"
|
||||||
recordingId: "REC-20250123-001"
|
recordingId: "REC-20250123-001"
|
||||||
text: "안녕하세요, 오늘 회의를 시작하겠습니다."
|
text: "안녕하세요, 오늘 회의를 시작하겠습니다."
|
||||||
speakerId: "SPK-001"
|
|
||||||
speakerName: "김철수"
|
|
||||||
timestamp: 1234567890
|
timestamp: 1234567890
|
||||||
duration: 3.5
|
duration: 3.5
|
||||||
confidence: 0.92
|
confidence: 0.92
|
||||||
@ -290,94 +283,6 @@ paths:
|
|||||||
security:
|
security:
|
||||||
- BearerAuth: []
|
- BearerAuth: []
|
||||||
|
|
||||||
/transcripts/batch:
|
|
||||||
post:
|
|
||||||
tags:
|
|
||||||
- Transcription
|
|
||||||
summary: 배치 음성-텍스트 변환
|
|
||||||
description: |
|
|
||||||
전체 오디오 파일을 배치로 변환 (비동기 처리)
|
|
||||||
|
|
||||||
**처리 흐름:**
|
|
||||||
1. 전체 오디오 파일 업로드
|
|
||||||
2. Azure Batch Transcription Job 생성
|
|
||||||
3. 비동기 처리 시작
|
|
||||||
4. Job ID 반환 (202 Accepted)
|
|
||||||
5. 처리 완료 시 Callback으로 결과 수신
|
|
||||||
|
|
||||||
**처리 시간:**
|
|
||||||
- 파일 업로드: 1-2초
|
|
||||||
- Azure 배치 처리: 5-30초 (파일 크기 따라)
|
|
||||||
- 총 처리 시간: 7-33초
|
|
||||||
operationId: batchTranscription
|
|
||||||
x-user-story: UFR-STT-020
|
|
||||||
x-controller: TranscriptController
|
|
||||||
requestBody:
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
multipart/form-data:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/BatchTranscriptionRequest'
|
|
||||||
responses:
|
|
||||||
'202':
|
|
||||||
description: 배치 작업 접수됨
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/BatchTranscriptionResponse'
|
|
||||||
example:
|
|
||||||
jobId: "JOB-20250123-001"
|
|
||||||
recordingId: "REC-20250123-001"
|
|
||||||
status: "PROCESSING"
|
|
||||||
estimatedCompletionTime: "2025-01-23T10:31:00Z"
|
|
||||||
callbackUrl: "https://api.example.com/stt/v1/transcripts/callback"
|
|
||||||
'400':
|
|
||||||
$ref: '#/components/responses/BadRequest'
|
|
||||||
'500':
|
|
||||||
$ref: '#/components/responses/InternalServerError'
|
|
||||||
security:
|
|
||||||
- BearerAuth: []
|
|
||||||
|
|
||||||
/transcripts/callback:
|
|
||||||
post:
|
|
||||||
tags:
|
|
||||||
- Transcription
|
|
||||||
summary: 배치 변환 완료 콜백
|
|
||||||
description: |
|
|
||||||
Azure Speech Service로부터 배치 변환 완료 콜백 수신
|
|
||||||
|
|
||||||
**처리 흐름:**
|
|
||||||
1. 배치 결과 수신
|
|
||||||
2. 세그먼트별 DB 저장
|
|
||||||
3. 전체 텍스트 병합
|
|
||||||
4. TranscriptionCompleted 이벤트 발행
|
|
||||||
operationId: batchTranscriptionCallback
|
|
||||||
x-user-story: UFR-STT-020
|
|
||||||
x-controller: TranscriptController
|
|
||||||
requestBody:
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/BatchCallbackRequest'
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: 콜백 처리 성공
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/TranscriptionCompleteResponse'
|
|
||||||
example:
|
|
||||||
jobId: "JOB-20250123-001"
|
|
||||||
recordingId: "REC-20250123-001"
|
|
||||||
status: "COMPLETED"
|
|
||||||
segmentCount: 120
|
|
||||||
totalDuration: 1800
|
|
||||||
averageConfidence: 0.88
|
|
||||||
'400':
|
|
||||||
$ref: '#/components/responses/BadRequest'
|
|
||||||
'500':
|
|
||||||
$ref: '#/components/responses/InternalServerError'
|
|
||||||
|
|
||||||
/transcripts/{recordingId}:
|
/transcripts/{recordingId}:
|
||||||
get:
|
get:
|
||||||
@ -389,7 +294,7 @@ paths:
|
|||||||
|
|
||||||
**응답 데이터:**
|
**응답 데이터:**
|
||||||
- 전체 텍스트
|
- 전체 텍스트
|
||||||
- 화자별 세그먼트 목록
|
- 세그먼트 목록
|
||||||
- 타임스탬프 정보
|
- 타임스탬프 정보
|
||||||
- 신뢰도 점수
|
- 신뢰도 점수
|
||||||
operationId: getTranscription
|
operationId: getTranscription
|
||||||
@ -404,13 +309,6 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
type: boolean
|
type: boolean
|
||||||
default: false
|
default: false
|
||||||
- name: speakerId
|
|
||||||
in: query
|
|
||||||
description: 특정 화자의 발언만 필터링
|
|
||||||
required: false
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
example: "SPK-001"
|
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: 변환 텍스트 조회 성공
|
description: 변환 텍스트 조회 성공
|
||||||
@ -420,16 +318,13 @@ paths:
|
|||||||
$ref: '#/components/schemas/TranscriptionResponse'
|
$ref: '#/components/schemas/TranscriptionResponse'
|
||||||
example:
|
example:
|
||||||
recordingId: "REC-20250123-001"
|
recordingId: "REC-20250123-001"
|
||||||
fullText: "김철수: 안녕하세요...\n이영희: 네, 안녕하세요..."
|
fullText: "안녕하세요, 오늘 회의를 시작하겠습니다..."
|
||||||
segmentCount: 120
|
segmentCount: 120
|
||||||
totalDuration: 1800
|
totalDuration: 1800
|
||||||
averageConfidence: 0.88
|
averageConfidence: 0.88
|
||||||
speakerCount: 3
|
|
||||||
segments:
|
segments:
|
||||||
- transcriptId: "TRS-SEG-001"
|
- transcriptId: "TRS-SEG-001"
|
||||||
text: "안녕하세요, 오늘 회의를 시작하겠습니다."
|
text: "안녕하세요, 오늘 회의를 시작하겠습니다."
|
||||||
speakerId: "SPK-001"
|
|
||||||
speakerName: "김철수"
|
|
||||||
timestamp: 0
|
timestamp: 0
|
||||||
duration: 3.5
|
duration: 3.5
|
||||||
confidence: 0.92
|
confidence: 0.92
|
||||||
@ -440,179 +335,6 @@ paths:
|
|||||||
security:
|
security:
|
||||||
- BearerAuth: []
|
- BearerAuth: []
|
||||||
|
|
||||||
/speakers/identify:
|
|
||||||
post:
|
|
||||||
tags:
|
|
||||||
- Speaker
|
|
||||||
summary: 화자 식별
|
|
||||||
description: |
|
|
||||||
음성 데이터로부터 화자 식별
|
|
||||||
|
|
||||||
**처리 흐름:**
|
|
||||||
1. Voice signature 생성
|
|
||||||
2. 기존 프로필과 매칭
|
|
||||||
3. 신규 화자 자동 등록
|
|
||||||
4. 화자 정보 반환
|
|
||||||
|
|
||||||
**정확도:**
|
|
||||||
- 화자 식별 정확도: > 90%
|
|
||||||
- 처리 시간: ~300ms
|
|
||||||
operationId: identifySpeaker
|
|
||||||
x-user-story: UFR-STT-010
|
|
||||||
x-controller: SpeakerController
|
|
||||||
requestBody:
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/IdentifySpeakerRequest'
|
|
||||||
example:
|
|
||||||
recordingId: "REC-20250123-001"
|
|
||||||
audioFrame: "base64_encoded_audio_frame"
|
|
||||||
timestamp: 1234567890
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: 화자 식별 성공
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/SpeakerIdentificationResponse'
|
|
||||||
example:
|
|
||||||
speakerId: "SPK-001"
|
|
||||||
speakerName: "김철수"
|
|
||||||
confidence: 0.95
|
|
||||||
isNewSpeaker: false
|
|
||||||
profileId: "PROFILE-12345"
|
|
||||||
'400':
|
|
||||||
$ref: '#/components/responses/BadRequest'
|
|
||||||
'500':
|
|
||||||
$ref: '#/components/responses/InternalServerError'
|
|
||||||
security:
|
|
||||||
- BearerAuth: []
|
|
||||||
|
|
||||||
/speakers/{speakerId}:
|
|
||||||
get:
|
|
||||||
tags:
|
|
||||||
- Speaker
|
|
||||||
summary: 화자 정보 조회
|
|
||||||
description: 특정 화자의 상세 정보 조회
|
|
||||||
operationId: getSpeaker
|
|
||||||
x-user-story: UFR-STT-010
|
|
||||||
x-controller: SpeakerController
|
|
||||||
parameters:
|
|
||||||
- name: speakerId
|
|
||||||
in: path
|
|
||||||
description: 화자 ID
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
example: "SPK-001"
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: 화자 정보 조회 성공
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/SpeakerDetailResponse'
|
|
||||||
example:
|
|
||||||
speakerId: "SPK-001"
|
|
||||||
speakerName: "김철수"
|
|
||||||
profileId: "PROFILE-12345"
|
|
||||||
totalSegments: 45
|
|
||||||
totalDuration: 450
|
|
||||||
averageConfidence: 0.92
|
|
||||||
firstAppeared: "2025-01-23T10:30:15Z"
|
|
||||||
lastAppeared: "2025-01-23T11:00:00Z"
|
|
||||||
'404':
|
|
||||||
$ref: '#/components/responses/NotFound'
|
|
||||||
'500':
|
|
||||||
$ref: '#/components/responses/InternalServerError'
|
|
||||||
security:
|
|
||||||
- BearerAuth: []
|
|
||||||
|
|
||||||
put:
|
|
||||||
tags:
|
|
||||||
- Speaker
|
|
||||||
summary: 화자 정보 업데이트
|
|
||||||
description: 화자 이름 등 정보 수정
|
|
||||||
operationId: updateSpeaker
|
|
||||||
x-user-story: UFR-STT-010
|
|
||||||
x-controller: SpeakerController
|
|
||||||
parameters:
|
|
||||||
- name: speakerId
|
|
||||||
in: path
|
|
||||||
description: 화자 ID
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
example: "SPK-001"
|
|
||||||
requestBody:
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/UpdateSpeakerRequest'
|
|
||||||
example:
|
|
||||||
speakerName: "김철수 팀장"
|
|
||||||
userId: "USER-123"
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: 화자 정보 업데이트 성공
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/SpeakerDetailResponse'
|
|
||||||
'404':
|
|
||||||
$ref: '#/components/responses/NotFound'
|
|
||||||
'500':
|
|
||||||
$ref: '#/components/responses/InternalServerError'
|
|
||||||
security:
|
|
||||||
- BearerAuth: []
|
|
||||||
|
|
||||||
/recordings/{recordingId}/speakers:
|
|
||||||
get:
|
|
||||||
tags:
|
|
||||||
- Speaker
|
|
||||||
summary: 녹음의 화자 목록 조회
|
|
||||||
description: 특정 녹음에 참여한 모든 화자 목록 조회
|
|
||||||
operationId: getRecordingSpeakers
|
|
||||||
x-user-story: UFR-STT-010
|
|
||||||
x-controller: SpeakerController
|
|
||||||
parameters:
|
|
||||||
- $ref: '#/components/parameters/RecordingIdParam'
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: 화자 목록 조회 성공
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/SpeakerListResponse'
|
|
||||||
example:
|
|
||||||
recordingId: "REC-20250123-001"
|
|
||||||
speakerCount: 3
|
|
||||||
speakers:
|
|
||||||
- speakerId: "SPK-001"
|
|
||||||
speakerName: "김철수"
|
|
||||||
segmentCount: 45
|
|
||||||
totalDuration: 450
|
|
||||||
speakingRatio: 0.45
|
|
||||||
- speakerId: "SPK-002"
|
|
||||||
speakerName: "이영희"
|
|
||||||
segmentCount: 38
|
|
||||||
totalDuration: 380
|
|
||||||
speakingRatio: 0.38
|
|
||||||
- speakerId: "SPK-003"
|
|
||||||
speakerName: "박민수"
|
|
||||||
segmentCount: 17
|
|
||||||
totalDuration: 170
|
|
||||||
speakingRatio: 0.17
|
|
||||||
'404':
|
|
||||||
$ref: '#/components/responses/NotFound'
|
|
||||||
'500':
|
|
||||||
$ref: '#/components/responses/InternalServerError'
|
|
||||||
security:
|
|
||||||
- BearerAuth: []
|
|
||||||
|
|
||||||
components:
|
components:
|
||||||
securitySchemes:
|
securitySchemes:
|
||||||
BearerAuth:
|
BearerAuth:
|
||||||
@ -657,7 +379,7 @@ components:
|
|||||||
example: "ko-KR"
|
example: "ko-KR"
|
||||||
attendeeCount:
|
attendeeCount:
|
||||||
type: integer
|
type: integer
|
||||||
description: 참석자 수 (화자 식별 최적화용)
|
description: 참석자 수
|
||||||
minimum: 1
|
minimum: 1
|
||||||
maximum: 50
|
maximum: 50
|
||||||
example: 5
|
example: 5
|
||||||
@ -809,10 +531,6 @@ components:
|
|||||||
type: integer
|
type: integer
|
||||||
description: 녹음 시간 (초)
|
description: 녹음 시간 (초)
|
||||||
example: 300
|
example: 300
|
||||||
speakerCount:
|
|
||||||
type: integer
|
|
||||||
description: 화자 수
|
|
||||||
example: 3
|
|
||||||
segmentCount:
|
segmentCount:
|
||||||
type: integer
|
type: integer
|
||||||
description: 세그먼트 수
|
description: 세그먼트 수
|
||||||
@ -866,14 +584,6 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
description: 변환된 텍스트
|
description: 변환된 텍스트
|
||||||
example: "안녕하세요, 오늘 회의를 시작하겠습니다."
|
example: "안녕하세요, 오늘 회의를 시작하겠습니다."
|
||||||
speakerId:
|
|
||||||
type: string
|
|
||||||
description: 화자 ID
|
|
||||||
example: "SPK-001"
|
|
||||||
speakerName:
|
|
||||||
type: string
|
|
||||||
description: 화자 이름
|
|
||||||
example: "김철수"
|
|
||||||
timestamp:
|
timestamp:
|
||||||
type: integer
|
type: integer
|
||||||
description: 타임스탬프 (ms)
|
description: 타임스탬프 (ms)
|
||||||
@ -895,89 +605,6 @@ components:
|
|||||||
description: 낮은 신뢰도 경고 플래그 (< 60%)
|
description: 낮은 신뢰도 경고 플래그 (< 60%)
|
||||||
example: false
|
example: false
|
||||||
|
|
||||||
BatchTranscriptionRequest:
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- recordingId
|
|
||||||
- audioFile
|
|
||||||
properties:
|
|
||||||
recordingId:
|
|
||||||
type: string
|
|
||||||
description: 녹음 ID
|
|
||||||
example: "REC-20250123-001"
|
|
||||||
audioFile:
|
|
||||||
type: string
|
|
||||||
format: binary
|
|
||||||
description: 오디오 파일 (WAV, MP3 등)
|
|
||||||
language:
|
|
||||||
type: string
|
|
||||||
description: 음성 인식 언어
|
|
||||||
default: "ko-KR"
|
|
||||||
example: "ko-KR"
|
|
||||||
callbackUrl:
|
|
||||||
type: string
|
|
||||||
format: uri
|
|
||||||
description: 처리 완료 콜백 URL
|
|
||||||
example: "https://api.example.com/stt/v1/transcripts/callback"
|
|
||||||
|
|
||||||
BatchTranscriptionResponse:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
jobId:
|
|
||||||
type: string
|
|
||||||
description: 배치 작업 ID
|
|
||||||
example: "JOB-20250123-001"
|
|
||||||
recordingId:
|
|
||||||
type: string
|
|
||||||
description: 녹음 ID
|
|
||||||
example: "REC-20250123-001"
|
|
||||||
status:
|
|
||||||
type: string
|
|
||||||
description: 작업 상태
|
|
||||||
enum:
|
|
||||||
- QUEUED
|
|
||||||
- PROCESSING
|
|
||||||
- COMPLETED
|
|
||||||
- FAILED
|
|
||||||
example: "PROCESSING"
|
|
||||||
estimatedCompletionTime:
|
|
||||||
type: string
|
|
||||||
format: date-time
|
|
||||||
description: 예상 완료 시간
|
|
||||||
example: "2025-01-23T10:31:00Z"
|
|
||||||
callbackUrl:
|
|
||||||
type: string
|
|
||||||
format: uri
|
|
||||||
description: 콜백 URL
|
|
||||||
example: "https://api.example.com/stt/v1/transcripts/callback"
|
|
||||||
|
|
||||||
BatchCallbackRequest:
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- jobId
|
|
||||||
- status
|
|
||||||
- segments
|
|
||||||
properties:
|
|
||||||
jobId:
|
|
||||||
type: string
|
|
||||||
description: 배치 작업 ID
|
|
||||||
example: "JOB-20250123-001"
|
|
||||||
status:
|
|
||||||
type: string
|
|
||||||
description: 작업 상태
|
|
||||||
enum:
|
|
||||||
- COMPLETED
|
|
||||||
- FAILED
|
|
||||||
example: "COMPLETED"
|
|
||||||
segments:
|
|
||||||
type: array
|
|
||||||
description: 변환 세그먼트 목록
|
|
||||||
items:
|
|
||||||
$ref: '#/components/schemas/TranscriptionSegment'
|
|
||||||
error:
|
|
||||||
type: string
|
|
||||||
description: 오류 메시지 (실패 시)
|
|
||||||
example: "Audio file format not supported"
|
|
||||||
|
|
||||||
TranscriptionSegment:
|
TranscriptionSegment:
|
||||||
type: object
|
type: object
|
||||||
@ -986,10 +613,6 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
description: 변환된 텍스트
|
description: 변환된 텍스트
|
||||||
example: "안녕하세요, 오늘 회의를 시작하겠습니다."
|
example: "안녕하세요, 오늘 회의를 시작하겠습니다."
|
||||||
speakerId:
|
|
||||||
type: string
|
|
||||||
description: 화자 ID
|
|
||||||
example: "SPK-001"
|
|
||||||
timestamp:
|
timestamp:
|
||||||
type: integer
|
type: integer
|
||||||
description: 시작 타임스탬프 (ms)
|
description: 시작 타임스탬프 (ms)
|
||||||
@ -1061,10 +684,6 @@ components:
|
|||||||
format: float
|
format: float
|
||||||
description: 평균 신뢰도 점수
|
description: 평균 신뢰도 점수
|
||||||
example: 0.88
|
example: 0.88
|
||||||
speakerCount:
|
|
||||||
type: integer
|
|
||||||
description: 화자 수
|
|
||||||
example: 3
|
|
||||||
segments:
|
segments:
|
||||||
type: array
|
type: array
|
||||||
description: 세그먼트 목록
|
description: 세그먼트 목록
|
||||||
@ -1082,14 +701,6 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
description: 변환된 텍스트
|
description: 변환된 텍스트
|
||||||
example: "안녕하세요, 오늘 회의를 시작하겠습니다."
|
example: "안녕하세요, 오늘 회의를 시작하겠습니다."
|
||||||
speakerId:
|
|
||||||
type: string
|
|
||||||
description: 화자 ID
|
|
||||||
example: "SPK-001"
|
|
||||||
speakerName:
|
|
||||||
type: string
|
|
||||||
description: 화자 이름
|
|
||||||
example: "김철수"
|
|
||||||
timestamp:
|
timestamp:
|
||||||
type: integer
|
type: integer
|
||||||
description: 타임스탬프 (ms)
|
description: 타임스탬프 (ms)
|
||||||
@ -1105,151 +716,6 @@ components:
|
|||||||
description: 신뢰도 점수
|
description: 신뢰도 점수
|
||||||
example: 0.92
|
example: 0.92
|
||||||
|
|
||||||
IdentifySpeakerRequest:
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- recordingId
|
|
||||||
- audioFrame
|
|
||||||
- timestamp
|
|
||||||
properties:
|
|
||||||
recordingId:
|
|
||||||
type: string
|
|
||||||
description: 녹음 ID
|
|
||||||
example: "REC-20250123-001"
|
|
||||||
audioFrame:
|
|
||||||
type: string
|
|
||||||
format: byte
|
|
||||||
description: Base64 인코딩된 오디오 프레임
|
|
||||||
example: "UklGRiQAAABXQVZFZm10IBAAAAABA..."
|
|
||||||
timestamp:
|
|
||||||
type: integer
|
|
||||||
description: 타임스탬프 (ms)
|
|
||||||
example: 1234567890
|
|
||||||
|
|
||||||
SpeakerIdentificationResponse:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
speakerId:
|
|
||||||
type: string
|
|
||||||
description: 화자 ID
|
|
||||||
example: "SPK-001"
|
|
||||||
speakerName:
|
|
||||||
type: string
|
|
||||||
description: 화자 이름
|
|
||||||
example: "김철수"
|
|
||||||
confidence:
|
|
||||||
type: number
|
|
||||||
format: float
|
|
||||||
description: 식별 신뢰도 (0-1)
|
|
||||||
minimum: 0
|
|
||||||
maximum: 1
|
|
||||||
example: 0.95
|
|
||||||
isNewSpeaker:
|
|
||||||
type: boolean
|
|
||||||
description: 신규 화자 여부
|
|
||||||
example: false
|
|
||||||
profileId:
|
|
||||||
type: string
|
|
||||||
description: Azure Speaker Profile ID
|
|
||||||
example: "PROFILE-12345"
|
|
||||||
|
|
||||||
SpeakerDetailResponse:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
speakerId:
|
|
||||||
type: string
|
|
||||||
description: 화자 ID
|
|
||||||
example: "SPK-001"
|
|
||||||
speakerName:
|
|
||||||
type: string
|
|
||||||
description: 화자 이름
|
|
||||||
example: "김철수"
|
|
||||||
profileId:
|
|
||||||
type: string
|
|
||||||
description: Azure Speaker Profile ID
|
|
||||||
example: "PROFILE-12345"
|
|
||||||
userId:
|
|
||||||
type: string
|
|
||||||
description: 연결된 사용자 ID
|
|
||||||
example: "USER-123"
|
|
||||||
totalSegments:
|
|
||||||
type: integer
|
|
||||||
description: 총 발언 세그먼트 수
|
|
||||||
example: 45
|
|
||||||
totalDuration:
|
|
||||||
type: integer
|
|
||||||
description: 총 발언 시간 (초)
|
|
||||||
example: 450
|
|
||||||
averageConfidence:
|
|
||||||
type: number
|
|
||||||
format: float
|
|
||||||
description: 평균 식별 신뢰도
|
|
||||||
example: 0.92
|
|
||||||
firstAppeared:
|
|
||||||
type: string
|
|
||||||
format: date-time
|
|
||||||
description: 최초 등장 시간
|
|
||||||
example: "2025-01-23T10:30:15Z"
|
|
||||||
lastAppeared:
|
|
||||||
type: string
|
|
||||||
format: date-time
|
|
||||||
description: 최근 등장 시간
|
|
||||||
example: "2025-01-23T11:00:00Z"
|
|
||||||
|
|
||||||
UpdateSpeakerRequest:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
speakerName:
|
|
||||||
type: string
|
|
||||||
description: 화자 이름
|
|
||||||
example: "김철수 팀장"
|
|
||||||
userId:
|
|
||||||
type: string
|
|
||||||
description: 연결할 사용자 ID
|
|
||||||
example: "USER-123"
|
|
||||||
|
|
||||||
SpeakerListResponse:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
recordingId:
|
|
||||||
type: string
|
|
||||||
description: 녹음 ID
|
|
||||||
example: "REC-20250123-001"
|
|
||||||
speakerCount:
|
|
||||||
type: integer
|
|
||||||
description: 화자 수
|
|
||||||
example: 3
|
|
||||||
speakers:
|
|
||||||
type: array
|
|
||||||
description: 화자 목록
|
|
||||||
items:
|
|
||||||
$ref: '#/components/schemas/SpeakerSummary'
|
|
||||||
|
|
||||||
SpeakerSummary:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
speakerId:
|
|
||||||
type: string
|
|
||||||
description: 화자 ID
|
|
||||||
example: "SPK-001"
|
|
||||||
speakerName:
|
|
||||||
type: string
|
|
||||||
description: 화자 이름
|
|
||||||
example: "김철수"
|
|
||||||
segmentCount:
|
|
||||||
type: integer
|
|
||||||
description: 발언 세그먼트 수
|
|
||||||
example: 45
|
|
||||||
totalDuration:
|
|
||||||
type: integer
|
|
||||||
description: 총 발언 시간 (초)
|
|
||||||
example: 450
|
|
||||||
speakingRatio:
|
|
||||||
type: number
|
|
||||||
format: float
|
|
||||||
description: 발언 비율 (0-1)
|
|
||||||
example: 0.45
|
|
||||||
|
|
||||||
ErrorResponse:
|
ErrorResponse:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
|||||||
@ -1,14 +1,13 @@
|
|||||||
@startuml
|
@startuml
|
||||||
!theme mono
|
!theme mono
|
||||||
|
|
||||||
title STT Service - 음성 녹음 시작 및 화자 인식 (통합)
|
title STT Service - 음성 녹음 시작 및 실시간 인식
|
||||||
|
|
||||||
participant "Frontend<<E>>" as Frontend
|
participant "Frontend<<E>>" as Frontend
|
||||||
participant "API Gateway<<E>>" as Gateway
|
participant "API Gateway<<E>>" as Gateway
|
||||||
participant "RecordingController" as Controller
|
participant "RecordingController" as Controller
|
||||||
participant "RecordingService" as Service
|
participant "RecordingService" as Service
|
||||||
participant "AudioStreamManager" as StreamManager
|
participant "AudioStreamManager" as StreamManager
|
||||||
participant "SpeakerIdentifier" as Speaker
|
|
||||||
participant "RecordingRepository" as Repository
|
participant "RecordingRepository" as Repository
|
||||||
participant "AzureSpeechClient" as AzureClient
|
participant "AzureSpeechClient" as AzureClient
|
||||||
database "STT DB" as DB
|
database "STT DB" as DB
|
||||||
@ -51,7 +50,6 @@ note right
|
|||||||
- 언어: ko-KR
|
- 언어: ko-KR
|
||||||
- Format: PCM 16kHz
|
- Format: PCM 16kHz
|
||||||
- 샘플레이트: 16kHz
|
- 샘플레이트: 16kHz
|
||||||
- 화자 식별 활성화
|
|
||||||
- 실시간 스트리밍 모드
|
- 실시간 스트리밍 모드
|
||||||
- Continuous recognition
|
- Continuous recognition
|
||||||
end note
|
end note
|
||||||
@ -108,32 +106,12 @@ deactivate AzureClient
|
|||||||
StreamManager --> Service: recognized text
|
StreamManager --> Service: recognized text
|
||||||
deactivate StreamManager
|
deactivate StreamManager
|
||||||
|
|
||||||
== 화자 식별 ==
|
== 세그먼트 저장 ==
|
||||||
|
|
||||||
Service -> Speaker: identifySpeaker(audioFrame)
|
|
||||||
activate Speaker
|
|
||||||
|
|
||||||
Speaker -> AzureClient: analyzeSpeakerProfile()\n(Speaker Recognition API)
|
|
||||||
activate AzureClient
|
|
||||||
note right
|
|
||||||
화자 식별:
|
|
||||||
- Voice signature 생성
|
|
||||||
- 기존 프로필과 매칭
|
|
||||||
- 신규 화자 자동 등록
|
|
||||||
end note
|
|
||||||
|
|
||||||
AzureClient --> Speaker: speakerId
|
|
||||||
deactivate AzureClient
|
|
||||||
|
|
||||||
Speaker --> Service: speaker info
|
|
||||||
deactivate Speaker
|
|
||||||
|
|
||||||
== 화자별 세그먼트 저장 ==
|
|
||||||
|
|
||||||
Service -> Repository: saveSttSegment(segment)
|
Service -> Repository: saveSttSegment(segment)
|
||||||
activate Repository
|
activate Repository
|
||||||
|
|
||||||
Repository -> DB: STT 세그먼트 저장\n(세션ID, 텍스트, 화자ID, 타임스탬프, 신뢰도)
|
Repository -> DB: STT 세그먼트 저장\n(세션ID, 텍스트, 타임스탬프, 신뢰도)
|
||||||
activate DB
|
activate DB
|
||||||
DB --> Repository: segment saved
|
DB --> Repository: segment saved
|
||||||
deactivate DB
|
deactivate DB
|
||||||
@ -141,24 +119,13 @@ deactivate DB
|
|||||||
Repository --> Service: saved
|
Repository --> Service: saved
|
||||||
deactivate Repository
|
deactivate Repository
|
||||||
|
|
||||||
Service -> Repository: updateSpeakerInfo(recordingId, speakerId)
|
Service --> Controller: streaming response\n{text, timestamp, confidence}
|
||||||
activate Repository
|
|
||||||
|
|
||||||
Repository -> DB: 화자 정보 저장/업데이트\n(녹음ID, 화자ID, 세그먼트수)
|
|
||||||
activate DB
|
|
||||||
DB --> Repository: 업데이트 완료
|
|
||||||
deactivate DB
|
|
||||||
|
|
||||||
Repository --> Service: 완료
|
|
||||||
deactivate Repository
|
|
||||||
|
|
||||||
Service --> Controller: streaming response\n{text, speaker, timestamp, confidence}
|
|
||||||
deactivate Service
|
deactivate Service
|
||||||
|
|
||||||
Controller --> Gateway: WebSocket message
|
Controller --> Gateway: WebSocket message
|
||||||
deactivate Controller
|
deactivate Controller
|
||||||
|
|
||||||
Gateway --> Frontend: 실시간 자막 전송\n{text, speaker, timestamp}
|
Gateway --> Frontend: 실시간 자막 전송\n{text, timestamp}
|
||||||
deactivate Gateway
|
deactivate Gateway
|
||||||
|
|
||||||
note over Frontend, EventHub
|
note over Frontend, EventHub
|
||||||
@ -166,12 +133,10 @@ note over Frontend, EventHub
|
|||||||
- DB 녹음 생성: ~100ms
|
- DB 녹음 생성: ~100ms
|
||||||
- Azure 인식기 초기화: ~500ms
|
- Azure 인식기 초기화: ~500ms
|
||||||
- Blob 경로 생성: ~200ms
|
- Blob 경로 생성: ~200ms
|
||||||
- 화자 식별: ~300ms
|
|
||||||
- 실시간 인식 지연: < 1초
|
- 실시간 인식 지연: < 1초
|
||||||
- 총 초기화 시간: ~1.1초
|
- 총 초기화 시간: ~0.8초
|
||||||
|
|
||||||
정확도:
|
정확도:
|
||||||
- 화자 식별 정확도: > 90%
|
|
||||||
- 음성 인식 정확도: 60-95%
|
- 음성 인식 정확도: 60-95%
|
||||||
end note
|
end note
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
@startuml
|
@startuml
|
||||||
!theme mono
|
!theme mono
|
||||||
|
|
||||||
title STT Service - 음성-텍스트 변환 (실시간/배치 통합)
|
title STT Service - 음성-텍스트 변환 (실시간 전용)
|
||||||
|
|
||||||
participant "Frontend<<E>>" as Frontend
|
participant "Frontend<<E>>" as Frontend
|
||||||
participant "API Gateway<<E>>" as Gateway
|
participant "API Gateway<<E>>" as Gateway
|
||||||
@ -15,7 +15,7 @@ database "STT DB" as DB
|
|||||||
database "Azure Blob Storage<<E>>" as BlobStorage
|
database "Azure Blob Storage<<E>>" as BlobStorage
|
||||||
queue "Azure Event Hubs<<E>>" as EventHub
|
queue "Azure Event Hubs<<E>>" as EventHub
|
||||||
|
|
||||||
== 음성 데이터 스트리밍 수신 (실시간 모드) ==
|
== 음성 데이터 스트리밍 수신 ==
|
||||||
|
|
||||||
Frontend -> Gateway: POST /api/transcripts/stream\n(audioData, recordingId, timestamp)
|
Frontend -> Gateway: POST /api/transcripts/stream\n(audioData, recordingId, timestamp)
|
||||||
activate Gateway
|
activate Gateway
|
||||||
@ -26,7 +26,6 @@ activate Controller
|
|||||||
Controller -> Service: processAudioStream(audioData, recordingId)
|
Controller -> Service: processAudioStream(audioData, recordingId)
|
||||||
activate Service
|
activate Service
|
||||||
|
|
||||||
alt 실시간 변환 모드
|
|
||||||
Service -> Engine: streamingTranscribe(audioData)
|
Service -> Engine: streamingTranscribe(audioData)
|
||||||
activate Engine
|
activate Engine
|
||||||
|
|
||||||
@ -38,7 +37,6 @@ alt 실시간 변환 모드
|
|||||||
Azure Speech 설정:
|
Azure Speech 설정:
|
||||||
- Mode: Continuous
|
- Mode: Continuous
|
||||||
- 언어: ko-KR
|
- 언어: ko-KR
|
||||||
- 화자 식별 활성화
|
|
||||||
- 타임스탬프 자동 기록
|
- 타임스탬프 자동 기록
|
||||||
- 신뢰도 점수 계산
|
- 신뢰도 점수 계산
|
||||||
- Profanity filter
|
- Profanity filter
|
||||||
@ -49,7 +47,7 @@ alt 실시간 변환 모드
|
|||||||
BlobStorage --> AzureClient: 저장 완료
|
BlobStorage --> AzureClient: 저장 완료
|
||||||
deactivate BlobStorage
|
deactivate BlobStorage
|
||||||
|
|
||||||
AzureClient --> Engine: RecognitionResult\n(text, speakerId, confidence, timestamp, duration)
|
AzureClient --> Engine: RecognitionResult\n(text, confidence, timestamp, duration)
|
||||||
deactivate AzureClient
|
deactivate AzureClient
|
||||||
|
|
||||||
== 정확도 검증 및 처리 ==
|
== 정확도 검증 및 처리 ==
|
||||||
@ -71,7 +69,7 @@ alt 실시간 변환 모드
|
|||||||
Service -> TranscriptRepo: createTranscript(recordingId, segment)
|
Service -> TranscriptRepo: createTranscript(recordingId, segment)
|
||||||
activate TranscriptRepo
|
activate TranscriptRepo
|
||||||
|
|
||||||
TranscriptRepo -> DB: 변환 결과 저장\n(텍스트ID, 녹음ID, 화자ID, 텍스트, 신뢰도, 타임스탬프, 경고플래그)
|
TranscriptRepo -> DB: 변환 결과 저장\n(텍스트ID, 녹음ID, 텍스트, 신뢰도, 타임스탬프, 경고플래그)
|
||||||
activate DB
|
activate DB
|
||||||
DB --> TranscriptRepo: transcriptId 반환
|
DB --> TranscriptRepo: transcriptId 반환
|
||||||
deactivate DB
|
deactivate DB
|
||||||
@ -79,19 +77,6 @@ alt 실시간 변환 모드
|
|||||||
TranscriptRepo --> Service: TranscriptEntity 반환
|
TranscriptRepo --> Service: TranscriptEntity 반환
|
||||||
deactivate TranscriptRepo
|
deactivate TranscriptRepo
|
||||||
|
|
||||||
== 화자 정보 업데이트 ==
|
|
||||||
|
|
||||||
Service -> RecordingRepo: updateSpeakerInfo(recordingId, speakerId)
|
|
||||||
activate RecordingRepo
|
|
||||||
|
|
||||||
RecordingRepo -> DB: 화자 정보 저장/업데이트\n(녹음ID, 화자ID, 세그먼트수)
|
|
||||||
activate DB
|
|
||||||
DB --> RecordingRepo: 업데이트 완료
|
|
||||||
deactivate DB
|
|
||||||
|
|
||||||
RecordingRepo --> Service: 완료
|
|
||||||
deactivate RecordingRepo
|
|
||||||
|
|
||||||
== 이벤트 발행 ==
|
== 이벤트 발행 ==
|
||||||
|
|
||||||
Service -> EventHub: TranscriptSegmentReady 이벤트 발행
|
Service -> EventHub: TranscriptSegmentReady 이벤트 발행
|
||||||
@ -102,7 +87,6 @@ alt 실시간 변환 모드
|
|||||||
- recordingId
|
- recordingId
|
||||||
- meetingId
|
- meetingId
|
||||||
- text
|
- text
|
||||||
- speakerId
|
|
||||||
- timestamp
|
- timestamp
|
||||||
- confidence
|
- confidence
|
||||||
end note
|
end note
|
||||||
@ -112,128 +96,18 @@ alt 실시간 변환 모드
|
|||||||
Service --> Controller: TranscriptResponse\n(transcriptId, text, confidence, warningFlag)
|
Service --> Controller: TranscriptResponse\n(transcriptId, text, confidence, warningFlag)
|
||||||
deactivate Service
|
deactivate Service
|
||||||
|
|
||||||
Controller --> Gateway: 200 OK\n(transcriptId, text, speakerId, timestamp, confidence)
|
Controller --> Gateway: 200 OK\n(transcriptId, text, timestamp, confidence)
|
||||||
deactivate Controller
|
deactivate Controller
|
||||||
|
|
||||||
Gateway --> Frontend: 실시간 자막 응답
|
Gateway --> Frontend: 실시간 자막 응답
|
||||||
deactivate Gateway
|
deactivate Gateway
|
||||||
|
|
||||||
else 배치 변환 모드
|
|
||||||
Gateway -> Controller: POST /api/v1/stt/transcribe\n{sessionId, audioFile}
|
|
||||||
activate Controller
|
|
||||||
|
|
||||||
Controller -> Service: transcribeAudio(sessionId, audioFile)
|
|
||||||
activate Service
|
|
||||||
|
|
||||||
Service -> RecordingRepo: findSessionById(sessionId)
|
|
||||||
activate RecordingRepo
|
|
||||||
RecordingRepo -> DB: STT 세션 조회\n(세션ID 기준)
|
|
||||||
DB --> RecordingRepo: session data
|
|
||||||
RecordingRepo --> Service: RecordingEntity
|
|
||||||
deactivate RecordingRepo
|
|
||||||
|
|
||||||
Service -> Engine: batchTranscribe(audioFile)
|
|
||||||
activate Engine
|
|
||||||
|
|
||||||
Engine -> AzureClient: batchTranscriptionAsync(audioUrl)
|
|
||||||
activate AzureClient
|
|
||||||
note right
|
|
||||||
배치 처리:
|
|
||||||
- 전체 파일 업로드
|
|
||||||
- 백그라운드 처리
|
|
||||||
- Callback URL 제공
|
|
||||||
- 화자별 그룹화
|
|
||||||
- 문장 경계 보정
|
|
||||||
end note
|
|
||||||
|
|
||||||
AzureClient --> Engine: transcription job ID
|
|
||||||
deactivate AzureClient
|
|
||||||
|
|
||||||
Engine --> Service: job submitted
|
|
||||||
deactivate Engine
|
|
||||||
|
|
||||||
Service -> RecordingRepo: updateSessionStatus(sessionId, "PROCESSING")
|
|
||||||
activate RecordingRepo
|
|
||||||
RecordingRepo -> DB: 세션 상태 업데이트\n(상태='처리중')
|
|
||||||
DB --> RecordingRepo: updated
|
|
||||||
RecordingRepo --> Service: updated
|
|
||||||
deactivate RecordingRepo
|
|
||||||
|
|
||||||
Service --> Controller: 202 Accepted\n{jobId, status}
|
|
||||||
deactivate Service
|
|
||||||
|
|
||||||
Controller --> Gateway: 202 Accepted
|
|
||||||
deactivate Controller
|
|
||||||
|
|
||||||
== 배치 처리 완료 (Callback) ==
|
|
||||||
|
|
||||||
AzureClient -> Controller: POST /api/v1/stt/callback\n{jobId, segments}
|
|
||||||
activate Controller
|
|
||||||
|
|
||||||
Controller -> Service: processBatchResult(jobId, segments)
|
|
||||||
activate Service
|
|
||||||
|
|
||||||
loop 각 세그먼트 처리
|
|
||||||
Service -> TranscriptRepo: createTranscript(recordingId, segment)
|
|
||||||
activate TranscriptRepo
|
|
||||||
TranscriptRepo -> DB: 변환 결과 저장
|
|
||||||
DB --> TranscriptRepo: saved
|
|
||||||
TranscriptRepo --> Service: saved
|
|
||||||
deactivate TranscriptRepo
|
|
||||||
end
|
|
||||||
|
|
||||||
== 전체 텍스트 통합 ==
|
|
||||||
|
|
||||||
Service -> TranscriptRepo: aggregateTranscription(sessionId)
|
|
||||||
activate TranscriptRepo
|
|
||||||
TranscriptRepo -> DB: 세그먼트 목록 조회\n(세션ID 기준, 타임스탬프 순 정렬)
|
|
||||||
DB --> TranscriptRepo: ordered segments
|
|
||||||
TranscriptRepo --> Service: segments
|
|
||||||
deactivate TranscriptRepo
|
|
||||||
|
|
||||||
Service -> Service: mergeSegments(segments)
|
|
||||||
note right
|
|
||||||
세그먼트 병합:
|
|
||||||
- 화자별 그룹화
|
|
||||||
- 시간 순서 정렬
|
|
||||||
- 문장 경계 보정
|
|
||||||
end note
|
|
||||||
|
|
||||||
Service -> RecordingRepo: saveTranscription(fullText)
|
|
||||||
activate RecordingRepo
|
|
||||||
RecordingRepo -> DB: 전체 텍스트 저장 및 상태 업데이트\n(전체텍스트, 상태='완료')
|
|
||||||
DB --> RecordingRepo: saved
|
|
||||||
RecordingRepo --> Service: updated session
|
|
||||||
deactivate RecordingRepo
|
|
||||||
|
|
||||||
Service -> EventHub: TranscriptionCompletedEvent 발행
|
|
||||||
note right
|
|
||||||
Event:
|
|
||||||
- sessionId
|
|
||||||
- meetingId
|
|
||||||
- fullText
|
|
||||||
- completedAt
|
|
||||||
end note
|
|
||||||
|
|
||||||
Service --> Controller: TranscriptionResponse\n{sessionId, text, segments}
|
|
||||||
deactivate Service
|
|
||||||
|
|
||||||
Controller --> Gateway: 200 OK\n{transcription, metadata}
|
|
||||||
deactivate Controller
|
|
||||||
end
|
|
||||||
|
|
||||||
note over Frontend, EventHub
|
note over Frontend, EventHub
|
||||||
**실시간 모드 처리 시간:**
|
**처리 시간:**
|
||||||
- Azure STT 처리: 1-3초
|
- Azure STT 처리: 1-3초
|
||||||
- DB 저장: ~100ms
|
- DB 저장: ~100ms
|
||||||
- Event 발행: ~50ms
|
- Event 발행: ~50ms
|
||||||
- 총 처리 시간: 1-4초
|
- 총 처리 시간: 1-3초
|
||||||
|
|
||||||
**배치 모드 처리 시간:**
|
|
||||||
- 파일 업로드: ~1-2초
|
|
||||||
- Azure 배치 처리: 5-30초 (파일 크기에 따라)
|
|
||||||
- DB 저장: ~500ms
|
|
||||||
- 총 처리 시간: 7-33초
|
|
||||||
|
|
||||||
**정확도 경고 기준:**
|
**정확도 경고 기준:**
|
||||||
- < 60%: 수동 수정 권장 (경고 플래그)
|
- < 60%: 수동 수정 권장 (경고 플래그)
|
||||||
|
|||||||
@ -1,527 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="ko">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>검증 완료 - 회의록 서비스</title>
|
|
||||||
<link rel="stylesheet" href="common.css">
|
|
||||||
<style>
|
|
||||||
/* 페이지별 커스텀 스타일만 유지 */
|
|
||||||
/* 공통 스타일(헤더, 메인콘텐츠, 액션바)은 common.css 사용 */
|
|
||||||
|
|
||||||
.progress-container {
|
|
||||||
margin-bottom: var(--space-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: var(--space-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-percentage {
|
|
||||||
font-size: var(--font-h2);
|
|
||||||
font-weight: var(--font-weight-bold);
|
|
||||||
color: var(--primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.verification-card {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-md);
|
|
||||||
padding: var(--space-md);
|
|
||||||
background: var(--white);
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
box-shadow: var(--shadow-md);
|
|
||||||
margin-bottom: var(--space-md);
|
|
||||||
transition: all var(--transition-normal);
|
|
||||||
}
|
|
||||||
|
|
||||||
.verification-card:hover {
|
|
||||||
box-shadow: var(--shadow-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.verification-card.verified {
|
|
||||||
border-left: 4px solid var(--success);
|
|
||||||
}
|
|
||||||
|
|
||||||
.verification-card.unverified {
|
|
||||||
border-left: 4px solid var(--gray-300);
|
|
||||||
}
|
|
||||||
|
|
||||||
.verification-card.locked {
|
|
||||||
background: var(--gray-100);
|
|
||||||
}
|
|
||||||
|
|
||||||
.verify-icon {
|
|
||||||
font-size: 32px;
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.verify-icon.verified {
|
|
||||||
color: var(--success);
|
|
||||||
}
|
|
||||||
|
|
||||||
.verify-icon.unverified {
|
|
||||||
color: var(--gray-300);
|
|
||||||
}
|
|
||||||
|
|
||||||
.lock-icon {
|
|
||||||
font-size: 20px;
|
|
||||||
color: var(--gray-500);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 편집 모드 스타일 */
|
|
||||||
.edit-field {
|
|
||||||
width: 100%;
|
|
||||||
padding: var(--space-sm);
|
|
||||||
border: 1px solid var(--gray-300);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
font-size: var(--font-small);
|
|
||||||
line-height: 1.6;
|
|
||||||
margin-bottom: var(--space-sm);
|
|
||||||
transition: border-color var(--transition-normal);
|
|
||||||
}
|
|
||||||
|
|
||||||
.edit-field:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: var(--primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.edit-label {
|
|
||||||
display: block;
|
|
||||||
font-weight: var(--font-weight-medium);
|
|
||||||
margin-bottom: var(--space-xs);
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="page">
|
|
||||||
<!-- Header -->
|
|
||||||
<header class="header">
|
|
||||||
<div class="header-left">
|
|
||||||
<button class="back-btn" onclick="history.back()">←</button>
|
|
||||||
<h1 class="header-title">검증 완료</h1>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<!-- Main Content -->
|
|
||||||
<main class="main-content has-action-bar">
|
|
||||||
<!-- Progress Bar -->
|
|
||||||
<div class="progress-container">
|
|
||||||
<div class="progress-header">
|
|
||||||
<h2 class="text-small font-bold">전체 진행률</h2>
|
|
||||||
<span class="progress-percentage" id="progressText">50%</span>
|
|
||||||
</div>
|
|
||||||
<div class="progress" style="height: 12px;">
|
|
||||||
<div class="progress-bar progress-bar-success" id="progressBar" style="width: 50%;"></div>
|
|
||||||
</div>
|
|
||||||
<p class="text-small text-muted mt-sm">4개 섹션 중 2개 검증 완료</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Meeting Info -->
|
|
||||||
<div class="card mb-lg">
|
|
||||||
<div class="card-header">
|
|
||||||
<h3 class="card-title">2025년 1분기 제품 기획 회의</h3>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="text-small text-muted">
|
|
||||||
<div style="display: flex; gap: var(--space-md); margin-bottom: var(--space-xs);">
|
|
||||||
<span>📅 2025-10-25 14:00</span>
|
|
||||||
<span>⏱️ 90분</span>
|
|
||||||
</div>
|
|
||||||
<div>👥 김민준, 박서연, 이준호, 최유진</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Section List -->
|
|
||||||
<div>
|
|
||||||
<h2 class="text-small font-bold mb-md">섹션별 검증 상태</h2>
|
|
||||||
|
|
||||||
<!-- 섹션 1 - 검증 완료 -->
|
|
||||||
<div class="verification-card verified">
|
|
||||||
<div class="verify-icon verified">✓</div>
|
|
||||||
<div style="flex: 1;">
|
|
||||||
<h3 class="text-small font-bold" style="margin: 0 0 4px 0;">회의 개요</h3>
|
|
||||||
<div style="display: flex; align-items: center; gap: var(--space-sm);">
|
|
||||||
<div class="avatar-group">
|
|
||||||
<div class="avatar avatar-green avatar-sm">김</div>
|
|
||||||
<div class="avatar avatar-blue avatar-sm">박</div>
|
|
||||||
</div>
|
|
||||||
<span class="text-caption text-muted">2명 검증 완료</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-secondary btn-sm" onclick="viewSection(0)">보기</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 섹션 2 - 검증 완료 + 잠금 -->
|
|
||||||
<div class="verification-card verified locked">
|
|
||||||
<div class="verify-icon verified">✓</div>
|
|
||||||
<div style="flex: 1;">
|
|
||||||
<h3 class="text-small font-bold" style="margin: 0 0 4px 0;">
|
|
||||||
논의 사항
|
|
||||||
<span class="lock-icon">🔒</span>
|
|
||||||
</h3>
|
|
||||||
<div style="display: flex; align-items: center; gap: var(--space-sm);">
|
|
||||||
<div class="avatar-group">
|
|
||||||
<div class="avatar avatar-green avatar-sm">김</div>
|
|
||||||
<div class="avatar avatar-blue avatar-sm">박</div>
|
|
||||||
<div class="avatar avatar-yellow avatar-sm">이</div>
|
|
||||||
</div>
|
|
||||||
<span class="text-caption text-muted">3명 검증 완료 · 잠금됨</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-secondary btn-sm" onclick="unlockSection(1)">잠금해제</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 섹션 3 - 미검증 -->
|
|
||||||
<div class="verification-card unverified">
|
|
||||||
<div class="verify-icon unverified">○</div>
|
|
||||||
<div style="flex: 1;">
|
|
||||||
<h3 class="text-small font-bold" style="margin: 0 0 4px 0;">결정 사항</h3>
|
|
||||||
<div style="display: flex; align-items: center; gap: var(--space-sm);">
|
|
||||||
<div class="avatar-group">
|
|
||||||
<div class="avatar avatar-blue avatar-sm">박</div>
|
|
||||||
</div>
|
|
||||||
<span class="text-caption text-muted">1명 검증 완료</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-primary btn-sm" onclick="verifySection(2)">검증하기</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 섹션 4 - 미검증 -->
|
|
||||||
<div class="verification-card unverified">
|
|
||||||
<div class="verify-icon unverified">○</div>
|
|
||||||
<div style="flex: 1;">
|
|
||||||
<h3 class="text-small font-bold" style="margin: 0 0 4px 0;">액션 아이템</h3>
|
|
||||||
<div style="display: flex; align-items: center; gap: var(--space-sm);">
|
|
||||||
<span class="text-caption text-muted">아직 검증되지 않음</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-primary btn-sm" onclick="verifySection(3)">검증하기</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<!-- 하단 액션 바 -->
|
|
||||||
<div class="action-bar">
|
|
||||||
<button class="btn btn-secondary" onclick="saveLater()">나중에 하기</button>
|
|
||||||
<button class="btn btn-primary" id="completeBtn" disabled onclick="completeAllVerification()">
|
|
||||||
모두 검증 완료
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Section View Modal -->
|
|
||||||
<div id="sectionModal" class="modal-overlay">
|
|
||||||
<div class="modal" style="max-height: 80vh; overflow-y: auto;">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h2 class="modal-title" id="sectionTitle">회의 개요</h2>
|
|
||||||
<button class="modal-close" onclick="closeModal('sectionModal')">✕</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div id="sectionContent">
|
|
||||||
<!-- 섹션 내용이 여기에 표시됨 -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button class="btn btn-secondary btn-sm" onclick="closeModal('sectionModal')">닫기</button>
|
|
||||||
<button class="btn btn-primary btn-sm" id="editBtn" onclick="toggleEditMode()">편집</button>
|
|
||||||
<button class="btn btn-primary btn-sm" id="saveBtn" style="display: none;" onclick="saveSection()">저장</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Verification Confirm Modal -->
|
|
||||||
<div id="verifyModal" class="modal-overlay">
|
|
||||||
<div class="modal">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h2 class="modal-title">섹션 검증</h2>
|
|
||||||
<button class="modal-close" onclick="closeModal('verifyModal')">✕</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<p class="text-small mb-md" id="verifyMessage">이 섹션의 내용을 검증하시겠습니까?</p>
|
|
||||||
<div class="card" style="background: var(--primary-light);">
|
|
||||||
<p class="text-small font-medium">검증 후에는 다른 참석자들도 이 섹션이 확인되었음을 알 수 있습니다.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button class="btn btn-secondary btn-sm" onclick="closeModal('verifyModal')">취소</button>
|
|
||||||
<button class="btn btn-primary btn-sm" onclick="confirmVerification()">검증 완료</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Unlock Confirm Modal -->
|
|
||||||
<div id="unlockModal" class="modal-overlay">
|
|
||||||
<div class="modal">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h2 class="modal-title">섹션 잠금 해제</h2>
|
|
||||||
<button class="modal-close" onclick="closeModal('unlockModal')">✕</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<p class="text-small mb-md">이 섹션의 잠금을 해제하시겠습니까?</p>
|
|
||||||
<div class="card" style="background: transparent; color: var(--error); border: 1px solid var(--error);">
|
|
||||||
<p class="text-small font-medium">⚠️ 잠금 해제 시 다른 참석자들이 내용을 수정할 수 있습니다.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button class="btn btn-sm" style="background: transparent; color: var(--error); border: 1px solid var(--error);" onclick="closeModal('unlockModal')">취소</button>
|
|
||||||
<button class="btn btn-error btn-sm" onclick="confirmUnlock()">잠금 해제</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="common.js"></script>
|
|
||||||
<script>
|
|
||||||
// 섹션 검증 상태
|
|
||||||
const sectionVerifications = [
|
|
||||||
{ name: '회의 개요', verified: true, locked: false, verifiers: ['김민준', '박서연'] },
|
|
||||||
{ name: '논의 사항', verified: true, locked: true, verifiers: ['김민준', '박서연', '이준호'] },
|
|
||||||
{ name: '결정 사항', verified: false, locked: false, verifiers: ['박서연'] },
|
|
||||||
{ name: '액션 아이템', verified: false, locked: false, verifiers: [] }
|
|
||||||
];
|
|
||||||
|
|
||||||
let currentSectionIndex = -1;
|
|
||||||
let isEditMode = false;
|
|
||||||
let originalContent = '';
|
|
||||||
|
|
||||||
// 섹션 데이터 (편집 가능한 필드)
|
|
||||||
const sectionData = [
|
|
||||||
{
|
|
||||||
purpose: '2025년 1분기 신제품 개발 방향 수립',
|
|
||||||
attendees: '김민준(PM), 박서연(AI), 이준호(Backend), 최유진(Frontend)',
|
|
||||||
datetime: '2025년 10월 25일 14:00 - 15:30',
|
|
||||||
location: '본사 2층 대회의실'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
topic1: 'AI 모델 정확도',
|
|
||||||
detail1: '현재 STT 정확도: 92%, 목표 정확도: 95% 이상',
|
|
||||||
topic2: '사용자 인터페이스',
|
|
||||||
detail2: 'Mobile First 디자인 채택, 실시간 협업 기능 필수'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// 진행률 업데이트
|
|
||||||
function updateProgress() {
|
|
||||||
const totalSections = sectionVerifications.length;
|
|
||||||
const verifiedCount = sectionVerifications.filter(s => s.verified).length;
|
|
||||||
const percentage = Math.round((verifiedCount / totalSections) * 100);
|
|
||||||
|
|
||||||
$('#progressBar').style.width = percentage + '%';
|
|
||||||
$('#progressText').textContent = percentage + '%';
|
|
||||||
|
|
||||||
const completeBtn = $('#completeBtn');
|
|
||||||
if (percentage === 100) {
|
|
||||||
completeBtn.disabled = false;
|
|
||||||
completeBtn.style.opacity = '1';
|
|
||||||
} else {
|
|
||||||
completeBtn.disabled = true;
|
|
||||||
completeBtn.style.opacity = '0.5';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 섹션 보기
|
|
||||||
function viewSection(index) {
|
|
||||||
currentSectionIndex = index;
|
|
||||||
isEditMode = false;
|
|
||||||
const section = sectionVerifications[index];
|
|
||||||
|
|
||||||
$('#sectionTitle').textContent = section.name;
|
|
||||||
|
|
||||||
// 편집 모드 초기화
|
|
||||||
$('#editBtn').style.display = 'inline-block';
|
|
||||||
$('#saveBtn').style.display = 'none';
|
|
||||||
|
|
||||||
// 회의 개요만 편집 가능
|
|
||||||
if (index === 0) {
|
|
||||||
renderSectionContent(index, false);
|
|
||||||
} else {
|
|
||||||
// 다른 섹션은 읽기 전용
|
|
||||||
const sampleContent = `
|
|
||||||
<p><strong>1. AI 모델 정확도</strong></p>
|
|
||||||
<p>- 현재 STT 정확도: 92%</p>
|
|
||||||
<p>- 목표 정확도: 95% 이상</p>
|
|
||||||
<br>
|
|
||||||
<p><strong>2. 사용자 인터페이스</strong></p>
|
|
||||||
<p>- Mobile First 디자인 채택</p>
|
|
||||||
<p>- 실시간 협업 기능 필수</p>
|
|
||||||
`;
|
|
||||||
$('#sectionContent').innerHTML = sampleContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
openModal('sectionModal');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 섹션 내용 렌더링
|
|
||||||
function renderSectionContent(index, editMode) {
|
|
||||||
const data = sectionData[index];
|
|
||||||
const contentDiv = $('#sectionContent');
|
|
||||||
|
|
||||||
if (editMode && index === 0) {
|
|
||||||
// 회의 개요 편집 모드
|
|
||||||
contentDiv.innerHTML = `
|
|
||||||
<div>
|
|
||||||
<label class="edit-label">회의 목적</label>
|
|
||||||
<input type="text" class="edit-field" id="edit_purpose" value="${data.purpose}">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="edit-label">참석자</label>
|
|
||||||
<input type="text" class="edit-field" id="edit_attendees" value="${data.attendees}">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="edit-label">일시</label>
|
|
||||||
<input type="text" class="edit-field" id="edit_datetime" value="${data.datetime}">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="edit-label">장소</label>
|
|
||||||
<input type="text" class="edit-field" id="edit_location" value="${data.location}">
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
} else if (index === 0) {
|
|
||||||
// 회의 개요 보기 모드
|
|
||||||
contentDiv.innerHTML = `
|
|
||||||
<p><strong>회의 목적:</strong> ${data.purpose}</p>
|
|
||||||
<p><strong>참석자:</strong> ${data.attendees}</p>
|
|
||||||
<p><strong>일시:</strong> ${data.datetime}</p>
|
|
||||||
<p><strong>장소:</strong> ${data.location}</p>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 섹션 검증
|
|
||||||
function verifySection(index) {
|
|
||||||
currentSectionIndex = index;
|
|
||||||
const section = sectionVerifications[index];
|
|
||||||
|
|
||||||
$('#verifyMessage').textContent = `"${section.name}" 섹션의 내용을 검증하시겠습니까?`;
|
|
||||||
openModal('verifyModal');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 검증 확인
|
|
||||||
function confirmVerification() {
|
|
||||||
const section = sectionVerifications[currentSectionIndex];
|
|
||||||
section.verified = true;
|
|
||||||
|
|
||||||
if (!section.verifiers.includes('김민준')) {
|
|
||||||
section.verifiers.push('김민준');
|
|
||||||
}
|
|
||||||
|
|
||||||
closeModal('verifyModal');
|
|
||||||
showToast(`"${section.name}" 섹션이 검증되었습니다`, 'success');
|
|
||||||
|
|
||||||
// 화면 새로고침 시뮬레이션
|
|
||||||
setTimeout(() => {
|
|
||||||
location.reload();
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 섹션 잠금 해제
|
|
||||||
function unlockSection(index) {
|
|
||||||
currentSectionIndex = index;
|
|
||||||
openModal('unlockModal');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 잠금 해제 확인
|
|
||||||
function confirmUnlock() {
|
|
||||||
const section = sectionVerifications[currentSectionIndex];
|
|
||||||
section.locked = false;
|
|
||||||
|
|
||||||
closeModal('unlockModal');
|
|
||||||
showToast(`"${section.name}" 섹션의 잠금이 해제되었습니다`, 'success');
|
|
||||||
|
|
||||||
// 화면 새로고침 시뮬레이션
|
|
||||||
setTimeout(() => {
|
|
||||||
location.reload();
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 편집 모드 토글
|
|
||||||
function toggleEditMode() {
|
|
||||||
if (currentSectionIndex !== 0) {
|
|
||||||
// 회의 개요가 아닌 경우
|
|
||||||
closeModal('sectionModal');
|
|
||||||
showToast('이 섹션은 회의록수정 화면에서 수정할 수 있습니다', 'info');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isEditMode = !isEditMode;
|
|
||||||
|
|
||||||
if (isEditMode) {
|
|
||||||
// 편집 모드로 전환
|
|
||||||
renderSectionContent(currentSectionIndex, true);
|
|
||||||
$('#editBtn').style.display = 'none';
|
|
||||||
$('#saveBtn').style.display = 'inline-block';
|
|
||||||
} else {
|
|
||||||
// 보기 모드로 전환
|
|
||||||
renderSectionContent(currentSectionIndex, false);
|
|
||||||
$('#editBtn').style.display = 'inline-block';
|
|
||||||
$('#saveBtn').style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 섹션 저장
|
|
||||||
function saveSection() {
|
|
||||||
if (currentSectionIndex !== 0) return;
|
|
||||||
|
|
||||||
// 편집된 값 가져오기
|
|
||||||
const updatedData = {
|
|
||||||
purpose: $('#edit_purpose').value,
|
|
||||||
attendees: $('#edit_attendees').value,
|
|
||||||
datetime: $('#edit_datetime').value,
|
|
||||||
location: $('#edit_location').value
|
|
||||||
};
|
|
||||||
|
|
||||||
// 데이터 업데이트
|
|
||||||
sectionData[currentSectionIndex] = updatedData;
|
|
||||||
|
|
||||||
// 보기 모드로 전환
|
|
||||||
isEditMode = false;
|
|
||||||
renderSectionContent(currentSectionIndex, false);
|
|
||||||
$('#editBtn').style.display = 'inline-block';
|
|
||||||
$('#saveBtn').style.display = 'none';
|
|
||||||
|
|
||||||
// 성공 메시지
|
|
||||||
showToast('회의 개요가 저장되었습니다', 'success');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 모두 검증 완료
|
|
||||||
function completeAllVerification() {
|
|
||||||
if (confirm('모든 섹션 검증을 완료하고 회의록을 확정하시겠습니까?')) {
|
|
||||||
showToast('회의록이 최종 확정되었습니다', 'success');
|
|
||||||
|
|
||||||
// 회의 종료 화면 또는 대시보드로 이동
|
|
||||||
setTimeout(() => {
|
|
||||||
alert('회의록이 확정되었습니다.\n참석자들에게 알림이 전송되었습니다.');
|
|
||||||
// navigateTo('01-대시보드.html');
|
|
||||||
}, 1500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 나중에 하기
|
|
||||||
function saveLater() {
|
|
||||||
if (confirm('검증을 나중에 완료하시겠습니까?\n회의록은 임시 저장됩니다.')) {
|
|
||||||
// 회의록 상태를 '작성중'으로 저장
|
|
||||||
// 실제로는 Meeting Service API 호출하여 임시 저장
|
|
||||||
showToast('회의록이 임시 저장되었습니다', 'info');
|
|
||||||
|
|
||||||
// 대시보드로 이동
|
|
||||||
setTimeout(() => {
|
|
||||||
navigateTo('02-대시보드.html');
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 초기 진행률 업데이트
|
|
||||||
updateProgress();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -25,92 +25,85 @@
|
|||||||
color: var(--gray-700);
|
color: var(--gray-700);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 통계 카드 그리드 */
|
/* 통계 카드 그리드 - 10-회의록상세조회와 동일한 디자인 */
|
||||||
.stats-grid {
|
.stats-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: repeat(4, 1fr);
|
||||||
gap: var(--space-md);
|
gap: var(--space-md);
|
||||||
margin-bottom: var(--space-lg);
|
margin-bottom: var(--space-lg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-card {
|
/* 모바일에서 gap 축소 */
|
||||||
background: var(--white);
|
@media (max-width: 600px) {
|
||||||
padding: var(--space-md);
|
.stats-grid {
|
||||||
border-radius: var(--radius-lg);
|
gap: var(--space-xs);
|
||||||
box-shadow: var(--shadow-md);
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
padding: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: var(--font-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: var(--font-xs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
padding: var(--space-lg);
|
||||||
|
background: var(--gray-100);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-value {
|
.stat-value {
|
||||||
font-size: var(--font-h1);
|
font-size: var(--font-h1);
|
||||||
font-weight: var(--font-weight-bold);
|
font-weight: var(--font-weight-bold);
|
||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
margin-bottom: var(--space-xs);
|
margin-bottom: var(--space-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-label {
|
.stat-label {
|
||||||
font-size: var(--font-small);
|
font-size: var(--font-body);
|
||||||
color: var(--gray-500);
|
color: var(--gray-600);
|
||||||
}
|
|
||||||
|
|
||||||
/* 키워드 클라우드 */
|
|
||||||
.keyword-cloud {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: var(--space-sm);
|
|
||||||
padding: var(--space-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.keyword-tag {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 6px 12px;
|
|
||||||
background: var(--primary-light);
|
|
||||||
color: var(--primary-dark);
|
|
||||||
border-radius: 16px;
|
|
||||||
font-size: var(--font-small);
|
|
||||||
font-weight: var(--font-weight-medium);
|
font-weight: var(--font-weight-medium);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 발언 통계 바 차트 */
|
/* 키워드 섹션 - 10-회의록상세조회와 동일한 디자인 */
|
||||||
.speaker-stats {
|
.keywords-section {
|
||||||
padding: var(--space-md) 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.speaker-item {
|
.keywords-title {
|
||||||
display: flex;
|
font-size: var(--font-h4);
|
||||||
align-items: center;
|
font-weight: var(--font-weight-bold);
|
||||||
gap: var(--space-md);
|
color: var(--gray-900);
|
||||||
margin-bottom: var(--space-md);
|
margin-bottom: var(--space-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.speaker-info {
|
.keyword-tags {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
flex-wrap: wrap;
|
||||||
gap: var(--space-sm);
|
gap: var(--space-sm);
|
||||||
min-width: 120px;
|
margin: var(--space-md) 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.speaker-bar-container {
|
.keyword-tag {
|
||||||
flex: 1;
|
padding: 6px 12px;
|
||||||
height: 32px;
|
background: var(--primary-light);
|
||||||
background: var(--gray-100);
|
color: var(--primary);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
overflow: hidden;
|
font-size: var(--font-small);
|
||||||
position: relative;
|
cursor: pointer;
|
||||||
|
transition: all var(--transition-fast);
|
||||||
}
|
}
|
||||||
|
|
||||||
.speaker-bar {
|
.keyword-tag:hover {
|
||||||
height: 100%;
|
|
||||||
background: var(--primary);
|
background: var(--primary);
|
||||||
border-radius: 16px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-end;
|
|
||||||
padding-right: var(--space-sm);
|
|
||||||
color: var(--white);
|
color: var(--white);
|
||||||
font-size: var(--font-caption);
|
|
||||||
font-weight: var(--font-weight-bold);
|
|
||||||
transition: width 1s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 안건 카드 */
|
/* 안건 카드 */
|
||||||
@ -182,99 +175,69 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.agenda-section {
|
.agenda-section {
|
||||||
margin-bottom: var(--space-md);
|
margin-bottom: var(--space-lg);
|
||||||
|
padding-bottom: var(--space-md);
|
||||||
|
border-bottom: 1px solid var(--gray-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agenda-section:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.agenda-section-title {
|
.agenda-section-title {
|
||||||
font-size: var(--font-small);
|
font-size: var(--font-small);
|
||||||
font-weight: var(--font-weight-bold);
|
font-weight: var(--font-weight-bold);
|
||||||
color: var(--gray-500);
|
color: var(--gray-900);
|
||||||
margin-bottom: var(--space-xs);
|
margin-bottom: var(--space-sm);
|
||||||
text-transform: uppercase;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agenda-section-title::before {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
width: 4px;
|
||||||
|
height: 16px;
|
||||||
|
background: var(--primary);
|
||||||
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.agenda-section-content {
|
.agenda-section-content {
|
||||||
font-size: var(--font-body);
|
font-size: var(--font-body);
|
||||||
color: var(--gray-700);
|
color: var(--gray-700);
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
|
padding-left: var(--space-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Todo 리스트 (읽기 전용) */
|
.agenda-section-content ul {
|
||||||
.todo-list-item {
|
margin: 0;
|
||||||
padding: var(--space-md);
|
padding-left: var(--space-lg);
|
||||||
background: var(--gray-50);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
margin-bottom: var(--space-sm);
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.todo-header {
|
.agenda-section-content li {
|
||||||
display: flex;
|
margin-bottom: var(--space-xs);
|
||||||
justify-content: space-between;
|
|
||||||
align-items: flex-start;
|
|
||||||
margin-bottom: var(--space-sm);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.todo-checkbox {
|
/* 하단 버튼 비율 조정 (1:2:1) */
|
||||||
margin-right: var(--space-sm);
|
.action-bar .btn:nth-child(1) {
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.todo-content {
|
|
||||||
flex: 1;
|
|
||||||
font-weight: var(--font-weight-medium);
|
|
||||||
color: var(--gray-700);
|
|
||||||
}
|
|
||||||
|
|
||||||
.todo-meta {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-md);
|
|
||||||
font-size: var(--font-small);
|
|
||||||
color: var(--gray-500);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 하단 액션 바 - 3개 버튼 배치 */
|
|
||||||
.action-bar {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
background: var(--white);
|
|
||||||
padding: var(--space-md);
|
|
||||||
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
|
|
||||||
display: flex;
|
|
||||||
gap: var(--space-sm);
|
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-bar .btn {
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-bar .btn-primary {
|
.action-bar .btn:nth-child(2) {
|
||||||
flex: 2; /* 바로 최종 확정 버튼 강조 */
|
flex: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.action-bar .btn:nth-child(3) {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 데스크톱 반응형 */
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
.stats-grid {
|
.stats-grid {
|
||||||
grid-template-columns: repeat(4, 1fr);
|
grid-template-columns: repeat(4, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-bar {
|
|
||||||
position: static;
|
|
||||||
box-shadow: none;
|
|
||||||
justify-content: center;
|
|
||||||
gap: var(--space-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-bar .btn {
|
|
||||||
flex: 0 1 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-bar .btn-primary {
|
|
||||||
flex: 0 1 250px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.readonly-notice {
|
.readonly-notice {
|
||||||
@ -303,44 +266,42 @@
|
|||||||
🔒 이 화면은 <strong>확인 전용</strong>입니다. 내용을 수정하려면 "회의록 수정" 버튼을 클릭하세요.
|
🔒 이 화면은 <strong>확인 전용</strong>입니다. 내용을 수정하려면 "회의록 수정" 버튼을 클릭하세요.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 회의 통계 및 키워드 카드 -->
|
||||||
|
<div class="card mb-lg">
|
||||||
<!-- 통계 카드 그리드 -->
|
<!-- 통계 카드 그리드 -->
|
||||||
<div class="stats-grid">
|
<div class="stats-grid">
|
||||||
<div class="stat-card">
|
<div class="stat-item">
|
||||||
<div class="stat-value" id="durationValue">0</div>
|
<div class="stat-value" id="participantsValue">4명</div>
|
||||||
<div class="stat-label">회의 시간 (분)</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-value" id="participantsValue">0</div>
|
|
||||||
<div class="stat-label">참석자</div>
|
<div class="stat-label">참석자</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
<div class="stat-item">
|
||||||
<div class="stat-value" id="agendasValue">0</div>
|
<div class="stat-value" id="durationValue">90분</div>
|
||||||
<div class="stat-label">안건</div>
|
<div class="stat-label">회의 시간</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
<div class="stat-item">
|
||||||
<div class="stat-value" id="todosValue">0</div>
|
<div class="stat-value" id="agendasValue">3개</div>
|
||||||
<div class="stat-label">Todo</div>
|
<div class="stat-label">주요 안건</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-value" id="todosValue">5개</div>
|
||||||
|
<div class="stat-label">Todo 생성</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 주요 키워드 -->
|
<!-- 주요 키워드 -->
|
||||||
<div class="card mb-md">
|
<div class="keywords-section">
|
||||||
<h3 class="card-title">주요 키워드</h3>
|
<h3 class="keywords-title">주요 키워드</h3>
|
||||||
<div class="keyword-cloud">
|
<div class="keyword-tags">
|
||||||
<span class="keyword-tag">신제품 기획</span>
|
<span class="keyword-tag">#신제품기획</span>
|
||||||
<span class="keyword-tag">예산 편성</span>
|
<span class="keyword-tag">#예산편성</span>
|
||||||
<span class="keyword-tag">일정 조율</span>
|
<span class="keyword-tag">#일정조율</span>
|
||||||
<span class="keyword-tag">시장 조사</span>
|
<span class="keyword-tag">#시장조사</span>
|
||||||
<span class="keyword-tag">UI/UX</span>
|
<span class="keyword-tag">#UI/UX</span>
|
||||||
<span class="keyword-tag">개발 스펙</span>
|
<span class="keyword-tag">#개발스펙</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 발언 통계 -->
|
|
||||||
<div class="card mb-md">
|
|
||||||
<h3 class="card-title">발언 통계</h3>
|
|
||||||
<div class="speaker-stats" id="speakerStats"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 안건별 AI 요약 -->
|
<!-- 안건별 AI 요약 -->
|
||||||
<div class="card mb-md">
|
<div class="card mb-md">
|
||||||
@ -352,7 +313,7 @@
|
|||||||
|
|
||||||
<!-- 하단 액션 바 (3가지 선택 옵션) -->
|
<!-- 하단 액션 바 (3가지 선택 옵션) -->
|
||||||
<div class="action-bar">
|
<div class="action-bar">
|
||||||
<button class="btn btn-ghost" onclick="navigateTo('02-대시보드.html')">
|
<button class="btn btn-neutral" onclick="navigateTo('02-대시보드.html')">
|
||||||
대시보드
|
대시보드
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-secondary" onclick="navigateTo('11-회의록수정.html')">
|
<button class="btn btn-secondary" onclick="navigateTo('11-회의록수정.html')">
|
||||||
@ -459,9 +420,6 @@
|
|||||||
const totalTodos = SAMPLE_AGENDAS.reduce((sum, agenda) => sum + (agenda.todos?.length || 0), 0);
|
const totalTodos = SAMPLE_AGENDAS.reduce((sum, agenda) => sum + (agenda.todos?.length || 0), 0);
|
||||||
animateCounter('todosValue', totalTodos);
|
animateCounter('todosValue', totalTodos);
|
||||||
|
|
||||||
// 발언 통계 렌더링
|
|
||||||
renderSpeakerStats();
|
|
||||||
|
|
||||||
// 안건 리스트 렌더링
|
// 안건 리스트 렌더링
|
||||||
renderAgendaList();
|
renderAgendaList();
|
||||||
}
|
}
|
||||||
@ -482,42 +440,6 @@
|
|||||||
}, 30);
|
}, 30);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 발언 통계 렌더링
|
|
||||||
function renderSpeakerStats() {
|
|
||||||
const stats = [
|
|
||||||
{ user: SAMPLE_MEETINGS[0].participants[0], count: 15, duration: 35 },
|
|
||||||
{ user: SAMPLE_MEETINGS[0].participants[1], count: 12, duration: 28 },
|
|
||||||
{ user: SAMPLE_MEETINGS[0].participants[2], count: 10, duration: 20 },
|
|
||||||
{ user: SAMPLE_MEETINGS[0].participants[3], count: 8, duration: 17 }
|
|
||||||
];
|
|
||||||
|
|
||||||
const maxDuration = Math.max(...stats.map(s => s.duration));
|
|
||||||
const container = $('#speakerStats');
|
|
||||||
|
|
||||||
stats.forEach(stat => {
|
|
||||||
const percentage = (stat.duration / maxDuration) * 100;
|
|
||||||
const item = createElement('div', { className: 'speaker-item' }, `
|
|
||||||
<div class="speaker-info">
|
|
||||||
${createAvatar(stat.user, 'sm')}
|
|
||||||
<span class="text-small">${stat.user.name}</span>
|
|
||||||
</div>
|
|
||||||
<div class="speaker-bar-container">
|
|
||||||
<div class="speaker-bar" style="width: 0%;" data-width="${percentage}">
|
|
||||||
${stat.duration}분
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`);
|
|
||||||
container.appendChild(item);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 애니메이션 시작
|
|
||||||
setTimeout(() => {
|
|
||||||
$$('.speaker-bar').forEach(bar => {
|
|
||||||
bar.style.width = bar.dataset.width + '%';
|
|
||||||
});
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 안건 리스트 렌더링
|
// 안건 리스트 렌더링
|
||||||
function renderAgendaList() {
|
function renderAgendaList() {
|
||||||
const container = $('#agendaList');
|
const container = $('#agendaList');
|
||||||
@ -554,59 +476,85 @@
|
|||||||
`));
|
`));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 발언자별 의견
|
|
||||||
if (agenda.details.opinions && agenda.details.opinions.length > 0) {
|
|
||||||
const opinionsHtml = agenda.details.opinions.map(op =>
|
|
||||||
`<li><strong>${op.speaker}:</strong> ${op.opinion}</li>`
|
|
||||||
).join('');
|
|
||||||
content.appendChild(createElement('div', { className: 'agenda-section' }, `
|
|
||||||
<div class="agenda-section-title">발언자별 의견</div>
|
|
||||||
<div class="agenda-section-content"><ul>${opinionsHtml}</ul></div>
|
|
||||||
`));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 결정 사항
|
// 결정 사항
|
||||||
if (agenda.details.decisions && agenda.details.decisions.length > 0) {
|
if (agenda.details.decisions && agenda.details.decisions.length > 0) {
|
||||||
const decisionsHtml = agenda.details.decisions.map(d => `<li>✓ ${d}</li>`).join('');
|
const decisionsSection = createElement('div', { className: 'agenda-section' }, `
|
||||||
content.appendChild(createElement('div', { className: 'agenda-section' }, `
|
|
||||||
<div class="agenda-section-title">결정 사항</div>
|
<div class="agenda-section-title">결정 사항</div>
|
||||||
<div class="agenda-section-content"><ul>${decisionsHtml}</ul></div>
|
`);
|
||||||
`));
|
|
||||||
|
const decisionsList = createElement('ul', {
|
||||||
|
className: 'agenda-section-content',
|
||||||
|
style: 'list-style: none; padding: 0; margin: 0; padding-left: var(--space-md);'
|
||||||
|
});
|
||||||
|
|
||||||
|
agenda.details.decisions.forEach(decision => {
|
||||||
|
const decisionItem = createElement('li', {
|
||||||
|
style: 'display: flex; align-items: flex-start; gap: var(--space-sm); padding: var(--space-xs) 0; font-size: var(--font-body); color: var(--gray-700);'
|
||||||
|
}, `
|
||||||
|
<span style="color: var(--gray-500); margin-top: 2px;">•</span>
|
||||||
|
<span style="flex: 1;">${decision}</span>
|
||||||
|
`);
|
||||||
|
decisionsList.appendChild(decisionItem);
|
||||||
|
});
|
||||||
|
|
||||||
|
decisionsSection.appendChild(decisionsList);
|
||||||
|
content.appendChild(decisionsSection);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 보류 사항
|
// 보류 사항
|
||||||
if (agenda.details.pending && agenda.details.pending.length > 0) {
|
if (agenda.details.pending && agenda.details.pending.length > 0) {
|
||||||
const pendingHtml = agenda.details.pending.map(p => `<li>⏸ ${p}</li>`).join('');
|
const pendingSection = createElement('div', { className: 'agenda-section' }, `
|
||||||
content.appendChild(createElement('div', { className: 'agenda-section' }, `
|
|
||||||
<div class="agenda-section-title">보류 사항</div>
|
<div class="agenda-section-title">보류 사항</div>
|
||||||
<div class="agenda-section-content"><ul>${pendingHtml}</ul></div>
|
`);
|
||||||
`));
|
|
||||||
|
const pendingList = createElement('ul', {
|
||||||
|
className: 'agenda-section-content',
|
||||||
|
style: 'list-style: none; padding: 0; margin: 0; padding-left: var(--space-md);'
|
||||||
|
});
|
||||||
|
|
||||||
|
agenda.details.pending.forEach(pending => {
|
||||||
|
const pendingItem = createElement('li', {
|
||||||
|
style: 'display: flex; align-items: flex-start; gap: var(--space-sm); padding: var(--space-xs) 0; font-size: var(--font-body); color: var(--gray-700);'
|
||||||
|
}, `
|
||||||
|
<span style="color: var(--gray-500); margin-top: 2px;">•</span>
|
||||||
|
<span style="flex: 1;">${pending}</span>
|
||||||
|
`);
|
||||||
|
pendingList.appendChild(pendingItem);
|
||||||
|
});
|
||||||
|
|
||||||
|
pendingSection.appendChild(pendingList);
|
||||||
|
content.appendChild(pendingSection);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Todo 목록
|
// Todo 목록 - 제목만 간단히 표시
|
||||||
if (agenda.todos && agenda.todos.length > 0) {
|
if (agenda.todos && agenda.todos.length > 0) {
|
||||||
const todosSection = createElement('div', { className: 'agenda-section' }, `
|
const todosSection = createElement('div', { className: 'agenda-section' }, `
|
||||||
<div class="agenda-section-title">Todo 자동 추출 결과</div>
|
<div class="agenda-section-title">Todo 자동 추출 결과</div>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
agenda.todos.forEach(todo => {
|
const todoList = createElement('ul', {
|
||||||
const todoItem = createElement('div', { className: 'todo-list-item' }, `
|
className: 'agenda-section-content',
|
||||||
<div class="todo-header">
|
style: 'list-style: none; padding: 0; margin: 0; padding-left: var(--space-md);'
|
||||||
<input type="checkbox" class="todo-checkbox" disabled>
|
|
||||||
<div class="todo-content">${todo.title}</div>
|
|
||||||
${createBadge(todo.priority === 'high' ? '높음' : todo.priority === 'medium' ? '보통' : '낮음',
|
|
||||||
`priority-${todo.priority}`)}
|
|
||||||
</div>
|
|
||||||
<div class="todo-meta">
|
|
||||||
${createAvatar(todo.assignee, 'sm')}
|
|
||||||
<span>${todo.assignee.name}</span>
|
|
||||||
<span>•</span>
|
|
||||||
<span>${formatDate(todo.dueDate)}</span>
|
|
||||||
</div>
|
|
||||||
`);
|
|
||||||
todosSection.appendChild(todoItem);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
agenda.todos.forEach(todo => {
|
||||||
|
const todoItem = createElement('li', {
|
||||||
|
style: 'display: flex; align-items: flex-start; gap: var(--space-sm); padding: var(--space-xs) 0; font-size: var(--font-body); color: var(--gray-700);'
|
||||||
|
}, `
|
||||||
|
<span style="color: var(--gray-500); margin-top: 2px;">•</span>
|
||||||
|
<span style="flex: 1;">${todo.title}</span>
|
||||||
|
`);
|
||||||
|
todoList.appendChild(todoItem);
|
||||||
|
});
|
||||||
|
|
||||||
|
todosSection.appendChild(todoList);
|
||||||
|
|
||||||
|
// 안내 문구 추가
|
||||||
|
const notice = createElement('p', {
|
||||||
|
style: 'font-size: var(--font-small); color: var(--gray-500); margin-top: var(--space-md); padding-top: var(--space-sm); border-top: 1px solid var(--gray-200);'
|
||||||
|
}, '💡 담당자 및 마감일은 회의록 수정 화면에서 지정할 수 있습니다.');
|
||||||
|
todosSection.appendChild(notice);
|
||||||
|
|
||||||
content.appendChild(todosSection);
|
content.appendChild(todosSection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -165,6 +165,23 @@
|
|||||||
.participant {
|
.participant {
|
||||||
width: calc(50% - var(--space-md) / 2);
|
width: calc(50% - var(--space-md) / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 통계 그리드: 모바일에서도 4열 유지, gap만 축소 */
|
||||||
|
.stats-grid {
|
||||||
|
gap: var(--space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
padding: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: var(--font-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: var(--font-xs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 회의록 섹션 */
|
/* 회의록 섹션 */
|
||||||
@ -333,9 +350,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* 대시보드 탭 콘텐츠 */
|
/* 대시보드 탭 콘텐츠 */
|
||||||
.dashboard-section {
|
|
||||||
margin-bottom: var(--space-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.key-points {
|
.key-points {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
@ -393,7 +407,7 @@
|
|||||||
|
|
||||||
.stats-grid {
|
.stats-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(4, 1fr);
|
||||||
gap: var(--space-md);
|
gap: var(--space-md);
|
||||||
margin-top: var(--space-md);
|
margin-top: var(--space-md);
|
||||||
}
|
}
|
||||||
@ -724,7 +738,11 @@
|
|||||||
<!-- 기본 정보 카드 -->
|
<!-- 기본 정보 카드 -->
|
||||||
<div class="info-card">
|
<div class="info-card">
|
||||||
<div class="meeting-basic-info">
|
<div class="meeting-basic-info">
|
||||||
<h2>2025년 1분기 제품 기획 회의</h2>
|
<div id="meeting-title-container" style="display: flex; align-items: center; gap: var(--space-sm); margin-bottom: var(--space-sm);">
|
||||||
|
<span class="badge badge-complete">확정완료</span>
|
||||||
|
<!-- 생성자일 경우 👑 아이콘이 JavaScript로 추가됨 -->
|
||||||
|
<h2 style="margin: 0;">2025년 1분기 제품 기획 회의</h2>
|
||||||
|
</div>
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="info-icon">📅</span>
|
<span class="info-icon">📅</span>
|
||||||
<span>2025년 10월 25일 14:00 (90분)</span>
|
<span>2025년 10월 25일 14:00 (90분)</span>
|
||||||
@ -733,10 +751,6 @@
|
|||||||
<span class="info-icon">📍</span>
|
<span class="info-icon">📍</span>
|
||||||
<span>본사 2층 대회의실</span>
|
<span>본사 2층 대회의실</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-row">
|
|
||||||
<span class="info-icon">✅</span>
|
|
||||||
<span class="badge badge-complete">확정완료</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="participants-list">
|
<div class="participants-list">
|
||||||
@ -951,10 +965,8 @@
|
|||||||
<!-- 대시보드 탭 (기본 노출 탭 - 유저스토리 UFR-MEET-047 요구사항) -->
|
<!-- 대시보드 탭 (기본 노출 탭 - 유저스토리 UFR-MEET-047 요구사항) -->
|
||||||
<div id="dashboard-content" class="tab-content active">
|
<div id="dashboard-content" class="tab-content active">
|
||||||
<!-- 핵심내용 -->
|
<!-- 핵심내용 -->
|
||||||
<div class="section dashboard-section">
|
<div class="card mb-lg">
|
||||||
<div class="section-header">
|
<h3 class="card-title">💡 핵심내용</h3>
|
||||||
<h3 class="section-title">💡 핵심내용</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ol class="key-points">
|
<ol class="key-points">
|
||||||
<li class="key-point">
|
<li class="key-point">
|
||||||
@ -1004,10 +1016,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 결정사항 -->
|
<!-- 결정사항 -->
|
||||||
<div class="section dashboard-section">
|
<div class="card mb-lg">
|
||||||
<div class="section-header">
|
<h3 class="card-title">✅ 결정사항</h3>
|
||||||
<h3 class="section-title">✅ 결정사항</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="decision-card">
|
<div class="decision-card">
|
||||||
<div class="decision-content">베타 버전 출시일: 2025년 12월 1일</div>
|
<div class="decision-content">베타 버전 출시일: 2025년 12월 1일</div>
|
||||||
@ -1033,10 +1043,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Todo 진행상황 -->
|
<!-- Todo 진행상황 -->
|
||||||
<div class="section dashboard-section">
|
<div class="card mb-lg">
|
||||||
<div class="section-header">
|
<h3 class="card-title">📋 Todo 진행상황</h3>
|
||||||
<h3 class="section-title">📋 Todo 진행상황</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 전체 진행률 -->
|
<!-- 전체 진행률 -->
|
||||||
<div style="margin-bottom: var(--space-lg);">
|
<div style="margin-bottom: var(--space-lg);">
|
||||||
@ -1182,10 +1190,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 관련회의록 -->
|
<!-- 관련회의록 -->
|
||||||
<div class="section dashboard-section">
|
<div class="card mb-lg">
|
||||||
<div class="section-header">
|
<h3 class="card-title">📚 관련회의록 (3건)</h3>
|
||||||
<h3 class="section-title">📚 관련회의록 (3건)</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="reference-item" onclick="window.open('10-회의록상세조회.html', '_blank')">
|
<div class="reference-item" onclick="window.open('10-회의록상세조회.html', '_blank')">
|
||||||
<div class="reference-header">
|
<div class="reference-header">
|
||||||
@ -1729,6 +1735,20 @@
|
|||||||
* 페이지 초기화
|
* 페이지 초기화
|
||||||
*/
|
*/
|
||||||
function initPage() {
|
function initPage() {
|
||||||
|
// 회의 생성자 확인 후 👑 표시
|
||||||
|
const currentUser = '김민준'; // 현재 로그인 사용자
|
||||||
|
const isCreator = checkIfUserIsCreator(CURRENT_MEETING_ID, currentUser);
|
||||||
|
|
||||||
|
if (isCreator) {
|
||||||
|
const titleContainer = document.getElementById('meeting-title-container');
|
||||||
|
const badge = titleContainer.querySelector('.badge');
|
||||||
|
const crownIcon = document.createElement('span');
|
||||||
|
crownIcon.textContent = '👑';
|
||||||
|
crownIcon.style.fontSize = '24px';
|
||||||
|
// badge 다음에 👑 삽입
|
||||||
|
badge.insertAdjacentElement('afterend', crownIcon);
|
||||||
|
}
|
||||||
|
|
||||||
updateTodoProgress();
|
updateTodoProgress();
|
||||||
updateFilterCounts();
|
updateFilterCounts();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -604,7 +604,7 @@
|
|||||||
const statusBadge = minute.status === 'complete' ?
|
const statusBadge = minute.status === 'complete' ?
|
||||||
'<span class="badge badge-complete">확정완료</span>' :
|
'<span class="badge badge-complete">확정완료</span>' :
|
||||||
'<span class="badge badge-draft">작성중</span>';
|
'<span class="badge badge-draft">작성중</span>';
|
||||||
const crownEmoji = isCreator ? '<span style="font-size: 16px; flex-shrink: 0;" title="생성자">👑</span>' : '';
|
const creatorBadge = isCreator ? '<span class="creator-badge" title="생성자">👑</span>' : '';
|
||||||
|
|
||||||
// 검증완료율 실시간 계산 (작성중 상태일 때만 표시)
|
// 검증완료율 실시간 계산 (작성중 상태일 때만 표시)
|
||||||
const completionRate = minute.status === 'draft'
|
const completionRate = minute.status === 'draft'
|
||||||
@ -615,7 +615,7 @@
|
|||||||
<div class="meeting-item" data-status="${minute.status}" data-type="${participationType}" data-date="${minute.date}" onclick="navigateTo('10-회의록상세조회.html')">
|
<div class="meeting-item" data-status="${minute.status}" data-type="${participationType}" data-date="${minute.date}" onclick="navigateTo('10-회의록상세조회.html')">
|
||||||
<div class="meeting-header">
|
<div class="meeting-header">
|
||||||
${statusBadge}
|
${statusBadge}
|
||||||
${crownEmoji}
|
${creatorBadge}
|
||||||
<h3 class="meeting-title">${minute.title}</h3>
|
<h3 class="meeting-title">${minute.title}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="meeting-meta">
|
<div class="meeting-meta">
|
||||||
|
|||||||
@ -179,6 +179,7 @@ a:hover {
|
|||||||
font-size: var(--font-h3);
|
font-size: var(--font-h3);
|
||||||
font-weight: var(--font-weight-bold);
|
font-weight: var(--font-weight-bold);
|
||||||
color: var(--gray-900);
|
color: var(--gray-900);
|
||||||
|
margin-bottom: var(--space-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-body {
|
.card-body {
|
||||||
@ -258,6 +259,28 @@ a:hover {
|
|||||||
background: var(--gray-100);
|
background: var(--gray-100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Outline Button (회색 테두리) */
|
||||||
|
.btn-outline {
|
||||||
|
background: var(--white);
|
||||||
|
color: var(--gray-700);
|
||||||
|
border: 1px solid var(--gray-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline:hover:not(:disabled) {
|
||||||
|
background: var(--gray-50);
|
||||||
|
border-color: var(--gray-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Neutral Button (진한 회색 배경) */
|
||||||
|
.btn-neutral {
|
||||||
|
background: #424242;
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-neutral:hover:not(:disabled) {
|
||||||
|
background: var(--gray-900);
|
||||||
|
}
|
||||||
|
|
||||||
/* Error Button */
|
/* Error Button */
|
||||||
.btn-error {
|
.btn-error {
|
||||||
background: var(--error);
|
background: var(--error);
|
||||||
@ -372,6 +395,21 @@ a:hover {
|
|||||||
color: var(--gray-700);
|
color: var(--gray-700);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Creator Badge (생성자 표시) */
|
||||||
|
.creator-badge {
|
||||||
|
font-size: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: 4px;
|
||||||
|
cursor: default;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.creator-badge[title] {
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
|
|
||||||
/* Priority Badges */
|
/* Priority Badges */
|
||||||
.badge-high {
|
.badge-high {
|
||||||
background: #FFEBEE;
|
background: #FFEBEE;
|
||||||
@ -1238,9 +1276,9 @@ input[type="date"]::-webkit-calendar-picker-indicator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ========================================
|
/* ========================================
|
||||||
22. 섹션 카드 컴포넌트
|
22. 안건 카드 컴포넌트
|
||||||
======================================== */
|
======================================== */
|
||||||
.section {
|
.agenda {
|
||||||
background: var(--white);
|
background: var(--white);
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
@ -1248,14 +1286,14 @@ input[type="date"]::-webkit-calendar-picker-indicator {
|
|||||||
margin-bottom: var(--space-lg);
|
margin-bottom: var(--space-lg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-header {
|
.agenda-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: var(--space-md);
|
margin-bottom: var(--space-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-title {
|
.agenda-title {
|
||||||
font-size: var(--font-h3);
|
font-size: var(--font-h3);
|
||||||
font-weight: var(--font-weight-bold);
|
font-weight: var(--font-weight-bold);
|
||||||
color: var(--gray-900);
|
color: var(--gray-900);
|
||||||
@ -1264,7 +1302,7 @@ input[type="date"]::-webkit-calendar-picker-indicator {
|
|||||||
gap: var(--space-sm);
|
gap: var(--space-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-content {
|
.agenda-content {
|
||||||
color: var(--gray-700);
|
color: var(--gray-700);
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -333,6 +333,41 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 생성자 배지
|
||||||
|
회의 생성자를 나타내는 크라운 아이콘 배지입니다.
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* 생성자 배지 (👑 아이콘) */
|
||||||
|
.creator-badge {
|
||||||
|
font-size: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: 4px;
|
||||||
|
cursor: default;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 툴팁 제공 시 */
|
||||||
|
.creator-badge[title] {
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**사용 예시**:
|
||||||
|
```html
|
||||||
|
<div class="meeting-header">
|
||||||
|
<span class="badge badge-complete">확정완료</span>
|
||||||
|
<span class="creator-badge" title="생성자">👑</span>
|
||||||
|
<h3 class="meeting-title">2024년 4분기 제품 기획 회의</h3>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**사용 위치**:
|
||||||
|
- 12-회의록목록조회: 회의록 카드 헤더
|
||||||
|
- 02-대시보드: 최근 회의 카드, 내 회의록 카드
|
||||||
|
- 10-회의록상세조회: 회의록 정보 섹션
|
||||||
|
|
||||||
### D-day 배지
|
### D-day 배지
|
||||||
Todo 마감일 표시를 위한 D-day 배지 스타일입니다.
|
Todo 마감일 표시를 위한 D-day 배지 스타일입니다.
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
- **작성일**: 2025-10-21
|
- **작성일**: 2025-10-21
|
||||||
- **최종 수정일**: 2025-10-24
|
- **최종 수정일**: 2025-10-24
|
||||||
- **작성자**: 이미준 (서비스 기획자)
|
- **작성자**: 이미준 (서비스 기획자)
|
||||||
- **버전**: 1.4.16
|
- **버전**: 1.4.18
|
||||||
- **설계 철학**: Mobile First Design
|
- **설계 철학**: Mobile First Design
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -847,7 +847,15 @@ graph TD
|
|||||||
- 상태 표시 없음 (발언 중/온라인 등 제거)
|
- 상태 표시 없음 (발언 중/온라인 등 제거)
|
||||||
- 참석자 수 동적 업데이트 (초대 성공 시)
|
- 참석자 수 동적 업데이트 (초대 성공 시)
|
||||||
|
|
||||||
- **AI 제안 탭**: AI가 생성한 회의록 개선 제안 (3가지 유형)
|
- **AI 제안 탭**: AI가 생성한 회의록 개선 제안
|
||||||
|
|
||||||
|
- **실시간 주요 메모 추천** (UFR-MEET-030):
|
||||||
|
- 음성→텍스트 변환 후 AI가 실시간 분석
|
||||||
|
- **중요한 내용으로 판단된 경우에만** 주요 메모 항목 추천
|
||||||
|
- 추천 빈도는 중요 내용 발생에 따라 가변적 (고정 간격 아님)
|
||||||
|
- 각 추천 항목에 "주요 메모에 추가" 버튼 제공
|
||||||
|
- 클릭 시 해당 안건의 주요 메모에 자동 저장
|
||||||
|
- 실시간 업데이트: 새로운 추천은 상단에 표시
|
||||||
|
|
||||||
- **논의사항 제안 카드**: 제안 내용 + "논의사항에 적용" 버튼
|
- **논의사항 제안 카드**: 제안 내용 + "논의사항에 적용" 버튼
|
||||||
- 제안 구조:
|
- 제안 구조:
|
||||||
@ -976,7 +984,6 @@ graph TD
|
|||||||
- 회의 총 시간
|
- 회의 총 시간
|
||||||
- 참석자 수
|
- 참석자 수
|
||||||
- 주요 키워드 (태그 클라우드)
|
- 주요 키워드 (태그 클라우드)
|
||||||
- 발언 통계 (화자별 발언 횟수 및 시간 - 바 차트)
|
|
||||||
|
|
||||||
- **안건별 AI 요약 섹션** (신규)
|
- **안건별 AI 요약 섹션** (신규)
|
||||||
- **안건 카드** (안건 개수만큼 반복):
|
- **안건 카드** (안건 개수만큼 반복):
|
||||||
@ -986,7 +993,6 @@ graph TD
|
|||||||
- 🔒 "편집 불가" 아이콘 표시
|
- 🔒 "편집 불가" 아이콘 표시
|
||||||
- 민트 그린 좌측 액센트 라인
|
- 민트 그린 좌측 액센트 라인
|
||||||
- **상세 요약 정리** (읽기 전용)
|
- **상세 요약 정리** (읽기 전용)
|
||||||
- 논의 주제
|
|
||||||
- 발언자별 의견
|
- 발언자별 의견
|
||||||
- 결정 사항
|
- 결정 사항
|
||||||
- 보류 사항
|
- 보류 사항
|
||||||
@ -1237,9 +1243,9 @@ graph TD
|
|||||||
|
|
||||||
#### 주요 기능
|
#### 주요 기능
|
||||||
1. 회의 기본 정보 표시
|
1. 회의 기본 정보 표시
|
||||||
2. **섹션별 AI 요약 표시** (섹션 최상단)
|
2. **안건별 AI 요약 표시** (안건 최상단)
|
||||||
3. 섹션별 상세 내용 표시
|
3. 안건별 상세 내용 표시
|
||||||
4. **참고자료 표시** (섹션 하단)
|
4. **참고자료 표시** (안건 하단)
|
||||||
5. Todo 항목 및 진행 상황 표시
|
5. Todo 항목 및 진행 상황 표시
|
||||||
6. 첨부파일 다운로드
|
6. 첨부파일 다운로드
|
||||||
7. 회의록 수정/공유 액션
|
7. 회의록 수정/공유 액션
|
||||||
@ -1263,17 +1269,17 @@ graph TD
|
|||||||
- "대시보드" 탭 (기본 활성)
|
- "대시보드" 탭 (기본 활성)
|
||||||
- "회의록" 탭
|
- "회의록" 탭
|
||||||
|
|
||||||
- **회의록 탭 콘텐츠** (섹션별 구조)
|
- **회의록 탭 콘텐츠** (안건별 구조)
|
||||||
- 각 섹션:
|
- 각 안건:
|
||||||
- 섹션 제목
|
- 안건 제목
|
||||||
- 검증 완료 배지 (검증된 경우)
|
- 검증 완료 배지 (검증된 경우)
|
||||||
- **AI 회의 내용 요약 영역** (섹션 최상단, 강조 박스)
|
- **AI 회의 내용 요약 영역** (안건 최상단, 강조 박스)
|
||||||
- 요약 아이콘 (💡)
|
- 요약 아이콘 (💡)
|
||||||
- AI 자동 생성 요약 (2-3문장)
|
- AI 자동 생성 요약 (2-3문장)
|
||||||
- 요약 생성/수정 시간
|
- 요약 생성/수정 시간
|
||||||
- "수정" 버튼 (권한 있는 경우)
|
- "수정" 버튼 (권한 있는 경우)
|
||||||
- 섹션 내용 (마크다운 렌더링)
|
- 안건 내용 (마크다운 렌더링)
|
||||||
- **참고자료 영역** (섹션 하단, 별도 영역)
|
- **참고자료 영역** (안건 하단, 별도 영역)
|
||||||
- "참고자료" 라벨
|
- "참고자료" 라벨
|
||||||
- 관련 회의록 링크 리스트 (최대 3개):
|
- 관련 회의록 링크 리스트 (최대 3개):
|
||||||
- 링크 아이콘 (📄)
|
- 링크 아이콘 (📄)
|
||||||
@ -1331,17 +1337,17 @@ graph TD
|
|||||||
- 대시보드 (기본 활성)
|
- 대시보드 (기본 활성)
|
||||||
- 회의록
|
- 회의록
|
||||||
- **메인 영역**:
|
- **메인 영역**:
|
||||||
- 회의록 탭: 전체 회의록 내용 (섹션별 구조)
|
- 회의록 탭: 전체 회의록 내용 (안건별 구조)
|
||||||
- 대시보드 탭: 핵심내용, 결정사항, Todo 진행상황, 참고자료 (11-회의록대시보드.html 구조 참조)
|
- 대시보드 탭: 핵심내용, 결정사항, Todo 진행상황, 참고자료 (11-회의록대시보드.html 구조 참조)
|
||||||
|
|
||||||
#### 인터랙션
|
#### 인터랙션
|
||||||
1. **탭 전환**
|
1. **탭 전환**
|
||||||
- "회의록" 탭: 전체 회의록 내용 표시 (섹션별 구조)
|
- "회의록" 탭: 전체 회의록 내용 표시 (안건별 구조)
|
||||||
- "대시보드" 탭: 핵심내용, 결정사항, Todo, 참고자료 요약 표시
|
- "대시보드" 탭: 핵심내용, 결정사항, Todo, 참고자료 요약 표시
|
||||||
- 탭 전환 시 URL 변경 없이 클라이언트 사이드 렌더링
|
- 탭 전환 시 URL 변경 없이 클라이언트 사이드 렌더링
|
||||||
|
|
||||||
2. **회의록 탭 인터랙션**
|
2. **회의록 탭 인터랙션**
|
||||||
- **섹션 네비게이션**: 섹션 제목 클릭 → 해당 섹션으로 스크롤
|
- **안건 네비게이션**: 안건 제목 클릭 → 해당 안건으로 스크롤
|
||||||
- **접기/펼치기**: 긴 내용은 초기 접힌 상태, 클릭으로 펼침
|
- **접기/펼치기**: 긴 내용은 초기 접힌 상태, 클릭으로 펼침
|
||||||
- **AI 요약 편집**:
|
- **AI 요약 편집**:
|
||||||
- "수정" 버튼 클릭 (권한 있는 경우) → 인라인 편집 모드
|
- "수정" 버튼 클릭 (권한 있는 경우) → 인라인 편집 모드
|
||||||
@ -1354,10 +1360,10 @@ graph TD
|
|||||||
|
|
||||||
3. **대시보드 탭 인터랙션**
|
3. **대시보드 탭 인터랙션**
|
||||||
- **핵심내용 섹션**:
|
- **핵심내용 섹션**:
|
||||||
- 키워드 태그 클릭 → 해당 키워드 관련 섹션으로 스크롤
|
- 키워드 태그 클릭 → 해당 키워드 관련 안건으로 스크롤
|
||||||
- 통계 항목 클릭 → 상세 정보 툴팁 표시
|
- 통계 항목 클릭 → 상세 정보 툴팁 표시
|
||||||
- **결정사항 섹션**:
|
- **결정사항 섹션**:
|
||||||
- 결정사항 카드 클릭 → 회의록 탭의 해당 섹션으로 이동
|
- 결정사항 카드 클릭 → 회의록 탭의 해당 안건으로 이동
|
||||||
- 배경 설명 접기/펼치기
|
- 배경 설명 접기/펼치기
|
||||||
- **Todo 진행상황**:
|
- **Todo 진행상황**:
|
||||||
- 필터 탭 클릭 → 해당 상태의 Todo만 표시
|
- 필터 탭 클릭 → 해당 상태의 Todo만 표시
|
||||||
@ -1384,8 +1390,8 @@ graph TD
|
|||||||
- **입력**: 회의록 ID, 활성 탭 (회의록/대시보드/타임라인)
|
- **입력**: 회의록 ID, 활성 탭 (회의록/대시보드/타임라인)
|
||||||
- **출력**:
|
- **출력**:
|
||||||
- **회의 기본 정보**: 제목, 일시, 참석자, 장소, 상태, 작성자, 수정 시간
|
- **회의 기본 정보**: 제목, 일시, 참석자, 장소, 상태, 작성자, 수정 시간
|
||||||
- **섹션별 AI 요약**: 자동 생성 요약, 수정 이력
|
- **안건별 AI 요약**: 자동 생성 요약, 수정 이력
|
||||||
- **섹션별 내용**: 마크다운 형식
|
- **안건별 내용**: 마크다운 형식
|
||||||
- **참고자료 목록**:
|
- **참고자료 목록**:
|
||||||
- 관련 회의록 (제목, 날짜, 관련도, 요약)
|
- 관련 회의록 (제목, 날짜, 관련도, 요약)
|
||||||
- 프로젝트 문서 (제목, 작성자, 관련도)
|
- 프로젝트 문서 (제목, 작성자, 관련도)
|
||||||
@ -1403,7 +1409,7 @@ graph TD
|
|||||||
|
|
||||||
#### 에러 처리
|
#### 에러 처리
|
||||||
- **회의록 로딩 실패**: "회의록을 불러올 수 없습니다" + 재시도 버튼
|
- **회의록 로딩 실패**: "회의록을 불러올 수 없습니다" + 재시도 버튼
|
||||||
- **AI 요약 로딩 실패**: "요약을 불러올 수 없습니다" (섹션 내용은 정상 표시)
|
- **AI 요약 로딩 실패**: "요약을 불러올 수 없습니다" (안건 내용은 정상 표시)
|
||||||
- **참고자료 로딩 실패**: "참고자료를 불러올 수 없습니다" (빈 상태 표시)
|
- **참고자료 로딩 실패**: "참고자료를 불러올 수 없습니다" (빈 상태 표시)
|
||||||
- **대시보드 데이터 로딩 실패**: "대시보드를 불러올 수 없습니다" + 재시도 버튼
|
- **대시보드 데이터 로딩 실패**: "대시보드를 불러올 수 없습니다" + 재시도 버튼
|
||||||
- **권한 없음**: "수정" 버튼 비활성화, "조회 권한만 있습니다" 메시지
|
- **권한 없음**: "수정" 버튼 비활성화, "조회 권한만 있습니다" 메시지
|
||||||
@ -1708,8 +1714,10 @@ graph TD
|
|||||||
- 각 회의록 항목 (meeting-item):
|
- 각 회의록 항목 (meeting-item):
|
||||||
- **좌측 영역**:
|
- **좌측 영역**:
|
||||||
- 회의 제목 (H5, 볼드)
|
- 회의 제목 (H5, 볼드)
|
||||||
|
- **생성자 표시**: 현재 사용자가 회의 생성자인 경우 👑 아이콘 표시 (16px, title="생성자")
|
||||||
- 메타정보 (Caption, 회색):
|
- 메타정보 (Caption, 회색):
|
||||||
- 회의 일시 (날짜 + 시간) · 참석자 수
|
- 회의 일시 (날짜 + 시간) · 참석자 수
|
||||||
|
- 검증완료율 (작성중 상태일 때만): "✓ {completionRate}% 검증완료" 배지
|
||||||
- 최종 수정 시간 (Caption, 회색):
|
- 최종 수정 시간 (Caption, 회색):
|
||||||
- 상대 시간 표시 ("1시간 전", "어제", "3일 전")
|
- 상대 시간 표시 ("1시간 전", "어제", "3일 전")
|
||||||
- **우측 영역**:
|
- **우측 영역**:
|
||||||
@ -2130,6 +2138,8 @@ graph TD
|
|||||||
| 1.4.14 | 2025-10-24 | 이미준 | 12-회의록목록조회 화면 데이터 아키텍처 문서화<br>- **데이터 아키텍처 섹션 추가**: 데이터/뷰 레이어 분리 구조 설명<br> - 데이터 레이어: common.js → SAMPLE_MINUTES 배열 (30개 샘플)<br> - 뷰 레이어: 12-회의록목록조회.html → renderMeetings(), createMeetingCard() 함수<br> - 렌더링 방식: 동적 렌더링, 초기 10개 표시, "10개 더보기" 버튼으로 추가 로딩<br>- **정렬 옵션 레이블 변경**: "최신순" → "최근수정순", "회의일시순" → "최근회의순"<br>- **페이지네이션 기능 문서화**: 초기 10개 표시, "10개 더보기" 버튼 기능 설명<br>- **샘플 데이터 분포 명시**: 총 30개 (작성중 13개, 확정완료 17개)<br>- **프로토타입 파일 경로 추가**: design/uiux/prototype/12-회의록목록조회.html<br>- **스타일 가이드 버전 동기화**: v1.2.4 |
|
| 1.4.14 | 2025-10-24 | 이미준 | 12-회의록목록조회 화면 데이터 아키텍처 문서화<br>- **데이터 아키텍처 섹션 추가**: 데이터/뷰 레이어 분리 구조 설명<br> - 데이터 레이어: common.js → SAMPLE_MINUTES 배열 (30개 샘플)<br> - 뷰 레이어: 12-회의록목록조회.html → renderMeetings(), createMeetingCard() 함수<br> - 렌더링 방식: 동적 렌더링, 초기 10개 표시, "10개 더보기" 버튼으로 추가 로딩<br>- **정렬 옵션 레이블 변경**: "최신순" → "최근수정순", "회의일시순" → "최근회의순"<br>- **페이지네이션 기능 문서화**: 초기 10개 표시, "10개 더보기" 버튼 기능 설명<br>- **샘플 데이터 분포 명시**: 총 30개 (작성중 13개, 확정완료 17개)<br>- **프로토타입 파일 경로 추가**: design/uiux/prototype/12-회의록목록조회.html<br>- **스타일 가이드 버전 동기화**: v1.2.4 |
|
||||||
| 1.4.15 | 2025-10-24 | 이미준 | 06-검증완료 화면 삭제 (유저스토리 v2.1.2 변경사항 반영)<br>- **화면 삭제**: 06-검증완료 화면 전체 삭제<br> - 안건별 검증 기능이 11-회의록수정 화면으로 통합됨<br> - 섹션별 검증 방식에서 안건별 검증 방식으로 변경 (유저스토리 UFR-COLLAB-030 → 안건 기반 구조로 전환)<br>- **유저스토리 매핑 업데이트**:<br> - Collaboration 서비스: UFR-COLLAB-010 ~ UFR-COLLAB-030 → UFR-COLLAB-010 ~ UFR-COLLAB-020로 변경<br> - 프로토타입 화면 목록 테이블에서 06-검증완료 행 제거<br>- **화면 번호 유지**: 다른 화면 번호는 변경하지 않음 (프로토타입 파일명 유지)<br> - 07-회의종료, 09-Todo관리, 10-회의록상세조회, 11-회의록수정, 12-회의록목록조회 번호 유지<br>- **변경 이력**: 과거 버전의 UFR-COLLAB-030 언급은 역사적 맥락으로 유지 |
|
| 1.4.15 | 2025-10-24 | 이미준 | 06-검증완료 화면 삭제 (유저스토리 v2.1.2 변경사항 반영)<br>- **화면 삭제**: 06-검증완료 화면 전체 삭제<br> - 안건별 검증 기능이 11-회의록수정 화면으로 통합됨<br> - 섹션별 검증 방식에서 안건별 검증 방식으로 변경 (유저스토리 UFR-COLLAB-030 → 안건 기반 구조로 전환)<br>- **유저스토리 매핑 업데이트**:<br> - Collaboration 서비스: UFR-COLLAB-010 ~ UFR-COLLAB-030 → UFR-COLLAB-010 ~ UFR-COLLAB-020로 변경<br> - 프로토타입 화면 목록 테이블에서 06-검증완료 행 제거<br>- **화면 번호 유지**: 다른 화면 번호는 변경하지 않음 (프로토타입 파일명 유지)<br> - 07-회의종료, 09-Todo관리, 10-회의록상세조회, 11-회의록수정, 12-회의록목록조회 번호 유지<br>- **변경 이력**: 과거 버전의 UFR-COLLAB-030 언급은 역사적 맥락으로 유지 |
|
||||||
| 1.4.16 | 2025-10-24 | 이미준 | 사용자 역할 용어 통일 (유저스토리 v2.1.2 반영)<br>- **용어 정의 명확화**: "회의 생성자"와 "회의 참석자" 용어로 통일<br> - 설계 목표: "회의록 작성자" → "회의 참석자"로 수정<br>- **화면별 권한 정보 추가**:<br> - 03-회의예약: 모든 사용자 (예약 생성 시 자동으로 회의 생성자가 됨)<br> - 04-템플릿선택: 회의 생성자 전용<br> - 05-회의진행: 회의 시작/종료는 회의 생성자 전용, 회의록 편집은 모든 참석자<br> - 07-회의종료: 회의 생성자 전용<br> - 09-Todo관리: 모든 회의 참석자 (본인이 담당자인 Todo만 조회/수정 가능)<br> - 10-회의록상세조회: 모든 회의 참석자 (조회 전용)<br> - 11-회의록수정: 검증완료 전(모든 참석자), 검증완료 후(회의 생성자만) - 기존 권한 제어 유지<br> - 12-회의록목록조회: 모든 회의 참석자 (본인이 참석한 회의록만 조회)<br>- **스타일 가이드 동기화**: design/uiux/style-guide.md v1.2.5 (용어 정의 섹션 추가)<br>- **통일성 달성**: 유저스토리, 화면설계서, 스타일 가이드 간 용어 완전 통일 |
|
| 1.4.16 | 2025-10-24 | 이미준 | 사용자 역할 용어 통일 (유저스토리 v2.1.2 반영)<br>- **용어 정의 명확화**: "회의 생성자"와 "회의 참석자" 용어로 통일<br> - 설계 목표: "회의록 작성자" → "회의 참석자"로 수정<br>- **화면별 권한 정보 추가**:<br> - 03-회의예약: 모든 사용자 (예약 생성 시 자동으로 회의 생성자가 됨)<br> - 04-템플릿선택: 회의 생성자 전용<br> - 05-회의진행: 회의 시작/종료는 회의 생성자 전용, 회의록 편집은 모든 참석자<br> - 07-회의종료: 회의 생성자 전용<br> - 09-Todo관리: 모든 회의 참석자 (본인이 담당자인 Todo만 조회/수정 가능)<br> - 10-회의록상세조회: 모든 회의 참석자 (조회 전용)<br> - 11-회의록수정: 검증완료 전(모든 참석자), 검증완료 후(회의 생성자만) - 기존 권한 제어 유지<br> - 12-회의록목록조회: 모든 회의 참석자 (본인이 참석한 회의록만 조회)<br>- **스타일 가이드 동기화**: design/uiux/style-guide.md v1.2.5 (용어 정의 섹션 추가)<br>- **통일성 달성**: 유저스토리, 화면설계서, 스타일 가이드 간 용어 완전 통일 |
|
||||||
|
| 1.4.17 | 2025-10-24 | 강지수 | 10-회의록상세조회 화면 용어 통일 (섹션 → 안건)<br>- **용어 변경 (요구사항설계검토-report-V1.2.md 반영)**:<br> - 모든 "섹션별" → "안건별"로 용어 통일<br> - 주요 기능, UI 구성요소, 인터랙션, 데이터 요구사항, 에러 처리 섹션 전체 업데이트<br>- **CSS 클래스명 변경 (공통 스타일 + 프로토타입)**:<br> - common.css: `.section` → `.agenda`, `.section-header` → `.agenda-header`, `.section-title` → `.agenda-title`, `.section-content` → `.agenda-content`<br> - 10-회의록상세조회.html: 모든 section 클래스를 agenda 클래스로 일괄 변경<br>- **HTML 주석 업데이트**: "회의록 섹션" → "회의록 안건", "섹션 내용" → "안건 내용"<br>- **일관성 달성**: 유저스토리 v2.1.2의 안건 기반 구조와 완전히 일치 |
|
||||||
|
| 1.4.18 | 2025-10-24 | 강지수 | 12-회의록목록조회 화면 생성자 표시 기능 추가 (유저스토리 v2.1.3 반영)<br>- **목록 표시 정보 추가**: 회의 생성자 표시 (👑 아이콘)<br> - 현재 사용자가 회의 생성자인 경우 회의록 카드 헤더에 👑 아이콘 표시<br> - 위치: 상태 배지와 회의 제목 사이<br> - 스타일: font-size 16px, title="생성자" 툴팁 제공<br>- **UI 구성요소 업데이트**: 회의록 목록 섹션 명세 수정<br> - 좌측 영역에 "생성자 표시" 항목 추가<br> - 검증완료율 표시 조건 명시 (작성중 상태일 때만)<br>- **프로토타입 수정**: design/uiux/prototype/12-회의록목록조회.html<br> - createMeetingCard() 함수: crownEmoji 변수를 creatorBadge로 변경 및 .creator-badge 클래스 적용<br> - common.css: .creator-badge 스타일 추가 (inline-flex, 16px, margin-left 4px, cursor help)<br>- **스타일 가이드 업데이트**: design/uiux/style-guide.md v1.2.6<br> - 생성자 배지 섹션 추가 (배지 시스템 내 우선순위 배지 다음)<br> - 사용 예시 및 사용 위치 명시 (12-회의록목록조회, 02-대시보드, 10-회의록상세조회) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -2145,3 +2155,5 @@ graph TD
|
|||||||
- 타이포그래피
|
- 타이포그래피
|
||||||
- 컴포넌트 라이브러리
|
- 컴포넌트 라이브러리
|
||||||
- 아이콘 세트
|
- 아이콘 세트
|
||||||
|
| 1.4.17 | 2025-10-24 | 강지수 | 07-회의종료 화면 STT 한계 반영 (유저스토리 v2.1.2)<br>- **STT 화자 식별 불가 반영**: STT는 화자를 식별할 수 없으므로 화자 관련 기능 제거<br> - 발언 통계 섹션 삭제<br> - 안건별 "발언자별 의견" 섹션 삭제<br>- **통계 영역 디자인 개선**: 정보성 디자인으로 명확화<br> - 배경색: var(--white) → var(--gray-50)<br> - 숫자 색상: var(--primary) → var(--gray-900)<br> - 라벨 색상: var(--gray-500) → var(--gray-600)<br> - 정보 표시 전용으로 시각적 구분 명확화<br>- **안건 섹션 구분 개선**:<br> - 안건 간 하단 보더 추가 (1px solid var(--gray-200))<br> - 섹션 제목에 primary 색상 세로 바 추가 (::before pseudo-element)<br> - 콘텐츠 영역 좌측 패딩 추가로 계층 구조 명확화<br>- **연관 문서 업데이트**:<br> - 유저스토리 UFR-MEET-040: "발언 횟수 (화자별)" 항목 제거<br> - UI/UX 설계서 07-회의종료: 발언 통계 및 발언자별 의견 항목 제거 |
|
||||||
|
| 1.4.18 | 2025-10-24 | 강지수 | 05-회의진행 실시간 주요 메모 추천 기능 명확화 (유저스토리 v2.1.1)<br>- **AI 제안 탭 기능 상세화**: 실시간 주요 메모 추천 기능 명시 추가<br> - UFR-MEET-030: 실시간 AI 주요 메모 추천<br> - 음성→텍스트 변환 후 AI가 실시간 분석<br> - **중요한 내용으로 판단된 경우에만** 주요 메모 항목 추천<br> - 추천 빈도는 중요 내용 발생에 따라 가변적 (3-5초 고정 간격 아님)<br> - 각 추천 항목에 "주요 메모에 추가" 버튼 제공<br> - 실시간 업데이트: 새로운 추천은 상단에 표시<br>- **프로토타입 확인**: 05-회의진행.html의 AI 제안 탭이 실시간 주요 메모 추천 기능을 포함하고 있음을 확인<br>- **참조**: design/uiux/요구사항설계검토-report-V1.2.md (실시간 주요 메모 추천 명시 부족 개선) |
|
||||||
|
|||||||
766
design/uiux/요구사항설계검토-report-V1.2.md
Normal file
766
design/uiux/요구사항설계검토-report-V1.2.md
Normal file
@ -0,0 +1,766 @@
|
|||||||
|
# 유저스토리 v2.1.2 vs UI/UX 설계 크로스 체크 리포트
|
||||||
|
|
||||||
|
**작성일**: 2025-10-24
|
||||||
|
**작성자**: AI Assistant (Claude)
|
||||||
|
**버전**: 1.0
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 주요 발견사항 요약
|
||||||
|
|
||||||
|
### 전체 요약
|
||||||
|
- **분석 대상**: 유저스토리 v2.1.2, UI/UX 설계서 v1.4.14, 프로토타입 파일 13개
|
||||||
|
- **불일치 항목 수**: 총 12개 (🔴 높음 4개, 🟡 중간 5개, 🟢 낮음 3개)
|
||||||
|
- **주요 이슈**: 07-회의종료 화면 기능 불일치, 06-검증완료 화면 존재 여부, 용어 사용 불일치
|
||||||
|
|
||||||
|
### 중요도별 분류
|
||||||
|
|
||||||
|
#### 🔴 높음 (즉시 수정 필요)
|
||||||
|
1. **07-회의종료 화면 편집 불가 정책 미반영**
|
||||||
|
2. **07-회의종료 화면 3가지 선택 옵션 미반영**
|
||||||
|
3. **07-회의종료 화면 안건별 AI 요약 표시 미반영**
|
||||||
|
4. **06-검증완료 화면 삭제 필요**
|
||||||
|
|
||||||
|
#### 🟡 중간 (우선 수정 권장)
|
||||||
|
5. **11-회의록수정 화면 안건 기반 구조 미반영**
|
||||||
|
6. **11-회의록수정 화면 안건별 AI 한줄 요약 미표시**
|
||||||
|
7. **11-회의록수정 화면 안건별 검증 UI 미구현**
|
||||||
|
8. **용어 통일 필요: "작성자" → "생성자"/"참석자"**
|
||||||
|
9. **회의록목록조회 화면 "생성자" 표시 미반영**
|
||||||
|
|
||||||
|
#### 🟢 낮음 (검토 후 수정)
|
||||||
|
10. **05-회의진행 화면 실시간 주요 메모 추천 기능 명시 부족**
|
||||||
|
11. **10-회의록상세조회 화면 안건별 표시 명시 부족**
|
||||||
|
12. **스타일 가이드 안건 관련 컴포넌트 누락**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 화면별 상세 분석
|
||||||
|
|
||||||
|
### 07-회의종료
|
||||||
|
|
||||||
|
#### 유저스토리 v2.1.2 요구사항
|
||||||
|
- **UFR-MEET-040 (회의종료)**
|
||||||
|
- 회의 종료 화면은 **확인 전용 (편집 불가)**
|
||||||
|
- **안건별 AI 요약 전체 표시**:
|
||||||
|
- 안건별 AI 한줄 요약 (편집 불가)
|
||||||
|
- 안건별 상세 요약 (확인만 가능)
|
||||||
|
- Todo 자동 추출 결과 (확인만 가능)
|
||||||
|
- 사용자에게 **3가지 선택 옵션** 제공:
|
||||||
|
- 옵션 1: 회의록 수정 화면으로 이동
|
||||||
|
- 옵션 2: 바로 최종 확정 (모든 안건 자동 검증 완료)
|
||||||
|
- 옵션 3: 대시보드로 이동
|
||||||
|
- 회의록 상태: 옵션 1, 3 선택 시 "작성중", 옵션 2 선택 시 "확정완료"
|
||||||
|
|
||||||
|
#### UI/UX 설계서 내용
|
||||||
|
- **화면 목적**: 회의 통계 표시 및 최종 회의록 확정
|
||||||
|
- 주요 기능:
|
||||||
|
1. 회의 통계 표시
|
||||||
|
2. 주요 키워드 클라우드
|
||||||
|
3. AI 자동 추출된 Todo 항목 확인
|
||||||
|
4. **최종 회의록 확정** ← 편집 불가 정책 미반영
|
||||||
|
5. 다음 액션 선택 (공유, 수정, 대시보드 복귀) ← 3가지 옵션 불일치
|
||||||
|
- **AI Todo 추출 결과**:
|
||||||
|
- "AI가 추출한 Todo" 섹션
|
||||||
|
- Todo 항목 리스트 (담당자, 마감일)
|
||||||
|
- **"Todo 수정" 버튼** ← 편집 불가 정책 위반
|
||||||
|
- **하단 액션**:
|
||||||
|
- "회의록 공유하기" 버튼 ← v2.1.2에 없음
|
||||||
|
- "회의록 수정하기" 버튼
|
||||||
|
- "대시보드로 돌아가기" 버튼
|
||||||
|
|
||||||
|
#### 프로토타입 구현 상태 (07-회의종료.html)
|
||||||
|
```html
|
||||||
|
<!-- AI Todo 추출 결과 -->
|
||||||
|
<button class="btn btn-ghost btn-sm" onclick="openModal('todoEditModal')">수정</button>
|
||||||
|
<!-- ← 편집 불가 정책 위반 -->
|
||||||
|
|
||||||
|
<!-- 하단 액션 바 -->
|
||||||
|
<button class="btn btn-secondary" onclick="navigateTo('11-회의록수정.html')">수정</button>
|
||||||
|
<button class="btn btn-primary" onclick="navigateTo('02-대시보드.html')">대시보드로 이동</button>
|
||||||
|
<!-- ← 3가지 옵션 미구현, "바로 최종 확정" 옵션 없음 -->
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 불일치 사항
|
||||||
|
1. 🔴 **편집 불가 정책 미반영**
|
||||||
|
- 유저스토리: 확인 전용, 편집 불가
|
||||||
|
- UI/UX 설계서 & 프로토타입: "Todo 수정" 버튼 존재
|
||||||
|
- **수정 필요**: "수정" 버튼 제거, 확인만 가능하도록 변경
|
||||||
|
|
||||||
|
2. 🔴 **3가지 선택 옵션 미반영**
|
||||||
|
- 유저스토리: 회의록 수정 / 바로 최종 확정 / 대시보드 이동
|
||||||
|
- 프로토타입: 수정 / 대시보드 이동만 있음
|
||||||
|
- **수정 필요**: "바로 최종 확정" 버튼 추가, 선택 시 모든 안건 자동 검증 처리
|
||||||
|
|
||||||
|
3. 🔴 **안건별 AI 요약 표시 미반영**
|
||||||
|
- 유저스토리: 안건별 AI 한줄 요약 + 상세 요약 전체 표시
|
||||||
|
- UI/UX 설계서 & 프로토타입: Todo만 표시, 안건 구조 없음
|
||||||
|
- **수정 필요**: 안건별 섹션으로 구조화, 각 안건의 AI 요약 표시
|
||||||
|
|
||||||
|
4. 🟢 **"회의록 공유하기" 버튼 존재**
|
||||||
|
- UI/UX 설계서: "회의록 공유하기" 버튼 있음
|
||||||
|
- 유저스토리 v2.1.2: 공유 기능 제거됨 (v2.0.1에서)
|
||||||
|
- **수정 필요**: "공유" 버튼 제거
|
||||||
|
|
||||||
|
#### 권장 수정사항
|
||||||
|
```
|
||||||
|
[하단 액션 바 수정안]
|
||||||
|
- "회의록 수정" 버튼 (옵션 1)
|
||||||
|
- "바로 최종 확정" 버튼 (옵션 2, Primary)
|
||||||
|
- "대시보드로 이동" 버튼 (옵션 3)
|
||||||
|
|
||||||
|
[AI Todo 추출 결과 섹션 수정안]
|
||||||
|
- "Todo 수정" 버튼 제거
|
||||||
|
- 확인만 가능하도록 readonly 처리
|
||||||
|
|
||||||
|
[안건별 AI 요약 표시 추가]
|
||||||
|
- 각 안건별 카드로 표시
|
||||||
|
- 안건 제목
|
||||||
|
- AI 한줄 요약 (읽기 전용)
|
||||||
|
- 상세 요약 (읽기 전용)
|
||||||
|
- Todo 목록 (읽기 전용)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 11-회의록수정
|
||||||
|
|
||||||
|
#### 유저스토리 v2.1.2 요구사항
|
||||||
|
- **UFR-MEET-055 (회의록수정)**
|
||||||
|
- 진입 경로: 10-회의록상세조회 → "수정" 버튼 클릭
|
||||||
|
- **안건 기반 회의록 구조**:
|
||||||
|
- 각 안건별 섹션
|
||||||
|
- 안건별 AI 한줄 요약 (편집 불가)
|
||||||
|
- 안건별 상세 요약 (편집 가능)
|
||||||
|
- 안건별 검증 상태 (체크박스)
|
||||||
|
- 수정 가능 항목:
|
||||||
|
- ✅ 회의 제목
|
||||||
|
- ❌ 회의 일시/장소 (readonly)
|
||||||
|
- ✅ 참석자 목록 (회의 생성자만)
|
||||||
|
- ✅ 안건별 AI 요약 (AI 재생성)
|
||||||
|
- ✅ 안건별 내용
|
||||||
|
- ✅ 관련회의록
|
||||||
|
- 검증완료 안건: 회의 생성자만 잠금 해제 후 수정 가능
|
||||||
|
|
||||||
|
- **UFR-AI-036 (AI한줄요약)**
|
||||||
|
- 각 안건마다 편집 불가능한 AI 한줄 요약 제공
|
||||||
|
- 30자 이내 간결한 표현
|
||||||
|
- 회의 종료 시 1회 생성, 생성 후 편집 불가
|
||||||
|
|
||||||
|
- **UFR-COLLAB-030 (검증완료)**
|
||||||
|
- 안건별 검증 완료 처리
|
||||||
|
- 11-회의록수정 화면에서 안건별 검증 처리
|
||||||
|
- 별도의 06-검증완료 화면 불필요
|
||||||
|
|
||||||
|
#### UI/UX 설계서 내용
|
||||||
|
- **주요 기능**:
|
||||||
|
1. 회의 기본 정보 표시 및 수정
|
||||||
|
2. 회의록 내용 수정 **(섹션별)** ← 안건별이 아닌 섹션별
|
||||||
|
3. AI 요약 수정 (섹션별)
|
||||||
|
4. 참고자료 편집
|
||||||
|
5. Todo 수정 (회의 생성자만)
|
||||||
|
6. 자동 저장
|
||||||
|
|
||||||
|
- **섹션 구조**:
|
||||||
|
- 섹션 1 편집: "1. 신제품 기획 방향"
|
||||||
|
- 섹션 2 편집: "2. 개발 일정 및 리소스"
|
||||||
|
- 섹션 3 편집: "3. 마케팅 전략"
|
||||||
|
- ← "섹션" 용어 사용, "안건" 용어 없음
|
||||||
|
|
||||||
|
- **AI 요약 편집**:
|
||||||
|
- AI 요약 텍스트 필드 (편집 가능)
|
||||||
|
- "AI 재생성" 버튼
|
||||||
|
- ← AI 한줄 요약 (편집 불가) 항목 없음
|
||||||
|
|
||||||
|
- **검증 완료 표시**:
|
||||||
|
- 체크박스 (검증 완료, disabled)
|
||||||
|
- 🔒 읽기 전용 배지
|
||||||
|
- ← 섹션별 검증, 안건별 검증 아님
|
||||||
|
|
||||||
|
#### 프로토타입 구현 상태 (11-회의록수정.html)
|
||||||
|
```html
|
||||||
|
<!-- 섹션 1 편집 -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h3 class="section-title">
|
||||||
|
1. 신제품 기획 방향
|
||||||
|
<span class="badge badge-complete">검증완료</span>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- AI 요약 편집 -->
|
||||||
|
<div class="ai-summary-edit">
|
||||||
|
<div class="ai-summary-header">
|
||||||
|
<span class="ai-summary-label">💡 AI 요약</span>
|
||||||
|
<button class="btn-secondary btn-sm" onclick="regenerateSummary(1)">AI 재생성</button>
|
||||||
|
</div>
|
||||||
|
<textarea class="ai-summary-textarea" readonly>
|
||||||
|
신제품은 AI 기반 회의록 자동화 서비스로 결정...
|
||||||
|
</textarea>
|
||||||
|
<!-- ← AI 한줄 요약 (편집 불가) 항목 없음 -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 검증 완료 (읽기 전용) -->
|
||||||
|
<div class="verification-lock">
|
||||||
|
<input type="checkbox" class="checkbox" id="verify-1" checked disabled>
|
||||||
|
<label for="verify-1">
|
||||||
|
<span class="font-medium">검증 완료</span>
|
||||||
|
<span class="text-caption text-muted"> (잠금됨 · 회의 생성자만 수정 가능)</span>
|
||||||
|
</label>
|
||||||
|
<span class="readonly-badge">🔒 읽기 전용</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 불일치 사항
|
||||||
|
1. 🟡 **안건 기반 구조 미반영**
|
||||||
|
- 유저스토리: "안건별" 회의록 구조
|
||||||
|
- UI/UX 설계서 & 프로토타입: "섹션별" 구조
|
||||||
|
- **수정 필요**: "섹션" 용어를 "안건"으로 통일
|
||||||
|
|
||||||
|
2. 🟡 **안건별 AI 한줄 요약 미표시**
|
||||||
|
- 유저스토리: 각 안건마다 편집 불가능한 AI 한줄 요약 (30자 이내)
|
||||||
|
- 프로토타입: AI 요약은 있지만 "한줄 요약"과 "상세 요약" 구분 없음
|
||||||
|
- **수정 필요**:
|
||||||
|
- AI 한줄 요약 (읽기 전용, 30자) 추가
|
||||||
|
- 기존 AI 요약을 "상세 요약"으로 명칭 변경
|
||||||
|
|
||||||
|
3. 🟡 **안건별 검증 UI 구현 상태**
|
||||||
|
- 유저스토리: 11-회의록수정 화면에서 안건별 검증 처리
|
||||||
|
- 프로토타입: 검증완료 체크박스 있으나 disabled (수정 불가)
|
||||||
|
- **검토 필요**:
|
||||||
|
- 회의 생성자일 때 체크박스 활성화 필요
|
||||||
|
- 잠금 해제 버튼 추가 고려
|
||||||
|
|
||||||
|
#### 권장 수정사항
|
||||||
|
```
|
||||||
|
[안건 구조 수정안]
|
||||||
|
<!-- 안건 1 편집 -->
|
||||||
|
<div class="agenda-section">
|
||||||
|
<div class="agenda-header">
|
||||||
|
<h3 class="agenda-title">
|
||||||
|
안건 1. 신제품 기획 방향
|
||||||
|
<span class="badge badge-complete">검증완료</span>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- AI 한줄 요약 (편집 불가) -->
|
||||||
|
<div class="ai-oneline-summary-readonly">
|
||||||
|
<span class="ai-icon">✨</span>
|
||||||
|
<span class="summary-text">AI 기반 회의록 서비스 개발 방향 결정</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- AI 상세 요약 (편집 가능) -->
|
||||||
|
<div class="ai-summary-edit">
|
||||||
|
<div class="ai-summary-header">
|
||||||
|
<span class="ai-summary-label">💡 AI 상세 요약</span>
|
||||||
|
<button class="btn-secondary btn-sm">AI 재생성</button>
|
||||||
|
</div>
|
||||||
|
<textarea class="ai-summary-textarea">...</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 안건별 검증 (회의 생성자만 활성화) -->
|
||||||
|
<div class="agenda-verification">
|
||||||
|
<input type="checkbox" id="verify-agenda-1" checked>
|
||||||
|
<label>안건 검증 완료</label>
|
||||||
|
<button class="btn-ghost btn-sm" v-if="isCreator && isLocked">잠금 해제</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 06-검증완료
|
||||||
|
|
||||||
|
#### 유저스토리 v2.1.2 요구사항
|
||||||
|
- **UFR-COLLAB-030 (검증완료)**:
|
||||||
|
- 11-회의록수정 화면에서 안건별 검증 처리
|
||||||
|
- **별도의 06-검증완료 화면 불필요**
|
||||||
|
|
||||||
|
#### UI/UX 설계서 내용
|
||||||
|
- **화면 존재**: 06-검증완료 화면 정의됨
|
||||||
|
- 주요 기능:
|
||||||
|
1. 섹션별 검증 상태 표시
|
||||||
|
2. 검증 완료 체크 (참석자별)
|
||||||
|
3. 미검증 섹션 안내
|
||||||
|
4. 섹션 잠금 (회의 생성자만)
|
||||||
|
|
||||||
|
#### 프로토타입 구현 상태
|
||||||
|
- **파일 존재**: `06-검증완료.html` (528줄)
|
||||||
|
- 주요 기능 구현:
|
||||||
|
- 섹션별 검증 카드
|
||||||
|
- 검증 완료 버튼
|
||||||
|
- 잠금 해제 버튼
|
||||||
|
- 진행률 표시
|
||||||
|
|
||||||
|
#### 불일치 사항
|
||||||
|
1. 🔴 **06-검증완료 화면 삭제 필요**
|
||||||
|
- 유저스토리: 별도 화면 불필요, 11-회의록수정에 통합
|
||||||
|
- UI/UX 설계서 & 프로토타입: 06-검증완료 화면 존재
|
||||||
|
- **수정 필요**:
|
||||||
|
- 06-검증완료.html 파일 삭제
|
||||||
|
- UI/UX 설계서에서 해당 화면 설명 제거
|
||||||
|
- 모든 링크 및 내비게이션에서 제거
|
||||||
|
|
||||||
|
#### 권장 수정사항
|
||||||
|
```
|
||||||
|
1. 프로토타입 파일 삭제
|
||||||
|
- design/uiux/prototype/06-검증완료.html 삭제
|
||||||
|
|
||||||
|
2. UI/UX 설계서 수정
|
||||||
|
- "### 06-검증완료" 섹션 전체 삭제
|
||||||
|
- 프로토타입 화면 목록 테이블에서 제거
|
||||||
|
|
||||||
|
3. 11-회의록수정 화면에 검증 기능 통합
|
||||||
|
- 각 안건별 검증 체크박스 추가
|
||||||
|
- 회의 생성자는 검증 상태 변경 가능
|
||||||
|
- 참석자는 자신의 검증만 처리 가능
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 10-회의록상세조회
|
||||||
|
|
||||||
|
#### 유저스토리 v2.1.2 요구사항
|
||||||
|
- **UFR-MEET-047 (회의록상세조회)**
|
||||||
|
- 회의 기본 정보 표시 (제목, 일시, 참석자, 장소, 상태)
|
||||||
|
- **안건별 상세 내용 표시** (섹션별 → 안건별)
|
||||||
|
- AI 요약 섹션 (안건별)
|
||||||
|
- 상세 내용 섹션 (논의 사항, 결정 사항 등)
|
||||||
|
- 관련 회의록 섹션
|
||||||
|
|
||||||
|
#### UI/UX 설계서 내용
|
||||||
|
- 탭 구성: 대시보드 / 회의록 (기본: 대시보드)
|
||||||
|
- **대시보드 탭**:
|
||||||
|
- 핵심내용 카드 (AI 요약)
|
||||||
|
- 결정사항 카드
|
||||||
|
- Todo 진행상황 카드
|
||||||
|
- 참고자료 카드
|
||||||
|
- **회의록 탭**:
|
||||||
|
- 회의 기본 정보
|
||||||
|
- 섹션별 AI 요약 및 내용 ← 안건별이 아님
|
||||||
|
|
||||||
|
#### 프로토타입 구현 상태 (10-회의록상세조회.html)
|
||||||
|
- 탭: 대시보드 / 회의록
|
||||||
|
- 섹션 구조 (회의록 탭):
|
||||||
|
```html
|
||||||
|
<div class="section">
|
||||||
|
<h3 class="section-title">1. 신제품 기획 방향</h3>
|
||||||
|
<div class="ai-summary">...</div>
|
||||||
|
<div class="section-content">...</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 불일치 사항
|
||||||
|
1. 🟢 **안건별 표시 명시 부족**
|
||||||
|
- 유저스토리: "안건별 상세 내용 표시"
|
||||||
|
- UI/UX 설계서 & 프로토타입: "섹션별" 용어 사용
|
||||||
|
- **수정 필요**: "섹션"을 "안건"으로 명칭 변경
|
||||||
|
|
||||||
|
#### 권장 수정사항
|
||||||
|
```
|
||||||
|
[UI/UX 설계서 수정]
|
||||||
|
- "섹션별 상세 내용 표시" → "안건별 상세 내용 표시"
|
||||||
|
- "섹션별 AI 요약" → "안건별 AI 요약"
|
||||||
|
|
||||||
|
[프로토타입 수정]
|
||||||
|
- class="section" → class="agenda"
|
||||||
|
- class="section-title" → class="agenda-title"
|
||||||
|
- HTML 주석 및 변수명 일괄 변경
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 05-회의진행
|
||||||
|
|
||||||
|
#### 유저스토리 v2.1.2 요구사항
|
||||||
|
- **UFR-AI-010 (회의록자동작성) - 시나리오 1**:
|
||||||
|
- **실시간 AI 주요 메모 작성**
|
||||||
|
- 텍스트 변환되면 자동으로 주요 메모 항목 추천
|
||||||
|
- 실시간 업데이트 (3-5초 간격)
|
||||||
|
- 참석자가 필요한 항목만 선택하여 저장
|
||||||
|
|
||||||
|
#### UI/UX 설계서 내용
|
||||||
|
- 주요 기능:
|
||||||
|
1. 음성 녹음 및 STT
|
||||||
|
2. 회의록 실시간 편집
|
||||||
|
3. 참석자 목록 관리
|
||||||
|
4. AI 제안 기능 (우측 탭)
|
||||||
|
- **데이터 출력**:
|
||||||
|
- 실시간 텍스트 변환 결과 (STT)
|
||||||
|
- 편집된 회의록 내용
|
||||||
|
- AI 제안 목록 (회의록 개선 제안) ← 주요 메모 추천과 차이
|
||||||
|
|
||||||
|
#### 프로토타입 구현 상태 (05-회의진행.html)
|
||||||
|
- 우측 탭:
|
||||||
|
- 참석자
|
||||||
|
- AI 제안 (논의사항 제안, 결정사항 제안, 액션아이템 제안)
|
||||||
|
- 용어 사전
|
||||||
|
- 관련 자료
|
||||||
|
- ← 실시간 주요 메모 추천 기능 명시 없음
|
||||||
|
|
||||||
|
#### 불일치 사항
|
||||||
|
1. 🟢 **실시간 주요 메모 추천 기능 명시 부족**
|
||||||
|
- 유저스토리: 실시간 AI 주요 메모 항목 추천 (3-5초 간격)
|
||||||
|
- UI/UX 설계서: "AI 제안 목록 (회의록 개선 제안)"
|
||||||
|
- 프로토타입: 논의사항/결정사항/액션아이템 제안만 있음
|
||||||
|
- **검토 필요**:
|
||||||
|
- 실시간 주요 메모 추천 기능이 "AI 제안"에 포함된 것인지 명확화
|
||||||
|
- 별도 UI 필요 여부 검토
|
||||||
|
|
||||||
|
#### 권장 수정사항
|
||||||
|
```
|
||||||
|
[UI/UX 설계서 명확화]
|
||||||
|
- "AI 제안 기능" 섹션에 다음 추가:
|
||||||
|
"실시간 AI 주요 메모 추천:
|
||||||
|
- 텍스트 변환 후 3-5초 간격으로 주요 메모 항목 자동 추천
|
||||||
|
- 참석자가 선택하여 저장
|
||||||
|
- 우측 'AI 제안' 탭에서 확인 가능"
|
||||||
|
|
||||||
|
[프로토타입 검토]
|
||||||
|
- 현재 AI 제안 탭 기능이 실시간 주요 메모 추천인지 확인
|
||||||
|
- 필요 시 별도 UI 컴포넌트 추가
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 12-회의록목록조회
|
||||||
|
|
||||||
|
#### 유저스토리 v2.1.2 요구사항
|
||||||
|
- **UFR-MEET-046 (회의록목록조회)**
|
||||||
|
- 필터: 참여 유형(참석한/생성한), 상태(전체/작성중/확정완료)
|
||||||
|
- 목록 표시 정보:
|
||||||
|
- 회의 제목
|
||||||
|
- 회의 일시
|
||||||
|
- 참석자 수
|
||||||
|
- 회의록 상태
|
||||||
|
- 검증 완료율
|
||||||
|
- **생성자 표시 (👑 아이콘)** ← v2.1.2에서 추가됨
|
||||||
|
- 마지막 수정 시간
|
||||||
|
|
||||||
|
#### UI/UX 설계서 내용
|
||||||
|
- 필터 및 정렬:
|
||||||
|
- 참여 유형: 참석한 회의 / 생성한 회의
|
||||||
|
- 상태: 전체 / 작성중 / 확정완료
|
||||||
|
- 목록 카드 정보:
|
||||||
|
- 회의 제목
|
||||||
|
- 날짜/시간
|
||||||
|
- 참석자 수
|
||||||
|
- 상태 배지
|
||||||
|
- 검증률 (작성중인 경우)
|
||||||
|
- ← 생성자 표시 명시 없음
|
||||||
|
|
||||||
|
#### 프로토타입 구현 상태 (12-회의록목록조회.html)
|
||||||
|
- 필터 및 정렬 구현됨
|
||||||
|
- 회의록 카드:
|
||||||
|
```javascript
|
||||||
|
// common.js - renderMinuteCard 함수
|
||||||
|
<div class="minute-card">
|
||||||
|
<div class="minute-title">${minute.title}</div>
|
||||||
|
<!-- 생성자 표시 로직 없음 -->
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 불일치 사항
|
||||||
|
1. 🟡 **생성자 표시 미반영**
|
||||||
|
- 유저스토리: 생성자 표시 (👑 아이콘)
|
||||||
|
- UI/UX 설계서: 명시 없음
|
||||||
|
- 프로토타입: 구현 없음
|
||||||
|
- **수정 필요**:
|
||||||
|
- 생성자 표시 UI 추가
|
||||||
|
- 현재 사용자가 생성자일 경우 👑 아이콘 표시
|
||||||
|
|
||||||
|
#### 권장 수정사항
|
||||||
|
```
|
||||||
|
[UI/UX 설계서 수정]
|
||||||
|
"목록 표시 정보" 섹션에 추가:
|
||||||
|
- 생성자 표시: 현재 사용자가 회의 생성자인 경우 👑 아이콘 표시
|
||||||
|
|
||||||
|
[프로토타입 수정 - common.js]
|
||||||
|
function renderMinuteCard(minute, currentUserId) {
|
||||||
|
const isCreator = minute.creatorId === currentUserId;
|
||||||
|
return `
|
||||||
|
<div class="minute-card">
|
||||||
|
<div class="minute-header">
|
||||||
|
<h3 class="minute-title">
|
||||||
|
${minute.title}
|
||||||
|
${isCreator ? '<span class="creator-badge">👑</span>' : ''}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
...
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 용어 사용 불일치
|
||||||
|
|
||||||
|
### "작성자" vs "생성자"/"참석자"
|
||||||
|
|
||||||
|
#### 유저스토리 v2.1.2 용어 정책
|
||||||
|
- **v2.1.2 주요 변경사항**: 역할 용어 통일
|
||||||
|
- "작성자" → "회의 생성자" 또는 "참석자"
|
||||||
|
- 회의를 만든 사람: **회의 생성자** (creator)
|
||||||
|
- 회의에 참여한 사람: **참석자** (attendee)
|
||||||
|
|
||||||
|
#### UI/UX 설계서 용어 사용 현황
|
||||||
|
- **일관성 있는 곳**: 대부분 "회의 생성자", "참석자" 사용
|
||||||
|
- **"작성자" 사용 위치**:
|
||||||
|
1. UFR-TODO-020 (Todo완료): "Todo 작성자에게 완료 알림 발송"
|
||||||
|
2. UFR-TODO-040 (Todo관리): "담당자 본인 OR 회의 작성자인 경우에만 노출"
|
||||||
|
3. 일부 화면 설명에서 혼용
|
||||||
|
|
||||||
|
#### 프로토타입 용어 사용 현황
|
||||||
|
- 대부분 "회의 생성자" 사용
|
||||||
|
- 일부 주석에서 "작성자" 사용
|
||||||
|
|
||||||
|
#### 불일치 사항
|
||||||
|
1. 🟡 **용어 통일 필요**
|
||||||
|
- 유저스토리: "회의 생성자" 일관 사용
|
||||||
|
- UI/UX 설계서: 일부 "작성자" 혼재
|
||||||
|
- **수정 필요**: 모든 "작성자"를 "회의 생성자"로 변경
|
||||||
|
|
||||||
|
#### 권장 수정사항
|
||||||
|
```
|
||||||
|
[UI/UX 설계서 일괄 변경]
|
||||||
|
1. 검색 및 치환:
|
||||||
|
- "Todo 작성자" → "Todo 담당자" 또는 "회의 생성자"
|
||||||
|
- "회의 작성자" → "회의 생성자"
|
||||||
|
- "회의록 작성자" → "회의 생성자"
|
||||||
|
|
||||||
|
2. 컨텍스트별 명확화:
|
||||||
|
- 회의를 만든 사람: "회의 생성자"
|
||||||
|
- Todo를 만든 사람: "Todo 담당자"
|
||||||
|
- 회의에 참여한 사람: "참석자"
|
||||||
|
|
||||||
|
[프로토타입 수정]
|
||||||
|
- 주석 및 변수명에서 "작성자" → "생성자" 변경
|
||||||
|
- 예: creator, isCreator 등으로 통일
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 권장 수정사항 우선순위
|
||||||
|
|
||||||
|
### Phase 1: 즉시 수정 (🔴 높음)
|
||||||
|
|
||||||
|
#### 1.1 07-회의종료 화면 전면 개편
|
||||||
|
**담당**: 강지수 (Product Designer)
|
||||||
|
**공수**: 2일
|
||||||
|
**작업 내역**:
|
||||||
|
1. 프로토타입 수정 (07-회의종료.html):
|
||||||
|
- 안건별 AI 요약 표시 추가
|
||||||
|
- "Todo 수정" 버튼 제거
|
||||||
|
- 하단 액션 3가지 옵션 구현:
|
||||||
|
- "회의록 수정" (옵션 1)
|
||||||
|
- "바로 최종 확정" (옵션 2, Primary)
|
||||||
|
- "대시보드로 이동" (옵션 3)
|
||||||
|
- "바로 최종 확정" 버튼 클릭 시 모든 안건 자동 검증 처리 로직 추가
|
||||||
|
|
||||||
|
2. UI/UX 설계서 수정 (uiux.md):
|
||||||
|
- "07-회의종료" 섹션 전체 재작성
|
||||||
|
- 편집 불가 정책 명시
|
||||||
|
- 안건별 AI 요약 표시 설명 추가
|
||||||
|
- 3가지 선택 옵션 상세 설명
|
||||||
|
- 각 옵션별 회의록 상태 변경 로직 명시
|
||||||
|
|
||||||
|
#### 1.2 06-검증완료 화면 삭제
|
||||||
|
**담당**: 강지수 (Product Designer)
|
||||||
|
**공수**: 0.5일
|
||||||
|
**작업 내역**:
|
||||||
|
1. 파일 삭제:
|
||||||
|
- design/uiux/prototype/06-검증완료.html 삭제
|
||||||
|
|
||||||
|
2. UI/UX 설계서 수정:
|
||||||
|
- "### 06-검증완료" 섹션 전체 삭제
|
||||||
|
- 프로토타입 화면 목록 테이블에서 제거
|
||||||
|
- 모든 화면에서 06-검증완료 링크 제거
|
||||||
|
|
||||||
|
3. 11-회의록수정 화면에 검증 기능 통합:
|
||||||
|
- 각 안건별 검증 체크박스 UI 추가
|
||||||
|
- 권한별 활성화 로직 구현
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 2: 우선 수정 (🟡 중간)
|
||||||
|
|
||||||
|
#### 2.1 11-회의록수정 화면 안건 기반 구조 전환
|
||||||
|
**담당**: 강지수 (Product Designer)
|
||||||
|
**공수**: 3일
|
||||||
|
**작업 내역**:
|
||||||
|
1. 프로토타입 수정 (11-회의록수정.html):
|
||||||
|
- "섹션" → "안건" 용어 변경
|
||||||
|
- AI 한줄 요약 (읽기 전용) UI 추가
|
||||||
|
- AI 상세 요약 (편집 가능) 명칭 변경
|
||||||
|
- 안건별 검증 체크박스 추가
|
||||||
|
- 잠금 해제 버튼 추가 (회의 생성자만)
|
||||||
|
|
||||||
|
2. UI/UX 설계서 수정:
|
||||||
|
- "### 11-회의록수정" 섹션 재작성
|
||||||
|
- 안건 기반 구조 설명 추가
|
||||||
|
- AI 한줄 요약 vs 상세 요약 구분 설명
|
||||||
|
- 안건별 검증 UI 설명 추가
|
||||||
|
|
||||||
|
3. 스타일 가이드 업데이트 (style-guide.md):
|
||||||
|
- 안건 카드 컴포넌트 추가
|
||||||
|
- AI 한줄 요약 스타일 정의
|
||||||
|
- 안건별 검증 UI 스타일 정의
|
||||||
|
|
||||||
|
#### 2.2 용어 통일 ("작성자" → "생성자")
|
||||||
|
**담당**: 강지수 (Product Designer)
|
||||||
|
**공수**: 1일
|
||||||
|
**작업 내역**:
|
||||||
|
1. UI/UX 설계서 일괄 변경:
|
||||||
|
- "작성자" 검색 및 컨텍스트 확인
|
||||||
|
- "회의 생성자" 또는 "Todo 담당자"로 변경
|
||||||
|
|
||||||
|
2. 프로토타입 수정:
|
||||||
|
- 주석 및 변수명 일괄 변경
|
||||||
|
- creator, isCreator 등으로 통일
|
||||||
|
|
||||||
|
3. 용어 사전 추가 (uiux.md):
|
||||||
|
- "회의 생성자": 회의를 생성한 사람
|
||||||
|
- "참석자": 회의에 참여한 사람
|
||||||
|
- "Todo 담당자": Todo를 담당하는 사람
|
||||||
|
|
||||||
|
#### 2.3 회의록목록조회 생성자 표시 추가
|
||||||
|
**담당**: 강지수 (Product Designer)
|
||||||
|
**공수**: 0.5일
|
||||||
|
**작업 내역**:
|
||||||
|
1. 프로토타입 수정 (common.js):
|
||||||
|
- renderMinuteCard 함수에 생성자 표시 로직 추가
|
||||||
|
- 👑 아이콘 추가
|
||||||
|
|
||||||
|
2. UI/UX 설계서 수정:
|
||||||
|
- 목록 표시 정보에 "생성자 표시" 항목 추가
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 3: 검토 후 수정 (🟢 낮음)
|
||||||
|
|
||||||
|
#### 3.1 05-회의진행 화면 실시간 주요 메모 추천 명확화
|
||||||
|
**담당**: 강지수, 도그냥
|
||||||
|
**공수**: 1일
|
||||||
|
**작업 내역**:
|
||||||
|
1. 현재 구현 검토:
|
||||||
|
- AI 제안 탭 기능이 실시간 주요 메모 추천인지 확인
|
||||||
|
- 유저스토리와 일치하는지 검증
|
||||||
|
|
||||||
|
2. UI/UX 설계서 명확화:
|
||||||
|
- 실시간 주요 메모 추천 기능 설명 추가
|
||||||
|
- AI 제안 탭과의 관계 명시
|
||||||
|
|
||||||
|
3. 필요 시 프로토타입 수정:
|
||||||
|
- 별도 UI 컴포넌트 추가
|
||||||
|
- 3-5초 간격 업데이트 로직 구현
|
||||||
|
|
||||||
|
#### 3.2 10-회의록상세조회 안건별 표시 명칭 변경
|
||||||
|
**담당**: 강지수
|
||||||
|
**공수**: 0.5일
|
||||||
|
**작업 내역**:
|
||||||
|
1. UI/UX 설계서 수정:
|
||||||
|
- "섹션별" → "안건별" 용어 변경
|
||||||
|
|
||||||
|
2. 프로토타입 수정:
|
||||||
|
- class 명칭 변경
|
||||||
|
- 주석 및 변수명 변경
|
||||||
|
|
||||||
|
#### 3.3 스타일 가이드 안건 컴포넌트 추가
|
||||||
|
**담당**: 강지수
|
||||||
|
**공수**: 1일
|
||||||
|
**작업 내역**:
|
||||||
|
1. style-guide.md 수정:
|
||||||
|
- 안건 카드 컴포넌트 스타일 정의
|
||||||
|
- AI 한줄 요약 스타일 추가
|
||||||
|
- 안건별 검증 UI 스타일 추가
|
||||||
|
- 예시 코드 작성
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 총 작업 공수 및 일정
|
||||||
|
|
||||||
|
### 공수 요약
|
||||||
|
- **Phase 1 (즉시 수정)**: 2.5일
|
||||||
|
- 07-회의종료 화면 전면 개편: 2일
|
||||||
|
- 06-검증완료 화면 삭제: 0.5일
|
||||||
|
|
||||||
|
- **Phase 2 (우선 수정)**: 4.5일
|
||||||
|
- 11-회의록수정 안건 기반 구조 전환: 3일
|
||||||
|
- 용어 통일: 1일
|
||||||
|
- 생성자 표시 추가: 0.5일
|
||||||
|
|
||||||
|
- **Phase 3 (검토 후 수정)**: 2.5일
|
||||||
|
- 실시간 주요 메모 추천 명확화: 1일
|
||||||
|
- 안건별 표시 명칭 변경: 0.5일
|
||||||
|
- 스타일 가이드 업데이트: 1일
|
||||||
|
|
||||||
|
**총 공수**: 9.5일
|
||||||
|
|
||||||
|
### 권장 일정
|
||||||
|
- **Week 1 (5일)**: Phase 1 완료 + Phase 2 시작
|
||||||
|
- Day 1-2: 07-회의종료 전면 개편
|
||||||
|
- Day 3: 06-검증완료 삭제 + 용어 통일
|
||||||
|
- Day 4-5: 11-회의록수정 안건 구조 전환 (50%)
|
||||||
|
|
||||||
|
- **Week 2 (4.5일)**: Phase 2 완료 + Phase 3
|
||||||
|
- Day 6-7: 11-회의록수정 안건 구조 전환 완료
|
||||||
|
- Day 8: 생성자 표시 추가 + Phase 3 검토
|
||||||
|
- Day 9-10: Phase 3 수정 작업
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 체크리스트
|
||||||
|
|
||||||
|
### Phase 1 완료 체크리스트
|
||||||
|
- [ ] 07-회의종료.html 안건별 AI 요약 표시 구현
|
||||||
|
- [ ] 07-회의종료.html "Todo 수정" 버튼 제거
|
||||||
|
- [ ] 07-회의종료.html 3가지 선택 옵션 구현
|
||||||
|
- [ ] 07-회의종료.html "바로 최종 확정" 로직 구현
|
||||||
|
- [ ] uiux.md "07-회의종료" 섹션 재작성
|
||||||
|
- [ ] 06-검증완료.html 파일 삭제
|
||||||
|
- [ ] uiux.md "06-검증완료" 섹션 삭제
|
||||||
|
- [ ] 프로토타입 화면 목록 테이블 업데이트
|
||||||
|
|
||||||
|
### Phase 2 완료 체크리스트
|
||||||
|
- [ ] 11-회의록수정.html "섹션" → "안건" 용어 변경
|
||||||
|
- [ ] 11-회의록수정.html AI 한줄 요약 UI 추가
|
||||||
|
- [ ] 11-회의록수정.html 안건별 검증 체크박스 추가
|
||||||
|
- [ ] uiux.md "11-회의록수정" 섹션 재작성
|
||||||
|
- [ ] uiux.md 전체 "작성자" → "생성자" 변경
|
||||||
|
- [ ] 프로토타입 용어 통일 (creator, isCreator)
|
||||||
|
- [ ] common.js 생성자 표시 로직 추가
|
||||||
|
- [ ] uiux.md "12-회의록목록조회" 생성자 표시 설명 추가
|
||||||
|
|
||||||
|
### Phase 3 완료 체크리스트
|
||||||
|
- [ ] 05-회의진행 실시간 주요 메모 추천 검토 완료
|
||||||
|
- [ ] uiux.md 실시간 주요 메모 추천 설명 추가
|
||||||
|
- [ ] 10-회의록상세조회 "섹션" → "안건" 변경
|
||||||
|
- [ ] style-guide.md 안건 컴포넌트 스타일 추가
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 결론
|
||||||
|
|
||||||
|
### 주요 발견사항
|
||||||
|
1. 유저스토리 v2.1.2의 핵심 변경사항인 **"안건 기반 회의록 구조"**가 UI/UX 설계서와 프로토타입에 충분히 반영되지 않았습니다.
|
||||||
|
|
||||||
|
2. **07-회의종료 화면**의 "확인 전용" 정책과 "3가지 선택 옵션"이 구현되지 않아, 사용자 경험에 큰 영향을 미칠 수 있습니다.
|
||||||
|
|
||||||
|
3. **06-검증완료 화면**이 여전히 존재하여, 유저스토리의 "11-회의록수정 통합" 방침과 불일치합니다.
|
||||||
|
|
||||||
|
4. **용어 사용**이 일부 혼재되어 있어, 전체적인 일관성 개선이 필요합니다.
|
||||||
|
|
||||||
|
### 권장사항
|
||||||
|
1. **Phase 1 (즉시 수정)** 항목을 최우선으로 처리하여 핵심 사용자 플로우를 유저스토리와 일치시켜야 합니다.
|
||||||
|
|
||||||
|
2. **Phase 2 (우선 수정)** 항목은 전체적인 일관성과 정확성을 위해 2주 내 완료를 권장합니다.
|
||||||
|
|
||||||
|
3. **Phase 3 (검토 후 수정)** 항목은 검토 과정에서 실제 불일치 여부를 확인한 후 수정 여부를 결정하시기 바랍니다.
|
||||||
|
|
||||||
|
4. 모든 수정 작업 후 **통합 테스트**를 통해 유저스토리, UI/UX 설계서, 프로토타입 간 완전한 일관성을 확보해야 합니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**보고서 종료**
|
||||||
@ -49,7 +49,7 @@
|
|||||||
- 실시간 협업: WebSocket 기반 실시간 동기화, 버전 관리, 충돌 해결
|
- 실시간 협업: WebSocket 기반 실시간 동기화, 버전 관리, 충돌 해결
|
||||||
- 템플릿 관리: 회의록 템플릿 관리
|
- 템플릿 관리: 회의록 템플릿 관리
|
||||||
- 통계 생성: 회의 및 Todo 통계
|
- 통계 생성: 회의 및 Todo 통계
|
||||||
3. **STT** - 음성 녹음 관리, 음성-텍스트 변환, 화자 식별 (기본 기능)
|
3. **STT** - 음성 스트리밍 처리, 실시간 음성-텍스트 변환 (기본 기능)
|
||||||
4. **AI** - AI 기반 회의록 자동화, Todo 추출, 지능형 검색 (RAG 통합)
|
4. **AI** - AI 기반 회의록 자동화, Todo 추출, 지능형 검색 (RAG 통합)
|
||||||
- LLM 기반 회의록 자동 작성
|
- LLM 기반 회의록 자동 작성
|
||||||
- Todo 자동 추출 및 담당자 식별
|
- Todo 자동 추출 및 담당자 식별
|
||||||
@ -216,7 +216,6 @@ UFR-MEET-040: [회의종료] 회의 생성자로서 | 나는, 회의를 종료
|
|||||||
- 회의 통계 자동 생성
|
- 회의 통계 자동 생성
|
||||||
- 회의 총 시간
|
- 회의 총 시간
|
||||||
- 참석자 수
|
- 참석자 수
|
||||||
- 발언 횟수 (화자별)
|
|
||||||
- 주요 키워드
|
- 주요 키워드
|
||||||
|
|
||||||
[처리 결과]
|
[처리 결과]
|
||||||
@ -478,30 +477,29 @@ UFR-MEET-055: [회의록수정] 회의 참석자로서 | 나는, 검증이 완
|
|||||||
3. STT 서비스 (음성 인식 및 변환 - 기본 기능)
|
3. STT 서비스 (음성 인식 및 변환 - 기본 기능)
|
||||||
1) 음성 인식 및 변환
|
1) 음성 인식 및 변환
|
||||||
UFR-STT-010: [음성녹음인식] 회의 참석자로서 | 나는, 발언 내용이 자동으로 기록되기 위해 | 음성이 실시간으로 녹음되고 인식되기를 원한다.
|
UFR-STT-010: [음성녹음인식] 회의 참석자로서 | 나는, 발언 내용이 자동으로 기록되기 위해 | 음성이 실시간으로 녹음되고 인식되기를 원한다.
|
||||||
- 시나리오: 음성 녹음 및 발언 인식
|
- 시나리오: 음성 실시간 인식
|
||||||
회의가 시작된 상황에서 | 참석자가 발언을 시작하면 | 음성이 자동으로 녹음되고 화자가 식별되며 발언이 인식된다.
|
회의가 시작된 상황에서 | 참석자가 발언을 시작하면 | 음성이 실시간으로 텍스트로 변환된다.
|
||||||
|
|
||||||
[음성 녹음 처리]
|
[음성 스트리밍 처리]
|
||||||
- 오디오 스트림 실시간 캡처
|
- 오디오 스트림 실시간 캡처
|
||||||
- 회의 ID와 연결
|
- 회의 ID와 연결
|
||||||
- 음성 데이터 저장 (Azure 스토리지)
|
- **음성 파일은 저장하지 않음** (실시간 스트리밍만 처리)
|
||||||
|
|
||||||
[발언 인식 처리]
|
[음성 인식 처리]
|
||||||
- AI 음성인식 엔진 연동 (Azure Speech 등)
|
- AI 음성인식 엔진 연동 (Azure Speech 등)
|
||||||
- 화자 자동 식별
|
- 실시간 텍스트 변환
|
||||||
- 참석자 목록 매칭
|
|
||||||
- 음성 특징 분석
|
|
||||||
- 타임스탬프 기록
|
- 타임스탬프 기록
|
||||||
- 발언 구간 구분
|
|
||||||
|
|
||||||
[처리 결과]
|
[처리 결과]
|
||||||
- 음성 녹음이 시작됨 (녹음 ID)
|
- 음성 스트리밍이 시작됨 (세션 ID)
|
||||||
- 발언이 인식됨 (발언 ID, 화자, 타임스탬프)
|
- 텍스트가 변환됨 (세그먼트 ID, 텍스트, 타임스탬프)
|
||||||
- 실시간으로 텍스트 변환 요청 (UFR-STT-020 연동)
|
- 실시간으로 텍스트 변환 요청 (UFR-STT-020 연동)
|
||||||
|
- **음성 파일은 저장되지 않고 스트리밍만 처리됨**
|
||||||
|
- **화자 식별 기능 없음** (단순 텍스트 변환만)
|
||||||
|
|
||||||
[성능 요구사항]
|
[성능 요구사항]
|
||||||
- 발언 인식 지연 시간: 1초 이내
|
- 음성 인식 지연 시간: 1초 이내
|
||||||
- 화자 식별 정확도: 90% 이상
|
- 변환 정확도: 85% 이상
|
||||||
|
|
||||||
[비고]
|
[비고]
|
||||||
- STT는 기본 기능으로 경쟁사 대부분이 제공하는 기능임
|
- STT는 기본 기능으로 경쟁사 대부분이 제공하는 기능임
|
||||||
@ -1271,5 +1269,6 @@ UFR-TODO-040: [Todo관리] Todo 담당자로서 | 나는, 나의 Todo를 효율
|
|||||||
| 2.1.0 | 2025-10-24 | 강지수 (Product Designer) | 회의 종료 후 워크플로우 개선 및 안건 기반 회의록 구조 도입<br>- **UFR-MEET-040 (회의종료)**: 회의 종료 시 사용자 선택 옵션 제공<br> - AI가 STT 텍스트를 주요 안건으로 요약 정리 (템플릿 및 메모 항목 반영)<br> - 종료 후 선택: 회의록 수정 화면 이동 OR 대시보드 이동<br> - 회의록 상태: 작성중으로 저장<br>- **UFR-AI-010 (회의록자동작성)**: 실시간 + 종료 시 이중 처리 방식<br> - 시나리오 1: 실시간 AI 주요 메모 작성 (회의 진행 중)<br> - 시나리오 2: 회의 종료 시 전체 안건 요약 (AI 한줄 요약 + 상세 요약)<br>- **UFR-AI-020 (Todo자동추출)**: Todo 기본값 정책 추가<br> - 담당자 기본값: 회의록 생성자<br> - 마감일 기본값: 다음 회의 날짜 OR 오늘<br> - 우선순위 기본값: 보통<br> - Todo 독립성: 회의록 확정 상태와 무관하게 완료 처리 가능<br>- **UFR-AI-036 (AI한줄요약)**: 신규 유저스토리 추가<br> - 각 안건별 편집 불가능한 AI 한줄 요약 (30자 이내)<br> - 편집 가능한 상세 요약과 함께 제공<br>- **UFR-MEET-050 (최종확정)**: 안건 검증 요구사항 추가<br> - 모든 안건 검증 완료 시 최종 확정 가능<br> - 검증률 = 검증 완료된 안건 수 / 전체 안건 수<br>- **UFR-COLLAB-030 (검증완료)**: 안건별 검증으로 변경<br> - 섹션 검증 → 안건별 검증<br> - 11-회의록수정 화면에서 안건별 검증 처리<br> - 06-검증완료 화면 불필요 (11-회의록수정에 통합) |
|
| 2.1.0 | 2025-10-24 | 강지수 (Product Designer) | 회의 종료 후 워크플로우 개선 및 안건 기반 회의록 구조 도입<br>- **UFR-MEET-040 (회의종료)**: 회의 종료 시 사용자 선택 옵션 제공<br> - AI가 STT 텍스트를 주요 안건으로 요약 정리 (템플릿 및 메모 항목 반영)<br> - 종료 후 선택: 회의록 수정 화면 이동 OR 대시보드 이동<br> - 회의록 상태: 작성중으로 저장<br>- **UFR-AI-010 (회의록자동작성)**: 실시간 + 종료 시 이중 처리 방식<br> - 시나리오 1: 실시간 AI 주요 메모 작성 (회의 진행 중)<br> - 시나리오 2: 회의 종료 시 전체 안건 요약 (AI 한줄 요약 + 상세 요약)<br>- **UFR-AI-020 (Todo자동추출)**: Todo 기본값 정책 추가<br> - 담당자 기본값: 회의록 생성자<br> - 마감일 기본값: 다음 회의 날짜 OR 오늘<br> - 우선순위 기본값: 보통<br> - Todo 독립성: 회의록 확정 상태와 무관하게 완료 처리 가능<br>- **UFR-AI-036 (AI한줄요약)**: 신규 유저스토리 추가<br> - 각 안건별 편집 불가능한 AI 한줄 요약 (30자 이내)<br> - 편집 가능한 상세 요약과 함께 제공<br>- **UFR-MEET-050 (최종확정)**: 안건 검증 요구사항 추가<br> - 모든 안건 검증 완료 시 최종 확정 가능<br> - 검증률 = 검증 완료된 안건 수 / 전체 안건 수<br>- **UFR-COLLAB-030 (검증완료)**: 안건별 검증으로 변경<br> - 섹션 검증 → 안건별 검증<br> - 11-회의록수정 화면에서 안건별 검증 처리<br> - 06-검증완료 화면 불필요 (11-회의록수정에 통합) |
|
||||||
| 2.1.1 | 2025-10-24 | 강지수 (Product Designer) | 회의 종료 화면 정책 명확화 및 실시간 협업 충돌 방지 개선<br>- **UFR-MEET-040 (회의종료)**: 회의 종료 화면 정책 및 옵션 추가<br> - 회의 종료 화면은 확인 전용 (편집 불가) 명시<br> - 안건별 AI 요약 전체 표시 (한줄 요약 + 상세 요약 + Todo)<br> - 옵션 추가: "바로 최종 확정" (옵션 2)<br> - 3가지 선택 옵션: 회의록 수정 / 바로 최종 확정 / 대시보드 이동<br>- **UFR-COLLAB-020 (충돌해결)**: 안건 기반 충돌 방지 메커니즘 강화<br> - 안건 단위 독립 편집으로 충돌 최소화<br> - 다른 안건 동시 편집 시 충돌 없음<br> - 동일 안건 내 다른 필드 편집 시 자동 병합<br> - 동일 필드 동시 수정 시에만 Last Write Wins 적용<br> - 편집 중 안건 실시간 표시 (편집자 이름 및 아이콘)<br>- **UFR-MEET-050 (최종확정)**: 회의 종료 화면 바로 확정 시나리오 추가<br> - 시나리오 2: 회의 종료 화면에서 바로 최종 확정<br> - 바로 확정 시 모든 안건 자동 검증 완료 처리<br> - 필수 항목 자동 충족 (AI 생성 내용 활용)<br> - 회의록 수정 화면 거치지 않고 바로 확정 완료 |
|
| 2.1.1 | 2025-10-24 | 강지수 (Product Designer) | 회의 종료 화면 정책 명확화 및 실시간 협업 충돌 방지 개선<br>- **UFR-MEET-040 (회의종료)**: 회의 종료 화면 정책 및 옵션 추가<br> - 회의 종료 화면은 확인 전용 (편집 불가) 명시<br> - 안건별 AI 요약 전체 표시 (한줄 요약 + 상세 요약 + Todo)<br> - 옵션 추가: "바로 최종 확정" (옵션 2)<br> - 3가지 선택 옵션: 회의록 수정 / 바로 최종 확정 / 대시보드 이동<br>- **UFR-COLLAB-020 (충돌해결)**: 안건 기반 충돌 방지 메커니즘 강화<br> - 안건 단위 독립 편집으로 충돌 최소화<br> - 다른 안건 동시 편집 시 충돌 없음<br> - 동일 안건 내 다른 필드 편집 시 자동 병합<br> - 동일 필드 동시 수정 시에만 Last Write Wins 적용<br> - 편집 중 안건 실시간 표시 (편집자 이름 및 아이콘)<br>- **UFR-MEET-050 (최종확정)**: 회의 종료 화면 바로 확정 시나리오 추가<br> - 시나리오 2: 회의 종료 화면에서 바로 최종 확정<br> - 바로 확정 시 모든 안건 자동 검증 완료 처리<br> - 필수 항목 자동 충족 (AI 생성 내용 활용)<br> - 회의록 수정 화면 거치지 않고 바로 확정 완료 |
|
||||||
| 2.1.2 | 2025-10-24 | 강지수 (Product Designer) | 역할 용어 통일 및 권한 체계 명확화<br>- **용어 통일**: "회의록 작성자" → "회의 생성자" 또는 "회의 참석자"로 명확히 구분<br> - 생성자 권한 필요: UFR-MEET-010 (회의예약), UFR-MEET-020 (템플릿선택), UFR-MEET-030 (회의시작), UFR-MEET-040 (회의종료), UFR-MEET-050 (최종확정)<br> - 참석자 권한: UFR-MEET-046 (목록조회), UFR-MEET-047 (상세조회), UFR-AI-010~040 (AI 기능), UFR-RAG-010~020 (RAG 기능)<br>- **역할 정의**:<br> - 생성자: 회의 예약을 생성한 사람 (특별 권한: 참석자 관리, 회의 시작/종료, 최종 확정)<br> - 참석자: 회의에 참여하는 전체 인원 (생성자 포함, 기본 권한: 안건 편집, 검증, Todo 관리)<br>- **권한 체계 명확화**:<br> - 회의록 상세 조회 화면: 역할 표시 "생성자/참석자"로 변경<br> - Todo 편집 권한: 담당자 본인 OR 회의 생성자 |
|
| 2.1.2 | 2025-10-24 | 강지수 (Product Designer) | 역할 용어 통일 및 권한 체계 명확화<br>- **용어 통일**: "회의록 작성자" → "회의 생성자" 또는 "회의 참석자"로 명확히 구분<br> - 생성자 권한 필요: UFR-MEET-010 (회의예약), UFR-MEET-020 (템플릿선택), UFR-MEET-030 (회의시작), UFR-MEET-040 (회의종료), UFR-MEET-050 (최종확정)<br> - 참석자 권한: UFR-MEET-046 (목록조회), UFR-MEET-047 (상세조회), UFR-AI-010~040 (AI 기능), UFR-RAG-010~020 (RAG 기능)<br>- **역할 정의**:<br> - 생성자: 회의 예약을 생성한 사람 (특별 권한: 참석자 관리, 회의 시작/종료, 최종 확정)<br> - 참석자: 회의에 참여하는 전체 인원 (생성자 포함, 기본 권한: 안건 편집, 검증, Todo 관리)<br>- **권한 체계 명확화**:<br> - 회의록 상세 조회 화면: 역할 표시 "생성자/참석자"로 변경<br> - Todo 편집 권한: 담당자 본인 OR 회의 생성자 |
|
||||||
|
| 2.1.3 | 2025-10-24 | 강지수 (Product Designer) | 회의록 목록 조회 화면 생성자 표시 기능 추가 (UFR-MEET-046)<br>- **목록 표시 정보 추가**: 생성자 표시 (👑 아이콘)<br> - 현재 사용자가 회의 생성자인 경우 회의록 카드에 👑 아이콘 표시<br> - 아이콘 크기: 16px, title 속성 "생성자"로 툴팁 제공<br>- **UI/UX 설계서 업데이트**: 12-회의록목록조회 화면 UI 구성요소 명세 추가<br>- **스타일 가이드 업데이트**: creator-badge 스타일 추가 (배지 섹션)<br>- **프로토타입 수정**: 12-회의록목록조회.html, common.css<br> - createMeetingCard() 함수: creatorBadge 변수 추가 및 렌더링<br> - common.css: .creator-badge 스타일 정의 |
|
||||||
|
|
||||||
---
|
---
|
||||||
46
docker-compose.test.yml
Normal file
46
docker-compose.test.yml
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# Test Database
|
||||||
|
postgres-test:
|
||||||
|
image: postgres:15-alpine
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: sttdb_test
|
||||||
|
POSTGRES_USER: testuser
|
||||||
|
POSTGRES_PASSWORD: testpass
|
||||||
|
ports:
|
||||||
|
- "5433:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres_test_data:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U testuser -d sttdb_test"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
# Test Redis
|
||||||
|
redis-test:
|
||||||
|
image: redis:7-alpine
|
||||||
|
ports:
|
||||||
|
- "6380:6379"
|
||||||
|
command: redis-server --requirepass testpass
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "-a", "testpass", "ping"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
# Test Azure Storage Emulator (Azurite)
|
||||||
|
azurite-test:
|
||||||
|
image: mcr.microsoft.com/azure-storage/azurite
|
||||||
|
ports:
|
||||||
|
- "10000:10000" # Blob service
|
||||||
|
- "10001:10001" # Queue service
|
||||||
|
- "10002:10002" # Table service
|
||||||
|
command: azurite --blobHost 0.0.0.0 --queueHost 0.0.0.0 --tableHost 0.0.0.0
|
||||||
|
volumes:
|
||||||
|
- azurite_data:/data
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_test_data:
|
||||||
|
azurite_data:
|
||||||
279
logs/ai.log
279
logs/ai.log
@ -1438,3 +1438,282 @@ Caused by: io.lettuce.core.RedisCommandExecutionException: NOAUTH HELLO must be
|
|||||||
2025-10-23 16:37:29 [SpringApplicationShutdownHook] DEBUG o.h.type.spi.TypeConfiguration$Scope - Un-scoping TypeConfiguration [org.hibernate.type.spi.TypeConfiguration$Scope@1d9c4491] from SessionFactory [org.hibernate.internal.SessionFactoryImpl@20505460]
|
2025-10-23 16:37:29 [SpringApplicationShutdownHook] DEBUG o.h.type.spi.TypeConfiguration$Scope - Un-scoping TypeConfiguration [org.hibernate.type.spi.TypeConfiguration$Scope@1d9c4491] from SessionFactory [org.hibernate.internal.SessionFactoryImpl@20505460]
|
||||||
2025-10-23 16:37:29 [SpringApplicationShutdownHook] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown initiated...
|
2025-10-23 16:37:29 [SpringApplicationShutdownHook] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown initiated...
|
||||||
2025-10-23 16:37:29 [SpringApplicationShutdownHook] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown completed.
|
2025-10-23 16:37:29 [SpringApplicationShutdownHook] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown completed.
|
||||||
|
2025-10-23 17:10:40 [main] INFO com.unicorn.hgzero.ai.AiApplication - Starting AiApplication using Java 23.0.2 with PID 43872 (/Users/jominseo/HGZero/ai/build/classes/java/main started by jominseo in /Users/jominseo/HGZero)
|
||||||
|
2025-10-23 17:10:40 [main] DEBUG com.unicorn.hgzero.ai.AiApplication - Running with Spring Boot v3.3.0, Spring v6.1.8
|
||||||
|
2025-10-23 17:10:40 [main] INFO com.unicorn.hgzero.ai.AiApplication - No active profile set, falling back to 1 default profile: "default"
|
||||||
|
2025-10-23 17:10:40 [main] INFO o.s.d.r.c.RepositoryConfigurationDelegate - Multiple Spring Data modules found, entering strict repository configuration mode
|
||||||
|
2025-10-23 17:10:40 [main] INFO o.s.d.r.c.RepositoryConfigurationDelegate - Bootstrapping Spring Data JPA repositories in DEFAULT mode.
|
||||||
|
2025-10-23 17:10:40 [main] INFO o.s.d.r.c.RepositoryConfigurationDelegate - Finished Spring Data repository scanning in 3 ms. Found 0 JPA repository interfaces.
|
||||||
|
2025-10-23 17:10:40 [main] INFO o.s.d.r.c.RepositoryConfigurationDelegate - Multiple Spring Data modules found, entering strict repository configuration mode
|
||||||
|
2025-10-23 17:10:40 [main] INFO o.s.d.r.c.RepositoryConfigurationDelegate - Bootstrapping Spring Data Redis repositories in DEFAULT mode.
|
||||||
|
2025-10-23 17:10:40 [main] INFO o.s.d.r.c.RepositoryConfigurationDelegate - Finished Spring Data repository scanning in 0 ms. Found 0 Redis repository interfaces.
|
||||||
|
2025-10-23 17:10:41 [main] INFO o.s.b.w.e.tomcat.TomcatWebServer - Tomcat initialized with port 8084 (http)
|
||||||
|
2025-10-23 17:10:41 [main] INFO o.a.catalina.core.StandardService - Starting service [Tomcat]
|
||||||
|
2025-10-23 17:10:41 [main] INFO o.a.catalina.core.StandardEngine - Starting Servlet engine: [Apache Tomcat/10.1.24]
|
||||||
|
2025-10-23 17:10:41 [main] INFO o.a.c.c.C.[Tomcat].[localhost].[/] - Initializing Spring embedded WebApplicationContext
|
||||||
|
2025-10-23 17:10:41 [main] INFO o.s.b.w.s.c.ServletWebServerApplicationContext - Root WebApplicationContext: initialization completed in 776 ms
|
||||||
|
2025-10-23 17:10:41 [main] INFO o.h.jpa.internal.util.LogHelper - HHH000204: Processing PersistenceUnitInfo [name: default]
|
||||||
|
2025-10-23 17:10:41 [main] INFO org.hibernate.Version - HHH000412: Hibernate ORM core version 6.5.2.Final
|
||||||
|
2025-10-23 17:10:41 [main] INFO o.h.c.i.RegionFactoryInitiator - HHH000026: Second-level cache disabled
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration boolean -> org.hibernate.type.BasicTypeReference@3e2d65e1
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration boolean -> org.hibernate.type.BasicTypeReference@3e2d65e1
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Boolean -> org.hibernate.type.BasicTypeReference@3e2d65e1
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration numeric_boolean -> org.hibernate.type.BasicTypeReference@1174676f
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration org.hibernate.type.NumericBooleanConverter -> org.hibernate.type.BasicTypeReference@1174676f
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration true_false -> org.hibernate.type.BasicTypeReference@71f8ce0e
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration org.hibernate.type.TrueFalseConverter -> org.hibernate.type.BasicTypeReference@71f8ce0e
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration yes_no -> org.hibernate.type.BasicTypeReference@4fd92289
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration org.hibernate.type.YesNoConverter -> org.hibernate.type.BasicTypeReference@4fd92289
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration byte -> org.hibernate.type.BasicTypeReference@1a8e44fe
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration byte -> org.hibernate.type.BasicTypeReference@1a8e44fe
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Byte -> org.hibernate.type.BasicTypeReference@1a8e44fe
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration binary -> org.hibernate.type.BasicTypeReference@287317df
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration byte[] -> org.hibernate.type.BasicTypeReference@287317df
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration [B -> org.hibernate.type.BasicTypeReference@287317df
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration binary_wrapper -> org.hibernate.type.BasicTypeReference@1fcc3461
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration wrapper-binary -> org.hibernate.type.BasicTypeReference@1fcc3461
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration image -> org.hibernate.type.BasicTypeReference@1987807b
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration blob -> org.hibernate.type.BasicTypeReference@71469e01
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.sql.Blob -> org.hibernate.type.BasicTypeReference@71469e01
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_blob -> org.hibernate.type.BasicTypeReference@41bbb219
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_blob_wrapper -> org.hibernate.type.BasicTypeReference@3f2ae973
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration short -> org.hibernate.type.BasicTypeReference@1a8b22b5
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration short -> org.hibernate.type.BasicTypeReference@1a8b22b5
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Short -> org.hibernate.type.BasicTypeReference@1a8b22b5
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration integer -> org.hibernate.type.BasicTypeReference@5f781173
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration int -> org.hibernate.type.BasicTypeReference@5f781173
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Integer -> org.hibernate.type.BasicTypeReference@5f781173
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration long -> org.hibernate.type.BasicTypeReference@43cf5bff
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration long -> org.hibernate.type.BasicTypeReference@43cf5bff
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Long -> org.hibernate.type.BasicTypeReference@43cf5bff
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration float -> org.hibernate.type.BasicTypeReference@2b464384
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration float -> org.hibernate.type.BasicTypeReference@2b464384
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Float -> org.hibernate.type.BasicTypeReference@2b464384
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration double -> org.hibernate.type.BasicTypeReference@681b42d3
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration double -> org.hibernate.type.BasicTypeReference@681b42d3
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Double -> org.hibernate.type.BasicTypeReference@681b42d3
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration big_integer -> org.hibernate.type.BasicTypeReference@77f7352a
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.math.BigInteger -> org.hibernate.type.BasicTypeReference@77f7352a
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration big_decimal -> org.hibernate.type.BasicTypeReference@4ede8888
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.math.BigDecimal -> org.hibernate.type.BasicTypeReference@4ede8888
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration character -> org.hibernate.type.BasicTypeReference@571db8b4
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration char -> org.hibernate.type.BasicTypeReference@571db8b4
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Character -> org.hibernate.type.BasicTypeReference@571db8b4
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration character_nchar -> org.hibernate.type.BasicTypeReference@65a2755e
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration string -> org.hibernate.type.BasicTypeReference@2b3242a5
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.String -> org.hibernate.type.BasicTypeReference@2b3242a5
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration nstring -> org.hibernate.type.BasicTypeReference@11120583
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration characters -> org.hibernate.type.BasicTypeReference@2bf0c70d
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration char[] -> org.hibernate.type.BasicTypeReference@2bf0c70d
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration [C -> org.hibernate.type.BasicTypeReference@2bf0c70d
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration wrapper-characters -> org.hibernate.type.BasicTypeReference@5d8e4fa8
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration text -> org.hibernate.type.BasicTypeReference@649009d6
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration ntext -> org.hibernate.type.BasicTypeReference@652f26da
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration clob -> org.hibernate.type.BasicTypeReference@484a5ddd
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.sql.Clob -> org.hibernate.type.BasicTypeReference@484a5ddd
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration nclob -> org.hibernate.type.BasicTypeReference@6796a873
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.sql.NClob -> org.hibernate.type.BasicTypeReference@6796a873
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_clob -> org.hibernate.type.BasicTypeReference@3acc3ee
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_clob_char_array -> org.hibernate.type.BasicTypeReference@1f293cb7
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_clob_character_array -> org.hibernate.type.BasicTypeReference@5972e3a
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_nclob -> org.hibernate.type.BasicTypeReference@5790cbcb
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_nclob_character_array -> org.hibernate.type.BasicTypeReference@32c6d164
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_nclob_char_array -> org.hibernate.type.BasicTypeReference@645c9f0f
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration Duration -> org.hibernate.type.BasicTypeReference@58068b40
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.Duration -> org.hibernate.type.BasicTypeReference@58068b40
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration LocalDateTime -> org.hibernate.type.BasicTypeReference@999cd18
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.LocalDateTime -> org.hibernate.type.BasicTypeReference@999cd18
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration LocalDate -> org.hibernate.type.BasicTypeReference@dd060be
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.LocalDate -> org.hibernate.type.BasicTypeReference@dd060be
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration LocalTime -> org.hibernate.type.BasicTypeReference@df432ec
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.LocalTime -> org.hibernate.type.BasicTypeReference@df432ec
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration OffsetDateTime -> org.hibernate.type.BasicTypeReference@6144e499
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.OffsetDateTime -> org.hibernate.type.BasicTypeReference@6144e499
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration OffsetDateTimeWithTimezone -> org.hibernate.type.BasicTypeReference@26f204a4
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration OffsetDateTimeWithoutTimezone -> org.hibernate.type.BasicTypeReference@28295554
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration OffsetTime -> org.hibernate.type.BasicTypeReference@4e671ef
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.OffsetTime -> org.hibernate.type.BasicTypeReference@4e671ef
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration OffsetTimeUtc -> org.hibernate.type.BasicTypeReference@42403dc6
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration OffsetTimeWithTimezone -> org.hibernate.type.BasicTypeReference@74a1d60e
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration OffsetTimeWithoutTimezone -> org.hibernate.type.BasicTypeReference@16c0be3b
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration ZonedDateTime -> org.hibernate.type.BasicTypeReference@219edc05
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.ZonedDateTime -> org.hibernate.type.BasicTypeReference@219edc05
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration ZonedDateTimeWithTimezone -> org.hibernate.type.BasicTypeReference@62f37bfd
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration ZonedDateTimeWithoutTimezone -> org.hibernate.type.BasicTypeReference@1818d00b
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration date -> org.hibernate.type.BasicTypeReference@b3a8455
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.sql.Date -> org.hibernate.type.BasicTypeReference@b3a8455
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration time -> org.hibernate.type.BasicTypeReference@5c930fc3
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.sql.Time -> org.hibernate.type.BasicTypeReference@5c930fc3
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration timestamp -> org.hibernate.type.BasicTypeReference@25c6ab3f
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.sql.Timestamp -> org.hibernate.type.BasicTypeReference@25c6ab3f
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.util.Date -> org.hibernate.type.BasicTypeReference@25c6ab3f
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration calendar -> org.hibernate.type.BasicTypeReference@7b80af04
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.util.Calendar -> org.hibernate.type.BasicTypeReference@7b80af04
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.util.GregorianCalendar -> org.hibernate.type.BasicTypeReference@7b80af04
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration calendar_date -> org.hibernate.type.BasicTypeReference@2447940d
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration calendar_time -> org.hibernate.type.BasicTypeReference@60ee7a51
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration instant -> org.hibernate.type.BasicTypeReference@70e1aa20
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.Instant -> org.hibernate.type.BasicTypeReference@70e1aa20
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration uuid -> org.hibernate.type.BasicTypeReference@e67d3b7
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.util.UUID -> org.hibernate.type.BasicTypeReference@e67d3b7
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration pg-uuid -> org.hibernate.type.BasicTypeReference@e67d3b7
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration uuid-binary -> org.hibernate.type.BasicTypeReference@1618c98a
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration uuid-char -> org.hibernate.type.BasicTypeReference@5b715ea
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration class -> org.hibernate.type.BasicTypeReference@787a0fd6
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Class -> org.hibernate.type.BasicTypeReference@787a0fd6
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration currency -> org.hibernate.type.BasicTypeReference@48b09105
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration Currency -> org.hibernate.type.BasicTypeReference@48b09105
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.util.Currency -> org.hibernate.type.BasicTypeReference@48b09105
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration locale -> org.hibernate.type.BasicTypeReference@18b45500
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.util.Locale -> org.hibernate.type.BasicTypeReference@18b45500
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration serializable -> org.hibernate.type.BasicTypeReference@25110bb9
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.io.Serializable -> org.hibernate.type.BasicTypeReference@25110bb9
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration timezone -> org.hibernate.type.BasicTypeReference@dbda472
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.util.TimeZone -> org.hibernate.type.BasicTypeReference@dbda472
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration ZoneOffset -> org.hibernate.type.BasicTypeReference@41492479
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.ZoneOffset -> org.hibernate.type.BasicTypeReference@41492479
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration url -> org.hibernate.type.BasicTypeReference@7bef7505
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.net.URL -> org.hibernate.type.BasicTypeReference@7bef7505
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration vector -> org.hibernate.type.BasicTypeReference@568ef502
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration row_version -> org.hibernate.type.BasicTypeReference@36f05595
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration object -> org.hibernate.type.JavaObjectType@3c46e6f6
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Object -> org.hibernate.type.JavaObjectType@3c46e6f6
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration null -> org.hibernate.type.NullType@1c79d093
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_date -> org.hibernate.type.BasicTypeReference@746fd19b
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_time -> org.hibernate.type.BasicTypeReference@61d7bb61
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_timestamp -> org.hibernate.type.BasicTypeReference@33f81280
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_calendar -> org.hibernate.type.BasicTypeReference@3991fe6d
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_calendar_date -> org.hibernate.type.BasicTypeReference@3a0e7f89
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_calendar_time -> org.hibernate.type.BasicTypeReference@665ed71a
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_binary -> org.hibernate.type.BasicTypeReference@15c1b543
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_serializable -> org.hibernate.type.BasicTypeReference@23954300
|
||||||
|
2025-10-23 17:10:41 [main] INFO o.s.o.j.p.SpringPersistenceUnitInfo - No LoadTimeWeaver setup: ignoring JPA class transformer
|
||||||
|
2025-10-23 17:10:41 [main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
|
||||||
|
2025-10-23 17:10:41 [main] INFO com.zaxxer.hikari.pool.HikariPool - HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@5f5c0eda
|
||||||
|
2025-10-23 17:10:41 [main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed.
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.h.t.d.sql.spi.DdlTypeRegistry - addDescriptor(2003, org.hibernate.type.descriptor.sql.internal.ArrayDdlTypeImpl@31b7112d) replaced previous registration(org.hibernate.type.descriptor.sql.internal.ArrayDdlTypeImpl@47fc9ce)
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.h.t.d.sql.spi.DdlTypeRegistry - addDescriptor(6, org.hibernate.type.descriptor.sql.internal.CapacityDependentDdlType@1b5d1d9) replaced previous registration(org.hibernate.type.descriptor.sql.internal.DdlTypeImpl@703a2bc9)
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.h.t.d.jdbc.spi.JdbcTypeRegistry - addDescriptor(2004, BlobTypeDescriptor(BLOB_BINDING)) replaced previous registration(BlobTypeDescriptor(DEFAULT))
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.h.t.d.jdbc.spi.JdbcTypeRegistry - addDescriptor(2005, ClobTypeDescriptor(CLOB_BINDING)) replaced previous registration(ClobTypeDescriptor(DEFAULT))
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration JAVA_OBJECT -> org.hibernate.type.JavaObjectType@e460ca1
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Object -> org.hibernate.type.JavaObjectType@e460ca1
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Type registration key [java.lang.Object] overrode previous entry : `org.hibernate.type.JavaObjectType@3c46e6f6`
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration org.hibernate.type.DurationType -> basicType@1(java.time.Duration,3015)
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration Duration -> basicType@1(java.time.Duration,3015)
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.Duration -> basicType@1(java.time.Duration,3015)
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration org.hibernate.type.OffsetDateTimeType -> basicType@2(java.time.OffsetDateTime,3003)
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration OffsetDateTime -> basicType@2(java.time.OffsetDateTime,3003)
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.OffsetDateTime -> basicType@2(java.time.OffsetDateTime,3003)
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration org.hibernate.type.ZonedDateTimeType -> basicType@3(java.time.ZonedDateTime,3003)
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration ZonedDateTime -> basicType@3(java.time.ZonedDateTime,3003)
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.ZonedDateTime -> basicType@3(java.time.ZonedDateTime,3003)
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration org.hibernate.type.OffsetTimeType -> basicType@4(java.time.OffsetTime,3007)
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration OffsetTime -> basicType@4(java.time.OffsetTime,3007)
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.OffsetTime -> basicType@4(java.time.OffsetTime,3007)
|
||||||
|
2025-10-23 17:10:41 [main] DEBUG o.h.type.spi.TypeConfiguration$Scope - Scoping TypeConfiguration [org.hibernate.type.spi.TypeConfiguration@268e30d4] to MetadataBuildingContext [org.hibernate.boot.internal.MetadataBuildingContextRootImpl@7c50709a]
|
||||||
|
2025-10-23 17:10:41 [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-23 17:10:41 [main] DEBUG o.h.type.spi.TypeConfiguration$Scope - Scoping TypeConfiguration [org.hibernate.type.spi.TypeConfiguration@268e30d4] to SessionFactoryImplementor [org.hibernate.internal.SessionFactoryImpl@263f6e96]
|
||||||
|
2025-10-23 17:10:41 [main] TRACE o.h.type.spi.TypeConfiguration$Scope - Handling #sessionFactoryCreated from [org.hibernate.internal.SessionFactoryImpl@263f6e96] for TypeConfiguration
|
||||||
|
2025-10-23 17:10:41 [main] INFO o.s.o.j.LocalContainerEntityManagerFactoryBean - Initialized JPA EntityManagerFactory for persistence unit 'default'
|
||||||
|
2025-10-23 17:10:41 [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-23 17:10:42 [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-23 17:10:42 [main] WARN o.s.b.a.s.s.UserDetailsServiceAutoConfiguration -
|
||||||
|
|
||||||
|
Using generated security password: 4d9e182a-5838-4fd3-8e57-70219ebe9076
|
||||||
|
|
||||||
|
This generated password is for development use only. Your security configuration must be updated before running your application in production.
|
||||||
|
|
||||||
|
2025-10-23 17:10:42 [main] INFO o.s.s.c.a.a.c.InitializeUserDetailsBeanManagerConfigurer$InitializeUserDetailsManagerConfigurer - Global AuthenticationManager configured with UserDetailsService bean with name inMemoryUserDetailsManager
|
||||||
|
2025-10-23 17:10:42 [main] INFO o.s.b.a.e.web.EndpointLinksResolver - Exposing 3 endpoints beneath base path '/actuator'
|
||||||
|
2025-10-23 17:10:42 [main] INFO o.s.s.web.DefaultSecurityFilterChain - Will secure any request with [org.springframework.security.web.session.DisableEncodeUrlFilter@23d1090, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@46c6541f, org.springframework.security.web.context.SecurityContextHolderFilter@67744663, org.springframework.security.web.header.HeaderWriterFilter@1a2bcce1, org.springframework.web.filter.CorsFilter@aba3735, org.springframework.security.web.csrf.CsrfFilter@417c9b17, org.springframework.security.web.authentication.logout.LogoutFilter@75b598a5, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@401b7109, org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@24a024c8, org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@16bba8ae, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@7f36b021, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@387cd426, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@6295cc30, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@7c689379, org.springframework.security.web.access.ExceptionTranslationFilter@ae5eeee, org.springframework.security.web.access.intercept.AuthorizationFilter@431bf770]
|
||||||
|
2025-10-23 17:10:42 [main] INFO o.s.b.w.e.tomcat.TomcatWebServer - Tomcat started on port 8084 (http) with context path '/'
|
||||||
|
2025-10-23 17:10:42 [main] INFO com.unicorn.hgzero.ai.AiApplication - Started AiApplication in 2.293 seconds (process running for 2.491)
|
||||||
|
2025-10-23 17:10:42 [RMI TCP Connection(1)-127.0.0.1] INFO o.a.c.c.C.[Tomcat].[localhost].[/] - Initializing Spring DispatcherServlet 'dispatcherServlet'
|
||||||
|
2025-10-23 17:10:42 [RMI TCP Connection(1)-127.0.0.1] INFO o.s.web.servlet.DispatcherServlet - Initializing Servlet 'dispatcherServlet'
|
||||||
|
2025-10-23 17:10:42 [RMI TCP Connection(1)-127.0.0.1] INFO o.s.web.servlet.DispatcherServlet - Completed initialization in 1 ms
|
||||||
|
2025-10-23 17:10:42 [boundedElastic-1] WARN o.s.b.a.d.r.RedisReactiveHealthIndicator - Redis health check failed
|
||||||
|
org.springframework.data.redis.RedisConnectionFailureException: Unable to connect to Redis
|
||||||
|
at org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory$ExceptionTranslatingConnectionProvider.translateException(LettuceConnectionFactory.java:1847)
|
||||||
|
at org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory$ExceptionTranslatingConnectionProvider.getConnection(LettuceConnectionFactory.java:1778)
|
||||||
|
at org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory$SharedConnection.getNativeConnection(LettuceConnectionFactory.java:1580)
|
||||||
|
at org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory$SharedConnection.lambda$getConnection$0(LettuceConnectionFactory.java:1560)
|
||||||
|
at org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory.doInLock(LettuceConnectionFactory.java:1521)
|
||||||
|
at org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory$SharedConnection.getConnection(LettuceConnectionFactory.java:1557)
|
||||||
|
at org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory.getSharedReactiveConnection(LettuceConnectionFactory.java:1268)
|
||||||
|
at org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory.getReactiveConnection(LettuceConnectionFactory.java:1143)
|
||||||
|
at org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory.getReactiveConnection(LettuceConnectionFactory.java:119)
|
||||||
|
at reactor.core.publisher.MonoSupplier.call(MonoSupplier.java:67)
|
||||||
|
at reactor.core.publisher.FluxSubscribeOnCallable$CallableSubscribeOnSubscription.run(FluxSubscribeOnCallable.java:228)
|
||||||
|
at reactor.core.scheduler.SchedulerTask.call(SchedulerTask.java:68)
|
||||||
|
at reactor.core.scheduler.SchedulerTask.call(SchedulerTask.java:28)
|
||||||
|
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
|
||||||
|
at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304)
|
||||||
|
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
|
||||||
|
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
|
||||||
|
at java.base/java.lang.Thread.run(Thread.java:1575)
|
||||||
|
Caused by: io.lettuce.core.RedisConnectionException: Unable to connect to 20.249.177.114/<unresolved>:6379
|
||||||
|
at io.lettuce.core.RedisConnectionException.create(RedisConnectionException.java:78)
|
||||||
|
at io.lettuce.core.RedisConnectionException.create(RedisConnectionException.java:56)
|
||||||
|
at io.lettuce.core.AbstractRedisClient.getConnection(AbstractRedisClient.java:350)
|
||||||
|
at io.lettuce.core.RedisClient.connect(RedisClient.java:215)
|
||||||
|
at org.springframework.data.redis.connection.lettuce.StandaloneConnectionProvider.lambda$getConnection$1(StandaloneConnectionProvider.java:112)
|
||||||
|
at java.base/java.util.Optional.orElseGet(Optional.java:364)
|
||||||
|
at org.springframework.data.redis.connection.lettuce.StandaloneConnectionProvider.getConnection(StandaloneConnectionProvider.java:112)
|
||||||
|
at org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory$ExceptionTranslatingConnectionProvider.getConnection(LettuceConnectionFactory.java:1776)
|
||||||
|
... 16 common frames omitted
|
||||||
|
Caused by: io.lettuce.core.RedisCommandExecutionException: NOAUTH HELLO must be called with the client already authenticated, otherwise the HELLO <proto> AUTH <user> <pass> option can be used to authenticate the client and select the RESP protocol version at the same time
|
||||||
|
at io.lettuce.core.internal.ExceptionFactory.createExecutionException(ExceptionFactory.java:147)
|
||||||
|
at io.lettuce.core.internal.ExceptionFactory.createExecutionException(ExceptionFactory.java:116)
|
||||||
|
at io.lettuce.core.protocol.AsyncCommand.completeResult(AsyncCommand.java:120)
|
||||||
|
at io.lettuce.core.protocol.AsyncCommand.complete(AsyncCommand.java:111)
|
||||||
|
at io.lettuce.core.protocol.CommandWrapper.complete(CommandWrapper.java:63)
|
||||||
|
at io.lettuce.core.protocol.CommandHandler.complete(CommandHandler.java:745)
|
||||||
|
at io.lettuce.core.protocol.CommandHandler.decode(CommandHandler.java:680)
|
||||||
|
at io.lettuce.core.protocol.CommandHandler.channelRead(CommandHandler.java:597)
|
||||||
|
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:442)
|
||||||
|
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
|
||||||
|
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412)
|
||||||
|
at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1407)
|
||||||
|
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:440)
|
||||||
|
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
|
||||||
|
at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:918)
|
||||||
|
at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:166)
|
||||||
|
at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:788)
|
||||||
|
at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:724)
|
||||||
|
at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:650)
|
||||||
|
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:562)
|
||||||
|
at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:994)
|
||||||
|
at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
|
||||||
|
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
|
||||||
|
... 1 common frames omitted
|
||||||
|
2025-10-23 17:11:28 [http-nio-8084-exec-1] DEBUG o.s.security.web.FilterChainProxy - Securing GET /swagger-ui.html
|
||||||
|
2025-10-23 17:11:28 [http-nio-8084-exec-1] DEBUG o.s.s.w.a.AnonymousAuthenticationFilter - Set SecurityContextHolder to anonymous SecurityContext
|
||||||
|
2025-10-23 17:11:28 [http-nio-8084-exec-1] DEBUG o.s.s.w.s.HttpSessionRequestCache - Saved request http://localhost:8084/swagger-ui.html?continue to session
|
||||||
|
2025-10-23 17:11:28 [http-nio-8084-exec-1] DEBUG o.s.s.w.a.DelegatingAuthenticationEntryPoint - Trying to match using And [Not [RequestHeaderRequestMatcher [expectedHeaderName=X-Requested-With, expectedHeaderValue=XMLHttpRequest]], MediaTypeRequestMatcher [contentNegotiationStrategy=org.springframework.web.accept.ContentNegotiationManager@49c72fb7, matchingMediaTypes=[application/xhtml+xml, image/*, text/html, text/plain], useEquals=false, ignoredMediaTypes=[*/*]]]
|
||||||
|
2025-10-23 17:11:28 [http-nio-8084-exec-1] DEBUG o.s.s.w.a.DelegatingAuthenticationEntryPoint - Match found! Executing org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint@7d91e9c9
|
||||||
|
2025-10-23 17:11:28 [http-nio-8084-exec-1] DEBUG o.s.s.web.DefaultRedirectStrategy - Redirecting to http://localhost:8084/login
|
||||||
|
2025-10-23 17:11:28 [http-nio-8084-exec-2] DEBUG o.s.security.web.FilterChainProxy - Securing GET /login
|
||||||
|
2025-10-23 17:11:28 [http-nio-8084-exec-3] DEBUG o.s.security.web.FilterChainProxy - Securing GET /favicon.ico
|
||||||
|
2025-10-23 17:11:28 [http-nio-8084-exec-3] DEBUG o.s.s.w.a.AnonymousAuthenticationFilter - Set SecurityContextHolder to anonymous SecurityContext
|
||||||
|
2025-10-23 17:11:28 [http-nio-8084-exec-3] DEBUG o.s.s.w.a.DelegatingAuthenticationEntryPoint - Trying to match using And [Not [RequestHeaderRequestMatcher [expectedHeaderName=X-Requested-With, expectedHeaderValue=XMLHttpRequest]], MediaTypeRequestMatcher [contentNegotiationStrategy=org.springframework.web.accept.ContentNegotiationManager@49c72fb7, matchingMediaTypes=[application/xhtml+xml, image/*, text/html, text/plain], useEquals=false, ignoredMediaTypes=[*/*]]]
|
||||||
|
2025-10-23 17:11:28 [http-nio-8084-exec-3] DEBUG o.s.s.w.a.DelegatingAuthenticationEntryPoint - Match found! Executing org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint@7d91e9c9
|
||||||
|
2025-10-23 17:11:28 [http-nio-8084-exec-3] DEBUG o.s.s.web.DefaultRedirectStrategy - Redirecting to http://localhost:8084/login
|
||||||
|
2025-10-23 17:11:28 [http-nio-8084-exec-4] DEBUG o.s.security.web.FilterChainProxy - Securing GET /login
|
||||||
|
2025-10-23 17:11:43 [http-nio-8084-exec-5] DEBUG o.s.security.web.FilterChainProxy - Securing GET /swagger-ui.html
|
||||||
|
2025-10-23 17:11:43 [http-nio-8084-exec-5] DEBUG o.s.s.w.a.AnonymousAuthenticationFilter - Set SecurityContextHolder to anonymous SecurityContext
|
||||||
|
2025-10-23 17:11:43 [http-nio-8084-exec-5] DEBUG o.s.s.w.s.HttpSessionRequestCache - Saved request http://localhost:8084/swagger-ui.html?continue to session
|
||||||
|
2025-10-23 17:11:43 [http-nio-8084-exec-5] DEBUG o.s.s.w.a.DelegatingAuthenticationEntryPoint - Trying to match using And [Not [RequestHeaderRequestMatcher [expectedHeaderName=X-Requested-With, expectedHeaderValue=XMLHttpRequest]], MediaTypeRequestMatcher [contentNegotiationStrategy=org.springframework.web.accept.ContentNegotiationManager@49c72fb7, matchingMediaTypes=[application/xhtml+xml, image/*, text/html, text/plain], useEquals=false, ignoredMediaTypes=[*/*]]]
|
||||||
|
2025-10-23 17:11:43 [http-nio-8084-exec-5] DEBUG o.s.s.w.a.DelegatingAuthenticationEntryPoint - Match found! Executing org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint@7d91e9c9
|
||||||
|
2025-10-23 17:11:43 [http-nio-8084-exec-5] DEBUG o.s.s.web.DefaultRedirectStrategy - Redirecting to http://localhost:8084/login
|
||||||
|
2025-10-23 17:11:43 [http-nio-8084-exec-6] DEBUG o.s.security.web.FilterChainProxy - Securing GET /login
|
||||||
|
2025-10-23 17:11:43 [http-nio-8084-exec-7] DEBUG o.s.security.web.FilterChainProxy - Securing GET /favicon.ico
|
||||||
|
2025-10-23 17:11:43 [http-nio-8084-exec-7] DEBUG o.s.s.w.a.AnonymousAuthenticationFilter - Set SecurityContextHolder to anonymous SecurityContext
|
||||||
|
2025-10-23 17:11:43 [http-nio-8084-exec-7] DEBUG o.s.s.w.a.DelegatingAuthenticationEntryPoint - Trying to match using And [Not [RequestHeaderRequestMatcher [expectedHeaderName=X-Requested-With, expectedHeaderValue=XMLHttpRequest]], MediaTypeRequestMatcher [contentNegotiationStrategy=org.springframework.web.accept.ContentNegotiationManager@49c72fb7, matchingMediaTypes=[application/xhtml+xml, image/*, text/html, text/plain], useEquals=false, ignoredMediaTypes=[*/*]]]
|
||||||
|
2025-10-23 17:11:43 [http-nio-8084-exec-7] DEBUG o.s.s.w.a.DelegatingAuthenticationEntryPoint - Match found! Executing org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint@7d91e9c9
|
||||||
|
2025-10-23 17:11:43 [http-nio-8084-exec-7] DEBUG o.s.s.web.DefaultRedirectStrategy - Redirecting to http://localhost:8084/login
|
||||||
|
2025-10-23 17:11:43 [http-nio-8084-exec-8] DEBUG o.s.security.web.FilterChainProxy - Securing GET /login
|
||||||
|
2025-10-23 17:12:37 [SpringApplicationShutdownHook] INFO o.s.o.j.LocalContainerEntityManagerFactoryBean - Closing JPA EntityManagerFactory for persistence unit 'default'
|
||||||
|
2025-10-23 17:12:37 [SpringApplicationShutdownHook] TRACE o.h.type.spi.TypeConfiguration$Scope - Handling #sessionFactoryClosed from [org.hibernate.internal.SessionFactoryImpl@263f6e96] for TypeConfiguration
|
||||||
|
2025-10-23 17:12:37 [SpringApplicationShutdownHook] DEBUG o.h.type.spi.TypeConfiguration$Scope - Un-scoping TypeConfiguration [org.hibernate.type.spi.TypeConfiguration$Scope@7aa91cdd] from SessionFactory [org.hibernate.internal.SessionFactoryImpl@263f6e96]
|
||||||
|
2025-10-23 17:12:37 [SpringApplicationShutdownHook] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown initiated...
|
||||||
|
2025-10-23 17:12:37 [SpringApplicationShutdownHook] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown completed.
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
1
meeting/logs/meeting-start.log
Normal file
1
meeting/logs/meeting-start.log
Normal file
@ -0,0 +1 @@
|
|||||||
|
nohup: ./gradlew: No such file or directory
|
||||||
1
meeting/logs/meeting.log
Normal file
1
meeting/logs/meeting.log
Normal file
@ -0,0 +1 @@
|
|||||||
|
nohup: ./gradlew: No such file or directory
|
||||||
@ -38,16 +38,21 @@ public class Meeting {
|
|||||||
*/
|
*/
|
||||||
private String purpose;
|
private String purpose;
|
||||||
|
|
||||||
/**
|
|
||||||
* 회의 장소
|
|
||||||
*/
|
|
||||||
private String location;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 회의 일시
|
* 회의 일시
|
||||||
*/
|
*/
|
||||||
private LocalDateTime scheduledAt;
|
private LocalDateTime scheduledAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 회의 종료 예정 일시
|
||||||
|
*/
|
||||||
|
private LocalDateTime endTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 회의 장소
|
||||||
|
*/
|
||||||
|
private String location;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 회의 시작 일시
|
* 회의 시작 일시
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -83,8 +83,8 @@ public class MeetingDTO {
|
|||||||
.meetingId(meeting.getMeetingId())
|
.meetingId(meeting.getMeetingId())
|
||||||
.title(meeting.getTitle())
|
.title(meeting.getTitle())
|
||||||
.startTime(meeting.getStartedAt() != null ? meeting.getStartedAt() : meeting.getScheduledAt())
|
.startTime(meeting.getStartedAt() != null ? meeting.getStartedAt() : meeting.getScheduledAt())
|
||||||
.endTime(meeting.getEndedAt())
|
|
||||||
.purpose(meeting.getPurpose())
|
.purpose(meeting.getPurpose())
|
||||||
|
.endTime(meeting.getEndedAt() != null ? meeting.getEndedAt() : meeting.getEndTime())
|
||||||
.location(meeting.getLocation())
|
.location(meeting.getLocation())
|
||||||
.agenda(meeting.getDescription())
|
.agenda(meeting.getDescription())
|
||||||
.participants(meeting.getParticipants().stream()
|
.participants(meeting.getParticipants().stream()
|
||||||
|
|||||||
@ -6,6 +6,8 @@ import com.unicorn.hgzero.meeting.biz.domain.Meeting;
|
|||||||
import com.unicorn.hgzero.meeting.biz.usecase.in.meeting.*;
|
import com.unicorn.hgzero.meeting.biz.usecase.in.meeting.*;
|
||||||
import com.unicorn.hgzero.meeting.biz.usecase.out.MeetingReader;
|
import com.unicorn.hgzero.meeting.biz.usecase.out.MeetingReader;
|
||||||
import com.unicorn.hgzero.meeting.biz.usecase.out.MeetingWriter;
|
import com.unicorn.hgzero.meeting.biz.usecase.out.MeetingWriter;
|
||||||
|
import com.unicorn.hgzero.meeting.infra.cache.CacheService;
|
||||||
|
import com.unicorn.hgzero.meeting.infra.event.publisher.EventPublisher;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@ -32,6 +34,8 @@ public class MeetingService implements
|
|||||||
|
|
||||||
private final MeetingReader meetingReader;
|
private final MeetingReader meetingReader;
|
||||||
private final MeetingWriter meetingWriter;
|
private final MeetingWriter meetingWriter;
|
||||||
|
private final CacheService cacheService;
|
||||||
|
private final EventPublisher eventPublisher;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 회의 생성
|
* 회의 생성
|
||||||
@ -41,26 +45,75 @@ public class MeetingService implements
|
|||||||
public Meeting createMeeting(CreateMeetingCommand command) {
|
public Meeting createMeeting(CreateMeetingCommand command) {
|
||||||
log.info("Creating meeting: {}", command.title());
|
log.info("Creating meeting: {}", command.title());
|
||||||
|
|
||||||
// 회의 ID 생성
|
// 1. 회의 시간 유효성 검사
|
||||||
|
if (command.scheduledAt().isAfter(command.endTime()) ||
|
||||||
|
command.scheduledAt().isEqual(command.endTime())) {
|
||||||
|
log.warn("Invalid meeting time: start={}, end={}",
|
||||||
|
command.scheduledAt(), command.endTime());
|
||||||
|
throw new BusinessException(ErrorCode.INVALID_MEETING_TIME);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 중복 회의 체크
|
||||||
|
long conflictCount = meetingReader.countConflictingMeetings(
|
||||||
|
command.organizerId(),
|
||||||
|
command.scheduledAt(),
|
||||||
|
command.endTime()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (conflictCount > 0) {
|
||||||
|
log.warn("Meeting time conflict detected: organizerId={}, count={}",
|
||||||
|
command.organizerId(), conflictCount);
|
||||||
|
throw new BusinessException(ErrorCode.MEETING_TIME_CONFLICT);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 회의 ID 생성
|
||||||
String meetingId = UUID.randomUUID().toString();
|
String meetingId = UUID.randomUUID().toString();
|
||||||
|
|
||||||
// 회의 도메인 객체 생성
|
// 4. 회의 도메인 객체 생성
|
||||||
Meeting meeting = Meeting.builder()
|
Meeting meeting = Meeting.builder()
|
||||||
.meetingId(meetingId)
|
.meetingId(meetingId)
|
||||||
.title(command.title())
|
.title(command.title())
|
||||||
.purpose(command.purpose())
|
.purpose(command.purpose())
|
||||||
.description(command.description())
|
.description(command.agenda())
|
||||||
.location(command.location())
|
.location(command.location())
|
||||||
.scheduledAt(command.scheduledAt())
|
.scheduledAt(command.scheduledAt())
|
||||||
|
.endTime(command.endTime())
|
||||||
.status("SCHEDULED")
|
.status("SCHEDULED")
|
||||||
.organizerId(command.organizerId())
|
.organizerId(command.organizerId())
|
||||||
.participants(command.participants())
|
.participants(command.participants())
|
||||||
.templateId(command.templateId())
|
.templateId(command.templateId())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// 회의 저장
|
// 5. 회의 저장
|
||||||
Meeting savedMeeting = meetingWriter.save(meeting);
|
Meeting savedMeeting = meetingWriter.save(meeting);
|
||||||
|
|
||||||
|
// 6. 캐시 저장 (TTL: 10분)
|
||||||
|
try {
|
||||||
|
cacheService.cacheMeeting(meetingId, savedMeeting, 600);
|
||||||
|
log.debug("Meeting cached: meetingId={}", meetingId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Failed to cache meeting: meetingId={}", meetingId, e);
|
||||||
|
// 캐시 실패는 비즈니스 로직에 영향을 주지 않으므로 계속 진행
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. 참석자 초대 이벤트 발행 (비동기)
|
||||||
|
try {
|
||||||
|
eventPublisher.publishMeetingCreated(
|
||||||
|
meetingId,
|
||||||
|
command.title(),
|
||||||
|
command.scheduledAt(),
|
||||||
|
command.location(),
|
||||||
|
command.participants(),
|
||||||
|
command.organizerId(),
|
||||||
|
command.organizerId() // organizerName은 나중에 User 서비스 연동 시 개선
|
||||||
|
);
|
||||||
|
log.debug("Meeting invitation events published: meetingId={}, participants={}",
|
||||||
|
meetingId, command.participants().size());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to publish meeting invitation events: meetingId={}", meetingId, e);
|
||||||
|
// 이벤트 발행 실패는 비즈니스 로직에 영향을 주지 않으므로 계속 진행
|
||||||
|
}
|
||||||
|
|
||||||
log.info("Meeting created successfully: {}", savedMeeting.getMeetingId());
|
log.info("Meeting created successfully: {}", savedMeeting.getMeetingId());
|
||||||
return savedMeeting;
|
return savedMeeting;
|
||||||
}
|
}
|
||||||
@ -73,21 +126,31 @@ public class MeetingService implements
|
|||||||
public Meeting startMeeting(String meetingId) {
|
public Meeting startMeeting(String meetingId) {
|
||||||
log.info("Starting meeting: {}", meetingId);
|
log.info("Starting meeting: {}", meetingId);
|
||||||
|
|
||||||
|
// Redis 캐시 조회 기능 필요
|
||||||
|
|
||||||
// 회의 조회
|
// 회의 조회
|
||||||
Meeting meeting = meetingReader.findById(meetingId)
|
Meeting meeting = meetingReader.findById(meetingId)
|
||||||
.orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND));
|
.orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND));
|
||||||
|
|
||||||
|
// 권한 검증 (생성자 or 참석자)
|
||||||
|
|
||||||
// 회의 상태 검증
|
// 회의 상태 검증
|
||||||
if (!"SCHEDULED".equals(meeting.getStatus())) {
|
if (!"SCHEDULED".equals(meeting.getStatus())) {
|
||||||
throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE);
|
throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 세션 생성 기능 필요
|
||||||
|
|
||||||
// 회의 시작
|
// 회의 시작
|
||||||
meeting.start();
|
meeting.start();
|
||||||
|
|
||||||
// 저장
|
// 저장
|
||||||
Meeting updatedMeeting = meetingWriter.save(meeting);
|
Meeting updatedMeeting = meetingWriter.save(meeting);
|
||||||
|
|
||||||
|
// 회의록 초안 생성 필요
|
||||||
|
|
||||||
|
// 이벤트 발행 필요
|
||||||
|
|
||||||
log.info("Meeting started successfully: {}", meetingId);
|
log.info("Meeting started successfully: {}", meetingId);
|
||||||
return updatedMeeting;
|
return updatedMeeting;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,8 +21,9 @@ public interface CreateMeetingUseCase {
|
|||||||
record CreateMeetingCommand(
|
record CreateMeetingCommand(
|
||||||
String title,
|
String title,
|
||||||
String purpose,
|
String purpose,
|
||||||
String description,
|
String agenda,
|
||||||
LocalDateTime scheduledAt,
|
LocalDateTime scheduledAt,
|
||||||
|
LocalDateTime endTime,
|
||||||
String location,
|
String location,
|
||||||
String organizerId,
|
String organizerId,
|
||||||
List<String> participants,
|
List<String> participants,
|
||||||
|
|||||||
@ -40,4 +40,14 @@ public interface MeetingReader {
|
|||||||
* 템플릿 ID로 회의 목록 조회
|
* 템플릿 ID로 회의 목록 조회
|
||||||
*/
|
*/
|
||||||
List<Meeting> findByTemplateId(String templateId);
|
List<Meeting> findByTemplateId(String templateId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 주최자의 특정 시간대 중복 회의 개수 조회
|
||||||
|
*
|
||||||
|
* @param organizerId 주최자 ID
|
||||||
|
* @param startTime 회의 시작 시간
|
||||||
|
* @param endTime 회의 종료 시간
|
||||||
|
* @return 중복 회의 개수
|
||||||
|
*/
|
||||||
|
long countConflictingMeetings(String organizerId, LocalDateTime startTime, LocalDateTime endTime);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,6 +23,9 @@ public class CacheConfig {
|
|||||||
@Value("${spring.data.redis.port:6379}")
|
@Value("${spring.data.redis.port:6379}")
|
||||||
private int redisPort;
|
private int redisPort;
|
||||||
|
|
||||||
|
@Value("${spring.data.redis.password:}")
|
||||||
|
private String redisPassword;
|
||||||
|
|
||||||
@Value("${spring.data.redis.database:1}")
|
@Value("${spring.data.redis.database:1}")
|
||||||
private int database;
|
private int database;
|
||||||
|
|
||||||
@ -33,7 +36,15 @@ public class CacheConfig {
|
|||||||
public RedisConnectionFactory redisConnectionFactory() {
|
public RedisConnectionFactory redisConnectionFactory() {
|
||||||
var factory = new LettuceConnectionFactory(redisHost, redisPort);
|
var factory = new LettuceConnectionFactory(redisHost, redisPort);
|
||||||
factory.setDatabase(database);
|
factory.setDatabase(database);
|
||||||
|
|
||||||
|
// 비밀번호가 설정된 경우에만 적용
|
||||||
|
if (redisPassword != null && !redisPassword.isEmpty()) {
|
||||||
|
factory.setPassword(redisPassword);
|
||||||
|
log.info("Redis 연결 설정 - host: {}, port: {}, database: {}, password: ****", redisHost, redisPort, database);
|
||||||
|
} else {
|
||||||
log.info("Redis 연결 설정 - host: {}, port: {}, database: {}", redisHost, redisPort, database);
|
log.info("Redis 연결 설정 - host: {}, port: {}, database: {}", redisHost, redisPort, database);
|
||||||
|
}
|
||||||
|
|
||||||
return factory;
|
return factory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import com.azure.messaging.eventhubs.EventHubProducerClient;
|
|||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
@ -14,6 +15,7 @@ import org.springframework.context.annotation.Configuration;
|
|||||||
*/
|
*/
|
||||||
@Configuration
|
@Configuration
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
@org.springframework.boot.autoconfigure.condition.ConditionalOnExpression("'${eventhub.connection-string:}'.length() > 0")
|
||||||
public class EventHubConfig {
|
public class EventHubConfig {
|
||||||
|
|
||||||
@Value("${eventhub.connection-string}")
|
@Value("${eventhub.connection-string}")
|
||||||
|
|||||||
@ -12,11 +12,15 @@ import org.springframework.security.config.annotation.web.configurers.AbstractHt
|
|||||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||||
|
import org.springframework.security.web.firewall.HttpFirewall;
|
||||||
|
import org.springframework.security.web.firewall.StrictHttpFirewall;
|
||||||
import org.springframework.web.cors.CorsConfiguration;
|
import org.springframework.web.cors.CorsConfiguration;
|
||||||
import org.springframework.web.cors.CorsConfigurationSource;
|
import org.springframework.web.cors.CorsConfigurationSource;
|
||||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Spring Security 설정
|
* Spring Security 설정
|
||||||
@ -50,8 +54,8 @@ public class SecurityConfig {
|
|||||||
// Meeting API endpoints (for testing)
|
// Meeting API endpoints (for testing)
|
||||||
.requestMatchers("/api/meetings/**").permitAll()
|
.requestMatchers("/api/meetings/**").permitAll()
|
||||||
// All other requests require authentication
|
// All other requests require authentication
|
||||||
.requestMatchers("/api/templates/**").permitAll()
|
// .anyRequest().authenticated()
|
||||||
.anyRequest().authenticated()
|
.anyRequest().permitAll()
|
||||||
)
|
)
|
||||||
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider),
|
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider),
|
||||||
UsernamePasswordAuthenticationFilter.class)
|
UsernamePasswordAuthenticationFilter.class)
|
||||||
@ -72,7 +76,8 @@ public class SecurityConfig {
|
|||||||
// 허용할 헤더
|
// 허용할 헤더
|
||||||
configuration.setAllowedHeaders(Arrays.asList(
|
configuration.setAllowedHeaders(Arrays.asList(
|
||||||
"Authorization", "Content-Type", "X-Requested-With", "Accept",
|
"Authorization", "Content-Type", "X-Requested-With", "Accept",
|
||||||
"Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers"
|
"Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers",
|
||||||
|
"X-User-Id", "X-User-Name", "X-User-Email"
|
||||||
));
|
));
|
||||||
|
|
||||||
// 자격 증명 허용
|
// 자격 증명 허용
|
||||||
@ -85,4 +90,24 @@ public class SecurityConfig {
|
|||||||
source.registerCorsConfiguration("/**", configuration);
|
source.registerCorsConfiguration("/**", configuration);
|
||||||
return source;
|
return source;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HttpFirewall 설정
|
||||||
|
* 한글을 포함한 모든 문자를 헤더 값으로 허용
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public HttpFirewall allowUrlEncodedSlashHttpFirewall() {
|
||||||
|
StrictHttpFirewall firewall = new StrictHttpFirewall();
|
||||||
|
|
||||||
|
// 한글을 포함한 모든 문자를 허용하도록 설정
|
||||||
|
firewall.setAllowedHeaderValues(header -> true);
|
||||||
|
|
||||||
|
// URL 인코딩된 슬래시 허용
|
||||||
|
firewall.setAllowUrlEncodedSlash(true);
|
||||||
|
|
||||||
|
// 세미콜론 허용
|
||||||
|
firewall.setAllowSemicolon(true);
|
||||||
|
|
||||||
|
return firewall;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,6 +32,36 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
HttpServletResponse response,
|
HttpServletResponse response,
|
||||||
FilterChain filterChain) throws ServletException, IOException {
|
FilterChain filterChain) throws ServletException, IOException {
|
||||||
|
|
||||||
|
// 1. X-User-* 헤더를 통한 인증 (개발/테스트용)
|
||||||
|
String headerUserId = request.getHeader("X-User-Id");
|
||||||
|
String headerUserName = request.getHeader("X-User-Name");
|
||||||
|
String headerUserEmail = request.getHeader("X-User-Email");
|
||||||
|
|
||||||
|
if (StringUtils.hasText(headerUserId)) {
|
||||||
|
// X-User-* 헤더가 있으면 이를 사용하여 인증
|
||||||
|
UserPrincipal userPrincipal = UserPrincipal.builder()
|
||||||
|
.userId(headerUserId)
|
||||||
|
.username(headerUserName != null ? headerUserName : "unknown")
|
||||||
|
.email(headerUserEmail)
|
||||||
|
.authority("USER")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
UsernamePasswordAuthenticationToken authentication =
|
||||||
|
new UsernamePasswordAuthenticationToken(
|
||||||
|
userPrincipal,
|
||||||
|
null,
|
||||||
|
Collections.singletonList(new SimpleGrantedAuthority("USER"))
|
||||||
|
);
|
||||||
|
|
||||||
|
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||||
|
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||||
|
|
||||||
|
log.debug("헤더 기반 인증된 사용자: {} ({})", userPrincipal.getUsername(), headerUserId);
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. JWT 토큰을 통한 인증
|
||||||
String token = jwtTokenProvider.resolveToken(request);
|
String token = jwtTokenProvider.resolveToken(request);
|
||||||
|
|
||||||
if (StringUtils.hasText(token) && jwtTokenProvider.validateToken(token)) {
|
if (StringUtils.hasText(token) && jwtTokenProvider.validateToken(token)) {
|
||||||
@ -69,7 +99,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||||
|
|
||||||
log.debug("인증된 사용자: {} ({})", userPrincipal.getUsername(), userId);
|
log.debug("JWT 기반 인증된 사용자: {} ({})", userPrincipal.getUsername(), userId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -23,6 +23,11 @@ public class UserPrincipal {
|
|||||||
*/
|
*/
|
||||||
private final String username;
|
private final String username;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사용자 이메일
|
||||||
|
*/
|
||||||
|
private final String email;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 사용자 권한
|
* 사용자 권한
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -70,6 +70,7 @@ public class MeetingController {
|
|||||||
request.getPurpose(),
|
request.getPurpose(),
|
||||||
request.getAgenda(),
|
request.getAgenda(),
|
||||||
request.getStartTime(),
|
request.getStartTime(),
|
||||||
|
request.getEndTime(),
|
||||||
request.getLocation(),
|
request.getLocation(),
|
||||||
userId,
|
userId,
|
||||||
request.getParticipants(),
|
request.getParticipants(),
|
||||||
@ -153,6 +154,8 @@ public class MeetingController {
|
|||||||
|
|
||||||
log.info("회의 시작 요청 - meetingId: {}, userId: {}", meetingId, userId);
|
log.info("회의 시작 요청 - meetingId: {}, userId: {}", meetingId, userId);
|
||||||
|
|
||||||
|
// meeting id 유효성 검증 필요
|
||||||
|
|
||||||
var sessionData = startMeetingUseCase.startMeeting(meetingId);
|
var sessionData = startMeetingUseCase.startMeeting(meetingId);
|
||||||
var response = SessionResponse.from(sessionData);
|
var response = SessionResponse.from(sessionData);
|
||||||
|
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import java.util.List;
|
|||||||
public class CreateMeetingRequest {
|
public class CreateMeetingRequest {
|
||||||
|
|
||||||
@NotBlank(message = "회의 제목은 필수입니다")
|
@NotBlank(message = "회의 제목은 필수입니다")
|
||||||
|
@jakarta.validation.constraints.Size(max = 100, message = "회의 제목은 100자를 초과할 수 없습니다")
|
||||||
@Schema(description = "회의 제목", example = "Q1 전략 회의", required = true)
|
@Schema(description = "회의 제목", example = "Q1 전략 회의", required = true)
|
||||||
private String title;
|
private String title;
|
||||||
|
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import org.springframework.stereotype.Component;
|
|||||||
*/
|
*/
|
||||||
@Component
|
@Component
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
@org.springframework.boot.autoconfigure.condition.ConditionalOnBean(name = "eventProducer")
|
||||||
public class EventHubPublisher implements EventPublisher {
|
public class EventHubPublisher implements EventPublisher {
|
||||||
|
|
||||||
private final EventHubProducerClient eventProducer;
|
private final EventHubProducerClient eventProducer;
|
||||||
@ -120,6 +121,34 @@ public class EventHubPublisher implements EventPublisher {
|
|||||||
EventHubConstants.EVENT_TYPE_MINUTES_FINALIZED);
|
EventHubConstants.EVENT_TYPE_MINUTES_FINALIZED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void publishMeetingCreated(String meetingId, String title, LocalDateTime startTime,
|
||||||
|
String location, java.util.List<String> participants,
|
||||||
|
String organizerId, String organizerName) {
|
||||||
|
// 각 참석자에게 개별 알림 이벤트 발행
|
||||||
|
for (String participantEmail : participants) {
|
||||||
|
NotificationRequestEvent event = NotificationRequestEvent.builder()
|
||||||
|
.notificationType("MEETING_INVITATION")
|
||||||
|
.recipientEmail(participantEmail)
|
||||||
|
.recipientId(participantEmail)
|
||||||
|
.recipientName(participantEmail)
|
||||||
|
.title("회의 초대")
|
||||||
|
.message(String.format("'%s' 회의에 초대되었습니다. 일시: %s, 장소: %s",
|
||||||
|
title, startTime, location))
|
||||||
|
.relatedEntityId(meetingId)
|
||||||
|
.relatedEntityType("MEETING")
|
||||||
|
.requestedBy(organizerId)
|
||||||
|
.requestedByName(organizerName)
|
||||||
|
.eventTime(LocalDateTime.now())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
publishNotificationRequest(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("회의 생성 알림 발행 완료 - meetingId: {}, participants count: {}",
|
||||||
|
meetingId, participants.size());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 이벤트 발행 공통 메서드
|
* 이벤트 발행 공통 메서드
|
||||||
*
|
*
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import com.unicorn.hgzero.meeting.infra.event.dto.TodoAssignedEvent;
|
|||||||
import com.unicorn.hgzero.meeting.infra.event.dto.NotificationRequestEvent;
|
import com.unicorn.hgzero.meeting.infra.event.dto.NotificationRequestEvent;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 이벤트 발행 인터페이스
|
* 이벤트 발행 인터페이스
|
||||||
@ -59,4 +60,19 @@ public interface EventPublisher {
|
|||||||
* 회의록 확정 이벤트 발행 (편의 메서드)
|
* 회의록 확정 이벤트 발행 (편의 메서드)
|
||||||
*/
|
*/
|
||||||
void publishMinutesFinalized(String minutesId, String title, String finalizedBy, String finalizedByName);
|
void publishMinutesFinalized(String minutesId, String title, String finalizedBy, String finalizedByName);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 회의 생성 알림 발행 (편의 메서드)
|
||||||
|
* 참석자에게 회의 초대 이메일 발송
|
||||||
|
*
|
||||||
|
* @param meetingId 회의 ID
|
||||||
|
* @param title 회의 제목
|
||||||
|
* @param startTime 회의 시작 시간
|
||||||
|
* @param location 회의 장소
|
||||||
|
* @param participants 참석자 이메일 목록
|
||||||
|
* @param organizerId 주최자 ID
|
||||||
|
* @param organizerName 주최자 이름
|
||||||
|
*/
|
||||||
|
void publishMeetingCreated(String meetingId, String title, LocalDateTime startTime,
|
||||||
|
String location, List<String> participants, String organizerId, String organizerName);
|
||||||
}
|
}
|
||||||
@ -0,0 +1,69 @@
|
|||||||
|
package com.unicorn.hgzero.meeting.infra.event.publisher;
|
||||||
|
|
||||||
|
import com.unicorn.hgzero.meeting.infra.event.dto.MeetingStartedEvent;
|
||||||
|
import com.unicorn.hgzero.meeting.infra.event.dto.MeetingEndedEvent;
|
||||||
|
import com.unicorn.hgzero.meeting.infra.event.dto.TodoAssignedEvent;
|
||||||
|
import com.unicorn.hgzero.meeting.infra.event.dto.NotificationRequestEvent;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
|
import org.springframework.context.annotation.Primary;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No-Op EventPublisher 구현체
|
||||||
|
* EventHub가 설정되지 않은 경우 사용되는 더미 구현체
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@Primary
|
||||||
|
@ConditionalOnMissingBean(name = "eventProducer")
|
||||||
|
@Slf4j
|
||||||
|
public class NoOpEventPublisher implements EventPublisher {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void publishMeetingStarted(MeetingStartedEvent event) {
|
||||||
|
log.debug("[NoOp] Meeting started event: {}", event.getMeetingId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void publishMeetingEnded(MeetingEndedEvent event) {
|
||||||
|
log.debug("[NoOp] Meeting ended event: {}", event.getMeetingId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void publishTodoAssigned(TodoAssignedEvent event) {
|
||||||
|
log.debug("[NoOp] Todo assigned event: {}", event.getTodoId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void publishNotificationRequest(NotificationRequestEvent event) {
|
||||||
|
log.debug("[NoOp] Notification request event: {}", event.getRecipientId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void publishTodoAssigned(String todoId, String title, String assigneeId, String assigneeName,
|
||||||
|
String assignedBy, String assignedByName, LocalDate dueDate) {
|
||||||
|
log.debug("[NoOp] Todo assigned: todoId={}, title={}", todoId, title);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void publishTodoCompleted(String todoId, String title, String assigneeId, String assigneeName,
|
||||||
|
String completedBy, String completedByName) {
|
||||||
|
log.debug("[NoOp] Todo completed: todoId={}, title={}", todoId, title);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void publishMinutesFinalized(String minutesId, String title, String finalizedBy, String finalizedByName) {
|
||||||
|
log.debug("[NoOp] Minutes finalized: minutesId={}, title={}", minutesId, title);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void publishMeetingCreated(String meetingId, String title, LocalDateTime startTime,
|
||||||
|
String location, List<String> participants, String organizerId, String organizerName) {
|
||||||
|
log.debug("[NoOp] Meeting created: meetingId={}, title={}, participants={}",
|
||||||
|
meetingId, title, participants.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -77,4 +77,9 @@ public class MeetingGateway implements MeetingReader, MeetingWriter {
|
|||||||
public void delete(String meetingId) {
|
public void delete(String meetingId) {
|
||||||
meetingJpaRepository.deleteById(meetingId);
|
meetingJpaRepository.deleteById(meetingId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long countConflictingMeetings(String organizerId, LocalDateTime startTime, LocalDateTime endTime) {
|
||||||
|
return meetingJpaRepository.countConflictingMeetings(organizerId, startTime, endTime);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,12 +31,21 @@ public class MeetingEntity extends BaseTimeEntity {
|
|||||||
@Column(name = "title", length = 200, nullable = false)
|
@Column(name = "title", length = 200, nullable = false)
|
||||||
private String title;
|
private String title;
|
||||||
|
|
||||||
|
@Column(name = "purpose", length = 500)
|
||||||
|
private String purpose;
|
||||||
|
|
||||||
@Column(name = "description", columnDefinition = "TEXT")
|
@Column(name = "description", columnDefinition = "TEXT")
|
||||||
private String description;
|
private String description;
|
||||||
|
|
||||||
@Column(name = "scheduled_at", nullable = false)
|
@Column(name = "scheduled_at", nullable = false)
|
||||||
private LocalDateTime scheduledAt;
|
private LocalDateTime scheduledAt;
|
||||||
|
|
||||||
|
@Column(name = "end_time")
|
||||||
|
private LocalDateTime endTime;
|
||||||
|
|
||||||
|
@Column(name = "location", length = 200)
|
||||||
|
private String location;
|
||||||
|
|
||||||
@Column(name = "started_at")
|
@Column(name = "started_at")
|
||||||
private LocalDateTime startedAt;
|
private LocalDateTime startedAt;
|
||||||
|
|
||||||
@ -60,8 +69,11 @@ public class MeetingEntity extends BaseTimeEntity {
|
|||||||
return Meeting.builder()
|
return Meeting.builder()
|
||||||
.meetingId(this.meetingId)
|
.meetingId(this.meetingId)
|
||||||
.title(this.title)
|
.title(this.title)
|
||||||
|
.purpose(this.purpose)
|
||||||
.description(this.description)
|
.description(this.description)
|
||||||
.scheduledAt(this.scheduledAt)
|
.scheduledAt(this.scheduledAt)
|
||||||
|
.endTime(this.endTime)
|
||||||
|
.location(this.location)
|
||||||
.startedAt(this.startedAt)
|
.startedAt(this.startedAt)
|
||||||
.endedAt(this.endedAt)
|
.endedAt(this.endedAt)
|
||||||
.status(this.status)
|
.status(this.status)
|
||||||
@ -75,8 +87,11 @@ public class MeetingEntity extends BaseTimeEntity {
|
|||||||
return MeetingEntity.builder()
|
return MeetingEntity.builder()
|
||||||
.meetingId(meeting.getMeetingId())
|
.meetingId(meeting.getMeetingId())
|
||||||
.title(meeting.getTitle())
|
.title(meeting.getTitle())
|
||||||
|
.purpose(meeting.getPurpose())
|
||||||
.description(meeting.getDescription())
|
.description(meeting.getDescription())
|
||||||
.scheduledAt(meeting.getScheduledAt())
|
.scheduledAt(meeting.getScheduledAt())
|
||||||
|
.endTime(meeting.getEndTime())
|
||||||
|
.location(meeting.getLocation())
|
||||||
.startedAt(meeting.getStartedAt())
|
.startedAt(meeting.getStartedAt())
|
||||||
.endedAt(meeting.getEndedAt())
|
.endedAt(meeting.getEndedAt())
|
||||||
.status(meeting.getStatus())
|
.status(meeting.getStatus())
|
||||||
|
|||||||
@ -37,4 +37,26 @@ public interface MeetingJpaRepository extends JpaRepository<MeetingEntity, Strin
|
|||||||
* 템플릿 ID로 회의 목록 조회
|
* 템플릿 ID로 회의 목록 조회
|
||||||
*/
|
*/
|
||||||
List<MeetingEntity> findByTemplateId(String templateId);
|
List<MeetingEntity> findByTemplateId(String templateId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 주최자의 특정 시간대 중복 회의 개수 조회
|
||||||
|
* 회의 상태가 SCHEDULED 또는 IN_PROGRESS이고,
|
||||||
|
* 시간이 겹치는 회의가 있는지 확인
|
||||||
|
*
|
||||||
|
* @param organizerId 주최자 ID
|
||||||
|
* @param startTime 회의 시작 시간
|
||||||
|
* @param endTime 회의 종료 시간
|
||||||
|
* @return 중복 회의 개수
|
||||||
|
*/
|
||||||
|
@org.springframework.data.jpa.repository.Query(
|
||||||
|
"SELECT COUNT(m) FROM MeetingEntity m WHERE " +
|
||||||
|
"m.organizerId = :organizerId AND " +
|
||||||
|
"m.status IN ('SCHEDULED', 'IN_PROGRESS') AND " +
|
||||||
|
"((m.scheduledAt < :endTime AND m.endTime > :startTime))"
|
||||||
|
)
|
||||||
|
long countConflictingMeetings(
|
||||||
|
@org.springframework.data.repository.query.Param("organizerId") String organizerId,
|
||||||
|
@org.springframework.data.repository.query.Param("startTime") LocalDateTime startTime,
|
||||||
|
@org.springframework.data.repository.query.Param("endTime") LocalDateTime endTime
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,7 @@ spring:
|
|||||||
datasource:
|
datasource:
|
||||||
url: jdbc:${DB_KIND:postgresql}://${DB_HOST:4.230.48.72}:${DB_PORT:5432}/${DB_NAME:meetingdb}
|
url: jdbc:${DB_KIND:postgresql}://${DB_HOST:4.230.48.72}:${DB_PORT:5432}/${DB_NAME:meetingdb}
|
||||||
username: ${DB_USERNAME:hgzerouser}
|
username: ${DB_USERNAME:hgzerouser}
|
||||||
password: ${DB_PASSWORD:}
|
password: ${DB_PASSWORD:Hi5Jessica!}
|
||||||
driver-class-name: org.postgresql.Driver
|
driver-class-name: org.postgresql.Driver
|
||||||
hikari:
|
hikari:
|
||||||
maximum-pool-size: 20
|
maximum-pool-size: 20
|
||||||
@ -35,7 +35,7 @@ spring:
|
|||||||
redis:
|
redis:
|
||||||
host: ${REDIS_HOST:20.249.177.114}
|
host: ${REDIS_HOST:20.249.177.114}
|
||||||
port: ${REDIS_PORT:6379}
|
port: ${REDIS_PORT:6379}
|
||||||
password: ${REDIS_PASSWORD:}
|
password: ${REDIS_PASSWORD:Hi5Jessica!}
|
||||||
timeout: 2000ms
|
timeout: 2000ms
|
||||||
lettuce:
|
lettuce:
|
||||||
pool:
|
pool:
|
||||||
@ -51,7 +51,7 @@ server:
|
|||||||
|
|
||||||
# JWT Configuration
|
# JWT Configuration
|
||||||
jwt:
|
jwt:
|
||||||
secret: ${JWT_SECRET:}
|
secret: ${JWT_SECRET:hgzero-jwt-secret-key-for-dev-environment-only-do-not-use-in-production-minimum-256-bits}
|
||||||
access-token-validity: ${JWT_ACCESS_TOKEN_VALIDITY:3600}
|
access-token-validity: ${JWT_ACCESS_TOKEN_VALIDITY:3600}
|
||||||
refresh-token-validity: ${JWT_REFRESH_TOKEN_VALIDITY:604800}
|
refresh-token-validity: ${JWT_REFRESH_TOKEN_VALIDITY:604800}
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,18 @@ bootJar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
// Common module
|
||||||
|
implementation project(':common')
|
||||||
|
|
||||||
|
// Spring Boot starters
|
||||||
|
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||||
|
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
|
||||||
|
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
||||||
|
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
|
||||||
|
|
||||||
|
// Database
|
||||||
|
runtimeOnly 'org.postgresql:postgresql'
|
||||||
|
|
||||||
// Azure Speech SDK
|
// Azure Speech SDK
|
||||||
implementation "com.microsoft.cognitiveservices.speech:client-sdk:${azureSpeechVersion}"
|
implementation "com.microsoft.cognitiveservices.speech:client-sdk:${azureSpeechVersion}"
|
||||||
|
|
||||||
@ -14,4 +26,11 @@ dependencies {
|
|||||||
|
|
||||||
// WebSocket
|
// WebSocket
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-websocket'
|
implementation 'org.springframework.boot:spring-boot-starter-websocket'
|
||||||
|
|
||||||
|
// Test dependencies
|
||||||
|
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||||
|
testRuntimeOnly 'com.h2database:h2'
|
||||||
|
testImplementation('it.ozimov:embedded-redis:0.7.3') {
|
||||||
|
exclude group: 'org.slf4j', module: 'slf4j-simple'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,146 +0,0 @@
|
|||||||
package com.unicorn.hgzero.stt.controller;
|
|
||||||
|
|
||||||
import com.unicorn.hgzero.common.dto.ApiResponse;
|
|
||||||
import com.unicorn.hgzero.stt.dto.SpeakerDto;
|
|
||||||
import com.unicorn.hgzero.stt.service.SpeakerService;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
|
||||||
import io.swagger.v3.oas.annotations.media.Content;
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.validation.annotation.Validated;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
|
|
||||||
import jakarta.validation.Valid;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 화자 관리 컨트롤러
|
|
||||||
* 화자 식별 및 관리 기능을 제공
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/v1/stt/speakers")
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
@Validated
|
|
||||||
@Tag(name = "화자 관리", description = "화자 식별 및 관리 API")
|
|
||||||
public class SpeakerController {
|
|
||||||
|
|
||||||
private final SpeakerService speakerService;
|
|
||||||
|
|
||||||
@Operation(
|
|
||||||
summary = "화자 식별",
|
|
||||||
description = "음성 데이터로부터 화자를 식별합니다."
|
|
||||||
)
|
|
||||||
@ApiResponses(value = {
|
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
|
||||||
responseCode = "200",
|
|
||||||
description = "화자 식별 성공",
|
|
||||||
content = @Content(schema = @Schema(implementation = SpeakerDto.IdentificationResponse.class))
|
|
||||||
),
|
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
|
||||||
responseCode = "400",
|
|
||||||
description = "잘못된 요청"
|
|
||||||
),
|
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
|
||||||
responseCode = "404",
|
|
||||||
description = "녹음을 찾을 수 없음"
|
|
||||||
)
|
|
||||||
})
|
|
||||||
@PostMapping("/identify")
|
|
||||||
public ResponseEntity<ApiResponse<SpeakerDto.IdentificationResponse>> identifySpeaker(
|
|
||||||
@Valid @RequestBody SpeakerDto.IdentifyRequest request) {
|
|
||||||
log.info("화자 식별 요청 - recordingId: {}", request.getRecordingId());
|
|
||||||
|
|
||||||
SpeakerDto.IdentificationResponse response = speakerService.identifySpeaker(request);
|
|
||||||
|
|
||||||
return ResponseEntity.ok(ApiResponse.success(response));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(
|
|
||||||
summary = "화자 정보 조회",
|
|
||||||
description = "화자의 상세 정보를 조회합니다."
|
|
||||||
)
|
|
||||||
@ApiResponses(value = {
|
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
|
||||||
responseCode = "200",
|
|
||||||
description = "조회 성공",
|
|
||||||
content = @Content(schema = @Schema(implementation = SpeakerDto.DetailResponse.class))
|
|
||||||
),
|
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
|
||||||
responseCode = "404",
|
|
||||||
description = "화자를 찾을 수 없음"
|
|
||||||
)
|
|
||||||
})
|
|
||||||
@GetMapping("/{speakerId}")
|
|
||||||
public ResponseEntity<ApiResponse<SpeakerDto.DetailResponse>> getSpeaker(
|
|
||||||
@Parameter(description = "화자 ID", required = true)
|
|
||||||
@PathVariable String speakerId) {
|
|
||||||
log.debug("화자 조회 요청 - speakerId: {}", speakerId);
|
|
||||||
|
|
||||||
SpeakerDto.DetailResponse response = speakerService.getSpeaker(speakerId);
|
|
||||||
|
|
||||||
return ResponseEntity.ok(ApiResponse.success(response));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(
|
|
||||||
summary = "화자 정보 수정",
|
|
||||||
description = "화자의 이름 및 사용자 연결 정보를 수정합니다."
|
|
||||||
)
|
|
||||||
@ApiResponses(value = {
|
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
|
||||||
responseCode = "200",
|
|
||||||
description = "수정 성공",
|
|
||||||
content = @Content(schema = @Schema(implementation = SpeakerDto.DetailResponse.class))
|
|
||||||
),
|
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
|
||||||
responseCode = "400",
|
|
||||||
description = "잘못된 요청"
|
|
||||||
),
|
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
|
||||||
responseCode = "404",
|
|
||||||
description = "화자를 찾을 수 없음"
|
|
||||||
)
|
|
||||||
})
|
|
||||||
@PutMapping("/{speakerId}")
|
|
||||||
public ResponseEntity<ApiResponse<SpeakerDto.DetailResponse>> updateSpeaker(
|
|
||||||
@Parameter(description = "화자 ID", required = true)
|
|
||||||
@PathVariable String speakerId,
|
|
||||||
@Valid @RequestBody SpeakerDto.UpdateRequest request) {
|
|
||||||
log.info("화자 정보 수정 요청 - speakerId: {}, speakerName: {}",
|
|
||||||
speakerId, request.getSpeakerName());
|
|
||||||
|
|
||||||
SpeakerDto.DetailResponse response = speakerService.updateSpeaker(speakerId, request);
|
|
||||||
|
|
||||||
return ResponseEntity.ok(ApiResponse.success(response));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(
|
|
||||||
summary = "녹음별 화자 목록 조회",
|
|
||||||
description = "특정 녹음에 참여한 화자 목록과 통계를 조회합니다."
|
|
||||||
)
|
|
||||||
@ApiResponses(value = {
|
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
|
||||||
responseCode = "200",
|
|
||||||
description = "조회 성공",
|
|
||||||
content = @Content(schema = @Schema(implementation = SpeakerDto.ListResponse.class))
|
|
||||||
),
|
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
|
||||||
responseCode = "404",
|
|
||||||
description = "녹음을 찾을 수 없음"
|
|
||||||
)
|
|
||||||
})
|
|
||||||
@GetMapping("/recordings/{recordingId}")
|
|
||||||
public ResponseEntity<ApiResponse<SpeakerDto.ListResponse>> getRecordingSpeakers(
|
|
||||||
@Parameter(description = "녹음 ID", required = true)
|
|
||||||
@PathVariable String recordingId) {
|
|
||||||
log.debug("녹음별 화자 목록 조회 요청 - recordingId: {}", recordingId);
|
|
||||||
|
|
||||||
SpeakerDto.ListResponse response = speakerService.getRecordingSpeakers(recordingId);
|
|
||||||
|
|
||||||
return ResponseEntity.ok(ApiResponse.success(response));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -12,11 +12,9 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.http.MediaType;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
|
||||||
|
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
|
|
||||||
@ -64,64 +62,7 @@ public class TranscriptionController {
|
|||||||
return ResponseEntity.ok(ApiResponse.success(response));
|
return ResponseEntity.ok(ApiResponse.success(response));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(
|
|
||||||
summary = "배치 음성 변환",
|
|
||||||
description = "오디오 파일을 업로드하여 배치 변환 작업을 시작합니다."
|
|
||||||
)
|
|
||||||
@ApiResponses(value = {
|
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
|
||||||
responseCode = "200",
|
|
||||||
description = "배치 작업 시작 성공",
|
|
||||||
content = @Content(schema = @Schema(implementation = TranscriptionDto.BatchResponse.class))
|
|
||||||
),
|
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
|
||||||
responseCode = "400",
|
|
||||||
description = "잘못된 파일 형식 또는 요청"
|
|
||||||
),
|
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
|
||||||
responseCode = "404",
|
|
||||||
description = "녹음 세션을 찾을 수 없음"
|
|
||||||
)
|
|
||||||
})
|
|
||||||
@PostMapping(value = "/batch", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
|
||||||
public ResponseEntity<ApiResponse<TranscriptionDto.BatchResponse>> transcribeAudioBatch(
|
|
||||||
@Parameter(description = "배치 변환 요청 정보")
|
|
||||||
@Valid @RequestPart("request") TranscriptionDto.BatchRequest request,
|
|
||||||
@Parameter(description = "변환할 오디오 파일")
|
|
||||||
@RequestPart("audioFile") MultipartFile audioFile) {
|
|
||||||
log.info("배치 음성 변환 요청 - recordingId: {}, fileSize: {}",
|
|
||||||
request.getRecordingId(), audioFile.getSize());
|
|
||||||
|
|
||||||
TranscriptionDto.BatchResponse response = transcriptionService.transcribeAudioBatch(request, audioFile);
|
|
||||||
|
|
||||||
return ResponseEntity.ok(ApiResponse.success(response));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(
|
|
||||||
summary = "배치 변환 완료 콜백",
|
|
||||||
description = "Azure 배치 변환 완료 시 호출되는 콜백 엔드포인트입니다."
|
|
||||||
)
|
|
||||||
@ApiResponses(value = {
|
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
|
||||||
responseCode = "200",
|
|
||||||
description = "콜백 처리 성공",
|
|
||||||
content = @Content(schema = @Schema(implementation = TranscriptionDto.CompleteResponse.class))
|
|
||||||
),
|
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
|
||||||
responseCode = "400",
|
|
||||||
description = "잘못된 콜백 데이터"
|
|
||||||
)
|
|
||||||
})
|
|
||||||
@PostMapping("/batch/callback")
|
|
||||||
public ResponseEntity<ApiResponse<TranscriptionDto.CompleteResponse>> processBatchCallback(
|
|
||||||
@Valid @RequestBody TranscriptionDto.BatchCallbackRequest request) {
|
|
||||||
log.info("배치 변환 콜백 처리 - jobId: {}, status: {}",
|
|
||||||
request.getJobId(), request.getStatus());
|
|
||||||
|
|
||||||
TranscriptionDto.CompleteResponse response = transcriptionService.processBatchCallback(request);
|
|
||||||
|
|
||||||
return ResponseEntity.ok(ApiResponse.success(response));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "변환 결과 조회",
|
summary = "변환 결과 조회",
|
||||||
@ -141,15 +82,10 @@ public class TranscriptionController {
|
|||||||
@GetMapping("/{recordingId}")
|
@GetMapping("/{recordingId}")
|
||||||
public ResponseEntity<ApiResponse<TranscriptionDto.Response>> getTranscription(
|
public ResponseEntity<ApiResponse<TranscriptionDto.Response>> getTranscription(
|
||||||
@Parameter(description = "녹음 ID", required = true)
|
@Parameter(description = "녹음 ID", required = true)
|
||||||
@PathVariable String recordingId,
|
@PathVariable String recordingId) {
|
||||||
@Parameter(description = "세그먼트 정보 포함 여부")
|
log.debug("변환 결과 조회 요청 - recordingId: {}", recordingId);
|
||||||
@RequestParam(required = false, defaultValue = "false") Boolean includeSegments,
|
|
||||||
@Parameter(description = "특정 화자 필터")
|
|
||||||
@RequestParam(required = false) String speakerId) {
|
|
||||||
log.debug("변환 결과 조회 요청 - recordingId: {}, includeSegments: {}, speakerId: {}",
|
|
||||||
recordingId, includeSegments, speakerId);
|
|
||||||
|
|
||||||
TranscriptionDto.Response response = transcriptionService.getTranscription(recordingId, includeSegments, speakerId);
|
TranscriptionDto.Response response = transcriptionService.getTranscription(recordingId);
|
||||||
|
|
||||||
return ResponseEntity.ok(ApiResponse.success(response));
|
return ResponseEntity.ok(ApiResponse.success(response));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -50,16 +50,6 @@ public class Recording {
|
|||||||
*/
|
*/
|
||||||
private final Integer duration;
|
private final Integer duration;
|
||||||
|
|
||||||
/**
|
|
||||||
* 파일 크기 (bytes)
|
|
||||||
*/
|
|
||||||
private final Long fileSize;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 저장 경로
|
|
||||||
*/
|
|
||||||
private final String storagePath;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 언어 설정
|
* 언어 설정
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -1,72 +0,0 @@
|
|||||||
package com.unicorn.hgzero.stt.domain;
|
|
||||||
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.ToString;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 화자 도메인 모델
|
|
||||||
* 음성 화자 정보를 나타내는 도메인 객체
|
|
||||||
*/
|
|
||||||
@Getter
|
|
||||||
@Builder
|
|
||||||
@ToString
|
|
||||||
public class Speaker {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 화자 ID
|
|
||||||
*/
|
|
||||||
private final String speakerId;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 화자 이름
|
|
||||||
*/
|
|
||||||
private final String speakerName;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Azure Speaker Profile ID
|
|
||||||
*/
|
|
||||||
private final String profileId;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 연결된 사용자 ID
|
|
||||||
*/
|
|
||||||
private final String userId;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 총 발언 세그먼트 수
|
|
||||||
*/
|
|
||||||
private final Integer totalSegments;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 총 발언 시간 (초)
|
|
||||||
*/
|
|
||||||
private final Integer totalDuration;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 평균 식별 신뢰도
|
|
||||||
*/
|
|
||||||
private final Double averageConfidence;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 최초 등장 시간
|
|
||||||
*/
|
|
||||||
private final LocalDateTime firstAppeared;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 최근 등장 시간
|
|
||||||
*/
|
|
||||||
private final LocalDateTime lastAppeared;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 생성 시간
|
|
||||||
*/
|
|
||||||
private final LocalDateTime createdAt;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 수정 시간
|
|
||||||
*/
|
|
||||||
private final LocalDateTime updatedAt;
|
|
||||||
}
|
|
||||||
@ -50,7 +50,6 @@ public class RecordingDto {
|
|||||||
private final String sessionId;
|
private final String sessionId;
|
||||||
private final String status;
|
private final String status;
|
||||||
private final String streamUrl;
|
private final String streamUrl;
|
||||||
private final String storagePath;
|
|
||||||
private final Integer estimatedInitTime;
|
private final Integer estimatedInitTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,8 +94,6 @@ public class RecordingDto {
|
|||||||
private final LocalDateTime startTime;
|
private final LocalDateTime startTime;
|
||||||
private final LocalDateTime endTime;
|
private final LocalDateTime endTime;
|
||||||
private final Integer duration;
|
private final Integer duration;
|
||||||
private final Long fileSize;
|
|
||||||
private final String storagePath;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -116,7 +113,6 @@ public class RecordingDto {
|
|||||||
private final Integer duration;
|
private final Integer duration;
|
||||||
private final Integer speakerCount;
|
private final Integer speakerCount;
|
||||||
private final Integer segmentCount;
|
private final Integer segmentCount;
|
||||||
private final String storagePath;
|
|
||||||
private final String language;
|
private final String language;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,109 +0,0 @@
|
|||||||
package com.unicorn.hgzero.stt.dto;
|
|
||||||
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.ToString;
|
|
||||||
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
|
||||||
import jakarta.validation.constraints.DecimalMax;
|
|
||||||
import jakarta.validation.constraints.DecimalMin;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 화자 관련 DTO 클래스들
|
|
||||||
*/
|
|
||||||
public class SpeakerDto {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 화자 식별 요청 DTO
|
|
||||||
*/
|
|
||||||
@Getter
|
|
||||||
@Builder
|
|
||||||
@ToString
|
|
||||||
public static class IdentifyRequest {
|
|
||||||
|
|
||||||
@NotBlank(message = "녹음 ID는 필수입니다")
|
|
||||||
private final String recordingId;
|
|
||||||
|
|
||||||
@NotBlank(message = "오디오 프레임은 필수입니다")
|
|
||||||
private final String audioFrame;
|
|
||||||
|
|
||||||
private final Long timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 화자 식별 응답 DTO
|
|
||||||
*/
|
|
||||||
@Getter
|
|
||||||
@Builder
|
|
||||||
@ToString
|
|
||||||
public static class IdentificationResponse {
|
|
||||||
|
|
||||||
private final String speakerId;
|
|
||||||
private final String speakerName;
|
|
||||||
private final Double confidence;
|
|
||||||
private final Boolean isNewSpeaker;
|
|
||||||
private final String profileId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 화자 상세 응답 DTO
|
|
||||||
*/
|
|
||||||
@Getter
|
|
||||||
@Builder
|
|
||||||
@ToString
|
|
||||||
public static class DetailResponse {
|
|
||||||
|
|
||||||
private final String speakerId;
|
|
||||||
private final String speakerName;
|
|
||||||
private final String profileId;
|
|
||||||
private final String userId;
|
|
||||||
private final Integer totalSegments;
|
|
||||||
private final Integer totalDuration;
|
|
||||||
private final Double averageConfidence;
|
|
||||||
private final LocalDateTime firstAppeared;
|
|
||||||
private final LocalDateTime lastAppeared;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 화자 정보 업데이트 요청 DTO
|
|
||||||
*/
|
|
||||||
@Getter
|
|
||||||
@Builder
|
|
||||||
@ToString
|
|
||||||
public static class UpdateRequest {
|
|
||||||
|
|
||||||
private final String speakerName;
|
|
||||||
private final String userId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 화자 목록 응답 DTO
|
|
||||||
*/
|
|
||||||
@Getter
|
|
||||||
@Builder
|
|
||||||
@ToString
|
|
||||||
public static class ListResponse {
|
|
||||||
|
|
||||||
private final String recordingId;
|
|
||||||
private final Integer speakerCount;
|
|
||||||
private final List<SpeakerSummary> speakers;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 화자 요약 DTO
|
|
||||||
*/
|
|
||||||
@Getter
|
|
||||||
@Builder
|
|
||||||
@ToString
|
|
||||||
public static class SpeakerSummary {
|
|
||||||
|
|
||||||
private final String speakerId;
|
|
||||||
private final String speakerName;
|
|
||||||
private final Integer segmentCount;
|
|
||||||
private final Integer totalDuration;
|
|
||||||
private final Double speakingRatio;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -62,12 +62,10 @@ public class RecordingEvent {
|
|||||||
private LocalDateTime startTime;
|
private LocalDateTime startTime;
|
||||||
private LocalDateTime endTime;
|
private LocalDateTime endTime;
|
||||||
private Integer duration;
|
private Integer duration;
|
||||||
private Long fileSize;
|
|
||||||
private String storagePath;
|
|
||||||
private LocalDateTime eventTime;
|
private LocalDateTime eventTime;
|
||||||
|
|
||||||
public static RecordingStopped of(String recordingId, String meetingId, String stoppedBy,
|
public static RecordingStopped of(String recordingId, String meetingId, String stoppedBy,
|
||||||
LocalDateTime startTime, Integer duration, Long fileSize, String storagePath) {
|
LocalDateTime startTime, Integer duration) {
|
||||||
return RecordingStopped.builder()
|
return RecordingStopped.builder()
|
||||||
.eventId(java.util.UUID.randomUUID().toString())
|
.eventId(java.util.UUID.randomUUID().toString())
|
||||||
.eventType("RecordingStopped")
|
.eventType("RecordingStopped")
|
||||||
@ -77,8 +75,6 @@ public class RecordingEvent {
|
|||||||
.startTime(startTime)
|
.startTime(startTime)
|
||||||
.endTime(LocalDateTime.now())
|
.endTime(LocalDateTime.now())
|
||||||
.duration(duration)
|
.duration(duration)
|
||||||
.fileSize(fileSize)
|
|
||||||
.storagePath(storagePath)
|
|
||||||
.eventTime(LocalDateTime.now())
|
.eventTime(LocalDateTime.now())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,120 +0,0 @@
|
|||||||
package com.unicorn.hgzero.stt.event;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 화자 관련 이벤트 정의
|
|
||||||
*/
|
|
||||||
public class SpeakerEvent {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 신규 화자 식별 이벤트
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
@Builder
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
public static class NewSpeakerIdentified {
|
|
||||||
private String eventId;
|
|
||||||
private String eventType;
|
|
||||||
private String speakerId;
|
|
||||||
private String speakerName;
|
|
||||||
private String profileId;
|
|
||||||
private String recordingId;
|
|
||||||
private String meetingId;
|
|
||||||
private Double confidence;
|
|
||||||
private LocalDateTime firstAppeared;
|
|
||||||
private LocalDateTime eventTime;
|
|
||||||
|
|
||||||
public static NewSpeakerIdentified of(String speakerId, String speakerName, String profileId,
|
|
||||||
String recordingId, String meetingId, Double confidence) {
|
|
||||||
return NewSpeakerIdentified.builder()
|
|
||||||
.eventId(java.util.UUID.randomUUID().toString())
|
|
||||||
.eventType("NewSpeakerIdentified")
|
|
||||||
.speakerId(speakerId)
|
|
||||||
.speakerName(speakerName)
|
|
||||||
.profileId(profileId)
|
|
||||||
.recordingId(recordingId)
|
|
||||||
.meetingId(meetingId)
|
|
||||||
.confidence(confidence)
|
|
||||||
.firstAppeared(LocalDateTime.now())
|
|
||||||
.eventTime(LocalDateTime.now())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 화자 정보 업데이트 이벤트
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
@Builder
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
public static class SpeakerUpdated {
|
|
||||||
private String eventId;
|
|
||||||
private String eventType;
|
|
||||||
private String speakerId;
|
|
||||||
private String oldSpeakerName;
|
|
||||||
private String newSpeakerName;
|
|
||||||
private String oldUserId;
|
|
||||||
private String newUserId;
|
|
||||||
private String updatedBy;
|
|
||||||
private LocalDateTime eventTime;
|
|
||||||
|
|
||||||
public static SpeakerUpdated of(String speakerId, String oldSpeakerName, String newSpeakerName,
|
|
||||||
String oldUserId, String newUserId, String updatedBy) {
|
|
||||||
return SpeakerUpdated.builder()
|
|
||||||
.eventId(java.util.UUID.randomUUID().toString())
|
|
||||||
.eventType("SpeakerUpdated")
|
|
||||||
.speakerId(speakerId)
|
|
||||||
.oldSpeakerName(oldSpeakerName)
|
|
||||||
.newSpeakerName(newSpeakerName)
|
|
||||||
.oldUserId(oldUserId)
|
|
||||||
.newUserId(newUserId)
|
|
||||||
.updatedBy(updatedBy)
|
|
||||||
.eventTime(LocalDateTime.now())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 화자 통계 업데이트 이벤트
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
@Builder
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
public static class SpeakerStatisticsUpdated {
|
|
||||||
private String eventId;
|
|
||||||
private String eventType;
|
|
||||||
private String speakerId;
|
|
||||||
private String recordingId;
|
|
||||||
private String meetingId;
|
|
||||||
private Integer totalSegments;
|
|
||||||
private Integer totalDuration;
|
|
||||||
private Double averageConfidence;
|
|
||||||
private LocalDateTime lastAppeared;
|
|
||||||
private LocalDateTime eventTime;
|
|
||||||
|
|
||||||
public static SpeakerStatisticsUpdated of(String speakerId, String recordingId, String meetingId,
|
|
||||||
Integer totalSegments, Integer totalDuration, Double averageConfidence) {
|
|
||||||
return SpeakerStatisticsUpdated.builder()
|
|
||||||
.eventId(java.util.UUID.randomUUID().toString())
|
|
||||||
.eventType("SpeakerStatisticsUpdated")
|
|
||||||
.speakerId(speakerId)
|
|
||||||
.recordingId(recordingId)
|
|
||||||
.meetingId(meetingId)
|
|
||||||
.totalSegments(totalSegments)
|
|
||||||
.totalDuration(totalDuration)
|
|
||||||
.averageConfidence(averageConfidence)
|
|
||||||
.lastAppeared(LocalDateTime.now())
|
|
||||||
.eventTime(LocalDateTime.now())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -43,12 +43,6 @@ public class RecordingEntity extends BaseTimeEntity {
|
|||||||
@Column(name = "duration")
|
@Column(name = "duration")
|
||||||
private Integer duration;
|
private Integer duration;
|
||||||
|
|
||||||
@Column(name = "file_size")
|
|
||||||
private Long fileSize;
|
|
||||||
|
|
||||||
@Column(name = "storage_path", length = 500)
|
|
||||||
private String storagePath;
|
|
||||||
|
|
||||||
@Column(name = "language", length = 10, nullable = false)
|
@Column(name = "language", length = 10, nullable = false)
|
||||||
private String language;
|
private String language;
|
||||||
|
|
||||||
@ -70,8 +64,6 @@ public class RecordingEntity extends BaseTimeEntity {
|
|||||||
.startTime(startTime)
|
.startTime(startTime)
|
||||||
.endTime(endTime)
|
.endTime(endTime)
|
||||||
.duration(duration)
|
.duration(duration)
|
||||||
.fileSize(fileSize)
|
|
||||||
.storagePath(storagePath)
|
|
||||||
.language(language)
|
.language(language)
|
||||||
.speakerCount(speakerCount)
|
.speakerCount(speakerCount)
|
||||||
.segmentCount(segmentCount)
|
.segmentCount(segmentCount)
|
||||||
@ -90,8 +82,6 @@ public class RecordingEntity extends BaseTimeEntity {
|
|||||||
.startTime(recording.getStartTime())
|
.startTime(recording.getStartTime())
|
||||||
.endTime(recording.getEndTime())
|
.endTime(recording.getEndTime())
|
||||||
.duration(recording.getDuration())
|
.duration(recording.getDuration())
|
||||||
.fileSize(recording.getFileSize())
|
|
||||||
.storagePath(recording.getStoragePath())
|
|
||||||
.language(recording.getLanguage())
|
.language(recording.getLanguage())
|
||||||
.speakerCount(recording.getSpeakerCount())
|
.speakerCount(recording.getSpeakerCount())
|
||||||
.segmentCount(recording.getSegmentCount())
|
.segmentCount(recording.getSegmentCount())
|
||||||
|
|||||||
@ -1,104 +0,0 @@
|
|||||||
package com.unicorn.hgzero.stt.repository.entity;
|
|
||||||
|
|
||||||
import com.unicorn.hgzero.common.entity.BaseTimeEntity;
|
|
||||||
import com.unicorn.hgzero.stt.domain.Speaker;
|
|
||||||
import jakarta.persistence.*;
|
|
||||||
import lombok.*;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 화자 엔티티
|
|
||||||
* 음성 화자 정보를 저장하는 JPA 엔티티
|
|
||||||
*/
|
|
||||||
@Entity
|
|
||||||
@Table(name = "speakers")
|
|
||||||
@Getter
|
|
||||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
|
||||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
|
||||||
@Builder
|
|
||||||
@ToString
|
|
||||||
public class SpeakerEntity extends BaseTimeEntity {
|
|
||||||
|
|
||||||
@Id
|
|
||||||
@Column(name = "speaker_id", length = 50)
|
|
||||||
private String speakerId;
|
|
||||||
|
|
||||||
@Column(name = "speaker_name", length = 100)
|
|
||||||
private String speakerName;
|
|
||||||
|
|
||||||
@Column(name = "profile_id", length = 100)
|
|
||||||
private String profileId;
|
|
||||||
|
|
||||||
@Column(name = "user_id", length = 50)
|
|
||||||
private String userId;
|
|
||||||
|
|
||||||
@Column(name = "total_segments")
|
|
||||||
private Integer totalSegments;
|
|
||||||
|
|
||||||
@Column(name = "total_duration")
|
|
||||||
private Integer totalDuration;
|
|
||||||
|
|
||||||
@Column(name = "average_confidence")
|
|
||||||
private Double averageConfidence;
|
|
||||||
|
|
||||||
@Column(name = "first_appeared")
|
|
||||||
private LocalDateTime firstAppeared;
|
|
||||||
|
|
||||||
@Column(name = "last_appeared")
|
|
||||||
private LocalDateTime lastAppeared;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 도메인 객체로 변환
|
|
||||||
*/
|
|
||||||
public Speaker toDomain() {
|
|
||||||
return Speaker.builder()
|
|
||||||
.speakerId(speakerId)
|
|
||||||
.speakerName(speakerName)
|
|
||||||
.profileId(profileId)
|
|
||||||
.userId(userId)
|
|
||||||
.totalSegments(totalSegments)
|
|
||||||
.totalDuration(totalDuration)
|
|
||||||
.averageConfidence(averageConfidence)
|
|
||||||
.firstAppeared(firstAppeared)
|
|
||||||
.lastAppeared(lastAppeared)
|
|
||||||
.createdAt(getCreatedAt())
|
|
||||||
.updatedAt(getUpdatedAt())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 도메인 객체에서 엔티티 생성
|
|
||||||
*/
|
|
||||||
public static SpeakerEntity fromDomain(Speaker speaker) {
|
|
||||||
return SpeakerEntity.builder()
|
|
||||||
.speakerId(speaker.getSpeakerId())
|
|
||||||
.speakerName(speaker.getSpeakerName())
|
|
||||||
.profileId(speaker.getProfileId())
|
|
||||||
.userId(speaker.getUserId())
|
|
||||||
.totalSegments(speaker.getTotalSegments())
|
|
||||||
.totalDuration(speaker.getTotalDuration())
|
|
||||||
.averageConfidence(speaker.getAverageConfidence())
|
|
||||||
.firstAppeared(speaker.getFirstAppeared())
|
|
||||||
.lastAppeared(speaker.getLastAppeared())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 화자 정보 업데이트
|
|
||||||
*/
|
|
||||||
public void updateSpeakerInfo(String speakerName, String userId) {
|
|
||||||
this.speakerName = speakerName;
|
|
||||||
this.userId = userId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 통계 정보 업데이트
|
|
||||||
*/
|
|
||||||
public void updateStatistics(Integer totalSegments, Integer totalDuration, Double averageConfidence) {
|
|
||||||
this.totalSegments = totalSegments;
|
|
||||||
this.totalDuration = totalDuration;
|
|
||||||
this.averageConfidence = averageConfidence;
|
|
||||||
this.lastAppeared = LocalDateTime.now();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,51 +0,0 @@
|
|||||||
package com.unicorn.hgzero.stt.repository.jpa;
|
|
||||||
|
|
||||||
import com.unicorn.hgzero.stt.repository.entity.SpeakerEntity;
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
import org.springframework.data.jpa.repository.Query;
|
|
||||||
import org.springframework.data.repository.query.Param;
|
|
||||||
import org.springframework.stereotype.Repository;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 화자 JPA Repository
|
|
||||||
* 화자 정보에 대한 데이터베이스 접근을 담당
|
|
||||||
*/
|
|
||||||
@Repository
|
|
||||||
public interface SpeakerRepository extends JpaRepository<SpeakerEntity, String> {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 사용자 ID로 화자 조회
|
|
||||||
*/
|
|
||||||
Optional<SpeakerEntity> findByUserId(String userId);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Azure Profile ID로 화자 조회
|
|
||||||
*/
|
|
||||||
Optional<SpeakerEntity> findByProfileId(String profileId);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 화자명으로 검색
|
|
||||||
*/
|
|
||||||
List<SpeakerEntity> findBySpeakerNameContaining(String speakerName);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 발언 비중이 높은 화자 조회
|
|
||||||
*/
|
|
||||||
@Query("SELECT s FROM SpeakerEntity s WHERE s.totalDuration > :minDuration ORDER BY s.totalDuration DESC")
|
|
||||||
List<SpeakerEntity> findActiveSpeakers(@Param("minDuration") Integer minDuration);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 신뢰도가 낮은 화자 조회
|
|
||||||
*/
|
|
||||||
@Query("SELECT s FROM SpeakerEntity s WHERE s.averageConfidence < :threshold ORDER BY s.averageConfidence ASC")
|
|
||||||
List<SpeakerEntity> findLowConfidenceSpeakers(@Param("threshold") Double threshold);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 사용자 ID 미지정 화자 조회
|
|
||||||
*/
|
|
||||||
@Query("SELECT s FROM SpeakerEntity s WHERE s.userId IS NULL ORDER BY s.totalDuration DESC")
|
|
||||||
List<SpeakerEntity> findUnassignedSpeakers();
|
|
||||||
}
|
|
||||||
@ -50,7 +50,6 @@ public class RecordingServiceImpl implements RecordingService {
|
|||||||
.language(request.getLanguage() != null ? request.getLanguage() : "ko-KR")
|
.language(request.getLanguage() != null ? request.getLanguage() : "ko-KR")
|
||||||
.speakerCount(0)
|
.speakerCount(0)
|
||||||
.segmentCount(0)
|
.segmentCount(0)
|
||||||
.storagePath(generateStoragePath(request.getMeetingId(), request.getSessionId()))
|
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
recordingRepository.save(recording);
|
recordingRepository.save(recording);
|
||||||
@ -65,7 +64,6 @@ public class RecordingServiceImpl implements RecordingService {
|
|||||||
.sessionId(request.getSessionId())
|
.sessionId(request.getSessionId())
|
||||||
.status("READY")
|
.status("READY")
|
||||||
.streamUrl(streamUrl)
|
.streamUrl(streamUrl)
|
||||||
.storagePath(recording.getStoragePath())
|
|
||||||
.estimatedInitTime(1100)
|
.estimatedInitTime(1100)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
@ -119,14 +117,13 @@ public class RecordingServiceImpl implements RecordingService {
|
|||||||
|
|
||||||
// 녹음 시간 계산 (임시로 30분으로 설정)
|
// 녹음 시간 계산 (임시로 30분으로 설정)
|
||||||
Integer duration = 1800; // 실제로는 startTime과 endTime 차이 계산
|
Integer duration = 1800; // 실제로는 startTime과 endTime 차이 계산
|
||||||
Long fileSize = 172800000L; // 실제로는 Azure Blob에서 파일 크기 조회
|
|
||||||
|
|
||||||
RecordingEntity savedRecording = recordingRepository.save(recording);
|
RecordingEntity savedRecording = recordingRepository.save(recording);
|
||||||
|
|
||||||
// 녹음 중지 이벤트 발행
|
// 녹음 중지 이벤트 발행 (음성 파일 저장하지 않으므로 파일 정보 제외)
|
||||||
RecordingEvent.RecordingStopped event = RecordingEvent.RecordingStopped.of(
|
RecordingEvent.RecordingStopped event = RecordingEvent.RecordingStopped.of(
|
||||||
recordingId, recording.getMeetingId(), request.getStoppedBy(),
|
recordingId, recording.getMeetingId(), request.getStoppedBy(),
|
||||||
recording.getStartTime(), duration, fileSize, recording.getStoragePath()
|
recording.getStartTime(), duration
|
||||||
);
|
);
|
||||||
eventPublisher.publishAsync("recording-events", event);
|
eventPublisher.publishAsync("recording-events", event);
|
||||||
|
|
||||||
@ -138,8 +135,6 @@ public class RecordingServiceImpl implements RecordingService {
|
|||||||
.startTime(recording.getStartTime())
|
.startTime(recording.getStartTime())
|
||||||
.endTime(endTime)
|
.endTime(endTime)
|
||||||
.duration(duration)
|
.duration(duration)
|
||||||
.fileSize(fileSize)
|
|
||||||
.storagePath(recording.getStoragePath())
|
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,7 +155,6 @@ public class RecordingServiceImpl implements RecordingService {
|
|||||||
.duration(recording.getDuration())
|
.duration(recording.getDuration())
|
||||||
.speakerCount(recording.getSpeakerCount())
|
.speakerCount(recording.getSpeakerCount())
|
||||||
.segmentCount(recording.getSegmentCount())
|
.segmentCount(recording.getSegmentCount())
|
||||||
.storagePath(recording.getStoragePath())
|
|
||||||
.language(recording.getLanguage())
|
.language(recording.getLanguage())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
@ -181,10 +175,4 @@ public class RecordingServiceImpl implements RecordingService {
|
|||||||
String.format("%03d", (int)(Math.random() * 1000));
|
String.format("%03d", (int)(Math.random() * 1000));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 저장 경로 생성
|
|
||||||
*/
|
|
||||||
private String generateStoragePath(String meetingId, String sessionId) {
|
|
||||||
return String.format("recordings/%s/%s.wav", meetingId, sessionId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -1,43 +0,0 @@
|
|||||||
package com.unicorn.hgzero.stt.service;
|
|
||||||
|
|
||||||
import com.unicorn.hgzero.stt.dto.SpeakerDto;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 화자 서비스 인터페이스
|
|
||||||
* 화자 식별 및 관리 기능을 정의
|
|
||||||
*/
|
|
||||||
public interface SpeakerService {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 화자 식별
|
|
||||||
*
|
|
||||||
* @param request 화자 식별 요청
|
|
||||||
* @return 화자 식별 응답
|
|
||||||
*/
|
|
||||||
SpeakerDto.IdentificationResponse identifySpeaker(SpeakerDto.IdentifyRequest request);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 화자 정보 조회
|
|
||||||
*
|
|
||||||
* @param speakerId 화자 ID
|
|
||||||
* @return 화자 상세 응답
|
|
||||||
*/
|
|
||||||
SpeakerDto.DetailResponse getSpeaker(String speakerId);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 화자 정보 업데이트
|
|
||||||
*
|
|
||||||
* @param speakerId 화자 ID
|
|
||||||
* @param request 화자 업데이트 요청
|
|
||||||
* @return 화자 상세 응답
|
|
||||||
*/
|
|
||||||
SpeakerDto.DetailResponse updateSpeaker(String speakerId, SpeakerDto.UpdateRequest request);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 녹음의 화자 목록 조회
|
|
||||||
*
|
|
||||||
* @param recordingId 녹음 ID
|
|
||||||
* @return 화자 목록 응답
|
|
||||||
*/
|
|
||||||
SpeakerDto.ListResponse getRecordingSpeakers(String recordingId);
|
|
||||||
}
|
|
||||||
@ -1,218 +0,0 @@
|
|||||||
package com.unicorn.hgzero.stt.service;
|
|
||||||
|
|
||||||
import com.unicorn.hgzero.common.exception.BusinessException;
|
|
||||||
import com.unicorn.hgzero.common.exception.ErrorCode;
|
|
||||||
import com.unicorn.hgzero.stt.dto.SpeakerDto;
|
|
||||||
import com.unicorn.hgzero.stt.event.SpeakerEvent;
|
|
||||||
import com.unicorn.hgzero.stt.event.publisher.EventPublisher;
|
|
||||||
import com.unicorn.hgzero.stt.repository.entity.SpeakerEntity;
|
|
||||||
import com.unicorn.hgzero.stt.repository.jpa.SpeakerRepository;
|
|
||||||
import com.unicorn.hgzero.stt.repository.jpa.TranscriptSegmentRepository;
|
|
||||||
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.List;
|
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 화자 서비스 구현체
|
|
||||||
* 화자 식별 및 관리 기능을 구현
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
@Transactional
|
|
||||||
public class SpeakerServiceImpl implements SpeakerService {
|
|
||||||
|
|
||||||
private final SpeakerRepository speakerRepository;
|
|
||||||
private final TranscriptSegmentRepository segmentRepository;
|
|
||||||
private final EventPublisher eventPublisher;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SpeakerDto.IdentificationResponse identifySpeaker(SpeakerDto.IdentifyRequest request) {
|
|
||||||
log.info("화자 식별 시작 - recordingId: {}", request.getRecordingId());
|
|
||||||
|
|
||||||
// Azure Speaker Recognition API 호출 시뮬레이션
|
|
||||||
String profileId = simulateAzureSpeakerRecognition(request.getAudioFrame());
|
|
||||||
Double confidence = simulateIdentificationConfidence();
|
|
||||||
|
|
||||||
// 기존 화자 조회
|
|
||||||
SpeakerEntity speaker = speakerRepository.findByProfileId(profileId).orElse(null);
|
|
||||||
boolean isNewSpeaker = false;
|
|
||||||
|
|
||||||
if (speaker == null) {
|
|
||||||
// 신규 화자 등록
|
|
||||||
String speakerId = generateSpeakerId();
|
|
||||||
String speakerName = "화자-" + speakerId.substring(4);
|
|
||||||
|
|
||||||
speaker = SpeakerEntity.builder()
|
|
||||||
.speakerId(speakerId)
|
|
||||||
.speakerName(speakerName)
|
|
||||||
.profileId(profileId)
|
|
||||||
.totalSegments(0)
|
|
||||||
.totalDuration(0)
|
|
||||||
.averageConfidence(confidence)
|
|
||||||
.firstAppeared(LocalDateTime.now())
|
|
||||||
.lastAppeared(LocalDateTime.now())
|
|
||||||
.build();
|
|
||||||
|
|
||||||
speakerRepository.save(speaker);
|
|
||||||
isNewSpeaker = true;
|
|
||||||
|
|
||||||
// 신규 화자 식별 이벤트 발행
|
|
||||||
SpeakerEvent.NewSpeakerIdentified event = SpeakerEvent.NewSpeakerIdentified.of(
|
|
||||||
speakerId, speakerName, profileId, request.getRecordingId(),
|
|
||||||
extractMeetingIdFromRecordingId(request.getRecordingId()), confidence
|
|
||||||
);
|
|
||||||
eventPublisher.publishAsync("speaker-events", event);
|
|
||||||
|
|
||||||
log.info("신규 화자 등록 완료 - speakerId: {}, confidence: {}", speakerId, confidence);
|
|
||||||
} else {
|
|
||||||
log.debug("기존 화자 식별 - speakerId: {}, confidence: {}", speaker.getSpeakerId(), confidence);
|
|
||||||
}
|
|
||||||
|
|
||||||
return SpeakerDto.IdentificationResponse.builder()
|
|
||||||
.speakerId(speaker.getSpeakerId())
|
|
||||||
.speakerName(speaker.getSpeakerName())
|
|
||||||
.confidence(confidence)
|
|
||||||
.isNewSpeaker(isNewSpeaker)
|
|
||||||
.profileId(profileId)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Transactional(readOnly = true)
|
|
||||||
public SpeakerDto.DetailResponse getSpeaker(String speakerId) {
|
|
||||||
log.debug("화자 정보 조회 - speakerId: {}", speakerId);
|
|
||||||
|
|
||||||
SpeakerEntity speaker = findSpeakerById(speakerId);
|
|
||||||
|
|
||||||
return SpeakerDto.DetailResponse.builder()
|
|
||||||
.speakerId(speaker.getSpeakerId())
|
|
||||||
.speakerName(speaker.getSpeakerName())
|
|
||||||
.profileId(speaker.getProfileId())
|
|
||||||
.userId(speaker.getUserId())
|
|
||||||
.totalSegments(speaker.getTotalSegments())
|
|
||||||
.totalDuration(speaker.getTotalDuration())
|
|
||||||
.averageConfidence(speaker.getAverageConfidence())
|
|
||||||
.firstAppeared(speaker.getFirstAppeared())
|
|
||||||
.lastAppeared(speaker.getLastAppeared())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SpeakerDto.DetailResponse updateSpeaker(String speakerId, SpeakerDto.UpdateRequest request) {
|
|
||||||
log.info("화자 정보 업데이트 - speakerId: {}", speakerId);
|
|
||||||
|
|
||||||
SpeakerEntity speaker = findSpeakerById(speakerId);
|
|
||||||
|
|
||||||
// 화자 정보 업데이트
|
|
||||||
String oldSpeakerName = speaker.getSpeakerName();
|
|
||||||
String oldUserId = speaker.getUserId();
|
|
||||||
speaker.updateSpeakerInfo(request.getSpeakerName(), request.getUserId());
|
|
||||||
SpeakerEntity savedSpeaker = speakerRepository.save(speaker);
|
|
||||||
|
|
||||||
// 화자 정보 업데이트 이벤트 발행
|
|
||||||
SpeakerEvent.SpeakerUpdated event = SpeakerEvent.SpeakerUpdated.of(
|
|
||||||
speakerId, oldSpeakerName, request.getSpeakerName(),
|
|
||||||
oldUserId, request.getUserId(), "SYSTEM"
|
|
||||||
);
|
|
||||||
eventPublisher.publishAsync("speaker-events", event);
|
|
||||||
|
|
||||||
log.info("화자 정보 업데이트 완료 - speakerId: {}, speakerName: {}",
|
|
||||||
speakerId, request.getSpeakerName());
|
|
||||||
|
|
||||||
return getSpeaker(speakerId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Transactional(readOnly = true)
|
|
||||||
public SpeakerDto.ListResponse getRecordingSpeakers(String recordingId) {
|
|
||||||
log.debug("녹음 화자 목록 조회 - recordingId: {}", recordingId);
|
|
||||||
|
|
||||||
// 녹음별 화자 통계 조회
|
|
||||||
List<Object[]> speakerStats = segmentRepository.getSpeakerStatisticsByRecording(recordingId);
|
|
||||||
|
|
||||||
List<SpeakerDto.SpeakerSummary> speakers = speakerStats.stream()
|
|
||||||
.map(stat -> {
|
|
||||||
String speakerId = (String) stat[0];
|
|
||||||
Long segmentCount = (Long) stat[1];
|
|
||||||
Double totalDuration = (Double) stat[2];
|
|
||||||
Double averageConfidence = (Double) stat[3];
|
|
||||||
|
|
||||||
// 화자 정보 조회
|
|
||||||
SpeakerEntity speaker = speakerRepository.findById(speakerId).orElse(null);
|
|
||||||
String speakerName = speaker != null ? speaker.getSpeakerName() : "알 수 없는 화자";
|
|
||||||
|
|
||||||
// 발언 비율 계산 (전체 시간 대비)
|
|
||||||
Double speakingRatio = calculateSpeakingRatio(recordingId, totalDuration);
|
|
||||||
|
|
||||||
return SpeakerDto.SpeakerSummary.builder()
|
|
||||||
.speakerId(speakerId)
|
|
||||||
.speakerName(speakerName)
|
|
||||||
.segmentCount(segmentCount.intValue())
|
|
||||||
.totalDuration(totalDuration.intValue())
|
|
||||||
.speakingRatio(speakingRatio)
|
|
||||||
.build();
|
|
||||||
})
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
return SpeakerDto.ListResponse.builder()
|
|
||||||
.recordingId(recordingId)
|
|
||||||
.speakerCount(speakers.size())
|
|
||||||
.speakers(speakers)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 화자 ID로 엔티티 조회
|
|
||||||
*/
|
|
||||||
private SpeakerEntity findSpeakerById(String speakerId) {
|
|
||||||
return speakerRepository.findById(speakerId)
|
|
||||||
.orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND, "화자를 찾을 수 없습니다"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 화자 ID 생성
|
|
||||||
*/
|
|
||||||
private String generateSpeakerId() {
|
|
||||||
return "SPK-" + String.format("%03d", (int)(Math.random() * 999) + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Azure Speaker Recognition 시뮬레이션
|
|
||||||
*/
|
|
||||||
private String simulateAzureSpeakerRecognition(String audioFrame) {
|
|
||||||
// 실제로는 Azure Cognitive Services Speaker Recognition API 호출
|
|
||||||
return "PROFILE-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 화자 식별 신뢰도 시뮬레이션
|
|
||||||
*/
|
|
||||||
private Double simulateIdentificationConfidence() {
|
|
||||||
// 실제로는 Azure Speaker Recognition API에서 반환
|
|
||||||
return 0.90 + (Math.random() * 0.10); // 0.90 ~ 1.0
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 발언 비율 계산
|
|
||||||
*/
|
|
||||||
private Double calculateSpeakingRatio(String recordingId, Double speakerDuration) {
|
|
||||||
// 전체 녹음 시간 조회 (임시로 1800초로 설정)
|
|
||||||
Double totalDuration = 1800.0;
|
|
||||||
return speakerDuration / totalDuration;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 녹음 ID에서 회의 ID 추출
|
|
||||||
*/
|
|
||||||
private String extractMeetingIdFromRecordingId(String recordingId) {
|
|
||||||
// 실제로는 Recording 엔티티에서 조회
|
|
||||||
return "MEETING-" + recordingId.substring(4, 12);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -2,7 +2,6 @@ package com.unicorn.hgzero.stt.service;
|
|||||||
|
|
||||||
import com.unicorn.hgzero.stt.dto.TranscriptionDto;
|
import com.unicorn.hgzero.stt.dto.TranscriptionDto;
|
||||||
import com.unicorn.hgzero.stt.dto.TranscriptSegmentDto;
|
import com.unicorn.hgzero.stt.dto.TranscriptSegmentDto;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 음성 변환 서비스 인터페이스
|
* 음성 변환 서비스 인터페이스
|
||||||
@ -18,30 +17,13 @@ public interface TranscriptionService {
|
|||||||
*/
|
*/
|
||||||
TranscriptSegmentDto.Response processAudioStream(TranscriptionDto.StreamRequest request);
|
TranscriptSegmentDto.Response processAudioStream(TranscriptionDto.StreamRequest request);
|
||||||
|
|
||||||
/**
|
|
||||||
* 배치 음성-텍스트 변환
|
|
||||||
*
|
|
||||||
* @param request 배치 변환 요청
|
|
||||||
* @param audioFile 오디오 파일
|
|
||||||
* @return 배치 변환 응답
|
|
||||||
*/
|
|
||||||
TranscriptionDto.BatchResponse transcribeAudioBatch(TranscriptionDto.BatchRequest request, MultipartFile audioFile);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 배치 변환 완료 콜백 처리
|
|
||||||
*
|
|
||||||
* @param request 배치 콜백 요청
|
|
||||||
* @return 변환 완료 응답
|
|
||||||
*/
|
|
||||||
TranscriptionDto.CompleteResponse processBatchCallback(TranscriptionDto.BatchCallbackRequest request);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 변환 텍스트 전체 조회
|
* 변환 텍스트 전체 조회
|
||||||
*
|
*
|
||||||
* @param recordingId 녹음 ID
|
* @param recordingId 녹음 ID
|
||||||
* @param includeSegments 세그먼트 포함 여부
|
|
||||||
* @param speakerId 화자 ID 필터
|
|
||||||
* @return 변환 결과 응답
|
* @return 변환 결과 응답
|
||||||
*/
|
*/
|
||||||
TranscriptionDto.Response getTranscription(String recordingId, Boolean includeSegments, String speakerId);
|
TranscriptionDto.Response getTranscription(String recordingId);
|
||||||
}
|
}
|
||||||
@ -16,9 +16,10 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.ZoneId;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@ -78,12 +79,14 @@ public class TranscriptionServiceImpl implements TranscriptionService {
|
|||||||
updateRecordingStatistics(request.getRecordingId());
|
updateRecordingStatistics(request.getRecordingId());
|
||||||
|
|
||||||
// 세그먼트 생성 이벤트 발행
|
// 세그먼트 생성 이벤트 발행
|
||||||
LocalDateTime timestampAsLocalDateTime = java.time.Instant.ofEpochMilli(request.getTimestamp())
|
LocalDateTime timestampAsDateTime = LocalDateTime.ofInstant(
|
||||||
.atZone(java.time.ZoneId.systemDefault()).toLocalDateTime();
|
Instant.ofEpochMilli(request.getTimestamp()),
|
||||||
|
ZoneId.systemDefault()
|
||||||
|
);
|
||||||
TranscriptionEvent.SegmentCreated event = TranscriptionEvent.SegmentCreated.of(
|
TranscriptionEvent.SegmentCreated event = TranscriptionEvent.SegmentCreated.of(
|
||||||
segmentId, request.getRecordingId(), recording.getMeetingId(),
|
segmentId, request.getRecordingId(), recording.getMeetingId(),
|
||||||
recognizedText, speakerId, "화자-" + speakerId.substring(4),
|
recognizedText, speakerId, "화자-" + speakerId.substring(4),
|
||||||
timestampAsLocalDateTime, 3.5, confidence, warningFlag
|
timestampAsDateTime, 3.5, confidence, warningFlag
|
||||||
);
|
);
|
||||||
eventPublisher.publishAsync("transcription-events", event);
|
eventPublisher.publishAsync("transcription-events", event);
|
||||||
|
|
||||||
@ -111,93 +114,21 @@ public class TranscriptionServiceImpl implements TranscriptionService {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public TranscriptionDto.BatchResponse transcribeAudioBatch(TranscriptionDto.BatchRequest request, MultipartFile audioFile) {
|
|
||||||
log.info("배치 음성 변환 시작 - recordingId: {}, fileSize: {}",
|
|
||||||
request.getRecordingId(), audioFile.getSize());
|
|
||||||
|
|
||||||
// 녹음 존재 확인
|
|
||||||
RecordingEntity recording = findRecordingById(request.getRecordingId());
|
|
||||||
|
|
||||||
// 배치 작업 ID 생성
|
|
||||||
String jobId = generateJobId();
|
|
||||||
|
|
||||||
// Azure Batch Transcription Job 생성 (실제로는 Azure Speech SDK 사용)
|
|
||||||
// 여기서는 시뮬레이션
|
|
||||||
|
|
||||||
log.info("배치 음성 변환 작업 생성 완료 - jobId: {}", jobId);
|
|
||||||
|
|
||||||
return TranscriptionDto.BatchResponse.builder()
|
|
||||||
.jobId(jobId)
|
|
||||||
.recordingId(request.getRecordingId())
|
|
||||||
.status("PROCESSING")
|
|
||||||
.estimatedCompletionTime(LocalDateTime.now().plusSeconds(30))
|
|
||||||
.callbackUrl(request.getCallbackUrl())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public TranscriptionDto.CompleteResponse processBatchCallback(TranscriptionDto.BatchCallbackRequest request) {
|
|
||||||
log.info("배치 변환 콜백 처리 - jobId: {}, status: {}", request.getJobId(), request.getStatus());
|
|
||||||
|
|
||||||
if ("COMPLETED".equals(request.getStatus()) && request.getSegments() != null) {
|
|
||||||
// 세그먼트 저장
|
|
||||||
for (TranscriptSegmentDto.Detail segmentDto : request.getSegments()) {
|
|
||||||
String segmentId = generateSegmentId();
|
|
||||||
|
|
||||||
TranscriptSegmentEntity segment = TranscriptSegmentEntity.builder()
|
|
||||||
.segmentId(segmentId)
|
|
||||||
.transcriptId(segmentDto.getTranscriptId())
|
|
||||||
.text(segmentDto.getText())
|
|
||||||
.speakerId(segmentDto.getSpeakerId())
|
|
||||||
.speakerName(segmentDto.getSpeakerName())
|
|
||||||
.timestamp(segmentDto.getTimestamp())
|
|
||||||
.duration(segmentDto.getDuration())
|
|
||||||
.confidence(segmentDto.getConfidence())
|
|
||||||
.warningFlag(segmentDto.getConfidence() < 0.6)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
segmentRepository.save(segment);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 전체 텍스트 통합 및 변환 결과 저장
|
|
||||||
String recordingId = extractRecordingIdFromJobId(request.getJobId());
|
|
||||||
aggregateAndSaveTranscription(recordingId, request.getSegments());
|
|
||||||
}
|
|
||||||
|
|
||||||
return TranscriptionDto.CompleteResponse.builder()
|
|
||||||
.jobId(request.getJobId())
|
|
||||||
.status(request.getStatus())
|
|
||||||
.segmentCount(request.getSegments() != null ? request.getSegments().size() : 0)
|
|
||||||
.totalDuration(calculateTotalDuration(request.getSegments()))
|
|
||||||
.averageConfidence(calculateAverageConfidence(request.getSegments()))
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public TranscriptionDto.Response getTranscription(String recordingId, Boolean includeSegments, String speakerId) {
|
public TranscriptionDto.Response getTranscription(String recordingId) {
|
||||||
log.debug("변환 텍스트 조회 - recordingId: {}, includeSegments: {}", recordingId, includeSegments);
|
log.debug("변환 텍스트 조회 - recordingId: {}", recordingId);
|
||||||
|
|
||||||
// 변환 결과 조회
|
// 변환 결과 조회
|
||||||
TranscriptionEntity transcription = transcriptionRepository.findByRecordingId(recordingId)
|
TranscriptionEntity transcription = transcriptionRepository.findByRecordingId(recordingId)
|
||||||
.orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND, "변환 결과를 찾을 수 없습니다"));
|
.orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND, "변환 결과를 찾을 수 없습니다"));
|
||||||
|
|
||||||
List<TranscriptSegmentDto.Detail> segments = null;
|
// 세그먼트 정보 포함
|
||||||
|
List<TranscriptSegmentEntity> segmentEntities = segmentRepository.findByRecordingIdOrderByTimestamp(recordingId);
|
||||||
if (Boolean.TRUE.equals(includeSegments)) {
|
List<TranscriptSegmentDto.Detail> segments = segmentEntities.stream()
|
||||||
List<TranscriptSegmentEntity> segmentEntities;
|
|
||||||
|
|
||||||
if (speakerId != null) {
|
|
||||||
segmentEntities = segmentRepository.findByRecordingIdAndSpeakerIdOrderByTimestamp(recordingId, speakerId);
|
|
||||||
} else {
|
|
||||||
segmentEntities = segmentRepository.findByRecordingIdOrderByTimestamp(recordingId);
|
|
||||||
}
|
|
||||||
|
|
||||||
segments = segmentEntities.stream()
|
|
||||||
.map(this::convertToSegmentDetail)
|
.map(this::convertToSegmentDetail)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
|
||||||
|
|
||||||
return TranscriptionDto.Response.builder()
|
return TranscriptionDto.Response.builder()
|
||||||
.recordingId(recordingId)
|
.recordingId(recordingId)
|
||||||
|
|||||||
@ -4,8 +4,8 @@ spring:
|
|||||||
|
|
||||||
# Database Configuration
|
# Database Configuration
|
||||||
datasource:
|
datasource:
|
||||||
url: jdbc:${DB_KIND:postgresql}://${DB_HOST:4.230.65.89}:${DB_PORT:5432}/${DB_NAME:sttdb}
|
url: jdbc:${DB_KIND:postgresql}://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:sttdb}
|
||||||
username: ${DB_USERNAME:hgzerouser}
|
username: ${DB_USERNAME:stt_user}
|
||||||
password: ${DB_PASSWORD:}
|
password: ${DB_PASSWORD:}
|
||||||
driver-class-name: org.postgresql.Driver
|
driver-class-name: org.postgresql.Driver
|
||||||
hikari:
|
hikari:
|
||||||
@ -21,6 +21,7 @@ spring:
|
|||||||
show-sql: ${SHOW_SQL:true}
|
show-sql: ${SHOW_SQL:true}
|
||||||
properties:
|
properties:
|
||||||
hibernate:
|
hibernate:
|
||||||
|
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||||
format_sql: true
|
format_sql: true
|
||||||
use_sql_comments: true
|
use_sql_comments: true
|
||||||
hibernate:
|
hibernate:
|
||||||
@ -29,7 +30,7 @@ spring:
|
|||||||
# Redis Configuration
|
# Redis Configuration
|
||||||
data:
|
data:
|
||||||
redis:
|
redis:
|
||||||
host: ${REDIS_HOST:20.249.177.114}
|
host: ${REDIS_HOST:localhost}
|
||||||
port: ${REDIS_PORT:6379}
|
port: ${REDIS_PORT:6379}
|
||||||
password: ${REDIS_PASSWORD:}
|
password: ${REDIS_PASSWORD:}
|
||||||
timeout: 2000ms
|
timeout: 2000ms
|
||||||
@ -39,7 +40,7 @@ spring:
|
|||||||
max-idle: 8
|
max-idle: 8
|
||||||
min-idle: 0
|
min-idle: 0
|
||||||
max-wait: -1ms
|
max-wait: -1ms
|
||||||
database: ${REDIS_DATABASE:2}
|
database: ${REDIS_DATABASE:3}
|
||||||
|
|
||||||
# Server Configuration
|
# Server Configuration
|
||||||
server:
|
server:
|
||||||
@ -47,9 +48,9 @@ server:
|
|||||||
|
|
||||||
# JWT Configuration
|
# JWT Configuration
|
||||||
jwt:
|
jwt:
|
||||||
secret: ${JWT_SECRET:}
|
secret: ${JWT_SECRET:HGZero_Secret_Key_For_Development_Only}
|
||||||
access-token-validity: ${JWT_ACCESS_TOKEN_VALIDITY:3600}
|
access-token-validity: ${JWT_ACCESS_TOKEN_VALIDITY:1800}
|
||||||
refresh-token-validity: ${JWT_REFRESH_TOKEN_VALIDITY:604800}
|
refresh-token-validity: ${JWT_REFRESH_TOKEN_VALIDITY:86400}
|
||||||
|
|
||||||
# CORS Configuration
|
# CORS Configuration
|
||||||
cors:
|
cors:
|
||||||
|
|||||||
@ -1,16 +1,19 @@
|
|||||||
package com.unicorn.hgzero.stt;
|
package com.unicorn.hgzero.stt;
|
||||||
|
|
||||||
|
import com.unicorn.hgzero.stt.config.TestConfig;
|
||||||
import org.junit.jupiter.api.DisplayName;
|
import org.junit.jupiter.api.DisplayName;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.context.annotation.Import;
|
||||||
import org.springframework.test.context.ActiveProfiles;
|
import org.springframework.test.context.ActiveProfiles;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* STT 애플리케이션 통합 테스트
|
* STT 애플리케이션 통합 테스트
|
||||||
* 전체 애플리케이션 컨텍스트 로딩 및 기본 설정 검증
|
* 전체 애플리케이션 컨텍스트 로딩 및 기본 설정 검증
|
||||||
*/
|
*/
|
||||||
@SpringBootTest
|
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
|
||||||
@ActiveProfiles("test")
|
@ActiveProfiles("test")
|
||||||
|
@Import(TestConfig.class)
|
||||||
@DisplayName("STT 애플리케이션 테스트")
|
@DisplayName("STT 애플리케이션 테스트")
|
||||||
class SttApplicationTest {
|
class SttApplicationTest {
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,82 @@
|
|||||||
|
package com.unicorn.hgzero.stt.config;
|
||||||
|
|
||||||
|
import com.unicorn.hgzero.stt.event.publisher.EventPublisher;
|
||||||
|
import com.unicorn.hgzero.stt.repository.jpa.RecordingRepository;
|
||||||
|
import com.unicorn.hgzero.stt.repository.jpa.SpeakerRepository;
|
||||||
|
import com.unicorn.hgzero.stt.repository.jpa.TranscriptionRepository;
|
||||||
|
import com.unicorn.hgzero.stt.repository.jpa.TranscriptSegmentRepository;
|
||||||
|
import com.unicorn.hgzero.stt.service.RecordingService;
|
||||||
|
import com.unicorn.hgzero.stt.service.SpeakerService;
|
||||||
|
import com.unicorn.hgzero.stt.service.TranscriptionService;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
|
import org.springframework.boot.autoconfigure.domain.EntityScan;
|
||||||
|
import org.springframework.boot.test.context.TestConfiguration;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.ComponentScan;
|
||||||
|
import org.springframework.context.annotation.Primary;
|
||||||
|
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||||
|
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||||
|
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import redis.embedded.RedisServer;
|
||||||
|
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import jakarta.annotation.PreDestroy;
|
||||||
|
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 테스트용 설정 클래스
|
||||||
|
* Embedded Redis 서버와 Mock Bean들을 제공
|
||||||
|
*/
|
||||||
|
@TestConfiguration
|
||||||
|
@EnableJpaRepositories(basePackages = "com.unicorn.hgzero.stt.repository.jpa")
|
||||||
|
@EntityScan(basePackages = "com.unicorn.hgzero.stt.repository.entity")
|
||||||
|
@ComponentScan(basePackages = {"com.unicorn.hgzero.stt.service", "com.unicorn.hgzero.stt.config"})
|
||||||
|
public class TestConfig {
|
||||||
|
|
||||||
|
private RedisServer redisServer;
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void startRedis() {
|
||||||
|
try {
|
||||||
|
redisServer = new RedisServer(6370);
|
||||||
|
redisServer.start();
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Redis 서버 시작 실패 시 로그만 남기고 계속 진행
|
||||||
|
System.err.println("Failed to start embedded Redis server: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreDestroy
|
||||||
|
public void stopRedis() {
|
||||||
|
if (redisServer != null && redisServer.isActive()) {
|
||||||
|
redisServer.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@Primary
|
||||||
|
public EventPublisher eventPublisher() {
|
||||||
|
return mock(EventPublisher.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@Primary
|
||||||
|
@ConditionalOnMissingBean
|
||||||
|
public RedisConnectionFactory redisConnectionFactory() {
|
||||||
|
// Embedded Redis에 연결하는 실제 ConnectionFactory
|
||||||
|
return new LettuceConnectionFactory("localhost", 6370);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@Primary
|
||||||
|
@ConditionalOnMissingBean
|
||||||
|
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
|
||||||
|
RedisTemplate<String, Object> template = new RedisTemplate<>();
|
||||||
|
template.setConnectionFactory(connectionFactory);
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
package com.unicorn.hgzero.stt.config;
|
||||||
|
|
||||||
|
import org.springframework.boot.test.context.TestConfiguration;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.ComponentScan;
|
||||||
|
import org.springframework.context.annotation.Primary;
|
||||||
|
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||||
|
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import redis.embedded.RedisServer;
|
||||||
|
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import jakarta.annotation.PreDestroy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebMvcTest용 최소한의 설정 클래스
|
||||||
|
* Repository는 MockBean으로 별도 처리
|
||||||
|
*/
|
||||||
|
@TestConfiguration
|
||||||
|
@ComponentScan(basePackages = "com.unicorn.hgzero.stt.config")
|
||||||
|
public class WebMvcTestConfig {
|
||||||
|
|
||||||
|
private RedisServer redisServer;
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void startRedis() {
|
||||||
|
try {
|
||||||
|
redisServer = new RedisServer(6371); // 다른 포트 사용
|
||||||
|
redisServer.start();
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("Failed to start embedded Redis server: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreDestroy
|
||||||
|
public void stopRedis() {
|
||||||
|
if (redisServer != null && redisServer.isActive()) {
|
||||||
|
redisServer.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@Primary
|
||||||
|
public RedisConnectionFactory redisConnectionFactory() {
|
||||||
|
return new LettuceConnectionFactory("localhost", 6371);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@Primary
|
||||||
|
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
|
||||||
|
RedisTemplate<String, Object> template = new RedisTemplate<>();
|
||||||
|
template.setConnectionFactory(connectionFactory);
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
package com.unicorn.hgzero.stt.controller;
|
package com.unicorn.hgzero.stt.controller;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.unicorn.hgzero.stt.config.WebMvcTestConfig;
|
||||||
import com.unicorn.hgzero.stt.dto.RecordingDto;
|
import com.unicorn.hgzero.stt.dto.RecordingDto;
|
||||||
import com.unicorn.hgzero.stt.service.RecordingService;
|
import com.unicorn.hgzero.stt.service.RecordingService;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
@ -9,6 +10,7 @@ import org.junit.jupiter.api.Test;
|
|||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
||||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||||
|
import org.springframework.context.annotation.Import;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.test.web.servlet.MockMvc;
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,109 @@
|
|||||||
|
package com.unicorn.hgzero.stt.controller;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.unicorn.hgzero.stt.dto.RecordingDto;
|
||||||
|
import com.unicorn.hgzero.stt.service.RecordingService;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 간단한 녹음 컨트롤러 테스트
|
||||||
|
*/
|
||||||
|
@WebMvcTest(RecordingController.class)
|
||||||
|
@DisplayName("간단한 녹음 컨트롤러 테스트")
|
||||||
|
class SimpleRecordingControllerTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MockMvc mockMvc;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
private RecordingService recordingService;
|
||||||
|
|
||||||
|
private RecordingDto.PrepareRequest prepareRequest;
|
||||||
|
private RecordingDto.PrepareResponse prepareResponse;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
prepareRequest = RecordingDto.PrepareRequest.builder()
|
||||||
|
.meetingId("MEETING-001")
|
||||||
|
.sessionId("SESSION-001")
|
||||||
|
.language("ko-KR")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
prepareResponse = RecordingDto.PrepareResponse.builder()
|
||||||
|
.recordingId("REC-20250123-001")
|
||||||
|
.sessionId("SESSION-001")
|
||||||
|
.status("READY")
|
||||||
|
.streamUrl("wss://api.example.com/stt/v1/ws/stt/SESSION-001")
|
||||||
|
.storagePath("recordings/MEETING-001/SESSION-001.wav")
|
||||||
|
.estimatedInitTime(1100)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("녹음 준비 API 성공")
|
||||||
|
void prepareRecording_Success() throws Exception {
|
||||||
|
// Given
|
||||||
|
when(recordingService.prepareRecording(any(RecordingDto.PrepareRequest.class)))
|
||||||
|
.thenReturn(prepareResponse);
|
||||||
|
|
||||||
|
// When & Then
|
||||||
|
mockMvc.perform(post("/api/v1/stt/recordings/prepare")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(objectMapper.writeValueAsString(prepareRequest)))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.success").value(true))
|
||||||
|
.andExpect(jsonPath("$.data.recordingId").value("REC-20250123-001"))
|
||||||
|
.andExpect(jsonPath("$.data.sessionId").value("SESSION-001"))
|
||||||
|
.andExpect(jsonPath("$.data.status").value("READY"));
|
||||||
|
|
||||||
|
verify(recordingService).prepareRecording(any(RecordingDto.PrepareRequest.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("녹음 시작 API 성공")
|
||||||
|
void startRecording_Success() throws Exception {
|
||||||
|
// Given
|
||||||
|
String recordingId = "REC-20250123-001";
|
||||||
|
RecordingDto.StartRequest startRequest = RecordingDto.StartRequest.builder()
|
||||||
|
.startedBy("user001")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
RecordingDto.StatusResponse statusResponse = RecordingDto.StatusResponse.builder()
|
||||||
|
.recordingId(recordingId)
|
||||||
|
.status("RECORDING")
|
||||||
|
.startTime(LocalDateTime.now())
|
||||||
|
.duration(0)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
when(recordingService.startRecording(eq(recordingId), any(RecordingDto.StartRequest.class)))
|
||||||
|
.thenReturn(statusResponse);
|
||||||
|
|
||||||
|
// When & Then
|
||||||
|
mockMvc.perform(post("/api/v1/stt/recordings/{recordingId}/start", recordingId)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(objectMapper.writeValueAsString(startRequest)))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.success").value(true))
|
||||||
|
.andExpect(jsonPath("$.data.recordingId").value(recordingId))
|
||||||
|
.andExpect(jsonPath("$.data.status").value("RECORDING"));
|
||||||
|
|
||||||
|
verify(recordingService).startRecording(eq(recordingId), any(RecordingDto.StartRequest.class));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,14 +1,21 @@
|
|||||||
package com.unicorn.hgzero.stt.integration;
|
package com.unicorn.hgzero.stt.integration;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.unicorn.hgzero.stt.config.TestConfig;
|
||||||
import com.unicorn.hgzero.stt.dto.RecordingDto;
|
import com.unicorn.hgzero.stt.dto.RecordingDto;
|
||||||
import com.unicorn.hgzero.stt.dto.TranscriptionDto;
|
import com.unicorn.hgzero.stt.dto.TranscriptionDto;
|
||||||
import com.unicorn.hgzero.stt.dto.SpeakerDto;
|
import com.unicorn.hgzero.stt.dto.SpeakerDto;
|
||||||
|
import com.unicorn.hgzero.stt.service.RecordingService;
|
||||||
|
import com.unicorn.hgzero.stt.service.SpeakerService;
|
||||||
|
import com.unicorn.hgzero.stt.service.TranscriptionService;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.DisplayName;
|
import org.junit.jupiter.api.DisplayName;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureWebMvc;
|
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureWebMvc;
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||||
|
import org.springframework.context.annotation.Import;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.test.context.ActiveProfiles;
|
import org.springframework.test.context.ActiveProfiles;
|
||||||
import org.springframework.test.web.servlet.MockMvc;
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
@ -16,6 +23,8 @@ import org.springframework.test.web.servlet.MvcResult;
|
|||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||||
|
|
||||||
@ -23,10 +32,9 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
|||||||
* STT API 통합 테스트
|
* STT API 통합 테스트
|
||||||
* 전체 워크플로우 시나리오 테스트
|
* 전체 워크플로우 시나리오 테스트
|
||||||
*/
|
*/
|
||||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
|
||||||
@AutoConfigureWebMvc
|
@AutoConfigureWebMvc
|
||||||
@ActiveProfiles("test")
|
@ActiveProfiles("test")
|
||||||
@Transactional
|
|
||||||
@DisplayName("STT API 통합 테스트")
|
@DisplayName("STT API 통합 테스트")
|
||||||
class SttApiIntegrationTest {
|
class SttApiIntegrationTest {
|
||||||
|
|
||||||
@ -36,6 +44,99 @@ class SttApiIntegrationTest {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private ObjectMapper objectMapper;
|
private ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
private RecordingService recordingService;
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
private SpeakerService speakerService;
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
private TranscriptionService transcriptionService;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
// RecordingService Mock 설정
|
||||||
|
when(recordingService.prepareRecording(any(RecordingDto.PrepareRequest.class)))
|
||||||
|
.thenReturn(RecordingDto.PrepareResponse.builder()
|
||||||
|
.recordingId("REC-20250123-001")
|
||||||
|
.sessionId("SESSION-INTEGRATION-001")
|
||||||
|
.status("READY")
|
||||||
|
.streamUrl("wss://api.example.com/stt/v1/ws/stt/SESSION-INTEGRATION-001")
|
||||||
|
.storagePath("recordings/MEETING-INTEGRATION-001/SESSION-INTEGRATION-001.wav")
|
||||||
|
.estimatedInitTime(1100)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
when(recordingService.startRecording(anyString(), any(RecordingDto.StartRequest.class)))
|
||||||
|
.thenReturn(RecordingDto.StatusResponse.builder()
|
||||||
|
.recordingId("REC-20250123-001")
|
||||||
|
.status("RECORDING")
|
||||||
|
.startTime(java.time.LocalDateTime.now())
|
||||||
|
.duration(0)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
when(recordingService.stopRecording(anyString(), any(RecordingDto.StopRequest.class)))
|
||||||
|
.thenReturn(RecordingDto.StatusResponse.builder()
|
||||||
|
.recordingId("REC-20250123-001")
|
||||||
|
.status("STOPPED")
|
||||||
|
.startTime(java.time.LocalDateTime.now().minusMinutes(30))
|
||||||
|
.endTime(java.time.LocalDateTime.now())
|
||||||
|
.duration(1800)
|
||||||
|
.fileSize(172800000L)
|
||||||
|
.storagePath("recordings/MEETING-INTEGRATION-001/SESSION-INTEGRATION-001.wav")
|
||||||
|
.build());
|
||||||
|
|
||||||
|
when(recordingService.getRecording(anyString()))
|
||||||
|
.thenReturn(RecordingDto.DetailResponse.builder()
|
||||||
|
.recordingId("REC-20250123-001")
|
||||||
|
.meetingId("MEETING-INTEGRATION-001")
|
||||||
|
.sessionId("SESSION-INTEGRATION-001")
|
||||||
|
.status("STOPPED")
|
||||||
|
.startTime(java.time.LocalDateTime.now().minusMinutes(30))
|
||||||
|
.endTime(java.time.LocalDateTime.now())
|
||||||
|
.duration(1800)
|
||||||
|
.speakerCount(3)
|
||||||
|
.segmentCount(45)
|
||||||
|
.storagePath("recordings/MEETING-INTEGRATION-001/SESSION-INTEGRATION-001.wav")
|
||||||
|
.language("ko-KR")
|
||||||
|
.build());
|
||||||
|
|
||||||
|
// TranscriptionService Mock 설정
|
||||||
|
when(transcriptionService.processAudioStream(any(TranscriptionDto.StreamRequest.class)))
|
||||||
|
.thenReturn(com.unicorn.hgzero.stt.dto.TranscriptSegmentDto.Response.builder()
|
||||||
|
.recordingId("REC-20250123-001")
|
||||||
|
.transcriptId("TRANS-001")
|
||||||
|
.text("안녕하세요")
|
||||||
|
.confidence(0.95)
|
||||||
|
.timestamp(System.currentTimeMillis())
|
||||||
|
.speakerId("SPK-001")
|
||||||
|
.duration(2.5)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
when(transcriptionService.getTranscription(anyString(), any(), any()))
|
||||||
|
.thenReturn(TranscriptionDto.Response.builder()
|
||||||
|
.recordingId("REC-20250123-001")
|
||||||
|
.fullText("안녕하세요. 오늘 회의를 시작하겠습니다.")
|
||||||
|
.segmentCount(45)
|
||||||
|
.speakerCount(3)
|
||||||
|
.totalDuration(1800)
|
||||||
|
.averageConfidence(0.92)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
// SpeakerService Mock 설정
|
||||||
|
when(speakerService.identifySpeaker(any(SpeakerDto.IdentifyRequest.class)))
|
||||||
|
.thenReturn(SpeakerDto.IdentificationResponse.builder()
|
||||||
|
.speakerId("SPK-001")
|
||||||
|
.confidence(0.95)
|
||||||
|
.isNewSpeaker(false)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
when(speakerService.getRecordingSpeakers(anyString()))
|
||||||
|
.thenReturn(SpeakerDto.ListResponse.builder()
|
||||||
|
.recordingId("REC-20250123-001")
|
||||||
|
.speakerCount(3)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("전체 STT 워크플로우 통합 테스트")
|
@DisplayName("전체 STT 워크플로우 통합 테스트")
|
||||||
void fullSttWorkflowIntegrationTest() throws Exception {
|
void fullSttWorkflowIntegrationTest() throws Exception {
|
||||||
@ -141,6 +242,17 @@ class SttApiIntegrationTest {
|
|||||||
@Test
|
@Test
|
||||||
@DisplayName("에러 케이스 통합 테스트")
|
@DisplayName("에러 케이스 통합 테스트")
|
||||||
void errorCasesIntegrationTest() throws Exception {
|
void errorCasesIntegrationTest() throws Exception {
|
||||||
|
// 존재하지 않는 녹음에 대한 Mock 설정
|
||||||
|
when(recordingService.startRecording(eq("NONEXISTENT-001"), any(RecordingDto.StartRequest.class)))
|
||||||
|
.thenThrow(new com.unicorn.hgzero.common.exception.BusinessException(
|
||||||
|
com.unicorn.hgzero.common.exception.ErrorCode.ENTITY_NOT_FOUND,
|
||||||
|
"녹음을 찾을 수 없습니다"));
|
||||||
|
|
||||||
|
when(transcriptionService.getTranscription(eq("NONEXISTENT-001"), any(), any()))
|
||||||
|
.thenThrow(new com.unicorn.hgzero.common.exception.BusinessException(
|
||||||
|
com.unicorn.hgzero.common.exception.ErrorCode.ENTITY_NOT_FOUND,
|
||||||
|
"변환 결과를 찾을 수 없습니다"));
|
||||||
|
|
||||||
// 존재하지 않는 녹음 시작 시도
|
// 존재하지 않는 녹음 시작 시도
|
||||||
RecordingDto.StartRequest startRequest = RecordingDto.StartRequest.builder()
|
RecordingDto.StartRequest startRequest = RecordingDto.StartRequest.builder()
|
||||||
.startedBy("test-user")
|
.startedBy("test-user")
|
||||||
|
|||||||
@ -50,6 +50,8 @@ class TranscriptionServiceTest {
|
|||||||
private TranscriptionServiceImpl transcriptionService;
|
private TranscriptionServiceImpl transcriptionService;
|
||||||
|
|
||||||
private RecordingEntity recordingEntity;
|
private RecordingEntity recordingEntity;
|
||||||
|
private TranscriptSegmentEntity segmentEntity;
|
||||||
|
private TranscriptionEntity transcriptionEntity;
|
||||||
private TranscriptionDto.StreamRequest streamRequest;
|
private TranscriptionDto.StreamRequest streamRequest;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
@ -60,6 +62,22 @@ class TranscriptionServiceTest {
|
|||||||
.sessionId("SESSION-001")
|
.sessionId("SESSION-001")
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
segmentEntity = TranscriptSegmentEntity.builder()
|
||||||
|
.segmentId("SEG-001")
|
||||||
|
.recordingId("REC-20250123-001")
|
||||||
|
.text("테스트 음성 변환 결과")
|
||||||
|
.timestamp(System.currentTimeMillis())
|
||||||
|
.confidence(0.95)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
transcriptionEntity = TranscriptionEntity.builder()
|
||||||
|
.transcriptId("TRANS-001")
|
||||||
|
.recordingId("REC-20250123-001")
|
||||||
|
.fullText("전체 음성 변환 결과")
|
||||||
|
.segmentCount(1)
|
||||||
|
.averageConfidence(0.95)
|
||||||
|
.build();
|
||||||
|
|
||||||
streamRequest = TranscriptionDto.StreamRequest.builder()
|
streamRequest = TranscriptionDto.StreamRequest.builder()
|
||||||
.recordingId("REC-20250123-001")
|
.recordingId("REC-20250123-001")
|
||||||
.audioData("base64-encoded-audio-data")
|
.audioData("base64-encoded-audio-data")
|
||||||
@ -73,7 +91,7 @@ class TranscriptionServiceTest {
|
|||||||
void processAudioStream_Success() {
|
void processAudioStream_Success() {
|
||||||
// Given
|
// Given
|
||||||
when(recordingRepository.findById(anyString())).thenReturn(Optional.of(recordingEntity));
|
when(recordingRepository.findById(anyString())).thenReturn(Optional.of(recordingEntity));
|
||||||
when(segmentRepository.save(any(TranscriptSegmentEntity.class))).thenReturn(any());
|
when(segmentRepository.save(any(TranscriptSegmentEntity.class))).thenReturn(segmentEntity);
|
||||||
when(segmentRepository.getSpeakerStatisticsByRecording(anyString())).thenReturn(List.of());
|
when(segmentRepository.getSpeakerStatisticsByRecording(anyString())).thenReturn(List.of());
|
||||||
when(segmentRepository.countByRecordingId(anyString())).thenReturn(1L);
|
when(segmentRepository.countByRecordingId(anyString())).thenReturn(1L);
|
||||||
when(recordingRepository.save(any(RecordingEntity.class))).thenReturn(recordingEntity);
|
when(recordingRepository.save(any(RecordingEntity.class))).thenReturn(recordingEntity);
|
||||||
@ -88,9 +106,9 @@ class TranscriptionServiceTest {
|
|||||||
assertThat(response.getConfidence()).isGreaterThan(0.8);
|
assertThat(response.getConfidence()).isGreaterThan(0.8);
|
||||||
assertThat(response.getSpeakerId()).isNotEmpty();
|
assertThat(response.getSpeakerId()).isNotEmpty();
|
||||||
|
|
||||||
verify(recordingRepository).findById("REC-20250123-001");
|
verify(recordingRepository, atLeastOnce()).findById("REC-20250123-001");
|
||||||
verify(segmentRepository).save(any(TranscriptSegmentEntity.class));
|
verify(segmentRepository).save(any(TranscriptSegmentEntity.class));
|
||||||
verify(eventPublisher).publishAsync(eq("transcription-events"), any());
|
verify(eventPublisher, atLeastOnce()).publishAsync(eq("transcription-events"), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -114,7 +132,7 @@ class TranscriptionServiceTest {
|
|||||||
void processAudioStream_LowConfidenceWarning() {
|
void processAudioStream_LowConfidenceWarning() {
|
||||||
// Given
|
// Given
|
||||||
when(recordingRepository.findById(anyString())).thenReturn(Optional.of(recordingEntity));
|
when(recordingRepository.findById(anyString())).thenReturn(Optional.of(recordingEntity));
|
||||||
when(segmentRepository.save(any(TranscriptSegmentEntity.class))).thenReturn(any());
|
when(segmentRepository.save(any(TranscriptSegmentEntity.class))).thenReturn(segmentEntity);
|
||||||
when(segmentRepository.getSpeakerStatisticsByRecording(anyString())).thenReturn(List.of());
|
when(segmentRepository.getSpeakerStatisticsByRecording(anyString())).thenReturn(List.of());
|
||||||
when(segmentRepository.countByRecordingId(anyString())).thenReturn(1L);
|
when(segmentRepository.countByRecordingId(anyString())).thenReturn(1L);
|
||||||
when(recordingRepository.save(any(RecordingEntity.class))).thenReturn(recordingEntity);
|
when(recordingRepository.save(any(RecordingEntity.class))).thenReturn(recordingEntity);
|
||||||
@ -191,8 +209,8 @@ class TranscriptionServiceTest {
|
|||||||
.segments(segments)
|
.segments(segments)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
when(segmentRepository.save(any(TranscriptSegmentEntity.class))).thenReturn(any());
|
when(segmentRepository.save(any(TranscriptSegmentEntity.class))).thenReturn(segmentEntity);
|
||||||
when(transcriptionRepository.save(any(TranscriptionEntity.class))).thenReturn(any());
|
when(transcriptionRepository.save(any(TranscriptionEntity.class))).thenReturn(transcriptionEntity);
|
||||||
|
|
||||||
// When
|
// When
|
||||||
TranscriptionDto.CompleteResponse response = transcriptionService.processBatchCallback(callbackRequest);
|
TranscriptionDto.CompleteResponse response = transcriptionService.processBatchCallback(callbackRequest);
|
||||||
|
|||||||
@ -1,55 +1,131 @@
|
|||||||
# STT 서비스 테스트 설정
|
# 테스트 환경별 설정 선택
|
||||||
spring:
|
# 1. 단위 테스트용 (기본)
|
||||||
profiles:
|
# 2. Docker 통합 테스트용 (integration-test profile 활성화 시)
|
||||||
active: test
|
|
||||||
|
|
||||||
# 데이터베이스 설정 (H2 인메모리)
|
spring:
|
||||||
|
application:
|
||||||
|
name: stt-test
|
||||||
|
|
||||||
|
# Bean Override 허용
|
||||||
|
main:
|
||||||
|
allow-bean-definition-overriding: true
|
||||||
|
|
||||||
|
# In-Memory Database (기본값)
|
||||||
datasource:
|
datasource:
|
||||||
url: jdbc:h2:mem:testdb
|
url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
|
||||||
driver-class-name: org.h2.Driver
|
|
||||||
username: sa
|
username: sa
|
||||||
password:
|
password:
|
||||||
|
driver-class-name: org.h2.Driver
|
||||||
|
|
||||||
# JPA 설정
|
|
||||||
jpa:
|
jpa:
|
||||||
|
show-sql: false
|
||||||
hibernate:
|
hibernate:
|
||||||
ddl-auto: create-drop
|
ddl-auto: create-drop
|
||||||
show-sql: true
|
|
||||||
properties:
|
properties:
|
||||||
hibernate:
|
hibernate:
|
||||||
dialect: org.hibernate.dialect.H2Dialect
|
dialect: org.hibernate.dialect.H2Dialect
|
||||||
format_sql: true
|
|
||||||
|
|
||||||
# Redis 설정 (임베디드)
|
# Mock Redis (handled by TestConfig)
|
||||||
|
data:
|
||||||
redis:
|
redis:
|
||||||
host: localhost
|
host: localhost
|
||||||
port: 6370
|
port: 6370
|
||||||
timeout: 2000ms
|
password:
|
||||||
|
database: 0
|
||||||
|
|
||||||
# JWT 설정
|
# Test Server
|
||||||
security:
|
server:
|
||||||
jwt:
|
port: 0
|
||||||
secret: test-secret-key-for-jwt-token-generation-test
|
|
||||||
expiration: 86400
|
|
||||||
|
|
||||||
# Azure 서비스 설정 (테스트용 더미)
|
# Mock Azure Services
|
||||||
azure:
|
azure:
|
||||||
speech:
|
speech:
|
||||||
subscription-key: test-key
|
subscription-key: test-key
|
||||||
region: koreacentral
|
region: eastus
|
||||||
endpoint: https://test.cognitiveservices.azure.com/
|
language: ko-KR
|
||||||
|
blob:
|
||||||
storage:
|
connection-string: DefaultEndpointsProtocol=https;AccountName=test;AccountKey=test;EndpointSuffix=core.windows.net
|
||||||
connection-string: DefaultEndpointsProtocol=https;AccountName=testaccount;AccountKey=testkey;EndpointSuffix=core.windows.net
|
|
||||||
container-name: test-recordings
|
container-name: test-recordings
|
||||||
|
eventhub:
|
||||||
event-hubs:
|
connection-string: Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test
|
||||||
connection-string: Endpoint=sb://test-eventhub.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test
|
name: test-events
|
||||||
consumer-group: test-group
|
consumer-group: test-group
|
||||||
|
|
||||||
# 로깅 설정
|
---
|
||||||
|
# Docker 통합 테스트용 설정
|
||||||
|
spring:
|
||||||
|
config:
|
||||||
|
activate:
|
||||||
|
on-profile: integration-test
|
||||||
|
|
||||||
|
# Real PostgreSQL (via Docker)
|
||||||
|
datasource:
|
||||||
|
url: jdbc:postgresql://localhost:5433/sttdb_test
|
||||||
|
username: testuser
|
||||||
|
password: testpass
|
||||||
|
driver-class-name: org.postgresql.Driver
|
||||||
|
|
||||||
|
jpa:
|
||||||
|
show-sql: true
|
||||||
|
hibernate:
|
||||||
|
ddl-auto: update
|
||||||
|
properties:
|
||||||
|
hibernate:
|
||||||
|
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||||
|
|
||||||
|
# Real Redis (via Docker)
|
||||||
|
data:
|
||||||
|
redis:
|
||||||
|
host: localhost
|
||||||
|
port: 6380
|
||||||
|
password: testpass
|
||||||
|
database: 0
|
||||||
|
|
||||||
|
# Real Server
|
||||||
|
server:
|
||||||
|
port: 8083
|
||||||
|
|
||||||
|
# Azure Emulator (Azurite)
|
||||||
|
azure:
|
||||||
|
speech:
|
||||||
|
subscription-key: test-key
|
||||||
|
region: eastus
|
||||||
|
language: ko-KR
|
||||||
|
blob:
|
||||||
|
connection-string: DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;
|
||||||
|
container-name: test-recordings
|
||||||
|
eventhub:
|
||||||
|
connection-string: Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test
|
||||||
|
name: test-events
|
||||||
|
consumer-group: test-group
|
||||||
|
|
||||||
|
---
|
||||||
|
# 공통 설정
|
||||||
|
jwt:
|
||||||
|
secret: test-secret-key-for-testing-purposes-only-not-for-production-use
|
||||||
|
access-token-validity: 3600
|
||||||
|
refresh-token-validity: 604800
|
||||||
|
|
||||||
|
cors:
|
||||||
|
allowed-origins: "*"
|
||||||
|
|
||||||
|
management:
|
||||||
|
endpoints:
|
||||||
|
enabled-by-default: false
|
||||||
|
endpoint:
|
||||||
|
health:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
springdoc:
|
||||||
|
api-docs:
|
||||||
|
enabled: false
|
||||||
|
swagger-ui:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
logging:
|
logging:
|
||||||
level:
|
level:
|
||||||
com.unicorn.hgzero.stt: DEBUG
|
com.unicorn.hgzero.stt: INFO
|
||||||
org.springframework.web: DEBUG
|
org.springframework: WARN
|
||||||
org.hibernate.SQL: DEBUG
|
org.hibernate: WARN
|
||||||
|
pattern:
|
||||||
|
console: "%d{HH:mm:ss} %-5level %logger{36} - %msg%n"
|
||||||
|
|||||||
50
test-scripts/run-integration-tests.sh
Executable file
50
test-scripts/run-integration-tests.sh
Executable file
@ -0,0 +1,50 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# STT 서비스 통합 테스트 실행 스크립트
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🚀 STT 서비스 통합 테스트 시작"
|
||||||
|
|
||||||
|
# 1. Docker 테스트 환경 시작
|
||||||
|
echo "📦 Docker 테스트 환경 시작..."
|
||||||
|
docker-compose -f docker-compose.test.yml up -d
|
||||||
|
|
||||||
|
# 2. 서비스 준비 대기
|
||||||
|
echo "⏳ 서비스 준비 대기 중..."
|
||||||
|
sleep 15
|
||||||
|
|
||||||
|
# 3. 데이터베이스 준비 확인
|
||||||
|
echo "🔍 PostgreSQL 연결 확인..."
|
||||||
|
until docker-compose -f docker-compose.test.yml exec -T postgres-test pg_isready -U testuser -d sttdb_test; do
|
||||||
|
echo "PostgreSQL 준비 중..."
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
# 4. Redis 연결 확인
|
||||||
|
echo "🔍 Redis 연결 확인..."
|
||||||
|
until docker-compose -f docker-compose.test.yml exec -T redis-test redis-cli -a testpass ping | grep PONG; do
|
||||||
|
echo "Redis 준비 중..."
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
# 5. 통합 테스트 실행
|
||||||
|
echo "🧪 통합 테스트 실행..."
|
||||||
|
export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk-23.0.2.jdk/Contents/Home
|
||||||
|
./gradlew :stt:test -Dspring.profiles.active=integration-test
|
||||||
|
|
||||||
|
# 6. 결과 출력
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo "✅ 통합 테스트 성공!"
|
||||||
|
else
|
||||||
|
echo "❌ 통합 테스트 실패!"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 7. Docker 환경 정리 (선택)
|
||||||
|
read -p "Docker 테스트 환경을 정리하시겠습니까? (y/N): " -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
echo "🧹 Docker 테스트 환경 정리 중..."
|
||||||
|
docker-compose -f docker-compose.test.yml down -v
|
||||||
|
echo "✅ 정리 완료"
|
||||||
|
fi
|
||||||
Loading…
x
Reference in New Issue
Block a user