diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 1ccfff7..3b47ae3 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -25,11 +25,15 @@ jobs: java-version: '17' distribution: "adopt" - - name: Make application.properties + - name: Make application.properties & Firebase Service Account Key & DB Config run: | cd ./src/main/resources touch ./application.properties echo "${{ secrets.PROPERTIES }}" > ./application.properties + echo "${{ secrets.RDS_CONFIG_INI }}" > ./crawling/config.ini + mkdir firebase + touch ./firebase/once-firebase-adminsdk.json + echo '${{ secrets.ONCE_FIREBASE_ADMINSDK }}' > ./firebase/once-firebase-adminsdk.json shell: bash - name: Build with Gradle @@ -51,11 +55,15 @@ jobs: java-version: '17' distribution: "adopt" - - name: Make application.properties + - name: Make application.properties & Firebase Service Account Key & DB Config run: | cd ./src/main/resources touch ./application.properties echo "${{ secrets.PROPERTIES }}" > ./application.properties + echo "${{ secrets.RDS_CONFIG_INI }}" > ./crawling/config.ini + mkdir firebase + touch ./firebase/once-firebase-adminsdk.json + echo '${{ secrets.ONCE_FIREBASE_ADMINSDK }}' > ./firebase/once-firebase-adminsdk.json shell: bash - name: Build with Gradle @@ -66,9 +74,7 @@ jobs: - name: Docker build & push to prod run: | echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin - # docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} - # docker build -f Dockerfile -t ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO }} . - docker build -t ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO }} . + docker build -f Dockerfile -t ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO }} . docker push ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO }} - name: Deploy start @@ -83,5 +89,5 @@ jobs: sudo docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} sudo docker rm -f $(docker ps -qa) sudo docker pull ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO }} - docker-compose up -d - docker image prune -f + sudo docker-compose up -d + sudo docker image prune -f \ No newline at end of file diff --git a/.gitignore b/.gitignore index 4fa1fea..c5fab5c 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,6 @@ out/ ### Security ### -application.properties \ No newline at end of file +application.properties +once-firebase-adminsdk.json +config.ini \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 5852303..70aea44 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,25 @@ -FROM openjdk:17-jdk +FROM openjdk:17-jdk-slim-bullseye + +RUN apt-get update && apt-get install -y python3 python3-pip wget unzip curl && apt-get install -y systemd && apt-get install -y tzdata + +RUN ln -snf /usr/share/zoneinfo/Asia/Seoul /etc/localtime + +RUN wget https://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/google-chrome-stable_114.0.5735.198-1_amd64.deb && \ + apt -y install ./google-chrome-stable_114.0.5735.198-1_amd64.deb + +RUN wget -O /tmp/chromedriver.zip https://chromedriver.storage.googleapis.com/114.0.5735.90/chromedriver_linux64.zip && \ + unzip /tmp/chromedriver.zip -d /usr/bin && \ + chmod +x /usr/bin/chromedriver + +COPY ./requirements.txt . +RUN pip install --no-cache-dir --upgrade pip && \ + pip install -r requirements.txt + +COPY ./src/main/resources/crawling /crawling + ARG JAR_FILE=build/libs/once-0.0.1-SNAPSHOT.jar COPY ${JAR_FILE} /app.jar -ENTRYPOINT ["java","-jar","/app.jar"] +ENV TZ=Asia/Seoul + +ENTRYPOINT ["java","-jar","/app.jar"] \ No newline at end of file diff --git a/build.gradle b/build.gradle index c10190d..e04cf8d 100644 --- a/build.gradle +++ b/build.gradle @@ -42,6 +42,14 @@ dependencies { implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' + + //FCM + implementation 'com.google.firebase:firebase-admin:9.2.0' + + //CODEF + implementation 'com.googlecode.json-simple:json-simple:1.1.1' + + implementation 'commons-io:commons-io:2.11.0' } tasks.named('test') { @@ -50,4 +58,17 @@ tasks.named('test') { jar { enabled = false +} +sourceSets{ + main{ + resources { + srcDirs 'src/main/resources' + } + } +} + +tasks { + processResources { + duplicatesStrategy = org.gradle.api.file.DuplicatesStrategy.INCLUDE + } } \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 4c9d5e1..5309a0a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,4 +8,13 @@ services: - 8080 restart: always ports: - - 8080:8080 \ No newline at end of file + - 8080:8080 + environment: + - TZ=Asia/Seoul + shm_size: 2gb + logging: + driver: json-file + options: + mode: non-blocking + stdin_open: true + tty: true \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c550437 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +beautifulsoup4==4.12.2 +pandas==2.1.3 +selenium==4.15.2 +webdriver-manager==4.0.1 +pymysql==1.1.0 +boto3==1.19.0 +Pillow==9.0.0 \ No newline at end of file diff --git a/src/main/java/ewha/lux/once/OnceApplication.java b/src/main/java/ewha/lux/once/OnceApplication.java index 9a78a7e..324912d 100644 --- a/src/main/java/ewha/lux/once/OnceApplication.java +++ b/src/main/java/ewha/lux/once/OnceApplication.java @@ -3,9 +3,13 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; @EnableJpaAuditing @SpringBootApplication +@EnableScheduling +@EnableAsync public class OnceApplication { public static void main(String[] args) { diff --git a/src/main/java/ewha/lux/once/domain/card/controller/CardController.java b/src/main/java/ewha/lux/once/domain/card/controller/CardController.java index 7d0ee61..ab43784 100644 --- a/src/main/java/ewha/lux/once/domain/card/controller/CardController.java +++ b/src/main/java/ewha/lux/once/domain/card/controller/CardController.java @@ -1,8 +1,9 @@ package ewha.lux.once.domain.card.controller; -import ewha.lux.once.domain.card.dto.CardGoalRequestDto; -import ewha.lux.once.domain.card.dto.CardPerformanceRequestDto; +import com.fasterxml.jackson.core.JsonProcessingException; +import ewha.lux.once.domain.card.dto.*; import ewha.lux.once.domain.card.service.CardService; +import ewha.lux.once.domain.card.service.CrawlingService; import ewha.lux.once.global.common.CommonResponse; import ewha.lux.once.global.common.CustomException; import ewha.lux.once.global.common.ResponseCode; @@ -12,6 +13,7 @@ import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; + @Controller @RestController @RequiredArgsConstructor @@ -19,6 +21,47 @@ public class CardController { private final CardService cardService; + private final CrawlingService crawlingService; + + // ** 추후 삭제해야 함 - 테스트용 ** ================================== + @GetMapping("/test/{companyID}") + @ResponseBody + public CommonResponse testtest(@AuthenticationPrincipal UserAccount user, @PathVariable("companyID") int companyId) { + try { + crawlingService.cardCrawlingTest(companyId); + return new CommonResponse<>(ResponseCode.SUCCESS); + } catch (CustomException e) { + return new CommonResponse<>(e.getStatus()); + } + } + + @GetMapping("/test/summary") + @ResponseBody + public CommonResponse testSummary(@AuthenticationPrincipal UserAccount user, @RequestBody TestSummaryDto testSummaryDto) { + try { + cardService.updateBenefitSummaryTest(testSummaryDto.getPrompt(), testSummaryDto.getModel_name()); + return new CommonResponse<>(ResponseCode.SUCCESS); + } catch (CustomException e) { + return new CommonResponse<>(e.getStatus()); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + @GetMapping("/test/summary/index") + @ResponseBody + public CommonResponse testSummaryByIndex(@AuthenticationPrincipal UserAccount user, @RequestBody TestSummaryIndexDto testSummaryIndexDto) { + try { + cardService.updateBenefitSummaryTestByIndex(testSummaryIndexDto.getPrompt(), testSummaryIndexDto.getModel_name(), testSummaryIndexDto.getStart_index(), testSummaryIndexDto.getEnd_index()); + return new CommonResponse<>(ResponseCode.SUCCESS); + } catch (CustomException e) { + return new CommonResponse<>(e.getStatus()); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + // ============================================================ // [Get] 마이월렛 조회 @GetMapping("") @@ -65,4 +108,51 @@ public CommonResponse cardGoal(@AuthenticationPrincipal UserAccount user, @Re return new CommonResponse<>(e.getStatus()); } } -} + + // [Get] CODEF 보유 카드 조회 + @PostMapping("/list") + @ResponseBody + public CommonResponse codefCardList(@AuthenticationPrincipal UserAccount user, @RequestBody CodefCardListRequestDto codefCardListRequestDto) { + try { + return new CommonResponse<>(ResponseCode.SUCCESS, cardService.getCodefCardList(user.getUsers(), codefCardListRequestDto)); + } catch (CustomException e) { + return new CommonResponse<>(e.getStatus()); + } + } + + // [Post] 주카드 등록 + @PostMapping("/main") + @ResponseBody + public CommonResponse registerMainCard(@AuthenticationPrincipal UserAccount user, @RequestBody MainCardRequestDto mainCardRequestDto) { + try { + cardService.postRegisterCard(user.getUsers(), mainCardRequestDto); + return new CommonResponse<>(ResponseCode.SUCCESS); + } catch (CustomException e) { + return new CommonResponse<>(e.getStatus()); + } + } + + // [Get] 주카드 실적 업데이트 + @GetMapping("/main/performance") + @ResponseBody + public CommonResponse registerMainCard(@AuthenticationPrincipal UserAccount user) { + try { + cardService.updateOwnedCardsPerformance(user.getUsers()); + return new CommonResponse<>(ResponseCode.SUCCESS); + } catch (CustomException e) { + return new CommonResponse<>(e.getStatus()); + } + } + + // [Get] 카드사 연결 현황 + @GetMapping("/connect") + @ResponseBody + public CommonResponse checkConnectedCardCompany (@AuthenticationPrincipal UserAccount user) { + try { + return new CommonResponse<>(ResponseCode.SUCCESS,cardService.getConnectedCardCompany(user.getUsers())); + } catch (CustomException e) { + return new CommonResponse<>(e.getStatus()); + } + } + +} \ No newline at end of file diff --git a/src/main/java/ewha/lux/once/domain/card/dto/CodefCardListRequestDto.java b/src/main/java/ewha/lux/once/domain/card/dto/CodefCardListRequestDto.java new file mode 100644 index 0000000..90c1563 --- /dev/null +++ b/src/main/java/ewha/lux/once/domain/card/dto/CodefCardListRequestDto.java @@ -0,0 +1,17 @@ +package ewha.lux.once.domain.card.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class CodefCardListRequestDto { + private String code; + private String id; + private String password; + +} diff --git a/src/main/java/ewha/lux/once/domain/card/dto/CodefCardListResponseDto.java b/src/main/java/ewha/lux/once/domain/card/dto/CodefCardListResponseDto.java new file mode 100644 index 0000000..156df77 --- /dev/null +++ b/src/main/java/ewha/lux/once/domain/card/dto/CodefCardListResponseDto.java @@ -0,0 +1,15 @@ +package ewha.lux.once.domain.card.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class CodefCardListResponseDto { + private String cardName; + private String cardImg; +} diff --git a/src/main/java/ewha/lux/once/domain/card/dto/ConnectedCardCompanyResponseDto.java b/src/main/java/ewha/lux/once/domain/card/dto/ConnectedCardCompanyResponseDto.java new file mode 100644 index 0000000..3ad55bc --- /dev/null +++ b/src/main/java/ewha/lux/once/domain/card/dto/ConnectedCardCompanyResponseDto.java @@ -0,0 +1,15 @@ +package ewha.lux.once.domain.card.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class ConnectedCardCompanyResponseDto { + private String cardCompanyName; + private String connectedAt; +} diff --git a/src/main/java/ewha/lux/once/domain/card/dto/GoogleMapPlaceResponseDto.java b/src/main/java/ewha/lux/once/domain/card/dto/GoogleMapPlaceResponseDto.java new file mode 100644 index 0000000..2a14e7b --- /dev/null +++ b/src/main/java/ewha/lux/once/domain/card/dto/GoogleMapPlaceResponseDto.java @@ -0,0 +1,26 @@ +package ewha.lux.once.domain.card.dto; + +import java.util.List; + +import lombok.ToString; + +@ToString +public class GoogleMapPlaceResponseDto { + + private List places; + + public GoogleMapPlaceResponseDto() { + } + + public GoogleMapPlaceResponseDto(List places) { + this.places = places; + } + + public List getPlaces() { + return places; + } + + public void setPlaces(List places) { + this.places = places; + } +} \ No newline at end of file diff --git a/src/main/java/ewha/lux/once/domain/card/dto/MainCardRequestDto.java b/src/main/java/ewha/lux/once/domain/card/dto/MainCardRequestDto.java new file mode 100644 index 0000000..e997c79 --- /dev/null +++ b/src/main/java/ewha/lux/once/domain/card/dto/MainCardRequestDto.java @@ -0,0 +1,17 @@ +package ewha.lux.once.domain.card.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class MainCardRequestDto { + private String code; + private String cardName; + +} diff --git a/src/main/java/ewha/lux/once/domain/card/dto/MyWalletResponseDto.java b/src/main/java/ewha/lux/once/domain/card/dto/MyWalletResponseDto.java index fd7a5e1..634162c 100644 --- a/src/main/java/ewha/lux/once/domain/card/dto/MyWalletResponseDto.java +++ b/src/main/java/ewha/lux/once/domain/card/dto/MyWalletResponseDto.java @@ -14,6 +14,7 @@ public class MyWalletResponseDto { @NoArgsConstructor @Builder public static class MyWalletProfileDto { + private String nickname; List ownedCardList; } @@ -24,6 +25,7 @@ public static class MyWalletProfileDto { public static class OwnedCardListDto { private Long ownedCardId; private String cardName; + private String cardCompany; private int cardType; private String cardImg; private boolean isMaincard; @@ -39,6 +41,7 @@ public static class OwnedCardListDto { @Builder public static class CardBenefitListDto { private String category; + private String name; private String benefit; } } diff --git a/src/main/java/ewha/lux/once/domain/card/dto/Place.java b/src/main/java/ewha/lux/once/domain/card/dto/Place.java new file mode 100644 index 0000000..64680d8 --- /dev/null +++ b/src/main/java/ewha/lux/once/domain/card/dto/Place.java @@ -0,0 +1,34 @@ +package ewha.lux.once.domain.card.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@ToString +@Getter +@Setter +@AllArgsConstructor +public class Place { + private String formattedAddress; + private Location location; + private DisplayName displayName; + + @Getter + @Setter + @ToString + @AllArgsConstructor + public static class Location{ + private double latitude; + private double longitude; + + } + @Getter + @Setter + @ToString + @AllArgsConstructor + public static class DisplayName{ + private String text; + private String languageCode; + } +} \ No newline at end of file diff --git a/src/main/java/ewha/lux/once/domain/card/dto/SearchStoresRequestDto.java b/src/main/java/ewha/lux/once/domain/card/dto/SearchStoresRequestDto.java new file mode 100644 index 0000000..e274279 --- /dev/null +++ b/src/main/java/ewha/lux/once/domain/card/dto/SearchStoresRequestDto.java @@ -0,0 +1,16 @@ +package ewha.lux.once.domain.card.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class SearchStoresRequestDto { + private double latitude; + private double longitude; + +} diff --git a/src/main/java/ewha/lux/once/domain/card/dto/SearchStoresResponseDto.java b/src/main/java/ewha/lux/once/domain/card/dto/SearchStoresResponseDto.java new file mode 100644 index 0000000..d7db473 --- /dev/null +++ b/src/main/java/ewha/lux/once/domain/card/dto/SearchStoresResponseDto.java @@ -0,0 +1,17 @@ +package ewha.lux.once.domain.card.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class SearchStoresResponseDto { + private String store; + private String storeName; + private double latitude; + private double longitude; +} diff --git a/src/main/java/ewha/lux/once/domain/card/dto/TestSummaryDto.java b/src/main/java/ewha/lux/once/domain/card/dto/TestSummaryDto.java new file mode 100644 index 0000000..c2c2817 --- /dev/null +++ b/src/main/java/ewha/lux/once/domain/card/dto/TestSummaryDto.java @@ -0,0 +1,15 @@ +package ewha.lux.once.domain.card.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class TestSummaryDto { + private String prompt; + private String model_name; +} diff --git a/src/main/java/ewha/lux/once/domain/card/dto/TestSummaryIndexDto.java b/src/main/java/ewha/lux/once/domain/card/dto/TestSummaryIndexDto.java new file mode 100644 index 0000000..22923f2 --- /dev/null +++ b/src/main/java/ewha/lux/once/domain/card/dto/TestSummaryIndexDto.java @@ -0,0 +1,17 @@ +package ewha.lux.once.domain.card.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class TestSummaryIndexDto { + private String prompt; + private String model_name; + private long start_index; + private long end_index; +} diff --git a/src/main/java/ewha/lux/once/domain/card/entity/BenefitSummary.java b/src/main/java/ewha/lux/once/domain/card/entity/BenefitSummary.java new file mode 100644 index 0000000..a7c80f4 --- /dev/null +++ b/src/main/java/ewha/lux/once/domain/card/entity/BenefitSummary.java @@ -0,0 +1,30 @@ +package ewha.lux.once.domain.card.entity; + +import ewha.lux.once.global.common.BaseEntity; +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Table(name="benefit_summary") +@AllArgsConstructor +@NoArgsConstructor(access= AccessLevel.PROTECTED) +@Getter +@Builder +public class BenefitSummary extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "benefit_summary_id") + private Long id; + + @Column(name = "benefit_field", nullable = false) + private String benefitField; + + @Lob + @Column(name = "benefit_contents", nullable = false, columnDefinition = "LONGTEXT") + private String benefitContents; + + @ManyToOne + @JoinColumn(name = "cardId") + private Card card; +} diff --git a/src/main/java/ewha/lux/once/domain/card/entity/Card.java b/src/main/java/ewha/lux/once/domain/card/entity/Card.java index b1265f5..c03c7c8 100644 --- a/src/main/java/ewha/lux/once/domain/card/entity/Card.java +++ b/src/main/java/ewha/lux/once/domain/card/entity/Card.java @@ -27,12 +27,9 @@ public class Card extends BaseEntity { private String imgUrl; @Lob - @Column(name = "benefits") + @Column(name = "benefits", columnDefinition = "LONGTEXT") private String benefits; - @Column(name = "benefitSummary") - private String benefitSummary; - @Enumerated(EnumType.STRING) @Column(name = "type") private CardType type; diff --git a/src/main/java/ewha/lux/once/domain/card/entity/ConnectedCardCompany.java b/src/main/java/ewha/lux/once/domain/card/entity/ConnectedCardCompany.java new file mode 100644 index 0000000..330c428 --- /dev/null +++ b/src/main/java/ewha/lux/once/domain/card/entity/ConnectedCardCompany.java @@ -0,0 +1,32 @@ +package ewha.lux.once.domain.card.entity; + +import ewha.lux.once.domain.user.entity.Users; +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDateTime; + +@Entity +@Table(name="ConnectedCardCompany") +@Getter +@Setter +@Builder +@NoArgsConstructor(access= AccessLevel.PROTECTED) +@AllArgsConstructor +public class ConnectedCardCompany { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "connectedCardCompanyId") + private Long id; + + @ManyToOne + @JoinColumn(name = "userId") + private Users users; + + @Column(name = "cardCompany", nullable = false) + private String cardCompany; + + @Column(name = "name", nullable = false) + private LocalDateTime connectedAt; + +} diff --git a/src/main/java/ewha/lux/once/domain/card/entity/EventSummary.java b/src/main/java/ewha/lux/once/domain/card/entity/EventSummary.java new file mode 100644 index 0000000..231209c --- /dev/null +++ b/src/main/java/ewha/lux/once/domain/card/entity/EventSummary.java @@ -0,0 +1,37 @@ +package ewha.lux.once.domain.card.entity; + +import jakarta.persistence.*; +import lombok.*; + +import java.sql.Date; + +@Entity +@Table(name="event_summary") +@AllArgsConstructor +@NoArgsConstructor(access= AccessLevel.PROTECTED) +@Getter +@Builder +public class EventSummary { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "event_summary_id") + private Long id; + + @Column(name = "event_field") + private String eventField; + + @Lob + @Column(name = "event_contents") + private String eventContents; + + @Column(name = "start_date") + private Date startDate; + + @Column(name = "expire_date") + private Date expireDate; + + @ManyToOne + @JoinColumn(name = "cardCompanyId") + private CardCompany cardCompany; +} diff --git a/src/main/java/ewha/lux/once/domain/card/entity/OwnedCard.java b/src/main/java/ewha/lux/once/domain/card/entity/OwnedCard.java index ec556ed..9e5b545 100644 --- a/src/main/java/ewha/lux/once/domain/card/entity/OwnedCard.java +++ b/src/main/java/ewha/lux/once/domain/card/entity/OwnedCard.java @@ -39,4 +39,6 @@ public class OwnedCard extends BaseEntity { public void releaseMaincard() { this.isMain = false; } + + public void setMaincard() { this.isMain = true; } } diff --git a/src/main/java/ewha/lux/once/domain/card/service/CardService.java b/src/main/java/ewha/lux/once/domain/card/service/CardService.java index 337a5b8..26f8961 100644 --- a/src/main/java/ewha/lux/once/domain/card/service/CardService.java +++ b/src/main/java/ewha/lux/once/domain/card/service/CardService.java @@ -1,35 +1,52 @@ package ewha.lux.once.domain.card.service; -import ewha.lux.once.domain.card.dto.CardGoalRequestDto; -import ewha.lux.once.domain.card.dto.CardPerformanceRequestDto; -import ewha.lux.once.domain.card.dto.MontlyBenefitResponseDto; -import ewha.lux.once.domain.card.dto.MyWalletResponseDto; +import com.fasterxml.jackson.core.JsonProcessingException; +import ewha.lux.once.domain.card.dto.*; +import ewha.lux.once.domain.card.entity.BenefitSummary; import ewha.lux.once.domain.card.entity.Card; +import ewha.lux.once.domain.card.entity.ConnectedCardCompany; import ewha.lux.once.domain.card.entity.OwnedCard; +import ewha.lux.once.domain.home.dto.BenefitDto; import ewha.lux.once.domain.home.entity.ChatHistory; +import ewha.lux.once.domain.home.service.CODEFAPIService; +import ewha.lux.once.domain.home.service.CODEFAsyncService; +import ewha.lux.once.domain.home.service.OpenaiService; import ewha.lux.once.domain.user.entity.Users; import ewha.lux.once.global.common.CustomException; import ewha.lux.once.global.common.ResponseCode; -import ewha.lux.once.global.repository.CardRepository; -import ewha.lux.once.global.repository.ChatHistoryRepository; -import ewha.lux.once.global.repository.OwnedCardRepository; -import ewha.lux.once.global.repository.UsersRepository; +import ewha.lux.once.global.repository.*; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; import java.util.stream.Collectors; +import static ewha.lux.once.domain.home.service.HomeService.getCategory; + @Service @RequiredArgsConstructor +@Slf4j public class CardService { private final OwnedCardRepository ownedCardRepository; private final ChatHistoryRepository chatHistoryRepository; private final UsersRepository usersRepository; + private final CardRepository cardRepository; + private final CardCompanyRepository cardCompanyRepository; + private final BenefitSummaryRepository benefitSummaryRepository; + private final ConnectedCardCompanyRepository connectedCardCompanyRepository; + private final CODEFAPIService codefapi; + private final CODEFAsyncService codefAsyncService; + private final OpenaiService openaiService; + @Value("${google-map.api-key}") + private String apiKey; public MyWalletResponseDto.MyWalletProfileDto getMyWalletInfo(Users nowUser) throws CustomException { List ownedCards = ownedCardRepository.findOwnedCardByUsers(nowUser); @@ -40,30 +57,36 @@ public MyWalletResponseDto.MyWalletProfileDto getMyWalletInfo(Users nowUser) thr List ownedCardList = ownedCards.stream() .map(ownedCard -> { - String cardSummary = ownedCard.getCard().getBenefitSummary(); - List cardBenefitList = splitCardSummary(cardSummary); + List cardSummary = benefitSummaryRepository.findByCard(ownedCard.getCard()); + List cardBenefitList = new ArrayList<>(); + for (BenefitSummary summary : cardSummary) { + String category= getCategory(summary.getBenefitField()+summary.getBenefitContents()); + cardBenefitList.add(new MyWalletResponseDto.CardBenefitListDto(category,summary.getBenefitField(), summary.getBenefitContents())); + } return new MyWalletResponseDto.OwnedCardListDto( ownedCard.getId(), ownedCard.getCard().getName(), + ownedCard.getCard().getCardCompany().getName(), ownedCard.getCard().getType().ordinal(), ownedCard.getCard().getImgUrl(), ownedCard.isMain(), ownedCard.getPerformanceCondition(), ownedCard.getCurrentPerformance(), - ownedCard.getPerformanceCondition() - ownedCard.getCurrentPerformance(), + Math.max(ownedCard.getPerformanceCondition() - ownedCard.getCurrentPerformance(), 0), cardBenefitList ); }) .toList(); return MyWalletResponseDto.MyWalletProfileDto.builder() + .nickname(nowUser.getNickname()) .ownedCardList(ownedCardList) .build(); } public void postCardPerformance(Users nowUser, CardPerformanceRequestDto cardPerformanceRequestDto) throws CustomException { - OwnedCard ownedCard = ownedCardRepository.findOwnedCardByCardIdAndUsers(cardPerformanceRequestDto.getOwnedCardId(), nowUser); - if(ownedCard != null) { + OwnedCard ownedCard = ownedCardRepository.findOwnedCardByIdAndUsers(cardPerformanceRequestDto.getOwnedCardId(), nowUser); + if (ownedCard != null) { ownedCard.setPerformanceCondition(cardPerformanceRequestDto.getPerformanceCondition()); ownedCardRepository.save(ownedCard); } else { @@ -74,13 +97,13 @@ public void postCardPerformance(Users nowUser, CardPerformanceRequestDto cardPer public MontlyBenefitResponseDto.MontlyBenefitProfileDto getMontlyBenefitInfo(Users nowUser, int month) throws CustomException { List chatHistories = chatHistoryRepository.findByUsers(nowUser); - if(chatHistories.isEmpty()){ + if (chatHistories.isEmpty()) { new CustomException(ResponseCode.CHAT_HISTORY_NOT_FOUND); } int receivedSum = chatHistories.stream() .filter(chatHistory -> chatHistory.isHasPaid() && chatHistory.getCreatedAt().getMonthValue() == month) - .mapToInt(ChatHistory::getPaymentAmount) + .mapToInt(ChatHistory::getDiscount) .sum(); List categories = chatHistories.stream() @@ -103,11 +126,12 @@ public MontlyBenefitResponseDto.MontlyBenefitProfileDto getMontlyBenefitInfo(Use List benefitList = categories.stream() .map(category -> new MontlyBenefitResponseDto.BenefitListDto( category, - categoryGetDiscount.getOrDefault(category,0), - categoryGetPaymentAmount.getOrDefault(category,0) + categoryGetDiscount.getOrDefault(category, 0), + categoryGetPaymentAmount.getOrDefault(category, 0) )) .collect(Collectors.toList()); + return MontlyBenefitResponseDto.MontlyBenefitProfileDto.builder() .month(month) .receivedSum(receivedSum) @@ -131,9 +155,218 @@ private List splitCardSummary(String car if (parts.length == 2) { String category = parts[0].trim(); String benefit = parts[1].trim(); - cardBenefitList.add(new MyWalletResponseDto.CardBenefitListDto(category, benefit)); + cardBenefitList.add(new MyWalletResponseDto.CardBenefitListDto("카테고리",category, benefit)); } } return cardBenefitList; } + + public List getCodefCardList(Users nowUser, CodefCardListRequestDto codefCardListRequestDto) throws CustomException { + // 커넥티드 아이디 + String connectedId; + if (nowUser.getConnectedId() == null) { // 저장되어있지 않은 경우 + // 계정 생성 + connectedId = codefapi.CreateConnectedID(codefCardListRequestDto); + nowUser.setConnectedId(connectedId); + usersRepository.save(nowUser); + } else if (codefapi.IsRegistered(codefCardListRequestDto.getCode(), nowUser.getConnectedId()) == "0") { // 커넥티드 아이디가 해당 카드사와 연결되어있지 않은 경우 + // 계정 추가 + connectedId = codefapi.AddToConnectedID(nowUser, codefCardListRequestDto); + } else { + connectedId = nowUser.getConnectedId(); + } + JSONArray dataArray = codefapi.GetCardList(codefCardListRequestDto.getCode(), connectedId); + + List cardDTOList = new ArrayList<>(); + for (Object obj : dataArray) { + JSONObject dataObject = (JSONObject) obj; + String resCardName = (String) dataObject.get("resCardName"); + String resImageLink = (String) dataObject.get("resImageLink"); + + CodefCardListResponseDto cardDTO = new CodefCardListResponseDto(resCardName, resImageLink); + cardDTOList.add(cardDTO); + } + String companyname= cardCompanyRepository.findByCode(codefCardListRequestDto.getCode()).get().getName(); + Optional existingRecord = connectedCardCompanyRepository.findByUsersAndCardCompany(nowUser, companyname); + + if (existingRecord.isPresent()) { + ConnectedCardCompany record = existingRecord.get(); + record.setConnectedAt(LocalDateTime.now()); // 연결 일자 업데이트 + connectedCardCompanyRepository.save(record); + } else { + ConnectedCardCompany record = ConnectedCardCompany.builder() + .users(nowUser) + .cardCompany(companyname) + .connectedAt(LocalDateTime.now()) + .build(); + connectedCardCompanyRepository.save(record); + } + return cardDTOList; + + } + + // 주카드 등록 + public void postRegisterCard(Users nowUser, MainCardRequestDto mainCardRequestDto) throws CustomException { + // 카드 + Optional optionalCard = cardRepository.findCardByName(mainCardRequestDto.getCardName()); + Card card = optionalCard.orElseThrow(() -> new CustomException(ResponseCode.CARD_NOT_FOUND)); + // 보유 카드 가져오기 + Optional optionalOwnedCard = ownedCardRepository.findOwnedCardByUsersAndCard(nowUser, card); + OwnedCard ownedCard; + if (optionalOwnedCard.isPresent()) { + // 이미 등록된 경우 + ownedCard = optionalOwnedCard.get(); + } else { + ownedCard = OwnedCard.builder() + .users(nowUser) + .card(card) + .build(); + } + + String connectedId = nowUser.getConnectedId(); + + // 실적 조회 + HashMap performResult = codefapi.Performace(mainCardRequestDto.getCode(), connectedId, mainCardRequestDto.getCardName()); + int performanceCondition = (int) performResult.get("performanceCondition"); + int currentPerformance = (int) performResult.get("currentPerformance"); + String cardNo = (String) performResult.get("resCardNo"); + + // 단골가게 저장 + codefAsyncService.saveFavorite(mainCardRequestDto.getCode(), connectedId, ownedCard, nowUser, cardNo); + + ownedCard.setMaincard(); + ownedCard.setPerformanceCondition(performanceCondition); + ownedCard.setCurrentPerformance(currentPerformance); + + ownedCardRepository.save(ownedCard); + } + + // 보유 주카드 실적 업데이트 + public void updateOwnedCardsPerformance(Users nowUser) throws CustomException { + codefAsyncService.updateOwnedCardsPerformanceCodef(nowUser); + } + + // 매주 월요일 06:00 AM 카드 혜택 정보 요약 작업 + @Scheduled(cron = "0 0 6 ? * 1") + public void updateBenefitSummary() throws CustomException, JsonProcessingException { + + List cardList = cardRepository.findAll(); + + int index = 1; + for (Card card : cardList) { + // 기존의 BenefitSummary 삭제 + List existingSummaries = benefitSummaryRepository.findByCard(card); + benefitSummaryRepository.deleteAll(existingSummaries); + + log.info("[" + card.getName() + "] - 카드 혜택 요약 중... (" + index + "/" + cardList.size() + ")"); + BenefitDto[] benefitJson = openaiService.gptBenefitSummary(card.getBenefits()); + + for (BenefitDto benefit : benefitJson) { + BenefitSummary benefitSummary = BenefitSummary.builder() + .benefitField(benefit.getBenefit_field()) + .benefitContents(benefit.getContent()) + .card(card) + .build(); + + benefitSummaryRepository.save(benefitSummary); + } + index++; + } + log.info("전체 카드 혜택 요약 완료"); + } + + public List getConnectedCardCompany(Users nowUser)throws CustomException{ + List connectedList = connectedCardCompanyRepository.findAllByUsers(nowUser); + if (connectedList.isEmpty()) { + throw new CustomException(ResponseCode.NO_CONNECTED_CARD_COMPANY); + } + + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd"); + List responseDtoList = connectedList.stream() + .map(connected -> { + String companyName = connected.getCardCompany(); + String connectedAt = connected.getConnectedAt().format(formatter); + return new ConnectedCardCompanyResponseDto(companyName, connectedAt); + }) + .collect(Collectors.toList()); + return responseDtoList; + } + + // ** 추후 삭제해야 함 - 테스트용 ** ================================== + // GET /card/test/summary : 전체 카드 혜택 요약 + public void updateBenefitSummaryTest(String prompt, String model_name) throws CustomException, JsonProcessingException { + + List cardList = cardRepository.findAll(); + + int index = 1; + for (Card card : cardList) { + // 기존의 BenefitSummary 삭제 + List existingSummaries = benefitSummaryRepository.findByCard(card); + benefitSummaryRepository.deleteAll(existingSummaries); + + log.info("[" + card.getName() + "] - 카드 혜택 요약 중... (" + index + "/" + cardList.size() + ")"); + + BenefitDto[] benefitJson = openaiService.gptBenefitSummaryTest(card.getBenefits(), prompt, model_name); + + if (benefitJson == null) { + System.out.println("===========PASS=========" + card.getName()); +// cardRepository.delete(card); + continue; + } + for (BenefitDto benefit : benefitJson) { + BenefitSummary benefitSummary = BenefitSummary.builder() + .benefitField(benefit.getBenefit_field()) + .benefitContents(benefit.getContent()) + .card(card) + .build(); + + benefitSummaryRepository.save(benefitSummary); + } + index++; + } + log.info("전체 카드 혜택 요약 완료"); + } + + /* + GET /card/test/summary/index : 일부 카드 혜택 요약 + "prompt": 프롬프트 내용, + "model_name": open_ai model, + "start_index": 시작 인덱스, + "end_index": 종료 인덱스 (포함) + */ + public void updateBenefitSummaryTestByIndex(String prompt, String model_name, long start_index, long end_index) throws CustomException, JsonProcessingException { + + List cardList = cardRepository.findByIdBetween(start_index, end_index); + + + int index = 1; + for (Card card : cardList) { + // 기존의 BenefitSummary 삭제 + List existingSummaries = benefitSummaryRepository.findByCard(card); + benefitSummaryRepository.deleteAll(existingSummaries); + + log.info("[" + card.getName() + " (card_id=" + card.getId() + ")] - 카드 혜택 요약 중... (" + index + "/" + cardList.size() + ")"); + + BenefitDto[] benefitJson = openaiService.gptBenefitSummaryTest(card.getBenefits(), prompt, model_name); + + if (benefitJson == null) { + System.out.println("===========PASS=========" + card.getName()); +// cardRepository.delete(card); + continue; + } + for (BenefitDto benefit : benefitJson) { + BenefitSummary benefitSummary = BenefitSummary.builder() + .benefitField(benefit.getBenefit_field()) + .benefitContents(benefit.getContent()) + .card(card) + .build(); + + benefitSummaryRepository.save(benefitSummary); + } + index++; + } + log.info("일부 카드 혜택 요약 완료"); + } + // ============================================================ + } diff --git a/src/main/java/ewha/lux/once/domain/card/service/CrawlingService.java b/src/main/java/ewha/lux/once/domain/card/service/CrawlingService.java new file mode 100644 index 0000000..88a8a47 --- /dev/null +++ b/src/main/java/ewha/lux/once/domain/card/service/CrawlingService.java @@ -0,0 +1,91 @@ +package ewha.lux.once.domain.card.service; + +import ewha.lux.once.global.common.CustomException; +import ewha.lux.once.global.common.ResponseCode; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.util.List; +import java.util.stream.Collectors; + +@Service +@Slf4j +@RequiredArgsConstructor +public class CrawlingService { + private static final Logger LOG = LoggerFactory.getLogger(CrawlingService.class); + + // 매주 월요일 00:00 카드 혜택 크롤링 + @Scheduled(cron = "0 0 0 ? * 1") + public void cardCrawling() throws CustomException { + String[] cardCompanyList = {"Kookmin", "Hana", "Samsung", "Shinhan", "Lotte", "Hyundai"}; + for (String cardCompany : cardCompanyList){ + crawling(cardCompany); + } + // 카드 혜택 요약 진행 + } + + // ** 추후 삭제해야 함 - 테스트용 ** ================================== + public void cardCrawlingTest(int companyId) throws CustomException { + String[] cardCompanyList = {"Kookmin", "Hyundai", "Samsung", "Shinhan", "Lotte", "Hana"}; + + crawling(cardCompanyList[companyId - 1]); + + } + // ============================================================ + + + private static void crawling(String cardCompany) throws CustomException{ + LOG.info(cardCompany+" 크롤링 시작"); + executeFile(cardCompany+"/credit.py"); + executeInsertData(cardCompany,"Credit"); + executeFile(cardCompany+"/debit.py"); + executeInsertData(cardCompany,"Debit"); + } + + private static void executeFile(String path) throws CustomException { + try { + ProcessBuilder pb = new ProcessBuilder("python3", "-u", "/crawling/"+path); + pb.redirectErrorStream(true); + Process p = pb.start(); + BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream())); + + + String line; + while ((line = br.readLine()) != null) { + // 실행 결과 처리 + LOG.info(line); + } + + p.waitFor(); + + } catch (Exception e){ + throw new CustomException(ResponseCode.CARD_BENEFITS_CRAWLING_FAIL); + } + } + private static void executeInsertData(String firstInput, String secondInput) throws CustomException { + try { + ProcessBuilder pb = new ProcessBuilder("python3", "/crawling/DatabaseInsert.py",firstInput,secondInput); + pb.redirectErrorStream(true); + Process p = pb.start(); + BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream())); + + List results; + + results = br.lines().collect(Collectors.toList()); + + for (String result : results) { + LOG.info(result); + } + p.waitFor(); + + } catch (Exception e){ + throw new CustomException(ResponseCode.CARD_BENEFITS_INSERT_FAIL); + } + } +} diff --git a/src/main/java/ewha/lux/once/domain/home/config/GeminiRestTemplateConfig.java b/src/main/java/ewha/lux/once/domain/home/config/GeminiRestTemplateConfig.java new file mode 100644 index 0000000..e52d546 --- /dev/null +++ b/src/main/java/ewha/lux/once/domain/home/config/GeminiRestTemplateConfig.java @@ -0,0 +1,20 @@ +package ewha.lux.once.domain.home.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +@RequiredArgsConstructor +public class GeminiRestTemplateConfig { + @Bean + @Qualifier("geminiRestTemplate") + public RestTemplate geminiRestTemplate() { + RestTemplate restTemplate = new RestTemplate(); + restTemplate.getInterceptors().add((request, body, execution) -> execution.execute(request, body)); + + return restTemplate; + } +} diff --git a/src/main/java/ewha/lux/once/domain/home/config/OpenaiRestTemplateConfig.java b/src/main/java/ewha/lux/once/domain/home/config/OpenaiRestTemplateConfig.java new file mode 100644 index 0000000..0d71db2 --- /dev/null +++ b/src/main/java/ewha/lux/once/domain/home/config/OpenaiRestTemplateConfig.java @@ -0,0 +1,25 @@ +package ewha.lux.once.domain.home.config; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class OpenaiRestTemplateConfig { + + @Value("${openai.api.key}") + private String openaiApiKey; + + @Bean + @Qualifier("openaiRestTemplate") + public RestTemplate openaiRestTemplate() { + RestTemplate restTemplate = new RestTemplate(); + restTemplate.getInterceptors().add((request, body, execution) -> { + request.getHeaders().add("Authorization", "Bearer " + openaiApiKey); + return execution.execute(request, body); + }); + return restTemplate; + } +} diff --git a/src/main/java/ewha/lux/once/domain/home/controller/HomeController.java b/src/main/java/ewha/lux/once/domain/home/controller/HomeController.java index 6cf2f70..0546510 100644 --- a/src/main/java/ewha/lux/once/domain/home/controller/HomeController.java +++ b/src/main/java/ewha/lux/once/domain/home/controller/HomeController.java @@ -1,5 +1,8 @@ package ewha.lux.once.domain.home.controller; +import ewha.lux.once.domain.card.dto.SearchStoresRequestDto; +import ewha.lux.once.domain.home.dto.AnnounceFavoriteRequestDto; +import ewha.lux.once.domain.home.dto.BeaconRequestDto; import ewha.lux.once.domain.home.service.HomeService; import ewha.lux.once.global.common.CommonResponse; import ewha.lux.once.global.common.CustomException; @@ -67,8 +70,39 @@ public CommonResponse announcedetail(@PathVariable Long announceId) { } catch (CustomException e){ return new CommonResponse<>(e.getStatus()); } - } + // [Post] 사용자 근처 단골가게 조회 + @PostMapping("/gps") + @ResponseBody + public CommonResponse nearFavorite(@AuthenticationPrincipal UserAccount user, @RequestBody SearchStoresRequestDto nearFavoriteRequestDto){ + try { + return new CommonResponse<>(ResponseCode.SUCCESS, homeService.searchStores(nearFavoriteRequestDto, user.getUsers())); + } catch (CustomException e){ + return new CommonResponse<>(e.getStatus()); + } + } + // [Post] 알림 생성 요청 + @PostMapping("/announcement") + @ResponseBody + public CommonResponse announceFavorite(@AuthenticationPrincipal UserAccount user, @RequestBody AnnounceFavoriteRequestDto announceFavoriteRequestDto){ + try { + homeService.postAnnounceFavorite(announceFavoriteRequestDto, user.getUsers()); + return new CommonResponse<>(ResponseCode.SUCCESS); + } catch (CustomException e){ + return new CommonResponse<>(e.getStatus()); + } + } + // [Post] 비콘 알림 생성 요청 + @PostMapping("/beacon") + @ResponseBody + public CommonResponse beaconAnnouncement(@AuthenticationPrincipal UserAccount user, @RequestBody BeaconRequestDto beaconRequestDto){ + try { + homeService.postBeaconAnnouncement(beaconRequestDto, user.getUsers()); + return new CommonResponse<>(ResponseCode.SUCCESS); + } catch (CustomException e){ + return new CommonResponse<>(e.getStatus()); + } + } -} +} \ No newline at end of file diff --git a/src/main/java/ewha/lux/once/domain/home/dto/AnnouncListDto.java b/src/main/java/ewha/lux/once/domain/home/dto/AnnouncListDto.java deleted file mode 100644 index 181bbf0..0000000 --- a/src/main/java/ewha/lux/once/domain/home/dto/AnnouncListDto.java +++ /dev/null @@ -1,19 +0,0 @@ -package ewha.lux.once.domain.home.dto; - -import lombok.Getter; -import lombok.Setter; - -import java.util.List; - -@Getter -@Setter -public class AnnouncListDto { - private long announceCount; - private List announceTodayList; - private List announcePastList; - public AnnouncListDto(Long announceCount,List announceTodayList,List announcePastList){ - this.announceCount = announceCount; - this.announceTodayList = announceTodayList; - this.announcePastList = announcePastList; - } -} diff --git a/src/main/java/ewha/lux/once/domain/home/dto/AnnounceFavoriteRequestDto.java b/src/main/java/ewha/lux/once/domain/home/dto/AnnounceFavoriteRequestDto.java new file mode 100644 index 0000000..b2764c1 --- /dev/null +++ b/src/main/java/ewha/lux/once/domain/home/dto/AnnounceFavoriteRequestDto.java @@ -0,0 +1,13 @@ +package ewha.lux.once.domain.home.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class AnnounceFavoriteRequestDto { + private String store; + private String storeName; + private double latitude; + private double longitude; +} \ No newline at end of file diff --git a/src/main/java/ewha/lux/once/domain/home/dto/AnnounceListDto.java b/src/main/java/ewha/lux/once/domain/home/dto/AnnounceListDto.java new file mode 100644 index 0000000..49e9d35 --- /dev/null +++ b/src/main/java/ewha/lux/once/domain/home/dto/AnnounceListDto.java @@ -0,0 +1,18 @@ +package ewha.lux.once.domain.home.dto; + +import lombok.*; + +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class AnnounceListDto { + + private String nickname; + private long announceCount; + private List announceTodayList; + private List announcePastList; + +} diff --git a/src/main/java/ewha/lux/once/domain/home/dto/AnnouncementRequestDto.java b/src/main/java/ewha/lux/once/domain/home/dto/AnnouncementRequestDto.java new file mode 100644 index 0000000..8d1a89a --- /dev/null +++ b/src/main/java/ewha/lux/once/domain/home/dto/AnnouncementRequestDto.java @@ -0,0 +1,15 @@ +package ewha.lux.once.domain.home.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +public class AnnouncementRequestDto { + private String targetToken; + private String title; + private String body; + private String announceId; +} \ No newline at end of file diff --git a/src/main/java/ewha/lux/once/domain/home/dto/BeaconRequestDto.java b/src/main/java/ewha/lux/once/domain/home/dto/BeaconRequestDto.java new file mode 100644 index 0000000..4d00532 --- /dev/null +++ b/src/main/java/ewha/lux/once/domain/home/dto/BeaconRequestDto.java @@ -0,0 +1,17 @@ +package ewha.lux.once.domain.home.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class BeaconRequestDto { + private String proximityUUID; + private Integer major; + private Integer minor; +} \ No newline at end of file diff --git a/src/main/java/ewha/lux/once/domain/home/dto/BenefitDto.java b/src/main/java/ewha/lux/once/domain/home/dto/BenefitDto.java new file mode 100644 index 0000000..2147b64 --- /dev/null +++ b/src/main/java/ewha/lux/once/domain/home/dto/BenefitDto.java @@ -0,0 +1,15 @@ +package ewha.lux.once.domain.home.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class BenefitDto { + private String benefit_field; + private String content; +} diff --git a/src/main/java/ewha/lux/once/domain/home/dto/ChatDto.java b/src/main/java/ewha/lux/once/domain/home/dto/ChatDto.java index 2550351..3054df6 100644 --- a/src/main/java/ewha/lux/once/domain/home/dto/ChatDto.java +++ b/src/main/java/ewha/lux/once/domain/home/dto/ChatDto.java @@ -1,26 +1,19 @@ package ewha.lux.once.domain.home.dto; -import lombok.Getter; -import lombok.Setter; +import lombok.*; -@Getter -@Setter +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder public class ChatDto { + private String nickname; - private int ownedCardCount; private Long chatId; private String cardName; + private String cardCompany; private String cardImg; private String benefit; private int discount; - - public ChatDto(String nickname, int ownedCardCount, Long chatId, String cardName, String cardImg, String benefit, int discount){ - this.nickname = nickname; - this.ownedCardCount = ownedCardCount; - this.chatId = chatId; - this.cardName = cardName; - this.cardImg = cardImg; - this.benefit = benefit; - this.discount = discount; - } + private Boolean isMain; } diff --git a/src/main/java/ewha/lux/once/domain/home/dto/FCMTokenDto.java b/src/main/java/ewha/lux/once/domain/home/dto/FCMTokenDto.java new file mode 100644 index 0000000..1414ee4 --- /dev/null +++ b/src/main/java/ewha/lux/once/domain/home/dto/FCMTokenDto.java @@ -0,0 +1,14 @@ +package ewha.lux.once.domain.home.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class FCMTokenDto { + private String token; +} diff --git a/src/main/java/ewha/lux/once/domain/home/dto/GeminiChatRequest.java b/src/main/java/ewha/lux/once/domain/home/dto/GeminiChatRequest.java new file mode 100644 index 0000000..f8f6ec3 --- /dev/null +++ b/src/main/java/ewha/lux/once/domain/home/dto/GeminiChatRequest.java @@ -0,0 +1,50 @@ +package ewha.lux.once.domain.home.dto; + +import lombok.*; + +import java.util.ArrayList; +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Getter @Setter +@Builder +public class GeminiChatRequest { + private List contents; + private GenerationConfig generationConfig; + + @Getter @Setter + public static class Content { + private Parts parts; + } + + @Getter @Setter + public static class Parts { + private String text; + + } + + @Getter @Setter + public static class GenerationConfig { + private int candidate_count; + private int max_output_tokens; + private double temperature; + + } + + public GeminiChatRequest(String prompt) { + this.contents = new ArrayList<>(); + Content content = new Content(); + Parts parts = new Parts(); + + parts.setText(prompt); + content.setParts(parts); + + this.contents.add(content); + this.generationConfig = new GenerationConfig(); + this.generationConfig.setCandidate_count(1); + this.generationConfig.setMax_output_tokens(1000); + this.generationConfig.setTemperature(0.7); + } +} diff --git a/src/main/java/ewha/lux/once/domain/home/dto/GeminiChatResponse.java b/src/main/java/ewha/lux/once/domain/home/dto/GeminiChatResponse.java new file mode 100644 index 0000000..0eeff31 --- /dev/null +++ b/src/main/java/ewha/lux/once/domain/home/dto/GeminiChatResponse.java @@ -0,0 +1,54 @@ +package ewha.lux.once.domain.home.dto; + +import lombok.*; + +import java.util.List; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class GeminiChatResponse { + + private List candidates; + private PromptFeedback promptFeedback; + + + @Getter @Setter + public static class Candidate { + private Content content; + private String finishReason; + private int index; + private List safetyRatings; + + } + + @Getter @Setter + @ToString + public static class Content { + private List parts; + private String role; + + } + + @Getter @Setter + @ToString + public static class Parts { + private String text; + + } + + @Getter @Setter + public static class SafetyRating { + private String category; + private String probability; + } + + @Getter @Setter + public static class PromptFeedback { + private List safetyRatings; + + } +} + + diff --git a/src/main/java/ewha/lux/once/domain/home/dto/HomeDto.java b/src/main/java/ewha/lux/once/domain/home/dto/HomeDto.java index d1c25fd..7c1b5b1 100644 --- a/src/main/java/ewha/lux/once/domain/home/dto/HomeDto.java +++ b/src/main/java/ewha/lux/once/domain/home/dto/HomeDto.java @@ -11,5 +11,6 @@ @AllArgsConstructor public class HomeDto { private String nickname; + private int ownedCardCount; private List keywordList; } diff --git a/src/main/java/ewha/lux/once/domain/home/dto/Message.java b/src/main/java/ewha/lux/once/domain/home/dto/Message.java new file mode 100644 index 0000000..ab1d525 --- /dev/null +++ b/src/main/java/ewha/lux/once/domain/home/dto/Message.java @@ -0,0 +1,12 @@ +package ewha.lux.once.domain.home.dto; + +import lombok.*; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Getter @Setter +public class Message { + private String role; + private String content; +} diff --git a/src/main/java/ewha/lux/once/domain/home/dto/OpenaiChatRequest.java b/src/main/java/ewha/lux/once/domain/home/dto/OpenaiChatRequest.java new file mode 100644 index 0000000..2eeb793 --- /dev/null +++ b/src/main/java/ewha/lux/once/domain/home/dto/OpenaiChatRequest.java @@ -0,0 +1,25 @@ +package ewha.lux.once.domain.home.dto; + +import lombok.*; + +import java.util.ArrayList; +import java.util.List; + +@Data +@Getter @Setter +@NoArgsConstructor +@AllArgsConstructor +public class OpenaiChatRequest { + private String model; + private List messages; + private final int n = 1; + private double temperature; + + public OpenaiChatRequest(String model, String prompt, String userInput) { + this.model = model; + this.messages = new ArrayList<>(); + this.messages.add(new Message("system", prompt)); + this.messages.add(new Message("user", userInput)); + } + +} diff --git a/src/main/java/ewha/lux/once/domain/home/dto/OpenaiChatResponse.java b/src/main/java/ewha/lux/once/domain/home/dto/OpenaiChatResponse.java new file mode 100644 index 0000000..df9d09f --- /dev/null +++ b/src/main/java/ewha/lux/once/domain/home/dto/OpenaiChatResponse.java @@ -0,0 +1,24 @@ +package ewha.lux.once.domain.home.dto; + + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class OpenaiChatResponse { + private List choices; + + @Data + @NoArgsConstructor + public static class Choice { + private int idx; + private Message message; + } +} diff --git a/src/main/java/ewha/lux/once/domain/home/entity/Beacon.java b/src/main/java/ewha/lux/once/domain/home/entity/Beacon.java new file mode 100644 index 0000000..ef9d89a --- /dev/null +++ b/src/main/java/ewha/lux/once/domain/home/entity/Beacon.java @@ -0,0 +1,40 @@ +package ewha.lux.once.domain.home.entity; + + +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Table(name="Beacon") +@Getter +@Setter +@Builder +@NoArgsConstructor(access= AccessLevel.PROTECTED) +@AllArgsConstructor +public class Beacon { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "beaconId") + private Long id; + + @Column(name = "proximityUUID", nullable = false) + private String proximityUUID; + + @Column(name = "major") + private Integer major; + + @Column(name = "minor") + private Integer minor; + + @Column(name = "name") + private String name; + + @Column(name = "store") + private String store; + + @Column(name = "latitude") + private String latitude; + + @Column(name = "longitude") + private String longitude; +} \ No newline at end of file diff --git a/src/main/java/ewha/lux/once/domain/home/entity/FCMToken.java b/src/main/java/ewha/lux/once/domain/home/entity/FCMToken.java new file mode 100644 index 0000000..3d22a16 --- /dev/null +++ b/src/main/java/ewha/lux/once/domain/home/entity/FCMToken.java @@ -0,0 +1,24 @@ +package ewha.lux.once.domain.home.entity; + +import ewha.lux.once.domain.user.entity.Users; +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Table(name="FCMToken") +public class FCMToken { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @Column(nullable = false, unique = true) + private String token; + @ManyToOne + @JoinColumn(name = "user_id") + private Users users; + +} diff --git a/src/main/java/ewha/lux/once/domain/home/entity/Favorite.java b/src/main/java/ewha/lux/once/domain/home/entity/Favorite.java index 1fa5b45..5d66d47 100644 --- a/src/main/java/ewha/lux/once/domain/home/entity/Favorite.java +++ b/src/main/java/ewha/lux/once/domain/home/entity/Favorite.java @@ -22,9 +22,8 @@ public class Favorite extends BaseEntity { @JoinColumn(name = "userId") private Users users; - @ManyToOne - @JoinColumn(name = "storeId") - private Store store; + @Column(name = "name",nullable = false) + private String name; } diff --git a/src/main/java/ewha/lux/once/domain/home/entity/Store.java b/src/main/java/ewha/lux/once/domain/home/entity/Store.java deleted file mode 100644 index 8cbe83a..0000000 --- a/src/main/java/ewha/lux/once/domain/home/entity/Store.java +++ /dev/null @@ -1,32 +0,0 @@ -package ewha.lux.once.domain.home.entity; - -import ewha.lux.once.global.common.BaseEntity; -import jakarta.persistence.*; -import lombok.*; - -@Entity -@Table(name="Store") -@Getter -@Setter -@Builder -@NoArgsConstructor(access= AccessLevel.PROTECTED) -@AllArgsConstructor -public class Store extends BaseEntity { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "storeId") - private Long id; - - @Column(name = "name",nullable = false) - private String name; - - @Column(name = "address") - private String address; - - @Column(name = "x") - private float x; - - @Column(name = "y") - private float y; - -} diff --git a/src/main/java/ewha/lux/once/domain/home/service/AnnouncementService.java b/src/main/java/ewha/lux/once/domain/home/service/AnnouncementService.java new file mode 100644 index 0000000..d2b4940 --- /dev/null +++ b/src/main/java/ewha/lux/once/domain/home/service/AnnouncementService.java @@ -0,0 +1,138 @@ +package ewha.lux.once.domain.home.service; + +import ewha.lux.once.domain.card.entity.OwnedCard; +import ewha.lux.once.domain.home.dto.AnnouncementRequestDto; +import ewha.lux.once.domain.home.entity.Announcement; +import ewha.lux.once.domain.home.entity.ChatHistory; +import ewha.lux.once.domain.home.entity.FCMToken; +import ewha.lux.once.global.repository.FCMTokenRepository; +import ewha.lux.once.domain.user.entity.Users; +import ewha.lux.once.global.common.CustomException; +import ewha.lux.once.global.repository.AnnouncementRepository; +import ewha.lux.once.global.repository.ChatHistoryRepository; +import ewha.lux.once.global.repository.OwnedCardRepository; +import ewha.lux.once.global.repository.UsersRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.util.HashMap; +import java.util.List; + +@Service +@Slf4j +@RequiredArgsConstructor +public class AnnouncementService { + private final UsersRepository usersRepository; + private final ChatHistoryRepository chatHistoryRepository; + private final AnnouncementRepository announcementRepository; + + private final FirebaseCloudMessageService firebaseCloudMessageService; + private final FCMTokenRepository fcmTokenRepository; + private final OwnedCardRepository ownedCardRepository; + private final CODEFAPIService codefapi; + + + // 매주 목요일 9:00 목표 응원 알림 생성 + @Scheduled(cron = "0 0 9 ? * 4") + public void cheeringBenefitGoalAnnounce() throws CustomException { + List usersList = usersRepository.findAll(); + String currentDate = String.valueOf(LocalDate.now().getMonthValue());; + + for ( Users users : usersList ) { + String content = ""; + String moreInfo = ""; + + int goalBenefitGoal = users.getBenefitGoal(); + + List paidChatHistories = chatHistoryRepository.findByUsersAndHasPaidTrue(users); + int receivedBenefit = 0; + if (!paidChatHistories.isEmpty()) { + receivedBenefit = paidChatHistories.stream() + .mapToInt(ChatHistory::getDiscount) + .sum(); + } + + int remainBenefit = Math.max(goalBenefitGoal - receivedBenefit, 0); + if (remainBenefit == 0){ + content = currentDate + "월 목표를 달성했어요!"; + moreInfo = "100"; + } else { + content = currentDate + "월 목표 혜택 달성까지 " + String.format("%,d", remainBenefit) + "원 남았어요."; + if(goalBenefitGoal==0) moreInfo="100"; + else moreInfo = String.valueOf((int) Math.ceil((double) receivedBenefit / goalBenefitGoal *100 )); + } + + + Announcement announcement = Announcement.builder() + .users(users) + .type(1) + .content(content) + .moreInfo(moreInfo) + .hasCheck(false) + .build(); + announcementRepository.save(announcement); + + List fcmTokens = fcmTokenRepository.findAllByUsers(users); + for ( FCMToken fcmToken : fcmTokens){ + String token = fcmToken.getToken(); + firebaseCloudMessageService.sendNotification(new AnnouncementRequestDto(token,"목표 응원 알림",content,announcement.getId().toString())); + } + } + } + + @Scheduled(cron = "0 0 21 10,15,25 * ?") + public void cardPerformanceAnnounce() throws CustomException { + String currentDate = String.valueOf(LocalDate.now().getMonthValue());; + List ownedCardList = ownedCardRepository.findOwnedCardByIsMain(true); + for (OwnedCard card : ownedCardList) { + // 실적 업데이트 + Users users = card.getUsers(); + HashMap performResult = codefapi.Performace(card.getCard().getCardCompany().getCode(),users.getConnectedId(),card.getCard().getName()); //????? + int performanceCondition = (int) performResult.get("performanceCondition"); + int currentPerformance = (int) performResult.get("performanceCondition"); + card.setPerformanceCondition(performanceCondition); + card.setCurrentPerformance(currentPerformance); + + ownedCardRepository.save(card); + String res = String.valueOf(Math.max(card.getPerformanceCondition()-card.getCurrentPerformance(),0)); + String content = "이번 달 "+card.getCard().getName()+" 실적까지\n"+res+"원 남았어요!"; + String moreInfo = card.getCard().getImgUrl(); + + Announcement announcement = Announcement.builder() + .users(users) + .type(3) + .content(content) + .moreInfo(moreInfo) + .hasCheck(false) + .build(); + announcementRepository.save(announcement); + List fcmTokens = fcmTokenRepository.findAllByUsers(users); + for ( FCMToken fcmToken : fcmTokens){ + String token = fcmToken.getToken(); + firebaseCloudMessageService.sendNotification(new AnnouncementRequestDto(token,currentDate+"월 실적 알림",content,announcement.getId().toString())); + } + + } + } + + // 매달 주카드 아닌 보유카드 실적 초기화 + @Scheduled(cron = "0 0 0 1 * ?") + public void resetNonMainOwnedCardPerformance() throws CustomException { + List ownedCardList = ownedCardRepository.findOwnedCardByIsMain(false); + for (OwnedCard card : ownedCardList) { + card.setCurrentPerformance(0); + ownedCardRepository.save(card); + } + } + + // 매주 월요일 4:00마다 CODEF 액세스 토큰 업데이트 + @Scheduled(cron = "0 0 4 ? * 1") + public void updateCODEFACCESSTOKEN() throws CustomException { + String token = codefapi.publishToken(); + // redis의 token 업데이트 + + } +} diff --git a/src/main/java/ewha/lux/once/domain/home/service/CODEFAPIService.java b/src/main/java/ewha/lux/once/domain/home/service/CODEFAPIService.java new file mode 100644 index 0000000..4f92c4e --- /dev/null +++ b/src/main/java/ewha/lux/once/domain/home/service/CODEFAPIService.java @@ -0,0 +1,384 @@ +package ewha.lux.once.domain.home.service; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import ewha.lux.once.domain.card.dto.CodefCardListRequestDto; +import ewha.lux.once.domain.user.entity.Users; +import ewha.lux.once.global.CODEF.ApiRequest; +import ewha.lux.once.global.CODEF.CommonConstant; +import ewha.lux.once.global.common.CustomException; +import ewha.lux.once.global.common.ResponseCode; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.codec.binary.Base64; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLDecoder; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.*; + + +@Service +@Slf4j +@RequiredArgsConstructor +public class CODEFAPIService { + private static ObjectMapper mapper = new ObjectMapper(); + @Value("${codef.public-key}") + private String PUBLIC_KEY; + @Value("${codef.client-id}") + private String CLIENT_ID; + @Value("${codef.seceret-key}") + private String SECERET_KEY; + private final ApiRequest apiRequest; + // ACCESS TOKEN 생성 + public String publishToken() throws CustomException { + BufferedReader br = null; + try { + // HTTP 요청을 위한 URL 오브젝트 생성 + URL url = new URL(CommonConstant.TOKEN_DOMAIN + CommonConstant.GET_TOKEN); + String params = "grant_type=client_credentials&scope=read"; + + HttpURLConnection con = (HttpURLConnection) url.openConnection(); + con.setRequestMethod("POST"); + con.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + + // 클라이언트아이디, 시크릿코드 Base64 인코딩 + String auth = CLIENT_ID + ":" + SECERET_KEY; + byte[] authEncBytes = Base64.encodeBase64(auth.getBytes()); + String authStringEnc = new String(authEncBytes); + String authHeader = "Basic " + authStringEnc; + + con.setRequestProperty("Authorization", authHeader); + con.setDoInput(true); + con.setDoOutput(true); + + // 리퀘스트 바디 전송 + OutputStream os = con.getOutputStream(); + os.write(params.getBytes()); + os.flush(); + os.close(); + + // 응답 코드 확인 + int responseCode = con.getResponseCode(); + if (responseCode == HttpURLConnection.HTTP_OK) { // 정상 응답 + br = new BufferedReader(new InputStreamReader(con.getInputStream())); + } else { // 에러 발생 + return null; + } + + // 응답 바디 read + String inputLine; + StringBuffer responseStr = new StringBuffer(); + while ((inputLine = br.readLine()) != null) { + responseStr.append(inputLine); + } + br.close(); + + HashMap tokenMap = mapper.readValue(URLDecoder.decode(responseStr.toString(), "UTF-8"), new TypeReference>() { + }); + + return tokenMap.get("access_token").toString(); + } catch (Exception e) { + + return null; + } + } + + // 계정 생성 + public String CreateConnectedID(CodefCardListRequestDto cardInfo) throws CustomException { + try { + String urlPath = CommonConstant.TEST_DOMAIN+CommonConstant.CREATE_ACCOUNT; + + HashMap bodyMap = new HashMap(); + List> list = new ArrayList>(); + + HashMap accountMap = new HashMap(); + accountMap.put("countryCode", "KR"); // 국가코드 + accountMap.put("businessType", "CD"); // 업무구분코드 + accountMap.put("clientType", "P"); // 고객구분(P: 개인, B: 기업) + accountMap.put("organization", cardInfo.getCode()); // 기관코드 + accountMap.put("loginType", "1"); // 로그인타입 (0: 인증서, 1: ID/PW) + accountMap.put("id", cardInfo.getId()); // 아이디 + String password = cardInfo.getPassword(); + accountMap.put("password", encryptRSA(password, PUBLIC_KEY)); // password RSA encrypt + + list.add(accountMap); + + bodyMap.put("accountList", list); + + + // CODEF API 호출 + JSONObject result = apiRequest.reqeust(urlPath, bodyMap); + + JSONObject data = (JSONObject) result.get("data"); + String connectedId = (String) data.get("connectedId"); + + return connectedId; + } catch (Exception e){ + throw new CustomException(ResponseCode.CODEF_CONNECTEDID_CREATE_FAIL); + } + } + + // 계정 추가 + public String AddToConnectedID(Users nowuser, CodefCardListRequestDto cardInfo) throws CustomException { + try { + String urlPath = CommonConstant.TEST_DOMAIN+CommonConstant.ADD_ACCOUNT; + + HashMap bodyMap = new HashMap(); + List> list = new ArrayList>(); + + HashMap accountMap1 = new HashMap(); + accountMap1.put("countryCode", "KR"); + accountMap1.put("businessType", "CD"); + accountMap1.put("clientType", "P"); + accountMap1.put("organization", cardInfo.getCode()); + accountMap1.put("loginType", "1"); + accountMap1.put("id", cardInfo.getId()); + String password1 = cardInfo.getPassword(); + accountMap1.put("password", encryptRSA(password1, PUBLIC_KEY)); + + list.add(accountMap1); + + bodyMap.put("accountList", list); + bodyMap.put("connectedId", nowuser.getConnectedId()); + + // CODEF API 호출 + JSONObject result = apiRequest.reqeust(urlPath, bodyMap); + String connectedId = result.get("connectedId").toString(); + + return connectedId; + } catch (Exception e){ + throw new CustomException(ResponseCode.CODEF_CONNECTEDID_ADD_FAIL); + } + } + // 계정 삭제 + public void DeleteConnectedID(Users nowuser, String code) throws CustomException { + try { + String urlPath = CommonConstant.TEST_DOMAIN+CommonConstant.DELETE_ACCOUNT; + + HashMap bodyMap = new HashMap(); + List> list = new ArrayList>(); + + HashMap accountMap = new HashMap(); + accountMap.put("countryCode", "KR"); + accountMap.put("businessType", "CD"); + accountMap.put("clientType", "P"); + accountMap.put("organization", code); + accountMap.put("loginType", "1"); + + list.add(accountMap); + + bodyMap.put("accountList", list); + bodyMap.put("connectedId", nowuser.getConnectedId()); + + // CODEF API 호출 + JSONObject result = apiRequest.reqeust(urlPath, bodyMap); + + } catch (Exception e){ + throw new CustomException(ResponseCode.CODEF_DELETE_CONNECTEDID_FAIL); + } + } + // 보유 카드 조회 + public JSONArray GetCardList(String code, String connectedId) throws CustomException { + try { + String urlPath = CommonConstant.TEST_DOMAIN+CommonConstant.KR_CD_P_001; + + HashMap accountMap = new HashMap(); + accountMap.put("organization", code); + accountMap.put("connectedId", connectedId); + accountMap.put("inquiryType", "1"); + + JSONObject result = apiRequest.reqeust(urlPath, accountMap); + JSONArray dataArray = (JSONArray) result.get("data"); + return dataArray; + } catch (Exception e){ + throw new CustomException(ResponseCode.CODEF_GET_CARD_LIST_FAIL); + } + } + // 승인 내역 조회 + public List GetHistory(String code, String connectedId,String cardName, String cardNo) throws CustomException { + try { + List resultList = new ArrayList<>(); + + String urlPath = CommonConstant.TEST_DOMAIN+CommonConstant.KR_CD_P_002; + + HashMap accountMap = new HashMap(); + accountMap.put("organization", code); // 기관코드 + accountMap.put("connectedId", connectedId); // 커넥티드아이디 + + LocalDate sixMonthsAgo = LocalDate.now().minusMonths(6); + String startDate = sixMonthsAgo.format(DateTimeFormatter.ofPattern("yyyyMMdd")); + String endDate = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); + accountMap.put("startDate", startDate); + accountMap.put("endDate", endDate); + +// accountMap.put("inquiryType", "1"); // 31.74s + + accountMap.put("inquiryType", "0"); // 14.22s + accountMap.put("cardNo", cardNo); + accountMap.put("cardName", cardName); + + accountMap.put("memberStoreInfoType", "2"); + + + accountMap.put("orderBy", "0"); + + + JSONObject result = apiRequest.reqeust(urlPath, accountMap); + + JSONArray dataArray = (JSONArray) result.get("data"); + + + // 맵을 사용하여 resMemberStore 개수 카운트 + Map storeCountMap = new HashMap<>(); + for (Object obj : dataArray) { + JSONObject dataObject = (JSONObject) obj; + String storeName = (String) dataObject.get("resMemberStoreName"); +// String storeAddr = (String) dataObject.get("resMemberStoreAddr"); + String storeKey = storeName; // 고유 키 생성 + + // 맵에 있는지 확인하고 카운트 업데이트 + storeCountMap.put(storeKey, storeCountMap.getOrDefault(storeKey, 0) + 1); + } + + // 많은 순서대로 정렬 + List> sortedEntries = new ArrayList<>(storeCountMap.entrySet()); + sortedEntries.sort(Map.Entry.comparingByValue(Comparator.reverseOrder())); + + // 상위 10개의 매장 정보 추출 + List topStores = new ArrayList<>(); + int count = 0; + for (Map.Entry entry : sortedEntries) { + if (count >= 10) break; + topStores.add(entry.getKey()); + count++; + } + return topStores; + + } catch (Exception e){ + throw new CustomException(ResponseCode.CODEF_GET_APPROVAL_LIST_FAIL); + } + } + // 실적 조회 + public HashMap Performace(String code, String connectedId, String cardName) throws CustomException { + try{ + String urlPath = CommonConstant.TEST_DOMAIN+CommonConstant.KR_CD_P_005; + + HashMap resultList = new HashMap<>(); + HashMap accountMap = new HashMap(); + + accountMap.put("organization", code); // 기관코드 + accountMap.put("connectedId", connectedId); // 커넥티드아이디 + JSONObject result = apiRequest.reqeust(urlPath, accountMap); + + JSONArray dataArray = (JSONArray) result.get("data"); + + Integer performanceCondition = null; + Integer currentPerformance = null; + String resCardNo = null; + + // data 배열의 각 요소에 대해 반복 + for (int i = 0; i < dataArray.size(); i++) { + JSONObject dataObject = (JSONObject) dataArray.get(i); + String resCardName = (String) dataObject.get("resCardName"); + + // 이미 있는 cardName인 경우 처리 + if (resCardName.equals(cardName)) { + resCardNo = (String) dataObject.get("resCardNo"); + JSONArray benefitList = (JSONArray) dataObject.get("resCardBenefitList"); + JSONObject firstBenefit = (JSONObject) benefitList.get(0); + JSONObject resCardPerformanceListFirst = (JSONObject) ((JSONArray) firstBenefit.get("resCardPerformanceList")).get(0); + + String resStandardUseAmtStr = (String)resCardPerformanceListFirst.get("resStandardUseAmt"); + String resCurrentUseAmtStr = (String)resCardPerformanceListFirst.get("resCurrentUseAmt"); + + if (resStandardUseAmtStr == "") { + performanceCondition = 0; + + } else{ + performanceCondition = Integer.parseInt(resStandardUseAmtStr); + } + if (resCurrentUseAmtStr != null) { + currentPerformance = Integer.parseInt(resCurrentUseAmtStr); + } else{ + currentPerformance = 0; + } + break; + } + } + resultList.put("performanceCondition",performanceCondition); + resultList.put("currentPerformance",currentPerformance); + resultList.put("resCardNo",resCardNo); + + return resultList; + } catch (Exception e){ + throw new CustomException(ResponseCode.CODEF_GET_CARD_PERFORMANCE_FAIL); + } + } + // 등록 여부 조회 + public String IsRegistered(String code, String connectedId) throws CustomException { + try { + String urlPath = CommonConstant.TEST_DOMAIN+CommonConstant.KR_CD_P_006; + HashMap accountMap = new HashMap(); + accountMap.put("organization", code); // 기관코드 + accountMap.put("connectedId", connectedId); // 커넥티드아이디 + JSONObject result = apiRequest.reqeust(urlPath, accountMap); + + return (String) ((JSONObject) result.get("data")).get("resRegistrationStatus"); + } catch (Exception e){ + throw new CustomException(ResponseCode.CODEF_REGISTRATION_STATUS_FAIL); + } + } + // 계정 목록 조회 + public boolean isEmptyAccountList(String connectedId) throws CustomException { + try { + String urlPath = CommonConstant.TEST_DOMAIN+CommonConstant.GET_ACCOUNTS; // "https://development.codef.io/v1/account/list" + HashMap accountMap = new HashMap(); + accountMap.put("connectedId", connectedId); + JSONObject result = apiRequest.reqeust(urlPath, accountMap); + + int accountListLength = ((JSONArray) ((JSONObject) result.get("data")).get("accountList")).size(); + + return ((JSONArray) ((JSONObject) result.get("data")).get("accountList")).isEmpty(); + } catch (Exception e){ + throw new CustomException(ResponseCode.CODEF_CONNECTEDID_ACCOUNT_LIST_FAIL); + } + } + + private static String encryptRSA(String plainText, String base64PublicKey) + throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, + InvalidKeyException, IllegalBlockSizeException, BadPaddingException { + + byte[] bytePublicKey = java.util.Base64.getDecoder().decode(base64PublicKey); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + PublicKey publicKey = keyFactory.generatePublic(new X509EncodedKeySpec(bytePublicKey)); + + Cipher cipher = Cipher.getInstance("RSA"); + cipher.init(Cipher.ENCRYPT_MODE, publicKey); + byte[] bytePlain = cipher.doFinal(plainText.getBytes()); + String encrypted = java.util.Base64.getEncoder().encodeToString(bytePlain); + + return encrypted; + + } +} \ No newline at end of file diff --git a/src/main/java/ewha/lux/once/domain/home/service/CODEFAsyncService.java b/src/main/java/ewha/lux/once/domain/home/service/CODEFAsyncService.java new file mode 100644 index 0000000..c9c6222 --- /dev/null +++ b/src/main/java/ewha/lux/once/domain/home/service/CODEFAsyncService.java @@ -0,0 +1,139 @@ +package ewha.lux.once.domain.home.service; + +import ewha.lux.once.domain.card.dto.Place; +import ewha.lux.once.domain.card.dto.GoogleMapPlaceResponseDto; +import ewha.lux.once.domain.card.entity.OwnedCard; +import ewha.lux.once.domain.home.dto.OpenaiChatRequest; +import ewha.lux.once.domain.home.dto.OpenaiChatResponse; +import ewha.lux.once.domain.home.entity.Favorite; +import ewha.lux.once.domain.user.entity.Users; +import ewha.lux.once.global.common.CustomException; +import ewha.lux.once.global.common.ResponseCode; +import ewha.lux.once.global.repository.FavoriteRepository; +import ewha.lux.once.global.repository.OwnedCardRepository; +import ewha.lux.once.global.repository.UsersRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class CODEFAsyncService { + @Value("${google-map.api-key}") + private String apiKey; + private final CODEFAPIService codefapi; + private final FavoriteRepository favoriteRepository; + private final UsersRepository usersRepository; + // private final RestTemplate restTemplate; + private final OwnedCardRepository ownedCardRepository; + + @Value("${openai.api.url}") + private String apiUrl; + + @Qualifier("openaiRestTemplate") + @Autowired + private RestTemplate restTemplate; + + @Async + public void saveFavorite(String code, String connectedId, OwnedCard ownedCard, Users nowUser, String cardNo) throws CustomException { + // 승인 내역 조회 -> 단골 가게 카드별 10개 + List favorites = codefapi.GetHistory(code,connectedId,ownedCard.getCard().getName(),cardNo); + + String system ="입력받은 가맹점명에서 브랜드 이름을 찾아서 뽑아줘. 출력은 단어만, 알아낼 수 없다면 null을 반환해줘."; + for (String keyword : favorites){ + OpenaiChatRequest request = new OpenaiChatRequest("gpt-4-turbo", system, keyword); + OpenaiChatResponse response = restTemplate.postForObject(apiUrl, request, OpenaiChatResponse.class); + if (response == null || response.getChoices() == null || response.getChoices().isEmpty()) { + throw new CustomException(ResponseCode.FAILED_TO_OPENAI); + } + String result = response.getChoices().get(0).getMessage().getContent(); + System.out.println(result); + if (!"null".equals(result)) { + boolean exists = favoriteRepository.existsByNameAndUsers(result,nowUser); + if (!exists) { + Favorite favorite = Favorite.builder() + .users(nowUser) + .name(result) + .build(); + favoriteRepository.save(favorite); + } + } + } + } + @Async + public void deleteConnectedID(Users nowUser,OwnedCard ownedCard) throws CustomException { + codefapi.DeleteConnectedID(nowUser,ownedCard.getCard().getCardCompany().getCode()); + if(codefapi.isEmptyAccountList(nowUser.getConnectedId())){ + nowUser.setConnectedId(null); + usersRepository.save(nowUser); + } + } + private HashMap searchStoreAddr (String textQuery) throws CustomException { + try { + String url = "https://places.googleapis.com/v1/places:searchText"; + // setup headers + HttpHeaders headers = new HttpHeaders(); + headers.add("accept", "application/json"); + headers.add("X-Goog-Api-Key", apiKey); + headers.add("X-Goog-FieldMask", "places.formattedAddress,places.location"); + + // request body parameters + Map requestBody = new HashMap(); + requestBody.put("textQuery", textQuery); + requestBody.put("maxResultCount", 1); + requestBody.put("languageCode", "ko"); + + + HttpEntity> requestData = new HttpEntity<>(requestBody, headers); + + ResponseEntity responseEntity = restTemplate.postForEntity(url, requestData, GoogleMapPlaceResponseDto.class); + + GoogleMapPlaceResponseDto responsebody = responseEntity.getBody(); + if (responsebody.getPlaces() == null){ + HashMap resultList = new HashMap<>(); + resultList.put("formattedAddress",null); + resultList.put("x",null); + resultList.put("y",null); + return resultList; + } + Place place = responsebody.getPlaces().get(0); + + HashMap resultList = new HashMap<>(); + resultList.put("formattedAddress",place.getFormattedAddress()); + resultList.put("x",place.getLocation().getLatitude()); + resultList.put("y",place.getLocation().getLongitude()); + return resultList; + + } catch (Exception e){ + throw new CustomException(ResponseCode.CODEF_GET_CARD_PERFORMANCE_FAIL); + } + } + @Async + public void updateOwnedCardsPerformanceCodef(Users nowUser)throws CustomException { + List ownedCards = ownedCardRepository.findOwnedCardByUsers(nowUser); + for (OwnedCard card : ownedCards) { + + if(card.isMain()==true) { + // 실적 업데이트 + HashMap performResult = codefapi.Performace(card.getCard().getCardCompany().getCode(), nowUser.getConnectedId(), card.getCard().getName()); + int performanceCondition = (int) performResult.get("performanceCondition"); + int currentPerformance = (int) performResult.get("currentPerformance"); + card.setPerformanceCondition(performanceCondition); + card.setCurrentPerformance(currentPerformance); + + ownedCardRepository.save(card); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/ewha/lux/once/domain/home/service/FirebaseCloudMessageService.java b/src/main/java/ewha/lux/once/domain/home/service/FirebaseCloudMessageService.java new file mode 100644 index 0000000..72a0a83 --- /dev/null +++ b/src/main/java/ewha/lux/once/domain/home/service/FirebaseCloudMessageService.java @@ -0,0 +1,50 @@ +package ewha.lux.once.domain.home.service; + +import com.google.firebase.messaging.FirebaseMessaging; +import com.google.firebase.messaging.FirebaseMessagingException; +import com.google.firebase.messaging.Message; +import com.google.firebase.messaging.Notification; +import ewha.lux.once.domain.home.dto.AnnouncementRequestDto; +import ewha.lux.once.domain.home.entity.FCMToken; +import ewha.lux.once.global.common.CustomException; +import ewha.lux.once.global.common.ResponseCode; +import ewha.lux.once.global.repository.FCMTokenRepository; +import ewha.lux.once.domain.user.entity.Users; +import lombok.RequiredArgsConstructor; + +import org.springframework.stereotype.Component; + + + +@Component +@RequiredArgsConstructor +public class FirebaseCloudMessageService { + private final FirebaseMessaging firebaseMessaging; + private final FCMTokenRepository fcmTokenRepository; + public void sendNotification (AnnouncementRequestDto requestDTO) throws CustomException { + Notification notification = Notification.builder() + .setTitle(requestDTO.getTitle()) + .setBody(requestDTO.getBody()) + .build(); + Message message = Message.builder() + .setToken(requestDTO.getTargetToken()) + .setNotification(notification) + .putData("announceId",requestDTO.getAnnounceId() ) + .build(); + try { + firebaseMessaging.send(message); + } catch (FirebaseMessagingException e) { + e.printStackTrace(); + throw new CustomException(ResponseCode.FCM_SEND_NOTIFICATION_FAIL); + } + } + + public void postFCMToken (Users users, String token) throws CustomException { + FCMToken fcmToken = FCMToken.builder() + .users(users) + .token(token) + .build(); + fcmTokenRepository.save(fcmToken); + } + +} \ No newline at end of file diff --git a/src/main/java/ewha/lux/once/domain/home/service/GeminiService.java b/src/main/java/ewha/lux/once/domain/home/service/GeminiService.java new file mode 100644 index 0000000..b6656ee --- /dev/null +++ b/src/main/java/ewha/lux/once/domain/home/service/GeminiService.java @@ -0,0 +1,59 @@ +package ewha.lux.once.domain.home.service; + +import ewha.lux.once.domain.card.entity.OwnedCard; +import ewha.lux.once.domain.home.dto.GeminiChatRequest; +import ewha.lux.once.domain.home.dto.GeminiChatResponse; +import ewha.lux.once.domain.user.entity.Users; +import ewha.lux.once.global.repository.OwnedCardRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.util.List; + +@Service @Slf4j +@RequiredArgsConstructor +public class GeminiService { + + @Qualifier("geminiRestTemplate") + @Autowired + private RestTemplate restTemplate; + + private final OwnedCardRepository ownedCardRepository; + + @Value("${gemini.api.url}") + private String apiUrl; + + @Value("${gemini.api.key}") + private String geminiApiKey; + + String prompt = "결제처, 결제금액, 카드들의 혜택 정보를 Input으로 하여 결제처에서 최적의 혜택을 누릴 수 있는 카드 번호, 혜택 정보, 할인 금액을 알려주어야 함.\n" + + "카드들의 혜택 정보에서 각 카드는 ///// 로 구분되고, 각 카드가 입력된 결제처에 해당되는 혜택을 가지고 있다면 할인 금액을 계산하고, 여러 카드 중 가장 할인 금액이 큰 카드의 고유번호 숫자, 결제처에 해당되는 혜택 정보 요약 텍스트(특수문자 없어야 함), 해당 혜택 적용 시 받게되는 할인 금액 숫자를 쉼표로 구분하여 제공해야 함. \n"; + public String gemini(Users nowUser, String keyword, int paymentAmount) { + List ownedCards = ownedCardRepository.findOwnedCardByUsers(nowUser); + prompt = prompt + "결제 금액: " + paymentAmount + ", 결제처: " + keyword + ", 카드들의 혜택 정보: "; + for (OwnedCard ownedCard : ownedCards) { + String name = ownedCard.getCard().getName(); + String id = ownedCard.getCard().getId().toString(); + String benefits = ownedCard.getCard().getBenefits(); + prompt = prompt + name + ", " + "카드 고유 번호 : " + id + ", " + benefits + "/////"; + } + + // Gemini 요청 보내는 부분 + String requestUrl = apiUrl + "?key=" + geminiApiKey; + GeminiChatRequest request = new GeminiChatRequest(prompt); + + GeminiChatResponse response = restTemplate.postForObject(requestUrl, request, GeminiChatResponse.class); + + String result = response.getCandidates().get(0).getContent().getParts().get(0).getText().toString(); // 응답만 추출 + + log.info(result); + + return result; + } + +} diff --git a/src/main/java/ewha/lux/once/domain/home/service/HomeService.java b/src/main/java/ewha/lux/once/domain/home/service/HomeService.java index ed6b90f..0256986 100644 --- a/src/main/java/ewha/lux/once/domain/home/service/HomeService.java +++ b/src/main/java/ewha/lux/once/domain/home/service/HomeService.java @@ -1,19 +1,26 @@ package ewha.lux.once.domain.home.service; -import ewha.lux.once.domain.home.dto.*; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import ewha.lux.once.domain.card.dto.GoogleMapPlaceResponseDto; +import ewha.lux.once.domain.card.dto.Place; +import ewha.lux.once.domain.card.dto.SearchStoresRequestDto; +import ewha.lux.once.domain.card.dto.SearchStoresResponseDto; import ewha.lux.once.domain.card.entity.Card; -import ewha.lux.once.domain.home.entity.Announcement; -import ewha.lux.once.domain.home.entity.ChatHistory; import ewha.lux.once.domain.card.entity.OwnedCard; +import ewha.lux.once.domain.home.dto.*; +import ewha.lux.once.domain.home.entity.*; +import ewha.lux.once.domain.user.entity.Users; import ewha.lux.once.global.common.CustomException; import ewha.lux.once.global.common.ResponseCode; -import ewha.lux.once.global.repository.CardRepository; -import ewha.lux.once.global.repository.AnnouncementRepository; -import ewha.lux.once.global.repository.ChatHistoryRepository; -import ewha.lux.once.global.repository.OwnedCardRepository; -import ewha.lux.once.domain.user.entity.Users; +import ewha.lux.once.global.repository.*; import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.web.client.RestTemplate; import java.time.LocalDate; import java.util.*; @@ -22,41 +29,124 @@ @Service @RequiredArgsConstructor public class HomeService { + @Value("${google-map.api-key}") + private String apiKey; + private final RestTemplate restTemplate; + private final FavoriteRepository favoriteRepository; private final CardRepository cardRepository; private final OwnedCardRepository ownedCardRepository; private final ChatHistoryRepository chatHistoryRepository; private final AnnouncementRepository announcementRepository; + + private final GeminiService geminiService; + private final OpenaiService openaiService; + private final FCMTokenRepository fcmTokenRepository; + private final FirebaseCloudMessageService firebaseCloudMessageService; + private final BeaconRepository beaconRepository; + + // 챗봇 카드 추천 public ChatDto getHomeChat(Users nowUser, String keyword, int paymentAmount) throws CustomException { - // 파인튜닝한 GPT에 keyword, paymentAmount, 보유 카드 번호, 해당 혜택 정보 전송 - // 파인튜닝한 GPT에게 cardId, benefit, discount 반환받음 - // 예시 데이터 - Card exampleCard = cardRepository.findById(1l).get(); - String benefit = "생활쇼핑 최대 15% 결제일 할인"; - int discount = 1500; - String category = "쇼핑"; // 카테고리 처리 로직 필요 + // 1. Gemini 사용하는 경우 +// String response = geminiService.gemini(nowUser, keyword, paymentAmount); + + // 2. GPT 사용하는 경우 + String response = openaiService.cardRecommend(nowUser, keyword, paymentAmount); + ObjectMapper objectMapper = new ObjectMapper(); + Integer cardId; + Card card; + String benefit; + Integer discount; + try { + Map map = objectMapper.readValue(response, Map.class); + cardId = (Integer) map.get("카드번호"); + card = cardRepository.findById(Long.valueOf(cardId)).orElse(null); + benefit = (String) map.get("혜택 정보"); + discount = (Integer) map.get("할인 금액"); + + } catch (JsonProcessingException e) { + throw new CustomException(ResponseCode.FAILED_TO_OPENAI_RECOMMEND); + } - // 채팅 객체 생성 + // 챗봇 대화 기록 ChatHistory chat = ChatHistory.builder() .users(nowUser) .keyword(keyword) .paymentAmount(paymentAmount) - .cardName(exampleCard.getName()) + .cardName(card.getName()) .benefit(benefit) .discount(discount) .hasPaid(false) - .category(category) + .category(getCategory(keyword)) .build(); // Chat 객체 저장 ChatHistory savedChat = chatHistoryRepository.save(chat); - //사용자 보유 카드 수 - int ownedCardCount = ownedCardRepository.countAllByUsers(nowUser); + OwnedCard ownedCard = ownedCardRepository.findOwnedCardByCardIdAndUsers(Long.valueOf(cardId),nowUser); + + // 챗봇 응답 + ChatDto chatDto = ChatDto.builder() + .nickname(nowUser.getNickname()) + .chatId(savedChat.getId()) + .cardName(card.getName()) + .cardCompany(card.getCardCompany().getName()) + .cardImg(card.getImgUrl()) + .benefit(benefit) + .discount(discount) + .isMain(ownedCard.isMain()) + .build(); + + return chatDto; + } + + /* + * 카테고리 처리 함수 + * @param keyword + */ + public static String getCategory(String keyword) { + + String[] convenienceStoreKeywords = {"편의점", "CU", "씨유", "GS25", "지에스", "세븐일레븐", "이마트24", "미니스톱"}; + String[] culturalKeywords = {"문화", "영화", "CGV", "씨지브이", "씨지비", "메가박스", "megabox", "롯데시네마", "OTT", "오티티", "넷플릭스", "netflix", "티빙", "tving", "디즈니플러스", "disney", "웨이브", "wavve", "왓챠", "watcha", "쿠팡플레이", "스포츠","놀이공원","에버랜드","롯데월드"}; + String[] cafeKeywords = {"카페", "커피", "cafe", "coffee", "스타벅스", "starbucks", "빽다방", "폴바셋", "커피빈", "투썸플레이스", "컴포즈", "매머드커피", "메가커피", "카페봄봄", "공차", "이디야"}; + String[] transportationKeywords = {"교통", "지하철", "택시", "버스", "bus", "기차", "티머니", "KTX", "무궁화호"}; + String[] shoppingKeywords = {"쇼핑", "백화점", "현대백화점", "롯데백화점", "신세계백화점", "롯데마트", "이마트", "emart", "홈플러스", "homeplus", "롯데몰", "스타필드", "아울렛", "쿠팡", "coupang", "G마켓", "11번가", "네이버쇼핑", "마켓컬리", "배달의민족", "요기요", "배달","서점","올리브영"}; + String[] bakeryKeywords = {"베이커리", "빵", "bread", "bakery", "파리바게트", "뚜레쥬르", "성심당", "앤티앤스", "홍종흔베이커리", "아우어베이커리"}; - return new ChatDto(nowUser.getNickname(), ownedCardCount, savedChat.getId(),exampleCard.getName(),exampleCard.getImgUrl(),benefit,discount); + for (String key : convenienceStoreKeywords) { + if (keyword.contains(key)) { + return "편의점"; + } + } + for (String key : culturalKeywords) { + if (keyword.contains(key)) { + return "문화생활"; + } + } + for (String key : cafeKeywords) { + if (keyword.contains(key)) { + return "카페"; + } + } + for (String key : transportationKeywords) { + if (keyword.contains(key)) { + return "교통"; + } + } + for (String key : shoppingKeywords) { + if (keyword.contains(key)) { + return "쇼핑"; + } + } + for (String key : bakeryKeywords) { + if (keyword.contains(key)) { + return "베이커리"; + } + } + return "기타"; } + // 홈 화면 기본정보 조회 public HomeDto getHome(Users nowUser) throws CustomException { // 사용자별 맞춤형 키워드 조회 List allChatHistory = chatHistoryRepository.findByUsers(nowUser); @@ -66,41 +156,48 @@ public HomeDto getHome(Users nowUser) throws CustomException { String keyword = chatHistory.getKeyword(); keywordFrequencyMap.put(keyword, keywordFrequencyMap.getOrDefault(keyword, 0) + 1); } + + int ownedCardCount = ownedCardRepository.countAllByUsers(nowUser); + + // 빈도수가 높은 순서로 정렬 List topKeywords = keywordFrequencyMap.entrySet().stream() .sorted(Map.Entry.comparingByValue().reversed()) .limit(3) // 상위 3개 키워드 .map(Map.Entry::getKey) .collect(Collectors.toList()); - List defaultKeywords = List.of("배달의 민족", "스타벅스","GS25"); // 고정 키워드 - while (topKeywords.size() < 3) { - topKeywords.add(defaultKeywords.get(topKeywords.size())); - } + List defaultKeywords = List.of("배달의 민족", "스타벅스", "GS25"); // 고정 키워드 + defaultKeywords.stream() + .filter(keyword -> !topKeywords.contains(keyword)) + .limit(3 - topKeywords.size()) + .forEach(topKeywords::add); - return new HomeDto(nowUser.getNickname(),topKeywords); + return new HomeDto(nowUser.getNickname(), ownedCardCount, topKeywords); } + + // 결제 여부 변경 public void getPayCardHistory(Users nowUser, Long chatId) throws CustomException { Optional optionalChatHistory = chatHistoryRepository.findById(chatId); - ChatHistory chatHistory = optionalChatHistory.orElseThrow(() -> new CustomException(ResponseCode.CHAT_HISTORY_NOT_FOUND)); + ChatHistory chatHistory = optionalChatHistory.orElseThrow(() -> new CustomException(ResponseCode.CHAT_HISTORY_NOT_FOUND)); int paymentAmount = chatHistory.getPaymentAmount(); String cardName = chatHistory.getCardName(); Optional optionalCard = cardRepository.findByName(cardName); Card card = optionalCard.orElseThrow(() -> new CustomException(ResponseCode.CARD_NOT_FOUND)); - OwnedCard ownedCard = ownedCardRepository.findOwnedCardByCardAndUsers(card,nowUser); + OwnedCard ownedCard = ownedCardRepository.findOwnedCardByCardAndUsers(card, nowUser); boolean isMain = ownedCard.isMain(); // 주카드인 경우 실제 실적을 불러옴 - if(chatHistory.isHasPaid()==true){ + if (chatHistory.isHasPaid() == true) { chatHistory.setHasPaid(false); - if(isMain==false) { - ownedCard.setCurrentPerformance(ownedCard.getCurrentPerformance()-paymentAmount); + if (isMain == false) { + ownedCard.setCurrentPerformance(ownedCard.getCurrentPerformance() - paymentAmount); } } else { chatHistory.setHasPaid(true); - if(isMain==false) { - ownedCard.setCurrentPerformance(ownedCard.getCurrentPerformance()+paymentAmount); + if (isMain == false) { + ownedCard.setCurrentPerformance(ownedCard.getCurrentPerformance() + paymentAmount); } } @@ -110,7 +207,10 @@ public void getPayCardHistory(Users nowUser, Long chatId) throws CustomException return; } - public AnnouncListDto getAnnounce(Users nowUser) throws CustomException { + // 알림 리스트 조회 + public AnnounceListDto getAnnounce(Users nowUser) throws CustomException { + String nickname = nowUser.getNickname(); + LocalDate today = LocalDate.now(); LocalDate thisWeek = today.minusDays(7); @@ -136,8 +236,10 @@ public AnnouncListDto getAnnounce(Users nowUser) throws CustomException { && announcement.getCreatedAt().toLocalDate().isAfter(thisWeek)) .count(); - return new AnnouncListDto(uncheckedcnt,todayAnnounceDto,recentAnnounceDto); + return new AnnounceListDto(nickname, uncheckedcnt, todayAnnounceDto, recentAnnounceDto); } + + // 알림 상세 조회 public AnnounceDetailDto getAnnounceDetail(Long announceId) throws CustomException { Optional optionalAnnouncement = announcementRepository.findById(announceId); Announcement announcement = optionalAnnouncement.orElseThrow(() -> new CustomException(ResponseCode.ANNOUNCEMENT_NOT_FOUND)); @@ -145,4 +247,151 @@ public AnnounceDetailDto getAnnounceDetail(Long announceId) throws CustomExcepti announcementRepository.save(announcement); return new AnnounceDetailDto(announcement); } -} + public List searchStores(SearchStoresRequestDto dto, Users nowuser) throws CustomException { + // 단골가게 가져오기 + List favorites = favoriteRepository.findAllByUsers(nowuser).get(); + + if(favorites.isEmpty()){ + throw new CustomException(ResponseCode.NO_FAVORITE_STORE); + } + + double latitude = dto.getLatitude(); + double longitude = dto.getLongitude(); + + // 단골 가게 검색 시작 + String url = "https://places.googleapis.com/v1/places:searchText"; + // headers + HttpHeaders headers = new HttpHeaders(); + headers.add("accept", "application/json"); + headers.add("X-Goog-Api-Key", apiKey); + headers.add("X-Goog-FieldMask", "places.formattedAddress,places.location,places.displayName"); + + List resultList = new ArrayList<>(); + + for (Favorite favorite : favorites) { + // request body + Map requestBody = new HashMap(); + requestBody.put("textQuery", favorite.getName()); + requestBody.put("maxResultCount", 10); + requestBody.put("languageCode", "ko"); + Map locationBias = new HashMap(); + Map circle = new HashMap(); + Map center = new HashMap(); + center.put("latitude", latitude); + center.put("longitude", longitude); + circle.put("center", center); + circle.put("radius", 500.0); + locationBias.put("circle", circle); + requestBody.put("locationBias", locationBias); + + GoogleMapPlaceResponseDto responsebody; + try { + + HttpEntity> requestData = new HttpEntity<>(requestBody, headers); + ResponseEntity responseEntity = restTemplate.postForEntity(url, requestData, GoogleMapPlaceResponseDto.class); + responsebody = responseEntity.getBody(); + } catch (Exception e){ + throw new CustomException(ResponseCode.GOOGLE_MAP_SEARCH_PLACE_FAIL); + } + System.out.println(responsebody); + if (responsebody.getPlaces() == null){ + throw new CustomException(ResponseCode.NO_SEARCHED_FAVORITE_STORE); + } + + List placeList = responsebody.getPlaces(); + for( Place place : placeList){ + SearchStoresResponseDto searchedResult = new SearchStoresResponseDto(); + + searchedResult.setStore(favorite.getName()); + searchedResult.setStoreName(place.getDisplayName().getText()); + searchedResult.setLatitude(place.getLocation().getLatitude()); + searchedResult.setLongitude(place.getLocation().getLongitude()); + + resultList.add(searchedResult); + } + } + return resultList; + } + public void postAnnounceFavorite(AnnounceFavoriteRequestDto dto, Users nowUser) throws CustomException { + List fcmTokens = fcmTokenRepository.findAllByUsers(nowUser); + String keyword = dto.getStore(); + int paymentAmount=10000; + String response = openaiService.cardRecommend(nowUser, keyword, paymentAmount); + ObjectMapper objectMapper = new ObjectMapper(); + Integer cardId; + Card card; + String benefit; + Integer discount; + try { + Map map = objectMapper.readValue(response, Map.class); + cardId = (Integer) map.get("카드번호"); + card = cardRepository.findById(Long.valueOf(cardId)).orElse(null); + benefit = (String) map.get("혜택 정보"); + discount = (Integer) map.get("할인 금액"); + + } catch ( JsonProcessingException e) { + throw new CustomException(ResponseCode.FAILED_TO_OPENAI_RECOMMEND); + } + + String contents = dto.getStoreName()+" 근처시군요.\n"+card.getName()+" 사용해 보세요!"; + String title = dto.getStoreName()+" 근처시군요."; + String content = card.getName()+" 사용해 보세요!"; + String moreInfo = dto.getLatitude()+", "+dto.getLongitude(); + Announcement announcement = Announcement.builder() + .users(nowUser) + .type(0) + .content(contents) + .moreInfo(moreInfo) + .hasCheck(false) + .build(); + announcementRepository.save(announcement); + for ( FCMToken fcmToken : fcmTokens){ + + String token = fcmToken.getToken(); + firebaseCloudMessageService.sendNotification(new AnnouncementRequestDto(token,title,content,announcement.getId().toString())); + } + } + public void postBeaconAnnouncement(BeaconRequestDto dto, Users nowUser)throws CustomException { + List fcmTokens = fcmTokenRepository.findAllByUsers(nowUser); + + Beacon beacon = beaconRepository.findAllByProximityUUIDAndMajorAndMinor(dto.getProximityUUID(),dto.getMajor(),dto.getMinor()); + + String keyword = beacon.getName(); + int paymentAmount=10000; + String response = openaiService.cardRecommend(nowUser, keyword, paymentAmount); + ObjectMapper objectMapper = new ObjectMapper(); + Integer cardId; + Card card; + String benefit; + Integer discount; + try { + Map map = objectMapper.readValue(response, Map.class); + cardId = (Integer) map.get("카드번호"); + card = cardRepository.findById(Long.valueOf(cardId)).orElse(null); + benefit = (String) map.get("혜택 정보"); + discount = (Integer) map.get("할인 금액"); + + } catch ( JsonProcessingException e) { + throw new CustomException(ResponseCode.FAILED_TO_OPENAI_RECOMMEND); + } + + String title = beacon.getStore()+" 근처시군요."; + String content = card.getName()+" 사용해 보세요!"; + String contents = beacon.getStore()+" 근처시군요.\n"+card.getName()+" 사용해 보세요!"; + + String moreInfo = beacon.getLatitude()+", "+beacon.getLongitude(); + Announcement announcement = Announcement.builder() + .users(nowUser) + .type(0) + .content(contents) + .moreInfo(moreInfo) + .hasCheck(false) + .build(); + announcementRepository.save(announcement); + for ( FCMToken fcmToken : fcmTokens){ + String token = fcmToken.getToken(); + firebaseCloudMessageService.sendNotification(new AnnouncementRequestDto(token,title,content,announcement.getId().toString())); + } + + } +} \ No newline at end of file diff --git a/src/main/java/ewha/lux/once/domain/home/service/OpenaiService.java b/src/main/java/ewha/lux/once/domain/home/service/OpenaiService.java new file mode 100644 index 0000000..dfefe9d --- /dev/null +++ b/src/main/java/ewha/lux/once/domain/home/service/OpenaiService.java @@ -0,0 +1,168 @@ +package ewha.lux.once.domain.home.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import ewha.lux.once.domain.card.entity.BenefitSummary; +import ewha.lux.once.domain.card.entity.Card; +import ewha.lux.once.domain.card.entity.OwnedCard; +import ewha.lux.once.domain.home.dto.BenefitDto; +import ewha.lux.once.domain.home.dto.OpenaiChatRequest; +import ewha.lux.once.domain.home.dto.OpenaiChatResponse; +import ewha.lux.once.domain.user.entity.Users; +import ewha.lux.once.global.common.CustomException; +import ewha.lux.once.global.common.ResponseCode; +import ewha.lux.once.global.repository.BenefitSummaryRepository; +import ewha.lux.once.global.repository.CardRepository; +import ewha.lux.once.global.repository.OwnedCardRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestTemplate; + +import java.util.List; + +@Service +@Slf4j +@RequiredArgsConstructor +public class OpenaiService { + + @Qualifier("openaiRestTemplate") + @Autowired + private RestTemplate restTemplate; + private final BenefitSummaryRepository benefitSummaryRepository; + private final OwnedCardRepository ownedCardRepository; + private final CardRepository cardRepository; + + @Value("${openai.model}") + private String model; + + @Value("${openai.api.url}") + private String apiUrl; + + + // 결제할 카드 추천 + public String cardRecommend(Users nowUser, String keyword, int paymentAmount) throws CustomException { + String prompt = "결제 금액, 결제처, 카드들의 혜택 정보를 입력으로 받아, 각 카드별로 결제처에 해당하는 혜택이 있다면 할인 금액을 계산합니다. 가장 큰 할인을 받을 수 있는 카드의 \"카드번호\", \"혜택 정보\", \"할인 금액\"을 JSON 형식으로 반환합니다.\\" + +"```를 붙이지 않습니다. 결제처에 해당하는 카드의 혜택이 없거나, 결제처가 분야·브랜드명이 아니라면, 모든 value에 0을 넣어 반환합니다.\\" + +"\"카드번호\" 는 해당 카드의 '카드 고유 번호', \"혜택 정보\"는 결제처에 해당되는 혜택 정보 요약 텍스트(특수문자 없이 20자 이내)를 의미합니다."; + + List ownedCards = ownedCardRepository.findOwnedCardByUsers(nowUser); + + String userInput = "{\"결제 금액\": " + paymentAmount + ", \"결제처\": \"" + keyword + "\", \"카드들의 혜택 정보\": ["; + for (OwnedCard ownedCard : ownedCards) { + String name = ownedCard.getCard().getName(); + String id = ownedCard.getCard().getId().toString(); + Card card = ownedCard.getCard(); + userInput = userInput +"{\"이름\": \""+ name + "\", " + "\"카드 고유 번호\" : " + id + ", \"혜택\": [ "; + List beneList = benefitSummaryRepository.findByCard(card); + for( BenefitSummary benefit : beneList){ + userInput += "\""+benefit.getBenefitField()+" "+benefit.getBenefitContents()+"\","; + } + userInput = userInput.substring(0, userInput.length() - 1); + userInput += "\" },"; + } + userInput = userInput.substring(0, userInput.length() - 1); + userInput += "]"; + + // gpt 요청 보내는 부분 + OpenaiChatRequest request = new OpenaiChatRequest(model, prompt, userInput); + OpenaiChatResponse response = restTemplate.postForObject(apiUrl, request, OpenaiChatResponse.class); + + if (response == null || response.getChoices() == null || response.getChoices().isEmpty()) { + throw new CustomException(ResponseCode.FAILED_TO_OPENAI); + } + + String result = response.getChoices().get(0).getMessage().getContent(); + + log.info(result); + + return result; + } + + // 카드 혜택 요약 + public BenefitDto[] gptBenefitSummary(String benefits) throws CustomException, JsonProcessingException { + + // ** 프롬프트 수정 필요 ** + String prompt = "입력된 데이터를 [] 사이에 주어진 key를 가지는 JSON 형식의 list로 요약하여 제공해 줘 [benefit_field, content]\\nbenefit_field는 혜택의 분야, content는 혜택 할인율 정보를 핵심만 나타냄. \\ output 형식은 다음과 같음. [{\n" + + " \"benefit_field\": \"편의점\",\n" + + " \"content\": \"4대 편의점 이용 시 10% 적립\"\n" + + " },\n" + + " {\n" + + " \"benefit_field\": \"커피 업종\",\n" + + " \"content\": \"커피 업종 이용 시 10% 적립\"\n" + + " },\n" + + " {\n" + + " \"benefit_field\": \"해외 이용금액\",\n" + + " \"content\": \"해외 이용금액 1% 적립\"\n" + + " },\n" + + " {\n" + + " \"benefit_field\": \"디지털 구독\",\n" + + " \"content\": \"디지털 구독 영역 이용 시 10% 적립\"\n" + + " },\n" + + " {\n" + + " \"benefit_field\": \"One Pick 쇼핑몰\",\n" + + " \"content\": \"One Pick 온라인 쇼핑몰 가맹점 최대 3천 포인트 적립\"\n" + + " } ]"; + + // gpt 요청 보내는 부분 + OpenaiChatRequest request = new OpenaiChatRequest(model, prompt, benefits); + OpenaiChatResponse response = restTemplate.postForObject(apiUrl, request, OpenaiChatResponse.class); + + if (response == null || response.getChoices() == null || response.getChoices().isEmpty()) { + throw new CustomException(ResponseCode.FAILED_TO_OPENAI); + } + + String result = response.getChoices().get(0).getMessage().getContent(); + + ObjectMapper objectMapper = new ObjectMapper(); + BenefitDto[] benefitJson = objectMapper.readValue(result, BenefitDto[].class); + + return benefitJson; + } + // ** 추후 삭제해야 함 - 테스트용 ** ================================== + public BenefitDto[] gptBenefitSummaryTest(String benefits, String prompt, String model_name) throws CustomException, JsonProcessingException { + try { + // gpt 요청 보내는 부분 + OpenaiChatRequest request = new OpenaiChatRequest(model_name, prompt, benefits); + OpenaiChatResponse response = restTemplate.postForObject(apiUrl, request, OpenaiChatResponse.class); + + if (response == null || response.getChoices() == null || response.getChoices().isEmpty()) { + throw new CustomException(ResponseCode.FAILED_TO_OPENAI); + } + + String result = response.getChoices().get(0).getMessage().getContent(); + + ObjectMapper objectMapper = new ObjectMapper(); + BenefitDto[] benefitJson = objectMapper.readValue(result, BenefitDto[].class); + + return benefitJson; + } catch(CustomException | JsonProcessingException | HttpClientErrorException e){ + e.printStackTrace(); + System.out.println("===========오류=========="); + int i=2; + while (i>0) { + try { + OpenaiChatRequest request = new OpenaiChatRequest("gpt-4-turbo-preview", prompt, benefits); + OpenaiChatResponse response = restTemplate.postForObject(apiUrl, request, OpenaiChatResponse.class); + String result = response.getChoices().get(0).getMessage().getContent(); + + ObjectMapper objectMapper = new ObjectMapper(); + BenefitDto[] benefitJson = objectMapper.readValue(result, BenefitDto[].class); + return benefitJson; +// } catch (JsonProcessingException | InterruptedException| HttpClientErrorException ex) { + } catch (JsonProcessingException | HttpClientErrorException ex) { + ex.printStackTrace(); + i--; + } + } + return null; + } + } + // ============================================================ +} + diff --git a/src/main/java/ewha/lux/once/domain/mypage/service/MypageService.java b/src/main/java/ewha/lux/once/domain/mypage/service/MypageService.java index 2eab41b..b58501a 100644 --- a/src/main/java/ewha/lux/once/domain/mypage/service/MypageService.java +++ b/src/main/java/ewha/lux/once/domain/mypage/service/MypageService.java @@ -5,6 +5,7 @@ import ewha.lux.once.domain.card.entity.CardType; import ewha.lux.once.domain.card.entity.OwnedCard; import ewha.lux.once.domain.home.entity.ChatHistory; +import ewha.lux.once.domain.home.service.CODEFAsyncService; import ewha.lux.once.domain.mypage.dto.CardListResponseDto; import ewha.lux.once.domain.mypage.dto.ChatHistoryResponseDto; import ewha.lux.once.domain.mypage.dto.MypageResponseDto; @@ -33,6 +34,7 @@ public class MypageService { private final ChatHistoryRepository chatHistoryRepository; private final CardRepository cardRepository; private final CardCompanyRepository cardCompanyRepository; + private final CODEFAsyncService codefAsyncService; public MypageResponseDto getMypageInfo(Users nowUser) throws CustomException { @@ -143,6 +145,8 @@ public String patchReleaseMaincard(Users nowUser, Long ownedCardId) throws Custo } ownedCard.releaseMaincard(); + + codefAsyncService.deleteConnectedID(nowUser,ownedCard); } else { throw new CustomException(ResponseCode.INVALID_OWNED_CARD); } diff --git a/src/main/java/ewha/lux/once/domain/user/controller/UserController.java b/src/main/java/ewha/lux/once/domain/user/controller/UserController.java index 1f78194..e232cb7 100644 --- a/src/main/java/ewha/lux/once/domain/user/controller/UserController.java +++ b/src/main/java/ewha/lux/once/domain/user/controller/UserController.java @@ -1,5 +1,7 @@ package ewha.lux.once.domain.user.controller; +import ewha.lux.once.domain.home.dto.FCMTokenDto; +import ewha.lux.once.domain.home.service.FirebaseCloudMessageService; import ewha.lux.once.domain.user.dto.*; import ewha.lux.once.domain.user.entity.Users; import ewha.lux.once.domain.user.service.UserService; @@ -27,6 +29,7 @@ public class UserController { private final UserService userService; private final JwtProvider jwtProvider; + private final FirebaseCloudMessageService firebaseCloudMessageService; // [Post] 회원가입 @PostMapping("/signup") @@ -98,9 +101,9 @@ public CommonResponse searchCard(@Param("code") String code) throws CustomExc // [Get] 카드 이름 검색 @GetMapping("/card/searchname") @ResponseBody - public CommonResponse searchCardName(@Param("name") String name) { + public CommonResponse searchCardName(@Param("name") String name,@Param("code") String code) { try{ - return new CommonResponse<>(ResponseCode.SUCCESS, userService.getSearchCardName(name)); + return new CommonResponse<>(ResponseCode.SUCCESS, userService.getSearchCardName(name, code)); } catch (CustomException e) { return new CommonResponse<>(e.getStatus()); } @@ -173,6 +176,18 @@ public CommonResponse editUserInfo(@AuthenticationPrincipal UserAccount userA return new CommonResponse<>(e.getStatus()); } } + + // [Post] FCM Token 등록 + @PostMapping("/token") + public CommonResponse saveFCMToken(@AuthenticationPrincipal UserAccount userAccount,@RequestBody FCMTokenDto fcmTokenDto) { + try { + firebaseCloudMessageService.postFCMToken(userAccount.getUsers(), fcmTokenDto.getToken()); + return new CommonResponse<>(ResponseCode.SUCCESS); + } catch (CustomException e) { + return new CommonResponse<>(e.getStatus()); + } + } + } diff --git a/src/main/java/ewha/lux/once/domain/user/dto/CardNameSearchDto.java b/src/main/java/ewha/lux/once/domain/user/dto/CardNameSearchDto.java index 13e8e90..5b8726b 100644 --- a/src/main/java/ewha/lux/once/domain/user/dto/CardNameSearchDto.java +++ b/src/main/java/ewha/lux/once/domain/user/dto/CardNameSearchDto.java @@ -14,5 +14,6 @@ public class CardNameSearchDto { private String cardName; private String cardImg; private String companyName; + private String type; } diff --git a/src/main/java/ewha/lux/once/domain/user/dto/CardSearchDto.java b/src/main/java/ewha/lux/once/domain/user/dto/CardSearchDto.java index 60740b3..87f2e9c 100644 --- a/src/main/java/ewha/lux/once/domain/user/dto/CardSearchDto.java +++ b/src/main/java/ewha/lux/once/domain/user/dto/CardSearchDto.java @@ -13,5 +13,6 @@ public class CardSearchDto { private Long cardId; private String cardName; private String cardImg; + private String type; } diff --git a/src/main/java/ewha/lux/once/domain/user/dto/UserEditResponseDto.java b/src/main/java/ewha/lux/once/domain/user/dto/UserEditResponseDto.java index a8ca123..897af62 100644 --- a/src/main/java/ewha/lux/once/domain/user/dto/UserEditResponseDto.java +++ b/src/main/java/ewha/lux/once/domain/user/dto/UserEditResponseDto.java @@ -17,15 +17,17 @@ @AllArgsConstructor public class UserEditResponseDto { private String userProfileImg; + private String username; private String nickname; private String loginId; private String birthday; private String userPhoneNum; private String createdAt; - public static UserEditResponseDto fromEntity(Users users){ + public static UserEditResponseDto fromEntity(Users users) { UserEditResponseDto userEditResponseDto = new UserEditResponseDto(); userEditResponseDto.setUserProfileImg(users.getProfileImg()); + userEditResponseDto.setUsername(users.getUsername()); userEditResponseDto.setNickname(users.getNickname()); userEditResponseDto.setLoginId(users.getLoginId()); @@ -39,14 +41,14 @@ public static UserEditResponseDto fromEntity(Users users){ if (birthday != null) { SimpleDateFormat birthdayFormatter = new SimpleDateFormat("yyyy.MM.dd"); userEditResponseDto.setBirthday(birthdayFormatter.format(birthday)); - } else{ + } else { userEditResponseDto.setBirthday(""); } String phone = users.getPhone(); if (phone != null) { userEditResponseDto.setUserPhoneNum(phone); - }else{ + } else { userEditResponseDto.setUserPhoneNum(""); } diff --git a/src/main/java/ewha/lux/once/domain/user/entity/Users.java b/src/main/java/ewha/lux/once/domain/user/entity/Users.java index a68f428..03f829a 100644 --- a/src/main/java/ewha/lux/once/domain/user/entity/Users.java +++ b/src/main/java/ewha/lux/once/domain/user/entity/Users.java @@ -2,6 +2,7 @@ import ewha.lux.once.domain.user.dto.EditUserInfoRequestDto; import ewha.lux.once.global.common.BaseEntity; +import ewha.lux.once.global.common.ColumnEncryptor; import jakarta.persistence.*; import lombok.*; @@ -56,6 +57,11 @@ public class Users extends BaseEntity implements UserDetails { @Column(name = "benefitGoal") private int benefitGoal; + @Column(name = "connectedId") + @Convert(converter = ColumnEncryptor.class) + private String connectedId; + + @Override public Collection getAuthorities() { List authorities = new ArrayList<>(); @@ -89,12 +95,19 @@ public void setLastLogin() { } public void editUserInfo(EditUserInfoRequestDto requestDto){ - this.username = requestDto.getUsername(); - this.nickname = requestDto.getNickname(); - this.birthday = requestDto.getBirthday(); - this.phone = requestDto.getUserPhoneNum(); + + if (requestDto.getUsername() != null) + this.username = requestDto.getUsername(); + if (requestDto.getNickname() != null) + this.nickname = requestDto.getNickname(); + if (requestDto.getBirthday() != null) + this.birthday = requestDto.getBirthday(); + if (requestDto.getUserPhoneNum() != null) + this.phone = requestDto.getUserPhoneNum(); } + + public void setProfileImg(String profileImg) { this.profileImg = profileImg; } @@ -104,4 +117,6 @@ public void updatePassword(String password) { } public void setCardGoal(int goal) {this.benefitGoal = goal;} + + public void setConnectedId(String connectedId) {this.connectedId = connectedId;} } diff --git a/src/main/java/ewha/lux/once/domain/user/service/UserService.java b/src/main/java/ewha/lux/once/domain/user/service/UserService.java index 49f32a4..0aca133 100644 --- a/src/main/java/ewha/lux/once/domain/user/service/UserService.java +++ b/src/main/java/ewha/lux/once/domain/user/service/UserService.java @@ -1,16 +1,15 @@ package ewha.lux.once.domain.user.service; -import ewha.lux.once.domain.card.entity.Card; -import ewha.lux.once.domain.card.entity.CardCompany; -import ewha.lux.once.domain.card.entity.OwnedCard; +import ewha.lux.once.domain.card.entity.*; +import ewha.lux.once.domain.home.entity.Announcement; +import ewha.lux.once.domain.home.entity.ChatHistory; +import ewha.lux.once.domain.home.entity.FCMToken; +import ewha.lux.once.domain.home.entity.Favorite; import ewha.lux.once.domain.user.dto.*; import ewha.lux.once.domain.user.entity.Users; import ewha.lux.once.global.common.CustomException; import ewha.lux.once.global.common.ResponseCode; -import ewha.lux.once.global.repository.CardCompanyRepository; -import ewha.lux.once.global.repository.CardRepository; -import ewha.lux.once.global.repository.OwnedCardRepository; -import ewha.lux.once.global.repository.UsersRepository; +import ewha.lux.once.global.repository.*; import lombok.RequiredArgsConstructor; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; @@ -24,10 +23,7 @@ import java.sql.Timestamp; import java.text.ParseException; import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Optional; +import java.util.*; import java.util.stream.Collectors; @Service @@ -38,11 +34,16 @@ public class UserService implements UserDetailsService { private final CardRepository cardRepository; private final CardCompanyRepository cardCompanyRepository; private final OwnedCardRepository ownedCardRepository; + private final AnnouncementRepository announcementRepository; + private final ChatHistoryRepository chatHistoryRepository; + private final ConnectedCardCompanyRepository connectedCardCompanyRepository; + private final FavoriteRepository favoriteRepository; + private final FCMTokenRepository fcmTokenRepository; private final S3Uploader s3Uploader; public Users signup(SignupRequestDto request) throws CustomException, ParseException { String loginId = request.getLoginId(); - String username = request.getUsername(); + String username = request.getUsername(); String password = request.getPassword(); String nickname = request.getNickname(); String phone = request.getUserPhoneNum(); @@ -86,7 +87,7 @@ public Users authenticate(SignInRequestDto request) throws CustomException { Optional optionalUsers = usersRepository.findByLoginId(loginId); Users users = optionalUsers.orElseThrow(() -> new CustomException(ResponseCode.INVALID_USER_ID)); - if (!passwordEncoder.matches(password, users.getPassword())){ + if (!passwordEncoder.matches(password, users.getPassword())) { throw new CustomException(ResponseCode.FAILED_TO_LOGIN); } @@ -96,6 +97,18 @@ public Users authenticate(SignInRequestDto request) throws CustomException { } public void deleteUsers(Users nowUser) throws CustomException { + List announcementList = announcementRepository.findAnnouncementByUsers(nowUser); + announcementRepository.deleteAll(announcementList); + List chatHistoryList = chatHistoryRepository.findByUsers(nowUser); + chatHistoryRepository.deleteAll(chatHistoryList); + List connectedCardCompanyList = connectedCardCompanyRepository.findAllByUsers(nowUser); + connectedCardCompanyRepository.deleteAll(connectedCardCompanyList); + List ownedCardList = ownedCardRepository.findOwnedCardByUsers(nowUser); + ownedCardRepository.deleteAll(ownedCardList); + List favoriteList = favoriteRepository.findAllByUsers(nowUser).get(); + favoriteRepository.deleteAll(favoriteList); + List fcmTokenList = fcmTokenRepository.findAllByUsers(nowUser); + fcmTokenRepository.deleteAll(fcmTokenList); usersRepository.delete(nowUser); return; } @@ -122,6 +135,8 @@ public List getSearchCard(String code) throws CustomException cardSearchDto.setCardId(card.getId()); cardSearchDto.setCardName(card.getName()); cardSearchDto.setCardImg(card.getImgUrl()); + if (card.getType().toString() == "DebitCard") cardSearchDto.setType("체크카드"); + else cardSearchDto.setType("신용카드"); cardSearchDtos.add(cardSearchDto); } cardSearchListDto.setCardList(cardSearchDtos); @@ -131,8 +146,10 @@ public List getSearchCard(String code) throws CustomException return response; } - public List getSearchCardName(String name) throws CustomException{ - List cards = cardRepository.findAllByNameContains(name); + public List getSearchCardName(String name, String code) throws CustomException { + String[] codes = code.split(","); + List cardCompanies = cardCompanyRepository.findByCodeIn(Arrays.asList(codes)); + List cards = cardRepository.findByNameContainingAndCardCompanyIn(name, cardCompanies); if (cards.isEmpty()) { throw new CustomException(ResponseCode.NO_SEARCH_RESULTS); } @@ -141,12 +158,17 @@ public List getSearchCardName(String name) throws CustomExcep card.getId(), card.getName(), card.getImgUrl(), - card.getCardCompany().getName() + card.getCardCompany().getName(), + getCardTypeName(card.getType()) )) .collect(Collectors.toList()); } - public void postSearchCard(Users nowUser,postSearchCardListRequestDto requestDto) throws CustomException { + private String getCardTypeName(CardType type) { + return type == CardType.CreditCard ? "신용카드" : "체크카드"; + } + + public void postSearchCard(Users nowUser, postSearchCardListRequestDto requestDto) throws CustomException { List card_list = requestDto.getCardList(); for (Long cardId : card_list) { Optional optionalCard = cardRepository.findById(cardId); @@ -164,8 +186,8 @@ public void postSearchCard(Users nowUser,postSearchCardListRequestDto requestDto } public String patchEditProfile(Users nowUser, MultipartFile userProfileImg) throws IOException, CustomException { - if(!userProfileImg.isEmpty()) { - String storedFileName = s3Uploader.upload(userProfileImg,nowUser.getLoginId()+"-profile.png"); + if (!userProfileImg.isEmpty()) { + String storedFileName = s3Uploader.upload(userProfileImg, nowUser.getLoginId() + "-profile.png"); nowUser.setProfileImg(storedFileName); } usersRepository.save(nowUser); @@ -173,7 +195,7 @@ public String patchEditProfile(Users nowUser, MultipartFile userProfileImg) thro } public boolean getIdDuplicateCheck(String loginId) throws CustomException { - if(usersRepository.existsByLoginId(loginId)) { + if (usersRepository.existsByLoginId(loginId)) { return false; } return true; @@ -185,6 +207,7 @@ public boolean postCheckPassword(Users nowUser, ChangePasswordDto checkPasswordR public String patchChangePassword(Users nowUser, ChangePasswordDto changePasswordDto) throws CustomException { nowUser.updatePassword(passwordEncoder.encode(changePasswordDto.getPassword())); + usersRepository.save(nowUser); return ResponseCode.CHANGE_PW_SUCCESS.getMessage(); } diff --git a/src/main/java/ewha/lux/once/global/CODEF/ApiRequest.java b/src/main/java/ewha/lux/once/global/CODEF/ApiRequest.java new file mode 100644 index 0000000..6eebf32 --- /dev/null +++ b/src/main/java/ewha/lux/once/global/CODEF/ApiRequest.java @@ -0,0 +1,39 @@ +package ewha.lux.once.global.CODEF; + + +import java.io.IOException; +import java.net.URLEncoder; +import java.util.HashMap; + +import org.json.simple.JSONObject; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class ApiRequest { + + private static ObjectMapper mapper = new ObjectMapper(); + + @Value("${codef.access-token}") + // redis의 토큰 가져오기 + private String ACCESS_TOKEN; + @Value("${codef.client-id}") + private String CLIENT_ID; + @Value("${codef.seceret-key}") + private String SECERET_KEY; + + public JSONObject reqeust(String urlPath, HashMap bodyMap) throws IOException { + + // POST요청을 위한 리퀘스트바디 생성(UTF-8 인코딩) + String bodyString = mapper.writeValueAsString(bodyMap); + bodyString = URLEncoder.encode(bodyString, "UTF-8"); + + // API 요청 + JSONObject json = (JSONObject) HttpRequest.post(urlPath, ACCESS_TOKEN, bodyString); + + return json; + } + +} \ No newline at end of file diff --git a/src/main/java/ewha/lux/once/global/CODEF/CommonConstant.java b/src/main/java/ewha/lux/once/global/CODEF/CommonConstant.java new file mode 100644 index 0000000..9a1ea4b --- /dev/null +++ b/src/main/java/ewha/lux/once/global/CODEF/CommonConstant.java @@ -0,0 +1,29 @@ +package ewha.lux.once.global.CODEF; + +public class CommonConstant { + + public static final String API_DOMAIN = "https://api.codef.io"; // API서버 도메인 + public static final String TEST_DOMAIN = "https://development.codef.io"; // API서버 데모 도메인 + + public static final String TOKEN_DOMAIN = "https://oauth.codef.io"; // OAUTH2.0 테스트 도메인 + public static final String GET_TOKEN = "/oauth/token"; // OAUTH2.0 토큰 발급 요청 URL + + public static final String KR_CD_P_001 = "/v1/kr/card/p/account/card-list"; // 카드 개인 보유카드 + public static final String KR_CD_P_002 = "/v1/kr/card/p/account/approval-list"; // 카드 개인 승인내역 + public static final String KR_CD_P_003 = "/v1/kr/card/p/account/billing-list"; // 카드 개인 청구내역 + public static final String KR_CD_P_004 = "/v1/kr/card/p/account/limit"; // 카드 개인 한도조회 + public static final String KR_CD_P_005 = "/v1/kr/card/p/account/result-check-list"; // 카드 개인 실적조회 + public static final String KR_CD_P_006 = "/v1/kr/card/p/user/registration-status"; // 카드 개인 등록 여부 조회 + + public static final String GET_CONNECTED_IDS = "/v1/account/connectedId-list"; // 커넥티드아이디 목록 조회 + public static final String GET_ACCOUNTS = "/v1/account/list"; // 계정 목록 조회 + public static final String CREATE_ACCOUNT = "/v1/account/create"; // 계정 등록(커넥티드아이디 발급) + public static final String ADD_ACCOUNT = "/v1/account/add"; // 계정 추가 + public static final String UPDATE_ACCOUNT = "/v1/account/update"; // 계정 수정 + public static final String DELETE_ACCOUNT = "/v1/account/delete"; // 계정 삭제 + + public static String getRequestDomain() { + return CommonConstant.TEST_DOMAIN; + } + +} \ No newline at end of file diff --git a/src/main/java/ewha/lux/once/global/CODEF/HttpRequest.java b/src/main/java/ewha/lux/once/global/CODEF/HttpRequest.java new file mode 100644 index 0000000..a02b81c --- /dev/null +++ b/src/main/java/ewha/lux/once/global/CODEF/HttpRequest.java @@ -0,0 +1,39 @@ +package ewha.lux.once.global.CODEF; + +import java.net.URLDecoder; +import java.util.List; + +import org.json.simple.parser.JSONParser; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; + +import org.springframework.http.*; +import org.springframework.web.client.RestTemplate; + + +public class HttpRequest { + private static final RestTemplate restTemplate = new RestTemplate(); + public static Object post(String url_path, String token, String bodyString) { + try { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setAccept(List.of(MediaType.APPLICATION_JSON)); + + + if (token != null) { + headers.setBearerAuth(token); + } + + HttpEntity requestEntity = new HttpEntity<>(bodyString, headers); + ResponseEntity responseEntity = restTemplate.exchange(url_path, HttpMethod.POST, requestEntity, String.class); + + String response = responseEntity.getBody(); + Object obj = new JSONParser().parse(URLDecoder.decode(response, "UTF-8")); + + return obj; + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } +} \ No newline at end of file diff --git a/src/main/java/ewha/lux/once/global/common/ColumnEncryptor.java b/src/main/java/ewha/lux/once/global/common/ColumnEncryptor.java new file mode 100644 index 0000000..e129bf6 --- /dev/null +++ b/src/main/java/ewha/lux/once/global/common/ColumnEncryptor.java @@ -0,0 +1,63 @@ +package ewha.lux.once.global.common; + +import jakarta.persistence.AttributeConverter; +import org.apache.commons.codec.binary.Hex; +import org.springframework.beans.factory.annotation.Value; + +import javax.annotation.PostConstruct; +import javax.crypto.Cipher; +import javax.crypto.spec.SecretKeySpec; +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; + +public class ColumnEncryptor implements AttributeConverter { + @Value("${spring.encrypt.key}") + private String key; + + private Cipher encryptCipher; + private Cipher decryptCipher; + + @PostConstruct + public void init() throws Exception{ + encryptCipher = Cipher.getInstance("AES"); + encryptCipher.init(Cipher.ENCRYPT_MODE, generateMySQLAESKey(key, "UTF-8")); + decryptCipher = Cipher.getInstance("AES"); + decryptCipher.init(Cipher.DECRYPT_MODE, generateMySQLAESKey(key, "UTF-8")); + } + + // 암호화 + @Override + public String convertToDatabaseColumn(String attribute) { + try { + return new String(Hex.encodeHex(encryptCipher.doFinal(attribute.getBytes(StandardCharsets.UTF_8)))).toUpperCase(); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + // 복호화 + @Override + public String convertToEntityAttribute(String dbData) { + try { + if (dbData == null) return null; + return new String(decryptCipher.doFinal(Hex.decodeHex(dbData.toCharArray()))); + }catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + public static SecretKeySpec generateMySQLAESKey(final String key, final String encoding) { + try { + final byte[] finalKey = new byte[16]; + int i = 0; + for(byte b : key.getBytes(encoding)) + finalKey[i++%16] ^= b; + return new SecretKeySpec(finalKey, "AES"); + } catch(UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/src/main/java/ewha/lux/once/global/common/ResponseCode.java b/src/main/java/ewha/lux/once/global/common/ResponseCode.java index 28c7723..75b4c14 100644 --- a/src/main/java/ewha/lux/once/global/common/ResponseCode.java +++ b/src/main/java/ewha/lux/once/global/common/ResponseCode.java @@ -5,7 +5,7 @@ @Getter public enum ResponseCode { /* - 1000 : Request 성공 + 1000 : Request 성공 */ SUCCESS(1000, true, "요청에 성공하였습니다."), CHANGE_PW_SUCCESS(1001, true, "비밀 번호 수정을 성공했습니다."), @@ -35,11 +35,35 @@ public enum ResponseCode { OWNED_CARD_NOT_FOUND(3104, false, "보유한 카드가 없습니다."), INVALID_OWNED_CARD(3105, false, "보유한 카드가 아닙니다."), INVALID_MAINCARD(3106, false, "주카드가 아닙니다."), + CARD_BENEFITS_CRAWLING_FAIL(3107, false, "카드 혜택 데이터 크롤링에 실패했습니다."), + CARD_BENEFITS_INSERT_FAIL(3108, false, "카드 혜택 데이베이스 저장에 실패했습니다."), + NO_CONNECTED_CARD_COMPANY(3109, false, "연결된 카드사가 없습니다"), // 3200~ : mypage 관련 오류 CHAT_HISTORY_NOT_FOUND(3200, false, "채팅이 존재하지 않습니다."), + // 3300~ : 챗봇 관련 오류 + FAILED_TO_GEMINI(3300, false, "요청 과정에서 오류가 발생했습니다."), + FAILED_TO_OPENAI(3301, false, "요청 과정에서 오류가 발생했습니다."), + FAILED_TO_OPENAI_RECOMMEND(3302, false, "잘못된 응답이 반환되었습니다."), + + // 3400~ : 알림 관련 오류 + FCM_SEND_NOTIFICATION_FAIL(3400,false,"FCM 알림 전송에 실패하였습니다."), + AES_ENCRYPTION_ERROR(3401,false,"값을 암호화하는데 실패하였습니다."), + GOOGLE_MAP_SEARCH_PLACE_FAIL(3402,false,"Google Map 장소 검색에 실패하였습니다."), + NO_FAVORITE_STORE(3403,false,"등록된 단골가게가 없습니다."), + NO_SEARCHED_FAVORITE_STORE(3404,false,"Google Map 장소 검색 결과가 없습니다."), + + // 3500~ : codef api 관련 오류 + CODEF_REGISTRATION_STATUS_FAIL(3500,false,"CODEF API 등록 여부 확인에 실패하였습니다."), + CODEF_CONNECTEDID_CREATE_FAIL(3501,false,"CODEF API 커넥티드아이디-계정 등록에 실패하였습니다."), + CODEF_CONNECTEDID_ADD_FAIL(3502,false,"CODEF API 커넥티드아이디-계정 추가에 실패하였습니다."), + CODEF_GET_CARD_LIST_FAIL(3503,false,"CODEF API 보유 카드 조회에 실패하였습니다."), + CODEF_GET_CARD_PERFORMANCE_FAIL(3504,false,"CODEF API 카드 실적 조회에 실패하였습니다."), + CODEF_GET_APPROVAL_LIST_FAIL(3505,false,"CODEF API 카드 승인 내역 조회에 실패하였습니다."), + CODEF_DELETE_CONNECTEDID_FAIL(3506,false,"CODEF API 커넥티드아이디-계정 삭제 실패하였습니다."), + CODEF_CONNECTEDID_ACCOUNT_LIST_FAIL(3507,false,"CODEF API 계정 목록 조회에 실패하였습니다."), // ===================================== diff --git a/src/main/java/ewha/lux/once/global/config/AppConfig.java b/src/main/java/ewha/lux/once/global/config/AppConfig.java index ed7d85e..7cf7d95 100644 --- a/src/main/java/ewha/lux/once/global/config/AppConfig.java +++ b/src/main/java/ewha/lux/once/global/config/AppConfig.java @@ -5,6 +5,7 @@ import org.springframework.http.HttpMethod; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.web.client.RestTemplate; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -15,4 +16,8 @@ public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } } diff --git a/src/main/java/ewha/lux/once/global/config/AsyncConfig.java b/src/main/java/ewha/lux/once/global/config/AsyncConfig.java new file mode 100644 index 0000000..2d0618e --- /dev/null +++ b/src/main/java/ewha/lux/once/global/config/AsyncConfig.java @@ -0,0 +1,21 @@ +package ewha.lux.once.global.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +@EnableAsync +@Configuration +public class AsyncConfig { + @Bean + public ThreadPoolTaskExecutor threadPoolTaskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(5); + executor.setMaxPoolSize(30); + executor.setQueueCapacity(50); + executor.setThreadNamePrefix("LSH-ASYNC-"); + executor.initialize(); + return executor; + } +} \ No newline at end of file diff --git a/src/main/java/ewha/lux/once/global/config/FCMConfig.java b/src/main/java/ewha/lux/once/global/config/FCMConfig.java new file mode 100644 index 0000000..b307fcc --- /dev/null +++ b/src/main/java/ewha/lux/once/global/config/FCMConfig.java @@ -0,0 +1,42 @@ +package ewha.lux.once.global.config; + +import com.google.auth.oauth2.GoogleCredentials; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.messaging.FirebaseMessaging; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ClassPathResource; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + + +@Configuration +public class FCMConfig { + @Bean + FirebaseMessaging firebaseMessaging() throws IOException { + ClassPathResource resource = new ClassPathResource("./firebase/once-firebase-adminsdk.json"); + + InputStream refreshToken = resource.getInputStream(); + + FirebaseApp firebaseApp = null; + List firebaseAppList = FirebaseApp.getApps(); + + if (firebaseAppList != null && !firebaseAppList.isEmpty()) { + for (FirebaseApp app : firebaseAppList) { + if (app.getName().equals(FirebaseApp.DEFAULT_APP_NAME)) { + firebaseApp = app; + } + } + } else { + FirebaseOptions options = FirebaseOptions.builder() + .setCredentials(GoogleCredentials.fromStream(refreshToken)) + .build(); + firebaseApp = FirebaseApp.initializeApp(options); + } + return FirebaseMessaging.getInstance(firebaseApp); + } + +} diff --git a/src/main/java/ewha/lux/once/global/config/SecurityConfig.java b/src/main/java/ewha/lux/once/global/config/SecurityConfig.java index fd29dba..fbffa45 100644 --- a/src/main/java/ewha/lux/once/global/config/SecurityConfig.java +++ b/src/main/java/ewha/lux/once/global/config/SecurityConfig.java @@ -57,7 +57,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .requestMatchers("/user/duplicate").permitAll() .requestMatchers("/user/login").permitAll() .requestMatchers("/user/auto").permitAll() - .requestMatchers("/user/card/search").permitAll() + .requestMatchers("/user/card/**").permitAll() .anyRequest().authenticated() ) .addFilterBefore(new JwtAuthFilter(jwtProvider), UsernamePasswordAuthenticationFilter.class); diff --git a/src/main/java/ewha/lux/once/global/repository/BeaconRepository.java b/src/main/java/ewha/lux/once/global/repository/BeaconRepository.java new file mode 100644 index 0000000..d9916fc --- /dev/null +++ b/src/main/java/ewha/lux/once/global/repository/BeaconRepository.java @@ -0,0 +1,7 @@ +package ewha.lux.once.global.repository; + +import ewha.lux.once.domain.home.entity.Beacon; +import org.springframework.data.jpa.repository.JpaRepository; +public interface BeaconRepository extends JpaRepository { + Beacon findAllByProximityUUIDAndMajorAndMinor(String uuid, int major, int minor); +} \ No newline at end of file diff --git a/src/main/java/ewha/lux/once/global/repository/BenefitSummaryRepository.java b/src/main/java/ewha/lux/once/global/repository/BenefitSummaryRepository.java new file mode 100644 index 0000000..ca64ecc --- /dev/null +++ b/src/main/java/ewha/lux/once/global/repository/BenefitSummaryRepository.java @@ -0,0 +1,11 @@ +package ewha.lux.once.global.repository; + +import ewha.lux.once.domain.card.entity.BenefitSummary; +import ewha.lux.once.domain.card.entity.Card; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface BenefitSummaryRepository extends JpaRepository { + List findByCard(Card card); +} diff --git a/src/main/java/ewha/lux/once/global/repository/CardCompanyRepository.java b/src/main/java/ewha/lux/once/global/repository/CardCompanyRepository.java index bd0e3ed..b5977c8 100644 --- a/src/main/java/ewha/lux/once/global/repository/CardCompanyRepository.java +++ b/src/main/java/ewha/lux/once/global/repository/CardCompanyRepository.java @@ -3,9 +3,11 @@ import ewha.lux.once.domain.card.entity.CardCompany; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; import java.util.Optional; public interface CardCompanyRepository extends JpaRepository { Optional findByCode(String code); + List findByCodeIn(List codes); -} +} \ No newline at end of file diff --git a/src/main/java/ewha/lux/once/global/repository/CardRepository.java b/src/main/java/ewha/lux/once/global/repository/CardRepository.java index 9c3446e..dded49c 100644 --- a/src/main/java/ewha/lux/once/global/repository/CardRepository.java +++ b/src/main/java/ewha/lux/once/global/repository/CardRepository.java @@ -11,4 +11,9 @@ public interface CardRepository extends JpaRepository { List findAllByCardCompany(CardCompany cardCompany); Optional findByName(String name); List findAllByNameContains(String name); + Optional findCardByName(String name); + List findByNameContainingAndCardCompanyIn(String name, List cardCompanies); + + List findByIdBetween(long startIndex, long endIndex); } + diff --git a/src/main/java/ewha/lux/once/global/repository/ChatHistoryRepository.java b/src/main/java/ewha/lux/once/global/repository/ChatHistoryRepository.java index b2a1894..51deb76 100644 --- a/src/main/java/ewha/lux/once/global/repository/ChatHistoryRepository.java +++ b/src/main/java/ewha/lux/once/global/repository/ChatHistoryRepository.java @@ -13,4 +13,5 @@ public interface ChatHistoryRepository extends JpaRepository List findByUsers(Users users); List findByUsersAndHasPaidIsTrueAndCreatedAtBetween(Users nowUser, LocalDateTime startOfMonth, LocalDateTime endOfMonth); List findByUsersAndCreatedAtBetween(Users nowUser, LocalDateTime startDate, LocalDateTime endDate); + List findByUsersAndHasPaidTrue(Users nowUser); } diff --git a/src/main/java/ewha/lux/once/global/repository/ConnectedCardCompanyRepository.java b/src/main/java/ewha/lux/once/global/repository/ConnectedCardCompanyRepository.java new file mode 100644 index 0000000..4b38bdd --- /dev/null +++ b/src/main/java/ewha/lux/once/global/repository/ConnectedCardCompanyRepository.java @@ -0,0 +1,13 @@ +package ewha.lux.once.global.repository; + +import ewha.lux.once.domain.card.entity.ConnectedCardCompany; +import ewha.lux.once.domain.user.entity.Users; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +public interface ConnectedCardCompanyRepository extends JpaRepository { + Optional findByUsersAndCardCompany(Users users, String cardcompany); + List findAllByUsers(Users users); +} diff --git a/src/main/java/ewha/lux/once/global/repository/FCMTokenRepository.java b/src/main/java/ewha/lux/once/global/repository/FCMTokenRepository.java new file mode 100644 index 0000000..19e913b --- /dev/null +++ b/src/main/java/ewha/lux/once/global/repository/FCMTokenRepository.java @@ -0,0 +1,11 @@ +package ewha.lux.once.global.repository; + +import ewha.lux.once.domain.home.entity.FCMToken; +import ewha.lux.once.domain.user.entity.Users; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface FCMTokenRepository extends JpaRepository { + List findAllByUsers(Users users); +} diff --git a/src/main/java/ewha/lux/once/global/repository/FavoriteRepository.java b/src/main/java/ewha/lux/once/global/repository/FavoriteRepository.java new file mode 100644 index 0000000..0855944 --- /dev/null +++ b/src/main/java/ewha/lux/once/global/repository/FavoriteRepository.java @@ -0,0 +1,13 @@ +package ewha.lux.once.global.repository; + +import ewha.lux.once.domain.home.entity.Favorite; +import ewha.lux.once.domain.user.entity.Users; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +public interface FavoriteRepository extends JpaRepository { + Optional> findAllByUsers(Users users); + Boolean existsByNameAndUsers(String name, Users users); +} diff --git a/src/main/java/ewha/lux/once/global/repository/OwnedCardRepository.java b/src/main/java/ewha/lux/once/global/repository/OwnedCardRepository.java index c613ff8..8b46d00 100644 --- a/src/main/java/ewha/lux/once/global/repository/OwnedCardRepository.java +++ b/src/main/java/ewha/lux/once/global/repository/OwnedCardRepository.java @@ -6,13 +6,20 @@ import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; +import java.util.Optional; public interface OwnedCardRepository extends JpaRepository { int countAllByUsers(Users users); + OwnedCard findOwnedCardByCardAndUsers(Card card, Users users); - OwnedCard findOwnedCardByCardIdAndUsers(Long cardId, Users users); + + OwnedCard findOwnedCardByCardIdAndUsers(Long ownedCardId, Users users); List findOwnedCardByUsers(Users nowUser); OwnedCard findOwnedCardByIdAndUsers(Long ownedCardId, Users nowUser); + + List findOwnedCardByIsMain(boolean isMain); + + Optional findOwnedCardByUsersAndCard(Users users, Card card); } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 1dcd6bf..d2da036 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -26,4 +26,26 @@ spring: static: ${AWS_S3_REGION} auto: false stack: - auto: false \ No newline at end of file + auto: false + encrypt: + key: ${AES_ENCRYPTION_KEY} + +codef: + access-token: ${ACCESS_TOKEN} + client-id: ${CLIENT_ID} + seceret-key: ${SECERET_KEY} + public-key: ${PUBLIC_KEY} + +google-map: + api-key: ${GOOGLE_CLOUD_API_KEY} + +gemini: + api: + url: ${GEMINI_URL} + key: ${GEMINI_KEY} + +openai: + model: ${OPENAI_MODEL} + api: + url: https://api.openai.com/v1/chat/completions + key: ${OPENAI_KEY} \ No newline at end of file diff --git a/src/main/resources/crawling/DatabaseInsert.py b/src/main/resources/crawling/DatabaseInsert.py new file mode 100644 index 0000000..1927bbd --- /dev/null +++ b/src/main/resources/crawling/DatabaseInsert.py @@ -0,0 +1,79 @@ +import sys + +import csv +import pymysql +import datetime +import configparser + +config = configparser.ConfigParser() +config.read('./crawling/config.ini') + +db_host = config['database']['host'] +db_user = config['database']['user'] +db_password = config['database']['password'] +db_database = config['database']['database'] +db_charset = config['database']['charset'] + +# 데이터베이스 연결 설정 +connection = pymysql.connect( + host=db_host, + user=db_user, + password=db_password, + database=db_database, + charset=db_charset, + cursorclass=pymysql.cursors.DictCursor +) + +current_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') + +''' + Ex) + 1. 국민 신용카드 데이터베이스 저장 + python3 ./DatabaseInsert.py Kookmin Credit + + 2. 국민 체크카드 데이터베이스 저장 + python3 ./DatabaseInsert.py Kookmin Debit +''' +cardCompany = sys.argv[1] +if sys.argv[2] == 'Credit': + insert_type = '신용카드' + csv_file = 'credit' +elif sys.argv[2] == 'Debit': + insert_type = '체크카드' + csv_file = 'debit' + +try: + with connection.cursor() as cursor: + # CSV 파일 읽기 + with open(f'/crawling/{cardCompany}/{csv_file}_benefit.csv', 'r', encoding='utf-8') as csvfile: + csvreader = csv.reader(csvfile) + + next(csvreader) + + for row in csvreader: + card_company_id, name, img_url, benefits, created_at, type = row + + select_query = "SELECT * FROM card WHERE name = %s" + cursor.execute(select_query, (name,)) + existing_card = cursor.fetchone() + + if existing_card: # 이미 존재하는 카드인 경우 + update_query = """ + UPDATE card + SET benefits = %s, updated_at = %s + WHERE name = %s + """ + cursor.execute(update_query, (benefits, current_time, name)) + else: # 새로운 레코드 삽입 + insert_query = """ + INSERT INTO card + (card_company_id, name, img_url, benefits, created_at, type) + VALUES (%s, %s, %s, %s, %s, %s) + """ + cursor.execute(insert_query, (card_company_id, name, img_url, benefits, created_at, type)) + + connection.commit() + print(f"{current_time} [{cardCompany} {insert_type}] --- DB update 완료 ", flush=True) + +finally: + connection.close() \ No newline at end of file diff --git a/src/main/resources/crawling/Hana/credit.py b/src/main/resources/crawling/Hana/credit.py new file mode 100644 index 0000000..a01ebc4 --- /dev/null +++ b/src/main/resources/crawling/Hana/credit.py @@ -0,0 +1,260 @@ +import pandas as pd +import time +from datetime import datetime +from selenium.webdriver.common.by import By + +from selenium import webdriver +from selenium.webdriver.chrome.service import Service +from selenium.webdriver.chrome.options import Options +from webdriver_manager.chrome import ChromeDriverManager +from bs4 import BeautifulSoup + +''' + 카드 정보 조회 + hana_creditCardInfos.csv : card_name, card_url, card_img +''' +url = "https://www.hanacard.co.kr/OPI31000000D.web?schID=pcd&mID=OPI31000005P&CT_ID=241704030444153#none" + +chrome_options = Options() +chrome_options.add_argument('--headless') +chrome_options.add_argument('--no-sandbox') +chrome_options.add_argument("--disable-dev-shm-usage") + +service = Service(executable_path=r'/usr/bin/chromedriver') +driver = webdriver.Chrome(service=service,options=chrome_options) + +driver.implicitly_wait(20) +print("======= [하나] 신용 카드 정보 크롤링 =======", flush=True) +print("웹 페이지에 접속 중...", flush=True) +driver.get(url) +time.sleep(3) + +html = driver.page_source + +soup = BeautifulSoup(html, 'html.parser') + +card_names = [] +card_urls = [] +card_imgs = [] + +tabs = driver.find_elements(By.CSS_SELECTOR, "#stc_list > li") + +for tab in tabs: + tab.click() + + time.sleep(3) + tab_content = driver.page_source + soup = BeautifulSoup(tab_content, 'html.parser') + + # 각 탭에서 카드 정보 추출 ========== + main_area = soup.find('article', {'class': 'card_main_area'}) + card_ul = main_area.find('ul',{'class': 'card_slide_area'}) + card_li = card_ul.find_all('li', {'class': 'li'}) + + for i in range(len(card_li)): + # 카드 이름 + name = card_li[i].find('dl',{'class': 'txt'}).find('dt').text + card_names.append(name) + + # 카드 고유 번호 + url_btn = card_li[i].find('ul', {'class': 'btn'}).find_all('li')[1] + a_tag = url_btn.find('a', {'class': 'btn_ty04'}) + + onclick_value = a_tag.get('onclick') + url = onclick_value.split("'")[1] + card_urls.append(url) + + # 카드 이미지 + img = card_li[i].find('img')['src'] + card_imgs.append('https://www.hanacard.co.kr' + img) + +print("작업을 완료했습니다.", flush=True) +driver.quit() + +data = {"card_name" : card_names, "card_url" : card_urls, "card_img": card_imgs} +df = pd.DataFrame(data) + +df.to_csv("/crawling/Hana/hana_creditCardInfos.csv", encoding = "utf-8-sig") + +''' + 전체 카드 혜택 크롤링 + credit_benefit.csv : card_company_id, name, img_url, benefits, created_at, type +''' +card_infos = pd.read_csv('/crawling/Hana/hana_creditCardInfos.csv') + + +card_urls = card_infos['card_url'].tolist() +name = card_infos['card_name'].tolist() +img_url = card_infos['card_img'].tolist() + +card_company_id = [6] * len(card_urls) # 하나카드 +benefits = [] + +created_at = [] +type = ["CreditCard"] * len(card_urls) + +print("======= [하나] 전체 카드 혜택 정보 크롤링 =======", flush=True) +mID_urls = [str(url).zfill(5) for url in card_urls] # 다섯 자리로 맞추기 + +for i, url in enumerate(card_urls): + url = f'https://www.hanacard.co.kr/OPI41000000D.web?schID=pcd&mID=PI410{mID_urls[i]}P&CD_PD_SEQ={url}&' + + chrome_options = Options() + chrome_options.add_argument('--headless') + chrome_options.add_argument('--no-sandbox') + chrome_options.add_argument("--disable-dev-shm-usage") + + service = Service(executable_path=r'/usr/bin/chromedriver') + driver = webdriver.Chrome(service=service,options=chrome_options) + + driver.implicitly_wait(20) + now = datetime.now() + created_at.append(now) + print(f"{now} [{card_names[i]}] --- 웹 페이지에 접속 중... ({i+1}/{len(card_urls)})", flush=True) + + time.sleep(3) + driver.get(url) + time.sleep(3) + + html = driver.page_source + soup = BeautifulSoup(html, 'html.parser') + + tab_list = soup.find_all('div', {'class': 'tab_cont'}) + benefit = '' + + if len(tab_list) == 0: # 탭이 하나도 없는 경우 + card_view_detail = soup.find('div', {'class': 'card_view_detail'}) + info_list = card_view_detail.find('ul', {'class': 'card_li'}).find_all('li', {'class': 'list'}) + + for info in info_list: + tit = info.find('dt', {'class': 'tit'}) + tit = str(tit.get_text(separator=' ', strip=True)) + benefit += f'[{tit}]' + + info_txt_list = info.find('dd', {'class': 'txt'}).find_all('li', {'class': 'blt1'}) + for txt in info_txt_list: + info_txt = str(txt.get_text(separator=' ', strip=True)) + benefit += f'{info_txt} ' + + elif len(tab_list) == 1: + title = tab_list[0].find('h5', {'class': 'blind'}) + if title: + title = str(title.get_text(separator=' ', strip=True)) + benefit += f'###{title}' + + cont_list = tab_list[0].find_all('div', {'class': 'cont'}) + + for cont in cont_list: + cont_tit = cont.find('h6', {'class': 't_tit'}) + if cont_tit: + cont_tit = str(cont_tit.get_text(separator=' ', strip=True)) + benefit += f'[{cont_tit}]' + + tables = cont.select('table') + for table_num, table in enumerate(tables): + + # 표가 있을 때만 출력 + if table: + caption = table.find('caption').text + benefit += caption + rows = table.find_all('tr') + for j, row in enumerate(rows): + cells = row.find_all(['th', 'td']) + row_data = [] + + for cell in cells: + colspan = int(cell.get('colspan', 1)) + content = cell.text.strip() + row_data.extend([content] * colspan) + + row_string = ' '.join(['({})'.format(data) for data in row_data]) + + # 각 행을 설명하는 문장 출력 + sentence = f"표의 {j + 1}번째 행은 {row_string}로 이루어져 있습니다." + benefit += sentence + + cont_ul = cont.find_all('ul', recursive=False) + if cont_ul: + for ul in cont_ul: + cont_li = ul.find_all('li', recursive=False) + for li in cont_li: + # li 태그 하위에 table이 없는 경우에만 처리 + if not li.find('table'): + benefit += li.text + else: + # 주요 혜택 + title = tab_list[0].find('h5', {'class': 'blind'}).text + benefit += f'###{title}' + + info_list = tab_list[0].find('ul', {'class': 'card_info_list'}).find_all('li') + + for info in info_list: + tit = info.find('div', {'class': 'tit'}).find('p') + tit = str(tit.get_text(separator=' ', strip=True)) + benefit += f'[{tit}]' + + info_txt = info.find('div', {'class': 'inner'}).find('p') + info_txt = str(info_txt.get_text(separator=' ', strip=True)) + benefit += info_txt + + # 주요 혜택 제외 나머지 탭 + for tab in tab_list[1:]: + title = tab.find('h5', {'class': 'blind'}) + if title: + title = str(title.get_text(separator=' ', strip=True)) + benefit += f'###{title}' + + cont_list = tab.find_all('div', {'class': 'cont'}) + + for cont in cont_list: + cont_tit = cont.find('h6', {'class': 't_tit'}) + if cont_tit: + cont_tit = str(cont_tit.get_text(separator=' ', strip=True)) + benefit += f'[{cont_tit}]' + + + tables = cont.select('table') + for table_num, table in enumerate(tables): + # 표가 있을 때만 출력 + if table: + caption = table.find('caption').text + benefit += caption + rows = table.find_all('tr') + for j, row in enumerate(rows): + cells = row.find_all(['th', 'td']) + row_data = [] + + for cell in cells: + colspan = int(cell.get('colspan', 1)) + content = cell.text.strip() + row_data.extend([content] * colspan) + + row_string = ' '.join(['({})'.format(data) for data in row_data]) + + # 각 행을 설명하는 문장 출력 + sentence = f"표의 {j + 1}번째 행은 {row_string}로 이루어져 있습니다." + benefit += sentence + + cont_ul = cont.find_all('ul', recursive=False) + if cont_ul: + for ul in cont_ul: + cont_li = ul.find_all('li', recursive=False) + for li in cont_li: + # li 태그 하위에 table이 없는 경우에만 처리 + if not li.find('table'): + benefit += li.text + + benefits.append(benefit) + +print("작업을 완료했습니다.", flush=True) +driver.quit() + +''' + 전체 카드 혜택 크롤링 + credit_benefit.csv : card_company_id, name, img_url, benefits, created_at, type +''' + +data = {"card_company_id": card_company_id, "name": name, "img_url": img_url, "benefits" : benefits, "created_at": created_at, "type": type} +df = pd.DataFrame(data) + +df.to_csv("/crawling/Hana/credit_benefit.csv", encoding = "utf-8-sig", index=False) diff --git a/src/main/resources/crawling/Hana/debit.py b/src/main/resources/crawling/Hana/debit.py new file mode 100644 index 0000000..6a731f7 --- /dev/null +++ b/src/main/resources/crawling/Hana/debit.py @@ -0,0 +1,266 @@ +import pandas as pd +import time +from datetime import datetime +from selenium.webdriver.common.by import By + +from selenium import webdriver +from selenium.webdriver.chrome.service import Service +from selenium.webdriver.chrome.options import Options +from webdriver_manager.chrome import ChromeDriverManager +from bs4 import BeautifulSoup + +''' + 카드 정보 조회 + hana_debitCardInfos.csv : card_name, card_url, card_img +''' +url = "https://www.hanacard.co.kr/OPI31000000D.web?schID=pcd&mID=OPI31000005P&CT_ID=241704050328506" + +chrome_options = Options() +chrome_options.add_argument('--headless') +chrome_options.add_argument('--no-sandbox') +chrome_options.add_argument("--disable-dev-shm-usage") + +service = Service(executable_path=r'/usr/bin/chromedriver') +driver = webdriver.Chrome(service=service,options=chrome_options) + +driver.implicitly_wait(20) +print("======= [하나] 체크 카드 정보 크롤링 =======", flush=True) +print("웹 페이지에 접속 중...", flush=True) +driver.get(url) +time.sleep(3) + +html = driver.page_source + +soup = BeautifulSoup(html, 'html.parser') + +card_names = [] +card_urls = [] +card_imgs = [] + +tabs = driver.find_elements(By.CSS_SELECTOR, "#stc_list > li") + +for tab in tabs: + tab.click() + + time.sleep(3) + tab_content = driver.page_source + soup = BeautifulSoup(tab_content, 'html.parser') + + # 각 탭에서 카드 정보 추출 ========== + main_area = soup.find('article', {'class': 'card_main_area'}) + card_ul = main_area.find('ul',{'class': 'card_slide_area'}) + card_li = card_ul.find_all('li', {'class': 'li'}) + + for i in range(len(card_li)): + # 카드 이름 + name = card_li[i].find('dl',{'class': 'txt'}).find('dt').text + card_names.append(name) + + # 카드 고유 번호 + url_btn = card_li[i].find('ul', {'class': 'btn'}).find_all('li')[1] + a_tag = url_btn.find('a', {'class': 'btn_ty04'}) + + onclick_value = a_tag.get('onclick') + url = onclick_value.split("'")[1] + card_urls.append(url) + + # 카드 이미지 + img = card_li[i].find('img')['src'] + card_imgs.append('https://www.hanacard.co.kr' + img) + +print("작업을 완료했습니다.", flush=True) +driver.quit() + +data = {"card_name" : card_names, "card_url" : card_urls, "card_img": card_imgs} +df = pd.DataFrame(data) + +df.to_csv("/crawling/Hana/hana_debitCardInfos.csv", encoding = "utf-8-sig") + +''' + 전체 카드 혜택 크롤링 + debit_benefit.csv : card_company_id, name, img_url, benefits, created_at, type +''' +card_infos = pd.read_csv('/crawling/Hana/hana_debitCardInfos.csv') + + +card_urls = card_infos['card_url'].tolist() +name = card_infos['card_name'].tolist() +img_url = card_infos['card_img'].tolist() + +card_company_id = [6] * len(card_urls) # 하나카드 +benefits = [] + +created_at = [] +type = ["DebitCard"] * len(card_urls) + +print("======= [하나] 전체 카드 혜택 정보 크롤링 =======", flush=True) +mID_urls = [str(url).zfill(5) for url in card_urls] # 다섯 자리로 맞추기 + +for i, url in enumerate(card_urls): + url = f'https://www.hanacard.co.kr/OPI41000000D.web?schID=pcd&mID=PI410{mID_urls[i]}P&CD_PD_SEQ={url}&' + + chrome_options = Options() + chrome_options.add_argument('--headless') + chrome_options.add_argument('--no-sandbox') + chrome_options.add_argument("--disable-dev-shm-usage") + + service = Service(executable_path=r'/usr/bin/chromedriver') + driver = webdriver.Chrome(service=service,options=chrome_options) + + driver.implicitly_wait(20) + now = datetime.now() + created_at.append(now) + print(f"{now} [{card_names[i]}] --- 웹 페이지에 접속 중... ({i+1}/{len(card_urls)})", flush=True) + + time.sleep(3) + driver.get(url) + time.sleep(3) + + html = driver.page_source + soup = BeautifulSoup(html, 'html.parser') + + tab_list = soup.find_all('div', {'class': 'tab_cont'}) + benefit = '' + + if len(tab_list) == 0: # 탭이 하나도 없는 경우 + card_view_detail = soup.find('div', {'class': 'card_view_detail'}) + card_list = card_view_detail.find('ul', {'class': 'card_li'}) + if card_list is not None: + info_list = card_list.find_all('li', {'class': 'list'}) + for info in info_list: + tit = info.find('dt', {'class': 'tit'}) + if tit is not None: + tit_text = str(tit.get_text(separator=' ', strip=True)) + benefit += f'[{tit}]' + + info_txt = info.find('dd', {'class': 'txt'}) + if info_txt is not None: + info_txt_list = info_txt.find_all('li', {'class': 'blt1'}) + for txt in info_txt_list: + info_txt = str(txt.get_text(separator=' ', strip=True)) + benefit += f'{info_txt} ' + + elif len(tab_list) == 1: + title = tab_list[0].find('h5', {'class': 'blind'}) + if title: + title = str(title.get_text(separator=' ', strip=True)) + benefit += f'###{title}' + + cont_list = tab_list[0].find_all('div', {'class': 'cont'}) + + for cont in cont_list: + cont_tit = cont.find('h6', {'class': 't_tit'}) + if cont_tit: + cont_tit = str(cont_tit.get_text(separator=' ', strip=True)) + benefit += f'[{cont_tit}]' + + tables = cont.select('table') + for table_num, table in enumerate(tables): + + # 표가 있을 때만 출력 + if table: + caption = table.find('caption').text + benefit += caption + rows = table.find_all('tr') + for j, row in enumerate(rows): + cells = row.find_all(['th', 'td']) + row_data = [] + + for cell in cells: + colspan = int(cell.get('colspan', 1)) + content = cell.text.strip() + row_data.extend([content] * colspan) + + row_string = ' '.join(['({})'.format(data) for data in row_data]) + + # 각 행을 설명하는 문장 출력 + sentence = f"표의 {j + 1}번째 행은 {row_string}로 이루어져 있습니다." + benefit += sentence + + cont_ul = cont.find_all('ul', recursive=False) + if cont_ul: + for ul in cont_ul: + cont_li = ul.find_all('li', recursive=False) + for li in cont_li: + # li 태그 하위에 table이 없는 경우에만 처리 + if not li.find('table'): + benefit += li.text + else: + # 주요 혜택 + title = tab_list[0].find('h5', {'class': 'blind'}).text + benefit += f'###{title}' + + info_list = tab_list[0].find('ul', {'class': 'card_info_list'}).find_all('li') + + for info in info_list: + tit = info.find('div', {'class': 'tit'}).find('p') + tit = str(tit.get_text(separator=' ', strip=True)) + benefit += f'[{tit}]' + + info_txt = info.find('div', {'class': 'inner'}).find('p') + info_txt = str(info_txt.get_text(separator=' ', strip=True)) + benefit += info_txt + + # 주요 혜택 제외 나머지 탭 + for tab in tab_list[1:]: + title = tab.find('h5', {'class': 'blind'}) + if title: + title = str(title.get_text(separator=' ', strip=True)) + benefit += f'###{title}' + + cont_list = tab.find_all('div', {'class': 'cont'}) + + for cont in cont_list: + cont_tit = cont.find('h6', {'class': 't_tit'}) + if cont_tit: + cont_tit = str(cont_tit.get_text(separator=' ', strip=True)) + benefit += f'[{cont_tit}]' + + + tables = cont.select('table') + for table_num, table in enumerate(tables): + # 표가 있을 때만 출력 + if table: + caption_element = table.find('caption') + if caption_element is not None: + caption_text = caption_element.text + benefit += caption_text + rows = table.find_all('tr') + for j, row in enumerate(rows): + cells = row.find_all(['th', 'td']) + row_data = [] + + for cell in cells: + colspan = int(cell.get('colspan', 1)) + content = cell.text.strip() + row_data.extend([content] * colspan) + + row_string = ' '.join(['({})'.format(data) for data in row_data]) + + # 각 행을 설명하는 문장 출력 + sentence = f"표의 {j + 1}번째 행은 {row_string}로 이루어져 있습니다." + benefit += sentence + + cont_ul = cont.find_all('ul', recursive=False) + if cont_ul: + for ul in cont_ul: + cont_li = ul.find_all('li', recursive=False) + for li in cont_li: + # li 태그 하위에 table이 없는 경우에만 처리 + if not li.find('table'): + benefit += li.text + + benefits.append(benefit) + +print("작업을 완료했습니다.", flush=True) +driver.quit() + +''' + 전체 카드 혜택 크롤링 + debit_benefit.csv : card_company_id, name, img_url, benefits, created_at, type +''' + +data = {"card_company_id": card_company_id, "name": name, "img_url": img_url, "benefits" : benefits, "created_at": created_at, "type": type} +df = pd.DataFrame(data) + +df.to_csv("/crawling/Hana/debit_benefit.csv", encoding = "utf-8-sig", index=False) \ No newline at end of file diff --git a/src/main/resources/crawling/Hyundai/credit.py b/src/main/resources/crawling/Hyundai/credit.py new file mode 100644 index 0000000..7f12430 --- /dev/null +++ b/src/main/resources/crawling/Hyundai/credit.py @@ -0,0 +1,461 @@ +import pandas as pd +import time +from datetime import datetime + +from selenium import webdriver +from selenium.webdriver.chrome.service import Service +from selenium.webdriver.chrome.options import Options +from bs4 import BeautifulSoup + +''' + 신용 카드 리스트 조회 + hyundai_creditcardInfos.csv : card_name, card_url, card_img +''' +url = "https://www.hyundaicard.com/cpc/ma/CPCMA0101_01.hc?cardflag=ALL" + +chrome_options = Options() +chrome_options.add_argument('--headless') +chrome_options.add_argument('--no-sandbox') +chrome_options.add_argument("--disable-dev-shm-usage") + +service = Service(executable_path='/usr/bin/chromedriver') +driver = webdriver.Chrome(service=service,options=chrome_options) + +driver.implicitly_wait(20) +print("======= [현대] 신용 카드 리스트 크롤링 =======", flush=True) +print("웹 페이지에 접속 중...", flush=True) +driver.get(url) +time.sleep(3) + +html = driver.page_source + +soup = BeautifulSoup(html, 'html.parser') + +card_names = [] +card_urls = [] +card_imgs = [] + +card_section = soup.find_all('ul', class_='list05') + +for i in range(len(card_section)): + list_elements = card_section[i].find_all('div', class_='card_plt') + + for element in list_elements: + card_name_element = element.find('span', class_='h4_b_lt') + card_name = card_name_element.text.strip() + + if card_name == "기아": + url = 'https://www.hyundaicard.com/cpc/cr/CPCCR0621_11.hc?cardflag=K#aTab_1' + driver.get(url) + time.sleep(3) + html = driver.page_source + soup_deep1 = BeautifulSoup(html, 'html.parser') + ul_class = soup_deep1.find_all('ul', class_='list05') + for i in range(len(ul_class)): + li_class= ul_class[i].find_all('li') + if li_class: + for j in range(len(li_class)): + name = li_class[j].find('span', class_='h4_b_lt') + if name: + card_names.append(name.text.strip()) + card_img_element = element.find('img').get('src') + card_imgs.append(card_img_element) + card_url_code = card_img_element[-12:-6] + card_urls.append('https://www.hyundaicard.com/cpc/cr/CPCCR0201_01.hc?cardflag=K&cardWcd=' + card_url_code + '&eventCode=00000') + continue + + if card_name == "현대자동차": + url = 'https://www.hyundaicard.com/cpc/cr/CPCCR0621_11.hc?cardflag=H' + driver.get(url) + time.sleep(3) + html = driver.page_source + soup_deep1 = BeautifulSoup(html, 'html.parser') + ul_class = soup_deep1.find_all('ul', class_='list05') + for i in range(len(ul_class)): + li_class= ul_class[i].find_all('li') + if li_class: + for j in range(len(li_class)): + name = li_class[j].find('span', class_='h4_b_lt') + if name: + card_names.append(name.text.strip()) + card_img_element = element.find('img').get('src') + card_imgs.append(card_img_element) + card_url_code = card_img_element[-11:-6] + card_urls.append('https://www.hyundaicard.com/cpc/cr/CPCCR0201_01.hc?cardflag=H&cardWcd=' + card_url_code + '&eventCode=00000') + continue + + if card_name == "대한항공": + url = 'https://www.hyundaicard.com/cpc/cr/CPCCR0621_11.hc?cardflag=D' + driver.get(url) + time.sleep(3) + html = driver.page_source + soup_deep1 = BeautifulSoup(html, 'html.parser') + ul_class = soup_deep1.find_all('ul', class_='list05') + for i in range(len(ul_class)): + li_class= ul_class[i].find_all('li') + if li_class: + for j in range(len(li_class)): + name = li_class[j].find('span', class_='h4_b_lt') + if name: + card_names.append(name.text.strip()) + card_img_element = element.find('img').get('src') + card_imgs.append(card_img_element) + card_url_code = card_img_element[-11:-6] + card_urls.append('https://www.hyundaicard.com/cpc/cr/CPCCR0201_01.hc?cardflag=D&cardWcd=' + card_url_code + '&eventCode=00000') + continue + + if card_name == "American Express": + url = 'https://www.hyundaicard.com/cpc/ma/CPCMA0101_01.hc?cardflag=AX' + driver.get(url) + time.sleep(3) + html = driver.page_source + soup_deep1 = BeautifulSoup(html, 'html.parser') + ul_class = soup_deep1.find_all('ul', class_='list05') + for i in range(len(ul_class)): + li_class= ul_class[i].find_all('li') + if li_class: + for j in range(len(li_class)): + name = li_class[j].find('span', class_='h4_b_lt') + if name: + card_names.append(name.text.strip()) + card_img_element = element.find('img').get('src') + card_imgs.append(card_img_element) + card_url_code = card_img_element[-10:-6] + card_urls.append('https://www.hyundaicard.com/cpc/cr/CPCCR0201_01.hc?cardWcd=' + card_url_code) + continue + + if card_name == "이마트": + url = 'https://www.hyundaicard.com/cpc/cr/CPCCR0621_11.hc?cardflag=E' + driver.get(url) + time.sleep(3) + html = driver.page_source + soup_deep1 = BeautifulSoup(html, 'html.parser') + ul_class = soup_deep1.find_all('ul', class_='list05') + for i in range(len(ul_class)): + li_class= ul_class[i].find_all('li') + if li_class: + for j in range(len(li_class)): + name = li_class[j].find('span', class_='h4_b_lt') + if name: + card_names.append(name.text.strip()) + card_img_element = element.find('img').get('src') + card_imgs.append(card_img_element) + card_url_code = card_img_element[-11:-6] + card_urls.append('https://www.hyundaicard.com/cpc/cr/CPCCR0201_01.hc?cardflag=E&cardWcd=' + card_url_code + '&eventCode=00000') + continue + + if card_name == "지마켓(스마일카드)": + url = 'https://www.hyundaicard.com/cpc/cr/CPCCR0621_11.hc?cardflag=S' + driver.get(url) + time.sleep(3) + html = driver.page_source + soup_deep1 = BeautifulSoup(html, 'html.parser') + ul_class = soup_deep1.find_all('ul', class_='list05') + for i in range(len(ul_class)): + li_class= ul_class[i].find_all('li') + if li_class: + for j in range(len(li_class)): + name = li_class[j].find('span', class_='h4_b_lt') + if name: + card_names.append(name.text.strip()) + card_img_element = element.find('img').get('src') + card_imgs.append(card_img_element) + card_url_code = card_img_element[-11:-6] + card_urls.append('https://www.hyundaicard.com/cpc/cr/CPCCR0201_01.hc?cardflag=S&cardWcd=' + card_url_code + '&eventCode=00000') + continue + + if card_name == "코스트코": + url = 'https://www.hyundaicard.com/cpc/cr/CPCCR0621_11.hc?cardflag=T' + driver.get(url) + time.sleep(3) + html = driver.page_source + soup_deep1 = BeautifulSoup(html, 'html.parser') + ul_class = soup_deep1.find_all('ul', class_='list05') + for i in range(len(ul_class)): + li_class= ul_class[i].find_all('li') + if li_class: + for j in range(len(li_class)): + name = li_class[j].find('span', class_='h4_b_lt') + if name: + card_names.append(name.text.strip()) + card_img_element = element.find('img').get('src') + card_imgs.append(card_img_element) + card_url_code = card_img_element[-12:-6] + card_urls.append('https://www.hyundaicard.com/cpc/cr/CPCCR0201_01.hc?cardflag=T&cardWcd=' + card_url_code + '&eventCode=00000') + continue + + if card_name == "미래에셋증권": + url = 'https://www.hyundaicard.com/cpc/cr/CPCCR0621_11.hc?cardflag=MA' + driver.get(url) + time.sleep(3) + html = driver.page_source + soup_deep1 = BeautifulSoup(html, 'html.parser') + ul_class = soup_deep1.find_all('ul', class_='list05') + for i in range(len(ul_class)): + li_class= ul_class[i].find_all('li') + if li_class: + for j in range(len(li_class)): + name = li_class[j].find('span', class_='h4_b_lt') + if name: + card_names.append(name.text.strip()) + card_img_element = element.find('img').get('src') + card_imgs.append(card_img_element) + card_url_code = card_img_element[-14:-6] + card_urls.append('https://www.hyundaicard.com/cpc/cr/CPCCR0201_01.hc?cardflag=MA&cardWcd=' + card_url_code + '&eventCode=00000') + continue + + if card_name == "넥슨": + url = 'https://www.hyundaicard.com/cpc/cr/CPCCR0621_11.hc?cardflag=N' + driver.get(url) + time.sleep(3) + html = driver.page_source + soup_deep1 = BeautifulSoup(html, 'html.parser') + ul_class = soup_deep1.find_all('ul', class_='list05') + for i in range(len(ul_class)): + li_class= ul_class[i].find_all('li') + if li_class: + for j in range(len(li_class)): + name = li_class[j].find('span', class_='h4_b_lt') + if name: + card_names.append(name.text.strip()) + card_img_element = element.find('img').get('src') + card_imgs.append(card_img_element) + card_url_code = card_img_element[-11:-6] + card_urls.append('https://www.hyundaicard.com/cpc/cr/CPCCR0201_01.hc?cardflag=MA&cardWcd=' + card_url_code + '&eventCode=00000') + continue + + if card_name == "KT": + url = 'https://www.hyundaicard.com/cpc/cr/CPCCR0621_11.hc?cardflag=J&ctgrCd=050401' + driver.get(url) + time.sleep(3) + html = driver.page_source + soup_deep1 = BeautifulSoup(html, 'html.parser') + ul_class = soup_deep1.find_all('ul', class_='list05') + for i in range(len(ul_class)): + li_class= ul_class[i].find_all('li') + if li_class: + for j in range(len(li_class)): + name = li_class[j].find('span', class_='h4_b_lt') + if name: + card_names.append(name.text.strip()) + card_img_element = element.find('img').get('src') + card_imgs.append(card_img_element) + card_url_code = card_img_element[-11:-6] + card_urls.append('https://www.hyundaicard.com/cpc/cr/CPCCR0201_01.hc?cardflag=MA&cardWcd=' + card_url_code + '&eventCode=00000') + continue + + if card_name == "SKT": + url = 'https://www.hyundaicard.com/cpc/cr/CPCCR0621_11.hc?cardflag=J&ctgrCd=050403' + driver.get(url) + time.sleep(3) + html = driver.page_source + soup_deep1 = BeautifulSoup(html, 'html.parser') + ul_class = soup_deep1.find_all('ul', class_='list05') + for i in range(len(ul_class)): + li_class= ul_class[i].find_all('li') + if li_class: + for j in range(len(li_class)): + name = li_class[j].find('span', class_='h4_b_lt') + if name: + card_names.append(name.text.strip()) + card_img_element = element.find('img').get('src') + card_imgs.append(card_img_element) + card_url_code = card_img_element[-14:-6] + card_urls.append('https://www.hyundaicard.com/cpc/cr/CPCCR0201_01.hc?cardflag=MA&cardWcd=' + card_url_code + '&eventCode=00000') + continue + + if card_name == "롯데면세점": + url = 'https://www.hyundaicard.com/cpc/cr/CPCCR0621_11.hc?cardflag=J&ctgrCd=050605' + driver.get(url) + time.sleep(3) + html = driver.page_source + soup_deep1 = BeautifulSoup(html, 'html.parser') + ul_class = soup_deep1.find_all('ul', class_='list05') + for i in range(len(ul_class)): + li_class= ul_class[i].find_all('li') + if li_class: + for j in range(len(li_class)): + name = li_class[j].find('span', class_='h4_b_lt') + if name: + card_names.append(name.text.strip()) + card_img_element = element.find('img').get('src') + card_imgs.append(card_img_element) + card_url_code = card_img_element[-12:-6] + card_urls.append('https://www.hyundaicard.com/cpc/cr/CPCCR0201_01.hc?cardflag=MA&cardWcd=' + card_url_code + '&eventCode=00000') + continue + + if card_name == "SC제일은행": + url = 'https://www.hyundaicard.com/cpc/cr/CPCCR0621_11.hc?cardflag=J&ctgrCd=050703' + driver.get(url) + time.sleep(3) + html = driver.page_source + soup_deep1 = BeautifulSoup(html, 'html.parser') + ul_class = soup_deep1.find_all('ul', class_='list05') + for i in range(len(ul_class)): + li_class= ul_class[i].find_all('li') + if li_class: + for j in range(len(li_class)): + name = li_class[j].find('span', class_='h4_b_lt') + if name: + card_names.append(name.text.strip()) + card_img_element = element.find('img').get('src') + card_imgs.append(card_img_element) + card_url_code = card_img_element[-12:-6] + card_urls.append('https://www.hyundaicard.com/cpc/cr/CPCCR0201_01.hc?cardflag=MA&cardWcd=' + card_url_code + '&eventCode=00000') + continue + + if card_name == "경차": + url = 'https://www.hyundaicard.com/cpc/cr/CPCCR0621_11.hc?cardflag=J&ctgrCd=050802' + driver.get(url) + time.sleep(3) + html = driver.page_source + soup_deep1 = BeautifulSoup(html, 'html.parser') + ul_class = soup_deep1.find_all('ul', class_='list05') + for i in range(len(ul_class)): + li_class= ul_class[i].find_all('li') + if li_class: + for j in range(len(li_class)): + name = li_class[j].find('span', class_='h4_b_lt') + if name: + card_names.append(name.text.strip()) + card_img_element = element.find('img').get('src') + card_imgs.append(card_img_element) + card_url_code = card_img_element[-11:-6] + card_urls.append('https://www.hyundaicard.com/cpc/cr/CPCCR0201_01.hc?cardflag=MA&cardWcd=' + card_url_code + '&eventCode=00000') + continue + + if card_name == "화물차": + url = 'https://www.hyundaicard.com/cpc/cr/CPCCR0621_11.hc?cardflag=J&ctgrCd=050803' + driver.get(url) + time.sleep(3) + html = driver.page_source + soup_deep1 = BeautifulSoup(html, 'html.parser') + ul_class = soup_deep1.find_all('ul', class_='list05') + for i in range(len(ul_class)): + li_class= ul_class[i].find_all('li') + if li_class: + for j in range(len(li_class)): + name = li_class[j].find('span', class_='h4_b_lt') + if name: + card_names.append(name.text.strip()) + card_img_element = element.find('img').get('src') + card_imgs.append(card_img_element) + card_url_code = card_img_element[-10:-6] + card_urls.append('https://www.hyundaicard.com/cpc/cr/CPCCR0201_01.hc?cardflag=MA&cardWcd=' + card_url_code + '&eventCode=00000') + continue + + if card_name in ["쏘카", "제네시스", "야놀자ㆍ인터파크(NOL 카드)", "무신사", "SSG.COM", "네이버", "배달의민족", "스타벅스", "GS칼텍스", "LG U+", "기타", "현대홈쇼핑", "예스24", "하이마트", "The CJ", "coway-현대카드M Edition3", "LG전자-현대카드M Edition3", "햇살론", "인플카 현대카드"]: + card_names.append(card_name_element.text.strip()) + + card_img_element = element.find('img').get('src') + card_imgs.append(card_img_element) + + card_url_a = element.find('a') + card_url = card_url_a.get('href') + card_urls.append('https://www.hyundaicard.com' + card_url) + continue + + + if card_name == "체크카드" or card_name == "Gift카드" or card_name == "후불하이패스카드": + continue + + card_names.append(card_name_element.text.strip()) + + card_img_element = element.find('img').get('src') + card_imgs.append(card_img_element) + + card_code_a = element.find('a') + card_code = card_code_a.get('onclick') + if card_code: + start_index = card_code.find("'") + 1 + end_index = card_code.find("'", start_index) + card_url_code = card_code[start_index:end_index] + + if card_name in ["MY BUSINESS M Food&Drink", "MY BUSINESS M Retail&Service", "MY BUSINESS M Online Seller", "MY BUSINESS X Food&Drink", "MY BUSINESS X Retail&Service", "MY BUSINESS X Online Seller", "MY BUSINESS ZERO Food&Drink", "MY BUSINESS ZERO Retail&Service", "MY BUSINESS ZERO Online Seller"]: + card_code2 = card_code_a.get('href') + start_index = card_code2.find("'") + 1 + end_index = card_code2.find("'", start_index) + card_url_code = card_code2[start_index:end_index] + + card_urls.append('https://www.hyundaicard.com/cpc/cr/CPCCR0201_01.hc?cardWcd=' + card_url_code) + +print("작업을 완료했습니다.", flush=True) +driver.quit() + +data = {"card_name" : card_names, "card_url" : card_urls, "card_img": card_imgs} +df = pd.DataFrame(data) + +df.to_csv("/crawling/Hyundai/hyundai_creditcardInfos.csv", encoding = "utf-8-sig") + + +''' + 신용 카드 혜택 크롤링 + credit_benefit.csv : card_company_id, name, img_url, benefits, created_at, type +''' +card_infos = pd.read_csv('/crawling/Hyundai/hyundai_creditcardInfos.csv') + +card_urls = card_infos['card_url'].tolist() +name = card_infos['card_name'].tolist() +img_url = card_infos['card_img'].tolist() + +card_company_id = [2] * len(card_urls) +benefits = [] + +created_at = [] +type = ["CreditCard"] * len(card_urls) + +print("======= [현대] 전체 카드 혜택 정보 크롤링 =======", flush=True) +for i in range(len(card_urls)): + + chrome_options = Options() + chrome_options.add_argument('--headless') + chrome_options.add_argument('--no-sandbox') + chrome_options.add_argument("--disable-dev-shm-usage") + + service = Service(executable_path=r'/usr/bin/chromedriver') + driver = webdriver.Chrome(service=service,options=chrome_options) + + driver.implicitly_wait(20) + now = datetime.now() + created_at.append(now) + print(f"{now} [{card_names[i]}] --- 웹 페이지에 접속 중... ({i+1}/{len(card_urls)})", flush=True) + + time.sleep(3) + driver.get(card_urls[i]) + time.sleep(3) + + html = driver.page_source + soup = BeautifulSoup(html, 'html.parser') + + # 상세 혜택 ============================================= + popup_containers = soup.find_all('div', class_='modal_pop') + + benefit_title = [] + benefit = '' + + for i in range(len(popup_containers)): + title = popup_containers[i].find('div', class_='layer_head') + if title and (title.text.strip()[-2:] == "적립" or title.text.strip()[-2:] == "사용" + or title.text.strip()[-2:] == "할인" or title.text.strip()[-5:] == "업그레이드" + or title.text.strip()[-3:] == "보너스" or title.text.strip()[-3:] == "서비스" + or title.text.strip()[-3:] == "트래블" or title.text.strip()[-3:] == "리워드" + or title.text.strip()[-2:] == "절감" or title.text.strip()[-2:] == "지원"): + benefit += f'###{title.text.strip()}' + item_list = popup_containers[i].find('div', class_='layer_body') + benefit += item_list.text.strip() + + benefits.append(benefit) + +print("작업을 완료했습니다.", flush=True) +driver.quit() + +''' + 신용 카드 혜택 크롤링 + credit_benefit.csv : card_company_id, name, img_url, benefits, created_at, type +''' + +data = {"card_company_id": card_company_id, "name": name, "img_url": img_url, "benefits" : benefits, "created_at": created_at, "type": type} +df = pd.DataFrame(data) + +df.to_csv("/crawling/Hyundai/credit_benefit.csv", encoding = "utf-8-sig", index=False) + diff --git a/src/main/resources/crawling/Hyundai/debit.py b/src/main/resources/crawling/Hyundai/debit.py new file mode 100644 index 0000000..74df840 --- /dev/null +++ b/src/main/resources/crawling/Hyundai/debit.py @@ -0,0 +1,127 @@ +import pandas as pd +import time +from datetime import datetime + +from selenium import webdriver +from selenium.webdriver.chrome.service import Service +from selenium.webdriver.chrome.options import Options +from webdriver_manager.chrome import ChromeDriverManager +from bs4 import BeautifulSoup + +''' + 체크 카드 리스트 조회 + hyundai_checkcardInfos.csv : card_name, card_url, card_img +''' +url = "https://www.hyundaicard.com/cpc/cr/CPCCR0621_11.hc?cardflag=C#aTab_2" + +chrome_options = Options() +chrome_options.add_argument('--headless') +chrome_options.add_argument('--no-sandbox') +chrome_options.add_argument("--disable-dev-shm-usage") + +driver = webdriver.Chrome(executable_path="/usr/bin/chromedriver",chrome_options=chrome_options) + +driver.implicitly_wait(20) +print("======= [현대] 신용 카드 리스트 크롤링 =======", flush=True) +print("웹 페이지에 접속 중...", flush=True) +driver.get(url) +time.sleep(3) + +html = driver.page_source + +soup = BeautifulSoup(html, 'html.parser') + +card_names = [] +card_urls = [] +card_imgs = [] + +ul_class = soup.find_all('ul', class_='list05') +for i in range(len(ul_class)): + li_class= ul_class[i].find_all('li') + if li_class: + for j in range(len(li_class)): + name = li_class[j].find('span', class_='h4_b_lt') + if name: + card_names.append(name.text.strip()) + card_img_element = li_class[j].find('img').get('src') + card_imgs.append(card_img_element) + card_url_code = card_img_element[-9:-6] + card_urls.append('https://www.hyundaicard.com/cpc/cr/CPCCR0201_01.hc?cardflag=C&cardWcd=' + card_url_code + '&eventCode=00000') + +print("작업을 완료했습니다.", flush=True) +driver.quit() + +data = {"card_name" : card_names, "card_url" : card_urls, "card_img": card_imgs} +df = pd.DataFrame(data) + +df.to_csv("/crawling/Hyundai/hyundai_checkcardInfos.csv", encoding = "utf-8-sig") + +''' + 신용 카드 혜택 크롤링 + debit_benefit.csv : card_company_id, name, img_url, benefits, created_at, type +''' +card_infos = pd.read_csv('/crawling/Hyundai/hyundai_checkcardInfos.csv') + +card_urls = card_infos['card_url'].tolist() +name = card_infos['card_name'].tolist() +img_url = card_infos['card_img'].tolist() + +card_company_id = [2] * len(card_urls) +benefits = [] + +created_at = [] +type = ["DebitCard"] * len(card_urls) + +print("======= [현대] 전체 카드 혜택 정보 크롤링 =======", flush=True) +for i in range(len(card_urls)): + + chrome_options = Options() + chrome_options.add_argument('--headless') + chrome_options.add_argument('--no-sandbox') + chrome_options.add_argument("--disable-dev-shm-usage") + + driver = webdriver.Chrome(executable_path="/usr/bin/chromedriver",chrome_options=chrome_options) + + driver.implicitly_wait(20) + now = datetime.now() + created_at.append(now) + print(f"{now} [{card_names[i]}] --- 웹 페이지에 접속 중... ({i+1}/{len(card_urls)})", flush=True) + + time.sleep(3) + driver.get(card_urls[i]) + time.sleep(3) + + html = driver.page_source + soup = BeautifulSoup(html, 'html.parser') + + # 상세 혜택 ============================================= + popup_containers = soup.find_all('div', class_='modal_pop') + + benefit_title = [] + benefit = '' + + for i in range(len(popup_containers)): + title = popup_containers[i].find('div', class_='layer_head') + if title and (title.text.strip()[-2:] == "적립" or title.text.strip()[-2:] == "사용" + or title.text.strip()[-2:] == "할인" or title.text.strip()[-5:] == "업그레이드" + or title.text.strip()[-3:] == "보너스" or title.text.strip()[-3:] == "서비스" + or title.text.strip()[-3:] == "트래블" or title.text.strip()[-3:] == "리워드"): + benefit += f'###{title.text.strip()}' + item_list = popup_containers[i].find('div', class_='layer_body') + benefit += item_list.text.strip() + + benefits.append(benefit) + +print("작업을 완료했습니다.", flush=True) +driver.quit() + +''' + 체크 카드 혜택 크롤링 + debit_benefit.csv : card_company_id, name, img_url, benefits, created_at, type +''' + +data = {"card_company_id": card_company_id, "name": name, "img_url": img_url, "benefits" : benefits, "created_at": created_at, "type": type} +df = pd.DataFrame(data) + +df.to_csv("/crawling/Hyundai/debit_benefit.csv", encoding = "utf-8-sig", index=False) + diff --git a/src/main/resources/crawling/Kookmin/credit.py b/src/main/resources/crawling/Kookmin/credit.py new file mode 100644 index 0000000..7428cbb --- /dev/null +++ b/src/main/resources/crawling/Kookmin/credit.py @@ -0,0 +1,166 @@ +import pandas as pd +from urllib.request import urlopen +from bs4 import BeautifulSoup +import re +from datetime import datetime + +name = [] +img_url = [] +benefits = [] +created_at = [] + +def findtableortext(str): # 테이블 추출 + try: + thead = str.find('thead').findAll('th') + sentence = " 표의 칼럼은 " + + for columnname in thead: + columnname=columnname.text + sentence+=columnname.replace('\n','')+", " + sentence=sentence[0:-2] + sentence += " 로 이루어져 있습니다." + j=1 + tbodies = str.find('tbody').findAll('tr') + + for tbody in tbodies: + + tds = tbody.findAll('td') + sentence += f"표의 {j}번째 행은 " + + for td in tds: + sentence+=td.text.replace(",","").replace('\n','')+", " + sentence=sentence[0:-2] + sentence += " 로 이루어져 있습니다." + j+=1 + + return sentence + except: + sentence=str.text.replace('\n', '') + return sentence + +def findBenefitminititle(str): # 소제목 찾기 + sentence="" + benetitle="" + benecontent="" + benetitle=str.find('div',{'class','tit'}) + for child in str.find_all(recursive=False): + if benetitle is None:continue + title=str.find('div',{'class','tit'}).text + text=child.text + if text == title: continue + benecontent+=text + if benetitle is not None: sentence+="["+benetitle.text.replace('\n', '')+"] " + if benecontent is not None: sentence+=benecontent.replace('\n', '') + + return sentence + +def findBenefit(detail): # 카드 혜택 크롤링 + benefitsentence="" + content=detail.h2.text[7:] + if content!="": + if content.replace(' ','')=="서비스요약" or content.replace(' ','')=="서비스한눈에보기": return "" + benefitsentence+="\n<"+content+"> " + for child in detail.find_all(recursive=False): + if str(child)[1:3]=="h2": + continue + if str(child)[1:6]=="style": + continue + if str(child)[:17] == '
" + for child in detail.find_all(recursive=False): + if str(child)[1:3]=="h2": + continue + if str(child)[1:6]=="style": + continue + if str(child)[:17] == '
" + details = beneList.find('div',{'class','toggleCont'}).findAll(recursive=False) + for detail in details: + try: + thead = detail.find('thead').findAll('th') + sentence = "표의 칼럼은 " + + for columnname in thead: + columnname=columnname.text + sentence+=columnname.replace('\n','')+", " + sentence=sentence[0:-2] + sentence += " 로 이루어져 있습니다." + benefits+=sentence + j=1 + tbodies = detail.find('tbody').findAll('tr') + + for tbody in tbodies: + tds = tbody.findAll('td') + sentence += f"표의 {j}번째 행은 " + for td in tds: + sentence+=td.text.replace(",","").replace('\n','')+", " + sentence=sentence[0:-2] + sentence += " 로 이루어져 있습니다." + j+=1 + benefits+=sentence + + except: + if str(detail)[1:3]=="h3": + benefits+="["+detail.text+"]" + elif str(detail)[1:3]=="h4": + benefits+="/"+detail.text+": " + elif str(detail)[1:6]=="style": + continue + else: + benefits+=detail.text.replace("\n","").replace('\r','').replace('\t','') + benefits.replace('\n','').replace('\t','') + benefits+="\n" + else: + for bene in benes: + titlebene=bene.find('h3').text + if titlebene=="L.POINT" or titlebene=="가족카드" or titlebene== "가족카드 안내" or titlebene== "연회비" or titlebene== "혜택 모아보기": continue + benefits+="<"+titlebene+">" + sections = bene.findAll('div',{'class','toggle'}) + for section in sections: + beneNames=section.find('h4') + beneNameText=beneNames.text + benefits+="["+beneNameText+"] " + details = section.find('div',{'class','toggleCont'}).findAll(recursive=False) + + for detail in details: + try: + thead = detail.find('thead').findAll('th') + sentence = "표의 칼럼은 " + for columnname in thead: + columnname=columnname.text + sentence+=columnname.replace(",","").replace('\n','').replace('\t','').replace('\r','')+", " + sentence=sentence[0:-2] + sentence += " 로 이루어져 있습니다." + benefits+=sentence + j=1 + tbodies = detail.find('tbody').findAll('tr') + for tbody in tbodies: + tds = tbody.findAll('td') + sentence += f"표의 {j}번째 행은 " + for td in tds: + sentence+=td.text.replace(",","").replace('\n','').replace('\t','').replace('\r','')+", " + sentence=sentence[0:-2] + sentence += " 로 이루어져 있습니다." + j+=1 + benefits+=sentence + + except: + if str(detail)[1:3]=="h5": + benefits+="/"+detail.text.replace("\n","").replace('\r','').replace('\t','')+": " + elif str(detail)[1:7]=="script": + continue + else: + benefits+=detail.text.replace("\n","").replace('\r','').replace('\t','') + benefits+="\n" + benefits=benefits.replace("'","") + + return benefits + +def cardList(associate): + url = 'https://www.lottecard.co.kr/app/LPCDADA_V100.lc' + + chrome_options = Options() + chrome_options.add_argument('--headless') + chrome_options.add_argument('--no-sandbox') + chrome_options.add_argument("--disable-dev-shm-usage") + + service = Service(executable_path='/usr/bin/chromedriver') + driver = webdriver.Chrome(service=service,options=chrome_options) + + # 웹 페이지 로드 + driver.get(url) + + if(associate): + # 제휴 카드 클릭 + link = driver.find_element(By.LINK_TEXT,"제휴") + link.click() + time.sleep(5) + + # Selenium으로 페이지 스크랩 + rendered_html = driver.page_source + + # BeautifulSoup을 사용하여 HTML 파싱 + soup = BeautifulSoup(rendered_html, 'html.parser') + + # 더보기 버튼 끝까지 누르기 + while True: + new_render_html=driver.page_source + soup = BeautifulSoup(new_render_html, 'html.parser') + if soup.find('button',{'id':'btnMore'}) is None: break + driver.find_element("id","btnMore").click() + time.sleep(3) + + return soup.find('ul', {'id':'ajaxCardList'}).findAll('li') + + +s3 = s3_connection() +name = [] +img_url = [] +benefits = [] +created_at = [] + + +for card in cardList(False): + cardNo = card.find('a').get('onclick') + cardNo = re.search(r"'(.*?)'", cardNo).group(1) + + cardurl='https://www.lottecard.co.kr/app/LPCDADB_V100.lc?vtCdKndC='+cardNo + + cardImg= "https:" + card.find('img').get('src') + img_url.append(s3_put_object(cardImg,cardNo)) + + cardName=card.find('b').text + name.append(cardName) + + print(datetime.now().strftime("%Y-%m-%d %H:%M:%S")+" ["+cardName+"] --- 웹 페이지에 접속 중... ", flush=True) + + benefit = cardCrawling(cardurl) + benefits.append(benefit) + + now_datetime = datetime.now() + formatted_now = now_datetime.strftime("%Y-%m-%d %H:%M:%S.%f") + created_at.append(formatted_now) + +for card in cardList(True): + cardNo = card.find('a').get('onclick') + cardNo = re.search(r"'(.*?)'", cardNo).group(1) + + cardurl='https://www.lottecard.co.kr/app/LPCDADB_V100.lc?vtCdKndC='+cardNo + + cardImg= "https:" + card.find('img').get('src') + img_url.append(s3_put_object(cardImg,cardNo)) + + cardName=card.find('b').text + name.append(cardName) + + print(datetime.now().strftime("%Y-%m-%d %H:%M:%S")+" ["+cardName+"](제휴) --- 웹 페이지에 접속 중... ", flush=True) + + benefit = cardCrawling(cardurl) + benefits.append(benefit) + + now_datetime = datetime.now() + formatted_now = now_datetime.strftime("%Y-%m-%d %H:%M:%S.%f") + created_at.append(formatted_now) + + +card_company_id = [5] * len(name) +type = ["CreditCard"] * len(name) + +data = {"card_company_id":card_company_id, "name" : name, "img_url" : img_url, "benefits": benefits, "created_at": created_at,"type":type} +df = pd.DataFrame(data) + +df.to_csv("/crawling/Lotte/credit_benefit.csv", encoding = "utf-8-sig", index=False) +print(datetime.now().strftime("%Y-%m-%d %H:%M:%S")+" 롯데카드 신용카드 크롤링 완료", flush=True) \ No newline at end of file diff --git a/src/main/resources/crawling/Lotte/debit.py b/src/main/resources/crawling/Lotte/debit.py new file mode 100644 index 0000000..324978c --- /dev/null +++ b/src/main/resources/crawling/Lotte/debit.py @@ -0,0 +1,253 @@ + +from urllib.request import urlopen +from bs4 import BeautifulSoup +from selenium import webdriver +from selenium.webdriver.chrome.options import Options +from selenium.webdriver.chrome.service import Service +import time +import re +import pandas as pd +from datetime import datetime +from io import BytesIO +from PIL import Image +import boto3 +from selenium.webdriver.common.by import By +import configparser + +config = configparser.ConfigParser() +config.read('/crawling/config.ini') + +AWS_S3_ACCESSKEY = config['s3']['AWS_S3_ACCESSKEY'] +AWS_S3_SECRETKEY = config['s3']['AWS_S3_SECRETKEY'] +AWS_S3_BUCKET = config['s3']['AWS_S3_BUCKET'] +AWS_S3_REGION = config['s3']['AWS_S3_REGION'] + + +# S3연결 +def s3_connection(): + try: + s3 = boto3.client( + service_name="s3", + region_name="ap-northeast-2", + aws_access_key_id=AWS_S3_ACCESSKEY, + aws_secret_access_key=AWS_S3_SECRETKEY, + ) + except Exception as e: + print(e, flush=True) + else: + print("s3 연결 성공", flush=True) + return s3 + +# 이미지 저장, 주소반환 +def s3_put_object(cardImg,cardNo): + try: + data = urlopen(cardImg).read() + img = Image.open(BytesIO(data)) + w, h = img.size + if w < h : # 이미지가 세로인 경우 + img = img.rotate(90, expand=True) + + image_fileobj = BytesIO() + img.save(image_fileobj, format='PNG') + image_fileobj.seek(0) + + # S3에 업로드 + s3.upload_fileobj(image_fileobj, AWS_S3_BUCKET, "lottecard/"+cardNo+".png",ExtraArgs={"ContentType": "image/jpg", "ACL": "public-read"}) + return "https://{AWS_S3_BUCKET}.s3."+AWS_S3_REGION+".amazonaws.com/lottecard/"+cardNo+".png" + else: + return cardImg + except Exception as e: + return False + +# 페이지 크롤링 함수 +def cardCrawling (cardurl): + benefits="" + + cardhtml=urlopen(cardurl) + cardbs=BeautifulSoup(cardhtml,'html.parser') + + # benefits + benes = cardbs.findAll('div',{'class','bnfCont'}) + if len(benes) == 0: + beneLists = cardbs.find('ul',{'class','toggleList'}).findAll('li',recursive=False) + for beneList in beneLists: + titlebene = beneList.find('a').text.replace('\t','').replace('\r','').replace('\n','').replace(' ','') + if titlebene=="L.POINT" or titlebene=="가족카드" or titlebene== "가족카드안내" or titlebene== "연회비": continue + benefits+="<"+titlebene+"> " + details = beneList.find('div',{'class','toggleCont'}).findAll(recursive=False) + for detail in details: + try: + thead = detail.find('thead').findAll('th') + sentence = "표의 칼럼은 " + + for columnname in thead: + columnname=columnname.text + sentence+=columnname.replace('\n','')+", " + sentence=sentence[0:-2] + sentence += " 로 이루어져 있습니다." + benefits+=sentence + j=1 + tbodies = detail.find('tbody').findAll('tr') + + for tbody in tbodies: + tds = tbody.findAll('td') + sentence += f"표의 {j}번째 행은 " + for td in tds: + sentence+=td.text.replace(",","").replace('\n','')+", " + sentence=sentence[0:-2] + sentence += " 로 이루어져 있습니다." + j+=1 + benefits+=sentence + + except: + if str(detail)[1:3]=="h3": + benefits+="["+detail.text+"]" + elif str(detail)[1:3]=="h4": + benefits+="/"+detail.text+": " + elif str(detail)[1:6]=="style": + continue + else: + benefits+=detail.text.replace("\n","").replace('\r','').replace('\t','') + benefits.replace('\n','').replace('\t','') + benefits+="\n" + else: + for bene in benes: + titlebene=bene.find('h3').text + if titlebene=="L.POINT" or titlebene=="가족카드" or titlebene== "가족카드 안내" or titlebene== "연회비" or titlebene== "혜택 모아보기": continue + benefits+="<"+titlebene+">" + sections = bene.findAll('div',{'class','toggle'}) + for section in sections: + beneNames=section.find('h4') + beneNameText=beneNames.text + benefits+="["+beneNameText+"] " + details = section.find('div',{'class','toggleCont'}).findAll(recursive=False) + + for detail in details: + try: + thead = detail.find('thead').findAll('th') + sentence = "표의 칼럼은 " + for columnname in thead: + columnname=columnname.text + sentence+=columnname.replace(",","").replace('\n','').replace('\t','').replace('\r','')+", " + sentence=sentence[0:-2] + sentence += " 로 이루어져 있습니다." + benefits+=sentence + j=1 + tbodies = detail.find('tbody').findAll('tr') + for tbody in tbodies: + tds = tbody.findAll('td') + sentence += f"표의 {j}번째 행은 " + for td in tds: + sentence+=td.text.replace(",","").replace('\n','').replace('\t','').replace('\r','')+", " + sentence=sentence[0:-2] + sentence += " 로 이루어져 있습니다." + j+=1 + benefits+=sentence + + except: + if str(detail)[1:3]=="h5": + benefits+="/"+detail.text.replace("\n","").replace('\r','').replace('\t','')+": " + elif str(detail)[1:7]=="script": + continue + else: + benefits+=detail.text.replace("\n","").replace('\r','').replace('\t','') + benefits+="\n" + benefits=benefits.replace("'","") + + return benefits + +def cardList(associate): + url = 'https://www.lottecard.co.kr/app/LPCDAEA_V100.lc' + + chrome_options = Options() + chrome_options.add_argument('--headless') + chrome_options.add_argument('--no-sandbox') + chrome_options.add_argument("--disable-dev-shm-usage") + + service = Service(executable_path='/usr/bin/chromedriver') + driver = webdriver.Chrome(service=service,options=chrome_options) + + # 웹 페이지 로드 + driver.get(url) + + if(associate): + # 제휴 카드 클릭 + link = driver.find_element(By.LINK_TEXT,"제휴") + link.click() + time.sleep(5) + + # Selenium으로 페이지 스크랩 + rendered_html = driver.page_source + + # BeautifulSoup을 사용하여 HTML 파싱 + soup = BeautifulSoup(rendered_html, 'html.parser') + + # 더보기 버튼 끝까지 누르기 + while True: + new_render_html=driver.page_source + soup = BeautifulSoup(new_render_html, 'html.parser') + if soup.find('button',{'id':'btnMore'}) is None: break + driver.find_element("id","btnMore").click() + time.sleep(3) + + return soup.find('ul', {'id':'ajaxCardList'}).findAll('li') + + +s3 = s3_connection() +name = [] +img_url = [] +benefits = [] +created_at = [] + + +for card in cardList(False): + cardNo = card.find('a').get('onclick') + cardNo = re.search(r"'(.*?)'", cardNo).group(1) + + cardurl='https://www.lottecard.co.kr/app/LPCDADB_V100.lc?vtCdKndC='+cardNo + + cardImg= "https:" + card.find('img').get('src') + img_url.append(s3_put_object(cardImg,cardNo)) + + cardName=card.find('b').text + name.append(cardName) + + print(datetime.now().strftime("%Y-%m-%d %H:%M:%S")+" ["+cardName+"] --- 웹 페이지에 접속 중... ", flush=True) + + benefit = cardCrawling(cardurl) + benefits.append(benefit) + + now_datetime = datetime.now() + formatted_now = now_datetime.strftime("%Y-%m-%d %H:%M:%S.%f") + created_at.append(formatted_now) + +for card in cardList(True): + cardNo = card.find('a').get('onclick') + cardNo = re.search(r"'(.*?)'", cardNo).group(1) + + cardurl='https://www.lottecard.co.kr/app/LPCDADB_V100.lc?vtCdKndC='+cardNo + + cardImg= "https:" + card.find('img').get('src') + img_url.append(s3_put_object(cardImg,cardNo)) + + cardName=card.find('b').text + name.append(cardName) + + print(datetime.now().strftime("%Y-%m-%d %H:%M:%S")+" ["+cardName+"](제휴) --- 웹 페이지에 접속 중... ", flush=True) + + benefit = cardCrawling(cardurl) + benefits.append(benefit) + + now_datetime = datetime.now() + formatted_now = now_datetime.strftime("%Y-%m-%d %H:%M:%S.%f") + created_at.append(formatted_now) + + +card_company_id = [5] * len(name) +type = ["DebitCard"] * len(name) + +data = {"card_company_id":card_company_id, "name" : name, "img_url" : img_url, "benefits": benefits, "created_at": created_at,"type":type} +df = pd.DataFrame(data) + +df.to_csv("/crawling/Lotte/debit_benefit.csv", encoding = "utf-8-sig", index=False) +print(datetime.now().strftime("%Y-%m-%d %H:%M:%S")+" 롯데카드 체크카드 크롤링 완료", flush=True) \ No newline at end of file diff --git a/src/main/resources/crawling/Samsung/credit.py b/src/main/resources/crawling/Samsung/credit.py new file mode 100644 index 0000000..d29f329 --- /dev/null +++ b/src/main/resources/crawling/Samsung/credit.py @@ -0,0 +1,173 @@ +import pandas as pd +import time +from datetime import datetime + +from selenium import webdriver +from selenium.webdriver.chrome.service import Service +from selenium.webdriver.chrome.options import Options +from webdriver_manager.chrome import ChromeDriverManager +from bs4 import BeautifulSoup + +''' + 신용 카드 리스트 조회 + samsung_creditcardInfos.csv : card_name, card_url, card_img +''' +url = "https://www.samsungcard.com/home/card/cardinfo/PGHPPDCCardCardinfoRecommendPC001" + +chrome_options = Options() +chrome_options.add_argument('--headless') +chrome_options.add_argument('--no-sandbox') +chrome_options.add_argument("--disable-dev-shm-usage") + +service = Service(executable_path=r'/usr/bin/chromedriver') +driver = webdriver.Chrome(service=service,options=chrome_options) + +driver = webdriver.Chrome(service=webdriver.ChromeService(ChromeDriverManager().install()), options=chrome_options) + +driver.implicitly_wait(20) +print("======= [삼성] 신용 카드 리스트 크롤링 =======", flush=True) +print("웹 페이지에 접속 중...", flush=True) +driver.get(url) +time.sleep(3) + +html = driver.page_source + +soup = BeautifulSoup(html, 'html.parser') + +card_names = [] +card_urls = [] +card_imgs = [] + +card_tab_section = soup.find_all('div', class_='tab-section') + +for i in range(2, len(card_tab_section)): + ul_tag = card_tab_section[i].find('ul', class_='lists') + list_elements = ul_tag.find_all('li') + + for idx, element in enumerate(list_elements, 1): + card_name_element = element.find('div', class_='tit-h4') + card_names.append(card_name_element.text.strip()) + + card_img_element = element.find('img').get('src') + card_imgs.append(card_img_element) + + card_url_code = card_img_element[-11:-4] + card_urls.append('https://www.samsungcard.com/home/card/cardinfo/PGHPPCCCardCardinfoDetails001?code=' + card_url_code) + +print("작업을 완료했습니다.", flush=True) +driver.quit() + +# 중복 제거 +unique_card_urls = list(set(card_urls)) + +data = {"card_name": [], "card_url": [], "card_img": []} + +# 중복 제거된 card_url에 해당하는 정보만 데이터에 추가 +for url in unique_card_urls: + index = card_urls.index(url) + data["card_name"].append(card_names[index]) + data["card_url"].append(card_urls[index]) + data["card_img"].append(card_imgs[index]) + +df = pd.DataFrame(data) + +df.to_csv("/crawling/Samsung/samsung_creditcardInfos.csv", encoding = "utf-8-sig") + +''' + 신용 카드 혜택 크롤링 + credit_benefit.csv : card_company_id, name, img_url, benefits, created_at, type +''' +card_infos = pd.read_csv('/crawling/Samsung/samsung_creditcardInfos.csv') + +card_urls = card_infos['card_url'].tolist() +name = card_infos['card_name'].tolist() +img_url = card_infos['card_img'].tolist() + +card_company_id = [3] * len(card_urls) +benefits = [] + +created_at = [] +type = ["CreditCard"] * len(card_urls) + +print("======= [삼성] 전체 카드 혜택 정보 크롤링 =======", flush=True) +for i in range(len(card_urls)): + + chrome_options = Options() + chrome_options.add_argument('--headless') + chrome_options.add_argument('--no-sandbox') + chrome_options.add_argument("--disable-dev-shm-usage") + + service = Service(executable_path=r'/usr/bin/chromedriver') + driver = webdriver.Chrome(service=service,options=chrome_options) + + driver.implicitly_wait(20) + now = datetime.now() + created_at.append(now) + print(f"{now} [{card_names[i]}] --- 웹 페이지에 접속 중... ({i+1}/{len(card_urls)})", flush=True) + + time.sleep(3) + driver.get(card_urls[i]) + time.sleep(3) + + html = driver.page_source + soup = BeautifulSoup(html, 'html.parser') + + # 상세 혜택 ============================================= + tab_container = soup.find('section', class_='tab-container') + tab_list = tab_container.find_all('div', role='tabpanel') + + benefit_title = [] + benefit = '' + + for i in range(len(tab_list)): + tab_name = tab_list[i].find('div', class_='dot-title').text.strip() + if tab_name not in ["요약", "카드이용TIP", "카드 디자인 소개", "네이버 디지털콘텐츠 적립 혜택 적용방법", "신세계포인트 적립 서비스", "세무지원 서비스", "BIZ SERVICE", "SPECIAL PLATE", "신세계백화점 제휴 서비스"]: + benefit_title.append(tab_name) + + for i in range(len(tab_list)): + tab_name = tab_list[i].find('div', class_='dot-title').text.strip() + if tab_name in ["요약", "카드이용TIP", "카드 디자인 소개", "네이버 디지털콘텐츠 적립 혜택 적용방법", "신세계포인트 적립 서비스", "세무지원 서비스", "BIZ SERVICE", "SPECIAL PLATE", "신세계백화점 제휴 서비스"]: + continue + + j = 0 + benefit += f'###{benefit_title[j]}' + j += 1 + + benefit_list = tab_list[i].find_all('h5', class_='tit04') + + for title in benefit_list: + if (title.text.strip() == "유의사항") or (title.text.strip() == "국제 브랜드사 서비스 공통 유의사항"): + break + + benefit += f'[{title.text.strip()}]' + next_sibling = title.find_next_sibling() + if next_sibling is not None and next_sibling.name == 'div' and 'table_col' in next_sibling.get('class', []): + table = next_sibling.select_one('table') + if table: + rows = table.find_all('tr') + for j, row in enumerate(rows): + cells = row.find_all(['th', 'td']) + row_data = ['(' + cell.text.strip() + ')' for cell in cells] + row_string = ' '.join(row_data) + + sentence = f"표의 {j + 1}번째 행은 {row_string}로 이루어져 있습니다." + benefit += sentence + + elif next_sibling is not None and next_sibling.name == 'ul': + benefit += next_sibling.text.strip() + + benefits.append(benefit) + +print("작업을 완료했습니다.", flush=True) +driver.quit() + +''' + 신용 카드 혜택 크롤링 + credit_benefit.csv : card_company_id, name, img_url, benefits, created_at, type +''' + +data = {"card_company_id": card_company_id, "name": name, "img_url": img_url, "benefits" : benefits, "created_at": created_at, "type": type} +df = pd.DataFrame(data) + +df.to_csv("/crawling/Samsung/credit_benefit.csv", encoding = "utf-8-sig", index=False) + diff --git a/src/main/resources/crawling/Samsung/debit.py b/src/main/resources/crawling/Samsung/debit.py new file mode 100644 index 0000000..fb6dfbc --- /dev/null +++ b/src/main/resources/crawling/Samsung/debit.py @@ -0,0 +1,147 @@ +import pandas as pd +import time +from datetime import datetime + +from selenium import webdriver +from selenium.webdriver.chrome.service import Service +from selenium.webdriver.chrome.options import Options +from webdriver_manager.chrome import ChromeDriverManager +from bs4 import BeautifulSoup + +''' + 체크 카드 리스트 조회 + samsung_checkcardInfos.csv : card_name, card_url, card_img +''' +url = "https://www.samsungcard.com/home/card/cardinfo/PGHPPCCCardCardinfoCheckcard001" + +chrome_options = Options() +chrome_options.add_argument('--headless') +chrome_options.add_argument('--no-sandbox') +chrome_options.add_argument("--disable-dev-shm-usage") + +service = Service(executable_path=r'/usr/bin/chromedriver') +driver = webdriver.Chrome(service=service,options=chrome_options) + +driver.implicitly_wait(20) +print("======= [삼성] 체크 카드 리스트 크롤링 =======", flush=True) +print("웹 페이지에 접속 중...", flush=True) +driver.get(url) +time.sleep(3) + +html = driver.page_source + +soup = BeautifulSoup(html, 'html.parser') + +card_names = [] +card_urls = [] +card_imgs = [] + +ul_tag = soup.find('ul', class_='lists') +list_elements = ul_tag.find_all('li') + +for idx, element in enumerate(list_elements, 1): + card_name_element = element.find('div', class_='tit-h4') + card_names.append(card_name_element.text.strip()) + + card_img_element = element.find('img').get('src') + card_imgs.append(card_img_element) + + card_url_code = card_img_element[-8:-4] + card_urls.append('https://www.samsungcard.com/home/card/cardinfo/PGHPPCCCardCardinfoDetails001?code=ABP' + card_url_code) + +print("작업을 완료했습니다.", flush=True) +driver.quit() + +data = {"card_name" : card_names, "card_url" : card_urls, "card_img": card_imgs} +df = pd.DataFrame(data) + +df.to_csv("/crawling/Samsung/samsung_checkcardInfos.csv", encoding = "utf-8-sig") + +''' + 체크 카드 혜택 크롤링 + debit_benefit.csv : card_company_id, name, img_url, benefits, created_at, type +''' +card_infos = pd.read_csv('/crawling/Samsung/samsung_checkcardInfos.csv') + +card_urls = card_infos['card_url'].tolist() +name = card_infos['card_name'].tolist() +img_url = card_infos['card_img'].tolist() + +card_company_id = [3] * len(card_urls) +benefits = [] + +created_at = [] +type = ["DebitCard"] * len(card_urls) + +print("======= [삼성] 전체 카드 혜택 정보 크롤링 =======",flush=True) +for i in range(len(card_urls)): + + chrome_options = Options() + chrome_options.add_argument('--headless') + chrome_options.add_argument('--no-sandbox') + chrome_options.add_argument("--disable-dev-shm-usage") + + service = Service(executable_path=r'/usr/bin/chromedriver') + driver = webdriver.Chrome(service=service,options=chrome_options) + + driver.implicitly_wait(20) + now = datetime.now() + created_at.append(now) + print(f"{now} [{card_names[i]}] --- 웹 페이지에 접속 중... ({i+1}/{len(card_urls)})", flush=True) + + time.sleep(3) + driver.get(card_urls[i]) + time.sleep(3) + + html = driver.page_source + soup = BeautifulSoup(html, 'html.parser') + + # 상세 혜택 ============================================= + tab_container = soup.find('section', class_='tab-container') + tab_list = tab_container.find_all('div', class_='tab-section') + + benefit_title = [] + benefit = '' + # print(i) + + for i in range(1, len(tab_list)): + benefit_title.append(tab_list[i].find('div', class_='dot-title').text.strip()) + + for i in range(1, len(tab_list)): + benefit += f'###{benefit_title[i-1]}' + benefit_list = tab_list[i].find_all('h5', class_='tit04') + + for title in benefit_list: + if (title.text.strip() == "유의사항") or (title.text.strip() == "문의"): + break + benefit += f'[{title.text.strip()}]' + next_sibling = title.find_next_sibling() + if next_sibling.name == 'div' and 'table_col' in next_sibling.get('class', []): + table = next_sibling.select_one('table') + if table: + rows = table.find_all('tr') + for j, row in enumerate(rows): + cells = row.find_all(['th', 'td']) + row_data = ['(' + cell.text.strip() + ')' for cell in cells] + row_string = ' '.join(row_data) + + sentence = f"표의 {j + 1}번째 행은 {row_string}로 이루어져 있습니다." + benefit += sentence + + elif next_sibling.name == 'ul': + benefit += next_sibling.text.strip() + + benefits.append(benefit) + +print("작업을 완료했습니다.", flush=True) +driver.quit() + +''' + 체크 카드 혜택 크롤링 + debit_benefit.csv : card_company_id, name, img_url, benefits, created_at, type +''' + +data = {"card_company_id": card_company_id, "name": name, "img_url": img_url, "benefits" : benefits, "created_at": created_at, "type": type} +df = pd.DataFrame(data) + +df.to_csv("/crawling/Samsung/debit_benefit.csv", encoding = "utf-8-sig", index=False) diff --git a/src/main/resources/crawling/Shinhan/credit.py b/src/main/resources/crawling/Shinhan/credit.py new file mode 100644 index 0000000..739a7e7 --- /dev/null +++ b/src/main/resources/crawling/Shinhan/credit.py @@ -0,0 +1,211 @@ +import pandas as pd +import time +from datetime import datetime + +from selenium import webdriver +from selenium.webdriver.chrome.service import Service +from selenium.webdriver.chrome.options import Options +from webdriver_manager.chrome import ChromeDriverManager +from bs4 import BeautifulSoup + +''' + 카드 정보 조회 + shinhan_cardInfos.csv : card_name, card_url, card_img +''' +url = "https://www.shinhancard.com/pconts/html/card/credit/MOBFM281/MOBFM281R11.html" + +chrome_options = Options() +chrome_options.add_argument('--headless') +chrome_options.add_argument('--no-sandbox') +chrome_options.add_argument("--disable-dev-shm-usage") + +service = Service(executable_path=r'/usr/bin/chromedriver') +driver = webdriver.Chrome(service=service,options=chrome_options) + +driver.implicitly_wait(20) +print("======= [신한] 신용 카드 정보 크롤링 =======", flush=True) +print("웹 페이지에 접속 중...", flush=True) +driver.get(url) +time.sleep(3) + +html = driver.page_source + +soup = BeautifulSoup(html, 'html.parser') + +card_names = [] +card_urls = [] +card_imgs = [] + +div_tag = soup.find('div', {'data-plugin-view': 'cmmCardList'}) +ul_tag = div_tag.find('ul', {'class': 'card_thumb_list_wrap'}) + +list_elements = ul_tag.find_all('li') + +for idx, element in enumerate(list_elements, 1): + card_name_element = element.find('a', class_='card_name') + + card_names.append(card_name_element.text.strip()) + +a_tag = ul_tag.find_all('a') + +for i in range(len(a_tag)): + card_urls.append(a_tag[i].get('href').split('/')[-1]) + +card_urls = list(dict.fromkeys(card_urls)) # 중복 제거 + +for i in range(0, len(a_tag), 3): + card_imgs.append('https://www.shinhancard.com' + a_tag[i].find('img')['src']) + +print("작업을 완료했습니다.", flush=True) +driver.quit() + +data = {"card_name" : card_names, "card_url" : card_urls, "card_img": card_imgs} +df = pd.DataFrame(data) + +df.to_csv("/crawling/Shinhan/shinhan_cardInfos.csv", encoding = "utf-8-sig") + +''' + 전체 카드 혜택 크롤링 + credit_benefit.csv : card_company_id, name, img_url, benefits, created_at, type +''' +card_infos = pd.read_csv('/crawling/Shinhan/shinhan_cardInfos.csv') + + +card_urls = card_infos['card_url'].tolist() +name = card_infos['card_name'].tolist() +img_url = card_infos['card_img'].tolist() + +card_company_id = [4] * len(card_urls) # 신한카드 +benefits = [] + +created_at = [] +type = ["CreditCard"] * len(card_urls) + +print("======= [신한] 전체 카드 혜택 정보 크롤링 =======", flush=True) +for i in range(len(card_urls)): + url = f'https://www.shinhancard.com/pconts/html/card/apply/credit/{card_urls[i]}' + + + chrome_options = Options() + chrome_options.add_argument('--headless') + chrome_options.add_argument('--no-sandbox') + chrome_options.add_argument("--disable-dev-shm-usage") + + service = Service(executable_path=r'/usr/bin/chromedriver') + driver = webdriver.Chrome(service=service,options=chrome_options) + + driver.implicitly_wait(20) + now = datetime.now() + created_at.append(now) + print(f"{now} [{card_names[i]}] --- 웹 페이지에 접속 중... ({i+1}/{len(card_urls)})", flush=True) + + time.sleep(3) + driver.get(url) + time.sleep(3) + + html = driver.page_source + soup = BeautifulSoup(html, 'html.parser') + + # 상세 혜택 ============================================= + tap_wrap = soup.find('div', {'class':'tab_wrap'}) + hidden_tap = tap_wrap.find_all('li', {'aria-hidden':'true'}) + + benefit_title = [] # 탭 제목들 (혜택 제목) + benefit = '' + for i in range(len(hidden_tap)): + benefit_title.append(hidden_tap[i].find('h2', {'class':'hidden-text'}).text.strip()) + + for i in range(len(hidden_tap)): + benefit += f'###{benefit_title[i]}' + tit_dep2 = hidden_tap[i].find_all(class_="tit_dep2") + + for title in tit_dep2: + benefit += f'[{title.text.strip()}]' + next_sibling = title.find_next_sibling() + while next_sibling: + if next_sibling.name == 'div' and 'table_wrap' in next_sibling.get('class', []): + table = next_sibling.select_one('table') + # 표가 있을 때만 출력 + if table: + rows = table.find_all('tr') + benefit += table.find('strong').text.strip() + for j, row in enumerate(rows): + + cells = row.find_all(['th', 'td']) + row_data = ['(' + cell.text.strip() + ')' for cell in cells] + row_string = ' '.join(row_data) + + # 각 행을 설명하는 문장 출력 + sentence = f"표의 {j + 1}번째 행은 {row_string}로 이루어져 있습니다." + benefit += sentence + + elif next_sibling.name == 'p': + class_list = next_sibling.get('class', []) + if ('marker_dot' in class_list) or ('marker_refer' in class_list): + benefit += next_sibling.text.strip() + elif next_sibling.name == 'ul': + class_list = next_sibling.get('class', []) + if ('marker_dot' in class_list) or ('marker_refer' in class_list) or ('marker_hyphen' in class_list): + li_list = next_sibling.find_all('li', recursive=False) + for li in li_list: + benefit += li.text.strip() + + # 다음 형제 요소 찾을 때 특정 조건을 만족하면 루프 종료 + if 'tit_dep2' in next_sibling.get('class', []) or ('h4' in next_sibling.name and 'tit_dep3' in next_sibling.get('class', [])): + break + + next_sibling = next_sibling.find_next_sibling() + + tit_dep3 = hidden_tap[i].find_all('h4', class_="tit_dep3") + if tit_dep3: + for title in tit_dep3: + benefit += f'[{title.text.strip()}]' + next_sibling = title.find_next_sibling() + while next_sibling: + if next_sibling.name == 'div' and 'table_wrap' in next_sibling.get('class', []): + table = next_sibling.select_one('table') + # 표가 있을 때만 출력 + if table: + rows = table.find_all('tr') + benefit += table.find('strong').text.strip() + for j, row in enumerate(rows): + + cells = row.find_all(['th', 'td']) + row_data = ['(' + cell.text.strip() + ')' for cell in cells] + row_string = ' '.join(row_data) + + # 각 행을 설명하는 문장 출력 + sentence = f"표의 {j + 1}번째 행은 {row_string}로 이루어져 있습니다." + benefit += sentence + elif next_sibling.name == 'p': + class_list = next_sibling.get('class', []) + if ('marker_dot' in class_list) or ('marker_refer' in class_list): + benefit += next_sibling.text.strip() + + elif next_sibling.name == 'ul': + class_list = next_sibling.get('class', []) + if ('marker_dot' in class_list) or ('marker_refer' in class_list) or ('marker_hyphen' in class_list): + li_list = next_sibling.find_all('li', recursive=False) + for li in li_list: + benefit += li.text.strip() + + # 다음 형제 요소 찾을 때 특정 조건을 만족하면 루프 종료 + if 'tit_dep3' in next_sibling.get('class', []): + break + + next_sibling = next_sibling.find_next_sibling() + + benefits.append(benefit) + +print("작업을 완료했습니다.", flush=True) +driver.quit() + +''' + 전체 카드 혜택 크롤링 + credit_benefit.csv : card_company_id, name, img_url, benefits, created_at, type +''' + +data = {"card_company_id": card_company_id, "name": name, "img_url": img_url, "benefits" : benefits, "created_at": created_at, "type": type} +df = pd.DataFrame(data) + +df.to_csv("/crawling/Shinhan/credit_benefit.csv", encoding = "utf-8-sig", index=False) \ No newline at end of file diff --git a/src/main/resources/crawling/Shinhan/debit.py b/src/main/resources/crawling/Shinhan/debit.py new file mode 100644 index 0000000..63002bf --- /dev/null +++ b/src/main/resources/crawling/Shinhan/debit.py @@ -0,0 +1,210 @@ +import pandas as pd +import time +from datetime import datetime + +from selenium import webdriver +from selenium.webdriver.chrome.service import Service +from selenium.webdriver.chrome.options import Options +from webdriver_manager.chrome import ChromeDriverManager +from bs4 import BeautifulSoup + +''' + 카드 정보 조회 + shinhan_checkCardInfos.csv : card_name, card_url, card_img +''' +url = "https://www.shinhancard.com/pconts/html/card/check/MOBFM282R11.html?crustMenuId=ms527" + +chrome_options = Options() +chrome_options.add_argument('--headless') +chrome_options.add_argument('--no-sandbox') +chrome_options.add_argument("--disable-dev-shm-usage") + +service = Service(executable_path=r'/usr/bin/chromedriver') +driver = webdriver.Chrome(service=service,options=chrome_options) + +driver.implicitly_wait(20) +print("======= [신한] 체크 카드 정보 크롤링 =======", flush=True) +print("웹 페이지에 접속 중...", flush=True) +driver.get(url) +time.sleep(3) + +html = driver.page_source + +soup = BeautifulSoup(html, 'html.parser') + +card_names = [] +card_urls = [] +card_imgs = [] + +div_tag = soup.find('div', {'data-plugin-view': 'cmmCardList'}) +ul_tag = div_tag.find('ul', {'class': 'card_thumb_list_wrap'}) + +list_elements = ul_tag.find_all('li') + +for idx, element in enumerate(list_elements, 1): + card_name_element = element.find('a', class_='card_name') + + card_names.append(card_name_element.text.strip()) + +a_tag = ul_tag.find_all('a') + +for i in range(len(a_tag)): + card_urls.append(a_tag[i].get('href').split('/')[-1]) + +card_urls = list(dict.fromkeys(card_urls)) # 중복 제거 + +for i in range(0, len(a_tag), 3): + card_imgs.append('https://www.shinhancard.com' + a_tag[i].find('img')['src']) + +print("작업을 완료했습니다.", flush=True) +driver.quit() + +data = {"card_name" : card_names, "card_url" : card_urls, "card_img": card_imgs} +df = pd.DataFrame(data) + +df.to_csv("/crawling/Shinhan/shinhan_checkCardInfos.csv", encoding = "utf-8-sig") + +''' + 전체 카드 혜택 크롤링 + credit_benefit.csv : card_company_id, name, img_url, benefits, created_at, type +''' +card_infos = pd.read_csv('/crawling/Shinhan/shinhan_checkCardInfos.csv') + + +card_urls = card_infos['card_url'].tolist() +name = card_infos['card_name'].tolist() +img_url = card_infos['card_img'].tolist() + +card_company_id = [4] * len(card_urls) # 신한카드 +benefits = [] + +created_at = [] +type = ["DebitCard"] * len(card_urls) + +print("======= [신한] 전체 카드 혜택 정보 크롤링 =======", flush=True) +for i in range(len(card_urls)): + url = f'https://www.shinhancard.com/pconts/html/card/apply/check/{card_urls[i]}' + + chrome_options = Options() + chrome_options.add_argument('--headless') + chrome_options.add_argument('--no-sandbox') + chrome_options.add_argument("--disable-dev-shm-usage") + + service = Service(executable_path=r'/usr/bin/chromedriver') + driver = webdriver.Chrome(service=service,options=chrome_options) + + driver.implicitly_wait(20) + now = datetime.now() + created_at.append(now) + print(f"{now} [{card_names[i]}] --- 웹 페이지에 접속 중... ({i+1}/{len(card_urls)})", flush=True) + + time.sleep(3) + driver.get(url) + time.sleep(3) + + html = driver.page_source + soup = BeautifulSoup(html, 'html.parser') + + # 상세 혜택 ============================================= + tap_wrap = soup.find('div', {'class':'tab_wrap'}) + hidden_tap = tap_wrap.find_all('li', {'aria-hidden':'true'}) + + benefit_title = [] # 탭 제목들 (혜택 제목) + benefit = '' + for i in range(len(hidden_tap)): + benefit_title.append(hidden_tap[i].find('h2', {'class':'hidden-text'}).text.strip()) + + for i in range(len(hidden_tap)): + benefit += f'###{benefit_title[i]}' + tit_dep2 = hidden_tap[i].find_all(class_="tit_dep2") + + for title in tit_dep2: + benefit += f'[{title.text.strip()}]' + next_sibling = title.find_next_sibling() + while next_sibling: + if next_sibling.name == 'div' and 'table_wrap' in next_sibling.get('class', []): + table = next_sibling.select_one('table') + # 표가 있을 때만 출력 + if table: + rows = table.find_all('tr') + benefit += table.find('strong').text.strip() + for j, row in enumerate(rows): + + cells = row.find_all(['th', 'td']) + row_data = ['(' + cell.text.strip() + ')' for cell in cells] + row_string = ' '.join(row_data) + + # 각 행을 설명하는 문장 출력 + sentence = f"표의 {j + 1}번째 행은 {row_string}로 이루어져 있습니다." + benefit += sentence + + elif next_sibling.name == 'p': + class_list = next_sibling.get('class', []) + if ('marker_dot' in class_list) or ('marker_refer' in class_list): + benefit += next_sibling.text.strip() + elif next_sibling.name == 'ul': + class_list = next_sibling.get('class', []) + if ('marker_dot' in class_list) or ('marker_refer' in class_list) or ('marker_hyphen' in class_list): + li_list = next_sibling.find_all('li', recursive=False) + for li in li_list: + benefit += li.text.strip() + + # 다음 형제 요소 찾을 때 특정 조건을 만족하면 루프 종료 + if 'tit_dep2' in next_sibling.get('class', []) or ('h4' in next_sibling.name and 'tit_dep3' in next_sibling.get('class', [])): + break + + next_sibling = next_sibling.find_next_sibling() + + tit_dep3 = hidden_tap[i].find_all('h4', class_="tit_dep3") + if tit_dep3: + for title in tit_dep3: + benefit += f'[{title.text.strip()}]' + next_sibling = title.find_next_sibling() + while next_sibling: + if next_sibling.name == 'div' and 'table_wrap' in next_sibling.get('class', []): + table = next_sibling.select_one('table') + # 표가 있을 때만 출력 + if table: + rows = table.find_all('tr') + benefit += table.find('strong').text.strip() + for j, row in enumerate(rows): + + cells = row.find_all(['th', 'td']) + row_data = ['(' + cell.text.strip() + ')' for cell in cells] + row_string = ' '.join(row_data) + + # 각 행을 설명하는 문장 출력 + sentence = f"표의 {j + 1}번째 행은 {row_string}로 이루어져 있습니다." + benefit += sentence + elif next_sibling.name == 'p': + class_list = next_sibling.get('class', []) + if ('marker_dot' in class_list) or ('marker_refer' in class_list): + benefit += next_sibling.text.strip() + + elif next_sibling.name == 'ul': + class_list = next_sibling.get('class', []) + if ('marker_dot' in class_list) or ('marker_refer' in class_list) or ('marker_hyphen' in class_list): + li_list = next_sibling.find_all('li', recursive=False) + for li in li_list: + benefit += li.text.strip() + + # 다음 형제 요소 찾을 때 특정 조건을 만족하면 루프 종료 + if 'tit_dep3' in next_sibling.get('class', []): + break + + next_sibling = next_sibling.find_next_sibling() + + benefits.append(benefit) + +print("작업을 완료했습니다.", flush=True) +driver.quit() + +''' + 전체 카드 혜택 크롤링 + debit_benefit.csv : card_company_id, name, img_url, benefits, created_at, type +''' + +data = {"card_company_id": card_company_id, "name": name, "img_url": img_url, "benefits" : benefits, "created_at": created_at, "type": type} +df = pd.DataFrame(data) + +df.to_csv("/crawling/Shinhan/debit_benefit.csv", encoding = "utf-8-sig", index=False) \ No newline at end of file