Skip to content

Commit 0580d48

Browse files
[FEAT]: 시나리오 Service Layer 구현 (#20)
* [Refactor] Scenario Entity timelinetitles컬럼 추가 * [Feat] Scenario Repository ì작성 * [Refactor] SceneCompare 구조 변경 및 관련코드수정 * [Refactor] Scene 도메인 ErrorCode 추가 * [Feat] ScenarioService 작성 * [Refactor] 시나리오 Controller Service 연결 * [Feat] Object Mapper를 Object Mapper 구현위해 JsonConfig 추가 * [Refactor] 시나리오 Service ã�JSON Helper 메서드 구현 * [Refactor] 마이너한 개선
1 parent d2bf731 commit 0580d48

File tree

13 files changed

+674
-45
lines changed

13 files changed

+674
-45
lines changed

.gitignore

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,10 @@ log.txt
77
CLAUDE.md
88

99
# DB
10-
db_dev.mv.db
10+
db_dev.mv.db
11+
12+
# QueryDSL Generated
13+
**/generated/
14+
**/build/generated/
15+
src/main/generated/
16+
back/src/main/generated/
Lines changed: 55 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
package com.back.domain.scenario.controller;
22

33
import com.back.domain.scenario.dto.*;
4-
import com.back.domain.scenario.entity.ScenarioStatus;
54
import com.back.domain.scenario.service.ScenarioService;
65
import com.back.global.common.ApiResponse;
6+
import com.back.global.exception.ApiException;
7+
import com.back.global.exception.ErrorCode;
78
import io.swagger.v3.oas.annotations.Operation;
89
import io.swagger.v3.oas.annotations.Parameter;
910
import io.swagger.v3.oas.annotations.tags.Tag;
1011
import jakarta.validation.Valid;
1112
import lombok.RequiredArgsConstructor;
1213
import org.springframework.http.HttpStatus;
1314
import org.springframework.web.bind.annotation.*;
15+
1416
import java.security.Principal;
15-
import java.time.LocalDateTime;
1617
import java.util.List;
1718

1819
/**
@@ -29,72 +30,92 @@ public class ScenarioController {
2930

3031
@PostMapping
3132
@Operation(summary = "시나리오 생성", description = "DecisionLine을 기반으로 AI 시나리오를 생성합니다.")
32-
public ApiResponse<Long> createScenario(
33+
public ApiResponse<ScenarioStatusResponse> createScenario(
3334
@Valid @RequestBody ScenarioCreateRequest request,
3435
Principal principal
3536
) {
36-
// Mock: 실제로는 scenarioService.createScenario(request, principal) 호출
37-
return ApiResponse.success(1001L, "시나리오 생성 요청이 접수되었습니다.", HttpStatus.CREATED);
37+
Long userId = getUserIdFromPrincipal(principal);
38+
39+
ScenarioStatusResponse scenarioCreateResponse = scenarioService.createScenario(userId, request);
40+
41+
return ApiResponse.success(scenarioCreateResponse, "시나리오 생성 요청이 접수되었습니다.", HttpStatus.CREATED);
3842
}
3943

4044
@GetMapping("/{scenarioId}/status")
4145
@Operation(summary = "시나리오 상태 조회", description = "시나리오 생성 진행 상태를 조회합니다.")
4246
public ApiResponse<ScenarioStatusResponse> getScenarioStatus(
43-
@Parameter(description = "시나리오 ID") @PathVariable Long scenarioId
47+
@Parameter(description = "시나리오 ID") @PathVariable Long scenarioId,
48+
Principal principal
4449
) {
45-
// Mock
46-
ScenarioStatusResponse mockResponse = new ScenarioStatusResponse(
47-
scenarioId,
48-
ScenarioStatus.PROCESSING,
49-
"AI가 시나리오를 생성 중입니다..."
50-
);
51-
return ApiResponse.success(mockResponse, "상태를 성공적으로 조회했습니다.");
50+
Long userId = getUserIdFromPrincipal(principal);
51+
52+
ScenarioStatusResponse scenarioStatusResponse = scenarioService.getScenarioStatus(scenarioId, userId);
53+
54+
return ApiResponse.success(scenarioStatusResponse, "상태를 성공적으로 조회했습니다.");
5255
}
5356

5457
@GetMapping("/info/{scenarioId}")
5558
@Operation(summary = "시나리오 상세 조회", description = "완성된 시나리오의 상세 정보를 조회합니다.")
5659
public ApiResponse<ScenarioDetailResponse> getScenarioDetail(
57-
@Parameter(description = "시나리오 ID") @PathVariable Long scenarioId
60+
@Parameter(description = "시나리오 ID") @PathVariable Long scenarioId,
61+
Principal principal
5862
) {
59-
// Mock: 실제로는 큰 DTO라서 null로 처리하거나 간단한 Mock 생성
60-
return ApiResponse.success(null, "시나리오 상세 정보를 성공적으로 조회했습니다.");
63+
Long userId = getUserIdFromPrincipal(principal);
64+
65+
ScenarioDetailResponse scenarioDetailResponse = scenarioService.getScenarioDetail(scenarioId, userId);
66+
67+
return ApiResponse.success(scenarioDetailResponse, "시나리오 상세 정보를 성공적으로 조회했습니다.");
6168
}
6269

6370
@GetMapping("/{scenarioId}/timeline")
6471
@Operation(summary = "시나리오 타임라인 조회", description = "시나리오의 선택 경로를 시간순으로 조회합니다.")
6572
public ApiResponse<TimelineResponse> getScenarioTimeline(
66-
@Parameter(description = "시나리오 ID") @PathVariable Long scenarioId
73+
@Parameter(description = "시나리오 ID") @PathVariable Long scenarioId,
74+
Principal principal
6775
) {
68-
// Mock 타임라인 생성
69-
List<TimelineResponse.TimelineEvent> mockEvents = List.of(
70-
new TimelineResponse.TimelineEvent(2020, "창업 도전"),
71-
new TimelineResponse.TimelineEvent(2022, "해외 진출"),
72-
new TimelineResponse.TimelineEvent(2025, "상장 성공")
73-
);
74-
TimelineResponse mockResponse = new TimelineResponse(scenarioId, mockEvents);
75-
return ApiResponse.success(mockResponse, "타임라인을 성공적으로 조회했습니다.");
76+
Long userId = getUserIdFromPrincipal(principal);
77+
78+
TimelineResponse timelineResponse = scenarioService.getScenarioTimeline(scenarioId, userId);
79+
80+
return ApiResponse.success(timelineResponse, "타임라인을 성공적으로 조회했습니다.");
7681
}
7782

7883
@GetMapping("/baselines")
7984
@Operation(summary = "베이스라인 목록 조회", description = "사용자의 베이스라인 목록을 조회합니다.")
8085
public ApiResponse<List<BaselineListResponse>> getBaselines(
8186
Principal principal
8287
) {
83-
// Mock 베이스라인 목록
84-
List<BaselineListResponse> mockBaselines = List.of(
85-
new BaselineListResponse(1001L, "대학 졸업 이후", List.of("교육", "진로"), LocalDateTime.now()),
86-
new BaselineListResponse(1002L, "미국 대학 이후", List.of("해외", "교육"), LocalDateTime.now())
87-
);
88-
return ApiResponse.success(mockBaselines, "베이스라인 목록을 성공적으로 조회했습니다.");
88+
Long userId = getUserIdFromPrincipal(principal);
89+
90+
List<BaselineListResponse> baselines = scenarioService.getBaselines(userId);
91+
92+
return ApiResponse.success(baselines, "베이스라인 목록을 성공적으로 조회했습니다.");
8993
}
9094

9195
@GetMapping("/compare/{baseId}/{compareId}")
9296
@Operation(summary = "시나리오 비교 분석 결과 조회", description = "두 시나리오를 비교 분석 결과를 조회합니다.")
9397
public ApiResponse<ScenarioCompareResponse> compareScenarios(
9498
@Parameter(description = "기준 시나리오 ID") @PathVariable Long baseId,
95-
@Parameter(description = "비교 시나리오 ID") @PathVariable Long compareId
99+
@Parameter(description = "비교 시나리오 ID") @PathVariable Long compareId,
100+
Principal principal
96101
) {
97-
// Mock: 복잡한 DTO라서 null 처리
98-
return ApiResponse.success(null, "시나리오 비교를 성공적으로 조회했습니다.");
102+
Long userId = getUserIdFromPrincipal(principal);
103+
104+
ScenarioCompareResponse scenarioCompareResponse = scenarioService.compareScenarios(baseId, compareId, userId);
105+
106+
return ApiResponse.success(scenarioCompareResponse, "시나리오 비교를 성공적으로 조회했습니다.");
107+
}
108+
109+
// Principal에서 userId 추출하는 Helper 메서드 (Mock 구현)
110+
// TODO: JWT 파싱으로 교체 예정
111+
private Long getUserIdFromPrincipal(Principal principal) {
112+
if (principal == null || principal.getName() == null) {
113+
throw new ApiException(ErrorCode.HANDLE_ACCESS_DENIED);
114+
}
115+
// TODO: 실제로는 JWT 토큰에서 userId 추출
116+
// String token = principal.getName();
117+
// return jwtProvider.getUserIdFromToken(token);
118+
119+
return 1L; // Mock userId
99120
}
100121
}

back/src/main/java/com/back/domain/scenario/dto/BaselineListResponse.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public record BaselineListResponse(
3131
* @return BaselineListResponse
3232
*/
3333

34+
// TODO: 구현 필요
3435
public static BaselineListResponse from(BaseLine baseLine, List<String> tags) {
3536
throw new UnsupportedOperationException("구현 예정");
3637
}

back/src/main/java/com/back/domain/scenario/dto/ScenarioCompareResponse.java

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
package com.back.domain.scenario.dto;
22

3-
import com.back.domain.scenario.entity.Scenario;
4-
import com.back.domain.scenario.entity.SceneCompare;
5-
import com.back.domain.scenario.entity.SceneType;
6-
import com.back.domain.scenario.entity.Type;
3+
import com.back.domain.scenario.entity.*;
74
import io.swagger.v3.oas.annotations.media.Schema;
85

6+
import java.util.Arrays;
97
import java.util.List;
108

119
/** │ │
@@ -48,6 +46,47 @@ public static ScenarioCompareResponse from(
4846
List<SceneType> baseIndicators,
4947
List<SceneType> compareIndicators
5048
) {
51-
throw new UnsupportedOperationException("구현 예정");
49+
// TOTAL 타입에서 종합 분석 추출
50+
String overallAnalysis = compareResults.stream()
51+
.filter(compare -> compare.getResultType() == SceneCompareResultType.TOTAL)
52+
.findFirst()
53+
.map(SceneCompare::getCompareResult)
54+
.orElse("비교 분석 결과가 없습니다.");
55+
56+
// 5개 지표별 비교 결과 생성
57+
List<IndicatorComparison> indicators = Arrays.stream(Type.values())
58+
.map(type -> {
59+
// 기준 시나리오의 해당 지표 점수 찾기
60+
int baseScore = baseIndicators.stream()
61+
.filter(indicator -> indicator.getType() == type)
62+
.findFirst()
63+
.map(SceneType::getPoint)
64+
.orElse(0);
65+
66+
// 비교 시나리오의 해당 지표 점수 찾기
67+
int compareScore = compareIndicators.stream()
68+
.filter(indicator -> indicator.getType() == type)
69+
.findFirst()
70+
.map(SceneType::getPoint)
71+
.orElse(0);
72+
73+
// SceneCompare에서 해당 지표 분석 찾기 (TOTAL 제외하고)
74+
String analysis = compareResults.stream()
75+
.filter(compare -> compare.getResultType() != SceneCompareResultType.TOTAL)
76+
.filter(compare -> compare.getResultType().name().equals(type.name()))
77+
.findFirst()
78+
.map(SceneCompare::getCompareResult)
79+
.orElse(type.name() + " 분석 결과가 없습니다.");
80+
81+
return new IndicatorComparison(type, baseScore, compareScore, analysis);
82+
})
83+
.toList();
84+
85+
return new ScenarioCompareResponse(
86+
baseScenario.getId(),
87+
compareScenario.getId(),
88+
overallAnalysis,
89+
indicators
90+
);
5291
}
5392
}

back/src/main/java/com/back/domain/scenario/dto/TimelineResponse.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public record TimelineEvent(
3232
* @return TimelineResponse
3333
*/
3434

35+
// TODO: DecisionNode에서 TimelineEvent로 변환하는 로직 구현 필요
3536
public static TimelineResponse from(Long scenarioId, List<DecisionNode> decisionNodes) {
3637
throw new UnsupportedOperationException("구현 예정");
3738
}

back/src/main/java/com/back/domain/scenario/entity/Scenario.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
@Entity
1818
@Table(name = "scenarios")
1919
@Getter
20-
@Setter
20+
@Setter // TODO:제거 필요한 롬복만 사용, 롬복 동작원리 공부하기
2121
@NoArgsConstructor
2222
@AllArgsConstructor
2323
@Builder
@@ -66,10 +66,9 @@ public class Scenario extends BaseEntity {
6666
@Column(columnDefinition = "TEXT")
6767
private String description;
6868

69-
// 시나리오 비교 결과 (다른 시나리오와의 비교 분석)
70-
@ManyToOne(fetch = FetchType.LAZY)
71-
@JoinColumn(name = "scene_compare_id")
72-
private SceneCompare sceneCompare;
69+
// 타임라인 제목들을 JSON 형태로 저장
70+
@Column(columnDefinition = "TEXT")
71+
private String timelineTitles; // {"2020": "대학원 진학", "2022": "연구실 변경", "2025": "해외 학회"} 형태
7372

7473
// 시나리오 대표 이미지 URL
7574
private String img;

back/src/main/java/com/back/domain/scenario/entity/SceneCompare.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
@AllArgsConstructor
2121
@Builder
2222
public class SceneCompare extends BaseEntity {
23+
@ManyToOne(fetch = FetchType.LAZY)
24+
@JoinColumn(name = "scenario_id", nullable = false)
25+
private Scenario scenario;
2326

2427
@Column(columnDefinition = "TEXT")
2528
private String compareResult;
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,64 @@
11
package com.back.domain.scenario.repository;
22

33
import com.back.domain.scenario.entity.Scenario;
4+
import com.back.domain.scenario.entity.ScenarioStatus;
5+
import org.springframework.data.domain.Page;
6+
import org.springframework.data.domain.Pageable;
7+
import org.springframework.data.jpa.repository.EntityGraph;
48
import org.springframework.data.jpa.repository.JpaRepository;
9+
import org.springframework.data.jpa.repository.Query;
10+
import org.springframework.data.repository.query.Param;
511
import org.springframework.stereotype.Repository;
612

13+
import java.util.List;
14+
import java.util.Optional;
15+
716
/**
817
* 시나리오 엔티티에 대한 데이터베이스 접근을 담당하는 JpaRepository.
918
*/
1019
@Repository
1120
public interface ScenarioRepository extends JpaRepository<Scenario, Long> {
21+
22+
// 사용자별 시나리오 조회 (권한 검증)
23+
Optional<Scenario> findByIdAndUserId(Long id, Long userId);
24+
25+
// 베이스 시나리오 존재 확인
26+
boolean existsByDecisionLine_BaseLineId(Long baseLineId);
27+
28+
// 베이스 시나리오 조회 (비교 기준)
29+
Optional<Scenario> findFirstByDecisionLine_BaseLineIdOrderByCreatedDateAsc(Long baseLineId);
30+
31+
// 연관 Entity 정보 포함 조회 (EntityGraph로 N+1 쿼리 방지, 확장 기능 대비)
32+
@EntityGraph(attributePaths = {"user", "decisionLine", "sceneCompare"})
33+
Optional<Scenario> findWithAllRelationsById(Long id);
34+
35+
// 사용자별 특정 상태 시나리오 목록 조회
36+
List<Scenario> findByUserIdAndStatusOrderByCreatedDateDesc(Long userId, ScenarioStatus status);
37+
38+
// DecisionLine 기반 시나리오 존재 확인 (시나리오 중복 생성 방지)
39+
boolean existsByDecisionLineIdAndStatus(Long decisionLineId, ScenarioStatus status);
40+
41+
// 베이스라인별 완료된 시나리오 조회 (비교용)
42+
List<Scenario> findByDecisionLine_BaseLineIdAndStatusOrderByCreatedDateAsc(Long baseLineId, ScenarioStatus status);
43+
44+
// 사용자의 최신 시나리오 조회 (확장 기능 대비)
45+
Optional<Scenario> findTopByUserIdOrderByCreatedDateDesc(Long userId);
46+
47+
// 사용자별 베이스라인 시나리오 제외 시나리오 목록 조회 (MyPage용, 페이징구현 및 N+1 방지)
48+
@EntityGraph(attributePaths = {"user", "decisionLine", "decisionLine.baseLine"})
49+
@Query("SELECT s FROM Scenario s " +
50+
"WHERE s.user.id = :userId " +
51+
"AND s.decisionLine.baseLine.id = :baseLineId " +
52+
"AND s.status = :status " +
53+
"AND s.id != (SELECT MIN(s2.id) FROM Scenario s2 " +
54+
"WHERE s2.decisionLine.baseLine.id = :baseLineId " +
55+
"AND s2.status = :status) " +
56+
"ORDER BY s.createdDate DESC")
57+
Page<Scenario> findUserNonBaseScenariosByBaseLineId(@Param("userId") Long userId,
58+
@Param("baseLineId") Long baseLineId,
59+
@Param("status") ScenarioStatus status,
60+
Pageable pageable);
61+
62+
// 특정 상태의 시나리오들 조회 (상태별 처리용)
63+
List<Scenario> findByStatusOrderByCreatedDateAsc(ScenarioStatus status);
1264
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.back.domain.scenario.repository;
2+
3+
import com.back.domain.scenario.entity.SceneCompare;
4+
import com.back.domain.scenario.entity.SceneCompareResultType;
5+
import org.springframework.data.jpa.repository.JpaRepository;
6+
import org.springframework.stereotype.Repository;
7+
8+
import java.util.List;
9+
10+
/**
11+
* 시나리오 비교 결과 엔티티에 대한 데이터베이스 접근을 담당하는 JpaRepository.
12+
*/
13+
@Repository
14+
public interface SceneCompareRepository extends JpaRepository<SceneCompare, Long> {
15+
16+
// 특정 시나리오의 모든 비교 결과 조회 (6개: TOTAL + 5개 지표)
17+
List<SceneCompare> findByScenarioIdOrderByResultType(Long scenarioId);
18+
19+
// 특정 시나리오의 특정 타입 비교 결과 조회
20+
SceneCompare findByScenarioIdAndResultType(Long scenarioId, SceneCompareResultType resultType);
21+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,25 @@
11
package com.back.domain.scenario.repository;
22

33
import com.back.domain.scenario.entity.SceneType;
4+
import com.back.domain.scenario.entity.Type;
45
import org.springframework.data.jpa.repository.JpaRepository;
56
import org.springframework.stereotype.Repository;
67

8+
import java.util.List;
9+
import java.util.Optional;
10+
711
/**
812
* 시나리오 유형 엔티티에 대한 데이터베이스 접근을 담당하는 JpaRepository.
913
*/
1014
@Repository
1115
public interface SceneTypeRepository extends JpaRepository<SceneType, Long> {
16+
17+
// 특정 시나리오의 지표들 조회 (타입 순서대로)
18+
List<SceneType> findByScenarioIdOrderByTypeAsc(Long scenarioId);
19+
20+
// 특정 시나리오의 특정 지표 조회
21+
Optional<SceneType> findByScenarioIdAndType(Long scenarioId, Type type);
22+
23+
// 시나리오별 지표 존재 여부 확인
24+
boolean existsByScenarioId(Long scenarioId);
1225
}

0 commit comments

Comments
 (0)