Skip to content

Commit b2e05f4

Browse files
committed
refactor node & scenario make
1 parent c13bea3 commit b2e05f4

File tree

7 files changed

+325
-65
lines changed

7 files changed

+325
-65
lines changed

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

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
/**
2-
* [DTO-RES] DecisionNode 응답(보강)
3-
* - followPolicy/pinnedCommitId/virtual 플래그를 추가해 해석 상태를 노출
4-
* - effective* 필드는 Resolver가 계산한 최종 표현값(렌더 우선 사용)
2+
* [DTO-RES] DecisionNode 응답(보강 최종본)
3+
* - 정책/핀/가상(followPolicy/pinnedCommitId/virtual)과 effective* 해석값 노출
4+
* - 렌더 편의: childrenIds/root/pivot 링크 + 단일 패스 렌더용 힌트(renderPhase, incoming*)
55
*/
66
package com.back.domain.node.dto.decision;
77

88
import com.back.domain.node.entity.FollowPolicy;
99
import com.back.domain.node.entity.NodeCategory;
10+
1011
import java.util.List;
1112

1213
public record DecNodeDto(
@@ -40,10 +41,16 @@ public record DecNodeDto(
4041
List<String> effectiveOptions,
4142
String effectiveDescription,
4243

44+
// 기본 렌더 편의
4345
List<Long> childrenIds, // 이 DECISION의 자식들 id(시간순)
4446
Boolean root, // 라인 헤더면 true
4547
Long pivotLinkBaseNodeId, // 베이스 분기 슬롯에서 올라온 첫 노드면 해당 BaseNode id
46-
Integer pivotSlotIndex // 0/1 (분기 슬롯 인덱스), 아니면 null
48+
Integer pivotSlotIndex, // 0/1 (분기 슬롯 인덱스), 아니면 null
49+
50+
// 단일 패스 렌더용 최소 힌트
51+
Integer renderPhase, // 1=from-base 라인, 2..N=fork 깊이(부모 라인 +1)
52+
Long incomingFromId, // 이 노드로 "들어오는" 에지의 from 노드 id(루트면 포크 원본, 아니면 parentId)
53+
String incomingEdgeType // "normal" | "fork"
4754
) {
4855
// === 호환 오버로드(기존 서비스 호출 유지) ===
4956
public DecNodeDto(
@@ -62,7 +69,7 @@ public DecNodeDto(
6269
parentOptionIndex, description, aiNextSituation, aiNextRecommendedOption,
6370
followPolicy, pinnedCommitId, virtual, effectiveCategory, effectiveSituation,
6471
effectiveDecision, effectiveOptions, effectiveDescription,
65-
null, null, null, null); // ← 새 필드는 null 기본값
72+
null, null, null, null,
73+
null, null, null); // 새 필드는 null 기본값
6674
}
67-
6875
}

