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
11 changes: 10 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.5.9'
id 'org.springframework.boot' version '3.4.2'
id 'io.spring.dependency-management' version '1.1.7'
}

Expand All @@ -24,6 +24,12 @@ repositories {
mavenCentral()
}

dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:2024.0.0"
}
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
Expand Down Expand Up @@ -55,6 +61,9 @@ dependencies {
// redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'

// openfeign
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'

}

tasks.named('test') {
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/sopt/comfit/ComfitApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@EnableFeignClients
@SpringBootApplication
public class ComfitApplication {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package sopt.comfit.company.domain;

import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface CompanyIssueRepository extends JpaRepository<CompanyIssue, Long> {
List<CompanyIssue> findByCompanyId(Long companyId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package sopt.comfit.report.controller;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.web.bind.annotation.*;
import sopt.comfit.global.annotation.LoginUser;
import sopt.comfit.global.dto.PageDto;
import sopt.comfit.report.dto.command.MatchExperienceCommandDto;
import sopt.comfit.report.dto.request.MatchExperienceRequestDto;
import sopt.comfit.report.dto.response.AIReportResponseDto;
import sopt.comfit.report.dto.response.GetReportSummaryResponseDto;
import sopt.comfit.report.service.AIReportService;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/ai-reports")
public class AIReportController implements AIReportSwagger {

private final AIReportService aiReportService;

@Override
public AIReportResponseDto matchExperience(@LoginUser Long userId,
@Valid @RequestBody MatchExperienceRequestDto requestDto){
return aiReportService.matchExperience(MatchExperienceCommandDto.of(userId, requestDto));
}

@Override
public PageDto<GetReportSummaryResponseDto> getReportList(@LoginUser Long userId,
@RequestParam(defaultValue = "0") int page,
@RequestParam(required = false) String keyword){
Pageable pageable = PageRequest.of(page, 4);
return aiReportService.getReportList(userId, pageable, keyword);
}

@Override
public AIReportResponseDto getReport(@LoginUser Long userId,
@PathVariable Long reportId){
return aiReportService.getReport(userId, reportId);
}
}
97 changes: 97 additions & 0 deletions src/main/java/sopt/comfit/report/controller/AIReportSwagger.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package sopt.comfit.report.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import sopt.comfit.global.annotation.LoginUser;
import sopt.comfit.global.dto.CommonApiResponse;
import sopt.comfit.global.dto.CustomErrorResponse;
import sopt.comfit.global.dto.PageDto;
import sopt.comfit.report.dto.request.MatchExperienceRequestDto;
import sopt.comfit.report.dto.response.AIReportResponseDto;
import sopt.comfit.report.dto.response.GetReportSummaryResponseDto;

@Tag(name = "AI-Report", description = "AI-Report 관련 API")
public interface AIReportSwagger {

@Operation(
summary = "AI 리포트 생성 API",
description = "AI 리포트 생성 API입니다"
)
@ApiResponses({
@ApiResponse(responseCode = "201", description = "AI 응답 생성 성공",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = CommonApiResponse.class))),

@ApiResponse(responseCode = "403", description = "권한 오류",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = CustomErrorResponse.class))),
@ApiResponse(responseCode = "401", description = "헤더값 오류",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = CustomErrorResponse.class))),
@ApiResponse(responseCode = "404", description = "사용자/기업/경험 id 값 오류",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = CustomErrorResponse.class))),
@ApiResponse(responseCode = "500", description = "응답 파싱 및 AI 호출 실패",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = CustomErrorResponse.class)))
})
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
@SecurityRequirement(name = "JWT")
AIReportResponseDto matchExperience(@LoginUser Long userId,
@Valid @RequestBody MatchExperienceRequestDto requestDto);

@Operation(
summary = "AI_Report 리스트 조회/ 검색",
description = "AI_Report 리스트 조회/검색 API입니다, " +
"KeyWord 값은 선택입니다"
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "AI Report 리스트 조회 성공",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = CommonApiResponse.class))),

@ApiResponse(responseCode = "403", description = "권한 오류",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = CustomErrorResponse.class))),
@ApiResponse(responseCode = "401", description = "헤더값 오류",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = CustomErrorResponse.class)))
})
@GetMapping
@SecurityRequirement(name = "JWT")
PageDto<GetReportSummaryResponseDto> getReportList(@LoginUser Long userId,
@RequestParam(defaultValue = "0") int page,
@RequestParam(required = false) String keyword);
@Operation(
summary = "Report 단일 조회",
description = "Report 단일 조회 API입니다"
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "AI Report 단일 조회",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = CommonApiResponse.class))),

@ApiResponse(responseCode = "403", description = "권한 오류",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = CustomErrorResponse.class))),
@ApiResponse(responseCode = "401", description = "헤더값 오류",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = CustomErrorResponse.class))),
@ApiResponse(responseCode = "404", description = "리포트 id 값 오류",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = CustomErrorResponse.class)))
})
@GetMapping("/{reportId}")
@SecurityRequirement(name = "JWT")
AIReportResponseDto getReport(@LoginUser Long userId,
@PathVariable Long reportId);
}
54 changes: 50 additions & 4 deletions src/main/java/sopt/comfit/report/domain/AIReport.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@

