diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index eeeba4b..1604dcc 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -13,7 +13,8 @@ jobs: runs-on: ubuntu-latest if: ${{ github.event.workflow_run.conclusion == 'success' }} env: - IMAGE_NAME: mov-api + SPRING_IMAGE_NAME: mov-api + FASTAPI_IMAGE_NAME: mov-fastapi steps: - name: Deploy to EC2 uses: appleboy/ssh-action@master @@ -24,27 +25,41 @@ jobs: script: | # 環境 변수 설정 DOCKER_USERNAME=${{ secrets.DOCKERHUB_USERNAME }} - IMAGE_NAME=${{ env.IMAGE_NAME }} - CONTAINER_NAME=mov-api + SPRING_IMAGE_NAME=${{ env.SPRING_IMAGE_NAME }} + FASTAPI_IMAGE_NAME=${{ env.FASTAPI_IMAGE_NAME }} + SPRING_CONTAINER_NAME=mov-api + FASTAPI_CONTAINER_NAME=mov-fastapi ENV_FILE_PATH=/home/${{ secrets.EC2_USERNAME }}/.env - echo "CI/CD: [1/5] Creating .env file..." + echo "CI/CD: [1/8] Creating .env file..." echo "${{ secrets.ENV_FILE }}" > $ENV_FILE_PATH - echo "CI/CD: [2/5] Pulling latest Docker image..." - docker pull $DOCKER_USERNAME/$IMAGE_NAME:latest + echo "CI/CD: [2/8] Pulling latest Spring Boot Docker image..." + docker pull $DOCKER_USERNAME/$SPRING_IMAGE_NAME:latest - echo "CI/CD: [3/5] Stopping and removing existing container..." - docker stop $CONTAINER_NAME || true - docker rm $CONTAINER_NAME || true + echo "CI/CD: [3/8] Stopping and removing existing Spring Boot container..." + docker stop $SPRING_CONTAINER_NAME || true + docker rm $SPRING_CONTAINER_NAME || true - echo "CI/CD: [4/5] Starting new container..." - docker run -d --name $CONTAINER_NAME \ + echo "CI/CD: [4/8] Starting new Spring Boot container..." + docker run -d --name $SPRING_CONTAINER_NAME \ --env-file $ENV_FILE_PATH \ -p 8080:8080 \ - $DOCKER_USERNAME/$IMAGE_NAME:latest + $DOCKER_USERNAME/$SPRING_IMAGE_NAME:latest - echo "CI/CD: [5/5] Cleaning up unused images..." + echo "CI/CD: [5/8] Pulling latest FastAPI Docker image..." + docker pull $DOCKER_USERNAME/$FASTAPI_IMAGE_NAME:latest + + echo "CI/CD: [6/8] Stopping and removing existing FastAPI container..." + docker stop $FASTAPI_CONTAINER_NAME || true + docker rm $FASTAPI_CONTAINER_NAME || true + + echo "CI/CD: [7/8] Starting new FastAPI container..." + docker run -d --name $FASTAPI_CONTAINER_NAME \ + -p 8000:8000 \ + $DOCKER_USERNAME/$FASTAPI_IMAGE_NAME:latest + + echo "CI/CD: [8/8] Cleaning up unused images..." docker image prune -f echo "CI/CD: Deployment complete." diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2de3a15..baf322c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,10 +7,11 @@ on: branches: [ "main" ] workflow_dispatch: # Actions 탭에서 수동 실행 버튼 제공(필요할 때 수동으로 돌릴 수 있게) env: - IMAGE_NAME: mov-api + SPRING_IMAGE_NAME: mov-api + FASTAPI_IMAGE_NAME: mov-fastapi jobs: - build-and-push: + build-spring-boot: runs-on: ubuntu-latest steps: - name: Checkout @@ -22,10 +23,30 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Build and conditionally push Docker image + - name: Build and conditionally push Spring Boot Docker image uses: docker/build-push-action@v4 with: context: . # main 브랜치에 push될 때만 Docker Hub에 이미지를 푸시합니다. push: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} - tags: ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}:latest \ No newline at end of file + tags: ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.SPRING_IMAGE_NAME }}:latest + + build-fastapi: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and conditionally push FastAPI Docker image + uses: docker/build-push-action@v4 + with: + context: ./ai_recommendation + # main 브랜치에 push될 때만 Docker Hub에 이미지를 푸시합니다. + push: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} + tags: ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.FASTAPI_IMAGE_NAME }}:latest \ No newline at end of file diff --git a/ai_recommendation/.dockerignore b/ai_recommendation/.dockerignore new file mode 100644 index 0000000..234de48 --- /dev/null +++ b/ai_recommendation/.dockerignore @@ -0,0 +1,34 @@ +# Python +venv/ +__pycache__/ +*.pyc +*.pyo +*.pyd +.Python +*.so +*.egg +*.egg-info/ +dist/ +build/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Git +.git/ +.gitignore + +# Testing +.pytest_cache/ +.coverage +htmlcov/ + +# Logs +*.log diff --git a/ai_recommendation/.gitignore b/ai_recommendation/.gitignore new file mode 100644 index 0000000..03141b9 --- /dev/null +++ b/ai_recommendation/.gitignore @@ -0,0 +1,55 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +*.egg +*.egg-info/ +dist/ +build/ +*.whl + +# Virtual Environment +venv/ +env/ +ENV/ +.venv + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store + +# Jupyter Notebook +.ipynb_checkpoints + +# Testing +.pytest_cache/ +.coverage +htmlcov/ +*.cover +.hypothesis/ + +# Logs +*.log +logs/ + +# Environment variables +.env +.env.local + +# Model files (선택적 - 학습된 모델을 Git에서 제외하려면 주석 해제) +# models/*.cbm +# models/*.pkl + +# Data files (선택적 - 대용량 데이터를 제외하려면 주석 해제) +# data/*.csv +# data/*.json + +# Temporary files +*.tmp +*.bak +*.cache diff --git a/ai_recommendation/Dockerfile b/ai_recommendation/Dockerfile new file mode 100644 index 0000000..7f8b24a --- /dev/null +++ b/ai_recommendation/Dockerfile @@ -0,0 +1,22 @@ +# FastAPI 추천 시스템 Docker 이미지 +FROM python:3.9-slim + +WORKDIR /app + +# 시스템 패키지 업데이트 및 필수 도구 설치 +RUN apt-get update && apt-get install -y \ + gcc \ + && rm -rf /var/lib/apt/lists/* + +# Python 의존성 설치 +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# 애플리케이션 코드 복사 +COPY . . + +# 포트 노출 +EXPOSE 8000 + +# FastAPI 서버 실행 +CMD ["uvicorn", "api.server:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/ai_recommendation/README.md b/ai_recommendation/README.md new file mode 100644 index 0000000..5b4bb84 --- /dev/null +++ b/ai_recommendation/README.md @@ -0,0 +1,249 @@ +# MOV AI 추천 시스템 + +CatBoost와 규칙 기반 필터링을 사용한 AI 기반 운동 패키지 추천 시스템 + +## 시스템 아키텍처 + +``` +프론트엔드 (React) + ↓ +Spring Boot (8080 포트) + ↓ (프록시) +FastAPI (8000 포트) + ↓ (ML 추천 엔진) +MySQL RDS (패키지 데이터) +``` + +## 프로젝트 구조 + +``` +ai_recommendation/ +├── config/ # 설정 파일 (settings.py) +├── data/ # 학습 데이터 (training_data.csv) +├── models/ # ML 모델 (trainer, predictor, catboost_model.cbm) +├── services/ # 비즈니스 로직 (추천, 필터링) +├── api/ # FastAPI 서버 및 스키마 +├── Dockerfile # FastAPI Docker 이미지 +├── requirements.txt # Python 의존성 +└── .dockerignore # Docker 빌드 제외 파일 +``` + +## 로컬 개발 환경 설정 + +### 1. 가상환경 생성 및 활성화 + +```bash +cd ai_recommendation +python3 -m venv venv +source venv/bin/activate # Mac/Linux +# venv\Scripts\activate # Windows +``` + +### 2. 의존성 설치 + +```bash +pip install -r requirements.txt +``` + +### 3. 모델 학습 (선택) + +이미 학습된 모델(`models/catboost_model.cbm`)이 있으면 생략 가능 + +```bash +python -m models.trainer +``` + +### 4. FastAPI 서버 시작 + +```bash +uvicorn api.server:app --host 0.0.0.0 --port 8000 --reload +``` + +### 5. Spring Boot 서버 연동 + +FastAPI는 Spring Boot의 패키지 메타데이터를 가져오므로, Spring Boot 서버가 먼저 실행되어야 합니다: + +```bash +# 별도 터미널에서 Spring Boot 실행 +cd .. # 프로젝트 루트로 이동 +./gradlew bootRun +``` + +## Docker 배포 + +### 로컬에서 Docker 빌드 테스트 + +```bash +# FastAPI 이미지 빌드 +cd ai_recommendation +docker build -t mov-fastapi . + +# 컨테이너 실행 +docker run -d -p 8000:8000 mov-fastapi +``` + +### 자동 배포 (CI/CD) + +`main` 브랜치에 push하면 GitHub Actions가 자동으로: +1. Spring Boot와 FastAPI Docker 이미지 빌드 +2. Docker Hub에 푸시 +3. EC2 서버에 배포 + +## API 엔드포인트 + +### 1. 프론트엔드 → Spring Boot (추천) + +**엔드포인트**: `POST /api/ai/recommendations` +**설명**: 프론트엔드가 호출하는 메인 추천 API (Spring Boot가 FastAPI로 프록시) + +**요청 예시**: +```json +{ + "purpose": "다이어트", + "preferred_time": "저녁(18시~23시)", + "preferred_intensity": "땀이 흠뻑 젖도록 중강도", + "travel_time": "30분 이내", + "environment": "실내", + "preferred_sports": ["웨이트 & 크로스핏", "실내 수영"], + "recovery_level": "평범함", + "budget_range": "이만원대", + "avoid_factors": ["가벼운 웜업 위주"] +} +``` + +**응답 예시**: +```json +{ + "recommendations": [ + { + "pass_id": 2, + "name": "체중감량 시작 (웨이트1회+수영1회)", + "price": 29000, + "intensity": "HIGH", + "purposeTag": "DIET", + "predicted_score": 0.85 + } + ], + "total_count": 10 +} +``` + +### 2. FastAPI 직접 호출 (테스트용) + +**엔드포인트**: `POST http://localhost:8000/api/recommendations` +**설명**: FastAPI 서버에 직접 추천 요청 (개발/테스트용) + +### 3. 헬스 체크 + +```bash +GET http://localhost:8000/health +``` + +**응답**: +```json +{ + "status": "healthy", + "model_loaded": true, + "version": "1.0.0" +} +``` + +### 4. 모델 재학습 + +```bash +POST http://localhost:8000/api/train +``` + +**주의**: 프로덕션에서는 인증 추가 권장 + +## Spring Boot 통합 + +FastAPI는 다음 Spring Boot 엔드포인트를 호출합니다: + +``` +GET http://localhost:8080/api/passes/metadata +``` + +이 엔드포인트는 모든 패키지 메타데이터(pass_id, name, price, intensity, purposeTag)를 JSON 배열로 반환합니다. + +## 환경 설정 + +`config/settings.py` 또는 `.env` 파일에서 설정 가능: + +```env +SPRING_BOOT_URL=http://localhost:8080 # Spring Boot 서버 주소 +API_HOST=0.0.0.0 +API_PORT=8000 +TOP_N_RECOMMENDATIONS=10 # 반환할 추천 개수 +MIN_SCORE_THRESHOLD=0.3 # 최소 점수 임계값 +``` + +## 주요 특징 + +### 1. 하이브리드 추천 시스템 +- **ML 기반 점수**: CatBoost를 사용한 협업 필터링 +- **규칙 기반 필터**: 사용자 선호도(강도, 목적, 환경 등) 반영 + +### 2. 9개 설문 항목 지원 +- 운동 목적, 선호 시간, 선호 강도, 이동 시간 +- 운동 환경, 관심 운동 종목, 회복 정도 +- 예산 범위, 피하고 싶은 요소 + +### 3. 실시간 통합 +- Spring Boot와 FastAPI 간 실시간 데이터 교환 +- 패키지 메타데이터 자동 동기화 + +### 4. 자동 배포 +- Docker 컨테이너화 +- GitHub Actions CI/CD 파이프라인 + +## 추천 알고리즘 흐름 + +``` +1. 프론트엔드에서 9개 설문 응답 제출 + ↓ +2. Spring Boot가 FastAPI로 프록시 + ↓ +3. FastAPI가 Spring Boot에서 패키지 메타데이터 조회 + ↓ +4. CatBoost 모델로 각 패키지에 점수 부여 + ↓ +5. 규칙 기반 필터 적용 (목적, 강도, 환경 등) + ↓ +6. 상위 10개 패키지 반환 + ↓ +7. Spring Boot → 프론트엔드로 응답 +``` + +## 학습 데이터 구조 + +**파일**: `data/training_data.csv` + +**컬럼**: +- `purpose`: 운동 목적 (다이어트, 근육 증가, 체력 향상, 취미 탐색) +- `preferredIntensity`: 선호 강도 +- `interestedSportIds`: 관심 운동 종목 (쉼표로 구분된 ID) +- `price`: 패키지 가격 +- `preferredEnvironment`: 운동 환경 (실내/실외/상관없음) +- `avoidFactors`: 피하고 싶은 요소 +- `recoveryCondition`: 회복 정도 +- `purchased_pass_id`: 구매한 패키지 ID (타겟 변수) + +## 트러블슈팅 + +### FastAPI 서버가 Spring Boot에 연결 못함 +- Spring Boot 서버가 실행 중인지 확인 +- `http://localhost:8080/api/passes/metadata` 엔드포인트 확인 + +### 모델 파일이 없다는 오류 +```bash +python -m models.trainer # 모델 재학습 +``` + +### Docker 이미지 빌드 실패 +- `requirements.txt` 파일 확인 +- `.dockerignore`에 `venv/` 포함되었는지 확인 + +## 라이선스 + +이 프로젝트는 해커톤용으로 제작되었습니다. diff --git a/ai_recommendation/api/__init__.py b/ai_recommendation/api/__init__.py new file mode 100644 index 0000000..13e9eb7 --- /dev/null +++ b/ai_recommendation/api/__init__.py @@ -0,0 +1 @@ +"""API module for FastAPI server.""" diff --git a/ai_recommendation/api/schemas.py b/ai_recommendation/api/schemas.py new file mode 100644 index 0000000..f6b6e4b --- /dev/null +++ b/ai_recommendation/api/schemas.py @@ -0,0 +1,80 @@ +""" +API 요청/응답 검증을 위한 Pydantic 스키마 +""" +from pydantic import BaseModel, Field +from typing import List, Optional + + +class UserSurveyRequest(BaseModel): + """사용자 설문 요청 스키마""" + + purpose: str = Field(..., description="운동 목적") + preferred_time: str = Field(..., description="선호 운동 시간") + preferred_intensity: str = Field(..., description="선호 운동 강도") + travel_time: str = Field(..., description="이동 가능 시간") + environment: str = Field(..., description="운동 환경 (실내/실외)") + preferred_sports: List[str] = Field(default=[], description="관심 운동 종목") + recovery_level: str = Field(..., description="회복 정도") + budget_range: str = Field(..., description="1회 기준 패키지 예산") + avoid_factors: List[str] = Field(default=[], description="피하고 싶은 요소") + + class Config: + json_schema_extra = { + "example": { + "purpose": "다이어트", + "preferred_time": "저녁(18시~23시)", + "preferred_intensity": "땀이 흠뻑 젖도록 중강도", + "travel_time": "30분 이내", + "environment": "실내", + "preferred_sports": ["웨이트 & 크로스핏", "실내 수영"], + "recovery_level": "평범함", + "budget_range": "이만원대", + "avoid_factors": ["가벼운 웜업 위주"] + } + } + + +class PassRecommendation(BaseModel): + """단일 패키지 추천""" + + pass_id: int + name: str + price: int + intensity: str + purpose_tag: str = Field(..., alias="purposeTag") + predicted_score: float + + class Config: + populate_by_name = True + + +class RecommendationResponse(BaseModel): + """추천 API 응답""" + + recommendations: List[PassRecommendation] + total_count: int + + class Config: + json_schema_extra = { + "example": { + "recommendations": [ + { + "pass_id": 2, + "name": "체중감량 시작 (웨이트1회+수영1회)", + "price": 29000, + "intensity": "HIGH", + "purposeTag": "DIET", + "predicted_score": 0.85 + } + ], + "total_count": 1 + } + } + + +class HealthCheckResponse(BaseModel): + """헬스 체크 응답""" + + status: str + model_loaded: bool + version: str diff --git a/ai_recommendation/api/server.py b/ai_recommendation/api/server.py new file mode 100644 index 0000000..5b86ba0 --- /dev/null +++ b/ai_recommendation/api/server.py @@ -0,0 +1,175 @@ +""" +추천 API를 위한 FastAPI 서버 +""" +from fastapi import FastAPI, HTTPException +from fastapi.middleware.cors import CORSMiddleware +import pandas as pd +import httpx +from typing import List +import logging + +from api.schemas import ( + UserSurveyRequest, + RecommendationResponse, + PassRecommendation, + HealthCheckResponse +) +from services.recommendation_service import RecommendationService +from config.settings import settings + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# FastAPI 앱 초기화 +app = FastAPI( + title=settings.API_TITLE, + version=settings.API_VERSION, + description="AI 기반 운동 패키지 추천 시스템" +) + +# CORS 미들웨어 +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # 프로덕션에서는 적절히 설정 필요 + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# 추천 서비스 초기화 +rec_service = None + + +@app.on_event("startup") +async def startup_event(): + """서비스 시작 시 초기화""" + global rec_service + try: + logger.info("추천 서비스 초기화 중...") + rec_service = RecommendationService() + logger.info("추천 서비스 준비 완료") + except Exception as e: + logger.error(f"추천 서비스 초기화 실패: {e}") + raise + + +@app.get("/health", response_model=HealthCheckResponse) +async def health_check(): + """헬스 체크 엔드포인트""" + return HealthCheckResponse( + status="healthy", + model_loaded=rec_service is not None, + version=settings.API_VERSION + ) + + +@app.post("/api/recommendations", response_model=RecommendationResponse) +async def get_recommendations(survey: UserSurveyRequest): + """ + 사용자 설문을 기반으로 맞춤형 패키지 추천을 제공합니다. + + Args: + survey: 사용자 설문 응답 + + Returns: + 추천 패키지 리스트 + """ + if rec_service is None: + raise HTTPException( + status_code=503, + detail="Recommendation service not initialized" + ) + + try: + # Fetch pass metadata from Spring Boot backend + async with httpx.AsyncClient() as client: + response = await client.get( + f"{settings.SPRING_BOOT_URL}/api/passes/metadata" + ) + response.raise_for_status() + pass_data = response.json() + + # DataFrame으로 변환 + pass_metadata = pd.DataFrame(pass_data) + + # 일관성을 위해 passId를 pass_id로 변경 + if 'passId' in pass_metadata.columns: + pass_metadata = pass_metadata.rename(columns={'passId': 'pass_id'}) + + # API 필드명을 학습 데이터 특성명으로 매핑 (학습된 특성만 포함) + user_survey_dict = { + 'purpose': survey.purpose, + 'preferredIntensity': survey.preferred_intensity, + 'interestedSportIds': ','.join(survey.preferred_sports) if survey.preferred_sports else '', + 'preferredEnvironment': survey.environment, + 'avoidFactors': ','.join(survey.avoid_factors) if survey.avoid_factors else '', + 'recoveryCondition': survey.recovery_level if survey.recovery_level else 'missing', + # 아래 필드들은 규칙 기반 필터링용으로 보관 (ML 학습에는 미사용) + 'budgetRange': survey.budget_range, + 'preferredTime': survey.preferred_time, + 'travelTime': survey.travel_time + } + + recommendations = rec_service.get_recommendations( + user_survey=user_survey_dict, + pass_metadata=pass_metadata, + top_n=settings.TOP_N_RECOMMENDATIONS + ) + + # 응답 모델로 변환 + pass_recommendations = [ + PassRecommendation(**rec) for rec in recommendations + ] + + return RecommendationResponse( + recommendations=pass_recommendations, + total_count=len(pass_recommendations) + ) + + except httpx.HTTPError as e: + logger.error(f"Spring Boot에서 데이터 가져오기 실패: {e}") + raise HTTPException( + status_code=502, + detail="백엔드에서 패키지 메타데이터 가져오기 실패" + ) + except Exception as e: + logger.error(f"추천 생성 중 오류: {e}") + raise HTTPException( + status_code=500, + detail=f"내부 서버 오류: {str(e)}" + ) + + +@app.post("/api/train") +async def trigger_training(): + """ + 모델 재학습을 트리거합니다. + (프로덕션에서는 인증으로 보호해야 함) + """ + try: + from models.trainer import ModelTrainer + + trainer = ModelTrainer() + trainer.train() + + # Reload the model in the prediction service + global rec_service + rec_service = RecommendationService() + + return {"status": "success", "message": "Model retrained successfully"} + except Exception as e: + logger.error(f"Training failed: {e}") + raise HTTPException( + status_code=500, + detail=f"Training failed: {str(e)}" + ) + + +if __name__ == "__main__": + import uvicorn + uvicorn.run( + "server:app", + host=settings.API_HOST, + port=settings.API_PORT, + reload=True + ) diff --git a/ai_recommendation/catboost_info/catboost_training.json b/ai_recommendation/catboost_info/catboost_training.json new file mode 100644 index 0000000..a208591 --- /dev/null +++ b/ai_recommendation/catboost_info/catboost_training.json @@ -0,0 +1,104 @@ +{ +"meta":{"test_sets":[],"test_metrics":[],"learn_metrics":[{"best_value":"Min","name":"RMSE"}],"launch_mode":"Train","parameters":"","iteration_count":100,"learn_sets":["learn"],"name":"experiment"}, +"iterations":[ +{"learn":[8.490161308],"iteration":0,"passed_time":0.06005424324,"remaining_time":5.945370081}, +{"learn":[8.373806216],"iteration":1,"passed_time":0.06296145459,"remaining_time":3.085111275}, +{"learn":[8.238263683],"iteration":2,"passed_time":0.06381481825,"remaining_time":2.06334579}, +{"learn":[8.169642811],"iteration":3,"passed_time":0.06404623184,"remaining_time":1.537109564}, +{"learn":[8.060850251],"iteration":4,"passed_time":0.06612191257,"remaining_time":1.256316339}, +{"learn":[7.983631348],"iteration":5,"passed_time":0.06676377904,"remaining_time":1.045965872}, +{"learn":[7.90596026],"iteration":6,"passed_time":0.06730098023,"remaining_time":0.8941415945}, +{"learn":[7.84104998],"iteration":7,"passed_time":0.06859237973,"remaining_time":0.7888123669}, +{"learn":[7.800442432],"iteration":8,"passed_time":0.06970907321,"remaining_time":0.7048361847}, +{"learn":[7.739154128],"iteration":9,"passed_time":0.07082518338,"remaining_time":0.6374266504}, +{"learn":[7.659037593],"iteration":10,"passed_time":0.07217570709,"remaining_time":0.5839670846}, +{"learn":[7.616938274],"iteration":11,"passed_time":0.07345364843,"remaining_time":0.5386600885}, +{"learn":[7.579837032],"iteration":12,"passed_time":0.07420247181,"remaining_time":0.4965857729}, +{"learn":[7.517777219],"iteration":13,"passed_time":0.07480167218,"remaining_time":0.4594959862}, +{"learn":[7.472993593],"iteration":14,"passed_time":0.07539058101,"remaining_time":0.4272132924}, +{"learn":[7.460014938],"iteration":15,"passed_time":0.07583920005,"remaining_time":0.3981558003}, +{"learn":[7.439039108],"iteration":16,"passed_time":0.0764624001,"remaining_time":0.373316424}, +{"learn":[7.408351879],"iteration":17,"passed_time":0.07709997495,"remaining_time":0.3512332192}, +{"learn":[7.376363098],"iteration":18,"passed_time":0.07769434205,"remaining_time":0.3312232477}, +{"learn":[7.341687642],"iteration":19,"passed_time":0.07843408222,"remaining_time":0.3137363289}, +{"learn":[7.315079848],"iteration":20,"passed_time":0.07912107308,"remaining_time":0.2976459416}, +{"learn":[7.265641769],"iteration":21,"passed_time":0.0796911905,"remaining_time":0.2825414936}, +{"learn":[7.237556607],"iteration":22,"passed_time":0.08030476568,"remaining_time":0.2688463894}, +{"learn":[7.216932833],"iteration":23,"passed_time":0.08099933978,"remaining_time":0.2564979093}, +{"learn":[7.199431055],"iteration":24,"passed_time":0.08168870561,"remaining_time":0.2450661168}, +{"learn":[7.140077907],"iteration":25,"passed_time":0.08228140607,"remaining_time":0.2341855403}, +{"learn":[7.136040156],"iteration":26,"passed_time":0.08285331513,"remaining_time":0.224010815}, +{"learn":[7.100280434],"iteration":27,"passed_time":0.08346780696,"remaining_time":0.2146315036}, +{"learn":[7.093441644],"iteration":28,"passed_time":0.0841041735,"remaining_time":0.2059102179}, +{"learn":[7.06745704],"iteration":29,"passed_time":0.08476841467,"remaining_time":0.1977929676}, +{"learn":[7.058277419],"iteration":30,"passed_time":0.08538940641,"remaining_time":0.1900602917}, +{"learn":[7.020819748],"iteration":31,"passed_time":0.08662709829,"remaining_time":0.1840825839}, +{"learn":[6.979784953],"iteration":32,"passed_time":0.08719996567,"remaining_time":0.1770423545}, +{"learn":[6.969701843],"iteration":33,"passed_time":0.08769379244,"remaining_time":0.1702291265}, +{"learn":[6.955113677],"iteration":34,"passed_time":0.08854894774,"remaining_time":0.1644480458}, +{"learn":[6.936496441],"iteration":35,"passed_time":0.08917873103,"remaining_time":0.1585399663}, +{"learn":[6.914271202],"iteration":36,"passed_time":0.08974680681,"remaining_time":0.1528121305}, +{"learn":[6.894095561],"iteration":37,"passed_time":0.09093966595,"remaining_time":0.1483752444}, +{"learn":[6.889508873],"iteration":38,"passed_time":0.09158232408,"remaining_time":0.1432441479}, +{"learn":[6.878562348],"iteration":39,"passed_time":0.0921472749,"remaining_time":0.1382209123}, +{"learn":[6.877649424],"iteration":40,"passed_time":0.0923778135,"remaining_time":0.1329339267}, +{"learn":[6.868589073],"iteration":41,"passed_time":0.09297763886,"remaining_time":0.1283976918}, +{"learn":[6.860057595],"iteration":42,"passed_time":0.09355563117,"remaining_time":0.1240156041}, +{"learn":[6.84587272],"iteration":43,"passed_time":0.09414991494,"remaining_time":0.1198271645}, +{"learn":[6.844351981],"iteration":44,"passed_time":0.09446991069,"remaining_time":0.1154632242}, +{"learn":[6.829992647],"iteration":45,"passed_time":0.09504698635,"remaining_time":0.111576897}, +{"learn":[6.807966727],"iteration":46,"passed_time":0.09557389601,"remaining_time":0.1077748189}, +{"learn":[6.753632391],"iteration":47,"passed_time":0.09614693006,"remaining_time":0.1041591742}, +{"learn":[6.737092303],"iteration":48,"passed_time":0.09673700554,"remaining_time":0.1006854547}, +{"learn":[6.725357951],"iteration":49,"passed_time":0.09745737097,"remaining_time":0.09745737097}, +{"learn":[6.716157638],"iteration":50,"passed_time":0.09809761245,"remaining_time":0.09425064726}, +{"learn":[6.705247775],"iteration":51,"passed_time":0.0987884366,"remaining_time":0.0911893261}, +{"learn":[6.674716369],"iteration":52,"passed_time":0.09943480301,"remaining_time":0.08817803286}, +{"learn":[6.674473045],"iteration":53,"passed_time":0.09984821418,"remaining_time":0.08505588615}, +{"learn":[6.668211191],"iteration":54,"passed_time":0.1004430396,"remaining_time":0.08218066877}, +{"learn":[6.668210643],"iteration":55,"passed_time":0.1006380787,"remaining_time":0.0790727761}, +{"learn":[6.64975285],"iteration":56,"passed_time":0.1013199863,"remaining_time":0.07643437561}, +{"learn":[6.643012495],"iteration":57,"passed_time":0.1019986856,"remaining_time":0.07386111715}, +{"learn":[6.628949237],"iteration":58,"passed_time":0.1027675504,"remaining_time":0.07141473839}, +{"learn":[6.618335048],"iteration":59,"passed_time":0.1034827909,"remaining_time":0.06898852724}, +{"learn":[6.606559507],"iteration":60,"passed_time":0.1042776553,"remaining_time":0.0666693206}, +{"learn":[6.580633084],"iteration":61,"passed_time":0.1050106455,"remaining_time":0.0643613634}, +{"learn":[6.575532704],"iteration":62,"passed_time":0.1056739701,"remaining_time":0.06206249035}, +{"learn":[6.563112937],"iteration":63,"passed_time":0.1069511614,"remaining_time":0.06016002829}, +{"learn":[6.55308881],"iteration":64,"passed_time":0.1088041784,"remaining_time":0.05858686532}, +{"learn":[6.54671997],"iteration":65,"passed_time":0.1094362117,"remaining_time":0.05637623027}, +{"learn":[6.535521809],"iteration":66,"passed_time":0.1100764949,"remaining_time":0.05421678105}, +{"learn":[6.511893223],"iteration":67,"passed_time":0.1107515276,"remaining_time":0.05211836591}, +{"learn":[6.491592668],"iteration":68,"passed_time":0.1114505183,"remaining_time":0.05007197197}, +{"learn":[6.49159265],"iteration":69,"passed_time":0.1119578865,"remaining_time":0.04798195136}, +{"learn":[6.491296353],"iteration":70,"passed_time":0.1122777573,"remaining_time":0.04585992902}, +{"learn":[6.475482935],"iteration":71,"passed_time":0.1130121225,"remaining_time":0.04394915875}, +{"learn":[6.471701064],"iteration":72,"passed_time":0.1135996564,"remaining_time":0.04201631125}, +{"learn":[6.463257275],"iteration":73,"passed_time":0.1142348562,"remaining_time":0.04013657111}, +{"learn":[6.454720942],"iteration":74,"passed_time":0.1147973904,"remaining_time":0.03826579681}, +{"learn":[6.44463254],"iteration":75,"passed_time":0.1156254628,"remaining_time":0.03651330403}, +{"learn":[6.432834162],"iteration":76,"passed_time":0.1168516131,"remaining_time":0.03490372859}, +{"learn":[6.426164054],"iteration":77,"passed_time":0.1175940199,"remaining_time":0.03316754408}, +{"learn":[6.425435023],"iteration":78,"passed_time":0.1179542651,"remaining_time":0.03135493124}, +{"learn":[6.40969427],"iteration":79,"passed_time":0.118564257,"remaining_time":0.02964106425}, +{"learn":[6.409694237],"iteration":80,"passed_time":0.1187986289,"remaining_time":0.02786634505}, +{"learn":[6.40165081],"iteration":81,"passed_time":0.1192938723,"remaining_time":0.02618645978}, +{"learn":[6.385068653],"iteration":82,"passed_time":0.1198573232,"remaining_time":0.02454909029}, +{"learn":[6.371703627],"iteration":83,"passed_time":0.120522981,"remaining_time":0.02295675828}, +{"learn":[6.358914748],"iteration":84,"passed_time":0.1210822235,"remaining_time":0.02136745121}, +{"learn":[6.352662962],"iteration":85,"passed_time":0.1219634618,"remaining_time":0.01985451704}, +{"learn":[6.347804514],"iteration":86,"passed_time":0.1225845786,"remaining_time":0.01831723588}, +{"learn":[6.340796969],"iteration":87,"passed_time":0.12310503,"remaining_time":0.01678704954}, +{"learn":[6.339095617],"iteration":88,"passed_time":0.123680064,"remaining_time":0.01528630005}, +{"learn":[6.333251992],"iteration":89,"passed_time":0.1242406815,"remaining_time":0.01380452017}, +{"learn":[6.332902776],"iteration":90,"passed_time":0.1246462178,"remaining_time":0.01232764792}, +{"learn":[6.323785642],"iteration":91,"passed_time":0.1252164602,"remaining_time":0.01088838785}, +{"learn":[6.311846341],"iteration":92,"passed_time":0.1258084107,"remaining_time":0.009469450268}, +{"learn":[6.305869337],"iteration":93,"passed_time":0.1263173206,"remaining_time":0.008062807698}, +{"learn":[6.30586888],"iteration":94,"passed_time":0.1266302331,"remaining_time":0.006664749111}, +{"learn":[6.305416066],"iteration":95,"passed_time":0.1272138504,"remaining_time":0.005300577098}, +{"learn":[6.282069342],"iteration":96,"passed_time":0.1277621347,"remaining_time":0.003951406229}, +{"learn":[6.281660413],"iteration":97,"passed_time":0.1284322925,"remaining_time":0.002621067194}, +{"learn":[6.269315886],"iteration":98,"passed_time":0.1305252647,"remaining_time":0.001318437017}, +{"learn":[6.268990016],"iteration":99,"passed_time":0.1311366732,"remaining_time":0} +]} \ No newline at end of file diff --git a/ai_recommendation/catboost_info/learn/events.out.tfevents b/ai_recommendation/catboost_info/learn/events.out.tfevents new file mode 100644 index 0000000..bd79887 Binary files /dev/null and b/ai_recommendation/catboost_info/learn/events.out.tfevents differ diff --git a/ai_recommendation/catboost_info/learn_error.tsv b/ai_recommendation/catboost_info/learn_error.tsv new file mode 100644 index 0000000..aa712cc --- /dev/null +++ b/ai_recommendation/catboost_info/learn_error.tsv @@ -0,0 +1,101 @@ +iter RMSE +0 8.490161308 +1 8.373806216 +2 8.238263683 +3 8.169642811 +4 8.060850251 +5 7.983631348 +6 7.90596026 +7 7.84104998 +8 7.800442432 +9 7.739154128 +10 7.659037593 +11 7.616938274 +12 7.579837032 +13 7.517777219 +14 7.472993593 +15 7.460014938 +16 7.439039108 +17 7.408351879 +18 7.376363098 +19 7.341687642 +20 7.315079848 +21 7.265641769 +22 7.237556607 +23 7.216932833 +24 7.199431055 +25 7.140077907 +26 7.136040156 +27 7.100280434 +28 7.093441644 +29 7.06745704 +30 7.058277419 +31 7.020819748 +32 6.979784953 +33 6.969701843 +34 6.955113677 +35 6.936496441 +36 6.914271202 +37 6.894095561 +38 6.889508873 +39 6.878562348 +40 6.877649424 +41 6.868589073 +42 6.860057595 +43 6.84587272 +44 6.844351981 +45 6.829992647 +46 6.807966727 +47 6.753632391 +48 6.737092303 +49 6.725357951 +50 6.716157638 +51 6.705247775 +52 6.674716369 +53 6.674473045 +54 6.668211191 +55 6.668210643 +56 6.64975285 +57 6.643012495 +58 6.628949237 +59 6.618335048 +60 6.606559507 +61 6.580633084 +62 6.575532704 +63 6.563112937 +64 6.55308881 +65 6.54671997 +66 6.535521809 +67 6.511893223 +68 6.491592668 +69 6.49159265 +70 6.491296353 +71 6.475482935 +72 6.471701064 +73 6.463257275 +74 6.454720942 +75 6.44463254 +76 6.432834162 +77 6.426164054 +78 6.425435023 +79 6.40969427 +80 6.409694237 +81 6.40165081 +82 6.385068653 +83 6.371703627 +84 6.358914748 +85 6.352662962 +86 6.347804514 +87 6.340796969 +88 6.339095617 +89 6.333251992 +90 6.332902776 +91 6.323785642 +92 6.311846341 +93 6.305869337 +94 6.30586888 +95 6.305416066 +96 6.282069342 +97 6.281660413 +98 6.269315886 +99 6.268990016 diff --git a/ai_recommendation/catboost_info/time_left.tsv b/ai_recommendation/catboost_info/time_left.tsv new file mode 100644 index 0000000..7759d48 --- /dev/null +++ b/ai_recommendation/catboost_info/time_left.tsv @@ -0,0 +1,101 @@ +iter Passed Remaining +0 60 5945 +1 62 3085 +2 63 2063 +3 64 1537 +4 66 1256 +5 66 1045 +6 67 894 +7 68 788 +8 69 704 +9 70 637 +10 72 583 +11 73 538 +12 74 496 +13 74 459 +14 75 427 +15 75 398 +16 76 373 +17 77 351 +18 77 331 +19 78 313 +20 79 297 +21 79 282 +22 80 268 +23 80 256 +24 81 245 +25 82 234 +26 82 224 +27 83 214 +28 84 205 +29 84 197 +30 85 190 +31 86 184 +32 87 177 +33 87 170 +34 88 164 +35 89 158 +36 89 152 +37 90 148 +38 91 143 +39 92 138 +40 92 132 +41 92 128 +42 93 124 +43 94 119 +44 94 115 +45 95 111 +46 95 107 +47 96 104 +48 96 100 +49 97 97 +50 98 94 +51 98 91 +52 99 88 +53 99 85 +54 100 82 +55 100 79 +56 101 76 +57 101 73 +58 102 71 +59 103 68 +60 104 66 +61 105 64 +62 105 62 +63 106 60 +64 108 58 +65 109 56 +66 110 54 +67 110 52 +68 111 50 +69 111 47 +70 112 45 +71 113 43 +72 113 42 +73 114 40 +74 114 38 +75 115 36 +76 116 34 +77 117 33 +78 117 31 +79 118 29 +80 118 27 +81 119 26 +82 119 24 +83 120 22 +84 121 21 +85 121 19 +86 122 18 +87 123 16 +88 123 15 +89 124 13 +90 124 12 +91 125 10 +92 125 9 +93 126 8 +94 126 6 +95 127 5 +96 127 3 +97 128 2 +98 130 1 +99 131 0 diff --git a/ai_recommendation/config/settings.py b/ai_recommendation/config/settings.py new file mode 100644 index 0000000..cb19b9f --- /dev/null +++ b/ai_recommendation/config/settings.py @@ -0,0 +1,45 @@ +""" +AI 추천 시스템 설정 파일 +""" +from pathlib import Path +from pydantic_settings import BaseSettings + + +class Settings(BaseSettings): + """애플리케이션 설정""" + + # 프로젝트 경로 + PROJECT_ROOT: Path = Path(__file__).parent.parent + DATA_DIR: Path = PROJECT_ROOT / "data" + MODELS_DIR: Path = PROJECT_ROOT / "models" + + # 데이터 파일 + TRAINING_DATA_PATH: Path = DATA_DIR / "training_data.csv" + MODEL_PATH: Path = MODELS_DIR / "catboost_model.cbm" + + # 모델 파라미터 + CATBOOST_ITERATIONS: int = 100 + CATBOOST_LEARNING_RATE: float = 0.1 + CATBOOST_DEPTH: int = 6 + CATBOOST_LOSS_FUNCTION: str = "RMSE" + + # API 설정 + API_HOST: str = "0.0.0.0" + API_PORT: int = 8000 + API_TITLE: str = "MOV AI Recommendation API" + API_VERSION: str = "1.0.0" + + # Spring Boot 백엔드 + SPRING_BOOT_URL: str = "http://localhost:8080" + + # 추천 설정 + TOP_N_RECOMMENDATIONS: int = 10 + MIN_SCORE_THRESHOLD: float = 0.3 + + class Config: + env_file = ".env" + env_file_encoding = "utf-8" + + +# 전역 설정 인스턴스 +settings = Settings() diff --git a/ai_recommendation/data/__init__.py b/ai_recommendation/data/__init__.py new file mode 100644 index 0000000..2f12ba1 --- /dev/null +++ b/ai_recommendation/data/__init__.py @@ -0,0 +1 @@ +"""Data module for training data management.""" diff --git a/ai_recommendation/data/training_data.csv b/ai_recommendation/data/training_data.csv new file mode 100644 index 0000000..66c9942 --- /dev/null +++ b/ai_recommendation/data/training_data.csv @@ -0,0 +1,1001 @@ +purpose,preferredIntensity,interestedSportIds,price,preferredEnvironment,avoidFactors,recoveryCondition,purchased_pass_id +취미 탐색,가벼운 웜업 위주,"1,3",30000,실내,땀이 흐를 정도의 중강도,높음/컨디션 양호,1 +취미 탐색,살짝 땀이 나는 정도,1,30000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,1 +취미 탐색,살짝 땀이 나는 정도,1,30000,실내,끝나면 기진맥진 고강도,평범함,1 +취미 탐색,살짝 땀이 나는 정도,3,50000,실외,끝나면 기진맥진 고강도,평범함,1 +취미 탐색,가벼운 웜업 위주,"1,3",50000,실외,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",평범함,1 +취미 탐색,가벼운 웜업 위주,1,10000,실내,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",높음/컨디션 양호,1 +취미 탐색,살짝 땀이 나는 정도,3,50000,실내,땀이 흐를 정도의 중강도,평범함,1 +취미 탐색,가벼운 웜업 위주,3,30000,실내,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",평범함,1 +취미 탐색,가벼운 웜업 위주,"1,3",30000,실내,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",매우 지침/피로함,1 +스트레스 해소,끝나면 기진맥진 고강도,"8,7",100000,실외,가벼운 웜업 위주,매우 지침/피로함,1 +취미 탐색,가벼운 웜업 위주,"1,3",30000,실내,땀이 흐를 정도의 중강도,높음/컨디션 양호,1 +취미 탐색,살짝 땀이 나는 정도,3,30000,실내,땀이 흐를 정도의 중강도,높음/컨디션 양호,1 +취미 탐색,살짝 땀이 나는 정도,3,30000,실내,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",평범함,1 +취미 탐색,가벼운 웜업 위주,"1,3",30000,실내,끝나면 기진맥진 고강도,평범함,1 +취미 탐색,가벼운 웜업 위주,3,50000,실내,끝나면 기진맥진 고강도,평범함,1 +취미 탐색,끝나면 기진맥진 고강도,9,70000,상관없음,,높음/컨디션 양호,1 +"회복(통증완화, 재활 등)",땀이 흐를 정도의 중강도,5,70000,실외,,매우 지침/피로함,1 +취미 탐색,가벼운 웜업 위주,1,30000,실내,땀이 흐를 정도의 중강도,평범함,1 +취미 탐색,가벼운 웜업 위주,"1,3",30000,실내,땀이 흐를 정도의 중강도,평범함,1 +취미 탐색,살짝 땀이 나는 정도,3,30000,실내,끝나면 기진맥진 고강도,높음/컨디션 양호,1 +취미 탐색,살짝 땀이 나는 정도,1,30000,실내,땀이 흐를 정도의 중강도,평범함,1 +"회복(통증완화, 재활 등)",끝나면 기진맥진 고강도,"6,4",100000,실내,,높음/컨디션 양호,1 +취미 탐색,살짝 땀이 나는 정도,"9,6",10000,실외,,평범함,1 +취미 탐색,살짝 땀이 나는 정도,3,30000,실외,땀이 흐를 정도의 중강도,평범함,1 +취미 탐색,가벼운 웜업 위주,3,30000,실내,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",평범함,1 +다이어트,끝나면 기진맥진 고강도,4,100000,실내,,높음/컨디션 양호,1 +취미 탐색,살짝 땀이 나는 정도,"1,3",50000,실내,끝나면 기진맥진 고강도,평범함,1 +취미 탐색,살짝 땀이 나는 정도,1,30000,실내,땀이 흐를 정도의 중강도,높음/컨디션 양호,1 +취미 탐색,살짝 땀이 나는 정도,"1,3",50000,실내,끝나면 기진맥진 고강도,높음/컨디션 양호,1 +취미 탐색,가벼운 웜업 위주,"1,3",30000,실내,끝나면 기진맥진 고강도,평범함,1 +취미 탐색,가벼운 웜업 위주,1,30000,실내,땀이 흐를 정도의 중강도,평범함,1 +취미 탐색,끝나면 기진맥진 고강도,"6,3",70000,상관없음,가벼운 웜업 위주,매우 지침/피로함,1 +"회복(통증완화, 재활 등)",땀이 흐를 정도의 중강도,"4,2",10000,실외,,매우 지침/피로함,1 +취미 탐색,살짝 땀이 나는 정도,"1,3",70000,실외,땀이 흐를 정도의 중강도,평범함,1 +다이어트,살짝 땀이 나는 정도,"6,7",100000,실내,,매우 지침/피로함,2 +취미 탐색,땀이 흐를 정도의 중강도,3,10000,실외,,매우 지침/피로함,2 +취미 탐색,땀이 흐를 정도의 중강도,"5,3",100000,실외,,높음/컨디션 양호,2 +다이어트,끝나면 기진맥진 고강도,"1,5",30000,실내,가벼운 웜업 위주,높음/컨디션 양호,2 +다이어트,끝나면 기진맥진 고강도,"1,5",30000,실내,,평범함,2 +다이어트,끝나면 기진맥진 고강도,"1,5",50000,상관없음,,평범함,2 +다이어트,끝나면 기진맥진 고강도,"1,5",30000,실내,가벼운 웜업 위주,평범함,2 +다이어트,끝나면 기진맥진 고강도,"1,5",50000,실내,,높음/컨디션 양호,2 +다이어트,끝나면 기진맥진 고강도,5,50000,실내,,높음/컨디션 양호,2 +다이어트,끝나면 기진맥진 고강도,5,30000,실내,,높음/컨디션 양호,2 +다이어트,끝나면 기진맥진 고강도,5,30000,상관없음,,매우 지침/피로함,2 +다이어트,끝나면 기진맥진 고강도,"1,5",30000,실내,가벼운 웜업 위주,높음/컨디션 양호,2 +다이어트,가벼운 웜업 위주,"9,8",100000,실내,가벼운 웜업 위주,평범함,2 +다이어트,끝나면 기진맥진 고강도,"1,5",50000,실외,가벼운 웜업 위주,평범함,2 +다이어트,끝나면 기진맥진 고강도,"1,5",30000,상관없음,,높음/컨디션 양호,2 +취미 탐색,살짝 땀이 나는 정도,"4,3",100000,실내,,매우 지침/피로함,2 +다이어트,끝나면 기진맥진 고강도,1,50000,실내,,평범함,2 +다이어트,끝나면 기진맥진 고강도,"1,5",30000,실외,,높음/컨디션 양호,2 +취미 탐색,땀이 흐를 정도의 중강도,3,70000,실외,,평범함,2 +다이어트,끝나면 기진맥진 고강도,"1,5",70000,실내,가벼운 웜업 위주,평범함,2 +다이어트,끝나면 기진맥진 고강도,1,50000,실내,가벼운 웜업 위주,높음/컨디션 양호,2 +다이어트,끝나면 기진맥진 고강도,1,50000,실내,,평범함,2 +다이어트,끝나면 기진맥진 고강도,1,10000,실내,,높음/컨디션 양호,2 +다이어트,끝나면 기진맥진 고강도,"1,5",30000,실내,가벼운 웜업 위주,매우 지침/피로함,2 +다이어트,끝나면 기진맥진 고강도,1,30000,상관없음,가벼운 웜업 위주,평범함,2 +다이어트,끝나면 기진맥진 고강도,"1,5",10000,실내,가벼운 웜업 위주,높음/컨디션 양호,2 +다이어트,끝나면 기진맥진 고강도,"1,5",50000,실내,가벼운 웜업 위주,높음/컨디션 양호,2 +다이어트,끝나면 기진맥진 고강도,"1,5",30000,실내,가벼운 웜업 위주,매우 지침/피로함,2 +다이어트,가벼운 웜업 위주,"4,2",10000,실외,땀이 흐를 정도의 중강도,높음/컨디션 양호,2 +체력 증진,땀이 흐를 정도의 중강도,"9,8",100000,상관없음,,매우 지침/피로함,2 +취미 탐색,살짝 땀이 나는 정도,8,70000,실외,,매우 지침/피로함,2 +다이어트,끝나면 기진맥진 고강도,5,50000,상관없음,,높음/컨디션 양호,2 +다이어트,끝나면 기진맥진 고강도,"1,5",30000,상관없음,가벼운 웜업 위주,평범함,2 +다이어트,끝나면 기진맥진 고강도,5,30000,실내,가벼운 웜업 위주,평범함,2 +"회복(통증완화, 재활 등)",살짝 땀이 나는 정도,4,50000,실내,끝나면 기진맥진 고강도,평범함,3 +"회복(통증완화, 재활 등)",가벼운 웜업 위주,3,50000,실내,땀이 흐를 정도의 중강도,매우 지침/피로함,3 +"회복(통증완화, 재활 등)",가벼운 웜업 위주,"3,4",50000,실내,끝나면 기진맥진 고강도,매우 지침/피로함,3 +"회복(통증완화, 재활 등)",가벼운 웜업 위주,"3,4",30000,상관없음,끝나면 기진맥진 고강도,평범함,3 +"회복(통증완화, 재활 등)",살짝 땀이 나는 정도,4,30000,실내,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",매우 지침/피로함,3 +"회복(통증완화, 재활 등)",가벼운 웜업 위주,3,30000,실내,끝나면 기진맥진 고강도,매우 지침/피로함,3 +체력 증진,끝나면 기진맥진 고강도,2,70000,실외,"땀이 흐를 정도의 중강도,가벼운 웜업 위주",높음/컨디션 양호,3 +체력 증진,땀이 흐를 정도의 중강도,"2,7",70000,실외,가벼운 웜업 위주,매우 지침/피로함,3 +"회복(통증완화, 재활 등)",살짝 땀이 나는 정도,"3,4",30000,실내,땀이 흐를 정도의 중강도,매우 지침/피로함,3 +"회복(통증완화, 재활 등)",가벼운 웜업 위주,3,50000,실내,끝나면 기진맥진 고강도,평범함,3 +"회복(통증완화, 재활 등)",살짝 땀이 나는 정도,4,50000,실내,땀이 흐를 정도의 중강도,매우 지침/피로함,3 +"회복(통증완화, 재활 등)",가벼운 웜업 위주,"3,4",100000,실내,끝나면 기진맥진 고강도,평범함,3 +"회복(통증완화, 재활 등)",땀이 흐를 정도의 중강도,2,70000,실외,,높음/컨디션 양호,3 +"회복(통증완화, 재활 등)",살짝 땀이 나는 정도,"3,4",50000,실내,끝나면 기진맥진 고강도,평범함,3 +취미 탐색,끝나면 기진맥진 고강도,8,70000,실내,끝나면 기진맥진 고강도,매우 지침/피로함,3 +"회복(통증완화, 재활 등)",살짝 땀이 나는 정도,3,50000,상관없음,땀이 흐를 정도의 중강도,평범함,3 +"회복(통증완화, 재활 등)",가벼운 웜업 위주,"3,4",50000,실내,끝나면 기진맥진 고강도,매우 지침/피로함,3 +"회복(통증완화, 재활 등)",가벼운 웜업 위주,3,30000,실내,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",평범함,3 +"회복(통증완화, 재활 등)",가벼운 웜업 위주,"3,4",50000,상관없음,땀이 흐를 정도의 중강도,평범함,3 +"회복(통증완화, 재활 등)",끝나면 기진맥진 고강도,"7,9",30000,실외,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",매우 지침/피로함,3 +"회복(통증완화, 재활 등)",살짝 땀이 나는 정도,"3,4",50000,실내,땀이 흐를 정도의 중강도,매우 지침/피로함,3 +"회복(통증완화, 재활 등)",살짝 땀이 나는 정도,4,30000,실외,끝나면 기진맥진 고강도,평범함,3 +"회복(통증완화, 재활 등)",살짝 땀이 나는 정도,"3,4",70000,실내,땀이 흐를 정도의 중강도,평범함,3 +"회복(통증완화, 재활 등)",살짝 땀이 나는 정도,"3,4",70000,실내,땀이 흐를 정도의 중강도,매우 지침/피로함,3 +"회복(통증완화, 재활 등)",가벼운 웜업 위주,3,50000,실내,땀이 흐를 정도의 중강도,평범함,3 +"회복(통증완화, 재활 등)",살짝 땀이 나는 정도,"3,4",70000,실내,땀이 흐를 정도의 중강도,매우 지침/피로함,3 +다이어트,땀이 흐를 정도의 중강도,7,10000,상관없음,끝나면 기진맥진 고강도,평범함,3 +다이어트,땀이 흐를 정도의 중강도,"7,6",10000,실내,,높음/컨디션 양호,3 +체력 증진,끝나면 기진맥진 고강도,"1,6",10000,실내,,평범함,3 +취미 탐색,끝나면 기진맥진 고강도,"7,8",100000,실외,"땀이 흐를 정도의 중강도,가벼운 웜업 위주",높음/컨디션 양호,3 +"회복(통증완화, 재활 등)",가벼운 웜업 위주,3,70000,실내,땀이 흐를 정도의 중강도,평범함,3 +"회복(통증완화, 재활 등)",살짝 땀이 나는 정도,3,30000,실내,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",평범함,3 +"회복(통증완화, 재활 등)",살짝 땀이 나는 정도,4,10000,실내,끝나면 기진맥진 고강도,평범함,3 +"회복(통증완화, 재활 등)",가벼운 웜업 위주,"3,4",50000,상관없음,땀이 흐를 정도의 중강도,평범함,3 +취미 탐색,땀이 흐를 정도의 중강도,"2,3",30000,상관없음,끝나면 기진맥진 고강도,평범함,4 +스트레스 해소,살짝 땀이 나는 정도,6,50000,실외,끝나면 기진맥진 고강도,높음/컨디션 양호,4 +스트레스 해소,가벼운 웜업 위주,4,30000,실내,땀이 흐를 정도의 중강도,평범함,4 +취미 탐색,살짝 땀이 나는 정도,"5,1",100000,실내,,평범함,4 +스트레스 해소,살짝 땀이 나는 정도,6,10000,상관없음,땀이 흐를 정도의 중강도,높음/컨디션 양호,4 +스트레스 해소,살짝 땀이 나는 정도,4,50000,실내,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",높음/컨디션 양호,4 +스트레스 해소,살짝 땀이 나는 정도,4,30000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,4 +스트레스 해소,가벼운 웜업 위주,4,30000,실외,끝나면 기진맥진 고강도,평범함,4 +스트레스 해소,가벼운 웜업 위주,"6,4",30000,실외,끝나면 기진맥진 고강도,높음/컨디션 양호,4 +"회복(통증완화, 재활 등)",땀이 흐를 정도의 중강도,"3,1",10000,상관없음,,매우 지침/피로함,4 +스트레스 해소,살짝 땀이 나는 정도,4,30000,상관없음,땀이 흐를 정도의 중강도,평범함,4 +스트레스 해소,살짝 땀이 나는 정도,6,50000,실내,땀이 흐를 정도의 중강도,높음/컨디션 양호,4 +스트레스 해소,살짝 땀이 나는 정도,6,30000,실내,끝나면 기진맥진 고강도,평범함,4 +스트레스 해소,살짝 땀이 나는 정도,4,50000,실내,땀이 흐를 정도의 중강도,높음/컨디션 양호,4 +"회복(통증완화, 재활 등)",끝나면 기진맥진 고강도,"8,7",70000,실외,,높음/컨디션 양호,4 +스트레스 해소,가벼운 웜업 위주,"6,4",30000,실내,끝나면 기진맥진 고강도,높음/컨디션 양호,4 +스트레스 해소,살짝 땀이 나는 정도,"6,4",50000,실내,땀이 흐를 정도의 중강도,평범함,4 +스트레스 해소,땀이 흐를 정도의 중강도,2,10000,실내,,높음/컨디션 양호,4 +스트레스 해소,살짝 땀이 나는 정도,4,100000,실외,땀이 흐를 정도의 중강도,평범함,4 +스트레스 해소,살짝 땀이 나는 정도,4,50000,실내,끝나면 기진맥진 고강도,높음/컨디션 양호,4 +취미 탐색,살짝 땀이 나는 정도,1,70000,실외,,평범함,4 +스트레스 해소,살짝 땀이 나는 정도,"6,4",30000,실내,끝나면 기진맥진 고강도,평범함,4 +스트레스 해소,가벼운 웜업 위주,4,50000,실내,끝나면 기진맥진 고강도,평범함,4 +스트레스 해소,살짝 땀이 나는 정도,4,50000,실내,끝나면 기진맥진 고강도,평범함,4 +스트레스 해소,가벼운 웜업 위주,6,50000,실내,끝나면 기진맥진 고강도,높음/컨디션 양호,4 +스트레스 해소,살짝 땀이 나는 정도,4,30000,상관없음,땀이 흐를 정도의 중강도,높음/컨디션 양호,4 +스트레스 해소,가벼운 웜업 위주,4,50000,실외,땀이 흐를 정도의 중강도,평범함,4 +스트레스 해소,살짝 땀이 나는 정도,4,50000,실내,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",평범함,4 +스트레스 해소,살짝 땀이 나는 정도,6,50000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,4 +스트레스 해소,가벼운 웜업 위주,6,30000,상관없음,땀이 흐를 정도의 중강도,높음/컨디션 양호,4 +스트레스 해소,가벼운 웜업 위주,6,30000,실외,끝나면 기진맥진 고강도,높음/컨디션 양호,4 +스트레스 해소,살짝 땀이 나는 정도,4,50000,실내,끝나면 기진맥진 고강도,높음/컨디션 양호,4 +스트레스 해소,살짝 땀이 나는 정도,"6,4",50000,상관없음,끝나면 기진맥진 고강도,평범함,4 +스트레스 해소,가벼운 웜업 위주,4,30000,실외,땀이 흐를 정도의 중강도,평범함,4 +체력 증진,땀이 흐를 정도의 중강도,1,30000,실내,,평범함,5 +체력 증진,땀이 흐를 정도의 중강도,1,50000,실내,끝나면 기진맥진 고강도,높음/컨디션 양호,5 +체력 증진,땀이 흐를 정도의 중강도,"1,2",100000,상관없음,,평범함,5 +체력 증진,땀이 흐를 정도의 중강도,2,100000,실내,,평범함,5 +체력 증진,땀이 흐를 정도의 중강도,2,30000,실내,끝나면 기진맥진 고강도,높음/컨디션 양호,5 +취미 탐색,살짝 땀이 나는 정도,5,70000,실내,"끝나면 기진맥진 고강도,가벼운 웜업 위주",높음/컨디션 양호,5 +체력 증진,땀이 흐를 정도의 중강도,1,30000,실내,끝나면 기진맥진 고강도,평범함,5 +체력 증진,땀이 흐를 정도의 중강도,"1,2",50000,실내,끝나면 기진맥진 고강도,평범함,5 +체력 증진,가벼운 웜업 위주,7,10000,실내,,높음/컨디션 양호,5 +체력 증진,땀이 흐를 정도의 중강도,"1,2",50000,실내,끝나면 기진맥진 고강도,높음/컨디션 양호,5 +취미 탐색,살짝 땀이 나는 정도,"9,6",100000,실외,,평범함,5 +체력 증진,땀이 흐를 정도의 중강도,2,30000,실외,끝나면 기진맥진 고강도,높음/컨디션 양호,5 +체력 증진,땀이 흐를 정도의 중강도,2,30000,상관없음,,높음/컨디션 양호,5 +체력 증진,땀이 흐를 정도의 중강도,2,30000,실내,끝나면 기진맥진 고강도,높음/컨디션 양호,5 +체력 증진,가벼운 웜업 위주,"5,4",70000,실외,,평범함,5 +체력 증진,가벼운 웜업 위주,6,30000,실외,,매우 지침/피로함,5 +"회복(통증완화, 재활 등)",살짝 땀이 나는 정도,7,70000,실내,,매우 지침/피로함,5 +체력 증진,땀이 흐를 정도의 중강도,"1,2",30000,실내,,평범함,5 +체력 증진,땀이 흐를 정도의 중강도,1,50000,실내,끝나면 기진맥진 고강도,평범함,5 +체력 증진,땀이 흐를 정도의 중강도,1,50000,실내,,평범함,5 +체력 증진,땀이 흐를 정도의 중강도,"1,2",50000,상관없음,끝나면 기진맥진 고강도,평범함,5 +다이어트,살짝 땀이 나는 정도,3,100000,실외,,평범함,5 +체력 증진,땀이 흐를 정도의 중강도,"1,2",50000,상관없음,,높음/컨디션 양호,5 +체력 증진,땀이 흐를 정도의 중강도,"1,2",50000,실내,끝나면 기진맥진 고강도,높음/컨디션 양호,5 +스트레스 해소,끝나면 기진맥진 고강도,5,10000,실내,,매우 지침/피로함,5 +다이어트,살짝 땀이 나는 정도,6,50000,실외,끝나면 기진맥진 고강도,평범함,5 +체력 증진,가벼운 웜업 위주,4,10000,실내,,평범함,5 +체력 증진,땀이 흐를 정도의 중강도,"1,2",50000,실내,끝나면 기진맥진 고강도,평범함,5 +체력 증진,땀이 흐를 정도의 중강도,1,50000,실내,끝나면 기진맥진 고강도,높음/컨디션 양호,5 +취미 탐색,가벼운 웜업 위주,"8,5",100000,실내,땀이 흐를 정도의 중강도,평범함,5 +체력 증진,땀이 흐를 정도의 중강도,1,50000,실내,,높음/컨디션 양호,5 +"회복(통증완화, 재활 등)",끝나면 기진맥진 고강도,3,50000,상관없음,,높음/컨디션 양호,5 +체력 증진,땀이 흐를 정도의 중강도,"1,2",30000,실내,,평범함,5 +체력 증진,땀이 흐를 정도의 중강도,"1,2",50000,실내,,평범함,5 +다이어트,땀이 흐를 정도의 중강도,"1,6",30000,실내,끝나면 기진맥진 고강도,높음/컨디션 양호,6 +다이어트,살짝 땀이 나는 정도,"7,9",100000,실내,땀이 흐를 정도의 중강도,평범함,6 +다이어트,끝나면 기진맥진 고강도,2,30000,상관없음,,평범함,6 +다이어트,땀이 흐를 정도의 중강도,1,30000,실내,끝나면 기진맥진 고강도,높음/컨디션 양호,6 +스트레스 해소,살짝 땀이 나는 정도,3,50000,상관없음,"땀이 흐를 정도의 중강도,가벼운 웜업 위주",높음/컨디션 양호,6 +스트레스 해소,가벼운 웜업 위주,"7,3",100000,상관없음,,높음/컨디션 양호,6 +다이어트,땀이 흐를 정도의 중강도,"1,6",50000,실내,,평범함,6 +다이어트,땀이 흐를 정도의 중강도,6,50000,상관없음,끝나면 기진맥진 고강도,평범함,6 +다이어트,땀이 흐를 정도의 중강도,1,50000,상관없음,끝나면 기진맥진 고강도,평범함,6 +다이어트,땀이 흐를 정도의 중강도,6,50000,실내,끝나면 기진맥진 고강도,평범함,6 +다이어트,땀이 흐를 정도의 중강도,1,50000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,6 +다이어트,땀이 흐를 정도의 중강도,"1,6",70000,실내,,높음/컨디션 양호,6 +다이어트,가벼운 웜업 위주,5,10000,실외,"가벼운 웜업 위주,땀이 흐를 정도의 중강도",매우 지침/피로함,6 +다이어트,땀이 흐를 정도의 중강도,6,30000,실내,끝나면 기진맥진 고강도,높음/컨디션 양호,6 +다이어트,땀이 흐를 정도의 중강도,"1,6",50000,실내,끝나면 기진맥진 고강도,높음/컨디션 양호,6 +다이어트,땀이 흐를 정도의 중강도,6,50000,실외,,높음/컨디션 양호,6 +다이어트,땀이 흐를 정도의 중강도,6,50000,상관없음,,높음/컨디션 양호,6 +다이어트,땀이 흐를 정도의 중강도,1,30000,실내,,평범함,6 +다이어트,땀이 흐를 정도의 중강도,"1,6",50000,실외,끝나면 기진맥진 고강도,평범함,6 +다이어트,끝나면 기진맥진 고강도,5,70000,실외,,매우 지침/피로함,6 +"회복(통증완화, 재활 등)",끝나면 기진맥진 고강도,9,100000,실내,끝나면 기진맥진 고강도,높음/컨디션 양호,6 +다이어트,땀이 흐를 정도의 중강도,"1,6",30000,실내,끝나면 기진맥진 고강도,높음/컨디션 양호,6 +다이어트,땀이 흐를 정도의 중강도,6,30000,실내,끝나면 기진맥진 고강도,매우 지침/피로함,6 +다이어트,가벼운 웜업 위주,7,50000,실외,가벼운 웜업 위주,평범함,6 +스트레스 해소,가벼운 웜업 위주,"5,4",50000,상관없음,,높음/컨디션 양호,6 +다이어트,땀이 흐를 정도의 중강도,6,50000,실내,,높음/컨디션 양호,6 +다이어트,땀이 흐를 정도의 중강도,1,50000,실내,,높음/컨디션 양호,6 +다이어트,땀이 흐를 정도의 중강도,6,30000,실내,끝나면 기진맥진 고강도,평범함,6 +다이어트,땀이 흐를 정도의 중강도,1,50000,실내,,높음/컨디션 양호,6 +다이어트,땀이 흐를 정도의 중강도,1,50000,상관없음,,높음/컨디션 양호,6 +다이어트,땀이 흐를 정도의 중강도,"1,6",10000,실외,끝나면 기진맥진 고강도,높음/컨디션 양호,6 +체력 증진,가벼운 웜업 위주,"8,3",50000,실내,,높음/컨디션 양호,6 +다이어트,땀이 흐를 정도의 중강도,"1,6",50000,실내,,평범함,6 +다이어트,땀이 흐를 정도의 중강도,"1,6",50000,실내,,높음/컨디션 양호,6 +체력 증진,끝나면 기진맥진 고강도,"1,2",30000,실내,가벼운 웜업 위주,평범함,7 +다이어트,가벼운 웜업 위주,"4,1",70000,실외,끝나면 기진맥진 고강도,높음/컨디션 양호,7 +체력 증진,끝나면 기진맥진 고강도,"1,2",30000,실내,,높음/컨디션 양호,7 +체력 증진,끝나면 기진맥진 고강도,2,50000,실내,가벼운 웜업 위주,높음/컨디션 양호,7 +취미 탐색,땀이 흐를 정도의 중강도,6,100000,실내,끝나면 기진맥진 고강도,매우 지침/피로함,7 +체력 증진,끝나면 기진맥진 고강도,2,30000,실내,가벼운 웜업 위주,평범함,7 +체력 증진,끝나면 기진맥진 고강도,1,50000,실내,가벼운 웜업 위주,매우 지침/피로함,7 +체력 증진,끝나면 기진맥진 고강도,"1,2",30000,상관없음,,평범함,7 +체력 증진,끝나면 기진맥진 고강도,2,50000,실내,,높음/컨디션 양호,7 +체력 증진,끝나면 기진맥진 고강도,1,30000,상관없음,가벼운 웜업 위주,높음/컨디션 양호,7 +스트레스 해소,가벼운 웜업 위주,8,70000,실외,"땀이 흐를 정도의 중강도,가벼운 웜업 위주",높음/컨디션 양호,7 +체력 증진,살짝 땀이 나는 정도,"4,7",100000,실내,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",높음/컨디션 양호,7 +체력 증진,끝나면 기진맥진 고강도,2,30000,상관없음,,높음/컨디션 양호,7 +체력 증진,끝나면 기진맥진 고강도,"1,2",10000,실내,가벼운 웜업 위주,높음/컨디션 양호,7 +체력 증진,끝나면 기진맥진 고강도,1,30000,실내,가벼운 웜업 위주,평범함,7 +체력 증진,끝나면 기진맥진 고강도,1,50000,실내,가벼운 웜업 위주,매우 지침/피로함,7 +체력 증진,끝나면 기진맥진 고강도,2,30000,상관없음,,높음/컨디션 양호,7 +체력 증진,끝나면 기진맥진 고강도,1,50000,상관없음,,평범함,7 +스트레스 해소,살짝 땀이 나는 정도,6,30000,실외,땀이 흐를 정도의 중강도,매우 지침/피로함,7 +체력 증진,끝나면 기진맥진 고강도,"1,2",30000,실내,,높음/컨디션 양호,7 +체력 증진,끝나면 기진맥진 고강도,2,10000,상관없음,가벼운 웜업 위주,높음/컨디션 양호,7 +체력 증진,끝나면 기진맥진 고강도,1,30000,실외,가벼운 웜업 위주,평범함,7 +체력 증진,끝나면 기진맥진 고강도,2,30000,실내,가벼운 웜업 위주,높음/컨디션 양호,7 +체력 증진,끝나면 기진맥진 고강도,"1,2",100000,실내,,높음/컨디션 양호,7 +체력 증진,끝나면 기진맥진 고강도,"1,2",50000,실내,,평범함,7 +취미 탐색,땀이 흐를 정도의 중강도,"7,1",100000,상관없음,,평범함,7 +체력 증진,끝나면 기진맥진 고강도,2,30000,실내,,매우 지침/피로함,7 +스트레스 해소,땀이 흐를 정도의 중강도,"8,9",100000,실외,,높음/컨디션 양호,7 +체력 증진,끝나면 기진맥진 고강도,1,30000,실내,가벼운 웜업 위주,평범함,7 +체력 증진,끝나면 기진맥진 고강도,"1,2",30000,실내,,평범함,7 +체력 증진,끝나면 기진맥진 고강도,"1,2",50000,상관없음,,평범함,7 +체력 증진,끝나면 기진맥진 고강도,2,50000,실내,,평범함,7 +체력 증진,가벼운 웜업 위주,4,70000,실외,,평범함,7 +체력 증진,끝나면 기진맥진 고강도,"1,2",30000,실내,,높음/컨디션 양호,7 +체력 증진,땀이 흐를 정도의 중강도,"5,3",10000,실내,끝나면 기진맥진 고강도,평범함,8 +체력 증진,땀이 흐를 정도의 중강도,"5,3",10000,상관없음,,평범함,8 +체력 증진,땀이 흐를 정도의 중강도,"5,3",30000,실내,끝나면 기진맥진 고강도,매우 지침/피로함,8 +체력 증진,땀이 흐를 정도의 중강도,5,50000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,8 +체력 증진,땀이 흐를 정도의 중강도,"5,3",30000,실외,끝나면 기진맥진 고강도,평범함,8 +"회복(통증완화, 재활 등)",끝나면 기진맥진 고강도,6,30000,상관없음,땀이 흐를 정도의 중강도,매우 지침/피로함,8 +체력 증진,땀이 흐를 정도의 중강도,5,50000,실내,끝나면 기진맥진 고강도,높음/컨디션 양호,8 +체력 증진,땀이 흐를 정도의 중강도,5,50000,실내,끝나면 기진맥진 고강도,매우 지침/피로함,8 +체력 증진,땀이 흐를 정도의 중강도,3,10000,실내,끝나면 기진맥진 고강도,평범함,8 +체력 증진,땀이 흐를 정도의 중강도,"5,3",50000,실내,끝나면 기진맥진 고강도,높음/컨디션 양호,8 +스트레스 해소,가벼운 웜업 위주,"6,7",10000,실내,,높음/컨디션 양호,8 +체력 증진,땀이 흐를 정도의 중강도,3,10000,실내,끝나면 기진맥진 고강도,높음/컨디션 양호,8 +체력 증진,땀이 흐를 정도의 중강도,5,30000,실내,끝나면 기진맥진 고강도,높음/컨디션 양호,8 +체력 증진,땀이 흐를 정도의 중강도,5,50000,실내,끝나면 기진맥진 고강도,높음/컨디션 양호,8 +체력 증진,땀이 흐를 정도의 중강도,"5,3",70000,실외,,평범함,8 +체력 증진,땀이 흐를 정도의 중강도,3,50000,실내,끝나면 기진맥진 고강도,평범함,8 +체력 증진,살짝 땀이 나는 정도,8,10000,실외,끝나면 기진맥진 고강도,높음/컨디션 양호,8 +체력 증진,땀이 흐를 정도의 중강도,5,50000,실내,끝나면 기진맥진 고강도,매우 지침/피로함,8 +체력 증진,땀이 흐를 정도의 중강도,"5,3",30000,상관없음,,높음/컨디션 양호,8 +체력 증진,땀이 흐를 정도의 중강도,5,70000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,8 +체력 증진,땀이 흐를 정도의 중강도,"5,3",70000,실내,끝나면 기진맥진 고강도,매우 지침/피로함,8 +체력 증진,땀이 흐를 정도의 중강도,3,50000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,8 +체력 증진,땀이 흐를 정도의 중강도,3,30000,실외,,평범함,8 +취미 탐색,가벼운 웜업 위주,"7,8",100000,상관없음,,평범함,8 +체력 증진,땀이 흐를 정도의 중강도,"5,3",30000,실내,,평범함,8 +체력 증진,땀이 흐를 정도의 중강도,5,30000,실내,끝나면 기진맥진 고강도,평범함,8 +체력 증진,땀이 흐를 정도의 중강도,3,50000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,8 +체력 증진,땀이 흐를 정도의 중강도,"5,3",50000,실내,,높음/컨디션 양호,8 +체력 증진,살짝 땀이 나는 정도,"6,9",30000,상관없음,,높음/컨디션 양호,8 +체력 증진,땀이 흐를 정도의 중강도,5,50000,실내,끝나면 기진맥진 고강도,높음/컨디션 양호,8 +체력 증진,땀이 흐를 정도의 중강도,"5,3",10000,실외,끝나면 기진맥진 고강도,평범함,8 +체력 증진,땀이 흐를 정도의 중강도,3,50000,상관없음,,평범함,8 +체력 증진,땀이 흐를 정도의 중강도,"5,3",50000,실내,,매우 지침/피로함,8 +체력 증진,땀이 흐를 정도의 중강도,"5,3",100000,실외,끝나면 기진맥진 고강도,높음/컨디션 양호,8 +취미 탐색,땀이 흐를 정도의 중강도,7,30000,실외,끝나면 기진맥진 고강도,매우 지침/피로함,9 +취미 탐색,땀이 흐를 정도의 중강도,7,30000,실외,끝나면 기진맥진 고강도,평범함,9 +취미 탐색,땀이 흐를 정도의 중강도,4,50000,상관없음,끝나면 기진맥진 고강도,평범함,9 +취미 탐색,끝나면 기진맥진 고강도,"8,5",100000,실내,가벼운 웜업 위주,평범함,9 +취미 탐색,땀이 흐를 정도의 중강도,4,50000,실외,끝나면 기진맥진 고강도,매우 지침/피로함,9 +취미 탐색,땀이 흐를 정도의 중강도,"7,4",50000,실외,,평범함,9 +취미 탐색,땀이 흐를 정도의 중강도,7,10000,상관없음,끝나면 기진맥진 고강도,평범함,9 +취미 탐색,땀이 흐를 정도의 중강도,7,30000,상관없음,끝나면 기진맥진 고강도,매우 지침/피로함,9 +취미 탐색,땀이 흐를 정도의 중강도,"7,4",50000,상관없음,,평범함,9 +취미 탐색,끝나면 기진맥진 고강도,"8,3",100000,상관없음,땀이 흐를 정도의 중강도,매우 지침/피로함,9 +취미 탐색,살짝 땀이 나는 정도,"2,8",50000,상관없음,,매우 지침/피로함,9 +취미 탐색,땀이 흐를 정도의 중강도,4,30000,상관없음,끝나면 기진맥진 고강도,평범함,9 +취미 탐색,땀이 흐를 정도의 중강도,"7,4",50000,상관없음,끝나면 기진맥진 고강도,평범함,9 +취미 탐색,땀이 흐를 정도의 중강도,4,100000,실내,끝나면 기진맥진 고강도,높음/컨디션 양호,9 +체력 증진,끝나면 기진맥진 고강도,9,50000,실내,,높음/컨디션 양호,9 +취미 탐색,땀이 흐를 정도의 중강도,7,30000,실내,끝나면 기진맥진 고강도,높음/컨디션 양호,9 +취미 탐색,땀이 흐를 정도의 중강도,"7,4",50000,상관없음,,평범함,9 +취미 탐색,땀이 흐를 정도의 중강도,7,30000,상관없음,끝나면 기진맥진 고강도,평범함,9 +취미 탐색,땀이 흐를 정도의 중강도,"7,4",30000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,9 +취미 탐색,땀이 흐를 정도의 중강도,4,50000,상관없음,,높음/컨디션 양호,9 +취미 탐색,땀이 흐를 정도의 중강도,7,30000,상관없음,끝나면 기진맥진 고강도,평범함,9 +취미 탐색,땀이 흐를 정도의 중강도,"7,4",30000,상관없음,끝나면 기진맥진 고강도,매우 지침/피로함,9 +취미 탐색,땀이 흐를 정도의 중강도,"7,4",30000,상관없음,,매우 지침/피로함,9 +취미 탐색,땀이 흐를 정도의 중강도,"7,4",30000,상관없음,,매우 지침/피로함,9 +취미 탐색,땀이 흐를 정도의 중강도,4,30000,상관없음,끝나면 기진맥진 고강도,평범함,9 +취미 탐색,땀이 흐를 정도의 중강도,"7,4",50000,상관없음,,평범함,9 +취미 탐색,땀이 흐를 정도의 중강도,"7,4",50000,상관없음,,평범함,9 +취미 탐색,땀이 흐를 정도의 중강도,4,30000,실외,,평범함,9 +취미 탐색,살짝 땀이 나는 정도,5,10000,실외,"가벼운 웜업 위주,끝나면 기진맥진 고강도",매우 지침/피로함,9 +취미 탐색,땀이 흐를 정도의 중강도,7,70000,실외,끝나면 기진맥진 고강도,높음/컨디션 양호,9 +취미 탐색,땀이 흐를 정도의 중강도,4,30000,상관없음,끝나면 기진맥진 고강도,평범함,9 +취미 탐색,땀이 흐를 정도의 중강도,7,50000,실외,,평범함,9 +취미 탐색,땀이 흐를 정도의 중강도,7,30000,실내,끝나면 기진맥진 고강도,매우 지침/피로함,9 +취미 탐색,땀이 흐를 정도의 중강도,7,30000,상관없음,끝나면 기진맥진 고강도,평범함,9 +체력 증진,땀이 흐를 정도의 중강도,"8,1",30000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,10 +체력 증진,땀이 흐를 정도의 중강도,8,50000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,10 +체력 증진,땀이 흐를 정도의 중강도,8,50000,상관없음,끝나면 기진맥진 고강도,매우 지침/피로함,10 +체력 증진,땀이 흐를 정도의 중강도,8,10000,상관없음,,높음/컨디션 양호,10 +체력 증진,땀이 흐를 정도의 중강도,"8,1",50000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,10 +체력 증진,땀이 흐를 정도의 중강도,"8,1",50000,상관없음,끝나면 기진맥진 고강도,평범함,10 +다이어트,끝나면 기진맥진 고강도,"9,1",70000,실외,가벼운 웜업 위주,높음/컨디션 양호,10 +체력 증진,땀이 흐를 정도의 중강도,1,50000,실외,끝나면 기진맥진 고강도,평범함,10 +체력 증진,땀이 흐를 정도의 중강도,8,100000,상관없음,끝나면 기진맥진 고강도,평범함,10 +체력 증진,땀이 흐를 정도의 중강도,8,10000,실내,,높음/컨디션 양호,10 +체력 증진,땀이 흐를 정도의 중강도,8,30000,상관없음,,높음/컨디션 양호,10 +체력 증진,땀이 흐를 정도의 중강도,1,50000,실내,,높음/컨디션 양호,10 +체력 증진,땀이 흐를 정도의 중강도,1,30000,실내,,높음/컨디션 양호,10 +체력 증진,땀이 흐를 정도의 중강도,1,70000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,10 +체력 증진,땀이 흐를 정도의 중강도,"8,1",30000,상관없음,끝나면 기진맥진 고강도,평범함,10 +체력 증진,땀이 흐를 정도의 중강도,1,30000,실내,끝나면 기진맥진 고강도,높음/컨디션 양호,10 +스트레스 해소,가벼운 웜업 위주,3,10000,실외,,매우 지침/피로함,10 +체력 증진,땀이 흐를 정도의 중강도,8,50000,실내,끝나면 기진맥진 고강도,평범함,10 +체력 증진,땀이 흐를 정도의 중강도,8,30000,상관없음,끝나면 기진맥진 고강도,평범함,10 +체력 증진,땀이 흐를 정도의 중강도,"8,1",50000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,10 +체력 증진,땀이 흐를 정도의 중강도,"8,1",10000,상관없음,끝나면 기진맥진 고강도,평범함,10 +체력 증진,땀이 흐를 정도의 중강도,"8,1",50000,실내,끝나면 기진맥진 고강도,평범함,10 +체력 증진,땀이 흐를 정도의 중강도,8,50000,상관없음,끝나면 기진맥진 고강도,매우 지침/피로함,10 +체력 증진,땀이 흐를 정도의 중강도,"8,1",50000,상관없음,끝나면 기진맥진 고강도,평범함,10 +체력 증진,땀이 흐를 정도의 중강도,8,30000,실내,,평범함,10 +체력 증진,땀이 흐를 정도의 중강도,"8,1",50000,실내,끝나면 기진맥진 고강도,높음/컨디션 양호,10 +체력 증진,땀이 흐를 정도의 중강도,1,50000,실외,끝나면 기진맥진 고강도,매우 지침/피로함,10 +체력 증진,가벼운 웜업 위주,"6,7",10000,실내,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",매우 지침/피로함,10 +체력 증진,땀이 흐를 정도의 중강도,1,10000,상관없음,,매우 지침/피로함,10 +체력 증진,땀이 흐를 정도의 중강도,"8,1",70000,실외,끝나면 기진맥진 고강도,평범함,10 +체력 증진,땀이 흐를 정도의 중강도,8,50000,상관없음,끝나면 기진맥진 고강도,매우 지침/피로함,10 +체력 증진,땀이 흐를 정도의 중강도,8,50000,상관없음,끝나면 기진맥진 고강도,매우 지침/피로함,10 +체력 증진,땀이 흐를 정도의 중강도,8,50000,상관없음,끝나면 기진맥진 고강도,평범함,10 +체력 증진,땀이 흐를 정도의 중강도,1,30000,실내,,평범함,10 +취미 탐색,가벼운 웜업 위주,"9,3",50000,상관없음,땀이 흐를 정도의 중강도,평범함,11 +취미 탐색,살짝 땀이 나는 정도,9,100000,실내,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",매우 지침/피로함,11 +취미 탐색,살짝 땀이 나는 정도,"9,3",30000,실외,끝나면 기진맥진 고강도,높음/컨디션 양호,11 +취미 탐색,가벼운 웜업 위주,3,30000,상관없음,땀이 흐를 정도의 중강도,평범함,11 +취미 탐색,가벼운 웜업 위주,9,10000,실내,땀이 흐를 정도의 중강도,높음/컨디션 양호,11 +취미 탐색,끝나면 기진맥진 고강도,"8,5",100000,실내,,매우 지침/피로함,11 +취미 탐색,가벼운 웜업 위주,3,30000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,11 +취미 탐색,가벼운 웜업 위주,9,50000,상관없음,땀이 흐를 정도의 중강도,평범함,11 +취미 탐색,살짝 땀이 나는 정도,9,30000,실내,땀이 흐를 정도의 중강도,높음/컨디션 양호,11 +취미 탐색,살짝 땀이 나는 정도,9,50000,상관없음,끝나면 기진맥진 고강도,평범함,11 +취미 탐색,살짝 땀이 나는 정도,"9,3",50000,상관없음,끝나면 기진맥진 고강도,평범함,11 +취미 탐색,살짝 땀이 나는 정도,"9,3",30000,상관없음,땀이 흐를 정도의 중강도,높음/컨디션 양호,11 +취미 탐색,살짝 땀이 나는 정도,3,50000,상관없음,끝나면 기진맥진 고강도,평범함,11 +취미 탐색,살짝 땀이 나는 정도,3,30000,상관없음,끝나면 기진맥진 고강도,평범함,11 +"회복(통증완화, 재활 등)",끝나면 기진맥진 고강도,"9,8",100000,실외,,평범함,11 +취미 탐색,가벼운 웜업 위주,2,30000,실내,,매우 지침/피로함,11 +"회복(통증완화, 재활 등)",땀이 흐를 정도의 중강도,2,70000,상관없음,,평범함,11 +취미 탐색,가벼운 웜업 위주,"9,3",50000,상관없음,땀이 흐를 정도의 중강도,평범함,11 +취미 탐색,가벼운 웜업 위주,"9,3",30000,상관없음,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",평범함,11 +스트레스 해소,가벼운 웜업 위주,"6,8",70000,상관없음,,높음/컨디션 양호,11 +취미 탐색,살짝 땀이 나는 정도,"9,3",50000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,11 +"회복(통증완화, 재활 등)",땀이 흐를 정도의 중강도,"6,5",10000,실내,,평범함,11 +취미 탐색,살짝 땀이 나는 정도,"9,3",30000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,11 +취미 탐색,가벼운 웜업 위주,9,30000,상관없음,땀이 흐를 정도의 중강도,평범함,11 +취미 탐색,살짝 땀이 나는 정도,3,30000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,11 +취미 탐색,가벼운 웜업 위주,"9,3",50000,상관없음,끝나면 기진맥진 고강도,평범함,11 +취미 탐색,가벼운 웜업 위주,3,30000,상관없음,땀이 흐를 정도의 중강도,평범함,11 +취미 탐색,살짝 땀이 나는 정도,9,30000,상관없음,땀이 흐를 정도의 중강도,평범함,11 +취미 탐색,살짝 땀이 나는 정도,"9,3",50000,실내,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",평범함,11 +취미 탐색,가벼운 웜업 위주,3,30000,실외,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",평범함,11 +취미 탐색,살짝 땀이 나는 정도,9,50000,상관없음,땀이 흐를 정도의 중강도,평범함,11 +취미 탐색,살짝 땀이 나는 정도,9,100000,실내,땀이 흐를 정도의 중강도,평범함,11 +취미 탐색,살짝 땀이 나는 정도,"9,3",50000,실내,끝나면 기진맥진 고강도,평범함,11 +스트레스 해소,땀이 흐를 정도의 중강도,"2,4",50000,실내,,평범함,12 +체력 증진,살짝 땀이 나는 정도,3,10000,상관없음,,높음/컨디션 양호,12 +스트레스 해소,땀이 흐를 정도의 중강도,4,30000,실내,끝나면 기진맥진 고강도,높음/컨디션 양호,12 +스트레스 해소,땀이 흐를 정도의 중강도,"2,4",50000,상관없음,,평범함,12 +스트레스 해소,땀이 흐를 정도의 중강도,"2,4",30000,상관없음,,평범함,12 +스트레스 해소,땀이 흐를 정도의 중강도,"2,4",50000,실내,,높음/컨디션 양호,12 +스트레스 해소,땀이 흐를 정도의 중강도,4,70000,실내,끝나면 기진맥진 고강도,높음/컨디션 양호,12 +스트레스 해소,땀이 흐를 정도의 중강도,2,30000,실내,끝나면 기진맥진 고강도,높음/컨디션 양호,12 +스트레스 해소,땀이 흐를 정도의 중강도,2,30000,실내,,평범함,12 +스트레스 해소,땀이 흐를 정도의 중강도,"2,4",30000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,12 +스트레스 해소,살짝 땀이 나는 정도,"8,3",70000,실외,,평범함,12 +스트레스 해소,땀이 흐를 정도의 중강도,2,50000,실내,끝나면 기진맥진 고강도,평범함,12 +스트레스 해소,땀이 흐를 정도의 중강도,2,50000,실내,,높음/컨디션 양호,12 +스트레스 해소,끝나면 기진맥진 고강도,"3,9",10000,실내,,높음/컨디션 양호,12 +스트레스 해소,땀이 흐를 정도의 중강도,4,100000,실내,끝나면 기진맥진 고강도,높음/컨디션 양호,12 +스트레스 해소,땀이 흐를 정도의 중강도,"2,4",30000,실내,끝나면 기진맥진 고강도,높음/컨디션 양호,12 +스트레스 해소,땀이 흐를 정도의 중강도,2,30000,실내,끝나면 기진맥진 고강도,높음/컨디션 양호,12 +스트레스 해소,땀이 흐를 정도의 중강도,2,30000,상관없음,,높음/컨디션 양호,12 +스트레스 해소,땀이 흐를 정도의 중강도,"2,4",30000,실내,,높음/컨디션 양호,12 +스트레스 해소,땀이 흐를 정도의 중강도,2,50000,실내,,평범함,12 +스트레스 해소,땀이 흐를 정도의 중강도,2,100000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,12 +스트레스 해소,땀이 흐를 정도의 중강도,2,30000,상관없음,,높음/컨디션 양호,12 +스트레스 해소,땀이 흐를 정도의 중강도,2,50000,실내,,높음/컨디션 양호,12 +스트레스 해소,땀이 흐를 정도의 중강도,2,30000,실내,끝나면 기진맥진 고강도,평범함,12 +다이어트,땀이 흐를 정도의 중강도,"6,5",70000,실외,"가벼운 웜업 위주,끝나면 기진맥진 고강도",높음/컨디션 양호,12 +스트레스 해소,땀이 흐를 정도의 중강도,"2,4",10000,실내,,평범함,12 +"회복(통증완화, 재활 등)",끝나면 기진맥진 고강도,9,70000,실내,가벼운 웜업 위주,평범함,12 +스트레스 해소,땀이 흐를 정도의 중강도,2,50000,상관없음,끝나면 기진맥진 고강도,평범함,12 +스트레스 해소,땀이 흐를 정도의 중강도,"2,4",50000,실내,끝나면 기진맥진 고강도,평범함,12 +다이어트,살짝 땀이 나는 정도,"6,3",10000,실외,,평범함,12 +스트레스 해소,땀이 흐를 정도의 중강도,4,30000,실내,끝나면 기진맥진 고강도,평범함,12 +스트레스 해소,땀이 흐를 정도의 중강도,"2,4",30000,상관없음,,높음/컨디션 양호,12 +스트레스 해소,땀이 흐를 정도의 중강도,2,100000,실외,,평범함,12 +다이어트,끝나면 기진맥진 고강도,"5,6",50000,실내,가벼운 웜업 위주,높음/컨디션 양호,13 +취미 탐색,땀이 흐를 정도의 중강도,"9,8",70000,상관없음,끝나면 기진맥진 고강도,평범함,13 +다이어트,끝나면 기진맥진 고강도,5,50000,실내,,높음/컨디션 양호,13 +다이어트,끝나면 기진맥진 고강도,"5,6",50000,실내,가벼운 웜업 위주,높음/컨디션 양호,13 +다이어트,끝나면 기진맥진 고강도,5,70000,상관없음,가벼운 웜업 위주,평범함,13 +다이어트,끝나면 기진맥진 고강도,5,50000,실외,,평범함,13 +다이어트,끝나면 기진맥진 고강도,"5,6",30000,실내,,매우 지침/피로함,13 +다이어트,끝나면 기진맥진 고강도,6,30000,실내,,평범함,13 +체력 증진,살짝 땀이 나는 정도,8,100000,상관없음,"끝나면 기진맥진 고강도,가벼운 웜업 위주",매우 지침/피로함,13 +다이어트,끝나면 기진맥진 고강도,"5,6",30000,실내,,평범함,13 +다이어트,끝나면 기진맥진 고강도,"5,6",50000,상관없음,,평범함,13 +"회복(통증완화, 재활 등)",땀이 흐를 정도의 중강도,"9,2",10000,실내,끝나면 기진맥진 고강도,매우 지침/피로함,13 +다이어트,끝나면 기진맥진 고강도,5,100000,실내,,높음/컨디션 양호,13 +다이어트,끝나면 기진맥진 고강도,6,50000,상관없음,,높음/컨디션 양호,13 +다이어트,끝나면 기진맥진 고강도,"5,6",70000,실내,가벼운 웜업 위주,평범함,13 +다이어트,끝나면 기진맥진 고강도,5,30000,실내,,높음/컨디션 양호,13 +다이어트,끝나면 기진맥진 고강도,5,50000,실내,,높음/컨디션 양호,13 +다이어트,끝나면 기진맥진 고강도,"5,6",50000,실내,,평범함,13 +다이어트,끝나면 기진맥진 고강도,"5,6",50000,상관없음,가벼운 웜업 위주,평범함,13 +다이어트,끝나면 기진맥진 고강도,"5,6",30000,실내,,평범함,13 +다이어트,끝나면 기진맥진 고강도,6,30000,실내,가벼운 웜업 위주,평범함,13 +다이어트,끝나면 기진맥진 고강도,6,10000,실내,가벼운 웜업 위주,평범함,13 +다이어트,끝나면 기진맥진 고강도,"5,6",30000,실내,가벼운 웜업 위주,평범함,13 +다이어트,끝나면 기진맥진 고강도,"5,6",50000,상관없음,가벼운 웜업 위주,높음/컨디션 양호,13 +다이어트,땀이 흐를 정도의 중강도,4,100000,실내,,매우 지침/피로함,13 +"회복(통증완화, 재활 등)",가벼운 웜업 위주,2,70000,실내,"끝나면 기진맥진 고강도,땀이 흐를 정도의 중강도",높음/컨디션 양호,13 +체력 증진,가벼운 웜업 위주,"1,9",70000,상관없음,,매우 지침/피로함,13 +취미 탐색,가벼운 웜업 위주,"1,8",100000,상관없음,,매우 지침/피로함,13 +다이어트,끝나면 기진맥진 고강도,6,50000,실외,,평범함,13 +다이어트,끝나면 기진맥진 고강도,"5,6",30000,상관없음,,평범함,13 +다이어트,끝나면 기진맥진 고강도,"5,6",30000,실내,가벼운 웜업 위주,평범함,13 +체력 증진,가벼운 웜업 위주,4,10000,실외,,높음/컨디션 양호,13 +취미 탐색,살짝 땀이 나는 정도,"9,3",100000,상관없음,"끝나면 기진맥진 고강도,땀이 흐를 정도의 중강도",높음/컨디션 양호,13 +체력 증진,땀이 흐를 정도의 중강도,7,30000,실외,끝나면 기진맥진 고강도,평범함,14 +체력 증진,땀이 흐를 정도의 중강도,"7,1",50000,실외,끝나면 기진맥진 고강도,높음/컨디션 양호,14 +체력 증진,땀이 흐를 정도의 중강도,1,30000,실외,끝나면 기진맥진 고강도,평범함,14 +스트레스 해소,살짝 땀이 나는 정도,8,70000,실내,땀이 흐를 정도의 중강도,평범함,14 +체력 증진,땀이 흐를 정도의 중강도,"7,1",30000,상관없음,,높음/컨디션 양호,14 +체력 증진,살짝 땀이 나는 정도,"2,9",10000,상관없음,,높음/컨디션 양호,14 +체력 증진,땀이 흐를 정도의 중강도,"7,1",30000,실외,끝나면 기진맥진 고강도,평범함,14 +체력 증진,땀이 흐를 정도의 중강도,"7,1",30000,실외,,높음/컨디션 양호,14 +체력 증진,땀이 흐를 정도의 중강도,7,30000,상관없음,,매우 지침/피로함,14 +체력 증진,땀이 흐를 정도의 중강도,"7,1",30000,상관없음,끝나면 기진맥진 고강도,매우 지침/피로함,14 +체력 증진,땀이 흐를 정도의 중강도,"7,1",70000,상관없음,끝나면 기진맥진 고강도,평범함,14 +체력 증진,땀이 흐를 정도의 중강도,7,30000,실외,,평범함,14 +체력 증진,땀이 흐를 정도의 중강도,"7,1",10000,상관없음,끝나면 기진맥진 고강도,평범함,14 +체력 증진,땀이 흐를 정도의 중강도,1,30000,실내,,평범함,14 +체력 증진,땀이 흐를 정도의 중강도,"7,1",50000,실내,,높음/컨디션 양호,14 +체력 증진,땀이 흐를 정도의 중강도,1,50000,실외,끝나면 기진맥진 고강도,높음/컨디션 양호,14 +체력 증진,땀이 흐를 정도의 중강도,1,30000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,14 +체력 증진,땀이 흐를 정도의 중강도,1,70000,실내,끝나면 기진맥진 고강도,매우 지침/피로함,14 +체력 증진,땀이 흐를 정도의 중강도,"7,1",30000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,14 +체력 증진,땀이 흐를 정도의 중강도,7,50000,상관없음,끝나면 기진맥진 고강도,평범함,14 +체력 증진,땀이 흐를 정도의 중강도,7,50000,상관없음,끝나면 기진맥진 고강도,매우 지침/피로함,14 +체력 증진,살짝 땀이 나는 정도,"3,6",100000,실외,,높음/컨디션 양호,14 +체력 증진,땀이 흐를 정도의 중강도,7,50000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,14 +체력 증진,땀이 흐를 정도의 중강도,"7,1",30000,실외,끝나면 기진맥진 고강도,평범함,14 +다이어트,가벼운 웜업 위주,"6,9",10000,상관없음,땀이 흐를 정도의 중강도,매우 지침/피로함,14 +체력 증진,땀이 흐를 정도의 중강도,1,70000,상관없음,끝나면 기진맥진 고강도,매우 지침/피로함,14 +체력 증진,땀이 흐를 정도의 중강도,1,10000,상관없음,끝나면 기진맥진 고강도,평범함,14 +체력 증진,땀이 흐를 정도의 중강도,7,30000,실외,끝나면 기진맥진 고강도,높음/컨디션 양호,14 +체력 증진,땀이 흐를 정도의 중강도,1,50000,실외,끝나면 기진맥진 고강도,평범함,14 +체력 증진,땀이 흐를 정도의 중강도,1,70000,상관없음,끝나면 기진맥진 고강도,평범함,14 +체력 증진,땀이 흐를 정도의 중강도,1,50000,실외,끝나면 기진맥진 고강도,평범함,14 +체력 증진,땀이 흐를 정도의 중강도,"7,1",30000,실내,,높음/컨디션 양호,14 +체력 증진,땀이 흐를 정도의 중강도,1,100000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,14 +다이어트,끝나면 기진맥진 고강도,"2,5",100000,실내,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",높음/컨디션 양호,15 +체력 증진,끝나면 기진맥진 고강도,"7,6",100000,상관없음,가벼운 웜업 위주,높음/컨디션 양호,15 +다이어트,땀이 흐를 정도의 중강도,8,10000,상관없음,,평범함,15 +다이어트,땀이 흐를 정도의 중강도,8,30000,상관없음,끝나면 기진맥진 고강도,평범함,15 +스트레스 해소,살짝 땀이 나는 정도,"4,6",70000,상관없음,,매우 지침/피로함,15 +다이어트,땀이 흐를 정도의 중강도,8,50000,실외,,매우 지침/피로함,15 +다이어트,땀이 흐를 정도의 중강도,5,30000,상관없음,,평범함,15 +다이어트,땀이 흐를 정도의 중강도,"8,5",70000,상관없음,끝나면 기진맥진 고강도,평범함,15 +다이어트,땀이 흐를 정도의 중강도,"8,5",30000,상관없음,끝나면 기진맥진 고강도,평범함,15 +다이어트,땀이 흐를 정도의 중강도,5,30000,실내,,평범함,15 +다이어트,땀이 흐를 정도의 중강도,"8,5",50000,상관없음,,높음/컨디션 양호,15 +다이어트,땀이 흐를 정도의 중강도,"8,5",30000,실내,,높음/컨디션 양호,15 +다이어트,땀이 흐를 정도의 중강도,"8,5",30000,상관없음,,평범함,15 +다이어트,땀이 흐를 정도의 중강도,8,50000,실외,끝나면 기진맥진 고강도,평범함,15 +다이어트,살짝 땀이 나는 정도,5,100000,실외,,높음/컨디션 양호,15 +다이어트,땀이 흐를 정도의 중강도,5,30000,실외,끝나면 기진맥진 고강도,높음/컨디션 양호,15 +다이어트,땀이 흐를 정도의 중강도,5,30000,상관없음,,평범함,15 +다이어트,땀이 흐를 정도의 중강도,5,50000,상관없음,끝나면 기진맥진 고강도,매우 지침/피로함,15 +다이어트,땀이 흐를 정도의 중강도,5,50000,상관없음,,높음/컨디션 양호,15 +다이어트,땀이 흐를 정도의 중강도,8,50000,상관없음,끝나면 기진맥진 고강도,평범함,15 +다이어트,땀이 흐를 정도의 중강도,"8,5",70000,상관없음,끝나면 기진맥진 고강도,평범함,15 +다이어트,땀이 흐를 정도의 중강도,"8,5",50000,실내,끝나면 기진맥진 고강도,평범함,15 +"회복(통증완화, 재활 등)",살짝 땀이 나는 정도,4,70000,실외,,높음/컨디션 양호,15 +다이어트,땀이 흐를 정도의 중강도,"8,5",50000,상관없음,끝나면 기진맥진 고강도,매우 지침/피로함,15 +다이어트,땀이 흐를 정도의 중강도,8,50000,상관없음,끝나면 기진맥진 고강도,평범함,15 +다이어트,땀이 흐를 정도의 중강도,"8,5",70000,실외,끝나면 기진맥진 고강도,높음/컨디션 양호,15 +스트레스 해소,끝나면 기진맥진 고강도,9,70000,실내,,높음/컨디션 양호,15 +다이어트,땀이 흐를 정도의 중강도,5,50000,상관없음,,평범함,15 +체력 증진,가벼운 웜업 위주,"3,7",70000,실내,,평범함,15 +다이어트,땀이 흐를 정도의 중강도,5,30000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,15 +다이어트,땀이 흐를 정도의 중강도,"8,5",30000,상관없음,,평범함,15 +다이어트,땀이 흐를 정도의 중강도,8,50000,상관없음,,평범함,15 +스트레스 해소,끝나면 기진맥진 고강도,6,70000,상관없음,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",평범함,15 +체력 증진,가벼운 웜업 위주,"9,1",50000,실내,끝나면 기진맥진 고강도,높음/컨디션 양호,16 +체력 증진,가벼운 웜업 위주,1,50000,실내,끝나면 기진맥진 고강도,평범함,16 +체력 증진,가벼운 웜업 위주,"9,1",30000,실내,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",평범함,16 +체력 증진,가벼운 웜업 위주,"9,1",50000,상관없음,끝나면 기진맥진 고강도,평범함,16 +체력 증진,살짝 땀이 나는 정도,1,30000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,16 +체력 증진,살짝 땀이 나는 정도,1,30000,상관없음,끝나면 기진맥진 고강도,평범함,16 +체력 증진,가벼운 웜업 위주,9,10000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,16 +체력 증진,살짝 땀이 나는 정도,1,70000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,16 +체력 증진,살짝 땀이 나는 정도,1,50000,실내,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",높음/컨디션 양호,16 +체력 증진,끝나면 기진맥진 고강도,3,70000,실내,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",높음/컨디션 양호,16 +체력 증진,끝나면 기진맥진 고강도,"7,4",10000,상관없음,,매우 지침/피로함,16 +체력 증진,살짝 땀이 나는 정도,9,100000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,16 +체력 증진,가벼운 웜업 위주,1,30000,상관없음,땀이 흐를 정도의 중강도,높음/컨디션 양호,16 +체력 증진,가벼운 웜업 위주,"9,1",30000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,16 +체력 증진,가벼운 웜업 위주,9,50000,실내,땀이 흐를 정도의 중강도,높음/컨디션 양호,16 +체력 증진,살짝 땀이 나는 정도,"9,1",30000,실내,땀이 흐를 정도의 중강도,평범함,16 +체력 증진,살짝 땀이 나는 정도,9,50000,실내,끝나면 기진맥진 고강도,매우 지침/피로함,16 +다이어트,끝나면 기진맥진 고강도,"4,5",10000,상관없음,,매우 지침/피로함,16 +스트레스 해소,땀이 흐를 정도의 중강도,6,100000,실외,땀이 흐를 정도의 중강도,평범함,16 +체력 증진,살짝 땀이 나는 정도,9,30000,상관없음,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",높음/컨디션 양호,16 +체력 증진,가벼운 웜업 위주,1,50000,상관없음,끝나면 기진맥진 고강도,매우 지침/피로함,16 +체력 증진,가벼운 웜업 위주,"9,1",50000,상관없음,땀이 흐를 정도의 중강도,높음/컨디션 양호,16 +체력 증진,살짝 땀이 나는 정도,"9,1",10000,실외,끝나면 기진맥진 고강도,높음/컨디션 양호,16 +체력 증진,살짝 땀이 나는 정도,9,10000,상관없음,땀이 흐를 정도의 중강도,평범함,16 +체력 증진,가벼운 웜업 위주,9,30000,실외,땀이 흐를 정도의 중강도,평범함,16 +체력 증진,살짝 땀이 나는 정도,"9,1",70000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,16 +체력 증진,살짝 땀이 나는 정도,"9,1",50000,상관없음,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",평범함,16 +스트레스 해소,끝나면 기진맥진 고강도,"4,9",70000,상관없음,,평범함,16 +체력 증진,살짝 땀이 나는 정도,9,100000,상관없음,땀이 흐를 정도의 중강도,높음/컨디션 양호,16 +체력 증진,가벼운 웜업 위주,1,50000,실외,땀이 흐를 정도의 중강도,매우 지침/피로함,16 +다이어트,땀이 흐를 정도의 중강도,"6,3",70000,실내,가벼운 웜업 위주,높음/컨디션 양호,16 +체력 증진,살짝 땀이 나는 정도,"9,1",30000,실외,땀이 흐를 정도의 중강도,높음/컨디션 양호,16 +체력 증진,살짝 땀이 나는 정도,9,50000,상관없음,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",평범함,16 +스트레스 해소,가벼운 웜업 위주,"6,3",30000,실내,끝나면 기진맥진 고강도,평범함,17 +스트레스 해소,가벼운 웜업 위주,6,50000,상관없음,땀이 흐를 정도의 중강도,평범함,17 +스트레스 해소,가벼운 웜업 위주,3,50000,실내,땀이 흐를 정도의 중강도,높음/컨디션 양호,17 +스트레스 해소,살짝 땀이 나는 정도,3,30000,실내,땀이 흐를 정도의 중강도,높음/컨디션 양호,17 +스트레스 해소,가벼운 웜업 위주,6,50000,실내,끝나면 기진맥진 고강도,평범함,17 +스트레스 해소,살짝 땀이 나는 정도,3,50000,실외,땀이 흐를 정도의 중강도,높음/컨디션 양호,17 +스트레스 해소,살짝 땀이 나는 정도,"6,3",70000,실외,끝나면 기진맥진 고강도,높음/컨디션 양호,17 +체력 증진,살짝 땀이 나는 정도,2,100000,실내,,평범함,17 +스트레스 해소,가벼운 웜업 위주,"6,3",50000,상관없음,땀이 흐를 정도의 중강도,높음/컨디션 양호,17 +스트레스 해소,가벼운 웜업 위주,6,30000,실내,끝나면 기진맥진 고강도,평범함,17 +스트레스 해소,살짝 땀이 나는 정도,"6,3",50000,실내,땀이 흐를 정도의 중강도,높음/컨디션 양호,17 +취미 탐색,끝나면 기진맥진 고강도,4,70000,상관없음,,매우 지침/피로함,17 +스트레스 해소,살짝 땀이 나는 정도,6,50000,실내,땀이 흐를 정도의 중강도,높음/컨디션 양호,17 +스트레스 해소,살짝 땀이 나는 정도,3,50000,실내,땀이 흐를 정도의 중강도,평범함,17 +스트레스 해소,땀이 흐를 정도의 중강도,"5,8",70000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,17 +스트레스 해소,가벼운 웜업 위주,3,100000,실내,땀이 흐를 정도의 중강도,높음/컨디션 양호,17 +스트레스 해소,가벼운 웜업 위주,6,50000,상관없음,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",평범함,17 +스트레스 해소,살짝 땀이 나는 정도,8,10000,상관없음,,평범함,17 +취미 탐색,끝나면 기진맥진 고강도,8,10000,실내,,매우 지침/피로함,17 +스트레스 해소,살짝 땀이 나는 정도,"6,3",30000,실내,땀이 흐를 정도의 중강도,높음/컨디션 양호,17 +스트레스 해소,살짝 땀이 나는 정도,3,50000,실내,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",평범함,17 +스트레스 해소,가벼운 웜업 위주,6,30000,실내,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",높음/컨디션 양호,17 +스트레스 해소,가벼운 웜업 위주,"6,3",50000,실외,땀이 흐를 정도의 중강도,높음/컨디션 양호,17 +스트레스 해소,끝나면 기진맥진 고강도,9,70000,실외,"끝나면 기진맥진 고강도,가벼운 웜업 위주",높음/컨디션 양호,17 +스트레스 해소,살짝 땀이 나는 정도,"6,3",30000,상관없음,땀이 흐를 정도의 중강도,높음/컨디션 양호,17 +스트레스 해소,가벼운 웜업 위주,3,50000,실내,땀이 흐를 정도의 중강도,평범함,17 +스트레스 해소,가벼운 웜업 위주,"6,3",70000,상관없음,끝나면 기진맥진 고강도,평범함,17 +스트레스 해소,가벼운 웜업 위주,3,30000,상관없음,땀이 흐를 정도의 중강도,높음/컨디션 양호,17 +체력 증진,끝나면 기진맥진 고강도,"7,8",70000,상관없음,끝나면 기진맥진 고강도,평범함,17 +스트레스 해소,가벼운 웜업 위주,"6,3",30000,실내,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",높음/컨디션 양호,17 +스트레스 해소,가벼운 웜업 위주,3,70000,실내,땀이 흐를 정도의 중강도,높음/컨디션 양호,17 +스트레스 해소,땀이 흐를 정도의 중강도,"5,7",70000,실내,,높음/컨디션 양호,17 +스트레스 해소,살짝 땀이 나는 정도,6,50000,실외,땀이 흐를 정도의 중강도,높음/컨디션 양호,17 +체력 증진,끝나면 기진맥진 고강도,5,30000,실외,가벼운 웜업 위주,높음/컨디션 양호,18 +체력 증진,끝나면 기진맥진 고강도,2,30000,실내,가벼운 웜업 위주,매우 지침/피로함,18 +체력 증진,끝나면 기진맥진 고강도,"2,5",30000,상관없음,가벼운 웜업 위주,평범함,18 +스트레스 해소,땀이 흐를 정도의 중강도,"1,6",70000,실내,,평범함,18 +체력 증진,끝나면 기진맥진 고강도,2,30000,실내,,평범함,18 +스트레스 해소,살짝 땀이 나는 정도,7,50000,상관없음,끝나면 기진맥진 고강도,평범함,18 +체력 증진,끝나면 기진맥진 고강도,2,30000,실내,,평범함,18 +체력 증진,끝나면 기진맥진 고강도,2,70000,실내,,평범함,18 +체력 증진,끝나면 기진맥진 고강도,"2,5",30000,실내,,높음/컨디션 양호,18 +체력 증진,끝나면 기진맥진 고강도,5,10000,실내,,매우 지침/피로함,18 +체력 증진,끝나면 기진맥진 고강도,2,50000,상관없음,가벼운 웜업 위주,높음/컨디션 양호,18 +체력 증진,끝나면 기진맥진 고강도,5,100000,실내,,평범함,18 +스트레스 해소,땀이 흐를 정도의 중강도,"7,8",50000,실외,,매우 지침/피로함,18 +체력 증진,끝나면 기진맥진 고강도,5,30000,실외,가벼운 웜업 위주,높음/컨디션 양호,18 +체력 증진,끝나면 기진맥진 고강도,"2,5",50000,실내,,높음/컨디션 양호,18 +체력 증진,끝나면 기진맥진 고강도,5,50000,실내,,높음/컨디션 양호,18 +체력 증진,끝나면 기진맥진 고강도,"2,5",10000,상관없음,,평범함,18 +스트레스 해소,땀이 흐를 정도의 중강도,"9,1",10000,실내,끝나면 기진맥진 고강도,높음/컨디션 양호,18 +체력 증진,끝나면 기진맥진 고강도,2,30000,실내,,높음/컨디션 양호,18 +체력 증진,끝나면 기진맥진 고강도,"2,5",10000,실외,가벼운 웜업 위주,높음/컨디션 양호,18 +체력 증진,끝나면 기진맥진 고강도,5,50000,실내,,높음/컨디션 양호,18 +체력 증진,끝나면 기진맥진 고강도,2,50000,상관없음,,높음/컨디션 양호,18 +체력 증진,살짝 땀이 나는 정도,"4,7",10000,실외,땀이 흐를 정도의 중강도,매우 지침/피로함,18 +체력 증진,끝나면 기진맥진 고강도,5,50000,실내,가벼운 웜업 위주,평범함,18 +체력 증진,끝나면 기진맥진 고강도,"2,5",100000,실내,,평범함,18 +체력 증진,끝나면 기진맥진 고강도,5,30000,실내,가벼운 웜업 위주,매우 지침/피로함,18 +체력 증진,끝나면 기진맥진 고강도,2,50000,실내,,높음/컨디션 양호,18 +체력 증진,끝나면 기진맥진 고강도,5,30000,실내,가벼운 웜업 위주,매우 지침/피로함,18 +체력 증진,끝나면 기진맥진 고강도,5,10000,실내,,높음/컨디션 양호,18 +체력 증진,끝나면 기진맥진 고강도,5,30000,실외,,높음/컨디션 양호,18 +스트레스 해소,살짝 땀이 나는 정도,7,100000,상관없음,끝나면 기진맥진 고강도,매우 지침/피로함,18 +체력 증진,끝나면 기진맥진 고강도,"2,5",10000,실외,,평범함,18 +체력 증진,끝나면 기진맥진 고강도,"2,5",50000,실내,,매우 지침/피로함,18 +"회복(통증완화, 재활 등)",살짝 땀이 나는 정도,7,30000,실외,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",평범함,19 +"회복(통증완화, 재활 등)",가벼운 웜업 위주,3,50000,상관없음,끝나면 기진맥진 고강도,평범함,19 +"회복(통증완화, 재활 등)",살짝 땀이 나는 정도,"7,3",50000,상관없음,땀이 흐를 정도의 중강도,매우 지침/피로함,19 +체력 증진,가벼운 웜업 위주,1,10000,실외,,평범함,19 +취미 탐색,끝나면 기진맥진 고강도,"2,8",10000,상관없음,,평범함,19 +체력 증진,살짝 땀이 나는 정도,8,70000,실내,,높음/컨디션 양호,19 +"회복(통증완화, 재활 등)",땀이 흐를 정도의 중강도,"4,2",10000,상관없음,,높음/컨디션 양호,19 +"회복(통증완화, 재활 등)",살짝 땀이 나는 정도,3,30000,실외,땀이 흐를 정도의 중강도,평범함,19 +"회복(통증완화, 재활 등)",살짝 땀이 나는 정도,"7,3",50000,실내,땀이 흐를 정도의 중강도,매우 지침/피로함,19 +취미 탐색,땀이 흐를 정도의 중강도,6,100000,실외,땀이 흐를 정도의 중강도,평범함,19 +"회복(통증완화, 재활 등)",가벼운 웜업 위주,"7,3",50000,실외,끝나면 기진맥진 고강도,매우 지침/피로함,19 +"회복(통증완화, 재활 등)",가벼운 웜업 위주,"7,3",30000,상관없음,땀이 흐를 정도의 중강도,평범함,19 +"회복(통증완화, 재활 등)",살짝 땀이 나는 정도,3,50000,상관없음,땀이 흐를 정도의 중강도,매우 지침/피로함,19 +"회복(통증완화, 재활 등)",가벼운 웜업 위주,"7,3",30000,상관없음,땀이 흐를 정도의 중강도,평범함,19 +"회복(통증완화, 재활 등)",가벼운 웜업 위주,7,100000,상관없음,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",평범함,19 +"회복(통증완화, 재활 등)",가벼운 웜업 위주,"7,3",30000,상관없음,땀이 흐를 정도의 중강도,평범함,19 +"회복(통증완화, 재활 등)",살짝 땀이 나는 정도,"7,3",30000,실내,끝나면 기진맥진 고강도,평범함,19 +"회복(통증완화, 재활 등)",가벼운 웜업 위주,3,30000,실외,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",평범함,19 +"회복(통증완화, 재활 등)",살짝 땀이 나는 정도,3,10000,상관없음,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",평범함,19 +스트레스 해소,땀이 흐를 정도의 중강도,"8,6",100000,상관없음,,높음/컨디션 양호,19 +"회복(통증완화, 재활 등)",가벼운 웜업 위주,7,30000,상관없음,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",평범함,19 +"회복(통증완화, 재활 등)",가벼운 웜업 위주,"7,3",50000,상관없음,끝나면 기진맥진 고강도,평범함,19 +"회복(통증완화, 재활 등)",가벼운 웜업 위주,"7,3",30000,실외,땀이 흐를 정도의 중강도,평범함,19 +"회복(통증완화, 재활 등)",가벼운 웜업 위주,7,30000,상관없음,끝나면 기진맥진 고강도,평범함,19 +"회복(통증완화, 재활 등)",살짝 땀이 나는 정도,"7,3",50000,상관없음,끝나면 기진맥진 고강도,평범함,19 +"회복(통증완화, 재활 등)",가벼운 웜업 위주,3,50000,상관없음,끝나면 기진맥진 고강도,평범함,19 +"회복(통증완화, 재활 등)",가벼운 웜업 위주,"7,3",50000,상관없음,끝나면 기진맥진 고강도,평범함,19 +"회복(통증완화, 재활 등)",가벼운 웜업 위주,"7,3",10000,상관없음,끝나면 기진맥진 고강도,매우 지침/피로함,19 +"회복(통증완화, 재활 등)",살짝 땀이 나는 정도,3,30000,상관없음,땀이 흐를 정도의 중강도,평범함,19 +"회복(통증완화, 재활 등)",살짝 땀이 나는 정도,"7,3",50000,실내,땀이 흐를 정도의 중강도,평범함,19 +"회복(통증완화, 재활 등)",가벼운 웜업 위주,3,50000,상관없음,땀이 흐를 정도의 중강도,평범함,19 +"회복(통증완화, 재활 등)",가벼운 웜업 위주,3,50000,상관없음,땀이 흐를 정도의 중강도,평범함,19 +"회복(통증완화, 재활 등)",살짝 땀이 나는 정도,"7,3",50000,상관없음,땀이 흐를 정도의 중강도,평범함,19 +스트레스 해소,땀이 흐를 정도의 중강도,"8,6",30000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,20 +스트레스 해소,땀이 흐를 정도의 중강도,6,50000,실내,끝나면 기진맥진 고강도,평범함,20 +체력 증진,가벼운 웜업 위주,4,100000,상관없음,"끝나면 기진맥진 고강도,가벼운 웜업 위주",높음/컨디션 양호,20 +스트레스 해소,땀이 흐를 정도의 중강도,"8,6",30000,실내,,평범함,20 +"회복(통증완화, 재활 등)",살짝 땀이 나는 정도,3,10000,상관없음,,매우 지침/피로함,20 +스트레스 해소,땀이 흐를 정도의 중강도,8,30000,실내,끝나면 기진맥진 고강도,평범함,20 +스트레스 해소,땀이 흐를 정도의 중강도,"8,6",30000,상관없음,,높음/컨디션 양호,20 +스트레스 해소,땀이 흐를 정도의 중강도,6,50000,상관없음,,높음/컨디션 양호,20 +스트레스 해소,땀이 흐를 정도의 중강도,6,30000,상관없음,끝나면 기진맥진 고강도,평범함,20 +스트레스 해소,땀이 흐를 정도의 중강도,6,30000,상관없음,,평범함,20 +취미 탐색,살짝 땀이 나는 정도,5,10000,상관없음,끝나면 기진맥진 고강도,평범함,20 +스트레스 해소,땀이 흐를 정도의 중강도,6,30000,실외,끝나면 기진맥진 고강도,높음/컨디션 양호,20 +스트레스 해소,땀이 흐를 정도의 중강도,8,10000,실내,땀이 흐를 정도의 중강도,평범함,20 +스트레스 해소,땀이 흐를 정도의 중강도,"8,6",50000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,20 +스트레스 해소,땀이 흐를 정도의 중강도,"8,6",10000,상관없음,,평범함,20 +스트레스 해소,땀이 흐를 정도의 중강도,"8,6",30000,상관없음,,평범함,20 +스트레스 해소,땀이 흐를 정도의 중강도,8,50000,상관없음,,높음/컨디션 양호,20 +스트레스 해소,땀이 흐를 정도의 중강도,6,70000,상관없음,끝나면 기진맥진 고강도,평범함,20 +스트레스 해소,땀이 흐를 정도의 중강도,"8,6",50000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,20 +스트레스 해소,땀이 흐를 정도의 중강도,6,30000,상관없음,끝나면 기진맥진 고강도,평범함,20 +스트레스 해소,땀이 흐를 정도의 중강도,6,30000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,20 +스트레스 해소,땀이 흐를 정도의 중강도,"8,6",30000,실외,,높음/컨디션 양호,20 +스트레스 해소,땀이 흐를 정도의 중강도,"8,6",30000,실외,끝나면 기진맥진 고강도,평범함,20 +스트레스 해소,땀이 흐를 정도의 중강도,"8,6",30000,상관없음,끝나면 기진맥진 고강도,평범함,20 +스트레스 해소,땀이 흐를 정도의 중강도,"8,6",100000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,20 +스트레스 해소,땀이 흐를 정도의 중강도,8,50000,실내,,높음/컨디션 양호,20 +스트레스 해소,땀이 흐를 정도의 중강도,8,50000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,20 +스트레스 해소,땀이 흐를 정도의 중강도,"8,6",50000,상관없음,,평범함,20 +스트레스 해소,땀이 흐를 정도의 중강도,6,30000,실내,끝나면 기진맥진 고강도,높음/컨디션 양호,20 +스트레스 해소,땀이 흐를 정도의 중강도,"8,6",30000,실내,,높음/컨디션 양호,20 +스트레스 해소,땀이 흐를 정도의 중강도,6,30000,상관없음,,높음/컨디션 양호,20 +스트레스 해소,땀이 흐를 정도의 중강도,6,10000,실내,끝나면 기진맥진 고강도,평범함,20 +스트레스 해소,땀이 흐를 정도의 중강도,8,30000,상관없음,,높음/컨디션 양호,20 +스트레스 해소,살짝 땀이 나는 정도,"9,4",30000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,21 +스트레스 해소,살짝 땀이 나는 정도,9,50000,실내,땀이 흐를 정도의 중강도,높음/컨디션 양호,21 +스트레스 해소,살짝 땀이 나는 정도,4,10000,상관없음,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",평범함,21 +스트레스 해소,살짝 땀이 나는 정도,"9,4",30000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,21 +스트레스 해소,살짝 땀이 나는 정도,9,30000,상관없음,땀이 흐를 정도의 중강도,평범함,21 +취미 탐색,가벼운 웜업 위주,"8,2",70000,상관없음,,높음/컨디션 양호,21 +스트레스 해소,살짝 땀이 나는 정도,4,30000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,21 +스트레스 해소,살짝 땀이 나는 정도,"9,4",10000,상관없음,끝나면 기진맥진 고강도,평범함,21 +"회복(통증완화, 재활 등)",살짝 땀이 나는 정도,3,10000,실외,땀이 흐를 정도의 중강도,매우 지침/피로함,21 +스트레스 해소,가벼운 웜업 위주,"9,4",30000,상관없음,땀이 흐를 정도의 중강도,높음/컨디션 양호,21 +스트레스 해소,가벼운 웜업 위주,"9,4",50000,상관없음,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",높음/컨디션 양호,21 +스트레스 해소,살짝 땀이 나는 정도,"9,4",50000,실외,땀이 흐를 정도의 중강도,평범함,21 +스트레스 해소,살짝 땀이 나는 정도,9,10000,상관없음,땀이 흐를 정도의 중강도,높음/컨디션 양호,21 +스트레스 해소,살짝 땀이 나는 정도,"9,4",50000,상관없음,끝나면 기진맥진 고강도,평범함,21 +스트레스 해소,가벼운 웜업 위주,"9,4",30000,상관없음,끝나면 기진맥진 고강도,평범함,21 +스트레스 해소,가벼운 웜업 위주,"9,4",50000,상관없음,땀이 흐를 정도의 중강도,평범함,21 +스트레스 해소,끝나면 기진맥진 고강도,3,70000,실외,,평범함,21 +취미 탐색,끝나면 기진맥진 고강도,5,100000,상관없음,,매우 지침/피로함,21 +스트레스 해소,살짝 땀이 나는 정도,4,50000,상관없음,땀이 흐를 정도의 중강도,평범함,21 +스트레스 해소,가벼운 웜업 위주,4,50000,상관없음,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",높음/컨디션 양호,21 +취미 탐색,살짝 땀이 나는 정도,8,100000,실외,,평범함,21 +다이어트,땀이 흐를 정도의 중강도,6,100000,실외,땀이 흐를 정도의 중강도,높음/컨디션 양호,21 +다이어트,가벼운 웜업 위주,2,70000,실외,,평범함,21 +스트레스 해소,살짝 땀이 나는 정도,9,30000,상관없음,땀이 흐를 정도의 중강도,높음/컨디션 양호,21 +스트레스 해소,살짝 땀이 나는 정도,"9,4",50000,상관없음,땀이 흐를 정도의 중강도,높음/컨디션 양호,21 +스트레스 해소,살짝 땀이 나는 정도,"9,4",50000,상관없음,땀이 흐를 정도의 중강도,평범함,21 +스트레스 해소,가벼운 웜업 위주,9,30000,실외,땀이 흐를 정도의 중강도,평범함,21 +스트레스 해소,끝나면 기진맥진 고강도,"8,2",70000,상관없음,,평범함,21 +체력 증진,땀이 흐를 정도의 중강도,2,30000,실내,가벼운 웜업 위주,매우 지침/피로함,21 +다이어트,땀이 흐를 정도의 중강도,"8,7",70000,실내,땀이 흐를 정도의 중강도,높음/컨디션 양호,21 +스트레스 해소,가벼운 웜업 위주,"9,4",30000,실외,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",높음/컨디션 양호,21 +스트레스 해소,가벼운 웜업 위주,4,50000,상관없음,끝나면 기진맥진 고강도,평범함,21 +스트레스 해소,가벼운 웜업 위주,9,30000,실외,끝나면 기진맥진 고강도,높음/컨디션 양호,21 +"회복(통증완화, 재활 등)",가벼운 웜업 위주,8,10000,실외,땀이 흐를 정도의 중강도,평범함,22 +다이어트,끝나면 기진맥진 고강도,1,50000,실내,,평범함,22 +다이어트,끝나면 기진맥진 고강도,"2,1",100000,실내,,평범함,22 +다이어트,끝나면 기진맥진 고강도,1,50000,실내,,높음/컨디션 양호,22 +"회복(통증완화, 재활 등)",끝나면 기진맥진 고강도,"8,6",100000,상관없음,땀이 흐를 정도의 중강도,평범함,22 +다이어트,끝나면 기진맥진 고강도,"2,1",30000,실내,,높음/컨디션 양호,22 +다이어트,끝나면 기진맥진 고강도,1,50000,상관없음,,매우 지침/피로함,22 +다이어트,끝나면 기진맥진 고강도,"2,1",70000,실내,,매우 지침/피로함,22 +다이어트,땀이 흐를 정도의 중강도,"3,5",100000,실내,,높음/컨디션 양호,22 +다이어트,끝나면 기진맥진 고강도,2,70000,실외,가벼운 웜업 위주,높음/컨디션 양호,22 +다이어트,끝나면 기진맥진 고강도,"2,1",100000,상관없음,가벼운 웜업 위주,높음/컨디션 양호,22 +다이어트,끝나면 기진맥진 고강도,1,30000,실내,가벼운 웜업 위주,높음/컨디션 양호,22 +다이어트,끝나면 기진맥진 고강도,1,30000,실내,,높음/컨디션 양호,22 +취미 탐색,살짝 땀이 나는 정도,"9,3",100000,상관없음,땀이 흐를 정도의 중강도,높음/컨디션 양호,22 +취미 탐색,땀이 흐를 정도의 중강도,7,50000,실외,,매우 지침/피로함,22 +다이어트,끝나면 기진맥진 고강도,1,30000,실내,,평범함,22 +다이어트,끝나면 기진맥진 고강도,1,30000,실내,,높음/컨디션 양호,22 +다이어트,끝나면 기진맥진 고강도,"2,1",50000,실내,,높음/컨디션 양호,22 +다이어트,끝나면 기진맥진 고강도,1,50000,실내,,평범함,22 +다이어트,끝나면 기진맥진 고강도,"2,1",50000,실내,,평범함,22 +다이어트,끝나면 기진맥진 고강도,"2,1",50000,실내,가벼운 웜업 위주,평범함,22 +"회복(통증완화, 재활 등)",살짝 땀이 나는 정도,"7,6",10000,상관없음,땀이 흐를 정도의 중강도,평범함,22 +다이어트,끝나면 기진맥진 고강도,"2,1",30000,실내,가벼운 웜업 위주,높음/컨디션 양호,22 +다이어트,끝나면 기진맥진 고강도,1,50000,실내,가벼운 웜업 위주,평범함,22 +다이어트,끝나면 기진맥진 고강도,2,30000,실외,,평범함,22 +다이어트,끝나면 기진맥진 고강도,"2,1",70000,실외,가벼운 웜업 위주,높음/컨디션 양호,22 +다이어트,끝나면 기진맥진 고강도,2,50000,실내,,높음/컨디션 양호,22 +다이어트,끝나면 기진맥진 고강도,"2,1",50000,실내,,매우 지침/피로함,22 +다이어트,끝나면 기진맥진 고강도,2,30000,실내,,평범함,22 +"회복(통증완화, 재활 등)",살짝 땀이 나는 정도,"5,9",10000,상관없음,,매우 지침/피로함,22 +다이어트,끝나면 기진맥진 고강도,"2,1",50000,실내,가벼운 웜업 위주,평범함,22 +"회복(통증완화, 재활 등)",끝나면 기진맥진 고강도,"3,7",100000,상관없음,끝나면 기진맥진 고강도,평범함,22 +다이어트,끝나면 기진맥진 고강도,2,50000,실내,가벼운 웜업 위주,평범함,22 +"회복(통증완화, 재활 등)",가벼운 웜업 위주,"6,3",100000,실외,끝나면 기진맥진 고강도,평범함,23 +"회복(통증완화, 재활 등)",가벼운 웜업 위주,"5,4",30000,실내,땀이 흐를 정도의 중강도,평범함,23 +다이어트,살짝 땀이 나는 정도,"8,9",50000,상관없음,,매우 지침/피로함,23 +취미 탐색,땀이 흐를 정도의 중강도,2,10000,실외,"끝나면 기진맥진 고강도,땀이 흐를 정도의 중강도",매우 지침/피로함,23 +"회복(통증완화, 재활 등)",가벼운 웜업 위주,"5,4",70000,실내,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",평범함,23 +"회복(통증완화, 재활 등)",가벼운 웜업 위주,"5,4",30000,실내,땀이 흐를 정도의 중강도,매우 지침/피로함,23 +"회복(통증완화, 재활 등)",살짝 땀이 나는 정도,"5,4",30000,상관없음,땀이 흐를 정도의 중강도,평범함,23 +"회복(통증완화, 재활 등)",가벼운 웜업 위주,"5,4",30000,실내,끝나면 기진맥진 고강도,매우 지침/피로함,23 +"회복(통증완화, 재활 등)",가벼운 웜업 위주,"5,4",50000,상관없음,땀이 흐를 정도의 중강도,평범함,23 +"회복(통증완화, 재활 등)",살짝 땀이 나는 정도,"5,4",50000,실내,끝나면 기진맥진 고강도,평범함,23 +"회복(통증완화, 재활 등)",가벼운 웜업 위주,4,10000,실내,,매우 지침/피로함,23 +"회복(통증완화, 재활 등)",살짝 땀이 나는 정도,"5,4",50000,실내,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",평범함,23 +"회복(통증완화, 재활 등)",살짝 땀이 나는 정도,"5,4",70000,실내,끝나면 기진맥진 고강도,평범함,23 +"회복(통증완화, 재활 등)",살짝 땀이 나는 정도,4,50000,실내,땀이 흐를 정도의 중강도,평범함,23 +"회복(통증완화, 재활 등)",가벼운 웜업 위주,4,10000,실내,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",평범함,23 +"회복(통증완화, 재활 등)",가벼운 웜업 위주,4,50000,실내,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",평범함,23 +"회복(통증완화, 재활 등)",살짝 땀이 나는 정도,"5,4",50000,실내,끝나면 기진맥진 고강도,평범함,23 +"회복(통증완화, 재활 등)",가벼운 웜업 위주,4,100000,실내,끝나면 기진맥진 고강도,매우 지침/피로함,23 +"회복(통증완화, 재활 등)",살짝 땀이 나는 정도,"5,4",30000,상관없음,끝나면 기진맥진 고강도,평범함,23 +"회복(통증완화, 재활 등)",살짝 땀이 나는 정도,"5,4",30000,실내,끝나면 기진맥진 고강도,매우 지침/피로함,23 +"회복(통증완화, 재활 등)",살짝 땀이 나는 정도,5,70000,실내,땀이 흐를 정도의 중강도,평범함,23 +"회복(통증완화, 재활 등)",살짝 땀이 나는 정도,"5,4",50000,상관없음,끝나면 기진맥진 고강도,평범함,23 +"회복(통증완화, 재활 등)",살짝 땀이 나는 정도,"5,4",50000,실내,끝나면 기진맥진 고강도,평범함,23 +"회복(통증완화, 재활 등)",가벼운 웜업 위주,5,70000,실내,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",매우 지침/피로함,23 +"회복(통증완화, 재활 등)",살짝 땀이 나는 정도,"5,4",50000,상관없음,끝나면 기진맥진 고강도,평범함,23 +"회복(통증완화, 재활 등)",가벼운 웜업 위주,4,30000,상관없음,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",매우 지침/피로함,23 +다이어트,끝나면 기진맥진 고강도,"8,2",100000,상관없음,,평범함,23 +"회복(통증완화, 재활 등)",가벼운 웜업 위주,"5,4",30000,상관없음,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",매우 지침/피로함,23 +"회복(통증완화, 재활 등)",살짝 땀이 나는 정도,5,30000,실외,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",평범함,23 +"회복(통증완화, 재활 등)",살짝 땀이 나는 정도,5,50000,상관없음,,높음/컨디션 양호,23 +"회복(통증완화, 재활 등)",살짝 땀이 나는 정도,5,70000,실내,끝나면 기진맥진 고강도,평범함,23 +"회복(통증완화, 재활 등)",살짝 땀이 나는 정도,5,50000,실내,땀이 흐를 정도의 중강도,평범함,23 +스트레스 해소,땀이 흐를 정도의 중강도,"6,9",70000,실내,"끝나면 기진맥진 고강도,가벼운 웜업 위주",평범함,23 +체력 증진,땀이 흐를 정도의 중강도,6,30000,상관없음,,평범함,24 +체력 증진,땀이 흐를 정도의 중강도,6,30000,실내,끝나면 기진맥진 고강도,높음/컨디션 양호,24 +체력 증진,땀이 흐를 정도의 중강도,7,50000,실외,끝나면 기진맥진 고강도,높음/컨디션 양호,24 +취미 탐색,끝나면 기진맥진 고강도,5,100000,실내,끝나면 기진맥진 고강도,높음/컨디션 양호,24 +체력 증진,땀이 흐를 정도의 중강도,"7,6",10000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,24 +체력 증진,땀이 흐를 정도의 중강도,"7,6",70000,실외,,평범함,24 +체력 증진,살짝 땀이 나는 정도,"5,3",10000,실내,,매우 지침/피로함,24 +체력 증진,땀이 흐를 정도의 중강도,6,100000,실내,끝나면 기진맥진 고강도,매우 지침/피로함,24 +체력 증진,땀이 흐를 정도의 중강도,"7,6",50000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,24 +체력 증진,땀이 흐를 정도의 중강도,7,30000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,24 +체력 증진,땀이 흐를 정도의 중강도,7,50000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,24 +체력 증진,땀이 흐를 정도의 중강도,"7,6",70000,실내,끝나면 기진맥진 고강도,평범함,24 +체력 증진,땀이 흐를 정도의 중강도,6,30000,상관없음,,평범함,24 +체력 증진,살짝 땀이 나는 정도,5,10000,상관없음,,높음/컨디션 양호,24 +체력 증진,땀이 흐를 정도의 중강도,6,10000,실내,,평범함,24 +체력 증진,땀이 흐를 정도의 중강도,7,30000,실내,끝나면 기진맥진 고강도,높음/컨디션 양호,24 +체력 증진,땀이 흐를 정도의 중강도,6,70000,상관없음,,평범함,24 +체력 증진,가벼운 웜업 위주,"8,1",10000,실내,땀이 흐를 정도의 중강도,평범함,24 +체력 증진,땀이 흐를 정도의 중강도,7,50000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,24 +체력 증진,땀이 흐를 정도의 중강도,"7,6",30000,상관없음,끝나면 기진맥진 고강도,매우 지침/피로함,24 +체력 증진,땀이 흐를 정도의 중강도,6,50000,상관없음,,평범함,24 +체력 증진,땀이 흐를 정도의 중강도,7,70000,실내,,높음/컨디션 양호,24 +체력 증진,가벼운 웜업 위주,"2,3",10000,실외,,평범함,24 +체력 증진,땀이 흐를 정도의 중강도,"7,6",30000,상관없음,,평범함,24 +"회복(통증완화, 재활 등)",끝나면 기진맥진 고강도,1,30000,실외,,높음/컨디션 양호,24 +체력 증진,땀이 흐를 정도의 중강도,6,30000,상관없음,,평범함,24 +체력 증진,땀이 흐를 정도의 중강도,6,50000,실내,,평범함,24 +체력 증진,땀이 흐를 정도의 중강도,7,30000,실외,,높음/컨디션 양호,24 +체력 증진,땀이 흐를 정도의 중강도,7,50000,상관없음,,높음/컨디션 양호,24 +스트레스 해소,끝나면 기진맥진 고강도,5,50000,실외,땀이 흐를 정도의 중강도,높음/컨디션 양호,24 +체력 증진,땀이 흐를 정도의 중강도,6,30000,실외,,매우 지침/피로함,24 +체력 증진,땀이 흐를 정도의 중강도,"7,6",70000,상관없음,,높음/컨디션 양호,24 +체력 증진,땀이 흐를 정도의 중강도,7,50000,상관없음,끝나면 기진맥진 고강도,평범함,24 +취미 탐색,땀이 흐를 정도의 중강도,8,50000,상관없음,,평범함,25 +취미 탐색,땀이 흐를 정도의 중강도,"8,2",30000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,25 +취미 탐색,땀이 흐를 정도의 중강도,8,30000,실내,끝나면 기진맥진 고강도,평범함,25 +취미 탐색,살짝 땀이 나는 정도,"9,7",100000,실외,땀이 흐를 정도의 중강도,높음/컨디션 양호,25 +취미 탐색,땀이 흐를 정도의 중강도,"8,2",30000,상관없음,,높음/컨디션 양호,25 +취미 탐색,땀이 흐를 정도의 중강도,8,50000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,25 +취미 탐색,땀이 흐를 정도의 중강도,2,100000,실내,,높음/컨디션 양호,25 +취미 탐색,땀이 흐를 정도의 중강도,"8,2",70000,상관없음,끝나면 기진맥진 고강도,평범함,25 +취미 탐색,땀이 흐를 정도의 중강도,8,50000,실내,끝나면 기진맥진 고강도,평범함,25 +다이어트,끝나면 기진맥진 고강도,"4,1",70000,실외,땀이 흐를 정도의 중강도,매우 지침/피로함,25 +취미 탐색,땀이 흐를 정도의 중강도,2,50000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,25 +취미 탐색,땀이 흐를 정도의 중강도,8,70000,실내,끝나면 기진맥진 고강도,매우 지침/피로함,25 +취미 탐색,땀이 흐를 정도의 중강도,2,30000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,25 +취미 탐색,땀이 흐를 정도의 중강도,"8,2",50000,상관없음,,매우 지침/피로함,25 +취미 탐색,땀이 흐를 정도의 중강도,2,30000,실내,,평범함,25 +취미 탐색,가벼운 웜업 위주,"6,9",100000,상관없음,"끝나면 기진맥진 고강도,가벼운 웜업 위주",매우 지침/피로함,25 +취미 탐색,땀이 흐를 정도의 중강도,8,30000,상관없음,,매우 지침/피로함,25 +취미 탐색,땀이 흐를 정도의 중강도,"8,2",50000,실외,끝나면 기진맥진 고강도,평범함,25 +취미 탐색,땀이 흐를 정도의 중강도,8,50000,실내,끝나면 기진맥진 고강도,평범함,25 +체력 증진,살짝 땀이 나는 정도,3,50000,상관없음,,높음/컨디션 양호,25 +다이어트,살짝 땀이 나는 정도,4,70000,실내,,매우 지침/피로함,25 +취미 탐색,땀이 흐를 정도의 중강도,2,50000,상관없음,끝나면 기진맥진 고강도,평범함,25 +취미 탐색,땀이 흐를 정도의 중강도,2,30000,상관없음,끝나면 기진맥진 고강도,매우 지침/피로함,25 +취미 탐색,땀이 흐를 정도의 중강도,8,50000,실내,끝나면 기진맥진 고강도,높음/컨디션 양호,25 +취미 탐색,땀이 흐를 정도의 중강도,"8,2",30000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,25 +다이어트,가벼운 웜업 위주,5,70000,실외,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",높음/컨디션 양호,25 +취미 탐색,땀이 흐를 정도의 중강도,"8,2",30000,실외,끝나면 기진맥진 고강도,평범함,25 +취미 탐색,땀이 흐를 정도의 중강도,"8,2",100000,상관없음,,평범함,25 +취미 탐색,땀이 흐를 정도의 중강도,"8,2",50000,상관없음,,높음/컨디션 양호,25 +취미 탐색,땀이 흐를 정도의 중강도,"8,2",30000,실내,,평범함,25 +취미 탐색,땀이 흐를 정도의 중강도,"8,2",30000,실외,끝나면 기진맥진 고강도,평범함,25 +취미 탐색,땀이 흐를 정도의 중강도,2,100000,상관없음,,평범함,25 +취미 탐색,땀이 흐를 정도의 중강도,"8,2",70000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,25 +"회복(통증완화, 재활 등)",살짝 땀이 나는 정도,"1,3",100000,상관없음,,높음/컨디션 양호,26 +체력 증진,살짝 땀이 나는 정도,"9,5",50000,실외,끝나면 기진맥진 고강도,높음/컨디션 양호,26 +다이어트,끝나면 기진맥진 고강도,"7,6",10000,상관없음,,매우 지침/피로함,26 +체력 증진,가벼운 웜업 위주,"9,5",50000,상관없음,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",높음/컨디션 양호,26 +스트레스 해소,가벼운 웜업 위주,1,70000,실외,,높음/컨디션 양호,26 +체력 증진,살짝 땀이 나는 정도,9,30000,상관없음,끝나면 기진맥진 고강도,평범함,26 +체력 증진,살짝 땀이 나는 정도,9,30000,상관없음,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",평범함,26 +체력 증진,살짝 땀이 나는 정도,"7,1",10000,상관없음,"땀이 흐를 정도의 중강도,가벼운 웜업 위주",매우 지침/피로함,26 +체력 증진,살짝 땀이 나는 정도,"9,5",100000,상관없음,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",매우 지침/피로함,26 +체력 증진,가벼운 웜업 위주,"9,5",30000,상관없음,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",평범함,26 +체력 증진,가벼운 웜업 위주,"9,5",50000,상관없음,끝나면 기진맥진 고강도,평범함,26 +체력 증진,가벼운 웜업 위주,5,70000,상관없음,땀이 흐를 정도의 중강도,평범함,26 +체력 증진,가벼운 웜업 위주,5,100000,실외,끝나면 기진맥진 고강도,높음/컨디션 양호,26 +체력 증진,살짝 땀이 나는 정도,9,30000,실외,땀이 흐를 정도의 중강도,높음/컨디션 양호,26 +스트레스 해소,끝나면 기진맥진 고강도,2,10000,실내,,높음/컨디션 양호,26 +체력 증진,살짝 땀이 나는 정도,9,50000,실외,끝나면 기진맥진 고강도,평범함,26 +"회복(통증완화, 재활 등)",살짝 땀이 나는 정도,8,10000,실내,가벼운 웜업 위주,평범함,26 +체력 증진,살짝 땀이 나는 정도,"9,5",100000,상관없음,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",매우 지침/피로함,26 +취미 탐색,가벼운 웜업 위주,7,70000,상관없음,,평범함,26 +체력 증진,살짝 땀이 나는 정도,"9,5",50000,상관없음,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",높음/컨디션 양호,26 +체력 증진,가벼운 웜업 위주,9,50000,상관없음,땀이 흐를 정도의 중강도,평범함,26 +체력 증진,가벼운 웜업 위주,5,50000,상관없음,끝나면 기진맥진 고강도,평범함,26 +체력 증진,가벼운 웜업 위주,5,10000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,26 +체력 증진,가벼운 웜업 위주,9,50000,상관없음,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",매우 지침/피로함,26 +체력 증진,살짝 땀이 나는 정도,"4,7",70000,상관없음,,매우 지침/피로함,26 +체력 증진,가벼운 웜업 위주,"9,5",30000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,26 +체력 증진,땀이 흐를 정도의 중강도,2,100000,상관없음,,높음/컨디션 양호,26 +체력 증진,살짝 땀이 나는 정도,"9,5",50000,실내,땀이 흐를 정도의 중강도,매우 지침/피로함,26 +체력 증진,가벼운 웜업 위주,9,30000,실내,끝나면 기진맥진 고강도,평범함,26 +스트레스 해소,가벼운 웜업 위주,"4,6",10000,실외,땀이 흐를 정도의 중강도,평범함,26 +체력 증진,살짝 땀이 나는 정도,"9,5",50000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,26 +체력 증진,가벼운 웜업 위주,9,30000,상관없음,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",높음/컨디션 양호,26 +체력 증진,살짝 땀이 나는 정도,9,50000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,26 +다이어트,땀이 흐를 정도의 중강도,"6,5",50000,실내,,높음/컨디션 양호,27 +다이어트,끝나면 기진맥진 고강도,1,10000,실외,,높음/컨디션 양호,27 +다이어트,땀이 흐를 정도의 중강도,5,70000,실내,,평범함,27 +취미 탐색,살짝 땀이 나는 정도,1,10000,실외,끝나면 기진맥진 고강도,평범함,27 +다이어트,땀이 흐를 정도의 중강도,5,100000,실내,끝나면 기진맥진 고강도,높음/컨디션 양호,27 +다이어트,땀이 흐를 정도의 중강도,6,50000,실외,,매우 지침/피로함,27 +취미 탐색,끝나면 기진맥진 고강도,"7,4",70000,상관없음,땀이 흐를 정도의 중강도,매우 지침/피로함,27 +다이어트,땀이 흐를 정도의 중강도,"6,5",50000,실내,끝나면 기진맥진 고강도,높음/컨디션 양호,27 +체력 증진,살짝 땀이 나는 정도,4,10000,실내,,평범함,27 +다이어트,땀이 흐를 정도의 중강도,"6,5",50000,상관없음,,평범함,27 +다이어트,살짝 땀이 나는 정도,"8,3",70000,상관없음,,높음/컨디션 양호,27 +다이어트,땀이 흐를 정도의 중강도,6,70000,실내,,매우 지침/피로함,27 +취미 탐색,끝나면 기진맥진 고강도,"1,3",100000,실외,,매우 지침/피로함,27 +다이어트,땀이 흐를 정도의 중강도,5,30000,실외,끝나면 기진맥진 고강도,높음/컨디션 양호,27 +다이어트,땀이 흐를 정도의 중강도,"6,5",50000,실내,,높음/컨디션 양호,27 +"회복(통증완화, 재활 등)",가벼운 웜업 위주,4,10000,상관없음,,매우 지침/피로함,27 +다이어트,땀이 흐를 정도의 중강도,"6,5",70000,실내,,높음/컨디션 양호,27 +다이어트,땀이 흐를 정도의 중강도,6,30000,실내,,평범함,27 +다이어트,땀이 흐를 정도의 중강도,6,70000,실내,끝나면 기진맥진 고강도,높음/컨디션 양호,27 +"회복(통증완화, 재활 등)",가벼운 웜업 위주,"8,4",10000,상관없음,,평범함,27 +다이어트,땀이 흐를 정도의 중강도,6,100000,실내,끝나면 기진맥진 고강도,높음/컨디션 양호,27 +다이어트,가벼운 웜업 위주,"1,7",70000,실내,가벼운 웜업 위주,매우 지침/피로함,27 +다이어트,땀이 흐를 정도의 중강도,6,30000,실내,끝나면 기진맥진 고강도,높음/컨디션 양호,27 +다이어트,땀이 흐를 정도의 중강도,"6,5",30000,실내,끝나면 기진맥진 고강도,높음/컨디션 양호,27 +체력 증진,살짝 땀이 나는 정도,"7,2",70000,실외,,평범함,27 +다이어트,땀이 흐를 정도의 중강도,6,30000,실내,,평범함,27 +다이어트,땀이 흐를 정도의 중강도,"6,5",50000,실내,,매우 지침/피로함,27 +다이어트,땀이 흐를 정도의 중강도,"6,5",50000,실내,끝나면 기진맥진 고강도,평범함,27 +"회복(통증완화, 재활 등)",끝나면 기진맥진 고강도,8,100000,실내,,높음/컨디션 양호,27 +다이어트,땀이 흐를 정도의 중강도,6,30000,실외,끝나면 기진맥진 고강도,높음/컨디션 양호,27 +다이어트,땀이 흐를 정도의 중강도,6,30000,실내,,매우 지침/피로함,27 +스트레스 해소,살짝 땀이 나는 정도,9,70000,상관없음,"가벼운 웜업 위주,땀이 흐를 정도의 중강도",평범함,27 +다이어트,땀이 흐를 정도의 중강도,6,30000,실외,,평범함,27 +"회복(통증완화, 재활 등)",땀이 흐를 정도의 중강도,2,50000,실내,,평범함,28 +"회복(통증완화, 재활 등)",땀이 흐를 정도의 중강도,2,30000,실외,끝나면 기진맥진 고강도,평범함,28 +"회복(통증완화, 재활 등)",땀이 흐를 정도의 중강도,3,30000,실내,끝나면 기진맥진 고강도,매우 지침/피로함,28 +"회복(통증완화, 재활 등)",땀이 흐를 정도의 중강도,3,30000,실내,,매우 지침/피로함,28 +"회복(통증완화, 재활 등)",땀이 흐를 정도의 중강도,"2,3",30000,상관없음,끝나면 기진맥진 고강도,매우 지침/피로함,28 +"회복(통증완화, 재활 등)",땀이 흐를 정도의 중강도,2,70000,실내,,평범함,28 +"회복(통증완화, 재활 등)",가벼운 웜업 위주,"6,9",70000,상관없음,가벼운 웜업 위주,높음/컨디션 양호,28 +"회복(통증완화, 재활 등)",땀이 흐를 정도의 중강도,2,50000,상관없음,끝나면 기진맥진 고강도,평범함,28 +"회복(통증완화, 재활 등)",살짝 땀이 나는 정도,6,10000,실내,,높음/컨디션 양호,28 +"회복(통증완화, 재활 등)",땀이 흐를 정도의 중강도,"2,3",50000,상관없음,,평범함,28 +"회복(통증완화, 재활 등)",땀이 흐를 정도의 중강도,"2,3",30000,실내,끝나면 기진맥진 고강도,평범함,28 +"회복(통증완화, 재활 등)",땀이 흐를 정도의 중강도,"2,3",30000,실내,,평범함,28 +"회복(통증완화, 재활 등)",땀이 흐를 정도의 중강도,"2,3",30000,실내,끝나면 기진맥진 고강도,평범함,28 +"회복(통증완화, 재활 등)",땀이 흐를 정도의 중강도,"2,3",30000,상관없음,,평범함,28 +"회복(통증완화, 재활 등)",땀이 흐를 정도의 중강도,3,50000,실외,끝나면 기진맥진 고강도,평범함,28 +"회복(통증완화, 재활 등)",땀이 흐를 정도의 중강도,"2,3",10000,실외,,평범함,28 +체력 증진,끝나면 기진맥진 고강도,"4,6",100000,실외,끝나면 기진맥진 고강도,평범함,28 +"회복(통증완화, 재활 등)",땀이 흐를 정도의 중강도,2,30000,상관없음,,매우 지침/피로함,28 +"회복(통증완화, 재활 등)",땀이 흐를 정도의 중강도,2,30000,실내,끝나면 기진맥진 고강도,평범함,28 +"회복(통증완화, 재활 등)",땀이 흐를 정도의 중강도,"2,3",30000,실내,끝나면 기진맥진 고강도,평범함,28 +"회복(통증완화, 재활 등)",땀이 흐를 정도의 중강도,2,50000,상관없음,,평범함,28 +"회복(통증완화, 재활 등)",땀이 흐를 정도의 중강도,2,30000,실내,끝나면 기진맥진 고강도,평범함,28 +"회복(통증완화, 재활 등)",땀이 흐를 정도의 중강도,3,70000,실내,,평범함,28 +"회복(통증완화, 재활 등)",땀이 흐를 정도의 중강도,"2,3",30000,실내,,평범함,28 +"회복(통증완화, 재활 등)",땀이 흐를 정도의 중강도,"2,3",10000,실내,끝나면 기진맥진 고강도,평범함,28 +"회복(통증완화, 재활 등)",땀이 흐를 정도의 중강도,"2,3",30000,상관없음,끝나면 기진맥진 고강도,평범함,28 +"회복(통증완화, 재활 등)",땀이 흐를 정도의 중강도,"2,3",50000,상관없음,끝나면 기진맥진 고강도,평범함,28 +"회복(통증완화, 재활 등)",땀이 흐를 정도의 중강도,"2,3",50000,실내,,평범함,28 +"회복(통증완화, 재활 등)",끝나면 기진맥진 고강도,"8,7",70000,상관없음,"끝나면 기진맥진 고강도,땀이 흐를 정도의 중강도",평범함,28 +"회복(통증완화, 재활 등)",땀이 흐를 정도의 중강도,"2,3",100000,상관없음,끝나면 기진맥진 고강도,매우 지침/피로함,28 +"회복(통증완화, 재활 등)",땀이 흐를 정도의 중강도,3,50000,실내,끝나면 기진맥진 고강도,매우 지침/피로함,28 +"회복(통증완화, 재활 등)",땀이 흐를 정도의 중강도,2,30000,실내,,평범함,28 +"회복(통증완화, 재활 등)",살짝 땀이 나는 정도,8,100000,상관없음,끝나면 기진맥진 고강도,평범함,28 +취미 탐색,가벼운 웜업 위주,"6,9",100000,실외,가벼운 웜업 위주,평범함,29 +취미 탐색,땀이 흐를 정도의 중강도,7,10000,실내,끝나면 기진맥진 고강도,평범함,29 +다이어트,끝나면 기진맥진 고강도,"4,3",50000,실내,가벼운 웜업 위주,매우 지침/피로함,29 +취미 탐색,땀이 흐를 정도의 중강도,8,50000,실외,끝나면 기진맥진 고강도,높음/컨디션 양호,29 +취미 탐색,땀이 흐를 정도의 중강도,7,50000,상관없음,,평범함,29 +취미 탐색,땀이 흐를 정도의 중강도,"7,8",30000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,29 +취미 탐색,땀이 흐를 정도의 중강도,"7,8",30000,실외,,평범함,29 +취미 탐색,끝나면 기진맥진 고강도,"9,6",30000,상관없음,,평범함,29 +취미 탐색,땀이 흐를 정도의 중강도,"7,8",70000,실외,끝나면 기진맥진 고강도,평범함,29 +취미 탐색,땀이 흐를 정도의 중강도,"7,8",100000,실외,끝나면 기진맥진 고강도,평범함,29 +취미 탐색,땀이 흐를 정도의 중강도,7,50000,상관없음,,매우 지침/피로함,29 +취미 탐색,땀이 흐를 정도의 중강도,"7,8",30000,실외,끝나면 기진맥진 고강도,평범함,29 +취미 탐색,땀이 흐를 정도의 중강도,7,30000,실외,,높음/컨디션 양호,29 +취미 탐색,땀이 흐를 정도의 중강도,7,50000,실외,,평범함,29 +스트레스 해소,살짝 땀이 나는 정도,"1,9",70000,상관없음,끝나면 기진맥진 고강도,평범함,29 +취미 탐색,땀이 흐를 정도의 중강도,"7,8",30000,상관없음,,높음/컨디션 양호,29 +취미 탐색,땀이 흐를 정도의 중강도,"7,8",30000,실외,끝나면 기진맥진 고강도,높음/컨디션 양호,29 +취미 탐색,땀이 흐를 정도의 중강도,"7,8",50000,실외,,평범함,29 +취미 탐색,땀이 흐를 정도의 중강도,8,70000,실외,끝나면 기진맥진 고강도,평범함,29 +취미 탐색,땀이 흐를 정도의 중강도,"7,8",100000,실외,끝나면 기진맥진 고강도,평범함,29 +취미 탐색,땀이 흐를 정도의 중강도,"7,8",100000,상관없음,끝나면 기진맥진 고강도,매우 지침/피로함,29 +취미 탐색,땀이 흐를 정도의 중강도,"7,8",30000,실외,끝나면 기진맥진 고강도,매우 지침/피로함,29 +취미 탐색,땀이 흐를 정도의 중강도,8,10000,실외,끝나면 기진맥진 고강도,평범함,29 +취미 탐색,땀이 흐를 정도의 중강도,8,10000,실외,,높음/컨디션 양호,29 +취미 탐색,땀이 흐를 정도의 중강도,"7,8",30000,실외,끝나면 기진맥진 고강도,높음/컨디션 양호,29 +스트레스 해소,살짝 땀이 나는 정도,5,100000,실내,가벼운 웜업 위주,매우 지침/피로함,29 +취미 탐색,땀이 흐를 정도의 중강도,7,100000,상관없음,끝나면 기진맥진 고강도,평범함,29 +취미 탐색,땀이 흐를 정도의 중강도,"7,8",50000,실외,끝나면 기진맥진 고강도,평범함,29 +취미 탐색,땀이 흐를 정도의 중강도,8,30000,상관없음,끝나면 기진맥진 고강도,평범함,29 +"회복(통증완화, 재활 등)",끝나면 기진맥진 고강도,7,70000,실외,,높음/컨디션 양호,29 +취미 탐색,땀이 흐를 정도의 중강도,7,50000,실외,끝나면 기진맥진 고강도,평범함,29 +취미 탐색,땀이 흐를 정도의 중강도,8,30000,실외,끝나면 기진맥진 고강도,높음/컨디션 양호,29 +다이어트,가벼운 웜업 위주,3,100000,실외,,평범함,29 +스트레스 해소,살짝 땀이 나는 정도,9,30000,상관없음,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",평범함,30 +스트레스 해소,살짝 땀이 나는 정도,9,30000,상관없음,끝나면 기진맥진 고강도,평범함,30 +스트레스 해소,가벼운 웜업 위주,"9,6",50000,상관없음,땀이 흐를 정도의 중강도,평범함,30 +스트레스 해소,가벼운 웜업 위주,6,100000,상관없음,끝나면 기진맥진 고강도,평범함,30 +스트레스 해소,가벼운 웜업 위주,9,70000,상관없음,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",높음/컨디션 양호,30 +스트레스 해소,가벼운 웜업 위주,"9,6",30000,상관없음,땀이 흐를 정도의 중강도,평범함,30 +스트레스 해소,끝나면 기진맥진 고강도,2,100000,상관없음,땀이 흐를 정도의 중강도,매우 지침/피로함,30 +스트레스 해소,가벼운 웜업 위주,"9,6",50000,상관없음,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",평범함,30 +스트레스 해소,가벼운 웜업 위주,"9,6",100000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,30 +다이어트,끝나면 기진맥진 고강도,"1,3",30000,실내,,평범함,30 +스트레스 해소,가벼운 웜업 위주,6,50000,실외,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",높음/컨디션 양호,30 +스트레스 해소,가벼운 웜업 위주,3,10000,실외,땀이 흐를 정도의 중강도,매우 지침/피로함,30 +스트레스 해소,살짝 땀이 나는 정도,9,50000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,30 +스트레스 해소,살짝 땀이 나는 정도,6,30000,실외,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",높음/컨디션 양호,30 +취미 탐색,살짝 땀이 나는 정도,"3,7",70000,실내,,평범함,30 +스트레스 해소,가벼운 웜업 위주,9,50000,상관없음,땀이 흐를 정도의 중강도,평범함,30 +스트레스 해소,가벼운 웜업 위주,6,50000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,30 +스트레스 해소,가벼운 웜업 위주,9,30000,상관없음,땀이 흐를 정도의 중강도,높음/컨디션 양호,30 +스트레스 해소,가벼운 웜업 위주,6,30000,실내,끝나면 기진맥진 고강도,높음/컨디션 양호,30 +스트레스 해소,가벼운 웜업 위주,9,30000,실내,끝나면 기진맥진 고강도,높음/컨디션 양호,30 +스트레스 해소,가벼운 웜업 위주,9,30000,실내,끝나면 기진맥진 고강도,높음/컨디션 양호,30 +"회복(통증완화, 재활 등)",끝나면 기진맥진 고강도,2,70000,실내,땀이 흐를 정도의 중강도,매우 지침/피로함,30 +스트레스 해소,살짝 땀이 나는 정도,"9,6",30000,상관없음,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",높음/컨디션 양호,30 +스트레스 해소,가벼운 웜업 위주,"9,6",50000,실내,끝나면 기진맥진 고강도,평범함,30 +스트레스 해소,가벼운 웜업 위주,6,50000,상관없음,끝나면 기진맥진 고강도,높음/컨디션 양호,30 +스트레스 해소,가벼운 웜업 위주,6,100000,실외,땀이 흐를 정도의 중강도,높음/컨디션 양호,30 +스트레스 해소,가벼운 웜업 위주,9,30000,실외,끝나면 기진맥진 고강도,평범함,30 +스트레스 해소,살짝 땀이 나는 정도,1,70000,상관없음,,매우 지침/피로함,30 +스트레스 해소,가벼운 웜업 위주,9,50000,실외,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",높음/컨디션 양호,30 +스트레스 해소,가벼운 웜업 위주,"9,6",50000,실외,"땀이 흐를 정도의 중강도,끝나면 기진맥진 고강도",평범함,30 +스트레스 해소,가벼운 웜업 위주,9,30000,상관없음,땀이 흐를 정도의 중강도,평범함,30 +스트레스 해소,가벼운 웜업 위주,6,50000,실내,땀이 흐를 정도의 중강도,높음/컨디션 양호,30 +스트레스 해소,가벼운 웜업 위주,9,100000,실내,땀이 흐를 정도의 중강도,평범함,30 diff --git a/ai_recommendation/models/__init__.py b/ai_recommendation/models/__init__.py new file mode 100644 index 0000000..93501f8 --- /dev/null +++ b/ai_recommendation/models/__init__.py @@ -0,0 +1 @@ +"""Models module for ML training and prediction.""" diff --git a/ai_recommendation/models/catboost_model.cbm b/ai_recommendation/models/catboost_model.cbm new file mode 100644 index 0000000..2ddc833 Binary files /dev/null and b/ai_recommendation/models/catboost_model.cbm differ diff --git a/ai_recommendation/models/predictor.py b/ai_recommendation/models/predictor.py new file mode 100644 index 0000000..e195808 --- /dev/null +++ b/ai_recommendation/models/predictor.py @@ -0,0 +1,117 @@ +""" +추천을 위한 모델 예측기 +학습된 모델 로드 및 예측 담당 +""" +import pandas as pd +from catboost import CatBoostRegressor +from pathlib import Path +from typing import List, Dict +import logging + +from config.settings import settings + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +class ModelPredictor: + """모델 로드 및 예측을 처리합니다.""" + + def __init__(self, model_path: Path = settings.MODEL_PATH): + self.model = None + self.model_path = model_path + self._load_model() + + def _load_model(self) -> None: + """디스크에서 학습된 모델을 로드합니다.""" + if not self.model_path.exists(): + raise FileNotFoundError( + f"{self.model_path}에서 모델 파일을 찾을 수 없습니다. " + "먼저 모델을 학습하세요." + ) + + logger.info(f"{self.model_path}에서 모델 로드 중") + self.model = CatBoostRegressor() + self.model.load_model(str(self.model_path)) + logger.info("모델 로드 완료") + + def predict(self, features: pd.DataFrame) -> pd.Series: + """ + 주어진 특성에 대해 예측을 수행합니다. + + Args: + features: 학습 데이터와 동일한 구조의 DataFrame + + Returns: + 예측 점수의 Series + """ + if self.model is None: + raise ValueError("모델이 로드되지 않았습니다") + + predictions = self.model.predict(features) + return pd.Series(predictions, index=features.index) + + def predict_for_user( + self, + user_features: Dict, + pass_metadata: pd.DataFrame + ) -> pd.DataFrame: + """ + 주어진 사용자에 대해 모든 패키지의 점수를 예측합니다. + + Args: + user_features: 사용자 설문 응답 (purpose, preferredIntensity, interestedSportIds, preferredEnvironment, avoidFactors, recoveryCondition) + pass_metadata: 패키지 정보 DataFrame (price 컬럼 필수) + + Returns: + pass_id와 predicted_score를 포함한 DataFrame + """ + # 사용자 특성과 각 패스를 결합하여 특성 매트릭스 생성 + num_passes = len(pass_metadata) + + # 각 패스에 대해 사용자 특성 복제 + user_df = pd.DataFrame([user_features] * num_passes) + + # pass_metadata에서 price 추출하여 특성에 추가 + # 학습 데이터 컬럼: purpose, preferredIntensity, interestedSportIds, price, preferredEnvironment, avoidFactors, recoveryCondition + features = pd.DataFrame({ + 'purpose': user_df['purpose'], + 'preferredIntensity': user_df['preferredIntensity'], + 'interestedSportIds': user_df['interestedSportIds'], + 'price': pass_metadata['price'].values, + 'preferredEnvironment': user_df['preferredEnvironment'], + 'avoidFactors': user_df['avoidFactors'], + 'recoveryCondition': user_df['recoveryCondition'] + }) + + # 예측 수행 + scores = self.predict(features) + + # 결과 데이터프레임 생성 + result = pd.DataFrame({ + 'pass_id': pass_metadata['pass_id'].values, # server.py에서 pass_id로 변경됨 + 'predicted_score': scores + }) + + return result.sort_values('predicted_score', ascending=False) + + def get_top_n( + self, + user_features: Dict, + pass_metadata: pd.DataFrame, + n: int = settings.TOP_N_RECOMMENDATIONS + ) -> List[int]: + """ + 사용자를 위한 상위 N개 패키지 ID를 가져옵니다. + + Args: + user_features: 사용자 설문 응답 + pass_metadata: 패키지 정보 DataFrame + n: 추천 개수 + + Returns: + 상위 N개 패키지 ID 리스트 + """ + predictions = self.predict_for_user(user_features, pass_metadata) + top_n = predictions.head(n) + return top_n['pass_id'].tolist() diff --git a/ai_recommendation/models/trainer.py b/ai_recommendation/models/trainer.py new file mode 100644 index 0000000..68e71d3 --- /dev/null +++ b/ai_recommendation/models/trainer.py @@ -0,0 +1,130 @@ +""" +CatBoost 모델 학습기 +추천 모델 학습 담당 +""" +import pandas as pd +from catboost import CatBoostRegressor, Pool +from pathlib import Path +from typing import Tuple, List +import logging + +from config.settings import settings + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +class ModelTrainer: + """CatBoost 모델 학습을 처리합니다.""" + + def __init__(self): + self.model = None + self.categorical_features = None + + def prepare_features( + self, + df: pd.DataFrame + ) -> Tuple[pd.DataFrame, pd.Series, List[str]]: + """ + 데이터프레임에서 특성과 타겟을 준비합니다. + + Args: + df: 학습 데이터프레임 + + Returns: + (X, y, categorical_feature_names) 튜플 + """ + # 타겟 변수 - purchased_pass_id + target_col = 'purchased_pass_id' + + if target_col not in df.columns: + raise ValueError(f"타겟 컬럼 '{target_col}'을 데이터에서 찾을 수 없습니다") + + y = df[target_col] + X = df.drop(columns=[target_col], errors='ignore') + + # 실제 CSV 컬럼 기반으로 범주형 특성 정의 + categorical_features = [ + 'purpose', # 운동 목적 + 'preferredIntensity', # 선호 강도 + 'interestedSportIds', # 관심 종목 IDs (문자열로 처리) + 'preferredEnvironment', # 실내/실외 + 'avoidFactors', # 피하고 싶은 강도 + 'recoveryCondition', # 회복 상태 + ] + + # X에 존재하는 범주형 특성만 필터링 + categorical_features = [f for f in categorical_features if f in X.columns] + + # 모든 범주형 특성을 문자열로 변환하고 NaN 처리 + for col in categorical_features: + X[col] = X[col].fillna('missing').astype(str) + + logger.info(f"범주형 특성: {categorical_features}") + logger.info(f"특성 컬럼: {X.columns.tolist()}") + + return X, y, categorical_features + + def train( + self, + train_data_path: Path = settings.TRAINING_DATA_PATH, + save_model: bool = True + ) -> CatBoostRegressor: + """ + CatBoost 모델을 학습합니다. + + Args: + train_data_path: 학습 CSV 경로 + save_model: 학습된 모델 저장 여부 + + Returns: + 학습된 CatBoost 모델 + """ + logger.info(f"{train_data_path}에서 학습 데이터 로드 중...") + df = pd.read_csv(train_data_path) + + logger.info(f"특성 준비 중... 데이터셋 크기: {df.shape}") + X, y, cat_features = self.prepare_features(df) + + self.categorical_features = cat_features + + # CatBoost Pool 생성 + train_pool = Pool( + data=X, + label=y, + cat_features=cat_features + ) + + # 모델 초기화 + self.model = CatBoostRegressor( + iterations=settings.CATBOOST_ITERATIONS, + learning_rate=settings.CATBOOST_LEARNING_RATE, + depth=settings.CATBOOST_DEPTH, + loss_function=settings.CATBOOST_LOSS_FUNCTION, + verbose=50, + random_seed=42 + ) + + logger.info("CatBoost 모델 학습 중...") + self.model.fit(train_pool) + + logger.info("학습 완료!") + + if save_model: + self.save_model() + + return self.model + + def save_model(self, model_path: Path = settings.MODEL_PATH) -> None: + """학습된 모델을 디스크에 저장합니다.""" + if self.model is None: + raise ValueError("저장할 모델이 없습니다. 먼저 모델을 학습하세요.") + + model_path.parent.mkdir(parents=True, exist_ok=True) + self.model.save_model(str(model_path)) + logger.info(f"모델이 {model_path}에 저장되었습니다") + + +if __name__ == "__main__": + trainer = ModelTrainer() + trainer.train() diff --git a/ai_recommendation/requirements.txt b/ai_recommendation/requirements.txt new file mode 100644 index 0000000..3c47a8d --- /dev/null +++ b/ai_recommendation/requirements.txt @@ -0,0 +1,9 @@ +catboost==1.2.2 +fastapi==0.108.0 +httpx==0.25.2 +pandas==2.1.4 +pydantic==2.5.3 +pydantic-settings==2.1.0 +pydantic_core==2.14.6 +scikit-learn==1.3.2 +uvicorn==0.25.0 diff --git a/ai_recommendation/services/__init__.py b/ai_recommendation/services/__init__.py new file mode 100644 index 0000000..6b4dcab --- /dev/null +++ b/ai_recommendation/services/__init__.py @@ -0,0 +1 @@ +"""Services module for business logic.""" diff --git a/ai_recommendation/services/recommendation_service.py b/ai_recommendation/services/recommendation_service.py new file mode 100644 index 0000000..861e1a4 --- /dev/null +++ b/ai_recommendation/services/recommendation_service.py @@ -0,0 +1,119 @@ +""" +하이브리드 추천 서비스 +ML 기반 예측과 규칙 기반 필터링 결합 +""" +from typing import List, Dict +import pandas as pd +import logging + +from models.predictor import ModelPredictor +from services.rule_filter import RuleFilter +from config.settings import settings + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +class RecommendationService: + """ML과 규칙을 결합한 메인 추천 서비스""" + + def __init__(self): + self.predictor = ModelPredictor() + self.rule_filter = RuleFilter() + + def get_recommendations( + self, + user_survey: Dict, + pass_metadata: pd.DataFrame, + top_n: int = settings.TOP_N_RECOMMENDATIONS + ) -> List[Dict]: + """ + 사용자를 위한 하이브리드 추천을 제공합니다. + + 작업 흐름: + 1. ML 모델을 사용하여 모든 패키지에 점수 부여 + 2. 규칙 기반 필터 적용 + 3. 재순위화 후 상위 N개 반환 + + Args: + user_survey: 사용자 설문 응답 + pass_metadata: 패키지 정보가 포함된 DataFrame + top_n: 반환할 추천 개수 + + Returns: + 점수가 포함된 추천 패키지 딕셔너리 리스트 + """ + logger.info("추천 프로세스 시작") + + # 단계 1: ML 기반 점수 산출 + logger.info("단계 1: ML 점수 산출") + ml_predictions = self.predictor.predict_for_user( + user_survey, + pass_metadata + ) + + # 예측과 패키지 메타데이터 병합 + candidates = pass_metadata.merge( + ml_predictions, + on='pass_id', + how='inner' + ) + + logger.info(f"ML 모델이 {len(candidates)}개 패키지에 점수 부여") + + # 단계 2: 규칙 기반 필터링 + logger.info("단계 2: 규칙 기반 필터 적용") + filtered = self.rule_filter.apply_all_filters( + candidates, + user_survey + ) + + logger.info(f"필터링 후: {len(filtered)}개 패키지 남음") + + # 단계 3: 최종 순위 결정 + # 최소 점수 임계값으로 필터링 + filtered = filtered[ + filtered['predicted_score'] >= settings.MIN_SCORE_THRESHOLD + ] + + # 예측 점수로 정렬 + final_recommendations = filtered.sort_values( + 'predicted_score', + ascending=False + ).head(top_n) + + # 딕셔너리 리스트로 변환 + recommendations = final_recommendations[[ + 'pass_id', + 'name', + 'price', + 'intensity', + 'purposeTag', + 'predicted_score' + ]].to_dict('records') + + logger.info(f"{len(recommendations)}개 추천 반환") + + return recommendations + + def explain_recommendation( + self, + pass_id: int, + user_survey: Dict + ) -> Dict: + """ + 패키지가 추천된 이유에 대한 설명을 제공합니다. + + Args: + pass_id: 추천된 패키지 ID + user_survey: 사용자 설문 응답 + + Returns: + 설명 세부 정보가 포함된 딕셔너리 + """ + # TODO: SHAP 또는 특성 중요도 기반 설명 구현 + return { + 'pass_id': pass_id, + 'explanation': '사용자 선호도와 ML 모델 기반', + 'match_reasons': [] + } diff --git a/ai_recommendation/services/rule_filter.py b/ai_recommendation/services/rule_filter.py new file mode 100644 index 0000000..9f9fa25 --- /dev/null +++ b/ai_recommendation/services/rule_filter.py @@ -0,0 +1,164 @@ +""" +규칙 기반 필터링 서비스 +추천에 비즈니스 규칙을 적용 +""" +from typing import List, Dict +import pandas as pd +import logging + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +class RuleFilter: + """추천에 규칙 기반 필터링을 적용합니다.""" + + @staticmethod + def filter_by_intensity( + pass_candidates: pd.DataFrame, + user_preferred_intensity: str + ) -> pd.DataFrame: + """ + 강도 선호도로 패키지를 필터링합니다. + + Args: + pass_candidates: 후보 패키지 DataFrame + user_preferred_intensity: 사용자 선호 강도 (LOW/MID/HIGH) + + Returns: + 필터링된 DataFrame + """ + intensity_map = { + '가벼운 웜업 위주': 'LOW', + '살짝 땀이 나는 정도': 'LOW', + '땀이 흠뻑 젖도록 중강도': 'MID', + '끝나면 기진맥진 고강도': 'HIGH' + } + + target_intensity = intensity_map.get(user_preferred_intensity, 'MID') + + # 동일하거나 인접한 강도 레벨 허용 + intensity_tolerance = { + 'LOW': ['LOW', 'MID'], + 'MID': ['LOW', 'MID', 'HIGH'], + 'HIGH': ['MID', 'HIGH'] + } + + allowed_intensities = intensity_tolerance.get(target_intensity, ['MID']) + + filtered = pass_candidates[ + pass_candidates['intensity'].isin(allowed_intensities) + ] + + logger.info( + f"강도 필터: {len(pass_candidates)} -> {len(filtered)}개 패키지" + ) + + return filtered + + @staticmethod + def filter_by_purpose( + pass_candidates: pd.DataFrame, + user_purpose: str + ) -> pd.DataFrame: + """ + 목적으로 패키지를 필터링합니다. + + Args: + pass_candidates: 후보 패키지 DataFrame + user_purpose: 사용자 운동 목적 + + Returns: + 필터링된 DataFrame + """ + purpose_map = { + '다이어트': 'DIET', + '회복(통증완화, 재활 등)': 'REHAB', + '체력 증진': 'FITNESS', + '스트레스 해소': 'STRESS_RELIEF', + '취미 탐색': 'EXPLORE' + } + + target_purpose = purpose_map.get(user_purpose, 'FITNESS') + + # 목적 일치로 필터링 + filtered = pass_candidates[ + pass_candidates['purposeTag'] == target_purpose + ] + + # 결과가 너무 적으면 다른 목적도 포함 + if len(filtered) < 10: + # 충분한 후보가 없으면 predicted_score로 정렬된 모든 후보 반환 + filtered = pass_candidates.copy() + logger.info(f"{target_purpose}에 대한 일치 항목이 부족하여 모든 후보 반환") + + logger.info( + f"목적 필터: {len(pass_candidates)} -> {len(filtered)}개 패키지" + ) + + return filtered + + @staticmethod + def filter_by_sport_preference( + pass_candidates: pd.DataFrame, + preferred_sports: List[str] + ) -> pd.DataFrame: + """ + 사용자 선호 종목이 포함된 패키지를 우대합니다. + + Args: + pass_candidates: 후보 패키지 DataFrame + preferred_sports: 사용자가 관심 있는 종목 이름 리스트 + + Returns: + 일치하는 종목에 대한 점수가 높은 DataFrame + """ + if not preferred_sports: + return pass_candidates + + # pass_candidates에 종목 이름이 있는 'sports' 컬럼이 있다고 가정 + # PassItem 및 Sport 데이터와 조인 필요 + # 현재는 그대로 반환 + # TODO: 종목 매칭 로직 구현 + + return pass_candidates + + def apply_all_filters( + self, + pass_candidates: pd.DataFrame, + user_survey: Dict + ) -> pd.DataFrame: + """ + 모든 규칙 기반 필터를 적용합니다. + + Args: + pass_candidates: 후보 패키지 DataFrame + user_survey: 사용자 설문 응답 + + Returns: + 필터링된 DataFrame + """ + filtered = pass_candidates.copy() + + # 강도 필터 적용 + if 'preferred_intensity' in user_survey: + filtered = self.filter_by_intensity( + filtered, + user_survey['preferred_intensity'] + ) + + # 목적 필터 적용 + if 'purpose' in user_survey: + filtered = self.filter_by_purpose( + filtered, + user_survey['purpose'] + ) + + # 종목 선호도 우대 적용 + if 'preferred_sports' in user_survey: + filtered = self.filter_by_sport_preference( + filtered, + user_survey['preferred_sports'] + ) + + return filtered diff --git a/ai_recommendation/test_request.json b/ai_recommendation/test_request.json new file mode 100644 index 0000000..cd8f9ac --- /dev/null +++ b/ai_recommendation/test_request.json @@ -0,0 +1,9 @@ +{ + "purpose": "다이어트", + "preferred_time": "아침", + "preferred_intensity": "가벼운 웜업 위주", + "travel_time": "30분 이내", + "environment": "실내", + "preferred_sports": ["요가", "필라테스"], + "recovery_level": "보통/적당히 회복됨" +} diff --git a/src/main/java/com/api/mov/MovApplication.java b/src/main/java/com/api/mov/MovApplication.java index 7ba6f5a..c5038a9 100644 --- a/src/main/java/com/api/mov/MovApplication.java +++ b/src/main/java/com/api/mov/MovApplication.java @@ -1,6 +1,12 @@ package com.api.mov; +import com.api.mov.domain.facility.entity.Facility; +import com.api.mov.domain.facility.repository.FacilityRepository; +import com.api.mov.domain.pass.entity.Pass; +import com.api.mov.domain.pass.entity.PassItem; import com.api.mov.domain.pass.entity.Sport; +import com.api.mov.domain.pass.repository.PassItemRepository; +import com.api.mov.domain.pass.repository.PassRepository; import com.api.mov.domain.pass.repository.SportRepository; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; @@ -20,26 +26,596 @@ public static void main(String[] args) { } @Bean - public CommandLineRunner initData(SportRepository sportRepository) { + public CommandLineRunner initData( + SportRepository sportRepository, + FacilityRepository facilityRepository, + PassRepository passRepository, + PassItemRepository passItemRepository + ) { return args -> { - // DB에 데이터가 없을 때만 실행 (중복 방지) - if (sportRepository.count() == 0) { + + // ======================================== + // 1. Sport 마스터 데이터 (9종목) + // ======================================== + List sports = sportRepository.findAll(); + if (sports.isEmpty()) { System.out.println("Initializing Sport master data..."); - List sports = Arrays.asList( - Sport.builder().name("헬스/PT").build(), - Sport.builder().name("필라테스").build(), - Sport.builder().name("요가").build(), - Sport.builder().name("수영").build(), - Sport.builder().name("클라이밍").build(), - Sport.builder().name("크로스핏").build(), - Sport.builder().name("F45").build(), - Sport.builder().name("파워리프팅").build() - ); - - sportRepository.saveAll(sports); + sports = Arrays.asList( + Sport.builder().name("웨이트 & 크로스핏").build(), // ID: 1 + Sport.builder().name("실내 클라이밍").build(), // ID: 2 + Sport.builder().name("필라테스").build(), // ID: 3 + Sport.builder().name("요가").build(), // ID: 4 + Sport.builder().name("실내 수영").build(), // ID: 5 + Sport.builder().name("댄스").build(), // ID: 6 + Sport.builder().name("테니스").build(), // ID: 7 + Sport.builder().name("풋살").build(), // ID: 8 + Sport.builder().name("골프").build() // ID: 9 + ); + + sports = sportRepository.saveAll(sports); System.out.println("Sport data initialization complete."); } + + // ======================================== + // 2. Facility 샘플 데이터 (각 종목별 3개씩 = 총 27개) + // ======================================== + List allFacilities = facilityRepository.findAll(); + + Facility weight1, weight2, weight3; + Facility climb1, climb2, climb3; + Facility pilates1, pilates2, pilates3; + Facility yoga1, yoga2, yoga3; + Facility swim1, swim2, swim3; + Facility dance1, dance2, dance3; + Facility tennis1, tennis2, tennis3; + Facility futsal1, futsal2, futsal3; + Facility golf1, golf2, golf3; + + if (allFacilities.isEmpty()) { + System.out.println("Initializing Sample Facility data (27 facilities)..."); + + // 웨이트 & 크로스핏 (3개) + weight1 = facilityRepository.save(Facility.builder() + .name("파워짐 강남점").contact("010-1111-0001") + .address("서울시 강남구").detailAddress("테헤란로 123") + .price(15000).postCode("06234").sport(sports.get(0)).build()); + + weight2 = facilityRepository.save(Facility.builder() + .name("머슬팩토리 홍대점").contact("010-1111-0002") + .address("서울시 마포구").detailAddress("양화로 456") + .price(12000).postCode("04043").sport(sports.get(0)).build()); + + weight3 = facilityRepository.save(Facility.builder() + .name("크로스핏박스 역삼").contact("010-1111-0003") + .address("서울시 강남구").detailAddress("역삼로 789") + .price(18000).postCode("06235").sport(sports.get(0)).build()); + + // 실내 클라이밍 (3개) + climb1 = facilityRepository.save(Facility.builder() + .name("더클라임 홍대").contact("010-2222-0001") + .address("서울시 마포구").detailAddress("와우산로 111") + .price(16000).postCode("04043").sport(sports.get(1)).build()); + + climb2 = facilityRepository.save(Facility.builder() + .name("클라이밍파크 강남").contact("010-2222-0002") + .address("서울시 강남구").detailAddress("논현로 222") + .price(17000).postCode("06234").sport(sports.get(1)).build()); + + climb3 = facilityRepository.save(Facility.builder() + .name("볼더스 신촌점").contact("010-2222-0003") + .address("서울시 서대문구").detailAddress("신촌로 333") + .price(15000).postCode("03785").sport(sports.get(1)).build()); + + // 필라테스 (3개) + pilates1 = facilityRepository.save(Facility.builder() + .name("필라인 스튜디오").contact("010-3333-0001") + .address("서울시 강남구").detailAddress("선릉로 444") + .price(25000).postCode("06235").sport(sports.get(2)).build()); + + pilates2 = facilityRepository.save(Facility.builder() + .name("코어필라 송파점").contact("010-3333-0002") + .address("서울시 송파구").detailAddress("올림픽로 555") + .price(23000).postCode("05551").sport(sports.get(2)).build()); + + pilates3 = facilityRepository.save(Facility.builder() + .name("바디라인 필라테스").contact("010-3333-0003") + .address("서울시 서초구").detailAddress("반포대로 666") + .price(24000).postCode("06592").sport(sports.get(2)).build()); + + // 요가 (3개) + yoga1 = facilityRepository.save(Facility.builder() + .name("젠요가 센터").contact("010-4444-0001") + .address("서울시 서초구").detailAddress("강남대로 777") + .price(18000).postCode("06592").sport(sports.get(3)).build()); + + yoga2 = facilityRepository.save(Facility.builder() + .name("요가원 홍대점").contact("010-4444-0002") + .address("서울시 마포구").detailAddress("홍익로 888") + .price(16000).postCode("04043").sport(sports.get(3)).build()); + + yoga3 = facilityRepository.save(Facility.builder() + .name("힐링요가 강남점").contact("010-4444-0003") + .address("서울시 강남구").detailAddress("테헤란로 999") + .price(20000).postCode("06234").sport(sports.get(3)).build()); + + // 실내 수영 (3개) + swim1 = facilityRepository.save(Facility.builder() + .name("아쿠아스포츠 수영장").contact("010-5555-0001") + .address("서울시 송파구").detailAddress("올림픽로 101") + .price(14000).postCode("05551").sport(sports.get(4)).build()); + + swim2 = facilityRepository.save(Facility.builder() + .name("스위밍클럽 강남").contact("010-5555-0002") + .address("서울시 강남구").detailAddress("삼성로 202") + .price(15000).postCode("06234").sport(sports.get(4)).build()); + + swim3 = facilityRepository.save(Facility.builder() + .name("워터파크 수영센터").contact("010-5555-0003") + .address("서울시 마포구").detailAddress("마포대로 303") + .price(13000).postCode("04043").sport(sports.get(4)).build()); + + // 댄스 (3개) + dance1 = facilityRepository.save(Facility.builder() + .name("댄스플로우 스튜디오").contact("010-6666-0001") + .address("서울시 강남구").detailAddress("논현로 404") + .price(19000).postCode("06236").sport(sports.get(5)).build()); + + dance2 = facilityRepository.save(Facility.builder() + .name("리듬앤무브 홍대").contact("010-6666-0002") + .address("서울시 마포구").detailAddress("양화로 505") + .price(17000).postCode("04043").sport(sports.get(5)).build()); + + dance3 = facilityRepository.save(Facility.builder() + .name("댄스아카데미 강남").contact("010-6666-0003") + .address("서울시 강남구").detailAddress("역삼로 606") + .price(18000).postCode("06235").sport(sports.get(5)).build()); + + // 테니스 (3개) + tennis1 = facilityRepository.save(Facility.builder() + .name("테니스클럽 서초").contact("010-7777-0001") + .address("서울시 서초구").detailAddress("반포대로 707") + .price(22000).postCode("06592").sport(sports.get(6)).build()); + + tennis2 = facilityRepository.save(Facility.builder() + .name("코트에이스 송파").contact("010-7777-0002") + .address("서울시 송파구").detailAddress("올림픽로 808") + .price(20000).postCode("05551").sport(sports.get(6)).build()); + + tennis3 = facilityRepository.save(Facility.builder() + .name("스매시테니스 강남").contact("010-7777-0003") + .address("서울시 강남구").detailAddress("선릉로 909") + .price(23000).postCode("06234").sport(sports.get(6)).build()); + + // 풋살 (3개) + futsal1 = facilityRepository.save(Facility.builder() + .name("풋살파크 마포").contact("010-8888-0001") + .address("서울시 마포구").detailAddress("월드컵로 1010") + .price(8000).postCode("04043").sport(sports.get(7)).build()); + + futsal2 = facilityRepository.save(Facility.builder() + .name("골든풋살 강남").contact("010-8888-0002") + .address("서울시 강남구").detailAddress("강남대로 1111") + .price(9000).postCode("06234").sport(sports.get(7)).build()); + + futsal3 = facilityRepository.save(Facility.builder() + .name("풋살존 송파").contact("010-8888-0003") + .address("서울시 송파구").detailAddress("올림픽로 1212") + .price(7500).postCode("05551").sport(sports.get(7)).build()); + + // 골프 (3개) + golf1 = facilityRepository.save(Facility.builder() + .name("골프존 강남점").contact("010-9999-0001") + .address("서울시 강남구").detailAddress("테헤란로 1313") + .price(30000).postCode("06234").sport(sports.get(8)).build()); + + golf2 = facilityRepository.save(Facility.builder() + .name("스윙골프 서초점").contact("010-9999-0002") + .address("서울시 서초구").detailAddress("강남대로 1414") + .price(28000).postCode("06592").sport(sports.get(8)).build()); + + golf3 = facilityRepository.save(Facility.builder() + .name("프리미엄골프 송파").contact("010-9999-0003") + .address("서울시 송파구").detailAddress("올림픽로 1515") + .price(32000).postCode("05551").sport(sports.get(8)).build()); + + System.out.println("Facility data initialization complete (27 facilities created)."); + + } else { + // 기존 데이터 로드 + List weightFacilities = facilityRepository.findBySportId(sports.get(0).getId(), null).getContent(); + List climbFacilities = facilityRepository.findBySportId(sports.get(1).getId(), null).getContent(); + List pilatesFacilities = facilityRepository.findBySportId(sports.get(2).getId(), null).getContent(); + List yogaFacilities = facilityRepository.findBySportId(sports.get(3).getId(), null).getContent(); + List swimFacilities = facilityRepository.findBySportId(sports.get(4).getId(), null).getContent(); + List danceFacilities = facilityRepository.findBySportId(sports.get(5).getId(), null).getContent(); + List tennisFacilities = facilityRepository.findBySportId(sports.get(6).getId(), null).getContent(); + List futsalFacilities = facilityRepository.findBySportId(sports.get(7).getId(), null).getContent(); + List golfFacilities = facilityRepository.findBySportId(sports.get(8).getId(), null).getContent(); + + weight1 = weightFacilities.get(0); + weight2 = weightFacilities.get(1); + weight3 = weightFacilities.get(2); + + climb1 = climbFacilities.get(0); + climb2 = climbFacilities.get(1); + climb3 = climbFacilities.get(2); + + pilates1 = pilatesFacilities.get(0); + pilates2 = pilatesFacilities.get(1); + pilates3 = pilatesFacilities.get(2); + + yoga1 = yogaFacilities.get(0); + yoga2 = yogaFacilities.get(1); + yoga3 = yogaFacilities.get(2); + + swim1 = swimFacilities.get(0); + swim2 = swimFacilities.get(1); + swim3 = swimFacilities.get(2); + + dance1 = danceFacilities.get(0); + dance2 = danceFacilities.get(1); + dance3 = danceFacilities.get(2); + + tennis1 = tennisFacilities.get(0); + tennis2 = tennisFacilities.get(1); + tennis3 = tennisFacilities.get(2); + + futsal1 = futsalFacilities.get(0); + futsal2 = futsalFacilities.get(1); + futsal3 = futsalFacilities.get(2); + + golf1 = golfFacilities.get(0); + golf2 = golfFacilities.get(1); + golf3 = golfFacilities.get(2); + } + + // ======================================== + // 3. Pass 메타데이터 (Data 1 - AI 추천용) + // 총 30개, 모두 2종목 1회 체험 패키지 + // ======================================== + if (passRepository.count() == 0) { + System.out.println("Initializing Pass metadata (30 trial packages)..."); + + Pass p; + + // ========== LOW intensity (저강도) ========== + + // 1. 요가+필라테스 (LOW, EXPLORE) + p = passRepository.save(Pass.builder() + .name("운동 첫걸음 (요가1회+필라1회)") + .price(43000) + .description("운동이 처음이신 분들을 위한 입문 패키지입니다. 요가로 몸의 유연성을 깨우고 필라테스로 코어 근육을 느껴보세요.") + .intensity("LOW") + .purposeTag("EXPLORE") + .build()); + passItemRepository.save(PassItem.builder().pass(p).facility(yoga1).build()); + passItemRepository.save(PassItem.builder().pass(p).facility(pilates1).build()); + + // 3. 필라테스+요가 (LOW, REHAB) + p = passRepository.save(Pass.builder() + .name("몸 회복 케어 (필라1회+요가1회)") + .price(43000) + .description("허리 통증이나 잘못된 자세로 고생하시나요? 필라테스와 요가로 통증을 완화하고 바른 자세를 되찾아보세요.") + .intensity("LOW") + .purposeTag("REHAB") + .build()); + passItemRepository.save(PassItem.builder().pass(p).facility(pilates2).build()); + passItemRepository.save(PassItem.builder().pass(p).facility(yoga2).build()); + + // 4. 댄스+요가 (LOW, STRESS_RELIEF) + p = passRepository.save(Pass.builder() + .name("힐링 라이프 (댄스1회+요가1회)") + .price(35000) + .description("일상의 스트레스를 날려버리세요. 음악에 맞춰 몸을 움직이고 요가로 마음을 편안하게 만드는 힐링 패키지입니다.") + .intensity("LOW") + .purposeTag("STRESS_RELIEF") + .build()); + passItemRepository.save(PassItem.builder().pass(p).facility(dance1).build()); + passItemRepository.save(PassItem.builder().pass(p).facility(yoga3).build()); + + // ========== HIGH intensity (고강도) ========== + + // 2. 웨이트+수영 (HIGH, DIET) + p = passRepository.save(Pass.builder() + .name("체중감량 시작 (웨이트1회+수영1회)") + .price(29000) + .description("다이어트를 결심하셨나요? 웨이트 트레이닝으로 근육을 만들고 수영으로 지방을 태우는 효과적인 조합입니다.") + .intensity("HIGH") + .purposeTag("DIET") + .build()); + passItemRepository.save(PassItem.builder().pass(p).facility(weight1).build()); + passItemRepository.save(PassItem.builder().pass(p).facility(swim1).build()); + + // 5. 웨이트+클라이밍 (MID, FITNESS) + p = passRepository.save(Pass.builder() + .name("체력 업그레이드 (웨이트1회+클라이밍1회)") + .price(32000) + .description("체력의 한계를 뛰어넘고 싶으신가요? 웨이트로 근력을 키우고 클라이밍으로 전신 지구력을 향상시키는 균형잡힌 패키지입니다.") + .intensity("MID") + .purposeTag("FITNESS") + .build()); + passItemRepository.save(PassItem.builder().pass(p).facility(weight2).build()); + passItemRepository.save(PassItem.builder().pass(p).facility(climb1).build()); + + // 6. 웨이트+댄스 (MID, DIET) + p = passRepository.save(Pass.builder() + .name("탄탄 바디 만들기 (웨이트1회+댄스1회)") + .price(31000) + .description("몸매 변화를 원하신다면 이 패키지가 정답입니다. 웨이트로 근육을 만들고 댄스로 유산소 운동을 더해 탄탄한 몸을 만들어보세요.") + .intensity("MID") + .purposeTag("DIET") + .build()); + passItemRepository.save(PassItem.builder().pass(p).facility(weight3).build()); + passItemRepository.save(PassItem.builder().pass(p).facility(dance2).build()); + + // 7. 웨이트+클라이밍 (HIGH, FITNESS) + p = passRepository.save(Pass.builder() + .name("익스트림 도전 (웨이트1회+클라이밍1회)") + .price(35000) + .description("강도 높은 운동을 찾으시나요? 무거운 중량의 웨이트와 도전적인 클라이밍 루트로 한계를 돌파하는 강력한 패키지입니다.") + .intensity("HIGH") + .purposeTag("FITNESS") + .build()); + passItemRepository.save(PassItem.builder().pass(p).facility(weight1).build()); + passItemRepository.save(PassItem.builder().pass(p).facility(climb2).build()); + + // 8. 수영+필라테스 (MID, FITNESS) + p = passRepository.save(Pass.builder() + .name("수영 입문 (수영1회+필라1회)") + .price(39000) + .description("수영을 시작하고 싶으신가요? 필라테스로 수영에 필요한 코어 근육을 먼저 단련하고 수영장에서 자신감 있게 첫 물장구를 떠보세요.") + .intensity("MID") + .purposeTag("FITNESS") + .build()); + passItemRepository.save(PassItem.builder().pass(p).facility(swim2).build()); + passItemRepository.save(PassItem.builder().pass(p).facility(pilates3).build()); + + // 9. 테니스+요가 (MID, EXPLORE) + p = passRepository.save(Pass.builder() + .name("라켓 스포츠 입문 (테니스1회+요가1회)") + .price(40000) + .description("새로운 취미를 찾고 계신가요? 테니스로 라켓 스포츠의 재미를 느끼고 요가로 운동 후 근육을 이완시키는 균형잡힌 조합입니다.") + .intensity("MID") + .purposeTag("EXPLORE") + .build()); + passItemRepository.save(PassItem.builder().pass(p).facility(tennis1).build()); + passItemRepository.save(PassItem.builder().pass(p).facility(yoga1).build()); + + // 10. 풋살+웨이트 (MID, FITNESS) + p = passRepository.save(Pass.builder() + .name("팀 스포츠 체험 (풋살1회+웨이트1회)") + .price(23000) + .description("혼자가 아닌 함께하는 운동을 원하신다면! 풋살로 팀플레이의 즐거움을 느끼고 웨이트로 경기력을 높이는 실속있는 패키지입니다.") + .intensity("MID") + .purposeTag("FITNESS") + .build()); + passItemRepository.save(PassItem.builder().pass(p).facility(futsal1).build()); + passItemRepository.save(PassItem.builder().pass(p).facility(weight2).build()); + + // 11. 골프+필라테스 (LOW, EXPLORE) + p = passRepository.save(Pass.builder() + .name("골프 시작 (골프1회+필라1회)") + .price(55000) + .description("골프에 관심이 생기셨나요? 필라테스로 골프 스윙에 필요한 코어와 회전력을 키우고 스크린 골프장에서 첫 샷을 날려보세요.") + .intensity("LOW") + .purposeTag("EXPLORE") + .build()); + passItemRepository.save(PassItem.builder().pass(p).facility(golf1).build()); + passItemRepository.save(PassItem.builder().pass(p).facility(pilates1).build()); + + // 12. 클라이밍+요가 (MID, STRESS_RELIEF) + p = passRepository.save(Pass.builder() + .name("클라이밍 힐링 (클라이밍1회+요가1회)") + .price(34000) + .description("머리를 비우고 싶으신가요? 클라이밍으로 벽에만 집중하며 잡념을 날리고 요가로 몸과 마음을 이완시키는 힐링 패키지입니다.") + .intensity("MID") + .purposeTag("STRESS_RELIEF") + .build()); + passItemRepository.save(PassItem.builder().pass(p).facility(climb3).build()); + passItemRepository.save(PassItem.builder().pass(p).facility(yoga2).build()); + + // 13. 수영+댄스 (HIGH, DIET) + p = passRepository.save(Pass.builder() + .name("유산소 끝판왕 (수영1회+댄스1회)") + .price(32000) + .description("체지방을 확실하게 줄이고 싶다면 이 조합을 추천합니다. 수영으로 전신 칼로리를 태우고 댄스로 땀을 흘리며 즐겁게 다이어트하세요.") + .intensity("HIGH") + .purposeTag("DIET") + .build()); + passItemRepository.save(PassItem.builder().pass(p).facility(swim3).build()); + passItemRepository.save(PassItem.builder().pass(p).facility(dance3).build()); + + // 14. 테니스+웨이트 (MID, FITNESS) + p = passRepository.save(Pass.builder() + .name("파워 테니스 (테니스1회+웨이트1회)") + .price(37000) + .description("더 강한 스윙을 원하신다면! 웨이트로 어깨와 코어 근력을 키우고 테니스 코트에서 파워풀한 샷을 날려보세요.") + .intensity("MID") + .purposeTag("FITNESS") + .build()); + passItemRepository.save(PassItem.builder().pass(p).facility(tennis2).build()); + passItemRepository.save(PassItem.builder().pass(p).facility(weight3).build()); + + // 15. 풋살+수영 (MID, DIET) + p = passRepository.save(Pass.builder() + .name("체력 다이어트 (풋살1회+수영1회)") + .price(22000) + .description("즐겁게 살을 빼고 싶다면 이 패키지가 답입니다. 풋살로 뛰면서 칼로리를 소모하고 수영으로 무릎에 무리 없이 전신을 단련하세요.") + .intensity("MID") + .purposeTag("DIET") + .build()); + passItemRepository.save(PassItem.builder().pass(p).facility(futsal2).build()); + passItemRepository.save(PassItem.builder().pass(p).facility(swim1).build()); + + // 16. 골프+웨이트 (LOW, FITNESS) + p = passRepository.save(Pass.builder() + .name("골프 근력 강화 (골프1회+웨이트1회)") + .price(45000) + .description("골프 비거리를 늘리고 싶으신가요? 웨이트로 허리와 하체 근력을 강화하고 골프장에서 더 멀리 날아가는 샷을 경험해보세요.") + .intensity("LOW") + .purposeTag("FITNESS") + .build()); + passItemRepository.save(PassItem.builder().pass(p).facility(golf2).build()); + passItemRepository.save(PassItem.builder().pass(p).facility(weight1).build()); + + // 17. 댄스+필라테스 (LOW, STRESS_RELIEF) + p = passRepository.save(Pass.builder() + .name("리듬 힐링 (댄스1회+필라1회)") + .price(42000) + .description("일과 일상에 지쳐 있나요? 신나는 음악에 맞춰 댄스로 스트레스를 날리고 필라테스로 굳은 몸을 풀어주는 휴식 패키지입니다.") + .intensity("LOW") + .purposeTag("STRESS_RELIEF") + .build()); + passItemRepository.save(PassItem.builder().pass(p).facility(dance1).build()); + passItemRepository.save(PassItem.builder().pass(p).facility(pilates2).build()); + + // 18. 클라이밍+수영 (HIGH, FITNESS) + p = passRepository.save(Pass.builder() + .name("전신 운동 콤보 (클라이밍1회+수영1회)") + .price(31000) + .description("상하체를 모두 발달시키고 싶으신가요? 클라이밍으로 등과 팔 근육을 키우고 수영으로 하체와 심폐지구력을 강화하는 완벽한 조합입니다.") + .intensity("HIGH") + .purposeTag("FITNESS") + .build()); + passItemRepository.save(PassItem.builder().pass(p).facility(climb1).build()); + passItemRepository.save(PassItem.builder().pass(p).facility(swim2).build()); + + // 19. 테니스+필라테스 (LOW, REHAB) + p = passRepository.save(Pass.builder() + .name("부상 예방 패키지 (테니스1회+필라1회)") + .price(47000) + .description("운동 중 부상이 걱정되시나요? 필라테스로 관절을 보호하는 코어를 만들고 테니스로 안전하게 운동 강도를 높여보세요.") + .intensity("LOW") + .purposeTag("REHAB") + .build()); + passItemRepository.save(PassItem.builder().pass(p).facility(tennis3).build()); + passItemRepository.save(PassItem.builder().pass(p).facility(pilates3).build()); + + // 20. 풋살+댄스 (MID, STRESS_RELIEF) + p = passRepository.save(Pass.builder() + .name("즐거운 운동 (풋살1회+댄스1회)") + .price(26000) + .description("운동이 지루하게 느껴지시나요? 풋살로 친구들과 즐겁게 공을 차고 댄스로 신나는 음악에 몸을 맡기는 재미있는 패키지입니다.") + .intensity("MID") + .purposeTag("STRESS_RELIEF") + .build()); + passItemRepository.save(PassItem.builder().pass(p).facility(futsal3).build()); + passItemRepository.save(PassItem.builder().pass(p).facility(dance2).build()); + + // 21. 골프+요가 (LOW, STRESS_RELIEF) + p = passRepository.save(Pass.builder() + .name("골프 멘탈 강화 (골프1회+요가1회)") + .price(50000) + .description("골프는 정신력이 중요한 운동입니다. 요가로 집중력과 평정심을 기르고 골프장에서 흔들리지 않는 멘탈로 좋은 스코어를 만들어보세요.") + .intensity("LOW") + .purposeTag("STRESS_RELIEF") + .build()); + passItemRepository.save(PassItem.builder().pass(p).facility(golf3).build()); + passItemRepository.save(PassItem.builder().pass(p).facility(yoga3).build()); + + // 22. 클라이밍+웨이트 (HIGH, DIET) + p = passRepository.save(Pass.builder() + .name("근육 폭발 (클라이밍1회+웨이트1회)") + .price(33000) + .description("상체 근육을 집중적으로 키우고 싶으신가요? 클라이밍으로 등과 어깨를 자극하고 웨이트로 가슴과 팔 근육까지 완성하는 상체 특화 패키지입니다.") + .intensity("HIGH") + .purposeTag("DIET") + .build()); + passItemRepository.save(PassItem.builder().pass(p).facility(climb2).build()); + passItemRepository.save(PassItem.builder().pass(p).facility(weight2).build()); + + // 23. 수영+요가 (LOW, REHAB) + p = passRepository.save(Pass.builder() + .name("관절 회복 (수영1회+요가1회)") + .price(32000) + .description("무릎이나 관절이 안 좋으신가요? 물에서 무중력 상태로 부담 없이 운동하고 요가로 관절 가동범위를 넓히는 재활 중심 패키지입니다.") + .intensity("LOW") + .purposeTag("REHAB") + .build()); + passItemRepository.save(PassItem.builder().pass(p).facility(swim1).build()); + passItemRepository.save(PassItem.builder().pass(p).facility(yoga1).build()); + + // 24. 테니스+댄스 (MID, FITNESS) + p = passRepository.save(Pass.builder() + .name("민첩성 향상 (테니스1회+댄스1회)") + .price(39000) + .description("빠른 움직임이 필요한 스포츠를 준비 중이신가요? 테니스로 순발력을 키우고 댄스로 리듬감과 발놀림을 향상시키는 민첩성 강화 패키지입니다.") + .intensity("MID") + .purposeTag("FITNESS") + .build()); + passItemRepository.save(PassItem.builder().pass(p).facility(tennis1).build()); + passItemRepository.save(PassItem.builder().pass(p).facility(dance3).build()); + + // 25. 풋살+클라이밍 (MID, EXPLORE) + p = passRepository.save(Pass.builder() + .name("새로운 도전 (풋살1회+클라이밍1회)") + .price(24000) + .description("익숙한 운동에서 벗어나 새로운 것을 시도해보고 싶으신가요? 풋살로 팀 스포츠를, 클라이밍으로 익스트림 스포츠를 체험하는 탐험 패키지입니다.") + .intensity("MID") + .purposeTag("EXPLORE") + .build()); + passItemRepository.save(PassItem.builder().pass(p).facility(futsal1).build()); + passItemRepository.save(PassItem.builder().pass(p).facility(climb3).build()); + + // 26. 골프+수영 (LOW, FITNESS) + p = passRepository.save(Pass.builder() + .name("저강도 운동 (골프1회+수영1회)") + .price(44000) + .description("강도 높은 운동이 부담스러우신가요? 골프로 가볍게 몸을 움직이고 수영으로 천천히 심폐 기능을 키우는 편안한 운동 패키지입니다.") + .intensity("LOW") + .purposeTag("FITNESS") + .build()); + passItemRepository.save(PassItem.builder().pass(p).facility(golf1).build()); + passItemRepository.save(PassItem.builder().pass(p).facility(swim3).build()); + + // 27. 댄스+수영 (MID, DIET) + p = passRepository.save(Pass.builder() + .name("지방 연소 (댄스1회+수영1회)") + .price(31000) + .description("다이어트의 핵심은 유산소 운동입니다. 댄스로 땀을 흘리며 즐겁게 칼로리를 태우고 수영으로 전신 지방을 연소시키는 효율적인 다이어트 패키지입니다.") + .intensity("MID") + .purposeTag("DIET") + .build()); + passItemRepository.save(PassItem.builder().pass(p).facility(dance1).build()); + passItemRepository.save(PassItem.builder().pass(p).facility(swim2).build()); + + // 28. 클라이밍+필라테스 (MID, REHAB) + p = passRepository.save(Pass.builder() + .name("코어 강화 (클라이밍1회+필라1회)") + .price(41000) + .description("몸의 중심인 코어를 단단하게 만들고 싶으신가요? 클라이밍으로 실전에서 코어를 쓰는 법을 배우고 필라테스로 깊은 코어 근육까지 자극하는 집중 패키지입니다.") + .intensity("MID") + .purposeTag("REHAB") + .build()); + passItemRepository.save(PassItem.builder().pass(p).facility(climb1).build()); + passItemRepository.save(PassItem.builder().pass(p).facility(pilates1).build()); + + // 29. 테니스+풋살 (MID, EXPLORE) + p = passRepository.save(Pass.builder() + .name("구기 종목 탐험 (테니스1회+풋살1회)") + .price(30000) + .description("공을 다루는 운동에 흥미가 있으신가요? 테니스로 라켓과 공의 감각을, 풋살로 발과 공의 터치감을 익히며 구기 종목의 재미를 발견하는 패키지입니다.") + .intensity("MID") + .purposeTag("EXPLORE") + .build()); + passItemRepository.save(PassItem.builder().pass(p).facility(tennis2).build()); + passItemRepository.save(PassItem.builder().pass(p).facility(futsal2).build()); + + // 30. 골프+댄스 (LOW, STRESS_RELIEF) + p = passRepository.save(Pass.builder() + .name("여유로운 운동 (골프1회+댄스1회)") + .price(47000) + .description("운동이 부담스럽지 않은 편안한 조합을 원하신다면! 골프로 여유롭게 스윙을 즐기고 댄스로 가볍게 몸을 풀며 기분 좋게 땀 흘리는 힐링 패키지입니다.") + .intensity("LOW") + .purposeTag("STRESS_RELIEF") + .build()); + passItemRepository.save(PassItem.builder().pass(p).facility(golf2).build()); + passItemRepository.save(PassItem.builder().pass(p).facility(dance2).build()); + + + System.out.println("Pass metadata initialization complete (30 packages created)."); + } }; } } diff --git a/src/main/java/com/api/mov/domain/facility/entity/Facility.java b/src/main/java/com/api/mov/domain/facility/entity/Facility.java index f6affce..86150d1 100644 --- a/src/main/java/com/api/mov/domain/facility/entity/Facility.java +++ b/src/main/java/com/api/mov/domain/facility/entity/Facility.java @@ -47,7 +47,6 @@ public class Facility extends BaseEntity { private String weekendHours; //주말 영업시간 private String holidayClosedInfo; //휴무 안내 - @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "sport_id", nullable = false) private Sport sport; diff --git a/src/main/java/com/api/mov/domain/pass/entity/Pass.java b/src/main/java/com/api/mov/domain/pass/entity/Pass.java index 98723bc..ebfa92e 100644 --- a/src/main/java/com/api/mov/domain/pass/entity/Pass.java +++ b/src/main/java/com/api/mov/domain/pass/entity/Pass.java @@ -30,8 +30,15 @@ public class Pass extends BaseEntity { private String description; //패키지 설명 @Column(nullable = false) + @Builder.Default private Long viewCount = 0L; + @Column + private String intensity; + + @Column + private String purposeTag; + @OneToMany(mappedBy = "pass", cascade = CascadeType.ALL, orphanRemoval = true) @Builder.Default private List passItems = new ArrayList<>(); diff --git a/src/main/java/com/api/mov/domain/pass/service/PassService.java b/src/main/java/com/api/mov/domain/pass/service/PassService.java index 31fab0c..31dcbe1 100644 --- a/src/main/java/com/api/mov/domain/pass/service/PassService.java +++ b/src/main/java/com/api/mov/domain/pass/service/PassService.java @@ -5,6 +5,7 @@ import com.api.mov.domain.pass.web.dto.MyPassRes; import com.api.mov.domain.pass.web.dto.PassCreateReq; import com.api.mov.domain.pass.web.dto.PassDetailRes; +import com.api.mov.domain.pass.web.dto.PassMetadataRes; import java.util.List; @@ -15,4 +16,6 @@ public interface PassService { List getPasses(String passName, Integer minPrice, Integer maxPrice, String sortBy); PassDetailRes getPassDetail(Long passId); + + List getAllPassMetadata(); } diff --git a/src/main/java/com/api/mov/domain/pass/service/PassServiceImpl.java b/src/main/java/com/api/mov/domain/pass/service/PassServiceImpl.java index 77faaf8..60debbd 100644 --- a/src/main/java/com/api/mov/domain/pass/service/PassServiceImpl.java +++ b/src/main/java/com/api/mov/domain/pass/service/PassServiceImpl.java @@ -13,6 +13,7 @@ import com.api.mov.domain.pass.web.dto.PassCreateReq; import com.api.mov.domain.pass.web.dto.PassDetailRes; import com.api.mov.domain.pass.web.dto.PassItemInfoRes; +import com.api.mov.domain.pass.web.dto.PassMetadataRes; import com.api.mov.domain.user.entity.User; import com.api.mov.domain.user.repository.UserRepository; import com.api.mov.global.exception.CustomException; @@ -201,4 +202,13 @@ public PassDetailRes getPassDetail(Long passId) { passItemInfoList ); } + + @Override + @Transactional(readOnly = true) + public List getAllPassMetadata() { + List passes = passRepository.findAll(); + return passes.stream() + .map(PassMetadataRes::from) + .toList(); + } } diff --git a/src/main/java/com/api/mov/domain/pass/web/controller/PassController.java b/src/main/java/com/api/mov/domain/pass/web/controller/PassController.java index 7466438..157a2bf 100644 --- a/src/main/java/com/api/mov/domain/pass/web/controller/PassController.java +++ b/src/main/java/com/api/mov/domain/pass/web/controller/PassController.java @@ -5,6 +5,7 @@ import com.api.mov.domain.pass.web.dto.MyPassRes; import com.api.mov.domain.pass.web.dto.PassCreateReq; import com.api.mov.domain.pass.web.dto.PassDetailRes; +import com.api.mov.domain.pass.web.dto.PassMetadataRes; import com.api.mov.global.jwt.UserPrincipal; import com.api.mov.global.response.SuccessResponse; import lombok.RequiredArgsConstructor; @@ -61,4 +62,11 @@ public ResponseEntity> getPassDetail(@PathVariable Long passI .status(HttpStatus.OK) .body(SuccessResponse.ok(passDetailRes)); } + + // AI 추천 시스템용 패키지 메타데이터 조회 api + @GetMapping("/passes/metadata") + public ResponseEntity> getPassMetadata() { + List metadata = passService.getAllPassMetadata(); + return ResponseEntity.ok(metadata); + } } diff --git a/src/main/java/com/api/mov/domain/pass/web/dto/PassMetadataRes.java b/src/main/java/com/api/mov/domain/pass/web/dto/PassMetadataRes.java new file mode 100644 index 0000000..a0a104a --- /dev/null +++ b/src/main/java/com/api/mov/domain/pass/web/dto/PassMetadataRes.java @@ -0,0 +1,32 @@ +package com.api.mov.domain.pass.web.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * AI 추천 시스템용 패키지 메타데이터 응답 DTO + */ +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PassMetadataRes { + + private Long passId; + private String name; + private Integer price; + private String intensity; // LOW, MID, HIGH + private String purposeTag; // DIET, REHAB, FITNESS, STRESS_RELIEF, EXPLORE + + public static PassMetadataRes from(com.api.mov.domain.pass.entity.Pass pass) { + return PassMetadataRes.builder() + .passId(pass.getId()) + .name(pass.getName()) + .price(pass.getPrice()) + .intensity(pass.getIntensity()) + .purposeTag(pass.getPurposeTag()) + .build(); + } +} diff --git a/src/main/java/com/api/mov/domain/recommendation/service/AIRecommendationService.java b/src/main/java/com/api/mov/domain/recommendation/service/AIRecommendationService.java new file mode 100644 index 0000000..c568758 --- /dev/null +++ b/src/main/java/com/api/mov/domain/recommendation/service/AIRecommendationService.java @@ -0,0 +1,50 @@ +package com.api.mov.domain.recommendation.service; + +import com.api.mov.domain.recommendation.web.dto.AIRecommendationListRes; +import com.api.mov.domain.recommendation.web.dto.AISurveyRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +@Slf4j +@Service +@RequiredArgsConstructor +public class AIRecommendationService { + + private final RestTemplate restTemplate; + + @Value("${fastapi.url:http://localhost:8000}") + private String fastApiUrl; + + public AIRecommendationListRes getRecommendations(AISurveyRequest surveyRequest) { + try { + String url = fastApiUrl + "/api/recommendations"; + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + HttpEntity request = new HttpEntity<>(surveyRequest, headers); + + log.info("FastAPI 추천 요청 전송: {}", url); + + AIRecommendationListRes response = restTemplate.postForObject( + url, + request, + AIRecommendationListRes.class + ); + + log.info("FastAPI 추천 결과 수신: {} 개", response != null ? response.getTotalCount() : 0); + + return response; + + } catch (Exception e) { + log.error("FastAPI 추천 요청 실패: {}", e.getMessage(), e); + throw new RuntimeException("AI 추천 서비스 호출 실패: " + e.getMessage()); + } + } +} diff --git a/src/main/java/com/api/mov/domain/recommendation/web/controller/AIRecommendationController.java b/src/main/java/com/api/mov/domain/recommendation/web/controller/AIRecommendationController.java new file mode 100644 index 0000000..edd3494 --- /dev/null +++ b/src/main/java/com/api/mov/domain/recommendation/web/controller/AIRecommendationController.java @@ -0,0 +1,38 @@ +package com.api.mov.domain.recommendation.web.controller; + +import com.api.mov.domain.recommendation.web.dto.AIRecommendationListRes; +import com.api.mov.domain.recommendation.web.dto.AISurveyRequest; +import com.api.mov.domain.recommendation.service.AIRecommendationService; +import com.api.mov.global.response.SuccessResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/ai") +public class AIRecommendationController { + + private final AIRecommendationService aiRecommendationService; + + /** + * AI 기반 패키지 추천 API + */ + @PostMapping("/recommendations") + public ResponseEntity> getAIRecommendations( + @RequestBody AISurveyRequest surveyRequest + ) { + log.info("AI 추천 요청 수신: purpose={}, intensity={}", + surveyRequest.getPurpose(), + surveyRequest.getPreferred_intensity()); + + AIRecommendationListRes recommendations = aiRecommendationService.getRecommendations(surveyRequest); + + return ResponseEntity + .status(HttpStatus.OK) + .body(SuccessResponse.ok(recommendations)); + } +} diff --git a/src/main/java/com/api/mov/domain/recommendation/web/dto/AIRecommendationListRes.java b/src/main/java/com/api/mov/domain/recommendation/web/dto/AIRecommendationListRes.java new file mode 100644 index 0000000..47337bc --- /dev/null +++ b/src/main/java/com/api/mov/domain/recommendation/web/dto/AIRecommendationListRes.java @@ -0,0 +1,20 @@ +package com.api.mov.domain.recommendation.web.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AIRecommendationListRes { + private List recommendations; + + @JsonProperty("total_count") + private Integer totalCount; +} diff --git a/src/main/java/com/api/mov/domain/recommendation/web/dto/AIRecommendationRes.java b/src/main/java/com/api/mov/domain/recommendation/web/dto/AIRecommendationRes.java new file mode 100644 index 0000000..3e69460 --- /dev/null +++ b/src/main/java/com/api/mov/domain/recommendation/web/dto/AIRecommendationRes.java @@ -0,0 +1,26 @@ +package com.api.mov.domain.recommendation.web.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AIRecommendationRes { + @JsonProperty("pass_id") + private Long passId; + + private String name; + private Integer price; + private String intensity; + + @JsonProperty("purposeTag") + private String purposeTag; + + @JsonProperty("predicted_score") + private Double predictedScore; +} diff --git a/src/main/java/com/api/mov/domain/recommendation/web/dto/AISurveyRequest.java b/src/main/java/com/api/mov/domain/recommendation/web/dto/AISurveyRequest.java new file mode 100644 index 0000000..d0b8f62 --- /dev/null +++ b/src/main/java/com/api/mov/domain/recommendation/web/dto/AISurveyRequest.java @@ -0,0 +1,24 @@ +package com.api.mov.domain.recommendation.web.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AISurveyRequest { + private String purpose; // 운동 목적 + private String preferred_time; // 선호 운동 시간 + private String preferred_intensity; // 선호 운동 강도 + private String travel_time; // 이동 가능 시간 + private String environment; // 운동 환경 (실내/실외) + private List preferred_sports; // 관심 운동 종목 + private String recovery_level; // 회복 정도 + private String budget_range; // 1회 기준 패키지 예산 + private List avoid_factors; // 피하고 싶은 요소 +} diff --git a/src/main/java/com/api/mov/global/config/RestTemplateConfig.java b/src/main/java/com/api/mov/global/config/RestTemplateConfig.java new file mode 100644 index 0000000..cd1be18 --- /dev/null +++ b/src/main/java/com/api/mov/global/config/RestTemplateConfig.java @@ -0,0 +1,18 @@ +package com.api.mov.global.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class RestTemplateConfig { + + @Bean + public RestTemplate restTemplate() { + SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); + factory.setConnectTimeout(5000); // 5초 + factory.setReadTimeout(10000); // 10초 + return new RestTemplate(factory); + } +} diff --git a/src/main/java/com/api/mov/global/config/SecurityConfig.java b/src/main/java/com/api/mov/global/config/SecurityConfig.java index fd5d3fd..cd062fe 100644 --- a/src/main/java/com/api/mov/global/config/SecurityConfig.java +++ b/src/main/java/com/api/mov/global/config/SecurityConfig.java @@ -43,6 +43,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() .requestMatchers("/api/auth/signup","api/auth/login").permitAll() .requestMatchers("/api/passes").permitAll() + .requestMatchers("/api/passes/metadata").permitAll() // AI 추천 시스템용 + .requestMatchers("/api/ai/recommendations").permitAll() // AI 추천 API .requestMatchers(HttpMethod.GET,"/api/facilities/**").hasRole("USER") .requestMatchers(HttpMethod.POST,"/api/facilities/**").hasRole("USER")