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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,6 @@ shrimp-rules.md
shrimp_data
docs
.gstack/

# Gemini CLI
.gemini
4 changes: 4 additions & 0 deletions app/src/main/resources/application-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ spring:
thinking-budget: 0

q-asker:
ai:
gcs:
bucket-name: ci-dummy

hashid:
salt: ci-dummy-salt
min-length: 8
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/resources/config/ai-setting.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ spring:
top-p: 0.6
model: gemini-3-flash-preview
thinking-budget: 0
max-output-tokens: 25000


q-asker:
ai:
chat-timeout-ms: 180000
equalization-model: gemini-2.5-flash-lite
ox-thinking-level: LOW
chunk:
max-count-variants: 5
2 changes: 1 addition & 1 deletion app/src/main/resources/config/app-common.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ server:

spring:
lifecycle:
timeout-per-shutdown-phase: 60s
timeout-per-shutdown-phase: 300s

servlet:
multipart:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
CREATE TABLE feedback_board
(
feedback_board_id BIGINT AUTO_INCREMENT PRIMARY KEY KEY,
user_id VARCHAR(255),
content LONGTEXT,
created_at DATETIME(6) NOT NULL
);
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ plugins {
group = "com.icc.qasker"
// 프로젝트 버전 (Docker 이미지 태그, 배포 아티팩트 버전에 사용)
// 예: jib으로 빌드하면 Docker 이미지에 "1.7.0" 태그가 붙음
version = "2.4.2"
version = "2.4.3"

// Git hooks 경로를 .githooks/로 자동 설정
// 예: ./gradlew build 실행 시 자동으로 git config core.hooksPath .githooks 적용
Expand Down
2 changes: 1 addition & 1 deletion infra/blue-green/deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ EOF
# ==============================================================================
MAX_RETRIES=36
SLEEP_TIME=5
SHUTDOWN_TIMEOUT=60
SHUTDOWN_TIMEOUT=250
PULL_TIMEOUT=120
BLUE_PORT="${1:?BLUE_PORT is required}"
GREEN_PORT="${2:?GREEN_PORT is required}"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.icc.qasker.board.controller;

import com.icc.qasker.board.controller.dto.PostFeedbackRequest;
import com.icc.qasker.board.service.FeedbackService;
import com.icc.qasker.global.annotation.RateLimit;
import com.icc.qasker.global.annotation.UserId;
import com.icc.qasker.global.ratelimit.RateLimitTier;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Tag(name = "Board", description = "피드백 관련 API")
@RestController
@RequestMapping("/feedback")
@RequiredArgsConstructor
public class FeedbackBoardController {

private final FeedbackService feedbackService;

@Operation(summary = "피드백을 작성한다")
@RateLimit(RateLimitTier.WRITE)
@PostMapping
public ResponseEntity<Void> postFeedback(
@UserId String userId, @Valid @RequestBody PostFeedbackRequest request) {
feedbackService.postFeedback(userId, request);
return ResponseEntity.accepted().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.icc.qasker.board.controller.dto;

import jakarta.validation.constraints.NotBlank;

public record PostFeedbackRequest(@NotBlank(message = "피드백이 존재하지 않습니다.") String content) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.icc.qasker.board.entity;

import com.icc.qasker.global.entity.CreatedAt;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@Table(name = "feedback_board")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class FeedbackBoard extends CreatedAt {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long feedbackBoardId;

@Column(name = "user_id")
private String userId;

@Column(name = "content", columnDefinition = "LONGTEXT")
private String content;

@Builder
public FeedbackBoard(String userId, String content) {
this.userId = userId;
this.content = content;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.icc.qasker.board.repository;

import com.icc.qasker.board.entity.FeedbackBoard;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface FeedbackBoardRepository extends JpaRepository<FeedbackBoard, Long> {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.icc.qasker.board.service;

import com.icc.qasker.board.controller.dto.PostFeedbackRequest;
import com.icc.qasker.board.entity.FeedbackBoard;
import com.icc.qasker.board.repository.FeedbackBoardRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class FeedbackService {

private final FeedbackBoardRepository feedbackBoardRepository;

public void postFeedback(String userId, PostFeedbackRequest request) {
FeedbackBoard feedbackBoard =
FeedbackBoard.builder().userId(userId).content(request.content()).build();
feedbackBoardRepository.save(feedbackBoard);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ public class OciObjectStorageServiceImpl implements ObjectStorageService {
private final CdnProperties cdnProperties;
private final OciObjectStorageProperties ociProperties;
private final UploadManager uploadManager;
private final Timer uploadTimer;
private final Timer pdfUploadTimer;
private final Timer imageUploadTimer;

public OciObjectStorageServiceImpl(
CdnProperties cdnProperties,
Expand All @@ -37,16 +38,22 @@ public OciObjectStorageServiceImpl(
this.cdnProperties = cdnProperties;
this.ociProperties = ociProperties;
this.uploadManager = uploadManager;
this.uploadTimer =
this.pdfUploadTimer =
Timer.builder("file.upload.oci.duration")
.description("OCI Object Storage 파일 업로드 소요 시간")
.description("OCI Object Storage PDF 업로드 소요 시간")
.tag("type", "pdf")
.register(registry);
this.imageUploadTimer =
Timer.builder("file.upload.oci.duration")
.description("OCI Object Storage 이미지 업로드 소요 시간")
.tag("type", "image")
.register(registry);
}

@Override
public String uploadImage(
InputStream inputStream, long contentLength, String contentType, String originalFileName) {
return uploadTimer.record(
return imageUploadTimer.record(
() -> {
String extension = originalFileName.substring(originalFileName.lastIndexOf("."));
String objectName = "images/" + UUID.randomUUID() + extension;
Expand Down Expand Up @@ -77,7 +84,7 @@ public String uploadImage(

@Override
public String uploadPdf(Path pdfFile, String originalFileName) {
return uploadTimer.record(
return pdfUploadTimer.record(
() -> {
String uuid = UUID.randomUUID().toString();
String objectName = uuid + ".pdf";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public class GeminiClientConfig {
private final QAskerAiProperties aiProperties;

@Bean
@org.springframework.context.annotation.Profile("!test")
public Client googleGenAiClient(GoogleGenAiConnectionProperties properties) {
return Client.builder()
.project(properties.getProjectId())
Expand All @@ -27,6 +28,15 @@ public Client googleGenAiClient(GoogleGenAiConnectionProperties properties) {
.build();
}

@Bean
@org.springframework.context.annotation.Profile("test")
public Client googleGenAiClientTest() {
return Client.builder()
.apiKey("ci-dummy-key")
.httpOptions(HttpOptions.builder().timeout(aiProperties.getChatTimeoutMs()).build())
.build();
}

@Bean
public ChunkProperties chunkProperties() {
return aiProperties.getChunk();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import java.util.concurrent.atomic.AtomicInteger;
import lombok.Getter;
import lombok.Setter;
import org.springframework.ai.google.genai.common.GoogleGenAiThinkingLevel;
import org.springframework.boot.context.properties.ConfigurationProperties;

@Getter
Expand All @@ -16,12 +15,6 @@ public class QAskerAiProperties {
/** Gemini Chat API 타임아웃 (ms) */
private int chatTimeoutMs = 90_000;

/** 선택지 균등화에 사용할 모델 (미설정 시 기본 모델 사용) */
private String equalizationModel;

/** OX 퀴즈 생성 시 thinking level 오버라이드 (미설정 시 글로벌 기본값 사용) */
private GoogleGenAiThinkingLevel oxThinkingLevel;

/** 청크 분할 설정 */
private Chunk chunk = new Chunk();

Expand Down
Loading
Loading