Skip to content
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,10 @@ log.txt
CLAUDE.md

# DB
db_dev.mv.db
db_dev.mv.db

# QueryDSL Generated
**/generated/
**/build/generated/
src/main/generated/
back/src/main/generated/
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
package com.back.domain.scenario.controller;

import com.back.domain.scenario.dto.*;
import com.back.domain.scenario.entity.ScenarioStatus;
import com.back.domain.scenario.service.ScenarioService;
import com.back.global.common.ApiResponse;
import com.back.global.exception.ApiException;
import com.back.global.exception.ErrorCode;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

import java.security.Principal;
import java.time.LocalDateTime;
import java.util.List;

/**
Expand All @@ -29,72 +30,92 @@ public class ScenarioController {

@PostMapping
@Operation(summary = "시나리오 생성", description = "DecisionLine을 기반으로 AI 시나리오를 생성합니다.")
public ApiResponse<Long> createScenario(
public ApiResponse<ScenarioStatusResponse> createScenario(
@Valid @RequestBody ScenarioCreateRequest request,
Principal principal
) {
// Mock: 실제로는 scenarioService.createScenario(request, principal) 호출
return ApiResponse.success(1001L, "시나리오 생성 요청이 접수되었습니다.", HttpStatus.CREATED);
Long userId = getUserIdFromPrincipal(principal);

ScenarioStatusResponse scenarioCreateResponse = scenarioService.createScenario(userId, request);

return ApiResponse.success(scenarioCreateResponse, "시나리오 생성 요청이 접수되었습니다.", HttpStatus.CREATED);
}

@GetMapping("/{scenarioId}/status")
@Operation(summary = "시나리오 상태 조회", description = "시나리오 생성 진행 상태를 조회합니다.")
public ApiResponse<ScenarioStatusResponse> getScenarioStatus(
@Parameter(description = "시나리오 ID") @PathVariable Long scenarioId
@Parameter(description = "시나리오 ID") @PathVariable Long scenarioId,
Principal principal
) {
// Mock
ScenarioStatusResponse mockResponse = new ScenarioStatusResponse(
scenarioId,
ScenarioStatus.PROCESSING,
"AI가 시나리오를 생성 중입니다..."
);
return ApiResponse.success(mockResponse, "상태를 성공적으로 조회했습니다.");
Long userId = getUserIdFromPrincipal(principal);

ScenarioStatusResponse scenarioStatusResponse = scenarioService.getScenarioStatus(scenarioId, userId);

return ApiResponse.success(scenarioStatusResponse, "상태를 성공적으로 조회했습니다.");
}

@GetMapping("/info/{scenarioId}")
@Operation(summary = "시나리오 상세 조회", description = "완성된 시나리오의 상세 정보를 조회합니다.")
public ApiResponse<ScenarioDetailResponse> getScenarioDetail(
@Parameter(description = "시나리오 ID") @PathVariable Long scenarioId
@Parameter(description = "시나리오 ID") @PathVariable Long scenarioId,
Principal principal
) {
// Mock: 실제로는 큰 DTO라서 null로 처리하거나 간단한 Mock 생성
return ApiResponse.success(null, "시나리오 상세 정보를 성공적으로 조회했습니다.");
Long userId = getUserIdFromPrincipal(principal);

ScenarioDetailResponse scenarioDetailResponse = scenarioService.getScenarioDetail(scenarioId, userId);

return ApiResponse.success(scenarioDetailResponse, "시나리오 상세 정보를 성공적으로 조회했습니다.");
}

@GetMapping("/{scenarioId}/timeline")
@Operation(summary = "시나리오 타임라인 조회", description = "시나리오의 선택 경로를 시간순으로 조회합니다.")
public ApiResponse<TimelineResponse> getScenarioTimeline(
@Parameter(description = "시나리오 ID") @PathVariable Long scenarioId
@Parameter(description = "시나리오 ID") @PathVariable Long scenarioId,
Principal principal
) {
// Mock 타임라인 생성
List<TimelineResponse.TimelineEvent> mockEvents = List.of(
new TimelineResponse.TimelineEvent(2020, "창업 도전"),
new TimelineResponse.TimelineEvent(2022, "해외 진출"),
new TimelineResponse.TimelineEvent(2025, "상장 성공")
);
TimelineResponse mockResponse = new TimelineResponse(scenarioId, mockEvents);
return ApiResponse.success(mockResponse, "타임라인을 성공적으로 조회했습니다.");
Long userId = getUserIdFromPrincipal(principal);

TimelineResponse timelineResponse = scenarioService.getScenarioTimeline(scenarioId, userId);

return ApiResponse.success(timelineResponse, "타임라인을 성공적으로 조회했습니다.");
}

@GetMapping("/baselines")
@Operation(summary = "베이스라인 목록 조회", description = "사용자의 베이스라인 목록을 조회합니다.")
public ApiResponse<List<BaselineListResponse>> getBaselines(
Principal principal
) {
// Mock 베이스라인 목록
List<BaselineListResponse> mockBaselines = List.of(
new BaselineListResponse(1001L, "대학 졸업 이후", List.of("교육", "진로"), LocalDateTime.now()),
new BaselineListResponse(1002L, "미국 대학 이후", List.of("해외", "교육"), LocalDateTime.now())
);
return ApiResponse.success(mockBaselines, "베이스라인 목록을 성공적으로 조회했습니다.");
Long userId = getUserIdFromPrincipal(principal);

List<BaselineListResponse> baselines = scenarioService.getBaselines(userId);

return ApiResponse.success(baselines, "베이스라인 목록을 성공적으로 조회했습니다.");
}

@GetMapping("/compare/{baseId}/{compareId}")
@Operation(summary = "시나리오 비교 분석 결과 조회", description = "두 시나리오를 비교 분석 결과를 조회합니다.")
public ApiResponse<ScenarioCompareResponse> compareScenarios(
@Parameter(description = "기준 시나리오 ID") @PathVariable Long baseId,
@Parameter(description = "비교 시나리오 ID") @PathVariable Long compareId
@Parameter(description = "비교 시나리오 ID") @PathVariable Long compareId,
Principal principal
) {
// Mock: 복잡한 DTO라서 null 처리
return ApiResponse.success(null, "시나리오 비교를 성공적으로 조회했습니다.");
Long userId = getUserIdFromPrincipal(principal);

ScenarioCompareResponse scenarioCompareResponse = scenarioService.compareScenarios(baseId, compareId, userId);

return ApiResponse.success(scenarioCompareResponse, "시나리오 비교를 성공적으로 조회했습니다.");
}

// Principal에서 userId 추출하는 Helper 메서드 (Mock 구현)
// TODO: JWT 파싱으로 교체 예정
private Long getUserIdFromPrincipal(Principal principal) {
if (principal == null || principal.getName() == null) {
throw new ApiException(ErrorCode.HANDLE_ACCESS_DENIED);
}
// TODO: 실제로는 JWT 토큰에서 userId 추출
// String token = principal.getName();
// return jwtProvider.getUserIdFromToken(token);

return 1L; // Mock userId
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public record BaselineListResponse(
* @return BaselineListResponse
*/

