Skip to content

Commit 92b14dc

Browse files
authored
Merge pull request #174 from JS-Ninjaaaa/backend/130-firestore
面接の情報を保存するDBをFirestoreに変更した
2 parents 1901619 + 8e665cc commit 92b14dc

File tree

10 files changed

+317
-14
lines changed

10 files changed

+317
-14
lines changed

backend/.env.sample

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
GOOGLE_API_KEY=
22
GOOGLE_MODEL_NAME=
3+
4+
FIRESTORE_EMULATOR_HOST=
5+
GCP_PROJECT_ID=

backend/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ Dockerコンテナが削除される度にVS Codeの拡張機能をインスト
2626
ホストの環境でFastAPIを起動し、RedisのみをDockerで起動する。
2727

2828
```sh
29-
docker compose up redis -d
29+
docker compose up firestore-emulator -d
3030

3131
source .venv/bin/activate
3232
task start

backend/app/api/dependencies.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44
from app.domain.repositories.interview_repository import InterviewRepository
55
from app.domain.repositories.source_code_repository import SourceCodeRepository
66
from app.infrastructure.llm_clients.google.llm_client import GoogleLLMClient
7+
from app.infrastructure.repositories.firestore.interview_repository import (
8+
FirestoreInterviewRepository,
9+
)
710
from app.infrastructure.repositories.local.source_code_repository import (
811
LocalSourceCodeRepository,
912
)
10-
from app.infrastructure.repositories.redis.interview_repository import (
11-
RedisInterviewRepository,
12-
)
1313
from app.usecase.usecases.get_feedback_usecase import GetFeedbackUseCase
1414
from app.usecase.usecases.get_interview_result_usecase import GetInterviewResultUseCase
1515
from app.usecase.usecases.get_question_usecase import GetQuestionUseCase
@@ -18,7 +18,7 @@
1818

1919

2020
def get_interview_repository() -> InterviewRepository:
21-
return RedisInterviewRepository()
21+
return FirestoreInterviewRepository()
2222

2323

2424
def get_source_code_repository() -> SourceCodeRepository:
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import os
2+
3+
from app.domain.entities.interview_question import InterviewQuestion
4+
from app.domain.repositories.interview_repository import InterviewRepository
5+
from google.cloud import firestore
6+
7+
8+
class FirestoreInterviewRepository(InterviewRepository):
9+
def __init__(self) -> None:
10+
"""コンストラクタ"""
11+
self.db = firestore.Client(project=os.getenv("GCP_PROJECT_ID"))
12+
self.interviews_collection = self.db.collection("interviews")
13+
14+
def create_interview(self, questions: list[InterviewQuestion]) -> None:
15+
"""面接を作成する
16+
17+
Args:
18+
questions (list[InterviewQuestion]): 質問のリスト
19+
"""
20+
# Firestoreのバッチ書き込みを使用して効率的にデータを保存
21+
batch = self.db.batch()
22+
23+
for question in questions:
24+
# 面接IDをドキュメントIDとして使用
25+
# 質問をサブコレクションとして保存
26+
interview_doc = self.interviews_collection.document(question.interview_id)
27+
question_doc = interview_doc.collection("questions").document(
28+
question.question_id
29+
)
30+
31+
# 質問データを辞書形式で保存
32+
question_data = question.to_dict()
33+
batch.set(question_doc, question_data)
34+
35+
# バッチ書き込みを実行
36+
batch.commit()
37+
38+
def get_question(
39+
self,
40+
interview_id: str,
41+
question_id: str,
42+
) -> InterviewQuestion | None:
43+
"""質問を取得する
44+
45+
Args:
46+
interview_id (str): 面接ID
47+
question_id (str): 質問ID
48+
49+
Returns:
50+
InterviewQuestion | None: 質問
51+
"""
52+
try:
53+
# 特定の質問ドキュメントを取得
54+
question_doc = (
55+
self.interviews_collection.document(interview_id)
56+
.collection("questions")
57+
.document(question_id)
58+
.get()
59+
)
60+
61+
if not question_doc.exists:
62+
return None
63+
64+
# ドキュメントデータを辞書形式で取得
65+
question_data = question_doc.to_dict()
66+
if question_data is None:
67+
return None
68+
69+
return InterviewQuestion.from_dict(question_data)
70+
71+
except Exception:
72+
return None
73+
74+
def update_question(
75+
self,
76+
interview_id: str,
77+
question_id: str,
78+
question: InterviewQuestion,
79+
) -> InterviewQuestion:
80+
"""質問の情報を更新する
81+
82+
Args:
83+
interview_id (str): 面接ID
84+
question_id (str): 質問ID
85+
question (InterviewQuestion): 更新後の質問
86+
87+
Returns:
88+
InterviewQuestion: 更新後の質問
89+
"""
90+
# 質問ドキュメントを更新
91+
question_doc = (
92+
self.interviews_collection.document(interview_id)
93+
.collection("questions")
94+
.document(question_id)
95+
)
96+
97+
# 質問データを辞書形式で更新
98+
question_data = question.to_dict()
99+
question_doc.set(question_data)
100+
101+
return question
102+
103+
def get_all_questions(
104+
self,
105+
interview_id: str,
106+
) -> list[InterviewQuestion]:
107+
"""面接の質問をすべて取得する
108+
109+
Args:
110+
interview_id (str): 面接ID
111+
112+
Returns:
113+
list[InterviewQuestion]: 面接の質問のリスト
114+
"""
115+
try:
116+
# 面接の全質問ドキュメントを取得
117+
questions_docs = (
118+
self.interviews_collection.document(interview_id)
119+
.collection("questions")
120+
.stream()
121+
)
122+
123+
questions = []
124+
for doc in questions_docs:
125+
question_data = doc.to_dict()
126+
if question_data is not None:
127+
questions.append(InterviewQuestion.from_dict(question_data))
128+
129+
return questions
130+
131+
except Exception:
132+
return []

backend/docker-compose.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,35 @@ services:
44
build:
55
context: .
66
dockerfile: Dockerfile
7+
container_name: repo-interviewer-app
78
ports:
89
- "8000:8000"
910
environment:
1011
- REDIS_HOST=redis
1112
- REDIS_PORT=6379
1213
depends_on:
1314
- redis
15+
- firestore-emulator
1416
networks:
1517
- app-network
1618

1719
redis:
1820
image: "redis:7-alpine"
21+
container_name: repo-interviewer-redis
1922
ports:
2023
- "6379:6379"
2124
networks:
2225
- app-network
2326

27+
firestore-emulator:
28+
image: gcr.io/google.com/cloudsdktool/cloud-sdk:emulators
29+
container_name: repo-interviewer-firestore-emulator
30+
ports:
31+
- "8080:8080"
32+
command: gcloud beta emulators firestore start --host-port=0.0.0.0:8080
33+
networks:
34+
- app-network
35+
2436
networks:
2537
app-network:
2638
driver: bridge

backend/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ readme = "README.md"
66
requires-python = ">=3.12"
77
dependencies = [
88
"fastapi>=0.115.12",
9+
"google-cloud-firestore>=2.21.0",
910
"google-genai>=1.17.0",
1011
"gunicorn>=23.0.0",
1112
"python-dotenv>=1.1.0",

backend/tests/api/routers/interview/test_get_feedback.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,10 @@ def test_get_feedback_success(
6565
)
6666

6767

68-
def test_get_feedback_missing_question_id(client: TestClient):
68+
def test_get_feedback_missing_question_id(
69+
client: TestClient,
70+
mock_get_feedback_usecase: GetFeedbackUseCase,
71+
):
6972
request_body = {
7073
"message": message,
7174
}
@@ -78,7 +81,10 @@ def test_get_feedback_missing_question_id(client: TestClient):
7881
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
7982

8083

81-
def test_get_feedback_missing_message(client: TestClient):
84+
def test_get_feedback_missing_message(
85+
client: TestClient,
86+
mock_get_feedback_usecase: GetFeedbackUseCase, # GCPのFirestoreへ接続しないため
87+
):
8288
request_body = {
8389
"question_id": question_id,
8490
}
@@ -93,7 +99,7 @@ def test_get_feedback_missing_message(client: TestClient):
9399

94100
def test_get_feedback_use_case_raises_exception(
95101
client: TestClient,
96-
mock_get_feedback_usecase: GetFeedbackUseCase,
102+
mock_get_feedback_usecase: GetFeedbackUseCase, # GCPのFirestoreへ接続しないため
97103
):
98104
mock_get_feedback_usecase.execute.side_effect = Exception()
99105

backend/tests/api/routers/interview/test_get_question.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def test_get_question_success(
5353

5454
def test_get_question_not_found(
5555
client: TestClient,
56-
mock_get_question_usecase: GetQuestionUseCase,
56+
mock_get_question_usecase: GetQuestionUseCase, # GCPのFirestoreへ接続しないため
5757
):
5858
mock_get_question_usecase.execute.return_value = None
5959

@@ -64,7 +64,10 @@ def test_get_question_not_found(
6464
assert response.status_code == status.HTTP_404_NOT_FOUND
6565

6666

67-
def test_get_question_missing_question_id(client: TestClient):
67+
def test_get_question_missing_question_id(
68+
client: TestClient,
69+
mock_get_question_usecase: GetQuestionUseCase, # GCPのFirestoreへ接続しないため
70+
):
6871
response = client.get(
6972
f"/interview/{interview_id}",
7073
)
@@ -74,7 +77,7 @@ def test_get_question_missing_question_id(client: TestClient):
7477

7578
def test_get_question_use_case_raises_exception(
7679
client: TestClient,
77-
mock_get_question_usecase: GetQuestionUseCase,
80+
mock_get_question_usecase: GetQuestionUseCase, # GCPのFirestoreへ接続しないため
7881
):
7982
mock_get_question_usecase.execute.side_effect = Exception()
8083

backend/tests/api/routers/interview/test_set_up_interview.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,10 @@ def test_set_up_interview_success(
7979
)
8080

8181

82-
def test_set_up_interview_missing_source_code(client: TestClient):
82+
def test_set_up_interview_missing_source_code(
83+
client: TestClient,
84+
mock_set_up_interview_usecase: SetUpInterviewUseCase, # GCPのFirestoreへ接続しないため
85+
):
8386
response = client.post(
8487
"/interview",
8588
data={
@@ -91,7 +94,10 @@ def test_set_up_interview_missing_source_code(client: TestClient):
9194
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
9295

9396

94-
def test_set_up_interview_invalid_difficulty_format(client: TestClient):
97+
def test_set_up_interview_invalid_difficulty_format(
98+
client: TestClient,
99+
mock_set_up_interview_usecase: SetUpInterviewUseCase, # GCPのFirestoreへ接続しないため
100+
):
95101
response = client.post(
96102
"/interview",
97103
files={
@@ -110,7 +116,10 @@ def test_set_up_interview_invalid_difficulty_format(client: TestClient):
110116
assert response.status_code == status.HTTP_400_BAD_REQUEST
111117

112118

113-
def test_set_up_interview_minus_total_question(client: TestClient):
119+
def test_set_up_interview_minus_total_question(
120+
client: TestClient,
121+
mock_set_up_interview_usecase: SetUpInterviewUseCase, # GCPのFirestoreへ接続しないため
122+
):
114123
response = client.post(
115124
"/interview",
116125
files={

0 commit comments

Comments
 (0)