From 6ee4461b8ad904f9462b34c009724c01bc17d03c Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 9 Apr 2025 09:20:21 +0900 Subject: [PATCH 1/2] init commit --- .github/workflows/lint.yaml | 18 +++ .pre-commit-config.yaml | 24 +++ Dockerfile | 7 + README.md | 31 +++- copygo/clients.py | 50 ++++++ copygo/configs.py | 45 ++++++ copygo/errors.py | 40 +++++ copygo/logger.py | 12 ++ copygo/main.py | 26 ++++ copygo/prompts/__init__.py | 13 ++ copygo/prompts/copytone.py | 51 +++++++ copygo/prompts/image.py | 19 +++ copygo/prompts/message.py | 67 ++++++++ copygo/prompts/purpose.py | 30 ++++ copygo/prompts/strategy.py | 297 ++++++++++++++++++++++++++++++++++++ copygo/routers/__init__.py | 0 copygo/routers/copies.py | 25 +++ copygo/routers/images.py | 25 +++ copygo/schemas.py | 46 ++++++ copygo/services.py | 71 +++++++++ requirements.txt | 38 +++++ 21 files changed, 934 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/lint.yaml create mode 100644 .pre-commit-config.yaml create mode 100644 Dockerfile create mode 100644 copygo/clients.py create mode 100644 copygo/configs.py create mode 100644 copygo/errors.py create mode 100644 copygo/logger.py create mode 100644 copygo/main.py create mode 100644 copygo/prompts/__init__.py create mode 100644 copygo/prompts/copytone.py create mode 100644 copygo/prompts/image.py create mode 100644 copygo/prompts/message.py create mode 100644 copygo/prompts/purpose.py create mode 100644 copygo/prompts/strategy.py create mode 100644 copygo/routers/__init__.py create mode 100644 copygo/routers/copies.py create mode 100644 copygo/routers/images.py create mode 100644 copygo/schemas.py create mode 100644 copygo/services.py create mode 100644 requirements.txt diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 0000000..97c1f18 --- /dev/null +++ b/.github/workflows/lint.yaml @@ -0,0 +1,18 @@ +name: Lint + +on: + push: + paths: + - '**.py' + +jobs: + run-linters: + name: Run linters + runs-on: ubuntu-latest + + steps: + - name: Check out Git repository + uses: actions/checkout@v4 + + - name: Lint check ruff + uses: chartboost/ruff-action@v1 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..2430d23 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,24 @@ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-json + - id: check-toml + - id: check-added-large-files + - id: check-merge-conflict + +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.9.9 + hooks: + - id: ruff + args: [ --fix ] + - id: ruff-format + +- repo: https://github.com/pycqa/isort + rev: 6.0.1 + hooks: + - id: isort + args: ["--profile", "black"] diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ae6c19c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,7 @@ +FROM python:3.11-slim +WORKDIR /app +COPY requirements.txt /app/ +RUN pip install --no-cache-dir -r requirements.txt +COPY . /app/ +CMD ["uvicorn", "copygo.main:app", "--host", "0.0.0.0", "--port", "8080", "--reload"] +EXPOSE 8080 diff --git a/README.md b/README.md index fc56902..a7a5197 100644 --- a/README.md +++ b/README.md @@ -1 +1,30 @@ -# copygo \ No newline at end of file +# copygo# CopyGO + +## Installation + +To get started with this project, follow these steps: + +1. **Clone the repository:** +```bash +git clone https://github.com/hanacard-data/copygo.git +cd copygo +``` + +2. **Set up a virtual environment:** +``` +conda create -n copygo python=3.12 +conda activate copygo +``` + +3. **Install dependencies:** +```bash +pip install -r requirements.txt +``` + +## Usage + +To run the application, use the following command: + +```sh +$ uvicorn copygo.main:app --port 8080 --reload +``` diff --git a/copygo/clients.py b/copygo/clients.py new file mode 100644 index 0000000..9111897 --- /dev/null +++ b/copygo/clients.py @@ -0,0 +1,50 @@ +from openai import APIConnectionError, AsyncOpenAI +from retry import retry + +from copygo.configs import config +from copygo.errors import OpenAIException +from copygo.logger import logger + + +class OpenAIClient: + def __init__(self, api_key: str): + self.client = AsyncOpenAI(api_key=api_key) + + @retry(tries=5, delay=1, backoff=2, exceptions=APIConnectionError) + async def generate_copy(self, messages: list[dict[str, str]], **kwargs) -> str: + logger.info(f"Generating message completion for message: {messages}") + try: + response = await self.client.chat.completions.create( + model=config.LLM_MODEL, + messages=messages, + temperature=0, + **kwargs, + ) + return response.choices[0].message.content + except Exception as e: + logger.error(e) + raise OpenAIException(f"Completion failed: {e}") + + @retry(tries=5, delay=1, backoff=2, exceptions=APIConnectionError) + async def generate_image( + self, + prompt: str, + quality: str, + size: str, + style: str, + **kwargs, + ) -> str: + logger.info(f"Generating completion for prompt: {prompt}") + try: + response = await self.client.images.generate( + prompt=prompt, + model=config.IMAGE_MODEL, + quality=quality, + size=size, + style=style, + **kwargs, + ) + return response.data[0].url + except Exception as e: + logger.error(e) + raise OpenAIException(f"Completion failed: {e}") diff --git a/copygo/configs.py b/copygo/configs.py new file mode 100644 index 0000000..2208e0a --- /dev/null +++ b/copygo/configs.py @@ -0,0 +1,45 @@ +import logging + +from pydantic_settings import BaseSettings + + +class Config(BaseSettings): + TITLE: str = "CopyGo" + VERSION: str = "0.0.1" + DOCS_URL: str = "/docs" + REDOC_URL: str = "/redoc" + CONTACT: dict[str, str] = { + "name": "Hanacard CopyGo", + "email": "bigdata_card@hanafn.com", + } + SUMMARY: str = "CopyGo는 마케팅 전문가들이 필요한 모든 카피를 손쉽게 생성할 수 있도록 돕는 언어 모델입니다. 세분화된 마케팅 전략과 문체를 통해 고객의 니즈에 맞춘 맞춤형 콘텐츠를 빠르고 효율적으로 생성해 드립니다." + DESCRIPTION: str = "" + TAGS_METADATA: list[dict[str, str]] = [ + { + "name": "copy", + "description": "Operations with copies.", + }, + { + "name": "image", + "description": "Operations with images.", + }, + ] + LOG_LEVEL: int = logging.INFO + LLM_MODEL: str = "gpt-4o" + IMAGE_MODEL: str = "dall-e-3" + + @property + def fastapi_kwargs(self) -> dict[str, str, list[dict[str]]]: + return { + "title": self.TITLE, + "version": self.VERSION, + "docs_url": self.DOCS_URL, + "redoc_url": self.REDOC_URL, + "contact": self.CONTACT, + "summary": self.SUMMARY, + "description": self.DESCRIPTION, + "openapi_tags": self.TAGS_METADATA, + } + + +config: Config = Config() diff --git a/copygo/errors.py b/copygo/errors.py new file mode 100644 index 0000000..e82e609 --- /dev/null +++ b/copygo/errors.py @@ -0,0 +1,40 @@ +from fastapi import Request +from fastapi.responses import ORJSONResponse +from starlette import status + + +class BaseAPIException(Exception): + def __init__(self, code: str, message: str): + self.code = code + self.message = message + + +class BaseAuthException(Exception): + def __init__(self, code: str, message: str): + self.code = code + self.message = message + + +class OpenAIException(BaseAPIException): + def __init__(self, message: str): + super().__init__("OPENAI_ERROR", message) + + +async def api_error_handler(_: Request, exc: BaseAPIException) -> ORJSONResponse: + return ORJSONResponse( + content={ + "statusCode": exc.code, + "message": exc.message, + }, + status_code=status.HTTP_400_BAD_REQUEST, + ) + + +async def api_auth_error_handler(_: Request, exc: BaseAuthException) -> ORJSONResponse: + return ORJSONResponse( + content={ + "statusCode": exc.code, + "message": exc.message, + }, + status_code=status.HTTP_401_UNAUTHORIZED, + ) diff --git a/copygo/logger.py b/copygo/logger.py new file mode 100644 index 0000000..88df2e6 --- /dev/null +++ b/copygo/logger.py @@ -0,0 +1,12 @@ +import logging + +from copygo.configs import config + + +def init_logger() -> logging.Logger: + logger = logging.getLogger("uvicorn") + logger.setLevel(config.LOG_LEVEL) + return logger + + +logger = init_logger() diff --git a/copygo/main.py b/copygo/main.py new file mode 100644 index 0000000..6a29725 --- /dev/null +++ b/copygo/main.py @@ -0,0 +1,26 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import ORJSONResponse + +from copygo.configs import config +from copygo.errors import ( + BaseAPIException, + BaseAuthException, + api_auth_error_handler, + api_error_handler, +) +from copygo.routers.copies import copy_router +from copygo.routers.images import image_router + +app = FastAPI(default_response_class=ORJSONResponse, **config.fastapi_kwargs) +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], + allow_headers=["*"], +) +app.add_exception_handler(BaseAPIException, api_error_handler) +app.add_exception_handler(BaseAuthException, api_auth_error_handler) +app.include_router(copy_router) +app.include_router(image_router) diff --git a/copygo/prompts/__init__.py b/copygo/prompts/__init__.py new file mode 100644 index 0000000..1f8a076 --- /dev/null +++ b/copygo/prompts/__init__.py @@ -0,0 +1,13 @@ +# ruff: noqa +from copygo.prompts.copytone import COPY_TONE_DESCRIPTION_MAPPER, COPY_TONE_MAPPER +from copygo.prompts.image import IMAGE_PROPMT +from copygo.prompts.message import MESSAGE_PROPMT +from copygo.prompts.purpose import ( + CAMPAIGN_PURPOSE_DESCRIPTION_MAPPER, + CAMPAIGN_PURPOSE_MAPPER, +) +from copygo.prompts.strategy import ( + COPY_STRATEGY_DESCRIPTION_MAPPER, + COPY_STRATEGY_NAME_MAPPER, + EXAMPLE_COPY_MAPPER, +) diff --git a/copygo/prompts/copytone.py b/copygo/prompts/copytone.py new file mode 100644 index 0000000..69deec1 --- /dev/null +++ b/copygo/prompts/copytone.py @@ -0,0 +1,51 @@ +COPY_TONE_MAPPER: dict[int, str] = { + 1: "친근한 톤", + 2: "긍정적인 톤", + 3: "전문적인 톤", + 4: "정중한 톤", + 5: "하나금융그룹 톤", + 6: "마케팅 거장 William (Bill) Bernbach의 카피 톤", + 7: "마케팅 거장 Dan Wieden의 카피 톤", +} + +COPY_TONE_DESCRIPTION_MAPPER: dict[int, str] = { + 1: """ +일상적이고 편안한 말투를 사용하여 독자에게 친밀감을 느끼도록 유도하는 어조입니다. 가볍고 부드러운 분위기를 통해 독자가 부담 없이 메시지에 접근하고, 편안하게 내용을 이해하도록 돕습니다. + +<핵심 요소> +- 문체: 가벼운 대화체를 사용하며, 질문이나 감탄 등을 활용하여 자연스럽고 부드러운 흐름을 만듭니다. +- 상대높임법: 반말은 지양하고 경어를 사용하되, 격식 없이 친근하게 말을 건네는 듯한 표현을 사용합니다. +- 어조: 따뜻하고 가벼운 어조를 사용하여 마치 친구에게 이야기하듯 편안하게 전달합니다. +- 분위기: 자연스럽고 다정한 분위기를 조성하여 독자에게 편안함과 친근함을 제공합니다. +""", + 2: """ +밝고 희망찬 분위기를 통해 고객에게 긍정적인 에너지와 동기를 부여하는 어조입니다. 유쾌하고 활기찬 느낌을 주는 동시에, 고객에게 제품/서비스를 통해 좋은 경험을 할 수 있다는 기대감을 심어줍니다. 마치 친근한 사람이 따뜻하게 격려하고 응원하는 듯한 느낌을 전달합니다. 고객에게 제품/서비스를 경험해보도록 부드럽게 권하는 느낌을 주며, 긍정적인 단어와 표현을 적극적으로 사용합니다. + +<핵심 요소> +- 문체: 유쾌하고 활기찬 대화체를 사용하며, 짧고 간결한 문장으로 경쾌한 흐름을 유지합니다. +- 상대높임법: 공손한 높임말을 사용하되, 긍정적인 표현을 강조하여 부드럽고 친근한 느낌을 줍니다. +- 어조: 격려하고 응원하는 듯한 밝은 어조를 사용하며, 제품/서비스를 통해 얻을 수 있는 긍정적인 경험을 강조합니다. +- 분위기: 즐겁고 긍정적인 분위기를 조성하여 독자에게 활력과 기쁨을 선사합니다. +""", + 3: """ +정확한 정보와 논리적인 근거를 바탕으로 신뢰감을 주는 어조입니다. 객관적이고 명확한 설명을 통해 독자가 제품/서비스를 믿고 선택할 수 있도록 돕고, 합리적인 판단을 내릴 수 있도록 지원합니다. 마치 전문가가 차분하고 안정감 있는 말투로 설명하는 듯한 느낌을 전달합니다. 논리적인 근거와 데이터를 제시하여 주장에 대한 신뢰도를 높입니다. 깔끔하고 정확한 용어를 사용하며, 전문 용어 및 구체적인 정보를 활용하여 신뢰감을 더합니다. + +<핵심 요소> +- 문체: 간결하고 명확한 서술형 문장을 사용하여 객관적인 정보를 전달합니다. +- 상대높임법: 경어를 사용하며, 논리적이고 단정한 표현으로 격식을 갖춥니다. +- 어조: 이성적이고 신뢰를 주는 어조를 사용하며, 제품의 기능이나 효용을 논리적으로 설명하여 전문성을 강조합니다. +- 분위기: 차분하고 신뢰감을 주는 분위기를 조성하여 독자가 정보에 집중하고 제품/서비스에 대해 신뢰를 느끼도록 합니다. +""", + 4: """ +공손하고 예의 바른 말투를 사용하여 독자에게 존중감을 느끼도록 하는 어조입니다. 격식을 갖춘 표현과 높임말을 통해 독자를 배려하고 존중하는 마음을 전달하며, 신뢰감을 높여 긍정적인 관계를 형성합니다. 마치 고객에게 최상의 서비스를 제공하려는 듯한 느낌을 전달합니다. 격식을 갖추되, 지나치게 딱딱하거나 부담스럽지 않도록 균형을 유지합니다. 상황에 맞는 적절한 높임말을 사용하는 것이 중요합니다. 정중하고 예의 바른 단어를 선택하여 사용합니다. 간결하고 명확한 문장을 사용하되, 필요에 따라 정중한 표현을 덧붙여 문장을 구성합니다. + +<핵심 요소> +- 문체: 공손하고 격식을 갖춘 문장을 사용하여 고객에게 예의를 표현합니다. +- 상대높임법: 높임말을 일관되게 사용하여 고객에게 신뢰감과 존중을 느끼게 합니다. +- 어조: 차분하고 예의 있는 어조로, 정중하게 고객에게 제품/서비스의 가치를 설명합니다. +- 분위기: 신뢰할 수 있고 존중을 느낄 수 있는 격식 있는 분위기를 조성하여 고객이 특별 대우를 받는 듯한 느낌을 전달합니다. +""", + 5: "초등학생도 이해할 수 있는 쉬운 단어와 간결한 문장을 사용해, 핵심 내용을 명확하게 전달합니다. 따뜻하고 친절한 어조를 유지해, 마치 친절한 은행원이 옆에서 차근차근 설명해주는 듯한 느낌을 주어 고객에게 편안함과 친밀감을 제공합니다. 한자어 대신 쉬운 한글 단어를 사용하고, 금융 용어나 추상적인 내용을 설명할 때는 비유와 구체적인 예시를 활용해 이해를 돕습니다. 긍정적이고 희망찬 메시지를 통해 고객에게 안정감을 주며, 일관된 어조로 메시지의 전달력을 높입니다.", + 6: "William (Bill) Bernbach의 카피 스타일로 작성합니다. 도발적이고 고정관념을 깨는 접근으로 소비자의 흥미를 유발하는 것이 이 카피의 특징입니다. 직관적인 표현으로 고객의 관심과 즉각적인 참여를 유도하도록 구성합니다.", + 7: "Dan Wieden의 카피 스타일로 작성합니다. 간결하면서도 행동을 유도하는 강렬한 표현을 사용해 참신하게 구성합니다.", +} diff --git a/copygo/prompts/image.py b/copygo/prompts/image.py new file mode 100644 index 0000000..3755b42 --- /dev/null +++ b/copygo/prompts/image.py @@ -0,0 +1,19 @@ +IMAGE_PROPMT: str = """ +당신은 '하나카드'의 마케팅 이미지를 생성하는 디자이너입니다. +고객의 요구 사항을 분석하고, 이를 기반으로 캠페인 목적을 달성할 수 있는 마케팅 이미지를 작성하십시오. + +아래 절차를 단계별로 수행하고 최종 결과를 도출하십시오. + +### 절차: +#### 1단계: 고객 요구 조건 분석 +아래 고객이 원하는 캠페인의 제목과 설명을 분석하여, 캠페인 목적을 달성하기 위해 어떤 이미지가 효과적일지 고민하십시오. + +- 캠페인 제목: {campaign_title} +- 캠페인 설명: {campaign_description} + +#### 주의사항: +- 모든 캠페인은 '하나카드'에서 주관하는 캠페인입니다. + +#### 2단계: 마케팅 이미지 생성 +1단계에서 분석한 정보와 고민한 내용을 토대로, 캠페인 목적을 달성할 수 있는 이미지를 작성하십시오. +""" diff --git a/copygo/prompts/message.py b/copygo/prompts/message.py new file mode 100644 index 0000000..9ff200c --- /dev/null +++ b/copygo/prompts/message.py @@ -0,0 +1,67 @@ +MESSAGE_PROPMT: str = """ +당신은 '하나카드'의 마케팅 문구를 생성하는 카피라이터입니다. +고객의 요구 사항을 분석하고, 이를 기반으로 캠페인 목적을 달성할 수 있는 마케팅 문구를 작성하십시오. + +아래 절차를 단계별로 수행하고 최종 결과를 도출하십시오. + +### 절차: +#### 1단계: 고객 요구 조건 분석 +아래 고객이 원하는 문구의 요구 조건을 분석하여, 캠페인 목적을 달성하기 위해 어떤 문구 구성이 효과적일지 고민하십시오. + +- 캠페인 목적: {campaign_purpose} +- 캠페인 설명: {campaign_description} +- 타겟 고객: {target_customer} +- 문구 스타일 이름: {copy_tone} +- 문구 스타일 설명: {copy_tone_description} +- 적용할 마케팅 전략 이름: {copy_strategy_name} +- 마케팅 전략 설명: {copy_strategy_description} +- 마케팅 전략 예시: {example_copy} + +#### 주의사항: +- 적용할 마케팅 전략 또는 문구 스타일이 'none'인 경우, 다른 조건을 바탕으로 적합한 전략과 스타일을 스스로 설정하십시오. +- 제공된 마케팅 전략의 예시는 이해를 돕기 위한 참고용입니다. 예시가 실제 캠페인과 관련이 없을 수 있으니 주의하십시오. +- 모든 캠페인은 '하나카드'에서 주관하는 캠페인입니다. + +#### 2단계: 마케팅 문구 생성 +1단계에서 분석한 정보와 고민한 내용을 토대로, 아래 조건을 충족하면서 캠페인 목적을 달성할 수 있는 카피를 작성하십시오. + +- 생성할 문구 수: {copy_count} +- 제목 생성 여부: {is_title_included} # (Y/N) +- 본문 생성 여부: {is_content_included} # (Y/N) +- 제목 글자 수 범위: {min_title_character_count}-{max_title_character_count}자 (공백 포함) +- 본문 글자 수 범위: {min_content_character_count}-{max_content_character_count}자 (공백 포함) + +#### 공통 지침: +- 이모지(Emoji)는 어떠한 경우에도 사용하지 말고, 오직 텍스트만으로 문구를 작성하십시오. +- 문구의 첫 문장에 마케팅 전략의 핵심 메시지가 잘 드러나도록 작성하십시오. 전략의 느낌이 제목이나 첫 문장에서 바로 보이게 작성하세요. +- 마케팅 전략은 캠페인 전체 흐름과 자연스럽게 어우러지도록 구성하십시오. 전략이 캠페인의 핵심 메시지와 연결되도록 작성하십시오. +- 문구는 오직 제공된 정보와 사실에 근거하여 작성하십시오. 무작위 정보나 추측은 허용되지 않습니다. +- 고유 명사나 특정 영어 이름(예: 브랜드명, 회사명, 제품명 등)이 포함된 경우, 해당 부분은 원래 그대로 유지하십시오. 단, 이 경우를 제외하고 모든 문구는 한국어로 작성하십시오. +- 글자 수 범위는 반드시 준수하며, 범위를 초과하거나 미달하는 문구는 허용되지 않습니다. +- 불필요한 정보를 배제하고, 메시지를 짧고 간단하게 전달하여, 고객이 빠르게 이해할 수 있도록 작성합니다. +- 핵심 메시지가 쉽게 이해되도록 명확한 표현을 사용하여 혼동 없이 전달되게 작성합니다. +- 메시지를 받는 사람이 즉각적인 행동을 취하도록 직접적인 행동을 촉구할 수 있는 분위기를 전달합니다. +- 캠페인 참여 시 얻을 수 있는 혜택, 수치, 데이터 등을 구체적으로 제시해 고객이 혜택을 명확하게 인식하도록 합니다. + +#### 제목 작성 시 주의사항: +- 흥미를 끄는 후킹 포인트를 포함하여, 타겟의 관심을 즉각적으로 끌 수 있는 제목을 작성하십시오. +- 캠페인의 핵심 메시지를 명확하게 전달하여, 고객이 이 캠페인의 목적을 한눈에 이해할 수 있도록 제목을 작성하십시오. +- 제목은 짧고 간결하게 작성하되, 캠페인의 성격과 목적을 명확히 드러내야 합니다. + +#### 본문 작성 시 주의사항: +- 캠페인 목적과 혜택을 명확하게 연결하여, 고객이 이 혜택을 왜 받는지 설명하는 한두 문장을 포함하십시오. +- 캠페인 설명에 정보가 많을 경우(예: 150자 이상의 설명이나 혜택, 기간, 방법 등 여러 항목을 포함하는 경우), 각 항목을 글머리 기호(bullet points)나 목차 형식으로 분리하여 핵심 정보를 쉽게 읽을 수 있도록 구성하십시오. +- 긍정적인 어조를 유지하고, 고객에게 불쾌감을 주지 않도록 작성하십시오. 공격적이거나 협박처럼 들리는 표현을 피하십시오. + +### 결과 형식: +아래와 같은 형식으로만 출력하십시오. 생성된 제목과 본문 외에는 다른 설명이나 텍스트를 출력하지 마십시오. 제목 또는 본문이 없으면 해당 항목을 제외하고 출력하십시오. + +{{ + "phrase 1": + {{ + "title": "{{생성된 제목}}", + "content": "{{생성된 본문}}" + }} +}} + +""" diff --git a/copygo/prompts/purpose.py b/copygo/prompts/purpose.py new file mode 100644 index 0000000..30bca09 --- /dev/null +++ b/copygo/prompts/purpose.py @@ -0,0 +1,30 @@ +# 캠페인 목적 +CAMPAIGN_PURPOSE_MAPPER: dict[int, str] = { + 1: "앱 가입 유도", + 2: "앱 사용 유도", + 3: "특정 기능 사용 유도", + 4: "카드 발급 유도", + 5: "카드 갱신 유도", + 6: "카드 사용 유도", + 7: "(웰컴)카드 사용 유도", + 8: "(생일축하)혜택 받기", + 9: "이벤트 응모", + 10: "만족도 조사", + 11: "특정 상품 권유", + 12: "기타", +} + +CAMPAIGN_PURPOSE_DESCRIPTION_MAPPER: dict[int, str] = { + 1: "해당 앱에 가입하지 않은 고객들에게 앱 서비스에 가입하도록 장려하는 캠페인. 가입 혜택이나 특별 이벤트를 통해 유도할 수 있음.", + 2: "이미 해당 앱에 가입한 고객이 앱을 더 자주 사용하도록 유도하기 위한 캠페인. 예시로, 해당 페이지 접속시 하나머니를 제공할 수 있음.", + 3: "앱의 특정 기능을 고객이 사용해보도록 유도하는 캠페인. 예시로, 주소 확인 캠페인 등이 있음.", + 4: "신규 고객에게 신용카드 또는 체크카드를 발급받도록 권장하는 캠페인. 다양한 혜택이나 추가 서비스를 강조해 신규 카드 발급을 장려할 수 있음.", + 5: "카드의 유효기간이 임박한 고객에게 카드 갱신을 유도하는 캠페인.", + 6: "고객이 가지고 있는 카드를 더 자주 사용하도록 하는 캠페인. 일정 금액 이상 사용 시 추가 혜택을 제공하는 등의 캠페인이 예시가 됨.", + 7: "신규 가입 고객을 대상으로 환영의 의미로 제공하는 이벤트. 처음 가입한 고객이 카드를 사용하면 일정 혜택을 제공함.", + 8: "고객의 생일을 기념해 축하 메시지와 함께 특정 혜택을 제공하는 이벤트. 한정된 할인 혜택이나 하나머니 적립, 쿠폰 지급 등이 가능.", + 9: "고객들이 특정 이벤트에 참여하도록 유도하는 캠페인. 링크를 통해 직접 응모를 하는 등의 활동이 가능함.", + 10: "고객에게 서비스에 대한 만족도를 조사하기 위한 캠페인. 피드백을 받아 서비스 개선에 반영하고, 참여 고객에게 소정의 보상을 제공할 수 있음.", + 11: "고객의 관심사나 소비 패턴에 맞는 특정 상품을 추천하거나 권유하는 캠페인.", + 12: "위의 카테고리에 포함되지 않는 다양한 마케팅 목적의 캠페인.", +} diff --git a/copygo/prompts/strategy.py b/copygo/prompts/strategy.py new file mode 100644 index 0000000..610565c --- /dev/null +++ b/copygo/prompts/strategy.py @@ -0,0 +1,297 @@ +__all__ = [ + "COPY_STRATEGY_NAME_MAPPER", + "COPY_STRATEGY_DESCRIPTION_MAPPER", + "EXAMPLE_COPY_MAPPER", +] + +COPY_STRATEGY_NAME_MAPPER: dict[int, str] = { + 1: "손실지양 전략", + 2: "고립기피 전략", + 3: "희소성 전략", + 4: "사회적 증거 전략", + 5: "감정 호소 전략", + 6: "관계 강조 전략", + 7: "상품의 효과 전략", + 8: "상품의 새로움 전략", + 9: "흑백요리사", + 10: "MZ 타겟 전략", +} + +COPY_STRATEGY_DESCRIPTION_MAPPER: dict[int, str] = { + 1: """ +고객이 특정 행동을 취하지 않았을 때 발생할 수 있는 손실이나 불이익을 강조하여, 그 행동을 유도하는 전략입니다. 손실이나 불이익은 문제점, 비효율성, 기회 상실 등을 의미합니다. 고객의 '손해 보기 싫어하는 심리'를 자극하는 전략입니다. 이득이 되는 구체적인 금액이나 혜택을 제목 부분이나 첫 줄에 꼭 넣습니다. + +<핵심 요소> +- 손실/놓침 프레임: 긴급성을 부각하는 표현을 사용합니다. +- 기회비용 강조: 제품/서비스를 이용하지 않을 때 발생할 수 있는 구체적인 손실(금전적 손실, 기회 손실 등)을 제시합니다. +- 시간/수량 제한: 시간 제한, 한정 수량, 혜택 종료 시점을 명시하여 즉각적인 행동을 유도합니다. +- 구체적 수치화: 손실, 할인율, 절감액 등을 구체적인 수치로 제시합니다. +- 희망적인 메시지: 캠페인 참여로 손실 예방과 더 나은 결과를 얻을 수 있다는 희망을 제시합니다. +""", + 2: """ +많은 사람들이 캠페인에 이미 참여하고 있음을 부각시켜, 참여하지 않으면 뒤처질 수 있다는 심리를 자극하여 참여를 유도하는 전략입니다. 고객이 특정 행동을 취하지 않았을 때 사회적 소외감이나 트렌드에서 뒤처질 수 있다는 불안감을 자극하여, 즉각적인 행동을 유도하는 전략입니다. + +<핵심 요소> +- 트렌드 참여 프레임: 요즘 유행에 동참해야 한다는 메시지를 전달합니다. +- 다수 참여자임을 강조: 참여자 수가 많다라는 것을 전달합니다. +- 커뮤니티 조성: 제품/서비스 사용자들 간의 공통점을 강조하고 이에 따른 소속감을 부여하고, 정보 공유 및 교류를 활성화합니다. +""", + 3: """ +고객에게 캠페인 또는 제품의 공급이 제한적임을 강조하여 긴급성과 희소성을 부각시키고, 즉각적인 참여와 구매를 유도하는 방식입니다. 이는 고객이 한정된 자원을 놓칠 수 있다는 심리적 압박을 느끼게 하여, 구매 욕구를 자극하고 즉각적인 행동을 유도합니다. + +<핵심 요소> +- 한정된 기간과 수량 강조: 캠페인에 정해진 기간이나 남은 수량을 구체적으로 명시함으로써 고객이 긴급함을 느끼도록 합니다.고객이 기회를 놓치지 않기 위해 신속하게 결정하게 유도합니다. +- 접근성 제한을 통한 소유 욕구 자극: 특정 수량, 제한된 시간, 한정된 접근성 등을 설정하여 캠페인이나 제품의 특별함을 강조합니다. 이로 인해 고객은 '지금 놓치면 후회할 기회'라는 인식을 갖게 되고, 희소성에 의해 제품이나 서비스의 가치가 더 높게 느껴집니다. +- 즉각적인 참여와 행동 유도: 한정된 공급과 기간을 강조함으로써 고객이 즉각적으로 참여할 필요성을 느끼도록 합니다. 고객이 주저하지 않고 빠르게 결정하도록 이끄는 전략입니다. +""", + 4: """ +다수의 사람들이 이미 캠페인에 참여하고 있다는 사실을 부각시켜, 캠페인에 대한 신뢰감을 높이고 참여를 유도하는 전략입니다. 이 전략은 고객이 "다른 사람들이 이미 좋다고 하니 나도 참여해야겠다"는 생각을 갖게 하여, 긍정적 행동을 이끌어내는 데 중점을 둡니다. + +<핵심 요소> +- 다수 참여의 강조: 많은 사람들이 이미 참여하고 있거나 제품을 사용 중임을 알림으로써, 해당 캠페인이 신뢰할 만하다는 인식을 심어줍니다. 예를 들어, “10,000명 이상이 선택한 서비스”와 같은 표현으로 다수의 선택이 주는 신뢰를 강조합니다. +- 전문가 및 사용자 후기 활용: 고객의 신뢰를 강화하기 위해 전문가의 추천이나 실제 사용자 후기, 고객 만족도 등 객관적인 정보를 제공합니다. 이는 캠페인의 가치를 더욱 높이며, 타인의 긍정적인 경험이 참여 의사를 자극할 수 있도록 합니다. +- 수상 경력 및 인증 강조: 캠페인이 수상 경력이나 인증을 받았다는 점을 강조하여 신뢰도를 높입니다. “업계 최고 권위 상 수상” 또는 “전문 기관 인증”과 같은 문구를 통해 고객에게 높은 품질과 신뢰성을 전달합니다. +""", + 5: """ +고객의 감정을 자극하여 캠페인 참여를 유도하는 방식으로, 예상되는 어려움이나 공감할 수 있는 상황을 상기시키고, 고객의 감정에 깊이 호소하고 진정성있게 표현하며, 자연스럽게 참여를 이끌어냅니다. + +<핵심 요소> +- 예상되는 어려운 상황을 상기: 고객이 공감할 수 있는 어려운 상황이나 문제를 제시하여, 그 상황을 해결하고 싶은 마음이 들도록 합니다. +- 공감대 형성과 스토리텔링: 고객의 일상과 관련된 스토리텔링을 통해 “어머, 내 얘기 같아!”라는 공감대를 형성하여 감정적 연결을 강화합니다. 스토리 속에서 고객이 자신의 경험을 떠올리며 참여하고 싶어지도록 만듭니다. +- 긍정적인 감정 유발: 행복감, 편안함, 만족감과 같은 긍정적인 감정을 불러일으켜, 캠페인이 고객의 삶에 더 좋은 영향을 줄 것이라는 기대감을 심어줍니다. +""", + 6: """ +메시지를 받는 고객과 특별한 관계를 형성하고 있음을 전달하여, 개인적이고 특별한 혜택을 제공받는 느낌을 주어 캠페인 참여를 유도하는 전략입니다. 고객은 남들과 다른 특별한 대우를 받고 있다는 인식을 통해, 더욱 강한 소속감과 참여 욕구를 느끼게 됩니다. + +<핵심 요소> +- 개인적인 혜택 강조: 고객이 개인적으로 선별되었거나 특별한 혜택을 받을 자격이 있음을 알립니다. 예를 들어, “당신만을 위한 특별 혜택”과 같은 표현으로 고객이 특별히 선택받은 느낌을 가지도록 하여 캠페인에 대한 긍정적인 인식을 심어줍니다. +- 고객과의 특별한 관계 형성: 메시지를 통해 브랜드와 고객 사이에 특별한 관계가 있음을 강조하여 소속감을 부여합니다. “우리와 오랜 시간 함께해주신 고객님께 드리는 혜택”과 같은 문구를 사용하여, 고객이 브랜드와의 관계를 특별하게 느끼도록 유도합니다. +- 개인 맞춤형 메시지 제공: 고객의 관심사나 취향에 맞춘 맞춤형 메시지로 개인화된 경험을 제공합니다. 이를 통해 고객은 브랜드가 자신의 요구를 잘 이해하고 있다는 인식을 가지게 되어, 캠페인에 더욱 쉽게 참여할 수 있게 됩니다. +""", + 7: """ +상품이나 서비스의 기능적 장점과 구체적인 혜택을 강조하여, 고객이 실질적인 가치를 느끼도록 하는 전략입니다. 단순한 광고 문구를 지양하고, 상품이 고객의 삶에 어떤 긍정적인 변화를 가져다줄 수 있으며, 어떤 문제를 해결해 줄 수 있는지 구체적으로 제시하여 고객이 상품의 가치를 명확하게 인지하도록 유도합니다. 고객은 명확하고 현실적인 정보가 제공될 때 상품의 가치를 명확히 이해하고 구매 욕구를 느끼게 됩니다. + +<핵심 요소> +- 실질적인 장점과 혜택 강조: 상품이 제공하는 구체적이고 실용적인 장점을 분명하게 전달하여, 고객이 직접적인 혜택을 느낄 수 있도록 합니다. 구체적이고, 실질적 효과를 명확히 보여줍니다. +- 고객의 문제 해결에 초점: 고객이 겪고 있는 특정 문제를 해결할 수 있는 상품의 기능과 효과를 구체적으로 설명하여, 고객이 자신의 삶에 어떻게 긍정적인 변화를 가져올 수 있을지 상상할 수 있도록 합니다. +""", + 8: """ +새로 출시된 상품이나 업그레이드된 서비스의 혁신적 기능과 차별화된 가치를 강조하여, 고객이 최신 혜택과 기능을 가장 먼저 경험할 수 있도록 유도하는 전략입니다. 이 전략은 단순히 ‘새로움’을 강조하는 데 그치지 않고, 상품이 제공하는 구체적인 새로운 기능과 혜택을 명확히 전달하여 고객의 관심을 끌고 캠페인에 참여하게 합니다. + +<핵심 요소> +- 새로 출시되었음을 강조: 신제품 또는 업그레이드 상품이라는 점을 부각시켜, 고객이 최신 트렌드와 혜택을 선도적으로 경험할 기회를 놓치지 않도록 합니다. +- 혁신적인 기능과 개선된 성능 소개: 상품이 기존과 어떻게 다른지 구체적인 기능과 성능 개선점을 명확히 설명하여, 고객이 상품의 혁신적 가치를 쉽게 이해할 수 있도록 합니다. +""", + 9: """ +넷플릭스 프로그램 '흑백요리사'에 나온 유행어('이븐', '익힘 정도', '나야,~')를 활용해 사용자 관심을 끄는 마케팅 문구를 작성합니다. 문구 생성 시, 제목 또는 첫 줄에만 유행어를 사용합니다. + +사용 방법: +1. 이븐하네요/ 이븐하게: 혜택이 좋다는 의미를 "이벤트/ 혜택이 이븐하다" 또는 "이븐한 혜택"과 같은 표현으로 나타냅니다. +2. 익힘 정도를 저는 굉장히 중요시 여기거덩요: 이벤트, 혜택, 증정품에 대해 "익힘 정도"라는 표현을 통해 강조합니다. ({이벤트/혜택/증정품} 익힘 정도를 저는 굉장히 중요시 여기거덩요) +3. 나야, 000: "나야,"로 시작하여 이벤트나 혜택을 강조합니다. "나야," 뒤에 문구는 사람이 아닌 이벤트, 혜택, 증정품을 작성합니다. 단, "나야," 뒤에는 문장이 아닌 단어로 끝나야 합니다. (나야, {이벤트/ 혜택/ 증정품}) +""", + 10: """ +MZ세대(밀레니얼+Z세대) 특유의 관심사와 언어 스타일을 반영하여 트렌디하고 유쾌한 메시지로 캠페인 참여를 유도합니다. 각 문구에는 아래의 'MZ 용어 사전' 용어 중 하나의 용어만을 사용하며, 캠페인 목적과 내용에 가장 잘 맞는 단어를 선택해 자연스럽게 반영합니다. + + +- 찐친: '진짜 친한 친구'의 줄임말 +- 가심비: 가격 대비 심리적 만족을 추구하는 소비 트렌드 +- 극호: ‘극도로 호감’의 줄임말. 비슷한 의미의 표현으로는 ‘최애(=가장 사랑함)’가 있음. +- 눈팅: 온라인 커뮤니티 등에서 게시글을 쓰거나 댓글을 달지 않고 오직 게시물을 읽기만 하는 것을 의미함. +- 도파민 중독: 도파민이란 쾌락을 느끼게 해주는 신경전달물질을 뜻함. 도파민 중독이란 자극적인 콘텐츠만을 소비하며 도파민을 좇는 상태를 의미함. +- 많관부: ‘많은 관심 부탁드립니다’의 줄임말. 이벤트 참여나 카드 발급등 행동을 유도할 때 사용함. +- 알잘딱깔센: ‘알아서 잘 딱 깔끔하고 센스있게’의 줄임말. +- 잘알: '잘 안다', '잘 아는 사람'이라는 뜻의 신조어. +- 찐: ‘진짜’를 뜻하는 접두어. ‘찐팬’, ‘찐사랑’ 등 진심을 강조하고 싶을 때 쓰임. +- 최애: MZ세대가 최고로 사랑하는 대상, 1순위를 부르는 말. +- 뽐뿌: 무언가를 소비하고 싶은 충동을 부르는 온라인 유행어. +- MZ세대: 1980년 초반부터 2000년대 중반 사이에 태어난 밀레니얼 세대와 Z세대를 통칭하는 말. +- 꾸안꾸: ""꾸민 듯 안 꾸민 듯""의 줄임말로, 자연스러운 스타일링을 의미합니다. 주로 패션과 관련되어 사용합니다. +- 소확행: 소소하지만 확실한 행복 +- 꿀팁: 유용하고 실속 있는 팁 +- 핵이득: 엄청난 혜택, 큰 이득 +- 찐템: 진짜 좋은 물건, 진짜 추천템 +- 득템: 좋은 물건을 획득한 기쁨 +- 힙하다/힙한: 멋지고 최신 트렌드에 맞는 스타일 +- 힙스터: 유행에 민감하고 개성 있는 사람 +- 알짜배기: 꼭 필요한 핵심만 있는, 알찬 +- 행복지수: 삶의 만족도를 높이는, 행복을 주는 것 +- 일상루틴: 일상에서 규칙적으로 하는 활동 +- 인생00: 평생 사용하고 싶은 만큼 좋은 00 +- 꿀조합: 너무 잘 어울리는 조합 +- 월요병 극복: 일요일 저녁이나 월요일 아침에 느끼는 무기력증이나 피로감, 우울감, 불안감 등의 심리적, 신체적 증상을 극복한다는 의미 +- 텅장: 텅 빈 통장을 의미함. 텅장 탈출, 텅장 걱정 끝, 텅장 벗어나자 처럼 텅 빈 통장을 극복하자는 의미로 사용함. +- 취향저격: 취향에 딱 맞는 +- 심쿵: 마음이 두근거리는 +- 댕냥이: 강아지와 고양이를 귀엽게 부르는 말 +- 집콕: 집에서 즐기는 활동 +- 찐행복: 진짜 행복하고 만족스러운 기분 +- 만렙: 어떤 분야에서든 최고의 경지에 오른 사람 +- 역대급: '역대급'은 ""역사에 대대로 남을 만큼 엄청난 수준""이라는 뜻으로, 보통 최고 수준의 어떤 것, 즉 역대 최고를 의미함 +""", +} + + +EXAMPLE_COPY_MAPPER: dict[int, str] = { + 1: """ +"받을 수 있는 기회가 사라집니다, 지금 아니면 손해! 놓치지마세요!", "(광고) [특정 브랜드] 하나머니 적립 이벤트! [특정 브랜드]에서 결제 참여하고, 하나머니 적립이벤트 놓치지 마세요!", "이번 기회를 놓치면, 다음은 언제일지 몰라요! 지금 바로 참여하세요.", "지금 참여하지 않으면 혜택을 받을 수 없습니다. 기회를 잡으세요!", "놓치면 후회할 특별 혜택! 지금 이벤트에 참여하고 최대 00원 추가 적립 받으세요!", "(광고) 혹시 아직도 [특정 브랜드] 혜택 못 받고 계신가요? 지금 [특정 브랜드] [카드 이름]을 발급받지 않으면 혜택을 못받고 계신거예요! 지금 받아보세요!", "지금 바로 [카드 이름]을 발급 받고 절호의 찬스를 놓치지 마세요!", "이 혜택, 놓치면 후회할지도? 지금 바로 만나보세요!", "혜택 버스 떠나요! 지금 바로 탑승하세요!" +""", + 2: """ +"이미 많은 고객님들이 참여하셨어요, 고객님만 모르고 계셨나요?", "혼자만 모르고 계셨나요? 편의점에서 QR 결제하시고 최대 00천 하나머니 받아가세요!", "(광고) [특정 브랜드] 하나머니 적립 이벤트! 이미 대부분의 사람들이 혜택을 받아갔어요. [특정 브랜드]에서 하나카드로 결제하시고 00원 하나머니 적립 받으세요.", "모두가 경험하고 있는 혜택, 당신만 놓치지 마세요. 지금 바로 확인하세요!", "이미 많은 분들이 혜택을 누리고 있어요. 아직 참여하지 않으셨다면, 지금 바로 기회를 잡아보세요.", "남들은 벌써 [카드 이름]로 쏠쏠하게 혜택 받고 있다는 사실! 나만 없으면 손해! 놓치면 후회할 혜택들!", "다른 분들은 벌써 하나머니로 알뜰하게 장보고, 커피 마시고 있어요! 이 혜택, 놓치실 건가요?", "아직도 고민 중이세요? 다른 사람들은 벌써 혜택 누리는 중!", "세상이 다 아는 이벤트! 고객님도 아셔야죠!", "알만한 사람들은 다 아는 이 이벤트! 아직도 망설이시나요?" +""", + 3: """ +"미션 혜택 기간이 얼마 남지 않았어요!", "딱 한달간만! 진행되는 특별 이벤트", "D-0! 0일 동안만 참여할 수 있는 특별 이벤트!", "주말 한정! oo만 포인트 혜택, 놓치지 마세요!", "오.늘.특.가! 놓칠 수 없는 찬스!", "늦기 전에 계좌 개설하고 최대 00원 받아가세요!", "*긴급* 마감까지 00시간 전! 고객님! 오늘 가입하지 않으면 내일은 혜택이 사라져요! ", "똑똑! 고객님 쿠폰함에 있는 00원 쿠폰이에요. 이대로 사라지긴 너무 아쉬워요. 내일이 지나기 전에 꼭 써주세요.", "선착순 00명 한정! 카드발급하면 [특정 브랜드] 0개월 무료 이용권 증정!", "0일만 드려요! 결제계좌 변경하고 0만 하나머니 받자", "오늘 끝이예요! 혜택 막차 타세요!", "선착순 쟁탈전! 먼저 잡는 자가 승리한다!", "시간이 얼마 남지 않았어요! 카운트 다운 시작!", "지금 바로 사라지는 쿠폰을 사용해보세요! 망설이는 순간 사라집니다!", "0을 특전! 특가 배 출발해요~! 뿜뿜~!" +""", + 4: """ +"이미 00만 명이 발급한 [카드 이름], 지금 바로 발급받아 보세요.", "00만이 선택한 [카드 이름] 여행 경험을 함께 해보세요.", "00만 고객님이 믿고 선택한 혜택, 지금 바로 이용해보세요!", "일본 여행을 계획하는 고객 중 반 이상이 이미 사용 중!", "하나카드를 선택한 수많은 고객들의 혜택, 이제 당신 차례입니다!", "00만 명이 누렸습니다. 바로 사용해보세요!", "5명 중 4명이 선택한 이유", "모두가 애용하는 혜택, 당신도 누릴 수 있습니다.", "00명이 선택한 혜택, 놓치면 후회하실거예요!", "많은 리뷰들이 증명한 품질과 다양한 쇼핑 혜택! 만나보세요!", "안 써본 사람은 있어도, 한 번만 써본 사람은 없다!", "00명이 [특정 브랜드]과 함께합니다.", "벌써 많은 분들이 0월 이벤트에 응모했어요!", "00% 고객이 재이용한 혜택!" +""", + 5: """ +"하나카드는 언제나 당신의 편입니다. 따뜻한 혜택을 지금 받아보세요!", "해외에서 데이터 없어 난감해지기 싫다면! 지금 당장 로밍해보세요~", "삶의 작은 순간까지 소중히, 하나카드가 더 큰 혜택으로 당신을 응원합니다.", "소중한 사람을 위해, 이 순간을 놓치지 마세요.", "나만의 특별한 순간을 더욱 빛나게 해줄 카드, [카드 이름]", "힘들었던 하루, 나에게 작은 선물을 주세요. [카드 이름]으로 맛있는 저녁 식사 어떠세요?", "두근두근! 놓치면 후회할 이벤트! [카드 이름]과 함께 잊지 못할 추억을 만들어보세요!", "빛나는 순간, 소중한 추억, 그리고 [카드 이름]. 지금 바로 신청하고 당신의 이야기를 만들어가세요!", "[카드 이름]이 당신의 일상에 작은 행복을 더해드릴게요. 오늘 하루도 수고 많으셨습니다!", "지금 바로 이벤트에 참여하고 행운의 주인공이 되어보세요! 당신의 꿈을 [카드 이름]이 응원합니다!" +""", + 6: """ +"하나카드가 고객님에게만 추천해요.하나카드와 앞으로도 쭉 함께해요!", "ooo님에게만 살짝 보여드리는 커피 한 잔의 기회!", "이 메시지를 받으신 000 고객님께 드리는 혜택!", "이 메시지를 보신 분들께만 드리는 깜짝 쿠폰 도착!", "우리 고객들에게만 열리는 히든 이벤트!", "이 문자를 받으신 분들께만 드리는 추가 혜택!", "지금 꼭! 참여하셔야 하는 이유를 고객님께만 몰래 알려드려요!", "고객님 쿠폰함에만 비밀 쿠폰을 몰래 넣어두었어요.", "○○○님만을 위한 특별 초대!" +""", + 7: """ +"고객님께 딱 맞는 [카드 이름]! 지금보다 00% 더 할인 받아보세요!", "돈 버는 카드! 캐시백이 팡팡!", "깜짝 놀랄 속도! 1분이면 만나면 세상, 이제는 하나페이 할 시간", "압도적인 편리함! 앱의 상식을 바꾼 하나페이", "하나카드니까 하나면 끝! 메가급 기능, 앱은 하나", "더 똑똑해진 [카드 이름]으로, 더 나은 서비스를 받아보세요!", "환전 국가가 00개 더 추가됐어요!", "[카드 이름]으로 주유비 부담을 덜어드립니다! 리터당 최대 00원 할인, 주유 할인 혜택으로 더욱 가볍고 즐거운 드라이빙을 즐겨보세요!", "[카드 이름]으로 통신비 걱정 끝! 매월 00원 통신비 할인 혜택으로 더욱 스마트하고 알뜰한 통신 생활을 누려보세요!", "더욱 강력해진 혜택으로 해외여행 준비하자! [카드 이름] 바로 발급받으세요!" +""", + 8: """ +"새로운 서비스, 가장 먼저 만나보세요!", "새로운 [카드 이름]! 아직 발급하지 않으셨네요!", "새롭게 업그레이드된 [카드 이름]를 가장 먼저 경험해보세요!", "새롭게 출시된 [카드 이름]를 가장 먼저 사용해보세요!", "최신 혜택이 가득한 [카드 이름]로 결제해보세요! 이제껏 없던 경험, 지금 바로 누려보세요!", "평범함을 넘어선 신선함, 지금 바로 체험하세요.", "더욱 새로워진 [카드 이름]! 다양해진 혜택을 만나보세요!", "드디어 출시! 모두가 기다려온 새로운 [카드 이름]!" +""", + 9: """ +"8월혜택 익힘정도가 이븐하네요", "올가을, 혜택도 가격도 이븐하게 즐기자!", "퐁당 퐁당 연휴에 노는 일정이 이븐하게 채워졌어요.", "출석체크로 스타벅스 쿠폰 이븐하게 받아보기", "AXA 자동차보험 가입하고 3만하나머니 이븐하기 받아보자", "11월 혜택 익힘정도를 저는 굉장히 중요시 여기거덩요", "DB손해보험 자동차 가입혜택 익힘정도를 저는 굉장히 중요시 여기거덩요", "하나카드는 김*나님의 의견을 굉장히 중요시 여기거덩요", "나야, 9월 최고 혜택", "나야, 봄나들이 혜택", "나야, 밀리의서재 할인쿠폰", "나야, 봄맞이 깜짝선물" +""", + 10: """ +"항상 우리 카드를 이용해주셔서 감사해요! 찐친 고객님만을 위한 특별한 이벤트를 준비했어요", +"혹시 이런 혜택은 알고 계셨나요? 찐친 고객님께만 드리는 특별한 혜택이랍니다", +"(쉿) 찐친이니까 알려주는거야~ 마감하기전에 빨리 신청해!!", +"찐친에게 카드 추천하고 함께 혜택 받자!", +"가심비가 높은 혜택", +"가심비 혜택", +"가심비 추구하는 고객님에게 딱이에요", +"이 카드 진짜 극호. 한번 쓰면 절대 못 놓음", +"솔직히 이 카드 극호인데 안 사면 후회할 듯", +"이제까지 눈팅만했다면 이번 혜택은 주목해주세요", +"눈팅은 금물! 지금 바로 카드 혜택 확인하고 득템하세요!", +"이런 혜택은 처음이지? 눈팅만 하지 말고 카드 발급 받고 혜택 받아가", +"이 혜택은 눈팅만 하기 아까워요", +"이 혜택 눈팅하다가 놓치면 후회할걸요? 지금 바로 신청하세요", +"심장이 쿵쾅거리는 혜택! 도파민 폭발, 지금 바로 확인하세요", +"선착순 마감 임박! 숨막히는 혜택, 도파민 뿜뿜!", +"놓치면 후회할 혜택! 도파민 채우러 어서 오세요", +"스타벅스 쿠폰도 받고, 하나머니도 받고! 많관부", +"이벤트 참여하면 경품을 드려요. 많관부~!", +"트래블로그 카드 하나면 여행준비 알잘딱깔센", +"번거롭게 여행준비하지말고 트래블로그 카드로 알잘딱깔센하게 준비해보자", +"혜택 잘알이라면 놓칠 수 없는 기회! 지금 바로 신청하세요", +"쇼핑 잘알을 위한 특별한 혜택! 백화점 상품권 증정 이벤트!", +"카드혜택 잘알은 이미 다 신청했다", +"찐 고객만을 위한 찐 혜택! 지금 바로 확인하세요", +"찐 할인, 찐 이득! 하나카드로 더욱 알뜰하게 쇼핑하세요.", +"당신의 소비 패턴에 딱 맞는 찐 맞춤형 혜택을 경험하세요.", +"찐 행복", "찐 혜택", "찐 기회", "찐 특별한 카드", "찐 감동", "찐친", "찐후기", +"카드 사용하고 최애탬 받아가자", +"요즘 MZ 최애 카드 발급받고 3만 하나머니 받자", +"(쉿) 요즘 MZ 최애 카드 너한테만 알려줄게", +"핵인싸템 지금 안 사면 후회각! 뽐뿌주의! [상품명] 득템 찬스! 놓치지 마세요!", +"[상품명] 뽐뿌 왔다! 지름신 강림하는 기회 놓치지 마세요!", +"플렉스 해버렸지 뭐야~ [상품명] 뽐뿌 참을 수 없어서 결국 질렀다! 나만의 행복, 지금 바로!", +"인싸템 경고 [상품명] 뽐뿌 주의!", +"심쿵주의 [상품명] 뽐뿌 제대로다! 소장 욕구 뿜뿜! 지금 바로 확인!", +"MZ세대 필수템! [상품명] 없이는 살아남을 수 없어! 지금 바로 경험해봐!", +"MZ세대 취향 존중! [상품명]으로 나만의 개성을 표현해봐! ", +"MZ세대 핫템 [상품명] 놓치면 후회할지도 몰라! 지금 바로 확인!", +"MZ세대 소장 욕구 자극! [상품명] 한정판 출시! 품절되기 전에 서두르세요!", +"MZ세대를 위한 특별한 혜택! [상품명] 구매 시 특별 할인 쿠폰 증정!", +"꾸안꾸 스타일이 추구미인 분 주목!", +"꾸안꾸 패피들은 다 안다는 힙스터템 [상품명], 지금 바로 겟!", +"[상품명]으로 꾸안꾸 완성!", +"오늘의 소확행, 바로 [상품명]으로부터", +"소확행? 어렵지 않아요! [상품명] 하나면 충분!", +"일상에 지친 당신에게, [상품명]이 선사하는 소확행", +"나만의 소확행 레시피, [상품명]으로 완성해봐!", +"핵인싸템 꿀팁 대방출! [상품명] 득템 찬스 절대 놓치지 마세요", +"인생템 꿀팁! [상품명]으로 [문제점] 해결 완료!", +"텅장될 뻔했는데... [상품명] 꿀팁 덕분에 돈 아꼈다!", +"힙스터들은 다 안다는 꿀팁! [상품명]이면 충분해", +"꿀팁 알려드림! [상품명] 이렇게 사용하면 100배 더 좋음", +"이 카드로 여행가면 핵이득~ 이래도 안받을거야?" +"텅장될 걱정 놉! 쇼핑할땐 이 카드쓰면 핵이득!", +"이번 주 핵이득 혜택은 뭘까?", +"지금 가입하면 연회비 무료! 핵이득~", +"인싸들은 다 안다는 찐템! 설마 이 카드 아직도 없어?", +"이 카드 여행갈때 찐템이다.. 꼭 받아서 여행가야해", +"요즘 MZ의 여행 찐템은 바로 트래블로그카드! 바로 발급받고 쿠폰도 받자", +"요즘 여행 찐템은 바로 트래블로그카드! 바로 발급받고 커피쿠폰 받자", +"트래블로그 카드로 여행 쇼핑 득템하자", +"트래블로그 카드라면 확실한 득템 가능하지~!", +"여행준비 이렇게만 하면 득템 완전 가능!", +"(소곤소곤)너한테만 득템할 기회 알려줄게", +"요즘 힙하다고 소문난 그 카드!", +"요즘 힙한 애들은 다 이 카드 쓴대", +"힙스터들의 최애템! 이 카드 없이는 살아남을 수 없어!", +"힙한 라이프를 위한 필수템! 이 카드 하나면 힙스터 완성! (카드 신청하기)", +"힙스터라면 놓칠 수 없는 힙한 이벤트!", +"알짜배기 혜택 놓치지 마세요!", +"똑똑한 소비자들은 다 안다는 알짜배기 카드! 아직도 없어?", +"여행 갈 때 이 카드 하나면 알짜배기 혜택 끝판왕!", +"알짜배기 카드 쓰고 돈 아껴서 맛있는 거 사 먹자!", +"쇼핑할 때 이 카드 없으면 손해! 알짜배기 할인 혜택 놓치지 마세요!", +"친구야, 나만 알고 있는 알짜배기 카드 알려줄게! 같이 혜택 받자!", +"행복지수 높이는 꿀팁! 이 카드 하나면 충분해!", +"스트레스는 DOWN, 행복지수는 UP! 이 카드로 즐거운 쇼핑하세요!", +"행복지수 만렙 찍고 싶다면? 이 카드 혜택 놓치지 마세요!", +"행복지수 UP! 이벤트 참여하고 선물 받으세요!", +"내 일상루틴에 딱인 카드가 나왔어!", +"매일 쓰는 카드, 이왕이면 혜택 좋은 걸로! 일상루틴에 딱 맞는 카드 추천!", +"커피 한 잔도 혜택 받으면서! 일상루틴 속 소소한 즐거움을 더해줄 카드!", +"교통비, 통신비 할인까지! 일상루틴에 꼭 필요한 혜택 가득! (카드 신청하기)", +"드디어 당신의 인생 카드가 나왔어요", +"인생 카드 이거 하나면 걱정 끝! 교통, 통신, 쇼핑까지 모든 혜택을 한 장에!", +"인생 카드로 돈 아껴서 여행 가자!", +"쇼핑할 때 이 카드 없으면 손해! 인생 할인 놓치지 마세요!", +"쇼핑할 때 이 카드 없으면 손해! 꿀조합 할인 혜택 놓치지 마세요!", +"여행 갈 때 이 카드 하나면 꿀조합 완성! 항공권, 숙박, 렌터카까지 모두 할인!", +"월요병 극복엔 역시 쇼핑이지! 최대 50% 할인 혜택 받고 스트레스 날려버려!", +"월요병 극복! 이 카드와 함께라면 가능해!", +"월요병 극복엔 역시 맛있는 음식! 이 카드로 맛집 할인 받고 기분 전환!", +"월요병 날려버릴 카드 쓰고 기분좋은 월요일 보내자!", +"월요병 극복하고 싶다면? 이 카드 이벤트 참여하고 경품 받으세요!", +"텅장 탈출! 이 카드와 함께라면 가능해!", +"이번 달도 텅장? 이 카드 쓰고 알뜰하게 소비 생활 시작해봐!", +"텅장 걱정 끝! 이 카드로 똑똑하게 돈 관리하자!", +"텅장에서 벗어나 부자 되는 지름길! 이 카드 혜택 놓치지 말자", +"취향저격 탕탕! 이 카드 놓치면 후회할걸?", +"내 취향저격 탕탕! 이 카드, 너무 완벽해서 심쿵!", +"취향저격 제대로! 내가 원하는 혜택만 쏙쏙 골라 담았다!", +"당신의 취향을 저격할 카드가 출시됐습니다", +"친구야, 너의 취향을 저격할 카드가 여기 있어! 같이 핵이득 혜택 받자!", +"심쿵 혜택에 놀라지 마세요!", +"심쿵! 이 카드 혜택에 반할 수밖에 없어!", +"심쿵 유발 혜택! 소장 욕구 뿜뿜!", +"내가 카드 혜택에 심쿵할 줄이야..", +"댕냥이 집사라면 이 카드 놓치면 안 돼!", +"댕냥이 용품 쇼핑할 때 이 카드면 할인 듬뿍!", +"댕냥이 병원비 걱정 뚝! 이 카드로 부담 덜고 건강 지켜주세요!", +"댕냥이와 함께하는 행복한 일상! 이 카드로 더욱 풍족하게 만들어보세요!", +"댕냥이 집사라면 놓칠 수 없는 이벤트! 참여하고 푸짐한 선물 받으세요!", +"댕냥이 키우는 친구 소환! 이 카드 알려주고 같이 혜택 받자!", +"집콕 생활, 이 카드 하나면 완벽해!", +"집콕 생활 필수템! 배달 음식 할인받고 집에서 편하게 즐겨봐!", +"집콕하면서 돈 버는 방법? 이 카드면 충분해", +"집콕 생활이 더욱 즐거워지는 이벤트! 참여하고 푸짐한 경품 받으세요!", +"집콕 생활 꿀팁 알려줄게! 00카드라고 들어봤어?", +"찐행복 가득한 삶, 이 카드로 시작하세요!", +"찐행복을 느끼고 싶다면 이 카드 발급받자", +"찐행복 찾고 있다면? 이 카드 이벤트 참여하자", +"찐행복 느끼고 싶다면? 이 카드로 쇼핑하고 스트레스 날려버려!", +"찐행복 비결? 바로 이 카드! 지금 바로 신청하자", +"쇼핑 만렙 주목! 놓치면 안될 카드가 나왔다", +"여행 만렙은 모두가 가지고 있는 카드!", +"재테크 만렙이 잘 쓰는 카드 궁금해?", +"역대급 혜택, 놓치면 후회할걸?", +"역대급 혜택 폭탄! 바로 이 카드야", +"역대급 혜택 받고 여행 가자! 항공권, 숙박, 렌터카까지 모두 할인!", +"역대급 카드가 나왔어요..여행가는 분은 꼭 보셔야합니다", +"쇼핑할 때 이 카드 없으면 손해! 역대급 할인 혜택 놓치지 마세요!" +""", +} diff --git a/copygo/routers/__init__.py b/copygo/routers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/copygo/routers/copies.py b/copygo/routers/copies.py new file mode 100644 index 0000000..fa26a4f --- /dev/null +++ b/copygo/routers/copies.py @@ -0,0 +1,25 @@ +from fastapi import APIRouter + +from copygo.clients import OpenAIClient +from copygo.schemas import CopyRequest, ErrorResponse, Response +from copygo.services import CopyGoService + +copy_router = APIRouter() + + +@copy_router.post( + "/copy/generate", + response_model=Response, + responses={400: {"model": ErrorResponse}}, + tags=["copy"], +) +async def generate_copy(chat_request: CopyRequest) -> Response: + chat_service = CopyGoService(OpenAIClient(api_key=chat_request.api_key)) + response = await chat_service.generate_copy( + target_customer=chat_request.target_customer, + campaign_description=chat_request.campaign_description, + purpose=chat_request.purpose, + copy_tone=chat_request.copy_tone, + strategy=chat_request.strategy, + ) + return Response(data=response) diff --git a/copygo/routers/images.py b/copygo/routers/images.py new file mode 100644 index 0000000..8dc6712 --- /dev/null +++ b/copygo/routers/images.py @@ -0,0 +1,25 @@ +from fastapi import APIRouter + +from copygo.clients import OpenAIClient +from copygo.schemas import ErrorResponse, ImageRequest, Response +from copygo.services import CopyGoService + +image_router = APIRouter() + + +@image_router.post( + "/image/generate", + response_model=Response, + responses={400: {"model": ErrorResponse}}, + tags=["image"], +) +async def generate_image(image_request: ImageRequest) -> Response: + chat_service = CopyGoService(OpenAIClient(api_key=image_request.api_key)) + response = await chat_service.generate_image( + campaign_title=image_request.campaign_title, + campaign_description=image_request.campaign_description, + quality=image_request.quality, + size=image_request.size, + style=image_request.style, + ) + return Response(data=response) diff --git a/copygo/schemas.py b/copygo/schemas.py new file mode 100644 index 0000000..25a3edc --- /dev/null +++ b/copygo/schemas.py @@ -0,0 +1,46 @@ +from typing import Literal + +from pydantic import BaseModel, Field + +from copygo.prompts import ( + CAMPAIGN_PURPOSE_MAPPER, + COPY_STRATEGY_NAME_MAPPER, + COPY_TONE_MAPPER, +) + + +def _get_str(_dict: dict[str, str]) -> str: + return ", ".join(f"{key}: {value}" for key, value in _dict.items()) + + +class ErrorResponse(BaseModel): + message: str + statusCode: str + + +class CopyRequest(BaseModel): + api_key: str = Field(..., description="API KEY") + target_customer: str = Field(..., description="캠페인의 타겟 고객군에 대한 설명") + campaign_description: str = Field(..., description="캠페인의 주요 내용에 대한 설명") + purpose: int = Field(1, ge=1, le=12, description=_get_str(CAMPAIGN_PURPOSE_MAPPER)) + copy_tone: int = Field(1, ge=1, le=7, description=_get_str(COPY_TONE_MAPPER)) + strategy: int = Field( + 1, ge=1, le=10, description=_get_str(COPY_STRATEGY_NAME_MAPPER) + ) + + +class ImageRequest(BaseModel): + api_key: str = Field(..., description="API KEY") + campaign_title: str = Field(..., description="") + campaign_description: str = Field(..., description="") + quality: Literal["standard", "hd"] = Field("standard", description="") + size: Literal["1024x1024", "1792x1024", "1024x1792"] = Field( + "1024x1024", description="" + ) + style: Literal["vivid", "natural"] = Field("vivid", description="") + + +class Response(BaseModel): + message: str = "OK" + statusCode: str = "200" + data: str | None diff --git a/copygo/services.py b/copygo/services.py new file mode 100644 index 0000000..433c237 --- /dev/null +++ b/copygo/services.py @@ -0,0 +1,71 @@ +from copygo.clients import OpenAIClient +from copygo.prompts import ( + CAMPAIGN_PURPOSE_DESCRIPTION_MAPPER, + COPY_STRATEGY_DESCRIPTION_MAPPER, + COPY_STRATEGY_NAME_MAPPER, + COPY_TONE_DESCRIPTION_MAPPER, + COPY_TONE_MAPPER, + EXAMPLE_COPY_MAPPER, + IMAGE_PROPMT, + MESSAGE_PROPMT, +) + + +class CopyGoService: + def __init__(self, client: OpenAIClient): + self.client = client + + def get_message(self, message: str) -> list[dict[str, str]]: + return [ + {"role": "system", "content": "You are a helpful marketer."}, + {"role": "user", "content": message}, + ] + + async def generate_copy( + self, + target_customer: str, + campaign_description: str, + purpose: int, + copy_tone: int, + strategy: int, + ) -> str: + message = MESSAGE_PROPMT.format( + campaign_purpose=CAMPAIGN_PURPOSE_DESCRIPTION_MAPPER[purpose], + campaign_description=campaign_description, + target_customer=target_customer, + copy_tone=COPY_TONE_MAPPER[copy_tone], + copy_tone_description=COPY_TONE_DESCRIPTION_MAPPER[copy_tone], + copy_strategy_name=COPY_STRATEGY_NAME_MAPPER[strategy], + copy_strategy_description=COPY_STRATEGY_DESCRIPTION_MAPPER[strategy], + example_copy=EXAMPLE_COPY_MAPPER[strategy], + copy_count=1, # FIXME: redundant inputs below + is_title_included="Y", + is_content_included="Y", + min_title_character_count=30, + max_title_character_count=45, + min_content_character_count=120, + max_content_character_count=150, + ) + response = await self.client.generate_copy(messages=self.get_message(message)) + if response.startswith("```json") and response.endswith("```"): + response = response.strip("```json\n").strip("```") + return response + + async def generate_image( + self, + campaign_title: str, + campaign_description: str, + quality: str, + size: str, + style: str, + ) -> str: + prompt = IMAGE_PROPMT.format( + campaign_title=campaign_title, + campaign_description=campaign_description, + ) + return await self.client.generate_image( + prompt=prompt, + quality=quality, + size=size, + style=style, + ) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d9a681f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,38 @@ +annotated-types==0.7.0 +anyio==4.8.0 +black==25.1.0 +certifi==2025.1.31 +click==8.1.8 +colorama==0.4.6 +decorator==5.2.1 +distro==1.9.0 +fastapi==0.115.9 +h11==0.14.0 +httpcore==1.0.7 +httptools==0.6.4 +httpx==0.28.1 +idna==3.10 +isort==6.0.1 +jiter==0.8.2 +mypy-extensions==1.0.0 +openai==1.65.1 +orjson==3.10.15 +packaging==24.2 +pathspec==0.12.1 +platformdirs==4.3.6 +py==1.11.0 +pydantic==2.10.6 +pydantic-settings==2.8.1 +pydantic_core==2.27.2 +python-dotenv==1.0.1 +PyYAML==6.0.2 +retry==0.9.2 +setuptools==75.8.0 +sniffio==1.3.1 +starlette==0.45.3 +tqdm==4.67.1 +typing_extensions==4.12.2 +uvicorn==0.34.0 +watchfiles==1.0.4 +websockets==15.0 +wheel==0.45.1 From f630275c2437bed47022847cc4aa71f83696026f Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 9 Apr 2025 09:21:02 +0900 Subject: [PATCH 2/2] update pre-commit --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2430d23..8307403 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ repos: - id: check-merge-conflict - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.9.9 + rev: v0.11.4 hooks: - id: ruff args: [ --fix ]