Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
6 changes: 4 additions & 2 deletions wacruit/src/apps/member/views.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from http import HTTPStatus
from typing import Annotated

from fastapi import APIRouter
from fastapi import Depends
from fastapi import Response
from fastapi import Security
from fastapi.security import APIKeyHeader

Expand All @@ -17,13 +19,13 @@
v3_router = APIRouter(prefix="/v3/members", tags=["members"])


@v3_router.post("")
@v3_router.post("", status_code=HTTPStatus.CREATED)
def create_member(
admin_user: AdminUser,
request: MemberCreateRequest,
member_service: Annotated[MemberService, Depends()],
):
return member_service.create_member(request)
member_service.create_member(request)


@v3_router.get("/{member_id}")
Expand Down
5 changes: 5 additions & 0 deletions wacruit/src/apps/project/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,8 @@ def __init__(self):
class ProjectAlreadyExistsException(WacruitException):
def __init__(self):
super().__init__(status_code=409, detail="이미 존재하는 프로젝트입니다.")


class ProjectImageNotFoundException(WacruitException):
def __init__(self):
super().__init__(status_code=404, detail="존재하지 않는 프로젝트 이미지입니다.")
11 changes: 6 additions & 5 deletions wacruit/src/apps/project/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class Project(DeclarativeBase):
urls: Mapped[list["ProjectURL"] | None] = relationship(
back_populates="source_project"
)
image_urls: Mapped[list["ProjectImageURL"] | None] = relationship(
images: Mapped[list["ProjectImage"] | None] = relationship(
back_populates="source_project"
)

Expand All @@ -48,11 +48,12 @@ class ProjectURL(DeclarativeBase):
source_project: Mapped["Project"] = relationship(back_populates="urls")


class ProjectImageURL(DeclarativeBase):
__tablename__ = "project_image_url"
class ProjectImage(DeclarativeBase):
__tablename__ = "project_image"

id: Mapped[intpk]
project_id: Mapped[int] = mapped_column(ForeignKey("project.id"))
url: Mapped[str255]
object_key: Mapped[str255]
is_uploaded: Mapped[bool] = mapped_column(default=False)

source_project: Mapped["Project"] = relationship(back_populates="image_urls")
source_project: Mapped["Project"] = relationship(back_populates="images")
19 changes: 17 additions & 2 deletions wacruit/src/apps/project/repositories.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from fastapi import Depends
from sqlalchemy import update
from sqlalchemy.orm import joinedload
from sqlalchemy.orm import Session

from wacruit.src.apps.project.models import Project
from wacruit.src.apps.project.models import ProjectImageURL
from wacruit.src.apps.project.models import ProjectImage
from wacruit.src.apps.project.models import ProjectURL
from wacruit.src.database.connection import get_db_session
from wacruit.src.database.connection import Transaction
Expand All @@ -23,7 +24,7 @@ def get_project_by_id(self, project_id: int) -> Project | None:
self.session.query(Project)
.options(
joinedload(Project.urls),
joinedload(Project.image_urls),
joinedload(Project.images),
)
.filter(Project.id == project_id)
.first()
Expand All @@ -45,3 +46,17 @@ def update_project(self, project: Project) -> Project:
self.session.merge(project)

return project

def get_project_image_by_id(self, image_id: int) -> ProjectImage | None:
return (
self.session.query(ProjectImage).filter(ProjectImage.id == image_id).first()
)

def update_project_image(self, project_image_id: int) -> None:
with self.transaction:
query = (
update(ProjectImage)
.where(ProjectImage.id == project_image_id)
.values(is_uploaded=True)
)
self.session.execute(query)
27 changes: 22 additions & 5 deletions wacruit/src/apps/project/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ class ProjectCreateRequest(BaseModel):
thumbnail_url: str | None = Field(None, max_length=255)
project_type: ProjectType
is_active: bool
images: list[str] | None
urls: list[ProjectLinkDto] | None


Expand All @@ -31,10 +30,16 @@ class ProjectUpdateRequest(BaseModel):
thumbnail_url: str | None = Field(None, max_length=255)
project_type: ProjectType | None
is_active: bool | None
images: list[str] | None
urls: list[ProjectLinkDto] | None


class ProjectImageResponse(OrmModel):
id: int
project_id: int
object_key: str
is_uploaded: bool


class ProjectDetailResponse(OrmModel):
id: int
name: str
Expand All @@ -43,7 +48,7 @@ class ProjectDetailResponse(OrmModel):
thumbnail_url: str | None
project_type: ProjectType
is_active: bool
images: list[str] | None
images: list[ProjectImageResponse] | None
urls: list[ProjectLinkDto] | None

@classmethod
Expand All @@ -55,8 +60,8 @@ def from_orm(cls, obj):
]

