Skip to content

Commit 954f4d3

Browse files
authored
Merge pull request #85 from capstone-five-ai/dev
feat: 무중단 배포 및 버그 수정
2 parents a62b41e + e93f2a9 commit 954f4d3

File tree

20 files changed

+120
-114
lines changed

20 files changed

+120
-114
lines changed

.github/workflows/deploy-prod.yaml

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,53 @@ jobs:
5353
host: ${{ secrets.PROD_SERVER_HOST }}
5454
username: ${{ secrets.PROD_SERVER_USERNAME }}
5555
key: ${{ secrets.PROD_SERVER_PEM_KEY }}
56-
envs: GITHUB_SHA
56+
envs: GITHUB_SHA, DOCKERHUB_USERNAME
5757
script: |
58-
sudo docker stop $(sudo docker ps -a -q)
59-
sudo docker rm $(sudo docker ps -a -q)
60-
sudo docker pull ${{secrets.DOCKERHUB_USERNAME}}/qtudy-server:prod
61-
sudo docker run -d -p 8080:8080 --name qtudy-server-container ${{secrets.DOCKERHUB_USERNAME}}/qtudy-server:prod
62-
sudo docker image prune -f
58+
# 현재 활성화된 Nginx 설정 파일 확인
59+
CURRENT_CONF=$(sudo readlink /etc/nginx/sites-enabled/current)
60+
echo "Current Nginx configuration: $CURRENT_CONF"
61+
if [[ "$CURRENT_CONF" == *"blue" ]]; then
62+
NEW_CONF="/etc/nginx/sites-available/green"
63+
OLD_CONTAINER="blue-container"
64+
NEW_CONTAINER="green-container"
65+
NEW_PORT=8081
66+
else
67+
NEW_CONF="/etc/nginx/sites-available/blue"
68+
OLD_CONTAINER="green-container"
69+
NEW_CONTAINER="blue-container"
70+
NEW_PORT=8080
71+
fi
72+
73+
echo "New Nginx configuration: $NEW_CONF"
74+
echo "Old container: $OLD_CONTAINER"
75+
echo "New container: $NEW_CONTAINER"
76+
echo "New port: $NEW_PORT"
77+
78+
# 새 Docker 이미지 가져오기
79+
sudo docker pull ${{ secrets.DOCKERHUB_USERNAME }}/qtudy-server:prod
80+
81+
# 새로운 컨테이너 실행
82+
sudo docker run -d -p $NEW_PORT:8080 --name $NEW_CONTAINER ${{ secrets.DOCKERHUB_USERNAME }}/qtudy-server:prod
83+
84+
# 새로운 컨테이너가 준비될 때까지 대기
85+
echo "Waiting for the new container to be ready..."
86+
sleep 10
87+
88+
# 헬스 체크를 통해 새 컨테이너가 정상적으로 실행 중인지 확인
89+
until [[ "$(curl -s -o /dev/null -w ''%{http_code}'' http://localhost:$NEW_PORT/actuator/health)" == "200" ]]; do
90+
echo "Waiting for the new container to respond with HTTP 200..."
91+
sleep 2
92+
done
93+
94+
echo "New container is ready. Switching Nginx configuration."
95+
96+
# Nginx 설정 파일 교체 및 재시작
97+
sudo ln -sf $NEW_CONF /etc/nginx/sites-enabled/current
98+
sudo systemctl reload nginx
99+
100+
# 이전 컨테이너 중지 및 제거
101+
sudo docker stop $OLD_CONTAINER
102+
sudo docker rm $OLD_CONTAINER
103+
104+
# 사용하지 않는 Docker 이미지 정리
105+
sudo docker image prune -f

