diff --git a/.github/workflows/github-action.yml b/.github/workflows/github-action.yml new file mode 100644 index 0000000..75c7aef --- /dev/null +++ b/.github/workflows/github-action.yml @@ -0,0 +1,53 @@ +name: CI/CD with Docker + +on: + push: + branches: [ "deploy" ] + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + # 1. Checkout the code from GitHub repository + - name: Checkout code + uses: actions/checkout@v3 + + # 2. Set up Docker Buildx (for multi-platform support) + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + # 3. Log in to DockerHub (if using DockerHub for image storage) + - name: Log in to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + # 4. Create .env file from GitHub Secrets + - name: Create .env file + run: echo "${{ secrets.ENV }}" > ./src/.env + + # 5. Build and push Docker image + - name: Build & push Docker image + uses: docker/build-push-action@v6 + with: + context: . + file: ./Dockerfile + push: true + platforms: linux/amd64 + tags: ${{ secrets.DOCKER_REPO }}:latest + + # 6. SSH to EC2 and deploy Docker container + - name: SSH to EC2 & deploy Docker container + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.EC2_HOST }} + username: ${{ secrets.EC2_USER }} + key: ${{ secrets.EC2_PRIVATE_KEY }} + envs: GITHUB_SHA + script: | + sudo docker ps -qa | xargs -r sudo docker rm -f + sudo docker pull ${{ secrets.DOCKER_REPO }}:latest + sudo docker run -d -p 8000:8000 ${{ secrets.DOCKER_REPO }}:latest + sudo docker image prune -f diff --git a/Dockerfile b/Dockerfile index 6afaf92..ac5bcc8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,14 @@ FROM python:3.11 -COPY ./src /src -WORKDIR /src +WORKDIR /code -RUN pip install -r requirements.txt +COPY ./src /code/src/ -EXPOSE 8080 +RUN pip install --no-cache-dir -r /code/src/requirements.txt -CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8080"] \ No newline at end of file +# Upgrade pip to the latest version +RUN pip install --upgrade pip + +EXPOSE 8000 + +CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/src/core/database.py b/src/core/database.py index 0820b58..5907c9e 100644 --- a/src/core/database.py +++ b/src/core/database.py @@ -9,7 +9,7 @@ class Settings(BaseSettings): openai_api_key: str class Config: - env_file = ".env" + env_file = "src/.env" # 환경 변수 값 가져오기 settings = Settings() @@ -20,4 +20,4 @@ class Config: # 인덱스 설정 async def init_db(): - await db["wishes"].create_index([("created_at", ASCENDING)]) \ No newline at end of file + await db["wish"].create_index([("created_at", ASCENDING)]) \ No newline at end of file diff --git a/src/core/models.py b/src/core/models.py index a07edd4..dde40ea 100644 --- a/src/core/models.py +++ b/src/core/models.py @@ -18,7 +18,6 @@ class Song(BaseModel): modified_at: datetime class Wish(BaseModel): - wish_id: str nickname: str content: str is_displayed: bool = True diff --git a/src/domain/song/routers.py b/src/domain/song/routers.py index 8f7f8a2..1a48386 100644 --- a/src/domain/song/routers.py +++ b/src/domain/song/routers.py @@ -1,11 +1,16 @@ from fastapi import APIRouter, Query from src.domain.song.schemas import PaginatedResponse from src.domain.song.services import get_songs_by_category, get_total_songs_count +from typing import Optional router = APIRouter() @router.get("/list", response_model=PaginatedResponse) -async def get_songs_by_category_api(category: str, page: int = Query(1, ge=1), size: int = Query(10, ge=1)): +async def get_songs_by_category_api( + category: Optional[str] = None, # category를 선택적으로 지정 + page: int = Query(1, ge=1), + size: int = Query(10, ge=1) +): skip = (page - 1) * size songs = await get_songs_by_category(category, skip, size) total_items = await get_total_songs_count(category) @@ -23,7 +28,7 @@ async def get_songs_by_category_api(category: str, page: int = Query(1, ge=1), s @router.post("/", response_model=SongResponse) async def create_song_api(song: SongCreate): song_id = await create_song(song.dict()) - return {**song.dict(), "songid": song_id, "createdAt": datetime.now(), "modifiedAt": datetime.now()} + return {**song.dict(), "song_id": song_id, "created_at": datetime.now(), "modified_at": datetime.now()} ''' diff --git a/src/domain/song/schemas.py b/src/domain/song/schemas.py index 4c78220..16bb50a 100644 --- a/src/domain/song/schemas.py +++ b/src/domain/song/schemas.py @@ -8,8 +8,8 @@ class SongUnitResponse(BaseModel): artist: str category: str lyrics: str - coverPath: Optional[str] = None - youtubePath: str + cover_path: Optional[str] = None + youtube_path: str class Config: orm_mode = True diff --git a/src/domain/song/services.py b/src/domain/song/services.py index 06a8878..fda9676 100644 --- a/src/domain/song/services.py +++ b/src/domain/song/services.py @@ -1,19 +1,21 @@ from src.core.database import db from src.core.crud import get_many, get_by_id, count_by_column -from typing import List +from typing import Optional, List # 카테고리별 노래 목록 가져오기 -async def get_songs_by_category(category: str, skip: int, limit: int) -> List[dict]: - return await get_many("songs", {"category": category}, skip, limit) +async def get_songs_by_category(category: Optional[str], skip: int, limit: int) -> List[dict]: + # category가 None이면 전체 데이터를 가져오도록 필터를 빈 딕셔너리로 설정 + filter_query = {"category": category} if category else {} + return await get_many("song", filter_query, skip, limit) # 카테고리에 맞는 총 노래 수를 반환 async def get_total_songs_count(category: str) -> int: - count = await count_by_column("songs", "category", category) + count = await count_by_column("song", "category", category) return count -# obj_id로 노래 가져오기 -async def get_song_by_obj_id(obj_id: object): - return await get_by_id("songs", obj_id) +# _id로 노래 가져오기 +async def get_song_by_obj_id(_id: object): + return await get_by_id("song", _id) # 특정 카테고리에서 랜덤으로 한 곡을 선택 async def get_random_song_by_category(category: str) -> dict: @@ -27,11 +29,11 @@ async def get_random_song_by_category(category: str) -> dict: ''' # 노래 생성 async def create_song(song_data: dict) -> str: - song_data["createdAt"] = datetime.now() - song_data["modifiedAt"] = datetime.now() - return await insert("songs", song_data) + song_data["created_at"] = datetime.now() + song_data["modified_at"] = datetime.now() + return await insert("song", song_data) # 노래 검색 (페이징) async def search_songs(query: str, skip: int, limit: int) -> List[dict]: - return await get_many("songs", {"title": {"$regex": query, "$options": "i"}}, skip, limit) + return await get_many("song", {"title": {"$regex": query, "$options": "i"}}, skip, limit) ''' \ No newline at end of file diff --git a/src/domain/wish/schemas.py b/src/domain/wish/schemas.py index 7bdec6f..840cd15 100644 --- a/src/domain/wish/schemas.py +++ b/src/domain/wish/schemas.py @@ -6,16 +6,16 @@ class WishCreate(BaseModel): nickname: str content: str - is_displayed: bool = Field(alias="isDisplayed") + is_displayed: bool = Field(alias="is_displayed") # 랜덤4개 소원 응답 스키마 class WishResponse(BaseModel): wishid: str = Field(alias="wishId") nickname: str content: str - is_displayed: bool = Field(alias="isDisplayed") - created_at: datetime = Field(alias="createdAt") - songid: str + is_displayed: bool = Field(alias="is_displayed") + created_at: datetime = Field(alias="created_at") + song_id: str class Config: from_attributes = True @@ -26,15 +26,15 @@ class SongRecommendation(BaseModel): title: str artist: str lyrics: str - cover_path: str = Field(alias="coverPath") - youtube_path: str = Field(alias="youtubePath") - recommend_time: str = Field(alias="recommendtime") + cover_path: str = Field(alias="cover_path") + youtube_path: str = Field(alias="youtube_path") + recommend_time: str = Field(alias="recommend_time") class RecommendationResponse(BaseModel): nickname: str wish: str recommended_song: SongRecommendation - wish_id: str = Field(alias="wishId") + wish_id: str = Field(alias="_id") class Config: from_attributes = True diff --git a/src/domain/wish/services.py b/src/domain/wish/services.py index 165957c..d856888 100644 --- a/src/domain/wish/services.py +++ b/src/domain/wish/services.py @@ -29,24 +29,24 @@ async def process_wish(wish): midnight = datetime.strptime("00:00:00", "%H:%M:%S") recommend_time = midnight - start_time # 소원 생성 - wish_id = await create_wish( + _id = await create_wish( nickname=wish.nickname, content=wish.content, - song_id=recommended_song["songid"], + song_id=recommended_song["song_id"], is_displayed=wish.is_displayed, ) return { + "_id": _id, "nickname": wish.nickname, "wish": wish.content, "recommended_song": { "title": recommended_song["title"], "artist": recommended_song["artist"], "lyrics": recommended_song["lyrics"], - "coverPath": recommended_song["coverPath"], + "cover_path": recommended_song["cover_path"], "recommend_time": str(recommend_time), - "youtubePath": recommended_song["youtubePath"], - }, - "wish_id": wish_id + "youtube_path": recommended_song["youtube_path"], + } } # 소원 생성 @@ -58,20 +58,20 @@ async def create_wish(nickname: str, content: str, song_id: str, is_displayed: b "created_at": datetime.now(), "song_id": song_id, } - result = await db["wishes"].insert_one(wish_data) + result = await db["wish"].insert_one(wish_data) return str(result.inserted_id) # 특정 소원 가져오기 -async def get_wish_by_id(wish_id: str) -> dict: - return await get_by_id("wishes", wish_id) +async def get_wish_by_id(_id: str) -> dict: + return await get_by_id("wish", _id) # 랜덤 소원 가져오기 async def get_random_wishes(limit: int = 4) -> List[dict]: # MongoDB의 $sample 사용 (별도 crud 함수 필요할 수도 있음) - wishes = await db["wishes"].aggregate([{"$sample": {"size": limit}}]).to_list(length=limit) + wishes = await db["wish"].aggregate([{"$sample": {"size": limit}}]).to_list(length=limit) return wishes # 특정 노래의 wish 갯수 세기 -async def count_wishes_by_song_id(song_id: str) -> int: - wishes = await get_many("wishes", {"song_id": song_id}, 0, 0) # 모든 wish를 가져옴 +async def count_wishes_by_song_id(_id: str) -> int: + wishes = await get_many("wish", {"song_id": _id}, 0, 0) # 모든 wish를 가져옴 return len(wishes)