// TODO: 구현 필요
public static BaselineListResponse from(BaseLine baseLine, List<String> tags) {
throw new UnsupportedOperationException("구현 예정");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
package com.back.domain.scenario.dto;

import com.back.domain.scenario.entity.Scenario;
import com.back.domain.scenario.entity.SceneCompare;
import com.back.domain.scenario.entity.SceneType;
import com.back.domain.scenario.entity.Type;
import com.back.domain.scenario.entity.*;
import io.swagger.v3.oas.annotations.media.Schema;

import java.util.Arrays;
import java.util.List;

/** │ │
Expand Down Expand Up @@ -48,6 +46,47 @@ public static ScenarioCompareResponse from(
List<SceneType> baseIndicators,
List<SceneType> compareIndicators
) {
throw new UnsupportedOperationException("구현 예정");
// TOTAL 타입에서 종합 분석 추출
String overallAnalysis = compareResults.stream()
.filter(compare -> compare.getResultType() == SceneCompareResultType.TOTAL)
.findFirst()
.map(SceneCompare::getCompareResult)
.orElse("비교 분석 결과가 없습니다.");

// 5개 지표별 비교 결과 생성
List<IndicatorComparison> indicators = Arrays.stream(Type.values())
.map(type -> {
// 기준 시나리오의 해당 지표 점수 찾기
int baseScore = baseIndicators.stream()
.filter(indicator -> indicator.getType() == type)
.findFirst()
.map(SceneType::getPoint)
.orElse(0);

// 비교 시나리오의 해당 지표 점수 찾기
int compareScore = compareIndicators.stream()
.filter(indicator -> indicator.getType() == type)
.findFirst()
.map(SceneType::getPoint)
.orElse(0);

// SceneCompare에서 해당 지표 분석 찾기 (TOTAL 제외하고)
String analysis = compareResults.stream()
.filter(compare -> compare.getResultType() != SceneCompareResultType.TOTAL)
.filter(compare -> compare.getResultType().name().equals(type.name()))
.findFirst()
.map(SceneCompare::getCompareResult)
.orElse(type.name() + " 분석 결과가 없습니다.");

return new IndicatorComparison(type, baseScore, compareScore, analysis);
})
.toList();

return new ScenarioCompareResponse(
baseScenario.getId(),
compareScenario.getId(),
overallAnalysis,
indicators
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public record TimelineEvent(
* @return TimelineResponse
*/

// TODO: DecisionNode에서 TimelineEvent로 변환하는 로직 구현 필요
public static TimelineResponse from(Long scenarioId, List<DecisionNode> decisionNodes) {
throw new UnsupportedOperationException("구현 예정");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
@Entity
@Table(name = "scenarios")
@Getter
@Setter
@Setter // TODO:제거 필요한 롬복만 사용, 롬복 동작원리 공부하기
@NoArgsConstructor
@AllArgsConstructor
@Builder
Expand Down Expand Up @@ -66,10 +66,9 @@ public class Scenario extends BaseEntity {
@Column(columnDefinition = "TEXT")
private String description;

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

// 시나리오 대표 이미지 URL
private String img;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
@AllArgsConstructor
@Builder
public class SceneCompare extends BaseEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "scenario_id", nullable = false)
private Scenario scenario;

@Column(columnDefinition = "TEXT")
private String compareResult;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,64 @@
package com.back.domain.scenario.repository;

import com.back.domain.scenario.entity.Scenario;
import com.back.domain.scenario.entity.ScenarioStatus;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Optional;

/**
* 시나리오 엔티티에 대한 데이터베이스 접근을 담당하는 JpaRepository.
*/
@Repository
public interface ScenarioRepository extends JpaRepository<Scenario, Long> {

// 사용자별 시나리오 조회 (권한 검증)
Optional<Scenario> findByIdAndUserId(Long id, Long userId);

// 베이스 시나리오 존재 확인
boolean existsByDecisionLine_BaseLineId(Long baseLineId);

// 베이스 시나리오 조회 (비교 기준)
Optional<Scenario> findFirstByDecisionLine_BaseLineIdOrderByCreatedDateAsc(Long baseLineId);

// 연관 Entity 정보 포함 조회 (EntityGraph로 N+1 쿼리 방지, 확장 기능 대비)
@EntityGraph(attributePaths = {"user", "decisionLine", "sceneCompare"})
Optional<Scenario> findWithAllRelationsById(Long id);

// 사용자별 특정 상태 시나리오 목록 조회
List<Scenario> findByUserIdAndStatusOrderByCreatedDateDesc(Long userId, ScenarioStatus status);

// DecisionLine 기반 시나리오 존재 확인 (시나리오 중복 생성 방지)
boolean existsByDecisionLineIdAndStatus(Long decisionLineId, ScenarioStatus status);

// 베이스라인별 완료된 시나리오 조회 (비교용)
List<Scenario> findByDecisionLine_BaseLineIdAndStatusOrderByCreatedDateAsc(Long baseLineId, ScenarioStatus status);

// 사용자의 최신 시나리오 조회 (확장 기능 대비)
Optional<Scenario> findTopByUserIdOrderByCreatedDateDesc(Long userId);

// 사용자별 베이스라인 시나리오 제외 시나리오 목록 조회 (MyPage용, 페이징구현 및 N+1 방지)
@EntityGraph(attributePaths = {"user", "decisionLine", "decisionLine.baseLine"})
@Query("SELECT s FROM Scenario s " +
"WHERE s.user.id = :userId " +
"AND s.decisionLine.baseLine.id = :baseLineId " +
"AND s.status = :status " +
"AND s.id != (SELECT MIN(s2.id) FROM Scenario s2 " +
"WHERE s2.decisionLine.baseLine.id = :baseLineId " +
"AND s2.status = :status) " +
"ORDER BY s.createdDate DESC")
Page<Scenario> findUserNonBaseScenariosByBaseLineId(@Param("userId") Long userId,
@Param("baseLineId") Long baseLineId,
@Param("status") ScenarioStatus status,
Pageable pageable);

// 특정 상태의 시나리오들 조회 (상태별 처리용)
List<Scenario> findByStatusOrderByCreatedDateAsc(ScenarioStatus status);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.back.domain.scenario.repository;

import com.back.domain.scenario.entity.SceneCompare;
import com.back.domain.scenario.entity.SceneCompareResultType;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

/**
* 시나리오 비교 결과 엔티티에 대한 데이터베이스 접근을 담당하는 JpaRepository.
*/
@Repository
public interface SceneCompareRepository extends JpaRepository<SceneCompare, Long> {

// 특정 시나리오의 모든 비교 결과 조회 (6개: TOTAL + 5개 지표)
List<SceneCompare> findByScenarioIdOrderByResultType(Long scenarioId);

// 특정 시나리오의 특정 타입 비교 결과 조회
SceneCompare findByScenarioIdAndResultType(Long scenarioId, SceneCompareResultType resultType);
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,25 @@
package com.back.domain.scenario.repository;

import com.back.domain.scenario.entity.SceneType;
import com.back.domain.scenario.entity.Type;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Optional;

/**
* 시나리오 유형 엔티티에 대한 데이터베이스 접근을 담당하는 JpaRepository.
*/
@Repository
public interface SceneTypeRepository extends JpaRepository<SceneType, Long> {

// 특정 시나리오의 지표들 조회 (타입 순서대로)
List<SceneType> findByScenarioIdOrderByTypeAsc(Long scenarioId);

// 특정 시나리오의 특정 지표 조회
Optional<SceneType> findByScenarioIdAndType(Long scenarioId, Type type);

// 시나리오별 지표 존재 여부 확인
boolean existsByScenarioId(Long scenarioId);
}
Loading