.github/workflows/test.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@ jobs:
1717
steps:
1818
- name: 체크아웃
1919
uses: actions/checkout@v4
20+
with:
21+
submodules: true
22+
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
23+
24+
- name: 서브모듈 업데이트
25+
run: |
26+
git submodule update --remote
2027
2128
- name: JDK 11 설치
2229
uses: actions/setup-java@v4

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ dependencies {
5151

5252
// Spring Boot Actuator
5353
implementation 'org.springframework.boot:spring-boot-starter-actuator'
54-
54+
5555
// GPT 스크립트 테스트 편의성을 위해 추가
5656
testAnnotationProcessor 'org.projectlombok:lombok'
5757
testImplementation 'com.theokanning.openai-gpt3-java:service:0.18.2'

src/main/java/com/app/domain/categorizedproblem/service/CategorizedProblemService.java

Lines changed: 38 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,21 @@
66
import com.app.domain.category.service.CategoryService;
77
import com.app.domain.member.entity.Member;
88
import com.app.domain.member.service.MemberService;
9+
import com.app.domain.problem.aigeneratedproblem.dto.ProblemFile.AiRequest.AiGenerateProblemFromAiDto;
910
import com.app.domain.problem.entity.Problem;
1011
import com.app.domain.problem.membersavedproblem.dto.MemberSavedProblemDto;
1112
import com.app.domain.problem.membersavedproblem.mapper.MemberSavedProblemMapper;
1213
import com.app.domain.problem.service.ProblemService;
1314
import com.app.domain.summary.membersavedsummary.dto.MemberSavedSummaryDto;
15+
import com.app.global.config.ENUM.PdfType;
1416
import com.app.global.config.ENUM.ProblemType;
1517
import com.app.global.error.ErrorCode;
1618
import com.app.global.error.exception.BusinessException;
1719
import com.app.global.error.exception.EntityNotFoundException;
1820
import java.io.ByteArrayOutputStream;
21+
import java.io.File;
1922
import java.io.IOException;
23+
import java.nio.file.Files;
2024
import java.util.ArrayList;
2125
import java.util.List;
2226
import javax.servlet.http.HttpServletRequest;
@@ -32,6 +36,8 @@
3236
import org.springframework.stereotype.Service;
3337
import org.springframework.transaction.annotation.Transactional;
3438

39+
import static com.app.global.pdf.ProblemPdfMaker.CreatePdfFile;
40+
3541
@Service
3642
@Transactional
3743
@RequiredArgsConstructor
@@ -131,103 +137,45 @@ public MemberSavedSummaryDto.pdfResponse createCategorizedProblemsAnswerPdf(Long
131137
}
132138
}
133139

