Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
89dc5b0
Merge: Staging서버 오픈 (#173)
lunarbae628 Oct 25, 2025
60e6974
Merge: SSL 만료로 보이던 문제 원인 분석 및 구조 개선
lunarbae628 Feb 3, 2026
71fa30f
Merge: 생성 로직 보상트랜잭션 도입 및 서비스 분리
lunarbae628 Feb 28, 2026
d110f7a
Fix: application-prod.yml cors 리스트 오타 수정
lunarbae628 Feb 28, 2026
4def1a1
Merge: application-prod.yml cors 리스트 오타 수정
lunarbae628 Feb 28, 2026
51c3f51
Merge: Mongo delete outbox 전환 및 생성 보상 구조 정리
lunarbae628 Apr 1, 2026
8b8e785
Revise team member details and responsibilities
lunarbae628 Apr 1, 2026
3dd6f10
refactor: 커밋시 저장 삭제 안하게 수정
lunarbae628 Apr 6, 2026
b7c440f
refactor: 이어서 작업하기를 브랜치 생성 흐름으로 단순화
lunarbae628 Apr 7, 2026
fa3812e
test: 이어서 작업하기를 브랜치 생성 흐름으로 단순화한 것 테스트 + 정합성 통합 테스트 추가
lunarbae628 Apr 7, 2026
9ac5425
refactor: BlockDto 삭제
lunarbae628 Apr 7, 2026
c37e06e
docs: CreateBranch 스웨거로 정리
lunarbae628 Apr 7, 2026
ef1d0bf
refactor: 머지를 브랜치/작업장 생성 흐름으로 분리하고 관련 스웨거 및 테스트 정리
lunarbae628 Apr 7, 2026
c185024
chore: 미사용 import 정리
lunarbae628 Apr 7, 2026
ccc9636
chore: 노필요 주석 제거
lunarbae628 Apr 7, 2026
d67eaa8
test: doc / branch / merge / commit 예외 커스텀 예외 변환 테스트 적용
lunarbae628 Apr 8, 2026
7a1ca50
delete: compare api 삭제
lunarbae628 Apr 8, 2026
9600413
feat: 브랜치 mergeTargetCommit 필드를 추가하고 그래프 조회/머지 흐름/테스트를 함께 정리
lunarbae628 Apr 8, 2026
5664a00
feat: merge target commit 메타와 graph/swagger 스펙 추가
lunarbae628 Apr 8, 2026
11d2b78
feat: rootCommit도 삭제가능하도록 변경(MergeTargeCommit 삭제 불가 검사 추가)
lunarbae628 Apr 11, 2026
d7eec93
chore: BranchCreateContext 패키지 이동
lunarbae628 Apr 12, 2026
38afac1
refactor: 저장 삭제 API 제거 및 Save 제약 강화
lunarbae628 Apr 12, 2026
596048d
test: save없는 조건의 테스트 제거
lunarbae628 Apr 12, 2026
ce2a69b
docs: artifact version 1.0.0
lunarbae628 Apr 12, 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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Docsa는 문서의 변경 사항을 기록(commit) 단위로 추적하고, 버


## 👩‍💻 팀 이장님 소개
<table align="center"> <thead> <tr> <th style="text-align:center;">팀원</th> <th style="text-align:center;">역할</th> <th style="text-align:left;">담당 업무</th> </tr> </thead> <tbody> <tr> <td align="center"> <a href="https://github.com/sleepyhoon"> <img src="https://avatars.githubusercontent.com/u/101882530?v=4" width="60"><br/> <sub><b>한승훈</b></sub> </a> </td> <td align="center"><b>PO</b></td> <td> - 프론트 개발자님과 소통<br> - 프로젝트 일정 관리<br> - 저장 관련 API 구현 </td> </tr> <tr> <td align="center"> <a href="https://github.com/heets-blue"> <img src="https://avatars.githubusercontent.com/u/89324994?v=4" width="60"><br/> <sub><b>배문성</b></sub> </a> </td> <td align="center"><b>BE 팀장</b></td> <td> - 문서 관련 API 구현<br> - 이종간 트랜잭션 삭제 로직 설계 및 구현 <br> - CI/CD 및 인프라 구축</td> </tr> <tr> <td align="center"> <a href="https://github.com/Jeongmin39"> <img src="https://avatars.githubusercontent.com/u/80705450?v=4" width="60"><br/> <sub><b>한정민</b></sub> </a> </td> <td align="center"><b>AWS 관리자</b></td> <td> - 인증 및 사용자 관련 API 구현<br> - AWS 인프라 운영<br> - Docker 기반 배포<br> - 모니터링 시스템 구축 </td> </tr> <tr> <td align="center"> <a href="https://github.com/2ternal"> <img src="https://avatars.githubusercontent.com/u/26919446?v=4" width="60"><br/> <sub><b>권우철</b></sub> </a> </td> <td align="center"><b>BE 팀원</b></td> <td> - 기록(커밋) 관련 API 구현<br> - 병합기능(머지) API 구현<br> - 이종간 트랜잭션 삭제 로직 설계 </td> </tr> <tr> <td align="center"> <a href="https://github.com/ky1nonly"> <img src="https://avatars.githubusercontent.com/u/117032989?v=4" width="60"><br/> <sub><b>이예원</b></sub> </a> </td> <td align="center"><b>BE 팀원</b></td> <td> - 버전(브랜치) 관련 API 구현<br> - 그래프 조회 API 구현 </td> </tr> </tbody> </table>
<table align="center"> <thead> <tr> <th style="text-align:center;">팀원</th> <th style="text-align:center;">역할</th> <th style="text-align:left;">담당 업무</th> </tr> </thead> <tbody> <tr> <td align="center"> <a href="https://github.com/sleepyhoon"> <img src="https://avatars.githubusercontent.com/u/101882530?v=4" width="60"><br/> <sub><b>한승훈</b></sub> </a> </td> <td align="center"><b>PO</b></td> <td> - 프론트 개발자님과 소통<br> - 프로젝트 일정 관리<br> - 저장 관련 API 구현 </td> </tr> <tr> <td align="center"> <a href="https://github.com/heets-blue"> <img src="https://avatars.githubusercontent.com/u/89324994?v=4" width="60"><br/> <sub><b>배문성</b></sub> </a> </td> <td align="center"><b>BE 팀장</b></td> <td> - 문서 관련 API 구현<br> - Outbox 기반 이종간 정합성 삭제/보상 구조 설계 및 생성 경로 Orchestrator 패턴 리펙토링 <br> - CI/CD 및 인프라 구축</td> </tr> <tr> <td align="center"> <a href="https://github.com/Jeongmin39"> <img src="https://avatars.githubusercontent.com/u/80705450?v=4" width="60"><br/> <sub><b>한정민</b></sub> </a> </td> <td align="center"><b>AWS 관리자</b></td> <td> - 인증 및 사용자 관련 API 구현<br> - AWS 인프라 운영<br> - Docker 기반 배포<br> - 모니터링 시스템 구축 </td> </tr> <tr> <td align="center"> <a href="https://github.com/2ternal"> <img src="https://avatars.githubusercontent.com/u/26919446?v=4" width="60"><br/> <sub><b>권우철</b></sub> </a> </td> <td align="center"><b>BE 팀원</b></td> <td> - 기록(커밋) 관련 API 구현<br> - 병합기능(머지) API 구현<br> - 이종간 트랜잭션 삭제 로직 설계 </td> </tr> <tr> <td align="center"> <a href="https://github.com/ky1nonly"> <img src="https://avatars.githubusercontent.com/u/117032989?v=4" width="60"><br/> <sub><b>이예원</b></sub> </a> </td> <td align="center"><b>BE 팀원</b></td> <td> - 버전(브랜치) 관련 API 구현<br> - 그래프 조회 API 구현 </td> </tr> </tbody> </table>

<div align="center">

Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ plugins {
}

group = 'io.EJangs'
version = '0.0.1'
version = '1.0.0'

java {
toolchain {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

import io.ejangs.docsa.domain.block.dao.mongodb.BlockRepository;
import io.ejangs.docsa.domain.block.document.Block;
import io.ejangs.docsa.domain.block.dto.response.BlockDto;
import io.ejangs.docsa.domain.block.util.BlockMapper;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

Expand All @@ -14,8 +14,8 @@ public class BlockService {

private final BlockRepository blockRepository;

public List<Block> saveBlocks(List<BlockDto> blockDtos) {
List<Block> blocks = BlockMapper.toDocument(blockDtos);
public List<Block> saveBlocks(List<Map<String, Object>> blockDataList) {
List<Block> blocks = BlockMapper.toDocument(blockDataList);
return blockRepository.saveAll(blocks);
}

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
package io.ejangs.docsa.domain.block.util;

import io.ejangs.docsa.domain.block.document.Block;
import io.ejangs.docsa.domain.block.dto.response.BlockDto;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class BlockMapper {

public static Block toDocument(BlockDto block) {
public static Block toDocument(Map<String, Object> block) {
return Block.builder()
.content(block.data())
.content(block)
.build();
}

public static List<Block> toDocument(List<BlockDto> blocks) {
public static List<Block> toDocument(List<Map<String, Object>> blocks) {
return blocks.stream()
.map(BlockMapper::toDocument)
.collect(Collectors.toList());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import io.ejangs.docsa.domain.branch.dto.request.BranchRenameRequest;
import io.ejangs.docsa.domain.branch.dto.response.BranchCreateResponse;
import io.ejangs.docsa.domain.branch.dto.response.BranchRenameResponse;
import io.ejangs.docsa.domain.branch.swagger.CreateBranchOrSaveDocs;
import io.ejangs.docsa.domain.branch.swagger.CreateBranchDocs;
import io.ejangs.docsa.domain.branch.swagger.DeleteBranchDocs;
import io.ejangs.docsa.domain.branch.swagger.RenameBranchDocs;
import io.ejangs.docsa.domain.user.security.CustomUserDetails;
Expand All @@ -25,17 +25,15 @@ public class BranchController {

private final BranchService branchService;

// 브랜치에 이어서 새로운 저장 생성 or 새로운 브랜치 + 저장 생성
@PostMapping
@CreateBranchOrSaveDocs
public ResponseEntity<BranchCreateResponse> createBranchOrSave(
@CreateBranchDocs
public ResponseEntity<BranchCreateResponse> createBranch(
@AuthenticationPrincipal CustomUserDetails userDetails,
@PathVariable Long documentId, @Valid @RequestBody BranchCreateRequest request) {
return ResponseEntity.status(HttpStatus.CREATED)
.body(branchService.createBranchOrSave(documentId, request, userDetails.getId()));
.body(branchService.createBranch(documentId, request, userDetails.getId()));
}

// 브랜치 이름 수정
@PatchMapping("/{branchId}")
@RenameBranchDocs
public ResponseEntity<BranchRenameResponse> renameBranch(
Expand All @@ -47,7 +45,6 @@ public ResponseEntity<BranchRenameResponse> renameBranch(
userDetails.getId()));
}

// 브랜치 삭제
@DeleteMapping("/{branchId}")
@DeleteBranchDocs
public ResponseEntity<Void> deleteBranch(@AuthenticationPrincipal CustomUserDetails userDetails,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ public void checkBranchInDocOwnedByUser(Long documentId, Long branchId, Long use
}
}

public boolean checkFromOrRootCommitInBranch(Commit commit) {
return branchRepository.existsByRootCommitIdOrFromCommitId(commit.getId());
public boolean checkFromCommitOrMergeCommitInBranch(Commit commit) {
return branchRepository.existsByFromOrMergeTargetCommitId(commit.getId());
}

public void checkDuplicatedWithBranchName(Long docId, String branchName) {
Expand Down
30 changes: 5 additions & 25 deletions src/main/java/io/ejangs/docsa/domain/branch/app/BranchService.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package io.ejangs.docsa.domain.branch.app;

import io.ejangs.docsa.domain.branch.app.create.BranchCreateContext;
import io.ejangs.docsa.domain.branch.dto.BranchCreateContext;
import io.ejangs.docsa.domain.branch.app.create.BranchCreateOrchestrator;
import io.ejangs.docsa.domain.branch.dto.request.BranchCreateRequest;
import io.ejangs.docsa.domain.branch.dto.response.BranchCreateResponse;
Expand Down Expand Up @@ -45,18 +45,12 @@ public class BranchService {
private final BranchCreateOrchestrator branchCreateOrchestrator;
private final MongoDeleteOutboxFactory mongoDeleteOutboxFactory;

/**
* '이어서 작업하기' 로직으로, 브랜치를 생성하고 저장을 추가하거나 기존 브랜치에 저장을 추가합니다.
* <p>
* fromCommitId가 존재하면 기존 커밋에서 브랜치를 만들거나 저장(save)을 추가하는 상황입니다. fromCommitId가 null이면 최초 브랜치 생성으로,
* 이 경우는 doc 도메인에서 처리합니다.
*/
public BranchCreateResponse createBranchOrSave(Long documentId, BranchCreateRequest request,
public BranchCreateResponse createBranch(Long documentId, BranchCreateRequest request,
Long userId) {

BranchCreateContext context = prepareBranchCreateContext(documentId, request, userId);

return branchCreateOrchestrator.createBranchOrSave(context);
return branchCreateOrchestrator.create(context);
}

private BranchCreateContext prepareBranchCreateContext(Long documentId,
Expand All @@ -72,29 +66,15 @@ private BranchCreateContext prepareBranchCreateContext(Long documentId,
throw new CustomException(DocErrorCode.COMMIT_NOT_IN_DOCUMENT);
}

boolean isLeaf = fromBranch.getLeafCommit() != null
&& fromBranch.getLeafCommit().getId().equals(fromCommitId);
branchQueryService.checkDuplicatedWithBranchName(documentId, request.name());

boolean isRoot = fromBranch.getRootCommit() != null
&& fromBranch.getRootCommit().getId().equals(fromCommitId);
boolean hasSave = fromBranch.getSave() != null;

boolean createNewBranch =
!isLeaf || !fromBranch.getName().equals(request.name()) || (isLeaf && isRoot
&& hasSave);

if (createNewBranch) {
branchQueryService.checkDuplicatedWithBranchName(documentId, request.name());
}

return new BranchCreateContext(
fromBranch.getDoc(),
fromBranch,
fromCommit,
request.name(),
fromCommit.getCommitMongoId(),
isLeaf,
createNewBranch
fromCommit.getCommitMongoId()
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.ejangs.docsa.domain.branch.app.create;

import io.ejangs.docsa.domain.branch.app.BranchQueryService;
import io.ejangs.docsa.domain.branch.dto.BranchCreateContext;
import io.ejangs.docsa.domain.branch.dto.response.BranchCreateResponse;
import io.ejangs.docsa.domain.branch.entity.Branch;
import io.ejangs.docsa.domain.branch.util.BranchMapper;
Expand All @@ -19,16 +20,13 @@ public class BranchCreateMySqlTxService {
private final SaveQueryService saveQueryService;

@Transactional(rollbackFor = Exception.class)
public BranchCreateResponse createBranchOrSave(BranchCreateContext context, String saveContentId) {
Branch targetBranch = context.fromBranch();
public BranchCreateResponse createMySqlPart(BranchCreateContext context, String saveContentId) {

if (context.createNewBranch()) {
targetBranch = branchQueryService.createBranch(context.doc(), context.branchName(), context.fromCommit());
}
Branch newBranch = branchQueryService.createBranch(context.doc(), context.branchName(), context.fromCommit());

Save save = saveQueryService.createSave(targetBranch, saveContentId);
Save save = saveQueryService.createSave(newBranch, saveContentId);
RenewUpdatedAtHelper.touch(save);

return BranchMapper.toBranchCreateResponse(targetBranch, save);
return BranchMapper.toBranchCreateResponse(newBranch, save);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package io.ejangs.docsa.domain.branch.app.create;

import io.ejangs.docsa.domain.branch.dto.BranchCreateContext;
import io.ejangs.docsa.domain.branch.dto.response.BranchCreateResponse;
import io.ejangs.docsa.global.exception.CustomException;
import io.ejangs.docsa.global.exception.errorcode.BranchErrorCode;
import io.ejangs.docsa.global.mongo.outbox.dto.MongoIdsDto;
import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.DomainType;
import io.ejangs.docsa.global.mongo.outbox.entity.MongoDeleteOutbox.OriginType;
Expand All @@ -20,12 +23,12 @@ public class BranchCreateOrchestrator {
private final BranchCreateMySqlTxService branchCreateMySqlTxService;
private final MongoDeleteOutboxFactory mongoDeleteOutboxFactory;

public BranchCreateResponse createBranchOrSave(BranchCreateContext context) {
public BranchCreateResponse create(BranchCreateContext context) {
String saveContentId = branchCreateMongoTxService.createSaveContentFromCommit(
context.fromCommitMongoId());

try {
return branchCreateMySqlTxService.createBranchOrSave(context, saveContentId);
return branchCreateMySqlTxService.createMySqlPart(context, saveContentId);
} catch (Exception e) {
log.warn("[SAGA] 브랜치/저장 생성 실패 -> Mongo 삭제 Outbox 기록.", e);
MongoIdsDto compensateTarget = new MongoIdsDto(List.of(saveContentId), null, null);
Expand All @@ -36,7 +39,7 @@ public BranchCreateResponse createBranchOrSave(BranchCreateContext context) {
saveContentId,
compensateTarget
);
throw e;
throw new CustomException(BranchErrorCode.FAIL_CREATE_BRANCH);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public interface BranchRepository extends JpaRepository<Branch, Long> {
b.name,
b.createdAt,
b.fromCommit.id,
b.mergeTargetCommit.id,
b.rootCommit.id,
b.leafCommit.id,
(
Expand All @@ -34,11 +35,11 @@ public interface BranchRepository extends JpaRepository<Branch, Long> {
@Query("""
SELECT CASE WHEN EXISTS (
SELECT 1 FROM Branch b
WHERE b.rootCommit.id = :commitId OR b.fromCommit.id = :commitId
WHERE b.fromCommit.id = :commitId
OR b.mergeTargetCommit.id = :commitId
) THEN true ELSE false END
""")
boolean existsByRootCommitIdOrFromCommitId(@Param("commitId") Long commitId);
boolean existsByFromOrMergeTargetCommitId(@Param("commitId") Long commitId);

boolean existsByDocIdAndName(Long docId, String name);
}

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.ejangs.docsa.domain.branch.app.create;
package io.ejangs.docsa.domain.branch.dto;

import io.ejangs.docsa.domain.branch.entity.Branch;
import io.ejangs.docsa.domain.commit.entity.Commit;
Expand All @@ -9,9 +9,7 @@ public record BranchCreateContext(
Branch fromBranch,
Commit fromCommit,
String branchName,
String fromCommitMongoId,
boolean leafCommit,
boolean createNewBranch
String fromCommitMongoId
) {

}
23 changes: 20 additions & 3 deletions src/main/java/io/ejangs/docsa/domain/branch/entity/Branch.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ public class Branch extends BaseEntity {
@JoinColumn(name = "leaf_commit_id")
private Commit leafCommit;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "merge_target_commit_id")
private Commit mergeTargetCommit;

@OneToMany(mappedBy = "branch", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
private List<Commit> commits;

Expand All @@ -72,12 +76,16 @@ public void updateLeafCommit(Commit leafCommit) {
this.leafCommit = leafCommit;
}

public void initializeRootCommitIfNull(Commit commit) {
public void updateRootCommit(Commit commit) {
if (this.rootCommit == null) {
this.rootCommit = commit;
}
}

public void updateMergeTargetCommit(Commit mergeTargetCommit) {
this.mergeTargetCommit = mergeTargetCommit;
}

public void setSave(Save save) {
this.save = save;
if (save.getBranch() != this) {
Expand Down Expand Up @@ -109,7 +117,16 @@ public void removeCommit(Commit commit) {
this.commits.remove(commit);
}

public void removeSave() {
this.save = null;
public void detachRootCommit(Commit commit) {
if (this.rootCommit != null && this.rootCommit.getId().equals(commit.getId())) {
this.rootCommit = null;
}

if (this.leafCommit != null && this.leafCommit.getId().equals(commit.getId())) {
this.leafCommit = null;
}

removeCommit(commit);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package io.ejangs.docsa.domain.branch.merge.api;

import io.ejangs.docsa.domain.branch.merge.app.MergeService;
import io.ejangs.docsa.domain.branch.merge.dto.request.MergeRequest;
import io.ejangs.docsa.domain.branch.merge.dto.response.MergeResponse;
import io.ejangs.docsa.domain.branch.merge.swagger.MergeDocs;
import io.ejangs.docsa.domain.user.security.CustomUserDetails;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.tags.Tags;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
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.RestController;

@RestController
@RequiredArgsConstructor
@Tags({
@Tag(name = "Merge API")
})
public class MergeController {

private final MergeService mergeService;

@PostMapping("/api/document/{docId}/merge")
@MergeDocs
public ResponseEntity<MergeResponse> merge(
@AuthenticationPrincipal CustomUserDetails userDetails,
@PathVariable("docId") Long docId,
@RequestBody @Valid MergeRequest mergeRequest) {

return ResponseEntity.status(HttpStatus.CREATED)
.body(mergeService.merge(docId, mergeRequest, userDetails.getId()));
}
}
Loading
Loading