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 @@ -10,6 +10,7 @@
import com.deepdirect.deepwebide_be.chat.repository.ChatMessageReferenceRepository;
import com.deepdirect.deepwebide_be.chat.repository.ChatMessageRepository;
import com.deepdirect.deepwebide_be.file.domain.FileNode;
import com.deepdirect.deepwebide_be.file.domain.FileType;
import com.deepdirect.deepwebide_be.file.repository.FileNodeRepository;
import com.deepdirect.deepwebide_be.global.exception.ErrorCode;
import com.deepdirect.deepwebide_be.global.exception.GlobalException;
Expand Down Expand Up @@ -164,15 +165,9 @@ public CodePathListResponse getCodePaths(Long repositoryId, Long userId) {
List<FileNode> fileNodes = fileNodeRepository.findAllByRepositoryId(repositoryId);

List<String> paths = fileNodes.stream()
.filter(node -> node.getFileType() == FileType.FILE) // ← 폴더는 제외
.map(FileNode::getPath)
.sorted((a, b) -> {
// 폴더 먼저, 사전순 정렬
boolean isAFolder = a.endsWith("/");
boolean isBFolder = b.endsWith("/");
if (isAFolder && !isBFolder) return -1;
if (!isAFolder && isBFolder) return 1;
return a.compareToIgnoreCase(b);
})
.sorted(String::compareToIgnoreCase)
.toList();

return CodePathListResponse.builder().paths(paths).build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
import com.deepdirect.deepwebide_be.file.domain.FileContent;
import com.deepdirect.deepwebide_be.file.domain.FileNode;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.List;
import java.util.Optional;

public interface FileContentRepository extends JpaRepository<FileContent, Long> {
Expand All @@ -12,4 +15,6 @@ public interface FileContentRepository extends JpaRepository<FileContent, Long>

Optional<FileContent> findByFileNode(FileNode fileNode);

@Query("SELECT fc FROM FileContent fc WHERE fc.fileNode.repository.id = :repositoryId")
List<FileContent> findAllByRepositoryId(@Param("repositoryId") Long repositoryId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,33 +14,4 @@ public class HistorySaveRequest {
@Schema(description = "저장 메시지(커밋 메시지)", example = "1차 개발 완료")
private String message;

@Schema(description = "저장할 파일/폴더 노드 목록 (트리 구조 아님, 전체 파일/폴더의 납작한 배열)")
private List<NodeDto> nodes;

@Getter
@NoArgsConstructor
@Schema(description = "파일 또는 폴더 노드 정보")
public static class NodeDto {

@Schema(description = "파일/폴더 ID (생성 직후는 null일 수 있음, 기존 파일은 PK)",
example = "4")
private Long fileId;

@Schema(description = "파일/폴더명", example = "Main.java")
private String fileName;

@Schema(description = "노드 타입 (FILE: 파일, FOLDER: 폴더)", example = "FILE")
private String fileType;

@Schema(description = "부모 폴더 ID (최상위면 null)", example = "1")
private Long parentId;

@Schema(description = "파일/폴더 전체 경로", example = "src/Main.java")
private String path;

@Schema(description = "파일 내용 (파일일 때만 존재, 폴더면 null)",
example = "public class Main { ... }",
nullable = true)
private String content;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,118 +45,55 @@ public class HistoryService {

@Transactional
public HistorySaveResponse saveHistory(Long repositoryId, Long userId, HistorySaveRequest request) {

for (HistorySaveRequest.NodeDto dto : request.getNodes()) {
System.out.println(
"fileId=" + dto.getFileId() +
", fileName=" + dto.getFileName() +
", parentId=" + dto.getParentId() +
", path=" + dto.getPath()
);
}

// 1. 권한 체크 및 레포 조회
Repository repo = repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId)
.orElseThrow(() -> new GlobalException(ErrorCode.REPOSITORY_NOT_FOUND));

// 2. 요청에서 전체 fileId set 추출
Set<Long> requestFileIds = request.getNodes().stream()
.map(HistorySaveRequest.NodeDto::getFileId)
.collect(Collectors.toSet());

// 3. 현재 DB에 있는 FileNode 전체 조회 (삭제 대상 판별용)
// 2. 현재 DB의 파일/폴더 전체 조회
List<FileNode> dbNodes = fileNodeRepository.findAllByRepositoryId(repositoryId);

// 4. DB에 있지만 요청에 없는 FileNode 삭제 (자식 먼저 삭제)
List<FileNode> toDelete = dbNodes.stream()
.filter(node -> !requestFileIds.contains(node.getId()))
.sorted((a, b) -> b.getPath().length() - a.getPath().length())
.toList();
for (FileNode node : toDelete) {
if (node.getFileType() == FileType.FILE) {
fileContentRepository.deleteByFileNode(node);
}
fileNodeRepository.delete(node);
}

// 5. id → FileNode (DB에 이미 있던 것만)
Map<Long, FileNode> idToNode = dbNodes.stream()
.collect(Collectors.toMap(FileNode::getId, n -> n));

// 6. id → 요청 NodeDto
Map<Long, HistorySaveRequest.NodeDto> idToDto = request.getNodes().stream()
.collect(Collectors.toMap(HistorySaveRequest.NodeDto::getFileId, n -> n));

// 7. parent → 자식 순서로 재귀 저장
for (Long nodeId : idToDto.keySet()) {
saveNodeRecursive(nodeId, idToNode, idToDto, repo);
}

// 8. History & HistoryFile 기록 (스냅샷처럼)
History history = History.builder()
.repository(repo)
.message(request.getMessage())
.authorId(userId)
.createdAt(LocalDateTime.now())
.build();
history = historyRepository.save(history);

History finalHistory = history;
List<HistoryFile> historyFiles = request.getNodes().stream()
.map(dto -> HistoryFile.builder()
.history(finalHistory)
.fileId(dto.getFileId())
.fileName(dto.getFileName())
.fileType(dto.getFileType())
.parentId(dto.getParentId())
.path(dto.getPath())
.content(dto.getContent())
// 3. 파일 내용도 모두 조회 (FileContent와 Join, 또는 fileContentRepository 사용)
Map<Long, FileContent> nodeIdToContent = fileContentRepository.findAllByRepositoryId(repositoryId)
.stream().collect(Collectors.toMap(
c -> c.getFileNode().getId(), c -> c
));

// 4. History 생성
History history = historyRepository.save(
History.builder()
.repository(repo)
.message(request.getMessage())
.authorId(userId)
.createdAt(LocalDateTime.now())
.build()
);

// 5. HistoryFile (스냅샷) 기록
List<HistoryFile> historyFiles = dbNodes.stream()
.map(node -> HistoryFile.builder()
.history(history)
.fileId(node.getId())
.fileName(node.getName())
.fileType(node.getFileType().name())
.parentId(node.getParent() != null ? node.getParent().getId() : null)
.path(node.getPath())
.content(
node.getFileType() == FileType.FILE
? (nodeIdToContent.get(node.getId()) != null
? new String(nodeIdToContent.get(node.getId()).getContent(), StandardCharsets.UTF_8)
: null)
: null
)
.build()
).toList();

historyFileRepository.saveAll(historyFiles);

return HistorySaveResponse.builder()
.historyId(history.getId())
.build();
}

private void saveNodeRecursive(
Long clientFileId,
Map<Long, FileNode> idToNode,
Map<Long, HistorySaveRequest.NodeDto> idToDto,
Repository repo
) {
// 이미 저장된 노드는 무시
if (idToNode.containsKey(clientFileId)) return;

HistorySaveRequest.NodeDto dto = idToDto.get(clientFileId);

FileNode parent = null;
if (dto.getParentId() != null) {
saveNodeRecursive(dto.getParentId(), idToNode, idToDto, repo);
parent = idToNode.get(dto.getParentId());
}

FileNode fileNode = FileNode.builder()
.repository(repo)
.name(dto.getFileName())
.fileType(FileType.valueOf(dto.getFileType()))
.parent(parent)
.path(dto.getPath())
.build();
fileNode = fileNodeRepository.save(fileNode);

idToNode.put(clientFileId, fileNode);

if (fileNode.getFileType() == FileType.FILE) {
FileContent content = FileContent.builder()
.fileNode(fileNode)
.content(dto.getContent() == null ? new byte[0] : dto.getContent().getBytes(StandardCharsets.UTF_8))
.build();
fileContentRepository.save(content);
}
}

@Transactional(readOnly = true)
public HistoryDetailResponse getHistoryDetail(Long repositoryId, Long historyId, Long userId) {
// 1. 권한 체크 & 레포 확인
Expand Down
Original file line number Diff line number Diff line change
@@ -1,49 +1,38 @@
package com.deepdirect.deepwebide_be.member.controller;

import com.deepdirect.deepwebide_be.member.service.EmailVerificationService;
import com.deepdirect.deepwebide_be.member.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.view.RedirectView;

import java.util.HashMap;
import java.util.Map;

//@CrossOrigin(
// origins = {
// "http://localhost:5173",
// "https://www.deepdirect.site",
// "https://api.deepdirect.site"
// },
// allowCredentials = "true"
//)
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/auth/email")
@Tag(name = "EmailVerification", description = "이메일 인증 관련 API")
public class EmailVerificationController {

private final EmailVerificationService emailVerificationService;
private final UserService userService;

@Operation(
summary = "이메일 인증"
)
@GetMapping("/send-code")
public ResponseEntity<Map<String, Object>> verifyEmail(@RequestParam String code) {
public RedirectView verifyEmail(@RequestParam String code) {
boolean result = emailVerificationService.verifyEmailCode(code);
Map<String, Object> response = new HashMap<>();
String email = emailVerificationService.findVerifiedEmailByCode(code);

// TODO: 리다이랙트 넣기~
if (result) {
response.put("success", true);
response.put("message", "이메일 인증이 완료되었습니다.");
return ResponseEntity.ok(response);
} else {
response.put("success", false);
response.put("message", "인증 코드가 만료되었거나 유효하지 않습니다.");
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
userService.setEmailVerificationService(email);
}

RedirectView redirectView = new RedirectView();
redirectView.setUrl("https://www.deepdirect.site/sign-in");
return redirectView;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public String createVerification(String email) {
// 이메일 인증 요청 메일 발송
public void sendVerificationEmail(String email, String code) {
// String link = "http://localhost:8080/api/auth/email/send-code?code=" + code;
String link = "https://api.deepwebide.site/api/auth/email/send-code?code=" + code;
String link = "https://api.deepdirect.site/api/auth/email/send-code?code=" + code;

SimpleMailMessage message = new SimpleMailMessage();
message.setTo(email);
Expand All @@ -53,7 +53,6 @@ public void sendVerificationEmail(String email, String code) {
// 이메일 인증 코드 검증
public boolean verifyEmailCode(String code) {
return emailVerificationRepository.findByEmailCode(code)
.filter(verification -> !verification.isVerified()) // 기존 인증 여부 확인
.filter(verification ->
verification.getExpiresAt().isAfter(LocalDateTime.now())) // 만료 여부 확인
.map(verification -> {
Expand All @@ -64,4 +63,10 @@ public boolean verifyEmailCode(String code) {
})
.orElse(false);
}

public String findVerifiedEmailByCode(String code) {
return emailVerificationRepository.findByEmailCode(code)
.map(EmailVerification::getEmail)
.orElseThrow(() -> new IllegalArgumentException("해당 코드로 등록된 이메일이 없습니다."));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -266,4 +266,13 @@ public void verifyAndResetPassword(PasswordResetRequest request, String authoriz
userRepository.save(user);
reauthTokenService.delete(email);
}

@Transactional
public void setEmailVerificationService(String email) {
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new GlobalException(ErrorCode.USER_NOT_FOUND));

user.setEmailVerified(true);
userRepository.save(user);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@
public class NicknameGenerator {

private static final List<String> ADJECTIVES = List.of(
"슬기로운", "권태로운", "우는", "웃는", "기쁜", "화난", "차가운", "뜨거운", "평범한", "용감한", "행복한",
"느긋한", "조용한", "시끄러운", "상냥한", "무뚝뚝한", "겁쟁이인", "귀여운", "엉뚱한", "멍한", "똑똑한", "배고픈",
"침착한", "우울한", "광기어린", "수줍은", "분노한", "우아한", "애매한", "날카로운", "귀찮은", "엉망진창인",
"반짝이는", "무서운", "요란한", "재빠른", "정중한", "자신감 넘치는", "불안한", "느린", "심심한", "정신없는",
"끈질긴", "달콤한", "과묵한", "산만한", "진지한", "까칠한", "활기찬", "졸린", "자유로운", "호기심 많은", "고통스러운"
"겁쟁이인", "고상한", "고통스러운", "과묵한", "광기어린", "괴상한", "권태로운", "귀여운", "귀찮은", "기쁜",
"까칠한", "깨끗한", "끈질긴", "날카로운", "노련한", "느긋한", "느린", "달콤한", "따뜻한", "똑똑한",
"뜨거운", "멍한", "무뚝뚝한", "무서운", "반짝이는", "밝은", "배고픈", "부끄러운", "분노한", "불안한",
"빠른", "산만한", "상냥한", "상쾌한", "서투른", "수줍은", "쉬운", "슬기로운", "시끄러운", "신난",
"심란한", "심심한", "쓸쓸한", "애매한", "어두운", "어려운", "엉뚱한", "엉망진창인", "엉큼한", "여유로운",
"예민한", "요란한", "요망한", "용감한", "우는", "우아한", "우울한", "웃는", "의젓한", "자신감넘치는",
"자유로운", "작은", "장난스런", "재빠른", "정신없는", "정중한", "조급한", "조용한", "졸린", "즐거운",
"진지한", "차가운", "침착한", "큰", "평범한", "피곤한", "행복한", "호기심 많은", "홀가분한", "화난",
"활기찬", "활발한", "흥분한", "희망찬"
);

private static final Random RANDOM = new Random();
Expand Down