From 0264bcde551c388a4af62f6bfaa59a8a79968261 Mon Sep 17 00:00:00 2001 From: Seol_JY Date: Tue, 4 Mar 2025 20:08:09 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20p6spy=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=A7=84=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 + build.gradle | 3 + .../colla/global/config/log/P6SpyConfig.java | 18 +++++ .../global/config/log/P6SpyEventListener.java | 15 ++++ .../global/config/log/P6SpyFormatter.java | 71 +++++++++++++++++++ src/main/resources/application.yml | 6 +- 6 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 src/main/java/one/colla/global/config/log/P6SpyConfig.java create mode 100644 src/main/java/one/colla/global/config/log/P6SpyEventListener.java create mode 100644 src/main/java/one/colla/global/config/log/P6SpyFormatter.java diff --git a/.gitignore b/.gitignore index 762fe98..6054042 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,6 @@ logs/* ### application*.properties ### src/main/resources/*.properties + +### p6spy log ### +spy.log diff --git a/build.gradle b/build.gradle index e7b9bc6..69e88a6 100644 --- a/build.gradle +++ b/build.gradle @@ -85,6 +85,9 @@ dependencies { /* visibility */ implementation 'org.springframework.boot:spring-boot-starter-actuator:3.2.4' runtimeOnly 'io.micrometer:micrometer-registry-prometheus' + + implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.2' + } tasks.register('copyPrivate', Copy) { diff --git a/src/main/java/one/colla/global/config/log/P6SpyConfig.java b/src/main/java/one/colla/global/config/log/P6SpyConfig.java new file mode 100644 index 0000000..b9b1688 --- /dev/null +++ b/src/main/java/one/colla/global/config/log/P6SpyConfig.java @@ -0,0 +1,18 @@ +package one.colla.global.config.log; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class P6SpyConfig { + + @Bean + public P6SpyEventListener p6SpyCustomEventListener() { + return new P6SpyEventListener(); + } + + @Bean + public P6SpyFormatter p6SpyCustomFormatter() { + return new P6SpyFormatter(); + } +} diff --git a/src/main/java/one/colla/global/config/log/P6SpyEventListener.java b/src/main/java/one/colla/global/config/log/P6SpyEventListener.java new file mode 100644 index 0000000..ee67841 --- /dev/null +++ b/src/main/java/one/colla/global/config/log/P6SpyEventListener.java @@ -0,0 +1,15 @@ +package one.colla.global.config.log; + +import java.sql.SQLException; + +import com.p6spy.engine.common.ConnectionInformation; +import com.p6spy.engine.event.JdbcEventListener; +import com.p6spy.engine.spy.P6SpyOptions; + +public class P6SpyEventListener extends JdbcEventListener { + + @Override + public void onAfterGetConnection(ConnectionInformation connectionInformation, SQLException ex) { + P6SpyOptions.getActiveInstance().setLogMessageFormat(P6SpyFormatter.class.getName()); + } +} diff --git a/src/main/java/one/colla/global/config/log/P6SpyFormatter.java b/src/main/java/one/colla/global/config/log/P6SpyFormatter.java new file mode 100644 index 0000000..a9a6eb6 --- /dev/null +++ b/src/main/java/one/colla/global/config/log/P6SpyFormatter.java @@ -0,0 +1,71 @@ +package one.colla.global.config.log; + +import java.util.Locale; +import java.util.Set; + +import org.hibernate.engine.jdbc.internal.FormatStyle; + +import com.p6spy.engine.logging.Category; +import com.p6spy.engine.spy.appender.MessageFormattingStrategy; + +public class P6SpyFormatter implements MessageFormattingStrategy { + + private static final String NEW_LINE = System.lineSeparator(); + private static final String TAB = "\t"; + private static final Set DDL_KEYWORDS = Set.of("create", "alter", "drop", "comment"); + private static final String CONNECTION_ID_FORMAT = "Connection ID: %s"; + private static final String SEPARATOR = "-".repeat(200); + + @Override + public String formatMessage(int connectionId, String now, long elapsed, String category, String prepared, + String sql, String url) { + if (sql.trim().isEmpty()) { + return formatByCommand(category, connectionId); + } + return formatBySql(sql, category) + formatAdditionalInfo(elapsed, connectionId); + } + + private static String formatByCommand(String category, int connectionId) { + return String.format("%s | Category: %s", String.format(CONNECTION_ID_FORMAT, connectionId), category); + } + + private String formatBySql(String sql, String category) { + String formattedSql = isStatementDdl(sql, category) + ? formatDdl(sql) + : formatDml(sql); + + return NEW_LINE + removeTimeZoneOffset(formattedSql); + } + + private String formatDdl(String sql) { + return NEW_LINE + "Execute DDL : " + FormatStyle.DDL.getFormatter().format(sql); + } + + private String formatDml(String sql) { + return NEW_LINE + "Execute DML : " + FormatStyle.BASIC.getFormatter().format(sql); + } + + private String formatAdditionalInfo(long elapsed, int connectionId) { + return String.join( + NEW_LINE, + "", + "", + TAB + String.format(CONNECTION_ID_FORMAT, connectionId), + TAB + String.format("Execution Time: %s ms", elapsed), + "", + SEPARATOR + ); + } + + private boolean isStatementDdl(String sql, String category) { + return Category.STATEMENT.getName().equals(category) && isDdl(sql.trim().toLowerCase(Locale.ROOT)); + } + + private boolean isDdl(String lowerSql) { + return DDL_KEYWORDS.stream().anyMatch(lowerSql::startsWith); + } + + private String removeTimeZoneOffset(String sql) { + return sql.replace("+0900", ""); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 5e19ae9..a93518f 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -68,7 +68,6 @@ spring: websocket: allowed-origins: http://localhost:8080, http://localhost:3000, ${BASE_URL} - springdoc: default-consumes-media-type: application/json;charset=UTF-8 default-produces-media-type: application/json;charset=UTF-8 @@ -76,6 +75,11 @@ springdoc: url: /docs/open-api-3.0.1.yaml path: /swagger +decorator: + datasource: + p6spy: + enable-logging: true + jwt: secret-key: access-token: collaAccessAccessAccessTokenSecretKeyForCollaSystem From 420cf3acc0f932e0b78c9bb8ebe9da89f53f8d65 Mon Sep 17 00:00:00 2001 From: Seol_JY Date: Tue, 4 Mar 2025 20:36:41 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20status=20Api=20=EB=82=B4=EB=B6=80?= =?UTF-8?q?=20=EC=BF=BC=EB=A6=AC=20=EC=84=B1=EB=8A=A5=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/TeamspaceService.java | 11 ++++ .../teamspace/domain/TeamspaceRepository.java | 10 ++++ .../colla/user/application/UserService.java | 54 ++++++++++--------- .../one/colla/user/domain/UserRepository.java | 8 +++ 4 files changed, 59 insertions(+), 24 deletions(-) diff --git a/src/main/java/one/colla/teamspace/application/TeamspaceService.java b/src/main/java/one/colla/teamspace/application/TeamspaceService.java index 7569a86..df7c9ba 100644 --- a/src/main/java/one/colla/teamspace/application/TeamspaceService.java +++ b/src/main/java/one/colla/teamspace/application/TeamspaceService.java @@ -1,7 +1,9 @@ package one.colla.teamspace.application; import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.stream.Collectors; import org.apache.commons.lang3.tuple.Pair; import org.springframework.context.ApplicationEventPublisher; @@ -298,6 +300,15 @@ private Pair generateAndSaveInviteCodeByTeamspaceId(C return Pair.of(inviteCode, userTeamspace); } + @Transactional(readOnly = true) + public Map countParticipantsByTeamspaceIds(List teamspaceIds) { + return teamspaceRepository.countParticipantsByTeamspaceIds(teamspaceIds).stream() + .collect(Collectors.toMap( + arr -> (Long)arr[0], // 팀스페이스 ID + arr -> (Long)arr[1] // 참여자 수 + )); + } + /** * 현재 사용자가 특정 팀 스페이스의 참가자인지 확인하고, 해당 팀 스페이스에 대한 사용자-팀 매핑 엔티티를 반환합니다. * diff --git a/src/main/java/one/colla/teamspace/domain/TeamspaceRepository.java b/src/main/java/one/colla/teamspace/domain/TeamspaceRepository.java index 37940d7..92165c4 100644 --- a/src/main/java/one/colla/teamspace/domain/TeamspaceRepository.java +++ b/src/main/java/one/colla/teamspace/domain/TeamspaceRepository.java @@ -1,6 +1,16 @@ package one.colla.teamspace.domain; +import java.util.List; + import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface TeamspaceRepository extends JpaRepository { + @Query("SELECT t.id, COUNT(ut) " + + "FROM Teamspace t " + + "JOIN t.userTeamspaces ut " + + "WHERE t.id IN :teamspaceIds " + + "GROUP BY t.id") + List countParticipantsByTeamspaceIds(@Param("teamspaceIds") List teamspaceIds); } diff --git a/src/main/java/one/colla/user/application/UserService.java b/src/main/java/one/colla/user/application/UserService.java index a9ac00b..3c54629 100644 --- a/src/main/java/one/colla/user/application/UserService.java +++ b/src/main/java/one/colla/user/application/UserService.java @@ -1,6 +1,7 @@ package one.colla.user.application; import java.util.List; +import java.util.Map; import java.util.Optional; import org.springframework.stereotype.Service; @@ -16,7 +17,6 @@ import one.colla.infra.redis.lastseen.LastSeenTeamspace; import one.colla.infra.redis.lastseen.LastSeenTeamspaceService; import one.colla.teamspace.application.TeamspaceService; -import one.colla.teamspace.domain.Teamspace; import one.colla.teamspace.domain.UserTeamspace; import one.colla.user.application.dto.request.LastSeenUpdateRequest; import one.colla.user.application.dto.request.UpdateUserSettingRequest; @@ -45,44 +45,50 @@ public Optional getUserById(Long id) { @Transactional(readOnly = true) public UserStatusResponse getUserStatus(CustomUserDetails userDetails) { - final User user = userRepository.findById(userDetails.getUserId()) + final User user = userRepository.findByIdWithTeamspaces(userDetails.getUserId()) .orElseThrow(() -> new CommonException(ExceptionCode.NOT_FOUND_USER)); Long lastSeenTeamspaceId = lastSeenTeamspaceService.findByUserId(user.getId()) .map(LastSeenTeamspace::getTeamspaceId) .orElse(null); - ProfileDto profile = ProfileDto.of(user.getId(), user, lastSeenTeamspaceId); + List teamspaceIds = user.getUserTeamspaces().stream() + .map(ut -> ut.getTeamspace().getId()) + .toList(); + Map participantCountMap = teamspaceService.countParticipantsByTeamspaceIds(teamspaceIds); List participatedTeamspaces = user.getUserTeamspaces().stream() - .map(ut -> ParticipatedTeamspaceDto.of( - ut.getTeamspace().getId(), + .map(ut -> { + Long teamspaceId = ut.getTeamspace().getId(); + return ParticipatedTeamspaceDto.of( + teamspaceId, ut, - getNumOfTeamspaceParticipants(ut), - calculateUnreadMessageCount(ut.getUser().getId(), ut.getTeamspace()) - ) - ) + participantCountMap.getOrDefault(teamspaceId, 0L).intValue(), + 1 // 읽지 않은 메세지 임시 처리 (향후 최적화 가능) + ); + }) .toList(); log.info("사용자 관련 정보 조회 - 사용자 Id: {}", userDetails.getUserId()); return UserStatusResponse.of(profile, participatedTeamspaces); } - private int calculateUnreadMessageCount(Long userId, Teamspace teamspace) { - return teamspace.getChatChannels().stream() - .mapToInt(chatChannel -> userChatChannelRepository.findByUserIdAndChatChannelId(userId, chatChannel.getId()) - .map(userChatChannel -> { - Long lastReadMessageId = userChatChannel.getLastReadMessageId(); - if (lastReadMessageId == null) { - return chatChannelMessageRepository.countByChatChannel(chatChannel); - } else { - return chatChannelMessageRepository.countByChatChannelAndIdGreaterThan(chatChannel, - lastReadMessageId); - } - }) - .orElse(0)) - .sum(); - } + // TODO: API 분리 필요 + // private int calculateUnreadMessageCount(Long userId, Teamspace teamspace) { + // return teamspace.getChatChannels().stream() + // .mapToInt(chatChannel -> userChatChannelRepository.findByUserIdAndChatChannelId(userId, chatChannel.getId()) + // .map(userChatChannel -> { + // Long lastReadMessageId = userChatChannel.getLastReadMessageId(); + // if (lastReadMessageId == null) { + // return chatChannelMessageRepository.countByChatChannel(chatChannel); + // } else { + // return chatChannelMessageRepository.countByChatChannelAndIdGreaterThan(chatChannel, + // lastReadMessageId); + // } + // }) + // .orElse(0)) + // .sum(); + // } @Transactional public void updateLastSeenTeamspace(CustomUserDetails userDetails, LastSeenUpdateRequest request) { diff --git a/src/main/java/one/colla/user/domain/UserRepository.java b/src/main/java/one/colla/user/domain/UserRepository.java index 810cba1..c97ae6c 100644 --- a/src/main/java/one/colla/user/domain/UserRepository.java +++ b/src/main/java/one/colla/user/domain/UserRepository.java @@ -3,9 +3,17 @@ import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import one.colla.user.domain.vo.Email; public interface UserRepository extends JpaRepository { Optional findByEmail(Email email); + + @Query("SELECT DISTINCT u FROM User u " + + "LEFT JOIN FETCH u.userTeamspaces ut " + + "LEFT JOIN FETCH ut.teamspace t " + + "WHERE u.id = :userId") + Optional findByIdWithTeamspaces(@Param("userId") Long userId); }