images = None
if obj.image_urls:
images = [img.url for img in obj.image_urls]
if obj.images:
images = [ProjectImageResponse.from_orm(image) for image in obj.images]

return cls(
id=obj.id,
Expand Down Expand Up @@ -91,5 +96,17 @@ def from_orm(cls, obj):
)


class ProjectImageUploadRequest(BaseModel):
project_id: int
file_name: str


class ProjectListResponse(OrmModel):
projects: list[ProjectBriefResponse]


class PresignedUrlWithIdResponse(BaseModel):
object_name: str
presigned_url: str
fields: dict[str, str] = Field(default={})
project_image_id: int
100 changes: 83 additions & 17 deletions wacruit/src/apps/project/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,41 @@

from fastapi import Depends

from wacruit.src.apps.common.enums import ProjectType
from wacruit.src.apps.common.exceptions import InvalidProjectTypeException
from wacruit.src.apps.common.schemas import ListResponse
from wacruit.src.apps.member.exceptions import MemberAlreadyExistsException
from wacruit.src.apps.member.exceptions import MemberNotFoundException
from wacruit.src.apps.member.repositories import MemberRepository
from wacruit.src.apps.portfolio.file.aws.config import s3_config
from wacruit.src.apps.portfolio.file.aws.s3.client import S3Client
from wacruit.src.apps.portfolio.file.aws.s3.method import S3PresignedUrlMethod
from wacruit.src.apps.portfolio.file.aws.s3.utils import generate_presigned_post_url
from wacruit.src.apps.portfolio.file.aws.s3.utils import generate_presigned_url
from wacruit.src.apps.project.exceptions import ProjectAlreadyExistsException
from wacruit.src.apps.project.exceptions import ProjectImageNotFoundException
from wacruit.src.apps.project.exceptions import ProjectNotFoundException
from wacruit.src.apps.project.models import Project
from wacruit.src.apps.project.models import ProjectImageURL
from wacruit.src.apps.project.models import ProjectImage
from wacruit.src.apps.project.models import ProjectURL
from wacruit.src.apps.project.repositories import ProjectRepository
from wacruit.src.apps.project.schemas import PresignedUrlWithIdResponse
from wacruit.src.apps.project.schemas import ProjectBriefResponse
from wacruit.src.apps.project.schemas import ProjectCreateRequest
from wacruit.src.apps.project.schemas import ProjectDetailResponse
from wacruit.src.apps.project.schemas import ProjectLinkDto
from wacruit.src.apps.project.schemas import ProjectImageResponse
from wacruit.src.apps.project.schemas import ProjectUpdateRequest

_1_MIN = 60
_10_MIN = 10 * _1_MIN
_1_MB = 1024 * 1024
_50_MB = 50 * _1_MB


class ProjectService:
def __init__(
self,
member_repository: MemberRepository = Depends(),
project_repository: ProjectRepository = Depends(),
) -> None:
self._s3_config = s3_config
self._s3_client = S3Client(region_name=self._s3_config.bucket_region)
self.member_repository = member_repository
self.project_repository = project_repository

Expand All @@ -46,16 +56,6 @@ def create_project(self, request: ProjectCreateRequest):
# 프로젝트를 먼저 생성하여 project_id를 얻음
created_project = self.project_repository.create_project(project)

# 프로젝트가 생성된 후 이미지와 URL 추가
if request.images:
if created_project.image_urls is None:
created_project.image_urls = []
for image_url in request.images:
image_obj = ProjectImageURL(
project_id=created_project.id, url=image_url
)
created_project.image_urls.append(image_obj)

if request.urls:
if created_project.urls is None:
created_project.urls = []
Expand All @@ -68,7 +68,7 @@ def create_project(self, request: ProjectCreateRequest):
created_project.urls.append(url_obj)

# 이미지와 URL이 추가된 프로젝트를 업데이트
if request.images or request.urls:
if request.urls:
created_project = self.project_repository.update_project(created_project)

