From ecbf1700c4d9155223d53f9fc80db3c3228d07cc Mon Sep 17 00:00:00 2001 From: zzuhannn Date: Thu, 13 Nov 2025 18:58:17 +0900 Subject: [PATCH 01/12] =?UTF-8?q?feat:=20pass=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=EC=97=90=20=EA=B0=95=EB=8F=84=EC=99=80=20=EC=9A=B4?= =?UTF-8?q?=EB=8F=99=20=EB=AA=A9=EC=A0=81=20=ED=95=84=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/api/mov/domain/pass/entity/Pass.java | 6 ++++++ 1 file changed, 6 insertions(+) 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..cf02223 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 @@ -32,6 +32,12 @@ public class Pass extends BaseEntity { @Column(nullable = false) 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<>(); From 979e69ef0ef884e4e8d9d0f3359c064becdfa67d Mon Sep 17 00:00:00 2001 From: zzuhannn Date: Thu, 13 Nov 2025 19:11:42 +0900 Subject: [PATCH 02/12] =?UTF-8?q?refactor:=20=ED=8F=89=EC=A0=90=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=20=EB=B0=8F=20=EA=B4=80=EB=A0=A8=20=EB=A9=94?= =?UTF-8?q?=EC=86=8C=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/mov/domain/facility/entity/Facility.java | 14 -------------- 1 file changed, 14 deletions(-) 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 6b6ff74..5b1e380 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,9 +47,6 @@ public class Facility extends BaseEntity { private String weekendHours; //주말 영업시간 private String holidayClosedInfo; //휴무 안내 - @Column(nullable = false) - private Double rating = 0.0; //평균 평점 - @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "sport_id", nullable = false) private Sport sport; @@ -62,15 +59,4 @@ public class Facility extends BaseEntity { private List reservations = new ArrayList<>(); - //평점 메서드 - public void updateRating() { - if (reviews == null || reviews.isEmpty()){ - this.rating = 0.0; - } else { - this.rating = reviews.stream() - .mapToDouble(Review::getRating) - .average() - .orElse(0.0); - } - } } From 207c8cdda2ded2613d039941bf3de835f2ea616b Mon Sep 17 00:00:00 2001 From: zzuhannn Date: Thu, 13 Nov 2025 20:59:52 +0900 Subject: [PATCH 03/12] =?UTF-8?q?fix:=20Pass=20viewCount=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=EC=97=90=20@Builder.Default=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/api/mov/domain/pass/entity/Pass.java | 1 + 1 file changed, 1 insertion(+) 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 cf02223..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,6 +30,7 @@ public class Pass extends BaseEntity { private String description; //패키지 설명 @Column(nullable = false) + @Builder.Default private Long viewCount = 0L; @Column From c23de34b5d390ba914f1e91d1d9d8c764470e82b Mon Sep 17 00:00:00 2001 From: zzuhannn Date: Thu, 13 Nov 2025 21:00:18 +0900 Subject: [PATCH 04/12] =?UTF-8?q?feat:=20=EB=8D=94=EB=AF=B8=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=83=9D=EC=84=B1=20'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/api/mov/MovApplication.java | 606 +++++++++++++++++- 1 file changed, 591 insertions(+), 15 deletions(-) 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)."); + } }; } } From f9a47afd2b9db4106767d08141b2405130d23401 Mon Sep 17 00:00:00 2001 From: zzuhannn Date: Thu, 13 Nov 2025 23:53:26 +0900 Subject: [PATCH 05/12] =?UTF-8?q?feat:=20fastAPI=20=EC=84=9C=EB=B2=84?= =?UTF-8?q?=EC=97=90=EC=84=9C=20db=EC=97=90=20=EC=A1=B4=EC=9E=AC=ED=95=98?= =?UTF-8?q?=EB=8A=94=20pass=20=EC=A1=B0=ED=9A=8C=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/api/mov/domain/pass/service/PassService.java | 3 +++ .../api/mov/domain/pass/service/PassServiceImpl.java | 10 ++++++++++ 2 files changed, 13 insertions(+) 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(); + } } From 072b562d902b464c54c88663ef5d5fc3cba9c90c Mon Sep 17 00:00:00 2001 From: zzuhannn Date: Thu, 13 Nov 2025 23:54:59 +0900 Subject: [PATCH 06/12] =?UTF-8?q?feat:=20fastAPI=20=EC=84=9C=EB=B2=84?= =?UTF-8?q?=EC=97=90=EC=84=9C=20db=EC=97=90=20=EC=A1=B4=EC=9E=AC=ED=95=98?= =?UTF-8?q?=EB=8A=94=20pass=20=EC=A1=B0=ED=9A=8C=20=EC=97=94=EB=93=9C?= =?UTF-8?q?=ED=8F=AC=EC=9D=B8=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mov/domain/pass/web/controller/PassController.java | 8 ++++++++ 1 file changed, 8 insertions(+) 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); + } } From f690b6ac14925fb6230720551fdd49847d2e20fd Mon Sep 17 00:00:00 2001 From: zzuhannn Date: Fri, 14 Nov 2025 00:00:43 +0900 Subject: [PATCH 07/12] =?UTF-8?q?feat:=20=EC=A0=84=EC=B2=B4=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=20=EC=A1=B0=ED=9A=8C=20=EB=B0=98=ED=99=98=20?= =?UTF-8?q?dto=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/pass/web/dto/PassMetadataRes.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/main/java/com/api/mov/domain/pass/web/dto/PassMetadataRes.java 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(); + } +} From 4573178d5f3c2661a02b865a30c819cf0fd37163 Mon Sep 17 00:00:00 2001 From: zzuhannn Date: Fri, 14 Nov 2025 00:01:38 +0900 Subject: [PATCH 08/12] =?UTF-8?q?feat:=20securityConfig=EC=97=90=20permitA?= =?UTF-8?q?ll=20=EC=97=94=EB=93=9C=ED=8F=AC=EC=9D=B8=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/api/mov/global/config/SecurityConfig.java | 2 ++ 1 file changed, 2 insertions(+) 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") From 6c30871a7444f07410b68137c054e0f248b2a559 Mon Sep 17 00:00:00 2001 From: zzuhannn Date: Fri, 14 Nov 2025 00:07:52 +0900 Subject: [PATCH 09/12] =?UTF-8?q?feat:=20AI=EB=A5=BC=20=EC=9D=B4=EC=9A=A9?= =?UTF-8?q?=ED=95=B4=EC=84=9C=20=EC=84=A4=EB=AC=B8=20=EA=B8=B0=EB=B0=98=20?= =?UTF-8?q?=ED=8C=A8=ED=82=A4=EC=A7=80=20=EC=B6=94=EC=B2=9C=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/AIRecommendationService.java | 50 +++++++++++++++++++ .../AIRecommendationController.java | 38 ++++++++++++++ .../web/dto/AIRecommendationListRes.java | 20 ++++++++ .../web/dto/AIRecommendationRes.java | 26 ++++++++++ .../web/dto/AISurveyRequest.java | 24 +++++++++ 5 files changed, 158 insertions(+) create mode 100644 src/main/java/com/api/mov/domain/recommendation/service/AIRecommendationService.java create mode 100644 src/main/java/com/api/mov/domain/recommendation/web/controller/AIRecommendationController.java create mode 100644 src/main/java/com/api/mov/domain/recommendation/web/dto/AIRecommendationListRes.java create mode 100644 src/main/java/com/api/mov/domain/recommendation/web/dto/AIRecommendationRes.java create mode 100644 src/main/java/com/api/mov/domain/recommendation/web/dto/AISurveyRequest.java 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; // 피하고 싶은 요소 +} From 921fba5a8b9d09acd6fc6321170a87ece3f3317d Mon Sep 17 00:00:00 2001 From: zzuhannn Date: Fri, 14 Nov 2025 00:10:58 +0900 Subject: [PATCH 10/12] =?UTF-8?q?feat:=20RestTemplate=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=20=ED=8C=8C=EC=9D=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mov/global/config/RestTemplateConfig.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 src/main/java/com/api/mov/global/config/RestTemplateConfig.java 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); + } +} From 5a5a8e740f9c2762a364d8bde9585d9f20ae19e8 Mon Sep 17 00:00:00 2001 From: zzuhannn Date: Fri, 14 Nov 2025 00:20:11 +0900 Subject: [PATCH 11/12] =?UTF-8?q?feat:=20AI=20=EA=B8=B0=EB=B0=98=20?= =?UTF-8?q?=ED=8C=A8=ED=82=A4=EC=A7=80=20=EC=B6=94=EC=B2=9C=20=EC=8B=9C?= =?UTF-8?q?=EC=8A=A4=ED=85=9C=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EB=B0=B0?= =?UTF-8?q?=ED=8F=AC=20=EC=84=A4=EC=A0=95=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd.yml | 41 +- .github/workflows/ci.yml | 29 +- ai_recommendation/.dockerignore | 34 + ai_recommendation/.gitignore | 55 + ai_recommendation/Dockerfile | 22 + ai_recommendation/README.md | 152 +++ ai_recommendation/api/__init__.py | 1 + ai_recommendation/api/schemas.py | 80 ++ ai_recommendation/api/server.py | 175 +++ .../catboost_info/catboost_training.json | 104 ++ .../catboost_info/learn/events.out.tfevents | Bin 0 -> 4798 bytes .../catboost_info/learn_error.tsv | 101 ++ ai_recommendation/catboost_info/time_left.tsv | 101 ++ ai_recommendation/config/settings.py | 45 + ai_recommendation/data/__init__.py | 1 + ai_recommendation/data/training_data.csv | 1001 +++++++++++++++++ ai_recommendation/models/__init__.py | 1 + ai_recommendation/models/catboost_model.cbm | Bin 0 -> 113688 bytes ai_recommendation/models/predictor.py | 117 ++ ai_recommendation/models/trainer.py | 130 +++ ai_recommendation/requirements.txt | 9 + ai_recommendation/services/__init__.py | 1 + .../services/recommendation_service.py | 119 ++ ai_recommendation/services/rule_filter.py | 164 +++ ai_recommendation/test_request.json | 9 + 25 files changed, 2475 insertions(+), 17 deletions(-) create mode 100644 ai_recommendation/.dockerignore create mode 100644 ai_recommendation/.gitignore create mode 100644 ai_recommendation/Dockerfile create mode 100644 ai_recommendation/README.md create mode 100644 ai_recommendation/api/__init__.py create mode 100644 ai_recommendation/api/schemas.py create mode 100644 ai_recommendation/api/server.py create mode 100644 ai_recommendation/catboost_info/catboost_training.json create mode 100644 ai_recommendation/catboost_info/learn/events.out.tfevents create mode 100644 ai_recommendation/catboost_info/learn_error.tsv create mode 100644 ai_recommendation/catboost_info/time_left.tsv create mode 100644 ai_recommendation/config/settings.py create mode 100644 ai_recommendation/data/__init__.py create mode 100644 ai_recommendation/data/training_data.csv create mode 100644 ai_recommendation/models/__init__.py create mode 100644 ai_recommendation/models/catboost_model.cbm create mode 100644 ai_recommendation/models/predictor.py create mode 100644 ai_recommendation/models/trainer.py create mode 100644 ai_recommendation/requirements.txt create mode 100644 ai_recommendation/services/__init__.py create mode 100644 ai_recommendation/services/recommendation_service.py create mode 100644 ai_recommendation/services/rule_filter.py create mode 100644 ai_recommendation/test_request.json 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..de0cb22 --- /dev/null +++ b/ai_recommendation/README.md @@ -0,0 +1,152 @@ +# MOV AI 추천 시스템 + +CatBoost와 규칙 기반 필터링을 사용한 AI 기반 운동 패키지 추천 시스템 + +## 아키텍처 + +``` +ai_recommendation/ +├── config/ # 설정 파일 +├── data/ # 학습 데이터 (CSV) +├── models/ # ML 모델 (trainer, predictor) +├── services/ # 비즈니스 로직 (추천, 필터링) +├── api/ # FastAPI 서버 +└── utils/ # 유틸리티 +``` + +## 설치 및 실행 + +### 1. 의존성 설치 + +```bash +cd ai_recommendation +pip install -r requirements.txt +``` + +### 2. 학습 데이터 준비 + +`data/training_data.csv`에 학습 데이터 배치 + +예상 컬럼: +- 사용자 특성: purpose, preferredIntensity, interestedSportIds, preferredEnvironment, avoidFactors, recoveryCondition +- 패키지 특성: price +- 타겟: purchased_pass_id + +### 3. 모델 학습 + +```bash +python -m models.trainer +``` + +실행 결과: +- `data/training_data.csv`에서 데이터 로드 +- CatBoost 모델 학습 +- 모델을 `models/catboost_model.cbm`에 저장 + +### 4. API 서버 시작 + +```bash +python -m api.server +``` + +또는 uvicorn으로 직접 실행: + +```bash +uvicorn api.server:app --host 0.0.0.0 --port 8000 --reload +``` + +## API 사용법 + +### 추천 받기 + +```bash +POST /api/recommendations +Content-Type: application/json + +{ + "purpose": "다이어트", + "preferred_time": "저녁(18시~23시)", + "preferred_intensity": "땀이 흠뻑 젖도록 중강도", + "travel_time": "30분 이내", + "environment": "실내", + "preferred_sports": ["요가", "필라테스"], + "recovery_level": "보통/적당히 회복됨" +} +``` + +응답: +```json +{ + "recommendations": [ + { + "pass_id": 2, + "name": "체중감량 시작 (웨이트1회+수영1회)", + "price": 29000, + "intensity": "HIGH", + "purposeTag": "DIET", + "predicted_score": 0.85 + } + ], + "total_count": 10 +} +``` + +### 헬스 체크 + +```bash +GET /health +``` + +### 모델 재학습 트리거 + +```bash +POST /api/train +``` + +## Spring Boot 연동 + +FastAPI 서버는 Spring Boot 엔드포인트를 호출합니다: + +``` +GET http://localhost:8080/api/passes/metadata +``` + +이 엔드포인트는 모든 패키지 메타데이터를 JSON 배열로 반환해야 합니다. + +## 설정 + +`config/settings.py` 편집 또는 `.env` 파일 생성: + +```env +SPRING_BOOT_URL=http://localhost:8080 +API_PORT=8000 +TOP_N_RECOMMENDATIONS=10 +MIN_SCORE_THRESHOLD=0.3 +``` + +## 개발 + +테스트 실행: +```bash +pytest +``` + +코드 포맷팅: +```bash +black . +``` + +## 주요 특징 + +- CatBoost를 사용한 범주형 특성 효율적 처리 +- 하이브리드 접근: ML 점수 산출 + 규칙 기반 필터링 +- 30개 패키지와 9개 운동 종목을 위한 설계 +- Spring Boot 백엔드와 완전 통합 + +## 추천 흐름 + +1. **사용자 설문** → FastAPI 서버로 전송 +2. **Spring Boot에서 패키지 메타데이터 가져오기** +3. **ML 모델로 점수 예측** (CatBoost) +4. **규칙 기반 필터링** 적용 (강도, 목적 등) +5. **Top 10 추천 반환** 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 0000000000000000000000000000000000000000..bd798879df84ecb88e4466c495aa4bb832dbc38d GIT binary patch literal 4798 zcmZ|SX-pJn7{GBBSXejtj@uvy|(YRL! zUVD0y`1YSU6S&_3yrBm5vc;oOxYq#Q=)ySZ2ii3<+z(rUS1B4J-cC*a3%vg>@Vc{q z5`WwOcpUDx0pB2`zHFkJ;d#^qJ}Y&U^e4~Mr{aAr;1dPZpFPKAcz(5kKkx98^li%{ zgm_;E_>?yi;){lp6LGH#e6bnz<0rW4xYq;zg!l!WXWYpJ_wNCJwQ7X;i*XK}xYq|h zMlejgo?NLl?hSx%Q>4DP(A@y{hQRO7eNOtDn&%kiHv+zCQA~UmE3zE#8v|eGPW@_6 zPdn~SfR8U8BK?X{%~jl+0za5c{Ze)q!}Z|;zeAV$h??26c;5{8yBgFRWXvAGy*coy zt%GF#OfJt9_ZGl^okjgiD@i2oErE|#rQYJy=>^N5%+e$N9R%R*t2vO_x8YhU+*D(t@9##+&cijFQ596eXW?+g3sdw{Nt8qWd3x0V{P2;0zT&}>IG)T`M7rmUZ79C?@(tJ?)kvS zHg}VGG6O;w_A3|Qr;k&A!MWK6@4Etj-%e7mEVRtU`|ktKW>Y`w zqbI?=8}MVbon#)%6+bQ9!~cc)bn3avwG8iT571vTq<+EqnJeD+1U_E)l+5Fmevx5+ z-UGZreFyPzK^_nBz8C7Rw+Lc!zZdvB_i5jtzk%WV%^P?#9`&~Rf&F-YAMo3^QGdfk zBMJ9Dz_Yrako7Lub4qak0r2YC)N8j6hv0rc@bS*nKh53ShWi7+Hx0CtdHi}f;kfq& z-soHEZ>H58!@VEy7A4ewAEL(a|L6}qPlx(W?LEzSKLGXfzI}?g4+K6~)JE3pQDegJ zd<6lY8bp0k!ptXlKN$G6(N@yGEI79r_a6e!Potjidh-_ULx7j}p+39m!YJ-Tf#(@e zU!I_Q5BFifcZ{`=`HxETe!zV=@WTVtd*04uxc?6VFH=Q*rkM`I_mu#6<4Wp>^T!|I z^Be;HE5J_7iqN9+Ac z7Q_F0B=B_#)L(k?`3&9{0)Iq~`tpw++u=S6c$d=O$@R%eN@Lh>j{%=|gZk*9SpL5y C$F55N literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2ddc8330d7a639edcf3a7d9cff803431b9d284a3 GIT binary patch literal 113688 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 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": "보통/적당히 회복됨" +} From 643bdb2c41b8e9bb772eb4e7d33867b5adb23ce6 Mon Sep 17 00:00:00 2001 From: zzuhannn Date: Fri, 14 Nov 2025 00:33:10 +0900 Subject: [PATCH 12/12] =?UTF-8?q?chore:=20README.md=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ai_recommendation/README.md | 221 ++++++++++++++++++++++++++---------- 1 file changed, 159 insertions(+), 62 deletions(-) diff --git a/ai_recommendation/README.md b/ai_recommendation/README.md index de0cb22..5b4bb84 100644 --- a/ai_recommendation/README.md +++ b/ai_recommendation/README.md @@ -2,79 +2,116 @@ CatBoost와 규칙 기반 필터링을 사용한 AI 기반 운동 패키지 추천 시스템 -## 아키텍처 +## 시스템 아키텍처 + +``` +프론트엔드 (React) + ↓ +Spring Boot (8080 포트) + ↓ (프록시) +FastAPI (8000 포트) + ↓ (ML 추천 엔진) +MySQL RDS (패키지 데이터) +``` + +## 프로젝트 구조 ``` ai_recommendation/ -├── config/ # 설정 파일 -├── data/ # 학습 데이터 (CSV) -├── models/ # ML 모델 (trainer, predictor) +├── config/ # 설정 파일 (settings.py) +├── data/ # 학습 데이터 (training_data.csv) +├── models/ # ML 모델 (trainer, predictor, catboost_model.cbm) ├── services/ # 비즈니스 로직 (추천, 필터링) -├── api/ # FastAPI 서버 -└── utils/ # 유틸리티 +├── api/ # FastAPI 서버 및 스키마 +├── Dockerfile # FastAPI Docker 이미지 +├── requirements.txt # Python 의존성 +└── .dockerignore # Docker 빌드 제외 파일 ``` -## 설치 및 실행 +## 로컬 개발 환경 설정 -### 1. 의존성 설치 +### 1. 가상환경 생성 및 활성화 ```bash cd ai_recommendation -pip install -r requirements.txt +python3 -m venv venv +source venv/bin/activate # Mac/Linux +# venv\Scripts\activate # Windows ``` -### 2. 학습 데이터 준비 +### 2. 의존성 설치 -`data/training_data.csv`에 학습 데이터 배치 +```bash +pip install -r requirements.txt +``` -예상 컬럼: -- 사용자 특성: purpose, preferredIntensity, interestedSportIds, preferredEnvironment, avoidFactors, recoveryCondition -- 패키지 특성: price -- 타겟: purchased_pass_id +### 3. 모델 학습 (선택) -### 3. 모델 학습 +이미 학습된 모델(`models/catboost_model.cbm`)이 있으면 생략 가능 ```bash python -m models.trainer ``` -실행 결과: -- `data/training_data.csv`에서 데이터 로드 -- CatBoost 모델 학습 -- 모델을 `models/catboost_model.cbm`에 저장 - -### 4. API 서버 시작 +### 4. FastAPI 서버 시작 ```bash -python -m api.server +uvicorn api.server:app --host 0.0.0.0 --port 8000 --reload ``` -또는 uvicorn으로 직접 실행: +### 5. Spring Boot 서버 연동 + +FastAPI는 Spring Boot의 패키지 메타데이터를 가져오므로, Spring Boot 서버가 먼저 실행되어야 합니다: ```bash -uvicorn api.server:app --host 0.0.0.0 --port 8000 --reload +# 별도 터미널에서 Spring Boot 실행 +cd .. # 프로젝트 루트로 이동 +./gradlew bootRun ``` -## API 사용법 +## Docker 배포 -### 추천 받기 +### 로컬에서 Docker 빌드 테스트 ```bash -POST /api/recommendations -Content-Type: application/json +# 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": "보통/적당히 회복됨" + "preferred_sports": ["웨이트 & 크로스핏", "실내 수영"], + "recovery_level": "평범함", + "budget_range": "이만원대", + "avoid_factors": ["가벼운 웜업 위주"] } ``` -응답: +**응답 예시**: ```json { "recommendations": [ @@ -91,62 +128,122 @@ Content-Type: application/json } ``` -### 헬스 체크 +### 2. FastAPI 직접 호출 (테스트용) + +**엔드포인트**: `POST http://localhost:8000/api/recommendations` +**설명**: FastAPI 서버에 직접 추천 요청 (개발/테스트용) + +### 3. 헬스 체크 ```bash -GET /health +GET http://localhost:8000/health +``` + +**응답**: +```json +{ + "status": "healthy", + "model_loaded": true, + "version": "1.0.0" +} ``` -### 모델 재학습 트리거 +### 4. 모델 재학습 ```bash -POST /api/train +POST http://localhost:8000/api/train ``` -## Spring Boot 연동 +**주의**: 프로덕션에서는 인증 추가 권장 -FastAPI 서버는 Spring Boot 엔드포인트를 호출합니다: +## Spring Boot 통합 + +FastAPI는 다음 Spring Boot 엔드포인트를 호출합니다: ``` GET http://localhost:8080/api/passes/metadata ``` -이 엔드포인트는 모든 패키지 메타데이터를 JSON 배열로 반환해야 합니다. +이 엔드포인트는 모든 패키지 메타데이터(pass_id, name, price, intensity, purposeTag)를 JSON 배열로 반환합니다. -## 설정 +## 환경 설정 -`config/settings.py` 편집 또는 `.env` 파일 생성: +`config/settings.py` 또는 `.env` 파일에서 설정 가능: ```env -SPRING_BOOT_URL=http://localhost:8080 +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 +TOP_N_RECOMMENDATIONS=10 # 반환할 추천 개수 +MIN_SCORE_THRESHOLD=0.3 # 최소 점수 임계값 ``` -## 개발 +## 주요 특징 + +### 1. 하이브리드 추천 시스템 +- **ML 기반 점수**: CatBoost를 사용한 협업 필터링 +- **규칙 기반 필터**: 사용자 선호도(강도, 목적, 환경 등) 반영 + +### 2. 9개 설문 항목 지원 +- 운동 목적, 선호 시간, 선호 강도, 이동 시간 +- 운동 환경, 관심 운동 종목, 회복 정도 +- 예산 범위, 피하고 싶은 요소 + +### 3. 실시간 통합 +- Spring Boot와 FastAPI 간 실시간 데이터 교환 +- 패키지 메타데이터 자동 동기화 + +### 4. 자동 배포 +- Docker 컨테이너화 +- GitHub Actions CI/CD 파이프라인 + +## 추천 알고리즘 흐름 -테스트 실행: -```bash -pytest ``` +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 -black . +python -m models.trainer # 모델 재학습 ``` -## 주요 특징 - -- CatBoost를 사용한 범주형 특성 효율적 처리 -- 하이브리드 접근: ML 점수 산출 + 규칙 기반 필터링 -- 30개 패키지와 9개 운동 종목을 위한 설계 -- Spring Boot 백엔드와 완전 통합 +### Docker 이미지 빌드 실패 +- `requirements.txt` 파일 확인 +- `.dockerignore`에 `venv/` 포함되었는지 확인 -## 추천 흐름 +## 라이선스 -1. **사용자 설문** → FastAPI 서버로 전송 -2. **Spring Boot에서 패키지 메타데이터 가져오기** -3. **ML 모델로 점수 예측** (CatBoost) -4. **규칙 기반 필터링** 적용 (강도, 목적 등) -5. **Top 10 추천 반환** +이 프로젝트는 해커톤용으로 제작되었습니다.