Skip to content

Commit 707c9f3

Browse files
committed
♻️ application 패키지 리팩토링
1 parent 89c6a18 commit 707c9f3

22 files changed

Lines changed: 614 additions & 452 deletions

alembic/env.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,10 @@
1818

1919
from app.core.config import settings
2020
from app.db.base import Base
21-
import app.models.application # noqa: F401
22-
import app.models.email_verification # noqa: F401
23-
import app.models.event # noqa: F401
24-
import app.models.review_image # noqa: F401
25-
import app.models.review_submission # noqa: F401
21+
import app.application.models # noqa: F401
22+
import app.auth.models # noqa: F401
23+
import app.event.models # noqa: F401
2624
import app.store.models # noqa: F401
27-
import app.models.user # noqa: F401
2825

2926
target_metadata = Base.metadata
3027

app/api/v1/router.py

Lines changed: 0 additions & 6 deletions
This file was deleted.

app/application/__init__.py

Whitespace-only changes.

app/application/dependencies.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from fastapi import Depends
2+
from sqlalchemy.ext.asyncio import AsyncSession
3+
4+
from app.application.repository import ApplicationRepository
5+
from app.application.repository_impl import ApplicationRepositoryImpl
6+
from app.application.service import ApplicationService
7+
from app.application.service_impl import ApplicationServiceImpl
8+
from app.db.session import get_db
9+
10+
11+
def get_application_repository(
12+
db: AsyncSession = Depends(get_db),
13+
) -> ApplicationRepository:
14+
return ApplicationRepositoryImpl(db)
15+
16+
17+
def get_application_service(
18+
repo: ApplicationRepository = Depends(get_application_repository),
19+
) -> ApplicationService:
20+
return ApplicationServiceImpl(repo)
Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
from datetime import datetime
22
from uuid import UUID, uuid4
33

4-
from sqlalchemy import DateTime, Enum, ForeignKey, String, UniqueConstraint
4+
from sqlalchemy import (
5+
DateTime,
6+
Enum,
7+
ForeignKey,
8+
Integer,
9+
String,
10+
Text,
11+
UniqueConstraint,
12+
)
513
from sqlalchemy.orm import Mapped, mapped_column
614
from sqlalchemy.sql import func
715

@@ -24,3 +32,25 @@ class Application(Base):
2432
applied_at: Mapped[datetime] = mapped_column(
2533
DateTime(timezone=True), server_default=func.now()
2634
)
35+
36+
37+
class ReviewSubmission(Base):
38+
__tablename__ = "review_submissions"
39+
40+
id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid4)
41+
application_id: Mapped[UUID] = mapped_column(
42+
ForeignKey("applications.id"), unique=True
43+
)
44+
message: Mapped[str] = mapped_column(Text)
45+
created_at: Mapped[datetime] = mapped_column(
46+
DateTime(timezone=True), server_default=func.now()
47+
)
48+
49+
50+
class ReviewImage(Base):
51+
__tablename__ = "review_images"
52+
53+
id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid4)
54+
submission_id: Mapped[UUID] = mapped_column(ForeignKey("review_submissions.id"))
55+
image_key: Mapped[str] = mapped_column(String)
56+
order: Mapped[int] = mapped_column(Integer)

app/application/repository.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from abc import ABC, abstractmethod
2+
from uuid import UUID
3+
4+
from app.application.models import Application, ReviewImage, ReviewSubmission
5+
from app.auth.models import User
6+
from app.event.models import Event
7+
8+
9+
class ApplicationRepository(ABC):
10+
@abstractmethod
11+
async def find_event_by_id(self, event_id: UUID) -> Event | None: ...
12+
13+
@abstractmethod
14+
async def find_by_id(self, application_id: UUID) -> Application | None: ...
15+
16+
@abstractmethod
17+
async def find_by_event_and_reviewer(
18+
self, event_id: UUID, reviewer_id: UUID
19+
) -> Application | None: ...
20+
21+
@abstractmethod
22+
async def find_by_reviewer_id(
23+
self, reviewer_id: UUID, *, offset: int, limit: int
24+
) -> tuple[list[Application], int]: ...
25+
26+
@abstractmethod
27+
async def find_user_by_id(self, user_id: UUID) -> User | None: ...
28+
29+
@abstractmethod
30+
async def find_submission_by_application_id(
31+
self, application_id: UUID
32+
) -> ReviewSubmission | None: ...
33+
34+
@abstractmethod
35+
async def find_images_by_submission_id(
36+
self, submission_id: UUID
37+
) -> list[ReviewImage]: ...
38+
39+
@abstractmethod
40+
async def save_application(self, application: Application) -> None: ...
41+
42+
@abstractmethod
43+
async def delete(self, application: Application) -> None: ...
44+
45+
@abstractmethod
46+
async def save_review(
47+
self, application_id: UUID, message: str, image_keys: list[str]
48+
) -> None: ...