back/src/main/java/com/back/domain/node/service/DecisionFlowService.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,6 @@ private Long currentUserId() {
311311
public DecisionLineLifecycleDto completeDecisionLine(Long decisionLineId) {
312312
DecisionLine line = support.requireDecisionLine(decisionLineId);
313313
try { line.complete(); } catch (RuntimeException e) { throw support.mapDomainToApi(e); }
314-
decisionLineRepository.save(line);
315314
return new DecisionLineLifecycleDto(line.getId(), line.getStatus());
316315
}
317316

back/src/main/java/com/back/domain/node/service/NodeQueryService.java

Lines changed: 142 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.springframework.transaction.annotation.Transactional;
2626

2727
import java.util.*;
28+
import java.util.stream.Collectors;
2829

2930
@Service
3031
@RequiredArgsConstructor
@@ -52,13 +53,22 @@ public TreeDto getTreeForBaseLine(Long baseLineId) {
5253
List<BaseNode> orderedBase = support.getOrderedBaseNodes(baseLineId);
5354
List<BaseNodeDto> baseDtos = orderedBase.stream().map(mappers.BASE_READ::map).toList();
5455

55-
// 전체 라인 공용 pivot 역인덱스
56+
// 가장 많이 사용하는: 전체 라인 공용 pivot 역인덱스
5657
Map<Long, PivotMark> pivotIndex = buildPivotIndex(baseLineId);
5758

59+
// ===== (1) 노드 수집 =====
60+
record Key(Long baseId, Integer age) {}
61+
record View(DecisionNode dn, DecNodeDto dto, boolean isRoot, Long baseId, Integer age,
62+
List<Long> childrenIds, Long pivotBaseId, Integer pivotSlot) {}
63+
5864
List<DecNodeDto> decDtos = new ArrayList<>();
5965
List<DecisionLine> lines = decisionLineRepository.findByBaseLine_Id(baseLineId);
6066

67+
List<View> pool = new ArrayList<>();
68+
Map<Long, List<View>> byLine = new HashMap<>();
69+
6170
for (DecisionLine line : lines) {
71+
// 가장 많이 사용하는 함수 호출 위에 한줄로만 요약 주석: 라인의 노드를 타임라인 정렬로 로드
6272
List<DecisionNode> ordered = decisionNodeRepository
6373
.findByDecisionLine_IdOrderByAgeYearAscIdAsc(line.getId());
6474

@@ -67,29 +77,126 @@ public TreeDto getTreeForBaseLine(Long baseLineId) {
6777
for (DecisionNode dn : ordered) {
6878
DecNodeDto base = mappers.DECISION_READ.map(dn);
6979

70-
List<Long> childrenIds = childrenIndex.getOrDefault(dn.getId(), List.of());
7180
boolean isRoot = (dn.getParent() == null);
81+
List<Long> childrenIds = childrenIndex.getOrDefault(dn.getId(), List.of());
7282

7383
PivotMark mark = pivotIndex.get(base.id());
74-
Long pivotBaseId = (mark != null) ? mark.baseNodeId() : null;
84+
Long pivotBaseId = (mark != null) ? mark.baseNodeId() :
85+
(dn.getBaseNode() != null ? dn.getBaseNode().getId() : null);
7586
Integer pivotSlot = (mark != null) ? mark.slotIndex() : null;
7687

77-
// effective*는 DECISION_READ에서 이미 계산/주입되어 있다면 그대로 사용
78-
decDtos.add(new DecNodeDto(
79-
base.id(), base.userId(), base.type(), base.category(),
80-
base.situation(), base.decision(), base.ageYear(),
81-
base.decisionLineId(), base.parentId(), base.baseNodeId(),
82-
base.background(), base.options(), base.selectedIndex(),
83-
base.parentOptionIndex(), base.description(),
84-
base.aiNextSituation(), base.aiNextRecommendedOption(),
85-
base.followPolicy(), base.pinnedCommitId(), base.virtual(),
86-
base.effectiveCategory(), base.effectiveSituation(), base.effectiveDecision(),
87-
base.effectiveOptions(), base.effectiveDescription(),
88-
// ▼ 렌더 편의 필드
89-
List.copyOf(childrenIds), isRoot, pivotBaseId, pivotSlot
90-
));
88+
Long baseId = pivotBaseId;
89+
Integer age = dn.getAgeYear();
90+
91+
View v = new View(dn, base, isRoot, baseId, age, List.copyOf(childrenIds), pivotBaseId, pivotSlot);
92+
pool.add(v);
93+
byLine.computeIfAbsent(line.getId(), k -> new ArrayList<>()).add(v);
9194
}
9295
}
96+
97+
Map<Key, List<View>> byKey = pool.stream()
98+
.filter(v -> v.baseId != null && v.age != null)
99+
.collect(Collectors.groupingBy(v -> new Key(v.baseId, v.age)));
100+
101+
// ===== (2) 라인 간 포크 그래프 추론 → renderPhase 계산 =====
102+
Map<Long, View> rootCand = new HashMap<>(); // lineId -> 루트 후보
103+
for (Map.Entry<Long, List<View>> e : byLine.entrySet()) {
104+
List<View> vs = e.getValue().stream()
105+
.sorted(Comparator.comparing((View x) -> x.age)
106+
.thenComparing(x -> x.dn.getId()))
107+
.toList();
108+
109+
View first = vs.get(0);
110+
// 가장 중요한 함수: 라인 루트가 헤더면 다음 피벗을 루트 후보로 대체
111+
View cand = (first.isRoot && first.baseId == null && vs.size() > 1) ? vs.get(1) : first;
112+
rootCand.put(e.getKey(), cand);
113+
}
114+
115+
Map<Long, Set<Long>> g = new HashMap<>(); // originLineId -> {forkLineId}
116+
Map<Long, Integer> indeg = new HashMap<>(); // lineId -> indegree
117+
118+
for (Map.Entry<Long, View> e : rootCand.entrySet()) {
119+
Long lineId = e.getKey();
120+
View me = e.getValue();
121+
indeg.putIfAbsent(lineId, 0);
122+
123+
if (me.baseId != null && me.age != null) {
124+
List<View> same = byKey.getOrDefault(new Key(me.baseId, me.age), List.of());
125+
Optional<View> origin = same.stream()
126+
.filter(o -> !o.dn.getDecisionLine().getId().equals(lineId))
127+
.sorted(Comparator
128+
.comparing((View o) -> o.dn.getDecisionLine().getId())
129+
.thenComparing(o -> o.dn.getId()))
130+
.findFirst();
131+
132+
if (origin.isPresent()) {
133+
Long originLineId = origin.get().dn.getDecisionLine().getId();
134+
g.computeIfAbsent(originLineId, k -> new HashSet<>()).add(lineId);
135+
indeg.put(lineId, indeg.getOrDefault(lineId, 0) + 1);
136+
indeg.putIfAbsent(originLineId, 0);
137+
}
138+
}
139+
}
140+
141+
Map<Long, Integer> linePhase = new HashMap<>(); // lineId -> phase
142+
ArrayDeque<Long> q = new ArrayDeque<>();
143+
for (Map.Entry<Long, Integer> e : indeg.entrySet()) {
144+
if (e.getValue() == 0) { // indegree==0 → from-base
145+
linePhase.put(e.getKey(), 1);
146+
q.add(e.getKey());
147+
}
148+
}
149+
while (!q.isEmpty()) {
150+
Long u = q.poll();
151+
int next = linePhase.get(u) + 1;
152+
for (Long v : g.getOrDefault(u, Set.of())) {
153+
indeg.put(v, indeg.get(v) - 1);
154+
linePhase.put(v, Math.max(linePhase.getOrDefault(v, 1), next));
155+
if (indeg.get(v) == 0) q.add(v);
156+
}
157+
}
158+
159+
// ===== (3) DTO 주입: renderPhase + incomingFromId/incomingEdgeType =====
160+
for (View v : pool) {
161+
DecNodeDto b = v.dto;
162+
163+
Long lineId = v.dn.getDecisionLine().getId();
164+
Integer renderPhase = linePhase.getOrDefault(lineId, 1);
165+
166+
Long incomingFromId = (v.isRoot)
167+
? byKey.getOrDefault(new Key(v.baseId, v.age), List.of()).stream()
168+
.filter(o -> !o.dn.getDecisionLine().getId().equals(lineId))
169+
.sorted(Comparator
170+
.comparing((View o) -> o.dn.getDecisionLine().getId())
171+
.thenComparing(o -> o.dn.getId()))
172+
.map(o -> o.dn.getId())
173+
.findFirst()
174+
.orElse(null)
175+
: (v.dn.getParent() != null ? v.dn.getParent().getId() : null);
176+
177+
String incomingEdgeType = (v.isRoot && incomingFromId != null) ? "fork" : "normal";
178+
179+
decDtos.add(new DecNodeDto(
180+
b.id(), b.userId(), b.type(), b.category(),
181+
b.situation(), b.decision(), b.ageYear(),
182+
b.decisionLineId(), b.parentId(), b.baseNodeId(),
183+
b.background(), b.options(), b.selectedIndex(),
184+
b.parentOptionIndex(), b.description(),
185+
b.aiNextSituation(), b.aiNextRecommendedOption(),
186+
b.followPolicy(), b.pinnedCommitId(), b.virtual(),
187+
b.effectiveCategory(), b.effectiveSituation(), b.effectiveDecision(),
188+
b.effectiveOptions(), b.effectiveDescription(),
189+
v.childrenIds, v.isRoot, v.pivotBaseId, v.pivotSlot,
190+
renderPhase, incomingFromId, incomingEdgeType
191+
));
192+
}
193+
194+
// 출력 순서 보장: phase → ageYear → id
195+
decDtos.sort(Comparator
196+
.comparing(DecNodeDto::renderPhase, Comparator.nullsLast(Integer::compareTo))
197+
.thenComparing(DecNodeDto::ageYear, Comparator.nullsFirst(Integer::compareTo))
198+
.thenComparing(DecNodeDto::id));
199+
93200
return new TreeDto(baseDtos, decDtos);
94201
}
95202

@@ -142,7 +249,8 @@ public DecisionLineListDto getDecisionLines(Long userId) {
142249
// 가장 중요한: 특정 라인의 상세를 childrenIds/root/pivotLink*와 함께 반환
143250
public DecisionLineDetailDto getDecisionLineDetail(Long decisionLineId) {
144251
DecisionLine line = decisionLineRepository.findById(decisionLineId)
145-
.orElseThrow(() -> new ApiException(ErrorCode.DECISION_LINE_NOT_FOUND, "DecisionLine not found: " + decisionLineId));
252+
.orElseThrow(() -> new ApiException(ErrorCode.DECISION_LINE_NOT_FOUND,
253+
"DecisionLine not found: " + decisionLineId));
146254

147255
Long baseLineId = line.getBaseLine().getId();
148256
Long baseBranchId = (line.getBaseBranch() != null) ? line.getBaseBranch().getId() : null;
@@ -153,7 +261,7 @@ public DecisionLineDetailDto getDecisionLineDetail(Long decisionLineId) {
153261

154262
// parent→children 인덱스 구성
155263
Map<Long, List<Long>> childrenIndex = buildChildrenIndex(ordered);
156-
// 베이스 분기 슬롯 역인덱스 구성(altOpt1/2TargetDecisionId → (baseNodeId, slot))
264+
// 베이스 분기 슬롯 역인덱스 구성
157265
Map<Long, PivotMark> pivotIndex = buildPivotIndex(baseLineId);
158266

159267
List<DecNodeDto> nodes = ordered.stream().map(n -> {
@@ -178,12 +286,12 @@ public DecisionLineDetailDto getDecisionLineDetail(Long decisionLineId) {
178286
if (verId != null) {
179287
NodeAtomVersion v = versionRepo.findById(verId).orElse(null);
180288
if (v != null) {
181-
if (v.getCategory() != null) effCategory = v.getCategory();
182-
if (v.getSituation() != null) effSituation = v.getSituation();
183-
if (v.getDecision() != null) effDecision = v.getDecision();
289+
if (v.getCategory() != null) effCategory = v.getCategory();
290+
if (v.getSituation() != null) effSituation = v.getSituation();
291+
if (v.getDecision() != null) effDecision = v.getDecision();
184292
List<String> parsed = parseOptionsJson(v.getOptionsJson());
185-
if (parsed != null) effOpts = parsed;
186-
if (v.getDescription() != null) effDesc = v.getDescription();
293+
if (parsed != null) effOpts = parsed;
294+
if (v.getDescription() != null) effDesc = v.getDescription();
187295
}
188296
}
189297

@@ -194,6 +302,12 @@ public DecisionLineDetailDto getDecisionLineDetail(Long decisionLineId) {
194302
Long pivotBaseId = (mark != null) ? mark.baseNodeId() : null;
195303
Integer pivotSlot = (mark != null) ? mark.slotIndex() : null;
196304

305+
// ===== 라인 상세 전용 렌더 힌트 =====
306+
// 한줄 요약: 상세 화면은 한 라인만 보므로 phase=1 고정, incoming은 parent 기준(normal)
307+
Integer renderPhase = 1;
308+
Long incomingFromId = isRoot ? null : n.getParent().getId();
309+
String incomingEdgeType = "normal";
310+
197311
return new DecNodeDto(
198312
base.id(), base.userId(), base.type(), base.category(),
199313
base.situation(), base.decision(), base.ageYear(),
@@ -202,10 +316,11 @@ public DecisionLineDetailDto getDecisionLineDetail(Long decisionLineId) {
202316
base.parentOptionIndex(), base.description(),
203317
base.aiNextSituation(), base.aiNextRecommendedOption(),
204318
base.followPolicy(), base.pinnedCommitId(), base.virtual(),
205-
// effective*
319+
// effective* (최종 해석 반영)
206320
effCategory, effSituation, effDecision, effOpts, effDesc,
207-
// ▼ 렌더 편의 필드
208-
List.copyOf(childrenIds), isRoot, pivotBaseId, pivotSlot
321+
// ▼ 렌더 편의 + 단일 패스 힌트(라인 내부 한정)
322+
List.copyOf(childrenIds), isRoot, pivotBaseId, pivotSlot,
323+
renderPhase, incomingFromId, incomingEdgeType
209324
);
210325
}).toList();
211326

back/src/main/java/com/back/domain/scenario/controller/ScenarioController.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.back.domain.scenario.controller;
22

3+
import com.back.domain.node.dto.decision.DecisionNodeNextRequest;
34
import com.back.domain.scenario.dto.*;
45
import com.back.domain.scenario.service.ScenarioService;
56
import com.back.global.common.PageResponse;
@@ -42,12 +43,13 @@ private Long getUserId(CustomUserDetails userDetails) {
4243
@PostMapping
4344
@Operation(summary = "시나리오 생성", description = "DecisionLine을 기반으로 AI 시나리오를 생성합니다.")
4445
public ResponseEntity<ScenarioStatusResponse> createScenario(
45-
@Valid @RequestBody ScenarioCreateRequest request,
46+
@Valid @RequestPart("scenario") ScenarioCreateRequest request,
47+
@RequestPart(value = "lastDecision", required = false) DecisionNodeNextRequest lastDecision,
4648
@AuthenticationPrincipal CustomUserDetails userDetails
4749
) {
4850
Long userId = getUserId(userDetails);
4951

50-
ScenarioStatusResponse scenarioCreateResponse = scenarioService.createScenario(userId, request);
52+
ScenarioStatusResponse scenarioCreateResponse = scenarioService.createScenario(userId,request, lastDecision);
5153

5254
return ResponseEntity.status(HttpStatus.CREATED).body(scenarioCreateResponse);
5355
}

0 commit comments

Comments
 (0)