Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ public class SecurityPath {
// hasRole("USER")
public static final String[] USER_ENDPOINTS = {
"/api/auth/connect/github/state",
"/api/auth/logout",
"/api/auth/social",
"/api/users/me",
"/api/users/me/**",
"/api/auth/logout",
"/api/repos",
"/api/repos/**",
"/api/mindmaps/**",
"/api/history/**"
"/api/history/**",
"/api/s3/bucket/**"
};

// hasRole("ADMIN")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@ public ResponseEntity<MindmapResponseDto> createMindmap(
// ๋งˆ์ธ๋“œ๋งต ์ƒ์„ธ ์กฐํšŒ (์œ ์ € ์ธ๊ฐ€ ํ™•์ธํ•„์š”?)
@GetMapping("/{mapId}")
public ResponseEntity<MindmapDetailResponseDto> getMindmap(
@PathVariable Long mapId
@PathVariable Long mapId,
@AuthenticationPrincipal CustomUserDetails userDetails
) {
MindmapDetailResponseDto responseDto = mindmapService.getMindmap(mapId);
MindmapDetailResponseDto responseDto = mindmapService.getMindmap(mapId, userDetails.getId());
return ResponseEntity.ok(responseDto);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.teamEWSN.gitdeun.mindmap.controller;

import com.teamEWSN.gitdeun.mindmap.service.MindmapSseService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

@RestController
@RequiredArgsConstructor
public class MindmapSseController {

private final MindmapSseService mindmapSseService;

// ํด๋ผ์ด์–ธํŠธ์˜ ํŠน์ • ๋งˆ์ธ๋“œ๋งต์˜ ์—…๋ฐ์ดํŠธ๋ฅผ ๊ตฌ๋…
@GetMapping(value = "/api/mindmaps/{mapId}/subscribe", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter subscribeToMindmapUpdates(@PathVariable Long mapId) {
return mindmapSseService.subscribe(mapId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import com.teamEWSN.gitdeun.mindmap.mapper.MindmapMapper;
import com.teamEWSN.gitdeun.mindmapmember.repository.MindmapMemberRepository;
import com.teamEWSN.gitdeun.mindmap.repository.MindmapRepository;
import com.teamEWSN.gitdeun.mindmapmember.service.MindmapAuthService;
import com.teamEWSN.gitdeun.repo.entity.Repo;
import com.teamEWSN.gitdeun.repo.repository.RepoRepository;
import com.teamEWSN.gitdeun.repo.service.RepoService;
Expand All @@ -36,6 +37,8 @@ public class MindmapService {

private final VisitHistoryService visitHistoryService;
private final RepoService repoService;
private final MindmapSseService mindmapSseService;
private final MindmapAuthService mindmapAuthService;
private final MindmapMapper mindmapMapper;
private final MindmapRepository mindmapRepository;
private final MindmapMemberRepository mindmapMemberRepository;
Expand Down Expand Up @@ -117,9 +120,15 @@ private long findNextCheckSequence(User user) {

/**
* ๋งˆ์ธ๋“œ๋งต ์ƒ์„ธ ์ •๋ณด ์กฐํšŒ
* ์ €์žฅ์†Œ ์—…๋ฐ์ดํŠธ๋Š” Fast API Webhook ์•Œ๋ฆผ
* ๋งˆ์ธ๋“œ๋งต ๋ณ€๊ฒฝ์ด๋‚˜ ๋ฆฌ๋ทฐ ์ƒ์„ฑ ์‹œ SSE ์ ์šฉ์„ ํ†ตํ•œ ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ (์ƒˆ๋กœ๊ณ ์นจ x)
*/
@Transactional
public MindmapDetailResponseDto getMindmap(Long mapId) {
public MindmapDetailResponseDto getMindmap(Long mapId, Long userId) {
if (!mindmapAuthService.hasView(mapId, userId)) {
throw new GlobalException(ErrorCode.FORBIDDEN_ACCESS); // ๋ฉค๋ฒ„ ๊ถŒํ•œ ํ™•์ธ
}

Mindmap mindmap = mindmapRepository.findById(mapId)
.orElseThrow(() -> new GlobalException(ErrorCode.MINDMAP_NOT_FOUND));

Expand Down Expand Up @@ -148,9 +157,14 @@ public MindmapDetailResponseDto refreshMindmap(Long mapId, Long userId) {

// ๋ฐ์ดํ„ฐ ์ตœ์‹ ํ™”
mindmap.getRepo().updateWithAnalysis(dto);
mindmap.updateMapData(dto.getMapData()); // Mindmap ์—”ํ‹ฐํ‹ฐ์— ํŽธ์˜ ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€ ํ•„์š”
mindmap.updateMapData(dto.getMapData());

return mindmapMapper.toDetailResponseDto(mindmap);
MindmapDetailResponseDto responseDto = mindmapMapper.toDetailResponseDto(mindmap);

// ์—…๋ฐ์ดํŠธ๋œ ๋งˆ์ธ๋“œ๋งต ์ •๋ณด๋ฅผ ๋ชจ๋“  ๊ตฌ๋…์ž์—๊ฒŒ ๋ฐฉ์†ก
mindmapSseService.broadcastUpdate(mapId, responseDto);

return responseDto;
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.teamEWSN.gitdeun.mindmap.service;

import com.teamEWSN.gitdeun.mindmap.dto.MindmapDetailResponseDto;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.io.IOException;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Slf4j
@Service
public class MindmapSseService {

// ์Šค๋ ˆ๋“œ ์•ˆ์ „(thread-safe)ํ•œ ์ž๋ฃŒ๊ตฌ์กฐ ์‚ฌ์šฉ
private final Map<Long, List<SseEmitter>> emitters = new ConcurrentHashMap<>();

// ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๊ตฌ๋…์„ ์š”์ฒญํ•  ๋•Œ ํ˜ธ์ถœ
public SseEmitter subscribe(Long mapId) {
SseEmitter emitter = new SseEmitter(Long.MAX_VALUE); // ํƒ€์ž„์•„์›ƒ์„ ๋งค์šฐ ๊ธธ๊ฒŒ ์„ค์ •
// ํŠน์ • ๋งˆ์ธ๋“œ๋งต ID์— ํ•ด๋‹นํ•˜๋Š” Emitter ๋ฆฌ์ŠคํŠธ์— ์ถ”๊ฐ€
this.emitters.computeIfAbsent(mapId, k -> new CopyOnWriteArrayList<>()).add(emitter);

emitter.onCompletion(() -> this.emitters.get(mapId).remove(emitter));
emitter.onTimeout(() -> this.emitters.get(mapId).remove(emitter));

// ์—ฐ๊ฒฐ ์„ฑ๊ณต์„ ์•Œ๋ฆฌ๋Š” ๋”๋ฏธ ์ด๋ฒคํŠธ ์ „์†ก
try {
emitter.send(SseEmitter.event().name("connect").data("Connected!"));
} catch (IOException e) {
log.error("SSE ์—ฐ๊ฒฐ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ", e);
}

return emitter;
}

// ๋งˆ์ธ๋“œ๋งต์ด ์—…๋ฐ์ดํŠธ๋˜๋ฉด ์ด ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ๋ชจ๋“  ๊ตฌ๋…์ž์—๊ฒŒ ๋ฐฉ์†ก
public void broadcastUpdate(Long mapId, MindmapDetailResponseDto updatedMindmap) {
List<SseEmitter> mapEmitters = this.emitters.get(mapId);
if (mapEmitters == null) {
return;
}

mapEmitters.forEach(emitter -> {
try {
emitter.send(SseEmitter.event()
.name("mindmap-update") // ์ด๋ฒคํŠธ ์ด๋ฆ„ ์ง€์ •
.data(updatedMindmap)); // ์—…๋ฐ์ดํŠธ๋œ ๋งˆ์ธ๋“œ๋งต ๋ฐ์ดํ„ฐ ์ „์†ก
} catch (IOException e) {
log.error("SSE ๋ฐ์ดํ„ฐ ์ „์†ก ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ, emitter ์ œ๊ฑฐ", e);
mapEmitters.remove(emitter);
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,9 @@ public interface MindmapMemberRepository extends JpaRepository<MindmapMember, Lo
/* OWNER/EDITOR/VIEWER ์—ฌ๋ถ€ */
boolean existsByMindmapIdAndUserId(Long mindmapId, Long userId);

boolean existsByMindmapIdAndUserIdAndRole(
Long mindmapId, Long userId, MindmapRole role);
boolean existsByMindmapIdAndUserIdAndRole(Long mindmapId, Long userId, MindmapRole role);

boolean existsByMindmapIdAndUserIdAndRoleIn(
Long mindmapId, Long userId, Collection<MindmapRole> roles);
boolean existsByMindmapIdAndUserIdAndRoleIn(Long mindmapId, Long userId, Collection<MindmapRole> roles);

// ๊ถŒํ•œ ๋ณ€๊ฒฝ
Optional<MindmapMember> findByIdAndMindmapId(Long memberId, Long mindmapId);
Expand Down