app/application/repository_impl.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
from typing import cast
2+
from uuid import UUID
3+
4+
from sqlalchemy import func, select
5+
from sqlalchemy.exc import IntegrityError
6+
from sqlalchemy.ext.asyncio import AsyncSession
7+
8+
from app.application.models import Application, ReviewImage, ReviewSubmission
9+
from app.application.repository import ApplicationRepository
10+
from app.auth.models import User
11+
from app.core.exceptions import APPLICATION_003_APPLY, AppException
12+
from app.event.models import Event
13+
14+
15+
class ApplicationRepositoryImpl(ApplicationRepository):
16+
def __init__(self, db: AsyncSession) -> None:
17+
self.db = db
18+
19+
async def find_event_by_id(self, event_id: UUID) -> Event | None:
20+
return cast(
21+
Event | None,
22+
await self.db.scalar(select(Event).where(Event.id == event_id)),
23+
)
24+
25+
async def find_by_id(self, application_id: UUID) -> Application | None:
26+
return cast(
27+
Application | None,
28+
await self.db.scalar(
29+
select(Application).where(Application.id == application_id)
30+
),
31+
)
32+
33+
async def find_by_event_and_reviewer(
34+
self, event_id: UUID, reviewer_id: UUID
35+
) -> Application | None:
36+
return cast(
37+
Application | None,
38+
await self.db.scalar(
39+
select(Application).where(
40+
Application.event_id == event_id,
41+
Application.reviewer_id == reviewer_id,
42+
)
43+
),
44+
)
45+
46+
async def find_by_reviewer_id(
47+
self, reviewer_id: UUID, *, offset: int, limit: int
48+
) -> tuple[list[Application], int]:
49+
q = select(Application).where(Application.reviewer_id == reviewer_id)
50+
total = (
51+
await self.db.scalar(select(func.count()).select_from(q.subquery())) or 0
52+
)
53+
apps = list((await self.db.scalars(q.offset(offset).limit(limit))).all())
54+
return apps, int(total)
55+
56+
async def find_user_by_id(self, user_id: UUID) -> User | None:
57+
return cast(
58+
User | None,
59+
await self.db.scalar(select(User).where(User.id == user_id)),
60+
)
61+
62+
async def find_submission_by_application_id(
63+
self, application_id: UUID
64+
) -> ReviewSubmission | None:
65+
return cast(
66+
ReviewSubmission | None,
67+
await self.db.scalar(
68+
select(ReviewSubmission).where(
69+
ReviewSubmission.application_id == application_id
70+
)
71+
),
72+
)
73+
74+
async def find_images_by_submission_id(
75+
self, submission_id: UUID
76+
) -> list[ReviewImage]:
77+
return list(
78+
(
79+
await self.db.scalars(
80+
select(ReviewImage)
81+
.where(ReviewImage.submission_id == submission_id)
82+
.order_by(ReviewImage.order)
83+
)
84+
).all()
85+
)
86+
87+
async def save_application(self, application: Application) -> None:
88+
self.db.add(application)
89+
try:
90+
await self.db.commit()
91+
except IntegrityError as e:
92+
await self.db.rollback()
93+
raise AppException(APPLICATION_003_APPLY) from e
94+
95+
async def delete(self, application: Application) -> None:
96+
await self.db.delete(application)
97+
await self.db.commit()
98+
99+
async def save_review(
100+
self, application_id: UUID, message: str, image_keys: list[str]
101+
) -> None:
102+
submission = ReviewSubmission(
103+
application_id=application_id,
104+
message=message,
105+
)
106+
self.db.add(submission)
107+
await self.db.flush()
108+
for i, key in enumerate(image_keys):
109+
self.db.add(
110+
ReviewImage(submission_id=submission.id, image_key=key, order=i)
111+
)
112+
await self.db.commit()
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
from fastapi import APIRouter, BackgroundTasks, Depends, Query
2-
from sqlalchemy.ext.asyncio import AsyncSession
32

