Skip to content

Commit b0e4528

Browse files
committed
fix: fix all error
로컬환경에서 워커등 nginx 모방 및 운영디비와 일치하게 변경 로컬 정책 validation으로 변경,깃이그노어에 키 추가 및 가이드 및 null방어
1 parent 4f46f2a commit b0e4528

29 files changed

Lines changed: 453 additions & 392 deletions

File tree

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,7 @@ application-local.yml
4545
src/main/resources/keystore.p12
4646
src/test/resources/application-s3-secrets.properties
4747
spy.log
48-
htmlReport
48+
htmlReport
49+
50+
# 로컬 개발용 SSL 인증서 개인 키 제외
51+
nginx/certs/*.key

compose.yaml

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,23 @@
11
services:
2+
nginx:
3+
image: nginx:1.27
4+
container_name: pullit-local-nginx
5+
restart: unless-stopped
6+
ports:
7+
- "443:443"
8+
volumes:
9+
- ./nginx/conf.d:/etc/nginx/conf.d
10+
- ./nginx/certs:/etc/nginx/certs
11+
depends_on:
12+
- pullit-local-api
13+
networks:
14+
- pullit-local-network
15+
216
pullit-local-api:
317
build: .
418
container_name: pullit-local-api
519
restart: unless-stopped
6-
ports:
7-
- "8080:8080"
20+
ports: []
821
environment:
922
- SPRING_PROFILES_ACTIVE=local
1023
- DB_URL=jdbc:mariadb://mariadb:3306/mydatabase?createDatabaseIfNotExist=true&serverTimezone=Asia/Seoul&characterEncoding=UTF-8
@@ -14,6 +27,14 @@ services:
1427
- RABBITMQ_HOST=rabbitmq
1528
- SPRING_RABBITMQ_USERNAME=guest
1629
- SPRING_RABBITMQ_PASSWORD=guest
30+
- APP_GEMINI_APIKEY=${APP_GEMINI_APIKEY}
31+
- AWS_REGION=${AWS_REGION}
32+
- GOOGLE_API_KEY=${GOOGLE_API_KEY}
33+
- KAKAO_CLIENT_SECRET=${KAKAO_CLIENT_SECRET}
34+
- KAKAO_REDIRECT_URI=${KAKAO_REDIRECT_URI}
35+
- KAKAO_REST_API_KEY=${KAKAO_REST_API_KEY}
36+
- S3_ACCESS_KEY=${S3_ACCESS_KEY}
37+
- S3_SECRET_KEY=${S3_SECRET_KEY}
1738
depends_on:
1839
mariadb:
1940
condition: service_healthy
@@ -38,6 +59,14 @@ services:
3859
- RABBITMQ_HOST=rabbitmq
3960
- SPRING_RABBITMQ_USERNAME=guest
4061
- SPRING_RABBITMQ_PASSWORD=guest
62+
- APP_GEMINI_APIKEY=${APP_GEMINI_APIKEY}
63+
- AWS_REGION=${AWS_REGION}
64+
- GOOGLE_API_KEY=${GOOGLE_API_KEY}
65+
- KAKAO_CLIENT_SECRET=${KAKAO_CLIENT_SECRET}
66+
- KAKAO_REDIRECT_URI=${KAKAO_REDIRECT_URI}
67+
- KAKAO_REST_API_KEY=${KAKAO_REST_API_KEY}
68+
- S3_ACCESS_KEY=${S3_ACCESS_KEY}
69+
- S3_SECRET_KEY=${S3_SECRET_KEY}
4170
depends_on:
4271
mariadb:
4372
condition: service_healthy
@@ -59,6 +88,11 @@ services:
5988
- "127.0.0.1:3306:3306"
6089
networks:
6190
- pullit-local-network
91+
healthcheck:
92+
test: ["CMD", "mariadb", "-u", "myuser", "-psecret", "-e", "USE mydatabase;"]
93+
interval: 10s
94+
timeout: 5s
95+
retries: 10
6296

6397
prometheus:
6498
container_name: prometheus
@@ -100,9 +134,8 @@ services:
100134
healthcheck:
101135
test: ["CMD", "redis-cli", "ping"]
102136
interval: 10s
103-
timeout: 10s
137+
timeout: 5s
104138
retries: 5
105-
start_period: 30s
106139

107140
rabbitmq:
108141
image: 'rabbitmq:3-management-alpine'
@@ -113,9 +146,9 @@ services:
113146
networks:
114147
- pullit-local-network
115148
healthcheck:
116-
test: ["CMD", "rabbitmq-diagnostics", "ping"]
149+
test: ["CMD", "rabbitmq-diagnostics", "check_running"]
117150
interval: 10s
118-
timeout: 10s
151+
timeout: 5s
119152
retries: 5
120153

121154
networks:

nginx/certs/README.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# 로컬 개발 환경을 위한 SSL 인증서
2+
3+
이 디렉토리는 `https://localhost` 접속에 필요한 SSL 인증서(`localhost.crt`)와 개인 키(`localhost.key`)를 저장하는 곳입니다.
4+
5+
**주의**: 개인 키 파일(`*.key`)은 보안상의 이유로 Git 버전 관리에서 제외됩니다. 프로젝트를 처음 설정하는 경우, 아래의 방법으로 직접 인증서를 생성해야 합니다.
6+
7+
## 인증서 생성 방법 (mkcert 사용)
8+
9+
`mkcert`는 로컬 환경에서 신뢰할 수 있는 SSL 인증서를 손쉽게 만들어주는 도구입니다.
10+
11+
### 1. mkcert 설치
12+
13+
사용 중인 운영체제에 맞게 `mkcert`를 설치합니다.
14+
15+
- **Linux (Ubuntu/Debian)**:
16+
```bash
17+
sudo apt update
18+
sudo apt install libnss3-tools mkcert
19+
```
20+
- **macOS (Homebrew)**:
21+
```bash
22+
brew install mkcert
23+
brew install nss # Firefox 지원을 위해 필요
24+
```
25+
- **Windows (Chocolatey)**:
26+
```powershell
27+
choco install mkcert
28+
```
29+
30+
### 2. 로컬 인증 기관(CA) 설치
31+
32+
다음 명령어를 실행하여 내 컴퓨터에 로컬 CA를 생성하고 신뢰하도록 설정합니다. 이 작업은 컴퓨터 당 한 번만 수행하면 됩니다.
33+
34+
```bash
35+
mkcert -install
36+
```
37+
38+
### 3. 인증서 생성
39+
40+
프로젝트의 `nginx/certs` 디렉토리로 이동한 후, 다음 명령어를 실행하여 `localhost`에 대한 인증서와 개인 키를 생성합니다.
41+
42+
```bash
43+
# 터미널에서 `pullit/nginx/certs` 디렉토리로 이동
44+
mkcert localhost
45+
```
46+
47+
위 명령어를 실행하면 `localhost.pem` (인증서)과 `localhost-key.pem` (개인 키) 파일이 생성됩니다. Nginx 설정 파일(`default.conf`)이 `.crt``.key` 확장자를 사용하므로, 생성된 파일들의 이름을 아래와 같이 변경해주세요.
48+
49+
- `localhost.pem` -> `localhost.crt`
50+
- `localhost-key.pem` -> `localhost.key`
51+
52+
이제 `docker-compose up -d --build`을 실행하면 Nginx가 인증서를 정상적으로 로드하여 `https://localhost` 접속이 가능해집니다.

nginx/certs/localhost.crt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIEGDCCAoCgAwIBAgIQH4Dk0fG+dJTSY+jVUvQshjANBgkqhkiG9w0BAQsFADBn
3+
MR4wHAYDVQQKExVta2NlcnQgZGV2ZWxvcG1lbnQgQ0ExHjAcBgNVBAsMFW9zdW1h
4+
QG9zdW1hLUFFUk8tNS1YRTElMCMGA1UEAwwcbWtjZXJ0IG9zdW1hQG9zdW1hLUFF
5+
Uk8tNS1YRTAeFw0yNTExMDYxODQ3MzNaFw0yODAyMDYxODQ3MzNaMEkxJzAlBgNV
6+
BAoTHm1rY2VydCBkZXZlbG9wbWVudCBjZXJ0aWZpY2F0ZTEeMBwGA1UECwwVb3N1
7+
bWFAb3N1bWEtQUVSTy01LVhFMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
8+
AQEAzWvyAoXj/zo5v6RNhGYx+bVhNFUkmhKhgedUkku82zV0+e3xE/y0A+qVNdNJ
9+
RA9eUVbL70Vg/PV3m7QQEny2/nS0/M59pv6ybb3rXlvCRh0k3j5tVQIS3683bRcM
10+
17hh1rREVYj4NZc7BzXH4oLoFE9nILx3e8TfAEOkmzzNb016yvCB2/R0uSCXEIDk
11+
6pVtGtJEe3ua2wKUCBIgiIBAGp+1u5wJLQ1MO+PNJUtrR4VfqaE2zzFoke0qMWHf
12+
3QBBdKt/92+ihQzXL9EQpsN1WUQXtEIqGVKxOqsIPzCmvgJsOyNGboHlsaAdfAO/
13+
kl3qOn7ybR7851SP6YV7w181CQIDAQABo14wXDAOBgNVHQ8BAf8EBAMCBaAwEwYD
14+
VR0lBAwwCgYIKwYBBQUHAwEwHwYDVR0jBBgwFoAUMmK0hLADzDWo/PgOsIjlhP8B
15+
0nswFAYDVR0RBA0wC4IJbG9jYWxob3N0MA0GCSqGSIb3DQEBCwUAA4IBgQBwvr9z
16+
moO6xT0ukDDagy1362HDVMqt4PpwI0+J11Wmfz3NC4DLrTJf3FzkgU9oa4U/CheV
17+
1RUi8CccwfyjQDNxoqitkLTNGqaiP2UlR7JnE5RwK2kmA8BTxm2UIf1oFSFR3jb0
18+
jVNruysotvup33DGU932S/EN4CNZp8hplec4YwZwIkF1hegH0/phOKKNXaFPWdyK
19+
veBetd5uxQKskoI+OGMZo7VITcxc6dw4FSSoYaeVCkDT7/J9QpxHuiV+T25J8ZPq
20+
OYzGzEO1gvP6PkCxuI10FlkTkBbZSyLYkBKGUmtJCygFaXJcolIhBeDwi5hnrYDt
21+
8sjkQLexj71mM/2QvcsUZ1tqgfSofR5Bn9e4OWynfa7W0l8lfy0p1gLjqd4lkuPh
22+
RQ3emIREJAd7TVWRy2oUSrQSbBOy2lT3iwlPmG63F4Fe3C3Hq7VZmDgCtmXg5l+G
23+
q9O4k2UyAt2PQZFuzlLYtc4cCjyEq3WsBSOaCsUM5E2F443e3YTUbjer8D8=
24+
-----END CERTIFICATE-----

nginx/conf.d/default.conf

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
server {
2+
listen 443 ssl;
3+
server_name localhost;
4+
5+
ssl_certificate /etc/nginx/certs/localhost.crt;
6+
ssl_certificate_key /etc/nginx/certs/localhost.key;
7+
8+
# SSL Settings
9+
ssl_protocols TLSv1.2 TLSv1.3;
10+
ssl_ciphers HIGH:!aNULL:!MD5;
11+
12+
# SSE (Server-Sent Events)를 위한 특별 설정
13+
# 실시간 알림을 위해 Nginx가 응답을 버퍼링하지 않고 즉시 클라이언트에 전달하도록 함
14+
location /api/notifications/subscribe {
15+
proxy_pass http://pullit-local-api:8080;
16+
proxy_set_header Host $host;
17+
proxy_set_header X-Real-IP $remote_addr;
18+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
19+
proxy_set_header X-Forwarded-Proto $scheme;
20+
21+
# SSE를 위한 핵심 설정
22+
proxy_buffering off; # 응답 버퍼링 비활성화
23+
proxy_cache off; # 캐시 비활성화
24+
proxy_http_version 1.1; # HTTP/1.1 사용
25+
proxy_set_header Connection ''; # Connection 헤더를 비워서 keep-alive 연결 유지
26+
proxy_read_timeout 1h; # 타임아웃을 1시간으로 길게 설정
27+
keepalive_timeout 7200s; # Keep-alive 타임아웃 설정
28+
}
29+
30+
location / {
31+
proxy_pass http://pullit-local-api:8080;
32+
proxy_set_header Host $host;
33+
proxy_set_header X-Real-IP $remote_addr;
34+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
35+
proxy_set_header X-Forwarded-Proto $scheme;
36+
proxy_set_header Upgrade $http_upgrade;
37+
proxy_set_header Connection "upgrade";
38+
}
39+
}

src/main/java/kr/it/pullit/modules/learningsource/source/service/SourceService.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,9 @@ public List<Source> findByIdIn(List<Long> ids) {
148148
public void deleteSource(Long sourceId, Long memberId) {
149149
Source source = getOrElseThrow(sourceId, memberId);
150150
String filePath = source.getFilePath();
151+
152+
source.getQuestionSets().forEach(questionSet -> questionSet.getSources().remove(source));
153+
151154
sourceRepository.delete(source);
152155
deleteInS3(filePath);
153156
}

src/main/java/kr/it/pullit/modules/projection/learnstats/domain/LearnStats.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public int calculateLearningProgress() {
6464
if (this.totalQuestionCount == 0) {
6565
return 0;
6666
}
67-
return (int) (((double) this.totalSolvedQuestionCount / this.totalQuestionCount) * 100);
67+
return (int) (((double) this.totalCorrectQuestionCount / this.totalQuestionCount) * 100);
6868
}
6969

7070
public void onWeeklyReset() {

src/main/java/kr/it/pullit/modules/projection/outbox/domain/OutboxEvent.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public enum Status {
4949
private String eventType; // WEEKLY_RESET, QUESTION_SOLVED, ...
5050

5151
@Lob
52-
@Column(columnDefinition = "json", nullable = false)
52+
@Column(columnDefinition = "longtext", nullable = false)
5353
private String payload; // JSON 문자열
5454

5555
@Enumerated(EnumType.STRING)

src/main/java/kr/it/pullit/modules/questionset/client/GeminiClient.java

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22

33
import com.fasterxml.jackson.databind.ObjectMapper;
44
import com.google.genai.Client;
5-
import com.google.genai.types.FinishReason;
5+
import com.google.genai.ResponseStream;
6+
import com.google.genai.types.FinishReason.Known;
67
import com.google.genai.types.GenerateContentResponse;
78
import com.google.genai.types.HttpOptions;
89
import java.io.IOException;
910
import java.time.Duration;
11+
import java.util.stream.Collectors;
12+
import java.util.stream.StreamSupport;
1013
import kr.it.pullit.modules.questionset.api.LlmClient;
1114
import kr.it.pullit.modules.questionset.client.dto.request.GeminiRequest;
1215
import kr.it.pullit.modules.questionset.client.dto.request.LlmGeneratedQuestionRequest;
@@ -42,27 +45,35 @@ public LlmGeneratedQuestionSetResponse getLlmGeneratedQuestionContent(
4245
GeminiRequest geminiRequest = GeminiRequest.from(request, configBuilder);
4346
logRequestDetails(geminiRequest, request);
4447

45-
GenerateContentResponse response = callGeminiApi(geminiRequest);
46-
validateResponse(response);
48+
ResponseStream<GenerateContentResponse> responseStream = callGeminiApiStream(geminiRequest);
49+
String aggregatedResponse = aggregateStreamResponse(responseStream);
4750

48-
return parseResponse(response);
51+
return parseResponse(aggregatedResponse);
4952
} catch (IOException e) {
5053
throw LlmResponseParseException.create(e);
5154
} catch (Exception e) {
5255
throw LlmException.withCause(e);
5356
}
5457
}
5558

56-
private GenerateContentResponse callGeminiApi(GeminiRequest geminiRequest) {
57-
return client.models.generateContent(
59+
private ResponseStream<GenerateContentResponse> callGeminiApiStream(GeminiRequest geminiRequest) {
60+
return client.models.generateContentStream(
5861
geminiRequest.model(), geminiRequest.content(), geminiRequest.config());
5962
}
6063

61-
private void validateResponse(GenerateContentResponse response) {
62-
if (response.finishReason().knownEnum() != FinishReason.Known.STOP) {
63-
throw LlmException.generationFailed(
64-
"AI 모델이 비정상적으로 응답 생성을 중단했습니다. (사유: " + response.finishReason() + ")");
65-
}
64+
private String aggregateStreamResponse(ResponseStream<GenerateContentResponse> responseStream) {
65+
return StreamSupport.stream(responseStream.spliterator(), false)
66+
.peek(
67+
response -> {
68+
if (response.finishReason().knownEnum() != Known.STOP
69+
&& response.finishReason().knownEnum() != Known.FINISH_REASON_UNSPECIFIED) {
70+
throw LlmException.generationFailed(
71+
"AI 모델이 비정상적으로 응답 생성을 중단했습니다. (사유: " + response.finishReason() + ")");
72+
}
73+
})
74+
.map(GenerateContentResponse::text)
75+
.filter(text -> text != null && !text.isEmpty())
76+
.collect(Collectors.joining());
6677
}
6778

6879
private void logRequestDetails(GeminiRequest geminiRequest, LlmGeneratedQuestionRequest request) {
@@ -84,9 +95,7 @@ private void logRequestDetails(GeminiRequest geminiRequest, LlmGeneratedQuestion
8495
: "");
8596
}
8697

87-
private LlmGeneratedQuestionSetResponse parseResponse(GenerateContentResponse response)
88-
throws IOException {
89-
String rawResponse = response.text();
98+
private LlmGeneratedQuestionSetResponse parseResponse(String rawResponse) throws IOException {
9099
try {
91100
return mapper.readValue(rawResponse, LlmGeneratedQuestionSetResponse.class);
92101
} catch (IOException e) {

src/main/java/kr/it/pullit/modules/questionset/repository/QuestionSetRepository.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ public interface QuestionSetRepository {
1111

1212
Optional<QuestionSet> findById(Long id);
1313

14+
Optional<QuestionSet> findByIdWithQuestions(Long id);
15+
1416
Optional<QuestionSet> findByIdAndMemberId(Long id, Long memberId);
1517

1618
Optional<QuestionSet> findWithQuestionsForFirstSolving(Long id, Long memberId);

0 commit comments

Comments
 (0)