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 @@ -13,7 +13,10 @@
import com.back.domain.node.dto.base.BaseLineDto;
import com.back.domain.node.dto.base.BaseNodeDto;
import com.back.domain.node.service.NodeService;
import com.back.global.exception.ApiException;
import com.back.global.exception.ErrorCode;
import com.back.global.security.CustomUserDetails;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
Expand All @@ -31,7 +34,9 @@ public class BaseLineController {

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

Expand Down Expand Up @@ -65,6 +70,8 @@ public ResponseEntity<TreeDto> getTreeForBaseLine(@PathVariable Long baseLineId)
public ResponseEntity<List<BaseLineDto>> getMyBaseLines(
@AuthenticationPrincipal CustomUserDetails me
) {
if (me == null) throw new ApiException(ErrorCode.HANDLE_ACCESS_DENIED, "login required");

List<BaseLineDto> list = nodeService.getMyBaseLines(me.getId());
return ResponseEntity.ok(list);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,14 @@

import com.back.domain.node.dto.decision.*;
import com.back.domain.node.service.NodeService;
import com.back.global.exception.ApiException;
import com.back.global.exception.ErrorCode;
import com.back.global.security.CustomUserDetails;
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.*;

@RestController
Expand All @@ -23,31 +28,41 @@ public class DecisionFlowController {

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

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

// 라인 취소
@PostMapping("/{decisionLineId}/cancel")
public ResponseEntity<DecisionLineLifecycleDto> cancel(@PathVariable Long decisionLineId) {
public ResponseEntity<DecisionLineLifecycleDto> cancel(@AuthenticationPrincipal CustomUserDetails me,
@PathVariable Long decisionLineId) {
if (me == null) throw new ApiException(ErrorCode.HANDLE_ACCESS_DENIED, "login required");
return ResponseEntity.ok(nodeService.cancelDecisionLine(decisionLineId));
}

// 라인 완료
@PostMapping("/{decisionLineId}/complete")
public ResponseEntity<DecisionLineLifecycleDto> complete(@PathVariable Long decisionLineId) {
public ResponseEntity<DecisionLineLifecycleDto> complete(@AuthenticationPrincipal CustomUserDetails me,
@PathVariable Long decisionLineId) {
if (me == null) throw new ApiException(ErrorCode.HANDLE_ACCESS_DENIED, "login required");
return ResponseEntity.ok(nodeService.completeDecisionLine(decisionLineId));
}

// 분기점까지의 라인 복제(포크) 및 새로운 라인 생성
@PostMapping("/fork")
public ResponseEntity<DecNodeDto> fork(@RequestBody ForkFromDecisionRequest request) {
public ResponseEntity<DecNodeDto> fork(@AuthenticationPrincipal CustomUserDetails me,
@RequestBody ForkFromDecisionRequest request) {
if (me == null) throw new ApiException(ErrorCode.HANDLE_ACCESS_DENIED, "login required");
return ResponseEntity.status(HttpStatus.CREATED).body(nodeService.forkFromDecision(request));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,16 @@
import com.back.domain.node.dto.decision.DecisionLineDetailDto;
import com.back.domain.node.dto.decision.DecisionLineListDto;
import com.back.domain.node.service.NodeQueryService;
import com.back.global.exception.ApiException;
import com.back.global.exception.ErrorCode;
import com.back.global.security.CustomUserDetails;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

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

private final NodeQueryService nodeQueryService;

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

// 가장 많이 사용하는: 특정 결정 라인 상세
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* [API] DVCS 하이브리드 컨트롤러
* - 베이스 편집(커밋/패치), 결정 편집(오버라이드/승격), 팔로우 정책 전환, 브랜치 선택, 브랜치/커밋 조회
* - 규칙: Controller → Service → Repository (Controller는 얇게 유지)
*/
package com.back.domain.node.controller;

import com.back.domain.node.dto.dvcs.*;
import com.back.domain.node.service.DvcsFacadeService;
import com.back.global.exception.ApiException;
import com.back.global.exception.ErrorCode;
import com.back.global.security.CustomUserDetails;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/v1/dvcs")
@RequiredArgsConstructor
public class DvcsController {

private final DvcsFacadeService dvcs; // ★ 파사드 서비스

// 베이스 편집을 브랜치 커밋으로 반영
@PostMapping("/base/edit")
public ResponseEntity<EditAcknowledgeDto> editBase(@AuthenticationPrincipal CustomUserDetails me,
@RequestBody BaseEditRequest req) {
if (me == null) throw new ApiException(ErrorCode.HANDLE_ACCESS_DENIED, "login required");

return ResponseEntity.ok(dvcs.editBase(me.getId(), req));
}

// 결정 노드 편집(OVERRIDE 또는 업스트림 승격)
@PostMapping("/decision/edit")
public ResponseEntity<EditAcknowledgeDto> editDecision(@AuthenticationPrincipal CustomUserDetails me,
@RequestBody DecisionEditRequest req) {
if (me == null) throw new ApiException(ErrorCode.HANDLE_ACCESS_DENIED, "login required");

return ResponseEntity.ok(dvcs.editDecision(me.getId(), req));
}

// 팔로우 정책 전환(FOLLOW/PINNED/OVERRIDE) 및 핀 커밋 지정
@PostMapping("/decision/policy")
public ResponseEntity<EditAcknowledgeDto> changePolicy(@AuthenticationPrincipal CustomUserDetails me,
@RequestBody FollowPolicyChangeRequest req) {
if (me == null) throw new ApiException(ErrorCode.HANDLE_ACCESS_DENIED, "login required");

return ResponseEntity.ok(dvcs.changePolicy(me.getId(), req));
}

// 브랜치 생성 또는 기존 브랜치 선택 후 라인에 적용
@PostMapping("/branch/select")
public ResponseEntity<EditAcknowledgeDto> selectBranch(@AuthenticationPrincipal CustomUserDetails me,
@RequestBody BranchSelectRequest req) {
if (me == null) throw new ApiException(ErrorCode.HANDLE_ACCESS_DENIED, "login required");

return ResponseEntity.ok(dvcs.selectBranch(me.getId(), req));
}

// 특정 베이스라인의 브랜치/커밋 요약 조회
@GetMapping("/branches/{baseLineId}")
public ResponseEntity<List<BranchSummaryDto>> listBranches(@PathVariable Long baseLineId) {
return ResponseEntity.ok(dvcs.listBranches(baseLineId));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,23 @@
package com.back.domain.node.dto.base;

import com.back.domain.node.entity.NodeCategory;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;

import java.util.List;

public record BaseLineBulkCreateRequest(
Long userId,
String title,
List<BaseNodePayload> nodes
List<@Valid BaseNodePayload> nodes
) {
public record BaseNodePayload(
NodeCategory category,
String situation,
String decision,
@Min(1)
@Max(120)
Integer ageYear,
String description
) {}
Expand Down
15 changes: 12 additions & 3 deletions back/src/main/java/com/back/domain/node/dto/base/BaseNodeDto.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
/**
* [DTO-RES] BaseNode 응답
* - 고정 선택과 분기 2칸 및 각 타겟 링크를 포함한다
* [DTO-RES] BaseNode 응답(보강)
* - currentVersionId와 effective* 필드를 추가해 버전 해석 결과를 노출
*/
package com.back.domain.node.dto.base;

import com.back.domain.node.entity.NodeCategory;
import java.util.List;

public record BaseNodeDto(
Long id,
Expand All @@ -22,5 +23,13 @@ public record BaseNodeDto(
String altOpt2,
Long altOpt1TargetDecisionId,
Long altOpt2TargetDecisionId,
String description
String description,

// 버전/해석 결과
Long currentVersionId,
NodeCategory effectiveCategory,
String effectiveSituation,
String effectiveDecision,
List<String> effectiveOptions,
String effectiveDescription
) {}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
/**
* [DTO-RES] DecisionNode 응답
* - options/selectedIndex/parentOptionIndex를 포함해 프론트 렌더 정보를 제공한다
* [DTO-RES] DecisionNode 응답(보강)
* - followPolicy/pinnedCommitId/virtual 플래그를 추가해 해석 상태를 노출
* - effective* 필드는 Resolver가 계산한 최종 표현값(렌더 우선 사용)
*/
package com.back.domain.node.dto.decision;

import com.back.domain.node.entity.FollowPolicy;
import com.back.domain.node.entity.NodeCategory;
import java.util.List;

Expand All @@ -24,5 +26,43 @@ public record DecNodeDto(
Integer parentOptionIndex,
String description,
String aiNextSituation,
String aiNextRecommendedOption
) {}
String aiNextRecommendedOption,

// 정책/핀/가상
FollowPolicy followPolicy,
Long pinnedCommitId,
Boolean virtual,

// 해석 결과(렌더 우선 사용; null이면 상단 원본 사용)
NodeCategory effectiveCategory,
String effectiveSituation,
String effectiveDecision,
List<String> effectiveOptions,
String effectiveDescription,

List<Long> childrenIds, // 이 DECISION의 자식들 id(시간순)
Boolean root, // 라인 헤더면 true
Long pivotLinkBaseNodeId, // 베이스 분기 슬롯에서 올라온 첫 노드면 해당 BaseNode id
Integer pivotSlotIndex // 0/1 (분기 슬롯 인덱스), 아니면 null
) {
// === 호환 오버로드(기존 서비스 호출 유지) ===
public DecNodeDto(
Long id, Long userId, String type, NodeCategory category,
String situation, String decision, Integer ageYear,
Long decisionLineId, Long parentId, Long baseNodeId,
String background, List<String> options, Integer selectedIndex,
Integer parentOptionIndex, String description,
String aiNextSituation, String aiNextRecommendedOption,
FollowPolicy followPolicy, Long pinnedCommitId, Boolean virtual,
NodeCategory effectiveCategory, String effectiveSituation, String effectiveDecision,
List<String> effectiveOptions, String effectiveDescription
) {
this(id, userId, type, category, situation, decision, ageYear,
decisionLineId, parentId, baseNodeId, background, options, selectedIndex,
parentOptionIndex, description, aiNextSituation, aiNextRecommendedOption,
followPolicy, pinnedCommitId, virtual, effectiveCategory, effectiveSituation,
effectiveDecision, effectiveOptions, effectiveDescription,
null, null, null, null); // ← 새 필드는 null 기본값
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
package com.back.domain.node.dto.decision;

import com.back.domain.node.entity.NodeCategory;
import jakarta.validation.constraints.Size;

import java.util.List;

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

import com.back.domain.node.entity.NodeCategory;

import java.util.List;

public record ForkFromDecisionRequest(
Long userId,
Long parentDecisionNodeId,
Integer targetOptionIndex,
Boolean keepUntilParent,
String lineTitle
) {}
Integer targetOptionIndex, // 0..2 (입력 옵션이 없을 때 강제 선택으로 사용)
List<String> options, // 선택지 교체(0~3개; 보통 1~3)
Integer selectedIndex, // null이면 단일 옵션 시 0으로 정규화
NodeCategory category, // 선택(교체)
String situation, // 선택(교체)
String description // 선택(교체)
) {
// 가장 많이 사용하는: 구버전 호환(keepUntilParent/lineTitle을 받던 생성 시그니처 흡수)
public ForkFromDecisionRequest(Long userId,
Long parentDecisionNodeId,
Integer targetOptionIndex,
Boolean keepUntilParent,
String lineTitle) {
this(userId, parentDecisionNodeId, targetOptionIndex,
null, null, null, null, null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* [DTO-REQ] 베이스 편집 요청
* - 특정 브랜치에서 ageYear 대상 콘텐츠를 패치로 반영
*/
package com.back.domain.node.dto.dvcs;

import com.back.domain.node.entity.NodeCategory;

public record BaseEditRequest(
Long baseLineId,
Long branchId,
Integer ageYear,
NodeCategory category,
String situation,
String decision,
String optionsJson,
String description,
String contentHash,
String message
) {}
Loading