import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.type.SqlTypes;
import sopt.comfit.company.domain.Company;
import sopt.comfit.experience.domain.Experience;
import sopt.comfit.global.base.BaseTimeEntity;
import sopt.comfit.user.domain.User;

@Entity
@Getter
Expand All @@ -17,12 +20,15 @@ public class AIReport extends BaseTimeEntity {
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@JdbcTypeCode(SqlTypes.JSON)
@Column(name = "perspectives", nullable = false, columnDefinition = "JSONB")
private String perspectives;

@Column(name = "density", nullable = false, columnDefinition = "TEXT")
@JdbcTypeCode(SqlTypes.JSON)
@Column(name = "density", nullable = false, columnDefinition = "JSONB")
private String density;

@JdbcTypeCode(SqlTypes.JSON)
@Column(name = "appeal_point", nullable = false, columnDefinition = "JSONB")
private String appealPoint;

Expand All @@ -33,10 +39,50 @@ public class AIReport extends BaseTimeEntity {
private String guidance;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
@JoinColumn(name = "experience_id", nullable = false)
private Experience experience;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "company_id", nullable = false)
private Company company;

@Builder(access = AccessLevel.PROTECTED)
private AIReport(final String perspectives,
final String density,
final String appealPoint,
final String suggestion,
final String guidance,
final Experience experience,
final Company company) {
this.perspectives = perspectives;
this.density = density;
this.appealPoint = appealPoint;
this.suggestion = suggestion;
this.guidance = guidance;
this.experience = experience;
this.company = company;
}

public static AIReport create(
final String perspectives,
final String density,
final String appealPoint,
final String suggestion,
final String guidance,
final Experience experience,
final Company company) {

return AIReport.builder()
.perspectives(perspectives)
.density(density)
.appealPoint(appealPoint)
.suggestion(suggestion)
.guidance(guidance)
.experience(experience)
.company(company)
.build();
}



}
19 changes: 19 additions & 0 deletions src/main/java/sopt/comfit/report/domain/AIReportRepository.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,28 @@
package sopt.comfit.report.domain;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.Optional;

public interface AIReportRepository extends JpaRepository<AIReport, Long> {
Page<AIReport> findByExperienceUserId(Long experienceUserId, Pageable pageable);
@Query("""
SELECT ar FROM AIReport ar
JOIN ar.experience e
JOIN ar.company c
WHERE e.user.id = :userId
AND (c.name LIKE %:keyword%)
""")
Page<AIReport> findByExperienceUserIdAndKeyword(
@Param("userId") Long userId,
@Param("keyword") String keyword,
Pageable pageable
);

Optional<AIReport> findByExperienceUserIdAndId(Long userId, Long id);
boolean existsByCompanyIdAndUserId(Long companyId, Long userId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package sopt.comfit.report.dto.command;

import sopt.comfit.report.dto.request.MatchExperienceRequestDto;

public record MatchExperienceCommandDto(
Long userId,

Long companyId,

Long experienceId

) {
public static MatchExperienceCommandDto of(Long userId, MatchExperienceRequestDto request) {
return new MatchExperienceCommandDto(userId, request.companyId(), request.experienceId());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package sopt.comfit.report.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;

public record MatchExperienceRequestDto(
@NotNull
@Schema(example = "1")
Long companyId,

@NotNull
@Schema(example = "1")
Long experienceId
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package sopt.comfit.report.dto.response;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import sopt.comfit.global.exception.BaseException;
import sopt.comfit.report.domain.AIReport;
import sopt.comfit.report.exception.AIReportErrorCode;

import java.util.List;

public record AIReportResponseDto(
String companyName,

String experienceTitle,

List<Perspective> perspectives,

List<Density> density,

List<AppealPoint> appealPoint,

String suggestion,

String guidance
) {
public record Perspective(String perspective, String source, String reason) {}
public record Density(String perspective, String connection, String reason) {}
public record AppealPoint(String element, String importance, String starPhase, String direction, String placement) {}

private static final ObjectMapper objectMapper = new ObjectMapper();

public static AIReportResponseDto from(AIReport report) {
return new AIReportResponseDto(
report.getCompany().getName(),
report.getExperience().getTitle(),
fromJson(report.getPerspectives(), new TypeReference<List<Perspective>>() {}),
fromJson(report.getDensity(), new TypeReference<List<Density>>() {}),
fromJson(report.getAppealPoint(), new TypeReference<List<AppealPoint>>() {}),
report.getSuggestion(),
report.getGuidance()
);
}

private static <T> T fromJson(String json, TypeReference<T> typeRef) {
try {
return objectMapper.readValue(json, typeRef);
} catch (JsonProcessingException e) {
throw BaseException.type(AIReportErrorCode.AI_RESPONSE_PARSE_FAILED);
}
}
}

Loading