Skip to content

面接の情報を保存するDBをFirestoreに変更した #174

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jun 28, 2025
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
3 changes: 3 additions & 0 deletions backend/.env.sample
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
GOOGLE_API_KEY=
GOOGLE_MODEL_NAME=

FIRESTORE_EMULATOR_HOST=
GCP_PROJECT_ID=
2 changes: 1 addition & 1 deletion backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Dockerコンテナが削除される度にVS Codeの拡張機能をインスト
ホストの環境でFastAPIを起動し、RedisのみをDockerで起動する。

```sh
docker compose up redis -d
docker compose up firestore-emulator -d

source .venv/bin/activate
task start
Expand Down
8 changes: 4 additions & 4 deletions backend/app/api/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
from app.domain.repositories.interview_repository import InterviewRepository
from app.domain.repositories.source_code_repository import SourceCodeRepository
from app.infrastructure.llm_clients.google.llm_client import GoogleLLMClient
from app.infrastructure.repositories.firestore.interview_repository import (
FirestoreInterviewRepository,
)
from app.infrastructure.repositories.local.source_code_repository import (
LocalSourceCodeRepository,
)
from app.infrastructure.repositories.redis.interview_repository import (
RedisInterviewRepository,
)
from app.usecase.usecases.get_feedback_usecase import GetFeedbackUseCase
from app.usecase.usecases.get_interview_result_usecase import GetInterviewResultUseCase
from app.usecase.usecases.get_question_usecase import GetQuestionUseCase
Expand All @@ -18,7 +18,7 @@


def get_interview_repository() -> InterviewRepository:
return RedisInterviewRepository()
return FirestoreInterviewRepository()


def get_source_code_repository() -> SourceCodeRepository:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import os

from app.domain.entities.interview_question import InterviewQuestion
from app.domain.repositories.interview_repository import InterviewRepository
from google.cloud import firestore


class FirestoreInterviewRepository(InterviewRepository):
def __init__(self) -> None:
"""コンストラクタ"""
self.db = firestore.Client(project=os.getenv("GCP_PROJECT_ID"))
self.interviews_collection = self.db.collection("interviews")

def create_interview(self, questions: list[InterviewQuestion]) -> None:
"""面接を作成する

Args:
questions (list[InterviewQuestion]): 質問のリスト
"""
# Firestoreのバッチ書き込みを使用して効率的にデータを保存
batch = self.db.batch()

for question in questions:
# 面接IDをドキュメントIDとして使用
# 質問をサブコレクションとして保存
interview_doc = self.interviews_collection.document(question.interview_id)
question_doc = interview_doc.collection("questions").document(
question.question_id
)

# 質問データを辞書形式で保存
question_data = question.to_dict()
batch.set(question_doc, question_data)

# バッチ書き込みを実行
batch.commit()

def get_question(
self,
interview_id: str,
question_id: str,
) -> InterviewQuestion | None:
"""質問を取得する

Args:
interview_id (str): 面接ID
question_id (str): 質問ID

Returns:
InterviewQuestion | None: 質問
"""
try:
# 特定の質問ドキュメントを取得
question_doc = (
self.interviews_collection.document(interview_id)
.collection("questions")
.document(question_id)
.get()
)

if not question_doc.exists:
return None

# ドキュメントデータを辞書形式で取得
question_data = question_doc.to_dict()
if question_data is None:
return None

return InterviewQuestion.from_dict(question_data)

except Exception:
return None

def update_question(
self,
interview_id: str,
question_id: str,
question: InterviewQuestion,
) -> InterviewQuestion:
"""質問の情報を更新する

Args:
interview_id (str): 面接ID
question_id (str): 質問ID
question (InterviewQuestion): 更新後の質問

Returns:
InterviewQuestion: 更新後の質問
"""
# 質問ドキュメントを更新
question_doc = (
self.interviews_collection.document(interview_id)
.collection("questions")
.document(question_id)
)

# 質問データを辞書形式で更新
question_data = question.to_dict()
question_doc.set(question_data)

return question

def get_all_questions(
self,
interview_id: str,
) -> list[InterviewQuestion]:
"""面接の質問をすべて取得する

Args:
interview_id (str): 面接ID

Returns:
list[InterviewQuestion]: 面接の質問のリスト
"""
try:
# 面接の全質問ドキュメントを取得
questions_docs = (
self.interviews_collection.document(interview_id)
.collection("questions")
.stream()
)

questions = []
for doc in questions_docs:
question_data = doc.to_dict()
if question_data is not None:
questions.append(InterviewQuestion.from_dict(question_data))

return questions

except Exception:
return []
12 changes: 12 additions & 0 deletions backend/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,35 @@ services:
build:
context: .
dockerfile: Dockerfile
container_name: repo-interviewer-app
ports:
- "8000:8000"
environment:
- REDIS_HOST=redis
- REDIS_PORT=6379
depends_on:
- redis
- firestore-emulator
networks:
- app-network

redis:
image: "redis:7-alpine"
container_name: repo-interviewer-redis
ports:
- "6379:6379"
networks:
- app-network

firestore-emulator:
image: gcr.io/google.com/cloudsdktool/cloud-sdk:emulators
container_name: repo-interviewer-firestore-emulator
ports:
- "8080:8080"
command: gcloud beta emulators firestore start --host-port=0.0.0.0:8080
networks:
- app-network

networks:
app-network:
driver: bridge
1 change: 1 addition & 0 deletions backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"fastapi>=0.115.12",
"google-cloud-firestore>=2.21.0",
"google-genai>=1.17.0",
"gunicorn>=23.0.0",
"python-dotenv>=1.1.0",
Expand Down
12 changes: 9 additions & 3 deletions backend/tests/api/routers/interview/test_get_feedback.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,10 @@ def test_get_feedback_success(
)


def test_get_feedback_missing_question_id(client: TestClient):
def test_get_feedback_missing_question_id(
client: TestClient,
mock_get_feedback_usecase: GetFeedbackUseCase,
):
request_body = {
"message": message,
}
Expand All @@ -78,7 +81,10 @@ def test_get_feedback_missing_question_id(client: TestClient):
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY


def test_get_feedback_missing_message(client: TestClient):
def test_get_feedback_missing_message(
client: TestClient,
mock_get_feedback_usecase: GetFeedbackUseCase, # GCPのFirestoreへ接続しないため
):
request_body = {
"question_id": question_id,
}
Expand All @@ -93,7 +99,7 @@ def test_get_feedback_missing_message(client: TestClient):

def test_get_feedback_use_case_raises_exception(
client: TestClient,
mock_get_feedback_usecase: GetFeedbackUseCase,
mock_get_feedback_usecase: GetFeedbackUseCase, # GCPのFirestoreへ接続しないため
):
mock_get_feedback_usecase.execute.side_effect = Exception()

Expand Down
9 changes: 6 additions & 3 deletions backend/tests/api/routers/interview/test_get_question.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def test_get_question_success(

def test_get_question_not_found(
client: TestClient,
mock_get_question_usecase: GetQuestionUseCase,
mock_get_question_usecase: GetQuestionUseCase, # GCPのFirestoreへ接続しないため
):
mock_get_question_usecase.execute.return_value = None

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


def test_get_question_missing_question_id(client: TestClient):
def test_get_question_missing_question_id(
client: TestClient,
mock_get_question_usecase: GetQuestionUseCase, # GCPのFirestoreへ接続しないため
):
response = client.get(
f"/interview/{interview_id}",
)
Expand All @@ -74,7 +77,7 @@ def test_get_question_missing_question_id(client: TestClient):

def test_get_question_use_case_raises_exception(
client: TestClient,
mock_get_question_usecase: GetQuestionUseCase,
mock_get_question_usecase: GetQuestionUseCase, # GCPのFirestoreへ接続しないため
):
mock_get_question_usecase.execute.side_effect = Exception()

Expand Down
15 changes: 12 additions & 3 deletions backend/tests/api/routers/interview/test_set_up_interview.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,10 @@ def test_set_up_interview_success(
)


def test_set_up_interview_missing_source_code(client: TestClient):
def test_set_up_interview_missing_source_code(
client: TestClient,
mock_set_up_interview_usecase: SetUpInterviewUseCase, # GCPのFirestoreへ接続しないため
):
response = client.post(
"/interview",
data={
Expand All @@ -91,7 +94,10 @@ def test_set_up_interview_missing_source_code(client: TestClient):
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY


def test_set_up_interview_invalid_difficulty_format(client: TestClient):
def test_set_up_interview_invalid_difficulty_format(
client: TestClient,
mock_set_up_interview_usecase: SetUpInterviewUseCase, # GCPのFirestoreへ接続しないため
):
response = client.post(
"/interview",
files={
Expand All @@ -110,7 +116,10 @@ def test_set_up_interview_invalid_difficulty_format(client: TestClient):
assert response.status_code == status.HTTP_400_BAD_REQUEST


def test_set_up_interview_minus_total_question(client: TestClient):
def test_set_up_interview_minus_total_question(
client: TestClient,
mock_set_up_interview_usecase: SetUpInterviewUseCase, # GCPのFirestoreへ接続しないため
):
response = client.post(
"/interview",
files={
Expand Down
Loading