43
from app.api.v1.deps import require_login
5-
from app.db.session import get_db
6-
from app.schemas.application import (
4+
from app.application.dependencies import get_application_service
5+
from app.application.schemas import (
76
ApplicationCreateReq,
87
ApplicationListResp,
98
ReviewSubmissionReq,
109
)
11-
from app.services import application as application_service
10+
from app.application.service import ApplicationService
1211

1312
router = APIRouter(tags=["Application"])
1413

@@ -17,24 +16,20 @@
1716
async def create_application(
1817
body: ApplicationCreateReq,
1918
background_tasks: BackgroundTasks,
20-
db: AsyncSession = Depends(get_db),
2119
reviewer_id: str = Depends(require_login),
20+
service: ApplicationService = Depends(get_application_service),
2221
) -> None:
23-
await application_service.create_application(
24-
db, reviewer_id, body, background_tasks
25-
)
22+
await service.create_application(reviewer_id, body, background_tasks)
2623

2724

2825
@router.get("/application", response_model=ApplicationListResp)
2926
async def get_my_applications(
3027
page: int = Query(default=0, ge=0),
3128
size: int = Query(default=20, ge=1),
32-
db: AsyncSession = Depends(get_db),
3329
reviewer_id: str = Depends(require_login),
30+
service: ApplicationService = Depends(get_application_service),
3431
) -> ApplicationListResp:
35-
applications, total = await application_service.list_my_applications(
36-
db, reviewer_id, page, size
37-
)
32+
applications, total = await service.list_my_applications(reviewer_id, page, size)
3833
total_pages = max(1, (total + size - 1) // size)
3934
return ApplicationListResp(
4035
applications=applications,
@@ -48,17 +43,17 @@ async def get_my_applications(
4843
@router.delete("/application/{applicationId}", response_model=None)
4944
async def cancel_application(
5045
applicationId: str,
51-
db: AsyncSession = Depends(get_db),
5246
user_id: str = Depends(require_login),
47+
service: ApplicationService = Depends(get_application_service),
5348
) -> None:
54-
await application_service.cancel_application(db, applicationId, user_id)
49+
await service.cancel_application(applicationId, user_id)
5550

5651

5752
@router.post("/application/{applicationId}/submission", response_model=None)
5853
async def submit_review(
5954
applicationId: str,
6055
body: ReviewSubmissionReq,
61-
db: AsyncSession = Depends(get_db),
6256
user_id: str = Depends(require_login),
57+
service: ApplicationService = Depends(get_application_service),
6358
) -> None:
64-
await application_service.submit_review(db, applicationId, user_id, body)
59+
await service.submit_review(applicationId, user_id, body)

app/application/service.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from abc import ABC, abstractmethod
2+
3+
from fastapi import BackgroundTasks
4+
5+
from app.application.schemas import (
6+
ApplicationCreateReq,
7+
ApplicationItem,
8+
ReviewSubmissionReq,
9+
)
10+
11+
12+
class ApplicationService(ABC):
13+
@abstractmethod
14+
async def create_application(
15+
self,
16+
reviewer_id: str,
17+
data: ApplicationCreateReq,
18+
background_tasks: BackgroundTasks,
19+
) -> None: ...
20+
21+
@abstractmethod
22+
async def list_my_applications(
23+
self, reviewer_id: str, page: int, size: int
24+
) -> tuple[list[ApplicationItem], int]: ...
25+
26+
@abstractmethod
27+
async def cancel_application(self, application_id: str, user_id: str) -> None: ...
28+
29+
@abstractmethod
30+
async def submit_review(
31+
self, application_id: str, user_id: str, data: ReviewSubmissionReq
32+
) -> None: ...

0 commit comments

Comments
 (0)