From 07fb6049b038e295d649a4efd9361e4aa42ae22d Mon Sep 17 00:00:00 2001 From: willook Date: Sun, 18 May 2025 15:41:53 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20face=20=EA=B8=B0=EB=8A=A5,=20regist?= =?UTF-8?q?er,=20auth?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/users.py | 20 ++++++++++++++--- app/face/face_db.py | 17 ++++++++++++--- app/face/face_embedding.py | 15 +++++++++---- app/services/user_service.py | 27 +++++++++++++++++++++-- tests/conftest.py | 22 +++++++++++++++++++ tests/test_api_users.py | 37 +++++++++++++++++++++++++++++++ tests/test_face.py | 41 +++++++++++++++++++++++++++++++++++ tests/test_service_users.py | 42 ++++++++++++++++++++++++++++++++++++ 8 files changed, 209 insertions(+), 12 deletions(-) diff --git a/app/api/users.py b/app/api/users.py index 043dd98..2b9352f 100644 --- a/app/api/users.py +++ b/app/api/users.py @@ -1,5 +1,7 @@ -from fastapi import APIRouter, File, Form, UploadFile +from fastapi import APIRouter, File, Form, HTTPException, UploadFile + from app.models.user import UserResponse +from app.services.user_service import authenticate_user, register_user router = APIRouter() @@ -7,13 +9,25 @@ @router.post("/users/register", response_model=UserResponse) async def register(user_id: str = Form(...), image: UploadFile = File(...)): """회원 등록 엔드포인트""" - pass + image_data = await image.read() + registered_user = register_user(user_id, image_data) + + return UserResponse( + user_id=registered_user["user_id"], + registered_at=registered_user["registered_at"], + ) @router.post("/users/authenticate") async def authenticate(image: UploadFile = File(...)): """회원 인증 엔드포인트""" - pass + image_data = await image.read() + user_id = authenticate_user(image_data) + + if user_id is None: + raise HTTPException(status_code=401, detail="인증 실패: 사용자 없음") + + return {"user_id": user_id} @router.get("/users/{user_id}") diff --git a/app/face/face_db.py b/app/face/face_db.py index 380ae3a..07ebf4f 100644 --- a/app/face/face_db.py +++ b/app/face/face_db.py @@ -1,16 +1,27 @@ +import json +import os +from datetime import datetime + DB_PATH = "face_db.json" def save_user(user_id, embedding): """사용자 등록""" - pass + db = load_db() + db[user_id] = {"embedding": embedding, "registered_at": str(datetime.now())} + with open(DB_PATH, "w") as f: + json.dump(db, f) def load_db(): """데이터베이스 로드""" - pass + if not os.path.exists(DB_PATH): + return {} + with open(DB_PATH, "r") as f: + return json.load(f) def save_db(db): """데이터베이스 저장""" - pass + with open(DB_PATH, "w") as f: + json.dump(db, f) diff --git a/app/face/face_embedding.py b/app/face/face_embedding.py index 3acfb0c..1df879d 100644 --- a/app/face/face_embedding.py +++ b/app/face/face_embedding.py @@ -1,8 +1,15 @@ -def extract_embedding(image): +import numpy as np +from typing import Union +from deepface import DeepFace + + +def extract_embedding(img_path: Union[str, np.ndarray]) -> list: """이미지에서 임베딩 추출""" - pass + embedding = DeepFace.represent(img_path)[0]["embedding"] + return embedding -def verify_embedding(embedding1, embedding2): +def verify_embedding(embedding1: list, embedding2: list) -> bool: """두 임베딩이 같은 사람인지 검증""" - pass + is_same_person = DeepFace.verify(embedding1, embedding2)["verified"] + return is_same_person diff --git a/app/services/user_service.py b/app/services/user_service.py index 410b807..5b5753d 100644 --- a/app/services/user_service.py +++ b/app/services/user_service.py @@ -1,11 +1,34 @@ +from datetime import datetime + +import cv2 +import numpy as np + +from app.face.face_db import load_db, save_user +from app.face.face_embedding import extract_embedding, verify_embedding + + def register_user(user_id: str, image_bytes: bytes) -> dict: """이미지와 ID로 회원 등록""" - pass + image_np = np.frombuffer(image_bytes, dtype=np.uint8) + image = cv2.imdecode(image_np, cv2.IMREAD_COLOR) + embedding = extract_embedding(image) + save_user(user_id, embedding) + return {"user_id": user_id, "registered_at": str(datetime.now())} def authenticate_user(image_bytes: bytes): """이미지로 회원 인증""" - pass + image_np = np.frombuffer(image_bytes, dtype=np.uint8) + image = cv2.imdecode(image_np, cv2.IMREAD_COLOR) + db = load_db() + + embedding_to_check = extract_embedding(image) + + for user_id, data in db.items(): + if verify_embedding(data["embedding"], embedding_to_check): + return user_id + + return None # 인증 실패 def get_user(user_id): diff --git a/tests/conftest.py b/tests/conftest.py index e69de29..427b012 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -0,0 +1,22 @@ +import pytest +from fastapi.testclient import TestClient + +from app.face.face_db import save_user +from app.face.face_embedding import extract_embedding +from app.main import app + + +@pytest.fixture(scope="module") +def client(): + with TestClient(app) as c: + yield c + + +@pytest.fixture(scope="module") +def setup_user_db(): + image_paths = ["images/Aaron_Peirsol/Aaron_Peirsol_0001.jpg"] + user_ids = ["aaron_peirsol"] + for image_path, user_id in zip(image_paths, user_ids): + embedding = extract_embedding(image_path) + save_user(user_id, embedding) + yield diff --git a/tests/test_api_users.py b/tests/test_api_users.py index e69de29..eabc8da 100644 --- a/tests/test_api_users.py +++ b/tests/test_api_users.py @@ -0,0 +1,37 @@ +def test_register_user_api(client): + # Given: 사용자의 ID와 얼굴 이미지가 주어짐 + # Given: API 테스트 클라이언트가 fixture로 제공됨 + image_path = "images/Aaron_Peirsol/Aaron_Peirsol_0001.jpg" + user_id = "aaron_peirsol" + + # When: 사용자 등록 API (POST /users/register)를 호출 + with open(image_path, "rb") as img_file: + response = client.post( + "/users/register", + data={"user_id": user_id}, + files={"image": ("user1.jpg", img_file, "image/jpeg")}, + ) + + # Then: 사용자 등록 기능이 성공(200) + # Then: 응답 내용은 등록된 사용자 ID 및 등록 시각이 포함 + assert response.status_code == 200 + data = response.json() + assert data["user_id"] == user_id + assert data["registered_at"] is not None + + +def test_authenticate_user_api(client, setup_user_db): + # Given: 등록된 사용자의 얼굴 이미지가 주어짐 + image_path = "images/Aaron_Peirsol/Aaron_Peirsol_0001.jpg" + + # When: 사용자 인증 API (POST /users/authenticate)를 호출 + with open(image_path, "rb") as img_file: + response = client.post( + "/users/authenticate", + files={"image": ("user1.jpg", img_file, "image/jpeg")}, + ) + + # Then: 사용자 인증 기능이 성공(200) + assert response.status_code == 200 + data = response.json() + assert data["user_id"] == "aaron_peirsol" diff --git a/tests/test_face.py b/tests/test_face.py index e69de29..3a17646 100644 --- a/tests/test_face.py +++ b/tests/test_face.py @@ -0,0 +1,41 @@ +import pytest + +from app.face.face_embedding import extract_embedding, verify_embedding + + +def test_extract_embedding(): + # Given: 얼굴이 명확하게 보이는 이미지 파일이 주어짐 + image_path = "images/Aaron_Peirsol/Aaron_Peirsol_0001.jpg" + + # When: 얼굴 임베딩 추출 기능을 실행 + embedding = extract_embedding(image_path) + + # Then: 임베딩이 추출된다. 임베딩 길이는 0보다 큼 + assert embedding is not None + assert len(embedding) > 0 + + +def test_no_face_exception(): + # Given: 얼굴이 명확하게 보이지 않는 이미지 파일이 주어짐 + image_path = "tests/images/no_face.jpg" + + # When: 얼굴 임베딩 추출 기능을 실행 + # Then: 예외가 발생함 + with pytest.raises(ValueError): + extract_embedding(image_path) + + +def test_verify_embedding(): + # Given: 얼굴 임베딩이 주어짐 + image_path1 = "images/Aaron_Peirsol/Aaron_Peirsol_0001.jpg" + image_path2 = "images/Aaron_Peirsol/Aaron_Peirsol_0002.jpg" + image_path3 = "images/Olivia_Newton-John/Olivia_Newton-John_0001.jpg" + + embedding1 = extract_embedding(image_path1) + embedding2 = extract_embedding(image_path2) + embedding3 = extract_embedding(image_path3) + + # When: 얼굴 임베딩 검증 기능을 실행 + # Then: 동일 사람인 경우 검증 결과가 True, 다른 사람인 경우 검증 결과가 False + assert verify_embedding(embedding1, embedding2) + assert not verify_embedding(embedding1, embedding3) diff --git a/tests/test_service_users.py b/tests/test_service_users.py index e69de29..3f4cd3e 100644 --- a/tests/test_service_users.py +++ b/tests/test_service_users.py @@ -0,0 +1,42 @@ +from app.services.user_service import authenticate_user, register_user + + +def test_register_user(): + # Given: 사용자의 ID와 얼굴 이미지가 주어짐 + image_path = "images/Aaron_Peirsol/Aaron_Peirsol_0001.jpg" + user_id = "aaron_peirsol" + + # When: 사용자 등록 기능을 실행 + with open(image_path, "rb") as f: + image_data = f.read() + result = register_user(user_id, image_data) + + # Then: 사용자 등록 기능이 성공 + assert result["user_id"] == "aaron_peirsol" + assert result["registered_at"] is not None + + +def test_authenticate_registered_user(setup_user_db): + # Given: 이미 등록된 사용자의 얼굴이미지가 주어짐 + image_path = "images/Aaron_Peirsol/Aaron_Peirsol_0001.jpg" + + # When: 등록된 사용자와 동일한 인물의 얼굴 이미지를 제출 + with open(image_path, "rb") as f: + image_data = f.read() + user_id = authenticate_user(image_data) + + # Then: 인증 기능이 성공하여 사용자 ID를 반환 + assert user_id == "aaron_peirsol" + + +def test_authenticate_unregistered_user(setup_user_db): + # Given: 등록되지 않은 사용자의 얼굴 이미지 + image_path = "images/Natasha_McElhone/Natasha_McElhone_0001.jpg" + with open(image_path, "rb") as f: + image_data = f.read() + + # When: 인증 서비스를 호출 + user_id = authenticate_user(image_data) + + # Then: 인증 실패(None 반환) 해야 함 + assert user_id is None From 63f33a195316d8e69260059621478c635a60c52d Mon Sep 17 00:00:00 2001 From: willook Date: Sun, 18 May 2025 15:51:37 +0900 Subject: [PATCH 2/4] feat: get --- app/api/users.py | 9 +++++++-- app/services/user_service.py | 8 +++++++- tests/test_api_users.py | 14 ++++++++++++++ tests/test_service_users.py | 14 +++++++++++++- 4 files changed, 41 insertions(+), 4 deletions(-) diff --git a/app/api/users.py b/app/api/users.py index 2b9352f..71136d2 100644 --- a/app/api/users.py +++ b/app/api/users.py @@ -1,7 +1,7 @@ from fastapi import APIRouter, File, Form, HTTPException, UploadFile from app.models.user import UserResponse -from app.services.user_service import authenticate_user, register_user +from app.services.user_service import authenticate_user, get_user, register_user router = APIRouter() @@ -33,7 +33,12 @@ async def authenticate(image: UploadFile = File(...)): @router.get("/users/{user_id}") def get_user_info(user_id: str): """회원 정보 조회 엔드포인트""" - pass + user_info = get_user(user_id) + + if user_info is None: + raise HTTPException(status_code=404, detail="사용자가 존재하지 않습니다.") + + return user_info @router.delete("/users/{user_id}") diff --git a/app/services/user_service.py b/app/services/user_service.py index 5b5753d..455d5a5 100644 --- a/app/services/user_service.py +++ b/app/services/user_service.py @@ -33,7 +33,13 @@ def authenticate_user(image_bytes: bytes): def get_user(user_id): """user_id로 회원 정보 조회""" - pass + db = load_db() + user_data = db.get(user_id) + + if user_data: + return {"user_id": user_id, "registered_at": user_data["registered_at"]} + else: + return None # 사용자 없음 def delete_user(user_id): diff --git a/tests/test_api_users.py b/tests/test_api_users.py index eabc8da..a27f77a 100644 --- a/tests/test_api_users.py +++ b/tests/test_api_users.py @@ -35,3 +35,17 @@ def test_authenticate_user_api(client, setup_user_db): assert response.status_code == 200 data = response.json() assert data["user_id"] == "aaron_peirsol" + + +def test_get_registered_user_api(client, setup_user_db): + # Given: 등록된 사용자 ID + user_id = "aaron_peirsol" + + # When: 회원 조회 API 호출 + response = client.get(f"/users/{user_id}") + + # Then: 성공적으로 사용자 정보를 반환 + assert response.status_code == 200 + data = response.json() + assert data["user_id"] == user_id + assert "registered_at" in data diff --git a/tests/test_service_users.py b/tests/test_service_users.py index 3f4cd3e..a6e5276 100644 --- a/tests/test_service_users.py +++ b/tests/test_service_users.py @@ -1,4 +1,4 @@ -from app.services.user_service import authenticate_user, register_user +from app.services.user_service import authenticate_user, get_user, register_user def test_register_user(): @@ -40,3 +40,15 @@ def test_authenticate_unregistered_user(setup_user_db): # Then: 인증 실패(None 반환) 해야 함 assert user_id is None + + +def test_get_registered_user(setup_user_db): + # Given: 이미 등록된 사용자 ID + user_id = "aaron_peirsol" + + # When: 사용자 정보 조회 서비스를 호출 + user_info = get_user(user_id) + + # Then: 등록 시각 등 사용자 정보를 정확히 반환해야 함 + assert user_info["user_id"] == user_id + assert "registered_at" in user_info From 1c42e28357c214bf7b140e47a8343cde488cf6b1 Mon Sep 17 00:00:00 2001 From: willook Date: Sun, 18 May 2025 16:06:07 +0900 Subject: [PATCH 3/4] feat: delete --- app/api/users.py | 14 ++++++++++++-- app/services/user_service.py | 9 +++++++-- tests/test_api_users.py | 15 +++++++++++++++ tests/test_service_users.py | 22 +++++++++++++++++++++- 4 files changed, 55 insertions(+), 5 deletions(-) diff --git a/app/api/users.py b/app/api/users.py index 71136d2..e96a989 100644 --- a/app/api/users.py +++ b/app/api/users.py @@ -1,7 +1,12 @@ from fastapi import APIRouter, File, Form, HTTPException, UploadFile from app.models.user import UserResponse -from app.services.user_service import authenticate_user, get_user, register_user +from app.services.user_service import ( + authenticate_user, + delete_user, + get_user, + register_user, +) router = APIRouter() @@ -44,4 +49,9 @@ def get_user_info(user_id: str): @router.delete("/users/{user_id}") def delete_user_info(user_id: str): """회원 삭제 엔드포인트""" - pass + result = delete_user(user_id) + + if not result: + raise HTTPException(status_code=404, detail="사용자가 존재하지 않습니다.") + + return {"message": "사용자가 성공적으로 삭제되었습니다."} diff --git a/app/services/user_service.py b/app/services/user_service.py index 455d5a5..b38e6b9 100644 --- a/app/services/user_service.py +++ b/app/services/user_service.py @@ -3,7 +3,7 @@ import cv2 import numpy as np -from app.face.face_db import load_db, save_user +from app.face.face_db import load_db, save_db, save_user from app.face.face_embedding import extract_embedding, verify_embedding @@ -44,4 +44,9 @@ def get_user(user_id): def delete_user(user_id): """user_id로 회원 삭제""" - pass + db = load_db() + if user_id in db: + del db[user_id] + save_db(db) + return True + return False diff --git a/tests/test_api_users.py b/tests/test_api_users.py index a27f77a..97cb353 100644 --- a/tests/test_api_users.py +++ b/tests/test_api_users.py @@ -49,3 +49,18 @@ def test_get_registered_user_api(client, setup_user_db): data = response.json() assert data["user_id"] == user_id assert "registered_at" in data + + +def test_delete_registered_user_api(client, setup_user_db): + # Given: 등록된 사용자 ID + user_id = "aaron_peirsol" + + # When: 회원 삭제 API 호출 + response = client.delete(f"/users/{user_id}") + + # Then: 삭제 성공 확인 + assert response.status_code == 200 + + # 추가 확인: 삭제된 사용자 조회 시 404 확인 + response = client.get(f"/users/{user_id}") + assert response.status_code == 404 diff --git a/tests/test_service_users.py b/tests/test_service_users.py index a6e5276..8410a1a 100644 --- a/tests/test_service_users.py +++ b/tests/test_service_users.py @@ -1,4 +1,9 @@ -from app.services.user_service import authenticate_user, get_user, register_user +from app.services.user_service import ( + authenticate_user, + delete_user, + get_user, + register_user, +) def test_register_user(): @@ -52,3 +57,18 @@ def test_get_registered_user(setup_user_db): # Then: 등록 시각 등 사용자 정보를 정확히 반환해야 함 assert user_info["user_id"] == user_id assert "registered_at" in user_info + + +def test_delete_registered_user(setup_user_db): + # Given: 이미 등록된 사용자 ID + user_id = "aaron_peirsol" + + # When: 사용자 삭제 서비스를 호출하면 + result = delete_user(user_id) + + # Then: 삭제가 성공적으로 이루어졌음을 반환 + assert result is True + + # 추가 확인: 사용자가 실제로 삭제되었는지 조회하여 확인 + user_info = get_user(user_id) + assert user_info is None From f428ef8662063b5f2b88cc9268f76171f2420438 Mon Sep 17 00:00:00 2001 From: 202102711JiHyunWoo <105435641+202102711JiHyunWoo@users.noreply.github.com> Date: Tue, 27 May 2025 10:36:14 +0900 Subject: [PATCH 4/4] =?UTF-8?q?202102711=20=EC=A7=80=ED=98=84=EC=9A=B0=20t?= =?UTF-8?q?esting-python-week2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_service_users.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/test_service_users.py b/tests/test_service_users.py index 8410a1a..d3c44e4 100644 --- a/tests/test_service_users.py +++ b/tests/test_service_users.py @@ -72,3 +72,15 @@ def test_delete_registered_user(setup_user_db): # 추가 확인: 사용자가 실제로 삭제되었는지 조회하여 확인 user_info = get_user(user_id) assert user_info is None + + +def test_no_exist_user(setup_user_db): + + # Given: 등록되지 않은 회원 + user_id = "no_exist_user" + + # When: 사용자 정보 조회 + user_getid = get_user(user_id) + + # Then: 조회 실패 None 반환 + assert user_getid is None