From 575591cbe8ae2985bb8e83f991ecff8bf408427e Mon Sep 17 00:00:00 2001 From: JongHwa Paik Date: Thu, 19 Jun 2025 00:24:19 +0900 Subject: [PATCH 01/11] =?UTF-8?q?=E2=9C=A8=20feat=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20certi=EB=A1=9C=20=EB=A6=AC=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/v1/test/test_router.py | 5 +++++ app/test/dto/response/exam_response_dto.py | 13 +++++++++++++ app/test/usecase/test_usecase.py | 15 ++++++++++++++- domain/test/repository/test_repository.py | 8 +++++++- 4 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 app/test/dto/response/exam_response_dto.py diff --git a/api/v1/test/test_router.py b/api/v1/test/test_router.py index a9946d3..4d49869 100644 --- a/api/v1/test/test_router.py +++ b/api/v1/test/test_router.py @@ -40,6 +40,7 @@ from app.test.dto.request.create_dummy_data_request import CreateDummyDataRequest #시험모드 문제 리스트 출력 from app.test.dto.response.get_certificates_exam_list_response import GetCertificatesExamListResponse +from app.test.usecase.test_usecase import get_exam_list_by_certificate_id_usecase from app.test.usecase.exam_usecase import get_certificates_exam_list_usecase from app.test.usecase.exam_usecase import create_exam_usecase from app.test.usecase.exam_usecase import get_exam_info_usecase @@ -170,6 +171,10 @@ async def get_questions_by_exam_id( result = await get_questions_by_exam_id_usecase(exam_id, db) return ok(data=[item.dict() for item in result], message="문제 목록 조회 성공") +@router.get("/list/{certificate_id}") +async def get_exam_list(certificate_id: int = Path(...), db: AsyncSession = Depends(get_db)): + return await get_exam_list_by_certificate_id_usecase(certificate_id, db) + @router.post("/dummy-data", summary="유형별 Dummy Data 생성 API") async def create_dummy_data( request: CreateDummyDataRequest, # ← 여기에 request 추가 필요! diff --git a/app/test/dto/response/exam_response_dto.py b/app/test/dto/response/exam_response_dto.py new file mode 100644 index 0000000..8b27a48 --- /dev/null +++ b/app/test/dto/response/exam_response_dto.py @@ -0,0 +1,13 @@ +from pydantic import BaseModel + +class ExamResponseDTO(BaseModel): + id: int + name: str + year: int + month: int + trial: int | None + time: int + pass_rate: float | None + + class Config: + from_attributes = True # ✅ Pydantic v2 대응 \ No newline at end of file diff --git a/app/test/usecase/test_usecase.py b/app/test/usecase/test_usecase.py index 7f0e5b6..90b6561 100644 --- a/app/test/usecase/test_usecase.py +++ b/app/test/usecase/test_usecase.py @@ -3,8 +3,11 @@ from domain.test.entity.exam import Exam from domain.test.entity.question import Question from domain.user.entity.user import User -from exception.client_exception import NotFoundException from exception.success import ok +from sqlalchemy.ext.asyncio import AsyncSession +from app.test.dto.response.exam_response_dto import ExamResponseDTO +from domain.test.repository.test_repository import TestRepository +from exception.client_exception import NotFoundException async def get_test_mode_usecase( exam_id: int, @@ -44,3 +47,13 @@ async def get_test_mode_usecase( }, message="시험 조회 성공" ) + +async def get_exam_list_by_certificate_id_usecase(certificate_id: int, db: AsyncSession): + repo = TestRepository(db) + exams = await repo.get_exams_by_certificate_id(certificate_id) + + if not exams: + raise NotFoundException("해당 자격증에 대한 시험이 존재하지 않습니다.") + + exam_list = [ExamResponseDTO.from_orm(exam).model_dump() for exam in exams] + return ok(data={"exams": exam_list}, message="시험 리스트 조회 성공") diff --git a/domain/test/repository/test_repository.py b/domain/test/repository/test_repository.py index 23e53df..73eecc5 100644 --- a/domain/test/repository/test_repository.py +++ b/domain/test/repository/test_repository.py @@ -143,6 +143,11 @@ async def get_question_count_by_exam_id(self, exam_id: int) -> int: ) return result.scalar() + async def get_exams_by_certificate_id(self, certificate_id: int) -> list[Exam]: + stmt = select(Exam).where(Exam.certificate_id == certificate_id) + result = await self.db.execute(stmt) + return result.scalars().all() + async def get_exams_by_certificate_ids(self, certificate_ids: list[int]) -> list[Exam]: result = await self.db.execute( select(Exam) @@ -161,4 +166,5 @@ async def get_exam_by_id(self, exam_id: int) -> Exam | None: result = await self.db.execute( select(Exam).where(Exam.id == exam_id) ) - return result.scalars().first() \ No newline at end of file + return result.scalars().first() + From 22caab29b54292a818740b216f321e6f333300a7 Mon Sep 17 00:00:00 2001 From: JongHwa Paik Date: Thu, 19 Jun 2025 00:27:40 +0900 Subject: [PATCH 02/11] =?UTF-8?q?=E2=9C=A8=20=20feat=20:=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=EC=B2=98=EB=A6=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/test/usecase/test_usecase.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/test/usecase/test_usecase.py b/app/test/usecase/test_usecase.py index 90b6561..04581d9 100644 --- a/app/test/usecase/test_usecase.py +++ b/app/test/usecase/test_usecase.py @@ -3,7 +3,7 @@ from domain.test.entity.exam import Exam from domain.test.entity.question import Question from domain.user.entity.user import User -from exception.success import ok +from app.utils.dto.success import ok from sqlalchemy.ext.asyncio import AsyncSession from app.test.dto.response.exam_response_dto import ExamResponseDTO from domain.test.repository.test_repository import TestRepository From 2b720675c9c86407c52aac1b40a2b5ae1705fd2d Mon Sep 17 00:00:00 2001 From: JongHwa Paik Date: Thu, 19 Jun 2025 01:11:14 +0900 Subject: [PATCH 03/11] =?UTF-8?q?=E2=9C=A8=20feat=20:=20=EB=AC=B8=ED=95=AD?= =?UTF-8?q?=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/test/dto/response/exam_response_dto.py | 3 ++- app/test/usecase/test_usecase.py | 17 ++++++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/app/test/dto/response/exam_response_dto.py b/app/test/dto/response/exam_response_dto.py index 8b27a48..87b1464 100644 --- a/app/test/dto/response/exam_response_dto.py +++ b/app/test/dto/response/exam_response_dto.py @@ -8,6 +8,7 @@ class ExamResponseDTO(BaseModel): trial: int | None time: int pass_rate: float | None + question_count: int class Config: - from_attributes = True # ✅ Pydantic v2 대응 \ No newline at end of file + from_attributes = True \ No newline at end of file diff --git a/app/test/usecase/test_usecase.py b/app/test/usecase/test_usecase.py index 04581d9..053be4b 100644 --- a/app/test/usecase/test_usecase.py +++ b/app/test/usecase/test_usecase.py @@ -1,4 +1,4 @@ -from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import func from sqlalchemy.future import select from domain.test.entity.exam import Exam from domain.test.entity.question import Question @@ -55,5 +55,16 @@ async def get_exam_list_by_certificate_id_usecase(certificate_id: int, db: Async if not exams: raise NotFoundException("해당 자격증에 대한 시험이 존재하지 않습니다.") - exam_list = [ExamResponseDTO.from_orm(exam).model_dump() for exam in exams] - return ok(data={"exams": exam_list}, message="시험 리스트 조회 성공") + exam_list = [] + + for exam in exams: + # 각 시험에 대해 문제 수 계산 + stmt = select(func.count()).where(Question.exam_id == exam.id) + result = await db.execute(stmt) + question_count = result.scalar_one() + + dto = ExamResponseDTO.from_orm(exam).model_dump() + dto["question_count"] = question_count + exam_list.append(dto) + + return ok(data={"exams": exam_list}, message="시험 리스트 조회 성공") \ No newline at end of file From fcf16d2d94a475ede564bcea2adf50574497c8bc Mon Sep 17 00:00:00 2001 From: JongHwa Paik Date: Thu, 19 Jun 2025 01:21:05 +0900 Subject: [PATCH 04/11] =?UTF-8?q?=E2=9C=A8=20feat=20:=20=EC=98=A4=EB=A5=98?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/test/usecase/test_usecase.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/app/test/usecase/test_usecase.py b/app/test/usecase/test_usecase.py index 053be4b..86f867e 100644 --- a/app/test/usecase/test_usecase.py +++ b/app/test/usecase/test_usecase.py @@ -58,13 +58,20 @@ async def get_exam_list_by_certificate_id_usecase(certificate_id: int, db: Async exam_list = [] for exam in exams: - # 각 시험에 대해 문제 수 계산 stmt = select(func.count()).where(Question.exam_id == exam.id) result = await db.execute(stmt) question_count = result.scalar_one() - dto = ExamResponseDTO.from_orm(exam).model_dump() - dto["question_count"] = question_count - exam_list.append(dto) + dto = ExamResponseDTO( + id=exam.id, + name=exam.name, + year=exam.year, + month=exam.month, + trial=exam.trial, + time=exam.time, + pass_rate=exam.pass_rate, + question_count=question_count + ) + exam_list.append(dto.model_dump()) - return ok(data={"exams": exam_list}, message="시험 리스트 조회 성공") \ No newline at end of file + return ok(data={"exams": exam_list}, message="시험 리스트 조회 성공") From 5c6bfc1d4d38314cc7279efb63cb428a3c3d5ab3 Mon Sep 17 00:00:00 2001 From: seoiiwon Date: Thu, 19 Jun 2025 01:25:43 +0900 Subject: [PATCH 05/11] =?UTF-8?q?=F0=9F=94=A8=20refactor:=20dict=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=98=A4=EB=A5=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- domain/test/service/test_service.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/domain/test/service/test_service.py b/domain/test/service/test_service.py index 7ac3d4e..1947c28 100644 --- a/domain/test/service/test_service.py +++ b/domain/test/service/test_service.py @@ -35,10 +35,17 @@ async def check_user_submit( if is_correct: correct_count += 1 + raw_list = question.option_explanations or [] + option_explanations_dict: dict[str, str] = { + key: val + for item in raw_list + for key, val in item.items() + } + # 문제별 해설 DTO info_list.append(AnswerInfoDto( answer=question.answer, - option_explanations=question.option_explanations + option_explanations=option_explanations_dict )) # 문제별 사용자 응답 기록 From 646a196b77eca26d50799fe1888a19f0a785adfc Mon Sep 17 00:00:00 2001 From: seoiiwon Date: Thu, 19 Jun 2025 02:36:12 +0900 Subject: [PATCH 06/11] =?UTF-8?q?release=20auth=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/v1/auth/auth_router.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/api/v1/auth/auth_router.py b/api/v1/auth/auth_router.py index dcd9e82..98216f9 100644 --- a/api/v1/auth/auth_router.py +++ b/api/v1/auth/auth_router.py @@ -41,8 +41,7 @@ async def sign_up(request: SignUpRequest, db: AsyncSession = Depends(get_db)): print(info.values()) user_dto = UserCreateDTO( - **request.model_dump(exclude={"kakao_token"}), - **info + **{**request.model_dump(exclude={"kakao_token"}), **info} ) user = await SignUpUseCase(db).execute(user_dto) From 3ed72452139f5e00f80b6da2eb0febb2c53b29e9 Mon Sep 17 00:00:00 2001 From: seoiiwon Date: Thu, 19 Jun 2025 02:42:31 +0900 Subject: [PATCH 07/11] =?UTF-8?q?release=20=EC=98=A4=EB=A5=98=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/review/dto/response/review_note_list_response.py | 4 ++-- domain/review/service/review_note_test_service.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/review/dto/response/review_note_list_response.py b/app/review/dto/response/review_note_list_response.py index 28f50c2..7ce9e40 100644 --- a/app/review/dto/response/review_note_list_response.py +++ b/app/review/dto/response/review_note_list_response.py @@ -1,4 +1,4 @@ -from typing import List +from typing import List, Optional from pydantic import BaseModel @@ -7,5 +7,5 @@ class ReviewNoteListResponseDto(BaseModel): category: List[str] - selected_category: str + selected_category: Optional[str] = None exams: List[ExamItemInfo] \ No newline at end of file diff --git a/domain/review/service/review_note_test_service.py b/domain/review/service/review_note_test_service.py index 53d76af..038e59e 100644 --- a/domain/review/service/review_note_test_service.py +++ b/domain/review/service/review_note_test_service.py @@ -69,7 +69,7 @@ async def list_review_notes( return ReviewNoteListResponseDto( category = categories, - selected_category = selected, + selected_category = selected or "", exams = exams ) From a9656e7c16b267263e75748a9c1240b4448df299 Mon Sep 17 00:00:00 2001 From: seoiiwon Date: Thu, 19 Jun 2025 03:25:07 +0900 Subject: [PATCH 08/11] =?UTF-8?q?auth=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/v1/auth/auth_router.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/v1/auth/auth_router.py b/api/v1/auth/auth_router.py index 98216f9..a5efebc 100644 --- a/api/v1/auth/auth_router.py +++ b/api/v1/auth/auth_router.py @@ -45,7 +45,8 @@ async def sign_up(request: SignUpRequest, db: AsyncSession = Depends(get_db)): ) user = await SignUpUseCase(db).execute(user_dto) - token_data = TokenUseCase.generate_tokens(user_id=user.id) + token_uc = TokenUseCase() + token_data = token_uc.generate_tokens(user_id=user.id) return created(data=token_data, message="회원가입 성공") From fa9f65af1cd8d6872d989d0f6f2dca092422400a Mon Sep 17 00:00:00 2001 From: seoiiwon Date: Fri, 28 Nov 2025 16:47:23 +0900 Subject: [PATCH 09/11] =?UTF-8?q?=F0=9F=94=A8=20fix:=20update=20fastapi=20?= =?UTF-8?q?version?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index a422f17..7ba25b4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -fastapi==0.115.6 +fastapi==0.121.3 uvicorn==0.34.0 # db 관련 @@ -21,7 +21,7 @@ boto3~=1.38.32 python-jose[cryptography]~=3.5.0 requests-oauthlib requests~=2.32.3 -starlette>=0.49.1 + # test pytest From 3716d98f46c01deadd2fb6e07f4e253835e8928a Mon Sep 17 00:00:00 2001 From: seoiiwon Date: Sun, 30 Nov 2025 15:08:39 +0900 Subject: [PATCH 10/11] =?UTF-8?q?=F0=9F=94=A8=20fix:=20update=20fastapi=20?= =?UTF-8?q?version?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 7ba25b4..2e91558 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -fastapi==0.121.3 +fastapi==0.115.6 uvicorn==0.34.0 # db 관련 @@ -22,7 +22,6 @@ python-jose[cryptography]~=3.5.0 requests-oauthlib requests~=2.32.3 - # test pytest pytest-asyncio From 4dac6a19d09b07f07b7b1fa41e54de4a0c122fd7 Mon Sep 17 00:00:00 2001 From: seoiiwon Date: Sun, 30 Nov 2025 15:15:46 +0900 Subject: [PATCH 11/11] =?UTF-8?q?=F0=9F=94=A8=20fix:=20update=20deploy=20s?= =?UTF-8?q?cripts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 3380f4e..f086238 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -55,6 +55,15 @@ jobs: cache-from: type=local,src=/tmp/.buildx-cache cache-to: type=local,dest=/tmp/.buildx-cache-new + - name: Copy Docker Compose file via SCP + uses: appleboy/scp-action@v0.1.7 + with: + host: ${{ secrets.GCE_HOST }} + username: ${{ secrets.GCE_SSH_USER }} + key: ${{ secrets.GCE_SSH_KEY }} + source: "docker-compose.prod.yml" + target: "~/app-path" + - name: Deploy to GCE via SSH uses: appleboy/ssh-action@v1.0.0 with: @@ -62,6 +71,7 @@ jobs: username: ${{ secrets.GCE_SSH_USER }} key: ${{ secrets.GCE_SSH_KEY }} script: | + mkdir -p ~/app-path cd ~/app-path cat << 'EOF' > .env @@ -70,7 +80,9 @@ jobs: gcloud auth configure-docker ${{ env.ARTIFACT_REGISTRY }} --quiet - docker-compose -f docker-compose.prod.yml down - docker-compose -f docker-compose.prod.yml pull - docker-compose -f docker-compose.prod.yml run --rm -e MODE=prod fastapi alembic upgrade head - docker-compose -f docker-compose.prod.yml up -d \ No newline at end of file + docker compose -f docker-compose.prod.yml down + docker compose -f docker-compose.prod.yml pull + + docker compose -f docker-compose.prod.yml run --rm -e MODE=prod fastapi alembic upgrade head + + docker compose -f docker-compose.prod.yml up -d \ No newline at end of file