Skip to content

Commit d995f0f

Browse files
authored
Merge pull request #75 from prgrms-web-devcourse-final-project/node/8
[REFACTOR]: 기존 라인단위 관리에서 dvcs관리 형태로 로직 변경
2 parents 9915f46 + 6ff3381 commit d995f0f

File tree

55 files changed

+3879
-581
lines changed

Some content is hidden

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

55 files changed

+3879
-581
lines changed

back/src/main/java/com/back/domain/node/controller/BaseLineController.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@
1313
import com.back.domain.node.dto.base.BaseLineDto;
1414
import com.back.domain.node.dto.base.BaseNodeDto;
1515
import com.back.domain.node.service.NodeService;
16+
import com.back.global.exception.ApiException;
17+
import com.back.global.exception.ErrorCode;
1618
import com.back.global.security.CustomUserDetails;
19+
import jakarta.validation.Valid;
1720
import lombok.RequiredArgsConstructor;
1821
import org.springframework.http.HttpStatus;
1922
import org.springframework.http.ResponseEntity;
@@ -31,7 +34,9 @@ public class BaseLineController {
3134

3235
// 라인 단위 일괄 생성(헤더~꼬리까지)
3336
@PostMapping("/bulk")
34-
public ResponseEntity<BaseLineBulkCreateResponse> createBaseLineBulk(@RequestBody BaseLineBulkCreateRequest request) {
37+
public ResponseEntity<BaseLineBulkCreateResponse> createBaseLineBulk(@AuthenticationPrincipal CustomUserDetails me,
38+
@Valid @RequestBody BaseLineBulkCreateRequest request) {
39+
if (me == null) throw new ApiException(ErrorCode.HANDLE_ACCESS_DENIED, "login required");
3540
return ResponseEntity.status(HttpStatus.CREATED).body(nodeService.createBaseLineWithNodes(request));
3641
}
3742

@@ -65,6 +70,8 @@ public ResponseEntity<TreeDto> getTreeForBaseLine(@PathVariable Long baseLineId)
6570
public ResponseEntity<List<BaseLineDto>> getMyBaseLines(
6671
@AuthenticationPrincipal CustomUserDetails me
6772
) {
73+
if (me == null) throw new ApiException(ErrorCode.HANDLE_ACCESS_DENIED, "login required");
74+
6875
List<BaseLineDto> list = nodeService.getMyBaseLines(me.getId());
6976
return ResponseEntity.ok(list);
7077
}

back/src/main/java/com/back/domain/node/controller/DecisionFlowController.java

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,14 @@
99

1010
import com.back.domain.node.dto.decision.*;
1111
import com.back.domain.node.service.NodeService;
12+
import com.back.global.exception.ApiException;
13+
import com.back.global.exception.ErrorCode;
14+
import com.back.global.security.CustomUserDetails;
15+
import jakarta.validation.Valid;
1216
import lombok.RequiredArgsConstructor;
1317
import org.springframework.http.HttpStatus;
1418
import org.springframework.http.ResponseEntity;
19+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
1520
import org.springframework.web.bind.annotation.*;
1621

1722
@RestController
@@ -23,31 +28,41 @@ public class DecisionFlowController {
2328

2429
// from-base: 라인+피벗(순번/나이)+분기슬롯 인덱스만 받아 서버가 pivotBaseNodeId를 해석
2530
@PostMapping("/from-base")
26-
public ResponseEntity<DecNodeDto> createFromBase(@RequestBody DecisionNodeFromBaseRequest request) {
31+
public ResponseEntity<DecNodeDto> createFromBase(@Valid @RequestBody DecisionNodeFromBaseRequest request,
32+
@AuthenticationPrincipal CustomUserDetails me) {
33+
if (me == null) throw new ApiException(ErrorCode.HANDLE_ACCESS_DENIED, "login required");
2734
return ResponseEntity.status(HttpStatus.CREATED).body(nodeService.createDecisionNodeFromBase(request));
2835
}
2936

3037
// next: 부모 결정 id만 받아 서버가 라인/다음 나이/베이스 매칭을 해석
3138
@PostMapping("/next")
32-
public ResponseEntity<DecNodeDto> createNext(@RequestBody DecisionNodeNextRequest request) {
39+
public ResponseEntity<DecNodeDto> createNext(@AuthenticationPrincipal CustomUserDetails me,
40+
@RequestBody DecisionNodeNextRequest request) {
41+
if (me == null) throw new ApiException(ErrorCode.HANDLE_ACCESS_DENIED, "login required");
3342
return ResponseEntity.status(HttpStatus.CREATED).body(nodeService.createDecisionNodeNext(request));
3443
}
3544

3645
// 라인 취소
3746
@PostMapping("/{decisionLineId}/cancel")
38-
public ResponseEntity<DecisionLineLifecycleDto> cancel(@PathVariable Long decisionLineId) {
47+
public ResponseEntity<DecisionLineLifecycleDto> cancel(@AuthenticationPrincipal CustomUserDetails me,
48+
@PathVariable Long decisionLineId) {
49+
if (me == null) throw new ApiException(ErrorCode.HANDLE_ACCESS_DENIED, "login required");
3950
return ResponseEntity.ok(nodeService.cancelDecisionLine(decisionLineId));
4051
}
4152

4253
// 라인 완료
4354
@PostMapping("/{decisionLineId}/complete")
44-
public ResponseEntity<DecisionLineLifecycleDto> complete(@PathVariable Long decisionLineId) {
55+
public ResponseEntity<DecisionLineLifecycleDto> complete(@AuthenticationPrincipal CustomUserDetails me,
56+
@PathVariable Long decisionLineId) {
57+
if (me == null) throw new ApiException(ErrorCode.HANDLE_ACCESS_DENIED, "login required");
4558
return ResponseEntity.ok(nodeService.completeDecisionLine(decisionLineId));
4659
}
4760

4861
// 분기점까지의 라인 복제(포크) 및 새로운 라인 생성
4962
@PostMapping("/fork")
50-
public ResponseEntity<DecNodeDto> fork(@RequestBody ForkFromDecisionRequest request) {
63+
public ResponseEntity<DecNodeDto> fork(@AuthenticationPrincipal CustomUserDetails me,
64+
@RequestBody ForkFromDecisionRequest request) {
65+
if (me == null) throw new ApiException(ErrorCode.HANDLE_ACCESS_DENIED, "login required");
5166
return ResponseEntity.status(HttpStatus.CREATED).body(nodeService.forkFromDecision(request));
5267
}
5368
}

back/src/main/java/com/back/domain/node/controller/DecisionLineController.java

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,16 @@
88
import com.back.domain.node.dto.decision.DecisionLineDetailDto;
99
import com.back.domain.node.dto.decision.DecisionLineListDto;
1010
import com.back.domain.node.service.NodeQueryService;
11+
import com.back.global.exception.ApiException;
12+
import com.back.global.exception.ErrorCode;
13+
import com.back.global.security.CustomUserDetails;
1114
import lombok.RequiredArgsConstructor;
1215
import org.springframework.http.ResponseEntity;
13-
import org.springframework.web.bind.annotation.*;
16+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
17+
import org.springframework.web.bind.annotation.GetMapping;
18+
import org.springframework.web.bind.annotation.PathVariable;
19+
import org.springframework.web.bind.annotation.RequestMapping;
20+
import org.springframework.web.bind.annotation.RestController;
1421

1522
@RestController
1623
@RequestMapping("/api/v1/decision-lines")
@@ -19,10 +26,11 @@ public class DecisionLineController {
1926

2027
private final NodeQueryService nodeQueryService;
2128

22-
// 가장 중요한: 사용자별 결정 라인 목록(요약)
29+
// 가장 중요한: 인증 사용자별 결정 라인 목록(요약)
2330
@GetMapping
24-
public ResponseEntity<DecisionLineListDto> list(@RequestParam Long userId) {
25-
return ResponseEntity.ok(nodeQueryService.getDecisionLines(userId));
31+
public ResponseEntity<DecisionLineListDto> list(@AuthenticationPrincipal CustomUserDetails me) {
32+
if (me == null) throw new ApiException(ErrorCode.HANDLE_ACCESS_DENIED, "login required");
33+
return ResponseEntity.ok(nodeQueryService.getDecisionLines(me.getId()));
2634
}
2735

2836
// 가장 많이 사용하는: 특정 결정 라인 상세
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/**
2+
* [API] DVCS 하이브리드 컨트롤러
3+
* - 베이스 편집(커밋/패치), 결정 편집(오버라이드/승격), 팔로우 정책 전환, 브랜치 선택, 브랜치/커밋 조회
4+
* - 규칙: Controller → Service → Repository (Controller는 얇게 유지)
5+
*/
6+
package com.back.domain.node.controller;
7+
8+
import com.back.domain.node.dto.dvcs.*;
9+
import com.back.domain.node.service.DvcsFacadeService;
10+
import com.back.global.exception.ApiException;
11+
import com.back.global.exception.ErrorCode;
12+
import com.back.global.security.CustomUserDetails;
13+
import lombok.RequiredArgsConstructor;
14+
import org.springframework.http.ResponseEntity;
15+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
16+
import org.springframework.web.bind.annotation.*;
17+
18+
import java.util.List;
19+
20+
@RestController
21+
@RequestMapping("/api/v1/dvcs")
22+
@RequiredArgsConstructor
23+
public class DvcsController {
24+
25+
private final DvcsFacadeService dvcs; // ★ 파사드 서비스
26+
27+
// 베이스 편집을 브랜치 커밋으로 반영
28+
@PostMapping("/base/edit")
29+
public ResponseEntity<EditAcknowledgeDto> editBase(@AuthenticationPrincipal CustomUserDetails me,
30+
@RequestBody BaseEditRequest req) {
31+
if (me == null) throw new ApiException(ErrorCode.HANDLE_ACCESS_DENIED, "login required");
32+
33+
return ResponseEntity.ok(dvcs.editBase(me.getId(), req));
34+
}
35+
36+
// 결정 노드 편집(OVERRIDE 또는 업스트림 승격)
37+
@PostMapping("/decision/edit")
38+
public ResponseEntity<EditAcknowledgeDto> editDecision(@AuthenticationPrincipal CustomUserDetails me,
39+
@RequestBody DecisionEditRequest req) {
40+
if (me == null) throw new ApiException(ErrorCode.HANDLE_ACCESS_DENIED, "login required");
41+
42+
return ResponseEntity.ok(dvcs.editDecision(me.getId(), req));
43+
}
44+
45+
// 팔로우 정책 전환(FOLLOW/PINNED/OVERRIDE) 및 핀 커밋 지정
46+
@PostMapping("/decision/policy")
47+
public ResponseEntity<EditAcknowledgeDto> changePolicy(@AuthenticationPrincipal CustomUserDetails me,
48+
@RequestBody FollowPolicyChangeRequest req) {
49+
if (me == null) throw new ApiException(ErrorCode.HANDLE_ACCESS_DENIED, "login required");
50+
51+
return ResponseEntity.ok(dvcs.changePolicy(me.getId(), req));
52+
}
53+
54+
// 브랜치 생성 또는 기존 브랜치 선택 후 라인에 적용
55+
@PostMapping("/branch/select")
56+
public ResponseEntity<EditAcknowledgeDto> selectBranch(@AuthenticationPrincipal CustomUserDetails me,
57+
@RequestBody BranchSelectRequest req) {
58+
if (me == null) throw new ApiException(ErrorCode.HANDLE_ACCESS_DENIED, "login required");
59+
60+
return ResponseEntity.ok(dvcs.selectBranch(me.getId(), req));
61+
}
62+
63+
// 특정 베이스라인의 브랜치/커밋 요약 조회
64+
@GetMapping("/branches/{baseLineId}")
65+
public ResponseEntity<List<BranchSummaryDto>> listBranches(@PathVariable Long baseLineId) {
66+
return ResponseEntity.ok(dvcs.listBranches(baseLineId));
67+
}
68+
}

back/src/main/java/com/back/domain/node/dto/base/BaseLineBulkCreateRequest.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,23 @@
55
package com.back.domain.node.dto.base;
66

77
import com.back.domain.node.entity.NodeCategory;
8+
import jakarta.validation.Valid;
9+
import jakarta.validation.constraints.Max;
10+
import jakarta.validation.constraints.Min;
11+
812
import java.util.List;
913

1014
public record BaseLineBulkCreateRequest(
1115
Long userId,
1216
String title,
13-
List<BaseNodePayload> nodes
17+
List<@Valid BaseNodePayload> nodes
1418
) {
1519
public record BaseNodePayload(
1620
NodeCategory category,
1721
String situation,
1822
String decision,
23+
@Min(1)
24+
@Max(120)
1925
Integer ageYear,
2026
String description
2127
) {}

back/src/main/java/com/back/domain/node/dto/base/BaseNodeDto.java

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
/**
2-
* [DTO-RES] BaseNode 응답
3-
* - 고정 선택과 분기 2칸 및 각 타겟 링크를 포함한다
2+
* [DTO-RES] BaseNode 응답(보강)
3+
* - currentVersionId와 effective* 필드를 추가해 버전 해석 결과를 노출
44
*/
55
package com.back.domain.node.dto.base;
66

77
import com.back.domain.node.entity.NodeCategory;
8+
import java.util.List;
89

910
public record BaseNodeDto(
1011
Long id,
@@ -22,5 +23,13 @@ public record BaseNodeDto(
2223
String altOpt2,
2324
Long altOpt1TargetDecisionId,
2425
Long altOpt2TargetDecisionId,
25-
String description
26+
String description,
27+
28+
// 버전/해석 결과
29+
Long currentVersionId,
30+
NodeCategory effectiveCategory,
31+
String effectiveSituation,
32+
String effectiveDecision,
33+
List<String> effectiveOptions,
34+
String effectiveDescription
2635
) {}

back/src/main/java/com/back/domain/node/dto/decision/DecNodeDto.java

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
/**
2-
* [DTO-RES] DecisionNode 응답
3-
* - options/selectedIndex/parentOptionIndex를 포함해 프론트 렌더 정보를 제공한다
2+
* [DTO-RES] DecisionNode 응답(보강)
3+
* - followPolicy/pinnedCommitId/virtual 플래그를 추가해 해석 상태를 노출
4+
* - effective* 필드는 Resolver가 계산한 최종 표현값(렌더 우선 사용)
45
*/
56
package com.back.domain.node.dto.decision;
67

8+
import com.back.domain.node.entity.FollowPolicy;
79
import com.back.domain.node.entity.NodeCategory;
810
import java.util.List;
911

@@ -24,5 +26,43 @@ public record DecNodeDto(
2426
Integer parentOptionIndex,
2527
String description,
2628
String aiNextSituation,
27-
String aiNextRecommendedOption
28-
) {}
29+
String aiNextRecommendedOption,
30+
31+
// 정책/핀/가상
32+
FollowPolicy followPolicy,
33+
Long pinnedCommitId,
34+
Boolean virtual,
35+
36+
// 해석 결과(렌더 우선 사용; null이면 상단 원본 사용)
37+
NodeCategory effectiveCategory,
38+
String effectiveSituation,
39+
String effectiveDecision,
40+
List<String> effectiveOptions,
41+
String effectiveDescription,
42+
43+
List<Long> childrenIds, // 이 DECISION의 자식들 id(시간순)
44+
Boolean root, // 라인 헤더면 true
45+
Long pivotLinkBaseNodeId, // 베이스 분기 슬롯에서 올라온 첫 노드면 해당 BaseNode id
46+
Integer pivotSlotIndex // 0/1 (분기 슬롯 인덱스), 아니면 null
47+
) {
48+
// === 호환 오버로드(기존 서비스 호출 유지) ===
49+
public DecNodeDto(
50+
Long id, Long userId, String type, NodeCategory category,
51+
String situation, String decision, Integer ageYear,
52+
Long decisionLineId, Long parentId, Long baseNodeId,
53+
String background, List<String> options, Integer selectedIndex,
54+
Integer parentOptionIndex, String description,
55+
String aiNextSituation, String aiNextRecommendedOption,
56+
FollowPolicy followPolicy, Long pinnedCommitId, Boolean virtual,
57+
NodeCategory effectiveCategory, String effectiveSituation, String effectiveDecision,
58+
List<String> effectiveOptions, String effectiveDescription
59+
) {
60+
this(id, userId, type, category, situation, decision, ageYear,
61+
decisionLineId, parentId, baseNodeId, background, options, selectedIndex,
62+
parentOptionIndex, description, aiNextSituation, aiNextRecommendedOption,
63+
followPolicy, pinnedCommitId, virtual, effectiveCategory, effectiveSituation,
64+
effectiveDecision, effectiveOptions, effectiveDescription,
65+
null, null, null, null); // ← 새 필드는 null 기본값
66+
}
67+
68+
}

back/src/main/java/com/back/domain/node/dto/decision/DecisionNodeFromBaseRequest.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
package com.back.domain.node.dto.decision;
66

77
import com.back.domain.node.entity.NodeCategory;
8+
import jakarta.validation.constraints.Size;
9+
810
import java.util.List;
911

1012
public record DecisionNodeFromBaseRequest(
@@ -15,7 +17,8 @@ public record DecisionNodeFromBaseRequest(
1517
Integer selectedAltIndex, // 0 또는 1
1618
NodeCategory category, // 미지정 시 pivot.category 상속
1719
String situation, // 미지정 시 pivot.situation 상속
20+
@Size(min = 1, max = 3, message = "C001:options size must be 1..3")
1821
List<String> options, // 1~3개, null 가능(첫 결정 노드도 옵션 보유 가능)
19-
Integer selectedIndex, // 0..2, null 가능(주어지면 decision과 일치)
22+
Integer selectedIndex, // 0..2, null 가능(주어지면 decision과 일치)
2023
String description
2124
) {}
Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,31 @@
11
/**
2-
* [DTO-REQ] 결정 노드에서 세계선 포크(새 DecisionLine 생성)
3-
* - parentDecisionNodeId: 포크 기준이 되는 기존 결정 노드
4-
* - targetOptionIndex: 이 노드에서 새 라인에 적용할 선택지(0..2)
5-
* - keepUntilParent: true면 부모까지 복제(기본 true) — false면 부모 이전까지만 복제 후 새 노드부터 시작
6-
* - lineTitle: 새 라인에 붙일 식별용 메모(선택)
2+
* [DTO-REQ] 결정 노드에서 세계선 포크(옵션 교체 지원)
3+
* - parentDecisionNodeId 지점까지 타임라인 복제 후, 그 지점의 options/selectedIndex를 교체 가능
4+
* - 호환: 이전 keepUntilParent/lineTitle 형태로 생성되던 코드가 있으면 아래 보조 생성자가 흡수
75
*/
86
package com.back.domain.node.dto.decision;
97

8+
import com.back.domain.node.entity.NodeCategory;
9+
10+
import java.util.List;
11+
1012
public record ForkFromDecisionRequest(
1113
Long userId,
1214
Long parentDecisionNodeId,
13-
Integer targetOptionIndex,
14-
Boolean keepUntilParent,
15-
String lineTitle
16-
) {}
15+
Integer targetOptionIndex, // 0..2 (입력 옵션이 없을 때 강제 선택으로 사용)
16+
List<String> options, // 선택지 교체(0~3개; 보통 1~3)
17+
Integer selectedIndex, // null이면 단일 옵션 시 0으로 정규화
18+
NodeCategory category, // 선택(교체)
19+
String situation, // 선택(교체)
20+
String description // 선택(교체)
21+
) {
22+
// 가장 많이 사용하는: 구버전 호환(keepUntilParent/lineTitle을 받던 생성 시그니처 흡수)
23+
public ForkFromDecisionRequest(Long userId,
24+
Long parentDecisionNodeId,
25+
Integer targetOptionIndex,
26+
Boolean keepUntilParent,
27+
String lineTitle) {
28+
this(userId, parentDecisionNodeId, targetOptionIndex,
29+
null, null, null, null, null);
30+
}
31+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* [DTO-REQ] 베이스 편집 요청
3+
* - 특정 브랜치에서 ageYear 대상 콘텐츠를 패치로 반영
4+
*/
5+
package com.back.domain.node.dto.dvcs;
6+
7+
import com.back.domain.node.entity.NodeCategory;
8+
9+
public record BaseEditRequest(
10+
Long baseLineId,
11+
Long branchId,
12+
Integer ageYear,
13+
NodeCategory category,
14+
String situation,
15+
String decision,
16+
String optionsJson,
17+
String description,
18+
String contentHash,
19+
String message
20+
) {}

0 commit comments

Comments
 (0)