Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
86 changes: 55 additions & 31 deletions .github/workflows/ci-java.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ jobs:
run: |
./gradlew unitTest
./gradlew integrationTest
if [ "${{ github.base_ref }}" = "main" ]; then
if [ "${{ github.base_ref }}" = "main" ] || [[ "${{ github.ref }}" == refs/tags/* ]]; then
./gradlew e2eTest
fi
working-directory: apps/user-service
Expand Down Expand Up @@ -153,33 +153,57 @@ jobs:
echo "=== Image Layer Analysis ==="
docker history ghcr.io/${{ env.REPO_LC }}/user-service:${{ needs.set-image-tag.outputs.image-tag }} --human --no-trunc

# swagger-docs:
# name: Deploy Swagger Documentation
# runs-on: ubuntu-latest
# needs:
# - build
# - set-image-tag
# if: startsWith(github.ref, 'refs/tags/user-service-v')
#
# steps:
# - name: Checkout repository
# uses: actions/checkout@v4
#
# - name: Download OpenAPI spec artifacts
# uses: actions/download-artifact@v4
# with:
# name: openapi-spec-${{ github.run_id }}-${{ github.run_attempt }}
# path: ./openapi-spec
#
# - name: Generate Swagger UI
# uses: Legion2/swagger-ui-action@v1
# with:
# output: user-service-swagger-ui-${{ needs.set-image-tag.outputs.image-tag }}
# spec-file: openapi-spec/openapi3.yaml
#
# - name: Deploy to GitHub Pages
# uses: peaceiris/actions-gh-pages@v3
# with:
# github_token: ${{ secrets.GITHUB_TOKEN }}
# publish_dir: ./user-service-swagger-ui-${{ needs.set-image-tag.outputs.image-tag }}
# destination_dir: user-service/${{ needs.set-image-tag.outputs.image-tag }}
swagger-docs:
name: Deploy Swagger Documentation
runs-on: ubuntu-latest
needs:
- build
- set-image-tag
if: startsWith(github.ref, 'refs/tags/user-service-v')

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Download OpenAPI spec artifacts
uses: actions/download-artifact@v4
with:
name: openapi-spec-${{ github.run_id }}-${{ github.run_attempt }}
path: ./openapi-spec

- name: Check OpenAPI spec file exists
id: check-openapi
run: |
if [ -f "./openapi-spec/openapi3.yaml" ]; then
echo "openapi_exists=true" >> $GITHUB_OUTPUT
echo "✅ OpenAPI spec file found"
ls -la ./openapi-spec/
else
echo "openapi_exists=false" >> $GITHUB_OUTPUT
echo "❌ OpenAPI spec file not found"
echo "Available files:"
ls -la ./openapi-spec/ || echo "No openapi-spec directory found"
find . -name "*.yaml" -o -name "*.yml" -o -name "*.json" | grep -i openapi || echo "No OpenAPI files found"
fi

- name: Generate Swagger UI
if: steps.check-openapi.outputs.openapi_exists == 'true'
uses: Legion2/swagger-ui-action@v1
with:
output: user-service-swagger-ui-${{ needs.set-image-tag.outputs.image-tag }}
spec-file: openapi-spec/openapi3.yaml

- name: Deploy to GitHub Pages
if: steps.check-openapi.outputs.openapi_exists == 'true'
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./user-service-swagger-ui-${{ needs.set-image-tag.outputs.image-tag }}
destination_dir: user-service/${{ needs.set-image-tag.outputs.image-tag }}

- name: Skip deployment notice
if: steps.check-openapi.outputs.openapi_exists == 'false'
run: |
echo "⏭️ Skipping Swagger documentation deployment"
echo "Reason: OpenAPI spec file not found at ./openapi-spec/openapi3.yaml"
echo "Please check your build configuration to ensure OpenAPI spec is generated"
26 changes: 24 additions & 2 deletions apps/pre-processing-service/app/api/endpoints/product.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
CustomException,
)
from ...service.crawl_service import CrawlService
from ...service.s3_upload_service import S3UploadService
from ...service.search_service import SearchService
from ...service.match_service import MatchService
from ...service.similarity_service import SimilarityService
Expand Down Expand Up @@ -60,11 +61,11 @@ async def match(request: RequestSadaguMatch):
)
async def similarity(request: RequestSadaguSimilarity):
"""
매칭된 상품들 중 키워드와의 유사도를 계산하여 최적의 상품을 선택합니다.
매칭된 상품들 중 키워드와의 유사도를 계산하여 상위 10개 상품을 선택합니다.
"""
try:
similarity_service = SimilarityService()
response_data = similarity_service.select_product_by_similarity(request)
response_data = similarity_service.select_top_products_by_similarity(request)

if not response_data:
raise CustomException(
Expand Down Expand Up @@ -99,3 +100,24 @@ async def crawl(body: RequestSadaguCrawl):
raise HTTPException(status_code=e.status_code, detail=e.detail)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))


@router.post("/s3-upload", response_model=ResponseS3Upload, summary="S3 이미지 업로드")
async def s3_upload(request: RequestS3Upload):
"""
크롤링 완료 후 별도로 호출하여 이미지들을 S3 저장소에 업로드합니다.
"""
try:
s3_upload_service = S3UploadService()
response_data = await s3_upload_service.upload_crawled_products_to_s3(request)

if not response_data:
raise CustomException(
500, "S3 이미지 업로드에 실패했습니다.", "S3_UPLOAD_FAILED"
)

return response_data
except InvalidItemDataException as e:
raise HTTPException(status_code=e.status_code, detail=e.detail)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
97 changes: 91 additions & 6 deletions apps/pre-processing-service/app/model/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,10 @@ class SadaguSimilarityData(BaseModel):
keyword: str = Field(
..., title="분석 키워드", description="유사도 분석에 사용된 키워드"
)
selected_product: Optional[Dict] = Field(
None, title="선택된 상품", description="유사도 분석 결과 선택된 상품"
top_products: List[Dict] = Field(
default_factory=list,
title="선택된 상품들",
description="유사도 분석 결과 선택된 상위 상품 목록",
)
reason: Optional[str] = Field(
None, title="선택 이유", description="상품 선택 근거 및 점수 정보"
Expand All @@ -129,16 +131,23 @@ class ResponseSadaguSimilarity(ResponseBase[SadaguSimilarityData]):


class RequestSadaguCrawl(RequestBase):
product_url: HttpUrl = Field(
product_urls: List[HttpUrl] = Field(
..., title="상품 URL", description="크롤링할 상품 페이지의 URL"
)


# 응답 데이터 모델
class SadaguCrawlData(BaseModel):
product_url: str = Field(..., title="상품 URL", description="크롤링된 상품 URL")
product_detail: Optional[Dict] = Field(
None, title="상품 상세정보", description="크롤링된 상품의 상세 정보"
crawled_products: List[Dict] = Field(
...,
title="크롤링된 상품들",
description="크롤링된 상품들의 상세 정보 목록 (URL 포함)",
)
success_count: int = Field(
..., title="성공 개수", description="성공적으로 크롤링된 상품 개수"
)
fail_count: int = Field(
..., title="실패 개수", description="크롤링에 실패한 상품 개수"
)
crawled_at: Optional[str] = Field(
None, title="크롤링 시간", description="크롤링 완료 시간"
Expand All @@ -152,6 +161,81 @@ class ResponseSadaguCrawl(ResponseBase[SadaguCrawlData]):
pass


# ============== S3 이미지 업로드 ==============


class RequestS3Upload(RequestBase):
keyword: str = Field(
..., title="검색 키워드", description="폴더명 생성용 키워드"
) # 추가
crawled_products: List[Dict] = Field(
...,
title="크롤링된 상품 데이터",
description="이전 단계에서 크롤링된 상품들의 데이터",
)
base_folder: Optional[str] = Field(
"product", title="기본 폴더", description="S3 내 기본 저장 폴더 경로"
)


# S3 업로드된 이미지 정보
class S3ImageInfo(BaseModel):
index: int = Field(..., title="이미지 순번", description="상품 내 이미지 순번")
original_url: str = Field(
..., title="원본 URL", description="크롤링된 원본 이미지 URL"
)
s3_url: str = Field(..., title="S3 URL", description="S3에서 접근 가능한 URL")


# 상품별 S3 업로드 결과
class ProductS3UploadResult(BaseModel):
product_index: int = Field(..., title="상품 순번", description="크롤링 순번")
product_title: str = Field(..., title="상품 제목", description="상품명")
status: str = Field(..., title="업로드 상태", description="completed/skipped/error")
uploaded_images: List[S3ImageInfo] = Field(
default_factory=list, title="업로드 성공 이미지"
)
success_count: int = Field(
..., title="성공 개수", description="업로드 성공한 이미지 수"
)
fail_count: int = Field(
..., title="실패 개수", description="업로드 실패한 이미지 수"
)


# S3 업로드 요약 정보
class S3UploadSummary(BaseModel):
total_products: int = Field(
..., title="총 상품 수", description="처리 대상 상품 총 개수"
)
total_success_images: int = Field(
..., title="성공 이미지 수", description="업로드 성공한 이미지 총 개수"
)
total_fail_images: int = Field(
..., title="실패 이미지 수", description="업로드 실패한 이미지 총 개수"
)


# 응답 데이터 모델
class S3UploadData(BaseModel):
upload_results: List[ProductS3UploadResult] = Field(
..., title="업로드 결과", description="각 상품의 S3 업로드 결과"
)
summary: S3UploadSummary = Field(
..., title="업로드 요약", description="전체 업로드 결과 요약"
)
uploaded_at: str = Field(
..., title="업로드 완료 시간", description="S3 업로드 완료 시간"
)


# 최종 응답 모델
class ResponseS3Upload(ResponseBase[S3UploadData]):
"""S3 이미지 업로드 API 응답"""

pass


# ============== 블로그 콘텐츠 생성 ==============


Expand Down Expand Up @@ -193,6 +277,7 @@ class RequestBlogPublish(RequestBase):
tag: str = Field(..., title="블로그 태그", description="블로그 플랫폼 종류")
blog_id: str = Field(..., description="블로그 아이디")
blog_pw: str = Field(..., description="블로그 비밀번호")
blog_name: Optional[str] = Field(None, description="블로그 이름")
post_title: str = Field(..., description="포스팅 제목")
post_content: str = Field(..., description="포스팅 내용")
post_tags: List[str] = Field(default_factory=list, description="포스팅 태그 목록")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,13 @@ def _login(self) -> None:
pass

@abstractmethod
def _write_content(self, title: str, content: str, tags: List[str] = None) -> None:
def _write_content(self, title: str, content: str, tags: List[str] = None) -> str:
"""
플랫폼별 포스팅 작성 구현
:param title: 포스트 제목
:param content: 포스트 내용
:param tags: 포스트 태그 리스트
:return: 발행된 블로그 포스트 URL
"""
pass

Expand Down Expand Up @@ -96,14 +97,15 @@ def post_content(self, title: str, content: str, tags: List[str] = None) -> Dict
self._login()

# 3. 포스트 작성 및 발행
self._write_content(title, content, tags)
post_url = self._write_content(title, content, tags)

# 4. 결과 반환
return {
"platform": self._get_platform_name(),
"title": title,
"content_length": len(content),
"tag": self._get_platform_name(),
"post_title": title,
"tags": tags or [],
"publish_success": True,
"post_url": post_url,
}

def __del__(self):
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Dict
from typing import Dict, Optional
from app.errors.CustomException import CustomException
from app.model.schemas import RequestBlogPublish
from app.service.blog.blog_service_factory import BlogServiceFactory
Expand All @@ -10,31 +10,37 @@ class BlogPublishService:
def __init__(self):
self.factory = BlogServiceFactory()

def publish_content(self, request: RequestBlogPublish) -> Dict:
def publish_content(
self,
request: RequestBlogPublish,
) -> Dict:
"""
생성된 블로그 콘텐츠를 배포합니다.

Args:
request: 블로그 발행 요청 데이터
blog_id: 블로그 아이디
blog_password: 블로그 비밀번호
"""
try:
# 팩토리를 통해 적절한 서비스 생성
blog_service = self.factory.create_service(request.tag)
blog_service = self.factory.create_service(
request.tag,
blog_id=request.blog_id,
blog_password=request.blog_pw,
blog_name=request.blog_name,
)

# 공통 인터페이스로 포스팅 실행
blog_service.post_content(
response_data = blog_service.post_content(
title=request.post_title,
content=request.post_content,
tags=request.post_tags,
)

# 올바른 응답 데이터를 직접 구성
response_data = {
"tag": request.tag,
"post_title": request.post_title,
"publish_success": True, # 포스팅 성공 가정
}

if not response_data:
raise CustomException(
f"{request.tag} 블로그 포스팅에 실패했습니다.", status_code=500
500, f"{request.tag} 블로그 포스팅에 실패했습니다.", "POSTING_FAIL"
)

return response_data
Expand All @@ -45,5 +51,5 @@ def publish_content(self, request: RequestBlogPublish) -> Dict:
except Exception as e:
# 예상치 못한 예외 처리
raise CustomException(
f"블로그 포스팅 중 오류가 발생했습니다: {str(e)}", status_code=500
500, f"블로그 포스팅 중 오류가 발생했습니다: {str(e)}", "ERROR"
)
Loading
Loading