diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d554c8f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# VSCode 설정 - 기본적으로 모든 파일 제외 +.vscode/* \ No newline at end of file diff --git a/backend-down.sh b/backend-down.sh new file mode 100644 index 0000000..bdc509f --- /dev/null +++ b/backend-down.sh @@ -0,0 +1,3 @@ +#!/bin/bash +echo "🧹 Stopping backend, celery, and redis containers..." +docker compose -f docker-compose.backend.yml down \ No newline at end of file diff --git a/backend-up.sh b/backend-up.sh new file mode 100644 index 0000000..e8917c8 --- /dev/null +++ b/backend-up.sh @@ -0,0 +1,13 @@ +#!/bin/bash +set -e + +echo "🧹 Stopping and cleaning up old backend containers..." +docker compose -f docker-compose.backend.yml -p backend down + +echo "🔧 Building backend images..." +docker compose -f docker-compose.backend.yml -p backend build + +echo "🚀 Starting backend stack (FastAPI + Celery Worker)..." +docker compose -f docker-compose.backend.yml -p backend up -d + +echo "✅ Backend is up (Redis is expected to run on host:6379)" \ No newline at end of file diff --git a/backend/.coveragerc b/backend/.coveragerc new file mode 100644 index 0000000..07882a7 --- /dev/null +++ b/backend/.coveragerc @@ -0,0 +1,22 @@ +[run] +source = app +omit = + */tests/* + */migrations/* + */__init__.py + */conftest.py + */celery_app.py + */workers/* + +[report] +exclude_lines = + pragma: no cover + def __repr__ + raise AssertionError + raise NotImplementedError + if __name__ == .__main__.: + if TYPE_CHECKING: + @abstract + +[html] +directory = htmlcov diff --git a/backend/.env.prod b/backend/.env.prod new file mode 100644 index 0000000..60e0f31 --- /dev/null +++ b/backend/.env.prod @@ -0,0 +1,69 @@ +# PASSWORD_CRYPTO +CRYPTO_SECRET_KEY='UJMSDFliLZ9a6iPPKNgAuc_SAy4ISUmcwvbF0pS8XLs=' + +# DATABASE +DATABASE_USERNAME=postgres +DATABASE_PASSWORD='aio2o0656)^%^' +DATABASE_ADDRESS=host.docker.internal:5432 +DATABASE_NAME=o2sound_v2 +DATABASE_AUTO_COMMIT=false +DATABASE_AUTO_FLUSH=false + +# REDIS +REDIS_HOST=host.docker.internal +REDIS_PORT=6379 +REDIS_DB=0 + +# JWT +ACCESS_TOKEN_EXPIRE_MINUTES=30 +REFRESH_TOKEN_EXPIRE_MINUTES=43200 +JWT_SECRET_KEY=k0zH/43LgVhcJxCWWuiin4Y1rL4KT/KubHjqEPTsbLk +JWT_ALGORITHM=HS256 + +# KAKAO +KAKAO_REST_API_KEY=1b1aca55e745402a078c352cc4113f97 +KAKAO_CLIENT_SECRET_KEY=l9dkPEEfvB1CNs1OgrgniBMeBKOKeJ5N +KAKAO_REDIRECT_URL=http://localhost:8000/social_auth/kakao/callback + +# google +GOOGLE_CLIENT_ID=853325574184-c6bclkpmk4ja7pn99qjivclf4rdobost.apps.googleusercontent.com +GOOGLE_CLIENT_SECRET=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 +GOOGLE_API_KEY=AIzaSyA7ULe-vQqCif6qTsiy7swgP5rRQBrCWhI + +# SESSION +SESSION_SECRET_KEY=5c628fc0f9534c5ebd6f124ffb205ed868e520a948a3db8b74d5d2bc8c18db99 + +#OPENAI +OPENAI_API_KEY=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 + +# KTOPENAI +OPENAI_MIDM_BASE_URL="http://114.110.128.184:30345/v1" +OPENAI_MEDIM_API_KEY=dummy +OPENAI_MEDIM_MODEL="K-intelligence/Midm-2.0-Base-Instruct" + +# Mureka +USEAPINET_API_TOKEN=user:1542-uKNtvnwqG9ZCkgeADlmnE +MUREAKA_SESSION_TOKEN=Ffgon6czYjg2EG9589qCfrLjJcLR2n6L +MUREAKA_USER_ID=58620752494593 + +# Mureka API ( fix ) +MUSREKA_API_KEYS=op_mha4y17fL6SZztN4LYB1gsi1t3Fh6E7 + +# API +API_V1_STR='/api' +PROJECT_NAME='o2sound_v2 backend' + + +# Celery +CELERY_BROKER_URL=redis://localhost:6379/1 +CELERY_RESULT_BACKEND=redis://localhost:6379/2 + +# RedisManager +RedisManager_URL="redis://host.docker.internal:6379" + +# myBaseUrl +CURRENT_BACKEND_URL="https://o2sound-api.o2o.kr" + +# AI Server +AI_SERVER_URL="https://proxy3.aitrain.ktcloud.com:10259/" +AI_SERVER_COOKIE_VALUE='appproxy_permit="YWRjMjczNjQ5OWRhMDc1MDU2MzQ1MTdmYmYxY2FkMjRlOTZmODFmMjdmODE0YThkOWUyODFjYzE0NTkwMmIzYw=="' \ No newline at end of file diff --git a/backend/.env.test b/backend/.env.test new file mode 100644 index 0000000..61056a8 --- /dev/null +++ b/backend/.env.test @@ -0,0 +1,54 @@ +# Test Database Configuration +TEST_DATABASE_URL=postgresql://postgres:postgres@localhost:5432/test_o2sound + +# Test Environment - local로 설정 +ENVIRONMENT=local +BE_ENV=local + +# Redis (for testing) +REDIS_URL=redis://localhost:6379/1 + +# JWT Secret (for testing) +JWT_SECRET_KEY=test_secret_key_for_testing_only +JWT_ALGORITHM=HS256 +JWT_EXPIRATION_MINUTES=60 + +# Application Settings (for testing) +PROJECT_NAME=O2Sound Test +VERSION=1.0.0 +DEBUG=True + +# Celery (for testing) +CELERY_BROKER_URL=redis://localhost:6379/1 +CELERY_RESULT_BACKEND=redis://localhost:6379/1 + +# Google OAuth (for testing - use dummy values) +GOOGLE_CLIENT_ID=test_google_client_id +GOOGLE_CLIENT_SECRET=test_google_client_secret +GOOGLE_REDIRECT_URI=http://localhost:8000/api/v1/social/google/callback + +# 테스트용 환경변수 (LocalConfig에 필요한 필드들) +CRYPTO_SECRET_KEY=test_crypto_secret_key +DATABASE_USERNAME=postgres +DATABASE_PASSWORD=postgres +DATABASE_ADDRESS=localhost:5432 +DATABASE_NAME=test_o2sound +DATABASE_AUTO_COMMIT=true +DATABASE_AUTO_FLUSH=true +REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_DB=1 +KAKAO_REST_API_KEY=test_kakao_api_key +KAKAO_CLIENT_SECRET_KEY=test_kakao_secret +KAKAO_REDIRECT_URL=http://localhost:8000/kakao/callback +GOOGLE_CLIENT_ID=test_google_client_id +GOOGLE_CLIENT_SECRET=test_google_client_secret +GOOGLE_API_KEY=test_google_api_key +SESSION_SECRET_KEY=test_session_secret +OPENAI_API_KEY=test_openai_api_key +USEAPINET_API_TOKEN=test_useapi_token +MUREAKA_SESSION_TOKEN=test_mureaka_session +MUREAKA_USER_ID=test_mureaka_user +API_V1_STR=/api/v1 +PROJECT_NAME=O2Sound Test +RedisManager_URL=redis://localhost:6379/1 diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..0486353 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,16 @@ +.venv +__pycache__/ +*.py[cod] +/uploads/* +!/uploads/.gitkeep +/crawling_results/* +!/crawling_results/.gitkeep +/success_log/* +!/success_log/.gitkeep +/ai/* +!/ai/.gitkeep +__pycache__/ +*.pyc +*.pyo +*.local +.env.local \ No newline at end of file diff --git a/backend/.python-version b/backend/.python-version new file mode 100644 index 0000000..56bb660 --- /dev/null +++ b/backend/.python-version @@ -0,0 +1 @@ +3.12.7 diff --git a/backend/8689fbd4-ad32-4def-b698-4e96611390d2_generated_videoTEMP_MPY_wvf_snd.mp4 b/backend/8689fbd4-ad32-4def-b698-4e96611390d2_generated_videoTEMP_MPY_wvf_snd.mp4 new file mode 100644 index 0000000..4682e12 Binary files /dev/null and b/backend/8689fbd4-ad32-4def-b698-4e96611390d2_generated_videoTEMP_MPY_wvf_snd.mp4 differ diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..24a921f --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,66 @@ +# Python 3.10 slim 이미지 사용 (pyproject.toml에서 ^3.10 지정됨) +FROM python:3.12-slim + +# 작업 디렉토리 설정 +WORKDIR /app + +RUN mkdir -p /app/uploads + +# 시스템 패키지 업데이트 및 필요한 패키지 설치 +RUN apt-get update && apt-get install -y \ + curl \ + gnupg \ + wget \ + libgbm-dev \ + libnss3 \ + build-essential \ + libjpeg-dev \ + zlib1g-dev \ + libpng-dev \ + dos2unix \ + && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Poetry 설치 +RUN curl -sSL https://install.python-poetry.org | python3 - --version 1.8.3 + +# Google Chrome 설치 (최신 안정 버전) +RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | gpg --dearmor -o /usr/share/keyrings/googlechrome-linux-keyring.gpg \ + && echo "deb [arch=amd64 signed-by=/usr/share/keyrings/googlechrome-linux-keyring.gpg] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list \ + && apt-get update \ + && apt-get install -y google-chrome-stable \ + && rm -rf /var/lib/apt/lists/* + +# Chrome 환경 변수 설정 +ENV CHROME_BIN=/usr/bin/google-chrome + +# Poetry 경로 환경 변수 설정 +ENV PATH="/root/.local/bin:$PATH" +# Poetry 설정 - 가상환경 생성하지 않음 (컨테이너 내에서는 불필요) +RUN poetry config virtualenvs.create false + +# pyproject.toml과 poetry.lock을 복사 +COPY pyproject.toml poetry.lock* /app/ + +# 전역 패키지 설치 (로컬 가상환경이 아닌 글로벌 설치) +RUN poetry config virtualenvs.create false \ + && poetry install --no-root + +# client_secret.json 파일이 있는지 확인하고 권한 설정 +RUN if [ -f client_secret.json ]; then chmod 644 client_secret.json; fi + +# __pycache__ 디렉토리 정리 (빌드 시 생성될 수 있음) +RUN find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true + +# 전체 앱 코드 복사 +COPY . /app + +# run.sh를 복사하고 이름 변경 +COPY run-prod.sh /app/run-prod.sh +COPY run-celery.sh /app/run-celery.sh + +# 줄바꿈 문자 변환 및 실행 권한 부여 +RUN dos2unix /app/run-prod.sh /app/run-celery.sh \ + && chmod +x /app/run-prod.sh /app/run-celery.sh + +# 컨테이너 시작 시 스크립트 실행 +CMD ["/app/run-prod.sh"] \ No newline at end of file diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/core/celery_app.py b/backend/app/core/celery_app.py new file mode 100644 index 0000000..c2d409d --- /dev/null +++ b/backend/app/core/celery_app.py @@ -0,0 +1,22 @@ +from celery import Celery +from app.core.env_setting import EnvSetting + +settings = EnvSetting() + +celery_app = Celery( + "worker", + broker=settings.CELERY_BROKER_URL, + backend=settings.CELERY_RESULT_BACKEND, + include=["app.workers.tasks"] +) + +celery_app.conf.update( + task_serializer="json", + accept_content=["json"], + result_serializer="json", + timezone="UTC", + enable_utc=True, + task_track_started=True, + task_time_limit=30 * 60, + task_soft_time_limit=60, +) \ No newline at end of file diff --git a/backend/app/core/database.py b/backend/app/core/database.py new file mode 100644 index 0000000..59e5082 --- /dev/null +++ b/backend/app/core/database.py @@ -0,0 +1,181 @@ +from __future__ import annotations +from sqlalchemy import create_engine, Engine, text +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker, Session +from app.shared.logger import setup_logger +from typing import Generator +from sqlalchemy.orm import declarative_base +import abc +from app.core.env_setting import EnvSetting + + +logger = setup_logger(__name__) + +Base = declarative_base() + +settings = EnvSetting() + + +# def get_database_url() -> str: +# """PostgreSQL 데이터베이스 URL 생성""" +# return ( +# f"postgresql://{settings.DATABASE_USERNAME}:" +# f"{settings.DATABASE_PASSWORD}@{settings.DATABASE_ADDRESS}/" +# f"{settings.DATABASE_NAME}" +# ) + + + +def create_engine_by_env(): + db_url = get_database_url() + return create_engine(db_url, echo=True) # echo는 로그 확인용 + + +def get_session_local(): + engine = create_engine_by_env() + return sessionmaker(autocommit=False, autoflush=False, bind=engine) + +# engine = create_engine(settings.DATABASE_URL) +# SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +def get_db() -> Generator[Session, None, None]: + SessionLocal = get_session_local() + db = SessionLocal() + logger.debug("DB session created") + try: + yield db + except Exception as e: + db.rollback() + logger.error(f"DB session error: {str(e)}") + raise + finally: + db.close() + logger.debug("DB session closed") + + +def get_database_url() -> str: + """PostgreSQL 데이터베이스 URL 생성""" + return ( + f"postgresql://{settings.DATABASE_USERNAME}:" + f"{settings.DATABASE_PASSWORD}@{settings.DATABASE_ADDRESS}/" + f"{settings.DATABASE_NAME}" + ) + +def create_database_engine(): + """데이터베이스 엔진 생성""" + database_url = get_database_url() + engine = create_engine( + database_url, + echo=True if settings.ENVIRONMENT == "local" else False, # 로컬에서만 SQL 로그 출력 + pool_pre_ping=True, # 연결 상태 확인 + pool_recycle=3600, # 1시간마다 연결 재생성 + pool_size=10, # 연결 풀 크기 + max_overflow=20 # 최대 추가 연결 수 + ) + return engine + +# 엔진 인스턴스 +engine = create_database_engine() + +# 세션 팩토리 생성 +SessionLocal = sessionmaker( + bind=engine, + autocommit=settings.DATABASE_AUTO_COMMIT, + autoflush=settings.DATABASE_AUTO_FLUSH, + expire_on_commit=False +) + +# def get_db() -> Generator[Session, None, None]: +# """ +# 데이터베이스 세션 의존성 주입 함수 +# FastAPI Depends와 함께 사용 +# """ +# db = SessionLocal() +# try: +# yield db +# finally: +# db.close() + + +class DatabaseManager: + """데이터베이스 관리자 클래스""" + + @staticmethod + def get_session() -> Session: + """새로운 데이터베이스 세션 반환""" + return SessionLocal() + + @staticmethod + def close_session(db: Session) -> None: + """데이터베이스 세션 종료""" + db.close() + + @staticmethod + def commit_and_refresh(db: Session, instance) -> None: + """커밋 후 인스턴스 새로고침""" + db.commit() + db.refresh(instance) + + @staticmethod + def rollback(db: Session) -> None: + """롤백 실행""" + db.rollback() + +def create_session_maker(engine: Engine,settings): + return sessionmaker(autocommit=settings.DATABASE_AUTO_COMMIT, + autoflush=settings.DATABASE_AUTO_FLUSH, + bind=engine + ) + +def create_persistence_by_env(): + engine = create_engine_by_env() + session_maker = create_session_maker(engine, settings) + return engine, session_maker + + + +def create_tables(): + try: + engine = create_engine_by_env() + Base.metadata.create_all(bind=engine) + logger.info("✅ All tables created successfully.") + except Exception as e: + logger.error(f"❌ Failed to create tables: {str(e)}") + raise + + +# # 🔥 FK 무시하고 안전하게 전체 테이블 삭제 (PostgreSQL 전용) +# def drop_tables(): +# try: +# engine = create_engine_by_env() +# with engine.connect() as conn: +# conn.execute(text("SET session_replication_role = replica;")) +# BaseTable.metadata.drop_all(bind=engine) +# conn.execute(text("SET session_replication_role = DEFAULT;")) +# conn.commit() +# logger.warning("⚠️ All tables dropped successfully with FK constraints disabled temporarily.") +# except Exception as e: +# logger.error(f"❌ Failed to drop tables: {str(e)}") +# raise + +# # ======= + +class AbstractUnitOfWork(abc.ABC): + + def __enter__(self) -> AbstractUnitOfWork: + return self + + def __exit__(self, *args): + self.rollback() + + @abc.abstractmethod + def commit(self): + raise NotImplementedError + + @abc.abstractmethod + def rollback(self): + raise NotImplementedError + + @abc.abstractmethod + def flush(self): + raise NotImplementedError \ No newline at end of file diff --git a/backend/app/core/database/__init__.py b/backend/app/core/database/__init__.py new file mode 100644 index 0000000..6996bd6 --- /dev/null +++ b/backend/app/core/database/__init__.py @@ -0,0 +1,53 @@ +from app.core.database.connection import Base, engine, SessionLocal +from app.core.database.session import get_db, DatabaseManager +from sqlalchemy import text + +def create_tables(): + '''모든 데이터베이스 테이블 생성''' + Base.metadata.create_all(bind=engine) + +def drop_tables(): + '''모든 데이터베이스 테이블 삭제 (개발용)''' + print("🔄 강제 테이블 삭제 시작...") + try: + with engine.connect() as conn: + # 모든 테이블 조회 + result = conn.execute(text(""" + SELECT tablename FROM pg_tables + WHERE schemaname = 'public' + """)) + tables = [row[0] for row in result] + print(f"발견된 테이블: {tables}") + + # 각 테이블을 CASCADE로 삭제 + for table in tables: + conn.execute(text(f"DROP TABLE IF EXISTS {table} CASCADE")) + print(f"✅ {table} 테이블 삭제 완료") + + conn.commit() + print("✅ 모든 테이블 삭제 완료") + + except Exception as e: + print(f"❌ 테이블별 삭제 실패: {e}") + # 마지막 수단: 스키마 재생성 + try: + with engine.connect() as conn: + conn.execute(text("DROP SCHEMA public CASCADE")) + conn.execute(text("CREATE SCHEMA public")) + conn.execute(text("GRANT ALL ON SCHEMA public TO postgres")) + conn.execute(text("GRANT ALL ON SCHEMA public TO public")) + conn.commit() + print("✅ 스키마 재생성 완료") + except Exception as final_error: + print(f"❌ 스키마 재생성도 실패: {final_error}") + +# 패키지에서 내보낼 항목 +__all__ = [ + "Base", + "engine", + "SessionLocal", + "get_db", + "DatabaseManager", + "create_tables", + "drop_tables", +] \ No newline at end of file diff --git a/backend/app/core/database/connection.py b/backend/app/core/database/connection.py new file mode 100644 index 0000000..89818bd --- /dev/null +++ b/backend/app/core/database/connection.py @@ -0,0 +1,40 @@ +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from sqlalchemy.ext.declarative import declarative_base +from app.core.env_setting import EnvSetting + + +Base = declarative_base() +settings = EnvSetting() + +def get_database_url() -> str: + """PostgreSQL 데이터베이스 URL 생성""" + return ( + f"postgresql://{settings.DATABASE_USERNAME}:" + f"{settings.DATABASE_PASSWORD}@{settings.DATABASE_ADDRESS}/" + f"{settings.DATABASE_NAME}" + ) + +def create_database_engine(): + """데이터베이스 엔진 생성""" + database_url = get_database_url() + engine = create_engine( + database_url, + echo=True if settings.ENVIRONMENT == "local" else False, # 로컬에서만 SQL 로그 출력 + pool_pre_ping=True, # 연결 상태 확인 + pool_recycle=3600, # 1시간마다 연결 재생성 + pool_size=10, # 연결 풀 크기 + max_overflow=20 # 최대 추가 연결 수 + ) + return engine + +# 엔진 인스턴스 +engine = create_database_engine() + +# 세션 팩토리 생성 +SessionLocal = sessionmaker( + bind=engine, + autocommit=settings.DATABASE_AUTO_COMMIT, + autoflush=settings.DATABASE_AUTO_FLUSH, + expire_on_commit=False +) \ No newline at end of file diff --git a/backend/app/core/database/session.py b/backend/app/core/database/session.py new file mode 100644 index 0000000..147eeeb --- /dev/null +++ b/backend/app/core/database/session.py @@ -0,0 +1,38 @@ +from typing import Generator +from sqlalchemy.orm import Session +from app.core.database.connection import SessionLocal + +def get_db() -> Generator[Session, None, None]: + """ + 데이터베이스 세션 의존성 주입 함수 + FastAPI Depends와 함께 사용 + """ + db = SessionLocal() + try: + yield db + finally: + db.close() + +class DatabaseManager: + """데이터베이스 관리자 클래스""" + + @staticmethod + def get_session() -> Session: + """새로운 데이터베이스 세션 반환""" + return SessionLocal() + + @staticmethod + def close_session(db: Session) -> None: + """데이터베이스 세션 종료""" + db.close() + + @staticmethod + def commit_and_refresh(db: Session, instance) -> None: + """커밋 후 인스턴스 새로고침""" + db.commit() + db.refresh(instance) + + @staticmethod + def rollback(db: Session) -> None: + """롤백 실행""" + db.rollback() \ No newline at end of file diff --git a/backend/app/core/env_setting.py b/backend/app/core/env_setting.py new file mode 100644 index 0000000..a1ba333 --- /dev/null +++ b/backend/app/core/env_setting.py @@ -0,0 +1,171 @@ +import os +from typing import Literal +from pydantic import Field +from pydantic_settings import BaseSettings, SettingsConfigDict +from functools import lru_cache + +class BaseConfig(BaseSettings): + '''기본 설정 클래스''' + + ENVIRONMENT: Literal["local", "prod"] = "local" + + # CRYPTO 설정 + CRYPTO_SECRET_KEY: str = Field(..., description="CRYPTO 시크릿 키") + + # DATABASE 설정 + DATABASE_USERNAME: str = Field(..., description="데이터베이스 사용자 이름") + DATABASE_PASSWORD: str = Field(..., description="데이터베이스 비밀번호") + DATABASE_ADDRESS: str = Field(..., description="데이터베이스 주소") + DATABASE_NAME: str = Field(..., description="데이터베이스 이름") + DATABASE_AUTO_COMMIT: bool = Field(default=True, description="데이터베이스 자동 커밋 여부") + DATABASE_AUTO_FLUSH: bool = Field(default=True, description="데이터베이스 자동 플러시 여부") + + # REDIS 설정 + REDIS_HOST: str = Field(..., description="Redis 호스트") + REDIS_PORT: int = Field(..., description="Redis 포트") + REDIS_DB: int = Field(default=0, description="Redis 데이터베이스 번호") + + # JWT 설정 + ACCESS_TOKEN_EXPIRE_MINUTES: int = Field(default=30, description="액세스 토큰 만료 시간(분)") + REFRESH_TOKEN_EXPIRE_MINUTES: int = Field(default=43200, description="리프레시 토큰 만료 시간(분)") # 30일 + JWT_SECRET_KEY: str = Field(..., description="JWT 시크릿 키") + JWT_ALGORITHM: str = "HS256" + + # KAKAO 설정 + KAKAO_REST_API_KEY: str = Field(..., description="카카오 REST API 키") + KAKAO_CLIENT_SECRET_KEY: str = Field(..., description="카카오 클라이언트 시크릿 키") + KAKAO_REDIRECT_URL: str = Field(..., description="카카오 리다이렉트 URL") + + # GOOGLE 설정 + GOOGLE_CLIENT_ID: str = Field(..., description="구글 클라이언트 ID") + GOOGLE_CLIENT_SECRET: str = Field(..., description="구글 클라이언트 시크릿 키") + GOOGLE_API_KEY: str = Field(..., description="구글 API 키") + + # SESSION + SESSION_SECRET_KEY: str = Field(..., description="유튜브 세션 시크릿 키") + + # OpenAI 설정 + OPENAI_API_KEY: str = Field(..., description="OpenAI API 키") + + # KTOPENAI 설정 + OPENAI_MIDM_BASE_URL: str = Field(..., description="KT OPENAI 베이스 URL") + OPENAI_MEDIM_API_KEY: str = Field(..., description="KT OPENAI API 키") + OPENAI_MEDIM_MODEL: str = Field(..., description="KT OPENAI 모델") + + # Mureka 설정 + USEAPINET_API_TOKEN: str = Field(..., description="USEAPI 토큰") + MUREAKA_SESSION_TOKEN: str = Field(..., description="Mureka 세션 아이디") + MUREAKA_USER_ID: str = Field(..., description="Mureka 유저 아이디") + + # Mureka API ( fix ) + MUSREKA_API_KEYS: str = Field(..., description="Mureka API 키") + + # API + API_V1_STR: str = Field(..., description="API명") + PROJECT_NAME: str = Field(..., description="프로젝트 이름") + + # Celery + CELERY_BROKER_URL: str = Field(..., description="CELERY BROKER URL") + CELERY_RESULT_BACKEND: str = Field(..., description="CERLERY RESULT BACK") + + # RedisManager + RedisManager_URL: str = Field(..., description="RedisManager URL") + + # myBaseUrl + CURRENT_BACKEND_URL: str = Field(..., description="현재 백엔드 URL") + + # AI Server + AI_SERVER_URL: str = Field(..., description="AI Server URL") + AI_SERVER_COOKIE_VALUE: str = Field(..., description="AI Server Cookie Value") + + @property + def is_production(self) -> bool: + return self.ENVIRONMENT == "prod" + + @property + def is_local(self) -> bool: + return self.ENVIRONMENT == "local" + + @property + def db_url(self) -> str: + return f"postgresql://{self.DB_USER}:{self.DB_PASSWORD}@{self.DB_ADDRESS}/{self.DB_NAME}" + + @property + def redis_url(self) -> str: + return f"redis://{self.REDIS_HOST}:{self.REDIS_PORT}/{self.REDIS_DB}" + +class LocalConfig(BaseConfig): + ENVIRONMENT: Literal["local", "prod"] = "local" + DB_ECHO: bool = True # SQL 쿼리 출력 O + + model_config = SettingsConfigDict( + env_file=".env.local", + env_file_encoding="utf-8", + case_sensitive=True, + extra="ignore" + ) + +class ProdConfig(BaseConfig): + ENVIRONMENT: Literal["local", "prod"] = "prod" + DB_ECHO: bool = False # SQL 쿼리 출력 X + + model_config = SettingsConfigDict( + env_file=".env.prod", + env_file_encoding="utf-8", + case_sensitive=True, + extra="ignore" + ) + + +@lru_cache() +def get_config() -> BaseConfig: + '''환경에 따라 적절한 설정 반환 (캐싱됨)''' + env = os.getenv("BE_ENV", "local").lower() + + if env == "prod": + return ProdConfig() + elif env == "local": + return LocalConfig() + else: + print(f"Warning: Unknown environment '{env}', using local config") + return LocalConfig() + + +def EnvSetting() -> BaseConfig: + return get_config() + + +# class LocalConfig(BaseConfig): +# '''로컬 설정 클래스''' + +# model_config = SettingsConfigDict( +# env_file=".env.local", +# env_file_encoding="utf-8", +# case_sensitive=True, +# extra="ignore" +# ) + +# class ProdConfig(BaseConfig): +# '''프로덕션 설정 클래스''' + +# model_config = SettingsConfigDict( +# env_file=".env.prod", +# env_file_encoding="utf-8", +# case_sensitive=True, +# extra="ignore" +# ) + + +# def get_config() -> BaseConfig: +# '''환경에 따라 적절한 설정 반환 (캐싱됨)''' +# env = os.getenv("BE_ENV", "local").lower() +# if env == "prod": +# return ProdConfig() +# elif env == "local": +# return LocalConfig() +# else: +# # 기본값은 local +# print(f"Warning: Unknown environment '{env}', using local config") +# return LocalConfig() + +# settings = get_config() \ No newline at end of file diff --git a/backend/app/core/oauth_setting.py b/backend/app/core/oauth_setting.py new file mode 100644 index 0000000..8f4a1d2 --- /dev/null +++ b/backend/app/core/oauth_setting.py @@ -0,0 +1,49 @@ +import json +from pathlib import Path +from authlib.integrations.starlette_client import OAuth + +def get_oauth_client(): + """OAuth 클라이언트 생성 및 반환""" + oauth = OAuth() + + try: + # client_secret.json 파일 로드 + client_secret_path = Path("client_secret.json") + + if not client_secret_path.exists(): + raise FileNotFoundError("client_secret.json 파일을 찾을 수 없습니다.") + + with open(client_secret_path, "r", encoding="utf-8") as f: + client_secrets = json.load(f) + + # Google OAuth 클라이언트 정보 추출 + if 'web' in client_secrets: + web_config = client_secrets['web'] + elif 'installed' in client_secrets: + web_config = client_secrets['installed'] + else: + raise ValueError("client_secret.json 파일 형식이 올바르지 않습니다.") + + client_id = web_config.get('client_id') + client_secret = web_config.get('client_secret') + + if not client_id or not client_secret: + raise ValueError("client_secret.json에서 client_id 또는 client_secret을 찾을 수 없습니다.") + + # OAuth 클라이언트 등록 + oauth.register( + name='google', + client_id=client_id, + client_secret=client_secret, + server_metadata_url='https://accounts.google.com/.well-known/openid-configuration', + client_kwargs={ + 'scope': 'openid email profile' + } + ) + + print("✅ Google OAuth 클라이언트 설정 완료") + return oauth + + except Exception as e: + print(f"❌ Google OAuth 설정 실패: {e}") + raise e \ No newline at end of file diff --git a/backend/app/core/redis/__init__.py b/backend/app/core/redis/__init__.py new file mode 100644 index 0000000..7ec4a40 --- /dev/null +++ b/backend/app/core/redis/__init__.py @@ -0,0 +1,34 @@ +from .redis_manager import RedisManager +from .oauth_storage import GoogleOAuthStorage + +# 전역 Redis 매니저 인스턴스 +redis_manager = RedisManager() +google_storage = GoogleOAuthStorage(redis_manager) + +async def get_redis_manager() -> RedisManager: + """Redis 매니저 의존성""" + return redis_manager + +async def get_google_storage() -> GoogleOAuthStorage: + """Google 저장소 의존성""" + return google_storage + +# 하위 호환성을 위한 별칭 +async def get_oauth_storage() -> GoogleOAuthStorage: + """OAuth 저장소 의존성 (하위 호환성)""" + return google_storage + +async def get_youtube_storage() -> GoogleOAuthStorage: + """YouTube 저장소 의존성 (하위 호환성)""" + return google_storage + +__all__ = [ + "RedisManager", + "GoogleOAuthStorage", + "redis_manager", + "google_storage", + "get_redis_manager", + "get_google_storage", + "get_oauth_storage", # 하위 호환성 + "get_youtube_storage", # 하위 호환성 +] \ No newline at end of file diff --git a/backend/app/core/redis/oauth_storage.py b/backend/app/core/redis/oauth_storage.py new file mode 100644 index 0000000..a0b9f93 --- /dev/null +++ b/backend/app/core/redis/oauth_storage.py @@ -0,0 +1,97 @@ +import json +from typing import Optional, Dict, Any +from .redis_manager import RedisManager + +class GoogleOAuthStorage: + """Google OAuth 전용 Redis 저장소""" + + def __init__(self, redis_manager: RedisManager): + self.redis_manager = redis_manager + + # ============== state 관리 ============== + async def store_oauth_state(self, state: str, state_data: Dict[str, Any], ttl: int = 300): + """OAuth state 저장 (기본 5분 TTL)""" + try: + client = await self.redis_manager.get_client() + await client.set( + f"google_oauth_state:{state}", + json.dumps(state_data, ensure_ascii=False), + ex=ttl + ) + print(f"✅ OAuth state 저장 완료: {state[:8]}...") + except Exception as e: + print(f"❌ OAuth state 저장 실패: {e}") + raise e + + async def get_oauth_state(self, state: str) -> Optional[Dict[str, Any]]: + """OAuth state 조회""" + try: + client = await self.redis_manager.get_client() + data = await client.get(f"google_oauth_state:{state}") + if data: + result = json.loads(data) + print(f"✅ OAuth state 조회 완료: {state[:8]}...") + return result + else: + print(f"⚠️ OAuth state 없음: {state[:8]}...") + return None + except Exception as e: + print(f"❌ OAuth state 조회 실패: {e}") + return None + + async def delete_oauth_state(self, state: str): + """OAuth state 삭제""" + try: + client = await self.redis_manager.get_client() + result = await client.delete(f"google_oauth_state:{state}") + if result: + print(f"✅ OAuth state 삭제 완료: {state[:8]}...") + else: + print(f"⚠️ 삭제할 OAuth state 없음: {state[:8]}...") + except Exception as e: + print(f"❌ OAuth state 삭제 실패: {e}") + + + # ================ token 관리 ============== + # front로 정보들을 넘겨주기 위한 token + async def store_temp_token(self, temp_token_id: str, token_data: Dict[str, Any], ttl: int = 300): + """임시 토큰 저장 (기본 5분 TTL)""" + try: + client = await self.redis_manager.get_client() + await client.set( + f"google_temp_token:{temp_token_id}", + json.dumps(token_data, ensure_ascii=False), + ex=ttl + ) + print(f"✅ 임시 토큰 저장 완료: {temp_token_id[:8]}...") + except Exception as e: + print(f"❌ 임시 토큰 저장 실패: {e}") + raise e + + async def get_temp_token(self, temp_token_id: str) -> Optional[Dict[str, Any]]: + """임시 토큰 조회""" + try: + client = await self.redis_manager.get_client() + data = await client.get(f"google_temp_token:{temp_token_id}") + if data: + result = json.loads(data) + print(f"✅ 임시 토큰 조회 완료: {temp_token_id[:8]}...") + return result + else: + print(f"⚠️ 임시 토큰 없음: {temp_token_id[:8]}...") + return None + except Exception as e: + print(f"❌ 임시 토큰 조회 실패: {e}") + return None + + async def delete_temp_token(self, temp_token_id: str): + """임시 토큰 삭제""" + try: + client = await self.redis_manager.get_client() + result = await client.delete(f"google_temp_token:{temp_token_id}") + if result: + print(f"✅ 임시 토큰 삭제 완료: {temp_token_id[:8]}...") + else: + print(f"⚠️ 삭제할 임시 토큰 없음: {temp_token_id[:8]}...") + except Exception as e: + print(f"❌ 임시 토큰 삭제 실패: {e}") \ No newline at end of file diff --git a/backend/app/core/redis/redis.py b/backend/app/core/redis/redis.py new file mode 100644 index 0000000..048589f --- /dev/null +++ b/backend/app/core/redis/redis.py @@ -0,0 +1,12 @@ +import redis.asyncio as redis +from app.core.env_setting import EnvSetting + +settings = EnvSetting() +env = settings + +redis_client = redis.Redis( + host=env.REDIS_HOST, + port=env.REDIS_PORT, + db=env.REDIS_DB, + decode_responses=True +) \ No newline at end of file diff --git a/backend/app/core/redis/redis_manager.py b/backend/app/core/redis/redis_manager.py new file mode 100644 index 0000000..0c67915 --- /dev/null +++ b/backend/app/core/redis/redis_manager.py @@ -0,0 +1,64 @@ +import redis +import redis.asyncio as async_redis +from typing import Optional +from app.core.env_setting import EnvSetting + +settings = EnvSetting() + +class RedisManager: + def __init__(self, redis_url: Optional[str] = None): + self.redis_url = redis_url or settings.RedisManager_URL + self.async_client: Optional[async_redis.Redis] = None + self.sync_client: Optional[redis.Redis] = None + + # 비동기 클라이언트 메서드들 (기존) + async def connect(self): + """Redis 비동기 연결""" + if self.async_client is None: + self.async_client = async_redis.from_url( + self.redis_url, + encoding="utf-8", + decode_responses=True, + max_connections=20, + socket_connect_timeout=5, + socket_timeout=5, + ) + return self.async_client + + async def close(self): + """Redis 비동기 연결 종료""" + if self.async_client: + await self.async_client.close() + self.async_client = None + + async def get_client(self) -> async_redis.Redis: + """Redis 비동기 클라이언트 반환""" + if self.async_client is None: + await self.connect() + return self.async_client + + # 동기 클라이언트 메서드들 (새로 추가) ( celery 전용 ) + def connect_sync(self): + """Redis 동기 연결""" + if self.sync_client is None: + self.sync_client = redis.from_url( + self.redis_url, + encoding="utf-8", + decode_responses=True, + max_connections=20, + socket_connect_timeout=5, + socket_timeout=5, + ) + return self.sync_client + + def close_sync(self): + """Redis 동기 연결 종료""" + if self.sync_client: + self.sync_client.close() + self.sync_client = None + + def get_sync_client(self) -> redis.Redis: + """Redis 동기 클라이언트 반환""" + if self.sync_client is None: + self.connect_sync() + return self.sync_client \ No newline at end of file diff --git a/backend/app/dependencies.py b/backend/app/dependencies.py new file mode 100644 index 0000000..1794253 --- /dev/null +++ b/backend/app/dependencies.py @@ -0,0 +1,36 @@ +from fastapi import Depends +from app.core.database import get_db +from sqlalchemy.orm import Session +from app.services.auth_service import AuthService +from app.services.social.google_service import GoogleService +from app.core.redis import redis_manager # 기존 인스턴스 +from app.services.user_service import UserService +from app.services.order_service import OrderService +from app.services.video_service import VideoService +from app.services.get_my_video_result_service import GetMyVideoResultService + +def get_auth_service(db: Session = Depends(get_db)) -> AuthService: + """AuthService 의존성 주입""" + return AuthService(db) + +def get_google_service() -> GoogleService: + """Google OAuth 서비스 의존성 주입""" + return GoogleService(redis_manager) + + +def get_user_service(db: Session = Depends(get_db)) -> UserService: + """UserService 의존성 주입""" + return UserService(db) + + +def get_order_service(db: Session = Depends(get_db)) -> OrderService: + """OrderService 의존성 주입""" + return OrderService(db) + +def get_video_service(db: Session = Depends(get_db)) -> VideoService: + """VideoService 의존성 주입""" + return VideoService(db) + +def get_get_my_video_result_service(db: Session = Depends(get_db)) -> GetMyVideoResultService: + '''GetMyVideoResultService 의존성 주입''' + return GetMyVideoResultService(db) \ No newline at end of file diff --git a/backend/app/domain/enums/mureka_enum.py b/backend/app/domain/enums/mureka_enum.py new file mode 100644 index 0000000..d215f18 --- /dev/null +++ b/backend/app/domain/enums/mureka_enum.py @@ -0,0 +1,66 @@ +from enum import Enum + +class MureakaGenres(str, Enum): + POP = "pop" + RNB = "r&b" + ROCK = "rock" + DISCO = "disco" + ELECTRONIC = "electronic" + FOLK = "folk" + FUNK = "funk" + COUNTRY = "country" + HIPHOP = "hip hop" + JAZZ = "jazz" + LATIN = "latin" + METAL = "metal" + BLUES = "blues" + PUNK = "punk" + REGGAE = "reggae" + SOUL = "soul" + INDIAN = "indian" + AFROBEAT = "afrobeat" + WORLD_MUSIC = "world music" + INDIE = "indie" + CLASSICAL = "classical" + EXPERIMENTAL = "experimental" + CHILDREN = "children" + DANCE = "dance" + SYNTHPOP_80S = "80s synthpop" + OLD_SCHOOL_RAP_89S = "89's old school rap" + DEATH_METAL = "death metal" + ALTERNATIVE_ROCK = "lternative rock" + JPOP = "J-pop" + EDM = "EDM" + GRITTY_MELODY = "gritty melody" + KPOP = "K-pop" + BALLAD = "ballad" + TIP_HOP = "tip-hop" + NEW_WAVE = "new wave" + ORCHESTRAL = "orchestral" + GAME_MUSIC = "game music" + SWING = "swing" + +class MureakaMoods(str, Enum): + RELAXED = "relaxed" + ANGRY = "angry" + HAPPY = "happy" + ENERGETIC = "energetic" + SAD = "sad" + CALM = "calm" + INSPIRED = "inspired" + MYSTERIOUS = "mysterious" + MAJESTIC = "majestic" + QUIRKY = "quirky" + RESTLESS = "restless" + ROMANTIC = "romantic" + DARK = "dark" + WARM = "warm" + PASSIONATE = "passionate" + JOYFUL = "joyful" + EMOTIONAL = "emotional" + DARK_AMBIENT = "dark ambient" + EERIE = "eerie" + DREAMY = "dreamy" + MELANCHOLIC = "melancholic" + CHILL = "chill" + EMO = "emo" \ No newline at end of file diff --git a/backend/app/domain/models/__init__.py b/backend/app/domain/models/__init__.py new file mode 100644 index 0000000..6ffe1de --- /dev/null +++ b/backend/app/domain/models/__init__.py @@ -0,0 +1,19 @@ +from .user import User +from .item import Item +from .order import Order +from .video import Video +from .music import Music +from .photo import Photo +from .upload import Upload +from .channel import Channel + +__all__ = [ + "User", + "Item", + "Order", + "Video", + "Music", + "Photo", + "Upload", + "Channel", +] \ No newline at end of file diff --git a/backend/app/domain/models/base.py b/backend/app/domain/models/base.py new file mode 100644 index 0000000..76586c4 --- /dev/null +++ b/backend/app/domain/models/base.py @@ -0,0 +1,26 @@ +import uuid +from datetime import datetime, timezone +from sqlalchemy import Column, DateTime, Boolean, func +from sqlalchemy.dialects.postgresql import UUID +from app.core.database import Base + +class BaseModel(Base): + '''기본 모델 클래스''' + __abstract__ = True + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, unique=True, nullable=False) + created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False) + deleted = Column(Boolean, default=False, nullable=False) + + def soft_delete(self): + '''소프트 삭제''' + self.deleted = True + self.updated_at = datetime.now(timezone.utc) + + def to_dict(self) -> dict: + '''모델을 딕셔너리로 변환''' + return { + column.name: getattr(self, column.name) + for column in self.__table__.columns + } \ No newline at end of file diff --git a/backend/app/domain/models/channel.py b/backend/app/domain/models/channel.py new file mode 100644 index 0000000..3a73de0 --- /dev/null +++ b/backend/app/domain/models/channel.py @@ -0,0 +1,19 @@ +from sqlalchemy import Column, String, ForeignKey +from sqlalchemy.orm import relationship +from app.domain.models.base import BaseModel +from sqlalchemy.dialects.postgresql import UUID + +class Channel(BaseModel): + '''채널 모델''' + __tablename__ = "channels" + + # 외래키 추가 (BaseModel의 id를 참조) + user_id = Column(UUID(as_uuid=True), ForeignKey('users.id'), nullable=False) + + channel_id = Column(String, nullable=False, unique=True) + name = Column(String, nullable=False) + custom_url = Column(String, nullable=True) + platform = Column(String, nullable=False, default="youtube") + + # 관계 설정 ( channels --- N:1 --- users ) + user = relationship("User", back_populates="channels") \ No newline at end of file diff --git a/backend/app/domain/models/item.py b/backend/app/domain/models/item.py new file mode 100644 index 0000000..05274b7 --- /dev/null +++ b/backend/app/domain/models/item.py @@ -0,0 +1,26 @@ +from sqlalchemy.orm import relationship +from sqlalchemy import Column, String, ForeignKey +from sqlalchemy.dialects.postgresql import UUID +from app.domain.models.base import BaseModel +from sqlalchemy.dialects.postgresql import ARRAY + +class Item(BaseModel): + '''업체 모델''' + __tablename__ = "items" + + # 외래키 - users 테이블의 id 참조 + user_id = Column(UUID(as_uuid=True), ForeignKey('users.id'), nullable=False) + + # 업체 정보 + name = Column(String, nullable=False) + address = Column(String, nullable=False) + url = Column(String, nullable=False) + phone_number = Column(String, nullable=True) + thumbnail_url = Column(String, nullable=True) + hashtags = Column(ARRAY(String(30)), nullable=True) + description = Column(String, nullable=True) + # 관계 설정 + # ( items --- N:1 --- users ) + # ( items --- 1:N --- orders ) + user = relationship("User", back_populates="items") + orders = relationship("Order", back_populates="item", cascade="all, delete-orphan") \ No newline at end of file diff --git a/backend/app/domain/models/music.py b/backend/app/domain/models/music.py new file mode 100644 index 0000000..432bc3b --- /dev/null +++ b/backend/app/domain/models/music.py @@ -0,0 +1,26 @@ +from sqlalchemy import Column, String, ForeignKey, Boolean, Integer +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import relationship +from app.domain.models.base import BaseModel + +class Music(BaseModel): + '''음악 모델''' + __tablename__ = "musics" + + # 외래키 + order_id = Column(UUID(as_uuid=True), ForeignKey('orders.id'), nullable=False) + + index = Column(Integer, nullable=False) # 번호 + is_selected = Column(Boolean, nullable=False) # 선택 여부 + + title = Column(String, nullable=False) # 제목 + url = Column(String, nullable=False) # 주소 + duration = Column(Integer, nullable=False) # 재생 시간 + lyrics = Column(String, nullable=False) # 가사 + + # 관계 설정 + # ( musics --- N:1 --- orders ) + order = relationship("Order", back_populates="musics") + + # ( musics --- 1:1 --- videos ) + video = relationship("Video", back_populates="music", uselist=False, cascade="all, delete-orphan") diff --git a/backend/app/domain/models/order.py b/backend/app/domain/models/order.py new file mode 100644 index 0000000..7b56273 --- /dev/null +++ b/backend/app/domain/models/order.py @@ -0,0 +1,27 @@ +from sqlalchemy import Column, String, ForeignKey +from sqlalchemy.orm import relationship +from sqlalchemy.dialects.postgresql import UUID +from app.domain.models.base import BaseModel + +class Order(BaseModel): + '''주문 모델''' + __tablename__ = "orders" + + # 외래키들 + user_id = Column(UUID(as_uuid=True), ForeignKey('users.id'), nullable=False) + item_id = Column(UUID(as_uuid=True), ForeignKey('items.id'), nullable=True) + + # 주문 정보 + status = Column(String, nullable=False) + # 관계 설정 + # ( orders --- N:1 --- users ) + # ( orders --- N:1 --- items ) + user = relationship("User", back_populates="orders") + item = relationship("Item", back_populates="orders") + + # ( orders --- 1:N --- videos ) + # ( orders --- 1:N --- musics ) + # ( orders --- 1:N --- photos ) + videos = relationship("Video", back_populates="order", cascade="all, delete-orphan") + musics = relationship("Music", back_populates="order", cascade="all, delete-orphan") + photos = relationship("Photo", back_populates="order", cascade="all, delete-orphan") diff --git a/backend/app/domain/models/photo.py b/backend/app/domain/models/photo.py new file mode 100644 index 0000000..66ad3e2 --- /dev/null +++ b/backend/app/domain/models/photo.py @@ -0,0 +1,24 @@ +from sqlalchemy import Column, String, ForeignKey, Boolean, Integer +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import relationship +from app.domain.models.base import BaseModel + +class Photo(BaseModel): + '''사진 모델''' + __tablename__ = "photos" + + # 외래키 + order_id = Column(UUID(as_uuid=True), ForeignKey('orders.id'), nullable=False) + video_id = Column(UUID(as_uuid=True), ForeignKey('videos.id'), nullable=True) + + # 사진 정보 + name = Column(String, nullable=False) # 이름 + url = Column(String, nullable=False) # 주소 + video_index = Column(Integer, nullable=True) # 비디오 인덱스 ( 비디오에서의 순서 ) + is_selected = Column(Boolean, nullable=False) # 선택 여부 + + # 관계 설정 + # ( photos --- N:1 --- orders ) + # ( photos --- N:1 --- videos ) + order = relationship("Order", back_populates="photos") + video = relationship("Video", back_populates="photos") \ No newline at end of file diff --git a/backend/app/domain/models/upload.py b/backend/app/domain/models/upload.py new file mode 100644 index 0000000..5acb852 --- /dev/null +++ b/backend/app/domain/models/upload.py @@ -0,0 +1,30 @@ + +from sqlalchemy import Column, String, ForeignKey +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import relationship +from app.domain.models.base import BaseModel +from sqlalchemy.dialects.postgresql import ARRAY + +from sqlalchemy import Column, String, ForeignKey +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import relationship +from app.domain.models.base import BaseModel +from sqlalchemy.dialects.postgresql import ARRAY + +class Upload(BaseModel): + '''업로드 모델''' + __tablename__ = "uploads" + + # 외래키 + video_id = Column(UUID(as_uuid=True), ForeignKey('videos.id'), nullable=False) + + # 업로드 정보 + title = Column(String, nullable=False) # 제목 + description = Column(String, nullable=False) # 설명 + tags = Column(ARRAY(String(30)), nullable=True) # 태그그 + url = Column(String, nullable=False) # 주소 + platform = Column(String, nullable=False) # 플랫폼 + + # 관계 설정 + # ( uploads --- N:1 --- videos ) + video = relationship("Video", back_populates="uploads") \ No newline at end of file diff --git a/backend/app/domain/models/user.py b/backend/app/domain/models/user.py new file mode 100644 index 0000000..3a4da0f --- /dev/null +++ b/backend/app/domain/models/user.py @@ -0,0 +1,21 @@ +from sqlalchemy import Column, String +from sqlalchemy.orm import relationship +from app.domain.models.base import BaseModel + +class User(BaseModel): + '''사용자 모델''' + __tablename__ = "users" + + user_id = Column(String, nullable=False, unique=True) # 로그인용 실제 아이디 + name = Column(String, nullable=False) + password = Column(String, nullable=False) + + # 추 후, 필요하면 column 수정 ( nullable 수정 ) + email = Column(String, nullable=True, unique=True) + phone_number = Column(String, nullable=True, unique=True) + + + # 관계 설정 ( users --- 1:N --- items, orders, channels ) + items = relationship("Item", back_populates="user", cascade="all, delete-orphan") + orders = relationship("Order", back_populates="user", cascade="all, delete-orphan") + channels = relationship("Channel", back_populates="user", cascade="all, delete-orphan") \ No newline at end of file diff --git a/backend/app/domain/models/video.py b/backend/app/domain/models/video.py new file mode 100644 index 0000000..0ed0971 --- /dev/null +++ b/backend/app/domain/models/video.py @@ -0,0 +1,34 @@ +from sqlalchemy import Column, String, ForeignKey, Boolean, Integer +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import relationship +from app.domain.models.base import BaseModel + +class Video(BaseModel): + '''비디오 모델''' + __tablename__ = "videos" + + # 외래키 + music_id = Column(UUID(as_uuid=True), ForeignKey('musics.id'), nullable=False, unique=True) + order_id = Column(UUID(as_uuid=True), ForeignKey('orders.id'), nullable=False) + + title = Column(String, nullable=False) # 비디오 제목 + description = Column(String, nullable=False) # 비디오 설명 + url = Column(String, nullable=False) # 비디오 주소 + is_uploaded = Column(Boolean, nullable=False) # 비디오 업로드 여부 + download_count = Column(Integer, nullable=False) # 비디오 다운로드 수 + resolution = Column(String, nullable=False) # 비디오 해상도 + status = Column(String, nullable=True, default="완료됨") # 비디오 상태 ( 예: 준비중, 업로드중, 업로드완료 ) + thumbnail_url = Column(String, nullable=True) # 비디오 썸네일 주소 + + # 관계 설정 + # ( videos --- 1:N --- uploads ) + # ( videos --- 1:N --- photos ) + # video가 없어도 photo는 있을 수 있어서 cascade 안함 + uploads = relationship("Upload", back_populates="video", cascade="all, delete-orphan") + photos = relationship("Photo", back_populates="video", order_by="Photo.video_index") + + # ( videos --- N:1 --- orders ) + order = relationship("Order", back_populates="videos") + + # ( videos --- 1:1 --- musics ) + music = relationship("Music", back_populates="video", uselist=False) \ No newline at end of file diff --git a/backend/app/domain/value_object/password.py b/backend/app/domain/value_object/password.py new file mode 100644 index 0000000..5eee268 --- /dev/null +++ b/backend/app/domain/value_object/password.py @@ -0,0 +1,17 @@ +import re + +class Password(str): + def __new__(cls, value: str): + cls._validate(value) + return super().__new__(cls, value) + + @classmethod + def _validate(cls, value: str): + if len(value) < 8: + raise ValueError("비밀번호는 8자 이상이어야 합니다.") + + if not re.search(r"[A-Z]", value): + raise ValueError("비밀번호는 대문자 영어 1개 이상이어야 합니다.") + + if not re.search(r'[!@#$%^&*(),.?":{}|<>]', value): + raise ValueError("비밀번호에는 특수문자가 1개 이상 포함되어야 합니다.") \ No newline at end of file diff --git a/backend/app/domain/value_object/user_id.py b/backend/app/domain/value_object/user_id.py new file mode 100644 index 0000000..49804d7 --- /dev/null +++ b/backend/app/domain/value_object/user_id.py @@ -0,0 +1,10 @@ +import re + +class UserId(str): + def __init__(self, value: str): + if re.search(r"[^\w가-힣]", value): + raise ValueError("아이디에는 특수문자는 사용할 수 없습니다.") + self.value = value + + def __str__(self): + return self.value \ No newline at end of file diff --git a/backend/app/domain/value_object/user_name.py b/backend/app/domain/value_object/user_name.py new file mode 100644 index 0000000..1dc82bb --- /dev/null +++ b/backend/app/domain/value_object/user_name.py @@ -0,0 +1,10 @@ +import re + +class UserName(str): + def __init__(self, value: str): + if re.search(r"[^\w가-힣]", value): + raise ValueError("이름에는 특수문자는 사용할 수 없습니다.") + self.value = value + + def __str__(self): + return self.value \ No newline at end of file diff --git a/backend/app/infra/crawling_refactor/__init__.py b/backend/app/infra/crawling_refactor/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/ABOUT b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/ABOUT new file mode 100644 index 0000000..1627091 --- /dev/null +++ b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/ABOUT @@ -0,0 +1,9 @@ +Google Chrome + +Copyright 2025 Google LLC. All rights reserved. + +Chrome is made possible by the Chromium open source project +(https://www.chromium.org/) and other open source software +(chrome://credits). + +See the Terms of Service at chrome://terms. diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/MEIPreload/manifest.json b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/MEIPreload/manifest.json new file mode 100644 index 0000000..1b4fa8a --- /dev/null +++ b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/MEIPreload/manifest.json @@ -0,0 +1,8 @@ +{ + "name": "MEI Preload", + "icons": {}, + "version": "1.0.7.1652906823", + "manifest_version": 2, + "update_url": "https://clients2.google.com/service/update2/crx", + "description": "Contains preloaded data for Media Engagement" +} diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/MEIPreload/preloaded_data.pb b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/MEIPreload/preloaded_data.pb new file mode 100644 index 0000000..78f104c Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/MEIPreload/preloaded_data.pb differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/PrivacySandboxAttestationsPreloaded/manifest.json b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/PrivacySandboxAttestationsPreloaded/manifest.json new file mode 100644 index 0000000..a444944 --- /dev/null +++ b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/PrivacySandboxAttestationsPreloaded/manifest.json @@ -0,0 +1,6 @@ +{ + "manifest_version": 2, + "name": "Privacy Sandbox Attestations", + "version": "2025.5.21.0", + "pre_installed": true +} \ No newline at end of file diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/PrivacySandboxAttestationsPreloaded/privacy-sandbox-attestations.dat b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/PrivacySandboxAttestationsPreloaded/privacy-sandbox-attestations.dat new file mode 100644 index 0000000..3a590fe --- /dev/null +++ b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/PrivacySandboxAttestationsPreloaded/privacy-sandbox-attestations.dat @@ -0,0 +1,266 @@ + +https://2k.comhttps://33across.comhttps://360yield.comhttps://3lift.comhttps://ad-score.com https://ad.gthttps://adentifi.comhttps://adform.nethttps://adingo.jphttps://admatrix.jphttps://admixer.nethttps://adnami.iohttps://adnxs.comhttps://adsafeprotected.comhttps://adsrvr.orghttps://adthrive.comhttps://advividnetwork.comNhttps://aggregation-service-site-dot-clz200258-datateam-italy.ew.r.appspot.comhttps://anonymised.iohttps://aphub.aihttps://appier.nethttps://avads.nethttps://ayads.iohttps://bidswitch.nethttps://bidtheatre.nethttps://bing.comhttps://blendee.comhttps://bounceexchange.comhttps://bypass.jphttps://casalemedia.comhttps://cdn-net.comhttps://clickonometrics.plhttps://connected-stories.comhttps://crcldu.comhttps://creativecdn.comhttps://criteo.comhttps://ctnsnet.comhttps://daum.nethttps://display.iohttps://dotdashmeredith.comhttps://dotomi.comhttps://doubleclick.nethttps://dynalyst.jphttps://edkt.iohttps://ezoic.comhttps://fanbyte.comhttps://flashtalking.comhttps://fout.jphttps://fwmrm.nethttps://gama.globohttps://ghtinc.comhttps://gmossp-sp.jphttps://google-analytics.comhttps://gsspat.jphttps://gumgum.comhttps://html-load.comhttps://im-apps.nethttps://impact-ad.jphttps://indexww.comhttps://inmobi.comhttps://innovid.comhttps://jivox.comhttps://kidoz.nethttps://ladsp.comhttps://lucead.comhttps://mail.ruhttps://media.nethttps://mediaintelligence.dehttps://mediamath.comhttps://mediavine.comhttps://microad.jphttps://naver.comhttps://nhnace.comhttps://nodals.iohttps://onetag-sys.comhttps://openx.nethttps://optable.cohttps://outbrain.comhttps://pixfuture.com+https://privacy-sandbox-demos-ad-server.dev'https://privacy-sandbox-demos-dsp-a.dev'https://privacy-sandbox-demos-dsp-b.dev'https://privacy-sandbox-demos-dsp-x.dev'https://privacy-sandbox-demos-dsp-y.dev%https://privacy-sandbox-demos-dsp.dev*https://privacy-sandbox-demos-services.dev'https://privacy-sandbox-demos-ssp-a.dev'https://privacy-sandbox-demos-ssp-b.dev'https://privacy-sandbox-demos-ssp-x.dev'https://privacy-sandbox-demos-ssp-y.dev%https://privacy-sandbox-demos-ssp.dev https://privacy-sandbox-test.com0https://privacy-sandcastle-dev-ad-server.web.app-https://privacy-sandcastle-dev-dsp-a1.web.app-https://privacy-sandcastle-dev-dsp-b1.web.app,https://privacy-sandcastle-dev-dsp-x.web.app,https://privacy-sandcastle-dev-dsp-y.web.app*https://privacy-sandcastle-dev-dsp.web.app/https://privacy-sandcastle-dev-services.web.app,https://privacy-sandcastle-dev-ssp-a.web.app,https://privacy-sandcastle-dev-ssp-b.web.app,https://privacy-sandcastle-dev-ssp-x.web.app,https://privacy-sandcastle-dev-ssp-y.web.app*https://privacy-sandcastle-dev-ssp.web.apphttps://pub.networkhttps://pubmatic.comhttps://pubtm.comhttps://quantserve.comhttps://relevant-digital.comhttps://sascdn.comhttps://shinystat.comhttps://simeola.comhttps://singular.nethttps://sportradarserving.comhttps://t13.iohttps://teads.tvhttps://thepopradar.comhttps://theryn.iohttps://tncid.apphttps://toponad.comhttps://tpmark.nethttps://tribalfusion.comhttps://triptease.iohttps://uinterbox.comhttps://uol.com.br https://vg.nohttps://vpadn.comhttps://washingtonpost.comhttps://yahoo.co.jphttps://yahoo.comhttps://yandex.ruhttps://yelp.com +https://lwadm.com + +https://finn.no + +https://pinterest.com + +https://r2b2.io + +https://yieldmo.com + +https://facebook.com + +https://postrelease.com + +https://elnacional.cat + +https://dailymail.co.uk +! +https://dailymotion.com +% +https://audienceproject.com + +https://cpx.to + +https://worldhistory.org + +https://usemax.de +! +https://audience360.com.au +# +https://youronlinechoices.eu + +https://storygize.net + +https://tailtarget.com +" +https://appsflyersdk.com + +https://sephora.com + +https://docomo.ne.jp + +https://atomex.net + +https://getcapi.co +% +https://wepowerconnections.com + +https://aniview.com +# +https://adsmeasurement.com +% +https://creative-serving.com + +https://adroll.com + +https://trkkn.com + +https://taboola.com + +https://disqus.com + +https://torneos.gg + +https://globo.com + +https://yieldlab.net + +https://shinobi.jp +! +https://ebayadservices.com + +https://acxiom.com + +https://demand.supply + +https://aqfer.com + +https://i-mobile.co.jp + +https://sitescout.com + +https://tamedia.com.tw + +https://doubleverify.com + +https://dreammail.jp + +https://cazamba.com + +https://vidazoo.com + +https://tiktok.com + +https://iobeya.com +# +https://amazon-adsystem.com + +https://primecaster.net + +https://appsflyer.com + +https://bluems.com +! +https://weborama-tech.ru +# +https://explorefledge.com + +https://grxchange.gr + +https://moshimo.com + +https://coupang.com + +https://momento.dev + +https://unrulymedia.com + +https://tangooserver.com + +https://snapchat.com + +https://stackadapt.com +" +https://kompaspublishing.nl + + https://wp.pl + +https://apex-football.com + +https://get3rdspace.com +1 +(https://paa-reporting-advertising.amazon + +https://kargo.com + +https://permutive.app + +https://socdm.com +" +https://d-edgeconnect.media + +https://atirun.com + +https://insyta.com + +https://validate.audio + +https://tya-dev.com + +https://ebis.ne.jp + +https://a-mo.net + +https://verve.com + +https://onet.pl +( + https://smadexprivacysandbox.com + +https://fandom.com + +https://akpytela.cz +< +4https://shared-storage-demo-content-producer.web.app +" +https://media6degrees.com +6 +/https://ptb-msmt-static-5jyy5ulagq-uc.a.run.app + +https://logly.co.jp + +https://nexxen.tech + +https://xsoda.net +7 +/https://shared-storage-demo-publisher-b.web.app + +https://semafor.com + +https://linkedin.com + +https://samplicio.us + +https://ad-stir.com +" +https://rocksolidrustic.com + +https://retargetly.com +" +https://authorizedvault.com + +https://gokwik.co + +https://euleriancdn.net +& +https://adtrafficquality.google + +https://undertone.com + +https://connatix.com + +https://beaconmax.com + +https://weborama.fr + +https://metro.co.uk + +https://payment.goog + +https://trip.com + +https://eloan.co.jp + +https://alketech.eu + +https://appconsent.io +7 +/https://shared-storage-demo-publisher-a.web.app +% +https://googlesyndication.com + +https://seedtag.com + +https://getyourguide.com + +https://jkforum.net + +https://superfine.org + +https://adswizz.com + +https://gunosy.com + +https://admission.net + +https://open-bid.com + +https://presage.io + +https://convertunits.com + +https://azubiyo.de + +https://deepintent.com + +https://quora.com + +https://elle.com +& +https://googleadservices.com +" +https://rubiconproject.com + +https://boost-web.com + +https://halcy.de + +https://adscale.de + +https://ingereck.net +" +https://bright-nurse.com + \ No newline at end of file diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/WidevineCdm/LICENSE b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/WidevineCdm/LICENSE new file mode 100644 index 0000000..e711887 --- /dev/null +++ b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/WidevineCdm/LICENSE @@ -0,0 +1,8 @@ +Google LLC and its affiliates ("Google") own all legal right, title and +interest in and to the content decryption module software ("Software") and +related documentation, including any intellectual property rights in the +Software. You may not use, modify, sell, or otherwise distribute the Software +without a separate license agreement with Google. The Software is not open +source software. + +If you are interested in licensing the Software, please contact www.widevine.com diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/WidevineCdm/_platform_specific/linux_x64/libwidevinecdm.so b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/WidevineCdm/_platform_specific/linux_x64/libwidevinecdm.so new file mode 100644 index 0000000..6ef97af Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/WidevineCdm/_platform_specific/linux_x64/libwidevinecdm.so differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/WidevineCdm/manifest.json b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/WidevineCdm/manifest.json new file mode 100644 index 0000000..1cce894 --- /dev/null +++ b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/WidevineCdm/manifest.json @@ -0,0 +1,33 @@ +{ + "manifest_version": 2, + "update_url": "https://clients2.google.com/service/update2/crx", + "name": "WidevineCdm", + "description": "Widevine Content Decryption Module", + "version": "4.10.2891.0", + "minimum_chrome_version": "68.0.3430.0", + "x-cdm-module-versions": "4", + "x-cdm-interface-versions": "10", + "x-cdm-host-versions": "10", + "x-cdm-codecs": "vp8,vp09,avc1,av01", + "x-cdm-persistent-license-support": false, + "x-cdm-supported-encryption-schemes": [ + "cenc", + "cbcs" + ], + "icons": { + "16": "imgs/icon-128x128.png", + "128": "imgs/icon-128x128.png" + }, + "platforms": [ + { + "os": "linux", + "arch": "x64", + "sub_package_path": "_platform_specific/linux_x64/" + }, + { + "os": "linux", + "arch": "arm64", + "sub_package_path": "_platform_specific/linux_arm64/" + } + ] +} \ No newline at end of file diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/chrome b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/chrome new file mode 100644 index 0000000..5c4d5de Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/chrome differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/chrome-wrapper b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/chrome-wrapper new file mode 100644 index 0000000..718b9ef --- /dev/null +++ b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/chrome-wrapper @@ -0,0 +1,163 @@ +#!/bin/bash + +# Copyright 2011 The Chromium Authors +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +# Running Chromium via this script makes it possible to set Chromium as the +# default browser directly out of a compile, without needing to package it. + +DESKTOP="chromium-devel" +TITLE="Chromium" + +usage() { + echo "$0 [--gdb] [--help] [--man-page] [--] [chrome-options]" + echo + echo " --gdb Start within gdb" + echo " --help This help screen" + echo " --man-page Open the man page in the tree" +} + +# Check to see if there is a desktop file of the given name. +exists_desktop_file() { + # Build a search list from $XDG_DATA_HOME and $XDG_DATA_DIRS, the latter + # of which can itself be a colon-separated list of directories to search. + search="${XDG_DATA_HOME:-$HOME/.local/share}:${XDG_DATA_DIRS:-/usr/local/share:/usr/share}" + IFS=: + for dir in $search; do + unset IFS + [ "$dir" -a -d "$dir/applications" ] || continue + [ -r "$dir/applications/$DESKTOP.desktop" ] && return + done + # Didn't find it in the search path. + return 1 +} + +# Checks a file to see if it's a 32 or 64-bit. +check_executable() { + out=$(file $(readlink -f $1) 2> /dev/null) + echo $out | grep -qs "ELF 32-bit LSB" + if [ $? = 0 ]; then + echo 32 + return + fi + echo $out | grep -qs "ELF 64-bit LSB" + if [ $? = 0 ]; then + echo 64 + return + fi + echo neither +} + +# Generate a desktop file that will run this script. +generate_desktop_file() { + apps="${XDG_DATA_HOME:-$HOME/.local/share}/applications" + mkdir -p "$apps" + cat > "$apps/$DESKTOP.desktop" << EOF +[Desktop Entry] +Version=1.0 +Encoding=UTF-8 +Name=$TITLE +Exec=$CHROME_WRAPPER %U +Terminal=false +Icon=$HERE/product_logo_48.png +Type=Application +Categories=Application;Network;WebBrowser; +MimeType=text/html;text/xml;application/xhtml_xml; +EOF +} + +# Let the wrapped binary know that it has been run through the wrapper. +export CHROME_WRAPPER="`readlink -f "$0"`" +export CHROME_DESKTOP="$DESKTOP.desktop" + +HERE="`dirname "$CHROME_WRAPPER"`" + +# We include some xdg utilities next to the binary, and we want to prefer them +# over the system versions when we know the system versions are very old. We +# detect whether the system xdg utilities are sufficiently new to be likely to +# work for us by looking for xdg-settings. If we find it, we leave $PATH alone, +# so that the system xdg utilities (including any distro patches) will be used. +if ! which xdg-settings &> /dev/null; then + # Old xdg utilities. Prepend $HERE to $PATH to use ours instead. + export PATH="$HERE:$PATH" +else + # Use system xdg utilities. But first create mimeapps.list if it doesn't + # exist; some systems have bugs in xdg-mime that make it fail without it. + xdg_app_dir="${XDG_DATA_HOME:-$HOME/.local/share/applications}" + mkdir -p "$xdg_app_dir" + [ -f "$xdg_app_dir/mimeapps.list" ] || touch "$xdg_app_dir/mimeapps.list" +fi + +# Always use our ffmpeg and other shared libs. +export LD_LIBRARY_PATH="$HERE:$HERE/lib:$HERE/lib.target${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}" + +MISSING_LIBS=$(ldd "$HERE/chrome" 2> /dev/null |grep "not found$" | cut -d" " -f 1|sed 's/\t//') +CHROME_ARCH=$(check_executable "$HERE/chrome") +uname -m | grep -qs x86_64 +if [ $? = 1 ]; then + LIBDIRS="/lib /lib32 /usr/lib /usr/lib32" +else + LIBDIRS="/lib64 /lib /usr/lib64 /usr/lib" +fi + +echo $MISSING_LIBS | grep -qs libbz2.so.1.0 +if [ $? = 0 ]; then + for dir in $LIBDIRS + do + if [ -e "$dir/libbz2.so.1" ]; then + LIB_ARCH=$(check_executable "$dir/libbz2.so.1") + if [ "$CHROME_ARCH" = "$LIB_ARCH" ]; then + ln -snf "$dir/libbz2.so.1" "$HERE/libbz2.so.1.0" + break; + fi + fi + done +fi + +for lib in libnspr4.so.0d libnss3.so.1d libnssutil3.so.1d libplc4.so.0d libplds4.so.0d libsmime3.so.1d libssl3.so.1d +do + echo $MISSING_LIBS | grep -qs $lib + if [ $? = 0 ]; then + reallib=$(echo $lib | sed 's/\.[01]d$//') + for dir in $LIBDIRS + do + if [ -e "$dir/$reallib" ]; then + LIB_ARCH=$(check_executable "$dir/$reallib") + if [ "$CHROME_ARCH" = "$LIB_ARCH" ]; then + ln -snf "$dir/$reallib" "$HERE/$lib" + break; + fi + fi + done + fi +done + +# Custom version string for this release. This can be used to add a downstream +# vendor string or release channel information. +export CHROME_VERSION_EXTRA="custom" + +exists_desktop_file || generate_desktop_file + +CMD_PREFIX= +ARGS=() +while [ "$#" -gt 0 ]; do + case "$1" in + "--") + shift + break ;; + "--gdb") + CMD_PREFIX="gdb --args" ;; + "--help") + usage + exit 0 ;; + "--man-page") + exec man "$HERE/../../chrome/app/resources/manpage.1.in" ;; + *) + ARGS=( "${ARGS[@]}" "$1" ) ;; + esac + shift +done +set -- "${ARGS[@]}" "$@" + +exec $CMD_PREFIX "$HERE/chrome" "$@" diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/chrome_100_percent.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/chrome_100_percent.pak new file mode 100644 index 0000000..7fcda6a Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/chrome_100_percent.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/chrome_200_percent.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/chrome_200_percent.pak new file mode 100644 index 0000000..7178b41 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/chrome_200_percent.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/chrome_crashpad_handler b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/chrome_crashpad_handler new file mode 100644 index 0000000..a4e8d84 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/chrome_crashpad_handler differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/chrome_sandbox b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/chrome_sandbox new file mode 100644 index 0000000..ff17375 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/chrome_sandbox differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/deb.deps b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/deb.deps new file mode 100644 index 0000000..40d7e2a --- /dev/null +++ b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/deb.deps @@ -0,0 +1,30 @@ +ca-certificates +fonts-liberation +libasound2 (>= 1.0.17) +libatk-bridge2.0-0 (>= 2.5.3) +libatk1.0-0 (>= 2.11.90) +libatspi2.0-0 (>= 2.9.90) +libc6 (>= 2.17) +libcairo2 (>= 1.6.0) +libcups2 (>= 1.6.0) +libcurl3-gnutls | libcurl3-nss | libcurl4 | libcurl3 +libdbus-1-3 (>= 1.9.14) +libexpat1 (>= 2.1~beta3) +libgbm1 (>= 17.1.0~rc2) +libglib2.0-0 (>= 2.39.4) +libgtk-3-0 (>= 3.9.10) | libgtk-4-1 +libnspr4 (>= 2:4.9-2~) +libnss3 (>= 2:3.35) +libpango-1.0-0 (>= 1.14.0) +libudev1 (>= 183) +libvulkan1 +libx11-6 (>= 2:1.4.99.1) +libxcb1 (>= 1.9.2) +libxcomposite1 (>= 1:0.4.4-1) +libxdamage1 (>= 1:1.1) +libxext6 +libxfixes3 +libxkbcommon0 (>= 0.5.0) +libxrandr2 +wget +xdg-utils (>= 1.0.2) diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-af.hyb b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-af.hyb new file mode 100644 index 0000000..54e6c0e Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-af.hyb differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-as.hyb b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-as.hyb new file mode 100644 index 0000000..43a9527 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-as.hyb differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-be.hyb b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-be.hyb new file mode 100644 index 0000000..4da6b74 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-be.hyb differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-bg.hyb b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-bg.hyb new file mode 100644 index 0000000..3f46fa1 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-bg.hyb differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-bn.hyb b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-bn.hyb new file mode 100644 index 0000000..43a9527 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-bn.hyb differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-cs.hyb b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-cs.hyb new file mode 100644 index 0000000..4255d56 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-cs.hyb differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-cu.hyb b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-cu.hyb new file mode 100644 index 0000000..4ec90d3 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-cu.hyb differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-cy.hyb b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-cy.hyb new file mode 100644 index 0000000..5afe8aa Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-cy.hyb differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-da.hyb b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-da.hyb new file mode 100644 index 0000000..f33f430 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-da.hyb differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-de-1901.hyb b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-de-1901.hyb new file mode 100644 index 0000000..7de89ad Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-de-1901.hyb differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-de-1996.hyb b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-de-1996.hyb new file mode 100644 index 0000000..9880a9c Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-de-1996.hyb differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-de-ch-1901.hyb b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-de-ch-1901.hyb new file mode 100644 index 0000000..7e0b36a Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-de-ch-1901.hyb differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-el.hyb b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-el.hyb new file mode 100644 index 0000000..413defd Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-el.hyb differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-en-gb.hyb b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-en-gb.hyb new file mode 100644 index 0000000..8b2ca33 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-en-gb.hyb differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-en-us.hyb b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-en-us.hyb new file mode 100644 index 0000000..db1469a Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-en-us.hyb differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-es.hyb b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-es.hyb new file mode 100644 index 0000000..1ef2330 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-es.hyb differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-et.hyb b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-et.hyb new file mode 100644 index 0000000..bc42bf3 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-et.hyb differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-eu.hyb b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-eu.hyb new file mode 100644 index 0000000..b9d6f46 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-eu.hyb differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-fr.hyb b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-fr.hyb new file mode 100644 index 0000000..b24b5a2 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-fr.hyb differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-ga.hyb b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-ga.hyb new file mode 100644 index 0000000..3eb376f Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-ga.hyb differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-gl.hyb b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-gl.hyb new file mode 100644 index 0000000..604c80a Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-gl.hyb differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-gu.hyb b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-gu.hyb new file mode 100644 index 0000000..908ea1a Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-gu.hyb differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-hi.hyb b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-hi.hyb new file mode 100644 index 0000000..b0b9680 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-hi.hyb differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-hr.hyb b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-hr.hyb new file mode 100644 index 0000000..f73854c Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-hr.hyb differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-hu.hyb b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-hu.hyb new file mode 100644 index 0000000..95d8194 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-hu.hyb differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-hy.hyb b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-hy.hyb new file mode 100644 index 0000000..1bb1832 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-hy.hyb differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-it.hyb b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-it.hyb new file mode 100644 index 0000000..aadffdf Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-it.hyb differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-ka.hyb b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-ka.hyb new file mode 100644 index 0000000..818a72d Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-ka.hyb differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-kn.hyb b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-kn.hyb new file mode 100644 index 0000000..46bdbcf Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-kn.hyb differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-la.hyb b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-la.hyb new file mode 100644 index 0000000..c91ca2f Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-la.hyb differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-lt.hyb b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-lt.hyb new file mode 100644 index 0000000..98c190c Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-lt.hyb differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-lv.hyb b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-lv.hyb new file mode 100644 index 0000000..105c274 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-lv.hyb differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-ml.hyb b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-ml.hyb new file mode 100644 index 0000000..c716ff2 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-ml.hyb differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-mn-cyrl.hyb b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-mn-cyrl.hyb new file mode 100644 index 0000000..3c6a4a4 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-mn-cyrl.hyb differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-mr.hyb b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-mr.hyb new file mode 100644 index 0000000..b0b9680 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-mr.hyb differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-mul-ethi.hyb b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-mul-ethi.hyb new file mode 100644 index 0000000..1bfa7d9 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-mul-ethi.hyb differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-nb.hyb b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-nb.hyb new file mode 100644 index 0000000..1e897a0 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-nb.hyb differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-nl.hyb b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-nl.hyb new file mode 100644 index 0000000..09b81c5 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-nl.hyb differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-nn.hyb b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-nn.hyb new file mode 100644 index 0000000..74cf56e Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-nn.hyb differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-or.hyb b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-or.hyb new file mode 100644 index 0000000..e320ce8 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-or.hyb differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-pa.hyb b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-pa.hyb new file mode 100644 index 0000000..fd61325 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-pa.hyb differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-pt.hyb b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-pt.hyb new file mode 100644 index 0000000..10a669b Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-pt.hyb differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-ru.hyb b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-ru.hyb new file mode 100644 index 0000000..eddd313 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-ru.hyb differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-sk.hyb b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-sk.hyb new file mode 100644 index 0000000..303df31 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-sk.hyb differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-sl.hyb b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-sl.hyb new file mode 100644 index 0000000..2215e70 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-sl.hyb differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-sq.hyb b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-sq.hyb new file mode 100644 index 0000000..dfb9c8b Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-sq.hyb differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-sv.hyb b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-sv.hyb new file mode 100644 index 0000000..9f07d78 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-sv.hyb differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-ta.hyb b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-ta.hyb new file mode 100644 index 0000000..3cb21b5 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-ta.hyb differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-te.hyb b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-te.hyb new file mode 100644 index 0000000..4b34907 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-te.hyb differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-tk.hyb b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-tk.hyb new file mode 100644 index 0000000..1bc9345 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-tk.hyb differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-uk.hyb b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-uk.hyb new file mode 100644 index 0000000..fc65a25 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-uk.hyb differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-und-ethi.hyb b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-und-ethi.hyb new file mode 100644 index 0000000..3c98edb Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/hyph-und-ethi.hyb differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/manifest.json b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/manifest.json new file mode 100644 index 0000000..e1b5767 --- /dev/null +++ b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/hyphen-data/manifest.json @@ -0,0 +1,5 @@ +{ + "manifest_version": 2, + "name": "hyphens-data", + "version": "1.0.0.0" +} \ No newline at end of file diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/icudtl.dat b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/icudtl.dat new file mode 100644 index 0000000..734e56d Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/icudtl.dat differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/libEGL.so b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/libEGL.so new file mode 100644 index 0000000..ed395f1 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/libEGL.so differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/libGLESv2.so b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/libGLESv2.so new file mode 100644 index 0000000..73d21d0 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/libGLESv2.so differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/libvk_swiftshader.so b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/libvk_swiftshader.so new file mode 100644 index 0000000..0bcc70a Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/libvk_swiftshader.so differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/libvulkan.so.1 b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/libvulkan.so.1 new file mode 100644 index 0000000..d4e437b Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/libvulkan.so.1 differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/af.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/af.pak new file mode 100644 index 0000000..e26c8f8 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/af.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/am.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/am.pak new file mode 100644 index 0000000..b088e80 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/am.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/ar.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/ar.pak new file mode 100644 index 0000000..3ef5d25 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/ar.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/bg.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/bg.pak new file mode 100644 index 0000000..ce1ea87 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/bg.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/bn.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/bn.pak new file mode 100644 index 0000000..806a0fc Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/bn.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/ca.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/ca.pak new file mode 100644 index 0000000..79f3d9f Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/ca.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/cs.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/cs.pak new file mode 100644 index 0000000..724a212 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/cs.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/da.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/da.pak new file mode 100644 index 0000000..99019f9 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/da.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/de.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/de.pak new file mode 100644 index 0000000..06b3d64 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/de.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/el.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/el.pak new file mode 100644 index 0000000..970a289 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/el.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/en-GB.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/en-GB.pak new file mode 100644 index 0000000..c2caeb1 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/en-GB.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/en-US.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/en-US.pak new file mode 100644 index 0000000..a4623a5 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/en-US.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/es-419.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/es-419.pak new file mode 100644 index 0000000..394b812 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/es-419.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/es.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/es.pak new file mode 100644 index 0000000..7f48b87 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/es.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/et.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/et.pak new file mode 100644 index 0000000..5c65f75 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/et.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/fa.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/fa.pak new file mode 100644 index 0000000..0442e66 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/fa.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/fi.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/fi.pak new file mode 100644 index 0000000..e366ff8 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/fi.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/fil.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/fil.pak new file mode 100644 index 0000000..712bed2 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/fil.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/fr.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/fr.pak new file mode 100644 index 0000000..5d2bb65 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/fr.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/gu.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/gu.pak new file mode 100644 index 0000000..751469b Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/gu.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/he.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/he.pak new file mode 100644 index 0000000..ffaf0d3 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/he.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/hi.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/hi.pak new file mode 100644 index 0000000..1ea62c5 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/hi.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/hr.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/hr.pak new file mode 100644 index 0000000..d0d8677 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/hr.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/hu.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/hu.pak new file mode 100644 index 0000000..5d7b0fb Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/hu.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/id.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/id.pak new file mode 100644 index 0000000..7dbab63 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/id.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/it.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/it.pak new file mode 100644 index 0000000..38bb5c8 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/it.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/ja.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/ja.pak new file mode 100644 index 0000000..e985e36 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/ja.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/kn.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/kn.pak new file mode 100644 index 0000000..dde786c Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/kn.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/ko.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/ko.pak new file mode 100644 index 0000000..baf3074 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/ko.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/lt.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/lt.pak new file mode 100644 index 0000000..d0cc1cc Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/lt.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/lv.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/lv.pak new file mode 100644 index 0000000..309657f Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/lv.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/ml.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/ml.pak new file mode 100644 index 0000000..328d1df Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/ml.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/mr.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/mr.pak new file mode 100644 index 0000000..c18fb06 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/mr.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/ms.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/ms.pak new file mode 100644 index 0000000..8ecd763 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/ms.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/nb.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/nb.pak new file mode 100644 index 0000000..abb06b4 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/nb.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/nl.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/nl.pak new file mode 100644 index 0000000..99fb18f Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/nl.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/pl.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/pl.pak new file mode 100644 index 0000000..05647a8 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/pl.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/pt-BR.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/pt-BR.pak new file mode 100644 index 0000000..ea55147 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/pt-BR.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/pt-PT.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/pt-PT.pak new file mode 100644 index 0000000..2cf70c5 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/pt-PT.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/ro.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/ro.pak new file mode 100644 index 0000000..89ef3d6 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/ro.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/ru.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/ru.pak new file mode 100644 index 0000000..5228244 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/ru.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/sk.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/sk.pak new file mode 100644 index 0000000..fc29dbb Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/sk.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/sl.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/sl.pak new file mode 100644 index 0000000..80bb411 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/sl.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/sr.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/sr.pak new file mode 100644 index 0000000..e14d2eb Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/sr.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/sv.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/sv.pak new file mode 100644 index 0000000..071e0cf Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/sv.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/sw.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/sw.pak new file mode 100644 index 0000000..cd5400a Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/sw.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/ta.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/ta.pak new file mode 100644 index 0000000..290b0d4 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/ta.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/te.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/te.pak new file mode 100644 index 0000000..aa0e19d Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/te.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/th.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/th.pak new file mode 100644 index 0000000..95b8f9a Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/th.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/tr.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/tr.pak new file mode 100644 index 0000000..bb5b6e8 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/tr.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/uk.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/uk.pak new file mode 100644 index 0000000..c9894fb Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/uk.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/ur.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/ur.pak new file mode 100644 index 0000000..ca332e2 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/ur.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/vi.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/vi.pak new file mode 100644 index 0000000..dd9bed5 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/vi.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/zh-CN.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/zh-CN.pak new file mode 100644 index 0000000..e964aa5 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/zh-CN.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/zh-TW.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/zh-TW.pak new file mode 100644 index 0000000..77e917f Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/locales/zh-TW.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/product_logo_48.png b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/product_logo_48.png new file mode 100644 index 0000000..798479d Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/product_logo_48.png differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/resources.pak b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/resources.pak new file mode 100644 index 0000000..30223b4 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/resources.pak differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/resources/accessibility/reading_mode_gdocs_helper/content.js b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/resources/accessibility/reading_mode_gdocs_helper/content.js new file mode 100644 index 0000000..cdb0788 --- /dev/null +++ b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/resources/accessibility/reading_mode_gdocs_helper/content.js @@ -0,0 +1,11 @@ +"use strict"; +// Copyright 2024 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// TODO(b:272150598): Investigate sharing this with +// ../embedded_a11y_helper/content.ts. +(function () { + const s = document.createElement('script'); + s.src = chrome.runtime.getURL('reading_mode_gdocs_helper/gdocs_script.js'); + document.documentElement.appendChild(s); +})(); diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/resources/accessibility/reading_mode_gdocs_helper/gdocs_script.js b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/resources/accessibility/reading_mode_gdocs_helper/gdocs_script.js new file mode 100644 index 0000000..9df9c10 --- /dev/null +++ b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/resources/accessibility/reading_mode_gdocs_helper/gdocs_script.js @@ -0,0 +1,36 @@ +// Copyright 2017 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +function triggerDocsCanvasAnnotationMode() { + if (!chrome.runtime || !chrome.runtime.id) { + // Manifest v3: Not running in extension's runtime. + // Parse this script's URL to determine extension ID. + // The URL will look like + // `chrome-extension://extensionId/common/gdocs_script.js. + const extensionId = document.currentScript.src.split('/')[2]; + const scriptContents = ` + window['_docs_annotate_canvas_by_ext'] = "${extensionId}"; + `; + const policy = trustedTypes.createPolicy('gdocsPolicy', { + createScript: (text) => text, + }); + const sanitized = policy.createScript(scriptContents); + eval(sanitized); + } else { + // Manifest v2. + const extensionId = chrome.runtime.id; + const scriptContents = ` + window['_docs_annotate_canvas_by_ext'] = "${extensionId}"; + `; + const script = document.createElement('script'); + script.innerHTML = scriptContents; + document.documentElement.appendChild(script); + } +} + +// Docs renders content in Canvas without annotations by default. This script is +// used to trigger annotated Canvas which allows the a11y tree to be built for +// the document contents. This needs to run within the page's context, so +// install a script to be executed. +triggerDocsCanvasAnnotationMode(); diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/resources/accessibility/reading_mode_gdocs_helper_manifest.json b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/resources/accessibility/reading_mode_gdocs_helper_manifest.json new file mode 100644 index 0000000..afaa3c8 --- /dev/null +++ b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/resources/accessibility/reading_mode_gdocs_helper_manifest.json @@ -0,0 +1,26 @@ +{ + "key": "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDCQ+ePDzpLth/ZnUewmbuutBlIOU7RFD27Cy7Z/hFVAGA4wns+Xzj8SJ4YfcQQ1qIH7XfcPe6L+lT+IF8dMwHd5uXIFQWd8gHxeaQoIJcsPjnNgxbVR+TsAuzrgbCWz+gNz0/dDNjqVs+3Dv1QKGf6XE/+iJ4LfkzahIMtsUFp2UaOy+bYT7eh8FGmO8zuQmjAv5gM2I3K3C/8gUBvwm9yhipv/Dn7OtHLcBNHbCq0fbGIHLythTrhBTT81Z+RLAdQy787rS2gQH3IuFqLxuLfXFNBUI30QBdu2c5bL1PbePP+4w+UY8rBQ4ltqO8RDgIJl6zathCsBNtCLt3Yt5hnAgMBAAECggEAMHmkUwjwOYtJOFKsXG+IK0iKlICkX7zGyKE6QDBLX+QXnqI9AaveOund9WuQnMkKJqNFiQI1P92oDr/CLDWZricbSImiXK7SeFCru02im2otn1AqRu9JOvFh8ERs7b0UgTmtipHVoOgnrdzCLhnfFFSrq26ozWJnSBHP7/tuwLwERmBtRldo5HbV7SZWSd54vS2BimijBo4nob7vC+QZf+1p8KEWzi7GvjLxMfpaMMjaC6BcIfvUJPHvPETEMCaLgjb42d4fjr/HECge4elI1dKcXt+IiCJ0pP6Axo1VglIxNqkHbCXlkb/E+F0mST0drYz+Yix4WpxX6eSMg4aEfQKBgQD+E3mFXa3E73Fc5SoZjdvj3KnmEv7CrM3BC79fI9Ljc52PTpp7LKEpl7ywH3vAl06XW0ktwCL+fOth+frgJgRfAMQsq8u8vl9842+yl81i/yoPRRsxM8cri84d3AJRExffY6eddjc6WHqYUauhTz38gUXNvaz+bMNtGXlE2bTBJQKBgQDDvHyhAdWkZa9wTADbmL5UTlVZb/9jS8+ENUpHISqUkOKEwoxvaa071CccurIpxflfP9T+kzdreXGuKHqrziPl4lVfYCDJXo8UmvB9x7/BXvrm0+CGs8rt2i7gL66wVnf9YsGPs5B7KBK/Z8TwS7gie3MNMFLZS8qdcEvc+xzbmwKBgQDRntOlurJBRqOq1s8zIh0HE+mAjq9tghCHct/C6NV3Hs4hi+JcOWgF3tCoJnF9ZdhLe98WRe0ZNYsl3I6lG/iDQSiZCmwfHpm9eg+PszqasJbM3mEe6O8r+D5n2Dp5FV2eyqNhgET0eEc9IDSP88baavViV1lA2A8sFdY9fbhpmQKBgQCjkrDUHKRdblzei6Vr3omwCoZo55+Va5VP9vjL2HutDCdAqxSRRs7uYK0O5TZekoODheSJmp2Fw0etM0bQrMRzKGIQAlVj8xG/NnwjoPouryEeJZJM/5Nmkh76Wt6xnpFHv2/ilzz2rtZ7/kwmRCDtMB1FuEyEK0J3r5C2a2QCYQKBgASVezK2dt9N9kpNxFSJEdW5k7DUZAmAqMVndgIEWWbsurXot5rbgYfhezoST94XVa0K3juIY3lBigvC3ZTafEcLbNEfL8eYmcgb4zK85F2biyIw9Hv5fxqOPsMJ6Ltub3FaORlJGzHPz2783ZTirBsBQgSvQCqKtISVZlwrzXtl", + "manifest_version": 3, + "name": "Reading Mode Google Docs Helper", + "version": "138.0.7204.92", + "description": "Provides support for Reading Mode to work on Google Docs on non-Lacros platforms", + "incognito": "split", + "content_scripts": [ + { + "matches": [ "https://docs.google.com/document*", + "https://docs.sandbox.google.com/document*" ], + "all_frames": true, + "js": [ + "reading_mode_gdocs_helper/content.js" + ], + "run_at": "document_start" + } + ], + "web_accessible_resources": [ + { + "resources": ["reading_mode_gdocs_helper/gdocs_script.js"], + "matches": [ "https://docs.google.com/*", + "https://docs.sandbox.google.com/*" ] + } + ] +} \ No newline at end of file diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/resources/inspector_overlay/inspector_overlay_resources.grd b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/resources/inspector_overlay/inspector_overlay_resources.grd new file mode 100644 index 0000000..db7549e --- /dev/null +++ b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/resources/inspector_overlay/inspector_overlay_resources.grd @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/resources/inspector_overlay/main.js b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/resources/inspector_overlay/main.js new file mode 100644 index 0000000..b1f3ad1 --- /dev/null +++ b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/resources/inspector_overlay/main.js @@ -0,0 +1 @@ +!function(){"use strict";const t=new CSSStyleSheet;t.replaceSync('/*\n * Copyright 2019 The Chromium Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style license that can be\n * found in the LICENSE file.\n */\n\nbody {\n margin: 0;\n padding: 0;\n font-size: 13px;\n color: #222;\n}\n\nbody.platform-linux {\n font-family: "Google Sans Text", "Google Sans", system-ui, sans-serif;\n}\n\nbody.platform-mac {\n color: rgb(48 57 66);\n font-family: system-ui, sans-serif;\n}\n\nbody.platform-windows {\n font-family: system-ui, sans-serif;\n}\n\n.fill {\n position: absolute;\n inset: 0;\n}\n\n#canvas {\n pointer-events: none;\n}\n\n.hidden {\n display: none !important; /* stylelint-disable-line declaration-no-important */\n}\n');class n{viewportSize={width:800,height:600};viewportSizeForMediaQueries;deviceScaleFactor=1;emulationScaleFactor=1;pageScaleFactor=1;pageZoomFactor=1;scrollX=0;scrollY=0;style;canvas;canvasWidth=0;canvasHeight=0;platform;_window;_document;_context;_installed=!1;constructor(t,n=[]){this._window=t,this._document=t.document,Array.isArray(n)||(n=[n]),this.style=n}setCanvas(t){this.canvas=t,this._context=t.getContext("2d")}install(){for(const t of this.style)a(t);this._installed=!0}uninstall(){for(const t of this.style)document.adoptedStyleSheets=document.adoptedStyleSheets.filter((n=>n!==t));this._installed=!1}reset(t){t&&(this.viewportSize=t.viewportSize,this.viewportSizeForMediaQueries=t.viewportSizeForMediaQueries,this.deviceScaleFactor=t.deviceScaleFactor,this.pageScaleFactor=t.pageScaleFactor,this.pageZoomFactor=t.pageZoomFactor,this.emulationScaleFactor=t.emulationScaleFactor,this.scrollX=Math.round(t.scrollX),this.scrollY=Math.round(t.scrollY)),this.resetCanvas()}resetCanvas(){this.canvas&&this._context&&(this.canvas.width=this.deviceScaleFactor*this.viewportSize.width,this.canvas.height=this.deviceScaleFactor*this.viewportSize.height,this.canvas.style.width=this.viewportSize.width+"px",this.canvas.style.height=this.viewportSize.height+"px",this._context.scale(this.deviceScaleFactor,this.deviceScaleFactor),this.canvasWidth=this.viewportSize.width,this.canvasHeight=this.viewportSize.height)}setPlatform(t){this.platform=t,this.document.body.classList.add("platform-"+t),this._installed||this.install()}dispatch(t){this[t.shift()].apply(this,t)}eventHasCtrlOrMeta(t){return"mac"===this.platform?t.metaKey&&!t.ctrlKey:t.ctrlKey&&!t.metaKey}get context(){if(!this._context)throw new Error("Context object is missing");return this._context}get document(){if(!this._document)throw new Error("Document object is missing");return this._document}get window(){if(!this._window)throw new Error("Window object is missing");return this._window}get installed(){return this._installed}}function e(t,n,e){const o=i(n,e);return o.addEventListener("click",(function(t){t.stopPropagation()}),!1),t.appendChild(o),o}function o(t,n){const e=document.createTextNode(n);return t.appendChild(e),e}function i(t,n){const e=document.createElement(t);return n&&(e.className=n),e}function r(t,n){return t.length<=n?String(t):t.substr(0,n-1)+"…"}function s(t,n,e){return te&&(t=e),t}function a(t){document.adoptedStyleSheets=[...document.adoptedStyleSheets,t]}function l(t,n){const e=t[3];return[(1-e)*n[0]+e*t[0],(1-e)*n[1]+e*t[1],(1-e)*n[2]+e*t[2],e+n[3]*(1-e)]}function c([t,n,e]){const o=Math.max(t,n,e),i=Math.min(t,n,e),r=o-i;let s;return s=i===o?0:t===o?(1/6*(n-e)/r+1)%1:n===o?1/6*(e-t)/r+1/3:1/6*(t-n)/r+2/3,s}function d([t,n,e]){return.2126*(t<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4))+.7152*(n<=.04045?n/12.92:Math.pow((n+.055)/1.055,2.4))+.0722*(e<=.04045?e/12.92:Math.pow((e+.055)/1.055,2.4))}const h=.027,p=.1,u=5e-4;function m([t,n,e]){return.2126729*Math.pow(t,2.4)+.7151522*Math.pow(n,2.4)+.072175*Math.pow(e,2.4)}function g(t,n){return function(t,n){if(t=f(t),n=f(n),Math.abs(t-n)t?(e=1.14*(Math.pow(n,.56)-Math.pow(t,.57)),e=e-.1?0:e+h);return 100*e}(m(l(t,n)),m(n))}function f(t){return t>.022?t:t+Math.pow(.022-t,1.414)}const x=[[12,-1,-1,-1,-1,100,90,80,-1,-1],[14,-1,-1,-1,100,90,80,60,60,-1],[16,-1,-1,100,90,80,60,55,50,50],[18,-1,-1,90,80,60,55,50,40,40],[24,-1,100,80,60,55,50,40,38,35],[30,-1,90,70,55,50,40,38,35,40],[36,-1,80,60,50,40,38,35,30,25],[48,100,70,55,40,38,35,30,25,20],[60,90,60,50,38,35,30,25,20,20],[72,80,55,40,35,30,25,20,20,20],[96,70,50,35,30,25,20,20,20,20],[120,60,40,30,25,20,20,20,20,20]];x.reverse();const b={aa:3,aaa:4.5},y={aa:4.5,aaa:7};function v(t,n){return function(t,n){const e=72*parseFloat(t.replace("px",""))/96;return(isNaN(Number(n))?["bold","bolder"].includes(n):Number(n)>=600)?e>=14:e>=18}(t,n)?b:y}function w(t,n,e,o=1){e?.color&&(t.save(),t.translate(.5,.5),t.lineWidth=o,"dashed"===e.pattern&&t.setLineDash([3,3]),"dotted"===e.pattern&&t.setLineDash([2,2]),t.strokeStyle=e.color,t.stroke(n),t.restore())}function A(t,n,e,o,i){i&&(t.save(),i.fillColor&&(t.fillStyle=i.fillColor,t.fill(n)),i.hatchColor&&F(t,n,e,10,i.hatchColor,o,!1),t.restore())}function M(t,n,e){let o=0;function i(i){const r=[];for(let s=0;sparseInt(t,16)/255))}function B(t,n){if("rgb"===n){const[n,e,o,i]=t;return`rgb(${(255*n).toFixed()} ${(255*e).toFixed()} ${(255*o).toFixed()}${1===i?"":" / "+Math.round(100*i)/100})`}if("hsl"===n){const[n,e,o,i]=function([t,n,e,o]){const i=Math.max(t,n,e),r=Math.min(t,n,e),s=i-r,a=i+r,l=.5*a;let d;return d=0===l||1===l?0:l<=.5?s/a:s/(2-a),[c([t,n,e]),d,l,o]}(t);return`hsl(${Math.round(360*n)}deg ${Math.round(100*e)} ${Math.round(100*o)}${1===i?"":" / "+Math.round(100*(i??1))/100})`}if("hwb"===n){const[n,e,o,i]=function([t,n,e,o]){const i=c([t,n,e]),r=Math.max(t,n,e);return[i,Math.min(t,n,e),1-r,o]}(t);return`hwb(${Math.round(360*n)}deg ${Math.round(100*e)} ${Math.round(100*o)}${1===i?"":" / "+Math.round(100*(i??1))/100})`}throw new Error("NOT_REACHED")}function H(t,n,e,o,i,r,s){t.save();const a=M(n,r,s);return e&&(t.fillStyle=e,t.fill(a)),o&&("dashed"===i&&t.setLineDash([3,3]),"dotted"===i&&t.setLineDash([2,2]),t.lineWidth=2,t.strokeStyle=o,t.stroke(a)),t.restore(),a}const X=20,W="#1A73E8";function I(t,n,o,i,r,s,a=new DOMMatrix){const l=`grid-${r.gridLayerCounter++}-labels`;let c=document.getElementById(l);if(!c){const t=document.getElementById("grid-label-container");if(!t)throw new Error("#grid-label-container is not found");c=e(t,"div"),c.id=l}const d=t.gridHighlightConfig?.rowLineColor?t.gridHighlightConfig.rowLineColor:W,h=et(d);c.style.setProperty("--row-label-color",d),c.style.setProperty("--row-label-text-color",h);const p=t.gridHighlightConfig?.columnLineColor?t.gridHighlightConfig.columnLineColor:W,u=et(p);c.style.setProperty("--column-label-color",p),c.style.setProperty("--column-label-text-color",u),c.innerText="";const m=e(c,"div","area-names"),g=e(c,"div","line-names"),f=e(c,"div","line-numbers"),x=e(c,"div","track-sizes"),b=function(t,n){const e=Math.round(n.maxX-n.minX),o=Math.round(n.maxY-n.minY),i={rows:{positive:{positions:[],hasFirst:!1,hasLast:!1},negative:{positions:[],hasFirst:!1,hasLast:!1}},columns:{positive:{positions:[],hasFirst:!1,hasLast:!1},negative:{positions:[],hasFirst:!1,hasLast:!1}},bounds:{minX:Math.round(n.minX),maxX:Math.round(n.maxX),minY:Math.round(n.minY),maxY:Math.round(n.maxY),allPoints:n.allPoints,width:e,height:o}};if(t.gridHighlightConfig?.showLineNames){const n=T(t.rowLineNameOffsets||[]),e={positions:n.positions,names:n.names,hasFirst:!!n.positions.length&&R(n.positions).y===i.bounds.minY,hasLast:!!n.positions.length&&D(n.positions).y===i.bounds.maxY};i.rows.positive=e;const o=T(t.columnLineNameOffsets||[]),r={positions:o.positions,names:o.names,hasFirst:!!o.positions.length&&R(o.positions).x===i.bounds.minX,hasLast:!!o.positions.length&&D(o.positions).x===i.bounds.maxX};i.columns.positive=r}else{const n=({x:t,y:n})=>({x:Math.round(t),y:Math.round(n)});t.positiveRowLineNumberPositions&&(i.rows.positive={positions:t.positiveRowLineNumberPositions.map(n),hasFirst:Math.round(R(t.positiveRowLineNumberPositions).y)===i.bounds.minY,hasLast:Math.round(D(t.positiveRowLineNumberPositions).y)===i.bounds.maxY}),t.negativeRowLineNumberPositions&&(i.rows.negative={positions:t.negativeRowLineNumberPositions.map(n),hasFirst:Math.round(R(t.negativeRowLineNumberPositions).y)===i.bounds.minY,hasLast:Math.round(D(t.negativeRowLineNumberPositions).y)===i.bounds.maxY}),t.positiveColumnLineNumberPositions&&(i.columns.positive={positions:t.positiveColumnLineNumberPositions.map(n),hasFirst:Math.round(R(t.positiveColumnLineNumberPositions).x)===i.bounds.minX,hasLast:Math.round(D(t.positiveColumnLineNumberPositions).x)===i.bounds.maxX}),t.negativeColumnLineNumberPositions&&(i.columns.negative={positions:t.negativeColumnLineNumberPositions.map(n),hasFirst:Math.round(R(t.negativeColumnLineNumberPositions).x)===i.bounds.minX,hasLast:Math.round(D(t.negativeColumnLineNumberPositions).x)===i.bounds.maxX})}return i}(t,n);t.gridHighlightConfig?.showLineNames?function(t,n,e,o,i=new DOMMatrix,r="horizontal-tb"){for(const[s,a]of n.columns.positive.positions.entries()){j(N(t,Q(n.columns.positive.names[s]),"column"),S(a,i),n,r,e,o)}for(const[s,a]of n.rows.positive.positions.entries()){Z(N(t,Q(n.rows.positive.names[s]),"row"),S(a,i),n,r,e,o)}}(g,b,i,s,a,t.writingMode):function(t,n,e,o,i=new DOMMatrix,r="horizontal-tb"){if(!n.columns.positive.names)for(const[s,a]of U(n.columns.positive.positions,"x")){j(N(t,(s+1).toString(),"column"),S(a,i),n,r,e,o)}if(!n.rows.positive.names)for(const[s,a]of U(n.rows.positive.positions,"y")){Z(N(t,(s+1).toString(),"row"),S(a,i),n,r,e,o)}for(const[s,a]of U(n.columns.negative.positions,"x")){q(N(t,(-1*n.columns.negative.positions.length+s).toString(),"column"),S(a,i),n,r,e,o)}for(const[s,a]of U(n.rows.negative.positions,"y")){V(N(t,(-1*n.rows.negative.positions.length+s).toString(),"row"),S(a,i),n,r,e,o)}}(f,b,i,s,a,t.writingMode),function(t,n,e=new DOMMatrix,o="horizontal-tb"){for(const{name:i,bounds:r}of n){const n=N(t,i,"row"),{width:s,height:a}=_(n,o),l=S("vertical-rl"===o||"sideways-rl"===o?r.allPoints[3]:"sideways-lr"===o?r.allPoints[1]:r.allPoints[0],e),c=r.allPoints[1].x20,l=!s&&Math.abs(t[t.length-1][n]-i[n])>20;(r||s||a&&l)&&(yield[o,i],e=i)}}const D=t=>t[t.length-1],R=t=>t[0];function T(t){const n=[],e=[];for(const{name:o,x:i,y:r}of t){const t=Math.round(i),s=Math.round(r),a=n.findIndex((({x:n,y:e})=>n===t&&e===s));a>-1?e[a].push(o):(n.push({x:t,y:s}),e.push([o]))}return{positions:n,names:e}}function O(t,n,e,o,i,r=new DOMMatrix,s="horizontal-tb"){const{main:a,cross:l}=K(s),{crossSize:c}=G(s,o);for(const{x:o,y:d,computedSize:h,authoredSize:p}of n){const n=S({x:o,y:d},r),u=h.toFixed(2),m=N(t,`${p?p+"·":""}${`${u.endsWith(".00")?u.slice(0,-3):u}px`}`,e),g=_(m,s);let f=n[a]-g.mainSizet));else{const e=t.match(/[0-9.]+/g);if(!e)return null;n=e.slice(0,3).map((t=>parseInt(t,10)/255))}return n.length?d(n)>.2?"#121212":"white":null}function ot(t){return t.startsWith("horizontal")}function it(t){return"vertical-rl"===t||"sideways-rl"===t}function rt(t,n,e,o,i,r,s){const a=C(),l=M(t.gridBorder,a,r);n.save(),function(t,n,e){if(ot(t))return;const o=n.allPoints[0],i=n.allPoints[1],r=n.allPoints[3];e.translate(o.x,o.y),("vertical-rl"===t||"sideways-rl"===t)&&(e.rotate(90*Math.PI/180),e.translate(0,-1*(r.y-o.y)));"vertical-lr"===t&&(e.rotate(90*Math.PI/180),e.scale(1,-1));"sideways-lr"===t&&(e.rotate(-90*Math.PI/180),e.translate(-1*(i.x-o.x),0));e.translate(-1*o.x,-1*o.y)}(t.writingMode,a,n),t.gridHighlightConfig.gridBackgroundColor&&(n.fillStyle=t.gridHighlightConfig.gridBackgroundColor,n.fill(l)),t.gridHighlightConfig.gridBorderColor&&(n.save(),n.translate(.5,.5),n.lineWidth=0,t.gridHighlightConfig.gridBorderDash&&n.setLineDash([3,3]),n.strokeStyle=t.gridHighlightConfig.gridBorderColor,n.stroke(l),n.restore());const c=st(n,t,"row",r),d=st(n,t,"column",r);lt(n,t.rowGaps,t.gridHighlightConfig.rowGapColor,t.gridHighlightConfig.rowHatchColor,t.rotationAngle,r,!0),lt(n,t.columnGaps,t.gridHighlightConfig.columnGapColor,t.gridHighlightConfig.columnHatchColor,t.rotationAngle,r,!1);const h=function(t,n,e,o){if(!n||!Object.keys(n).length)return[];t.save(),e&&(t.strokeStyle=e);t.lineWidth=2;const i=[];for(const e in n){const r=n[e],s=C(),a=M(r,s,o);t.stroke(a),i.push({name:e,bounds:s})}return t.restore(),i}(n,t.areaNames,t.gridHighlightConfig.areaBorderColor,r),p=n.getTransform();p.scaleSelf(1/e),n.restore(),t.gridHighlightConfig.showGridExtensionLines&&(c&&at(n,c,t.gridHighlightConfig.rowLineColor,t.gridHighlightConfig.rowLineDash,p,o,i),d&&at(n,d,t.gridHighlightConfig.columnLineColor,t.gridHighlightConfig.columnLineDash,p,o,i)),I(t,a,h,{canvasWidth:o,canvasHeight:i},s,r,p)}function st(t,n,e,o){const i=n[`${e}s`],r=n.gridHighlightConfig[`${e}LineColor`],s=n.gridHighlightConfig[`${e}LineDash`];if(!r)return null;const a=C(),l=M(i,a,o);return t.save(),t.translate(.5,.5),s&&t.setLineDash([3,3]),t.lineWidth=0,t.strokeStyle=r,t.save(),t.stroke(l),t.restore(),t.restore(),a}function at(t,n,e,o,i,r,s){t.save(),t.strokeStyle=e,t.lineWidth=1,t.translate(.5,.5),o&&t.setLineDash([3,3]);for(let e=0;e\');\n}\n\n.element-layout-type.flex {\n background-image: url(\'data:image/svg+xml,\');\n}\n\n.element-description {\n flex: 1 1;\n font-weight: bold;\n word-wrap: break-word;\n word-break: break-all;\n}\n\n.dimensions {\n color: var(--sys-color-outline);\n text-align: right;\n margin-left: 10px;\n}\n\n.material-node-width {\n margin-right: 2px;\n}\n\n.material-node-height {\n margin-left: 2px;\n}\n\n.material-tag-name {\n /* Keep this in sync with inspectorCommon.css (--override-dom-tag-name-color) */\n color: rgb(136 18 128);\n}\n\n.material-class-name,\n.material-node-id {\n /* Keep this in sync with inspectorCommon.css (.webkit-html-attribute-value) */\n color: rgb(26 26 166);\n}\n\n.contrast-text {\n width: 16px;\n height: 16px;\n text-align: center;\n line-height: 16px;\n margin-right: 8px;\n border: 1px solid rgb(0 0 0 / 10%);\n padding: 0 1px;\n}\n\n.a11y-icon-not-ok {\n background-image: url(\'data:image/svg+xml,\');\n}\n\n.a11y-icon-warning {\n background-image: url(\'data:image/svg+xml,\');\n}\n\n.a11y-icon-ok {\n background-image: url(\'data:image/svg+xml,\');\n}\n\n@media (forced-colors: active) {\n :root,\n body {\n background-color: transparent;\n forced-color-adjust: none;\n }\n\n .tooltip-content {\n border-color: Highlight;\n background-color: canvas;\n forced-color-adjust: none;\n }\n\n .tooltip-content::after {\n background-color: Highlight;\n }\n\n .color-swatch-inner,\n .contrast-text,\n .separator {\n border-color: Highlight;\n }\n\n .section-name {\n color: Highlight;\n }\n\n .dimensions,\n .element-info-name,\n .element-info-value-color,\n .element-info-value-contrast,\n .element-info-value-icon,\n .element-info-value-text,\n .material-tag-name,\n .material-class-name,\n .material-node-id {\n color: canvastext;\n }\n}\n');function pt(t,n,e,o){const{baseSize:i,isHorizontalFlow:r}=t,s=vt(n),a=r?{p1:s.p1,p2:Lt(s.p1,s.p2,i),p3:Lt(s.p4,s.p3,i),p4:s.p4}:{p1:s.p1,p2:s.p2,p3:Lt(s.p2,s.p3,i),p4:Lt(s.p1,s.p4,i)};!function(t,n,e,o,i){const r=t.flexItemHighlightConfig,s=C(),a=M((c=e,["M",c.p1.x,c.p1.y,"L",c.p2.x,c.p2.y,"L",c.p3.x,c.p3.y,"L",c.p4.x,c.p4.y,"Z"]),s,i),l=Math.atan2(n.p4.y-n.p1.y,n.p4.x-n.p1.x)+45*Math.PI/180;var c;A(o,a,s,l,r.baseSizeBox),w(o,a,r.baseSizeBorder)}(t,s,a,e,o),function(t,n,e,o,i){const{isHorizontalFlow:r}=t,s=t.flexItemHighlightConfig;if(!s.flexibilityArrow)return;const a=r?{x:(e.p2.x+e.p3.x)/2,y:(e.p2.y+e.p3.y)/2}:{x:(e.p4.x+e.p3.x)/2,y:(e.p4.y+e.p3.y)/2},l=r?{x:(n.p2.x+n.p3.x)/2,y:(n.p2.y+n.p3.y)/2}:{x:(n.p4.x+n.p3.x)/2,y:(n.p4.y+n.p3.y)/2};if(l.x===a.x&&l.y===a.y)return;const c=yt([a,l]);if(w(o,M(c,C(),i),s.flexibilityArrow,1),!s.flexibilityArrow.color)return;const d=M(["M",l.x-5,l.y-5,"L",l.x,l.y,"L",l.x-5,l.y+5],C(),i),h=Math.atan2(l.y-a.y,l.x-a.x);o.save(),o.translate(l.x+.5,l.y+.5),o.rotate(h),o.translate(-l.x-.5,-l.y-.5),w(o,d,s.flexibilityArrow,1),o.restore()}(t,s,a,e,o)}function ut(t,n,e){const o=t.flexContainerHighlightConfig,i=C(),r=M(t.containerBorder,i,e),{isHorizontalFlow:s,isReverse:a,lines:l}=t;if(w(n,r,o.containerBorder),!l?.length)return;const c=function(t,n,e,o){const i=vt(t),r=[];for(const t of n){if(!t.length)continue;let s=vt(t[0].itemBorder);const a=[];for(const{itemBorder:n}of t){const t=vt(n);s=s?wt(s,t,e,o):t,a.push(t)}const l=1===n.length?i:At(s,i,e),c=a.map((t=>At(t,l,!e)));r.push({quad:l,items:a,extendedItems:c})}return r}(t.containerBorder,l,s,a);!function(t,n,e,o,i){const r=t.flexContainerHighlightConfig,s=o.map(((t,n)=>{const e=o[n+1]?.quad;return{path:i?xt(t.quad,e):bt(t.quad,e),items:t.extendedItems.map(((n,e)=>{const o=t.extendedItems[e+1]&&t.extendedItems[e+1];return i?bt(n,o):xt(n,o)}))}})),a=s.length>1;for(const{path:t,items:o}of s){for(const t of o)w(n,M(t,C(),e),r.itemSeparator);a&&w(n,M(t,C(),e),r.lineSeparator)}}(t,n,e,c,s),function(t,n,e,o,i){const{isHorizontalFlow:r}=t,{mainDistributedSpace:s,crossDistributedSpace:a,rowGapSpace:l,columnGapSpace:c}=t.flexContainerHighlightConfig,d=r?c:l,h=r?l:c,p=s&&Boolean(s.fillColor||s.hatchColor),u=i.length>1&&a&&Boolean(a.fillColor||a.hatchColor),m=d&&Boolean(d.fillColor||d.hatchColor),g=i.length>1&&h&&Boolean(h.fillColor||h.hatchColor),f=s&&a&&d&&h&&s.fillColor===a.fillColor&&s.hatchColor===a.hatchColor&&s.fillColor===d.fillColor&&s.hatchColor===d.hatchColor&&s.fillColor===h.fillColor&&s.hatchColor===h.hatchColor,x=vt(o);if(f){return void gt(x,i.map((t=>t.extendedItems)).flat().map((t=>t)),s,n,e)}const b=function(t,n){const{crossGap:e,mainGap:o,isHorizontalFlow:i,isReverse:r}=t,s=[],a=[];if(e&&n.length>1)for(let t=0,o=t+1;tt.quad)),...g?b.crossGaps:[]],a,n,e)}if(p)for(const[t,o]of i.entries()){const i=[...o.extendedItems,...m?b.mainGaps[t]:[]];gt(o.quad,i,s,n,e)}if(g)for(const t of b.crossGaps)gt(t,[],h,n,e);if(m)for(const t of b.mainGaps)for(const o of t)gt(o,[],d,n,e)}(t,n,e,t.containerBorder,c),function(t,n,e,o,i){o.forEach((({quad:o,items:r},s)=>{!function(t,n,e,o,i,r){const{alignItemsStyle:s,isHorizontalFlow:a}=t,{crossAlignment:l}=t.flexContainerHighlightConfig;if(!l?.color)return;const c=[];switch(s){case"flex-start":c.push([a?o.p1:o.p4,a?o.p2:o.p1]);break;case"flex-end":c.push([a?o.p3:o.p2,a?o.p4:o.p3]);break;case"center":a?(c.push([{x:(o.p1.x+o.p4.x)/2,y:(o.p1.y+o.p4.y)/2},{x:(o.p2.x+o.p3.x)/2,y:(o.p2.y+o.p3.y)/2}]),c.push([{x:(o.p2.x+o.p3.x)/2,y:(o.p2.y+o.p3.y)/2},{x:(o.p1.x+o.p4.x)/2,y:(o.p1.y+o.p4.y)/2}])):(c.push([{x:(o.p1.x+o.p2.x)/2,y:(o.p1.y+o.p2.y)/2},{x:(o.p3.x+o.p4.x)/2,y:(o.p3.y+o.p4.y)/2}]),c.push([{x:(o.p3.x+o.p4.x)/2,y:(o.p3.y+o.p4.y)/2},{x:(o.p1.x+o.p2.x)/2,y:(o.p1.y+o.p2.y)/2}]));break;case"stretch":case"normal":c.push([a?o.p1:o.p4,a?o.p2:o.p1]),c.push([a?o.p3:o.p2,a?o.p4:o.p3]);break;case"baseline":if(a){const t=i[0],n=Mt([t.p1,t.p2],[o.p2,o.p3]),e=Mt([t.p1,t.p2],[o.p1,o.p4]),s=r[0],a=Math.atan2(t.p4.y-t.p1.y,t.p4.x-t.p1.x);c.push([{x:n.x+s*Math.cos(a),y:n.y+s*Math.sin(a)},{x:e.x+s*Math.cos(a),y:e.y+s*Math.sin(a)}])}}for(const o of c){w(n,M(yt(o),C(),e),l,2),mt(t,n,e,o[0],o[1])}}(t,n,e,o,r,i[s])}))}(t,n,e,c,l.map((t=>t.map((t=>t.baseline)))))}function mt(t,n,e,o,i){const{crossAlignment:r}=t.flexContainerHighlightConfig;if(!r?.color)return;const s=Math.atan2(i.y-o.y,i.x-o.x),a={x:-2*Math.cos(s-.5*Math.PI)+(o.x+i.x)/2,y:-2*Math.sin(s-.5*Math.PI)+(o.y+i.y)/2},l=M(["M",a.x,a.y,"L",a.x+5.5,a.y+6,"L",a.x+2.5,a.y+6,"L",a.x+2.5,a.y+6+5,"L",a.x-2.5,a.y+6+5,"L",a.x-2.5,a.y+6,"L",a.x-5.5,a.y+6,"Z"],C(),e);n.save(),n.translate(a.x,a.y),n.rotate(s),n.translate(-a.x,-a.y),n.fillStyle=r.color,n.fill(l),n.lineWidth=1,n.strokeStyle="white",n.stroke(l),n.restore()}function gt(t,n,e,o,i){if(e){if(e.fillColor){const r=Y(t,n,C(),i);o.fillStyle=e.fillColor,o.fill(r)}if(e.hatchColor){const r=180*Math.atan2(t.p2.y-t.p1.y,t.p2.x-t.p1.x)/Math.PI,s=C();F(o,Y(t,n,s,i),s,10,e.hatchColor,r,!1)}}}function ft(t,n,e,o,i){i&&([t,n]=[n,t]);const r=o?Math.atan2(t.p4.y-t.p1.y,t.p4.x-t.p1.x):Math.atan2(t.p2.y-t.p1.y,t.p2.x-t.p1.x),s=St(o?t.p4:t.p2,n.p1),a=s/2-e/2,l=s/2+e/2;return o?{p1:{x:Math.round(t.p4.x+a*Math.cos(r)),y:Math.round(t.p4.y+a*Math.sin(r))},p2:{x:Math.round(t.p3.x+a*Math.cos(r)),y:Math.round(t.p3.y+a*Math.sin(r))},p3:{x:Math.round(t.p3.x+l*Math.cos(r)),y:Math.round(t.p3.y+l*Math.sin(r))},p4:{x:Math.round(t.p4.x+l*Math.cos(r)),y:Math.round(t.p4.y+l*Math.sin(r))}}:{p1:{x:Math.round(t.p2.x+a*Math.cos(r)),y:Math.round(t.p2.y+a*Math.sin(r))},p2:{x:Math.round(t.p2.x+l*Math.cos(r)),y:Math.round(t.p2.y+l*Math.sin(r))},p3:{x:Math.round(t.p3.x+l*Math.cos(r)),y:Math.round(t.p3.y+l*Math.sin(r))},p4:{x:Math.round(t.p3.x+a*Math.cos(r)),y:Math.round(t.p3.y+a*Math.sin(r))}}}function xt(t,n){const e=n&&t.p4.y===n.p1.y,o=["M",t.p1.x,t.p1.y,"L",t.p2.x,t.p2.y];return e?o:[...o,"M",t.p3.x,t.p3.y,"L",t.p4.x,t.p4.y]}function bt(t,n){const e=n&&t.p2.x===n.p1.x,o=["M",t.p1.x,t.p1.y,"L",t.p4.x,t.p4.y];return e?o:[...o,"M",t.p3.x,t.p3.y,"L",t.p2.x,t.p2.y]}function yt(t){return["M",t[0].x,t[0].y,"L",t[1].x,t[1].y]}function vt(t){return{p1:{x:t[1],y:t[2]},p2:{x:t[4],y:t[5]},p3:{x:t[7],y:t[8]},p4:{x:t[10],y:t[11]}}}function wt(t,n,e,o){o&&([t,n]=[n,t]);const i=e?[t.p1,t.p4]:[t.p1,t.p2],r=e?[n.p2,n.p3]:[n.p4,n.p3],s=e?[t.p1,t.p2]:[t.p1,t.p4],a=e?[t.p4,t.p3]:[t.p2,t.p3],l=e?[n.p1,n.p2]:[n.p1,n.p4],c=e?[n.p4,n.p3]:[n.p2,n.p3];let d,h,p,u;return e?(d=Mt(i,l),Ct(i,d)&&(d=t.p1),h=Mt(r,s),Ct(r,h)&&(h=n.p2),p=Mt(r,a),Ct(r,p)&&(p=n.p3),u=Mt(i,c),Ct(i,u)&&(u=t.p4)):(d=Mt(i,l),Ct(i,d)&&(d=t.p1),h=Mt(i,c),Ct(i,h)&&(h=t.p2),p=Mt(r,a),Ct(r,p)&&(p=n.p3),u=Mt(r,s),Ct(r,u)&&(u=n.p4)),{p1:d,p2:h,p3:p,p4:u}}function At(t,n,e){return{p1:e?Mt([n.p1,n.p4],[t.p1,t.p2]):Mt([n.p1,n.p2],[t.p1,t.p4]),p2:e?Mt([n.p2,n.p3],[t.p1,t.p2]):Mt([n.p1,n.p2],[t.p2,t.p3]),p3:e?Mt([n.p2,n.p3],[t.p3,t.p4]):Mt([n.p3,n.p4],[t.p2,t.p3]),p4:e?Mt([n.p1,n.p4],[t.p3,t.p4]):Mt([n.p3,n.p4],[t.p1,t.p4])}}function Mt([t,n],[e,o]){const i=((t.x*n.y-t.y*n.x)*(e.x-o.x)-(t.x-n.x)*(e.x*o.y-e.y*o.x))/((t.x-n.x)*(e.y-o.y)-(t.y-n.y)*(e.x-o.x)),r=((t.x*n.y-t.y*n.x)*(e.y-o.y)-(t.y-n.y)*(e.x*o.y-e.y*o.x))/((t.x-n.x)*(e.y-o.y)-(t.y-n.y)*(e.x-o.x));return{x:Object.is(i,-0)?0:i,y:Object.is(r,-0)?0:r}}function Ct([t,n],e){return!(t.xn.x))&&(!(t.x>n.x&&(e.x>t.x||e.xn.y))&&(!(t.y>n.y&&(e.y>t.y||e.y{t.stopPropagation(),t.preventDefault(),this.originX=void 0,this.originY=void 0,this.document.body.style.cursor="default",this.document.body.removeEventListener("mousemove",e),this.document.body.addEventListener("mousemove",this.boundMousemove)};this.document.body.addEventListener("mouseup",o,{once:!0}),window.addEventListener("mouseout",o,{once:!0}),this.document.body.addEventListener("mousemove",e)}onDrag(t,n){if(!this.originX&&!this.originY)return;let e,o;if(this.originX){const t=this.originX.coord-n.clientX;e=Math.round(this.originX.value-t)}if(this.originY){const t=this.originY.coord-n.clientY;o=Math.round(this.originY.value-t)}t.update({width:e,height:o})}}function Pt(t,n){return"start"===n?{x:(t.minX+t.maxX)/2,y:t.minY}:"center"===n?{x:(t.minX+t.maxX)/2,y:(t.minY+t.maxY)/2}:"end"===n?{x:(t.minX+t.maxX)/2,y:t.maxY}:void 0}function Ft(t,n,e){let o=0,i=!0;n.x===e.minX?(o=-.5*Math.PI,i=!1):n.x===e.maxX?(o=.5*Math.PI,i=!1):n.y===e.minY?(o=0,i=!1):n.y===e.maxY&&(o=Math.PI,i=!1);const r=o+(i?2*Math.PI:Math.PI);t.save(),t.beginPath(),t.lineWidth=5,t.strokeStyle="white",t.arc(n.x,n.y,6,o,r),t.stroke(),t.fillStyle="#4585f6",t.arc(n.x,n.y,4,o,r),t.fill(),t.restore()}function Yt(t,n,e){!function(t,n,e){H(n,t.paddingBox,t.scrollPaddingColor,void 0,void 0,C(),e),n.save(),n.globalCompositeOperation="destination-out",H(n,t.snapport,"white",void 0,void 0,C(),e),n.restore()}(t,n,e);const o=function(t,n,e){const o=[];for(const i of t.snapAreas){const r=C();H(n,i.path,t.scrollMarginColor,t.snapAreaBorder.color,t.snapAreaBorder.pattern,r,e),n.save(),n.globalCompositeOperation="destination-out",H(n,i.borderBox,"white",void 0,void 0,C(),e),n.restore(),o.push(r)}return o}(t,n,e);!function(t,n,e){H(n,t.snapport,void 0,t.snapportBorder.color,void 0,C(),e)}(t,n,e),function(t,n,e){for(let r=0;r{const o=n.isPointInDraggablePath(t,e);if(o)return{type:o.type,initialWidth:o.initialWidth,initialHeight:o.initialHeight,id:o.highlightIndex,update:({width:t,height:n})=>{window.InspectorOverlayHost.send({highlightType:"isolatedElement",highlightIndex:o.highlightIndex,newWidth:`${t}px`,newHeight:`${n}px`,resizerType:o.type})}}}})),this.dragHandler.install()),this.context.save();const{widthPath:e,heightPath:o,bidirectionPath:i,currentWidth:r,currentHeight:s,highlightIndex:a}=function(t,n,e,o,i){const{currentX:r,currentY:s,currentWidth:a,currentHeight:l,highlightIndex:c}=t;n.save(),n.fillStyle=t.isolationModeHighlightConfig.maskColor,n.fillRect(0,0,e,o),n.clearRect(r,s,a,l),n.restore();const d=C(),h=M(t.widthResizerBorder,d,i);A(n,h,d,0,{fillColor:t.isolationModeHighlightConfig.resizerColor});const p=M(t.heightResizerBorder,d,i);A(n,p,d,0,{fillColor:t.isolationModeHighlightConfig.resizerColor});const u=M(t.bidirectionResizerBorder,d,i);return A(n,u,d,0,{fillColor:t.isolationModeHighlightConfig.resizerColor}),{widthPath:h,heightPath:p,bidirectionPath:u,currentWidth:a,currentHeight:l,highlightIndex:c}}(t,this.context,this.canvasWidth,this.canvasHeight,this.emulationScaleFactor);this.draggableBorders.set(a,{widthPath:e,heightPath:o,bidirectionPath:i,highlightIndex:a,initialWidth:r,initialHeight:s}),this.context.restore()}isPointInDraggablePath(t,n){for(const{widthPath:e,heightPath:o,bidirectionPath:i,highlightIndex:r,initialWidth:s,initialHeight:a}of this.draggableBorders.values()){if(this.context.isPointInPath(e,t,n))return{type:"width",highlightIndex:r,initialWidth:s};if(this.context.isPointInPath(o,t,n))return{type:"height",highlightIndex:r,initialHeight:a};if(this.context.isPointInPath(i,t,n))return{type:"bidirection",highlightIndex:r,initialWidth:s,initialHeight:a}}}}function Bt(t){return 0===t[3]}const Ht="rgba(0,0,0,0.2)",Xt="rgba(0,0,0,0.7)",Wt="rgba(255, 255, 255, 0.8)";const It="rgba(128, 128, 128, 0.3)";const Ut=new CSSStyleSheet;Ut.replaceSync('/*\n * Copyright 2021 The Chromium Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style license that can be\n * found in the LICENSE file.\n */\n\nbody {\n background-color: rgb(0 0 0 / 31%);\n}\n\n.controls-line {\n display: flex;\n justify-content: center;\n margin: 10px 0;\n}\n\n.message-box {\n padding: 2px 4px;\n display: flex;\n align-items: center;\n cursor: default;\n overflow: hidden;\n}\n\n#paused-in-debugger {\n white-space: nowrap;\n text-overflow: ellipsis;\n overflow: hidden;\n}\n\n.controls-line > * {\n background-color: rgb(255 255 194);\n border: 1px solid rgb(202 202 202);\n height: 22px;\n box-sizing: border-box;\n}\n\n.controls-line .button {\n width: 26px;\n margin-left: -1px;\n margin-right: 0;\n padding: 0;\n flex-shrink: 0;\n flex-grow: 0;\n cursor: pointer;\n}\n\n.controls-line .button .glyph {\n width: 100%;\n height: 100%;\n background-color: rgb(0 0 0 / 75%);\n opacity: 80%;\n mask-repeat: no-repeat;\n mask-position: center;\n position: relative;\n}\n\n.controls-line .button:active .glyph {\n top: 1px;\n left: 1px;\n}\n\n#resume-button .glyph {\n mask-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAAKCAYAAABv7tTEAAAAAXNSR0IArs4c6QAAAFJJREFUKM+10bEJgGAMBeEPbR3BLRzEVdzEVRzELRzBVohVwEJ+iODBlQfhBeJhsmHU4C0KnFjQV6J0x1SNAhdWDJUoPTB3PvLLeaUhypM3n3sD/qc7lDrdpIEAAAAASUVORK5CYII=");\n mask-size: 13px 10px;\n background-color: rgb(66 129 235);\n}\n\n#step-over-button .glyph {\n mask-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAAKCAYAAAC5Sw6hAAAAAXNSR0IArs4c6QAAAOFJREFUKM+N0j8rhXEUB/DPcxW35CqhvIBrtqibkklhV8qkTHe4ZbdblcXgPVhuMdqUTUl5A2KRRCF5LGc4PT1P7qnfcr5/zu/8KdTHLFaxjHnc4RZXKI0QYxjgLQTVd42l/0wmg5iFX3iq5H6w22RS4DyRH7CB8cAXcBTGJT6xUmd0mEwuMdFQcA3fwXvGTAan8BrgPabTL9fRRyfx91PRMwyjGwcJ2EyCfsrfpPw2Pipz24NT/MZciiQYVshzOKnZ5Hturxt3k2MnCpS4SPkeHpPR8Sh3tYgttBoW9II2/AHiaEqvD2Fc0wAAAABJRU5ErkJggg==");\n mask-size: 18px 10px;\n}\n');const Dt=new CSSStyleSheet;Dt.replaceSync("/*\n * Copyright 2021 The Chromium Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style license that can be\n * found in the LICENSE file.\n */\n\nbody {\n cursor: crosshair;\n}\n\n#zone {\n background-color: #0003;\n border: 1px solid #fffd;\n display: none;\n position: absolute;\n}\n");let Rt=null,Tt=null;function Ot(){if(!Rt)throw new Error("Error calculating currentRect: no anchor was defined.");if(!Tt)throw new Error("Error calculating currentRect: no position was defined.");return{x:Math.min(Rt.x,Tt.x),y:Math.min(Rt.y,Tt.y),width:Math.abs(Rt.x-Tt.x),height:Math.abs(Rt.y-Tt.y)}}function Qt(){Rt=null,Tt=null}const Nt=new CSSStyleSheet;Nt.replaceSync("/*\n * Copyright 2021 The Chromium Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style license that can be\n * found in the LICENSE file.\n */\n\n:root {\n --border-radius: 4px;\n}\n\n.source-order-label-container {\n display: block;\n min-width: 20px;\n position: absolute;\n text-align: center;\n align-items: center;\n background-color: #fff;\n font-family: Menlo, Consolas, monospace;\n font-size: 12px;\n font-weight: bold;\n padding: 2px;\n border: 1.5px solid;\n}\n\n.top-corner {\n border-bottom-right-radius: var(--border-radius);\n}\n\n.bottom-corner {\n border-top-right-radius: var(--border-radius);\n}\n\n.above-element {\n border-top-right-radius: var(--border-radius);\n border-top-left-radius: var(--border-radius);\n}\n\n.below-element {\n border-bottom-right-radius: var(--border-radius);\n border-bottom-left-radius: var(--border-radius);\n}\n\n.above-element-wider {\n border-top-right-radius: var(--border-radius);\n border-top-left-radius: var(--border-radius);\n border-bottom-right-radius: var(--border-radius);\n}\n\n.below-element-wider {\n border-bottom-right-radius: var(--border-radius);\n border-bottom-left-radius: var(--border-radius);\n border-top-right-radius: var(--border-radius);\n}\n\n.bottom-corner-wider {\n border-top-right-radius: var(--border-radius);\n border-bottom-right-radius: var(--border-radius);\n}\n\n.bottom-corner-taller {\n border-top-right-radius: var(--border-radius);\n border-top-left-radius: var(--border-radius);\n}\n\n.bottom-corner-wider-taller {\n border-top-left-radius: var(--border-radius);\n border-top-right-radius: var(--border-radius);\n border-bottom-right-radius: var(--border-radius);\n}\n");const Jt=300,Kt={topCorner:"top-corner",aboveElement:"above-element",belowElement:"below-element",aboveElementWider:"above-element-wider",belowElementWider:"below-element-wider",bottomCornerWider:"bottom-corner-wider",bottomCornerTaller:"bottom-corner-taller",bottomCornerWiderTaller:"bottom-corner-wider-taller"};function Gt(t){return t%1?t.toFixed(2):String(t)}const Zt=new CSSStyleSheet;Zt.replaceSync('/*\n * Copyright 2023 The Chromium Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style license that can be\n * found in the LICENSE file.\n */\n\n:root {\n --wco-theme-color: #121212;\n --wco-icon-color: #fff;\n}\n\n.image-group {\n display: flex;\n background-color: var(--wco-theme-color);\n align-items: center;\n}\n\n.image-group-left {\n float: left;\n justify-content: flex-start;\n gap: 4px;\n padding-left: 12px;\n}\n\n.image-group-right {\n float: right;\n justify-content: flex-end;\n gap: 2px;\n padding-right: 17px;\n}\n\n.windows-right-image-group {\n width: 238px;\n height: 33px;\n}\n\n.linux-right-image-group {\n width: 196px;\n height: 34px;\n}\n\n.mac-left-image-group {\n width: 74px;\n height: 40px;\n}\n\n.mac-right-image-group {\n width: 100px;\n height: 40px;\n}\n\n.image {\n width: 33px;\n height: 33px;\n background-color: var(--wco-icon-color);\n}\n\n#mac-chevron,\n#mac-ellipsis {\n width: 40px;\n height: 40px;\n background-color: var(--wco-icon-color);\n}\n\n#close {\n mask-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAfCAYAAACPvW/2AAABhWlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw0AcxV9TpSJVBytIcchQneyiIuJUqlgEC6Wt0KqDyaUfQpOGJMXFUXAtOPixWHVwcdbVwVUQBD9AXF2cFF2kxP8lhRYxHhz34929x907QGhUmGp2xQBVs4x0Ii7m8iti4BUBhDGEfsxKzNSTmYUsPMfXPXx8vYvyLO9zf44+pWAywCcSx5huWMTrxNObls55nzjEypJCfE48btAFiR+5Lrv8xrnksMAzQ0Y2PUccIhZLHSx3MCsbKvEUcURRNcoXci4rnLc4q5Uaa92TvzBY0JYzXKc5ggQWkUQKImTUsIEKLERp1Ugxkab9uIc/7PhT5JLJtQFGjnlUoUJy/OB/8Ltbszg54SYF40D3i21/jAKBXaBZt+3vY9tungD+Z+BKa/urDWDmk/R6W4scAQPbwMV1W5P3gMsdYPhJlwzJkfw0hWIReD+jb8oDg7dA76rbW2sfpw9AlrpaugEODoGxEmWveby7p7O3f8+0+vsB9f9y2zZ6P+8AAAAGYktHRAD/AP8A/6C9p5MAAAAJcEhZcwAALiMAAC4jAXilP3YAAAAHdElNRQfnBxsWBAcQDgJxAAAAGXRFWHRDb21tZW50AENyZWF0ZWQgd2l0aCBHSU1QV4EOFwAAAPlJREFUWMPtlTFOxDAQRd8k5gYb+i1W6Si4/ym22wpatJHogWQo+JaMlAjZgS3QPClSIk/s55mxDEEQBME3rPYHdzcgAbOZLRsxnWLezcxr5u8aNpGAR2Bw935FpgfuFZNqJ28RmoFnYAQOpZTeB409KbZ6t3U1NlvcfVK5R4lMGs4yF2DaKumvCklqdverPkdl2oCTZK5mNt+kqTf65UFznYGXVpnWHlrblAGuZxdpZ3YGleksmdPXkDeXLO2UyQ2c+2kpGr1JKjXIdMChlMkLF6dtLDK1/HWGeuC4dpp0+rLUEXgF3m5xddwBHz9cHb1inCAIguAf8QkteHDWohPAIAAAAABJRU5ErkJggg==");\n}\n\n#maximize {\n mask-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAfCAYAAACPvW/2AAABhWlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw0AcxV9TpSJVBytIcchQneyiIuJUqlgEC6Wt0KqDyaUfQpOGJMXFUXAtOPixWHVwcdbVwVUQBD9AXF2cFF2kxP8lhRYxHhz34929x907QGhUmGp2xQBVs4x0Ii7m8iti4BUBhDGEfsxKzNSTmYUsPMfXPXx8vYvyLO9zf44+pWAywCcSx5huWMTrxNObls55nzjEypJCfE48btAFiR+5Lrv8xrnksMAzQ0Y2PUccIhZLHSx3MCsbKvEUcURRNcoXci4rnLc4q5Uaa92TvzBY0JYzXKc5ggQWkUQKImTUsIEKLERp1Ugxkab9uIc/7PhT5JLJtQFGjnlUoUJy/OB/8Ltbszg54SYF40D3i21/jAKBXaBZt+3vY9tungD+Z+BKa/urDWDmk/R6W4scAQPbwMV1W5P3gMsdYPhJlwzJkfw0hWIReD+jb8oDg7dA76rbW2sfpw9AlrpaugEODoGxEmWveby7p7O3f8+0+vsB9f9y2zZ6P+8AAAAGYktHRAD/AP8A/6C9p5MAAAAJcEhZcwAALiMAAC4jAXilP3YAAAAHdElNRQfnBxsWBACOapfSAAAAGXRFWHRDb21tZW50AENyZWF0ZWQgd2l0aCBHSU1QV4EOFwAAAGJJREFUWMPt07sNgDAMhOEzQhkHxqHK0GEcmp8NiGTJQHFf64culiKZmdm/RWYI2CVtk7YzIsYrrwA60B7qDeiZ3Uv6tBFXplYWqIoDOdBngWbfPrt3Tc4NSQcw6zEzM6t2A1K/HsQFSWEQAAAAAElFTkSuQmCC");\n}\n\n#minimize {\n mask-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAfCAYAAACPvW/2AAABhWlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw0AcxV9TpSJVBytIcchQneyiIuJUqlgEC6Wt0KqDyaUfQpOGJMXFUXAtOPixWHVwcdbVwVUQBD9AXF2cFF2kxP8lhRYxHhz34929x907QGhUmGp2xQBVs4x0Ii7m8iti4BUBhDGEfsxKzNSTmYUsPMfXPXx8vYvyLO9zf44+pWAywCcSx5huWMTrxNObls55nzjEypJCfE48btAFiR+5Lrv8xrnksMAzQ0Y2PUccIhZLHSx3MCsbKvEUcURRNcoXci4rnLc4q5Uaa92TvzBY0JYzXKc5ggQWkUQKImTUsIEKLERp1Ugxkab9uIc/7PhT5JLJtQFGjnlUoUJy/OB/8Ltbszg54SYF40D3i21/jAKBXaBZt+3vY9tungD+Z+BKa/urDWDmk/R6W4scAQPbwMV1W5P3gMsdYPhJlwzJkfw0hWIReD+jb8oDg7dA76rbW2sfpw9AlrpaugEODoGxEmWveby7p7O3f8+0+vsB9f9y2zZ6P+8AAAAGYktHRAD/AP8A/6C9p5MAAAAJcEhZcwAALiMAAC4jAXilP3YAAAAHdElNRQfnBxsWAzIJ/FCVAAAAGXRFWHRDb21tZW50AENyZWF0ZWQgd2l0aCBHSU1QV4EOFwAAADNJREFUWMPt0LERACEMA0E5/+ZolO4+NiUwEDK78SlRAgC8rW5G3T2SfJvsr6rpYgCAMwvylgUCKbPyMgAAAABJRU5ErkJggg==");\n}\n\n#mac-ellipsis,\n#ellipsis {\n mask-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAfCAYAAACPvW/2AAABhWlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw0AcxV9TpSJVBytIcchQneyiIuJUqlgEC6Wt0KqDyaUfQpOGJMXFUXAtOPixWHVwcdbVwVUQBD9AXF2cFF2kxP8lhRYxHhz34929x907QGhUmGp2xQBVs4x0Ii7m8iti4BUBhDGEfsxKzNSTmYUsPMfXPXx8vYvyLO9zf44+pWAywCcSx5huWMTrxNObls55nzjEypJCfE48btAFiR+5Lrv8xrnksMAzQ0Y2PUccIhZLHSx3MCsbKvEUcURRNcoXci4rnLc4q5Uaa92TvzBY0JYzXKc5ggQWkUQKImTUsIEKLERp1Ugxkab9uIc/7PhT5JLJtQFGjnlUoUJy/OB/8Ltbszg54SYF40D3i21/jAKBXaBZt+3vY9tungD+Z+BKa/urDWDmk/R6W4scAQPbwMV1W5P3gMsdYPhJlwzJkfw0hWIReD+jb8oDg7dA76rbW2sfpw9AlrpaugEODoGxEmWveby7p7O3f8+0+vsB9f9y2zZ6P+8AAAAGYktHRAD/AP8A/6C9p5MAAAAJcEhZcwAALiMAAC4jAXilP3YAAAAHdElNRQfnBxsTEiHYUPCwAAAAGXRFWHRDb21tZW50AENyZWF0ZWQgd2l0aCBHSU1QV4EOFwAAAEBJREFUWMPt0aENACAQBMENEnqj/24OjwISAmJHrrrPgyRJeitJS9JO2oqyOwboQE9Sd9qVQb++rM5XrzZJkiQY1Fw4YEmaUfMAAAAASUVORK5CYII=");\n}\n\n#mac-chevron,\n#chevron {\n mask-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAfCAYAAACPvW/2AAABhWlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw0AcxV9TpSJVBytIcchQneyiIuJUqlgEC6Wt0KqDyaUfQpOGJMXFUXAtOPixWHVwcdbVwVUQBD9AXF2cFF2kxP8lhRYxHhz34929x907QGhUmGp2xQBVs4x0Ii7m8iti4BUBhDGEfsxKzNSTmYUsPMfXPXx8vYvyLO9zf44+pWAywCcSx5huWMTrxNObls55nzjEypJCfE48btAFiR+5Lrv8xrnksMAzQ0Y2PUccIhZLHSx3MCsbKvEUcURRNcoXci4rnLc4q5Uaa92TvzBY0JYzXKc5ggQWkUQKImTUsIEKLERp1Ugxkab9uIc/7PhT5JLJtQFGjnlUoUJy/OB/8Ltbszg54SYF40D3i21/jAKBXaBZt+3vY9tungD+Z+BKa/urDWDmk/R6W4scAQPbwMV1W5P3gMsdYPhJlwzJkfw0hWIReD+jb8oDg7dA76rbW2sfpw9AlrpaugEODoGxEmWveby7p7O3f8+0+vsB9f9y2zZ6P+8AAAAGYktHRAD/AP8A/6C9p5MAAAAJcEhZcwAALiMAAC4jAXilP3YAAAAHdElNRQfnBxsTEjCy4NBCAAAAGXRFWHRDb21tZW50AENyZWF0ZWQgd2l0aCBHSU1QV4EOFwAAAKBJREFUWMPtk7sKAkEMRU/WRrtFv0hZ9J9VXD9psdFCiM0IFruYeZT3VDMh4R4yDAghhBBZWLTR3U9AB4xm9grOrIED8Dazc2Smy5BfATtgSEERmQHYpllaC92ACeiBo7tvAjI98ADuzZ9sIehiZs/cnmZC/wJrZYqEloIBr5UpFpqRmlL5e75Gf2IzoRkpajbTROhHap+uY+lmhBBCiEI+sBxN3vpZhO0AAAAASUVORK5CYII=");\n}\n\n#mac-close,\n#mac-minimize,\n#mac-maximize {\n width: 14px;\n height: 14px;\n border-radius: 50%;\n}\n\n#mac-close {\n background-color: #ff5f57;\n}\n\n#mac-minimize {\n background-color: #ffbd2e;\n}\n\n#mac-maximize {\n background-color: #28c941;\n}\n');function Vt(t){t.classList.add("hidden")}function jt(t){t.classList.remove("hidden")}function qt(t,n,e){const o=function(t){const n=i("div");for(const e of t){const t=i("div");t.id=e,t.classList.add("image"),n.append(t)}return n}(e);return o.classList.add("image-group"),o.classList.add(`image-group-${n}`),o.classList.add(`${t}-${n}-image-group`),o.classList.add("hidden"),o}a(t);const $t=new CSSStyleSheet;$t.replaceSync('\n/* Grid row and column labels */\n.grid-label-content {\n position: absolute;\n -webkit-user-select: none;\n padding: 2px;\n font-family: Menlo, monospace;\n font-size: 10px;\n min-width: 17px;\n min-height: 15px;\n border-radius: 2px;\n box-sizing: border-box;\n z-index: 1;\n background-clip: padding-box;\n pointer-events: none;\n text-align: center;\n display: flex;\n justify-content: center;\n align-items: center;\n}\n\n.grid-label-content[data-direction=row] {\n background-color: var(--row-label-color, #1A73E8);\n color: var(--row-label-text-color, #121212);\n}\n\n.grid-label-content[data-direction=column] {\n background-color: var(--column-label-color, #1A73E8);\n color: var(--column-label-text-color,#121212);\n}\n\n.line-names ul,\n.line-names .line-name {\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.line-names .line-name {\n max-width: 100px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.line-names .grid-label-content,\n.line-numbers .grid-label-content,\n.track-sizes .grid-label-content {\n border: 1px solid white;\n --inner-corner-avoid-distance: 15px;\n}\n\n.grid-label-content.top-left.inner-shared-corner,\n.grid-label-content.top-right.inner-shared-corner {\n transform: translateY(var(--inner-corner-avoid-distance));\n}\n\n.grid-label-content.bottom-left.inner-shared-corner,\n.grid-label-content.bottom-right.inner-shared-corner {\n transform: translateY(calc(var(--inner-corner-avoid-distance) * -1));\n}\n\n.grid-label-content.left-top.inner-shared-corner,\n.grid-label-content.left-bottom.inner-shared-corner {\n transform: translateX(var(--inner-corner-avoid-distance));\n}\n\n.grid-label-content.right-top.inner-shared-corner,\n.grid-label-content.right-bottom.inner-shared-corner {\n transform: translateX(calc(var(--inner-corner-avoid-distance) * -1));\n}\n\n.line-names .grid-label-content::before,\n.line-numbers .grid-label-content::before,\n.track-sizes .grid-label-content::before {\n position: absolute;\n z-index: 1;\n pointer-events: none;\n content: "";\n width: 3px;\n height: 3px;\n border: 1px solid white;\n border-width: 0 1px 1px 0;\n}\n\n.line-names .grid-label-content[data-direction=row]::before,\n.line-numbers .grid-label-content[data-direction=row]::before,\n.track-sizes .grid-label-content[data-direction=row]::before {\n background: var(--row-label-color, #1A73E8);\n}\n\n.line-names .grid-label-content[data-direction=column]::before,\n.line-numbers .grid-label-content[data-direction=column]::before,\n.track-sizes .grid-label-content[data-direction=column]::before {\n background: var(--column-label-color, #1A73E8);\n}\n\n.grid-label-content.bottom-mid::before {\n transform: translateY(-1px) rotate(45deg);\n top: 100%;\n}\n\n.grid-label-content.top-mid::before {\n transform: translateY(-3px) rotate(-135deg);\n top: 0%;\n}\n\n.grid-label-content.left-mid::before {\n transform: translateX(-3px) rotate(135deg);\n left: 0%\n}\n\n.grid-label-content.right-mid::before {\n transform: translateX(3px) rotate(-45deg);\n right: 0%;\n}\n\n.grid-label-content.right-top::before {\n transform: translateX(3px) translateY(-1px) rotate(-90deg) skewY(30deg);\n right: 0%;\n top: 0%;\n}\n\n.grid-label-content.right-bottom::before {\n transform: translateX(3px) translateY(-3px) skewX(30deg);\n right: 0%;\n top: 100%;\n}\n\n.grid-label-content.bottom-right::before {\n transform: translateX(1px) translateY(-1px) skewY(30deg);\n right: 0%;\n top: 100%;\n}\n\n.grid-label-content.bottom-left::before {\n transform: translateX(-1px) translateY(-1px) rotate(90deg) skewX(30deg);\n left: 0%;\n top: 100%;\n}\n\n.grid-label-content.left-top::before {\n transform: translateX(-3px) translateY(-1px) rotate(180deg) skewX(30deg);\n left: 0%;\n top: 0%;\n}\n\n.grid-label-content.left-bottom::before {\n transform: translateX(-3px) translateY(-3px) rotate(90deg) skewY(30deg);\n left: 0%;\n top: 100%;\n}\n\n.grid-label-content.top-right::before {\n transform: translateX(1px) translateY(-3px) rotate(-90deg) skewX(30deg);\n right: 0%;\n top: 0%;\n}\n\n.grid-label-content.top-left::before {\n transform: translateX(-1px) translateY(-3px) rotate(180deg) skewY(30deg);\n left: 0%;\n top: 0%;\n}\n\n@media (forced-colors: active) {\n .grid-label-content {\n border-color: Highlight;\n background-color: Canvas;\n color: Text;\n forced-color-adjust: none;\n }\n .grid-label-content::before {\n background-color: Canvas;\n border-color: Highlight;\n }\n}');const _t=new class extends n{tooltip;persistentOverlay;gridLabelState={gridLayerCounter:0};reset(t){super.reset(t),this.tooltip.innerHTML="",this.gridLabelState.gridLayerCounter=0,this.persistentOverlay&&this.persistentOverlay.reset(t)}install(){this.document.body.classList.add("fill");const t=this.document.createElement("canvas");t.id="canvas",t.classList.add("fill"),this.document.body.append(t);const n=this.document.createElement("div");n.id="tooltip-container",this.document.body.append(n),this.tooltip=n,this.persistentOverlay=new zt(this.window),this.persistentOverlay.renderGridMarkup(),this.persistentOverlay.setCanvas(t),this.setCanvas(t),super.install()}uninstall(){this.document.body.classList.remove("fill"),this.document.body.innerHTML="",super.uninstall()}drawHighlight(t){this.context.save();const n=C();let a=null,c=null;for(let e=t.paths.slice();e.length;){const t=e.pop();t&&(this.context.save(),H(this.context,t.path,t.fillColor,t.outlineColor,void 0,n,this.emulationScaleFactor),e.length&&(this.context.globalCompositeOperation="destination-out",H(this.context,e[e.length-1].path,"red",void 0,void 0,n,this.emulationScaleFactor)),this.context.restore(),"content"===t.name&&(a=t.path),"border"===t.name&&(c=t.path))}this.context.restore(),this.context.save();const h=Boolean(t.paths.length&&t.showRulers&&n.minX<20&&n.maxX+20=t)for(const[t,e]of[900,800,700,600,500,400,300,200,100].entries())if(o>=e){const e=n[n.length-1-t];return-1===e?null:e}return null}(n.fontSize,n.fontWeight);a.textContent=String(Math.floor(100*t)/100)+"%",e(r,"div",null===s||Math.abs(t)S&&tL;let F=E-C;F=s(F,A,c-b-A);let Y=a.minY-w-y,H=!0;Y<0?(Y=Math.min(h-y,a.maxY+w),H=!1):a.minY>h&&(Y=h-w-y);const X=F>=a.minX&&F+b<=a.maxX&&Y>=a.minY&&Y+y<=a.maxY,W=Fa.minX&&Ya.minY;if(W&&!X)return void(m.style.display="none");if(m.style.top=Y+"px",m.style.left=F+"px",m.style.setProperty("--arrow-visibility",P||X?"hidden":"visible"),P)return;m.style.setProperty("--arrow",H?"var(--arrow-down)":"var(--arrow-up)"),m.style.setProperty("--shadow-direction",H?"var(--shadow-up)":"var(--shadow-down)"),m.style.setProperty("--arrow-top",(H?y-1:-w)+"px"),m.style.setProperty("--arrow-left",E-F+"px")}(t.elementInfo,t.colorFormat,n,this.canvasWidth,this.canvasHeight)),t.gridInfo)for(const n of t.gridInfo)rt(n,this.context,this.deviceScaleFactor,this.canvasWidth,this.canvasHeight,this.emulationScaleFactor,this.gridLabelState);if(t.flexInfo)for(const n of t.flexInfo)ut(n,this.context,this.emulationScaleFactor);if(t.containerQueryInfo)for(const n of t.containerQueryInfo)ht(n,this.context,this.emulationScaleFactor);const u=t.flexInfo?.length&&t.flexInfo.some((t=>Object.keys(t.flexContainerHighlightConfig).length>0));if(t.flexItemInfo&&!u)for(const n of t.flexItemInfo){const t="content"===n.boxSizing?a:c;t&&pt(n,t,this.context,this.emulationScaleFactor)}return this.context.restore(),{bounds:n}}drawGridHighlight(t){this.persistentOverlay&&this.persistentOverlay.drawGridHighlight(t)}drawFlexContainerHighlight(t){this.persistentOverlay&&this.persistentOverlay.drawFlexContainerHighlight(t)}drawScrollSnapHighlight(t){this.persistentOverlay?.drawScrollSnapHighlight(t)}drawContainerQueryHighlight(t){this.persistentOverlay?.drawContainerQueryHighlight(t)}drawIsolatedElementHighlight(t){this.persistentOverlay?.drawIsolatedElementHighlight(t)}drawAxis(t,n,e){t.save();const o=this.pageZoomFactor*this.pageScaleFactor*this.emulationScaleFactor,i=this.scrollX*this.pageScaleFactor,r=this.scrollY*this.pageScaleFactor;function s(t){return Math.round(t*o)}function a(t){return Math.round(t/o)}const l=this.canvasWidth/o,c=this.canvasHeight/o,d=50;t.save(),t.fillStyle=Wt,e?t.fillRect(0,s(c)-15,s(l),s(c)):t.fillRect(0,0,s(l),15),t.globalCompositeOperation="destination-out",t.fillStyle="red",n?t.fillRect(s(l)-15,0,s(l),s(c)):t.fillRect(0,0,15,s(c)),t.restore(),t.fillStyle=Wt,n?t.fillRect(s(l)-15,0,s(l),s(c)):t.fillRect(0,0,15,s(c)),t.lineWidth=1,t.strokeStyle=Xt,t.fillStyle=Xt;{t.save(),t.translate(-i,.5-r);const o=c+a(r);for(let e=100;ethis.window.InspectorOverlayHost.send("resume"))),r.addEventListener("click",(()=>this.window.InspectorOverlayHost.send("stepOver"))),super.install()}uninstall(){this.document.body.innerHTML="",this.document.removeEventListener("keydown",this.onKeyDown),super.uninstall()}drawPausedInDebuggerMessage(t){this.container.textContent=t}}(window,Ut),on=new class extends n{zone;constructor(t,n=[]){super(t,n),this.onMouseDown=this.onMouseDown.bind(this),this.onMouseUp=this.onMouseUp.bind(this),this.onMouseMove=this.onMouseMove.bind(this),this.onKeyDown=this.onKeyDown.bind(this)}install(){const t=this.document.documentElement;t.addEventListener("mousedown",this.onMouseDown,!0),t.addEventListener("mouseup",this.onMouseUp,!0),t.addEventListener("mousemove",this.onMouseMove,!0),t.addEventListener("keydown",this.onKeyDown,!0);const n=this.document.createElement("div");n.id="zone",this.document.body.append(n),this.zone=n,super.install()}uninstall(){this.document.body.innerHTML="";const t=this.document.documentElement;t.removeEventListener("mousedown",this.onMouseDown,!0),t.removeEventListener("mouseup",this.onMouseUp,!0),t.removeEventListener("mousemove",this.onMouseMove,!0),t.removeEventListener("keydown",this.onKeyDown,!0),super.uninstall()}onMouseDown(t){Rt={x:t.pageX,y:t.pageY},Tt=Rt,this.updateZone(),t.stopPropagation(),t.preventDefault()}onMouseUp(t){if(Rt&&Tt){const t=Ot();t.width>=5&&t.height>=5&&this.window.InspectorOverlayHost.send(t)}Qt(),this.updateZone(),t.stopPropagation(),t.preventDefault()}onMouseMove(t){Rt&&1===t.buttons?Tt={x:t.pageX,y:t.pageY}:Rt=null,this.updateZone(),t.stopPropagation(),t.preventDefault()}onKeyDown(t){Rt&&"Escape"===t.key&&(Qt(),this.updateZone(),t.stopPropagation(),t.preventDefault())}updateZone(){const t=this.zone;if(!Tt||!Rt)return void(t.style.display="none");t.style.display="block";const n=Ot();t.style.left=n.x+"px",t.style.top=n.y+"px",t.style.width=n.width+"px",t.style.height=n.height+"px"}}(window,Dt),rn=new class extends n{sourceOrderContainer;reset(t){super.reset(t),this.sourceOrderContainer.textContent=""}install(){this.document.body.classList.add("fill");const t=this.document.createElement("canvas");t.id="canvas",t.classList.add("fill"),this.document.body.append(t);const n=this.document.createElement("div");n.id="source-order-container",this.document.body.append(n),this.sourceOrderContainer=n,this.setCanvas(t),super.install()}uninstall(){this.document.body.classList.remove("fill"),this.document.body.innerHTML="",super.uninstall()}drawSourceOrder(t){const n=t.sourceOrder||0,e=t.paths.slice().pop();if(!e)throw new Error("No path provided");this.context.save();const o=C(),i=e.outlineColor;return this.context.save(),function(t,n,e,o,i,r){t.save();const s=M(n,i,r);e&&(t.strokeStyle=e,t.lineWidth=2,o||t.setLineDash([3,3]),t.stroke(s));t.restore()}(this.context,e.path,i,Boolean(n),o,this.emulationScaleFactor),this.context.restore(),this.context.save(),Boolean(n)&&this.drawSourceOrderLabel(n,i,o),this.context.restore(),{bounds:o}}drawSourceOrderLabel(t,n,o){const i=this.sourceOrderContainer,r=i.children,s=e(i,"div","source-order-label-container");s.style.color=n,s.textContent=String(t);const a=s.offsetHeight,l=function(t,n,e,o,i){let r;const s=t.minX+e>t.maxX,a=t.minY+n>t.maxY;if(!s&&!a||o.length>=Jt)return Kt.topCorner;let l=!1;for(let i=0;i=s.top,c=t.minY<=s.top+s.height&&t.minY>=s.top,d=t.minX>=s.left&&t.minX<=s.left+s.width,h=t.minX+e>=s.left&&t.minX+e<=s.left+s.width;if((d||h)&&(a||c)){l=!0;break}}t.minY-n>0&&!l?(r=Kt.aboveElement,s&&(r=Kt.aboveElementWider)):t.maxY+n{const n=t[0];if("setOverlay"===n){const n=t[1];cn&&cn.uninstall(),cn=ln[n],cn.setPlatform(dn),cn.installed||cn.install()}else"setPlatform"===n?dn=t[1]:"drawingFinished"===n||cn.dispatch(t)}}(); diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/rpm.deps b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/rpm.deps new file mode 100644 index 0000000..21f405e --- /dev/null +++ b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/rpm.deps @@ -0,0 +1,91 @@ +ca-certificates +ld-linux-x86-64.so.2()(64bit) +ld-linux-x86-64.so.2(GLIBC_2.2.5)(64bit) +ld-linux-x86-64.so.2(GLIBC_2.3)(64bit) +libX11.so.6()(64bit) +libXcomposite.so.1()(64bit) +libXdamage.so.1()(64bit) +libXext.so.6()(64bit) +libXfixes.so.3()(64bit) +libXrandr.so.2()(64bit) +libasound.so.2()(64bit) +libasound.so.2(ALSA_0.9)(64bit) +libasound.so.2(ALSA_0.9.0rc4)(64bit) +libatk-1.0.so.0()(64bit) +libatk-bridge-2.0.so.0()(64bit) +libatspi.so.0()(64bit) +libc.so.6()(64bit) +libc.so.6(GLIBC_2.10)(64bit) +libc.so.6(GLIBC_2.11)(64bit) +libc.so.6(GLIBC_2.14)(64bit) +libc.so.6(GLIBC_2.15)(64bit) +libc.so.6(GLIBC_2.16)(64bit) +libc.so.6(GLIBC_2.17)(64bit) +libc.so.6(GLIBC_2.18)(64bit) +libc.so.6(GLIBC_2.2.5)(64bit) +libc.so.6(GLIBC_2.25)(64bit) +libc.so.6(GLIBC_2.3)(64bit) +libc.so.6(GLIBC_2.3.2)(64bit) +libc.so.6(GLIBC_2.3.3)(64bit) +libc.so.6(GLIBC_2.3.4)(64bit) +libc.so.6(GLIBC_2.4)(64bit) +libc.so.6(GLIBC_2.6)(64bit) +libc.so.6(GLIBC_2.7)(64bit) +libc.so.6(GLIBC_2.8)(64bit) +libc.so.6(GLIBC_2.9)(64bit) +libcairo.so.2()(64bit) +libcups.so.2()(64bit) +libcurl.so.4()(64bit) +libdbus-1.so.3()(64bit) +libdbus-1.so.3(LIBDBUS_1_3)(64bit) +libdl.so.2()(64bit) +libdl.so.2(GLIBC_2.2.5)(64bit) +liberation-fonts +libexpat.so.1()(64bit) +libgbm.so.1()(64bit) +libgcc_s.so.1()(64bit) +libgcc_s.so.1(GCC_3.0)(64bit) +libgcc_s.so.1(GCC_3.3)(64bit) +libgcc_s.so.1(GCC_4.0.0)(64bit) +libgio-2.0.so.0()(64bit) +libglib-2.0.so.0()(64bit) +libgobject-2.0.so.0()(64bit) +libgtk-3.so.0()(64bit) +libm.so.6()(64bit) +libm.so.6(GLIBC_2.2.5)(64bit) +libnspr4.so()(64bit) +libnss3.so()(64bit) +libnss3.so(NSS_3.11)(64bit) +libnss3.so(NSS_3.12)(64bit) +libnss3.so(NSS_3.12.1)(64bit) +libnss3.so(NSS_3.2)(64bit) +libnss3.so(NSS_3.22)(64bit) +libnss3.so(NSS_3.3)(64bit) +libnss3.so(NSS_3.30)(64bit) +libnss3.so(NSS_3.39)(64bit) +libnss3.so(NSS_3.4)(64bit) +libnss3.so(NSS_3.5)(64bit) +libnss3.so(NSS_3.6)(64bit) +libnss3.so(NSS_3.9.2)(64bit) +libnssutil3.so()(64bit) +libnssutil3.so(NSSUTIL_3.12.3)(64bit) +libpango-1.0.so.0()(64bit) +libpthread.so.0()(64bit) +libpthread.so.0(GLIBC_2.12)(64bit) +libpthread.so.0(GLIBC_2.2.5)(64bit) +libpthread.so.0(GLIBC_2.3.2)(64bit) +libpthread.so.0(GLIBC_2.3.3)(64bit) +libpthread.so.0(GLIBC_2.3.4)(64bit) +libsmime3.so()(64bit) +libsmime3.so(NSS_3.10)(64bit) +libsmime3.so(NSS_3.2)(64bit) +libudev.so.1()(64bit) +libudev.so.1(LIBUDEV_183)(64bit) +libvulkan.so.1()(64bit) +libxcb.so.1()(64bit) +libxkbcommon.so.0()(64bit) +libxkbcommon.so.0(V_0.5.0)(64bit) +rpmlib(FileDigests) <= 4.6.0-1 +rtld(GNU_HASH) +wget +xdg-utils diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/v8_context_snapshot.bin b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/v8_context_snapshot.bin new file mode 100644 index 0000000..67d8d28 Binary files /dev/null and b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/v8_context_snapshot.bin differ diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/vk_swiftshader_icd.json b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/vk_swiftshader_icd.json new file mode 100644 index 0000000..28be1f3 --- /dev/null +++ b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/vk_swiftshader_icd.json @@ -0,0 +1 @@ +{"file_format_version": "1.0.0", "ICD": {"library_path": "./libvk_swiftshader.so", "api_version": "1.0.5"}} \ No newline at end of file diff --git a/backend/app/infra/crawling_refactor/browsers/chrome-linux64/xdg-mime b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/xdg-mime new file mode 100644 index 0000000..e0d9799 --- /dev/null +++ b/backend/app/infra/crawling_refactor/browsers/chrome-linux64/xdg-mime @@ -0,0 +1,1402 @@ +#!/bin/sh +#--------------------------------------------- +# xdg-mime +# +# Utility script to manipulate MIME related information +# on XDG compliant systems. +# +# Refer to the usage() function below for usage. +# +# Copyright 2009-2010, Fathi Boudra +# Copyright 2009-2010, Rex Dieter +# Copyright 2006, Kevin Krammer +# Copyright 2006, Jeremy White +# +# LICENSE: +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +# +#--------------------------------------------- + +manualpage() +{ +cat << _MANUALPAGE +Name + +xdg-mime - command line tool for querying information about file type handling +and adding descriptions for new file types + +Synopsis + +xdg-mime query { filetype | default } ... + +xdg-mime default application mimetype(s) + +xdg-mime install [--mode mode] [--novendor] mimetypes-file + +xdg-mime uninstall [--mode mode] mimetypes-file + +xdg-mime { --help | --manual | --version } + +Description + +The xdg-mime program can be used to query information about file types and to +add descriptions for new file types. + +Commands + +query + + Returns information related to file types. + + The query option is for use inside a desktop session only. It is not + recommended to use xdg-mime query as root. + + The following queries are supported: + + query filetype FILE: Returns the file type of FILE in the form of a MIME + type. + + query default mimetype: Returns the default application that the desktop + environment uses for opening files of type mimetype. The default + application is identified by its *.desktop file. + +default + + Ask the desktop environment to make application the default application for + opening files of type mimetype. An application can be made the default for + several file types by specifying multiple mimetypes. + + application is the desktop file id of the application and has the form + vendor-name.desktop application must already be installed in the desktop + menu before it can be made the default handler. The aplication's desktop + file must list support for all the MIME types that it wishes to be the + default handler for. + + Requests to make an application a default handler may be subject to system + policy or approval by the end-user. xdg-mime query can be used to verify + whether an application is the actual default handler for a specific file + type. + + The default option is for use inside a desktop session only. It is not + recommended to use xdg-mime default as root. + +install + Adds the file type descriptions provided in mimetypes-file to the desktop + environment. mimetypes-file must be a XML file that follows the + freedesktop.org Shared MIME-info Database specification and that has a + mime-info element as its document root. For each new file type one or more + icons with name type-subtype must be installed with the xdg-icon-resource + command in the mimetypes context. For example the filetype application/ + vnd.oasis.opendocument.text requires an icon named + application-vnd.oasis.opendocument.text to be installed (unless the file + type recommends another icon name). +uninstall + Removes the file type descriptions provided in mimetypes-file and + previously added with xdg-mime install from the desktop environment. + mimetypes-file must be a XML file that follows the freedesktop.org Shared + MIME-info Database specification and that has a mime-info element as its + document root. + +Options + +--mode mode + + mode can be user or system. In user mode the file is (un)installed for the + current user only. In system mode the file is (un)installed for all users + on the system. Usually only root is allowed to install in system mode. + + The default is to use system mode when called by root and to use user mode + when called by a non-root user. + +--novendor + + Normally, xdg-mime checks to ensure that the mimetypes-file to be installed + has a proper vendor prefix. This option can be used to disable that check. + + A vendor prefix consists of alpha characters ([a-zA-Z]) and is terminated + with a dash ("-"). Companies and organizations are encouraged to use a word + or phrase, preferably the organizations name, for which they hold a + trademark as their vendor prefix. The purpose of the vendor prefix is to + prevent name conflicts. + +--help + Show command synopsis. +--manual + Show this manualpage. +--version + Show the xdg-utils version information. + +Environment Variables + +xdg-mime honours the following environment variables: + +XDG_UTILS_DEBUG_LEVEL + Setting this environment variable to a non-zero numerical value makes + xdg-mime do more verbose reporting on stderr. Setting a higher value + increases the verbosity. +XDG_UTILS_INSTALL_MODE + This environment variable can be used by the user or administrator to + override the installation mode. Valid values are user and system. + +Exit Codes + +An exit code of 0 indicates success while a non-zero exit code indicates +failure. The following failure codes can be returned: + +1 + Error in command line syntax. +2 + One of the files passed on the command line did not exist. +3 + A required tool could not be found. +4 + The action failed. +5 + No permission to read one of the files passed on the command line. + +See Also + +xdg-icon-resource(1), xdg-desktop-menu(1) + +Examples + +xdg-mime query filetype /tmp/foobar.png + +Prints the MIME type of the file /tmp/foobar.png, in this case image/png + +xdg-mime query default image/png + +Prints the .desktop filename of the application which is registered to open PNG +files. + +xdg-mime install shinythings-shiny.xml + +Adds a file type description for "shiny"-files. "shinythings-" is used as the +vendor prefix. The file type description could look as folows. + +shinythings-shiny.xml: + + + + + Shiny new file type + + + + + +An icon for this new file type must also be installed, for example with: + +xdg-icon-resource install --context mimetypes --size 64 shiny-file-icon.png text-x-shiny + +_MANUALPAGE +} + +usage() +{ +cat << _USAGE +xdg-mime - command line tool for querying information about file type handling +and adding descriptions for new file types + +Synopsis + +xdg-mime query { filetype | default } ... + +xdg-mime default application mimetype(s) + +xdg-mime install [--mode mode] [--novendor] mimetypes-file + +xdg-mime uninstall [--mode mode] mimetypes-file + +xdg-mime { --help | --manual | --version } + +_USAGE +} + +#@xdg-utils-common@ + +#---------------------------------------------------------------------------- +# Common utility functions included in all XDG wrapper scripts +#---------------------------------------------------------------------------- + +DEBUG() +{ + [ -z "${XDG_UTILS_DEBUG_LEVEL}" ] && return 0; + [ ${XDG_UTILS_DEBUG_LEVEL} -lt $1 ] && return 0; + shift + echo "$@" >&2 +} + +# This handles backslashes but not quote marks. +first_word() +{ + read first rest + echo "$first" +} + +#------------------------------------------------------------- +# map a binary to a .desktop file +binary_to_desktop_file() +{ + search="${XDG_DATA_HOME:-$HOME/.local/share}:${XDG_DATA_DIRS:-/usr/local/share:/usr/share}" + binary="`which "$1"`" + binary="`readlink -f "$binary"`" + base="`basename "$binary"`" + IFS=: + for dir in $search; do + unset IFS + [ "$dir" ] || continue + [ -d "$dir/applications" -o -d "$dir/applnk" ] || continue + for file in "$dir"/applications/*.desktop "$dir"/applications/*/*.desktop "$dir"/applnk/*.desktop "$dir"/applnk/*/*.desktop; do + [ -r "$file" ] || continue + # Check to make sure it's worth the processing. + grep -q "^Exec.*$base" "$file" || continue + # Make sure it's a visible desktop file (e.g. not "preferred-web-browser.desktop"). + grep -Eq "^(NoDisplay|Hidden)=true" "$file" && continue + command="`grep -E "^Exec(\[[^]=]*])?=" "$file" | cut -d= -f 2- | first_word`" + command="`which "$command"`" + if [ x"`readlink -f "$command"`" = x"$binary" ]; then + # Fix any double slashes that got added path composition + echo "$file" | sed -e 's,//*,/,g' + return + fi + done + done +} + +#------------------------------------------------------------- +# map a .desktop file to a binary +## FIXME: handle vendor dir case +desktop_file_to_binary() +{ + search="${XDG_DATA_HOME:-$HOME/.local/share}:${XDG_DATA_DIRS:-/usr/local/share:/usr/share}" + desktop="`basename "$1"`" + IFS=: + for dir in $search; do + unset IFS + [ "$dir" -a -d "$dir/applications" ] || continue + file="$dir/applications/$desktop" + [ -r "$file" ] || continue + # Remove any arguments (%F, %f, %U, %u, etc.). + command="`grep -E "^Exec(\[[^]=]*])?=" "$file" | cut -d= -f 2- | first_word`" + command="`which "$command"`" + readlink -f "$command" + return + done +} + +#------------------------------------------------------------- +# Exit script on successfully completing the desired operation + +exit_success() +{ + if [ $# -gt 0 ]; then + echo "$@" + echo + fi + + exit 0 +} + + +#----------------------------------------- +# Exit script on malformed arguments, not enough arguments +# or missing required option. +# prints usage information + +exit_failure_syntax() +{ + if [ $# -gt 0 ]; then + echo "xdg-mime: $@" >&2 + echo "Try 'xdg-mime --help' for more information." >&2 + else + usage + echo "Use 'man xdg-mime' or 'xdg-mime --manual' for additional info." + fi + + exit 1 +} + +#------------------------------------------------------------- +# Exit script on missing file specified on command line + +exit_failure_file_missing() +{ + if [ $# -gt 0 ]; then + echo "xdg-mime: $@" >&2 + fi + + exit 2 +} + +#------------------------------------------------------------- +# Exit script on failure to locate necessary tool applications + +exit_failure_operation_impossible() +{ + if [ $# -gt 0 ]; then + echo "xdg-mime: $@" >&2 + fi + + exit 3 +} + +#------------------------------------------------------------- +# Exit script on failure returned by a tool application + +exit_failure_operation_failed() +{ + if [ $# -gt 0 ]; then + echo "xdg-mime: $@" >&2 + fi + + exit 4 +} + +#------------------------------------------------------------ +# Exit script on insufficient permission to read a specified file + +exit_failure_file_permission_read() +{ + if [ $# -gt 0 ]; then + echo "xdg-mime: $@" >&2 + fi + + exit 5 +} + +#------------------------------------------------------------ +# Exit script on insufficient permission to write a specified file + +exit_failure_file_permission_write() +{ + if [ $# -gt 0 ]; then + echo "xdg-mime: $@" >&2 + fi + + exit 6 +} + +check_input_file() +{ + if [ ! -e "$1" ]; then + exit_failure_file_missing "file '$1' does not exist" + fi + if [ ! -r "$1" ]; then + exit_failure_file_permission_read "no permission to read file '$1'" + fi +} + +check_vendor_prefix() +{ + file_label="$2" + [ -n "$file_label" ] || file_label="filename" + file=`basename "$1"` + case "$file" in + [a-zA-Z]*-*) + return + ;; + esac + + echo "xdg-mime: $file_label '$file' does not have a proper vendor prefix" >&2 + echo 'A vendor prefix consists of alpha characters ([a-zA-Z]) and is terminated' >&2 + echo 'with a dash ("-"). An example '"$file_label"' is '"'example-$file'" >&2 + echo "Use --novendor to override or 'xdg-mime --manual' for additional info." >&2 + exit 1 +} + +check_output_file() +{ + # if the file exists, check if it is writeable + # if it does not exists, check if we are allowed to write on the directory + if [ -e "$1" ]; then + if [ ! -w "$1" ]; then + exit_failure_file_permission_write "no permission to write to file '$1'" + fi + else + DIR=`dirname "$1"` + if [ ! -w "$DIR" -o ! -x "$DIR" ]; then + exit_failure_file_permission_write "no permission to create file '$1'" + fi + fi +} + +#---------------------------------------- +# Checks for shared commands, e.g. --help + +check_common_commands() +{ + while [ $# -gt 0 ] ; do + parm="$1" + shift + + case "$parm" in + --help) + usage + echo "Use 'man xdg-mime' or 'xdg-mime --manual' for additional info." + exit_success + ;; + + --manual) + manualpage + exit_success + ;; + + --version) + echo "xdg-mime 1.1.0 rc1" + exit_success + ;; + esac + done +} + +check_common_commands "$@" + +[ -z "${XDG_UTILS_DEBUG_LEVEL}" ] && unset XDG_UTILS_DEBUG_LEVEL; +if [ ${XDG_UTILS_DEBUG_LEVEL-0} -lt 1 ]; then + # Be silent + xdg_redirect_output=" > /dev/null 2> /dev/null" +else + # All output to stderr + xdg_redirect_output=" >&2" +fi + +#-------------------------------------- +# Checks for known desktop environments +# set variable DE to the desktop environments name, lowercase + +detectDE() +{ + # see https://bugs.freedesktop.org/show_bug.cgi?id=34164 + unset GREP_OPTIONS + + if [ -n "${XDG_CURRENT_DESKTOP}" ]; then + case "${XDG_CURRENT_DESKTOP}" in + GNOME) + DE=gnome; + ;; + KDE) + DE=kde; + ;; + LXDE) + DE=lxde; + ;; + XFCE) + DE=xfce + esac + fi + + if [ x"$DE" = x"" ]; then + # classic fallbacks + if [ x"$KDE_FULL_SESSION" = x"true" ]; then DE=kde; + elif [ x"$GNOME_DESKTOP_SESSION_ID" != x"" ]; then DE=gnome; + elif `dbus-send --print-reply --dest=org.freedesktop.DBus /org/freedesktop/DBus org.freedesktop.DBus.GetNameOwner string:org.gnome.SessionManager > /dev/null 2>&1` ; then DE=gnome; + elif xprop -root _DT_SAVE_MODE 2> /dev/null | grep ' = \"xfce4\"$' >/dev/null 2>&1; then DE=xfce; + elif xprop -root 2> /dev/null | grep -i '^xfce_desktop_window' >/dev/null 2>&1; then DE=xfce + fi + fi + + if [ x"$DE" = x"" ]; then + # fallback to checking $DESKTOP_SESSION + case "$DESKTOP_SESSION" in + gnome) + DE=gnome; + ;; + LXDE) + DE=lxde; + ;; + xfce|xfce4) + DE=xfce; + ;; + esac + fi + + if [ x"$DE" = x"" ]; then + # fallback to uname output for other platforms + case "$(uname 2>/dev/null)" in + Darwin) + DE=darwin; + ;; + esac + fi + + if [ x"$DE" = x"gnome" ]; then + # gnome-default-applications-properties is only available in GNOME 2.x + # but not in GNOME 3.x + which gnome-default-applications-properties > /dev/null 2>&1 || DE="gnome3" + fi +} + +#---------------------------------------------------------------------------- +# kfmclient exec/openURL can give bogus exit value in KDE <= 3.5.4 +# It also always returns 1 in KDE 3.4 and earlier +# Simply return 0 in such case + +kfmclient_fix_exit_code() +{ + version=`LC_ALL=C.UTF-8 kde-config --version 2>/dev/null | grep '^KDE'` + major=`echo $version | sed 's/KDE.*: \([0-9]\).*/\1/'` + minor=`echo $version | sed 's/KDE.*: [0-9]*\.\([0-9]\).*/\1/'` + release=`echo $version | sed 's/KDE.*: [0-9]*\.[0-9]*\.\([0-9]\).*/\1/'` + test "$major" -gt 3 && return $1 + test "$minor" -gt 5 && return $1 + test "$release" -gt 4 && return $1 + return 0 +} + +update_mime_database() +{ + if [ x"$mode" = x"user" -a -n "$DISPLAY" ] ; then + detectDE + if [ x"$DE" = x"kde" ] ; then + DEBUG 1 "Running kbuildsycoca" + if [ x"$KDE_SESSION_VERSION" = x"4" ]; then + eval 'kbuildsycoca4'$xdg_redirect_output + else + eval 'kbuildsycoca'$xdg_redirect_output + fi + fi + fi + for x in `echo "$PATH:/opt/gnome/bin" | sed 's/:/ /g'`; do + if [ -x $x/update-mime-database ] ; then + DEBUG 1 "Running $x/update-mime-database $1" + eval '$x/update-mime-database $1'$xdg_redirect_output + return + fi + done +} + +info_kde() +{ + if [ x"$KDE_SESSION_VERSION" = x"4" ]; then + DEBUG 1 "Running kmimetypefinder \"$1\"" + kmimetypefinder "$1" 2>/dev/null | head -n 1 + else + DEBUG 1 "Running kfile \"$1\"" + kfile "$1" 2> /dev/null | head -n 1 | cut -d "(" -f 2 | cut -d ")" -f 1 + fi + + if [ $? -eq 0 ]; then + exit_success + else + exit_failure_operation_failed + fi +} + +info_gnome() +{ + if gvfs-info --help 2>/dev/null 1>&2; then + DEBUG 1 "Running gvfs-info \"$1\"" + gvfs-info "$1" 2> /dev/null | grep standard::content-type | cut -d' ' -f4 + elif gnomevfs-info --help 2>/dev/null 1>&2; then + DEBUG 1 "Running gnomevfs-info \"$1\"" + gnomevfs-info --slow-mime "$1" 2> /dev/null | grep "^MIME" | cut -d ":" -f 2 | sed s/"^ "// + else + # according to https://bugs.freedesktop.org/show_bug.cgi?id=33094#c5 + # neither gvfs-info or gnomevfs-info are present in a default Ubuntu Natty + # install, so fallback to info_generic + info_generic "$1" + fi + + if [ $? -eq 0 ]; then + exit_success + else + exit_failure_operation_failed + fi +} + +info_generic() +{ + if mimetype --version >/dev/null 2>&1; then + DEBUG 1 "Running mimetype -b \"$1\"" + mimetype -b "$1" + else + DEBUG 1 "Running file --mime-type \"$1\"" + /usr/bin/file --mime-type "$1" 2> /dev/null | cut -d ":" -f 2 | sed s/"^ "// + fi + + if [ $? -eq 0 ]; then + exit_success + else + exit_failure_operation_failed + fi +} + +make_default_kde() +{ + # $1 is vendor-name.desktop + # $2 is mime/type + # + # On KDE 3, add to $KDE_CONFIG_PATH/profilerc: + # [$2 - 1] + # Application=$1 + # + # Remove all [$2 - *] sections, or even better, + # renumber [$2 - *] sections and remove duplicate + # + # On KDE 4, add $2=$1 to $XDG_DATA_APPS/mimeapps.list + # + # Example file: + # + # [Added Associations] + # text/plain=kde4-kate.desktop;kde4-kwrite.desktop; + # + # [Removed Associations] + # text/plain=gnome-gedit.desktop;gnu-emacs.desktop; + vendor="$1" + mimetype="$2" + if [ x"$KDE_SESSION_VERSION" = x"4" ]; then + default_dir=`kde4-config --path xdgdata-apps 2> /dev/null | cut -d ':' -f 1` + default_file="$default_dir/mimeapps.list" + else + default_dir=`kde-config --path config 2> /dev/null | cut -d ':' -f 1` + default_file="$default_dir/profilerc" + fi + if [ -z "$default_dir" ]; then + DEBUG 2 "make_default_kde: No kde runtime detected" + return + fi + DEBUG 2 "make_default_kde $vendor $mimetype" + DEBUG 1 "Updating $default_file" + mkdir -p "$default_dir" + [ -f "$default_file" ] || touch "$default_file" + if [ x"$KDE_SESSION_VERSION" = x"4" ]; then + [ -f "$default_file" ] || touch "$default_file" + awk -v application="$vendor" -v mimetype="$mimetype" ' + BEGIN { + prefix=mimetype "=" + associations=0 + found=0 + blanks=0 + } + { + suppress=0 + if (index($0, "[Added Associations]") == 1) { + associations=1 + } else if (index($0, "[") == 1) { + if (associations && !found) { + print prefix application + found=1 + } + associations=0 + } else if ($0 == "") { + blanks++ + suppress=1 + } else if (associations && index($0, prefix) == 1) { + value=substr($0, length(prefix) + 1, length) + split(value, apps, ";") + value=application ";" + count=0 + for (i in apps) { + count++ + } + for (i=0; i < count; i++) { + if (apps[i] != application && apps[i] != "") { + value=value apps[i] ";" + } + } + $0=prefix value + found=1 + } + if (!suppress) { + while (blanks > 0) { + print "" + blanks-- + } + print $0 + } + } + END { + if (!found) { + if (!associations) { + print "[Added Associations]" + } + print prefix application + } + while (blanks > 0) { + print "" + blanks-- + } + } +' "$default_file" > "${default_file}.new" && mv "${default_file}.new" "$default_file" + eval 'kbuildsycoca4'$xdg_redirect_output + else + awk -v application="$vendor" -v mimetype="$mimetype" ' + BEGIN { + header_start="[" mimetype " - " + suppress=0 + } + { + if (index($0, header_start) == 1 ) + suppress=1 + else + if (/^\[/) { suppress=0 } + + if (!suppress) { + print $0 + } + } + END { + print "" + print "[" mimetype " - 1]" + print "Application=" application + print "AllowAsDefault=true" + print "GenericServiceType=Application" + print "Preference=1" + print "ServiceType=" mimetype + } +' "$default_file" > "${default_file}.new" && mv "${default_file}.new" "$default_file" + fi +} + +make_default_generic() +{ + # $1 is vendor-name.desktop + # $2 is mime/type + # Add $2=$1 to XDG_DATA_HOME/applications/mimeapps.list + xdg_user_dir="$XDG_DATA_HOME" + [ -n "$xdg_user_dir" ] || xdg_user_dir="$HOME/.local/share" + default_file="$xdg_user_dir/applications/mimeapps.list" + DEBUG 2 "make_default_generic $1 $2" + DEBUG 1 "Updating $default_file" + [ -f "$default_file" ] || touch "$default_file" + awk -v mimetype="$2" -v application="$1" ' + BEGIN { + prefix=mimetype "=" + indefault=0 + added=0 + blanks=0 + found=0 + } + { + suppress=0 + if (index($0, "[Default Applications]") == 1) { + indefault=1 + found=1 + } else if (index($0, "[") == 1) { + if (!added && indefault) { + print prefix application + added=1 + } + indefault=0 + } else if ($0 == "") { + suppress=1 + blanks++ + } else if (indefault && !added && index($0, prefix) == 1) { + $0=prefix application + added=1 + } + if (!suppress) { + while (blanks > 0) { + print "" + blanks-- + } + print $0 + } + } + END { + if (!added) { + if (!found) { + print "" + print "[Default Applications]" + } + print prefix application + } + while (blanks > 0) { + print "" + blanks-- + } + } +' "$default_file" > "${default_file}.new" && mv "${default_file}.new" "$default_file" +} + +defapp_generic() +{ + MIME="$1" + xdg_user_dir="$XDG_DATA_HOME" + [ -n "$xdg_user_dir" ] || xdg_user_dir="$HOME/.local/share" + xdg_user_dir="$xdg_user_dir/$xdg_dir_name" + xdg_system_dirs="$XDG_DATA_DIRS" + [ -n "$xdg_system_dirs" ] || xdg_system_dirs=/usr/local/share/:/usr/share/ + + for x in `echo "$xdg_user_dir" | sed 's/:/ /g'`; do + mimeapps_list="$x/applications/mimeapps.list" + if [ -f "$mimeapps_list" ] ; then + DEBUG 2 "Checking $mimeapps_list" + trader_result=`awk -v mimetype="$MIME" ' + BEGIN { + prefix=mimetype "=" + indefault=0 + found=0 + } + { + if (index($0, "[Default Applications]") == 1) { + indefault=1 + } else if (index($0, "[") == 1) { + indefault=0 + } else if (!found && indefault && index($0, prefix) == 1) { + print substr($0, length(prefix) +1, length) + found=1 + } + } +' $mimeapps_list` + if [ -n "$trader_result" ] ; then + echo $trader_result + exit_success + fi + fi + done + + for x in `echo "$xdg_system_dirs" | sed 's/:/ /g'`; do + DEBUG 2 "Checking $x/applications/defaults.list" + trader_result=`grep "$MIME=" $x/applications/defaults.list 2> /dev/null | cut -d '=' -f 2 | cut -d ';' -f 1` + if [ -n "$trader_result" ] ; then + echo $trader_result + exit_success + fi + done + exit_success +} + +defapp_kde() +{ + MIME="$1" + if [ x"$KDE_SESSION_VERSION" = x"4" ]; then + KTRADER=`which ktraderclient 2> /dev/null` + MIMETYPE="--mimetype" + SERVICETYPE="--servicetype" + else + KTRADER=`which ktradertest 2> /dev/null` + fi + if [ -n "$KTRADER" ] ; then + DEBUG 1 "Running KDE trader query \"$MIME\" mimetype and \"Application\" servicetype" + trader_result=`$KTRADER $MIMETYPE "$MIME" $SERVICETYPE Application 2>/dev/null \ + | grep DesktopEntryPath | head -n 1 | cut -d ':' -f 2 | cut -d \' -f 2` + if [ -n "$trader_result" ] ; then + basename "$trader_result" + exit_success + else + exit_failure_operation_failed + fi + else + defapp_generic "$1" + fi +} + +[ x"$1" != x"" ] || exit_failure_syntax + +mode= +action= +filename= +mimetype= + +case $1 in + install) + action=install + ;; + + uninstall) + action=uninstall + ;; + + query) + shift + + if [ -z "$1" ] ; then + exit_failure_syntax "query type argument missing" + fi + + case $1 in + filetype) + action=info + + filename="$2" + if [ -z "$filename" ] ; then + exit_failure_syntax "FILE argument missing" + fi + case $filename in + -*) + exit_failure_syntax "unexpected option '$filename'" + ;; + esac + check_input_file "$filename" + filename=`readlink -f -- "$filename"` + ;; + + default) + action=defapp + mimetype="$2" + if [ -z "$mimetype" ] ; then + exit_failure_syntax "mimetype argument missing" + fi + case $mimetype in + -*) + exit_failure_syntax "unexpected option '$mimetype'" + ;; + + */*) + # Ok + ;; + + *) + exit_failure_syntax "mimetype '$mimetype' is not in the form 'minor/major'" + ;; + esac + ;; + + *) + exit_failure_syntax "unknown query type '$1'" + ;; + esac + ;; + + default) + action=makedefault + shift + + if [ -z "$1" ] ; then + exit_failure_syntax "application argument missing" + fi + case $1 in + -*) + exit_failure_syntax "unexpected option '$1'" + ;; + + *.desktop) + filename="$1" + ;; + + *) + exit_failure_syntax "malformed argument '$1', expected *.desktop" + ;; + esac + ;; + + *) + exit_failure_syntax "unknown command '$1'" + ;; +esac + +shift + + +if [ "$action" = "makedefault" ]; then + if [ -z "$1" ] ; then + exit_failure_syntax "mimetype argument missing" + fi + + while [ $# -gt 0 ] ; do + case $1 in + -*) + exit_failure_syntax "unexpected option '$1'" + ;; + esac + mimetype="$1" + shift + + make_default_kde "$filename" "$mimetype" + make_default_generic "$filename" "$mimetype" + done + exit_success +fi + +if [ "$action" = "info" ]; then + detectDE + + if [ x"$DE" = x"" ]; then + if [ -x /usr/bin/file ]; then + DE=generic + fi + fi + + case "$DE" in + kde) + info_kde "$filename" + ;; + + gnome*) + info_gnome "$filename" + ;; + + *) + info_generic "$filename" + ;; + esac + exit_failure_operation_impossible "no method available for quering MIME type of '$filename'" +fi + +if [ "$action" = "defapp" ]; then + detectDE + + case "$DE" in + kde) + defapp_kde "$mimetype" + ;; + + *) + defapp_generic "$mimetype" + ;; + esac + exit_failure_operation_impossible "no method available for quering default application for '$mimetype'" +fi + +vendor=true +while [ $# -gt 0 ] ; do + parm="$1" + shift + + case $parm in + --mode) + if [ -z "$1" ] ; then + exit_failure_syntax "mode argument missing for --mode" + fi + case "$1" in + user) + mode="user" + ;; + + system) + mode="system" + ;; + + *) + exit_failure_syntax "unknown mode '$1'" + ;; + esac + shift + ;; + + --novendor) + vendor=false + ;; + + -*) + exit_failure_syntax "unexpected option '$parm'" + ;; + + *) + if [ -n "$filename" ] ; then + exit_failure_syntax "unexpected argument '$parm'" + fi + + filename="$parm" + check_input_file "$filename" + ;; + esac +done + +if [ -z "$action" ] ; then + exit_failure_syntax "command argument missing" +fi + +if [ -n "$XDG_UTILS_INSTALL_MODE" ] ; then + if [ "$XDG_UTILS_INSTALL_MODE" = "system" ] ; then + mode="system" + elif [ "$XDG_UTILS_INSTALL_MODE" = "user" ] ; then + mode="user" + fi +fi + +if [ -z "$mode" ] ; then + if [ `whoami` = "root" ] ; then + mode="system" + else + mode="user" + fi +fi + +if [ -z "$filename" ] ; then + exit_failure_syntax "mimetypes-file argument missing" +fi + +if [ "$vendor" = "true" -a "$action" = "install" ] ; then + check_vendor_prefix "$filename" +fi + +xdg_base_dir= +xdg_dir_name=mime/packages/ + +xdg_user_dir="$XDG_DATA_HOME" +[ -n "$xdg_user_dir" ] || xdg_user_dir="$HOME/.local/share" +[ x"$mode" = x"user" ] && xdg_base_dir="$xdg_user_dir/mime" +xdg_user_dir="$xdg_user_dir/$xdg_dir_name" + +xdg_system_dirs="$XDG_DATA_DIRS" +[ -n "$xdg_system_dirs" ] || xdg_system_dirs=/usr/local/share/:/usr/share/ +for x in `echo $xdg_system_dirs | sed 's/:/ /g'`; do + if [ -w $x/$xdg_dir_name ] ; then + [ x"$mode" = x"system" ] && xdg_base_dir="$x/mime" + xdg_global_dir="$x/$xdg_dir_name" + break + fi +done +[ -w $xdg_global_dir ] || xdg_global_dir= +DEBUG 3 "xdg_user_dir: $xdg_user_dir" +DEBUG 3 "xdg_global_dir: $xdg_global_dir" + +# Find KDE3 mimelnk directory +kde_user_dir= +kde_global_dir= +kde_global_dirs=`kde${KDE_SESSION_VERSION}-config --path mime 2> /dev/null` +DEBUG 3 "kde_global_dirs: $kde_global_dirs" +first= +for x in `echo $kde_global_dirs | sed 's/:/ /g'` ; do + if [ -z "$first" ] ; then + first=false + kde_user_dir="$x" + elif [ -w $x ] ; then + kde_global_dir="$x" + fi +done +DEBUG 3 "kde_user_dir: $kde_user_dir" +DEBUG 3 "kde_global_dir: $kde_global_dir" + +# TODO: Gnome legacy support +# See http://forums.fedoraforum.org/showthread.php?t=26875 +gnome_user_dir="$HOME/.gnome/apps" +gnome_global_dir=/usr/share/gnome/apps +[ -w $gnome_global_dir ] || gnome_global_dir= +DEBUG 3 "gnome_user_dir: $gnome_user_dir" +DEBUG 3 "gnome_global_dir: $gnome_global_dir" + +if [ x"$mode" = x"user" ] ; then + xdg_dir="$xdg_user_dir" + kde_dir="$kde_user_dir" + gnome_dir="$gnome_user_dir" + my_umask=077 +else + xdg_dir="$xdg_global_dir" + kde_dir="$kde_global_dir" + gnome_dir="$gnome_global_dir" + my_umask=022 + if [ -z "${xdg_dir}${kde_dir}${gnome_dir}" ] ; then + exit_failure_operation_impossible "No writable system mimetype directory found." + fi +fi + +# echo "[xdg|$xdg_user_dir|$xdg_global_dir]" +# echo "[kde|$kde_user_dir|$kde_global_dir]" +# echo "[gnome|$gnome_user_dir|$gnome_global_dir]" +# echo "[using|$xdg_dir|$kde_dir|$gnome_dir]" + +basefile=`basename "$filename"` +#[ -z $vendor ] || basefile="$vendor-$basefile" + +mimetypes= +if [ -n "$kde_dir" ] ; then + DEBUG 2 "KDE3 mimelnk directory found, extracting mimetypes from XML file" + + mimetypes=`awk < "$filename" ' +# Strip XML comments +BEGIN { + suppress=0 +} +{ + do + if (suppress) { + if (match($0,/-->/)) { + $0=substr($0,RSTART+RLENGTH) + suppress=0 + } + else { + break + } + } + else { + if (match($0,//)) { + $0=substr($0,RSTART+RLENGTH) + suppress=0 + } + else { + break + } + } + else { + if (match($0,/ + + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/assets/images/error.png b/frontend/src/assets/images/error.png new file mode 100644 index 0000000..f084555 Binary files /dev/null and b/frontend/src/assets/images/error.png differ diff --git a/frontend/src/assets/images/facebook.png b/frontend/src/assets/images/facebook.png new file mode 100644 index 0000000..39315d0 Binary files /dev/null and b/frontend/src/assets/images/facebook.png differ diff --git a/frontend/src/assets/images/kakao_login.png b/frontend/src/assets/images/kakao_login.png new file mode 100644 index 0000000..c882acc Binary files /dev/null and b/frontend/src/assets/images/kakao_login.png differ diff --git a/frontend/src/assets/images/linkedIn.png b/frontend/src/assets/images/linkedIn.png new file mode 100644 index 0000000..bf29427 Binary files /dev/null and b/frontend/src/assets/images/linkedIn.png differ diff --git a/frontend/src/assets/images/logo.png b/frontend/src/assets/images/logo.png new file mode 100644 index 0000000..9bd18e6 Binary files /dev/null and b/frontend/src/assets/images/logo.png differ diff --git a/frontend/src/assets/images/naver.png b/frontend/src/assets/images/naver.png new file mode 100644 index 0000000..061b76b Binary files /dev/null and b/frontend/src/assets/images/naver.png differ diff --git a/frontend/src/assets/images/not_value.png b/frontend/src/assets/images/not_value.png new file mode 100644 index 0000000..7bfbe2e Binary files /dev/null and b/frontend/src/assets/images/not_value.png differ diff --git a/frontend/src/assets/images/toggle.png b/frontend/src/assets/images/toggle.png new file mode 100644 index 0000000..0aba83d Binary files /dev/null and b/frontend/src/assets/images/toggle.png differ diff --git a/frontend/src/assets/images/youtube.png b/frontend/src/assets/images/youtube.png new file mode 100644 index 0000000..84b9038 Binary files /dev/null and b/frontend/src/assets/images/youtube.png differ diff --git a/frontend/src/components/CheckBox/CheckBox.module.css b/frontend/src/components/CheckBox/CheckBox.module.css new file mode 100644 index 0000000..31228eb --- /dev/null +++ b/frontend/src/components/CheckBox/CheckBox.module.css @@ -0,0 +1,66 @@ +.wrapper { + display: inline-flex; + align-items: center; + cursor: pointer; +} + +.input { + display: none; +} + +.box { + display: flex; + align-items: center; + justify-content: center; + border: 2px solid rgba(0, 18, 39, 0.5); + transition: border-color 0.3s ease-in-out; + position: relative; + padding: 0; /* 중앙 정렬 정확도 높이기 */ +} + +/* 네모 */ +.square { + width: 1rem; + height: 1rem; + border-radius: 4px; +} + +/* 원 */ +.circle { + width: 1.25rem; + height: 1.25rem; + border-radius: 50%; +} + +.checked .box { + border-color: #0D52FF; +} + +/* 체크 아이콘 (네모 전용) */ +.checkmark { + width: 1rem; + height: 1rem; + stroke-dasharray: 24; + stroke-dashoffset: 24; + transition: stroke-dashoffset 0.3s ease-in-out, opacity 0.2s ease; + opacity: 0; +} + +.checked .checkmark { + stroke-dashoffset: 0; + opacity: 1; +} + +/* 내부 원 (라디오 전용) */ +.innerDot { + width: 0.5rem; + height: 0.5rem; + background-color: #0D52FF; + border-radius: 50%; + opacity: 0; + transition: opacity 0.2s ease; +} + +.checked .innerDot { + opacity: 1; +} diff --git a/frontend/src/components/CheckBox/CheckBox.tsx b/frontend/src/components/CheckBox/CheckBox.tsx new file mode 100644 index 0000000..15a12ed --- /dev/null +++ b/frontend/src/components/CheckBox/CheckBox.tsx @@ -0,0 +1,36 @@ +// CheckBox.tsx +import React from 'react'; +import styles from './CheckBox.module.css'; + +interface CheckBoxProps { + checked: boolean; + type?: 'square' | 'circle'; +} + +const CheckBox: React.FC = ({ checked, type = 'square' }) => { + return ( + + + {type === 'square' ? ( + + + + ) : ( + // circle일 때: 내부 원 (radio 스타일) + + )} + + + ); +}; + +export default CheckBox; diff --git a/frontend/src/components/CheckBox/index.tsx b/frontend/src/components/CheckBox/index.tsx new file mode 100644 index 0000000..fc07339 --- /dev/null +++ b/frontend/src/components/CheckBox/index.tsx @@ -0,0 +1 @@ +export { default } from "./CheckBox"; \ No newline at end of file diff --git a/frontend/src/components/Divider/Divider.module.css b/frontend/src/components/Divider/Divider.module.css new file mode 100644 index 0000000..7794e27 --- /dev/null +++ b/frontend/src/components/Divider/Divider.module.css @@ -0,0 +1,19 @@ +.divider{ + display: flex; + width: 100%; + justify-content: center; + align-items: center; + gap: 0.625rem; + box-sizing: border-box; +} +.dividerLine{ + width: 100%; + height: 1px; + background: #E2E2E2; +} +.dividerText{ + color: #001227; + font-size: 0.875rem; + font-weight: 500; + white-space: nowrap; +} \ No newline at end of file diff --git a/frontend/src/components/Divider/Divider.tsx b/frontend/src/components/Divider/Divider.tsx new file mode 100644 index 0000000..f652ebd --- /dev/null +++ b/frontend/src/components/Divider/Divider.tsx @@ -0,0 +1,13 @@ +import styles from "./Divider.module.css"; + +const Divider: React.FC = () => { + return ( +
+
+
또는
+
+
+ ); +}; + +export default Divider; \ No newline at end of file diff --git a/frontend/src/components/Divider/index.tsx b/frontend/src/components/Divider/index.tsx new file mode 100644 index 0000000..02512bb --- /dev/null +++ b/frontend/src/components/Divider/index.tsx @@ -0,0 +1 @@ +export { default } from "./Divider"; \ No newline at end of file diff --git a/frontend/src/components/ErrorPopUp/ErrorPopUp.module.css b/frontend/src/components/ErrorPopUp/ErrorPopUp.module.css new file mode 100644 index 0000000..9e5129f --- /dev/null +++ b/frontend/src/components/ErrorPopUp/ErrorPopUp.module.css @@ -0,0 +1,83 @@ +.container { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(28, 28, 28, 0.60); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; +} + +/* Content */ +.content { + width: 27.75rem; + display: inline-flex; + padding: 1.875rem; + flex-direction: column; + align-items: center; + border-radius: 0.5rem; + border: 1px solid #000; + background: #FFF; + border: none; +} +.errorIcon { + width: 5.625rem; + height: 5.625rem; + margin-bottom: 1.12rem; +} + +.title { + color: #001227; + font-size: 2.25rem; + font-style: normal; + font-weight: 700; + line-height: normal; + margin-top: 1.12rem; + margin-bottom: 1.12rem; +} +.description { + color: rgba(0, 18, 39, 0.50); + text-align: center; + font-size: 1.3125rem; + font-style: normal; + font-weight: 400; + line-height: normal; + white-space: pre-line !important; + word-wrap: break-word; + margin-bottom: 2.5rem; +} + +/* Button Container */ +.buttonContainer { + display: flex; + width: 90%; + justify-content: center; +} +.multipleButtons { + display: flex; + width: 90%; + justify-content: space-between; +} + +/* Button */ +.button { + width: 9.5rem; + height: 3.5rem; + display: inline-flex; + padding: 1.125rem 3.75rem; + justify-content: center; + align-items: center; + border-radius: 1.125rem; + border: none; + + color: #FFF; + font-size: 1.125rem; + font-style: normal; + font-weight: 400; + line-height: normal; + white-space: nowrap; + cursor: pointer; +} \ No newline at end of file diff --git a/frontend/src/components/ErrorPopUp/ErrorPopUp.tsx b/frontend/src/components/ErrorPopUp/ErrorPopUp.tsx new file mode 100644 index 0000000..990eb2d --- /dev/null +++ b/frontend/src/components/ErrorPopUp/ErrorPopUp.tsx @@ -0,0 +1,88 @@ +import React from "react"; +import styles from "./ErrorPopUp.module.css"; +import errorIcon from "@/assets/images/error.png"; + +interface ButtonConfig { + text: string; + onClick: () => void; + buttonColor?: string; +} + +interface ErrorPopUpProps { + isError: boolean; + title: string; + description: string[]; + buttons: ButtonConfig[]; // 버튼 배열 + setIsErrorPopUpOpen?: (isErrorPopUpOpen: boolean) => void; // 기본 닫기 기능용 (선택사항) +} + +export const ErrorPopUp: React.FC = ({ + isError, + title, + description, + buttons, + setIsErrorPopUpOpen +}) => { + // 기본 닫기 핸들러 (버튼이 없을 때의 fallback) + const handleDefaultClose = () => { + if (setIsErrorPopUpOpen) { + setIsErrorPopUpOpen(false); + } + }; + + const ErrorIcon = () => { + return ( + + + + + + + + + ) + } + const AlertIcon = () => { + return ( + + + + + + + + + ) + } + + return ( +
+
+ {isError ? : } +
{title}
+
+ {description.map((line, index, array) => ( + + {line} + {index < array.length - 1 &&
} +
+ ))} +
+
1 ? styles.multipleButtons : ""}`}> + {buttons.map((button, index) => ( + + ))} +
+
+
+ ); +}; + +export default ErrorPopUp; \ No newline at end of file diff --git a/frontend/src/components/ErrorPopUp/index.tsx b/frontend/src/components/ErrorPopUp/index.tsx new file mode 100644 index 0000000..a9224e8 --- /dev/null +++ b/frontend/src/components/ErrorPopUp/index.tsx @@ -0,0 +1 @@ +export { default } from "./ErrorPopUp"; \ No newline at end of file diff --git a/frontend/src/components/Footer/Footer.module.css b/frontend/src/components/Footer/Footer.module.css new file mode 100644 index 0000000..2261190 --- /dev/null +++ b/frontend/src/components/Footer/Footer.module.css @@ -0,0 +1,124 @@ +/* Footer */ +.footer { + background-color: var(--content1, #ffffff); + border-top: 1px solid var(--divider, #e2e8f0); + padding: 1.5rem 11.25rem; +} + +/* Container */ +/* .container { + max-width: 72rem; + margin: 0 auto; + padding: 0 1rem; +} */ + +/* Grid */ +.grid { + display: grid; + grid-template-columns: 1fr; + row-gap: 1.5rem; /* 수직 간격 */ + column-gap: 6.12rem; /* 수평 간격 */ +} +@media (min-width: 940px) { + .grid { + grid-template-columns: repeat(2, 1fr); + } +} +@media (min-width: 1150px) { + .grid { + grid-template-columns: repeat(4, 1fr); + } +} + +/* First Section */ +.firstSection{ + display: flex; + flex-direction: column; + gap: 1rem; +} +.logoContainer{ + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + width: 120px; + height: 49px; +} +.logo { + width: 100%; + height: 100%; + object-fit: cover; +} + +.description { + color: var(--foreground-500, #64748b); + font-size: 0.875rem; + line-height: 1.4; +} + +/* Second Section */ +.secondSection{ + display: flex; + flex-direction: column; + gap: 0.81rem; +} +.sectionTitle{ + color: #001227; + font-size: 1.25rem; + font-style: normal; + font-weight: 700; + line-height: normal; + word-break: keep-all; +} +.link{ + color: #71717A; + font-size: 1rem; + font-style: normal; + font-weight: 500; + line-height: normal; + word-break: keep-all; +} +.link:hover{ + text-decoration: underline; +} +/* Third Section */ +.thirdSection{ + display: flex; + flex-direction: column; + gap: 0.81rem; +} + + +/* Fourth Section */ +.fourthSection{ + display: flex; + flex-direction: column; + gap: 0.81rem; +} +.contactItem{ + display: flex; + align-items: center; + gap: 0.5rem; +} +.contactText{ + color: #71717A; + font-size: 1rem; + font-style: normal; + font-weight: 500; + line-height: normal; +} +.socialLinks{ + display: flex; + align-items: center; + gap: 1.25rem; +} + +/* Copyright */ +.copyright { + border-top: 1px solid var(--divider, #e2e8f0); + margin-top: 2.38rem; + padding-top: 1.56rem; + text-align: center; + font-size: 0.875rem; + color: var(--foreground-500, #64748b); +} \ No newline at end of file diff --git a/frontend/src/components/Footer/Footer.tsx b/frontend/src/components/Footer/Footer.tsx new file mode 100644 index 0000000..1b88248 --- /dev/null +++ b/frontend/src/components/Footer/Footer.tsx @@ -0,0 +1,90 @@ +import React, { useState } from "react"; +import styles from "./Footer.module.css"; +import logo from '../../assets/images/ADO2_Logo.svg'; +import mail from '../../assets/icons/mail.svg'; +import call from '../../assets/icons/call.svg'; +import linkedIn from '../../assets/images/linkedIn.png'; +import facebook from '../../assets/images/facebook.png'; +import youtube from '../../assets/images/youtube.png'; +import TermsOfService from "../Policies/TermsOfService/TermsOfService"; +import PrivacyPolicy from "../Policies/PrivacyPolicy/PrivacyPolicy"; + +export const Footer: React.FC = () => { + const [isTermsOfServiceOpen, setIsTermsOfServiceOpen] = useState(false); + const [isPrivacyPolicyOpen, setIsPrivacyPolicyOpen] = useState(false); + + const handleTermsOfServiceClick = () => { + setIsTermsOfServiceOpen(!isTermsOfServiceOpen); + } + + const handlePrivacyPolicyClick = () => { + setIsPrivacyPolicyOpen(!isPrivacyPolicyOpen); + } + + return ( +
+ {isTermsOfServiceOpen && setIsTermsOfServiceOpen(false)} buttonCount={1} />} + {isPrivacyPolicyOpen && setIsPrivacyPolicyOpen(false)} buttonCount={1} />} +
+
+
+ + logo + +
+ AI 기술로 홍보영상과
+ 음악을 자동으로 생성하는 서비스입니다. +
+
+ + + +
+
고객지원
+
이용약관
+ 문의하기 + 자주 묻는 질문 +
개인정보처리방침
+
+ +
+
연락처
+ +
+ mail +
o2oteam@o2o.kr
+
+ +
+ call +
070-4260-8310
+
+ + +
+
+ +
+ © 2025 에이아이오투오 All rights reserved. +
+
+
+ ); +}; + +export default Footer; \ No newline at end of file diff --git a/frontend/src/components/Footer/index.tsx b/frontend/src/components/Footer/index.tsx new file mode 100644 index 0000000..b12f605 --- /dev/null +++ b/frontend/src/components/Footer/index.tsx @@ -0,0 +1 @@ +export { default } from './Footer'; \ No newline at end of file diff --git a/frontend/src/components/Header/Header.module.css b/frontend/src/components/Header/Header.module.css new file mode 100644 index 0000000..1beec2a --- /dev/null +++ b/frontend/src/components/Header/Header.module.css @@ -0,0 +1,101 @@ +.container { + display: flex; + align-items: center; + padding: 16px 9.69rem; + background-color: #ffffff; + border-bottom: 1px solid #e5e7eb; +} +.logoContainer{ + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + gap: 10px; + width: 160px; + height: 49px; + cursor: pointer; +} + +.logo { + width: 100%; + height: 100%; + object-fit: cover; +} +.content { + display: flex; + align-items: center; + white-space: nowrap; + flex-wrap: nowrap; + margin-left: auto; +} + +.name, +.myPage, +.login, +.logout { + color: #001227; + font-size: 1rem; + font-style: normal; + font-weight: 500; + line-height: normal; + white-space: nowrap; +} + +.start { + margin-left: 2.29rem; + font-size: 1rem; + font-style: normal; + font-weight: 700; + line-height: normal; + cursor: pointer; + position: relative; + display: inline-block; + color: #001227; + transition: all 0.4s ease; + padding-bottom: 4px; + margin-top: 4px; + white-space: nowrap; + word-break: keep-all; +} + +.start::after { + content: ''; + position: absolute; + bottom: 0; + left: 50%; + width: 0; + height: 2px; + background: linear-gradient(90deg, rgba(192, 56, 255, 0.80) 0%, rgba(9, 210, 255, 0.80) 100%); + transition: all 0.4s ease; + transform: translateX(-50%); +} + +.start:hover { + background: linear-gradient(90deg, rgba(192, 56, 255, 0.80) 0%, rgba(9, 210, 255, 0.80) 100%); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; +} + +.start:hover::after { + width: 100%; +} + + +.icon{ + margin-right: 0.5rem; +} +.name{ + margin-right: 1.5rem; +} +.myPage{ + margin-right: 1.5rem; + cursor: pointer; +} +.logout{ + cursor: pointer; +} +.login{ + margin-right: 2.25rem; + cursor: pointer; +} \ No newline at end of file diff --git a/frontend/src/components/Header/Header.tsx b/frontend/src/components/Header/Header.tsx new file mode 100644 index 0000000..b4f2fb4 --- /dev/null +++ b/frontend/src/components/Header/Header.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import styles from './Header.module.css'; +import logo from '../../assets/images/ADO2_Logo.svg'; +import { useNavigate } from 'react-router-dom'; +import { useRecoilValue } from "recoil"; +import { authAtom } from "@/recoil/Atoms/authAtom"; +import { useLogout } from "@/hooks/useLogout"; +import Icon from '../../assets/icons/me.svg'; + +const Header: React.FC = () => { + const navigate = useNavigate(); + const authState = useRecoilValue(authAtom); + + const goToHome = () => { + localStorage.removeItem("task_id"); + localStorage.removeItem("order_id"); + navigate('/'); + if(localStorage.getItem("currentStep") === "2"){ + localStorage.removeItem("currentStep"); + } + } + const goToLogin = () => navigate('/login'); + const goToMyPage = () => navigate('/mypage'); + const goToStart = () => { + localStorage.removeItem("task_id"); + localStorage.removeItem("order_id"); + localStorage.removeItem("currentStep"); + navigate('/process'); + window.location.reload(); + } + const goToLogout = useLogout(); + + return ( +
+
+ logo +
+
새 동영상 만들기
+
+ {authState.isLoggedIn ? ( + <> + icon +
{authState.name}님
+
마이페이지
+
goToLogout()}>로그아웃
+ + ) : ( +
로그인
+ )} +
+
+ ); +} + +export default Header; diff --git a/frontend/src/components/Header/index.tsx b/frontend/src/components/Header/index.tsx new file mode 100644 index 0000000..579f1ac --- /dev/null +++ b/frontend/src/components/Header/index.tsx @@ -0,0 +1 @@ +export { default } from './Header'; diff --git a/frontend/src/components/LabeledInput/LabeledInput.module.css b/frontend/src/components/LabeledInput/LabeledInput.module.css new file mode 100644 index 0000000..c0caae5 --- /dev/null +++ b/frontend/src/components/LabeledInput/LabeledInput.module.css @@ -0,0 +1,93 @@ +.inputContainer{ + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 0.5rem; + width: 100%; + height: 5.19rem; +} + +.inputGroup { + display: flex; + width: 100%; + padding: 0.75rem; + flex-direction: column; + align-items: flex-start; + gap: 0.25rem; + box-sizing: border-box; + border-radius: 0.75rem; + border: 1px solid rgba(0, 18, 39, 0.50); + background: #FFF; + cursor: text; +} + +/* .inputGroup:hover { + background: #d1d5db; +} */ + +.inputLabel { + display: flex; + flex-direction: row; + gap: 0.25rem; +} + +.labelText { + color: #001227; + font-size: 0.75rem; + font-style: normal; + font-weight: 400; + line-height: normal; +} + +.required { + font-size: 0.75rem; + font-weight: 400; + color: #FF0000; +} + +.inputWrapper { + display: flex; + width: 100%; + flex-direction: row; + gap: 0.25rem; + align-items: center; +} + +.inputIcon { + width: 1rem; + height: 1rem; +} + +.input { + border: none; + background: transparent; + outline: none; + width: 100%; + color: #001227; + font-size: 1rem; + font-style: normal; + font-weight: 400; + line-height: normal; +} +.input::placeholder { + color: rgba(0, 18, 39, 0.50); + font-size: 1rem; + font-style: normal; + font-weight: 400; + line-height: normal; +} + + +.errorMessage{ + color: #FF1E1E; + font-size: 0.75rem; + font-style: normal; + font-weight: 400; + line-height: normal; +} + +.eyeIcon{ + width: 1rem; + height: 1rem; + cursor: pointer; +} \ No newline at end of file diff --git a/frontend/src/components/LabeledInput/LabeledInput.tsx b/frontend/src/components/LabeledInput/LabeledInput.tsx new file mode 100644 index 0000000..028f105 --- /dev/null +++ b/frontend/src/components/LabeledInput/LabeledInput.tsx @@ -0,0 +1,90 @@ +import React, { useRef, useEffect, useState } from "react"; +import styles from "./LabeledInput.module.css"; +import eyeOn from "@/assets/icons/eye_on.svg"; +import eyeOff from "@/assets/icons/eye_off.svg"; + +interface LabeledInputProps { + label: string; + required?: boolean; + icon?: React.ReactNode; + value: string; + onChange: (value: string) => void; + placeholder?: string; + type?: string; + autoComplete?: string; + errorMessage?: string; + passwordVisible?: boolean; + onErrorClear?: () => void; +} + +const LabeledInput: React.FC = ({ + label, + required = false, + icon, + value, + onChange, + placeholder, + type = "text", + errorMessage, + passwordVisible, + onErrorClear, +}) => { + const inputRef = useRef(null); + const [isPasswordVisible, setIsPasswordVisible] = useState(false); + + const inputType = + type === "password" ? (isPasswordVisible ? "text" : "password") : type; + + const handleInputChange = (newValue: string) => { + onChange(newValue); + // 값이 변경될 때 에러 메시지 초기화 + if (onErrorClear) { + onErrorClear(); + } + }; + + return ( +
+
inputRef.current?.focus()} + > +
+
{label}
+ {required &&
*
} +
+ +
+ {icon &&
{icon}
} + + handleInputChange(e.target.value)} + /> + + {passwordVisible && type === "password" && ( + toggle password visibility { + e.stopPropagation(); + setIsPasswordVisible((prev) => !prev); + }} + /> + )} +
+
+ + {errorMessage && ( +
{errorMessage}
+ )} +
+ ); +}; + +export default LabeledInput; diff --git a/frontend/src/components/LabeledInput/index.tsx b/frontend/src/components/LabeledInput/index.tsx new file mode 100644 index 0000000..59bdef2 --- /dev/null +++ b/frontend/src/components/LabeledInput/index.tsx @@ -0,0 +1 @@ +export { default } from "./LabeledInput"; \ No newline at end of file diff --git a/frontend/src/components/MenuTab/MenuTab.module.css b/frontend/src/components/MenuTab/MenuTab.module.css new file mode 100644 index 0000000..b63de1b --- /dev/null +++ b/frontend/src/components/MenuTab/MenuTab.module.css @@ -0,0 +1,67 @@ +.wrapper { + display: flex; + width: fit-content; + padding: 0.25rem; + gap: 0.25rem; + box-sizing: border-box; + border-radius: 0.75rem; + background: #F1F1F1; +} + +.container { + display: flex; + position: relative; + border-radius: 0.75rem; + box-sizing: border-box; +} + +.tab { + /* Layout & Display */ + position: relative; + flex: 1; + display: flex; + align-items: center; + justify-content: center; + + /* Box Model */ + padding: 0.625rem; + box-sizing: border-box; + + /* Typography */ + font-size: 1rem; + font-style: normal; + font-weight: 500; + line-height: normal; + text-align: center; + white-space: nowrap; + + /* Visual */ + color: #001227; + + /* Interaction */ + cursor: pointer; + transition: color 0.3s; + + /* Layer */ + z-index: 2; +} + +.active { + color: #FFF; +} + +.slider { + position: absolute; + top: 0; + bottom: 0; + border-radius: 0.5rem; + background: #4EA3FF; + transition: left 0.3s ease, width 0.3s ease; + z-index: 1; + box-sizing: border-box; + opacity: 0; /* 초기에 숨김 */ +} + +.slider.ready { + opacity: 1; /* 레이아웃 계산 완료 후 표시 */ +} \ No newline at end of file diff --git a/frontend/src/components/MenuTab/MenuTab.tsx b/frontend/src/components/MenuTab/MenuTab.tsx new file mode 100644 index 0000000..0e5f815 --- /dev/null +++ b/frontend/src/components/MenuTab/MenuTab.tsx @@ -0,0 +1,65 @@ +import React, { useState, useRef, useEffect } from 'react'; +import styles from './MenuTab.module.css'; + +interface MenuTabProps { + value: string[]; + onSelect?: (index: number) => void; +} + +const MenuTab: React.FC = ({ value, onSelect }) => { + const [activeIndex, setActiveIndex] = useState(0); + const [sliderStyle, setSliderStyle] = useState({ left: 0, width: 0 }); + const [isReady, setIsReady] = useState(false); + const tabsRef = useRef<(HTMLDivElement | null)[]>([]); + + useEffect(() => { + const updateSlider = () => { + const activeTab = tabsRef.current[activeIndex]; + if (activeTab) { + setSliderStyle({ + left: activeTab.offsetLeft, + width: activeTab.clientWidth, + }); + setIsReady(true); + } + }; + + // 레이아웃 완료를 보장하기 위해 다음 프레임에서 실행 + requestAnimationFrame(() => { + requestAnimationFrame(updateSlider); + }); + }, [activeIndex, value]); + + const handleClick = (index: number) => { + setActiveIndex(index); + if (onSelect) onSelect(index); + }; + + return ( +
+
+
+ {value.map((label, index) => ( +
{ + tabsRef.current[index] = el; + }} + className={`${styles.tab} ${activeIndex === index ? styles.active : ''}`} + onClick={() => handleClick(index)} + > + {label} +
+ ))} +
+
+ ); +}; + +export default MenuTab; \ No newline at end of file diff --git a/frontend/src/components/MenuTab/index.tsx b/frontend/src/components/MenuTab/index.tsx new file mode 100644 index 0000000..48656eb --- /dev/null +++ b/frontend/src/components/MenuTab/index.tsx @@ -0,0 +1 @@ +export { default } from './MenuTab'; \ No newline at end of file diff --git a/frontend/src/components/NotValue/NotValue.module.css b/frontend/src/components/NotValue/NotValue.module.css new file mode 100644 index 0000000..7c7a600 --- /dev/null +++ b/frontend/src/components/NotValue/NotValue.module.css @@ -0,0 +1,24 @@ +.container{ + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + gap: 2.5rem; + flex: 1; +} + +.image{ + width: 6.25rem; + height: 7.5rem; + aspect-ratio: 5/6; +} + +.text{ + color: rgba(0, 18, 39, 0.50); + font-size: 1.5rem; + font-style: normal; + font-weight: 500; + line-height: normal; +} diff --git a/frontend/src/components/NotValue/NotValue.tsx b/frontend/src/components/NotValue/NotValue.tsx new file mode 100644 index 0000000..c105647 --- /dev/null +++ b/frontend/src/components/NotValue/NotValue.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import styles from "./NotValue.module.css"; +import notValue from "@/assets/images/not_value.png"; + +interface NotValueProps { + text: string; +} + +const NotValue:React.FC = ({text}) => { + return ( +
+ notValue +
{text}
+
+ ) +} + +export default NotValue; \ No newline at end of file diff --git a/frontend/src/components/NotValue/index.tsx b/frontend/src/components/NotValue/index.tsx new file mode 100644 index 0000000..0d298ae --- /dev/null +++ b/frontend/src/components/NotValue/index.tsx @@ -0,0 +1 @@ +export { default } from "./NotValue"; \ No newline at end of file diff --git a/frontend/src/components/Policies/MarketingConsent/MarketingConsent.module.css b/frontend/src/components/Policies/MarketingConsent/MarketingConsent.module.css new file mode 100644 index 0000000..f12eefb --- /dev/null +++ b/frontend/src/components/Policies/MarketingConsent/MarketingConsent.module.css @@ -0,0 +1,91 @@ +.container { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(28, 28, 28, 0.60); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; +} + +/* Box Container */ +.boxContainer { + display: flex; + flex-direction: column; + width: 27.75rem; + height: auto; + padding: 2.5rem; + border-radius: 0.75rem; + background: #FFF; + border: none; +} + +/* header */ +.header { + color: #001227; + font-size: 1.5rem; + font-style: normal; + font-weight: 700; + line-height: normal; + margin-bottom: 1.25rem; +} + +/* Content Container */ +.contentContainer { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.text { + color: #000; + font-size: 1rem; + font-style: normal; + font-weight: 400; + line-height: normal; + word-break: keep-all; +} + +/* Bold Text Container */ +.boldTextContainer { + display: flex; +} + +.boldText { + color: #000; + font-size: 1rem; + font-style: normal; + font-weight: 700; + line-height: normal; + word-break: keep-all; +} + +/* Button Container */ +.buttonContainer { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 3.75rem; +} + +.button { + display: flex; + padding: 1.125rem 3.75rem; + justify-content: center; + align-items: center; + gap: 1.125rem; + border-radius: 1.125rem; + background: #B8B5FF; + border: none; + + color: #FFF; + font-size: 1.125rem; + font-style: normal; + font-weight: 400; + line-height: normal; + + cursor: pointer; +} \ No newline at end of file diff --git a/frontend/src/components/Policies/MarketingConsent/MarketingConsent.tsx b/frontend/src/components/Policies/MarketingConsent/MarketingConsent.tsx new file mode 100644 index 0000000..a1ea570 --- /dev/null +++ b/frontend/src/components/Policies/MarketingConsent/MarketingConsent.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import styles from './MarketingConsent.module.css'; + +interface MarketingConsentProps { + setOK: (ok: boolean) => void; + setIsMarketingConsentOpen: (isMarketingConsentOpen: boolean) => void; +} + +const MarketingConsent:React.FC = ({setIsMarketingConsentOpen, setOK}) => { + const handleClose = () => { + setIsMarketingConsentOpen(false); + } + + const handleOK = () => { + setOK(true); + setIsMarketingConsentOpen(false); + } + + return ( +
+
e.stopPropagation()}> +
광고성 정보 수신
+ +
+
귀하께서 제공하신 개인정보는 회사의 상품·서비스에 대한 이벤트, 혜택, 프로모션 안내 등 광고성 정보 전달을 목적으로 활용될 수 있습니다.
+
광고성 정보는 문자(SMS), 이메일, 서비스 알림 등 다양한 수단을 통해 발송될 수 있으며, 동의를 거부하실 수 있습니다. 다만, 동의하지 않으시는 경우 이벤트·혜택 안내 등의 제공이 제한될 수 있습니다.
+ +
+
- 수집 항목: 성명, 연락처(휴대전화번호, 이메일), 알림 수신 여부 등
+
+
+
- 이용 목적: 신상품 안내, 이벤트·혜택 정보 제공, 설문조사, 사전 알림 등
+
+
+
- 전송 방법: 문자, 이메일, 서비스 내 알림 등
+
+
+
- 보유 기간: 수신 동의 철회 또는 회원 탈퇴 시까지
+
+
+
+ + +
+
+
+ ); +}; + +export default MarketingConsent; \ No newline at end of file diff --git a/frontend/src/components/Policies/MarketingConsent/index.tsx b/frontend/src/components/Policies/MarketingConsent/index.tsx new file mode 100644 index 0000000..27ac0d3 --- /dev/null +++ b/frontend/src/components/Policies/MarketingConsent/index.tsx @@ -0,0 +1 @@ +export { default } from './MarketingConsent'; \ No newline at end of file diff --git a/frontend/src/components/Policies/PrivacyPolicy/PrivacyPolicy.module.css b/frontend/src/components/Policies/PrivacyPolicy/PrivacyPolicy.module.css new file mode 100644 index 0000000..268f2a3 --- /dev/null +++ b/frontend/src/components/Policies/PrivacyPolicy/PrivacyPolicy.module.css @@ -0,0 +1,88 @@ +.container { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(28, 28, 28, 0.60); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; +} + +/* Box Container */ +.boxContainer { + display: flex; + flex-direction: column; + width: 27.75rem; + height: auto; + padding: 2.5rem; + border-radius: 0.75rem; + background: #FFF; + border: none; +} + +/* Title */ +.header { + color: #001227; + font-size: 1.5rem; + font-style: normal; + font-weight: 700; + line-height: normal; + margin-bottom: 1.25rem; +} + +.contentContainer { + display: flex; + flex-direction: column; + gap: 1.5rem; + height: 25rem; + overflow-y: auto; + scrollbar-width: thin; +} + +.title { + color: #000; + font-size: 1rem; + font-style: normal; + font-weight: 700; + line-height: normal; +} + +.text { + color: #000; + font-size: 1rem; + font-style: normal; + font-weight: 400; + line-height: normal; + word-break: keep-all; +} + +/* Button Container */ +.buttonContainer { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 3.75rem; +} + +.button { + display: flex; + /* padding: 12px 25px; */ + padding: 18px 60px; + justify-content: center; + align-items: center; + gap: 1.125rem; + border-radius: 1.125rem; + background: #B8B5FF; + border: none; + + color: #FFF; + font-size: 1.125rem; + font-style: normal; + font-weight: 400; + line-height: normal; + + cursor: pointer; +} \ No newline at end of file diff --git a/frontend/src/components/Policies/PrivacyPolicy/PrivacyPolicy.tsx b/frontend/src/components/Policies/PrivacyPolicy/PrivacyPolicy.tsx new file mode 100644 index 0000000..9402f3c --- /dev/null +++ b/frontend/src/components/Policies/PrivacyPolicy/PrivacyPolicy.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import styles from './PrivacyPolicy.module.css'; + +interface PrivacyPolicyProps { + setOK: (ok: boolean) => void; + setIsPrivacyPolicyOpen: (isPrivacyPolicyOpen: boolean) => void; + buttonCount: number; +} + +const PrivacyPolicy: React.FC = ({ setIsPrivacyPolicyOpen, setOK, buttonCount }) => { + const handleClose = () => { + setIsPrivacyPolicyOpen(false); + } + + const handleOK = () => { + setOK(true); + setIsPrivacyPolicyOpen(false); + } + + return ( +
+
e.stopPropagation()}> +
개인정보처리방침
+ +
+
1. 개인정보의 수집 및 이용 목적
+
회사는 다음의 목적을 위하여 개인정보를 처리합니다. 처리하고 있는 개인정보는 다음의 목적 이외의 용도로는 이용되지 않으며, 이용 목적이 변경되는 경우에는 개인정보 보호법 제18조에 따라 별도의 동의를 받는 등 필요한 조치를 이행할 예정입니다.
+
회원 가입 및 관리: 회원 가입의사 확인, 회원제 서비스 제공에 따른 본인 식별·인증, 회원자격 유지·관리, 서비스 부정이용 방지, 각종 고지·통지 등을 목적으로 개인정보를 처리합니다.
+
서비스 제공: 콘텐츠 제공, 맞춤서비스 제공, 서비스 개선 등을 목적으로 개인정보를 처리합니다.
+
마케팅 및 광고에의 활용: 신규 서비스 개발 및 맞춤 서비스 제공, 이벤트 및 광고성 정보 제공 및 참여기회 제공, 서비스의 유효성 확인, 접속빈도 파악 또는 회원의 서비스 이용에 대한 통계 등을 목적으로 개인정보를 처리합니다.
+ +
2. 수집하는 개인정보의 항목
+
회사는 회원가입, 상담, 서비스 신청 등을 위해 아래와 같은 개인정보를 수집하고 있습니다.
+
수집항목: 이름, 이메일 주소, 비밀번호, 서비스 이용 기록, 접속 로그, 쿠키, 접속 IP 정보
+
개인정보 수집방법: 홈페이지(회원가입), 서비스 이용 과정에서 자동 생성
+ +
3. 개인정보의 보유 및 이용기간
+
회사는 법령에 따른 개인정보 보유·이용기간 또는 정보주체로부터 개인정보를 수집 시에 동의 받은 개인정보 보유·이용기간 내에서 개인정보를 처리·보유합니다.
+
회원 가입 및 관리: 회원탈퇴 시까지
+
다만, 다음의 사유에 해당하는 경우에는 해당 사유 종료 시까지
+
관계 법령 위반에 따른 수사·조사 등이 진행 중인 경우에는 해당 수사·조사 종료 시까지
+
서비스 이용에 따른 채권·채무관계 잔존 시에는 해당 채권·채무관계 정산 시까지
+ +
4. 개인정보의 파기절차 및 방법
+
회사는 원칙적으로 개인정보 처리목적이 달성된 경우에는 지체 없이 해당 개인정보를 파기합니다. 파기의 절차, 기한 및 방법은 다음과 같습니다.
+
파기절차: 이용자가 입력한 정보는 목적 달성 후 별도의 DB에 옮겨져 내부 방침 및 기타 관련 법령에 따라 일정기간 저장된 후 혹은 즉시 파기됩니다.
+
파기기한: 이용자의 개인정보는 개인정보의 보유기간이 경과된 경우에는 보유기간의 종료일로부터 5일 이내에, 개인정보의 처리 목적 달성, 해당 서비스의 폐지, 사업의 종료 등 그 개인정보가 불필요하게 되었을 때에는 개인정보의 처리가 불필요한 것으로 인정되는 날로부터 5일 이내에 그 개인정보를 파기합니다.
+
+ {buttonCount === 1 && ( +
+ +
+ )} + {buttonCount === 2 && ( +
+ + +
+ )} +
+
+ ); +}; + +export default PrivacyPolicy; \ No newline at end of file diff --git a/frontend/src/components/Policies/PrivacyPolicy/index.tsx b/frontend/src/components/Policies/PrivacyPolicy/index.tsx new file mode 100644 index 0000000..d551773 --- /dev/null +++ b/frontend/src/components/Policies/PrivacyPolicy/index.tsx @@ -0,0 +1 @@ +export { default } from './PrivacyPolicy'; \ No newline at end of file diff --git a/frontend/src/components/Policies/TermsOfService/TermsOfService.module.css b/frontend/src/components/Policies/TermsOfService/TermsOfService.module.css new file mode 100644 index 0000000..268f2a3 --- /dev/null +++ b/frontend/src/components/Policies/TermsOfService/TermsOfService.module.css @@ -0,0 +1,88 @@ +.container { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(28, 28, 28, 0.60); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; +} + +/* Box Container */ +.boxContainer { + display: flex; + flex-direction: column; + width: 27.75rem; + height: auto; + padding: 2.5rem; + border-radius: 0.75rem; + background: #FFF; + border: none; +} + +/* Title */ +.header { + color: #001227; + font-size: 1.5rem; + font-style: normal; + font-weight: 700; + line-height: normal; + margin-bottom: 1.25rem; +} + +.contentContainer { + display: flex; + flex-direction: column; + gap: 1.5rem; + height: 25rem; + overflow-y: auto; + scrollbar-width: thin; +} + +.title { + color: #000; + font-size: 1rem; + font-style: normal; + font-weight: 700; + line-height: normal; +} + +.text { + color: #000; + font-size: 1rem; + font-style: normal; + font-weight: 400; + line-height: normal; + word-break: keep-all; +} + +/* Button Container */ +.buttonContainer { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 3.75rem; +} + +.button { + display: flex; + /* padding: 12px 25px; */ + padding: 18px 60px; + justify-content: center; + align-items: center; + gap: 1.125rem; + border-radius: 1.125rem; + background: #B8B5FF; + border: none; + + color: #FFF; + font-size: 1.125rem; + font-style: normal; + font-weight: 400; + line-height: normal; + + cursor: pointer; +} \ No newline at end of file diff --git a/frontend/src/components/Policies/TermsOfService/TermsOfService.tsx b/frontend/src/components/Policies/TermsOfService/TermsOfService.tsx new file mode 100644 index 0000000..f93b6fc --- /dev/null +++ b/frontend/src/components/Policies/TermsOfService/TermsOfService.tsx @@ -0,0 +1,67 @@ +import React from 'react'; +import styles from './TermsOfService.module.css'; + +interface TermsOfServiceProps { + setOK: (ok: boolean) => void; + setIsTermsOfServiceOpen: (isTermsOfServiceOpen: boolean) => void; + buttonCount: number; +} + +const TermsOfService: React.FC = ({ setIsTermsOfServiceOpen, setOK, buttonCount }) => { + const handleClose = () => { + setIsTermsOfServiceOpen(false); + } + + const handleOK = () => { + setOK(true); + setIsTermsOfServiceOpen(false); + } + + return ( +
+
e.stopPropagation()}> +
이용약관
+ +
+
제 1 조 (목적)
+
이 약관은 오투사운드 서비스(이하 "서비스")를 제공하는 회사(이하 "회사")와 이를 이용하는 회원(이하 "회원") 간의 권리, 의무 및 책임사항, 서비스 이용조건 및 절차 등 기본적인 사항을 규정함을 목적으로 합니다.
+ +
제 2 조 (정의)
+
이 약관에서 사용하는 용어의 정의는 다음과 같습니다.
+
"서비스"란 회사가 제공하는 펜션 홍보 영상 및 음악 생성 서비스를 의미합니다.
+
"회원"이란 회사와 서비스 이용계약을 체결하고 회사가 제공하는 서비스를 이용하는 자를 의미합니다.
+
"아이디(ID)"란 회원의 식별과 서비스 이용을 위하여 회원이 설정하고 회사가 승인하는 문자와 숫자의 조합을 의미합니다.
+
"비밀번호"란 회원이 부여 받은 아이디와 일치되는 회원임을 확인하고 비밀보호를 위해 회원 자신이 정한 문자 또는 숫자의 조합을 의미합니다.
+ +
제 3 조 (약관의 게시와 개정)
+
회사는 이 약관의 내용을 회원이 쉽게 알 수 있도록 서비스 초기 화면에 게시합니다. 회사는 필요한 경우 관련법령을 위배하지 않는 범위 내에서 이 약관을 개정할 수 있습니다.
+ +
제 4 조 (서비스의 제공 및 변경)
+
회사는 다음과 같은 서비스를 제공합니다.
+
- 펜션 이미지 자동 수집 서비스
+
- AI 기반 음악 생성 서비스
+
- 홍보 영상 제작 서비스
+
- 기타 회사가 추가 개발하거나 다른 회사와의 제휴계약 등을 통해 회원에게 제공하는 일체의 서비스
+ +
제 5 조 (서비스 이용시간)
+
서비스 이용은 회사의 업무상 또는 기술상 특별한 지장이 없는 한 연중무휴, 1일 24시간 운영을 원칙으로 합니다.
+
단, 회사는 시스템 정기점검, 증설 및 교체를 위해 회사가 정한 날이나 시간에 서비스를 일시 중단할 수 있으며, 예정되어 있는 작업으로 인한 서비스 일시 중단은 서비스 페이지를 통해 사전에 공지합니다.
+
이용자는 회사가 정한 가입 양식에 따라 회원정보를 기입한 후 이 약관에 동의한다는 의사표시를 함으로서 회원가입을 신청합니다.
+
+ {buttonCount === 1 && ( +
+ +
+ )} + {buttonCount === 2 && ( +
+ + +
+ )} +
+
+ ); +}; + +export default TermsOfService; \ No newline at end of file diff --git a/frontend/src/components/Policies/TermsOfService/index.tsx b/frontend/src/components/Policies/TermsOfService/index.tsx new file mode 100644 index 0000000..fdbd56b --- /dev/null +++ b/frontend/src/components/Policies/TermsOfService/index.tsx @@ -0,0 +1 @@ +export { default } from './TermsOfService'; \ No newline at end of file diff --git a/frontend/src/components/ProgressBar/ProgressBar.module.css b/frontend/src/components/ProgressBar/ProgressBar.module.css new file mode 100644 index 0000000..6af6f4b --- /dev/null +++ b/frontend/src/components/ProgressBar/ProgressBar.module.css @@ -0,0 +1,99 @@ +.container { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(28, 28, 28, 0.60); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; +} + +/* Content */ +.boxContainer { + display: flex; + flex-direction: column; + width: 55.9375rem; + padding: 2.5rem; + border-radius: 1rem; + background: #FFF; + gap: 2.5rem; + border: none; +} + +/* Title Container */ +.titleContainer { + display: flex; + flex-direction: column; + align-items: center; + gap: 1.5rem; +} + +/* Title */ +.title { + color: #000; + text-align: center; + font-size: 2.25rem; + font-style: normal; + font-weight: 700; + line-height: normal; + user-select: none; +} + +/* Subtitle */ +.subTitle { + color: #C2C2C2; + font-size: 1.5rem; + font-style: normal; + font-weight: 700; + line-height: normal; + user-select: none; +} + + +/* Description */ +.description { + display: flex; + align-items: center; + flex-direction: row; + justify-content: space-between; +} + +/* Text */ +.text{ + display: flex; + text-align: left; + color: #000; + font-size: 1.25rem; + font-style: normal; + font-weight: 700; + line-height: normal; + user-select: none; +} +.number { + display: flex; + text-align: right; + color: #000; + font-size: 1.25rem; + font-style: normal; + font-weight: 700; + line-height: normal; + user-select: none; +} + +/* Progress Bar */ +.progressBar{ + width: 100%; + height: 1.5rem; + border-radius: 1.125rem; + background: #F1F1F1; +} + +.progressFill{ + height: 100%; + border-radius: 1.125rem; + background: #B8B5FF; + transition: width 0.1s ease; +} \ No newline at end of file diff --git a/frontend/src/components/ProgressBar/ProgressBar.tsx b/frontend/src/components/ProgressBar/ProgressBar.tsx new file mode 100644 index 0000000..7ed8b02 --- /dev/null +++ b/frontend/src/components/ProgressBar/ProgressBar.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import styles from './ProgressBar.module.css'; + +interface ProgressBarProps { + onComplete: () => void; +} + +const ProgressBar: React.FC = ({ onComplete }) => { + const [progress, setProgress] = React.useState(0); + + React.useEffect(() => { + if (progress < 100) { + const timer = setTimeout(() => { + setProgress(prev => prev + 1); + }, 50); // 2초에 걸쳐 100%까지 진행 + return () => clearTimeout(timer); + } else if (progress === 100) { + onComplete(); + } + }, [progress, onComplete]); + + return ( +
+
+
+
영상 생성 중
+
홍보 콘텐츠를 생성하고 있습니다. 잠시만 기다려 주세요.
+
+
+
영상 생성 중...
+
{progress}%
+
+
+
+
+
+
+ ); +}; + +export default ProgressBar; \ No newline at end of file diff --git a/frontend/src/components/ProgressBar/index.tsx b/frontend/src/components/ProgressBar/index.tsx new file mode 100644 index 0000000..fa15e6b --- /dev/null +++ b/frontend/src/components/ProgressBar/index.tsx @@ -0,0 +1 @@ +export { default } from './ProgressBar'; \ No newline at end of file diff --git a/frontend/src/components/SimpleInput/SimpleInput.module.css b/frontend/src/components/SimpleInput/SimpleInput.module.css new file mode 100644 index 0000000..3aabc00 --- /dev/null +++ b/frontend/src/components/SimpleInput/SimpleInput.module.css @@ -0,0 +1,42 @@ +.container { + display: flex; + padding: 1rem 1.25rem; + flex-direction: column; + align-items: flex-start; + gap: 0.5rem; + align-self: stretch; + border-radius: 0.75rem; + border: 1px solid rgba(0, 18, 39, 0.50); + background: #FFF; +} + +.label { + color: #001227; + font-size: 0.75rem; + font-style: normal; + font-weight: 500; + line-height: normal; +} + +.inputWrapper { + width: 100%; +} + +.input { + width: 100%; + border: none; + border-radius: 0.5rem; + background-color: transparent; + outline: none; + + color: #001227; + font-size: 1rem; + font-style: normal; + font-weight: 500; + line-height: normal; +} + +.input:disabled { + background-color: transparent; + cursor: not-allowed; +} diff --git a/frontend/src/components/SimpleInput/SimpleInput.tsx b/frontend/src/components/SimpleInput/SimpleInput.tsx new file mode 100644 index 0000000..a6bda05 --- /dev/null +++ b/frontend/src/components/SimpleInput/SimpleInput.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import styles from './SimpleInput.module.css'; + +interface SimpleInputProps { + label: string; + value: string; + onChange: (e: React.ChangeEvent) => void; + placeholder?: string; + disabled?: boolean; +} + +export const SimpleInput: React.FC = ({ + label, + value, + onChange, + placeholder = '', + disabled = false, +}) => { + return ( +
+
{label}
+
+ +
+
+ ); +}; + +export default SimpleInput; diff --git a/frontend/src/components/SimpleInput/index.tsx b/frontend/src/components/SimpleInput/index.tsx new file mode 100644 index 0000000..e206c3b --- /dev/null +++ b/frontend/src/components/SimpleInput/index.tsx @@ -0,0 +1 @@ +export { SimpleInput } from './SimpleInput'; \ No newline at end of file diff --git a/frontend/src/components/SimpleLoading/SimpleLoading.module.css b/frontend/src/components/SimpleLoading/SimpleLoading.module.css new file mode 100644 index 0000000..c9a74ae --- /dev/null +++ b/frontend/src/components/SimpleLoading/SimpleLoading.module.css @@ -0,0 +1,44 @@ +.container{ + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(28, 28, 28, 0.60); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; +} + +.loadingContainer{ + width: 4rem; + height: 4rem; +} +.loadingSpinner { + width: 100%; + height: 100%; + aspect-ratio: 1; + border-radius: 50%; + border: 0.25rem solid #fff; + animation: + spinnerClip 1.0s infinite linear alternate, + spinnerRotate 2.0s infinite linear; +} + +@keyframes spinnerClip { + 0% {clip-path: polygon(50% 50%,0 0, 50% 0%, 50% 0%, 50% 0%, 50% 0%, 50% 0% )} + 12.5% {clip-path: polygon(50% 50%,0 0, 50% 0%, 100% 0%, 100% 0%, 100% 0%, 100% 0% )} + 25% {clip-path: polygon(50% 50%,0 0, 50% 0%, 100% 0%, 100% 100%, 100% 100%, 100% 100% )} + 50% {clip-path: polygon(50% 50%,0 0, 50% 0%, 100% 0%, 100% 100%, 50% 100%, 0% 100% )} + 62.5% {clip-path: polygon(50% 50%,100% 0, 100% 0%, 100% 0%, 100% 100%, 50% 100%, 0% 100% )} + 75% {clip-path: polygon(50% 50%,100% 100%, 100% 100%, 100% 100%, 100% 100%, 50% 100%, 0% 100% )} + 100% {clip-path: polygon(50% 50%,50% 100%, 50% 100%, 50% 100%, 50% 100%, 50% 100%, 0% 100% )} +} + +@keyframes spinnerRotate { + 0% {transform:scaleY(1) rotate(0deg)} + 49.99%{transform:scaleY(1) rotate(135deg)} + 50% {transform:scaleY(-1) rotate(0deg)} + 100% {transform:scaleY(-1) rotate(-135deg)} +} \ No newline at end of file diff --git a/frontend/src/components/SimpleLoading/SimpleLoading.tsx b/frontend/src/components/SimpleLoading/SimpleLoading.tsx new file mode 100644 index 0000000..ec51722 --- /dev/null +++ b/frontend/src/components/SimpleLoading/SimpleLoading.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import styles from "./SimpleLoading.module.css"; + +interface SimpleLoadingProps { + isLoading: boolean; +} + +const SimpleLoading: React.FC = ({ isLoading }) => { + return ( +
+
+
+
+
+ ); +}; + +export default SimpleLoading; \ No newline at end of file diff --git a/frontend/src/components/SimpleLoading/index.tsx b/frontend/src/components/SimpleLoading/index.tsx new file mode 100644 index 0000000..f63baa9 --- /dev/null +++ b/frontend/src/components/SimpleLoading/index.tsx @@ -0,0 +1 @@ +export { default } from "./SimpleLoading"; \ No newline at end of file diff --git a/frontend/src/components/SocialLogin/SocialLogin.module.css b/frontend/src/components/SocialLogin/SocialLogin.module.css new file mode 100644 index 0000000..9478e67 --- /dev/null +++ b/frontend/src/components/SocialLogin/SocialLogin.module.css @@ -0,0 +1,9 @@ +.socialLoginContainer{ + display: flex; + width: 100%; + justify-content: center; + align-items: center; +} +.socialLoginImage{ + cursor: pointer; +} \ No newline at end of file diff --git a/frontend/src/components/SocialLogin/SocialLogin.tsx b/frontend/src/components/SocialLogin/SocialLogin.tsx new file mode 100644 index 0000000..f279d10 --- /dev/null +++ b/frontend/src/components/SocialLogin/SocialLogin.tsx @@ -0,0 +1,16 @@ +import styles from "./SocialLogin.module.css"; +import kakaoLogin from "../../assets/images/kakao_login.png"; + +const SocialLogin: React.FC = () => { + const handleKakaoLogin = () => { + window.location.href = "http://localhost:8800/social_auth/kakao/login"; + }; + + return ( +
+ kakaoLogin +
+ ); +}; + +export default SocialLogin; \ No newline at end of file diff --git a/frontend/src/components/SocialLogin/index.tsx b/frontend/src/components/SocialLogin/index.tsx new file mode 100644 index 0000000..2de0b8a --- /dev/null +++ b/frontend/src/components/SocialLogin/index.tsx @@ -0,0 +1 @@ +export { default } from "./SocialLogin"; \ No newline at end of file diff --git a/frontend/src/components/StepProgressBar/StepProgressBar.module.css b/frontend/src/components/StepProgressBar/StepProgressBar.module.css new file mode 100644 index 0000000..2dfa5ff --- /dev/null +++ b/frontend/src/components/StepProgressBar/StepProgressBar.module.css @@ -0,0 +1,177 @@ +/* Container */ +.container { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(28, 28, 28, 0.60); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; +} + +/* Box Container */ +.boxContainer { + display: inline-flex; + padding: 2.5rem; + flex-direction: column; + align-items: center; + gap: 2.5rem; + border-radius: 0.75rem; + background: #FFF; + border: none; +} + +/* Content Container */ +.contentContainer { + display: flex; + flex-direction: column; + align-items: center; + gap: 2.5rem; +} + +/* Header Container */ +.headerContainer { + display: flex; + flex-direction: row; + align-items: center; +} + +/* Header Title */ +.headerTitle { + color: #000; + font-size: 1.5rem; + font-style: normal; + font-weight: 700; + line-height: normal; +} + +/* Step Container */ +.stepContainer { + display: flex; + flex-direction: row; + align-items: center; + gap: 0.75rem; +} + +/* Step Box */ +.stepBox { + display: flex; + flex-direction: row; + align-items: center; + gap: 0.5rem; +} + +/* Step Circle */ +.stepCircle { + display: flex; + width: 1.75rem; + height: 1.75rem; + flex-direction: column; + justify-content: center; + align-items: center; + border-radius: 2.5rem; + background: #ADADAD; +} + +.stepCircle.active { + background: #8681FF; + color: #FFF; +} + +/* Step Circle Text */ +.stepCircleText { + display: flex; + justify-content: center; + align-items: center; + color: #FFF; + font-size: 1rem; + font-style: normal; + font-weight: 500; + line-height: normal; +} + +/* Step Text */ +.stepText { + color: #001227; + font-size: 1rem; + font-style: normal; + font-weight: 500; + line-height: normal; + word-break: keep-all; +} + +.stepText.active { + color: #8681FF; +} + +/* Dot Group */ +.dotGroup { + display: flex; + flex-direction: row; + gap: 0.5rem; +} + +/* Dot */ +.dot { + display: flex; + width: 0.25rem; + height: 0.25rem; + flex-direction: column; + justify-content: center; + align-items: center; + border-radius: 2.5rem; + background: #000; +} + +.dot.active { + background: #8681FF; +} + +/* Step Bottom Text */ +.stepBottomText { + color: #C2C2C2; + font-size: 1rem; + font-style: normal; + font-weight: 700; + line-height: normal; +} + +/* Loading Spinner */ +.loadingContainer { + width: 1.75rem; + height: 1.75rem; + display: flex; + justify-content: center; + align-items: center; +} + +.loadingSpinner { + width: 0.75rem; + height: 0.75rem; + aspect-ratio: 1; + border-radius: 50%; + border: 0.125rem solid #fff; + animation: + spinnerClip 1.0s infinite linear alternate, + spinnerRotate 2.0s infinite linear; +} + +@keyframes spinnerClip { + 0% {clip-path: polygon(50% 50%,0 0, 50% 0%, 50% 0%, 50% 0%, 50% 0%, 50% 0% )} + 12.5% {clip-path: polygon(50% 50%,0 0, 50% 0%, 100% 0%, 100% 0%, 100% 0%, 100% 0% )} + 25% {clip-path: polygon(50% 50%,0 0, 50% 0%, 100% 0%, 100% 100%, 100% 100%, 100% 100% )} + 50% {clip-path: polygon(50% 50%,0 0, 50% 0%, 100% 0%, 100% 100%, 50% 100%, 0% 100% )} + 62.5% {clip-path: polygon(50% 50%,100% 0, 100% 0%, 100% 0%, 100% 100%, 50% 100%, 0% 100% )} + 75% {clip-path: polygon(50% 50%,100% 100%, 100% 100%, 100% 100%, 100% 100%, 50% 100%, 0% 100% )} + 100% {clip-path: polygon(50% 50%,50% 100%, 50% 100%, 50% 100%, 50% 100%, 50% 100%, 0% 100% )} +} + +@keyframes spinnerRotate { + 0% {transform:scaleY(1) rotate(0deg)} + 49.99%{transform:scaleY(1) rotate(135deg)} + 50% {transform:scaleY(-1) rotate(0deg)} + 100% {transform:scaleY(-1) rotate(-135deg)} +} \ No newline at end of file diff --git a/frontend/src/components/StepProgressBar/StepProgressBar.tsx b/frontend/src/components/StepProgressBar/StepProgressBar.tsx new file mode 100644 index 0000000..b41dfc1 --- /dev/null +++ b/frontend/src/components/StepProgressBar/StepProgressBar.tsx @@ -0,0 +1,201 @@ +import React, { useState, useEffect } from "react"; +import styles from "./StepProgressBar.module.css"; +import { progressWorkflow } from "@/api/temp/temp"; + +interface StepProgressBarProps { + onComplete: () => void; +} + +const StepProgressBar: React.FC = ({ onComplete }) => { + const [currentIndex, setCurrentIndex] = useState(1); + const [allStep, setAllStep] = useState(false); + const [firstStep, setFirstStep] = useState(false); + const [secondStep, setSecondStep] = useState(false); + const [thirdStep, setThirdStep] = useState(false); + const [fourthStep, setFourthStep] = useState(false); + const [fifthStep, setFifthStep] = useState(false); + + const stepData = { + firstStep: ["URL 분석 중", "URL 분석 완료"], + secondStep: ["가사 생성 중", "가사 생성 완료"], + thirdStep: ["노래 생성 중", "노래 생성 완료"], + fourthStep: ["이미지 생성 중", "이미지 생성 완료"], + fifthStep: ["영상 생성 중", "영상 생성 완료"], + }; + + // 모든 스텝이 완료되면 onComplete 호출 + useEffect(() => { + if (allStep) { + onComplete(); + } + }, [allStep, onComplete]); + + // 모든 스텝 상태를 확인하여 allStep 설정 + useEffect(() => { + if (firstStep && secondStep && thirdStep && fourthStep && fifthStep) { + setAllStep(true); + } + }, [firstStep, secondStep, thirdStep, fourthStep, fifthStep]); + + const Spinner = () => { + return ( +
+
+
+ ); + }; + + + const ProgressWorkflow = async () => { + try{ + const taskId = localStorage.getItem("task_id"); + if (!taskId) return; + const response = await progressWorkflow({ + task_id: taskId, + }); + if (localStorage.getItem("order_id") === null){ + if(response.order_id){ + localStorage.setItem("order_id", response.order_id); + } + } + const success_crawling = response.step_status.crawling; + const success_lyrics = response.step_status.lyrics; + const success_song = response.step_status.music; + const success_image = response.step_status.images; + const success_video = response.step_status.video; + + if (success_crawling) { + setCurrentIndex(2); + setFirstStep(true); + } + if (success_lyrics) { + setCurrentIndex(3); + setSecondStep(true); + } + if (success_song) { + setCurrentIndex(4); + setThirdStep(true); + } + if (success_image) { + setCurrentIndex(5); + setFourthStep(true); + } + if (success_video) { + setFifthStep(true); + } + } catch (error){ + alert("프로세스 생성에 문제가 발생했습니다. 다시 시도해주세요."); + } + }; + + useEffect(() => { + ProgressWorkflow(); // 첫 실행 + + // 2초마다 폴링 (모든 스텝이 완료되지 않았을 때만) + const interval = setInterval(() => { + if (!allStep) { + ProgressWorkflow(); + } + }, 2000); + + // 컴포넌트 언마운트시 인터벌 정리 + return () => clearInterval(interval); + }, [allStep]); + + const getCurrentStepText = () => { + if (allStep) return "영상 생성을 완료했습니다."; + if (currentIndex === 1) return "매장 URL을 통해 정보를 분석 중 입니다"; + if (currentIndex === 2) return "매장 홍보를 위한 가사 생성 중 입니다"; + if (currentIndex === 3) return "가사에 맞춰 노래를 생성 중 입니다"; + if (currentIndex === 4) return "매장의 이미지를 수집하는 중 입니다"; + if (currentIndex === 5) return "모든 정보를 바탕으로 영상을 생성 중 입니다"; + return "준비중"; + }; + + // 스텝 박스 렌더링 함수 + const renderStepBox = (stepNumber: number, stepKey: keyof typeof stepData, isCompleted: boolean, showDots = true) => { + const isActive = currentIndex >= stepNumber; + const isInProgress = isActive && !isCompleted; + const stepCompleted = isActive && isCompleted; + + return ( +
+ {stepCompleted && ( + <> +
+
+
+
+ {stepData[stepKey][1]} +
+ {showDots && ( +
+
+
+
+
+ )} + + )} + {isInProgress && ( + <> +
+ +
+
+ {stepData[stepKey][0]} +
+ {showDots && ( +
+
+
+
+
+ )} + + )} + {!isActive && ( + <> +
+
{stepNumber}
+
+
+ {stepData[stepKey][0]} +
+ {showDots && ( +
+
+
+
+
+ )} + + )} +
+ ); + }; + + return ( +
+
+
+
+
홍보 컨텐츠 제작 단계
+
+
+ {renderStepBox(1, "firstStep", firstStep)} + {renderStepBox(2, "secondStep", secondStep)} + {renderStepBox(3, "thirdStep", thirdStep)} + {renderStepBox(4, "fourthStep", fourthStep)} + {renderStepBox(5, "fifthStep", fifthStep, false)} +
+
+ {getCurrentStepText()} +
+
+
+
+ ); +}; + +export default StepProgressBar; \ No newline at end of file diff --git a/frontend/src/components/StepProgressBar/index.tsx b/frontend/src/components/StepProgressBar/index.tsx new file mode 100644 index 0000000..a64be32 --- /dev/null +++ b/frontend/src/components/StepProgressBar/index.tsx @@ -0,0 +1 @@ +export { default } from "./StepProgressBar"; \ No newline at end of file diff --git a/frontend/src/components/StepTracker/StepTracker.module.css b/frontend/src/components/StepTracker/StepTracker.module.css new file mode 100644 index 0000000..aec4767 --- /dev/null +++ b/frontend/src/components/StepTracker/StepTracker.module.css @@ -0,0 +1,71 @@ +.container{ + display: flex; + align-items: center; + justify-content: center; +} + +.step{ + display: flex; + align-items: center; + justify-content: center; +} + + +.stepNumber{ + display: flex; + width: 1.75rem; + height: 1.75rem; + padding: 0.625rem; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 0.625rem; + border-radius: 2.5rem; + background: #F1F1F1; + margin-right: 0.5rem; +} + +.stepText{ + font-family: Pretendard; + font-size: 0.875rem; + font-style: normal; + font-weight: 500; + line-height: normal; + user-select: none; + word-break: keep-all; +} + +.stepLine{ + display: flex; + width: 1.75rem; + height: 0.0625rem; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 0.625rem; + background: rgba(0, 18, 39, 0.50); + margin: 0 0.75rem; +} + +/* Current Step */ +.currentStepNumber{ + background: #E1BFFF; + color: #8100F3; +} + +.currentStepText{ + color: #8100F3; +} + +/* Active Step */ +.prevStepNumber{ + background: #E1BFFF; + color: #FFF; +} + +.prevStepText{ + color: #BA6BFF; +} +.prevStepLine{ + background: #BA6BFF; +} \ No newline at end of file diff --git a/frontend/src/components/StepTracker/StepTracker.tsx b/frontend/src/components/StepTracker/StepTracker.tsx new file mode 100644 index 0000000..8184750 --- /dev/null +++ b/frontend/src/components/StepTracker/StepTracker.tsx @@ -0,0 +1,62 @@ +import React, { useState } from "react"; +import styles from "./StepTracker.module.css"; + +const StepTracker: React.FC<{step: number}> = ({step}) => { + const steps = [ + { number: 1, text: "URL 입력" }, + { number: 2, text: "영상 확인" }, + ]; + + // 최종 스탭 + // const steps = [ + // { number: 1, text: "URL 입력" }, + // { number: 2, text: "이미지 확인" }, + // { number: 3, text: "장르 확인" }, + // { number: 4, text: "노래 확인" }, + // { number: 5, text: "영상 확인" } + // ]; + + const getStepClasses = (stepNumber: number) => { + if (stepNumber < step) { + // 이전 단계 + return { + stepNumber: `${styles.stepNumber} ${styles.prevStepNumber}`, + stepText: `${styles.stepText} ${styles.prevStepText}`, + stepLine: `${styles.stepLine} ${styles.prevStepLine}` + }; + } else if (stepNumber === step) { + // 현재 단계 + return { + stepNumber: `${styles.stepNumber} ${styles.currentStepNumber}`, + stepText: `${styles.stepText} ${styles.currentStepText}`, + stepLine: `${styles.stepLine}` + }; + } else { + // 미래 단계 + return { + stepNumber: styles.stepNumber, + stepText: styles.stepText, + stepLine: styles.stepLine + }; + } + }; + + return ( +
+ {steps.map((stepItem, index) => { + const classes = getStepClasses(stepItem.number); + const isLastStep = index === steps.length - 1; + + return ( +
+
{stepItem.number}
+
{stepItem.text}
+ {!isLastStep &&
} +
+ ); + })} +
+ ); +}; + +export default StepTracker; \ No newline at end of file diff --git a/frontend/src/components/StepTracker/index.ts b/frontend/src/components/StepTracker/index.ts new file mode 100644 index 0000000..cac58f7 --- /dev/null +++ b/frontend/src/components/StepTracker/index.ts @@ -0,0 +1 @@ +export { default } from "./StepTracker"; \ No newline at end of file diff --git a/frontend/src/components/UploadToggle/UploadToggle.module.css b/frontend/src/components/UploadToggle/UploadToggle.module.css new file mode 100644 index 0000000..39c285a --- /dev/null +++ b/frontend/src/components/UploadToggle/UploadToggle.module.css @@ -0,0 +1,142 @@ +/* Container */ +.container { + position: relative; + display: inline-flex; + padding: 0.625rem; + justify-content: center; + align-items: center; + flex-direction: column; + gap: 0; + border-radius: 0.75rem; + background: #F6F6F6; + transition: all 0.3s ease-in-out; + z-index: 1000; +} + +/* contentContainer */ +.contentContainer { + display: flex; + flex-direction: row; + align-items: center; + cursor: pointer; + width: 100%; + padding: 0; +} + +/* contentText */ +.contentText { + color: #001227; + font-size: 1rem; + font-style: normal; + font-weight: 500; + line-height: normal; + margin-left: 0.62rem; + margin-right: 1rem; +} + +/* Expandable Content */ +.expandableContent { + position: absolute; + top: 100%; + left: 0; + width: 100%; + max-height: 0; + overflow: hidden; + background: #F6F6F6; + border-radius: 0 0 0.75rem 0.75rem; + transition: max-height 0.3s ease-in-out, padding 0.3s ease-in-out; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + z-index: 1001; +} + +.expandableContent.expanded { + max-height: 200px; /* 충분한 높이로 설정 */ +} + +.expandableInner { + display: flex; + flex-direction: column; +} + +/* Divider */ +.divider { + width: 100%; + display: flex; + height: 0.0625rem; + flex-direction: column; + justify-content: center; + align-items: center; + background: #C8C8C8; +} + +/* Platform Container */ +.platformContainer { + display: flex; + flex-direction: row; + align-items: flex-start; + gap: 0.5rem; + width: 100%; + text-align: left; + cursor: pointer; + padding: 0.625rem; +} +.platformContainer:hover { + background: #E0E0E0; +} + +/* Platform Text */ +.platformText { + color: #001227; + font-size: 1rem; + font-style: normal; + font-weight: 500; + line-height: normal; +} + +/* Simple Pop Up */ +.SimplePopUp { + position: fixed; + top: 2rem; + left: 50%; + z-index: 10001; +} + +.SimplePopUpContainer { + display: flex; + padding: 1rem 1.5rem; + flex-direction: column; + align-items: center; + gap: 0.5rem; + border-radius: 0.5rem; + background: #10B981; + box-shadow: 0 0.25rem 0.375rem rgba(0, 0, 0, 0.1); + animation: slideDown 0.4s ease-out; + transform: translateX(-50%); +} + +.SimplePopUpText { + color: #FFF; + font-size: 1.25rem; + font-style: normal; + font-weight: 500; + line-height: normal; +} +.SimplePopUpSubText { + color: #FFF; + font-size: 0.875rem; + font-style: normal; + font-weight: 400; + line-height: normal; +} + +/* Slide Down Animation */ +@keyframes slideDown { + 0% { + opacity: 0; + transform: translateX(-50%) translateY(-0.3rem) scale(0.98); + } + 100% { + opacity: 1; + transform: translateX(-50%) translateY(0) scale(1); + } +} \ No newline at end of file diff --git a/frontend/src/components/UploadToggle/UploadToggle.tsx b/frontend/src/components/UploadToggle/UploadToggle.tsx new file mode 100644 index 0000000..d94e87f --- /dev/null +++ b/frontend/src/components/UploadToggle/UploadToggle.tsx @@ -0,0 +1,89 @@ +import React, { useState } from "react"; +import styles from "./UploadToggle.module.css"; +import youtube from "@/assets/images/youtube.png"; +import naver from "@/assets/images/naver.png"; + +const uploadIcon = ( + + + +) + +const toggleDownIcon = ( + + + +) + +const toggleUpIcon = ( + + + +) + +const UploadToggle: React.FC = () => { + const [isToggle, setIsToggle] = useState(false); + const [isUploading, setIsUploading] = useState(false); + const [uploadSuccess, setUploadSuccess] = useState(false); + const [uploadFailMessage, setUploadFailMessage] = useState(""); + + const renderUploadPopUp = (uploadSuccess: boolean) => { + if (!isUploading) return null; + + return ( +
+
+
{uploadSuccess ? "업로드가 완료되었습니다!" : "업로드에 실패했습니다."}
+ {!uploadSuccess &&
{uploadFailMessage}
} +
+
+ ) + } + + const handleYoutubeUpload = () => { + setIsUploading(true); + setUploadSuccess(true); + setTimeout(() => { + setIsUploading(false); + }, 2000); + }; + + const handleNaverUpload = () => { + setIsUploading(true); + setUploadSuccess(false); + setUploadFailMessage("네이버 클립 업로드 중 오류가 발생했습니다."); + setTimeout(() => { + setIsUploading(false); + }, 2000); + }; + + return ( + <> + {renderUploadPopUp(uploadSuccess)} +
+
setIsToggle(!isToggle)}> + {uploadIcon} +
자동 업로드
+ {isToggle ? toggleUpIcon : toggleDownIcon} +
+ +
+
+
+
+ youtube +
Youtube
+
+
+
+ naver +
네이버 클립
+
+
+
+
+ + ) +} + +export default UploadToggle; \ No newline at end of file diff --git a/frontend/src/components/UploadToggle/index.tsx b/frontend/src/components/UploadToggle/index.tsx new file mode 100644 index 0000000..0aea45d --- /dev/null +++ b/frontend/src/components/UploadToggle/index.tsx @@ -0,0 +1 @@ +export { default } from "./UploadToggle"; \ No newline at end of file diff --git a/frontend/src/components/VideoUploadPopUp/VideoUploadPopUp.module.css b/frontend/src/components/VideoUploadPopUp/VideoUploadPopUp.module.css new file mode 100644 index 0000000..83789cd --- /dev/null +++ b/frontend/src/components/VideoUploadPopUp/VideoUploadPopUp.module.css @@ -0,0 +1,279 @@ +.container { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(28, 28, 28, 0.60); + display: flex; + justify-content: center; + align-items: center; + z-index: 5; +} + +/* Content */ +.content { + width: 36.525rem; + display: inline-flex; + padding: 2.5rem; + flex-direction: column; + justify-content: center; + gap: 1.5rem; + border-radius: 1rem; + background: #FFF; + z-index: 1000; +} + +/* Title */ +.title { + color: #001227; + font-size: 1.5rem; + font-style: normal; + font-weight: 700; + line-height: normal; +} + +/* Description */ +.description { + display: flex; + flex-direction: column; + gap: 0.5rem; +} +.descriptionTitle{ + color: #001227; + font-size: 1.25rem; + font-style: normal; + font-weight: 700; + line-height: normal; +} +.descriptionContent{ + color: rgba(0, 18, 39, 0.50); + font-size: 0.9375rem; + font-style: normal; + font-weight: 500; + line-height: normal; +} + +/* Video Wrapper */ +.videoWrapper { + display: flex; + width: 100%; + height: 14.0625rem; + background: #000; + border-radius: 0.5rem; +} + +.video { + width: 100%; + height: 100%; + aspect-ratio: 16/9; + object-fit: cover; + border-radius: 0.75rem; +} + +/* Channel Wrapper */ +.channelWrapper{ + display: flex; + flex-direction: column; + padding: 0.5625rem; + border-radius: 0.5625rem; + border: 0.75px solid rgba(0, 18, 39, 0.50); + background: #FFF; + gap: 0.19rem; +} +.channelHeader{ + color: #001227; + font-size: 0.6875rem; + font-style: normal; + font-weight: 400; + line-height: normal; +} +.channelContent{ + display: flex; + align-items: center; + gap: 0.5rem; +} +.channelName{ + color: #001227; + font-size: 1rem; + font-style: normal; + font-weight: 500; + line-height: normal; +} + +/* Input Container */ +.inputContainer { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +/* Input Wrapper */ +.inputWrapper { + display: flex; + flex-direction: column; + padding: 0.5625rem; + border-radius: 0.5625rem; + border: 0.75px solid rgba(0, 18, 39, 0.50); + background: #FFF; + gap: 0.19rem; +} +.icon{ + display: flex; + align-self: center; + justify-self: center; + width: 0.875rem; + height: 0.875rem; +} +.inputTitle { + color: #001227; + font-size: 0.6875rem; + font-style: normal; + font-weight: 400; + line-height: normal; +} +.inputContent{ + display: flex; + align-items: center; + gap: 0.19rem; +} +.input{ + width: 100%; + border: none; + outline: none; + color: #001227; + font-size: 1rem; + font-style: normal; + font-weight: 500; + line-height: normal; +} +.input:focus{ + border: none; + outline: none; +} + +/* Textarea Content */ +.textareaWrapper { + display: flex; + flex-direction: column; + padding: 0.5625rem; + border-radius: 0.5625rem; + border: 0.75px solid rgba(0, 18, 39, 0.50); + background: #FFF; + gap: 0.19rem; + height: auto; +} +.textareaTitle { + color: #001227; + font-size: 0.8rem; + font-style: normal; + font-weight: 400; + line-height: normal; +} +.textareaContent{ + display: flex; + flex-direction: row; + justify-content: flex-start; + gap: 0.19rem; +} +.descriptionIcon{ + display: flex; + width: 0.75rem; + height: 0.9375rem; + margin-top: 0.5rem; +} +.textarea { + width: 100%; + border: none; + resize: none; + outline: none; + + color: #001227; + font-family: Pretendard; + font-size: 1.125rem; + font-style: normal; + font-weight: 500; + line-height: 1.5; + + overflow-y: auto; + + /* 스크롤바 스타일 */ + scrollbar-width: thin; /* Firefox */ + scrollbar-color: #bbb transparent; /* Firefox */ + +} +.textarea::-webkit-scrollbar { + width: 0.375rem; +} + +.textarea::-webkit-scrollbar-thumb { + background-color: #bbb; + border-radius: 3px; +} + +.textarea::-webkit-scrollbar-track { + background: transparent; +} + +/* Bottom Text Container */ +.bottomTextContainer{ + display: flex; + align-items: center; + gap: 0.5rem; + background-color: #E5EFFF; + border-radius: 0.5625rem; + padding: 0.5625rem; + width: 100%; + height: 2.5rem; +} +.bottomText{ + color: #001227; + font-size: 1rem; + font-style: normal; + font-weight: 500; + line-height: normal; +} + +/* Button Container */ +.buttonContainer{ + display: flex; + justify-content: flex-end; + gap: 1rem; +} + +/* Button */ +.button{ + display: flex; + padding: 0.5625rem 1.875rem; + justify-content: center; + align-items: center; + gap: 0.5625rem; + border-radius: 0.5625rem; + background: #B8B5FF; + border: none; + color: #FFF; + font-size: 0.9375rem; + font-style: normal; + font-weight: 500; + line-height: normal; + cursor: pointer; +} + +/* Hashtags Container */ +.hashtagsContainer{ + display: flex; + flex-direction: row; + gap: 0.5rem; +} +.hashtagText{ + display: flex; + height: 2.3rem; + padding: 0.5rem 0.75rem; + justify-content: center; + align-items: center; + border-radius: 0.75rem; + background: #E0DFFF; + color: #001227; + font-size: 1.125rem; + white-space: nowrap; +} \ No newline at end of file diff --git a/frontend/src/components/VideoUploadPopUp/VideoUploadPopUp.tsx b/frontend/src/components/VideoUploadPopUp/VideoUploadPopUp.tsx new file mode 100644 index 0000000..95860a2 --- /dev/null +++ b/frontend/src/components/VideoUploadPopUp/VideoUploadPopUp.tsx @@ -0,0 +1,301 @@ +import { useState, useRef, useEffect } from "react"; +import styles from "./VideoUploadPopUp.module.css"; +import penIcon from "@/assets/icons/pen_underline.svg"; +import descriptionIcon from "@/assets/icons/description.svg"; +import tagIcon from "@/assets/icons/tag.svg"; +import blueErrorIcon from "@/assets/icons/blue_error.svg"; +import youtubeIcon from "@/assets/images/youtube.png"; +import { getYoutubeChannelInfo, uploadYoutubeVideo } from "@/api/social_login/google"; +import SimpleLoading from "@/components/SimpleLoading"; + +interface ChannelData { + platform: string; + channelName: string; + channelId: string; + subscriberCount: number; + thumbnailUrl: string; +} + +interface VideoUploadPopUpProps { + videoTitle: string; + videoPlaytime: string; + videoResolution: string; + videoExtension: string; + videoUrl: string; + setIsUploadPopUpOpen: (isUploadPopUpOpen: boolean) => void; +} + +const VideoUploadPopUp: React.FC = ({ + videoTitle, + videoPlaytime, + videoResolution, + videoExtension, + videoUrl, + setIsUploadPopUpOpen +}) => { + const [channelData, setChannelData] = useState(null); + const [uploading, setUploading] = useState(false); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [title, setTitle] = useState(videoTitle); + const [description, setDescription] = useState(""); + const [hashtags, setHashtags] = useState(""); + const [hashtagsList, setHashtagsList] = useState([]); + + const textareaRef = useRef(null); + + const closePopup = () => setIsUploadPopUpOpen(false); + + const handleUpload = async () => { + if (!channelData) { + alert('채널 정보가 없습니다.'); + return; + } + + if (!title.trim()) { + alert('영상 제목을 입력해주세요.'); + return; + } + + try { + setUploading(true); + console.log(channelData.channelId); + const uploadData = { + channel_id: channelData.channelId, + title: title.trim(), + description: description.trim(), + hashtags: hashtagsList, + video_url: videoUrl, + privacy_status: 'private' as const, + category_id: '22', + default_language: 'ko' + }; + + console.log('업로드 시작:', uploadData); + + const result = await uploadYoutubeVideo(uploadData); + + console.log('업로드 성공:', result); + alert(`업로드 완료!\n동영상 ID: ${result.video_id}\nYouTube 링크: ${result.links.youtube_url}`); + + closePopup(); + + } catch (error: any) { + console.error('업로드 실패:', error); + + if (error.response?.status === 401) { + alert('인증이 만료되었습니다. 다시 로그인해주세요.'); + } else if (error.response?.status === 403) { + alert('YouTube 업로드 권한이 없거나 할당량이 초과되었습니다.'); + } else { + alert(`업로드 실패: ${error.response?.data?.detail || error.message}`); + } + } finally { + setUploading(false); + } + }; + + const handleHashTagInput = (e: React.ChangeEvent) => { + const value = e.target.value; + + if (value.endsWith(" ")) { + const trimmedValue = value.trim(); + + if (trimmedValue.startsWith("#") && trimmedValue.length > 1) { + const newHashtag = trimmedValue.replace("#", ""); + + if (!hashtagsList.includes(newHashtag)) { + setHashtagsList([...hashtagsList, newHashtag]); + } + setHashtags(""); + } else { + setHashtags(""); + } + } else { + setHashtags(value); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Backspace' && hashtags === '' && hashtagsList.length > 0) { + e.preventDefault(); + setHashtagsList(hashtagsList.slice(0, -1)); + } + }; + + const adjustTextareaHeight = () => { + const textarea = textareaRef.current; + if (!textarea) return; + + textarea.style.height = "30px"; + textarea.style.overflowY = "hidden"; + + const scrollHeight = textarea.scrollHeight; + + if (scrollHeight <= 80) { + textarea.style.height = `${scrollHeight}px`; + textarea.style.overflowY = "hidden"; + } else { + textarea.style.height = "80px"; + textarea.style.overflowY = "auto"; + } + }; + + const fetchChannelInfo = async () => { + try { + setLoading(true); + setError(null); + + const channelInfo = await getYoutubeChannelInfo(); + + if (channelInfo.data && channelInfo.data.length > 0) { + const channel = channelInfo.data[0]; + + const tempChannel: ChannelData = { + platform: "Youtube", + channelName: channel.title || "Youtube Channel", + channelId: channel.channel_id, + subscriberCount: channel.subscriber_count, + thumbnailUrl: channel.thumbnail_url + }; + + setChannelData(tempChannel); + console.log('채널 정보 저장됨:', tempChannel); + + localStorage.setItem('youtubeChannelData', JSON.stringify(tempChannel)); + } else { + setError('채널 정보가 없습니다.'); + } + + } catch (error: any) { + console.error('API 호출 실패:', error); + setError('채널 정보를 불러오는데 실패했습니다.'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + adjustTextareaHeight(); + }, [description]); + + useEffect(() => { + fetchChannelInfo(); + }, []); + + const renderChannelInfo = () => { + if (channelData?.platform === "Youtube") { + return ( +
+ youtubeIcon +
{channelData.channelName}
+
+ ); + } + return
{channelData?.channelName}
; + }; + + const renderHashtags = () => { + return hashtagsList.map((tag, index) => ( +
{tag}
+ )); + }; + + if (loading) { + return ( +
+ +
+ ); + } + + return ( +
+
e.stopPropagation()}> +
영상 업로드
+ +
+
{videoTitle}
+
+ {videoPlaytime} · {videoResolution} · {videoExtension} +
+
+ +
+
+ +
+
+
연결 플랫폼
+ {renderChannelInfo()} +
+ +
+
영상 제목
+
+ penIcon + setTitle(e.target.value)} + /> +
+
+ +
+
영상 설명
+
+ descriptionIcon +