def get_project(self, project_id: int) -> ProjectDetailResponse:
Expand Down Expand Up @@ -98,3 +98,69 @@ def update_project(
if not updated_project:
raise ProjectNotFoundException
return ProjectDetailResponse.from_orm(updated_project)

def get_project_object_name(self, project_id: int, file_name: str) -> str:
return f"PROJECT/{project_id}/{file_name}"

def generate_presigned_url_for_post_image(
self, project_id: int, file_name: str
) -> PresignedUrlWithIdResponse:
project = self.project_repository.get_project_by_id(project_id)
if not project:
raise ProjectNotFoundException
object_name = self.get_project_object_name(project_id, file_name)
url, fields = generate_presigned_post_url(
s3_client=self._s3_client.client,
s3_bucket=self._s3_config.bucket_name,
s3_object=object_name,
expires_in=_10_MIN,
conditions=[
["content-length-range", 0, _50_MB],
],
)
project_image = ProjectImage(
project_id=project_id,
object_key=object_name,
)
if project.images is None:
project.images = []
project.images.append(project_image)
self.project_repository.update_project(project)
return PresignedUrlWithIdResponse(
object_name=object_name,
presigned_url=url,
fields=fields,
project_image_id=project_image.id,
)

def generate_presigned_url_for_get_image(
self, file_id: int
) -> PresignedUrlWithIdResponse:
project_image = self.project_repository.get_project_image_by_id(file_id)
if not project_image or not project_image.is_uploaded:
raise ProjectImageNotFoundException
object_name = project_image.object_key
url = generate_presigned_url(
s3_client=self._s3_client.client,
client_method=S3PresignedUrlMethod.GET,
method_parameters={
"Bucket": self._s3_config.bucket_name,
"Key": object_name,
},
expires_in=_10_MIN,
)
return PresignedUrlWithIdResponse(
object_name=object_name,
presigned_url=url,
project_image_id=project_image.id,
)

def register_project_image_info_in_db(self, file_id: int) -> ProjectImageResponse:
project_image = self.project_repository.get_project_image_by_id(file_id)
if not project_image:
raise ProjectImageNotFoundException
self.project_repository.update_project_image(
project_image_id=file_id,
)
project_image = self.project_repository.get_project_image_by_id(file_id)
return ProjectImageResponse.from_orm(project_image)
68 changes: 31 additions & 37 deletions wacruit/src/apps/project/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@
from fastapi import Depends

from wacruit.src.apps.common.schemas import ListResponse
from wacruit.src.apps.project.schemas import PresignedUrlWithIdResponse
from wacruit.src.apps.project.schemas import ProjectBriefResponse
from wacruit.src.apps.project.schemas import ProjectCreateRequest
from wacruit.src.apps.project.schemas import ProjectDetailResponse
from wacruit.src.apps.project.schemas import ProjectImageResponse
from wacruit.src.apps.project.schemas import ProjectImageUploadRequest
from wacruit.src.apps.project.schemas import ProjectUpdateRequest
from wacruit.src.apps.project.services import ProjectService
from wacruit.src.apps.user.dependencies import AdminUser
Expand All @@ -21,7 +24,7 @@ def create_project(
project_service: Annotated[ProjectService, Depends()],
admin_user: AdminUser,
):
return project_service.create_project(request)
project_service.create_project(request)


@v3_router.get("/{project_id}")
Expand Down Expand Up @@ -50,39 +53,30 @@ def update_project(
return project_service.update_project(project_id, request)


# @v3_router.post("/{project_id}/members")
# def add_project_member(
# admin_user: AdminUser,
# project_id: int,
# request: ProjectMemberCreateRequest,
# project_service: Annotated[ProjectService, Depends()],
# ):
# return project_service.add_project_member(project_id, request)


# @v3_router.get("/{project_id}/members")
# def list_project_members(
# project_id: int, project_service: Annotated[ProjectService, Depends()]
# ) -> ListResponse[ProjectMemberResponse]:
# return project_service.list_project_members(project_id)


# @v3_router.patch("/{project_id}/members/{member_id}")
# def update_project_member(
# admin_user: AdminUser,
# project_id: int,
# member_id: int,
# request: ProjectMemberUpdateRequest,
# project_service: Annotated[ProjectService, Depends()],
# ) -> ListResponse[ProjectMemberResponse]:
# return project_service.update_project_member(project_id, member_id, request)


# @v3_router.delete("/{project_id}/members/{member_id}")
# def delete_project_member(
# admin_user: AdminUser,
# project_id: int,
# member_id: int,
# project_service: Annotated[ProjectService, Depends()],
# ) -> ListResponse[ProjectMemberResponse]:
# return project_service.delete_project_member(project_id, member_id)
@v3_router.post("/image/upload")
def get_upload_project_image_url(
admin_user: AdminUser,
request: ProjectImageUploadRequest,
project_service: Annotated[ProjectService, Depends()],
) -> PresignedUrlWithIdResponse:
return project_service.generate_presigned_url_for_post_image(
project_id=request.project_id, file_name=request.file_name
)


@v3_router.get("/image/download/{file_id}")
def get_download_project_image_url(
admin_user: AdminUser,
file_id: int,
project_service: Annotated[ProjectService, Depends()],
) -> PresignedUrlWithIdResponse:
return project_service.generate_presigned_url_for_get_image(file_id=file_id)


@v3_router.get("/image/check-upload-completed/{file_id}")
def check_upload_project_image_completed(
admin_user: AdminUser,
file_id: int,
project_service: Annotated[ProjectService, Depends()],
) -> ProjectImageResponse:
return project_service.register_project_image_info_in_db(file_id=file_id)
Loading