Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
6c41af3
Merge pull request #41 from solid-connection/main
wibaek Jul 17, 2024
8776b9d
Merge pull request #42 from solid-connection/main
wibaek Jul 17, 2024
09ff0f7
Merge pull request #44 from solid-connection/main
wibaek Jul 17, 2024
50b6702
Merge pull request #55 from solid-connection/main
wibaek Aug 10, 2024
ce1026e
Merge pull request #64 from solid-connection/main
wibaek Aug 19, 2024
50fde8e
Merge pull request #69 from solid-connection/main
leesewon00 Aug 23, 2024
ccaf728
Merge pull request #74 from solid-connection/main
leesewon00 Aug 25, 2024
5a1d029
Merge pull request #77 from solid-connection/main
leesewon00 Aug 27, 2024
4a0c7b5
Merge pull request #92 from solid-connection/main
leesewon00 Sep 7, 2024
5b3f8c4
Merge pull request #95 from solid-connection/main
wibaek Sep 7, 2024
ed2b5f1
chore: release github action 임의 실행 추가
wibaek Oct 7, 2024
84ac06d
Merge pull request #105 from solid-connection/main
wibaek Oct 9, 2024
b3b17ea
Merge pull request #108 from solid-connection/main
wibaek Nov 9, 2024
2a8d022
Merge pull request #121 from solid-connection/main
wibaek Dec 16, 2024
42c02f6
Merge pull request #123 from solid-connection/main
wibaek Dec 16, 2024
04766a2
Merge pull request #127 from solid-connection/main
wibaek Dec 16, 2024
939d008
Merge pull request #142 from solid-connection/main
wibaek Jan 5, 2025
2af7b77
refactor: 기본 추천 대학 후보 추가 (#161)
nayonsoso Jan 27, 2025
ce1d88b
Merge release branch with ours strategy to sync with main
wibaek Feb 12, 2025
4dda1da
Merge pull request #199 from solid-connection/chore/release-sync
wibaek Feb 13, 2025
90a0db7
Merge pull request #209 from solid-connection/develop
wibaek Feb 15, 2025
1039236
Merge pull request #214 from solid-connection/develop
Gyuhyeok99 Feb 15, 2025
671f3e5
Merge pull request #216 from solid-connection/develop
nayonsoso Feb 16, 2025
ee146c1
Merge pull request #222 from solid-connection/develop
nayonsoso Feb 17, 2025
e3de221
Merge pull request #225 from solid-connection/develop
nayonsoso Feb 17, 2025
a7e2647
Merge pull request #228 from solid-connection/release
Gyuhyeok99 Feb 19, 2025
f1c58f4
Merge pull request #235 from solid-connection/develop
nayonsoso Feb 20, 2025
9091dd7
Merge pull request #238 from solid-connection/release
nayonsoso Feb 20, 2025
fa30d3f
Merge pull request #243 from solid-connection/develop
wibaek Feb 20, 2025
cc4c28d
Merge pull request #245 from solid-connection/release
nayonsoso Feb 21, 2025
0962b9e
Merge pull request #249 from solid-connection/develop
wibaek Feb 22, 2025
f8b2174
Merge pull request #251 from solid-connection/develop
wibaek Feb 24, 2025
27fade4
Merge pull request #271 from solid-connection/develop
nayonsoso Apr 4, 2025
44f6cae
Merge remote-tracking branch 'origin/master' into release
nayonsoso Apr 8, 2025
b481b40
Merge pull request #274 from solid-connection/release
nayonsoso Apr 9, 2025
003aea0
Merge pull request #281 from solid-connection/develop
wibaek Apr 13, 2025
9781c5b
Merge pull request #283 from solid-connection/release
wibaek Apr 15, 2025
645f620
[RELEASE] 250826 릴리즈
Gyuhyeok99 Aug 26, 2025
5e99dfd
[DEPLOY] v2.0.0
Gyuhyeok99 Aug 26, 2025
5dc19c3
fix: config.alloy 경로 수정
whqtker Aug 26, 2025
344e202
Merge pull request #490 from whqtker/hotfix/fix-docker-compose
whqtker Aug 26, 2025
99bc01b
hotfix: 모의지원 현황 어드민 권한 제거
Gyuhyeok99 Aug 26, 2025
aa60ef0
hotfix: import 제거
Gyuhyeok99 Aug 26, 2025
fab2e32
hotfix: 모의지원 현황 어드민 권한 제거
Gyuhyeok99 Aug 26, 2025
998e043
[RELEASE] 250927 릴리즈
Gyuhyeok99 Sep 28, 2025
a71cf7e
fix: 충돌 해결
Gyuhyeok99 Sep 28, 2025
edd2986
fix: 충돌 해결
Gyuhyeok99 Sep 28, 2025
6e8a52d
[RELEASE] 251103 릴리즈
Gyuhyeok99 Nov 4, 2025
fe70cdb
Merge remote-tracking branch 'origin/develop' into release
Gyuhyeok99 Jan 19, 2026
f96997a
[RELEASE] 260119 릴리즈 (#609)
whqtker Jan 19, 2026
3b84be8
fix: docker-compose 충돌 해결 (#610)
whqtker Jan 19, 2026
9de3200
fix: flyway 전용 DataSource를 사용하도록 수정 (#613)
sukangpunch Jan 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ dependencies {
implementation 'org.hibernate.validator:hibernate-validator'
implementation 'com.amazonaws:aws-java-sdk-s3:1.12.782'
implementation 'org.springframework.boot:spring-boot-starter-websocket'

// Database Proxy
implementation 'net.ttddyy.observation:datasource-micrometer:1.2.0'
}

tasks.named('test', Test) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.example.solidconnection.admin.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.example.solidconnection.admin.dto.UserBanRequest;
import com.example.solidconnection.admin.service.AdminUserBanService;
import com.example.solidconnection.common.resolver.AuthorizedUser;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@RequestMapping("/admin/users")
@RestController
public class AdminUserBanController {
private final AdminUserBanService adminUserBanService;

@PostMapping("/{user-id}/ban")
public ResponseEntity<Void> banUser(
@AuthorizedUser long adminId,
@PathVariable(name = "user-id") long userId,
@Valid @RequestBody UserBanRequest request
) {
adminUserBanService.banUser(userId, adminId, request);
return ResponseEntity.ok().build();
}

@PatchMapping("/{user-id}/unban")
public ResponseEntity<Void> unbanUser(
@AuthorizedUser long adminId,
@PathVariable(name = "user-id") long userId
) {
adminUserBanService.unbanUser(userId, adminId);
return ResponseEntity.ok().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.example.solidconnection.admin.dto;

import com.example.solidconnection.siteuser.domain.UserBanDuration;

import jakarta.validation.constraints.NotNull;

public record UserBanRequest(
@NotNull(message = "차단 기간을 입력해주세요.")
UserBanDuration duration
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package com.example.solidconnection.admin.service;

import static java.time.ZoneOffset.UTC;

import com.example.solidconnection.admin.dto.UserBanRequest;
import com.example.solidconnection.chat.repository.ChatMessageRepository;
import com.example.solidconnection.common.exception.CustomException;
import com.example.solidconnection.common.exception.ErrorCode;
import com.example.solidconnection.community.post.repository.PostRepository;
import com.example.solidconnection.report.repository.ReportRepository;
import com.example.solidconnection.siteuser.domain.SiteUser;
import com.example.solidconnection.siteuser.domain.UserBan;
import com.example.solidconnection.siteuser.domain.UserStatus;
import com.example.solidconnection.siteuser.repository.SiteUserRepository;
import com.example.solidconnection.siteuser.repository.UserBanRepository;
import java.time.ZonedDateTime;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@RequiredArgsConstructor
@Service
public class AdminUserBanService {

private final UserBanRepository userBanRepository;
private final ReportRepository reportRepository;
private final SiteUserRepository siteUserRepository;
private final PostRepository postRepository;
private final ChatMessageRepository chatMessageRepository;

@Transactional
public void banUser(long userId, long adminId, UserBanRequest request) {
SiteUser user = siteUserRepository.findById(userId)
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));
validateNotAlreadyBanned(userId);
validateReportExists(userId);

user.updateUserStatus(UserStatus.BANNED);
updateReportedContentIsDeleted(userId, true);
createUserBan(userId, adminId, request);
}

private void validateNotAlreadyBanned(long userId) {
if (userBanRepository.existsByBannedUserIdAndIsExpiredFalseAndExpiredAtAfter(userId, ZonedDateTime.now(UTC))) {
throw new CustomException(ErrorCode.ALREADY_BANNED_USER);
}
}

private void validateReportExists(long userId) {
if (!reportRepository.existsByReportedId(userId)) {
throw new CustomException(ErrorCode.REPORT_NOT_FOUND);
}
}

private void updateReportedContentIsDeleted(long userId, boolean isDeleted) {
postRepository.updateReportedPostsIsDeleted(userId, isDeleted);
chatMessageRepository.updateReportedChatMessagesIsDeleted(userId, isDeleted);
}

private void createUserBan(long userId, long adminId, UserBanRequest request) {
ZonedDateTime now = ZonedDateTime.now(UTC);
ZonedDateTime expiredAt = now.plusDays(request.duration().getDays());
UserBan userBan = new UserBan(userId, adminId, request.duration(), expiredAt);
userBanRepository.save(userBan);
}

@Transactional
public void unbanUser(long userId, long adminId) {
SiteUser user = siteUserRepository.findById(userId)
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));
UserBan userBan = findActiveBan(userId);
userBan.manuallyUnban(adminId);

user.updateUserStatus(UserStatus.REPORTED);
updateReportedContentIsDeleted(userId, false);
}

private UserBan findActiveBan(long userId) {
return userBanRepository
.findByBannedUserIdAndIsExpiredFalseAndExpiredAtAfter(userId, ZonedDateTime.now(UTC))
.orElseThrow(() -> new CustomException(ErrorCode.NOT_BANNED_USER));
}

@Transactional
@Scheduled(cron = "0 0 0 * * *")
public void expireUserBans() {
try {
ZonedDateTime now = ZonedDateTime.now(UTC);
List<Long> expiredUserIds = userBanRepository.findExpiredBannedUserIds(now);

if (expiredUserIds.isEmpty()) {
return;
}

userBanRepository.bulkExpireUserBans(now);
siteUserRepository.bulkUpdateUserStatus(expiredUserIds, UserStatus.REPORTED);
bulkUpdateReportedContentIsDeleted(expiredUserIds);
log.info("Finished processing expired blocks:: userIds={}", expiredUserIds);
} catch (Exception e) {
log.error("Failed to process expired blocks", e);
}
}

private void bulkUpdateReportedContentIsDeleted(List<Long> expiredUserIds) {
postRepository.bulkUpdateReportedPostsIsDeleted(expiredUserIds, false);
chatMessageRepository.bulkUpdateReportedChatMessagesIsDeleted(expiredUserIds, false);
}

}
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package com.example.solidconnection.auth.dto;

import com.example.solidconnection.siteuser.domain.AuthType;
import com.example.solidconnection.siteuser.domain.ExchangeStatus;
import com.example.solidconnection.siteuser.domain.Role;
import com.example.solidconnection.siteuser.domain.SiteUser;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.NotBlank;
import java.util.List;
Expand All @@ -20,27 +17,4 @@ public record SignUpRequest(

@NotBlank(message = "닉네임을 입력해주세요.")
String nickname) {

public SiteUser toOAuthSiteUser(String email, AuthType authType) {
return new SiteUser(
email,
this.nickname,
this.profileImageUrl,
this.exchangeStatus,
Role.MENTEE,
authType
);
}

public SiteUser toEmailSiteUser(String email, String encodedPassword) {
return new SiteUser(
email,
this.nickname,
this.profileImageUrl,
this.exchangeStatus,
Role.MENTEE,
AuthType.EMAIL,
encodedPassword
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import com.example.solidconnection.siteuser.domain.AuthType;
import com.example.solidconnection.siteuser.domain.Role;
import com.example.solidconnection.siteuser.domain.SiteUser;
import com.example.solidconnection.siteuser.domain.UserStatus;
import com.example.solidconnection.siteuser.repository.SiteUserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
Expand Down Expand Up @@ -56,7 +57,8 @@ public SignInResponse signUp(SignUpRequest signUpRequest) {
signUpRequest.exchangeStatus(),
Role.MENTEE,
authType,
password
password,
UserStatus.ACTIVE
));

// 관심 지역, 국가 저장
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.Where;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Where(clause = "is_deleted = false")
public class ChatMessage extends BaseEntity {

@Id
Expand All @@ -33,6 +35,9 @@ public class ChatMessage extends BaseEntity {
@ManyToOne(fetch = FetchType.LAZY)
private ChatRoom chatRoom;

@Column(name = "is_deleted", columnDefinition = "boolean default false", nullable = false)
private boolean isDeleted = false;

@OneToMany(mappedBy = "chatMessage", cascade = CascadeType.ALL, orphanRemoval = true)
private final List<ChatAttachment> chatAttachments = new ArrayList<>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

Expand Down Expand Up @@ -48,4 +49,20 @@ SELECT MAX(cm2.id)
GROUP BY cm.chatRoom.id
""")
List<UnreadCountDto> countUnreadMessagesBatch(@Param("chatRoomIds") List<Long> chatRoomIds, @Param("userId") long userId);

@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query(value = """
UPDATE chat_message cm SET cm.is_deleted = :isDeleted
WHERE cm.id IN (SELECT r.target_id FROM report r WHERE r.target_type = 'CHAT')
AND cm.sender_id IN (SELECT cp.id FROM chat_participant cp WHERE cp.site_user_id = :siteUserId)
""", nativeQuery = true)
void updateReportedChatMessagesIsDeleted(@Param("siteUserId") long siteUserId, @Param("isDeleted") boolean isDeleted);

@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query(value = """
UPDATE chat_message cm SET cm.is_deleted = :isDeleted
WHERE cm.id IN (SELECT r.target_id FROM report r WHERE r.target_type = 'CHAT')
AND cm.sender_id IN (SELECT cp.id FROM chat_participant cp WHERE cp.site_user_id IN :siteUserIds)
""", nativeQuery = true)
void bulkUpdateReportedChatMessagesIsDeleted(@Param("siteUserIds") List<Long> siteUserIds, @Param("isDeleted") boolean isDeleted);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.example.solidconnection.common.config.datasource;

import com.example.solidconnection.common.listener.QueryMetricsListener;
import com.zaxxer.hikari.HikariDataSource;
import javax.sql.DataSource;
import lombok.RequiredArgsConstructor;
import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.flyway.FlywayDataSource;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

@RequiredArgsConstructor
@Configuration
public class DataSourceConfig {

private final QueryMetricsListener queryMetricsListener;

// Driver
public static final String FLYWAY_MYSQL_DRIVER = "com.mysql.cj.jdbc.Driver";

// Pool Name
public static final String FLYWAY_POOL_NAME = "FlywayPool";

// Connection Pool Settings
public static final int FLYWAY_MINIMUM_IDLE = 0; // 유휴 커넥션을 0으로 설정하면 사용하지 않을 때 커넥션을 즉시 반납
public static final int FLYWAY_MAXIMUM_POOL_SIZE = 2;
public static final long FLYWAY_CONNECTION_TIMEOUT = 10000L;
public static final long FLYWAY_IDLE_TIMEOUT = 60000L; // 1분
public static final long FLYWAY_MAX_LIFETIME = 300000L; // 5분

@Bean
@Primary
public DataSource proxyDataSource(DataSourceProperties props) {
DataSource dataSource = props.initializeDataSourceBuilder().build();

return ProxyDataSourceBuilder
.create(dataSource)
.listener(queryMetricsListener)
.name("main")
.build();
}

// Flyway 전용 DataSource (Proxy 미적용)
@Bean
@FlywayDataSource
public DataSource flywayDataSource(
@Value("${spring.datasource.url}") String url,
@Value("${spring.flyway.user:${spring.datasource.username}}") String username,
@Value("${spring.flyway.password:${spring.datasource.password}}") String password
) {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
dataSource.setDriverClassName(FLYWAY_MYSQL_DRIVER);
dataSource.setPoolName(FLYWAY_POOL_NAME);

dataSource.setMinimumIdle(FLYWAY_MINIMUM_IDLE);
dataSource.setMaximumPoolSize(FLYWAY_MAXIMUM_POOL_SIZE);
dataSource.setConnectionTimeout(FLYWAY_CONNECTION_TIMEOUT);
dataSource.setIdleTimeout(FLYWAY_IDLE_TIMEOUT); // 1분으로 단축
dataSource.setMaxLifetime(FLYWAY_MAX_LIFETIME); // 최대 5분

return dataSource;
}
}
Loading
Loading