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
10 changes: 9 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,12 @@ db_dev.trace.db
**/generated/
**/build/generated/
src/main/generated/
back/src/main/generated/
back/src/main/generated/

# AI 생성 이미지 저장 경로
uploads/
./uploads/

# 테스트 이미지 경로 (LocalStorageServiceTest)
test-uploads/
./test-uploads/
8 changes: 8 additions & 0 deletions back/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,11 @@ out/

### Environment Variables ###
.env

# AI 생성 이미지 저장 경로
uploads/
./uploads/

# 테스트 이미지 경로 (LocalStorageServiceTest)
test-uploads/
./test-uploads/
3 changes: 3 additions & 0 deletions back/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")

// AWS SDK for S3
implementation("software.amazon.awssdk:s3:2.20.+")

// macOS Netty 네이티브 DNS 리졸버 (WebFlux 필요)
val isMacOS: Boolean = System.getProperty("os.name").startsWith("Mac OS X")
val architecture = System.getProperty("os.arch").lowercase()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import com.back.domain.scenario.dto.*;
import com.back.domain.scenario.service.ScenarioService;
import com.back.global.common.PageResponse;
import com.back.global.exception.ApiException;
import com.back.global.exception.ErrorCode;
import com.back.global.security.CustomUserDetails;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
Expand All @@ -30,12 +32,11 @@ public class ScenarioController {

/**
* 인증된 사용자의 ID를 안전하게 추출합니다.
* 테스트 환경에서 userDetails가 null일 수 있으므로 기본값을 제공합니다.
* 인증되지 않은 사용자는 예외를 발생시킵니다.
*/
private Long getUserId(CustomUserDetails userDetails) {
if (userDetails == null || userDetails.getUser() == null) {
// 테스트 환경이나 인증이 비활성화된 환경에서는 기본 사용자 ID 사용
return 1L;
if (userDetails == null || userDetails.getUser() == null || userDetails.getUser().getId() == null) {
throw new ApiException(ErrorCode.HANDLE_ACCESS_DENIED, "인증이 필요한 서비스입니다.");
}
return userDetails.getUser().getId();
}
Expand All @@ -49,6 +50,14 @@ public ResponseEntity<ScenarioStatusResponse> createScenario(
) {
Long userId = getUserId(userDetails);

// lastDecision 기본 검증: userId 일치 확인
if (lastDecision != null && lastDecision.userId() != null) {
if (!lastDecision.userId().equals(userId)) {
throw new ApiException(ErrorCode.HANDLE_ACCESS_DENIED,
"lastDecision의 userId가 인증된 사용자와 일치하지 않습니다.");
}
}

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

return ResponseEntity.status(HttpStatus.CREATED).body(scenarioCreateResponse);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.back.domain.scenario.dto;

import com.back.global.ai.dto.result.BaseScenarioResult;
import com.back.global.ai.dto.result.DecisionScenarioResult;
import lombok.Getter;

/**
* AI 시나리오 생성 결과를 담는 래퍼 클래스.
* 트랜잭션 분리를 위해 베이스 시나리오와 결정 시나리오 결과를 구분하여 전달합니다.
*/
@Getter
public class AiScenarioGenerationResult {
private final boolean isBaseScenario;
private final BaseScenarioResult baseResult;
private final DecisionScenarioResult decisionResult;

// 베이스 시나리오용 생성자
public AiScenarioGenerationResult(BaseScenarioResult baseResult) {
this.isBaseScenario = true;
this.baseResult = baseResult;
this.decisionResult = null;
}

// 결정 시나리오용 생성자
public AiScenarioGenerationResult(DecisionScenarioResult decisionResult) {
this.isBaseScenario = false;
this.baseResult = null;
this.decisionResult = decisionResult;
}
}
12 changes: 10 additions & 2 deletions back/src/main/java/com/back/domain/scenario/entity/Scenario.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,15 @@
* AI를 통해 생성된 시나리오의 상세 정보와 처리 상태를 저장합니다.
*/
@Entity
@Table(name = "scenarios")
@Table(name = "scenarios",
indexes = {
@Index(name = "idx_scenario_user_status", columnList = "user_id, status, created_date"),
@Index(name = "idx_scenario_baseline", columnList = "base_line_id")
},
uniqueConstraints = {
@UniqueConstraint(name = "uk_scenario_decision_line", columnNames = "decision_line_id")
}
)
@Getter
@Setter
@NoArgsConstructor
Expand All @@ -30,7 +38,7 @@ public class Scenario extends BaseEntity {

// 시나리오 생성의 기반이 된 선택 경로
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "decision_line_id", unique = true)
@JoinColumn(name = "decision_line_id")
private DecisionLine decisionLine;

// 시나리오가 속한 베이스라인 (하나의 BaseLine에 여러 Scenario 가능)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,9 @@ public interface SceneTypeRepository extends JpaRepository<SceneType, Long> {
@Query("SELECT st FROM SceneType st WHERE st.scenario.id IN :scenarioIds")
List<SceneType> findByScenarioIdIn(@Param("scenarioIds") List<Long> scenarioIds);

// 여러 시나리오의 지표들을 배치 조회 (시나리오 ID, 타입 순서대로 정렬)
@Query("SELECT st FROM SceneType st WHERE st.scenario.id IN :scenarioIds ORDER BY st.scenario.id ASC, st.type ASC")
List<SceneType> findByScenarioIdInOrderByScenarioIdAscTypeAsc(@Param("scenarioIds") List<Long> scenarioIds);

List<SceneType> findByScenarioId(Long scenarioId);
}
Loading