134-
public MemberSavedSummaryDto.pdfResponse createCategorizedProblemsPdf(Long categoryId) throws IOException {
140+
public MemberSavedSummaryDto.pdfResponse createCategorizedProblemsPdf(Long categoryId){
135141
List<CategorizedProblem> categorizedProblemList = categorizedProblemRepository.findByCategoryCategoryId(categoryId);
136142
Category category = categoryService.findVerifiedCategoryByCategoryId(categoryId);
137143
String title = category.getCategoryName();
138144

139-
try (PDDocument document = new PDDocument()) {
140-
PDPage page = new PDPage();
141-
document.addPage(page);
142-
PDPageContentStream contentStream = new PDPageContentStream(document, page);
143-
PDType0Font font = PDType0Font.load(document, getClass().getResourceAsStream("/fonts/malgun.ttf"));
144-
contentStream.setFont(font, 12);
145-
float yPosition = page.getMediaBox().getHeight() - 50;
146-
int linesInCurrentPage = 0;
147-
int maxLinesPerPage = 45;
148-
int problemNumber = 0; // 문제 번호
145+
// Pdf 생성 DTO 변환
146+
AiGenerateProblemFromAiDto[] problems = new AiGenerateProblemFromAiDto[categorizedProblemList.size()];
147+
for(int i = 0; i < categorizedProblemList.size(); i++) {
148+
Problem problem = categorizedProblemList.get(i).getProblem();
149+
problems[i] = AiGenerateProblemFromAiDto.create(
150+
problem.getProblemName(),
151+
problem.getProblemChoices(),
152+
problem.getProblemAnswer(),
153+
problem.getProblemCommentary()
154+
);
155+
}
149156

150-
for (CategorizedProblem categorizedProblem : categorizedProblemList) {
151-
problemNumber++;
152-
Problem problem = categorizedProblem.getProblem();
153-
String problemName = problemNumber + ". " + problem.getProblemName();
157+
File tempFile = null;
158+
byte[] pdfContent = null;
154159

155-
List<String> problemChoices = new ArrayList<>();
156-
if (problem.getProblemType() == ProblemType.MULTIPLE) {
157-
problemChoices = problem.getProblemChoices();
158-
}
159-
float maxWidth = page.getMediaBox().getWidth() - 100; // 페이지 폭에서 양쪽 여백을 뺀 값
160-
List<String> wrappedProblemName = wrapText(problemName, font, 12, maxWidth);
160+
try {
161+
tempFile = CreatePdfFile(category.getCategoryName(), problems, PdfType.PROBLEM); // Problem PDF 생성
161162

162-
for (String line : wrappedProblemName) {
163-
if (linesInCurrentPage >= maxLinesPerPage || yPosition < 50) {
164-
contentStream.close();
165-
page = new PDPage();
166-
document.addPage(page);
167-
contentStream = new PDPageContentStream(document, page);
168-
contentStream.setFont(font, 12);
169-
yPosition = page.getMediaBox().getHeight() - 50;
170-
linesInCurrentPage = 0;
171-
}
163+
// 파일을 바이트 배열로 변환
164+
pdfContent = Files.readAllBytes(tempFile.toPath());
172165

173-
contentStream.beginText();
174-
contentStream.newLineAtOffset(50, yPosition);
175-
contentStream.showText(line);
176-
contentStream.endText();
177-
yPosition -= 15; // 줄 간의 간격
178-
linesInCurrentPage++;
179-
}
180-
yPosition -= 10; // 문제명과 선택지 사이의 간격 조정
181-
182-
// 보기 리스트 출력
183-
if (problemChoices != null) {
184-
char choiceLetter = 'A'; // 선지 번호 (알파벳)
185-
int totalChoices = problemChoices.size();
186-
187-
for (String choice : problemChoices) {
188-
List<String> wrappedText = wrapText(choice, font, 12, maxWidth);
189-
boolean isFirstLineOfChoice = true;
190-
191-
for (String line : wrappedText) {
192-
if (linesInCurrentPage >= maxLinesPerPage || yPosition < 50) {
193-
contentStream.close();
194-
page = new PDPage();
195-
document.addPage(page);
196-
contentStream = new PDPageContentStream(document, page);
197-
contentStream.setFont(font, 12);
198-
yPosition = page.getMediaBox().getHeight() - 50;
199-
linesInCurrentPage = 0;
200-
}
201-
202-
contentStream.beginText();
203-
contentStream.newLineAtOffset(50, yPosition);
204-
205-
if (isFirstLineOfChoice) {
206-
contentStream.showText(choiceLetter + ". " + line); // 알파벳 번호 추가
207-
isFirstLineOfChoice = false; // 첫 줄 출력 후 false로 변경
208-
} else {
209-
contentStream.showText(line);
210-
}
211-
contentStream.endText();
212-
yPosition -= 15;
213-
linesInCurrentPage++;
214-
}
215-
if (choiceLetter == 'A' + totalChoices - 1) { // 마지막 선지 확인
216-
yPosition -= 20; // 마지막 선지 이후에 더 큰 간격 추가
217-
}
218-
choiceLetter++; // 다음 선지를 위해 알파벳 증가
219-
}
220-
}
166+
} catch (IOException e) {
167+
throw new BusinessException(ErrorCode.NOT_UPLOAD_PROBLEM);
168+
} finally {
169+
// 임시 파일 삭제
170+
if (tempFile != null && tempFile.exists()) {
171+
tempFile.delete();
221172
}
222-
223-
contentStream.close();
224-
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
225-
document.save(byteArrayOutputStream); // 문서를 ByteArrayOutputStream에 저장
226-
document.close(); // 문서를 닫아 리소스를 해제
227-
return new MemberSavedSummaryDto.pdfResponse(byteArrayOutputStream.toByteArray(), title);
228173
}
174+
175+
return new MemberSavedSummaryDto.pdfResponse(pdfContent, title);
229176
}
230177

178+
@CacheEvict(value = "categorizedProblem", key = "#result.getCategory().getCategoryId()")
231179
public CategorizedProblem updateCategorizedProblem(Long categorizedProblemId, MemberSavedProblemDto.Patch problemPatchDto) {
232180
CategorizedProblem categorizedProblem = findVerifiedCategorizedProblemByCategorizedProblemId(categorizedProblemId);
233181
problemService.updateProblem(
@@ -238,21 +186,23 @@ public CategorizedProblem updateCategorizedProblem(Long categorizedProblemId, Me
238186
}
239187

240188
@Transactional(readOnly = true)
241-
// @Cacheable(value = "categorizedProblem", key = "#categoryId")
189+
@Cacheable(value = "categorizedProblem", key = "#categoryId")
242190
public Page<CategorizedProblem> findCategorizedProblemsByCategoryId(Long categoryId, int page, int size) {
243191
PageRequest pageRequest = PageRequest.of(page, size);
244192
return categorizedProblemRepository.findByCategoryCategoryId(categoryId, pageRequest);
245193
}
246194

247-
public void deleteCategorizedProblem(Long categorizedProblemID){
195+
@CacheEvict(value = "categorizedProblem", key = "#result")
196+
public Long deleteCategorizedProblem(Long categorizedProblemID) {
248197
CategorizedProblem categorizedProblem = findVerifiedCategorizedProblemByCategorizedProblemId(categorizedProblemID);
249-
categorizedProblemRepository.deleteById(categorizedProblemID);
198+
categorizedProblemRepository.deleteById(categorizedProblem.getCategorizedProblemId());
250199

251200
Problem problem = categorizedProblem.getProblem();
252201
Long problemId = problem.getProblemId();
253202
if (problem.isMemberSavedProblem() && !isProblemUsedInOtherCategorizedProblems(problemId)) {
254203
problemService.deleteProblem(problemId);
255204
}
205+
return categorizedProblem.getCategory().getCategoryId();
256206
}
257207

258208
private boolean isProblemUsedInOtherCategorizedProblems(Long problemId) {

src/main/java/com/app/domain/categorizedsummary/service/CategorizedSummaryService.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ public SummaryDto.pdfResponse createSummaryPdf(Long categorizedSummaryId) throws
7575
return summaryService.createSummaryPdf(summaryId);
7676
}
7777

78+
@CacheEvict(value = "categorizedSummary", key = "#result.getCategory().getCategoryId()")
7879
public CategorizedSummary updateCategorizedSummary(Long categorizedSummaryId, SummaryDto.Patch summaryPatchDto) {
7980
CategorizedSummary categorizedSummary = findVerifiedCategorizedSummaryByCategorizedSummaryId(categorizedSummaryId);
8081
summaryService.updateSummary(
@@ -85,7 +86,8 @@ public CategorizedSummary updateCategorizedSummary(Long categorizedSummaryId, Su
8586
return categorizedSummaryRepository.save(categorizedSummary);
8687
}
8788

88-
public void deleteCategorizedSummary(Long categorizedSummaryId) {
89+
@CacheEvict(value = "categorizedSummary", key = "#result")
90+
public Long deleteCategorizedSummary(Long categorizedSummaryId) {
8991
CategorizedSummary categorizedSummary = findVerifiedCategorizedSummaryByCategorizedSummaryId(categorizedSummaryId);
9092

9193
categorizedSummaryRepository.deleteById(categorizedSummaryId);
@@ -95,6 +97,8 @@ public void deleteCategorizedSummary(Long categorizedSummaryId) {
9597
if (summary.isMemberSavedSummary() && !isSummaryUsedInOtherCategorizedSummarys(summaryId)) {
9698
summaryRepository.deleteById(summaryId);
9799
}
100+
Long categoryId = categorizedSummary.getCategory().getCategoryId();
101+
return categoryId;
98102
}
99103

100104
private boolean isSummaryUsedInOtherCategorizedSummarys(Long summaryId){
@@ -107,7 +111,7 @@ public CategorizedSummary findVerifiedCategorizedSummaryByCategorizedSummaryId(L
107111
}
108112

109113
@Transactional(readOnly = true)
110-
// @Cacheable(value = "categorizedSummary", key = "#categoryId")
114+
@Cacheable(value = "categorizedSummary", key = "#categoryId")
111115
public Page<CategorizedSummary> findCategorziedSummarysByCategoryId(Long categoryId, int page, int size) {
112116
PageRequest pageRequest = PageRequest.of(page, size);
113117
return categorizedSummaryRepository.findByCategoryCategoryId(categoryId, pageRequest);

src/main/java/com/app/domain/problem/aigeneratedproblem/dto/ProblemFile/AiRequest/AiGenerateProblemFromAiDto.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,8 @@ public class AiGenerateProblemFromAiDto {
2525

2626
@Schema(description = "문제 해설", example = "문제 해설 예시")
2727
private String problemCommentary;
28+
29+
public static AiGenerateProblemFromAiDto create(String problemName, List<String> problemChoices, String problemAnswer, String problemCommentary) {
30+
return new AiGenerateProblemFromAiDto(problemName, problemChoices, problemAnswer, problemCommentary);
31+
}
2832
}

src/main/java/com/app/domain/problem/aigeneratedproblem/service/ProblemFileService.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -268,8 +268,7 @@ public void UploadS3(AiGenerateProblemFromAiDto[] aiGenerateProblemFromAiDto, Ai
268268

269269

270270
} catch (IOException e) {
271-
272-
throw new BusinessException(ErrorCode.NOT_UPLOAD_PROBLEM);
271+
e.printStackTrace();
273272
} finally {
274273
if (tempFile != null) {
275274
tempFile.delete(); // 방금 생성한 파일 삭제

src/main/java/com/app/domain/summary/aigeneratedsummary/service/SummaryFileService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ public void UploadS3(AiGenerateSummaryFromAiDto aiGenerateSummaryFromAiDto, AiGe
207207

208208

209209
} catch (IOException e) {
210-
throw new BusinessException(ErrorCode.NOT_UPLOAD_PROBLEM);
210+
e.printStackTrace();
211211
} finally {
212212
if (tempFile != null) {
213213
tempFile.delete(); // 방금 생성한 파일 삭제

src/test/java/com/app/integration/fake/FakeSignUpController.java renamed to src/main/java/com/app/fake/FakeLoginController.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
package com.app.integration.fake;
1+
package com.app.fake;
22

33
import com.app.domain.member.constant.MemberType;
44
import com.app.domain.member.constant.Role;
55
import com.app.domain.member.entity.Member;
66
import com.app.domain.member.service.MemberService;
77
import com.app.global.jwt.dto.JwtTokenDto;
88
import com.app.global.jwt.service.TokenManager;
9-
import com.app.integration.dto.FakeSignUpRequest;
109
import lombok.RequiredArgsConstructor;
1110
import org.springframework.context.annotation.Profile;
1211
import org.springframework.http.ResponseEntity;
@@ -17,7 +16,7 @@
1716
@RequiredArgsConstructor
1817
@Profile(value = {"local", "test"})
1918
@RestController
20-
public class FakeSignUpController {
19+
public class FakeLoginController {
2120

2221
private final MemberService memberService;
2322
private final TokenManager tokenManager;

src/test/java/com/app/integration/dto/FakeSignUpRequest.java renamed to src/main/java/com/app/fake/FakeSignUpRequest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.app.integration.dto;
1+
package com.app.fake;
22

33
import lombok.Builder;
44
import lombok.Getter;

src/test/java/com/app/integration/category/CategoryIntegrationTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import com.app.domain.category.dto.CategoryDto;
66
import com.app.domain.category.dto.CategoryDto.RequestDto;
77
import com.app.domain.category.contsant.CategoryType;
8-
import com.app.integration.dto.FakeSignUpRequest;
8+
import com.app.fake.FakeSignUpRequest;
99
import io.restassured.RestAssured;
1010
import io.restassured.response.ExtractableResponse;
1111
import io.restassured.response.Response;

src/test/java/com/app/integration/file/FileIntegrationTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
import com.app.domain.problem.aigeneratedproblem.repository.ProblemFileRepository;
1212
import com.app.domain.summary.aigeneratedsummary.entity.SummaryFile;
1313
import com.app.domain.summary.aigeneratedsummary.repository.SummaryFileRepository;
14+
import com.app.fake.FakeSignUpRequest;
1415
import com.app.global.config.ENUM.PdfType;
15-
import com.app.integration.dto.FakeSignUpRequest;
1616
import io.restassured.RestAssured;
1717
import io.restassured.response.ExtractableResponse;
1818
import io.restassured.response.Response;

src/test/java/com/app/integration/problem/aigeneratedproblem/AiGeneratedProblemIntegrationTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
import com.app.domain.problem.aigeneratedproblem.repository.AiGeneratedProblemRepository;
66
import com.app.domain.problem.aigeneratedproblem.repository.ProblemFileRepository;
77
import com.app.domain.problem.aigeneratedproblem.service.AiGeneratedProblemService;
8+
import com.app.fake.FakeSignUpRequest;
89
import com.app.global.config.ENUM.ProblemType;
9-
import com.app.integration.dto.FakeSignUpRequest;
1010
import io.restassured.RestAssured;
1111
import io.restassured.response.ExtractableResponse;
1212
import io.restassured.response.Response;

0 commit comments

Comments
 (0)