Skip to content

Commit 0dfaa77

Browse files
authored
Merge pull request #26 from ASSU-org/feat/#21-popular-partenrship
[Feat/#21] 사용자 제휴내역 조회 및 인기 통계
2 parents 3f0bafb + a03beea commit 0dfaa77

File tree

67 files changed

+1420
-80
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

67 files changed

+1420
-80
lines changed

build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ dependencies {
8585
// h2 db (test)
8686
runtimeOnly 'com.h2database:h2'
8787

88+
// JSON 처리
89+
implementation 'com.fasterxml.jackson.core:jackson-databind'
8890
implementation group: 'org.javassist', name: 'javassist', version: '3.15.0-GA'
8991
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
9092

src/main/java/com/assu/server/domain/admin/entity/Admin.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
package com.assu.server.domain.admin.entity;
22

3+
4+
import com.assu.server.domain.user.entity.enums.Major;
35
import com.assu.server.domain.member.entity.Member;
6+
47
import jakarta.persistence.Entity;
8+
import jakarta.persistence.EnumType;
9+
import jakarta.persistence.Enumerated;
510
import jakarta.persistence.JoinColumn;
611
import jakarta.persistence.MapsId;
712
import jakarta.persistence.OneToOne;
@@ -38,4 +43,11 @@ public class Admin {
3843
private Boolean isSignVerified;
3944

4045
private LocalDateTime signVerifiedAt;
46+
47+
@Enumerated(EnumType.STRING)
48+
private Major major;
49+
50+
public void setMember(Member member) {
51+
this.member = member;
52+
}
4153
}

src/main/java/com/assu/server/domain/admin/repository/AdminRepository.java

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,30 @@
11
package com.assu.server.domain.admin.repository;
22

3-
import com.assu.server.domain.admin.entity.Admin;
3+
import java.util.List;
4+
import java.util.Optional;
5+
46
import org.springframework.data.jpa.repository.JpaRepository;
57
import org.springframework.data.jpa.repository.Query;
68
import org.springframework.data.repository.query.Param;
7-
89
import java.util.List;
10+
import com.assu.server.domain.admin.entity.Admin;
11+
import com.assu.server.domain.user.entity.enums.Department;
12+
import com.assu.server.domain.user.entity.enums.Major;
13+
import com.assu.server.domain.user.entity.enums.University;
914

1015
public interface AdminRepository extends JpaRepository<Admin, Long> {
16+
17+
// 여기 예원이 머지하고 수정
18+
@Query("SELECT a FROM Admin a WHERE " +
19+
"a.name LIKE %:university% OR " +
20+
"a.name LIKE %:department% OR " +
21+
"a.major = :major")
22+
List<Admin> findMatchingAdmins(@Param("university") String university,
23+
@Param("department") String department,
24+
@Param("major") Major major);
25+
26+
Optional<Admin> findByName(String name);
27+
1128
// 후보 수 카운트: 해당 partner와 ACTIVE 제휴가 없는 admin 수
1229
@Query(value = """
1330
SELECT COUNT(*)
@@ -34,4 +51,5 @@ SELECT COUNT(*)
3451
List<Admin> findPartnerWithOffset(@Param("partnerId") Long partnerId,
3552
@Param("offset") int offset,
3653
@Param("limit") int limit);
54+
3755
}
Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
package com.assu.server.domain.admin.service;
2-
32
import com.assu.server.domain.admin.dto.AdminResponseDTO;
43

4+
import java.util.List;
5+
6+
import com.assu.server.domain.admin.entity.Admin;
7+
import com.assu.server.domain.user.entity.enums.Department;
8+
import com.assu.server.domain.user.entity.enums.Major;
9+
import com.assu.server.domain.user.entity.enums.University;
10+
11+
// PaperQueryServiceImpl 이 AdminService 참조 중 -> 순환참조 문제 발생하지 않도록 주의
512
public interface AdminService {
13+
List<Admin> findMatchingAdmins(String university, String department, Major major);
614

715
AdminResponseDTO.RandomPartnerResponseDTO suggestRandomPartner(Long adminId);
16+
817
}

src/main/java/com/assu/server/domain/admin/service/AdminServiceImpl.java

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,38 @@
11
package com.assu.server.domain.admin.service;
22

3+
4+
import java.util.List;
5+
import org.springframework.stereotype.Service;
36
import com.assu.server.domain.admin.dto.AdminResponseDTO;
47
import com.assu.server.domain.admin.entity.Admin;
58
import com.assu.server.domain.admin.repository.AdminRepository;
9+
import com.assu.server.domain.user.entity.enums.Major;
10+
import jakarta.transaction.Transactional;
11+
import lombok.RequiredArgsConstructor;
612
import com.assu.server.domain.partner.entity.Partner;
713
import com.assu.server.domain.partner.repository.PartnerRepository;
814
import com.assu.server.global.apiPayload.code.status.ErrorStatus;
915
import com.assu.server.global.exception.DatabaseException;
10-
import lombok.RequiredArgsConstructor;
11-
import org.springframework.stereotype.Service;
12-
1316
import java.util.concurrent.ThreadLocalRandom;
1417

18+
1519
@Service
1620
@RequiredArgsConstructor
1721
public class AdminServiceImpl implements AdminService {
1822

1923
private final AdminRepository adminRepository;
2024
private final PartnerRepository partnerRepository;
25+
@Override
26+
@Transactional
27+
public List<Admin> findMatchingAdmins(String university, String department, Major major){
28+
2129

30+
List<Admin> adminList = adminRepository.findMatchingAdmins(university, department,major);
31+
32+
return adminList;
33+
}
2234
@Override
35+
@Transactional
2336
public AdminResponseDTO.RandomPartnerResponseDTO suggestRandomPartner(Long adminId) {
2437

2538
Admin admin = adminRepository.findById(adminId)
@@ -44,4 +57,5 @@ public AdminResponseDTO.RandomPartnerResponseDTO suggestRandomPartner(Long admin
4457
.partnerDetailAddress(picked.getDetailAddress())
4558
.build();
4659
}
60+
4761
}

src/main/java/com/assu/server/domain/auth/entity/SSUAuth.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
@Table(
1212
name = "ssu_auth",
1313
indexes = {
14-
@Index(name = "ux_ssu_auth_student_id", columnList = "student_id", unique = true)
14+
@Index(name = "ux_ssu_auth_student_id", columnList = "student_number", unique = true)
1515
}
1616
)
1717
@Getter
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package com.assu.server.domain.certification;
2+
3+
import java.time.Duration;
4+
import java.util.Optional;
5+
import java.util.concurrent.Executors;
6+
import java.util.concurrent.ScheduledExecutorService;
7+
import java.util.concurrent.TimeUnit;
8+
9+
import org.springframework.beans.factory.annotation.Autowired;
10+
import org.springframework.stereotype.Component;
11+
12+
import com.assu.server.domain.certification.component.CertificationSessionManager;
13+
import com.assu.server.domain.certification.entity.AssociateCertification;
14+
import com.assu.server.domain.certification.entity.enums.SessionStatus;
15+
import com.assu.server.domain.certification.repository.AssociateCertificationRepository;
16+
17+
@Component
18+
public class SessionTimeoutManager {
19+
20+
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
21+
22+
@Autowired
23+
private AssociateCertificationRepository certificationRepository;
24+
25+
@Autowired
26+
private CertificationSessionManager sessionManager;
27+
28+
public void scheduleTimeout(Long sessionId, Duration timeout) {
29+
scheduler.schedule(() -> {
30+
closeSession(sessionId);
31+
}, timeout.toMillis(), TimeUnit.MILLISECONDS);
32+
}
33+
34+
private void closeSession(Long sessionId) {
35+
Optional<AssociateCertification> certOpt = certificationRepository.findById(sessionId);
36+
certOpt.ifPresent(cert -> {
37+
if (cert.getStatus() == SessionStatus.OPENED) {
38+
cert.setStatus(SessionStatus.EXPIRED);
39+
certificationRepository.save(cert);
40+
}
41+
});
42+
// 이러면 인증 전에 만료되는 것은 EXPIRED로, 시간안에 인증 된 세션은 COMPLETED로 남음
43+
44+
// 메모리에서도 세션 제거
45+
sessionManager.removeSession(sessionId);
46+
}
47+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.assu.server.domain.certification.component;
2+
3+
import java.util.List;
4+
import java.util.Map;
5+
import java.util.Set;
6+
import java.util.concurrent.ConcurrentHashMap;
7+
8+
import org.springframework.stereotype.Component;
9+
10+
@Component
11+
public class CertificationSessionManager {
12+
private final Map<Long, Set<Long>> sessionUserMap = new ConcurrentHashMap<>();
13+
14+
public void openSession(Long sessionId) {
15+
sessionUserMap.put(sessionId, ConcurrentHashMap.newKeySet());
16+
}
17+
18+
public void addUserToSession(Long sessionId, Long userId) {
19+
sessionUserMap.getOrDefault(sessionId, ConcurrentHashMap.newKeySet()).add(userId);
20+
}
21+
22+
public int getCurrentUserCount(Long sessionId) {
23+
return sessionUserMap.getOrDefault(sessionId, Set.of()).size();
24+
}
25+
26+
public boolean hasUser(Long sessionId, Long userId) {
27+
return sessionUserMap.getOrDefault(sessionId, Set.of()).contains(userId);
28+
}
29+
public List<Long> snapshotUserIds(Long sessionId) {
30+
return List.copyOf(sessionUserMap.getOrDefault(sessionId, Set.of()));
31+
}
32+
33+
public void removeSession(Long sessionId) {
34+
sessionUserMap.remove(sessionId);
35+
}
36+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.assu.server.domain.certification.config;
2+
3+
import org.springframework.context.annotation.Configuration;
4+
import org.springframework.messaging.simp.config.ChannelRegistration;
5+
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
6+
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
7+
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
8+
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
9+
10+
import lombok.RequiredArgsConstructor;
11+
12+
@EnableWebSocketMessageBroker
13+
@Configuration
14+
@RequiredArgsConstructor
15+
public class CertifyWebSocketConfig implements WebSocketMessageBrokerConfigurer {
16+
17+
private final StompAuthChannelInterceptor stompAuthChannelInterceptor;
18+
@Override
19+
public void configureMessageBroker(MessageBrokerRegistry config) {
20+
config.enableSimpleBroker("/certification/progress"); // 인증현황을 받아보기 위한 구독 주소
21+
config.setApplicationDestinationPrefixes("/certification"); // 클라이언트가 인증 요청을 보내는 주소
22+
}
23+
24+
@Override
25+
public void registerStompEndpoints(StompEndpointRegistry registry) {
26+
registry.addEndpoint("/ws") // 클라이언트 WebSocket 연결 주소
27+
.setAllowedOriginPatterns("*").withSockJS(); // CORS 허용
28+
}
29+
30+
@Override
31+
public void configureClientInboundChannel(ChannelRegistration registration) {
32+
registration.interceptors(stompAuthChannelInterceptor);
33+
}
34+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.assu.server.domain.certification.config;
2+
3+
import com.assu.server.domain.auth.security.jwt.JwtUtil;
4+
import lombok.RequiredArgsConstructor;
5+
import org.springframework.messaging.*;
6+
import org.springframework.messaging.simp.stomp.*;
7+
import org.springframework.messaging.support.ChannelInterceptor;
8+
import org.springframework.security.core.Authentication;
9+
import org.springframework.stereotype.Component;
10+
11+
@Component
12+
@RequiredArgsConstructor
13+
public class StompAuthChannelInterceptor implements ChannelInterceptor {
14+
15+
private final JwtUtil jwtUtil;
16+
17+
@Override
18+
public Message<?> preSend(Message<?> message, MessageChannel channel) {
19+
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
20+
21+
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
22+
// 프론트에서 connect 시 Authorization 헤더 넣어야 함
23+
String authHeader = accessor.getFirstNativeHeader("Authorization");
24+
if (authHeader != null && authHeader.startsWith("Bearer ")) {
25+
String token = jwtUtil.getTokenFromHeader(authHeader);
26+
27+
// JwtUtil 이용해서 Authentication 복원
28+
Authentication authentication = jwtUtil.getAuthentication(token);
29+
30+
// WebSocket 세션에 Authentication(UserPrincipal) 저장
31+
accessor.setUser(authentication);
32+
}
33+
}
34+
35+
return message;
36+
}
37+
}

0 commit comments

Comments
 (0)