Skip to content

Commit 2cb8a02

Browse files
9keyyyyGukhee Jo
andauthored
feat: 운세 상세 테이블 생성, 오늘 운세 프롬프트 작성, 오늘 운세 조회 API 생성 (#16)
* feat: 운세 상세 테이블 생성, 오늘 운세 프롬프트 작성, 오늘 운세 조회 API 생성 * feat: hcx call/parsing 실패 시 fallback --------- Co-authored-by: Gukhee Jo <[email protected]>
1 parent 6686f3e commit 2cb8a02

File tree

12 files changed

+347
-59
lines changed

12 files changed

+347
-59
lines changed

migrations/env.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
# 모든 도메인 모델을 import하여 메타데이터에 포함해야 함
3131
from src.users.entities.models import User
3232
from src.lotto.entities.models import LottoStatistics, LottoDraws
33-
from src.fortune.entities.models import DailyFortuneResource, UserDailyFortuneSummary
33+
from src.fortune.entities.models import DailyFortuneResource, UserDailyFortuneSummary, UserDailyFortuneDetail
3434

3535
# 모델 메타데이터 설정
3636
target_metadata = Base.metadata
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""add_user_daily_fortune_detail_table
2+
3+
Revision ID: 926bb28822d1
4+
Revises: de9ee05d86b4
5+
Create Date: 2025-08-13 21:13:08.667387
6+
7+
"""
8+
from typing import Sequence, Union
9+
10+
from alembic import op
11+
import sqlalchemy as sa
12+
13+
14+
# revision identifiers, used by Alembic.
15+
revision: str = '926bb28822d1'
16+
down_revision: Union[str, Sequence[str], None] = 'de9ee05d86b4'
17+
branch_labels: Union[str, Sequence[str], None] = None
18+
depends_on: Union[str, Sequence[str], None] = None
19+
20+
21+
def upgrade() -> None:
22+
"""Upgrade schema."""
23+
# ### commands auto generated by Alembic - please adjust! ###
24+
op.create_table('user_daily_fortune_detail',
25+
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False, comment='운세 상세 기록 고유 ID'),
26+
sa.Column('user_id', sa.String(length=255), nullable=False, comment='유저 ID'),
27+
sa.Column('fortune_date', sa.Date(), nullable=False, comment='운세 날짜'),
28+
sa.Column('fortune_score', sa.Integer(), nullable=False, comment='운세 점수'),
29+
sa.Column('fortune_comment', sa.String(length=1000), nullable=False, comment='운세 코멘트'),
30+
sa.Column('fortune_details', sa.JSON(), nullable=False, comment='운세 상세 정보 (JSON)'),
31+
sa.Column('created_at', sa.DateTime(), nullable=False),
32+
sa.Column('updated_at', sa.DateTime(), nullable=False),
33+
sa.CheckConstraint('fortune_score >= 0 AND fortune_score <= 100', name='ck_fortune_score_range'),
34+
sa.CheckConstraint('length(fortune_comment) > 0', name='ck_fortune_comment_len'),
35+
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
36+
sa.PrimaryKeyConstraint('id')
37+
)
38+
# ### end Alembic commands ###
39+
40+
41+
def downgrade() -> None:
42+
"""Downgrade schema."""
43+
# ### commands auto generated by Alembic - please adjust! ###
44+
op.drop_table('user_daily_fortune_detail')
45+
# ### end Alembic commands ###

src/common/utils.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from datetime import date, datetime, timezone, timedelta
2+
3+
4+
def get_kst_date() -> date:
5+
"""한국 표준시(KST) 기준으로 오늘 날짜를 반환합니다."""
6+
kst_tz = timezone(timedelta(hours=9))
7+
kst_now = datetime.now(kst_tz)
8+
return kst_now.date()

src/fortune/entities/constants.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
"""
2+
운세 관련 상수 및 샘플 데이터
3+
"""
4+
5+
# 일일 운세 fallback 샘플 데이터
6+
DAILY_FORTUNE_FALLBACK_DATA = {
7+
"score": 75,
8+
"comment": "좋은 기운이 문을 두드리고 있소",
9+
"money_fortune": "적은 노력으로 일확천금",
10+
"job_fortune": "구직 성공의 기운",
11+
"love_fortune": "백억부자 애인 각",
12+
}

src/fortune/entities/models.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# src/fortune/entities/models.py
2-
from sqlalchemy import Column, Integer, Date, Enum as SAEnum, String, CheckConstraint, ForeignKey
2+
from sqlalchemy import Column, Integer, Date, Enum as SAEnum, String, CheckConstraint, ForeignKey, JSON
33
from sqlalchemy.orm import relationship
44
from src.config.database import Base
55
from src.fortune.entities.enums import FortuneType
@@ -26,3 +26,19 @@ class UserDailyFortuneSummary(Base):
2626
user_id = Column(String(255), ForeignKey("users.id"), nullable=False, comment="유저 ID")
2727
daily_fortune_resource_id = Column(Integer, ForeignKey("daily_fortune_resources.id"), nullable=False, comment="운세 리소스 ID")
2828
fortune_date = Column(Date, nullable=False, comment="운세 날짜")
29+
30+
31+
class UserDailyFortuneDetail(Base):
32+
__tablename__ = "user_daily_fortune_detail"
33+
34+
id = Column(Integer, primary_key=True, autoincrement=True, comment="운세 상세 기록 고유 ID")
35+
user_id = Column(String(255), ForeignKey("users.id"), nullable=False, comment="유저 ID")
36+
fortune_date = Column(Date, nullable=False, comment="운세 날짜")
37+
fortune_score = Column(Integer, nullable=False, comment="운세 점수")
38+
fortune_comment = Column(String(1000), nullable=False, comment="운세 코멘트")
39+
fortune_details = Column(JSON, nullable=False, comment="운세 상세 정보 (JSON)")
40+
41+
__table_args__ = (
42+
CheckConstraint("fortune_score >= 0 AND fortune_score <= 100", name="ck_fortune_score_range"),
43+
CheckConstraint("length(fortune_comment) > 0", name="ck_fortune_comment_len"),
44+
)

src/fortune/entities/schemas.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# src/fortune/entities/schemas.py
22
from datetime import date
3-
from typing import List, Optional
3+
from typing import List, Optional, Dict
44
from src.config.schemas import CommonBase
55
from src.fortune.entities.enums import FortuneType
66

@@ -37,4 +37,12 @@ class UserDailyFortuneSummary(CommonBase):
3737
fortune_date: date
3838
fortune_type: FortuneType
3939
image_url: str
40-
description: str
40+
description: str
41+
42+
class UserDailyFortuneDetail(CommonBase):
43+
id: int
44+
user_id: str
45+
fortune_date: date
46+
fortune_score: int
47+
fortune_comment: str
48+
fortune_details: Dict[str, str]

src/fortune/repository.py

Lines changed: 71 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
1-
# src/fortune/repository.py
21
from datetime import date
32
from typing import List, Optional, Tuple
3+
44
from fastapi import Depends
55
from sqlalchemy import select, desc, and_
66
from sqlalchemy.ext.asyncio import AsyncSession
7+
78
from src.common.dependencies import get_db_session
8-
from src.fortune.entities.models import UserDailyFortuneSummary as UserDailyFortuneSummaryModel, DailyFortuneResource as DailyFortuneResourceModel
99
from src.fortune.entities.enums import FortuneType
10+
from src.fortune.entities.models import (
11+
UserDailyFortuneSummary as UserDailyFortuneSummaryModel,
12+
DailyFortuneResource as DailyFortuneResourceModel,
13+
UserDailyFortuneDetail as UserDailyFortuneDetailModel,
14+
)
1015
from src.fortune.entities.schemas import UserDailyFortuneSummary
1116

1217

@@ -19,7 +24,7 @@ async def create_fortune(
1924
publish_date: date,
2025
fortune_type: FortuneType,
2126
image_url: str,
22-
description: str
27+
description: str,
2328
) -> DailyFortuneResourceModel:
2429
model = DailyFortuneResourceModel(
2530
publish_date=publish_date,
@@ -28,7 +33,7 @@ async def create_fortune(
2833
description=description,
2934
)
3035
self.session.add(model)
31-
await self.session.commit()
36+
await self.session.flush()
3237
await self.session.refresh(model)
3338
return model # DB 모델 반환
3439

@@ -38,7 +43,7 @@ async def update_fortune(
3843
publish_date: Optional[date],
3944
fortune_type: Optional[FortuneType],
4045
image_url: Optional[str],
41-
description: Optional[str]
46+
description: Optional[str],
4247
) -> DailyFortuneResourceModel:
4348
model = await self.get_fortune_by_id(resource_id)
4449
if not model:
@@ -53,7 +58,7 @@ async def update_fortune(
5358
if description is not None:
5459
model.description = description
5560

56-
await self.session.commit()
61+
await self.session.flush()
5762
await self.session.refresh(model)
5863
return model
5964

@@ -62,7 +67,7 @@ async def delete_fortune(self, resource_id: int) -> None:
6267
if not model:
6368
raise ValueError("리소스를 찾을 수 없습니다.")
6469
await self.session.delete(model)
65-
await self.session.commit()
70+
await self.session.flush()
6671

6772
async def get_fortunes(
6873
self,
@@ -71,7 +76,9 @@ async def get_fortunes(
7176
publish_date: Optional[date] = None,
7277
fortune_type: Optional[FortuneType] = None,
7378
) -> Tuple[List[DailyFortuneResourceModel], Optional[int]]:
74-
query = select(DailyFortuneResourceModel).order_by(desc(DailyFortuneResourceModel.id))
79+
query = select(DailyFortuneResourceModel).order_by(
80+
desc(DailyFortuneResourceModel.id)
81+
)
7582

7683
conditions = []
7784
if cursor:
@@ -91,25 +98,32 @@ async def get_fortunes(
9198
next_cursor = items[-1].id if len(items) == limit else None
9299
return items, next_cursor
93100

94-
async def get_fortune_by_id(self, resource_id: int) -> Optional[DailyFortuneResourceModel]:
101+
async def get_fortune_by_id(
102+
self, resource_id: int
103+
) -> Optional[DailyFortuneResourceModel]:
95104
return await self.session.get(DailyFortuneResourceModel, resource_id)
96105

97106
async def get_user_daily_fortune_summaries(
98-
self, user_id: str, fortune_date: date
107+
self, user_id: str, fortune_date: date
99108
) -> List[UserDailyFortuneSummary]:
100-
query = select(
101-
UserDailyFortuneSummaryModel.id,
102-
UserDailyFortuneSummaryModel.user_id,
103-
UserDailyFortuneSummaryModel.fortune_date,
104-
DailyFortuneResourceModel.fortune_type,
105-
DailyFortuneResourceModel.image_url,
106-
DailyFortuneResourceModel.description
107-
).join(
108-
DailyFortuneResourceModel,
109-
UserDailyFortuneSummaryModel.daily_fortune_resource_id == DailyFortuneResourceModel.id
110-
).filter(
111-
UserDailyFortuneSummaryModel.user_id == user_id,
112-
UserDailyFortuneSummaryModel.fortune_date == fortune_date
109+
query = (
110+
select(
111+
UserDailyFortuneSummaryModel.id,
112+
UserDailyFortuneSummaryModel.user_id,
113+
UserDailyFortuneSummaryModel.fortune_date,
114+
DailyFortuneResourceModel.fortune_type,
115+
DailyFortuneResourceModel.image_url,
116+
DailyFortuneResourceModel.description,
117+
)
118+
.join(
119+
DailyFortuneResourceModel,
120+
UserDailyFortuneSummaryModel.daily_fortune_resource_id
121+
== DailyFortuneResourceModel.id,
122+
)
123+
.filter(
124+
UserDailyFortuneSummaryModel.user_id == user_id,
125+
UserDailyFortuneSummaryModel.fortune_date == fortune_date,
126+
)
113127
)
114128

115129
result = await self.session.execute(query)
@@ -123,7 +137,39 @@ async def get_user_daily_fortune_summaries(
123137
fortune_date=summary[2],
124138
fortune_type=summary[3],
125139
image_url=summary[4],
126-
description=summary[5]
140+
description=summary[5],
127141
)
128142
for summary in summaries
129-
]
143+
]
144+
145+
async def get_user_daily_fortune_detail(
146+
self, user_id: str, fortune_date: date
147+
) -> Optional[UserDailyFortuneDetailModel]:
148+
"""특정 날짜의 사용자 운세 상세 정보 조회"""
149+
query = select(UserDailyFortuneDetailModel).filter(
150+
UserDailyFortuneDetailModel.user_id == user_id,
151+
UserDailyFortuneDetailModel.fortune_date == fortune_date,
152+
)
153+
result = await self.session.execute(query)
154+
return result.scalar_one_or_none()
155+
156+
async def create_user_daily_fortune_detail(
157+
self,
158+
user_id: str,
159+
fortune_date: date,
160+
fortune_score: int,
161+
fortune_comment: str,
162+
fortune_details: dict,
163+
) -> UserDailyFortuneDetailModel:
164+
"""사용자 운세 상세 정보 생성"""
165+
model = UserDailyFortuneDetailModel(
166+
user_id=user_id,
167+
fortune_date=fortune_date,
168+
fortune_score=fortune_score,
169+
fortune_comment=fortune_comment,
170+
fortune_details=fortune_details,
171+
)
172+
self.session.add(model)
173+
await self.session.flush()
174+
await self.session.refresh(model)
175+
return model

0 commit comments

Comments
 (0)