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
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ Jira와 GitHub를 동시에 관리해야 하는 수고를 줄이고 팀의 Git
### 📚 기술 아티클
|제목|작성자|
|---|---|
|Claude Code 개념부터 활용까지 (Harness, Context, Skills) (작성 예정)|홍지운, 황주희|
|[Claude Code 개념부터 활용까지 (Harness, Context, Skills)](https://github.com/softeerbootcamp-7th/WEB-Team4-Refit/wiki/%5B%ED%99%8D%EC%A7%80%EC%9A%B4%2C-%ED%99%A9%EC%A3%BC%ED%9D%AC%5D-Claude-Code-%EA%B0%9C%EB%85%90%EB%B6%80%ED%84%B0-%ED%99%9C%EC%9A%A9%EA%B9%8C%EC%A7%80-%28Harness%2C-Context%2C-Skills%29) |홍지운, 황주희|
|Orval과 n8n을 활용한 OpenAPI 주도 개발 (작성 예정)|홍지운, 황주희|
|Web Speech API: 서버 구축 없이 Realtime STT 구현하기 (작성 예정)|홍지운|
|PDF.js 기반 대용량 문서 하이라이팅 구현 및 렌더링 이슈 개선(작성 예정)|황주희|
Expand Down Expand Up @@ -213,9 +213,10 @@ Jira와 GitHub를 동시에 관리해야 하는 수고를 줄이고 팀의 Git
### 📚 기술 아티클
|파트|제목|작성자|
|---|---|---|
|BE|질문 카테고리 분류를 위한 클러스터링 배치 로직 설계 과정 (작성 예정)|권찬|
|BE|[질문 카테고리 분류를 위한 클러스터링 배치 로직 설계 과정](https://github.com/softeerbootcamp-7th/WEB-Team4-Refit/wiki/%5B%EA%B6%8C%EC%B0%AC%5D-%EC%A7%88%EB%AC%B8-%EC%B9%B4%ED%85%8C%EA%B3%A0%EB%A6%AC-%EB%B6%84%EC%84%9D%EC%9D%84-%EC%9C%84%ED%95%9C-%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0%EB%A7%81-%EB%A1%9C%EC%A7%81-%EC%84%A4%EA%B3%84-%EA%B3%BC%EC%A0%95)|권찬|
|BE|면접 기록의 질답 변환이 실패하는 문제의 해결 과정 (작성 예정)|권찬|
|BE|rest client와 web client의 성능 비교(작성 예정)|송영범|
|BE|[AWS S3 Presigned URL 무한 업로드 문제 해결](https://github.com/softeerbootcamp-7th/WEB-Team4-Refit/wiki/%5B송영범%5D-S3-Presigned%E2%80%90URL-무한-업로드-문제)|송영범|
|BE|벡터 데이터베이스 활용을 위한 객체지향적인 인터페이스 설계 과정 (작성 예정)|이장안|

### 기술 스택
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ public final class InterviewConstant {

public static final int RAW_TEXT_MAX_LENGTH = 10_000;
public static final int KPT_TEXT_MAX_LENGTH = 8_000;
public static final long QNA_SET_CONVERT_RESULT_TIMEOUT_MILLISECONDS = 30_000L;
public static final long QNA_SET_CONVERT_RESULT_TIMEOUT_MILLISECONDS = 180_000L;
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public void startRawTextConvertAsync(Long interviewId) {
GeminiGenerateRequest requestBody = GeminiGenerateRequest.from(prompt);
CompletableFuture<GeminiGenerateResponse> future =
geminiClient.sendAsyncTextGenerateRequest(requestBody, GenerateModel.GEMMA_3_27B_IT);
log.info("request sended");
log.info("[startRawTextConvertAsync] request sended");
future.thenApplyAsync(
response -> {
rawTextConvertService.processConvertSuccess(interviewId, response);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import com.shyashyashya.refit.global.gemini.dto.GeminiEmbeddingRequest;
import com.shyashyashya.refit.global.property.ClusteringProperty;
import com.shyashyashya.refit.global.vectordb.model.ScoredVector;
import java.util.ArrayList;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand Down Expand Up @@ -73,13 +74,36 @@ public void handleQuestionBatchEmbeddingEvent(QuestionBatchEmbeddingEvent event)
private List<List<Float>> generateEmbeddings(List<QnaSet> qnaSets) {
var outputDimensionality = GeminiEmbeddingRequest.OutputDimensionality.fromValue(
questionVectorRepository.getCollectionVectorDimension());
var requests = new GeminiBatchEmbeddingRequest(qnaSets.stream()
.map(qnaSet -> GeminiBatchEmbeddingRequest.GeminiEmbeddingRequest.of(
qnaSet.getQuestionText(),
GeminiEmbeddingRequest.TaskType.SEMANTIC_SIMILARITY,
outputDimensionality))
.toList());
return geminiClient.sendAsyncBatchEmbeddingRequest(requests).join().embeddings().stream()

List<GeminiBatchEmbeddingResponse.Embedding> embeddings = new ArrayList<>();
List<QnaSet> sendQnaSets = new ArrayList<>();
for (int i = 0; i < qnaSets.size(); i++) {
sendQnaSets.add(qnaSets.get(i));
if (sendQnaSets.size() == 100) {
var requests = new GeminiBatchEmbeddingRequest(sendQnaSets.stream()
.map(qnaSet -> GeminiBatchEmbeddingRequest.GeminiEmbeddingRequest.of(
qnaSet.getQuestionText(),
GeminiEmbeddingRequest.TaskType.SEMANTIC_SIMILARITY,
outputDimensionality))
.toList());
embeddings.addAll(geminiClient.sendAsyncBatchEmbeddingRequest(requests).join().embeddings().stream()
.toList());
sendQnaSets.clear();
}
}

if (!qnaSets.isEmpty()) {
var requests = new GeminiBatchEmbeddingRequest(sendQnaSets.stream()
.map(qnaSet -> GeminiBatchEmbeddingRequest.GeminiEmbeddingRequest.of(
qnaSet.getQuestionText(),
GeminiEmbeddingRequest.TaskType.SEMANTIC_SIMILARITY,
outputDimensionality))
.toList());
embeddings.addAll(geminiClient.sendAsyncBatchEmbeddingRequest(requests).join().embeddings().stream()
.toList());
}

return embeddings.stream()
.map(GeminiBatchEmbeddingResponse.Embedding::values)
.toList();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.jspecify.annotations.NonNull;
import org.slf4j.MDC;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
Expand Down Expand Up @@ -59,6 +60,7 @@ protected void doFilterInternal(

requestUserContext.setEmail(email);
requestUserContext.setUserId(userId);
MDC.put("userInfo", email);

if (isGuestRequest(userId)) {
validateGuestRequestNotIllegal(request);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.shyashyashya.refit.global.config;

import com.shyashyashya.refit.global.filter.LoggingFilter;
import com.shyashyashya.refit.global.filter.ApiLoggingFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
Expand All @@ -14,9 +14,9 @@ public class LoggingFilterConfig {
private final HandlerExceptionResolver handlerExceptionResolver;

@Bean
public FilterRegistrationBean<LoggingFilter> loggingFilter() {
FilterRegistrationBean<LoggingFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new LoggingFilter(handlerExceptionResolver));
public FilterRegistrationBean<ApiLoggingFilter> loggingFilter() {
FilterRegistrationBean<ApiLoggingFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new ApiLoggingFilter(handlerExceptionResolver));
registrationBean.addUrlPatterns("/*");
registrationBean.setOrder(org.springframework.core.Ordered.HIGHEST_PRECEDENCE + 1); // CORS 다음, 인증/보안 필터 전에 실행
return registrationBean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public Executor embeddingTaskExecutor() {
executor.setCorePoolSize(2);
executor.setMaxPoolSize(5);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("QuestionEmbedding-Thread-");
executor.setThreadNamePrefix("qna-embed-");
executor.initialize();
return executor;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.web.reactive.function.client.ExchangeStrategies;
import org.springframework.web.reactive.function.client.WebClient;

@Configuration
public class WebClientConfig {

@Bean
public WebClient webClient(WebClient.Builder builder) {
return builder.build();
ExchangeStrategies strategies = ExchangeStrategies.builder()
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024))
.build();
return builder.exchangeStrategies(strategies).build();
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

@RequiredArgsConstructor
@Slf4j
public class LoggingFilter extends OncePerRequestFilter {
public class ApiLoggingFilter extends OncePerRequestFilter {

private final HandlerExceptionResolver handlerExceptionResolver;
private static final String TRACE_ID = "traceId";
Expand All @@ -39,12 +39,14 @@ protected void doFilterInternal(
String clientIp = getClientIp(request);
String queryString = request.getQueryString();
String decodedQueryString = "";
MDC.put("userInfo", clientIp);

if (queryString != null && !queryString.isEmpty()) {
decodedQueryString = "?" + URLDecoder.decode(queryString, StandardCharsets.UTF_8);
}
String requestUriWithQuery = uri + decodedQueryString;

log.info("[Request] {} {} (IP: {})", method, requestUriWithQuery, clientIp);
log.info("[Req ] {} {}", method, requestUriWithQuery);
long startTime = System.currentTimeMillis();

try {
Expand All @@ -54,7 +56,8 @@ protected void doFilterInternal(
} finally {
long duration = System.currentTimeMillis() - startTime;
int status = response.getStatus();
log.info("[Response] HTTP {} ({} {}) - {}ms", status, method, requestUriWithQuery, duration);
log.info("[Resp] HTTP {} ({} {}) - {}ms", status, method, requestUriWithQuery, duration);
MDC.remove("userInfo");
MDC.clear();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import java.util.concurrent.CompletableFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;
Expand Down Expand Up @@ -68,13 +69,21 @@ public CompletableFuture<GeminiEmbeddingResponse> sendAsyncEmbeddingRequest(Gemi

public CompletableFuture<GeminiBatchEmbeddingResponse> sendAsyncBatchEmbeddingRequest(
GeminiBatchEmbeddingRequest requestBody) {
log.info(
"[sendAsyncBatchEmbeddingRequest] send generate embedding in batch: request size {}",
requestBody.requests().size());
return webClient
.post()
.uri(EMBEDDING_BATCH_ENDPOINT)
.header("x-goog-api-key", geminiProperty.apiKey())
.accept(MediaType.APPLICATION_JSON)
.bodyValue(requestBody)
.retrieve()
.onStatus(HttpStatusCode::isError, response -> response.bodyToMono(String.class)
.flatMap(body -> {
log.error("[sendAsyncBatchEmbeddingRequest] gemini error response : {}", body);
return response.createException();
}))
.bodyToMono(GeminiBatchEmbeddingResponse.class)
.timeout(Duration.ofSeconds(geminiProperty.webClientRequestTimeoutSec()))
.toFuture();
Expand Down
2 changes: 1 addition & 1 deletion backend/src/main/resources/application-gemini.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ spring:
prompt-path: "${GEMINI_PROMPT_PATH}"
rest-client-connect-timeout-sec: 3
rest-client-read-timeout-sec: 30
web-client-request-timeout-sec: 30
web-client-request-timeout-sec: 180
4 changes: 2 additions & 2 deletions backend/src/main/resources/logback-spring.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@
<!-- Console Appender -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(%5p) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%X{traceId:-NO_TRACE}]){yellow} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}</pattern>
<pattern>%clr(%d{MM-dd HH:mm:ss.SS}){faint} %clr(%5p) %clr([%X{traceId:-NO_TRACE}]){yellow} %clr([%-15.-15X{userInfo:-NO_INFO}]){blue} %clr([%13.13t]){faint} %clr(%-26.26logger{26}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}</pattern>
</encoder>
</appender>

<!-- Rolling File Appender (dev, prod) -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/refit-application.log</file>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %5p ${PID:- } --- [%X{traceId:-NO_TRACE}] [%15.15t] %-40.40logger{39} : %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}</pattern>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SS} %5p [%X{traceId:-NO_TRACE}] [%-15.-15X{userInfo:-NO_INFO}] [%13.13t] %-26.26logger{26} : %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}</pattern>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 매일 로그 파일을 새로 만들고, 최대 30일 보관 -->
Expand Down
6 changes: 6 additions & 0 deletions frontend/.claude/docs/design-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ Reusable UI components are in `src/ui/components/` and exported from `src/ui/com

- `Button`, `Input`, `Modal`, `Badge`, `Border`, `Checkbox`
- `NativeCombobox`, `SearchableCombobox`, `PlainCombobox`

### 접근성 (A11y) 규칙

- `Input`, `NativeCombobox`: `label` prop 전달 시 `useId()`로 자동 생성한 id를 `<label htmlFor>` — `<input id>` 에 연결. 외부에서 `id` prop을 직접 전달하면 해당 값 우선 사용.
- 아이콘 전용 버튼에는 반드시 `aria-label` 추가.
- 커스텀 인터랙티브 요소에는 `focus-visible:ring-2 focus-visible:ring-orange-500` 적용.
- `Navbar`, `MobileNavbar`
- `SidebarLayout`, `MinimizedSidebarLayout`, sidebar container components
- `TabBar`, `Table` family, `FadeScrollArea`
Expand Down
Loading