diff --git a/.github/workflows/cicd-dev.yml b/.github/workflows/cicd-dev.yml new file mode 100644 index 00000000..465322c5 --- /dev/null +++ b/.github/workflows/cicd-dev.yml @@ -0,0 +1,127 @@ +name: CI and CD in WashPedia Project + +on: + push: + branches: [ "develop" ] + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: "17" + distribution: "temurin" + + - name: Replace SQL Queries + run: | + file_path="${{ secrets.TARGET_SQL_PATH }}" + old_text="changeRequired" + new_text="${{ secrets.PUBLIC_KEY }}" + sed -i "s/$old_text/$new_text/g" $file_path + + - name: Set application yml file (api) + uses: microsoft/variable-substitution@v1 + with: + files: ./module-api/src/main/resources/application.yml + env: + spring.profiles.active: "dev" + + - name: Set application yml file (batch) + uses: microsoft/variable-substitution@v1 + with: + files: ./module-batch/src/main/resources/application.yml + env: + spring.profiles.active: "dev" + + - name: Set application-dev yml file (api) + uses: microsoft/variable-substitution@v1 + with: + files: ./module-api/src/main/resources/application-dev.yml + env: + spring.datasource.url: ${{ secrets.DB_URL }} + spring.datasource.username: ${{ secrets.DB_USER }} + spring.datasource.password: ${{ secrets.DB_PW }} + spring.mail.username: ${{ secrets.SMTP_GOOGLE_EMAIL }} + spring.mail.password: ${{ secrets.SMTP_PASSWORD }} + spring.data.redis.host: ${{ secrets.REDIS_DEV_HOST }} + spring.data.redis.password: ${{ secrets.REDIS_DEV_PASSWORD }} + constants.host-url: ${{ secrets.DEV_HOST_URL }} + aws.credentials.access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws.credentials.secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws.s3.bucket.name: ${{ secrets.AWS_S3_BUCKET_NAME }} + aws.s3.bucket.url: ${{ secrets.AWS_S3_BUCKET_URL }} + + - name: Set application-dev yml file (batch) + uses: microsoft/variable-substitution@v1 + with: + files: ./module-batch/src/main/resources/application-dev.yml + env: + spring.datasource.url: ${{ secrets.DB_URL }} + spring.datasource.username: ${{ secrets.DB_USER }} + spring.datasource.password: ${{ secrets.DB_PW }} + external.ecolife-api.path: ${{ secrets.ECOLIFE_PATH }} + external.ecolife-api.service-key: ${{ secrets.ECOLIFE_KEY }} + + - name: Grant execute permission And Build with Gradle (api) + working-directory: ./module-api + run: | + chmod +x ./gradlew + ./gradlew bootJar + + - name: Grant execute permission And Build with Gradle (batch) + working-directory: ./module-batch + run: | + chmod +x ./gradlew + ./gradlew bootJar + + - name: DockerHub Login + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + + - name: Docker Image Build And Push (api) + working-directory: ./module-api + run: | + docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.MODULE_API_NAME }} . --platform=linux/amd64 + docker push ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.MODULE_API_NAME }} + + - name: Docker Image Build And Push (batch) + working-directory: ./module-batch + run: | + docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.MODULE_BATCH_NAME }} . --platform=linux/amd64 + docker push ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.MODULE_BATCH_NAME }} + + - name: Application Run + uses: appleboy/ssh-action@v0.1.6 + with: + host: ${{ secrets.VULTR_IP }} + username: ${{ secrets.VULTR_USERNAME }} + password: ${{ secrets.VULTR_PW }} + script: | + # Install Docker and Docker Compose + dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo + dnf install -y docker-ce docker-ce-cli containerd.io + systemctl start docker + systemctl enable docker + curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose + chmod +x /usr/local/bin/docker-compose + + # Docker Compose + mkdir -p deploy + cd deploy + echo "${{ secrets.DB_INIT_SQL }}" > init.sql + echo "${{ secrets.DEV_REDIS_CONF }}" > redis.conf + echo "${{ secrets.DOCKER_COMPOSE }}" > docker-compose.yml + docker-compose pull + docker-compose down + docker-compose up -d \ No newline at end of file diff --git a/.github/workflows/cicd-prod.yml b/.github/workflows/cicd-prod.yml new file mode 100644 index 00000000..c56ed832 --- /dev/null +++ b/.github/workflows/cicd-prod.yml @@ -0,0 +1,106 @@ +name: Project CI/CD with AWS + +on: + push: + branches: [ "main" ] + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: "17" + distribution: "temurin" + + - name: Replace SQL Queries + run: | + file_path="${{ secrets.TARGET_SQL_PATH }}" + old_text="changeRequired" + new_text="${{ secrets.PUBLIC_KEY }}" + sed -i "s/$old_text/$new_text/g" $file_path + + - name: Set application yml file (api) + uses: microsoft/variable-substitution@v1 + with: + files: ./module-api/src/main/resources/application.yml + env: + spring.profiles.active: "prod" + + - name: Set application-prod yml file (api) + uses: microsoft/variable-substitution@v1 + with: + files: ./module-api/src/main/resources/application-prod.yml + env: + spring.datasource.url: ${{ secrets.DB_URL_AWS }} + spring.datasource.username: ${{ secrets.DB_USER }} + spring.datasource.password: ${{ secrets.DB_PW }} + spring.mail.username: ${{ secrets.SMTP_GOOGLE_EMAIL }} + spring.mail.password: ${{ secrets.SMTP_PASSWORD }} + spring.data.redis.host: ${{ secrets.REDIS_PROD_HOST }} + spring.data.redis.password: ${{ secrets.REDIS_PROD_PASSWORD }} + constants.host-url: ${{ secrets.PROD_HOST_URL }} + aws.credentials.access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws.credentials.secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws.s3.bucket.name: ${{ secrets.AWS_S3_BUCKET_NAME }} + aws.s3.bucket.url: ${{ secrets.AWS_S3_BUCKET_URL }} + + - name: Grant execute permission And Build with Gradle (api) + working-directory: ./module-api + run: | + chmod +x ./gradlew + ./gradlew bootJar + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v3 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ap-northeast-2 + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v2 + + - name: Build, tag, and push docker image to Amazon ECR + working-directory: ./module-api + env: + REGISTRY: ${{ steps.login-ecr.outputs.registry }} + REPOSITORY: washfit-api + IMAGE_TAG: latest + run: | + docker build -t $REGISTRY/$REPOSITORY:$IMAGE_TAG . + docker push $REGISTRY/$REPOSITORY:$IMAGE_TAG + + - name: Application Run + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ secrets.AWS_EC2_HOST }} + username: ${{ secrets.AWS_USERNAME }} + key: ${{ secrets.AWS_KEY }} + script: | + # Install Docker and Docker Compose + sudo dnf update + sudo dnf install docker + sudo systemctl start docker + sudo systemctl enable docker + sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose + sudo chmod +x /usr/local/bin/docker-compose + + # Docker Compose + mkdir -p deploy + cd deploy + echo "${{ secrets.PROD_REDIS_CONF }}" > redis.conf + echo "${{ secrets.DOCKER_COMPOSE_AWS }}" > docker-compose.yml + + docker-compose pull + docker-compose down + docker-compose up -d \ No newline at end of file diff --git a/.github/workflows/cr.yml b/.github/workflows/cr.yml deleted file mode 100644 index 193eeebd..00000000 --- a/.github/workflows/cr.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: Code Review - -permissions: - contents: read - pull-requests: write - -on: - pull_request: - types: [opened, reopened, synchronize] - -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: anc95/ChatGPT-CodeReview@main - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - LANGUAGE: Korean - PROMPT: 당신은 능력 있는 10년차 시니어 개발자로서 프로젝트의 코드 리뷰를 담당하고 있습니다. 코드 리뷰는 한국어로 작성하고 단순하거나 큰 이슈가 없다면 굳이 리뷰하지 않아도 됩니다. 추가로 코드 리뷰를 할 때 일일이 설명하지 않고 꼭 필요한 부분만 짚어서 짧게 설명해주셔도 됩니다. diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml deleted file mode 100644 index cf493d7c..00000000 --- a/.github/workflows/gradle.yml +++ /dev/null @@ -1,57 +0,0 @@ -name: Java CI with Gradle WashPedia Project - -on: - push: - branches: [ "send_develop_server" ] - -permissions: - contents: read - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - name: Set up JDK 17 - uses: actions/setup-java@v3 - with: - java-version: '17' - distribution: 'temurin' - - - name: Replace SQL Queries - run: | - sql_directory="./module-api/src/main/resources/db/migration" - old_text="changeRequired" - new_text="${{ secrets.PUBLIC_KEY }}" - find "$sql_directory" -type f -name "*.sql" -exec sed -i "s|$old_text|$new_text|g" {} + - - - name: Grant execute permission for gradlew - working-directory: ./module-api - run: chmod +x ./gradlew - - - name: Build with Gradle - working-directory: ./module-api - run: ./gradlew build - - - name: Install sshpass - run: sudo apt-get install -y sshpass - - - name: Copy JAR file to remote server using SCP - run: | - cd module-api/build/libs/ - export PASSWORD="${{ secrets.VULTR_PW }}" - sshpass -p "$PASSWORD" scp -o StrictHostKeyChecking=no -P 22 module-api-0.0.1-SNAPSHOT.jar ${{ secrets.VULTR_USER_NAME }}@${{ secrets.VULTR_IP }}:/root - - ## 터미널 접속 후 실행 -> 수정 필요 - - name: Application Run on Remote Server - run: | - export JAR_NAME="module-api-0.0.1-SNAPSHOT.jar" - export PASSWORD="${{ secrets.VULTR_PW }}" - sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no ${{ secrets.VULTR_USER_NAME }}@${{ secrets.VULTR_IP }} \ - "pgrep -f java | xargs kill -9 && \ - cd \$HOME && \ - echo pwd && \ - mkdir -p ./logs && \ - nohup java -jar $JAR_NAME --jasypt.encryptor.password=${{ secrets.PUBLIC_KEY }} >> ./logs/deploy_success.log 2> ./logs/deploy_error.log & " \ No newline at end of file diff --git a/.gitignore b/.gitignore index d384eed9..67b52154 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,8 @@ build/ .settings .springBeans .sts4-cache +.jqwik-database/ + bin/ !**/src/main/**/bin/ !**/src/test/**/bin/ diff --git a/README.md b/README.md index da948f97..8414fe3e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,49 @@ # F1-WashPedia -세차용품 안전정보 제공 및 검증 된 세차 관련 정보를 보여주는 플랫폼 + +## | 프로젝트 기획 및 목적 + 세차용품 안전정보 제공 및 검증 된 세차 관련 정보를 보여주는 플랫폼 + +## | 배포 주소 (로그인 필요시, 테스트 계정 기입) + 배포 주소 :: + - https://dev.washfit.site + + 테스트용 계정 :: + - ID : washfit01 + - PW : washfit1! + +## | 주요 기능 + 회원 :: 회원가입, 로그인, 로그아웃, 회원 기본정보 수정, 차량정보 등록/수정, 세차정보 등록/수정 + 제품 :: 제품 검색(키워드, 조회순, 위반제품, 최신순) + 배치 :: 제품 정보 요청 및 적재 + +## | 기능 작성시 상세한 기획/정책/규칙 작성 + 회원가입/로그인/로그아웃 :: + - JWT 토큰과 해시 알고리즘을 사용하여 구현. Spring Security 사용 X + + 제품 정보 요청 :: + - 초록누리 API 위해제품목록 요청 + - 목록 정보를 바탕으로 단일 제품 상세 정보 요청 + - 상세정보를 서비스에 사용하는 Product 테이블에 추가 + +## | 아키텍처 +### 멀티모듈 구조 + ![멀티모듈구조](https://github.com/Kernel360/F1-WashPedia-BE/assets/73059667/5e008117-fd3b-40b4-ac2e-2cfb52b42ae9) + + + +### 개발서버 인프라 구조 + ![개발서버인프라구조](https://github.com/Kernel360/F1-WashPedia-BE/assets/73059667/4ef76ccc-6140-4ecb-8704-a4a51f591435) + + +## | 그 외 정보들 + 배포 :: + - 운영 배포는 AWS 클라우드를 사용할 예정, 개발 배포를 vultr 사용중 + + 추후 구현해야 하는 기능 :: + - 회원탈퇴(탈퇴회원 관리 정책 필요) + - 아이디/비밀번호 찾기 + - 즐겨찾기 + - 세차장 위치 정보 검색 + - Admin 페이지 + - 금지어 필터링 + - (제품 추천 시스템) diff --git a/controllerTester.http b/controllerTester.http new file mode 100644 index 00000000..ed74e268 --- /dev/null +++ b/controllerTester.http @@ -0,0 +1,152 @@ + + +# Main-Controller API + +### 배너정보조회 +GET http://localhost:8080/banner + + +### 추천제품조회 +GET http://localhost:8080/recommend-products + + +### 조회순 +GET http://localhost:8080/products/rank?sortType=viewCnt-order + + + +### 위반제품목록 +GET http://localhost:8080/products/rank?sortType=violation-products + +<> 2024-02-23T161552-3.200.json + + +### 추천순 +GET http://localhost:8080/products/rank?sortType=recommend-order + +<> 2024-02-23T161552-4.200.json + + +### 최신순 +GET http://localhost:8080/products/rank?sortType=recent-order + +<> 2024-02-23T161552-5.200.json + + +## Product API Test + +### 제품리스트 조회 +GET http://localhost:8080/products + +<> 2024-02-23T161552-6.200.json + + +### 제품아이디로 제품조회 +GET http://localhost:8080/product/5 + +<> 2024-02-23T161552-7.200.json + +### 키워드로 제품 조회 +GET http://localhost:8080/products/search?keyword=더클래스 + +<> 2024-02-23T161553.200.json +<> 2024-02-23T164752.201.json +<> 2024-02-23T163943.201.json + + +### 마이페이지 회원비밀번호 검증요청 +GET http://localhost:8080/mypage/member/validate +Content-Type: application/json +Authorization: {{ authorization }} + +{ + "password": "zxcv1234" +} + +<> 2024-02-23T164933.200.json + + +### 마이페이지 회원정보 수정요청 +PUT http://localhost:8080/mypage/member +Authorization: {{ authorization }} +Content-Type: application/json + +{ + "gender": 2, + "age": 30 +} + +### 회원 세차정보(부가정보) 입력 +POST http://localhost:8080/member/wash +Authorization: {{ authorization}} +Content-Type: application/json + +{ + "washCount": 59, + "monthlyExpense": 63, + "interest": 44 +} + +### 회원 차량정보(부가정보) 입력 +POST http://localhost:8080/member/car +Content-Type: application/json +Authorization: {{ authorization }} + +{ + "carType": 11, + "carSize": 16, + "carColor": 21, + "drivingEnv": 36, + "parkingEnv": 40 +} + +### 마이페이지 차량정보 요청 +GET http://localhost:8080/mypage/car +Authorization: {{ authorization }} + + +### 마이페이지 세차정보 요청 +GET http://localhost:8080/mypage/wash +Authorization: {{authorization}} + + +### 세차장 정보조회 + +GET http://localhost:8080/washzones?minX=36.12&maxX= 36.88&minY=127.1&maxY=127.8&level=2 + +<> 2024-02-23T161554.201.json + + +### 세차장 키워드 검색 +GET http://localhost:8080/washzones/search?keyword=워시존 + +<> 2024-02-23T161554-1.201.json + + +### 세차장 정보 등록 + +POST http://127.0.0.1:8080/washzones +Content-Type: application/json + +{ + "name" : "샘플 세차장", + "address": "서울특별시 봉은사역", + "latitude": 37.777, + "longitude": 127.0808 +} + + +<> 2024-02-23T161554-2.201.json + + +### OCR 검색 기능 + +GET http://127.0.0.1:8080/products/CB19-12-1034 + +<> 2024-02-23T161554.200.json + + +#### 마이페이지 회원탈퇴 요청 + +DELETE http://localhost:8080/mypage/member +Authorization: {{authorization}} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index d7e47b07..00000000 --- a/docker-compose.yml +++ /dev/null @@ -1,50 +0,0 @@ -version: '3.8' -services: - webapp: - build: ./module-api - image: ${WEBAPP_IMAGE} - ports: - - "8080:8080" - depends_on: - - db-edit - - batch: - build: ./module-batch - image: ${BATCH_IMAGE} - ports: - - "8082:8082" - depends_on: - - db-edit -# -# admin: -# build: ./module-admin -# ports: -# - "8083:8080" -# depends_on: -# - db-read - - db-edit: - image: younglong/pg-washpedia:latest - volumes: - - ./postgreReadOnlyDB:/var/lib/postgresql/data - environment: - POSTGRES_DB: edit_db - POSTGRES_USER: ${USER} - POSTGRES_PASSWORD: ${PASSWORD} - ports: - - "5432:5432" - -# db-read: -# image: younglong/pg-washpedia:latest -# volumes: -# - ./postgreEditDB:/var/lib/postgresql/data -# environment: -# POSTGRES_DB: read_db -# POSTGRES_USER: ${{ secrets.PROJECT_USER }} -# POSTGRES_PASSWORD: ${{ secrets.POSTGRE_PASSWORD }} -# ports: -# - "5433:5433" - -volumes: - db-read-volume: {} - db-edit-volume: {} \ No newline at end of file diff --git a/http-client.env.json b/http-client.env.json new file mode 100644 index 00000000..0f70f692 --- /dev/null +++ b/http-client.env.json @@ -0,0 +1,5 @@ +{ + "dev": { + "authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ3YXNocGVkaWEiLCJqdGkiOiJ0ZXN0MSIsImlhdCI6MTcwODkzNzM5NSwiZXhwIjoxNzA4OTk3Mzk1fQ.9wE0gyuTUxFdPYigLWprEiI35P0BbCvI49irZ3Gfp3E" + }} + diff --git a/memberSetting.http b/memberSetting.http new file mode 100644 index 00000000..b9a67177 --- /dev/null +++ b/memberSetting.http @@ -0,0 +1,34 @@ +## MyPage API + +### 마이페이지 테스트를 위한 회원가입# +POST http://localhost:8080/member/join +Content-Type: application/json + +{ + "id": "test1", + "password": "zxcv1234", + "email": "kernel360@kernel360.co.kr", + "gender": "MALE", + "age": "AGE_20" +} + +### 마이페이지 검증을 위한 로그인 #1 +POST http://localhost:8080/member/login +Content-Type: application/json + +{ + "id": "test1", + "password": "zxcv1234" +} + +<> 2024-02-23T164810.200.json +<> 2024-02-23T163953.200.json +<> 2024-02-23T161553-1.200.json + + +### 마이페이지 회원(내)정보 +GET http://localhost:8080/mypage/member +Authorization: {{ authorization }} + +<> 2024-02-23T164831.200.json +<> 2024-02-23T164011.200.json \ No newline at end of file diff --git a/module-admin/build.gradle b/module-admin/build.gradle new file mode 100644 index 00000000..34d37f24 --- /dev/null +++ b/module-admin/build.gradle @@ -0,0 +1,41 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.2.0' + id 'io.spring.dependency-management' version '1.1.4' +} + +group = 'com.kernel360' +version = '0.0.1-SNAPSHOT' + +java { + sourceCompatibility = '17' +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation project(':module-common') + implementation project(':module-domain') + + implementation 'com.github.ulisesbocchio:jasypt-spring-boot-starter:3.0.5' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-web' + compileOnly 'org.projectlombok:lombok' + runtimeOnly 'org.postgresql:postgresql' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' +} + +tasks.named('test') { + useJUnitPlatform() +} + +tasks.register("prepareKotlinBuildScriptModel") {} \ No newline at end of file diff --git a/module-admin/gradle/wrapper/gradle-wrapper.jar b/module-admin/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..d64cd491 Binary files /dev/null and b/module-admin/gradle/wrapper/gradle-wrapper.jar differ diff --git a/module-admin/gradle/wrapper/gradle-wrapper.properties b/module-admin/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..1af9e093 --- /dev/null +++ b/module-admin/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/module-admin/gradlew b/module-admin/gradlew new file mode 100755 index 00000000..1aa94a42 --- /dev/null +++ b/module-admin/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/module-admin/gradlew.bat b/module-admin/gradlew.bat new file mode 100644 index 00000000..93e3f59f --- /dev/null +++ b/module-admin/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/module-admin/src/main/java/com/kernel360/ModuleAdminApplication.java b/module-admin/src/main/java/com/kernel360/ModuleAdminApplication.java new file mode 100644 index 00000000..8b458e3f --- /dev/null +++ b/module-admin/src/main/java/com/kernel360/ModuleAdminApplication.java @@ -0,0 +1,13 @@ +package com.kernel360; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ModuleAdminApplication { + + public static void main(String[] args) { + SpringApplication.run(ModuleAdminApplication.class, args); + } + +} diff --git a/module-admin/src/main/java/com/kernel360/product/code/ProductsBusinessCode.java b/module-admin/src/main/java/com/kernel360/product/code/ProductsBusinessCode.java new file mode 100644 index 00000000..bb3702e7 --- /dev/null +++ b/module-admin/src/main/java/com/kernel360/product/code/ProductsBusinessCode.java @@ -0,0 +1,34 @@ +package com.kernel360.product.code; +import com.kernel360.code.BusinessCode; +import org.springframework.http.HttpStatus; + +public enum ProductsBusinessCode implements BusinessCode { + GET_RECOMMEND_PRODUCT_DATA_SUCCESS(HttpStatus.OK.value(), "PMB001", "추천제품정보 조회 성공"), + GET_PRODUCT_DATA_SUCCESS(HttpStatus.OK.value(), "PMB002", "제품정보 조회 성공"), + UPDATE_PRODUCT_DATA_SUCCESS(HttpStatus.OK.value(), "PMB003", "제품정보 수정 성공"); + + private final int status; + private final String code; + private final String message; + + ProductsBusinessCode(int status, String code, String message) { + this.status = status; + this.code = code; + this.message = message; + } + + @Override + public int getStatus() { + return status; + } + + @Override + public String getCode() { + return code; + } + + @Override + public String getMessage() { + return message; + } +} diff --git a/module-api/src/main/java/com/kernel360/main/code/ProductsErrorCode.java b/module-admin/src/main/java/com/kernel360/product/code/ProductsErrorCode.java similarity index 96% rename from module-api/src/main/java/com/kernel360/main/code/ProductsErrorCode.java rename to module-admin/src/main/java/com/kernel360/product/code/ProductsErrorCode.java index aefde420..85b01e05 100644 --- a/module-api/src/main/java/com/kernel360/main/code/ProductsErrorCode.java +++ b/module-admin/src/main/java/com/kernel360/product/code/ProductsErrorCode.java @@ -1,4 +1,4 @@ -package com.kernel360.main.code; +package com.kernel360.product.code; import com.kernel360.code.ErrorCode; import org.springframework.http.HttpStatus; diff --git a/module-admin/src/main/java/com/kernel360/product/controller/ProductController.java b/module-admin/src/main/java/com/kernel360/product/controller/ProductController.java new file mode 100644 index 00000000..c4803fc2 --- /dev/null +++ b/module-admin/src/main/java/com/kernel360/product/controller/ProductController.java @@ -0,0 +1,58 @@ +package com.kernel360.product.controller; + +import com.kernel360.product.code.ProductsBusinessCode; +import com.kernel360.product.dto.ProductDetailDto; +import com.kernel360.product.dto.ProductDto; +import com.kernel360.product.dto.ProductUpdateRequest; +import com.kernel360.product.service.ProductService; +import com.kernel360.response.ApiResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + + +@RestController +@RequiredArgsConstructor +@RequestMapping("/admin") +public class ProductController { + + private final ProductService productService; + + @GetMapping("/products") + ResponseEntity>> findProductList(){ + final List products = productService.getProducts(); + + return ApiResponse.toResponseEntity(ProductsBusinessCode.GET_PRODUCT_DATA_SUCCESS, products); + } + @GetMapping("/products/search") + ResponseEntity>> findProductByKeyword(@RequestParam("keyword") String keyword, Pageable pageable){ + final Page list = productService.getProductsByKeyword(keyword, pageable); + + return ApiResponse.toResponseEntity(ProductsBusinessCode.GET_PRODUCT_DATA_SUCCESS, list); + } + @GetMapping("/product/{productNo}") + ResponseEntity> findProductById(@PathVariable("productNo") Long productNo) { + ProductDetailDto findProductDetailDto = productService.getProductById(productNo); + productService.updateViewCount(findProductDetailDto.productNo()); + + return ApiResponse.toResponseEntity(ProductsBusinessCode.GET_PRODUCT_DATA_SUCCESS, findProductDetailDto); + } + + @GetMapping("/products/{OCR_No}") + ResponseEntity>> findProductByOCR(@PathVariable("OCR_No") String reportNo, Pageable pageable) { + + return ApiResponse.toResponseEntity(ProductsBusinessCode.GET_PRODUCT_DATA_SUCCESS, productService.getProductByOCR(reportNo, pageable)); + } + + @PatchMapping("/product") + ResponseEntity> updateProduct(@RequestBody ProductUpdateRequest productUpdateRequest){ + productService.updateProduct(productUpdateRequest); + + return ApiResponse.toResponseEntity(ProductsBusinessCode.UPDATE_PRODUCT_DATA_SUCCESS); + } + +} diff --git a/module-admin/src/main/java/com/kernel360/product/dto/ProductDetailDto.java b/module-admin/src/main/java/com/kernel360/product/dto/ProductDetailDto.java new file mode 100644 index 00000000..1a2c1bd2 --- /dev/null +++ b/module-admin/src/main/java/com/kernel360/product/dto/ProductDetailDto.java @@ -0,0 +1,152 @@ +package com.kernel360.product.dto; + +import com.kernel360.product.entity.Product; + +import java.time.LocalDate; + +/** + * DTO for {@link com.kernel360.product.entity.Product} + */ +public record ProductDetailDto( + Long productNo, + String productName, + String barcode, + String imageSource, + String reportNumber, + String safetyStatus, + Integer viewCount, + String companyName, + String productType, + LocalDate issuedDate, + String safetyInspectionStandard, + String upperItem, + String item, + String propose, + String weight, + String usage, + String usagePrecaution, + String firstAid, + String mainSubstance, + String allergicSubstance, + String otherSubstance, + String preservative, + String surfactant, + String fluorescentWhitening, + String manufactureType, + String manufactureMethod, + String manufactureNation, + String violationInfo, + LocalDate createdAt, + String createdBy, + LocalDate modifiedAt, + String modifiedBy + //TODO 브랜드 엔티티 +) { + + public static ProductDetailDto of( + Long productNo, + String productName, + String barcode, + String imageSource, + String reportNumber, + String safetyStatus, + Integer viewCount, + String companyName, + String productType, + LocalDate issuedDate, + String safetyInspectionStandard, + String upperItem, + String item, + String propose, + String weight, + String usage, + String usagePrecaution, + String firstAid, + String mainSubstance, + String allergicSubstance, + String otherSubstance, + String preservative, + String surfactant, + String fluorescentWhitening, + String manufactureType, + String manufactureMethod, + String manufactureNation, + String violationInfo, + LocalDate createdAt, + String createdBy, + LocalDate modifiedAt, + String modifiedBy + ) { + return new ProductDetailDto( + productNo, + productName, + barcode, + imageSource, + reportNumber, + safetyStatus, + viewCount, + companyName, + productType, + issuedDate, + safetyInspectionStandard, + upperItem, + item, + propose, + weight, + usage, + usagePrecaution, + firstAid, + mainSubstance, + allergicSubstance, + otherSubstance, + preservative, + surfactant, + fluorescentWhitening, + manufactureType, + manufactureMethod, + manufactureNation, + violationInfo, + createdAt, + createdBy, + modifiedAt, + modifiedBy + ); + } + + public static ProductDetailDto from(Product entity) { + return ProductDetailDto.of( + entity.getProductNo(), + entity.getProductName(), + entity.getBarcode(), + entity.getImageSource(), + entity.getReportNumber(), + entity.getSafetyStatus().name(), + entity.getViewCount(), + entity.getCompanyName(), + entity.getProductType(), + entity.getIssuedDate(), + entity.getSafetyInspectionStandard(), + entity.getUpperItem(), + entity.getItem(), + entity.getPropose(), + entity.getWeight(), + entity.getUsage(), + entity.getUsagePrecaution(), + entity.getFirstAid(), + entity.getMainSubstance(), + entity.getAllergicSubstance(), + entity.getOtherSubstance(), + entity.getPreservative(), + entity.getSurfactant(), + entity.getFluorescentWhitening(), + entity.getManufactureType(), + entity.getManufactureMethod(), + entity.getManufactureNation(), + entity.getViolationInfo(), + entity.getCreatedAt(), + entity.getCreatedBy(), + entity.getModifiedAt(), + entity.getModifiedBy() + ); + } +} \ No newline at end of file diff --git a/module-admin/src/main/java/com/kernel360/product/dto/ProductDto.java b/module-admin/src/main/java/com/kernel360/product/dto/ProductDto.java new file mode 100644 index 00000000..3ae97ccc --- /dev/null +++ b/module-admin/src/main/java/com/kernel360/product/dto/ProductDto.java @@ -0,0 +1,83 @@ +package com.kernel360.product.dto; + +import com.kernel360.product.entity.Product; +import com.kernel360.product.entity.SafetyStatus; + +import java.time.LocalDate; + +/** + * DTO for {@link com.kernel360.product.entity.Product} + */ +public record ProductDto( + Long productNo, + String productName, + String barcode, + String imageSource, + String reportNumber, + SafetyStatus safetyStatus, + Integer viewCount, + String brand, + String upperItem, + LocalDate createdAt, + String createdBy, + LocalDate modifiedAt, + String modifiedBy +) { + + public static ProductDto of( + Long productNo, + String productName, + String barcode, + String imageSource, + String reportNumber, + SafetyStatus safetyStatus, + String brand, + String upperItem, + Integer viewCount, + LocalDate createdAt, + String createdBy, + LocalDate modifiedAt, + String modifiedBy + ) { + return new ProductDto( + productNo, + productName, + barcode, + imageSource, + reportNumber, + safetyStatus, + viewCount, + brand, + upperItem, + createdAt, + createdBy, + modifiedAt, + modifiedBy + ); + } + + public static ProductDto from(Product entity) { + return ProductDto.of( + entity.getProductNo(), + entity.getProductName(), + entity.getBarcode(), + entity.getImageSource(), + entity.getReportNumber(), + entity.getSafetyStatus(), + entity.getCompanyName(), + entity.getUpperItem(), + entity.getViewCount(), + entity.getCreatedAt(), + entity.getCreatedBy(), + entity.getModifiedAt(), + entity.getModifiedBy() + ); + } + + public Product toEntity() { + return Product.of( + productNo, + productName + ); + } +} diff --git a/module-admin/src/main/java/com/kernel360/product/dto/ProductUpdateRequest.java b/module-admin/src/main/java/com/kernel360/product/dto/ProductUpdateRequest.java new file mode 100644 index 00000000..63968aef --- /dev/null +++ b/module-admin/src/main/java/com/kernel360/product/dto/ProductUpdateRequest.java @@ -0,0 +1,40 @@ +package com.kernel360.product.dto; + + +import lombok.Builder; + +import java.time.LocalDate; + +public record ProductUpdateRequest( + Long productNo, + String productName, + String reportNumber, + String productType, + String companyName, + String manufactureNation, + String safetyStatus, + LocalDate issuedDate, + String barcode, + String imageSource, + Integer viewCount, + String safetyInspectionStandard, + String upperItem, + String item, + String propose, + String weight, + String usage, + String usagePrecaution, + String firstAid, + String mainSubstance, + String allergicSubstance, + String otherSubstance, + String preservative, + String surfactant, + String fluorescentWhitening, + String manufactureType, + String manufactureMethod, + String violationInfo +) { + +} + diff --git a/module-admin/src/main/java/com/kernel360/product/dto/RecommendProductsDto.java b/module-admin/src/main/java/com/kernel360/product/dto/RecommendProductsDto.java new file mode 100644 index 00000000..a45b4444 --- /dev/null +++ b/module-admin/src/main/java/com/kernel360/product/dto/RecommendProductsDto.java @@ -0,0 +1,39 @@ +package com.kernel360.product.dto; + + +import com.kernel360.product.entity.Product; + +public record RecommendProductsDto( + Long id, + String imageSource, + String alt, + String productName +) { + public static RecommendProductsDto of( + Long id, + String imageSource, + String alt, + String productName + ) { + return new RecommendProductsDto( + id, + imageSource, + alt, + productName + ); + } + + public static RecommendProductsDto from(Product entity) { + + return new RecommendProductsDto( + entity.getProductNo(), + "src/main/resources/static/suggestSample.png", +// FixMe:: entity.getImage() 같은걸로 변경해야 함 + "제품 이미지", + entity.getProductName()); + } + +} + + + diff --git a/module-admin/src/main/java/com/kernel360/product/service/ProductService.java b/module-admin/src/main/java/com/kernel360/product/service/ProductService.java new file mode 100644 index 00000000..5b04118d --- /dev/null +++ b/module-admin/src/main/java/com/kernel360/product/service/ProductService.java @@ -0,0 +1,141 @@ +package com.kernel360.product.service; + +import com.kernel360.exception.BusinessException; +import com.kernel360.likes.repository.LikeRepository; +import com.kernel360.product.code.ProductsErrorCode; +import com.kernel360.product.dto.ProductDetailDto; +import com.kernel360.product.dto.ProductDto; +import com.kernel360.product.dto.ProductUpdateRequest; +import com.kernel360.product.dto.RecommendProductsDto; +import com.kernel360.product.entity.Product; +import com.kernel360.product.entity.SafetyStatus; +import com.kernel360.product.repository.ProductRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class ProductService { + + private final ProductRepository productRepository; + private final LikeRepository likeRepository; + + @Transactional(readOnly = true) + public List getProducts() { + + return productRepository.findAll() + .stream() + .map(ProductDto::from) + .toList(); + } + + @Transactional(readOnly = true) + public Page getProductsByKeyword(String keyword, Pageable pageable) { + Page products = productRepository.findByProductNameContaining(keyword, pageable); + + return products.map(ProductDto::from); + } + + @Transactional(readOnly = true) + public Page getProductListOrderByViewCount(Pageable pageable) { + + return productRepository.findAllByOrderByViewCountDesc(pageable).map(ProductDto::from); + } + + @Transactional(readOnly = true) + public Page getRecommendProducts(Pageable pageable) { + Page productList = productRepository.findTop5ByOrderByProductNameDesc(pageable); + + return productList.map(RecommendProductsDto::from); + } + + @Transactional(readOnly = true) + public Page getViolationProducts(Pageable pageable) { + + return productRepository.findAllBySafetyStatusEquals(SafetyStatus.DANGER, pageable) + .map(ProductDto::from); + } + + @Transactional(readOnly = true) + public Page getRecentProducts(Pageable pageable) { + + return productRepository.findAllByOrderByCreatedAtDesc(pageable) + .map(ProductDto::from); + + } + + @Transactional(readOnly = true) + public ProductDetailDto getProductById(Long id) { + + return productRepository.findById(id) + .map(ProductDetailDto::from) + .orElseThrow(() -> new BusinessException(ProductsErrorCode.NOT_FOUND_PRODUCT)); + } + + @Transactional + public void updateViewCount(Long id) { + productRepository.updateViewCount(id); + } + + @Transactional(readOnly = true) + public Page getProductByOCR(String reportNo, Pageable pageable) { + + return productRepository.findProductByReportNumberEquals(reportNo, pageable) + .map(ProductDetailDto::from); + } + + @Transactional(readOnly = true) + public Page getFavoriteProducts(Pageable pageable) { + Page results = likeRepository.findTop20ByProductNoOrderByLikeCountDesc(pageable); + + List productDtos = results.getContent().stream() + .map(result -> { + Long productNo = (Long) result[0]; + Product product = productRepository.findById(productNo) + .orElseThrow(() -> new BusinessException(ProductsErrorCode.NOT_FOUND_PRODUCT)); + return ProductDto.from(product); + }) + .toList(); + + return new PageImpl<>(productDtos, pageable, results.getTotalElements()); + } + + @Transactional + public void updateProduct(ProductUpdateRequest productUpdateRequest) { + Product product = productRepository.findById(productUpdateRequest.productNo()) + .orElseThrow(() -> new BusinessException(ProductsErrorCode.NOT_FOUND_PRODUCT)); + + product.updateDetail( + productUpdateRequest.barcode(), + productUpdateRequest.imageSource(), + productUpdateRequest.reportNumber(), + productUpdateRequest.safetyStatus(), + productUpdateRequest.issuedDate(), + productUpdateRequest.safetyInspectionStandard(), + productUpdateRequest.upperItem(), + productUpdateRequest.item(), + productUpdateRequest.propose(), + productUpdateRequest.weight(), + productUpdateRequest.usage(), + productUpdateRequest.usagePrecaution(), + productUpdateRequest.firstAid(), + productUpdateRequest.mainSubstance(), + productUpdateRequest.allergicSubstance(), + productUpdateRequest.otherSubstance(), + productUpdateRequest.preservative(), + productUpdateRequest.surfactant(), + productUpdateRequest.fluorescentWhitening(), + productUpdateRequest.manufactureType(), + productUpdateRequest.manufactureMethod(), + productUpdateRequest.manufactureNation(), + productUpdateRequest.violationInfo() + ); + + } +} diff --git a/module-admin/src/main/resources/application-local.yml b/module-admin/src/main/resources/application-local.yml new file mode 100644 index 00000000..21df0c8c --- /dev/null +++ b/module-admin/src/main/resources/application-local.yml @@ -0,0 +1,46 @@ +spring: + datasource: + driver-class-name: org.postgresql.Driver + # local docker db + url: ENC(DPvTlKCZniOLZz5QHwB2I1p6FbeyEhWeo6DXSHL7kq8idfthgqkC/VJX78osBC5D/hzFwdXGDkxWUZ2euYPrAA==) + username: ENC(izKjVNL/2x4aTDQeLxQg4VU0v5kAKduTrnVq7LUa80U=) + password: ENC(VcPYkEOg9OzuwNw1UeYOgLqKsRkFoRUHwjRcxZzGgZQ=) + jpa: + show-sql: true + properties: + hibernate: + format_sql: true + dialect: org.hibernate.dialect.PostgreSQLDialect + ddl-auto: validate + +jasypt: + encryptor: + algorithm: PBEWithMD5AndTripleDES + +logging: + file: + path: ./logs/admin/ + name: local-admin-log + logback: + rollingpolicy: + max-history: 14 + max-file-size: 50MB + total-size-cap: 5GB + +management: + endpoints: + web: + exposure: + include: "*" + endpoint: + health: + show-details: always + +aws: + credentials: + access-key-id: ENC(vx2H0E5tIohXqzynDez9M7Kn3Vjj0t3sS3Wq6uyN5kgYeXhDyIylOA==) + secret-access-key: ENC(m/2qCJve4UP86W7Q0ZQhKSDGBnltnbGOnso7d/9wiNovq1Mzyto608rUnCQ+AhqUThpClK1cmxSOj6WHfAbZTQ==) + s3: + bucket: + name: ENC(JQIi11b8LB+99FnX02wCGwdXTOEax3VkuzgNqAVshK4=) + url: ENC(9P2gRaZoGkR4SCgoTS/6sEQP0kVWwVWFaDckr1/FUoRV1MPnGXQL6OJKsGlHegk8h1d69uFDKTuZpLntfyn3nVMXLz18t8ls) \ No newline at end of file diff --git a/module-admin/src/main/resources/application.yml b/module-admin/src/main/resources/application.yml new file mode 100644 index 00000000..5dae5ae8 --- /dev/null +++ b/module-admin/src/main/resources/application.yml @@ -0,0 +1,11 @@ +spring: + profiles: + active: local + servlet: + multipart: + max-file-size: 50MB + max-request-size: 500MB + +server: + port: 8082 + diff --git a/module-admin/src/main/resources/logback-spring.xml b/module-admin/src/main/resources/logback-spring.xml new file mode 100644 index 00000000..663d2464 --- /dev/null +++ b/module-admin/src/main/resources/logback-spring.xml @@ -0,0 +1,40 @@ + + + + + + + + + + ${CONSOLE_LOG_PATTERN} + + + + + + ${FILE_LOG_PATTERN} + + + + ${LOG_PATH}/${LOG_FILE}.%d{yyyy-MM-dd}-%i.log + ${LOGBACK_ROLLINGPOLICY_MAX_HISTORY} + ${LOGBACK_ROLLINGPOLICY_MAX_FILE_SIZE} + ${LOGBACK_ROLLINGPOLICY_TOTAL_SIZE_CAP} + + + + + + + + + + + + + + \ No newline at end of file diff --git a/module-admin/src/test/java/com/kernel360/ModuleAdminApplicationTests.java b/module-admin/src/test/java/com/kernel360/ModuleAdminApplicationTests.java new file mode 100644 index 00000000..c9c52f46 --- /dev/null +++ b/module-admin/src/test/java/com/kernel360/ModuleAdminApplicationTests.java @@ -0,0 +1,13 @@ +package com.kernel360; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ModuleAdminApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/module-api/Dockerfile b/module-api/Dockerfile index 54d1f410..d0739319 100644 --- a/module-api/Dockerfile +++ b/module-api/Dockerfile @@ -1,5 +1,11 @@ FROM openjdk:17-jdk -CMD ["./gradlew", "clean", "build"] + +WORKDIR /app + ARG JAR_FILE=./build/libs/module-api-0.0.1-SNAPSHOT.jar -COPY ${JAR_FILE} washpedia.jar -ENTRYPOINT ["java", "-jar", "washpedia.jar"] \ No newline at end of file + +COPY ${JAR_FILE} washpedia-api.jar + +EXPOSE 8080 + +ENTRYPOINT ["java", "-jar", "washpedia-api.jar"] \ No newline at end of file diff --git a/module-api/build.gradle b/module-api/build.gradle index eda87d95..52194452 100644 --- a/module-api/build.gradle +++ b/module-api/build.gradle @@ -36,6 +36,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-aop' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' //implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-mail' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' // jasypt implementation 'com.github.ulisesbocchio:jasypt-spring-boot-starter:3.0.5' @@ -67,6 +69,15 @@ dependencies { // rest docs testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor' + + //actuator + implementation 'org.springframework.boot:spring-boot-starter-actuator' + + //prometheus + implementation 'io.micrometer:micrometer-registry-prometheus' + + //mybatis + implementation group: 'org.mybatis.spring.boot', name: 'mybatis-spring-boot-starter', version: '3.0.3' } tasks.named('test') { @@ -75,24 +86,17 @@ tasks.named('test') { } asciidoctor { - dependsOn test - configurations 'asciidoctorExt' baseDirFollowsSourceFile() inputs.dir snippetsDir -} - -asciidoctor.doFirst { - delete file('src/main/resources/static/docs') -} - -task copyDocument(type: Copy) { - dependsOn asciidoctor - from file("build/docs/asciidoc") - into file("src/main/resources/static/docs") + configurations 'asciidoctorExt' + dependsOn test } bootJar { - dependsOn copyDocument + dependsOn asciidoctor + from ("${asciidoctor.outputDir}") { + into 'static/docs' + } } tasks.register("prepareKotlinBuildScriptModel") {} \ No newline at end of file diff --git a/module-api/src/docs/asciidoc/auth-api.adoc b/module-api/src/docs/asciidoc/auth-api.adoc new file mode 100644 index 00000000..44ab7eec --- /dev/null +++ b/module-api/src/docs/asciidoc/auth-api.adoc @@ -0,0 +1,14 @@ +== Auth API + +// [[]] 안에는 a 태그 이름 들어갑니다 (http://localhost:8080/docs/index#공통코드-조회) +[[Auth-JWT재발급]] +=== + +===== HTTP Request +include::{snippets}/auth/reissuanceJWT/http-request.adoc[] + +==== Response +include::{snippets}/auth/reissuanceJWT/response-fields.adoc[] + +===== HTTP Response 예시 +include::{snippets}/auth/reissuanceJWT/http-response.adoc[] \ No newline at end of file diff --git a/module-api/src/docs/asciidoc/index.adoc b/module-api/src/docs/asciidoc/index.adoc index c744ccd4..dea910ea 100644 --- a/module-api/src/docs/asciidoc/index.adoc +++ b/module-api/src/docs/asciidoc/index.adoc @@ -7,6 +7,8 @@ :sectlinks: include::overview.adoc[] -// include::sample-api.adoc[] + include::main-api.adoc[] +include::product-api.adoc[] + diff --git a/module-api/src/docs/asciidoc/member-find-credential-api.adoc b/module-api/src/docs/asciidoc/member-find-credential-api.adoc new file mode 100644 index 00000000..e34554f6 --- /dev/null +++ b/module-api/src/docs/asciidoc/member-find-credential-api.adoc @@ -0,0 +1,60 @@ +== 아이디/비밀번호 찾기 API + +[[아이디-찾기]] +=== 아이디 찾기 이메일 발송 + +===== HTTP Request +include::{snippets}/member/find-memberId/request-fields.adoc[] + +===== HTTP Request 예시 +include::{snippets}/member/find-memberId/http-request.adoc[] + +==== Response + +include::{snippets}/member/find-memberId/response-fields.adoc[] + +===== HTTP Response 예시 + +include::{snippets}/member/find-memberId/http-response.adoc[] + +[[비밀번호-찾기]] +=== 비밀번호 재설정 이메일 발송 + +===== HTTP Request +include::{snippets}/member/find-password/request-fields.adoc[] + +===== HTTP Request 예시 +include::{snippets}/member/find-password/http-request.adoc[] + +==== Response +include::{snippets}/member/find-password/response-fields.adoc[] + +===== HTTP Response 예시 +include::{snippets}/member/find-password/http-response.adoc[] + +=== 비밀번호 재설정 페이지 요청 +===== HTTP Request +include::{snippets}/member/get-reset-password/query-parameters.adoc[] + +===== HTTP Request 예시 +include::{snippets}/member/get-reset-password/http-request.adoc[] + +==== Response +include::{snippets}/member/get-reset-password/response-fields.adoc[] + +===== HTTP Response 예시 +include::{snippets}/member/get-reset-password/http-response.adoc[] + + +=== 비밀번호 재설정 요청 +===== HTTP Request +include::{snippets}/member/post-reset-password/http-request.adoc[] + +===== HTTP Request 예시 +include::{snippets}/member/post-reset-password/http-request.adoc[] + +==== Response +include::{snippets}/member/post-reset-password/response-fields.adoc[] + +===== HTTP Response 예시 +include::{snippets}/member/post-reset-password/http-response.adoc[] diff --git a/module-api/src/docs/asciidoc/mypage-api.adoc b/module-api/src/docs/asciidoc/mypage-api.adoc new file mode 100644 index 00000000..7456290c --- /dev/null +++ b/module-api/src/docs/asciidoc/mypage-api.adoc @@ -0,0 +1,65 @@ +== MyPage API + +[[회원정보-조회]] +=== Member 조회 + +===== HTTP Request +include::{snippets}/mypage-member/get-member-info/http-request.adoc[] + +==== Response +include::{snippets}/mypage-member/get-member-info/response-fields.adoc[] + +===== HTTP Response 예시 +include::{snippets}/mypage-member/get-member-info/http-response.adoc[] + + +[[차량정보-조회]] +=== CarInfo 조회 + +===== HTTP Request +include::{snippets}/mypage-car/get-car-info/http-request.adoc[] + +==== Response +include::{snippets}/mypage-car/get-car-info/response-fields.adoc[] + +===== HTTP Response 예시 +include::{snippets}/mypage-car/get-car-info/http-response.adoc[] + + +[[회원탈퇴-요청]] +=== Member Delete 요청 + +===== HTTP Request +include::{snippets}/mypage-member/delete-member/http-request.adoc[] + +===== Response +include::{snippets}/mypage-member/delete-member/response-fields.adoc[] + +===== HTTP Response 예시 +include::{snippets}/mypage-member/delete-member/http-response.adoc[] + + +[[비밀번호검증-요청]] +=== Password Validate 요청 + +===== HTTP Request +include::{snippets}/mypage-member/validate-password/http-request.adoc[] + +===== Response +include::{snippets}/mypage-member/validate-password/response-fields.adoc[] + +===== HTTP Response 예시 +include::{snippets}/mypage-member/validate-password/http-response.adoc[] + + +[[회원정보수정-요청]] +=== Update Member 요청 + +===== HTTP Request +include::{snippets}/mypage-member/update-member/http-request.adoc[] + +===== Response-Body +include::{snippets}/mypage-member/update-member/response-body.adoc[] + +===== HTTP Response 예시 +include::{snippets}/mypage-member/update-member/http-response.adoc[] \ No newline at end of file diff --git a/module-api/src/docs/asciidoc/overview.adoc b/module-api/src/docs/asciidoc/overview.adoc index 268ce84d..fc5b696b 100644 --- a/module-api/src/docs/asciidoc/overview.adoc +++ b/module-api/src/docs/asciidoc/overview.adoc @@ -8,7 +8,7 @@ |=== | 환경 | Domain | 개발 서버 -| `http://washpedia.my-project.life` +| `https://dev.washfit.site` | 운영 서버 -| +| `https://www.washfit.site` |=== \ No newline at end of file diff --git a/module-api/src/docs/asciidoc/product-api.adoc b/module-api/src/docs/asciidoc/product-api.adoc new file mode 100644 index 00000000..58ebae7d --- /dev/null +++ b/module-api/src/docs/asciidoc/product-api.adoc @@ -0,0 +1,39 @@ +== Product API + +// [[]] 안에는 a 태그 이름 들어갑니다 (http://localhost:8080/docs/index#공통코드-조회) +[[Product-조회]] +=== <제품 목록 조회> + +===== HTTP Request +include::{snippets}/products/get-products/http-request.adoc[] + +==== Response +include::{snippets}/products/get-products/response-fields.adoc[] + +===== HTTP Response 예시 +include::{snippets}/products/get-products/http-response.adoc[] + + +=== <제품 아이디로 제품 목록 조회> + +===== HTTP Request +include::{snippets}/product-id/get-product-id/http-request.adoc[] + +==== Response +include::{snippets}/product-id/get-product-id/response-fields.adoc[] + +===== HTTP Response 예시 +include::{snippets}/product-id/get-product-id//http-response.adoc[] + + +=== <제품 키워드로 제품 목록 조회> + +===== HTTP Request +include::{snippets}/products-search/get-products-by-search/http-request.adoc[] +include::{snippets}/products-search/get-products-by-search/query-parameters.adoc[] + +==== Response +include::{snippets}/products-search/get-products-by-search/response-fields.adoc[] + +===== HTTP Response 예시 +include::{snippets}/products-search/get-products-by-search/http-response.adoc[] diff --git a/module-api/src/main/java/com/kernel360/auth/code/AuthBusinessCode.java b/module-api/src/main/java/com/kernel360/auth/code/AuthBusinessCode.java new file mode 100644 index 00000000..92e20c39 --- /dev/null +++ b/module-api/src/main/java/com/kernel360/auth/code/AuthBusinessCode.java @@ -0,0 +1,30 @@ +package com.kernel360.auth.code; + +import com.kernel360.code.BusinessCode; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@RequiredArgsConstructor +public enum AuthBusinessCode implements BusinessCode { + + SUCCESS_REQUEST_REGENERATED_JWT(HttpStatus.CREATED.value(), "BAC001", "JWT 토큰 재발급 성공"); + + private final int status; + private final String code; + private final String message; + + @Override + public int getStatus() { + return status; + } + + @Override + public String getCode() { + return code; + } + + @Override + public String getMessage() { + return message; + } +} diff --git a/module-api/src/main/java/com/kernel360/auth/code/AuthErrorCode.java b/module-api/src/main/java/com/kernel360/auth/code/AuthErrorCode.java new file mode 100644 index 00000000..465b42c3 --- /dev/null +++ b/module-api/src/main/java/com/kernel360/auth/code/AuthErrorCode.java @@ -0,0 +1,30 @@ +package com.kernel360.auth.code; + +import com.kernel360.code.ErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@RequiredArgsConstructor +public enum AuthErrorCode implements ErrorCode { + + FAILED_GENERATED_JWT(HttpStatus.INTERNAL_SERVER_ERROR.value(), "EAC001", "재발급충족미달"); + + private final int status; + private final String code; + private final String message; + + @Override + public int getStatus() { + return status; + } + + @Override + public String getCode() { + return code; + } + + @Override + public String getMessage() { + return message; + } +} diff --git a/module-api/src/main/java/com/kernel360/auth/controller/AuthController.java b/module-api/src/main/java/com/kernel360/auth/controller/AuthController.java new file mode 100644 index 00000000..e185c251 --- /dev/null +++ b/module-api/src/main/java/com/kernel360/auth/controller/AuthController.java @@ -0,0 +1,30 @@ +package com.kernel360.auth.controller; + +import com.kernel360.auth.dto.AuthDto; +import com.kernel360.auth.service.AuthService; +import com.kernel360.response.ApiResponse; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import static com.kernel360.auth.code.AuthBusinessCode.SUCCESS_REQUEST_REGENERATED_JWT; +import static com.kernel360.auth.service.AuthService.getClientIP; + +@Slf4j +@RestController +@RequestMapping("/auth") +@RequiredArgsConstructor +public class AuthController { + + private final AuthService authService; + + @GetMapping("/reissuanceJWT") + public ResponseEntity> reissuanceJWT(HttpServletRequest request){ + log.info("Current Client IP :: "+getClientIP(request)); + return ApiResponse.toResponseEntity(SUCCESS_REQUEST_REGENERATED_JWT, AuthDto.of(authService.generateTokenAndSaveAuth(request))); + } +} diff --git a/module-api/src/main/java/com/kernel360/auth/dto/AuthDto.java b/module-api/src/main/java/com/kernel360/auth/dto/AuthDto.java new file mode 100644 index 00000000..cbdae7e3 --- /dev/null +++ b/module-api/src/main/java/com/kernel360/auth/dto/AuthDto.java @@ -0,0 +1,13 @@ +package com.kernel360.auth.dto; + +public record AuthDto( + String jwtToken +) { + public static AuthDto of( + String jwtToken + ){ + return new AuthDto( + jwtToken + ); + } +} diff --git a/module-api/src/main/java/com/kernel360/auth/service/AuthService.java b/module-api/src/main/java/com/kernel360/auth/service/AuthService.java new file mode 100644 index 00000000..f16f52aa --- /dev/null +++ b/module-api/src/main/java/com/kernel360/auth/service/AuthService.java @@ -0,0 +1,125 @@ +package com.kernel360.auth.service; + +import com.kernel360.auth.entity.Auth; +import com.kernel360.auth.repository.AuthRepository; +import com.kernel360.utils.ConvertSHA256; +import com.kernel360.utils.JWT; +import jakarta.servlet.http.HttpServletRequest; +import java.util.Objects; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +@Slf4j +@Service +@RequiredArgsConstructor +public class AuthService { + + private final JWT jwt; + private final AuthRepository authRepository; + + public Auth findOneByMemberNo(Long memberNo) { + + return authRepository.findOneByMemberNo(memberNo); + } + + public Auth findOneByJwt(String encryptToken) { + + return authRepository.findOneByJwtToken(encryptToken); + } + + public boolean validRequestToken(String requestToken) { + boolean result = jwt.validateToken(requestToken); + + if(Objects.isNull(findOneByJwt(ConvertSHA256.convertToSHA256(requestToken)))) { result = false; } + + return result; + } + + public String generateTokenAndSaveAuth(HttpServletRequest request) { + String newToken = jwt.generateToken(JWT.ownerId(request.getHeader("Authorization"))); + Auth storedAuth = authRepository.findOneByJwtToken(ConvertSHA256.convertToSHA256(request.getHeader("Authorization"))); + String clientIP = getClientIP(request); + + modifyAuthJwt(storedAuth, ConvertSHA256.convertToSHA256(newToken), clientIP); + + authRepository.save(storedAuth); + + return newToken; + } + + // TODO refactor :: auth 의 pk로 인해 조회 후 update - insert 를 결정하게 되어 줄일 필요가 있음. memberNo만으로 키를 잡는다면, flyway 또 수정해야함... 혹은 인증은 memorydb를 쓰는 방법 고려 + public void saveAuthByMember(Long memberNo, String encryptToken, HttpServletRequest request) { + Auth authJwt = findOneByMemberNo(memberNo); + String clientIP = getClientIP(request); + + //결과 없으면 entity 로 신규 생성 + authJwt = Optional.ofNullable(authJwt) + .map(modifyAuth -> modifyAuthJwt(modifyAuth, encryptToken, clientIP)) + .orElseGet(() -> createAuthJwt(memberNo, encryptToken, clientIP)); + + authRepository.save(authJwt); + } + + public Auth createAuthJwt(Long memberNo, String encryptToken, String clientIP) { + + return Auth.of(null, memberNo, encryptToken, null, clientIP); + } + + public Auth modifyAuthJwt(Auth modifyAuth, String encryptToken, String clientIP) { + modifyAuth.updateJwt(encryptToken, clientIP); + + return modifyAuth; + } + + /** + * @param request HttpServletRequest + * @return 로그인한 유저이고, 로그인한 IP 와 같으면 참을 반환, 그 이외에는 거짓을 반환 + */ + public boolean verifyClientIP(HttpServletRequest request) { + String clientIP = getClientIP(request); + if (!StringUtils.hasLength(request.getHeader("Authorization"))) { // 인증 토큰이 없는 경우(로그인 하지 않은 유저의 경우) + return true; + } + + // 인증 토큰이 있고 ip 주소가 같은 경우 true + if (StringUtils.hasLength(request.getHeader("Authorization"))) { + Auth auth = authRepository.findOneByJwtToken( + ConvertSHA256.convertToSHA256(request.getHeader("Authorization"))); + return auth.getClientIP().equals(clientIP); + } + // 그 이외 false + return false; + } + + public static String getClientIP(HttpServletRequest request) { + String ip = request.getHeader("X-Forwarded-For"); + log.info("> X-FORWARDED-FOR : " + ip); + + if (!StringUtils.hasLength(ip) || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + log.info("> Proxy-Client-IP : " + ip); + } + if (!StringUtils.hasLength(ip) || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + log.info("> WL-Proxy-Client-IP : " + ip); + } + if (!StringUtils.hasLength(ip) || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("HTTP_CLIENT_IP"); + log.info("> HTTP_CLIENT_IP : " + ip); + } + if (!StringUtils.hasLength(ip) || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("HTTP_X_FORWARDED_FOR"); + log.info("> HTTP_X_FORWARDED_FOR : " + ip); + } + if (!StringUtils.hasLength(ip) || "unknown".equalsIgnoreCase(ip)) { + ip = request.getRemoteAddr(); + log.info("> getRemoteAddr : " + ip); + } + log.info("> Result : IP Address : " + ip); + + return ip; + } +} diff --git a/module-api/src/main/java/com/kernel360/commoncode/controller/CommonCodeController.java b/module-api/src/main/java/com/kernel360/commoncode/controller/CommonCodeController.java index 0f91dc38..f4108a61 100644 --- a/module-api/src/main/java/com/kernel360/commoncode/controller/CommonCodeController.java +++ b/module-api/src/main/java/com/kernel360/commoncode/controller/CommonCodeController.java @@ -1,9 +1,9 @@ package com.kernel360.commoncode.controller; -import com.kernel360.commoncode.service.CommonCodeService; import com.kernel360.commoncode.dto.CommonCodeDto; +import com.kernel360.commoncode.service.CommonCodeService; import com.kernel360.response.ApiResponse; -import org.springframework.beans.factory.annotation.Autowired; +import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -15,25 +15,13 @@ import static com.kernel360.commoncode.code.CommonCodeBusinessCode.GET_COMMON_CODE_SUCCESS; @RestController +@RequiredArgsConstructor @RequestMapping("/commoncode") public class CommonCodeController { - final CommonCodeService commonCodeService; - - @Autowired - public CommonCodeController(CommonCodeService commonCodeService) { - this.commonCodeService = commonCodeService; - } - + private final CommonCodeService commonCodeService; @GetMapping("/{codeName}") - public List getCommonCode (@PathVariable String codeName){ - - return commonCodeService.getCodes(codeName); - } - - // FIXME :: 아래 메서드는 추후 삭제 예정입니다 - @GetMapping("/test/{codeName}") - public ResponseEntity>> getCommonCode_1 (@PathVariable String codeName){ + public ResponseEntity>> getCommonCode (@PathVariable String codeName){ List codes = commonCodeService.getCodes(codeName); return ApiResponse.toResponseEntity(GET_COMMON_CODE_SUCCESS, codes); diff --git a/module-api/src/main/java/com/kernel360/commoncode/service/CommonCodeService.java b/module-api/src/main/java/com/kernel360/commoncode/service/CommonCodeService.java index e53949ac..f0f7584e 100644 --- a/module-api/src/main/java/com/kernel360/commoncode/service/CommonCodeService.java +++ b/module-api/src/main/java/com/kernel360/commoncode/service/CommonCodeService.java @@ -1,21 +1,19 @@ package com.kernel360.commoncode.service; import com.kernel360.commoncode.dto.CommonCodeDto; +import com.kernel360.commoncode.mapper.CommonCodeMapper; import com.kernel360.commoncode.repository.CommonCodeRepository; -import org.springframework.beans.factory.annotation.Autowired; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import java.util.List; @Service +@RequiredArgsConstructor public class CommonCodeService { - final CommonCodeRepository commonCodeRepository; - - @Autowired - public CommonCodeService(CommonCodeRepository commonCodeRepository) { - this.commonCodeRepository = commonCodeRepository; - } + private final CommonCodeRepository commonCodeRepository; + private final CommonCodeMapper commonCodeMapper; public List getCodes(String codeName) { @@ -24,4 +22,12 @@ public List getCodes(String codeName) { .map(CommonCodeDto::from) .toList(); } + + public List getCodesMapper(String codeName) { + + return commonCodeMapper.findAllByUpperNameAndIsUsed(codeName,true) + .stream() + .map(CommonCodeDto::from) + .toList(); + } } diff --git a/module-api/src/main/java/com/kernel360/global/Interceptor/AcceptInterceptor.java b/module-api/src/main/java/com/kernel360/global/Interceptor/AcceptInterceptor.java index 358dc91e..df2ede03 100644 --- a/module-api/src/main/java/com/kernel360/global/Interceptor/AcceptInterceptor.java +++ b/module-api/src/main/java/com/kernel360/global/Interceptor/AcceptInterceptor.java @@ -1,76 +1,36 @@ package com.kernel360.global.Interceptor; -import com.kernel360.auth.entity.Auth; +import com.kernel360.auth.service.AuthService; import com.kernel360.exception.BusinessException; import com.kernel360.global.code.AcceptInterceptorErrorCode; -import com.kernel360.member.service.MemberService; -import com.kernel360.utils.ConvertSHA256; -import com.kernel360.utils.JWT; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; import org.springframework.web.servlet.HandlerInterceptor; @Component @RequiredArgsConstructor public class AcceptInterceptor implements HandlerInterceptor { - private final JWT jwt; - private final MemberService memberService; - private static final int CLOSING_PERIOD = 2; + private final AuthService authService; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { boolean result = true; String requestToken = request.getHeader("Authorization"); - if (requestToken == null || requestToken.isEmpty()) { throw new BusinessException(AcceptInterceptorErrorCode.DOSE_NOT_EXIST_REQUEST_TOKEN); } + if (!StringUtils.hasLength(requestToken)) { throw new BusinessException(AcceptInterceptorErrorCode.DOSE_NOT_EXIST_REQUEST_TOKEN); } - if (!validRequestToken(requestToken)) { throw new BusinessException(AcceptInterceptorErrorCode.FAILED_VALID_REQUEST_TOKEN); } + if (!authService.validRequestToken(requestToken)) { throw new BusinessException(AcceptInterceptorErrorCode.FAILED_VALID_REQUEST_TOKEN); } - long validRequestPeriod = validRequestPeriod(requestToken); - - if ( (validRequestPeriod <= CLOSING_PERIOD) ) { - String encryptToken = ConvertSHA256.convertToSHA256(requestToken); - Auth storedAuthInfo = getOneAuthByJwt(encryptToken); - String newToken = reGeneratedToken(requestToken, storedAuthInfo); - response.setHeader("Authorization", newToken); + //** 로그인한 IP와 다른 IP로 JWT 갱신을 시도하는 경우 **// + if (!authService.verifyClientIP(request)) { + throw new BusinessException(AcceptInterceptorErrorCode.FAILED_VERIFY_REQUEST_CLIENT_IP); } return result; } - private Auth getOneAuthByJwt(String encryptToken) { - - Auth result = memberService.findOneAuthByJwt(encryptToken); - - if(result == null) { throw new BusinessException(AcceptInterceptorErrorCode.FAILED_VALID_REQUEST_TOKEN_HASH); } - if(!encryptToken.equals(result.getJwtToken())) { throw new BusinessException(AcceptInterceptorErrorCode.FAILED_VALID_REQUEST_TOKEN_HASH); } - - return result; - } - - /** - * 토큰 자체의 유효성 검사 (서버 시크릿 키) - **/ - private boolean validRequestToken(String requestToken) { return jwt.validateToken(requestToken); } - - /** - * 토큰의 유효시간이 현재시간과 비교하여 2분 이하인지 - **/ - private long validRequestPeriod(String requestToken) { return jwt.checkedTime(requestToken); } - - /** - * 신규토큰 발급 후 저장 - **/ - private String reGeneratedToken(String requestToken, Auth storedAuthInfo) { - String newToken = jwt.generateToken(jwt.ownerId(requestToken)); - String newEncryptToken = ConvertSHA256.convertToSHA256(newToken); - - storedAuthInfo = memberService.modifyAuthJwt(storedAuthInfo, newEncryptToken); - memberService.reissuanceJwt(storedAuthInfo); - - return newToken; - } } diff --git a/module-api/src/main/java/com/kernel360/global/Interceptor/InterceptorConfig.java b/module-api/src/main/java/com/kernel360/global/Interceptor/InterceptorConfig.java index 818980a1..2ba476d2 100644 --- a/module-api/src/main/java/com/kernel360/global/Interceptor/InterceptorConfig.java +++ b/module-api/src/main/java/com/kernel360/global/Interceptor/InterceptorConfig.java @@ -13,7 +13,7 @@ public class InterceptorConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(acceptInterceptor) - .addPathPatterns("/member/testJwt"); + .addPathPatterns("/auth/**"); //** 인증 JWT 토큰 관련 **// // .addPathPatterns("/mypage/**"); //.excludePathPatterns("/public/**"); // 제외할 URL 패턴 } diff --git a/module-api/src/main/java/com/kernel360/global/code/AcceptInterceptorErrorCode.java b/module-api/src/main/java/com/kernel360/global/code/AcceptInterceptorErrorCode.java index 884a2628..0134c603 100644 --- a/module-api/src/main/java/com/kernel360/global/code/AcceptInterceptorErrorCode.java +++ b/module-api/src/main/java/com/kernel360/global/code/AcceptInterceptorErrorCode.java @@ -10,7 +10,8 @@ public enum AcceptInterceptorErrorCode implements ErrorCode { DOSE_NOT_EXIST_REQUEST_TOKEN(HttpStatus.UNAUTHORIZED.value(), "AIEC001", "요청자의 토큰이 존재하지 않음."), FAILED_VALID_REQUEST_TOKEN(HttpStatus.UNAUTHORIZED.value(), "AIEC002", "요청자의 토큰이 유효하지 않음."), FAILED_VALID_REQUEST_TOKEN_PERIOD(HttpStatus.BAD_REQUEST.value(), "AIEC003", "토큰 유효시간 조건이 충족되지 않음."), - FAILED_VALID_REQUEST_TOKEN_HASH(HttpStatus.BAD_REQUEST.value(), "AIEC004", "로그인시 발급한 토큰의 해시값과 불일치함."); + FAILED_VALID_REQUEST_TOKEN_HASH(HttpStatus.BAD_REQUEST.value(), "AIEC004", "로그인시 발급한 토큰의 해시값과 불일치함."), + FAILED_VERIFY_REQUEST_CLIENT_IP(HttpStatus.UNAUTHORIZED.value(), "AIEC005","로그인시 IP 주소와 불일치함"); private final int status; private final String code; diff --git a/module-api/src/main/java/com/kernel360/global/config/RedisConfig.java b/module-api/src/main/java/com/kernel360/global/config/RedisConfig.java new file mode 100644 index 00000000..05b821b9 --- /dev/null +++ b/module-api/src/main/java/com/kernel360/global/config/RedisConfig.java @@ -0,0 +1,41 @@ +package com.kernel360.global.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisConfig { + @Value("${spring.data.redis.host}") + private String host; + @Value("${spring.data.redis.port}") + private String port; + @Value("${spring.data.redis.password}") + private String password; + + @Bean + public RedisConnectionFactory redisConnectionFactory() { + RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(); + redisStandaloneConfiguration.setHostName(host); + redisStandaloneConfiguration.setPort(Integer.parseInt(port)); + redisStandaloneConfiguration.setPassword(password); + + return new LettuceConnectionFactory(redisStandaloneConfiguration); + } + + @Bean + public RedisTemplate redisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate<>(); + + redisTemplate.setConnectionFactory(redisConnectionFactory()); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setDefaultSerializer(new StringRedisSerializer()); + + return redisTemplate; + } +} diff --git a/module-api/src/main/java/com/kernel360/global/config/WebConfig.java b/module-api/src/main/java/com/kernel360/global/config/WebConfig.java new file mode 100644 index 00000000..ee5e13d9 --- /dev/null +++ b/module-api/src/main/java/com/kernel360/global/config/WebConfig.java @@ -0,0 +1,32 @@ +package com.kernel360.global.config; + +import com.kernel360.main.conveter.StringToSortConverter; +import org.springframework.context.annotation.Configuration; +import org.springframework.format.FormatterRegistry; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + + @Override + public void addFormatters(FormatterRegistry registry) { + registry.addConverter(new StringToSortConverter()); + } + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("/favicon.ico").addResourceLocations("classpath:/static/"); + } + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedHeaders("*") + .allowedMethods("*") + .allowCredentials(true) + .allowedOrigins("https://www.washfit.site", "https://dev.washfit.site") + .maxAge(3600); + } +} diff --git a/module-api/src/main/java/com/kernel360/likes/code/LikeBusinessCode.java b/module-api/src/main/java/com/kernel360/likes/code/LikeBusinessCode.java new file mode 100644 index 00000000..8baf32a9 --- /dev/null +++ b/module-api/src/main/java/com/kernel360/likes/code/LikeBusinessCode.java @@ -0,0 +1,35 @@ +package com.kernel360.likes.code; + +import com.kernel360.code.BusinessCode; +import org.springframework.http.HttpStatus; + +public enum LikeBusinessCode implements BusinessCode { + LIKE_ON_SUCCESS(HttpStatus.OK.value(), "BLO001", "좋아요 표시 성공"), + LIKE_OFF_SUCCESS(HttpStatus.OK.value(), "BLO002", "좋아요 취소 성공"), + LIKE_LIST_SEARCH_SUCCESS(HttpStatus.OK.value(), "BLO003", "즐겨찾기 목록조회 성공"); + + private final int status; + private final String code; + private final String message; + + LikeBusinessCode(int status, String code, String message) { + this.status = status; + this.code = code; + this.message = message; + } + + @Override + public int getStatus() { + return status; + } + + @Override + public String getCode() { + return code; + } + + @Override + public String getMessage() { + return message; + } +} diff --git a/module-api/src/main/java/com/kernel360/likes/code/LikeErrorCode.java b/module-api/src/main/java/com/kernel360/likes/code/LikeErrorCode.java new file mode 100644 index 00000000..cf2718e3 --- /dev/null +++ b/module-api/src/main/java/com/kernel360/likes/code/LikeErrorCode.java @@ -0,0 +1,33 @@ +package com.kernel360.likes.code; + +import com.kernel360.code.ErrorCode; +import org.springframework.http.HttpStatus; + +public enum LikeErrorCode implements ErrorCode { + NO_EXIST_LIKE_INFO(HttpStatus.BAD_REQUEST.value(), "ELO001", "좋아요 정보가 존재하지 않습니다."); + + private final int status; + private final String code; + private final String message; + LikeErrorCode(int status, String code, String message) { + this.status = status; + this.code = code; + this.message = message; + } + + @Override + public int getStatus() { + return status; + } + + @Override + public String getCode() { + return code; + } + + @Override + public String getMessage() { + return message; + } + +} diff --git a/module-api/src/main/java/com/kernel360/likes/controller/LikeController.java b/module-api/src/main/java/com/kernel360/likes/controller/LikeController.java new file mode 100644 index 00000000..34518519 --- /dev/null +++ b/module-api/src/main/java/com/kernel360/likes/controller/LikeController.java @@ -0,0 +1,47 @@ +package com.kernel360.likes.controller; + + +import com.kernel360.likes.code.LikeBusinessCode; +import com.kernel360.likes.entity.Like; +import com.kernel360.likes.service.LikeService; +import com.kernel360.product.dto.ProductDto; +import com.kernel360.response.ApiResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/likes") +public class LikeController { + + private final LikeService likeService; + + @GetMapping + public ResponseEntity>> getAllLikes( + @RequestHeader("Authorization") String token, Pageable pageable){ + Page likes = likeService.findAllLikes(token, pageable); + + return ApiResponse.toResponseEntity(LikeBusinessCode.LIKE_LIST_SEARCH_SUCCESS, likes); + } + + @PostMapping + public ResponseEntity> likeOn(@RequestParam("productNo") Long productNo, + @RequestHeader("Authorization") String token){ + likeService.heartOn(productNo, token); + + return ApiResponse.toResponseEntity(LikeBusinessCode.LIKE_ON_SUCCESS); + } + + @DeleteMapping + public ResponseEntity> likeOff(@RequestParam("productNo") Long productNo, + @RequestHeader("Authorization") String token) { + likeService.heartOff(productNo, token); + + return ApiResponse.toResponseEntity(LikeBusinessCode.LIKE_OFF_SUCCESS); + } +} diff --git a/module-api/src/main/java/com/kernel360/likes/service/LikeService.java b/module-api/src/main/java/com/kernel360/likes/service/LikeService.java new file mode 100644 index 00000000..519c2fba --- /dev/null +++ b/module-api/src/main/java/com/kernel360/likes/service/LikeService.java @@ -0,0 +1,56 @@ +package com.kernel360.likes.service; + +import com.kernel360.exception.BusinessException; +import com.kernel360.likes.code.LikeErrorCode; +import com.kernel360.likes.entity.Like; +import com.kernel360.likes.repository.LikeRepository; +import com.kernel360.member.service.MemberService; +import com.kernel360.product.code.ProductsErrorCode; +import com.kernel360.product.dto.ProductDto; +import com.kernel360.product.entity.Product; +import com.kernel360.product.repository.ProductRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + + + +@Service +@RequiredArgsConstructor +public class LikeService { + + private final LikeRepository likeRepository; + private final MemberService memberService; + private final ProductRepository productRepository; + + @Transactional + public void heartOn(Long productNo, String token) { + Long memberNo = memberService.findMemberByToken(token).memberNo(); + Product product = productRepository.findById(productNo) + .orElseThrow(() -> new BusinessException(ProductsErrorCode.NOT_FOUND_PRODUCT)); + + likeRepository.save(Like.of(memberNo, product.getProductNo())); + } + + @Transactional + public void heartOff(Long productNo, String token) { + Long memberNo = memberService.findMemberByToken(token).memberNo(); + Like like = likeRepository.findByMemberNoAndProductNo(memberNo, productNo) + .orElseThrow(() -> new BusinessException(LikeErrorCode.NO_EXIST_LIKE_INFO)); + + likeRepository.delete(like); + } + + @Transactional(readOnly = true) + public Page findAllLikes(String token, Pageable pageable) { + Long memberNo = memberService.findMemberByToken(token).memberNo(); + + return likeRepository.findAllByMemberNo(memberNo, pageable) + .map(like -> productRepository.findById(like.getId()) + .orElseThrow(() -> new BusinessException(ProductsErrorCode.NOT_FOUND_PRODUCT))) + .map(ProductDto::from); + + } +} diff --git a/module-api/src/main/java/com/kernel360/main/config/WebConfig.java b/module-api/src/main/java/com/kernel360/main/config/WebConfig.java deleted file mode 100644 index 78413a41..00000000 --- a/module-api/src/main/java/com/kernel360/main/config/WebConfig.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.kernel360.main.config; - -import com.kernel360.main.conveter.StringToSortConverter; -import org.springframework.context.annotation.Configuration; -import org.springframework.format.FormatterRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -@Configuration -public class WebConfig implements WebMvcConfigurer { - - @Override - public void addFormatters(FormatterRegistry registry) { - registry.addConverter(new StringToSortConverter()); - } -} diff --git a/module-api/src/main/java/com/kernel360/main/controller/MainContoller.java b/module-api/src/main/java/com/kernel360/main/controller/MainController.java similarity index 50% rename from module-api/src/main/java/com/kernel360/main/controller/MainContoller.java rename to module-api/src/main/java/com/kernel360/main/controller/MainController.java index 859be2c3..8eb4da28 100644 --- a/module-api/src/main/java/com/kernel360/main/controller/MainContoller.java +++ b/module-api/src/main/java/com/kernel360/main/controller/MainController.java @@ -1,45 +1,52 @@ package com.kernel360.main.controller; import com.kernel360.main.code.BannerBusinessCode; -import com.kernel360.main.code.ProductsBusinessCode; import com.kernel360.main.dto.BannerDto; import com.kernel360.main.dto.RecommendProductsDto; import com.kernel360.main.service.MainService; +import com.kernel360.product.code.ProductsBusinessCode; import com.kernel360.product.dto.ProductDto; import com.kernel360.product.service.ProductService; import com.kernel360.response.ApiResponse; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping @RequiredArgsConstructor -public class MainContoller { +public class MainController { private final ProductService productService; private final MainService mainService; @GetMapping("/banner") ResponseEntity> getBanner() { - return ApiResponse.toResponseEntity(BannerBusinessCode.GET_BANNER_DATA_SUCCESS, mainService.getSampleBanner()); + return ApiResponse.toResponseEntity(BannerBusinessCode.GET_BANNER_DATA_SUCCESS, mainService.getBanner()); } @GetMapping("/recommend-products") - ResponseEntity>> getRecommendProducts() { - List recommendProductList = productService.getRecommendProductList(); + ResponseEntity>> getRecommendProducts(Pageable pageable) { + Page recommendProducts = productService.getRecommendProducts(pageable); - return ApiResponse.toResponseEntity(ProductsBusinessCode.GET_RECOMMEND_PRODUCT_DATA_SUCCESS, recommendProductList); + return ApiResponse.toResponseEntity(ProductsBusinessCode.GET_RECOMMEND_PRODUCT_DATA_SUCCESS, recommendProducts); } + @GetMapping("/products/rank") - ResponseEntity>> getProducts(@RequestParam(name ="sortType", defaultValue = "viewCnt-order") Sort sortType){ - List productDtos = sortType.sort(productService); + ResponseEntity>> getProducts( + @RequestParam(name = "sortType", defaultValue = "viewCnt-order") Sort sortType, Pageable pageable) { + Page productDtos = sortType.sort(productService, pageable); return ApiResponse.toResponseEntity(ProductsBusinessCode.GET_PRODUCT_DATA_SUCCESS, productDtos); } + + } diff --git a/module-api/src/main/java/com/kernel360/main/controller/Sort.java b/module-api/src/main/java/com/kernel360/main/controller/Sort.java index 9e3df48a..9287028a 100644 --- a/module-api/src/main/java/com/kernel360/main/controller/Sort.java +++ b/module-api/src/main/java/com/kernel360/main/controller/Sort.java @@ -2,42 +2,42 @@ import com.kernel360.product.dto.ProductDto; import com.kernel360.product.service.ProductService; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; -import java.util.List; public enum Sort { VIEW_COUNT_PRODUCT_ORDER("viewCnt-order") { @Override - List sort(ProductService productService) { + Page sort(ProductService productService, Pageable pageable) { - return productService.getProductListOrderByViewCount(); + return productService.getProductListOrderByViewCount(pageable); } }, VIOLATION_PRODUCT_LIST("violation-products") { @Override - List sort(ProductService productService) { + Page sort(ProductService productService, Pageable pageable) { - return productService.getViolationProducts(); + return productService.getViolationProducts(pageable); } }, RECOMMENDATION_PRODUCT_ORDER("recommend-order") { @Override - List sort(ProductService productService) { -//Fixme :: 향후 Like Table 구현후, 정렬메소드 변경이 필요합니다.(임시로 violationProduct 리턴으로 구현) + Page sort(ProductService productService, Pageable pageable) { - return productService.getViolationProducts(); + return productService.getFavoriteProducts(pageable); } }, RECENT_PRODUCT_ORDER("recent-order") { @Override - List sort(ProductService productService) { + Page sort(ProductService productService, Pageable pageable) { - return productService.getRecentProducts(); + return productService.getRecentProducts(pageable); } }; - private String orderType; + private final String orderType; Sort(String orderType) { this.orderType = orderType; @@ -47,5 +47,5 @@ public String getOrderType() { return orderType; } - abstract List sort(ProductService productService); + abstract Page sort(ProductService productService, Pageable pageable); } diff --git a/module-api/src/main/java/com/kernel360/main/dto/BannerDto.java b/module-api/src/main/java/com/kernel360/main/dto/BannerDto.java index 42496227..2cf3b455 100644 --- a/module-api/src/main/java/com/kernel360/main/dto/BannerDto.java +++ b/module-api/src/main/java/com/kernel360/main/dto/BannerDto.java @@ -2,17 +2,17 @@ public record BannerDto ( Long id, - String image, + String imageSource, String alt ) { public static BannerDto of( Long id, - String image, + String imageSource, String alt ) { return new BannerDto( id, - image, + imageSource, alt ); } diff --git a/module-api/src/main/java/com/kernel360/main/dto/RecommendProductsDto.java b/module-api/src/main/java/com/kernel360/main/dto/RecommendProductsDto.java index 318ae61d..90c9be21 100644 --- a/module-api/src/main/java/com/kernel360/main/dto/RecommendProductsDto.java +++ b/module-api/src/main/java/com/kernel360/main/dto/RecommendProductsDto.java @@ -5,19 +5,19 @@ public record RecommendProductsDto( Long id, - String image, + String imageSource, String alt, String productName ) { public static RecommendProductsDto of( Long id, - String image, + String imageSource, String alt, String productName ) { return new RecommendProductsDto( id, - image, + imageSource, alt, productName ); diff --git a/module-api/src/main/java/com/kernel360/main/service/MainService.java b/module-api/src/main/java/com/kernel360/main/service/MainService.java index 072ea0fc..b150c8ed 100644 --- a/module-api/src/main/java/com/kernel360/main/service/MainService.java +++ b/module-api/src/main/java/com/kernel360/main/service/MainService.java @@ -7,7 +7,7 @@ public class MainService { - public BannerDto getSampleBanner() { + public BannerDto getBanner() { return BannerDto.of(1L, "classpath:static/bannerSample.png", "Banner Image"); } diff --git a/module-api/src/main/java/com/kernel360/member/code/MemberBusinessCode.java b/module-api/src/main/java/com/kernel360/member/code/MemberBusinessCode.java index 5862d7cc..1fb674c7 100644 --- a/module-api/src/main/java/com/kernel360/member/code/MemberBusinessCode.java +++ b/module-api/src/main/java/com/kernel360/member/code/MemberBusinessCode.java @@ -13,8 +13,17 @@ public enum MemberBusinessCode implements BusinessCode { SUCCESS_FIND_CAR_INFO_IN_MEMBER(HttpStatus.OK.value(), "BMC004", "차량정보 조회 성공"), SUCCESS_REQUEST_DELETE_MEMBER(HttpStatus.OK.value(), "BMC005", "회원이 탈퇴 되었습니다."), SUCCESS_REQUEST_UPDATE_MEMBER(HttpStatus.OK.value(), "BMC006", "회원정보가 변경 되었습니다."), - SUCCESS_REQUEST_CHANGE_PASSWORD_MEMBER(HttpStatus.OK.value(), "BMC007", "비밀번호가 변경 되었습니다."); - + SUCCESS_REQUEST_CHANGE_PASSWORD_MEMBER(HttpStatus.OK.value(), "BMC007", "비밀번호가 변경 되었습니다."), + SUCCESS_VALIDATE_PASSWORD_MEMBER(HttpStatus.OK.value(), "BMC008", "비밀번호가 확인 되었습니다."), + SUCCESS_REQUEST_UPDATE_WASH_INFO_MEMBER(HttpStatus.OK.value(), "BMC009", "WashInfo 정보가 변경 되었습니다."), + SUCCESS_REQUEST_UPDATE_CAR_INFO_MEMBER(HttpStatus.OK.value(), "BMC010", "CarInfo 정보가 변경 되었습니다."), + SUCCESS_REQUEST_FIND_MEMBER_ID(HttpStatus.OK.value(), "BMC011","회원 아이디 찾기 메일이 발송되었습니다."), + SUCCESS_REQUEST_SEND_RESET_PASSWORD_EMAIL(HttpStatus.OK.value(), "BMC012", "회원 비밀번호 초기화 메일이 발송되었습니다."), + SUCCESS_REQUEST_RESET_PASSWORD_PAGE(HttpStatus.FOUND.value(), "BMC013", "비밀번호 초기화 토큰이 유효하므로 비밀번호 초기화 페이지로 접근합니다."), + SUCCESS_REQUEST_RESET_PASSWORD(HttpStatus.OK.value(), "BMC014", "비밀번호가 초기화되었습니다."), + SUCCESS_REQUEST_LOGIN_MEMBER_KAKAO(HttpStatus.OK.value(), "BMC015", "로그인 성공"), + SUCCESS_REQUEST_SIGN_OUT_MEMBER(HttpStatus.OK.value(), "BMC016", "회원탈퇴 성공"), + SUCCESS_FIND_WASH_INFO_IN_MEMBER(HttpStatus.OK.value(), "BMC017","세차정보 조회 성공"); private final int status; private final String code; diff --git a/module-api/src/main/java/com/kernel360/member/code/MemberErrorCode.java b/module-api/src/main/java/com/kernel360/member/code/MemberErrorCode.java index 2aecc075..facf582c 100644 --- a/module-api/src/main/java/com/kernel360/member/code/MemberErrorCode.java +++ b/module-api/src/main/java/com/kernel360/member/code/MemberErrorCode.java @@ -11,12 +11,17 @@ public enum MemberErrorCode implements ErrorCode { FAILED_NOT_MAPPING_ORDINAL_TO_NAME(HttpStatus.INTERNAL_SERVER_ERROR.value(), "EMC001", "DB값과 ENUM의 ORDINAL이 불일치하여 NAME을 찾을 수 없음."), FAILED_NOT_MAPPING_ORDINAL_TO_VALUE(HttpStatus.INTERNAL_SERVER_ERROR.value(), "EMC002", "DB값과 ENUM의 VALUE가 불일치하여 VALUE을 찾을 수 없음."), - FAILED_NOT_MAPPING_ENUM_VALUEOF(HttpStatus.INTERNAL_SERVER_ERROR.value(), "EMC003", "PARAMETER 값과 ENUM의 NAME이 불일치함."), + FAILED_NOT_MAPPING_ENUM_VALUE_OF(HttpStatus.INTERNAL_SERVER_ERROR.value(), "EMC003", "PARAMETER 값과 ENUM의 NAME이 불일치함."), FAILED_GENERATE_JOIN_MEMBER_INFO(HttpStatus.INTERNAL_SERVER_ERROR.value(), "EMC004", "회원가입에 필요한 정보 생성 실패"), FAILED_GENERATE_LOGIN_REQUEST_INFO(HttpStatus.INTERNAL_SERVER_ERROR.value(), "EMC005", "정보 불일치로 인한 로그인 정보 생성 실패"), FAILED_REQUEST_LOGIN(HttpStatus.BAD_REQUEST.value(), "EMC006", "정보 불일치로 인한 로그인 실패"), - FAILED_FIND_MEMBER_INFO(HttpStatus.BAD_REQUEST.value(), "EMC007", "요청 회원정보가 존재하지 않습니다."); - + FAILED_FIND_MEMBER_INFO(HttpStatus.BAD_REQUEST.value(), "EMC007", "요청 회원정보가 존재하지 않습니다."), + EXPIRED_PASSWORD_RESET_TOKEN(HttpStatus.NOT_FOUND.value(), "EMC008", "유효하지 않은 비밀번호 초기화 토큰입니다"), + FAILED_REQUEST_LOGIN_FOR_KAKAO(HttpStatus.BAD_REQUEST.value(), "EMC009", "카카오 로그인 정보를 찾을 수 없습니다."), + FAILED_FIND_MEMBER_CAR_INFO(HttpStatus.BAD_REQUEST.value(), "EMC010", "요청 회원의 차량정보가 존재하지 않습니다."), + FAILED_FIND_MEMBER_WASH_INFO(HttpStatus.BAD_REQUEST.value(), "EMC011", "요청 회원의 세차정보가 존재하지 않습니다."), + WRONG_PASSWORD_REQUEST(HttpStatus.BAD_REQUEST.value(), "EMC012", "잘못된 비밀번호 입니다."), + FAILED_DUPLICATED_JOIN_MEMBER_INFO(HttpStatus.BAD_REQUEST.value(), "EMC013", "동일한 아이디로 가입한 회원이 이미 존재합니다."); private final int status; private final String code; diff --git a/module-api/src/main/java/com/kernel360/member/controller/MemberController.java b/module-api/src/main/java/com/kernel360/member/controller/MemberController.java index eda43672..18f7c26f 100644 --- a/module-api/src/main/java/com/kernel360/member/controller/MemberController.java +++ b/module-api/src/main/java/com/kernel360/member/controller/MemberController.java @@ -1,16 +1,24 @@ package com.kernel360.member.controller; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.kernel360.carinfo.entity.CarInfo; +import com.kernel360.member.code.MemberBusinessCode; +import com.kernel360.member.dto.CarInfoDto; +import com.kernel360.member.dto.MemberCredentialDto; import com.kernel360.member.dto.MemberDto; +import com.kernel360.member.dto.WashInfoDto; +import com.kernel360.member.service.FindCredentialService; import com.kernel360.member.service.MemberService; import com.kernel360.response.ApiResponse; +import com.kernel360.washinfo.entity.WashInfo; +import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import static com.kernel360.member.code.MemberBusinessCode.SUCCESS_REQUEST_JOIN_MEMBER_CREATED; -import static com.kernel360.member.code.MemberBusinessCode.SUCCESS_REQUEST_LOGIN_MEMBER; +import static com.kernel360.member.code.MemberBusinessCode.*; @Slf4j @RestController @@ -20,8 +28,10 @@ public class MemberController { private final MemberService memberService; + private final FindCredentialService findCredentialService; + @PostMapping("/join") - public ResponseEntity> joinMember(@RequestBody MemberDto joinRequestDto) { + public ResponseEntity> joinMember(@RequestBody MemberDto joinRequestDto) { memberService.joinMember(joinRequestDto); @@ -29,9 +39,9 @@ public ResponseEntity> joinMember(@RequestBody MemberDto joinRequestDto) { } @PostMapping("/login") - public ResponseEntity> login(@RequestBody MemberDto loginDto) { + public ResponseEntity> login(@RequestBody MemberDto loginDto, HttpServletRequest request) { - MemberDto memberInfo = memberService.login(loginDto); + MemberDto memberInfo = memberService.login(loginDto, request); //부가정보가 입력 되어있는가 > 차량정보, 세차정보, boolean (감싸서 보내든 말든 노상관) @@ -39,21 +49,80 @@ public ResponseEntity> login(@RequestBody MemberDto loginDto) { } @GetMapping("/duplicatedCheckId/{id}") - public boolean duplicatedCheckId (@PathVariable String id){ + public boolean duplicatedCheckId(@PathVariable String id) { return memberService.idDuplicationCheck(id); } @GetMapping("/duplicatedCheckEmail/{email}") - public boolean duplicatedCheckEmail (@PathVariable String email){ + public boolean duplicatedCheckEmail(@PathVariable String email) { return memberService.emailDuplicationCheck(email); } - @PostMapping("/testJwt") - public String testJwt (){ - return "checked"; + @PostMapping("/wash") + public ResponseEntity> saveWashInfo(@RequestBody WashInfoDto washInfo, @RequestHeader("Authorization") String authToken){ + memberService.saveWashInfo(washInfo, authToken); + + return ApiResponse.toResponseEntity(MemberBusinessCode.SUCCESS_REQUEST_UPDATE_WASH_INFO_MEMBER); + } + + @PostMapping("/car") + public ResponseEntity> saveCarInfo(@RequestBody CarInfoDto carInfo, @RequestHeader("Authorization") String authToken){ + memberService.saveCarInfo(carInfo, authToken); + + return ApiResponse.toResponseEntity(MemberBusinessCode.SUCCESS_REQUEST_UPDATE_CAR_INFO_MEMBER); + } + + @PostMapping("/find-memberId") + public ResponseEntity> sendMemberIdByEmail(@RequestBody MemberCredentialDto credentialDto) { + String memberId = memberService.findByEmail(credentialDto.email()).id(); + + findCredentialService.sendMemberId(credentialDto.email(), memberId); + + return ApiResponse.toResponseEntity(MemberBusinessCode.SUCCESS_REQUEST_FIND_MEMBER_ID); + } + + @PostMapping("/find-password") + public ResponseEntity> sendPasswordResetUriByEmail(@RequestBody MemberCredentialDto dto) { + //--입력받은 아이디를 데이터베이스에 조회, 없으면 예외 발생--/ + MemberDto memberDto = memberService.findByMemberId(dto.memberId()); + //--유효성이 검증된 아이디에 대해서 만료시간이 있는 비밀번호 초기화 (호스트 + UUID) 링크 생성 --// + String resetUri = findCredentialService.generatePasswordResetUri( memberDto); + //-- 가입시 입력한 이메일로 비밀번호 초기화 이메일 발송 --// + findCredentialService.sendPasswordResetUri(resetUri, memberDto); + + return ApiResponse.toResponseEntity(MemberBusinessCode.SUCCESS_REQUEST_SEND_RESET_PASSWORD_EMAIL); + } + + @GetMapping("/reset-password") + public ResponseEntity> getPasswordResetPage(@RequestParam String token) { + findCredentialService.getData(token); + + return ApiResponse.toResponseEntity(MemberBusinessCode.SUCCESS_REQUEST_RESET_PASSWORD_PAGE, token); + } + + @PostMapping("/reset-password") + public ResponseEntity> resetPassword(@RequestBody MemberCredentialDto credentialDto) { + String authKey = findCredentialService.resetPassword(credentialDto); + findCredentialService.getAndExpireData(authKey); + + return ApiResponse.toResponseEntity(MemberBusinessCode.SUCCESS_REQUEST_RESET_PASSWORD); + } + + @GetMapping("/login/forKakao") + public ResponseEntity> loginForKakao(@RequestHeader("Authorization") String accessToken,HttpServletRequest request) { + + MemberDto member = memberService.loginForKakao(accessToken,request); + + return ApiResponse.toResponseEntity(SUCCESS_REQUEST_LOGIN_MEMBER, member); } + @GetMapping("/signout") + public ResponseEntity> signOut(@RequestHeader("Authorization") String accessToken) { + + memberService.signOut(accessToken); + return ApiResponse.toResponseEntity(SUCCESS_REQUEST_SIGN_OUT_MEMBER); + } } diff --git a/module-api/src/main/java/com/kernel360/member/dto/CarInfoDto.java b/module-api/src/main/java/com/kernel360/member/dto/CarInfoDto.java new file mode 100644 index 00000000..75fcea67 --- /dev/null +++ b/module-api/src/main/java/com/kernel360/member/dto/CarInfoDto.java @@ -0,0 +1,48 @@ +package com.kernel360.member.dto; + +import com.kernel360.carinfo.entity.CarInfo; + +public record CarInfoDto( + Integer carType, + Integer carSize, + Integer carColor, + Integer drivingEnv, + Integer parkingEnv +) { + + public static CarInfoDto of( + Integer carType, + Integer carSize, + Integer carColor, + Integer drivingEnv, + Integer parkingEnv + ) { + return new CarInfoDto( + carType, + carSize, + carColor, + drivingEnv, + parkingEnv + ); + } + + public CarInfo toEntity() { + return CarInfo.of( + this.carType, + this.carSize, + this.carColor, + this.drivingEnv, + this.parkingEnv + ); + } + + public static CarInfoDto from(CarInfo entity) { + return CarInfoDto.of( + entity.getCarType(), + entity.getCarSize(), + entity.getCarColor(), + entity.getDrivingEnv(), + entity.getParkingEnv() + ); + } +} \ No newline at end of file diff --git a/module-api/src/main/java/com/kernel360/member/dto/KakaoUserDto.java b/module-api/src/main/java/com/kernel360/member/dto/KakaoUserDto.java new file mode 100644 index 00000000..c9c76c33 --- /dev/null +++ b/module-api/src/main/java/com/kernel360/member/dto/KakaoUserDto.java @@ -0,0 +1,16 @@ +package com.kernel360.member.dto; + +public record KakaoUserDto( + String id, + String email +) { + public static KakaoUserDto of( + String id, + String email + ){ + return new KakaoUserDto( + id, + email + ); + } +} diff --git a/module-api/src/main/java/com/kernel360/member/dto/MemberCredentialDto.java b/module-api/src/main/java/com/kernel360/member/dto/MemberCredentialDto.java new file mode 100644 index 00000000..4601f95d --- /dev/null +++ b/module-api/src/main/java/com/kernel360/member/dto/MemberCredentialDto.java @@ -0,0 +1,13 @@ +package com.kernel360.member.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record MemberCredentialDto(@JsonProperty("authToken") String authToken, + @JsonProperty("email") String email, + @JsonProperty("memberId") String memberId, + @JsonProperty("password") String password) { + public static MemberCredentialDto of(String authToken, String email, String memberId, String password) { + + return new MemberCredentialDto(authToken, email, memberId, password); + } +} diff --git a/module-api/src/main/java/com/kernel360/member/dto/MemberDto.java b/module-api/src/main/java/com/kernel360/member/dto/MemberDto.java index a580385f..7f7bee9e 100644 --- a/module-api/src/main/java/com/kernel360/member/dto/MemberDto.java +++ b/module-api/src/main/java/com/kernel360/member/dto/MemberDto.java @@ -66,6 +66,22 @@ public static MemberDto from(Member entity) { ); } + public static MemberDto fromKakao(MemberDto dto, String token) { + return MemberDto.of( + dto.memberNo(), + dto.id(), + dto.email(), + dto.password(), + dto.gender(), + dto.age(), + dto.createdAt(), + dto.createdBy(), + dto.modifiedAt(), + dto.modifiedBy(), + token + ); + } + public Member toEntity() { return Member.of( this.memberNo(), diff --git a/module-api/src/main/java/com/kernel360/member/dto/MemberInfo.java b/module-api/src/main/java/com/kernel360/member/dto/MemberInfo.java index 1a4c8de2..8170602f 100644 --- a/module-api/src/main/java/com/kernel360/member/dto/MemberInfo.java +++ b/module-api/src/main/java/com/kernel360/member/dto/MemberInfo.java @@ -1,12 +1,10 @@ package com.kernel360.member.dto; -public record MemberInfo( String id, - String password + + + +public record MemberInfo( + int gender, + int age ) { - static MemberInfo of( - String id, - String password - ) { - return new MemberInfo(id, password); - } } \ No newline at end of file diff --git a/module-api/src/main/java/com/kernel360/member/dto/PasswordDto.java b/module-api/src/main/java/com/kernel360/member/dto/PasswordDto.java new file mode 100644 index 00000000..5c40f7bf --- /dev/null +++ b/module-api/src/main/java/com/kernel360/member/dto/PasswordDto.java @@ -0,0 +1,6 @@ +package com.kernel360.member.dto; + +public record PasswordDto( + String password +) { +} diff --git a/module-api/src/main/java/com/kernel360/member/dto/WashInfoDto.java b/module-api/src/main/java/com/kernel360/member/dto/WashInfoDto.java new file mode 100644 index 00000000..12a4fcc3 --- /dev/null +++ b/module-api/src/main/java/com/kernel360/member/dto/WashInfoDto.java @@ -0,0 +1,45 @@ +package com.kernel360.member.dto; + + +import com.kernel360.washinfo.entity.WashInfo; + + +public record WashInfoDto( + Integer washNo, + Integer washCount, + Integer monthlyExpense, + Integer interest +) { + public static WashInfoDto of( + Integer washNo, + Integer washCount, + Integer monthlyExpense, + Integer interest + ) { + return new WashInfoDto( + washNo, + washCount, + monthlyExpense, + interest + ); + } + + public WashInfo toEntity() { + return WashInfo.of( + this.washNo, + this.washCount, + this.monthlyExpense, + this.interest + ); + } + + public static WashInfoDto from(WashInfo entity) { + + return WashInfoDto.of( + entity.getWashNo(), + entity.getWashCount(), + entity.getMonthlyExpense(), + entity.getInterest() + ); + } +} diff --git a/module-api/src/main/java/com/kernel360/member/enumset/Age.java b/module-api/src/main/java/com/kernel360/member/enumset/Age.java index d1af5a2d..239bddc1 100644 --- a/module-api/src/main/java/com/kernel360/member/enumset/Age.java +++ b/module-api/src/main/java/com/kernel360/member/enumset/Age.java @@ -10,7 +10,8 @@ public enum Age { AGE_30(30), AGE_40(40), AGE_50(50), - AGE_60(60); + AGE_60(60), + AGE_99(99); private final int value; diff --git a/module-api/src/main/java/com/kernel360/member/enumset/Gender.java b/module-api/src/main/java/com/kernel360/member/enumset/Gender.java index 0988155f..9ae46a1d 100644 --- a/module-api/src/main/java/com/kernel360/member/enumset/Gender.java +++ b/module-api/src/main/java/com/kernel360/member/enumset/Gender.java @@ -6,8 +6,9 @@ @RequiredArgsConstructor public enum Gender { - man(0), - woman(1); + MALE(0), + FEMALE(1), + OTHERS(99); private final int value; public static String ordinalToName(int value) { diff --git a/module-api/src/main/java/com/kernel360/member/service/EmailSender.java b/module-api/src/main/java/com/kernel360/member/service/EmailSender.java new file mode 100644 index 00000000..1668499d --- /dev/null +++ b/module-api/src/main/java/com/kernel360/member/service/EmailSender.java @@ -0,0 +1,5 @@ +package com.kernel360.member.service; + +public interface EmailSender { + void sendMail(String to, String subject, String content); +} diff --git a/module-api/src/main/java/com/kernel360/member/service/FindCredentialService.java b/module-api/src/main/java/com/kernel360/member/service/FindCredentialService.java new file mode 100644 index 00000000..fc3e4f1b --- /dev/null +++ b/module-api/src/main/java/com/kernel360/member/service/FindCredentialService.java @@ -0,0 +1,164 @@ +package com.kernel360.member.service; + +import com.kernel360.exception.BusinessException; +import com.kernel360.member.code.MemberErrorCode; +import com.kernel360.member.dto.MemberCredentialDto; +import com.kernel360.member.dto.MemberDto; +import java.time.Duration; +import java.util.Objects; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.stereotype.Service; +import org.springframework.web.util.UriComponentsBuilder; + +@Service +@RequiredArgsConstructor +public class FindCredentialService implements RedisUtils { + + @Value("${constants.password-reset-token.duration-minute}") + private int TOKEN_DURATION; + + @Value("${constants.host-url}") + private String HOST_HTTP_URL; + + private final RedisTemplate redisTemplate; + + private final MemberService memberService; + + private final SendEmailService emailService; + + public void sendMemberId(String email, String memberId) { + String textContent = "가입하신 아이디는 " + "'" + memberId + "' 입니다."; + String htmlContent = + "\n" + + + " \n" + + + " \n" + + " \n" + + " Wash-Fit 아이디/패스워드 찾기\n" + + " \n" + + " \n" + + " \n" + + " \n" + textContent + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " 기타 질문사항은 고객센터에 문의 주시길 바랍니다.\n" + + " \n" + + " \n" + + " \n" + + " \n" + + ""; + + emailService.sendMail(email, "[No-Reply] Wash-Fit 아이디/비밀번호 찾기", htmlContent); + } + + public void sendPasswordResetUri(String resetUri, MemberDto dto) { + + String htmlContent = "\n" + + "\n" + + "\n" + + " \n" + + " Password Recovery\n" + + "\n" + + "\n" + + "\n" + + " \n" + + " \n" + + " WashFit 계정의 비밀번호를 변경합니다.\n" + + " 아래의 링크를 통해 패스워드를 재설정하세요.\n" + + " \n" + + " 패스워드 재설정하기\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " 고객센터 운영시간\n" + + " 평일 10:00~19:00 (주말 및 공휴일 제외/ 점심시간 13:00~14:00)\n" + + " " + + "고객센터 문의하기\n" + + " \n" + + " \n" + + " FAST CAMPUS\n" + + " (주)데이원캠퍼니\n" + + " 서울특별시 강남구 테헤란로 231, 센트럴프라자 WEST 6층, 7층\n" + + " \n" + + "\n" + + "\n" + + ""; + + emailService.sendMail(dto.email(), "[No-Reply] Wash-Fit 아이디/비밀번호 찾기", htmlContent); + } + + public String generatePasswordResetUri(MemberDto memberDto) { + String resetToken = generateUUID(); + + String uriString = UriComponentsBuilder.fromHttpUrl(HOST_HTTP_URL) + .path("/member/reset-password") + .queryParam("token", resetToken) + .build() + .toUriString(); + setExpiringData(resetToken, memberDto.id(), TOKEN_DURATION); // duration 값 상수로 변경관리 필요 + + return uriString; + } + + public String resetPassword(MemberCredentialDto credentialDto) { + ValueOperations valueOperations = redisTemplate.opsForValue(); + String value = valueOperations.get(credentialDto.authToken()); + + if (Objects.isNull(value)) { + throw new BusinessException(MemberErrorCode.EXPIRED_PASSWORD_RESET_TOKEN); + } + + memberService.resetPasswordByMemberId(value, credentialDto.password()); + + return credentialDto.authToken(); + } + + private String generateUUID() { + + return UUID.randomUUID().toString(); + } + + @Override + public String getData(String key) { + ValueOperations valueOperations = redisTemplate.opsForValue(); + if (Objects.isNull(valueOperations.get(key))) { + throw new BusinessException(MemberErrorCode.EXPIRED_PASSWORD_RESET_TOKEN); + } + + return valueOperations.get(key); + } + + @Override + public void setData(String key, String value) { + ValueOperations valueOperations = redisTemplate.opsForValue(); + valueOperations.set(key, value); + } + + @Override + public void setExpiringData(String key, String value, int duration) { + ValueOperations valueOperations = redisTemplate.opsForValue(); + Duration expireDuration = Duration.ofMinutes(duration); + valueOperations.set(key, value, expireDuration); + } + + @Override + public void getAndExpireData(String key) { + ValueOperations valueOperations = redisTemplate.opsForValue(); + if (Objects.isNull(valueOperations.get(key))) { + throw new BusinessException(MemberErrorCode.EXPIRED_PASSWORD_RESET_TOKEN); + } + + valueOperations.getAndDelete(key); + } +} diff --git a/module-api/src/main/java/com/kernel360/member/service/KakaoRequest.java b/module-api/src/main/java/com/kernel360/member/service/KakaoRequest.java new file mode 100644 index 00000000..15164ff7 --- /dev/null +++ b/module-api/src/main/java/com/kernel360/member/service/KakaoRequest.java @@ -0,0 +1,47 @@ +package com.kernel360.member.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.kernel360.exception.BusinessException; +import com.kernel360.member.code.MemberErrorCode; +import com.kernel360.member.dto.KakaoUserDto; +import org.springframework.http.*; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +import java.util.HashMap; +import java.util.Map; + +@Component +public class KakaoRequest { + + public KakaoUserDto getKakaoUserByToken(String accessToken) { + + String url = "https://kapi.kakao.com/v2/user/me"; + + HttpHeaders headers = new HttpHeaders(); + + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + headers.set("Authorization", "Bearer "+accessToken); + headers.set("charset", "utf-8"); + + HttpEntity requestEntity = new HttpEntity<>(headers); + RestTemplate restTemplate = new RestTemplate(); + ResponseEntity responseEntity = restTemplate.exchange(url, HttpMethod.GET, requestEntity, String.class); + + ObjectMapper mapper = new ObjectMapper(); + Map kakaoResponse; + + try { + kakaoResponse = mapper.readValue(responseEntity.getBody(), HashMap.class); + }catch (Exception e){ + throw new BusinessException(MemberErrorCode.FAILED_REQUEST_LOGIN_FOR_KAKAO); + } + Map kakaoAccount = mapper.convertValue(kakaoResponse.get("kakao_account"), HashMap.class); + + KakaoUserDto dto = KakaoUserDto.of(kakaoResponse.get("id").toString(),kakaoAccount.get("email").toString()); + + return dto; + } + + +} diff --git a/module-api/src/main/java/com/kernel360/member/service/MemberService.java b/module-api/src/main/java/com/kernel360/member/service/MemberService.java index 58f89ea4..056d46a7 100644 --- a/module-api/src/main/java/com/kernel360/member/service/MemberService.java +++ b/module-api/src/main/java/com/kernel360/member/service/MemberService.java @@ -1,26 +1,31 @@ package com.kernel360.member.service; -import com.kernel360.auth.entity.Auth; -import com.kernel360.auth.repository.AuthRepository; +import com.kernel360.auth.service.AuthService; import com.kernel360.carinfo.entity.CarInfo; +import com.kernel360.carinfo.repository.CarInfoRepository; import com.kernel360.commoncode.service.CommonCodeService; import com.kernel360.exception.BusinessException; import com.kernel360.member.code.MemberErrorCode; -import com.kernel360.member.dto.MemberDto; +import com.kernel360.member.dto.*; import com.kernel360.member.entity.Member; +import com.kernel360.member.entity.WithdrawMember; import com.kernel360.member.enumset.Age; import com.kernel360.member.enumset.Gender; import com.kernel360.member.repository.MemberRepository; +import com.kernel360.member.repository.WithdrawMemberRepository; import com.kernel360.utils.ConvertSHA256; import com.kernel360.utils.JWT; +import com.kernel360.washinfo.entity.WashInfo; +import com.kernel360.washinfo.repository.WashInfoRepository; +import jakarta.servlet.http.HttpServletRequest; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.http.RequestEntity; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; - -import java.util.Map; -import java.util.Optional; +import org.springframework.web.bind.annotation.RequestHeader; @Slf4j @@ -29,27 +34,24 @@ public class MemberService { private final JWT jwt; - private final AuthRepository authRepository; + private final AuthService authService; private final MemberRepository memberRepository; private final CommonCodeService commonCodeService; + private final CarInfoRepository carInfoRepository; + private final WashInfoRepository washInfoRepository; + private final KakaoRequest kakaoRequest; + private final WithdrawMemberRepository withdrawMemberRepository; - - /** - * 가입 - **/ @Transactional public void joinMember(MemberDto requestDto) { - Member entity = getNewJoinMemberEntity(requestDto); - if (entity == null) { - throw new BusinessException(MemberErrorCode.FAILED_GENERATE_JOIN_MEMBER_INFO); - } - + if (entity == null) { throw new BusinessException(MemberErrorCode.FAILED_GENERATE_JOIN_MEMBER_INFO); } + // TODO :: ControllerAdvice 추가 고민해보기 + if(memberRepository.findOneById(entity.getId()) != null){ throw new BusinessException(MemberErrorCode.FAILED_DUPLICATED_JOIN_MEMBER_INFO);} memberRepository.save(entity); } protected Member getNewJoinMemberEntity(MemberDto requestDto) { - String encodePassword = ConvertSHA256.convertToSHA256(requestDto.password()); int genderOrdinal; int ageOrdinal; @@ -58,57 +60,30 @@ protected Member getNewJoinMemberEntity(MemberDto requestDto) { genderOrdinal = Gender.valueOf(requestDto.gender()).ordinal(); ageOrdinal = Age.valueOf(requestDto.age()).ordinal(); } catch (Exception e) { - throw new BusinessException(MemberErrorCode.FAILED_NOT_MAPPING_ENUM_VALUEOF); + throw new BusinessException(MemberErrorCode.FAILED_NOT_MAPPING_ENUM_VALUE_OF); } return Member.createJoinMember(requestDto.id(), requestDto.email(), encodePassword, genderOrdinal, ageOrdinal); } - /** - * 로그인 - **/ @Transactional - public MemberDto login(MemberDto loginDto) { - - Member loginEntity = getReqeustLoginEntity(loginDto); - if (loginEntity == null) { + public MemberDto login(MemberDto loginDto, HttpServletRequest request) { + Member loginEntity = newRequestLoginEntity(loginDto); + if (Objects.isNull(loginEntity)) { throw new BusinessException(MemberErrorCode.FAILED_GENERATE_LOGIN_REQUEST_INFO); } Member memberEntity = memberRepository.findOneByIdAndPassword(loginEntity.getId(), loginEntity.getPassword()); - if (memberEntity == null) { - throw new BusinessException(MemberErrorCode.FAILED_REQUEST_LOGIN); - } + if (Objects.isNull(memberEntity)) { throw new BusinessException(MemberErrorCode.FAILED_REQUEST_LOGIN); } - String token = jwt.generateToken(memberEntity.getId()); + String loginToken = jwt.generateToken(memberEntity.getId()); - String encryptToken = ConvertSHA256.convertToSHA256(token); + authService.saveAuthByMember(memberEntity.getMemberNo(), ConvertSHA256.convertToSHA256(loginToken), request); - Auth authJwt = authRepository.findOneByMemberNo(memberEntity.getMemberNo()); - - //결과 없으면 entity로 신규 생성 - authJwt = Optional.ofNullable(authJwt) - .map(modifyAuth -> modifyAuthJwt(modifyAuth, encryptToken)) - .orElseGet(() -> createAuthJwt(memberEntity.getMemberNo(), encryptToken)); - - authRepository.save(authJwt); - - return MemberDto.login(memberEntity, token); - } - - public Auth modifyAuthJwt(Auth modifyAuth, String encryptToken) { - modifyAuth.updateJwt(encryptToken); - - return modifyAuth; + return MemberDto.login(memberEntity, loginToken); } - protected Auth createAuthJwt(Long memberNo, String encryptToken) { - - return Auth.of(null, memberNo, encryptToken, null); - } - - - private Member getReqeustLoginEntity(MemberDto loginDto) { + private Member newRequestLoginEntity(MemberDto loginDto) { String encodePassword = ConvertSHA256.convertToSHA256(loginDto.password()); return Member.loginMember(loginDto.id(), encodePassword); @@ -128,55 +103,67 @@ public boolean emailDuplicationCheck(String email) { return member != null; } - public Auth findOneAuthByJwt(String encryptToken) { - return authRepository.findOneByJwtToken(encryptToken); - } - - public void reissuanceJwt(Auth storedAuthInfo) { - authRepository.save(storedAuthInfo); - } - - - public MemberDto findMemberByToken(RequestEntity request) { - String token = request.getHeaders().getFirst("Authorization"); + public MemberDto findMemberByToken(String token) { String id = JWT.ownerId(token); return MemberDto.from(memberRepository.findOneById(id)); } - public CarInfo findCarInfoByToken(RequestEntity request) { - MemberDto memberDto = findMemberByToken(request); + public CarInfo findCarInfoByToken(@RequestHeader("Authorization") String authToken) { + MemberDto memberDto = findMemberByToken(authToken); return memberDto.toEntity().getCarInfo(); } @Transactional public void deleteMember(String id) { - memberRepository.deleteMemberById(id); + Member member = memberRepository.findOneById(id); + + memberRepository.delete(member); } + @Transactional + public void deleteMemberByToken(String token) { + final String id = JWT.ownerId(token); + Member member = memberRepository.findOneById(id); + //Fixme :: 멤버 탈퇴시, Deleted Table을 만들고, 데이터를 백업한후, 삭제하는 방식이나, MemberTable에 삭제여부를 표시하는 방식으로 리팩토링 필요 + memberRepository.delete(member); + log.info("{} 회원 탈퇴 처리 완료", id); + } @Transactional - public void changePassword(MemberDto memberDto) { - memberRepository.updatePasswordById(memberDto.id(), memberDto.password()); + public void changePassword(String password, String token) { + String id = JWT.ownerId(token); + Member member = memberRepository.findOneById(id); + + if (!member.getPassword().equals(ConvertSHA256.convertToSHA256(password))) { + throw new BusinessException(MemberErrorCode.WRONG_PASSWORD_REQUEST); + } + + member.updatePassword(ConvertSHA256.convertToSHA256(password)); + log.info("{} 회원의 비밀번호가 변경되었습니다.", id); } @Transactional - public void updateMember(MemberDto memberDto) { - Member member = - Member.of(memberDto.memberNo(), memberDto.id(), memberDto.email(), memberDto.password(), - Integer.parseInt(memberDto.gender()), Integer.parseInt(memberDto.age())); + public void updateMember(MemberInfo memberInfo, String token) { + String id = JWT.ownerId(token); + Member existingMember = memberRepository.findOneById(id); + existingMember.updateFromInfo( memberInfo.gender(), memberInfo.age()); - memberRepository.save(member); + memberRepository.save(existingMember); } @Transactional(readOnly = true) - public Map getCarInfo(RequestEntity request) { - // CarInfo carInfo = memberService.findCarInfoByToken(request); - //FIXME :: CarInfo Data 없어서 에러발생 주석 처리해둠 + public Map getCarInfo(String token) { + String id = JWT.ownerId(token); + Member member = memberRepository.findOneById(id); + if(member.getCarInfo() == null){ + throw new BusinessException(MemberErrorCode.FAILED_FIND_MEMBER_CAR_INFO); + } + CarInfoDto carInfoDto = CarInfoDto.from(member.getCarInfo()); return Map.of( -// "car_info", carInfo, + "car_info", carInfoDto, "segment_options", commonCodeService.getCodes("segment"), "carType_options", commonCodeService.getCodes("cartype"), "color_options", commonCodeService.getCodes("color"), @@ -184,4 +171,105 @@ public Map getCarInfo(RequestEntity request) { "parking_options", commonCodeService.getCodes("parking") ); } + + @Transactional(readOnly = true) + public Optional getWashInfo(String token) { + String id = JWT.ownerId(token); + Member member = memberRepository.findOneById(id); + if (member == null) { + throw new BusinessException(MemberErrorCode.FAILED_FIND_MEMBER_INFO); + } + + return Optional.of(WashInfoDto.from(member.getWashInfo())); + } + + @Transactional + public void saveWashInfo(WashInfoDto washInfoDto, String token) { + String id = JWT.ownerId(token); + Member member = memberRepository.findOneById(id); + WashInfo washInfo = WashInfo.of(washInfoDto.washNo(), washInfoDto.washCount(), washInfoDto.monthlyExpense(), + washInfoDto.interest()); + washInfo.settingMember(member); + member.updateWashInfo(washInfo); + + washInfoRepository.save(washInfo); + } + + @Transactional + public void saveCarInfo(CarInfoDto carInfoDto, String token) { + String id = JWT.ownerId(token); + Member member = memberRepository.findOneById(id); + CarInfo carInfo = CarInfo.of(carInfoDto.carType(), carInfoDto.carSize(), carInfoDto.carColor(), + carInfoDto.drivingEnv(), carInfoDto.parkingEnv()); + carInfo.settingMember(member); + member.updateCarInfo(carInfo); + + carInfoRepository.save(carInfo); + } + + @Transactional(readOnly = true) + public MemberDto findByEmail(String email) { + Member member = memberRepository.findOneByEmail(email); + if (member == null) { + throw new BusinessException(MemberErrorCode.FAILED_FIND_MEMBER_INFO); + } + + return MemberDto.from(member); + } + + @Transactional(readOnly = true) + public MemberDto findByMemberId(String memberId) { + Member member = memberRepository.findOneById(memberId); + if (member == null) { + throw new BusinessException(MemberErrorCode.FAILED_FIND_MEMBER_INFO); + } + + return MemberDto.from(member); + } + + @Transactional + public void resetPasswordByMemberId(String memberId, String newPassword) { + Member member = memberRepository.findOneById(memberId); + if (member == null) { + throw new BusinessException(MemberErrorCode.FAILED_FIND_MEMBER_INFO); + } + + member.updatePassword(ConvertSHA256.convertToSHA256(newPassword)); + } + + @Transactional + public MemberDto loginForKakao(String accessToken, HttpServletRequest request) { + + KakaoUserDto kakaoUser = kakaoRequest.getKakaoUserByToken(accessToken); + if (Objects.isNull(memberRepository.findOneById(kakaoUser.id()))) { + memberRepository.save( + Member.createForKakao(kakaoUser.id(), kakaoUser.email(), "kakao", Gender.OTHERS.ordinal(), + Age.AGE_99.ordinal())); + } + + MemberDto memberDto = MemberDto.from(memberRepository.findOneById(kakaoUser.id())); + + String loginToken = jwt.generateToken(memberDto.id()); + + authService.saveAuthByMember(memberDto.memberNo(), ConvertSHA256.convertToSHA256(loginToken), request); + + return MemberDto.fromKakao(memberDto, loginToken); + } + + @Transactional + public void signOut(String accessToken) { + Member member = memberRepository.findOneById(jwt.ownerId(accessToken)); + + withdrawMemberRepository.save(WithdrawMember.of(member.getMemberNo(),member.getId(), member.getEmail(), null)); + + memberRepository.delete(member); + } + + @Transactional(readOnly = true) + public boolean validatePassword(String password, String token) { + String id = JWT.ownerId(token); + Member member = memberRepository.findOneById(id); + + return member.getPassword().equals(ConvertSHA256.convertToSHA256(password)); + } } diff --git a/module-api/src/main/java/com/kernel360/member/service/RedisUtils.java b/module-api/src/main/java/com/kernel360/member/service/RedisUtils.java new file mode 100644 index 00000000..efb769af --- /dev/null +++ b/module-api/src/main/java/com/kernel360/member/service/RedisUtils.java @@ -0,0 +1,12 @@ +package com.kernel360.member.service; + +public interface RedisUtils { + + Object getData(String key); + + void setData(String key, String value); + + void setExpiringData(String key, String value, int duration); + + void getAndExpireData(String key); +} diff --git a/module-api/src/main/java/com/kernel360/member/service/SendEmailService.java b/module-api/src/main/java/com/kernel360/member/service/SendEmailService.java new file mode 100644 index 00000000..8f3e9237 --- /dev/null +++ b/module-api/src/main/java/com/kernel360/member/service/SendEmailService.java @@ -0,0 +1,24 @@ +package com.kernel360.member.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class SendEmailService implements EmailSender { + + private final JavaMailSender mailSender; + + @Override + public void sendMail(String to, String subject, String content) { + + new Thread(() -> mailSender.send(mimeMessage -> { + MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, true); + mimeMessageHelper.setTo(to); + mimeMessageHelper.setSubject(subject); + mimeMessageHelper.setText(content, true); + })).start(); + } +} diff --git a/module-api/src/main/java/com/kernel360/mypage/controller/MyPageController.java b/module-api/src/main/java/com/kernel360/mypage/controller/MyPageController.java index d62d9227..e131c11c 100644 --- a/module-api/src/main/java/com/kernel360/mypage/controller/MyPageController.java +++ b/module-api/src/main/java/com/kernel360/mypage/controller/MyPageController.java @@ -1,17 +1,23 @@ package com.kernel360.mypage.controller; +import com.kernel360.exception.BusinessException; import com.kernel360.member.code.MemberBusinessCode; +import com.kernel360.member.code.MemberErrorCode; import com.kernel360.member.dto.MemberDto; +import com.kernel360.member.dto.MemberInfo; +import com.kernel360.member.dto.PasswordDto; +import com.kernel360.member.dto.WashInfoDto; import com.kernel360.member.service.MemberService; import com.kernel360.product.service.ProductService; import com.kernel360.response.ApiResponse; import lombok.RequiredArgsConstructor; -import org.springframework.http.RequestEntity; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.Map; +@Slf4j @RestController @RequestMapping("/mypage") @RequiredArgsConstructor @@ -20,42 +26,57 @@ public class MyPageController { private final ProductService productService; @GetMapping("/member") - ResponseEntity> myInfo(RequestEntity request) { - MemberDto dto = memberService.findMemberByToken(request); + ResponseEntity> myInfo(@RequestHeader("Authorization") String authToken) { + MemberDto dto = memberService.findMemberByToken(authToken); return ApiResponse.toResponseEntity(MemberBusinessCode.SUCCESS_FIND_REQUEST_MEMBER, dto); } @GetMapping("/car") - ResponseEntity>> myCar(RequestEntity request) { - Map carInfo = memberService.getCarInfo(request); + ResponseEntity>> myCar(@RequestHeader("Authorization") String authToken) { + Map carInfo = memberService.getCarInfo(authToken); return ApiResponse.toResponseEntity(MemberBusinessCode.SUCCESS_FIND_CAR_INFO_IN_MEMBER, carInfo); } + @GetMapping("/wash") + ResponseEntity> myWash(@RequestHeader("Authorization") String authToken) { + WashInfoDto washInfoDto = memberService.getWashInfo(authToken) + .orElseThrow(() -> new BusinessException(MemberErrorCode.FAILED_FIND_MEMBER_WASH_INFO)); + + return ApiResponse.toResponseEntity(MemberBusinessCode.SUCCESS_FIND_WASH_INFO_IN_MEMBER, washInfoDto); + } + @DeleteMapping("/member") - ResponseEntity> memberDelete(@RequestBody MemberDto memberDto) { - memberService.deleteMember(memberDto.id()); + ResponseEntity> memberDelete(@RequestHeader("Authorization") String authToken) { + memberService.deleteMemberByToken(authToken); return ApiResponse.toResponseEntity(MemberBusinessCode.SUCCESS_REQUEST_DELETE_MEMBER); } @PostMapping("/member") - ResponseEntity> changePassword(@RequestBody MemberDto memberDto) { - MemberDto dto = MemberDto.of(memberDto.id(), memberDto.password()); - memberService.changePassword(dto); + ResponseEntity> changePassword(@RequestBody String password, @RequestHeader("Authorization") String authToken) { + memberService.changePassword(password, authToken); - return ApiResponse.toResponseEntity(MemberBusinessCode.SUCCESS_REQUEST_CHANGE_PASSWORD_MEMBER); + return ApiResponse.toResponseEntity(MemberBusinessCode.SUCCESS_VALIDATE_PASSWORD_MEMBER); + } + + @GetMapping("/member/validate") + boolean validatePassword(@RequestBody PasswordDto password, @RequestHeader("Authorization") String authToken) { + + return memberService.validatePassword(password.password(), authToken); } @PutMapping("/member") - ResponseEntity> updateMember(@RequestBody MemberDto memberDto) { - memberService.updateMember(memberDto); + ResponseEntity> updateMember(@RequestBody MemberInfo memberInfo, + @RequestHeader("Authorization") String authToken) { + memberService.updateMember(memberInfo, authToken); return ApiResponse.toResponseEntity(MemberBusinessCode.SUCCESS_REQUEST_UPDATE_MEMBER); - } + + } diff --git a/module-api/src/main/java/com/kernel360/main/code/ProductsBusinessCode.java b/module-api/src/main/java/com/kernel360/product/code/ProductsBusinessCode.java similarity index 95% rename from module-api/src/main/java/com/kernel360/main/code/ProductsBusinessCode.java rename to module-api/src/main/java/com/kernel360/product/code/ProductsBusinessCode.java index b2180323..40105807 100644 --- a/module-api/src/main/java/com/kernel360/main/code/ProductsBusinessCode.java +++ b/module-api/src/main/java/com/kernel360/product/code/ProductsBusinessCode.java @@ -1,4 +1,4 @@ -package com.kernel360.main.code; +package com.kernel360.product.code; import com.kernel360.code.BusinessCode; import org.springframework.http.HttpStatus; diff --git a/module-api/src/main/java/com/kernel360/product/code/ProductsErrorCode.java b/module-api/src/main/java/com/kernel360/product/code/ProductsErrorCode.java new file mode 100644 index 00000000..85b01e05 --- /dev/null +++ b/module-api/src/main/java/com/kernel360/product/code/ProductsErrorCode.java @@ -0,0 +1,34 @@ +package com.kernel360.product.code; + +import com.kernel360.code.ErrorCode; +import org.springframework.http.HttpStatus; + +public enum ProductsErrorCode implements ErrorCode { + NOT_FOUND_RECOMMEND_PRODUCT(HttpStatus.BAD_REQUEST.value(), "ERPMB001", "추천제품 데이터가 존재하지 않습니다."), + NOT_FOUND_PRODUCT(HttpStatus.BAD_REQUEST.value(), "EPMB001", "제품 데이터가 존재하지 않습니다."); + + private final int status; + private final String code; + private final String message; + ProductsErrorCode(int status, String code, String message) { + this.status = status; + this.code = code; + this.message = message; + } + + @Override + public int getStatus() { + return status; + } + + @Override + public String getCode() { + return code; + } + + @Override + public String getMessage() { + return message; + } + +} diff --git a/module-api/src/main/java/com/kernel360/product/controller/ProductController.java b/module-api/src/main/java/com/kernel360/product/controller/ProductController.java index c96bb431..eaaf0e20 100644 --- a/module-api/src/main/java/com/kernel360/product/controller/ProductController.java +++ b/module-api/src/main/java/com/kernel360/product/controller/ProductController.java @@ -1,9 +1,12 @@ package com.kernel360.product.controller; -import com.kernel360.main.code.ProductsBusinessCode; +import com.kernel360.product.code.ProductsBusinessCode; +import com.kernel360.product.dto.ProductDetailDto; import com.kernel360.product.dto.ProductDto; import com.kernel360.product.service.ProductService; import com.kernel360.response.ApiResponse; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -18,23 +21,28 @@ public class ProductController { @GetMapping("/products") ResponseEntity>> findProductList(){ - final List productDtoList = productService.getProductList(); + final List products = productService.getProducts(); - return ApiResponse.toResponseEntity(ProductsBusinessCode.GET_PRODUCT_DATA_SUCCESS, productDtoList); + return ApiResponse.toResponseEntity(ProductsBusinessCode.GET_PRODUCT_DATA_SUCCESS, products); } + @GetMapping("/products/search") + ResponseEntity>> findProductByKeyword(@RequestParam("keyword") String keyword, Pageable pageable){ + final Page list = productService.getProductsByKeyword(keyword, pageable); - @GetMapping("/product/{id}") - ResponseEntity> findProductById(@PathVariable("id") Long productId) { - ProductDto product = productService.getProductById(productId); + return ApiResponse.toResponseEntity(ProductsBusinessCode.GET_PRODUCT_DATA_SUCCESS, list); + } + @GetMapping("/product/{productNo}") + ResponseEntity> findProductById(@PathVariable("productNo") Long productNo) { + ProductDetailDto findProductDetailDto = productService.getProductById(productNo); + productService.updateViewCount(findProductDetailDto.productNo()); - return ApiResponse.toResponseEntity(ProductsBusinessCode.GET_PRODUCT_DATA_SUCCESS, product); + return ApiResponse.toResponseEntity(ProductsBusinessCode.GET_PRODUCT_DATA_SUCCESS, findProductDetailDto); } - @GetMapping("/products/search") - ResponseEntity>> findProductByKeyword(@RequestParam("keyword") String keyword){ - final List list = productService.getProductListByKeyword(keyword); + @GetMapping("/products/{OCR_No}") + ResponseEntity>> findProductByOCR(@PathVariable("OCR_No") String reportNo, Pageable pageable) { - return ApiResponse.toResponseEntity(ProductsBusinessCode.GET_PRODUCT_DATA_SUCCESS, list); + return ApiResponse.toResponseEntity(ProductsBusinessCode.GET_PRODUCT_DATA_SUCCESS, productService.getProductByOCR(reportNo, pageable)); } } diff --git a/module-api/src/main/java/com/kernel360/product/dto/ProductDetailDto.java b/module-api/src/main/java/com/kernel360/product/dto/ProductDetailDto.java new file mode 100644 index 00000000..d7f48dca --- /dev/null +++ b/module-api/src/main/java/com/kernel360/product/dto/ProductDetailDto.java @@ -0,0 +1,156 @@ +package com.kernel360.product.dto; + +import com.kernel360.product.entity.Product; +import jakarta.persistence.Column; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; + +import java.time.LocalDate; + +/** + * DTO for {@link com.kernel360.product.entity.Product} + */ +public record ProductDetailDto( + Long productNo, + String productName, + String barcode, + String imageSource, + String reportNumber, + String safetyStatus, + Integer viewCount, + String companyName, + String productType, + LocalDate issuedDate, + String safetyInspectionStandard, + String upperItem, + String item, + String propose, + String weight, + String usage, + String usagePrecaution, + String firstAid, + String mainSubstance, + String allergicSubstance, + String otherSubstance, + String preservative, + String surfactant, + String fluorescentWhitening, + String manufactureType, + String manufactureMethod, + String manufactureNation, + String violationInfo, + LocalDate createdAt, + String createdBy, + LocalDate modifiedAt, + String modifiedBy + //TODO 브랜드 엔티티 +) { + + public static ProductDetailDto of( + Long productNo, + String productName, + String barcode, + String imageSource, + String reportNumber, + String safetyStatus, + Integer viewCount, + String companyName, + String productType, + LocalDate issuedDate, + String safetyInspectionStandard, + String upperItem, + String item, + String propose, + String weight, + String usage, + String usagePrecaution, + String firstAid, + String mainSubstance, + String allergicSubstance, + String otherSubstance, + String preservative, + String surfactant, + String fluorescentWhitening, + String manufactureType, + String manufactureMethod, + String manufactureNation, + String violationInfo, + LocalDate createdAt, + String createdBy, + LocalDate modifiedAt, + String modifiedBy + ) { + return new ProductDetailDto( + productNo, + productName, + barcode, + imageSource, + reportNumber, + safetyStatus, + viewCount, + companyName, + productType, + issuedDate, + safetyInspectionStandard, + upperItem, + item, + propose, + weight, + usage, + usagePrecaution, + firstAid, + mainSubstance, + allergicSubstance, + otherSubstance, + preservative, + surfactant, + fluorescentWhitening, + manufactureType, + manufactureMethod, + manufactureNation, + violationInfo, + createdAt, + createdBy, + modifiedAt, + modifiedBy + ); + } + + public static ProductDetailDto from(Product entity) { + return ProductDetailDto.of( + entity.getProductNo(), + entity.getProductName(), + entity.getBarcode(), + entity.getImageSource(), + entity.getReportNumber(), + entity.getSafetyStatus().name(), + entity.getViewCount(), + entity.getCompanyName(), + entity.getProductType(), + entity.getIssuedDate(), + entity.getSafetyInspectionStandard(), + entity.getUpperItem(), + entity.getItem(), + entity.getPropose(), + entity.getWeight(), + entity.getUsage(), + entity.getUsagePrecaution(), + entity.getFirstAid(), + entity.getMainSubstance(), + entity.getAllergicSubstance(), + entity.getOtherSubstance(), + entity.getPreservative(), + entity.getSurfactant(), + entity.getFluorescentWhitening(), + entity.getManufactureType(), + entity.getManufactureMethod(), + entity.getManufactureNation(), + entity.getViolationInfo(), + entity.getCreatedAt(), + entity.getCreatedBy(), + entity.getModifiedAt(), + entity.getModifiedBy() + ); + } +} \ No newline at end of file diff --git a/module-api/src/main/java/com/kernel360/product/dto/ProductDto.java b/module-api/src/main/java/com/kernel360/product/dto/ProductDto.java index 0feccb33..3ae97ccc 100644 --- a/module-api/src/main/java/com/kernel360/product/dto/ProductDto.java +++ b/module-api/src/main/java/com/kernel360/product/dto/ProductDto.java @@ -2,22 +2,26 @@ import com.kernel360.product.entity.Product; import com.kernel360.product.entity.SafetyStatus; + import java.time.LocalDate; /** * DTO for {@link com.kernel360.product.entity.Product} */ -public record ProductDto(Long productNo, - String productName, - String barcode, - String imageSource, - String reportNumber, - SafetyStatus safetyStatus, - Integer viewCount, - LocalDate createdAt, - String createdBy, - LocalDate modifiedAt, - String modifiedBy +public record ProductDto( + Long productNo, + String productName, + String barcode, + String imageSource, + String reportNumber, + SafetyStatus safetyStatus, + Integer viewCount, + String brand, + String upperItem, + LocalDate createdAt, + String createdBy, + LocalDate modifiedAt, + String modifiedBy ) { public static ProductDto of( @@ -27,6 +31,8 @@ public static ProductDto of( String imageSource, String reportNumber, SafetyStatus safetyStatus, + String brand, + String upperItem, Integer viewCount, LocalDate createdAt, String createdBy, @@ -41,6 +47,8 @@ public static ProductDto of( reportNumber, safetyStatus, viewCount, + brand, + upperItem, createdAt, createdBy, modifiedAt, @@ -53,9 +61,11 @@ public static ProductDto from(Product entity) { entity.getProductNo(), entity.getProductName(), entity.getBarcode(), - entity.getImage(), + entity.getImageSource(), entity.getReportNumber(), entity.getSafetyStatus(), + entity.getCompanyName(), + entity.getUpperItem(), entity.getViewCount(), entity.getCreatedAt(), entity.getCreatedBy(), @@ -63,4 +73,11 @@ public static ProductDto from(Product entity) { entity.getModifiedBy() ); } + + public Product toEntity() { + return Product.of( + productNo, + productName + ); + } } diff --git a/module-api/src/main/java/com/kernel360/product/service/ProductService.java b/module-api/src/main/java/com/kernel360/product/service/ProductService.java index 50bd33d3..5b756bd6 100644 --- a/module-api/src/main/java/com/kernel360/product/service/ProductService.java +++ b/module-api/src/main/java/com/kernel360/product/service/ProductService.java @@ -1,80 +1,110 @@ package com.kernel360.product.service; import com.kernel360.exception.BusinessException; -import com.kernel360.main.code.ProductsErrorCode; +import com.kernel360.likes.repository.LikeRepository; import com.kernel360.main.dto.RecommendProductsDto; +import com.kernel360.product.code.ProductsErrorCode; +import com.kernel360.product.dto.ProductDetailDto; import com.kernel360.product.dto.ProductDto; import com.kernel360.product.entity.Product; import com.kernel360.product.entity.SafetyStatus; import com.kernel360.product.repository.ProductRepository; + +import java.util.List; + import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.List; - @Service @RequiredArgsConstructor public class ProductService { private final ProductRepository productRepository; + private final LikeRepository likeRepository; @Transactional(readOnly = true) - public ProductDto getProductById(Long id) { + public List getProducts() { - return productRepository.findById(id) + return productRepository.findAll() + .stream() .map(ProductDto::from) - .orElseThrow(() -> new BusinessException(ProductsErrorCode.NOT_FOUND_PRODUCT)); + .toList(); + } + + @Transactional(readOnly = true) + public Page getProductsByKeyword(String keyword, Pageable pageable) { + Page products = productRepository.findByProductNameContaining(keyword, pageable); + return products.map(ProductDto::from); } @Transactional(readOnly = true) - public List getProductList() { + public Page getProductListOrderByViewCount(Pageable pageable) { - return productRepository.findAll() - .stream() - .map(ProductDto::from) - .toList(); + return productRepository.findAllByOrderByViewCountDesc(pageable).map(ProductDto::from); } @Transactional(readOnly = true) - public List getProductListByKeyword(String keyword) { - List products = productRepository.findByKeyword(keyword); + public Page getRecommendProducts(Pageable pageable) { + Page productList = productRepository.findTop5ByOrderByProductNameDesc(pageable); - return products.stream().map(ProductDto::from).toList(); + return productList.map(RecommendProductsDto::from); } @Transactional(readOnly = true) - public List getProductListOrderByViewCount() { - List products = productRepository.findTop5ByOrderByProductNameDesc(); -// List products = productRepository.findAllByOrderByViewCountDesc(); - //FIXME:: viewCount 값이 존재하지 않아,제품이룸순 데이터를 샘플로 전달한 후 향후 변경 + public Page getViolationProducts(Pageable pageable) { - return products.stream().map(ProductDto::from).toList(); + return productRepository.findAllBySafetyStatusEquals(SafetyStatus.DANGER, pageable) + .map(ProductDto::from); } @Transactional(readOnly = true) - public List getRecommendProductList() { - List productList = productRepository.findTop5ByOrderByProductNameDesc(); + public Page getRecentProducts(Pageable pageable) { + + return productRepository.findAllByOrderByCreatedAtDesc(pageable) + .map(ProductDto::from); - return productList.stream().map(RecommendProductsDto::from).toList(); } @Transactional(readOnly = true) - public List getViolationProducts() { + public ProductDetailDto getProductById(Long id) { - return productRepository.findAllBySafetyStatusEquals(SafetyStatus.DANGER) - .stream() - .map(ProductDto::from) - .toList(); + return productRepository.findById(id) + .map(ProductDetailDto::from) + .orElseThrow(() -> new BusinessException(ProductsErrorCode.NOT_FOUND_PRODUCT)); + } + + @Transactional + public void updateViewCount(Long id) { + productRepository.updateViewCount(id); } @Transactional(readOnly = true) - public List getRecentProducts() { + public Page getProductByOCR(String reportNo, Pageable pageable) { - return productRepository.findAllByOrderByCreatedAtDesc() - .stream() - .map(ProductDto::from) + return productRepository.findProductByReportNumberEquals(reportNo, pageable) + .map(ProductDetailDto::from); + } + + public Page getFavoriteProducts(Pageable pageable) { + Page results = likeRepository.findTop20ByProductNoOrderByLikeCountDesc(pageable); + + List productDtos = results.getContent().stream() + .map(result -> { + Long productNo = (Long) result[0]; + Product product = productRepository.findById(productNo) + .orElseThrow(() -> new BusinessException(ProductsErrorCode.NOT_FOUND_PRODUCT)); + return ProductDto.from(product); + }) .toList(); + + return new PageImpl<>(productDtos, pageable, results.getTotalElements()); } + + + } diff --git a/module-api/src/main/java/com/kernel360/review/code/ReviewBusinessCode.java b/module-api/src/main/java/com/kernel360/review/code/ReviewBusinessCode.java new file mode 100644 index 00000000..90571737 --- /dev/null +++ b/module-api/src/main/java/com/kernel360/review/code/ReviewBusinessCode.java @@ -0,0 +1,33 @@ +package com.kernel360.review.code; + +import com.kernel360.code.BusinessCode; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@RequiredArgsConstructor +public enum ReviewBusinessCode implements BusinessCode { + SUCCESS_GET_REVIEWS(HttpStatus.OK.value(), "BRV001", "리뷰 목록 조회 성공"), + SUCCESS_GET_REVIEW(HttpStatus.OK.value(), "BRV002", "리뷰 단건 조회 성공"), + SUCCESS_CREATE_REVIEW(HttpStatus.OK.value(), "BRV003", "리뷰 등록 성공"), + SUCCESS_UPDATE_REVIEW(HttpStatus.OK.value(), "BRV004", "리뷰 수정 성공"), + SUCCESS_DELETE_REVIEW(HttpStatus.OK.value(), "BRV005", "리뷰 삭제 성공"); + + private final int status; + private final String code; + private final String message; + + @Override + public int getStatus() { + return status; + } + + @Override + public String getCode() { + return code; + } + + @Override + public String getMessage() { + return message; + } +} diff --git a/module-api/src/main/java/com/kernel360/review/code/ReviewErrorCode.java b/module-api/src/main/java/com/kernel360/review/code/ReviewErrorCode.java new file mode 100644 index 00000000..6ea8b524 --- /dev/null +++ b/module-api/src/main/java/com/kernel360/review/code/ReviewErrorCode.java @@ -0,0 +1,29 @@ +package com.kernel360.review.code; + +import com.kernel360.code.ErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@RequiredArgsConstructor +public enum ReviewErrorCode implements ErrorCode { + INVALID_STAR_RATING_VALUE(HttpStatus.BAD_REQUEST.value(), "ERV001", "유효하지 않은 별점입니다."); + + private final int status; + private final String code; + private final String message; + + @Override + public int getStatus() { + return status; + } + + @Override + public String getCode() { + return code; + } + + @Override + public String getMessage() { + return message; + } +} diff --git a/module-api/src/main/java/com/kernel360/review/controller/ReviewController.java b/module-api/src/main/java/com/kernel360/review/controller/ReviewController.java new file mode 100644 index 00000000..6ec02f5f --- /dev/null +++ b/module-api/src/main/java/com/kernel360/review/controller/ReviewController.java @@ -0,0 +1,54 @@ +package com.kernel360.review.controller; + +import com.kernel360.response.ApiResponse; +import com.kernel360.review.code.ReviewBusinessCode; +import com.kernel360.review.dto.ReviewDto; +import com.kernel360.review.service.ReviewService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/reviews") +public class ReviewController { + + private final ReviewService reviewService; + + @GetMapping("/product/{productNo}") + public ResponseEntity>> getReviewsByProduct(@PathVariable Long productNo) { + List reviews = reviewService.getReviewsByProduct(productNo); + + return ApiResponse.toResponseEntity(ReviewBusinessCode.SUCCESS_GET_REVIEWS, reviews); + } + + @GetMapping("/{reviewNo}") + public ResponseEntity> getReview(@PathVariable Long reviewNo) { + ReviewDto review = reviewService.getReview(reviewNo); + + return ApiResponse.toResponseEntity(ReviewBusinessCode.SUCCESS_GET_REVIEW, review); + } + + @PostMapping("") + public ResponseEntity> createReview(@RequestBody ReviewDto reviewDto) { + reviewService.createReview(reviewDto); + + return ApiResponse.toResponseEntity(ReviewBusinessCode.SUCCESS_CREATE_REVIEW); + } + + @PatchMapping("") + public ResponseEntity> updateReview(@RequestBody ReviewDto reviewDto) { + reviewService.updateReview(reviewDto); + + return ApiResponse.toResponseEntity(ReviewBusinessCode.SUCCESS_UPDATE_REVIEW); + } + + @DeleteMapping("/{reviewNo}") + public ResponseEntity> deleteReview(@PathVariable Long reviewNo) { + reviewService.deleteReview(reviewNo); + + return ApiResponse.toResponseEntity(ReviewBusinessCode.SUCCESS_DELETE_REVIEW); + } +} diff --git a/module-api/src/main/java/com/kernel360/review/dto/ReviewDto.java b/module-api/src/main/java/com/kernel360/review/dto/ReviewDto.java new file mode 100644 index 00000000..06579d25 --- /dev/null +++ b/module-api/src/main/java/com/kernel360/review/dto/ReviewDto.java @@ -0,0 +1,75 @@ +package com.kernel360.review.dto; + +import com.kernel360.member.dto.MemberDto; +import com.kernel360.product.dto.ProductDto; +import com.kernel360.review.entity.Review; + +import java.math.BigDecimal; +import java.time.LocalDate; + +/** + * DTO for {@link com.kernel360.review.dto.ReviewDto} + */ +public record ReviewDto(Long reviewNo, + ProductDto productDto, + MemberDto memberDto, + BigDecimal starRating, + String title, + String contents, + LocalDate createdAt, + String createdBy, + LocalDate modifiedAt, + String modifiedBy) { + + public static ReviewDto of( + Long reviewNo, + ProductDto productDto, + MemberDto memberDto, + BigDecimal starRating, + String title, + String contents, + LocalDate createdAt, + String createdBy, + LocalDate modifiedAt, + String modifiedBy + ) { + return new ReviewDto( + reviewNo, + productDto, + memberDto, + starRating, + title, + contents, + createdAt, + createdBy, + modifiedAt, + modifiedBy + ); + } + + public static ReviewDto from(Review entity) { + return ReviewDto.of( + entity.getReviewNo(), + ProductDto.from(entity.getProduct()), + MemberDto.from(entity.getMember()), + entity.getStarRating(), + entity.getTitle(), + entity.getContents(), + entity.getCreatedAt(), + entity.getCreatedBy(), + entity.getModifiedAt(), + entity.getModifiedBy() + ); + } + + public Review toEntity() { + return Review.of( + reviewNo, + productDto.toEntity(), + memberDto.toEntity(), + starRating, + title, + contents + ); + } +} \ No newline at end of file diff --git a/module-api/src/main/java/com/kernel360/review/service/ReviewService.java b/module-api/src/main/java/com/kernel360/review/service/ReviewService.java new file mode 100644 index 00000000..86391415 --- /dev/null +++ b/module-api/src/main/java/com/kernel360/review/service/ReviewService.java @@ -0,0 +1,66 @@ +package com.kernel360.review.service; + +import com.kernel360.exception.BusinessException; +import com.kernel360.review.code.ReviewErrorCode; +import com.kernel360.review.dto.ReviewDto; +import com.kernel360.review.entity.Review; +import com.kernel360.review.repository.ReviewRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class ReviewService { + + private final ReviewRepository reviewRepository; + + private static final double MAX_STAR_RATING = 5.0; + + @Transactional(readOnly = true) + public List getReviewsByProduct(Long productNo) { + + return reviewRepository.findAllByProduct_ProductNo(productNo) + .stream().map(ReviewDto::from) + .toList(); + } + + @Transactional(readOnly = true) + public ReviewDto getReview(Long reviewNo) { + + return ReviewDto.from(reviewRepository.findByReviewNo(reviewNo)); + } + + @Transactional + public Review createReview(ReviewDto reviewDto) { + isValidStarRating(reviewDto.starRating()); + + return reviewRepository.save(reviewDto.toEntity()); + } + + + @Transactional + public void updateReview(ReviewDto reviewDto) { + isValidStarRating(reviewDto.starRating()); + + reviewRepository.save(reviewDto.toEntity()); + } + + @Transactional + public void deleteReview(Long reviewNo) { + reviewRepository.deleteById(reviewNo); + } + + private static void isValidStarRating(BigDecimal starRating) { + if (BigDecimal.ZERO.compareTo(starRating) > 0) { + throw new BusinessException(ReviewErrorCode.INVALID_STAR_RATING_VALUE); + } + + if (BigDecimal.valueOf(MAX_STAR_RATING).compareTo(starRating) < 0) { + throw new BusinessException(ReviewErrorCode.INVALID_STAR_RATING_VALUE); + } + } +} diff --git a/module-api/src/main/java/com/kernel360/washzone/code/WashZoneBusinessCode.java b/module-api/src/main/java/com/kernel360/washzone/code/WashZoneBusinessCode.java new file mode 100644 index 00000000..1968190e --- /dev/null +++ b/module-api/src/main/java/com/kernel360/washzone/code/WashZoneBusinessCode.java @@ -0,0 +1,34 @@ +package com.kernel360.washzone.code; + + +import com.kernel360.code.BusinessCode; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@RequiredArgsConstructor +public enum WashZoneBusinessCode implements BusinessCode { + + SUCCESS_REQUEST_WASH_ZONE_INFO(HttpStatus.CREATED.value(), "BWZC001", "Wash Zone 정보조회 성공"), + SUCCESS_SAVED_WASH_ZONE_INFO(HttpStatus.CREATED.value(), "BWZC002", "Wash Zone 정보등록 성공"); + + + private final int status; + private final String code; + private final String message; + + @Override + public int getStatus() { + return status; + } + + @Override + public String getCode() { + return code; + } + + @Override + public String getMessage() { + return message; + } +} + diff --git a/module-api/src/main/java/com/kernel360/washzone/code/WashZoneErrorCode.java b/module-api/src/main/java/com/kernel360/washzone/code/WashZoneErrorCode.java new file mode 100644 index 00000000..e763165f --- /dev/null +++ b/module-api/src/main/java/com/kernel360/washzone/code/WashZoneErrorCode.java @@ -0,0 +1,33 @@ +package com.kernel360.washzone.code; + + +import com.kernel360.code.ErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + + +@RequiredArgsConstructor +public enum WashZoneErrorCode implements ErrorCode { + + FAILED_NOT_MAPPING_ORDINAL_TO_NAME(HttpStatus.INTERNAL_SERVER_ERROR.value(), "EWZC001", "요청 데이터가 존재하지 않습니다."); + + private final int status; + private final String code; + private final String message; + + @Override + public int getStatus() { + return status; + } + + @Override + public String getCode() { + return code; + } + + @Override + public String getMessage() { + return message; + } +} + diff --git a/module-api/src/main/java/com/kernel360/washzone/controller/WashZoneController.java b/module-api/src/main/java/com/kernel360/washzone/controller/WashZoneController.java new file mode 100644 index 00000000..b2449732 --- /dev/null +++ b/module-api/src/main/java/com/kernel360/washzone/controller/WashZoneController.java @@ -0,0 +1,45 @@ +package com.kernel360.washzone.controller; + +import com.kernel360.response.ApiResponse; +import com.kernel360.washzone.code.WashZoneBusinessCode; +import com.kernel360.washzone.dto.KakaoMapDto; +import com.kernel360.washzone.dto.WashZoneDto; +import com.kernel360.washzone.service.WashZoneService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/washzones") +@RequiredArgsConstructor +public class WashZoneController { + + private final WashZoneService washZoneService; + + @GetMapping + public ResponseEntity>> getWashZones(@ModelAttribute KakaoMapDto kakaoMapDto) { + List washZones = washZoneService.getWashZones(kakaoMapDto); + + return ApiResponse.toResponseEntity(WashZoneBusinessCode.SUCCESS_REQUEST_WASH_ZONE_INFO, washZones); + } + + @GetMapping("/search") + public ResponseEntity>> getWashZoneByKeyword( + @RequestParam("keyword") String keyword, Pageable pageable){ + Page washZoneDtos = washZoneService.getWashZonesByKeyword(keyword, pageable); + + return ApiResponse.toResponseEntity(WashZoneBusinessCode.SUCCESS_REQUEST_WASH_ZONE_INFO, washZoneDtos); + } + + @PostMapping + public ResponseEntity> registerWashZone(@RequestBody WashZoneDto washZoneDto){ + WashZoneDto savedWashZoneDto = WashZoneDto.from(washZoneService.save(washZoneDto)); + + return ApiResponse.toResponseEntity(WashZoneBusinessCode.SUCCESS_SAVED_WASH_ZONE_INFO, savedWashZoneDto); + } + +} \ No newline at end of file diff --git a/module-api/src/main/java/com/kernel360/washzone/dto/KakaoMapDto.java b/module-api/src/main/java/com/kernel360/washzone/dto/KakaoMapDto.java new file mode 100644 index 00000000..bfdf8766 --- /dev/null +++ b/module-api/src/main/java/com/kernel360/washzone/dto/KakaoMapDto.java @@ -0,0 +1,10 @@ +package com.kernel360.washzone.dto; + +public record KakaoMapDto( + Double minX, + Double minY, + Double maxX, + Double maxY, + Integer level +) { +} diff --git a/module-api/src/main/java/com/kernel360/washzone/dto/WashZoneDto.java b/module-api/src/main/java/com/kernel360/washzone/dto/WashZoneDto.java new file mode 100644 index 00000000..a17a03d5 --- /dev/null +++ b/module-api/src/main/java/com/kernel360/washzone/dto/WashZoneDto.java @@ -0,0 +1,55 @@ +package com.kernel360.washzone.dto; + +import com.kernel360.washzone.entity.WashZone; + + +/** + * DTO for {@link com.kernel360.washzone.entity.WashZone} + */ +public record WashZoneDto( + String name, + String address, + Double latitude, + Double longitude, + String type, + String remarks) { + + public static WashZoneDto of ( + String name, + String address, + Double latitude, + Double longitude, + String type, + String remarks + ){ + return new WashZoneDto( + name, + address, + latitude, + longitude, + type, + remarks); + } + public static WashZoneDto from(WashZone washZone) { + + return WashZoneDto.of( + washZone.getName(), + washZone.getAddress(), + washZone.getLatitude(), + washZone.getLongitude(), + washZone.getType(), + washZone.getRemarks()); + } + + public WashZone toEntity(){ + + return WashZone.of( + this.name, + this.address, + this.latitude, + this.longitude, + this.type, + this.remarks + ); + } +} \ No newline at end of file diff --git a/module-api/src/main/java/com/kernel360/washzone/service/WashZoneService.java b/module-api/src/main/java/com/kernel360/washzone/service/WashZoneService.java new file mode 100644 index 00000000..8171f726 --- /dev/null +++ b/module-api/src/main/java/com/kernel360/washzone/service/WashZoneService.java @@ -0,0 +1,43 @@ +package com.kernel360.washzone.service; + +import com.kernel360.washzone.dto.KakaoMapDto; +import com.kernel360.washzone.dto.WashZoneDto; +import com.kernel360.washzone.entity.WashZone; +import com.kernel360.washzone.repository.WashZoneRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class WashZoneService { + + private final WashZoneRepository washZoneRepository; + + @Transactional(readOnly = true) + public List getWashZones(KakaoMapDto kakaoMapDto) { + + return + washZoneRepository.findWashZonesWithinBounds( + kakaoMapDto.minX(), kakaoMapDto.maxY(), kakaoMapDto.minY(), kakaoMapDto.maxY()) + .stream().map(WashZoneDto::from) + .toList(); + } + + @Transactional(readOnly = true) + public Page getWashZonesByKeyword(String keyword, Pageable pageable) { + + return washZoneRepository.findByKeyword(keyword, pageable).map(WashZoneDto::from); + } + + @Transactional + public WashZone save(WashZoneDto washZoneDto) { + + return washZoneRepository.save(washZoneDto.toEntity()); + } +} + diff --git a/module-api/src/main/resources/application-dev.yml b/module-api/src/main/resources/application-dev.yml new file mode 100644 index 00000000..e6fea07d --- /dev/null +++ b/module-api/src/main/resources/application-dev.yml @@ -0,0 +1,77 @@ +spring: + datasource: + driver-class-name: org.postgresql.Driver + url: ${DB_URL} + username: ${DB_USER} + password: ${DB_PW} + jpa: + show-sql: true + properties: + hibernate: + format_sql: true + dialect: org.hibernate.dialect.PostgreSQLDialect + ddl-auto: validate + flyway: + enabled: true + baseline-on-migrate: true + #JPA보다 flyway 우선 실행 + autoconfigure: + exclude: org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration + + mail: + host: smtp.gmail.com + username: ${SMTP_GOOGLE_EMAIL} + password: ${SMTP_PASSWORD} + port: 587 + properties: + mail: + smtp: + auth: true + connectiontimeout: 5000 + starttls: + enable: true + timeout: 5000 + writetimeout: 5000 + + data: + redis: + port: 6379 + lettuce: + pool: + max-active: 20 + max-idle: 20 + host: ${REDIS_DEV_HOST} + password: ${REDIS_DEV_PASSWORD} + +logging: + file: + path: ./logs/api/ + name: dev-api-log + logback: + rollingpolicy: + max-history: 14 + max-file-size: 50MB + total-size-cap: 5GB + +management: + endpoints: + web: + exposure: + include: "*" + endpoint: + health: + show-details: always + +constants: + host-url: ${DEV_HOST_URL} + password-reset-token: + duration-minute: 5 + +aws: + credentials: + access-key-id: ${AWS_ACCESS_KEY} + secret-access-key: ${AWS_SECRET_KEY} + s3: + bucket: + name: ${AWS_S3_BUCKET_NAME} + url: ${AWS_S3_BUCKET_URL} \ No newline at end of file diff --git a/module-api/src/main/resources/application-local.yml b/module-api/src/main/resources/application-local.yml index e7e846fb..323c548b 100644 --- a/module-api/src/main/resources/application-local.yml +++ b/module-api/src/main/resources/application-local.yml @@ -15,16 +15,69 @@ spring: flyway: enabled: true baseline-on-migrate: true + validate-on-migrate: false #JPA보다 flyway 우선 실행 autoconfigure: exclude: org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration + mail: + host: smtp.gmail.com + username: ENC(TEY+McqfBOprewh0EpDJIclJKk8o+bY/0CWpPynWeylc/HNeWGSI6Q==) + password: ENC(gAVZp58ZhI0MCJcvKFhB0lVNYPtMg35QIslSCJhu2pJB4ck+w1vscA==) + port: 587 + properties: + mail: + smtp: + auth: true + connectiontimeout: 5000 + starttls: + enable: true + timeout: 5000 + writetimeout: 5000 + + data: + redis: + port: 6379 + lettuce: + pool: + max-active: 20 + max-idle: 20 + host: 127.0.0.1 + password: wash1234 + +jasypt: + encryptor: + algorithm: PBEWithMD5AndTripleDES + logging: file: path: ./logs/api/ - name: api-log + name: local-api-log logback: rollingpolicy: max-history: 14 max-file-size: 50MB - total-size-cap: 5GB \ No newline at end of file + total-size-cap: 5GB + +management: + endpoints: + web: + exposure: + include: "*" + endpoint: + health: + show-details: always + +constants: + host-url: "http://localhost:8080" + password-reset-token: + duration-minute: 5 + +aws: + credentials: + access-key-id: ENC(vx2H0E5tIohXqzynDez9M7Kn3Vjj0t3sS3Wq6uyN5kgYeXhDyIylOA==) + secret-access-key: ENC(m/2qCJve4UP86W7Q0ZQhKSDGBnltnbGOnso7d/9wiNovq1Mzyto608rUnCQ+AhqUThpClK1cmxSOj6WHfAbZTQ==) + s3: + bucket: + name: ENC(JQIi11b8LB+99FnX02wCGwdXTOEax3VkuzgNqAVshK4=) + url: ENC(9P2gRaZoGkR4SCgoTS/6sEQP0kVWwVWFaDckr1/FUoRV1MPnGXQL6OJKsGlHegk8h1d69uFDKTuZpLntfyn3nVMXLz18t8ls) \ No newline at end of file diff --git a/module-api/src/main/resources/application-prod.yml b/module-api/src/main/resources/application-prod.yml new file mode 100644 index 00000000..983e1954 --- /dev/null +++ b/module-api/src/main/resources/application-prod.yml @@ -0,0 +1,77 @@ +spring: + datasource: + driver-class-name: org.postgresql.Driver + url: ${DB_URL_AWS} + username: ${DB_USER} + password: ${DB_PW} + jpa: + show-sql: true + properties: + hibernate: + format_sql: true + dialect: org.hibernate.dialect.PostgreSQLDialect + ddl-auto: validate + flyway: + enabled: true + baseline-on-migrate: true + #JPA보다 flyway 우선 실행 + autoconfigure: + exclude: org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration + + mail: + host: smtp.gmail.com + username: ${SMTP_GOOGLE_EMAIL} + password: ${SMTP_PASSWORD} + port: 587 + properties: + mail: + smtp: + auth: true + connectiontimeout: 5000 + starttls: + enable: true + timeout: 5000 + writetimeout: 5000 + + data: + redis: + port: 6379 + lettuce: + pool: + max-active: 20 + max-idle: 20 + host: ${REDIS_PROD_HOST} + password: ${REDIS_PROD_PASSWORD} + +logging: + file: + path: ./logs/api/ + name: api-log + logback: + rollingpolicy: + max-history: 14 + max-file-size: 50MB + total-size-cap: 5GB + +management: + endpoints: + web: + exposure: + include: "*" + endpoint: + health: + show-details: always + +constants: + host-url: ${PROD_HOST_URL} + password-reset-token: + duration-minute: 5 + +aws: + credentials: + access-key-id: ${AWS_ACCESS_KEY} + secret-access-key: ${AWS_SECRET_KEY} + s3: + bucket: + name: ${AWS_S3_BUCKET_NAME} + url: ${AWS_S3_BUCKET_URL} \ No newline at end of file diff --git a/module-api/src/main/resources/application.yml b/module-api/src/main/resources/application.yml index edbcd3b7..cc7635e9 100644 --- a/module-api/src/main/resources/application.yml +++ b/module-api/src/main/resources/application.yml @@ -1,9 +1,7 @@ spring: profiles: active: local - -jasypt: - encryptor: - algorithm: PBEWithMD5AndTripleDES - - + servlet: + multipart: + max-file-size: 50MB + max-request-size: 500MB \ No newline at end of file diff --git a/module-api/src/main/resources/db/migration/V1.0.0__init.sql b/module-api/src/main/resources/db/migration/V1.0.0__init.sql index 1021327c..e5201d32 100644 --- a/module-api/src/main/resources/db/migration/V1.0.0__init.sql +++ b/module-api/src/main/resources/db/migration/V1.0.0__init.sql @@ -1,38 +1,55 @@ -CREATE TABLE if not exists Member +CREATE TABLE if not exists Common_Code ( - member_no SERIAL PRIMARY KEY, - id VARCHAR NOT NULL, - email VARCHAR NOT NULL, - password VARCHAR NOT NULL, - gender CHAR, - birthdate DATE, - created_at DATE NOT NULL, - created_by VARCHAR NOT NULL, - modified_at DATE, - modified_by VARCHAR + code_no BIGINT NOT NULL, + code_name varchar NOT NULL, + upper_no BIGINT NULL, + upper_name varchar NULL, + sort_order int4 NOT NULL, + is_used bool NOT NULL DEFAULT true, + description varchar NULL, + sub_description varchar null, + created_at date NOT NULL, + created_by varchar NOT NULL, + modified_at date NULL, + modified_by varchar NULL ); -CREATE TABLE if not exists Brand + +CREATE TABLE if not exists Auth +( + auth_no BIGSERIAL NOT NULL, + member_no BIGINT NULL, + jwt_token varchar NULL, + sns_token varchar NULL, + created_at date NOT NULL, + created_by varchar NOT NULL, + modified_at date NULL, + modified_by varchar NULL +); + + +CREATE TABLE if not exists Member ( - brand_no SERIAL PRIMARY KEY, - brand_name VARCHAR NOT NULL, - description VARCHAR, - nation_name VARCHAR, + member_no BIGSERIAL PRIMARY KEY, + id VARCHAR NOT NULL UNIQUE, + email BYTEA NOT NULL UNIQUE, + password BYTEA NOT NULL, + gender int, + age int, created_at DATE NOT NULL, created_by VARCHAR NOT NULL, modified_at DATE, modified_by VARCHAR ); + CREATE TABLE if not exists Wash_Info ( - wash_no SERIAL PRIMARY KEY, - member_no INT NOT NULL, + wash_no BIGSERIAL PRIMARY KEY, + member_no BIGINT NOT NULL, wash_count INT, monthly_expense INT, - interest CHAR, - driving_env CHAR, - parking_env CHAR, + interest INT, created_at DATE NOT NULL, created_by VARCHAR NOT NULL, modified_at DATE, @@ -40,50 +57,78 @@ CREATE TABLE if not exists Wash_Info FOREIGN KEY (member_no) REFERENCES Member (member_no) ); + CREATE TABLE if not exists Car_Info ( - car_no SERIAL PRIMARY KEY, - member_no INT NOT NULL, - car_brand CHAR, - car_type CHAR, - car_size CHAR, - pearl BOOLEAN, - clear_coat BOOLEAN, + car_no BIGSERIAL PRIMARY KEY, + member_no BIGINT NOT NULL, + car_type INT, + car_size INT, + car_color INT, + driving_env INT, + parking_env INT, created_at DATE NOT NULL, created_by VARCHAR NOT NULL, modified_at DATE, - modified_by CHAR, + modified_by VARCHAR, FOREIGN KEY (member_no) REFERENCES Member (member_no) ); -CREATE TABLE if not exists Product + +CREATE TABLE if not exists Brand ( - product_no SERIAL PRIMARY KEY, - brand_no INT NOT NULL, - product_name VARCHAR NOT NULL, - barcode VARCHAR, + brand_no BIGSERIAL PRIMARY KEY, + brand_name VARCHAR NOT NULL, + company_name VARCHAR NOT NULL, description VARCHAR, - declare_no VARCHAR NOT NULL, - is_violation BOOLEAN NOT NULL DEFAULT false, + nation_name VARCHAR, created_at DATE NOT NULL, created_by VARCHAR NOT NULL, modified_at DATE, - modified_by VARCHAR, - view_count INT NOT NULL DEFAULT 0, - FOREIGN KEY (brand_no) REFERENCES Brand (brand_no) + modified_by VARCHAR ); -CREATE TABLE if not exists common_code + +CREATE TABLE if not exists product ( - code_no int4 NOT NULL, - code_name varchar NOT NULL, - upper_no int4 NULL, - upper_name varchar NULL, - sort_order int4 NOT NULL, - is_used bool NOT NULL DEFAULT true, - description varchar NULL, - created_at date NOT NULL, - created_by varchar NOT NULL, - modified_at date NULL, - modified_by varchar NULL -); \ No newline at end of file + product_no BIGSERIAL PRIMARY KEY, + created_at date NOT NULL, + created_by VARCHAR(255) NOT NULL, + modified_at date, + modified_by VARCHAR(255), + product_name VARCHAR(255) NOT NULL, + report_no VARCHAR(255), + product_type VARCHAR(255), + manufacture_nation VARCHAR(255), + company_name VARCHAR(255), + safety_status INTEGER NOT NULL, + issued_date date NOT NULL, + barcode VARCHAR(255), + img_src VARCHAR(255), + view_count INTEGER NOT NULL, + safety_inspection_standard TEXT, + upper_item VARCHAR(255), + item VARCHAR(255), + propose TEXT, + weight VARCHAR(255), + usage TEXT, + usage_precaution TEXT, + first_aid TEXT, + main_substance TEXT, + allergic_substance TEXT, + other_substance TEXT, + preservative TEXT, + surfactant TEXT, + fluorescent_whitening TEXT, + manufacture_type VARCHAR(255), + manufacture_method VARCHAR(255), + violation_info VARCHAR(255) -- 위반 여부 정보 별첨, 이 값이 있으면 주의해야 할 제품 -- +); + + +alter sequence auth_auth_no_seq increment by 50; +alter sequence member_member_no_seq increment by 50; +alter sequence wash_info_wash_no_seq increment by 50; +alter sequence car_info_car_no_seq increment by 50; +alter sequence brand_brand_no_seq increment by 50; +alter sequence product_product_no_seq increment by 50; \ No newline at end of file diff --git a/module-api/src/main/resources/db/migration/V1.0.1__alter_seq_increment.sql b/module-api/src/main/resources/db/migration/V1.0.1__alter_seq_increment.sql deleted file mode 100644 index 41797fd5..00000000 --- a/module-api/src/main/resources/db/migration/V1.0.1__alter_seq_increment.sql +++ /dev/null @@ -1,5 +0,0 @@ -alter sequence brand_brand_no_seq increment by 50; -alter sequence car_info_car_no_seq increment by 50; -alter sequence member_member_no_seq increment by 50; -alter sequence wash_info_wash_no_seq increment by 50; -alter sequence product_product_no_seq increment by 50; \ No newline at end of file diff --git a/module-api/src/main/resources/db/migration/V1.0.1__init_batch_tables.sql b/module-api/src/main/resources/db/migration/V1.0.1__init_batch_tables.sql new file mode 100644 index 00000000..66edf649 --- /dev/null +++ b/module-api/src/main/resources/db/migration/V1.0.1__init_batch_tables.sql @@ -0,0 +1,192 @@ +-- Autogenerated: do not edit this file + +CREATE TABLE if not exists BATCH_JOB_INSTANCE +( + JOB_INSTANCE_ID BIGINT NOT NULL PRIMARY KEY, + VERSION BIGINT, + JOB_NAME VARCHAR(100) NOT NULL, + JOB_KEY VARCHAR(32) NOT NULL, + constraint JOB_INST_UN unique (JOB_NAME, JOB_KEY) +); + + +CREATE TABLE if not exists BATCH_JOB_EXECUTION +( + JOB_EXECUTION_ID BIGINT NOT NULL PRIMARY KEY, + VERSION BIGINT, + JOB_INSTANCE_ID BIGINT NOT NULL, + CREATE_TIME TIMESTAMP NOT NULL, + START_TIME TIMESTAMP DEFAULT NULL, + END_TIME TIMESTAMP DEFAULT NULL, + STATUS VARCHAR(10), + EXIT_CODE VARCHAR(2500), + EXIT_MESSAGE VARCHAR(2500), + LAST_UPDATED TIMESTAMP, + constraint JOB_INST_EXEC_FK foreign key (JOB_INSTANCE_ID) + references BATCH_JOB_INSTANCE (JOB_INSTANCE_ID) +); + + +CREATE TABLE if not exists BATCH_JOB_EXECUTION_PARAMS +( + JOB_EXECUTION_ID BIGINT NOT NULL, + PARAMETER_NAME VARCHAR(100) NOT NULL, + PARAMETER_TYPE VARCHAR(100) NOT NULL, + PARAMETER_VALUE VARCHAR(2500), + IDENTIFYING CHAR(1) NOT NULL, + constraint JOB_EXEC_PARAMS_FK foreign key (JOB_EXECUTION_ID) + references BATCH_JOB_EXECUTION (JOB_EXECUTION_ID) +); + + +CREATE TABLE if not exists BATCH_STEP_EXECUTION +( + STEP_EXECUTION_ID BIGINT NOT NULL PRIMARY KEY, + VERSION BIGINT NOT NULL, + STEP_NAME VARCHAR(100) NOT NULL, + JOB_EXECUTION_ID BIGINT NOT NULL, + CREATE_TIME TIMESTAMP NOT NULL, + START_TIME TIMESTAMP DEFAULT NULL, + END_TIME TIMESTAMP DEFAULT NULL, + STATUS VARCHAR(10), + COMMIT_COUNT BIGINT, + READ_COUNT BIGINT, + FILTER_COUNT BIGINT, + WRITE_COUNT BIGINT, + READ_SKIP_COUNT BIGINT, + WRITE_SKIP_COUNT BIGINT, + PROCESS_SKIP_COUNT BIGINT, + ROLLBACK_COUNT BIGINT, + EXIT_CODE VARCHAR(2500), + EXIT_MESSAGE VARCHAR(2500), + LAST_UPDATED TIMESTAMP, + constraint JOB_EXEC_STEP_FK foreign key (JOB_EXECUTION_ID) + references BATCH_JOB_EXECUTION (JOB_EXECUTION_ID) +); + + +CREATE TABLE if not exists BATCH_STEP_EXECUTION_CONTEXT +( + STEP_EXECUTION_ID BIGINT NOT NULL PRIMARY KEY, + SHORT_CONTEXT VARCHAR(2500) NOT NULL, + SERIALIZED_CONTEXT TEXT, + constraint STEP_EXEC_CTX_FK foreign key (STEP_EXECUTION_ID) + references BATCH_STEP_EXECUTION (STEP_EXECUTION_ID) +); + + +CREATE TABLE if not exists BATCH_JOB_EXECUTION_CONTEXT +( + JOB_EXECUTION_ID BIGINT NOT NULL PRIMARY KEY, + SHORT_CONTEXT VARCHAR(2500) NOT NULL, + SERIALIZED_CONTEXT TEXT, + constraint JOB_EXEC_CTX_FK foreign key (JOB_EXECUTION_ID) + references BATCH_JOB_EXECUTION (JOB_EXECUTION_ID) +); + + +CREATE SEQUENCE if not exists BATCH_STEP_EXECUTION_SEQ MAXVALUE 9223372036854775807 NO CYCLE; +CREATE SEQUENCE if not exists BATCH_JOB_EXECUTION_SEQ MAXVALUE 9223372036854775807 NO CYCLE; +CREATE SEQUENCE if not exists BATCH_JOB_SEQ MAXVALUE 9223372036854775807 NO CYCLE; + + +CREATE TABLE if not exists reported_product +( + mst_id VARCHAR(255) NOT NULL, + inspct_org VARCHAR(255), + prdt_type VARCHAR(255), + issu_date TIMESTAMP WITHOUT TIME ZONE, + renew_yn VARCHAR(255), + safe_sd TEXT, + expr_date TIMESTAMP WITHOUT TIME ZONE, + upper_item VARCHAR(255), + kid_prt_pkg VARCHAR(255), + df VARCHAR(255), + propos TEXT, + dtrb_lmt VARCHAR(255), + wt_bulk VARCHAR(255), + icepnt VARCHAR(255), + stddusqy TEXT, + usmtd TEXT, + us_atnrpt TEXT, + frsaid TEXT, + sum1 TEXT, + sum2 TEXT, + sum3 TEXT, + sum4 TEXT, + sum5 TEXT, + sum6 TEXT, + mf_icm VARCHAR(255), + mf_mthd TEXT, + mf_nation VARCHAR(255), + comp_addr VARCHAR(255), + comp_tel VARCHAR(255), + in_comp_nm VARCHAR(255), + in_comp_addr VARCHAR(255), + in_comp_tel VARCHAR(255), + odm_comp_nm VARCHAR(255), + odm_comp_addr VARCHAR(255), + odm_comp_tel VARCHAR(255), + created_at date, + created_by VARCHAR(255) NOT NULL, + modified_at date, + modified_by VARCHAR(255), + prdt_nm VARCHAR(255), + slfsfcfst_no VARCHAR(255), + item VARCHAR(255), + est_no INTEGER, + reg_date TIMESTAMP WITHOUT TIME ZONE, + comp_nm VARCHAR(255), + CONSTRAINT pk_reportedproduct PRIMARY KEY (mst_id, est_no) +); + + +CREATE TABLE if not exists concerned_product +( + prdt_no VARCHAR(255) PRIMARY KEY, + prdt_name VARCHAR(255) NOT NULL, + slfsfcfst_no VARCHAR(255) NOT NULL, + item VARCHAR(255) NOT NULL, + comp_nm VARCHAR(255) NOT NULL, + inspected_organization VARCHAR(255), + issued_date DATE, + upper_item VARCHAR(255), + product_type VARCHAR(255), + renewed_type VARCHAR(255), + safety_inspection_standard TEXT, + kid_protect_package VARCHAR(255), + manufacture_nation VARCHAR(255), + product_definition VARCHAR(255), + manufacture TEXT, + created_at DATE NOT NULL DEFAULT CURRENT_DATE, + created_by VARCHAR(255) NOT NULL DEFAULT 'admin', + modified_at DATE, + modified_by VARCHAR(255) +); + +--- 위반제품 내역 테이블 생성 +CREATE TABLE if not exists violated_product +( + product_master_no VARCHAR(255), + product_arm_code VARCHAR(255), + product_arm VARCHAR(255), + actioned_date VARCHAR(255), + product_name VARCHAR(255), + company_name VARCHAR(255), + product_arm_code_name VARCHAR(255), + origin_institute VARCHAR(255), + model_name VARCHAR(255), + company_address VARCHAR(255), + violated_cn VARCHAR(255), + product_manufacture_country VARCHAR(255), + act_organization VARCHAR(255), + product_photo_url TEXT, + file_download_url TEXT, + action_cn VARCHAR(255), + etc_info TEXT, + created_at date, + created_by VARCHAR(255) NOT NULL, + modified_at date, + modified_by VARCHAR(255), + CONSTRAINT pk_violated_product PRIMARY KEY (product_master_no, product_arm_code) +); \ No newline at end of file diff --git a/module-api/src/main/resources/db/migration/V1.0.2__add_member_view_with_secure.sql b/module-api/src/main/resources/db/migration/V1.0.2__add_member_view_with_secure.sql new file mode 100644 index 00000000..6456f274 --- /dev/null +++ b/module-api/src/main/resources/db/migration/V1.0.2__add_member_view_with_secure.sql @@ -0,0 +1,135 @@ +-- AES 암호화를 위한 확장 모듈 설치 +CREATE EXTENSION IF NOT EXISTS pgcrypto; + + +-- 테이블 복호화 함수 +CREATE OR REPLACE FUNCTION washpedia_member_decrypt() RETURNS TRIGGER AS +$$ +BEGIN + -- 비밀번호 복호화 + NEW.password := pgp_sym_decrypt(NEW.password, 'changeRequired'); + -- 이메일 복호화 + NEW.email := pgp_sym_decrypt(NEW.email, 'changeRequired'); + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + + +-- 테이블 복호화 트리거 insert, update 이벤트 후 member_view 업데이트 +DROP TRIGGER IF EXISTS washpedia_member_decrypt_trigger ON member; + +CREATE TRIGGER washpedia_member_decrypt_trigger + AFTER INSERT OR UPDATE + ON member + FOR EACH ROW +EXECUTE FUNCTION washpedia_member_decrypt(); + + +-- 복호화 뷰 생성 +DROP VIEW IF EXISTS member_view; + +CREATE OR REPLACE VIEW member_view AS +SELECT member_no, + id, + encode(pgp_sym_decrypt(password, 'changeRequired')::bytea, 'escape') as password, + encode(pgp_sym_decrypt(email, 'changeRequired')::bytea, 'escape') as email, + gender, + age, + created_at, + created_by, + modified_at, + modified_by +FROM member; + + +/** 뷰 TO 테이블 바인딩 **/ + +-- 뷰 insert 함수 +CREATE + OR REPLACE FUNCTION member_view_insert_trigger() + RETURNS TRIGGER AS +$$ +BEGIN + INSERT INTO member (member_no, id, "password", email, gender, age, created_at, created_by, modified_at, modified_by) + VALUES (nextval('member_member_no_seq'::regclass), NEW.id, pgp_sym_encrypt(NEW.password::TEXT, 'changeRequired'), + pgp_sym_encrypt(NEW.email::TEXT, 'changeRequired'), NEW.gender, NEW.age, NEW.created_at, NEW.created_by, + NEW.modified_at, NEW.modified_by); + + RETURN NEW; + +END; +$$ + LANGUAGE plpgsql; + + +-- 뷰 insert 트리거 +DROP TRIGGER IF EXISTS member_view_insert_trigger ON member; + +CREATE TRIGGER member_view_insert_trigger + INSTEAD OF INSERT + ON member_view + FOR EACH ROW +EXECUTE FUNCTION member_view_insert_trigger(); + + +-- 뷰 update 함수 +CREATE + OR REPLACE FUNCTION member_view_update_trigger() + RETURNS TRIGGER AS +$$ +BEGIN + UPDATE member + SET id = NEW.id, + "password" = pgp_sym_encrypt(NEW.password::TEXT, 'changeRequired'), + email = pgp_sym_encrypt(NEW.email::TEXT, 'changeRequired'), + gender = NEW.gender, + age = NEW.age, + created_at = NEW.created_at, + created_by = NEW.created_by, + modified_at = NEW.modified_at, + modified_by = NEW.modified_by + WHERE member_no = NEW.member_no; + RETURN NEW; +END; +$$ + LANGUAGE plpgsql; + + +-- 뷰 update 트리거 +DROP TRIGGER IF EXISTS member_view_update_trigger ON member; + +CREATE TRIGGER member_view_update_trigger + INSTEAD OF UPDATE + ON member_view + FOR EACH ROW +EXECUTE FUNCTION member_view_update_trigger(); + + +-- 뷰 delete 함수 +CREATE + OR REPLACE FUNCTION member_view_delete_trigger() + RETURNS TRIGGER AS +$$ +BEGIN + DELETE + FROM member + WHERE id = OLD.id; + RETURN OLD; +END; +$$ + LANGUAGE plpgsql; + + +-- 뷰 delete 트리거 +DROP TRIGGER IF EXISTS member_view_delete_trigger ON member; + +CREATE TRIGGER member_view_delete_trigger + INSTEAD OF DELETE + ON member_view + FOR EACH ROW +EXECUTE FUNCTION member_view_delete_trigger(); + + +-- 테이블 member 권한 회수 설정 +REVOKE INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON member FROM wash_admin; \ No newline at end of file diff --git a/module-api/src/main/resources/db/migration/V1.0.2__insert_common_code_initial_data.sql b/module-api/src/main/resources/db/migration/V1.0.2__insert_common_code_initial_data.sql deleted file mode 100644 index 93f9369d..00000000 --- a/module-api/src/main/resources/db/migration/V1.0.2__insert_common_code_initial_data.sql +++ /dev/null @@ -1,48 +0,0 @@ -INSERT INTO common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(1, 'carbrand', 0, '', 1, false, '차량 브랜드', '2023-12-28', 'admin', NULL, NULL); -INSERT INTO common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(2, 'Hyundai', 1, 'carbrand', 1, false, '현대', '2023-12-28', 'admin', NULL, NULL); -INSERT INTO common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(3, 'Kia', 1, 'carbrand', 2, false, '기아', '2023-12-28', 'admin', NULL, NULL); -INSERT INTO common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(4, 'Renault', 1, 'carbrand', 3, false, '르노', '2023-12-28', 'admin', NULL, NULL); -INSERT INTO common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(5, 'KG Mobility', 1, 'carbrand', 4, false, 'KG모빌리티', '2023-12-28', 'admin', NULL, NULL); -INSERT INTO common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(5, 'Chevrolet', 1, 'carbrand', 5, false, '쉐보레', '2023-12-28', 'admin', NULL, NULL); -INSERT INTO common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(6, 'Benz', 1, 'carbrand', 6, false, '벤츠', '2023-12-28', 'admin', NULL, NULL); -INSERT INTO common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(7, 'BMW', 1, 'carbrand', 7, false, '벰베', '2023-12-28', 'admin', NULL, NULL); -INSERT INTO common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(8, 'Audi', 1, 'carbrand', 8, false, '아우디', '2023-12-28', 'admin', NULL, NULL); -INSERT INTO common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(9, 'ETC', 1, 'carbrand', 9, false, '기타', '2023-12-28', 'admin', NULL, NULL); -INSERT INTO common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(10, 'cartype', 0, '', 2, true, '차량 유형', '2023-12-28', 'admin', NULL, NULL); -INSERT INTO common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(11, 'Sedan', 10, 'cartype', 1, true, '세단', '2023-12-28', 'admin', NULL, NULL); -INSERT INTO common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(12, 'Hatchback', 10, 'cartype', 2, true, '해치백', '2023-12-28', 'admin', NULL, NULL); -INSERT INTO common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(13, 'SUV', 10, 'cartype', 3, true, 'SUV', '2023-12-28', 'admin', NULL, NULL); -INSERT INTO common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(14, 'ETC', 10, 'cartype', 4, true, '기타', '2023-12-28', 'admin', NULL, NULL); -INSERT INTO common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(15, 'segment', 0, '', 3, true, '차량 크기', '2023-12-28', 'admin', NULL, NULL); -INSERT INTO common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(16, 'Micro', 15, 'segment', 1, true, '경차', '2023-12-28', 'admin', NULL, NULL); -INSERT INTO common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(17, 'Subcompact', 15, 'segment', 2, true, '소형', '2023-12-28', 'admin', NULL, NULL); -INSERT INTO common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(18, 'Compact', 15, 'segment', 3, true, '중형', '2023-12-28', 'admin', NULL, NULL); -INSERT INTO common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(19, 'Fullsize', 15, 'segment', 4, true, '대형', '2023-12-28', 'admin', NULL, NULL); -INSERT INTO common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(20, 'color', 0, '', 4, true, '차량 색상', '2023-12-28', 'admin', NULL, NULL); -INSERT INTO common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(21, 'white', 20, 'color', 1, true, '흰색', '2023-12-28', 'admin', NULL, NULL); -INSERT INTO common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(22, 'gray', 20, 'color', 5, true, '쥐색', '2023-12-28', 'admin', NULL, NULL); -INSERT INTO common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(23, 'black', 20, 'color', 6, true, '검정색', '2023-12-28', 'admin', NULL, NULL); -INSERT INTO common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(24, 'red', 20, 'color', 4, true, '빨간색', '2023-12-28', 'admin', NULL, NULL); -INSERT INTO common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(25, 'yellow', 20, 'color', 2, true, '노란색', '2023-12-28', 'admin', NULL, NULL); -INSERT INTO common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(26, 'green', 20, 'color', 3, true, '초록색', '2023-12-28', 'admin', NULL, NULL); -INSERT INTO common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(27, 'blue', 20, 'color', 4, true, '파란색', '2023-12-28', 'admin', NULL, NULL); -INSERT INTO common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(28, 'etc', 20, 'color', 7, true, '기타', '2023-12-28', 'admin', NULL, NULL); -INSERT INTO common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(29, 'perl', 0, '', 5, true, '차량 페인트 펄', '2023-12-28', 'admin', NULL, NULL); -INSERT INTO common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(30, 'TRUE', 28, 'perl', 1, true, '있음', '2023-12-28', 'admin', NULL, NULL); -INSERT INTO common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(31, 'FALSE', 28, 'perl', 2, true, '없음', '2023-12-28', 'admin', NULL, NULL); -INSERT INTO common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(32, 'clearcoat', 0, '', 6, true, '차량 페인트 마감', '2023-12-28', 'admin', NULL, NULL); -INSERT INTO common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(33, 'TRUE', 31, 'clearcoat', 1, true, '있음', '2023-12-28', 'admin', NULL, NULL); -INSERT INTO common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(34, 'FALSE', 31, 'clearcoat', 2, true, '없음', '2023-12-28', 'admin', NULL, NULL); -INSERT INTO common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(35, 'driving', 0, '', 7, true, '주행환경', '2023-12-28', 'admin', NULL, NULL); -INSERT INTO common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(36, 'comport', 34, 'driving', 1, true, '도심', '2023-12-28', 'admin', NULL, NULL); -INSERT INTO common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(37, 'highway', 34, 'driving', 2, true, '고속', '2023-12-28', 'admin', NULL, NULL); -INSERT INTO common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(38, 'complex', 34, 'driving', 3, true, '복합', '2023-12-28', 'admin', NULL, NULL); -INSERT INTO common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(39, 'parking', 0, '', 8, true, '주차환경', '2023-12-28', 'admin', NULL, NULL); -INSERT INTO common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(40, 'house', 38, 'parking', 1, true, '실내/지하', '2023-12-28', 'admin', NULL, NULL); -INSERT INTO common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(41, 'road', 38, 'parking', 2, true, '노상', '2023-12-28', 'admin', NULL, NULL); -INSERT INTO common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(42, 'piloti', 38, 'parking', 3, true, '필로티', '2023-12-28', 'admin', NULL, NULL); -INSERT INTO common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(43, 'interest', 0, '', 9, true, '주요관심사', '2023-12-28', 'admin', NULL, NULL); -INSERT INTO common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(44, 'outercare', 42, 'washInfo', 1, true, '도장', '2023-12-28', 'admin', NULL, NULL); -INSERT INTO common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(45, 'tire', 42, 'washInfo', 2, true, '타이어', '2023-12-28', 'admin', NULL, NULL); -INSERT INTO common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(46, 'wheel', 42, 'washInfo', 3, true, '휠', '2023-12-28', 'admin', NULL, NULL); -INSERT INTO common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(47, 'innercare', 42, 'washInfo', 4, true, '실내', '2023-12-28', 'admin', NULL, NULL); \ No newline at end of file diff --git a/module-api/src/main/resources/db/migration/V1.0.3__initialize_batch_job_schema.sql b/module-api/src/main/resources/db/migration/V1.0.3__initialize_batch_job_schema.sql deleted file mode 100644 index 4b6daf44..00000000 --- a/module-api/src/main/resources/db/migration/V1.0.3__initialize_batch_job_schema.sql +++ /dev/null @@ -1,78 +0,0 @@ --- Autogenerated: do not edit this file - -CREATE TABLE if not exists BATCH_JOB_INSTANCE ( - JOB_INSTANCE_ID BIGINT NOT NULL PRIMARY KEY , - VERSION BIGINT , - JOB_NAME VARCHAR(100) NOT NULL, - JOB_KEY VARCHAR(32) NOT NULL, - constraint JOB_INST_UN unique (JOB_NAME, JOB_KEY) -) ; - -CREATE TABLE if not exists BATCH_JOB_EXECUTION ( - JOB_EXECUTION_ID BIGINT NOT NULL PRIMARY KEY , - VERSION BIGINT , - JOB_INSTANCE_ID BIGINT NOT NULL, - CREATE_TIME TIMESTAMP NOT NULL, - START_TIME TIMESTAMP DEFAULT NULL , - END_TIME TIMESTAMP DEFAULT NULL , - STATUS VARCHAR(10) , - EXIT_CODE VARCHAR(2500) , - EXIT_MESSAGE VARCHAR(2500) , - LAST_UPDATED TIMESTAMP, - constraint JOB_INST_EXEC_FK foreign key (JOB_INSTANCE_ID) - references BATCH_JOB_INSTANCE(JOB_INSTANCE_ID) -) ; - -CREATE TABLE if not exists BATCH_JOB_EXECUTION_PARAMS ( - JOB_EXECUTION_ID BIGINT NOT NULL , - PARAMETER_NAME VARCHAR(100) NOT NULL , - PARAMETER_TYPE VARCHAR(100) NOT NULL , - PARAMETER_VALUE VARCHAR(2500) , - IDENTIFYING CHAR(1) NOT NULL , - constraint JOB_EXEC_PARAMS_FK foreign key (JOB_EXECUTION_ID) - references BATCH_JOB_EXECUTION(JOB_EXECUTION_ID) -) ; - -CREATE TABLE if not exists BATCH_STEP_EXECUTION ( - STEP_EXECUTION_ID BIGINT NOT NULL PRIMARY KEY , - VERSION BIGINT NOT NULL, - STEP_NAME VARCHAR(100) NOT NULL, - JOB_EXECUTION_ID BIGINT NOT NULL, - CREATE_TIME TIMESTAMP NOT NULL, - START_TIME TIMESTAMP DEFAULT NULL , - END_TIME TIMESTAMP DEFAULT NULL , - STATUS VARCHAR(10) , - COMMIT_COUNT BIGINT , - READ_COUNT BIGINT , - FILTER_COUNT BIGINT , - WRITE_COUNT BIGINT , - READ_SKIP_COUNT BIGINT , - WRITE_SKIP_COUNT BIGINT , - PROCESS_SKIP_COUNT BIGINT , - ROLLBACK_COUNT BIGINT , - EXIT_CODE VARCHAR(2500) , - EXIT_MESSAGE VARCHAR(2500) , - LAST_UPDATED TIMESTAMP, - constraint JOB_EXEC_STEP_FK foreign key (JOB_EXECUTION_ID) - references BATCH_JOB_EXECUTION(JOB_EXECUTION_ID) -) ; - -CREATE TABLE if not exists BATCH_STEP_EXECUTION_CONTEXT ( - STEP_EXECUTION_ID BIGINT NOT NULL PRIMARY KEY, - SHORT_CONTEXT VARCHAR(2500) NOT NULL, - SERIALIZED_CONTEXT TEXT , - constraint STEP_EXEC_CTX_FK foreign key (STEP_EXECUTION_ID) - references BATCH_STEP_EXECUTION(STEP_EXECUTION_ID) -) ; - -CREATE TABLE if not exists BATCH_JOB_EXECUTION_CONTEXT ( - JOB_EXECUTION_ID BIGINT NOT NULL PRIMARY KEY, - SHORT_CONTEXT VARCHAR(2500) NOT NULL, - SERIALIZED_CONTEXT TEXT , - constraint JOB_EXEC_CTX_FK foreign key (JOB_EXECUTION_ID) - references BATCH_JOB_EXECUTION(JOB_EXECUTION_ID) -) ; - -CREATE SEQUENCE if not exists BATCH_STEP_EXECUTION_SEQ MAXVALUE 9223372036854775807 NO CYCLE; -CREATE SEQUENCE if not exists BATCH_JOB_EXECUTION_SEQ MAXVALUE 9223372036854775807 NO CYCLE; -CREATE SEQUENCE if not exists BATCH_JOB_SEQ MAXVALUE 9223372036854775807 NO CYCLE; diff --git a/module-api/src/main/resources/db/migration/V1.0.3__insert_initial_data.sql b/module-api/src/main/resources/db/migration/V1.0.3__insert_initial_data.sql new file mode 100644 index 00000000..523d5e3c --- /dev/null +++ b/module-api/src/main/resources/db/migration/V1.0.3__insert_initial_data.sql @@ -0,0 +1,137 @@ +-- Insert Into common_code (initial data) +TRUNCATE public.common_code; + +INSERT INTO public.common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, + created_by, modified_at, modified_by, sub_description) +VALUES (1, 'carbrand', 0, '', 1, false, '차량 브랜드', '2023-12-28', 'admin', NULL, NULL, NULL), + (10, 'cartype', 0, '', 2, true, '차량 유형', '2023-12-28', 'admin', NULL, NULL, NULL), + (15, 'segment', 0, '', 3, true, '차량 크기', '2023-12-28', 'admin', NULL, NULL, NULL), + (20, 'color', 0, '', 4, true, '차량 색상', '2023-12-28', 'admin', NULL, NULL, NULL), + (29, 'perl', 0, '', 5, true, '차량 페인트 펄', '2023-12-28', 'admin', NULL, NULL, NULL), + (30, 'TRUE', 28, 'perl', 1, true, '있음', '2023-12-28', 'admin', NULL, NULL, NULL), + (31, 'FALSE', 28, 'perl', 2, true, '없음', '2023-12-28', 'admin', NULL, NULL, NULL), + (32, 'clearcoat', 0, '', 6, true, '차량 페인트 마감', '2023-12-28', 'admin', NULL, NULL, NULL), + (33, 'TRUE', 31, 'clearcoat', 1, true, '있음', '2023-12-28', 'admin', NULL, NULL, NULL), + (34, 'FALSE', 31, 'clearcoat', 2, true, '없음', '2023-12-28', 'admin', NULL, NULL, NULL); + + +INSERT INTO public.common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, + created_by, modified_at, modified_by, sub_description) +VALUES (35, 'driving', 0, '', 7, true, '주행환경', '2023-12-28', 'admin', NULL, NULL, NULL), + (36, 'comport', 34, 'driving', 1, true, '도심', '2023-12-28', 'admin', NULL, NULL, NULL), + (37, 'highway', 34, 'driving', 2, true, '고속', '2023-12-28', 'admin', NULL, NULL, NULL), + (38, 'complex', 34, 'driving', 3, true, '복합', '2023-12-28', 'admin', NULL, NULL, NULL), + (39, 'parking', 0, '', 8, true, '주차환경', '2023-12-28', 'admin', NULL, NULL, NULL), + (40, 'house', 38, 'parking', 1, true, '실내/지하', '2023-12-28', 'admin', NULL, NULL, NULL), + (41, 'road', 38, 'parking', 2, true, '노상', '2023-12-28', 'admin', NULL, NULL, NULL), + (42, 'piloti', 38, 'parking', 3, true, '필로티', '2023-12-28', 'admin', NULL, NULL, NULL), + (43, 'interest', 0, '', 9, true, '주요관심사', '2023-12-28', 'admin', NULL, NULL, NULL), + (48, 'gender', 0, NULL, 10, true, NULL, '2024-01-10', 'admin', NULL, NULL, NULL); + + +INSERT INTO public.common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, + created_by, modified_at, modified_by, sub_description) +VALUES (49, 'man', 48, 'gender', 1, true, '남성', '2024-01-10', 'admin', NULL, NULL, NULL), + (50, 'woman', 48, 'gender', 2, true, '여성', '2024-01-10', 'admin', NULL, NULL, NULL), + (51, 'age', 0, NULL, 11, true, '연령대', '2024-01-10', 'admin', NULL, NULL, NULL), + (52, 'AGE_20', 51, 'age', 1, true, '20대 이하', '2024-01-10', 'admin', NULL, NULL, NULL), + (53, 'AGE_30', 51, 'age', 2, true, '30대', '2024-01-10', 'admin', NULL, NULL, NULL), + (54, 'AGE_40', 51, 'age', 3, true, '40대', '2024-01-10', 'admin', NULL, NULL, NULL), + (55, 'AGE_50', 51, 'age', 4, true, '50대', '2024-01-10', 'admin', NULL, NULL, NULL), + (56, 'AGE_60', 51, 'age', 5, true, '60대 이상', '2024-01-10', 'admin', NULL, NULL, NULL), + (57, 'frequency', 0, NULL, 12, true, '세차빈도', '2024-01-10', 'admin', NULL, NULL, NULL), + (58, 'month1', 57, 'frequency', 1, true, '월 평균 1회', '2024-01-10', 'admin', NULL, NULL, NULL); + + +INSERT INTO public.common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, + created_by, modified_at, modified_by, sub_description) +VALUES (59, 'month2', 57, 'frequency', 2, true, '월 평균 2회', '2024-01-10', 'admin', NULL, NULL, NULL), + (60, 'month3', 57, 'frequency', 3, true, '월 평균 3회', '2024-01-10', 'admin', NULL, NULL, NULL), + (61, 'month4', 57, 'frequency', 4, true, '월 평균 4회', '2024-01-10', 'admin', NULL, NULL, NULL), + (62, 'cost', 0, NULL, 13, true, '지출비용', '2024-01-10', 'admin', NULL, NULL, NULL), + (63, '1to3', 62, 'cost', 1, true, '월 평균 1~3만원', '2024-01-10', 'admin', NULL, NULL, NULL), + (64, '4to6', 62, 'cost', 2, true, '월 평균 4~6만원', '2024-01-10', 'admin', NULL, NULL, NULL), + (44, 'outercare', 42, 'interest', 1, true, '도장', '2023-12-28', 'admin', NULL, NULL, NULL), + (45, 'tire', 42, 'interest', 2, true, '타이어', '2023-12-28', 'admin', NULL, NULL, NULL), + (46, 'wheel', 42, 'interest', 3, true, '휠', '2023-12-28', 'admin', NULL, NULL, NULL), + (47, 'innercare', 42, 'interest', 4, true, '실내', '2023-12-28', 'admin', NULL, NULL, NULL); + + +INSERT INTO public.common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, + created_by, modified_at, modified_by, sub_description) +VALUES (2, 'hyundai', 1, 'carbrand', 1, false, '현대', '2023-12-28', 'admin', NULL, NULL, NULL), + (3, 'kia', 1, 'carbrand', 2, false, '기아', '2023-12-28', 'admin', NULL, NULL, NULL), + (4, 'renault', 1, 'carbrand', 3, false, '르노', '2023-12-28', 'admin', NULL, NULL, NULL), + (5, 'kg mobility', 1, 'carbrand', 4, false, 'KG모빌리티', '2023-12-28', 'admin', NULL, NULL, NULL), + (5, 'chevrolet', 1, 'carbrand', 5, false, '쉐보레', '2023-12-28', 'admin', NULL, NULL, NULL), + (6, 'benz', 1, 'carbrand', 6, false, '벤츠', '2023-12-28', 'admin', NULL, NULL, NULL), + (7, 'bmw', 1, 'carbrand', 7, false, '벰베', '2023-12-28', 'admin', NULL, NULL, NULL), + (8, 'audi', 1, 'carbrand', 8, false, '아우디', '2023-12-28', 'admin', NULL, NULL, NULL), + (9, 'etc', 1, 'carbrand', 9, false, '기타', '2023-12-28', 'admin', NULL, NULL, NULL), + (11, 'sedan', 10, 'cartype', 1, true, '세단', '2023-12-28', 'admin', NULL, NULL, NULL); + + +INSERT INTO public.common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, + created_by, modified_at, modified_by, sub_description) +VALUES (12, 'hatchback', 10, 'cartype', 2, true, '해치백', '2023-12-28', 'admin', NULL, NULL, NULL), + (13, 'suv', 10, 'cartype', 3, true, 'SUV', '2023-12-28', 'admin', NULL, NULL, NULL), + (14, 'etc', 10, 'cartype', 4, true, '기타', '2023-12-28', 'admin', NULL, NULL, NULL), + (16, 'micro', 15, 'segment', 1, true, '경차', '2023-12-28', 'admin', NULL, NULL, NULL), + (17, 'subcompact', 15, 'segment', 2, true, '소형', '2023-12-28', 'admin', NULL, NULL, NULL), + (18, 'compact', 15, 'segment', 3, true, '중형', '2023-12-28', 'admin', NULL, NULL, NULL), + (65, '7to9', 62, 'cost', 3, true, '월 평균 7~9만원', '2024-01-10', 'admin', NULL, NULL, NULL), + (66, '10over', 62, 'cost', 4, true, '월 평균 10만원 이상', '2024-01-10', 'admin', NULL, NULL, NULL), + (19, 'fullsize', 15, 'segment', 4, true, '대형', '2023-12-28', 'admin', NULL, NULL, NULL), + (21, 'white', 20, 'color', 1, true, '흰색', '2023-12-28', 'admin', NULL, NULL, 'FFFFFF'); + + +INSERT INTO public.common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, + created_by, modified_at, modified_by, sub_description) +VALUES (22, 'gray', 20, 'color', 5, true, '쥐색', '2023-12-28', 'admin', NULL, NULL, '808080'), + (23, 'black', 20, 'color', 6, true, '검정색', '2023-12-28', 'admin', NULL, NULL, '37383C'), + (24, 'red', 20, 'color', 4, true, '빨간색', '2023-12-28', 'admin', NULL, NULL, 'FF4500'), + (25, 'yellow', 20, 'color', 2, true, '노란색', '2023-12-28', 'admin', NULL, NULL, 'FFD400'), + (26, 'green', 20, 'color', 3, true, '초록색', '2023-12-28', 'admin', NULL, NULL, '2F4F4F'), + (27, 'blue', 20, 'color', 4, true, '파란색', '2023-12-28', 'admin', NULL, NULL, '145B7D'), + (28, 'etc', 20, 'color', 7, true, '기타', '2023-12-28', 'admin', NULL, NULL, '6E0000'); + + + +-- Insert Into brand (initial data) +TRUNCATE brand; + +Insert Into brand (brand_no, brand_name, company_name, nation_name, created_at, created_by) +VALUES (1, '더클래스', '코스메디슨(CosMedicine)', '대한민국', current_date, 'admin'), + (51, '크리스탈 코트', '(주) 불스원', '대한민국', current_date, 'admin'), + (101, '불스원', '(주) 불스원', '대한민국', current_date, 'admin'), + (151, '글로스브로', '주식회사 제이씨웍스', '대한민국', current_date, 'admin'), + (201, '파이어볼', '(주)파이어볼', '대한민국', current_date, 'admin'), + (251, '티에이씨시스템', '티에이씨시스템', '대한민국', current_date, 'admin'), + (301, '기온테크놀로지', '주식회사 기온테크놀로지(Gyeon Technology)', '대한민국', current_date, 'admin'), + (351, '루나틱폴리시', '(주) 불스원', '대한민국', current_date, 'admin'), + (401, '젠틀맨', '주식회사 에스피', '대한민국', current_date, 'admin'), + (451, '디아만테', '현일', '대한민국', current_date, 'admin'), + (501, '(주)오토왁스', '(주)오토왁스', '대한민국', current_date, 'admin'), + (551, '라보코스메디카', '주식회사 대흥아이앤씨(INC)', '이탈리아', current_date, 'admin'), + (601, '마프라', '주식회사 대흥아이앤씨(INC)', '이탈리아', current_date, 'admin'), + (651, '매니악', '주식회사 대흥아이앤씨(INC)', '이탈리아', current_date, 'admin'), + (701, 'EXQ', '이엑스큐(EXQ)', '대한민국', current_date, 'admin'), + (751, 'EXQ', '이엑스큐(EXQ)', '중국', current_date, 'admin'), + (801, '터틀왁스', '(주)에스씨에이', '미국', current_date, 'admin'), + (851, '잭스왁스', '주식회사 잭스왁스코리아', '미국', current_date, 'admin'), + (901, '오토브라이트', '주식회사 오토브라이트다이렉트코리아(AUTOBRITE DIRECT KOREA CO.,LTD)', '영국', current_date, 'admin'), + (951, '블루믹스', '제이웍스 (J-works)', '대한민국', current_date, 'admin'), + (1001, '림피오', '림피오(LIMPIO)', '대한민국', current_date, 'admin'), + (1051, '좀비', '한국씨앤에스', '대한민국', current_date, 'admin'), + (1101, '카프로', '(주)카프로코리아', '대한민국', current_date, 'admin'), + (1151, '이지카케어', '발스코리아(주)', '영국', current_date, 'admin'), + (1201, '니그린', '(주)다스모터스', '영국', current_date, 'admin'), + (1251, '도도쥬스', '주식회사투왁스', '영국', current_date, 'admin'), +-- (1301, '루미너스', '주식회사 엘엠', '대한민국', current_date, 'admin'), + (1351, '소낙스', '(주)알레스', '독일', current_date, 'admin'), + (1401, '스마트왁스', '에이큐에이 주식회사', '미국', current_date, 'admin'), + (1451, '케미컬가이', '에이큐에이 주식회사', '미국', current_date, 'admin'), + (1501, '리바이브', '주식회사 에스피', '영국', current_date, 'admin'), + (1551, '보닉스', '대흥아이앤씨', '브라질', current_date, 'admin'), + -- (1601, '더클래스', '더클래스', '대한민국', current_date, 'admin'), + (1651, '루미너스', '루미너스코리아', '대한민국', current_date, 'admin'); \ No newline at end of file diff --git a/module-api/src/main/resources/db/migration/V1.0.4__create_reported_product_table.sql b/module-api/src/main/resources/db/migration/V1.0.4__create_reported_product_table.sql deleted file mode 100644 index a2821fde..00000000 --- a/module-api/src/main/resources/db/migration/V1.0.4__create_reported_product_table.sql +++ /dev/null @@ -1,49 +0,0 @@ -CREATE TABLE if not exists reported_product -( - mst_id VARCHAR(255) NOT NULL, - inspct_org VARCHAR(255), - prdt_type VARCHAR(255), - issu_date TIMESTAMP WITHOUT TIME ZONE, - renew_yn VARCHAR(255), - safe_sd VARCHAR(10000), - expr_date TIMESTAMP WITHOUT TIME ZONE, - upper_item VARCHAR(255), - kid_prt_pkg VARCHAR(255), - df VARCHAR(255), - propos VARCHAR(10000), - dtrb_lmt VARCHAR(255), - wt_bulk VARCHAR(255), - icepnt VARCHAR(255), - stddusqy VARCHAR(255), - usmtd TEXT, - us_atnrpt TEXT, - frsaid TEXT, - sum1 TEXT, - sum2 TEXT, - sum3 TEXT, - sum4 TEXT, - sum5 TEXT, - sum6 TEXT, - mf_icm VARCHAR(255), - mf_mthd TEXT, - mf_nation VARCHAR(255), - comp_addr VARCHAR(255), - comp_tel VARCHAR(255), - in_comp_nm VARCHAR(255), - in_comp_addr VARCHAR(255), - in_comp_tel VARCHAR(255), - odm_comp_nm VARCHAR(255), - odm_comp_addr VARCHAR(255), - odm_comp_tel VARCHAR(255), - created_at date , - created_by VARCHAR(255) NOT NULL, - modified_at date, - modified_by VARCHAR(255), - prdt_nm VARCHAR(255), - slfsfcfst_no VARCHAR(255), - item VARCHAR(255), - est_no INTEGER, - reg_date TIMESTAMP WITHOUT TIME ZONE, - comp_nm VARCHAR(255), - CONSTRAINT pk_reportedproduct PRIMARY KEY (mst_id, est_no) -); \ No newline at end of file diff --git a/module-api/src/main/resources/db/migration/V1.0.4__init_washzone_table.sql b/module-api/src/main/resources/db/migration/V1.0.4__init_washzone_table.sql new file mode 100644 index 00000000..c92337ba --- /dev/null +++ b/module-api/src/main/resources/db/migration/V1.0.4__init_washzone_table.sql @@ -0,0 +1,119 @@ +CREATE TABLE if not exists wash_zone +( + washzone_no BIGSERIAL PRIMARY KEY, + washzone_name VARCHAR(255) NOT NULL, + washzone_address VARCHAR(255) NOT NULL, + latitude DOUBLE PRECISION NOT NULL, + longitude DOUBLE PRECISION NOT NULL, + type VARCHAR(50), + remarks TEXT + ); + +alter sequence wash_zone_washzone_no_seq increment by 50; + +TRUNCATE wash_zone; +INSERT INTO wash_zone (washZone_name, washZone_address, latitude, longitude, type, remarks) +VALUES ('오아시스셀프세차장', '충북 청주시 흥덕구 서현중로35번길 51', 36.6201044081534, 127.427299069691, '실내', NULL), + ('청주시민셀프세차장', '충북 청주시 상당구 용암북로120번길 15', 36.6199834136669, 127.511998444085, '실내', NULL), + ('와이퍼 현대오일뱅크 북악점', '서울 성북구 정릉로 218', 37.6054307579856, 127.008551321932, '개러지', NULL), + ('와이퍼 현대오일뱅크 수내점', '경기 성남시 분당구 불정로 274', 37.3676873528416, 127.128195747364, '개러지', NULL), + ('와이퍼 SK한양주유소점', '경기 용인시 기흥구 중부대로 716', 37.2630968068198, 127.138586199203, '개러지', NULL), + ('와이퍼 안성공도점', '경기 안성시 공도읍 서동대로 4290-10', 37.0037593903804, 127.179875971997, '개러지', NULL), + ('와이퍼 대구대천점', '대구 달서구 달서대로 101', 35.8157605652849, 128.5059067, '개러지', NULL), + ('와이퍼 광주첨단점', '광주 광산구 첨단중앙로170번길 92', 35.2204495812902, 126.8470518, '실내', NULL), + ('와이퍼 목포옥암점', '전남 목포시 남악1로42번길 9', 34.8096184702339, 126.4469466, '개러지', NULL), + ('워시존 & 프렌즈스크린 양평용문점', '경기 양평군 용문면 용문로 534 1층 용문워시존 개러지, 2층 프렌즈스크린 & 무인카페', 37.4973951416351, 127.6045943, '실내', + NULL), + ('워시존개러지 영종도점', '인천 중구 흰바위로232번길 45', 37.4775040493517, 126.5042237, '실내', NULL), + ('워시존개러지 연수점', '인천 연수구 한나루로 151-4', 37.4237545470558, 126.6543958, '실내', NULL), + ('워시존개러지 천안점 ', '충남 천안시 동남구 천안대로 724', 36.8126862983842, 127.1652982, '실내', NULL), + ('워시존개러지 위례점', '경기 성남시 수정구 위례광장로 9-8', 37.4651633861572, 127.14086, '실내', NULL), + ('워시존개러지 상암수색점 ', '경기 고양시 덕양구 화랑로 57-16', 37.6040376724194, 126.868495655701, '실내', NULL), + ('워시존개러지 용인신갈점', '경기 용인시 기흥구 용구대로 2214', 37.2853563, 127.1062424, '실내', NULL), + ('와이퍼 랜드워시점', '경기 용인시 수지구 대지로 92', 37.3298039212157, 127.119365621361, '개러지', NULL), + ('와이퍼 신탄진점', '대전 대덕구 신탄진북로 35', 36.45133717, 127.4348803, '실내', NULL), + ('워시존 개러지 대전점', '대전 서구 갈마로 321 워시존개러지대전점', 36.3294028398965, 127.387388476956, '실내', NULL), + ('유워시셀프세차장', '전남 순천시 서면 산단4길 2', 34.98280325, 127.5029927, '개러지', NULL), + ('워시존개러지 김포ic점', '경기 김포시 고촌읍 김포대로451번길 10', 37.604407526588, 126.758025856705, '실내', NULL), + ('W실내셀프세차장 인하대점', '인천 미추홀구 매소홀로271번길 7', 37.4454966966327, 126.652771351674, '실내', NULL), + ('워시존 ev스테이션', '경기 의정부시 문충로 213', 37.7386241534117, 127.112156570012, '실내', NULL), + ('워시존 세종점', '세종특별자치시 금남면 호탄길 30-2', 36.4740783097355, 127.297021215937, '실내', NULL), + ('워시킹 원주점 ', '강원특별자치도 원주시 바우골길 14', 37.3546857908121, 127.91757, '실내', NULL), + ('문스테이 개러지 ', '세종특별자치시 부강면 부강외천로 193', 36.5288014873425, 127.388620523588, '개러지', '6베이'), + ('워시스퀘어', '경기 파주시 경의로 974', 37.7038156016474, 126.760017067435, '개러지', NULL), + ('워시랜드 평택점', '경기 평택시 이화로 134-7', 37.00685204, 127.1361897, '개러지', NULL), + ('유워시셀프세차장', '경북 구미시 신비로 135-4', 36.1221389580088, 128.376745402053, '개러지', NULL), + ('차차워시존', '경기 화성시 꽃내음4길 6-13', 37.2836544866466, 126.813683074169, '실내', NULL), + ('김앤장디테일링센터', '충북 청주시 서원구 2순환로 1460-11', 36.6081530609173, 127.443883051234, '개러지', NULL), + ('오토핸즈', '경기 화성시 효행로 749-2 1층', 37.2024567722234, 127.012628989253, '실내', NULL), + ('워시빌리지 셀프세차장', '경기 화성시 효행로 1275', 37.2224131707798, 127.060079721069, '개러지', NULL), + ('아레나워시(ARENA WASH)', '경기 화성시 향남읍 발안공단로5길 7-17', 37.0861096592201, 126.907794187387, '실내', NULL), + ('한진셀프세차장', '경남 창원시 마산회원구 내서읍 함마대로 2694-14', 35.2543430194069, 128.50855217056, '개러지', NULL), + ('주성셀프세차장', '충북 청주시 청원구 오창읍 주성3길 14-4', 36.7374712798656, 127.454143545144, '실내', NULL), + ('탑클래스 송파', '경기 하남시 감일남로 17', 37.5070411892196, 127.142093903841, '실내', NULL), + ('엑스디테일', '대전 유성구 대학로76번길 41', 36.3602971650418, 127.346404771706, '개러지', NULL), + ('워시큐브', '충남 천안시 서북구 수레터2길 19-28', 36.8495016954412, 127.112326025162, '개러지', NULL), + ('엔터카워시', '충남 아산시 배방읍 용연로 8-27', 36.7953284449154, 127.08880120335, '실내', NULL), + ('워시랩 포승평택항점', '경기 평택시 포승읍 평택항로184번길 3-23', 36.977190745632, 126.842479107154, '실내', NULL), + ('워시원 팽성점 ', '경기 평택시 팽성읍 팽성대교길 136', 36.9738078436877, 127.039566297396, '개러지', NULL), + ('배트맨세차장', '경기 광주시 곤지암읍 광여로 43', 37.3507328144012, 127.350898648582, '개러지', NULL), + ('장인자동차정비 불스원 워시앤케어', '경남 김해시 덕정로 85', 35.1800104810245, 128.794728597974, '개러지', NULL), + ('공항실내셀프세차장빨래방', '대구 동구 공항로49길 39-1', 35.8945412389868, 128.643708, '개러지', NULL), + ('워시존 제천강제점', '충북 제천시 강저로 152', 37.1201475675074, 128.210778894801, '개러지', NULL), + ('킹셀프세차', '경남 창원시 마산회원구 내서읍 호계본동1길 81', 35.2676248768078, 128.521388407907, '개러지', NULL), + ('아지트실내셀프세차장', '광주 서구 칠성로 49', 35.1666767105967, 126.862931505639, '실내', NULL), + ('워시월드셀프세차', '경기 동두천시 강변로 292', 37.8967969893417, 127.045819808121, '실내', NULL), + ('더블유카워시 동두천점', '경기 동두천시 평화로2229번길 39', 37.8854132234783, 127.056111840824, '개러지', NULL), + ('골드워시손세차', '경기 파주시 탄현면 평화로 565', 37.7723036129461, 126.721693334932, '개러지', NULL), + ('워시몬', '강원특별자치도 원주시 지정면 신무로 80', 37.3624470588156, 127.898227826858, NULL, NULL), + ('워시홀릭', '인천광역시 남동구 청능대로468번길 13', 37.3981890130261, 126.71187930226, NULL, NULL), + ('워시플래닛 동인천점', '인천 동구 염전로 57', 37.4839524540463, 126.650648969367, NULL, NULL), + ('송정게러지', '광주 광산구 송도로 252-8', 35.135531327045, 126.798546494925, '개러지', NULL), + ('워시캠프', '경기 양평군 양평읍 양근로 369', 37.4796199039076, 127.505654213137, '개러지', NULL), + ('워시플러스 천안지점', '충남 천안시 동남구 만남로 138 1층', 36.8243006164703, 127.164116241246, '개러지', NULL), + ('몬스터워시', '경기 고양시 덕양구 고양대로 2020-1 1층', 37.6468588612041, 126.903466872731, '실내', NULL), + ('몬스터 개러지', '경기 고양시 일산동구 애니골길 16', 37.6692050138554, 126.790533305429, '개러지', NULL), + ('유워시셀프세차장', '경기 안양시 만안구 덕천로 80', 37.3883986603995, 126.935351383776, '실내', NULL), + ('골드워시셀프세차장', '경기 시흥시 대골길 10', 37.4496453981454, 126.791096872485, '개러지', NULL), + ('킹콩샤워 안양직영점', '경기 군포시 공단로 177', 37.3612014730865, 126.949164376614, NULL, NULL), + ('워시존 개러지 인천간석점', '인천 남동구 경원대로 934', 37.4661340060481, 126.690506502129, NULL, NULL), + ('킹콩샤워 검단점', '인천 서구 원당대로 531 1층', 37.59357954, 126.659978337421, NULL, NULL), + ('모토카워시', '경기 부천시 오정구 신흥로 398', 37.5190425980984, 126.775587927791, '개러지', NULL), + ('더블유카워시 삼송점', '경기 고양시 덕양구 신원1로 16', 37.6618150698875, 126.891732678365, '개러지', NULL), + ('벙커카워시', '충남 공주시 장기로 14 1층', 36.47842597, 127.143512692704, NULL, NULL), + ('김해워시존 baybee 봉황점', '경남 김해시 김해대로2324번길 29', 35.2241760290391, 128.87989691962, NULL, NULL), + ('워시탑', '충북 음성군 대소면 대성로77번길 43', 36.9778746589609, 127.487703328697, NULL, NULL), + ('킹콩샤워 남양주실내점', '경기 남양주시 오남읍 진건오남로 754 1', 37.696419651204, 127.207679264916, NULL, NULL), + ('삐까뻔쩍카워시', '경북 경주시 충효녹지길 136', 35.84516131, 129.176757513452, NULL, NULL), + ('넘버원굿카워시', '경기 군포시 번영로200번길 6', 37.3335375151994, 126.927230420719, NULL, NULL), + ('모토카워시 서창직영점', '인천 남동구 서창남순환로62번길 6-5', 37.4294245546357, 126.751371714328, NULL, NULL), + ('루프탑세차장', '충북 청주시 서원구 1순환로1063번길 61-30', 36.6101324173452, 127.485188682279, NULL, NULL), + ('워시랜드셀프세차장', '대구 달서구 와룡로13길 95', 35.8378411977344, 128.531287468949, NULL, NULL), + ('플레이카방', '경기 파주시 미래로 290', 37.7038931787284, 126.747086563138, NULL, NULL), + ('붐셀프세차장', '대구 달서구 월곡로99길 23', 35.8323671549041, 128.521125081838, NULL, NULL), + ('씽크클린', '대구 수성구 달구벌대로 3311-1', 35.8370428490592, 128.718210305231, NULL, NULL), + ('루이네워시존', '경기 화성시 영통로 1-3', 37.2309984689993, 127.064816327025, NULL, NULL), + ('영화24시셀프세차장', '경기 수원시 장안구 경수대로 815-2', 37.294336153729, 127.016606780604, NULL, NULL), + ('이지워시 관평점', '대전 유성구 테크노5로 4', 36.4309885531462, 127.393905375921, NULL, NULL), + ('루페스빅풋디테일링센터 울산점', '울산 북구 진장9길 14-2', 35.5630825070833, 129.359467877059, '개러지', '2'), + ('디팩토리셀프세차장', '경기 시흥시 시흥대로 963 1층', 37.4223881296214, 126.788734557351, '개러지', '6'), + ('셀세모셀프세차장 광주경안점', '경기 광주시 문화로 117', 37.4134694807059, 127.251131465745, NULL, NULL), + ('조군셀프세차장', '경기 화성시 봉담읍 효행로 218', 37.2280861684098, 126.97110341628, NULL, NULL), + ('카매니저디테일링 개러지-실내셀프개러지', '경기 포천시 중앙로149번길 3 1층', 37.89908317, 127.204287340934, NULL, NULL), + ('케이브', '대전 유성구 테크노4로 80-7 1층', 36.4277573909088, 127.389634237221, NULL, NULL), + ('GS칼텍스 21세기주유소', '대전 유성구 유성대로 845', 36.3673080046938, 127.338839590207, NULL, NULL), + ('유워시셀프세차', '경기 구리시 응달말로 15', 37.6020784349143, 127.135203630723, NULL, NULL), + ('아이원셀프세차장', '서울 영등포구 양평로22마길 28', 37.5413296544991, 126.892561158528, NULL, NULL), + ('905워시클럽', '충북 청주시 흥덕구 호암로 110-1', 36.6140436559106, 127.423664921438, NULL, NULL), + ('락킨카워시', '경기 용인시 기흥구 공세로 146', 37.2370304409132, 127.107826794658, NULL, NULL), + ('와이퍼 구의점', '서울 광진구 아차산로 450', 37.538512027051, 127.092338630686, NULL, NULL), + ('워시존개러지 경북도청점', '경북 안동시 풍천면 검무로 10-19', 36.5753681666582, 128.495158231572, NULL, NULL), + ('워시존개러지 전주점', '전북특별자치도 전주시 완산구 콩쥐팥쥐로 1683', 35.8230323628824, 127.083016478889, NULL, NULL), + ('워시존개러지 용인유방점', '경기 용인시 처인구 백옥대로 1269 1층', 37.2481868798392, 127.212251165966, NULL, NULL), + ('워시존개러지 강동점', '경기 하남시 초이로160번길 32', 37.5440074106716, 127.173865127123, NULL, NULL), + ('셀프킹 양주본점', '경기 양주시 평화로1597번길 20', 37.8324490353488, 127.05255093607, NULL, NULL), + ('카바스 실내셀프세차장', '서울 구로구 오리로13길 58', 37.4885709419257, 126.830325446217, NULL, NULL), + ('워시보이게러지 실내셀프세차', '인천 서구 청마로 109', 37.58805814, 126.67577768941, NULL, NULL), + ('마이온게러지', '경기 파주시 평화로 208 마이온게러지', 37.7712928807524, 126.758439177099, NULL, NULL), + ('마이개러지카페', '경기 하남시 산곡동로 31', 37.5065820573677, 127.230560960249, NULL, NULL), + ('로얄게러지', '충북 충주시 원호암4길 26', 36.9552164638785, 127.937717481269, NULL, NULL); diff --git a/module-api/src/main/resources/db/migration/V1.0.5__create_review_table.sql b/module-api/src/main/resources/db/migration/V1.0.5__create_review_table.sql new file mode 100644 index 00000000..8f237c9c --- /dev/null +++ b/module-api/src/main/resources/db/migration/V1.0.5__create_review_table.sql @@ -0,0 +1,16 @@ +CREATE TABLE if not exists Review ( + review_no BIGSERIAL PRIMARY KEY, + product_no BIGINT NOT NULL, + member_no BIGINT NOT NULL, + star_rating NUMERIC(3,1) NOT NULL, + title VARCHAR NOT NULL, + contents VARCHAR NOT NULL, + created_at DATE NOT NULL, + created_by VARCHAR NOT NULL, + modified_at DATE NULL, + modified_by VARCHAR NULL, + FOREIGN KEY (product_no) REFERENCES Product (product_no), + FOREIGN KEY (member_no) REFERENCES Member (member_no) +); + +AlTER SEQUENCE review_review_no_seq increment by 50; diff --git a/module-api/src/main/resources/db/migration/V1.0.5__insert_common_code_age_and_gender_and_create_table_auth.sql b/module-api/src/main/resources/db/migration/V1.0.5__insert_common_code_age_and_gender_and_create_table_auth.sql deleted file mode 100644 index d7015469..00000000 --- a/module-api/src/main/resources/db/migration/V1.0.5__insert_common_code_age_and_gender_and_create_table_auth.sql +++ /dev/null @@ -1,24 +0,0 @@ --- 공통코드 성별, 연령대 추가 -INSERT INTO public.common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(48, 'gender', 0, NULL, 10, true, NULL, '2024-01-10', 'admin', NULL, NULL); -INSERT INTO public.common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(49, 'man', 48, 'gender', 1, true, '남성', '2024-01-10', 'admin', NULL, NULL); -INSERT INTO public.common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(50, 'woman', 48, 'gender', 2, true, '여성', '2024-01-10', 'admin', NULL, NULL); -INSERT INTO public.common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(52, 'age', 0, NULL, 11, true, '연령대', '2024-01-10', 'admin', NULL, NULL); -INSERT INTO public.common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(53, '20s', 52, 'age', 1, true, '20대 이하', '2024-01-10', 'admin', NULL, NULL); -INSERT INTO public.common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(54, '30s', 52, 'age', 2, true, '30대', '2024-01-10', 'admin', NULL, NULL); -INSERT INTO public.common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(55, '40s', 52, 'age', 3, true, '40대', '2024-01-10', 'admin', NULL, NULL); -INSERT INTO public.common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(56, '50s', 52, 'age', 4, true, '50대', '2024-01-10', 'admin', NULL, NULL); -INSERT INTO public.common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, created_by, modified_at, modified_by) VALUES(57, '60s', 52, 'age', 5, true, '60대 이상', '2024-01-10', 'admin', NULL, NULL); - --- 권한 테이블 -CREATE TABLE if not exists Auth ( - auth_no int NOT NULL, - member_no int NULL, - jwt_token varchar NULL, - sns_token varchar NULL, - created_at date NOT NULL, - created_by varchar NOT NULL, - modified_at date NULL, - modified_by varchar NULL -); -CREATE SEQUENCE if not exists auth_auth_no_seq increment by 50; -alter sequence auth_auth_no_seq increment by 50; \ No newline at end of file diff --git a/module-api/src/main/resources/db/migration/V1.0.6__init_likes.sql b/module-api/src/main/resources/db/migration/V1.0.6__init_likes.sql new file mode 100644 index 00000000..d63b53df --- /dev/null +++ b/module-api/src/main/resources/db/migration/V1.0.6__init_likes.sql @@ -0,0 +1,13 @@ +CREATE TABLE IF NOT EXISTS likes ( + like_no BIGSERIAL PRIMARY KEY, + member_no BIGINT NOT NULL, + product_no BIGINT NOT NULL, + FOREIGN KEY (member_no) REFERENCES member(member_no) , + FOREIGN KEY (product_no) REFERENCES product(product_no) , + created_at date NOT NULL, + created_by varchar NOT NULL, + modified_at date NULL, + modified_by varchar NULL +); + +alter sequence likes_like_no_seq increment by 50; diff --git a/module-api/src/main/resources/db/migration/V1.0.6__modify_member_encrypt_and_create_decrypt_member_view.sql b/module-api/src/main/resources/db/migration/V1.0.6__modify_member_encrypt_and_create_decrypt_member_view.sql deleted file mode 100644 index e15420ab..00000000 --- a/module-api/src/main/resources/db/migration/V1.0.6__modify_member_encrypt_and_create_decrypt_member_view.sql +++ /dev/null @@ -1,102 +0,0 @@ --- 회원 테이블 필드 변경 -ALTER TABLE member ALTER COLUMN email TYPE BYTEA USING email::bytea; -ALTER TABLE member ALTER COLUMN password TYPE BYTEA USING password::bytea; - - --- AES 암호화를 위한 확장 모듈 설치 -CREATE EXTENSION IF NOT EXISTS pgcrypto; - - --- 테이블 복호화 함수 -CREATE OR REPLACE FUNCTION washpedia_member_decrypt() RETURNS TRIGGER AS $$ -BEGIN - -- 비밀번호 복호화 - NEW.password := pgp_sym_decrypt(NEW.password, 'changeRequired'); - -- 이메일 복호화 - NEW.email := pgp_sym_decrypt(NEW.email, 'changeRequired'); - -RETURN NEW; -END; -$$ LANGUAGE plpgsql; - --- 테이블 복호화 트리거 insert, update 이벤트 후 member_view 업데이트 -CREATE TRIGGER washpedia_member_decrypt_trigger - AFTER INSERT OR UPDATE ON member - FOR EACH ROW - EXECUTE FUNCTION washpedia_member_decrypt(); - --- 복호화 뷰 생성, 업데이트 -CREATE OR REPLACE VIEW member_view AS -SELECT - member_no, - id, - encode(pgp_sym_decrypt(password, 'changeRequired')::bytea, 'escape') as password, - encode(pgp_sym_decrypt(email, 'changeRequired')::bytea, 'escape') as email, - gender, - birthdate, - created_at, - created_by, - modified_at, - modified_by -FROM member; - - --- 뷰 insert 함수 -CREATE OR REPLACE FUNCTION member_view_insert_trigger() -RETURNS TRIGGER AS $$ -BEGIN -INSERT INTO member (member_no, id, "password", email, gender, birthdate, created_at, created_by, modified_at, modified_by) -VALUES (nextval('member_member_no_seq'::regclass), NEW.id, pgp_sym_encrypt(NEW.password::TEXT, 'changeRequired'), pgp_sym_encrypt(NEW.email::TEXT, 'changeRequired'), NEW.gender, NEW.birthdate, NEW.created_at, NEW.created_by, NEW.modified_at, NEW.modified_by); - -RETURN NEW; - -END; -$$ LANGUAGE plpgsql; - --- 뷰 insert 트리거 -CREATE TRIGGER member_view_insert_trigger - INSTEAD OF INSERT ON member_view - FOR EACH ROW EXECUTE FUNCTION member_view_insert_trigger(); - - --- 뷰 update 함수 -CREATE OR REPLACE FUNCTION member_view_update_trigger() -RETURNS TRIGGER AS $$ -BEGIN -UPDATE member SET - id = NEW.id, - "password" = pgp_sym_encrypt(NEW.password::TEXT, 'changeRequired'), - email = pgp_sym_encrypt(NEW.email::TEXT, 'changeRequired'), - gender = NEW.gender, - birthdate = NEW.birthdate, - created_at = NEW.created_at, - created_by = NEW.created_by, - modified_at = NEW.modified_at, - modified_by = NEW.modified_by -WHERE member_no = NEW.member_no; -RETURN NEW; -END; -$$ LANGUAGE plpgsql; - --- 뷰 update 트리거 -CREATE TRIGGER member_view_update_trigger - INSTEAD OF UPDATE ON member_view - FOR EACH ROW EXECUTE FUNCTION member_view_update_trigger(); - - --- 뷰 delete 함수 -CREATE OR REPLACE FUNCTION member_view_delete_trigger() -RETURNS TRIGGER AS $$ -BEGIN -DELETE FROM member WHERE id = OLD.id; -RETURN OLD; -END; -$$ LANGUAGE plpgsql; - --- 뷰 delete 트리거 -CREATE TRIGGER member_view_delete_trigger - INSTEAD OF DELETE ON member_view - FOR EACH ROW EXECUTE FUNCTION member_view_delete_trigger(); - --- 테이블 member 권한 회수 설정 -REVOKE INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON member FROM wash_admin; \ No newline at end of file diff --git a/module-api/src/main/resources/db/migration/V1.0.7__create_admin_table.sql b/module-api/src/main/resources/db/migration/V1.0.7__create_admin_table.sql new file mode 100644 index 00000000..aa1aa484 --- /dev/null +++ b/module-api/src/main/resources/db/migration/V1.0.7__create_admin_table.sql @@ -0,0 +1,16 @@ +CREATE TABLE IF NOT EXISTS admin +( + admin_no BIGSERIAL PRIMARY KEY, + id VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL, + password VARCHAR(255) NOT NULL, + created_at DATE NOT NULL, + created_by VARCHAR NOT NULL, + modified_at DATE, + modified_by VARCHAR, + CONSTRAINT admin_id_unique UNIQUE (id), + CONSTRAINT admin_email_unique UNIQUE (email) + ); + + +alter sequence admin_admin_no_seq increment by 50; diff --git a/module-api/src/main/resources/db/migration/V1.0.7__insert_sample_data_to_product_and_brand.sql b/module-api/src/main/resources/db/migration/V1.0.7__insert_sample_data_to_product_and_brand.sql deleted file mode 100644 index 3d1e10fd..00000000 --- a/module-api/src/main/resources/db/migration/V1.0.7__insert_sample_data_to_product_and_brand.sql +++ /dev/null @@ -1,93 +0,0 @@ -Insert Into brand (brand_no, brand_name, nation_name, created_at, created_by) -VALUES (1, '더클래스', '대한민국', current_date, 'admin'), - (51, '오가닉K', '대한민국', current_date, 'admin'), - (101, '(주)제일', '대한민국', current_date, 'admin'), - (151, '루나틱폴리시', '대한민국', current_date, 'admin'), - (201, '불스원', '대한민국', current_date, 'admin'), - (251, '도깨비', '대한민국', current_date, 'admin'), - (301, '(주) 에스엠테크', '대한민국', current_date, 'admin'), - (351, 'Quantum', '대한민국', current_date, 'admin'), - (401, '림피오', '대한민국', current_date, 'admin'), - (451, '(주)월드그린', '대한민국', current_date, 'admin'), - (501, '루미너스', '대한민국', current_date, 'admin'), - (551, '광동케미칼', '대한민국', current_date, 'admin'), - (601, '주식회사 오월', '대한민국', current_date, 'admin'), - (651, '주식회사 다온벤카코리아', '대한민국', current_date, 'admin'), - (701, '엠케이코리아(오토가디언)', '대한민국', current_date, 'admin'), - (751, '젠틀맨', '대한민국', current_date, 'admin'), - (801, '디아만테', '대한민국', current_date, 'admin'), - (851, '청원상사', '대한민국', current_date, 'admin'), - (901, '(주)오토왁스', '대한민국', current_date, 'admin'); - -INSERT INTO product (product_name, brand_no, declare_no, is_violation, view_count, created_by, created_at) -VALUES ('더클래스 불렛 피닉스 (Bullet Phoenix)', 1, 'HB21-07-1241', false, 0, 'admin', current_date), - ('더클래스 차량 외부용 왁스부스터', 1, 'DB24-06-0002', false, 0, 'admin', current_date), - ('인퓨전스 드레싱제', 1, 'EB22-06-0252', false, 0, 'admin', current_date), - - ('바마드 퍼포먼스 차량용 발수 유리막 코팅제 건식사용', 51, 'CB23-06-0181', false, 0, 'admin', current_date), - - ('레자왁스(+코팅)', 101, 'GB24-06-0001', false, 0, 'admin', current_date), - ('차량용 내부 원터치 살균제', 101, 'DB23-21-0063', false, 0, 'admin', current_date), - ('다용도세정제(+멀티크리너)', 101, 'GB24-01-0003', false, 0, 'admin', current_date), - ('휠클리너(+철분제거)', 101, 'GB24-01-0002', false, 0, 'admin', current_date), - - ('루나틱폴리시 하이브리드 고체왁스', 151, 'CB24-06-0003', false, 0, 'admin', current_date), - - ('크리스탈 타이어 매트 드레싱', 201, 'CB19-06-0109', false, 0, 'admin', current_date), - ('크리스탈 타이어코트 스프레이', 201, 'CB21-06-0917', false, 0, 'admin', current_date), - - ('도깨비 디(D)01 페인트 클렌져', 251, 'FB21-02-0365', false, 0, 'admin', current_date), - - ('에탄올 워셔액 에탄파워', 301, 'DA21-16-0017', false, 0, 'admin', current_date), - - ('퀀텀 울트라 뷰 에탄올 워셔액', 351, 'DA21-16-0005', false, 0, 'admin', current_date), - - ('림피오 타이어 드레싱 (Limpio Tire Dressing)', 401, 'FA21-06-0826', false, 0, 'admin', current_date), - - ('타이어 광택제', 451, 'CB20-06-0372', false, 0, 'admin', current_date), - - ('에스프리워시', 501, 'GB24-01-0006', false, 0, 'admin', current_date), - ('루멘 카샴푸', 501, 'GB23-01-0089', false, 0, 'admin', current_date), - - ('생활미소 상큼향 형광 카샴푸', 551, 'FB22-01-0808', false, 0, 'admin', current_date), - ('생활미소 달콤향 보라 카샴푸', 551, 'FB22-01-0811', false, 0, 'admin', current_date), - - ('대용량 중성 카샴푸 5L', 601, 'EB23-01-1012', false, 0, 'admin', current_date), - - ('뿌리는카샴푸', 651, 'DB21-01-1159', false, 0, 'admin', current_date), - - ('고농축프리미엄카샴푸', 701, 'DB23-01-0422', false, 0, 'admin', current_date), - - ('젠틀맨 유리세정제', 751, 'GB23-01-0957', false, 0, 'admin', current_date), - ('젠틀맨 철분제거제', 751, 'GB23-01-0899', false, 0, 'admin', current_date), - - ('디아만테 유막(클리닝에이전트)', 801, 'HB20-01-0775', false, 0, 'admin', current_date), - ('디아만테 아이언(클리닝에이전트)', 801, 'HB20-01-0775', false, 0, 'admin', current_date), - ('디아만테 버블폼(클리닝에이전트)', 801, 'HB20-01-0775', false, 0, 'admin', current_date), - ('디아만테 고농축스노우폼 (클리닝에이전트)', 801, 'HB20-01-0775', false, 0, 'admin', current_date), - ('디아만테 탈지제 (클리닝에이전트)', 801, 'HB20-01-0775', false, 0, 'admin', current_date), - - ('청원상사 컴파운드 광택코팅제', 851, 'HB23-06-0089', false, 0, 'admin', current_date), - - ('트림 앤 모터 코트', 901, 'HB21-06-0743', false, 0, 'admin', current_date), - ('카나우바 스프리츠', 901, 'HB21-06-0765', false, 0, 'admin', current_date), - ('패스트 왁스', 901, 'HB21-06-0763', false, 0, 'admin', current_date), - ('그래핀 딥글로스 세라믹 실런트', 901, 'HB21-06-0766', false, 0, 'admin', current_date), - ('글라이드', 901, 'HB21-06-0765', false, 0, 'admin', current_date), - ('파이널 폴리쉬', 901, 'HB21-06-0757', false, 0, 'admin', current_date), - ('울티마 페인트 실런트', 901, 'HB21-06-0757', false, 0, 'admin', current_date), - ('프리왁스 클린져', 901, 'HB21-06-0757', false, 0, 'admin', current_date), - ('세라믹 휠 실런트', 901, 'HB21-06-0766', false, 0, 'admin', current_date), - ('원스텝 세라믹 폴리쉬', 901, 'HB21-06-0766', false, 0, 'admin', current_date), - ('클레이 루브리컨트', 901, 'HB21-06-0765', false, 0, 'admin', current_date), - ('타이어앤트림 프로텍턴트 젤', 901, 'HB21-06-0757', false, 0, 'admin', current_date), - ('스월버스터 폴리쉬', 901, 'HB21-06-0757', false, 0, 'admin', current_date), - ('스프레이 왁스 플러스', 901, 'HB21-06-0765', false, 0, 'admin', current_date), - ('아크릴릭 스프레이 왁스', 901, 'HB21-06-0765', false, 0, 'admin', current_date), - ('리스토어 폴리쉬', 901, 'HB21-06-0757', false, 0, 'admin', current_date), - ('페인트웍 클린져', 901, 'HB21-06-0757', false, 0, 'admin', current_date), - ('얼티메이트 페인트 프로텍션', 901, 'HB21-06-0757', false, 0, 'admin', current_date), - ('크롬 폴리쉬', 901, 'HB21-06-0757', false, 0, 'admin', current_date), - ('알루미늄 폴리쉬', 901, 'HB21-06-0757', false, 0, 'admin', current_date), - ('이온 페인트 실런트', 901, 'HB21-06-0757', false, 0, 'admin', current_date), - ('알루미늄 실런트', 901, 'HB21-06-0757', false, 0, 'admin', current_date); \ No newline at end of file diff --git a/module-api/src/main/resources/db/migration/V1.0.8__insert_common_code_and_create_withraw_table.sql b/module-api/src/main/resources/db/migration/V1.0.8__insert_common_code_and_create_withraw_table.sql new file mode 100644 index 00000000..cb3bf045 --- /dev/null +++ b/module-api/src/main/resources/db/migration/V1.0.8__insert_common_code_and_create_withraw_table.sql @@ -0,0 +1,35 @@ +CREATE TABLE if not exists withdraw_member +( + member_no bigserial NOT NULL, + id varchar NOT NULL, + email varchar NOT NULL, + ip varchar, + created_at date NOT NULL, + created_by varchar(10485760) NULL, + modified_at date NULL, + modified_by varchar(10485760) NULL +); + + +INSERT INTO public.common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, + sub_description, created_at, created_by, modified_at, modified_by) +VALUES (67, 'OTHERS', 48, NULL, 3, true, '변경필요', NULL, '2024-01-10', 'admin', NULL, NULL) +ON CONFLICT DO NOTHING; +INSERT INTO public.common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, + sub_description, created_at, created_by, modified_at, modified_by) +VALUES (68, 'AGE_99', 51, NULL, 6, true, '변경필요', NULL, '2024-01-10', 'admin', NULL, NULL) +ON CONFLICT DO NOTHING; + +UPDATE public.common_code +SET code_name='MALE', + modified_at = CURRENT_TIMESTAMP, + modified_by = 'admin' +WHERE code_no = 49 +; + +UPDATE public.common_code +SET code_name='FEMALE', + modified_at = CURRENT_TIMESTAMP, + modified_by = 'admin' +WHERE code_no = 50 +; \ No newline at end of file diff --git a/module-api/src/main/resources/db/migration/V1.0.8__modify_member_no_column_type.sql b/module-api/src/main/resources/db/migration/V1.0.8__modify_member_no_column_type.sql deleted file mode 100644 index 73c731a5..00000000 --- a/module-api/src/main/resources/db/migration/V1.0.8__modify_member_no_column_type.sql +++ /dev/null @@ -1,82 +0,0 @@ -DROP VIEW member_view; - -ALTER SEQUENCE member_member_no_seq as bigint; - -ALTER TABLE member ALTER COLUMN member_no TYPE bigint USING member_no::bigint; -ALTER TABLE wash_info ALTER COLUMN member_no TYPE bigint USING member_no::bigint; -ALTER TABLE car_info ALTER COLUMN member_no TYPE bigint USING member_no::bigint; -ALTER TABLE auth ALTER COLUMN member_no TYPE bigint USING member_no::bigint; - --- ### member 테이블의 member_no 컬럼 타입 변경으로 인한 뷰 drop-create ### --- 복호화 뷰 생성, 업데이트 -CREATE OR REPLACE VIEW member_view AS -SELECT - member_no, - id, - encode(pgp_sym_decrypt(password, 'changeRequired')::bytea, 'escape') as password, - encode(pgp_sym_decrypt(email, 'changeRequired')::bytea, 'escape') as email, - gender, - birthdate, - created_at, - created_by, - modified_at, - modified_by -FROM member; - - --- 뷰 insert 함수 -CREATE OR REPLACE FUNCTION member_view_insert_trigger() - RETURNS TRIGGER AS $$ -BEGIN - INSERT INTO member (member_no, id, "password", email, gender, birthdate, created_at, created_by, modified_at, modified_by) - VALUES (nextval('member_member_no_seq'::regclass), NEW.id, pgp_sym_encrypt(NEW.password::TEXT, 'changeRequired'), pgp_sym_encrypt(NEW.email::TEXT, 'changeRequired'), NEW.gender, NEW.birthdate, NEW.created_at, NEW.created_by, NEW.modified_at, NEW.modified_by); - - RETURN NEW; - -END; -$$ LANGUAGE plpgsql; - --- 뷰 insert 트리거 -CREATE TRIGGER member_view_insert_trigger - INSTEAD OF INSERT ON member_view - FOR EACH ROW EXECUTE FUNCTION member_view_insert_trigger(); - - --- 뷰 update 함수 -CREATE OR REPLACE FUNCTION member_view_update_trigger() - RETURNS TRIGGER AS $$ -BEGIN - UPDATE member SET - id = NEW.id, - "password" = pgp_sym_encrypt(NEW.password::TEXT, 'changeRequired'), - email = pgp_sym_encrypt(NEW.email::TEXT, 'changeRequired'), - gender = NEW.gender, - birthdate = NEW.birthdate, - created_at = NEW.created_at, - created_by = NEW.created_by, - modified_at = NEW.modified_at, - modified_by = NEW.modified_by - WHERE member_no = NEW.member_no; - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - --- 뷰 update 트리거 -CREATE TRIGGER member_view_update_trigger - INSTEAD OF UPDATE ON member_view - FOR EACH ROW EXECUTE FUNCTION member_view_update_trigger(); - - --- 뷰 delete 함수 -CREATE OR REPLACE FUNCTION member_view_delete_trigger() - RETURNS TRIGGER AS $$ -BEGIN - DELETE FROM member WHERE id = OLD.id; - RETURN OLD; -END; -$$ LANGUAGE plpgsql; - --- 뷰 delete 트리거 -CREATE TRIGGER member_view_delete_trigger - INSTEAD OF DELETE ON member_view - FOR EACH ROW EXECUTE FUNCTION member_view_delete_trigger(); \ No newline at end of file diff --git a/module-api/src/main/resources/db/migration/V1.0.9__add_clientIP_to_auth.sql b/module-api/src/main/resources/db/migration/V1.0.9__add_clientIP_to_auth.sql new file mode 100644 index 00000000..579e6241 --- /dev/null +++ b/module-api/src/main/resources/db/migration/V1.0.9__add_clientIP_to_auth.sql @@ -0,0 +1,2 @@ +ALTER TABLE auth + ADD COLUMN client_ip varchar; \ No newline at end of file diff --git a/module-api/src/main/resources/db/migration/V1.0.9__modify_no_column_type_in_tables.sql b/module-api/src/main/resources/db/migration/V1.0.9__modify_no_column_type_in_tables.sql deleted file mode 100644 index ba741dcb..00000000 --- a/module-api/src/main/resources/db/migration/V1.0.9__modify_no_column_type_in_tables.sql +++ /dev/null @@ -1,15 +0,0 @@ --- SEQUENCE 데이터 타입 변경 -ALTER SEQUENCE brand_brand_no_seq as bigint; -ALTER SEQUENCE wash_info_wash_no_seq as bigint; -ALTER SEQUENCE car_info_car_no_seq as bigint; -ALTER SEQUENCE product_product_no_seq as bigint; - --- TABLE COLUMN 데이터 타입 변경 -ALTER TABLE brand ALTER COLUMN brand_no TYPE bigint USING brand_no::bigint; -ALTER TABLE wash_info ALTER COLUMN wash_no TYPE bigint USING wash_no::bigint; -ALTER TABLE car_info ALTER COLUMN car_no TYPE bigint USING car_no::bigint; -ALTER TABLE product ALTER COLUMN product_no TYPE bigint USING product_no::bigint; -ALTER TABLE product ALTER COLUMN brand_no TYPE bigint USING brand_no::bigint; -ALTER TABLE common_code ALTER COLUMN code_no TYPE bigint USING code_no::bigint; -ALTER TABLE common_code ALTER COLUMN upper_no TYPE bigint USING upper_no::bigint; -ALTER TABLE auth ALTER COLUMN auth_no TYPE bigint USING auth_no::bigint; \ No newline at end of file diff --git a/module-api/src/main/resources/db/migration/V1.1.0__alter_reported_product_column_type.sql b/module-api/src/main/resources/db/migration/V1.1.0__alter_reported_product_column_type.sql deleted file mode 100644 index 1b9f19d2..00000000 --- a/module-api/src/main/resources/db/migration/V1.1.0__alter_reported_product_column_type.sql +++ /dev/null @@ -1,4 +0,0 @@ -ALTER TABLE reported_product - ALTER COLUMN stddusqy TYPE TEXT USING stddusqy::TEXT, - ALTER COLUMN safe_sd TYPE TEXT USING safe_sd::TEXT, - ALTER COLUMN propos TYPE TEXT USING propos::TEXT; diff --git a/module-api/src/main/resources/db/migration/V1.1.1__alter_brand_and_product_table.sql b/module-api/src/main/resources/db/migration/V1.1.1__alter_brand_and_product_table.sql deleted file mode 100644 index 1a6ef887..00000000 --- a/module-api/src/main/resources/db/migration/V1.1.1__alter_brand_and_product_table.sql +++ /dev/null @@ -1,103 +0,0 @@ --- Brand 에 company_name 컬럼 추가 -drop table if exists product; -drop TABLE if exists Brand; - -CREATE TABLE if not exists Brand -( - brand_no BIGSERIAL PRIMARY KEY, - brand_name VARCHAR NOT NULL, - company_name VARCHAR NOT NULL, - description VARCHAR, - nation_name VARCHAR, - created_at DATE NOT NULL, - created_by VARCHAR NOT NULL, - modified_at DATE, - modified_by VARCHAR -); - - -Insert Into brand (brand_no, brand_name, company_name, nation_name, created_at, created_by) -VALUES (1, '더클래스', '코스메디슨(CosMedicine)', '대한민국', current_date, 'admin'), - (51, '크리스탈 코트', '(주) 불스원', '대한민국', current_date, 'admin'), - (101, '불스원', '(주) 불스원', '대한민국', current_date, 'admin'), - (151, '글로스브로', '주식회사 제이씨웍스', '대한민국', current_date, 'admin'), - (201, '파이어볼', '(주)파이어볼', '대한민국', current_date, 'admin'), - (251, '티에이씨시스템', '티에이씨시스템', '대한민국', current_date, 'admin'), - (301, '기온테크놀로지', '주식회사 기온테크놀로지(Gyeon Technology)', '대한민국', current_date, 'admin'), - (351, '루나틱폴리시', '(주) 불스원', '대한민국', current_date, 'admin'), - (401, '젠틀맨', '주식회사 에스피', '대한민국', current_date, 'admin'), - (451, '디아만테', '현일', '대한민국', current_date, 'admin'), - (501, '(주)오토왁스', '(주)오토왁스', '대한민국', current_date, 'admin'), - - (551, '라보코스메디카', '주식회사 대흥아이앤씨(INC)', '이탈리아', current_date, 'admin'), - (601, '마프라', '주식회사 대흥아이앤씨(INC)', '이탈리아', current_date, 'admin'), - (651, '매니악', '주식회사 대흥아이앤씨(INC)', '이탈리아', current_date, 'admin'), - (701, 'EXQ', '이엑스큐(EXQ)', '대한민국', current_date, 'admin'), - (751, 'EXQ', '이엑스큐(EXQ)', '중국', current_date, 'admin'), - - (801, '터틀왁스', '(주)에스씨에이', '미국', current_date, 'admin'), - (851, '잭스왁스', '주식회사 잭스왁스코리아', '미국', current_date, 'admin'), - (901, '오토브라이트', '주식회사 오토브라이트다이렉트코리아(AUTOBRITE DIRECT KOREA CO.,LTD)', '영국', current_date, 'admin'), - - (951, '블루믹스', '제이웍스 (J-works)', '대한민국', current_date, 'admin'), - (1001, '림피오', '림피오(LIMPIO)', '대한민국', current_date, 'admin'), - (1051, '좀비', '한국씨앤에스', '대한민국', current_date, 'admin'), - (1101, '카프로', '(주)카프로코리아', '대한민국', current_date, 'admin'), - - (1151, '이지카케어', '발스코리아(주)', '영국', current_date, 'admin'), - (1201, '니그린', '(주)다스모터스', '영국', current_date, 'admin'), - (1251, '도도쥬스', '주식회사투왁스', '영국', current_date, 'admin'), --- (1301, '루미너스', '주식회사 엘엠', '대한민국', current_date, 'admin'), - - (1351, '소낙스', '(주)알레스', '독일', current_date, 'admin'), - (1401, '스마트왁스', '에이큐에이 주식회사', '미국', current_date, 'admin'), - (1451, '케미컬가이', '에이큐에이 주식회사', '미국', current_date, 'admin'), - (1501, '리바이브', '주식회사 에스피', '영국', current_date, 'admin'), - (1551, '보닉스', '대흥아이앤씨', '브라질', current_date, 'admin'), - -- (1601, '더클래스', '더클래스', '대한민국', current_date, 'admin'), - (1651, '루미너스', '루미너스코리아', '대한민국', current_date, 'admin'); - -CREATE SEQUENCE IF NOT EXISTS product_product_no_seq START WITH 1 INCREMENT BY 50; - -CREATE TABLE product -( - product_no BIGSERIAL NOT NULL, - created_at date NOT NULL, - created_by VARCHAR(255) NOT NULL, - modified_at date, - modified_by VARCHAR(255), - product_name VARCHAR(255) NOT NULL, - report_no VARCHAR(255), - product_type VARCHAR(255), - manufacture_nation VARCHAR(255), - company_name VARCHAR(255), - safety_status INTEGER NOT NULL, - issued_date date NOT NULL, - barcode VARCHAR(255), - img_src VARCHAR(255), - view_count INTEGER NOT NULL, - safety_inspection_standard TEXT, - upper_item VARCHAR(255), - item VARCHAR(255), - propose TEXT, - weight VARCHAR(255), - usage TEXT, - usage_precaution TEXT, - first_aid TEXT, - main_substance TEXT, - allergic_substance TEXT, - other_substance TEXT, - preservative TEXT, - surfactant TEXT, - fluorescent_whitening TEXT, - manufacture_type VARCHAR(255), - manufacture_method VARCHAR(255), - brand_no BIGSERIAL, - CONSTRAINT pk_product PRIMARY KEY (product_no) -); - -ALTER TABLE product - ADD CONSTRAINT FK_PRODUCT_ON_BRAND_NO FOREIGN KEY (brand_no) REFERENCES brand (brand_no); - -alter sequence product_product_no_seq increment by 50; -alter sequence brand_brand_no_seq increment by 50; \ No newline at end of file diff --git a/module-api/src/main/resources/db/migration/V1.1.2__alter_table_member_and_add_common_code.sql b/module-api/src/main/resources/db/migration/V1.1.2__alter_table_member_and_add_common_code.sql deleted file mode 100644 index 5859e408..00000000 --- a/module-api/src/main/resources/db/migration/V1.1.2__alter_table_member_and_add_common_code.sql +++ /dev/null @@ -1,187 +0,0 @@ --- 복호화 view 삭제 -drop view IF EXISTS member_view; - --- 함수 삭제 -drop function IF EXISTS member_view_delete_trigger; -drop function IF EXISTS member_view_insert_trigger; -drop function IF EXISTS member_view_update_trigger; - - --- member 테이블 컬럼 자료형 변경 -ALTER TABLE public."member" DROP COLUMN IF EXISTS birthdate; -ALTER TABLE public."member" DROP COLUMN IF EXISTS gender; -ALTER TABLE public."member" ADD COLUMN IF NOT EXISTS age int; -ALTER TABLE public."member" ADD COLUMN IF NOT EXISTS gender int; - --- 복호화 뷰 생성 -CREATE -OR REPLACE VIEW member_view AS -SELECT member_no, - id, - encode(pgp_sym_decrypt(password, 'changeRequired')::bytea, 'escape') as password, - encode(pgp_sym_decrypt(email, 'changeRequired')::bytea, 'escape') as email, - gender, - age, - created_at, - created_by, - modified_at, - modified_by -FROM member; - -/** 뷰 TO 테이블 바인딩 **/ - --- 뷰 insert 함수 -CREATE -OR REPLACE FUNCTION member_view_insert_trigger() - RETURNS TRIGGER AS $$ -BEGIN -INSERT INTO member (member_no, id, "password", email, gender, age, created_at, created_by, modified_at, modified_by) -VALUES (nextval('member_member_no_seq'::regclass), NEW.id, pgp_sym_encrypt(NEW.password::TEXT, 'changeRequired'), - pgp_sym_encrypt(NEW.email::TEXT, 'changeRequired'), NEW.gender, NEW.age, NEW.created_at, NEW.created_by, - NEW.modified_at, NEW.modified_by); - -RETURN NEW; - -END; -$$ -LANGUAGE plpgsql; - --- 뷰 insert 트리거 -CREATE TRIGGER member_view_insert_trigger - INSTEAD OF INSERT - ON member_view - FOR EACH ROW EXECUTE FUNCTION member_view_insert_trigger(); - - --- 뷰 update 함수 -CREATE -OR REPLACE FUNCTION member_view_update_trigger() - RETURNS TRIGGER AS $$ -BEGIN -UPDATE member -SET id = NEW.id, - "password" = pgp_sym_encrypt(NEW.password::TEXT, 'changeRequired'), - email = pgp_sym_encrypt(NEW.email::TEXT, 'changeRequired'), - gender = NEW.gender, - age = NEW.age, - created_at = NEW.created_at, - created_by = NEW.created_by, - modified_at = NEW.modified_at, - modified_by = NEW.modified_by -WHERE member_no = NEW.member_no; -RETURN NEW; -END; -$$ -LANGUAGE plpgsql; - --- 뷰 update 트리거 -CREATE TRIGGER member_view_update_trigger - INSTEAD OF UPDATE - ON member_view - FOR EACH ROW EXECUTE FUNCTION member_view_update_trigger(); - - --- 뷰 delete 함수 -CREATE -OR REPLACE FUNCTION member_view_delete_trigger() - RETURNS TRIGGER AS $$ -BEGIN -DELETE -FROM member -WHERE id = OLD.id; -RETURN OLD; -END; -$$ -LANGUAGE plpgsql; - --- 뷰 delete 트리거 -CREATE TRIGGER member_view_delete_trigger - INSTEAD OF DELETE - ON member_view - FOR EACH ROW EXECUTE FUNCTION member_view_delete_trigger(); - -/** 공통 코드 추가 **/ - -TRUNCATE TABLE public.common_code; - -INSERT INTO public.common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, - created_by, modified_at, modified_by) -VALUES (1, 'carbrand', 0, '', 1, false, '차량 브랜드', '2023-12-28', 'admin', NULL, NULL), - (10, 'cartype', 0, '', 2, true, '차량 유형', '2023-12-28', 'admin', NULL, NULL), - (15, 'segment', 0, '', 3, true, '차량 크기', '2023-12-28', 'admin', NULL, NULL), - (20, 'color', 0, '', 4, true, '차량 색상', '2023-12-28', 'admin', NULL, NULL), - (29, 'perl', 0, '', 5, true, '차량 페인트 펄', '2023-12-28', 'admin', NULL, NULL), - (30, 'TRUE', 28, 'perl', 1, true, '있음', '2023-12-28', 'admin', NULL, NULL), - (31, 'FALSE', 28, 'perl', 2, true, '없음', '2023-12-28', 'admin', NULL, NULL), - (32, 'clearcoat', 0, '', 6, true, '차량 페인트 마감', '2023-12-28', 'admin', NULL, NULL), - (33, 'TRUE', 31, 'clearcoat', 1, true, '있음', '2023-12-28', 'admin', NULL, NULL), - (34, 'FALSE', 31, 'clearcoat', 2, true, '없음', '2023-12-28', 'admin', NULL, NULL); -INSERT INTO public.common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, - created_by, modified_at, modified_by) -VALUES (35, 'driving', 0, '', 7, true, '주행환경', '2023-12-28', 'admin', NULL, NULL), - (36, 'comport', 34, 'driving', 1, true, '도심', '2023-12-28', 'admin', NULL, NULL), - (37, 'highway', 34, 'driving', 2, true, '고속', '2023-12-28', 'admin', NULL, NULL), - (38, 'complex', 34, 'driving', 3, true, '복합', '2023-12-28', 'admin', NULL, NULL), - (39, 'parking', 0, '', 8, true, '주차환경', '2023-12-28', 'admin', NULL, NULL), - (40, 'house', 38, 'parking', 1, true, '실내/지하', '2023-12-28', 'admin', NULL, NULL), - (41, 'road', 38, 'parking', 2, true, '노상', '2023-12-28', 'admin', NULL, NULL), - (42, 'piloti', 38, 'parking', 3, true, '필로티', '2023-12-28', 'admin', NULL, NULL), - (43, 'interest', 0, '', 9, true, '주요관심사', '2023-12-28', 'admin', NULL, NULL), - (48, 'gender', 0, NULL, 10, true, NULL, '2024-01-10', 'admin', NULL, NULL); -INSERT INTO public.common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, - created_by, modified_at, modified_by) -VALUES (49, 'man', 48, 'gender', 1, true, '남성', '2024-01-10', 'admin', NULL, NULL), - (50, 'woman', 48, 'gender', 2, true, '여성', '2024-01-10', 'admin', NULL, NULL), - (21, 'FFFFFF', 20, 'color', 1, true, '흰색', '2023-12-28', 'admin', NULL, NULL), - (22, '808080', 20, 'color', 5, true, '쥐색', '2023-12-28', 'admin', NULL, NULL), - (23, '37383C', 20, 'color', 6, true, '검정색', '2023-12-28', 'admin', NULL, NULL), - (24, 'FF4500', 20, 'color', 4, true, '빨간색', '2023-12-28', 'admin', NULL, NULL), - (25, 'FFD400', 20, 'color', 2, true, '노란색', '2023-12-28', 'admin', NULL, NULL), - (51, 'age', 0, NULL, 11, true, '연령대', '2024-01-10', 'admin', NULL, NULL), - (52, 'AGE_20', 51, 'age', 1, true, '20대 이하', '2024-01-10', 'admin', NULL, NULL), - (53, 'AGE_30', 51, 'age', 2, true, '30대', '2024-01-10', 'admin', NULL, NULL); -INSERT INTO public.common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, - created_by, modified_at, modified_by) -VALUES (54, 'AGE_40', 51, 'age', 3, true, '40대', '2024-01-10', 'admin', NULL, NULL), - (55, 'AGE_50', 51, 'age', 4, true, '50대', '2024-01-10', 'admin', NULL, NULL), - (56, 'AGE_60', 51, 'age', 5, true, '60대 이상', '2024-01-10', 'admin', NULL, NULL), - (57, 'frequency', 0, NULL, 12, true, '세차빈도', '2024-01-10', 'admin', NULL, NULL), - (58, 'month1', 57, 'frequency', 1, true, '월 평균 1회', '2024-01-10', 'admin', NULL, NULL), - (59, 'month2', 57, 'frequency', 2, true, '월 평균 2회', '2024-01-10', 'admin', NULL, NULL), - (60, 'month3', 57, 'frequency', 3, true, '월 평균 3회', '2024-01-10', 'admin', NULL, NULL), - (61, 'month4', 57, 'frequency', 4, true, '월 평균 4회', '2024-01-10', 'admin', NULL, NULL), - (62, 'cost', 0, NULL, 13, true, '지출비용', '2024-01-10', 'admin', NULL, NULL), - (63, '1to3', 62, 'cost', 1, true, '월 평균 1~3만원', '2024-01-10', 'admin', NULL, NULL); -INSERT INTO public.common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, - created_by, modified_at, modified_by) -VALUES (64, '4to6', 62, 'cost', 2, true, '월 평균 4~6만원', '2024-01-10', 'admin', NULL, NULL), - (26, '2F4F4F', 20, 'color', 3, true, '초록색', '2023-12-28', 'admin', NULL, NULL), - (27, '145B7D', 20, 'color', 4, true, '파란색', '2023-12-28', 'admin', NULL, NULL), - (28, '6E0000', 20, 'color', 7, true, '기타', '2023-12-28', 'admin', NULL, NULL), - (44, 'outercare', 42, 'interest', 1, true, '도장', '2023-12-28', 'admin', NULL, NULL), - (45, 'tire', 42, 'interest', 2, true, '타이어', '2023-12-28', 'admin', NULL, NULL), - (46, 'wheel', 42, 'interest', 3, true, '휠', '2023-12-28', 'admin', NULL, NULL), - (47, 'innercare', 42, 'interest', 4, true, '실내', '2023-12-28', 'admin', NULL, NULL), - (2, 'hyundai', 1, 'carbrand', 1, false, '현대', '2023-12-28', 'admin', NULL, NULL), - (3, 'kia', 1, 'carbrand', 2, false, '기아', '2023-12-28', 'admin', NULL, NULL); -INSERT INTO public.common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, - created_by, modified_at, modified_by) -VALUES (4, 'renault', 1, 'carbrand', 3, false, '르노', '2023-12-28', 'admin', NULL, NULL), - (5, 'kg mobility', 1, 'carbrand', 4, false, 'KG모빌리티', '2023-12-28', 'admin', NULL, NULL), - (5, 'chevrolet', 1, 'carbrand', 5, false, '쉐보레', '2023-12-28', 'admin', NULL, NULL), - (6, 'benz', 1, 'carbrand', 6, false, '벤츠', '2023-12-28', 'admin', NULL, NULL), - (7, 'bmw', 1, 'carbrand', 7, false, '벰베', '2023-12-28', 'admin', NULL, NULL), - (8, 'audi', 1, 'carbrand', 8, false, '아우디', '2023-12-28', 'admin', NULL, NULL), - (9, 'etc', 1, 'carbrand', 9, false, '기타', '2023-12-28', 'admin', NULL, NULL), - (11, 'sedan', 10, 'cartype', 1, true, '세단', '2023-12-28', 'admin', NULL, NULL), - (12, 'hatchback', 10, 'cartype', 2, true, '해치백', '2023-12-28', 'admin', NULL, NULL), - (13, 'suv', 10, 'cartype', 3, true, 'SUV', '2023-12-28', 'admin', NULL, NULL); -INSERT INTO public.common_code (code_no, code_name, upper_no, upper_name, sort_order, is_used, description, created_at, - created_by, modified_at, modified_by) -VALUES (14, 'etc', 10, 'cartype', 4, true, '기타', '2023-12-28', 'admin', NULL, NULL), - (16, 'micro', 15, 'segment', 1, true, '경차', '2023-12-28', 'admin', NULL, NULL), - (17, 'subcompact', 15, 'segment', 2, true, '소형', '2023-12-28', 'admin', NULL, NULL), - (18, 'compact', 15, 'segment', 3, true, '중형', '2023-12-28', 'admin', NULL, NULL), - (65, '7to9', 62, 'cost', 3, true, '월 평균 7~9만원', '2024-01-10', 'admin', NULL, NULL), - (66, '10over', 62, 'cost', 4, true, '월 평균 10만원 이상', '2024-01-10', 'admin', NULL, NULL), - (19, 'fullsize', 15, 'segment', 4, true, '대형', '2023-12-28', 'admin', NULL, NULL); diff --git a/module-api/src/main/resources/db/migration/V1.1.3__create_concerned_product_table.sql b/module-api/src/main/resources/db/migration/V1.1.3__create_concerned_product_table.sql deleted file mode 100644 index 8cb08c7d..00000000 --- a/module-api/src/main/resources/db/migration/V1.1.3__create_concerned_product_table.sql +++ /dev/null @@ -1,22 +0,0 @@ -CREATE TABLE if not exists ConcernedProduct -( - prdt_no VARCHAR(255) PRIMARY KEY, - prdt_name VARCHAR(255) NOT NULL, - slfsfcfst_no VARCHAR(255) NOT NULL, - item VARCHAR(255) NOT NULL, - comp_nm VARCHAR(255) NOT NULL, - inspected_organization VARCHAR(255), - issued_date DATE, - upper_item VARCHAR(255), - product_type VARCHAR(255), - renewed_type VARCHAR(255), - safety_inspection_standard TEXT, - kid_protect_package VARCHAR(255), - manufacture_nation VARCHAR(255), - product_definition VARCHAR(255), - manufacture TEXT, - created_at DATE NOT NULL DEFAULT CURRENT_DATE, - created_by VARCHAR(255) NOT NULL DEFAULT 'admin', - modified_at DATE, - modified_by VARCHAR(255) -); \ No newline at end of file diff --git a/module-api/src/main/resources/logback-spring.xml b/module-api/src/main/resources/logback-spring.xml index a5902ea3..80f3e16e 100644 --- a/module-api/src/main/resources/logback-spring.xml +++ b/module-api/src/main/resources/logback-spring.xml @@ -34,6 +34,20 @@ + + + + + + + + + + + + + + diff --git a/module-api/src/test/java/com/kernel360/auth/controller/AuthControllerTest.java b/module-api/src/test/java/com/kernel360/auth/controller/AuthControllerTest.java new file mode 100644 index 00000000..e2a51a75 --- /dev/null +++ b/module-api/src/test/java/com/kernel360/auth/controller/AuthControllerTest.java @@ -0,0 +1,59 @@ +package com.kernel360.auth.controller; + +import static com.kernel360.common.utils.RestDocumentUtils.getDocumentRequest; +import static com.kernel360.common.utils.RestDocumentUtils.getDocumentResponse; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.kernel360.auth.dto.AuthDto; +import com.kernel360.common.ControllerTest; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureWebMvc; +import org.springframework.http.MediaType; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.test.web.servlet.MvcResult; + +@AutoConfigureWebMvc +class AuthControllerTest extends ControllerTest { + @Test + void 토큰_갱신요청이_왔을때_리스폰스로_상태코드201과_갱신토큰이_잘_보내지는지() throws Exception { + String requestToken = "token"; + String resultToken = "reissuanceToken"; + AuthDto dto = new AuthDto(resultToken); + + when(acceptInterceptor.preHandle(any(), any(), any())).thenReturn(true); + when(authService.generateTokenAndSaveAuth(any())).thenReturn(resultToken); + + try (MockedStatic mocked = Mockito.mockStatic(AuthDto.class)) { + mocked.when(() -> AuthDto.of(resultToken)).thenReturn(dto); + + MvcResult actions = mockMvc.perform(get("/auth/reissuanceJWT") + .header("Authorization", requestToken)) + .andExpect(status().isCreated()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.status").value(201)) + .andExpect(jsonPath("$.code").value("BAC001")) + .andExpect(jsonPath("$.message").value("JWT 토큰 재발급 성공")) + .andDo(document("auth/reissuanceJWT", + getDocumentRequest(), + getDocumentResponse(), + responseFields( + fieldWithPath("status").description("상태 코드"), + fieldWithPath("message").description("응답 메시지"), + fieldWithPath("code").description("비즈니스 코드"), + fieldWithPath("value.jwtToken").type(JsonFieldType.STRING) + .description("JWT 토큰") + ))) + .andReturn(); + } + } +} \ No newline at end of file diff --git a/module-api/src/test/java/com/kernel360/common/ControllerTest.java b/module-api/src/test/java/com/kernel360/common/ControllerTest.java index fce52ff9..7f5d3a0b 100644 --- a/module-api/src/test/java/com/kernel360/common/ControllerTest.java +++ b/module-api/src/test/java/com/kernel360/common/ControllerTest.java @@ -1,12 +1,15 @@ package com.kernel360.common; import com.fasterxml.jackson.databind.ObjectMapper; +import com.kernel360.auth.controller.AuthController; +import com.kernel360.auth.service.AuthService; import com.kernel360.commoncode.controller.CommonCodeController; import com.kernel360.commoncode.service.CommonCodeService; import com.kernel360.global.Interceptor.AcceptInterceptor; -import com.kernel360.main.controller.MainContoller; +import com.kernel360.main.controller.MainController; import com.kernel360.main.service.MainService; import com.kernel360.member.controller.MemberController; +import com.kernel360.member.service.FindCredentialService; import com.kernel360.member.service.MemberService; import com.kernel360.mypage.controller.MyPageController; import com.kernel360.product.controller.ProductController; @@ -21,8 +24,9 @@ CommonCodeController.class, MemberController.class, ProductController.class, - MainContoller.class, - MyPageController.class + MainController.class, + MyPageController.class, + AuthController.class }) @AutoConfigureRestDocs public abstract class ControllerTest { @@ -47,4 +51,10 @@ public abstract class ControllerTest { @MockBean protected MainService mainService; + + @MockBean + protected FindCredentialService findCredentialService; + + @MockBean + protected AuthService authService; } diff --git a/module-api/src/test/java/com/kernel360/common/utils/RestDocumentUtils.java b/module-api/src/test/java/com/kernel360/common/utils/RestDocumentUtils.java index 2e03072b..f3ba925a 100644 --- a/module-api/src/test/java/com/kernel360/common/utils/RestDocumentUtils.java +++ b/module-api/src/test/java/com/kernel360/common/utils/RestDocumentUtils.java @@ -8,8 +8,8 @@ public interface RestDocumentUtils { static OperationRequestPreprocessor getDocumentRequest() { - return preprocessRequest(modifyUris().scheme("http") - .host("washpedia.my-project.life") + return preprocessRequest(modifyUris().scheme("https") + .host("devapi.washfit.site") .removePort(), prettyPrint()); } diff --git a/module-api/src/test/java/com/kernel360/commoncode/controller/CommonCodeControllerRestDocsTest.java b/module-api/src/test/java/com/kernel360/commoncode/controller/CommonCodeControllerRestDocsTest.java index eb506fd7..42dc0b54 100644 --- a/module-api/src/test/java/com/kernel360/commoncode/controller/CommonCodeControllerRestDocsTest.java +++ b/module-api/src/test/java/com/kernel360/commoncode/controller/CommonCodeControllerRestDocsTest.java @@ -58,7 +58,7 @@ void commmonCodeSearch() throws Exception { //when ResultActions result = this.mockMvc.perform( - get("/commoncode/test/{codeName}", pathVariable)); + get("/commoncode/{codeName}", pathVariable)); //then diff --git a/module-api/src/test/java/com/kernel360/main/controller/MainContollerTest.java b/module-api/src/test/java/com/kernel360/main/controller/MainContollerTest.java deleted file mode 100644 index 4a55382a..00000000 --- a/module-api/src/test/java/com/kernel360/main/controller/MainContollerTest.java +++ /dev/null @@ -1,200 +0,0 @@ -package com.kernel360.main.controller; - -import com.kernel360.common.ControllerTest; -import com.kernel360.main.dto.BannerDto; -import com.kernel360.main.dto.RecommendProductsDto; -import com.kernel360.product.dto.ProductDto; -import com.navercorp.fixturemonkey.FixtureMonkey; -import com.navercorp.fixturemonkey.api.introspector.*; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.EnumSource; -import org.springframework.http.MediaType; -import org.springframework.restdocs.payload.JsonFieldType; - -import java.util.Arrays; -import java.util.List; - -import static com.kernel360.common.utils.RestDocumentUtils.getDocumentRequest; -import static com.kernel360.common.utils.RestDocumentUtils.getDocumentResponse; -import static org.mockito.Mockito.when; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; -import static org.springframework.restdocs.payload.PayloadDocumentation.*; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import static org.hamcrest.collection.IsCollectionWithSize.hasSize; -import static org.mockito.Mockito.*; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -class MainContollerTest extends ControllerTest { - private FixtureMonkey fixtureMonkey; - - @BeforeEach - void 준비() { - fixtureMonkey = FixtureMonkey.builder() - .objectIntrospector(new FailoverIntrospector( - Arrays.asList( - BuilderArbitraryIntrospector.INSTANCE, - FieldReflectionArbitraryIntrospector.INSTANCE, - ConstructorPropertiesArbitraryIntrospector.INSTANCE, - BeanArbitraryIntrospector.INSTANCE - ) - )) - .build(); - } - - @Test - void 메인페이지_배너요청이_왔을때_200_응답이_잘반환되는지() throws Exception { - //given - BannerDto bannerDto = BannerDto.of(1L, "classpath:static/bannerSample.png", "Banner Image"); - when(mainService.getSampleBanner()).thenReturn(bannerDto); - - //when & then - mockMvc.perform(get("/banner")) - .andExpect(status().isOk()) - .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(jsonPath("$.status").value(200)) - .andExpect(jsonPath("$.value.id").value(1L)) - .andExpect(jsonPath("$.value.image").value("classpath:static/bannerSample.png")) - .andExpect(jsonPath("$.value.alt").value("Banner Image")) - .andDo(document( - "banner/get-banner", - getDocumentRequest(), - getDocumentResponse(), - responseFields(beneathPath("value").withSubsectionId("value"), - fieldWithPath("id").type(JsonFieldType.NUMBER).description("배너 ID"), - fieldWithPath("image").type(JsonFieldType.STRING).description("배너 이미지 경로"), - fieldWithPath("alt").type(JsonFieldType.STRING).description("배너 대체 텍스트") - ) - )); - - verify(mainService, times(1)).getSampleBanner(); - } - - - @Test - void 메인페이지_추천상품을_호출할때_200응답과_데이터가_잘보내지는지() throws Exception { - //given - List recommendProductsDtos = fixtureMonkey.giveMeBuilder(RecommendProductsDto.class) - .setNotNull("id").setNotNull("image").setNotNull("alt").setNotNull("productName") - .sampleList(5); - when(productService.getRecommendProductList()).thenReturn(recommendProductsDtos); - - //when & then - mockMvc.perform(get("/recommend-products")) - .andExpect(status().isOk()) - .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(jsonPath("$.status").value(200)) - .andExpect(jsonPath("$.code").value("PMB001")) - .andExpect(jsonPath("$.message").value("추천제품정보 조회 성공")) - .andExpect(jsonPath("$.value", hasSize(5))) - .andExpect(jsonPath("$.value[0].id").isNotEmpty()) - .andExpect(jsonPath("$.value[0].image").isNotEmpty()) - .andExpect(jsonPath("$.value[0].alt").isNotEmpty()) - .andExpect(jsonPath("$.value[0].productName").isNotEmpty()) - .andDo(document( - "recommend-products/get-recommend-products", - getDocumentRequest(), - getDocumentResponse(), - responseFields( - fieldWithPath("status").type(JsonFieldType.NUMBER).description("상태 코드"), - fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"), - fieldWithPath("message").type(JsonFieldType.STRING).description("응답메세지"), - fieldWithPath("value").type(JsonFieldType.ARRAY).description("제품 리스트"), - fieldWithPath("value[].id").type(JsonFieldType.NUMBER).description("제품 ID"), - fieldWithPath("value[].image").type(JsonFieldType.STRING).description("이미지 URL"), - fieldWithPath("value[].alt").type(JsonFieldType.STRING).description("이미지 대체 텍스트"), - fieldWithPath("value[].productName").type(JsonFieldType.STRING).description("제품명") - ) - )); - } - @ParameterizedTest - @EnumSource(value = Sort.class, names = {"VIEW_COUNT_PRODUCT_ORDER", "VIOLATION_PRODUCT_LIST", "RECENT_PRODUCT_ORDER"}) - void 메인페이지_조회순으로_제품리스트_요청시_200응답과_데이터가_잘_반환되는지(Sort sortType) throws Exception { - // given - List productDtos = fixtureMonkey.giveMeBuilder(ProductDto.class) - .setNotNull("productNo") - .setNotNull("productName") - .setNotNull("viewCount") - .setNotNull("reportNumber") - .setNotNull("safetyStatus") - .setNotNull("createdAt") - .setNotNull("createdBy") - .sampleList(5); - - - if (sortType == Sort.VIEW_COUNT_PRODUCT_ORDER) { - when(productService.getProductListOrderByViewCount()).thenReturn(productDtos); - } else if (sortType == Sort.VIOLATION_PRODUCT_LIST) { - when(productService.getViolationProducts()).thenReturn(productDtos); - } else if (sortType == Sort.RECENT_PRODUCT_ORDER) { - when(productService.getRecentProducts()).thenReturn(productDtos); - } - - // when & then - mockMvc.perform(get("/products/rank") - .queryParam("sortType", sortType.getOrderType())) - .andExpect(status().isOk()) - .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(jsonPath("$.status").value(200)) - .andExpect(jsonPath("$.value", hasSize(5))) - .andExpect(jsonPath("$.value[0].productNo").isNotEmpty()) - .andExpect(jsonPath("$.value[0].productName").isNotEmpty()) - .andExpect(jsonPath("$.value[0].viewCount").isNotEmpty()) - .andDo(document( - "products/get-products-view-count-order", - getDocumentRequest(), - getDocumentResponse(), - responseFields( - fieldWithPath("status").type(JsonFieldType.NUMBER).description("상태 코드"), - fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"), - fieldWithPath("message").type(JsonFieldType.STRING).description("응답메세지"), - fieldWithPath("value").type(JsonFieldType.ARRAY).description("제품 리스트"), - fieldWithPath("value[].productNo").type(JsonFieldType.NUMBER).description("제품 ID"), - fieldWithPath("value[].productName").type(JsonFieldType.STRING).description("제품명"), - fieldWithPath("value[].barcode").type(JsonFieldType.STRING).optional().description("바코드"), - fieldWithPath("value[].imageSource").type(JsonFieldType.STRING).optional().description("이미지 소스"), - fieldWithPath("value[].reportNumber").type(JsonFieldType.STRING).description("신고 번호"), - fieldWithPath("value[].safetyStatus").type(JsonFieldType.STRING).description("안전 상태"), - fieldWithPath("value[].viewCount").type(JsonFieldType.NUMBER).description("조회수"), - fieldWithPath("value[].createdAt").type(JsonFieldType.STRING).description("생성날짜"), - fieldWithPath("value[].createdBy").type(JsonFieldType.STRING).description("생성자"), - fieldWithPath("value[].modifiedAt").type(JsonFieldType.STRING).optional().description("수정날짜"), - fieldWithPath("value[].modifiedBy").type(JsonFieldType.STRING).optional().description("수정자") - ) - )); - - if (sortType == Sort.VIEW_COUNT_PRODUCT_ORDER) { - verify(productService, times(1)).getProductListOrderByViewCount(); - } else if (sortType == Sort.VIOLATION_PRODUCT_LIST) { - verify(productService, times(1)).getViolationProducts(); - } else if (sortType == Sort.RECENT_PRODUCT_ORDER) { - verify(productService, times(1)).getRecentProducts(); - } - - } - - -//FIXME:: 좋아요 기능구현후, 데이터를 바탕으로 추천기능구현후, 테스트코드 변경예정 -// @Test -// void 메인페이지_추천제품_정렬_옵션으로_요청시_200응답과_데이터가_잘_반환되는지() throws Exception { -// // given -// List recommendProducts = fixtureMonkey.giveMeBuilder(RecommendProductsDto.class) -// .sampleList(5); -// when(productService.getRecommendProductList()).thenReturn(recommendProducts); -// -// // when & then -// mockMvc.perform(get("/products/rank") -// .queryParam("sortType", "recommend-order")) -// .andExpect(status().isOk()) -// .andExpect(content().contentType(MediaType.APPLICATION_JSON)) -// .andExpect(jsonPath("$.status").value(200)); -//// .andExpect(jsonPath("$.value", hasSize(5))); -// ; -// -// verify(productService, times(1)).getRecommendProductList(); -// -// } -} \ No newline at end of file diff --git a/module-api/src/test/java/com/kernel360/main/controller/MainControllerTest.java b/module-api/src/test/java/com/kernel360/main/controller/MainControllerTest.java new file mode 100644 index 00000000..bfcc0340 --- /dev/null +++ b/module-api/src/test/java/com/kernel360/main/controller/MainControllerTest.java @@ -0,0 +1,277 @@ +package com.kernel360.main.controller; + +import com.kernel360.common.ControllerTest; +import com.kernel360.main.dto.BannerDto; +import com.kernel360.main.dto.RecommendProductsDto; +import com.kernel360.product.dto.ProductDto; +import com.kernel360.product.entity.SafetyStatus; +import com.navercorp.fixturemonkey.FixtureMonkey; +import com.navercorp.fixturemonkey.api.introspector.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.http.MediaType; +import org.springframework.restdocs.payload.JsonFieldType; + +import java.util.Arrays; +import java.util.List; + +import static com.kernel360.common.utils.RestDocumentUtils.getDocumentRequest; +import static com.kernel360.common.utils.RestDocumentUtils.getDocumentResponse; +import static org.mockito.Mockito.when; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +class MainControllerTest extends ControllerTest { + private FixtureMonkey fixtureMonkey; + + @BeforeEach + void 준비() { + fixtureMonkey = FixtureMonkey.builder() + .objectIntrospector(new FailoverIntrospector( + Arrays.asList( + BuilderArbitraryIntrospector.INSTANCE, + FieldReflectionArbitraryIntrospector.INSTANCE, + ConstructorPropertiesArbitraryIntrospector.INSTANCE, + BeanArbitraryIntrospector.INSTANCE + ) + )) + .build(); + } + + @Test + void 메인페이지_배너요청이_왔을때_200_응답이_잘반환되는지() throws Exception { + //given + BannerDto bannerDto = BannerDto.of(1L, "classpath:static/bannerSample.png", "Banner Image"); + when(mainService.getBanner()).thenReturn(bannerDto); + + //when & then + mockMvc.perform(get("/banner")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.status").value(200)) + .andExpect(jsonPath("$.value.id").value(1L)) + .andExpect(jsonPath("$.value.imageSource").value("classpath:static/bannerSample.png")) + .andExpect(jsonPath("$.value.alt").value("Banner Image")) + .andDo(document( + "banner/get-banner", + getDocumentRequest(), + getDocumentResponse(), + responseFields(beneathPath("value").withSubsectionId("value"), + fieldWithPath("id").type(JsonFieldType.NUMBER).description("배너 ID"), + fieldWithPath("imageSource").type(JsonFieldType.STRING).description("배너 이미지 경로"), + fieldWithPath("alt").type(JsonFieldType.STRING).description("배너 대체 텍스트") + ) + )); + + verify(mainService, times(1)).getBanner(); + } + + @Test + void 메인페이지_추천상품을_호출할때_200응답과_데이터가_잘보내지는지() throws Exception { + // given + List recommendProductsDtos = fixtureMonkey.giveMeBuilder(RecommendProductsDto.class) + .setNotNull("id") + .setNotNull("imageSource") + .setNotNull("alt") + .setNotNull("productName") + .sampleList(5); + + Pageable pageable = PageRequest.of(0, 5); + Page page = new PageImpl<>(recommendProductsDtos, pageable, recommendProductsDtos.size()); + when(productService.getRecommendProducts(any(Pageable.class))).thenReturn(page); + +// when & then + mockMvc.perform(get("/recommend-products")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.status").value(200)) + .andExpect(jsonPath("$.code").value("PMB001")) + .andExpect(jsonPath("$.message").value("추천제품정보 조회 성공")) + .andExpect(jsonPath("$.value.content[*].id").exists()) + .andExpect(jsonPath("$.value.content[*].imageSource").exists()) + .andExpect(jsonPath("$.value.content[*].alt").exists()) + .andExpect(jsonPath("$.value.content[*].productName").exists()) + .andDo(document("recommend-products/get-recommend-products", + getDocumentRequest(), + getDocumentResponse(), + responseFields( + fieldWithPath("status").type(JsonFieldType.NUMBER).description("상태 코드"), + fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"), + fieldWithPath("message").type(JsonFieldType.STRING).description("응답메세지"), + fieldWithPath("value").type(JsonFieldType.OBJECT).description("응답 본문의 루트 객체"), + fieldWithPath("value.content[].id").type(JsonFieldType.NUMBER).description("제품 ID"), + fieldWithPath("value.content[].imageSource").type(JsonFieldType.STRING).description("이미지 URL"), + fieldWithPath("value.content[].alt").type(JsonFieldType.STRING).description("이미지 대체 텍스트"), + fieldWithPath("value.content[].productName").type(JsonFieldType.STRING).description("제품명"), + fieldWithPath("value.pageable").type(JsonFieldType.OBJECT).description("페이지 정보"), + fieldWithPath("value.pageable.pageNumber").type(JsonFieldType.NUMBER).description("페이지 번호"), + fieldWithPath("value.pageable.pageSize").type(JsonFieldType.NUMBER).description("페이지 크기"), + fieldWithPath("value.pageable.sort").type(JsonFieldType.OBJECT).description("정렬 정보"), + fieldWithPath("value.pageable.sort.empty").type(JsonFieldType.BOOLEAN).description("정렬이 비어 있는지 여부"), + fieldWithPath("value.pageable.sort.sorted").type(JsonFieldType.BOOLEAN).description("정렬이 되었는지 여부"), + fieldWithPath("value.pageable.sort.unsorted").type(JsonFieldType.BOOLEAN).description("정렬이 되지 않았는지 여부"), + fieldWithPath("value.pageable.offset").type(JsonFieldType.NUMBER).description("페이지 오프셋"), + fieldWithPath("value.pageable.paged").type(JsonFieldType.BOOLEAN).description("페이징 여부"), + fieldWithPath("value.pageable.unpaged").type(JsonFieldType.BOOLEAN).description("비페이징 여부"), + fieldWithPath("value.sort").type(JsonFieldType.OBJECT).description("정렬 정보"), + fieldWithPath("value.sort.empty").type(JsonFieldType.BOOLEAN).description("정렬이 비어 있는지 여부"), + fieldWithPath("value.sort.sorted").type(JsonFieldType.BOOLEAN).description("정렬이 되었는지 여부"), + fieldWithPath("value.sort.unsorted").type(JsonFieldType.BOOLEAN).description("정렬이 되지 않았는지 여부"), + fieldWithPath("value.totalPages").type(JsonFieldType.NUMBER).description("총 페이지 수"), + fieldWithPath("value.totalElements").type(JsonFieldType.NUMBER).description("총 요소 수"), + fieldWithPath("value.last").type(JsonFieldType.BOOLEAN).description("마지막 페이지 여부"), + fieldWithPath("value.number").type(JsonFieldType.NUMBER).description("현재 페이지 번호"), + fieldWithPath("value.size").type(JsonFieldType.NUMBER).description("페이지 크기"), + fieldWithPath("value.numberOfElements").type(JsonFieldType.NUMBER).description("현재 페이지의 요소 수"), + fieldWithPath("value.first").type(JsonFieldType.BOOLEAN).description("첫 페이지 여부"), + fieldWithPath("value.empty").type(JsonFieldType.BOOLEAN).description("비어 있는 페이지 여부") + ) + )); + + } + + @ParameterizedTest + @EnumSource(value = Sort.class, names = {"VIEW_COUNT_PRODUCT_ORDER", "VIOLATION_PRODUCT_LIST", "RECENT_PRODUCT_ORDER"}) + void 메인페이지_조회순으로_제품리스트_요청시_200응답과_데이터가_잘_반환되는지(Sort sortType) throws Exception { + // given + List productDtos = fixtureMonkey.giveMeBuilder(ProductDto.class) + .setNotNull("productNo") + .setNotNull("productName") + .setNotNull("viewCount") + .setNotNull("reportNumber") + .set("safetyStatus", SafetyStatus.SAFE) + .setNotNull("createdAt") + .setNotNull("createdBy") + .sampleList(20); + + + + Pageable pageable = PageRequest.of(0, 20); + Page productPage = new PageImpl<>(productDtos, pageable, productDtos.size()); + + if (sortType == Sort.VIEW_COUNT_PRODUCT_ORDER) { + when(productService.getProductListOrderByViewCount(any(Pageable.class))).thenReturn(productPage); + } else if (sortType == Sort.VIOLATION_PRODUCT_LIST) { + when(productService.getViolationProducts(any(Pageable.class))).thenReturn(productPage); + } else if (sortType == Sort.RECENT_PRODUCT_ORDER) { + when(productService.getRecentProducts(any(Pageable.class))).thenReturn(productPage); + } + + // when & then + mockMvc.perform(get("/products/rank") + .queryParam("sortType", sortType.getOrderType())) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.status").value(200)) + .andExpect(jsonPath("$.value.content[*].productNo").exists()) + .andExpect(jsonPath("$.value.content[*].productName").exists()) + .andExpect(jsonPath("$.value.content[*].viewCount").exists()) + .andExpect(jsonPath("$.value.content[*].reportNumber").exists()) + .andExpect(jsonPath("$.value.content[*].safetyStatus").exists()) + .andExpect(jsonPath("$.value.content[*].createdAt").exists()) + .andExpect(jsonPath("$.value.content[*].createdBy").exists()) + .andExpect(jsonPath("$.value.pageable.pageNumber").exists()) + .andExpect(jsonPath("$.value.pageable.pageSize").exists()) + .andExpect(jsonPath("$.value.pageable.sort.empty").exists()) + .andExpect(jsonPath("$.value.totalPages").exists()) + .andExpect(jsonPath("$.value.totalElements").exists()) + .andExpect(jsonPath("$.value.number").exists()) + .andExpect(jsonPath("$.value.size").exists()) + .andExpect(jsonPath("$.value.numberOfElements").exists()) + .andExpect(jsonPath("$.value.empty").exists()) + .andExpect(jsonPath("$.value.last").exists()) + .andExpect(jsonPath("$.value.first").exists()) + .andExpect(jsonPath("$.value.sort.empty").exists()) + .andDo(document("products/get-products-view-count-order", + getDocumentRequest(), + getDocumentResponse(), + queryParameters( + parameterWithName("sortType").description("정렬 기준, 가능한 값: VIEW_COUNT_PRODUCT_ORDER, VIOLATION_PRODUCT_LIST, RECENT_PRODUCT_ORDER") + ), + responseFields( + fieldWithPath("status").type(JsonFieldType.NUMBER).description("응답 상태 코드"), + fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"), + fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지"), + subsectionWithPath("value").description("응답 데이터의 루트 객체"), + fieldWithPath("value.content[].productNo").type(JsonFieldType.NUMBER).description("제품 번호"), + fieldWithPath("value.content[].productName").type(JsonFieldType.STRING).description("제품명"), + fieldWithPath("value.content[].barcode").type(JsonFieldType.STRING).optional().description("바코드").ignored(), + fieldWithPath("value.content[].imageSource").type(JsonFieldType.STRING).optional().description("이미지 소스").ignored(), + fieldWithPath("value.content[].reportNumber").type(JsonFieldType.STRING).description("보고서 번호"), + fieldWithPath("value.content[].safetyStatus").type(JsonFieldType.STRING).description("안전 상태"), + fieldWithPath("value.content[].viewCount").type(JsonFieldType.NUMBER).description("조회수"), + fieldWithPath("value.content[].brand").type(JsonFieldType.STRING).description("브랜드").optional(), + fieldWithPath("value.content[].upperItem").type(JsonFieldType.STRING).description("상위 항목").optional(), + fieldWithPath("value.content[].createdAt").type(JsonFieldType.STRING).description("생성 날짜"), + fieldWithPath("value.content[].createdBy").type(JsonFieldType.STRING).description("생성자"), + fieldWithPath("value.content[].modifiedAt").type(JsonFieldType.STRING).optional().description("수정 날짜").ignored(), + fieldWithPath("value.content[].modifiedBy").type(JsonFieldType.STRING).optional().description("수정자").ignored(), + fieldWithPath("value.pageable.pageNumber").type(JsonFieldType.NUMBER).description("페이지 번호"), + fieldWithPath("value.pageable.pageSize").type(JsonFieldType.NUMBER).description("페이지 크기"), + fieldWithPath("value.pageable.sort.empty").type(JsonFieldType.BOOLEAN).description("정렬이 비어 있는지 여부"), + fieldWithPath("value.pageable.sort.sorted").type(JsonFieldType.BOOLEAN).description("정렬이 되었는지 여부"), + fieldWithPath("value.pageable.sort.unsorted").type(JsonFieldType.BOOLEAN).description("정렬이 되지 않았는지 여부"), + fieldWithPath("value.pageable.offset").type(JsonFieldType.NUMBER).description("오프셋"), + fieldWithPath("value.pageable.paged").type(JsonFieldType.BOOLEAN).description("페이징 여부"), + fieldWithPath("value.pageable.unpaged").type(JsonFieldType.BOOLEAN).description("비페이징 여부"), + fieldWithPath("value.last").type(JsonFieldType.BOOLEAN).description("마지막 페이지 여부"), + fieldWithPath("value.totalPages").type(JsonFieldType.NUMBER).description("총 페이지 수"), + fieldWithPath("value.totalElements").type(JsonFieldType.NUMBER).description("총 요소 수"), + fieldWithPath("value.number").type(JsonFieldType.NUMBER).description("현재 페이지 번호"), + fieldWithPath("value.first").type(JsonFieldType.BOOLEAN).description("첫 페이지 여부"), + fieldWithPath("value.size").type(JsonFieldType.NUMBER).description("페이지 크기"), + fieldWithPath("value.numberOfElements").type(JsonFieldType.NUMBER).description("현재 페이지의 요소 수"), + fieldWithPath("value.empty").type(JsonFieldType.BOOLEAN).description("비어 있는 페이지 여부"), + fieldWithPath("value.sort.empty").type(JsonFieldType.BOOLEAN).description("정렬이 비어 있는지 여부"), + fieldWithPath("value.sort.sorted").type(JsonFieldType.BOOLEAN).description("정렬이 되었는지 여부"), + fieldWithPath("value.sort.unsorted").type(JsonFieldType.BOOLEAN).description("정렬이 되지 않았는지 여부") + ) + )); + + + if (sortType == Sort.VIEW_COUNT_PRODUCT_ORDER) { + verify(productService, times(1)).getProductListOrderByViewCount(pageable); + } else if (sortType == Sort.VIOLATION_PRODUCT_LIST) { + verify(productService, times(1)).getViolationProducts(pageable); + } else if (sortType == Sort.RECENT_PRODUCT_ORDER) { + verify(productService, times(1)).getRecentProducts(pageable); + } + + } + + +//FIXME:: 좋아요 기능구현후, 데이터를 바탕으로 추천기능구현후, 테스트코드 변경예정 +// @Test +// void 메인페이지_추천제품_정렬_옵션으로_요청시_200응답과_데이터가_잘_반환되는지() throws Exception { +// // given +// List recommendProducts = fixtureMonkey.giveMeBuilder(RecommendProductsDto.class) +// .sampleList(5); +// when(productService.getRecommendProductList()).thenReturn(recommendProducts); +// +// // when & then +// mockMvc.perform(get("/products/rank") +// .queryParam("sortType", "recommend-order")) +// .andExpect(status().isOk()) +// .andExpect(content().contentType(MediaType.APPLICATION_JSON)) +// .andExpect(jsonPath("$.status").value(200)); +//// .andExpect(jsonPath("$.value", hasSize(5))); +// ; +// +// verify(productService, times(1)).getRecommendProductList(); +// +// } +} \ No newline at end of file diff --git a/module-api/src/test/java/com/kernel360/member/controller/MemberControllerTest.java b/module-api/src/test/java/com/kernel360/member/controller/MemberControllerTest.java index da2b1a33..62732867 100644 --- a/module-api/src/test/java/com/kernel360/member/controller/MemberControllerTest.java +++ b/module-api/src/test/java/com/kernel360/member/controller/MemberControllerTest.java @@ -1,18 +1,28 @@ package com.kernel360.member.controller; +import static com.kernel360.common.utils.RestDocumentUtils.getDocumentRequest; +import static com.kernel360.common.utils.RestDocumentUtils.getDocumentResponse; +import static org.mockito.BDDMockito.given; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; + import com.fasterxml.jackson.databind.ObjectMapper; import com.kernel360.common.ControllerTest; +import com.kernel360.member.dto.MemberCredentialDto; import com.kernel360.member.dto.MemberDto; +import java.time.LocalDate; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; -import java.time.LocalDate; - -import static org.mockito.BDDMockito.given; - class MemberControllerTest extends ControllerTest { @@ -21,7 +31,7 @@ class MemberControllerTest extends ControllerTest { void 회원가입요청() throws Exception { /** given 목데이터 세팅 **/ - MemberDto memberDto = MemberDto.of("testID", "gunsight777@naver.com", "testPassword", "man","30"); + MemberDto memberDto = MemberDto.of("testID", "gunsight777@naver.com", "testPassword", "MALE", "30"); ObjectMapper objectMapper = new ObjectMapper(); String param = objectMapper.writeValueAsString(memberDto); @@ -30,38 +40,41 @@ class MemberControllerTest extends ControllerTest { mockMvc.perform(MockMvcRequestBuilders.post("/member/join") //이 다음 restful .contentType(MediaType.APPLICATION_JSON) .content(param)) - .andExpect(MockMvcResultMatchers.status().isCreated()) //기대 결과 상태값 - .andReturn(); + .andExpect(MockMvcResultMatchers.status().isCreated()) //기대 결과 상태값 + .andReturn(); } @Test @DisplayName("아이디_비밀번호를_받아_로그인_성공시_200반환") void 로그인() throws Exception { - + // given MemberDto memberDto = MemberDto.of("testID", "testPassword"); MemberDto memberInfo = new MemberDto(1L, - "test01", - "kernel360@kernel360.com", - "", - null, - null, - LocalDate.now(), - "test01", - null, - null, - "dummyToken" - ); + "test01", + "kernel360@kernel360.com", + "", + null, + null, + LocalDate.now(), + "test01", + null, + null, + "dummyToken" + ); + MockHttpServletRequest request = new MockHttpServletRequest(); + + // when ObjectMapper objectMapper = new ObjectMapper(); String param = objectMapper.writeValueAsString(memberDto); - given(memberService.login(memberDto)).willReturn(memberInfo); + given(memberService.login(memberDto, request)).willReturn(memberInfo); /** then **/ mockMvc.perform(MockMvcRequestBuilders.post("/member/login") .contentType(MediaType.APPLICATION_JSON) .content(param)) - .andExpect(MockMvcResultMatchers.status().isOk()) - .andReturn(); + .andExpect(MockMvcResultMatchers.status().isOk()) + .andReturn(); } @@ -77,8 +90,8 @@ class MemberControllerTest extends ControllerTest { mockMvc.perform(MockMvcRequestBuilders.get("/member/duplicatedCheckId/{id}", id) .contentType(MediaType.APPLICATION_JSON) .content(message)) - .andExpect(MockMvcResultMatchers.status().isOk()) - .andReturn(); + .andExpect(MockMvcResultMatchers.status().isOk()) + .andReturn(); } @Test @@ -93,9 +106,127 @@ class MemberControllerTest extends ControllerTest { mockMvc.perform(MockMvcRequestBuilders.get("/member/duplicatedCheckEmail/{email}", email) .contentType(MediaType.APPLICATION_JSON) .content(message)) - .andExpect(MockMvcResultMatchers.status().isOk()) - .andReturn(); + .andExpect(MockMvcResultMatchers.status().isOk()) + .andReturn(); + } + + + @Test + @DisplayName("이메일을 입력 받아 아이디 찾기 이메일 발송") + void 아이디_찾기_이메일_발송_검사() throws Exception { + /** given 목데이터 세팅 **/ + MemberCredentialDto dto = MemberCredentialDto.of(null, "kernel360@gmail.com", null, null); + MemberDto memberDto = MemberDto.of("testMemberId", "testPassword001"); + + given(memberService.findByEmail(dto.email())).willReturn(memberDto); + + ObjectMapper objectMapper = new ObjectMapper(); + String dtoAsString = objectMapper.writeValueAsString(dto); + + /** then **/ + mockMvc.perform(MockMvcRequestBuilders.post("/member/find-memberId") + .contentType(MediaType.APPLICATION_JSON) + .content(dtoAsString)) + .andExpect(MockMvcResultMatchers.status().isOk()).andDo(document( + "member/find-memberId", getDocumentRequest(), getDocumentResponse(), + requestFields( + fieldWithPath("email").type(JsonFieldType.STRING).description("회원가입시 입력한 이메일"), + fieldWithPath("authToken").type(JsonFieldType.NULL).description("비밀번호 재설정 UUID 토큰(사용하지 않음)"), + fieldWithPath("memberId").type(JsonFieldType.NULL).description("회원 아이디(사용하지 않음)"), + fieldWithPath("password").type(JsonFieldType.NULL).description("변경할 비밀번호(사용하지 않음)") + ), + responseFields( + fieldWithPath("status").type(JsonFieldType.NUMBER).description("HTTP 상태 코드"), + fieldWithPath("code").type(JsonFieldType.STRING).description("비즈니스 코드"), + fieldWithPath("message").type(JsonFieldType.STRING).description("상세 메시지"), + fieldWithPath("value").type(JsonFieldType.NULL).description("JSON BODY 데이터") + ))); } + @Test + @DisplayName("회원 아이디를 입력받아 비밀번호 재설정 이메일 발송에 성공한다") + void 회원_아이디로_비밀번호_재설정_이메일_발송_검사() throws Exception { + // given + MemberCredentialDto credentialDto = MemberCredentialDto.of(null, null, "kernel360", null); + MemberDto memberDto = MemberDto.of("testMemberId", "testPassword001"); + + given(memberService.findByMemberId(credentialDto.memberId())).willReturn(memberDto); + given(findCredentialService.generatePasswordResetUri(memberDto)).willReturn("테스트 URI"); + + ObjectMapper objectMapper = new ObjectMapper(); + String dtoAsString = objectMapper.writeValueAsString(credentialDto); + + // then + mockMvc.perform(MockMvcRequestBuilders.post("/member/find-password") + .contentType(MediaType.APPLICATION_JSON) + .content(dtoAsString)) + .andExpect(MockMvcResultMatchers.status().isOk()).andDo(document( + "member/find-password", getDocumentRequest(), getDocumentResponse(), + requestFields( + fieldWithPath("email").type(JsonFieldType.NULL).description("회원가입시 입력한 이메일(사용하지 않음)"), + fieldWithPath("authToken").type(JsonFieldType.NULL).description("비밀번호 재설정 UUID 토큰(사용하지 않음)"), + fieldWithPath("memberId").type(JsonFieldType.STRING).description("회원 아이디"), + fieldWithPath("password").type(JsonFieldType.NULL).description("변경할 비밀번호(사용하지 않음)") + ), + responseFields( + fieldWithPath("status").type(JsonFieldType.NUMBER).description("HTTP 상태 코드"), + fieldWithPath("code").type(JsonFieldType.STRING).description("비즈니스 코드"), + fieldWithPath("message").type(JsonFieldType.STRING).description("상세 메시지"), + fieldWithPath("value").type(JsonFieldType.NULL).description("JSON BODY 데이터") + ))); + } + + @Test + @DisplayName("비밀번호 재설정 토큰을 확인하여 비밀번호 재설정 페이지 반환에 성공") + void 비밀번호_재설정_토큰을_확인하고_비밀번호_재설정_페이지로_이동_검사() throws Exception { + String token = "testToken-1234-5678"; + given(findCredentialService.getData(token)).willReturn("kernel360-testId"); + + mockMvc.perform(MockMvcRequestBuilders.get("/member/reset-password?token="+token) + .contentType(MediaType.APPLICATION_JSON) + .content(token)) + .andExpect(MockMvcResultMatchers.status().isFound()).andDo(document( + "member/get-reset-password", getDocumentRequest(), getDocumentResponse(), + queryParameters( + parameterWithName("token").description("비밀번호 재설정 UUID 토큰") + ), + responseFields( + fieldWithPath("status").type(JsonFieldType.NUMBER).description("HTTP 상태 코드"), + fieldWithPath("code").type(JsonFieldType.STRING).description("비즈니스 코드"), + fieldWithPath("message").type(JsonFieldType.STRING).description("상세 메시지"), + fieldWithPath("value").type(JsonFieldType.STRING).description("JSON BODY 데이터 - 비밀번호 재설정 토큰") + ) + )); + } + + @Test + @DisplayName("재설정할 비밀번호와 재설정 토큰을 입력받아 비밀번호 재설정에 성공한다") + void 비밀번호_재설정_검사() throws Exception{ + MemberCredentialDto credentialDto = MemberCredentialDto.of("testToken", null, null, "resetPassword"); + given(findCredentialService.resetPassword(credentialDto)).willReturn("testToken"); + + ObjectMapper objectMapper = new ObjectMapper(); + String dtoAsString = objectMapper.writeValueAsString(credentialDto); + + mockMvc.perform(MockMvcRequestBuilders.post("/member/reset-password") + .contentType(MediaType.APPLICATION_JSON) + .content(dtoAsString)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(document( + "member/post-reset-password", getDocumentRequest(), getDocumentResponse(), + requestFields( + fieldWithPath("authToken").type(JsonFieldType.STRING).description("비밀번호 재설정 UUID 토큰"), + fieldWithPath("email").type(JsonFieldType.NULL).description("회원가입시 입력한 이메일(사용하지 않음)"), + fieldWithPath("memberId").type(JsonFieldType.NULL).description("회원 아이디(사용하지 않음)"), + fieldWithPath("password").type(JsonFieldType.STRING).description("변경할 비밀번호") + ), + responseFields( + fieldWithPath("status").type(JsonFieldType.NUMBER).description("HTTP 상태 코드"), + fieldWithPath("code").type(JsonFieldType.STRING).description("비즈니스 코드"), + fieldWithPath("message").type(JsonFieldType.STRING).description("상세 메시지"), + fieldWithPath("value").type(JsonFieldType.NULL).description("JSON BODY 데이터") + ) + )); + } +} -} \ No newline at end of file diff --git a/module-api/src/test/java/com/kernel360/member/service/MemberServiceTest.java b/module-api/src/test/java/com/kernel360/member/service/MemberServiceTest.java index 91e10550..86803709 100644 --- a/module-api/src/test/java/com/kernel360/member/service/MemberServiceTest.java +++ b/module-api/src/test/java/com/kernel360/member/service/MemberServiceTest.java @@ -2,6 +2,7 @@ import com.kernel360.auth.entity.Auth; import com.kernel360.auth.repository.AuthRepository; +import com.kernel360.auth.service.AuthService; import com.kernel360.member.dto.MemberDto; import com.kernel360.member.entity.Member; import com.kernel360.member.repository.MemberRepository; @@ -16,6 +17,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import java.util.Optional; +import org.springframework.mock.web.MockHttpServletRequest; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; @@ -35,6 +37,9 @@ class MemberServiceTest { @InjectMocks private MemberService memberService; + @InjectMocks + private AuthService authService; + @Mock private ConvertSHA256 convertSHA256; @@ -49,7 +54,7 @@ public void init() { void 회원가입_로직_테스트() { /** given **/ - MemberDto requestDto = MemberDto.of("testID", "gunsight777@naver.com", "testPassword", "man", "AGE_40"); + MemberDto requestDto = MemberDto.of("testID", "gunsight777@naver.com", "testPassword", "MALE", "AGE_40"); Member member = memberService.getNewJoinMemberEntity(requestDto); /** when **/ @@ -81,7 +86,8 @@ public void init() { /** given **/ MemberDto loginDto = MemberDto.of("test03", "1234qwer"); Member mockLoginEntity = Member.loginMember(loginDto.id(), loginDto.password()); - Member mockEntity = Member.of(502L, loginDto.id(), "test03@naver.com", "0eb9de69892882d54516e03e30098354a2e39cea36adab275b6300c737c942fd", 0, 0); + Member mockEntity = Member.of(502L, loginDto.id(), "test03@naver.com", + "0eb9de69892882d54516e03e30098354a2e39cea36adab275b6300c737c942fd", 0, 0); String mockToken = "dummy_token"; /** stub **/ @@ -108,7 +114,7 @@ public void init() { Member memberEntity = Member.of(502L, "test03", null, null, 0, 0); String mockToken = "mockToken"; Auth auth = Auth.jwt(null, 502L, mockToken); - + MockHttpServletRequest request = new MockHttpServletRequest(); /** stub **/ when(jwt.generateToken(anyString())).thenReturn(mockToken); when(authRepository.findOneByMemberNo(anyLong())).thenReturn(auth); @@ -117,10 +123,11 @@ public void init() { String token = jwt.generateToken(memberEntity.getId()); //mockToken return 정상 수행 확인 String encryptToken = convertSHA256.convertToSHA256(token); //해싱부분은 정적메소드여서 그런지 stub이 안됨...(?) Auth authJwt = authRepository.findOneByMemberNo(memberEntity.getMemberNo()); - + String clientIP = AuthService.getClientIP(request); authJwt = Optional.ofNullable(authJwt) - .map(modifyAuth -> memberService.modifyAuthJwt(modifyAuth, encryptToken)) - .orElseGet(() -> memberService.createAuthJwt(memberEntity.getMemberNo(), encryptToken)); + .map(modifyAuth -> authService.modifyAuthJwt(modifyAuth, encryptToken, clientIP)) + .orElseGet( + () -> authService.createAuthJwt(memberEntity.getMemberNo(), encryptToken, clientIP)); authRepository.save(authJwt); @@ -132,7 +139,7 @@ public void init() { @Test @DisplayName("회원가입시_아이디_중복_있으면_TRUE") - void 회원가입_아이디_중복_검사_있으면_TRUE(){ + void 회원가입_아이디_중복_검사_있으면_TRUE() { /** given **/ String id = "test01"; @@ -147,12 +154,12 @@ public void init() { /** then **/ verify(memberRepository).findOneById(id); - assertTrue(result,"중복있으면TRUE이다."); + assertTrue(result, "중복있으면TRUE이다."); } @Test @DisplayName("회원가입시_아이디_중복_없으면_FALSE") - void 회원가입_아이디_중복_검사_없으면_FALSE(){ + void 회원가입_아이디_중복_검사_없으면_FALSE() { /** given **/ String id = "test01"; @@ -166,12 +173,12 @@ public void init() { /** then **/ verify(memberRepository).findOneById(id); - assertFalse(result,"중복없으면FALSE이다."); + assertFalse(result, "중복없으면FALSE이다."); } @Test @DisplayName("회원가입시_이메일_중복_검사_있으면_TRUE") - void 회원가입_이메일_중복_있으면_TRUE(){ + void 회원가입_이메일_중복_있으면_TRUE() { /** given **/ String email = "kernel360@kernel360.co.kr"; @@ -187,12 +194,12 @@ public void init() { /** then **/ verify(memberRepository).findOneByEmail(email); - assertTrue(result,"중복있으면TRUE이다."); + assertTrue(result, "중복있으면TRUE이다."); } @Test @DisplayName("회원가입시_이메일_중복_검사_없으면_FALSE") - void 회원가입_이메일_중복_없으면_FALSE(){ + void 회원가입_이메일_중복_없으면_FALSE() { /** given **/ String email = "kernel360@kernel360.co.kr"; @@ -207,7 +214,7 @@ public void init() { /** then **/ verify(memberRepository).findOneByEmail(email); - assertFalse(result,"중복없으면FALSE이다."); + assertFalse(result, "중복없으면FALSE이다."); } } diff --git a/module-api/src/test/java/com/kernel360/mypage/controller/MyPageControllerTest.java b/module-api/src/test/java/com/kernel360/mypage/controller/MyPageControllerTest.java index 2584e3db..ae44a44c 100644 --- a/module-api/src/test/java/com/kernel360/mypage/controller/MyPageControllerTest.java +++ b/module-api/src/test/java/com/kernel360/mypage/controller/MyPageControllerTest.java @@ -1,57 +1,250 @@ -package com.kernel360.mypage.controller; - -import com.kernel360.common.ControllerTest; -import com.kernel360.product.dto.ProductDto; -import com.navercorp.fixturemonkey.FixtureMonkey; -import com.navercorp.fixturemonkey.api.introspector.*; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.http.MediaType; - -import java.util.Arrays; -import java.util.List; - -import static org.hamcrest.collection.IsCollectionWithSize.hasSize; -import static org.mockito.Mockito.*; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -class MyPageControllerTest extends ControllerTest { - - private FixtureMonkey fixtureMonkey; - - @BeforeEach - void 준비() { - fixtureMonkey = FixtureMonkey.builder() - .objectIntrospector(new FailoverIntrospector( - Arrays.asList( - BuilderArbitraryIntrospector.INSTANCE, - FieldReflectionArbitraryIntrospector.INSTANCE, - ConstructorPropertiesArbitraryIntrospector.INSTANCE, - BeanArbitraryIntrospector.INSTANCE - ) - )) - .build(); - } +//package com.kernel360.mypage.controller; +// +//import com.kernel360.common.ControllerTest; +//import com.kernel360.member.dto.CarInfoDto; +//import com.kernel360.member.dto.MemberDto; +//import com.kernel360.member.dto.MemberInfo; +//import com.navercorp.fixturemonkey.FixtureMonkey; +//import com.navercorp.fixturemonkey.api.introspector.*; +//import net.jqwik.api.Arbitraries; +//import org.junit.jupiter.api.BeforeEach; +//import org.junit.jupiter.api.Test; +//import org.springframework.http.MediaType; +// +//import java.util.Arrays; +//import java.util.Map; +// +//import static org.mockito.Mockito.*; +//import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +//import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +//import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +//import static org.springframework.restdocs.payload.PayloadDocumentation.*; +//import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +//import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +//import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +// +//class MyPageControllerTest extends ControllerTest { +// +// private FixtureMonkey fixtureMonkey; +// +// @BeforeEach +// void 준비() { +// fixtureMonkey = FixtureMonkey.builder() +// .objectIntrospector(new FailoverIntrospector( +// Arrays.asList( +// BuilderArbitraryIntrospector.INSTANCE, +// FieldReflectionArbitraryIntrospector.INSTANCE, +// ConstructorPropertiesArbitraryIntrospector.INSTANCE, +// BeanArbitraryIntrospector.INSTANCE +// ) +// )) +// .build(); +// } // // @Test -// void 마이페이지_메인요청이왔을때_200_응답이_잘반환되는지() throws Exception { -// //given -// List productDtoList = fixtureMonkey.giveMeBuilder(ProductDto.class).sampleList(5); +// void 회원정보_조회_요청시_200응답과_정보가_잘_반환되는지() throws Exception { +// // given +// String authToken = "Bearer test-token"; +// MemberDto expectedDto = fixtureMonkey.giveMeBuilder(MemberDto.class) +// .set("jwtToken", "Bearer test-token") +// .setNotNull("memberNo").setNotNull("id").setNotNull("email") +// .setNotNull("password").setNotNull("gender").setNotNull("age") +// .setNotNull("createdAt").setNotNull("createdBy").setNotNull("modifiedAt").setNotNull("modifiedBy") +// .sample(); +// // -// //when -// when(productService.getProductListOrderByViewCount()).thenReturn(productDtoList); +// when(memberService.findMemberByToken(eq(authToken))).thenReturn(expectedDto); +// +// // when & then +// mockMvc.perform(get("/mypage/member") +// .header("Authorization", authToken)) +// .andExpect(status().isOk()) +// .andExpect(content().contentType(MediaType.APPLICATION_JSON)) +// .andExpect(jsonPath("$.status").value(200)) +// .andExpect(jsonPath("$.code").value("BMC003")) +// .andExpect(jsonPath("$.message").value("회원 조회 성공")) +// .andExpect(jsonPath("$.value.memberNo").value(expectedDto.memberNo())) +// .andExpect(jsonPath("$.value.id").value(expectedDto.id())) +// .andExpect(jsonPath("$.value.email").value(expectedDto.email())) +// .andExpect(jsonPath("$.value.gender").value(expectedDto.gender())) +// .andExpect(jsonPath("$.value.age").value(expectedDto.age())) +// .andExpect(jsonPath("$.value.createdAt").value(expectedDto.createdAt().toString())) +// .andExpect(jsonPath("$.value.createdBy").value(expectedDto.createdBy())) +// .andExpect(jsonPath("$.value.modifiedAt").value(expectedDto.modifiedAt().toString())) +// .andExpect(jsonPath("$.value.modifiedBy").value(expectedDto.modifiedBy())) +// .andDo(document("mypage-member/get-member-info", +// responseFields( +// fieldWithPath("status").description("상태 코드"), +// fieldWithPath("code").description("비즈니스 코드"), +// fieldWithPath("message").description("응답 메세지"), +// fieldWithPath("value.memberNo").description("회원 번호"), +// fieldWithPath("value.id").description("회원 아이디"), +// fieldWithPath("value.email").description("이메일"), +// fieldWithPath("value.password").ignored(), +// fieldWithPath("value.gender").description("성별"), +// fieldWithPath("value.age").description("나이"), +// fieldWithPath("value.createdAt").description("생성 날짜"), +// fieldWithPath("value.createdBy").description("생성자"), +// fieldWithPath("value.modifiedAt").description("수정 날짜"), +// fieldWithPath("value.modifiedBy").description("수정자"), +// fieldWithPath("value.jwtToken").ignored() +// ))); +// +// verify(memberService).findMemberByToken(authToken); +// } +// +// @Test +// void 차량정보_조회_요청시_200응답과_정보가_잘_반환되는지() throws Exception { +// // given +// String authToken = "Bearer test-token"; +// CarInfoDto carInfoDto = fixtureMonkey.giveMeBuilder(CarInfoDto.class) +// .set("carType", Arbitraries.integers().between(11, 14)) +// .set("carSize", Arbitraries.integers().between(16, 19)) +// .set("carColor", Arbitraries.integers().between(21, 28)) +// .set("drivingEnv", Arbitraries.integers().between(36, 38)) +// .set("parkingEnv", Arbitraries.integers().between(40, 42)) +// .sample(); +// +// Map carInfo = Map.of( +// "car_info", carInfoDto, +// "segment_options", commonCodeService.getCodes("segment"), +// "carType_options", commonCodeService.getCodes("cartype"), +// "color_options", commonCodeService.getCodes("color"), +// "driving_options", commonCodeService.getCodes("driving"), +// "parking_options", commonCodeService.getCodes("parking") +// ); +// +// when(memberService.getCarInfo(authToken)).thenReturn(carInfo); +// +// // when & then +// mockMvc.perform(get("/mypage/car") +// .header("Authorization", authToken)) +// .andExpect(status().isOk()) +// .andExpect(content().contentType(MediaType.APPLICATION_JSON)) +// .andExpect(jsonPath("$.status").value(200)) +// .andExpect(jsonPath("$.code").value("BMC004")) +// .andExpect(jsonPath("$.message").value("차량정보 조회 성공")) +// .andExpect(jsonPath("$.value.car_info.carType").isNumber()) +// .andExpect(jsonPath("$.value.car_info.carSize").isNumber()) +// .andExpect(jsonPath("$.value.car_info.carColor").isNumber()) +// .andExpect(jsonPath("$.value.car_info.drivingEnv").isNumber()) +// .andExpect(jsonPath("$.value.car_info.parkingEnv").isNumber()) +// .andDo(document("mypage-car/get-car-info", +// responseFields( +// fieldWithPath("status").description("상태 코드"), +// fieldWithPath("code").description("비즈니스 코드"), +// fieldWithPath("message").description("응답 메세지"), +// fieldWithPath("value.car_info.carType").description("차량 유형 코드"), +// fieldWithPath("value.car_info.carSize").description("차량 크기 코드"), +// fieldWithPath("value.car_info.carColor").description("차량 색상 코드"), +// fieldWithPath("value.car_info.drivingEnv").description("주행 환경 코드"), +// fieldWithPath("value.car_info.parkingEnv").description("주차 환경 코드"), +// subsectionWithPath("value.segment_options").description("차량 세그먼트 옵션 목록"), +// subsectionWithPath("value.carType_options").description("차량 유형 옵션 목록"), +// subsectionWithPath("value.color_options").description("차량 색상 옵션 목록"), +// subsectionWithPath("value.driving_options").description("주행 환경 옵션 목록"), +// subsectionWithPath("value.parking_options").description("주차 환경 옵션 목록") +// ) +// )); +// } +// +// @Test +// void 회원_탈퇴_요청시_200응답과_탈퇴_성공_메시지가_반환되는지() throws Exception { +// // given +// String authToken = "Bearer test-token"; +// doNothing().when(memberService).deleteMemberByToken(authToken); +// +// // when & then +// mockMvc.perform(delete("/mypage/member") +// .header("Authorization", authToken)) +// .andExpect(status().isOk()) +// .andExpect(content().contentType(MediaType.APPLICATION_JSON)) +// .andExpect(jsonPath("$.status").value(200)) +// .andExpect(jsonPath("$.code").value("BMC005")) +// .andExpect(jsonPath("$.message").value("회원이 탈퇴 되었습니다.")) +// .andDo(print()) +// .andDo(document("mypage-member/delete-member", +// requestHeaders( +// headerWithName("Authorization").description("Bearer 토큰") +// ), +// responseFields( +// fieldWithPath("status").description("상태 코드"), +// fieldWithPath("code").description("비즈니스 코드"), +// fieldWithPath("message").description("응답 메시지"), +// fieldWithPath("value").description("응답 값").optional() +// ) +// )); +// +// verify(memberService).deleteMemberByToken(authToken); +// } +// +// @Test +// void 비밀번호_확인_요청시_200응답과_확인_성공_메시지가_반환되는지() throws Exception { +// // given +// String authToken = "Bearer test-token"; +// String password = "password123"; +// doNothing().when(memberService).changePassword(password, authToken); +// +// // when & then +// mockMvc.perform(post("/mypage/member") +// .content(password) +// .contentType(MediaType.TEXT_PLAIN) +// .header("Authorization", authToken)) +// .andExpect(status().isOk()) +// .andExpect(content().contentType(MediaType.APPLICATION_JSON)) +// .andExpect(jsonPath("$.status").value(200)) +// .andExpect(jsonPath("$.code").value("BMC008")) +// .andExpect(jsonPath("$.message").value("비밀번호가 확인 되었습니다.")) +// .andDo(print()) +// .andDo(document("mypage-member/validate-password", +// requestHeaders( +// headerWithName("Authorization").description("Bearer 인증 토큰"), +// headerWithName("Content-Type").description("콘텐츠 타입") +// ), +// responseFields( +// fieldWithPath("status").description("상태 코드"), +// fieldWithPath("code").description("비즈니스 코드"), +// fieldWithPath("message").description("응답 메시지"), +// fieldWithPath("value").description("응답 값").optional() +// ) +// )); +// +// verify(memberService).changePassword(password, authToken); +// } +// +// @Test +// void WhenRequestingAMemberInformationUpdate_ThenA200ResponseAndUpdateSuccessMessageAreReturned() throws Exception { +// // given +// MemberInfo memberInfo = fixtureMonkey.giveMeBuilder(MemberInfo.class).sample(); +// doNothing().when(memberService).updateMember(memberInfo); // -// //then -// mockMvc.perform(get("/mypage/main")) +// // when & then +// mockMvc.perform(put("/mypage/member") +// .contentType(MediaType.APPLICATION_JSON) +// .content(objectMapper.writeValueAsString(memberInfo))) // .andExpect(status().isOk()) // .andExpect(content().contentType(MediaType.APPLICATION_JSON)) -// .andExpect(jsonPath("$.Banner").value("http://localhost:8080/bannersample.png")) -// .andExpect(jsonPath("$.Suggest").value("http://localhost:8080/suggestsample.png")) -// .andExpect(jsonPath("$.Product", hasSize(5))); +// .andExpect(jsonPath("$.status").value(200)) +// .andExpect(jsonPath("$.code").value("BMC006")) +// .andExpect(jsonPath("$.message").value("회원정보가 변경 되었습니다.")) +// .andDo(document("mypage-member/update-member", +// requestHeaders( +// headerWithName("Content-Type").description("Content-Type") +// ), +// requestFields( +// fieldWithPath("id").description("Member ID"), +// fieldWithPath("email").description("Member Email"), +// fieldWithPath("gender").description("Member Gender"), +// fieldWithPath("age").description("Member Age") +// ), +// responseFields( +// fieldWithPath("status").description("Status Code"), +// fieldWithPath("code").description("Business Code"), +// fieldWithPath("message").description("Response message"), +// fieldWithPath("value").description("Response value").optional() +// ) +// )); // -// verify(productService, times(1)).getProductListOrderByViewCount(); +// verify(memberService).updateMember(any(MemberInfo.class)); // } - - -} \ No newline at end of file +//} \ No newline at end of file diff --git a/module-api/src/test/java/com/kernel360/product/controller/ProductControllerTest.java b/module-api/src/test/java/com/kernel360/product/controller/ProductControllerTest.java index 068ddb20..9fa9a300 100644 --- a/module-api/src/test/java/com/kernel360/product/controller/ProductControllerTest.java +++ b/module-api/src/test/java/com/kernel360/product/controller/ProductControllerTest.java @@ -1,24 +1,39 @@ package com.kernel360.product.controller; import com.kernel360.common.ControllerTest; +import com.kernel360.product.dto.ProductDetailDto; import com.kernel360.product.dto.ProductDto; import com.kernel360.product.entity.Product; +import com.kernel360.product.entity.SafetyStatus; import com.navercorp.fixturemonkey.FixtureMonkey; import com.navercorp.fixturemonkey.api.introspector.*; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureWebMvc; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.http.MediaType; +import static org.mockito.ArgumentMatchers.any; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.hamcrest.Matchers.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; +import static org.springframework.restdocs.request.RequestDocumentation.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.stream.Collectors; import static org.hamcrest.collection.IsCollectionWithSize.hasSize; import static org.mockito.Mockito.*; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@AutoConfigureWebMvc class ProductControllerTest extends ControllerTest { private FixtureMonkey fixtureMonkey; @@ -34,99 +49,260 @@ class ProductControllerTest extends ControllerTest { ) )) .build(); + } @Test void 제품목록요청_왔을때_200응답과_리스폰스가_잘반환되는지() throws Exception { + //todo 브랜드 // given - List products = fixtureMonkey.giveMeBuilder(Product.class) - .sampleList(5); - - List expectedDtos = products.stream() + List expectedDtos = fixtureMonkey.giveMeBuilder(Product.class) + .setNotNull("productNo") + .setNotNull("productName") + .setNotNull("barcode") + .setNotNull("imageSource") + .setNotNull("reportNumber") + .setNotNull("safetyStatus") + .setNotNull("viewCount") + .setNotNull("upperItem") + .setNotNull("createdAt") + .setNotNull("createdBy") + .setNotNull("modifiedAt") + .setNotNull("modifiedBy") + .sampleList(5).stream() .map(ProductDto::from) - .collect(Collectors.toList()); + .toList(); - //when - when(productService.getProductList()).thenReturn(expectedDtos); + when(productService.getProducts()).thenReturn(expectedDtos); // then mockMvc.perform(get("/products")) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(jsonPath("$.value", hasSize(5))); + .andExpect(jsonPath("$.value", hasSize(5))) + .andDo(document("products/get-products", + responseFields( + fieldWithPath("status").description("상태 코드"), + fieldWithPath("message").description("응답 메시지"), + fieldWithPath("code").description("비즈니스 코드"), + subsectionWithPath("value").description("제품 목록"), + fieldWithPath("value[].productNo").description("제품번호").optional(), + fieldWithPath("value[].productName").description("제품명").optional(), + fieldWithPath("value[].barcode").description("바코드").optional(), + fieldWithPath("value[].imageSource").description("이미지 소스").optional(), + fieldWithPath("value[].reportNumber").description("보고 번호").optional(), + fieldWithPath("value[].safetyStatus").description("안전 상태").optional(), + fieldWithPath("value[].viewCount").description("조회수").optional(), + fieldWithPath("value[].upperItem").description("상위 분류").optional(), + fieldWithPath("value[].createdAt").description("생성 날짜").optional(), + fieldWithPath("value[].createdBy").description("생성자").optional(), + fieldWithPath("value[].modifiedAt").description("수정된 날짜").optional(), + fieldWithPath("value[].modifiedBy").description("수정자").optional() + ))); - verify(productService, times(1)).getProductList(); + verify(productService, times(1)).getProducts(); } + @Test - void 제품아이디로_제품조회요청이왔을때_200응답과_리스폰스가_잘반환되는지() throws Exception { + void 제품고유번호로_제품상세조회요청이왔을때_200응답과_리스폰스가_잘반환되는지() throws Exception { + //TODO 브랜드, 제조국, grade, reviewCnt // given Product mockProduct = fixtureMonkey.giveMeBuilder(Product.class) - .set("productId", 1L) + .set("productNo", 1L) // Assuming you want to explicitly set some values + .setNotNull("productName") + .setNotNull("barcode") + .setNotNull("imageSource") + .setNotNull("reportNumber") + .setNotNull("safetyStatus") + .setNotNull("viewCount") + .setNotNull("companyName") + .setNotNull("productType") + .setNotNull("issuedDate") + .setNotNull("safetyInspectionStandard") + .setNotNull("upperItem") + .setNotNull("item") + .setNotNull("propose") + .setNotNull("weight") + .setNotNull("usage") + .setNotNull("usagePrecaution") + .setNotNull("firstAid") + .setNotNull("mainSubstance") + .setNotNull("allergicSubstance") + .setNotNull("otherSubstance") + .setNotNull("preservative") + .setNotNull("surfactant") + .setNotNull("fluorescentWhitening") + .setNotNull("manufactureType") + .setNotNull("manufactureMethod") + .setNotNull("violationInfo") + .setNotNull("createdAt") + .setNotNull("createdBy") + .setNotNull("modifiedAt") + .setNotNull("modifiedBy") .sample(); - when(productService.getProductById(1L)).thenReturn(ProductDto.from(mockProduct)); + ProductDetailDto productDetailDto = ProductDetailDto.from(mockProduct); + when(productService.getProductById(1L)).thenReturn(productDetailDto); // when & then - mockMvc.perform(get("/product/{id}", 1L)) + mockMvc.perform(get("/product/{productNo}", 1L)) .andExpect(status().isOk()) - .andExpect(content().contentType(MediaType.APPLICATION_JSON)); -// .andExpect(jsonPath("$.value.productName", is(mockProduct.getProductName()))) -// .andExpect(jsonPath("$.value.barcode", is(mockProduct.getBarcode()))) -// .andExpect(jsonPath("$.value.image", is(mockProduct.getImage()))) -// .andExpect(jsonPath("$.value.reportNumber", is(mockProduct.getReportNumber()))) -// .andExpect(jsonPath("$.value.productType", is(mockProduct.getProductType()))) -// .andExpect(jsonPath("$.value.companyName", is(mockProduct.getCompanyName()))) -// .andExpect(jsonPath("$.value.manufactureNation", is(mockProduct.getManufactureNation()))) -// .andExpect(jsonPath("$.value.safetyStatus", is(mockProduct.getSafetyStatus().toString()))) -// .andExpect(jsonPath("$.value.issuedDate", is(mockProduct.getIssuedDate().toString()))) -// .andExpect(jsonPath("$.value.viewCount", is(mockProduct.getViewCount()))) -// .andExpect(jsonPath("$.value.safetyInspectionStandard", is(mockProduct.getSafetyInspectionStandard()))) -// .andExpect(jsonPath("$.value.upperItem", is(mockProduct.getUpperItem()))) -// .andExpect(jsonPath("$.value.item", is(mockProduct.getItem()))) -// .andExpect(jsonPath("$.value.propose", is(mockProduct.getPropose()))) -// .andExpect(jsonPath("$.value.weight", is(mockProduct.getWeight()))) -// .andExpect(jsonPath("$.value.usage", is(mockProduct.getUsage()))) -// .andExpect(jsonPath("$.value.usagePrecaution", is(mockProduct.getUsagePrecaution()))) -// .andExpect(jsonPath("$.value.firstAid", is(mockProduct.getFirstAid()))) -// .andExpect(jsonPath("$.value.mainSubstance", is(mockProduct.getMainSubstance()))) -// .andExpect(jsonPath("$.value.allergicSubstance", is(mockProduct.getAllergicSubstance()))) -// .andExpect(jsonPath("$.value.otherSubstance", is(mockProduct.getOtherSubstance()))) -// .andExpect(jsonPath("$.value.preservative", is(mockProduct.getPreservative()))) -// .andExpect(jsonPath("$.value.surfactant", is(mockProduct.getSurfactant()))) -// .andExpect(jsonPath("$.value.fluorescentWhitening", is(mockProduct.getFluorescentWhitening()))) -// .andExpect(jsonPath("$.value.manufactureType", is(mockProduct.getManufactureType()))) -// .andExpect(jsonPath("$.value.manufactureMethod", is(mockProduct.getManufactureMethod()))) -// .andExpect(jsonPath("$.value.declareNo", is(mockProduct.getReportNumber()))) -// .andExpect(jsonPath("$.value.safetyStatus", is(mockProduct.getSafetyStatus()))); - + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.value.productName", notNullValue())) + .andExpect(jsonPath("$.value.barcode", notNullValue())) + .andExpect(jsonPath("$.value.imageSource", notNullValue())) + .andExpect(jsonPath("$.value.reportNumber", notNullValue())) + .andExpect(jsonPath("$.value.safetyStatus", notNullValue())) + .andExpect(jsonPath("$.value.viewCount", notNullValue())) + .andExpect(jsonPath("$.value.companyName", notNullValue())) + .andExpect(jsonPath("$.value.productType", notNullValue())) + .andExpect(jsonPath("$.value.issuedDate", notNullValue())) + .andExpect(jsonPath("$.value.safetyInspectionStandard", notNullValue())) + .andExpect(jsonPath("$.value.upperItem", notNullValue())) + .andExpect(jsonPath("$.value.item", notNullValue())) + .andExpect(jsonPath("$.value.propose", notNullValue())) + .andExpect(jsonPath("$.value.weight", notNullValue())) + .andExpect(jsonPath("$.value.usage", notNullValue())) + .andExpect(jsonPath("$.value.usagePrecaution", notNullValue())) + .andExpect(jsonPath("$.value.firstAid", notNullValue())) + .andExpect(jsonPath("$.value.mainSubstance", notNullValue())) + .andExpect(jsonPath("$.value.allergicSubstance", notNullValue())) + .andExpect(jsonPath("$.value.otherSubstance", notNullValue())) + .andExpect(jsonPath("$.value.preservative", notNullValue())) + .andExpect(jsonPath("$.value.surfactant", notNullValue())) + .andExpect(jsonPath("$.value.fluorescentWhitening", notNullValue())) + .andExpect(jsonPath("$.value.manufactureType", notNullValue())) + .andExpect(jsonPath("$.value.manufactureMethod", notNullValue())) + .andExpect(jsonPath("$.value.violationInfo", notNullValue())) + .andExpect(jsonPath("$.value.createdAt", notNullValue())) + .andExpect(jsonPath("$.value.createdBy", notNullValue())) + .andExpect(jsonPath("$.value.modifiedAt", notNullValue())) + .andExpect(jsonPath("$.value.modifiedBy", notNullValue())) + .andDo(document("product-id/get-product-id", + pathParameters( + parameterWithName("productNo").description("제품번호") + ), + responseFields( + fieldWithPath("status").description("상태 코드"), + fieldWithPath("message").description("응답 메시지"), + fieldWithPath("code").description("비즈니스 코드"), + subsectionWithPath("value").description("제품 목록"), + fieldWithPath("value.productNo").description("제품번호"), + fieldWithPath("value.productName").description("제품명"), + fieldWithPath("value.barcode").description("바코드"), + fieldWithPath("value.imageSource").description("이미지"), + fieldWithPath("value.reportNumber").description("신고번호"), + fieldWithPath("value.safetyStatus").description("위해우려등급"), + fieldWithPath("value.viewCount").description("조회수"), + fieldWithPath("value.companyName").description("제조사"), + fieldWithPath("value.productType").description("제품구분"), + fieldWithPath("value.issuedDate").description("신고년월"), + fieldWithPath("value.safetyInspectionStandard").description("제품검사기준"), + fieldWithPath("value.upperItem").description("품목군"), + fieldWithPath("value.item").description("품목"), + fieldWithPath("value.propose").description("제품용도"), + fieldWithPath("value.weight").description("무게"), + fieldWithPath("value.usage").description("사용방법"), + fieldWithPath("value.usagePrecaution").description("사용시 주의사항"), + fieldWithPath("value.firstAid").description("응급처치법"), + fieldWithPath("value.mainSubstance").description("주요 성분"), + fieldWithPath("value.allergicSubstance").description("알러지 반응 가능 물질"), + fieldWithPath("value.otherSubstance").description("기타 물질"), + fieldWithPath("value.preservative").description("보존제"), + fieldWithPath("value.surfactant").description("계면활성제"), + fieldWithPath("value.fluorescentWhitening").description("형광증백제"), + fieldWithPath("value.manufactureType").description("제조구분"), + fieldWithPath("value.manufactureMethod").description("제조방식"), + fieldWithPath("value.violationInfo").description("위반내역"), + fieldWithPath("value.createdAt").description("생성일"), + fieldWithPath("value.createdBy").description("생성자"), + fieldWithPath("value.modifiedAt").description("수정일"), + fieldWithPath("value.modifiedBy").description("수정자") + ))); verify(productService).getProductById(1L); - } + @Test - void 키워드로_검색했을때_200코드와_리스폰스가_잘반환되는지() throws Exception { + void 검색키워드로_제품조회요청시_200응답과_데이터가_잘_반환되는지() throws Exception { // given List products = new ArrayList<>(); - String keyword = "sample"; - for (int i = 0; i < 5; i++) { + String keyword = "더 클래스"; + + for (int i = 0; i < 20; i++) { ProductDto productDto = fixtureMonkey.giveMeBuilder(ProductDto.class) - .set("productName", keyword + i) + .set("productName", keyword + " " + (i + 1)) + .setNotNull("productNo") + .setNull("barcode") + .setNull("imageSource") + .setNotNull("reportNumber") + .set("safetyStatus", SafetyStatus.CONCERN) + .set("viewCount", 0) +// .setNotNull("brand") + .setNotNull("upperItem") + .setNotNull("createdAt") + .setNotNull("createdBy") + .setNotNull("modifiedAt") + .setNotNull("modifiedBy") .sample(); products.add(productDto); } - //when - when(productService.getProductListByKeyword(keyword)).thenReturn(products); - //then + Pageable pageable = PageRequest.of(0, 20); + Page productsPage = new PageImpl<>(products, pageable, products.size()); + when(productService.getProductsByKeyword(eq(keyword), any(Pageable.class))).thenReturn(productsPage); + +// when & then mockMvc.perform(get("/products/search").param("keyword", keyword)) - .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.value", hasSize(5))); - - verify(productService, times(1)).getProductListByKeyword(keyword); + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.status").value(200)) + .andExpect(jsonPath("$.code").value("PMB002")) + .andExpect(jsonPath("$.message").value("제품정보 조회 성공")) + .andExpect(jsonPath("$.value.content[*].productNo", notNullValue())) + .andExpect(jsonPath("$.value.content[*].productName", notNullValue())) + .andExpect(jsonPath("$.value.content[*].reportNumber", notNullValue())) + .andExpect(jsonPath("$.value.content[*].safetyStatus", notNullValue())) + .andExpect(jsonPath("$.value.content[*].viewCount", notNullValue())) + .andDo(document("products-search/get-products-by-search", + queryParameters( + parameterWithName("keyword").description("키워드"), + parameterWithName("size").description("사이즈").optional(), + parameterWithName("page").description("페이지").optional(), + parameterWithName("sort").description("정렬").optional() + ), + responseFields( + fieldWithPath("status").description("상태 코드"), + fieldWithPath("message").description("응답 메시지"), + fieldWithPath("code").description("비즈니스 코드"), + subsectionWithPath("value").description("제품 목록"), + fieldWithPath("value.content[*].productNo").description("제품 번호"), + fieldWithPath("value.content[*].productName").description("제품 이름"), + fieldWithPath("value.content[*].barcode").optional().description("바코드"), + fieldWithPath("value.content[*].imageSource").optional().description("이미지 소스"), + fieldWithPath("value.content[*].reportNumber").description("보고서 번호"), + fieldWithPath("value.content[*].safetyStatus").description("안전 상태"), + fieldWithPath("value.content[*].viewCount").description("조회수"), +// fieldWithPath("value.content[*].brand").description("브랜드"), + fieldWithPath("value.content[*].upperItem").description("상위 항목 카테고리"), + fieldWithPath("value.content[*].createdAt").description("생성 날짜"), + fieldWithPath("value.content[*].createdBy").description("생성자"), + fieldWithPath("value.content[*].modifiedAt").optional().description("수정 날짜"), + fieldWithPath("value.content[*].modifiedBy").optional().description("수정자"), + subsectionWithPath("value.pageable").description("페이지 정보"), + fieldWithPath("value.totalPages").description("총 페이지 수"), + fieldWithPath("value.totalElements").description("총 요소 수"), + fieldWithPath("value.last").description("마지막 페이지 여부"), + fieldWithPath("value.number").description("현재 페이지 번호"), + fieldWithPath("value.size").description("페이지 크기"), + fieldWithPath("value.numberOfElements").description("현재 페이지의 요소 수"), + fieldWithPath("value.first").description("첫 페이지 여부"), + fieldWithPath("value.empty").description("비어 있는 페이지 여부"), + subsectionWithPath("value.sort").description("정렬 정보") + ))); + verify(productService, times(1)).getProductsByKeyword(keyword, pageable); } - -} \ No newline at end of file +} diff --git a/module-api/src/test/java/com/kernel360/product/dto/ProductDtoTest.java b/module-api/src/test/java/com/kernel360/product/dto/ProductDtoTest.java index ef03d4b3..ded4f600 100644 --- a/module-api/src/test/java/com/kernel360/product/dto/ProductDtoTest.java +++ b/module-api/src/test/java/com/kernel360/product/dto/ProductDtoTest.java @@ -44,6 +44,6 @@ class ProductDtoTest { assertEquals(product.getSafetyStatus(), productDto.safetyStatus()); assertEquals(product.getBarcode(), productDto.barcode()); assertEquals(product.getReportNumber(), productDto.reportNumber()); - assertEquals(product.getImage(), productDto.imageSource()); + assertEquals(product.getImageSource(), productDto.imageSource()); } } \ No newline at end of file diff --git a/module-api/src/test/java/com/kernel360/product/service/ProductServiceTest.java b/module-api/src/test/java/com/kernel360/product/service/ProductServiceTest.java index 5f33bac5..1a11e022 100644 --- a/module-api/src/test/java/com/kernel360/product/service/ProductServiceTest.java +++ b/module-api/src/test/java/com/kernel360/product/service/ProductServiceTest.java @@ -1,5 +1,7 @@ package com.kernel360.product.service; +import com.kernel360.likes.repository.LikeRepository; +import com.kernel360.product.dto.ProductDetailDto; import com.kernel360.product.dto.ProductDto; import com.kernel360.product.entity.Product; import com.kernel360.product.repository.ProductRepository; @@ -8,6 +10,9 @@ import com.navercorp.fixturemonkey.jakarta.validation.plugin.JakartaValidationPlugin; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; import java.util.ArrayList; import java.util.Arrays; @@ -22,6 +27,9 @@ class ProductServiceTest { private FixtureMonkey fixtureMonkey; private ProductRepository productRepository; private ProductService productService; + private Pageable pageable; + + private LikeRepository likeRepository; @BeforeEach void 테스트준비() { fixtureMonkey = FixtureMonkey.builder() @@ -37,7 +45,8 @@ class ProductServiceTest { .build(); productRepository = mock(ProductRepository.class); - productService = new ProductService(productRepository); + pageable = mock(Pageable.class); + productService = new ProductService(productRepository, likeRepository); } @Test @@ -48,7 +57,7 @@ class ProductServiceTest { .sample(); when(productRepository.findById(1L)).thenReturn(Optional.of(product)); - ProductDto foundProduct = productService.getProductById(1L); + ProductDetailDto foundProduct = productService.getProductById(1L); //then then(foundProduct.productNo()).isEqualTo(product.getProductNo()); @@ -56,34 +65,33 @@ class ProductServiceTest { @Test void 전체_제품_목록_조회(){ - //given + //given & when List products = fixtureMonkey.giveMe(ProductDto.class, 3); - when(productService.getProductList()).thenReturn(products); - - //when -// List actualProducts = productService.getProductList(); + when(productService.getProducts()).thenReturn(products); //then then(products).isEqualTo(products); } @Test - void 키워드로_제품_목록_조회(){ - //given - List products = new ArrayList<>(); + void 키워드로_제품_목록_조회() { + // given String keyword = "sample"; - + List productList = new ArrayList<>(); for (int i = 0; i < 5; i++) { Product product = fixtureMonkey.giveMeBuilder(Product.class) - .set("productName", keyword + i) // Append a unique number to each productName + .set("productName", "sample" + i) .sample(); - products.add(product); + productList.add(product); } - //when - when(productRepository.findByKeyword(keyword)).thenReturn(products); - List productListByKeyword = productService.getProductListByKeyword(keyword); - //then - then(productListByKeyword).hasSize(5); - then(productListByKeyword).allSatisfy(productDto -> then(productDto.productName()).contains(keyword)); + Page productsPage = new PageImpl<>(productList, pageable, productList.size()); + + when(productRepository.findByProductNameContaining(keyword, pageable)).thenReturn(productsPage); + + Page productDtos = productService.getProductsByKeyword(keyword, pageable); + + // then + then(productDtos.getContent()).hasSize(5); + then(productDtos.getContent()).allSatisfy(productDto -> then(productDto.productName()).contains(keyword)); } } diff --git a/module-batch/Dockerfile b/module-batch/Dockerfile index 113ef925..8463c9bc 100644 --- a/module-batch/Dockerfile +++ b/module-batch/Dockerfile @@ -1,5 +1,11 @@ FROM openjdk:17-jdk -CMD ["./gradlew", "clean", "build"] + +WORKDIR /app + ARG JAR_FILE=./build/libs/module-batch-0.0.1-SNAPSHOT.jar -COPY ${JAR_FILE} washpedia.jar -ENTRYPOINT ["java", "-jar", "washpedia.jar"] \ No newline at end of file + +COPY ${JAR_FILE} washpedia-batch.jar + +EXPOSE 8081 + +ENTRYPOINT ["java", "-jar", "washpedia-batch.jar"] \ No newline at end of file diff --git a/module-batch/gradle/wrapper/gradle-wrapper.jar b/module-batch/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..d64cd491 Binary files /dev/null and b/module-batch/gradle/wrapper/gradle-wrapper.jar differ diff --git a/module-batch/gradle/wrapper/gradle-wrapper.properties b/module-batch/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..1af9e093 --- /dev/null +++ b/module-batch/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/module-batch/src/main/java/com/kernel360/modulebatch/product/dto/ProductDto.java b/module-batch/src/main/java/com/kernel360/modulebatch/product/dto/ProductDto.java index ec8bce12..4971c243 100644 --- a/module-batch/src/main/java/com/kernel360/modulebatch/product/dto/ProductDto.java +++ b/module-batch/src/main/java/com/kernel360/modulebatch/product/dto/ProductDto.java @@ -1,6 +1,5 @@ package com.kernel360.modulebatch.product.dto; -import com.kernel360.brand.entity.Brand; import com.kernel360.product.entity.Product; import com.kernel360.product.entity.SafetyStatus; import java.time.LocalDate; @@ -8,49 +7,55 @@ public record ProductDto(String productName, String barcode, String imageSource, SafetyStatus safetyStatus, Integer viewCount, String companyName, String reportNumber, - String productType, LocalDate issuedDate, String safetyInspectionStandard, + String productType, LocalDate issuedDate, + String safetyInspectionStandard, String upperItem, String item, String propose, String weight, String usage, String usagePrecaution, String firstAid, String mainSubstance, String allergicSubstance, String otherSubstance, String preservative, String surfactant, String fluorescentWhitening, String manufactureType, - String manufactureMethod, String manufactureNation, - Brand brand) { + String manufactureMethod, String manufactureNation, String violationInfo +) { public static ProductDto of(String productName, String barcode, String imageSource, SafetyStatus safetyStatus, Integer viewCount, String companyName, String reportNumber, - String productType, LocalDate issuedDate, String safetyInspectionStandard, + String productType, LocalDate issuedDate, + String safetyInspectionStandard, String upperItem, String item, String propose, String weight, String usage, String usagePrecaution, String firstAid, String mainSubstance, String allergicSubstance, String otherSubstance, String preservative, String Surfactant, String fluorescentWhitening, String manufactureType, String manufactureMethod, String manufactureCountry, - Brand brand) { + String violation_info) { return new ProductDto( productName, barcode, imageSource, safetyStatus, viewCount, companyName, reportNumber, - productType, issuedDate, safetyInspectionStandard, + productType, issuedDate, safetyInspectionStandard, upperItem, item, propose, weight, usage, usagePrecaution, firstAid, mainSubstance, allergicSubstance, otherSubstance, preservative, Surfactant, fluorescentWhitening, manufactureType, manufactureMethod, manufactureCountry, - brand + violation_info ); } public static Product toEntity(ProductDto productDto) { return Product.of(productDto.productName, productDto.barcode(), productDto.imageSource(), - productDto.reportNumber(), String.valueOf(productDto.safetyStatus()), productDto.viewCount, productDto.companyName(), - productDto.productType(), productDto.issuedDate(), productDto.safetyInspectionStandard, + productDto.reportNumber(), String.valueOf(productDto.safetyStatus()), productDto.viewCount, + productDto.companyName(), + productDto.productType(), productDto.issuedDate(), + productDto.safetyInspectionStandard, productDto.upperItem(), productDto.item(), productDto.propose(), productDto.weight(), productDto.usage(), productDto.usagePrecaution(), productDto.firstAid(), productDto.mainSubstance(), productDto.allergicSubstance(), productDto.otherSubstance(), productDto.preservative(), productDto.surfactant(), productDto.fluorescentWhitening(), productDto.manufactureType(), productDto.manufactureMethod(), - productDto.manufactureNation(), productDto.brand()); + productDto.manufactureNation(), productDto.violationInfo()); } + + } \ No newline at end of file diff --git a/module-batch/src/main/java/com/kernel360/modulebatch/product/dto/ProductJoinDto.java b/module-batch/src/main/java/com/kernel360/modulebatch/product/dto/ProductJoinDto.java new file mode 100644 index 00000000..b6c27277 --- /dev/null +++ b/module-batch/src/main/java/com/kernel360/modulebatch/product/dto/ProductJoinDto.java @@ -0,0 +1,12 @@ +package com.kernel360.modulebatch.product.dto; + +public record ProductJoinDto( + String productName, + String companyName, + String actionedDate, + String violatedCn, + String actionCn, + String etcInfo +) { + +} diff --git a/module-batch/src/main/java/com/kernel360/modulebatch/product/job/ImportProductFromReportedProductJobConfig.java b/module-batch/src/main/java/com/kernel360/modulebatch/product/job/ImportProductFromReportedProductJobConfig.java deleted file mode 100644 index 17618c7c..00000000 --- a/module-batch/src/main/java/com/kernel360/modulebatch/product/job/ImportProductFromReportedProductJobConfig.java +++ /dev/null @@ -1,250 +0,0 @@ -package com.kernel360.modulebatch.product.job; - -import com.kernel360.brand.entity.Brand; -import com.kernel360.ecolife.entity.ReportedProduct; -import com.kernel360.ecolife.repository.ReportedProductRepository; -import com.kernel360.modulebatch.product.dto.ProductDto; -import com.kernel360.product.entity.Product; -import com.kernel360.product.entity.SafetyStatus; -import com.kernel360.product.repository.ProductRepository; -import jakarta.persistence.EntityManagerFactory; -import java.time.LocalDate; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.batch.core.Job; -import org.springframework.batch.core.JobExecution; -import org.springframework.batch.core.JobExecutionListener; -import org.springframework.batch.core.Step; -import org.springframework.batch.core.configuration.annotation.JobScope; -import org.springframework.batch.core.configuration.annotation.StepScope; -import org.springframework.batch.core.job.builder.JobBuilder; -import org.springframework.batch.core.launch.support.RunIdIncrementer; -import org.springframework.batch.core.repository.JobRepository; -import org.springframework.batch.core.step.builder.StepBuilder; -import org.springframework.batch.item.ItemProcessor; -import org.springframework.batch.item.database.JpaItemWriter; -import org.springframework.batch.item.database.JpaPagingItemReader; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Configuration; -import org.springframework.dao.DataIntegrityViolationException; -import org.springframework.transaction.PlatformTransactionManager; -import org.springframework.web.client.ResourceAccessException; - -@Slf4j -@Configuration -@RequiredArgsConstructor -@ComponentScan("com.kernel360.product") -public class ImportProductFromReportedProductJobConfig { - - private final ProductRepository productRepository; - - private final ReportedProductRepository reportedProductRepository; - - private final EntityManagerFactory emf; - - @Bean - public Job importProductFromReportedProductJob(JobRepository jobRepository, - @Qualifier("importProductFromReportedProductStep") Step importProductFromReportedProductStep) { - log.info("Import Product from ReportedProduct by Brand Job Build Configuration"); - - return new JobBuilder("ImportProductFromReportedProductJob", jobRepository) - .start(importProductFromReportedProductStep) - .incrementer(new RunIdIncrementer()) - .listener(new ImportProductFromReportedProductListener()) - .build(); - } - - @Bean - @JobScope - public Step importProductFromReportedProductStep(JobRepository jobRepository, - PlatformTransactionManager transactionManager) throws Exception { - log.info("Import Product from ReportedProduct by Brand Step Build Configuration"); - - return new StepBuilder("ImportProductFromReportedProductStep", jobRepository) - .>chunk(1, transactionManager) - .reader(brandReader()) - .processor(reportedProductToProductListProcessor()) - .writer(productListWriter()) - .faultTolerant() - .retryLimit(2) - .retry(ResourceAccessException.class) - .skipLimit(10) - .skip(DataIntegrityViolationException.class) - .build(); - } - - /** - * @return Brand 를 읽어오는 JpaPagingReader - */ - @Bean - @StepScope - public JpaPagingItemReader brandReader() throws Exception { - JpaPagingItemReader itemReader = new JpaPagingItemReader<>(); - itemReader.setPageSize(50); - itemReader.setEntityManagerFactory(emf); - itemReader.setQueryString("select b from Brand b"); - itemReader.setName("jpaPagingBrandReader"); - itemReader.afterPropertiesSet(); - - return itemReader; - } - - @Bean - @StepScope - public ItemProcessor> reportedProductToProductListProcessor() throws Exception { - return brand -> { - //-- ReportedProduct 테이블에서 브랜드명과 제조사명으로 제품 검색 --// - List reportedProductList = reportedProductRepository - .findByBrandNameAndCompanyName( - "%" + brand.getCompanyName().replaceAll(" ", "%") + "%", - "%" + brand.getBrandName().replaceAll(" ", "%") + "%" - ); - List productDtoList = reportedProductList.stream() - .filter(rp -> rp.getInspectedOrganization() != null) - .map(rp -> ProductDto.of(rp.getProductName(), - null, - null, - "취하".equals(rp.getRenewType()) - ? SafetyStatus.CONCERN - : SafetyStatus.SAFE, - 0, - rp.getCompanyName(), - rp.getSafetyReportNumber(), - rp.getProductType(), - LocalDate.from(rp.getIssuedDate()), - rp.getSafeStandard(), rp.getUpperItem(), - rp.getItem(), - rp.getProductPropose(), rp.getWeightAndBulk(), - rp.getUseMethod(), - rp.getUsageAttentionReport(), - rp.getFirstAid(), rp.getMainSubstance(), - rp.getAllergicSubstance(), - rp.getOtherSubstance(), rp.getPreservative(), - rp.getSurfactant(), - rp.getFluorescentWhiteningAgent(), - rp.getManufacture(), rp.getManufactureMethod(), - rp.getManufactureNation(), brand)) - .collect(Collectors.toList()); - - return convertToProductList(productDtoList); - }; - } - - - /** - * entity 로 변환할 dto 에 해당하는 제품이 있는지 검사하기 위해 product 테이블에서 제품을 검색한 이후, 없으면 새 제품 추가 있으면 업데이트 - */ - private List convertToProductList(List productDtoList) { - List productList = new ArrayList<>(); - - for (ProductDto productDto : productDtoList) { - Optional foundProduct = productRepository.findProductByProductNameAndReportNumber( - productDto.productName(), "%" + getCompanyNameWithoutSlash(productDto.companyName()) + "%"); - - boolean foundInList = productList.stream().anyMatch( - product -> product.getProductName().equals(productDto.productName()) && - product.getCompanyName().equals(productDto.companyName())); - - if (foundInList) { // 현재 처리할 데이터에 이미 추가가 되어 있다면 - continue; - } - - if (foundProduct.isEmpty()) { - Product newProduct = generateNewProduct(productDto); - productList.add(newProduct); - continue; - } - - if (productDto.issuedDate().isAfter(foundProduct.get().getIssuedDate())) { // 저장된 데이터보다 최신의 제품 데이터라면 - foundProduct.get().updateDetail(productDto.barcode(), - productDto.imageSource(), - productDto.reportNumber(), - String.valueOf(productDto.safetyStatus()), - productDto.issuedDate(), - productDto.safetyInspectionStandard(), - productDto.upperItem(), - productDto.item(), - productDto.propose(), - productDto.weight(), - productDto.usage(), - productDto.usagePrecaution(), - productDto.firstAid(), - productDto.mainSubstance(), - productDto.allergicSubstance(), - productDto.otherSubstance(), - productDto.preservative(), - productDto.surfactant(), - productDto.fluorescentWhitening(), - productDto.manufactureType(), - productDto.manufactureMethod(), - getNation(productDto), - productDto.brand()); - } - } - - return productList; - } - - private static Product generateNewProduct(ProductDto productDto) { - return Product.of(productDto.productName(), productDto.barcode(), - productDto.imageSource(), productDto.reportNumber(), String.valueOf(productDto.safetyStatus()), - productDto.viewCount(), getCompanyNameWithoutSlash(productDto.companyName()), productDto.productType(), - productDto.issuedDate(), productDto.safetyInspectionStandard(), productDto.upperItem(), - productDto.item(), productDto.propose(), productDto.weight(), productDto.usage(), - productDto.usagePrecaution(), productDto.firstAid(), productDto.mainSubstance(), - productDto.allergicSubstance(), productDto.otherSubstance(), productDto.preservative(), - productDto.surfactant(), productDto.fluorescentWhitening(), productDto.manufactureType(), - productDto.manufactureMethod(), getNation(productDto), productDto.brand()); - } - - public static String getCompanyNameWithoutSlash(String companyName) { - int index = companyName.indexOf("/"); - if (index != -1) { - return companyName.substring(0, index); - } - return companyName; - - } - - private static String getNation(ProductDto productDto) { - String nation; - if (productDto.manufactureType().equals("수입")) { - nation = productDto.brand().getNationName(); - } else { - nation = "대한민국"; - } - return nation; - } - - /** - * List 를 저장하기 위한 JpaListWriter. 컴포지트 패턴과 유사하게 구현함. - */ - private JpaProductListWriter productListWriter() { - JpaItemWriter writer = new JpaItemWriter<>(); - writer.setEntityManagerFactory(emf); - - return new JpaProductListWriter<>(writer); - } - -//-- Execution Listener --// - - public static class ImportProductFromReportedProductListener implements JobExecutionListener { - @Override - public void beforeJob(JobExecution jobExecution) { - log.info("{} starts", jobExecution.getJobInstance().getJobName()); - } - - @Override - public void afterJob(JobExecution jobExecution) { - log.info("{} ends", jobExecution.getJobInstance().getJobName()); - } - } - -} - diff --git a/module-batch/src/main/java/com/kernel360/modulebatch/product/job/core/ImportProductFromConcernedProductJobConfig.java b/module-batch/src/main/java/com/kernel360/modulebatch/product/job/core/ImportProductFromConcernedProductJobConfig.java new file mode 100644 index 00000000..92a387b7 --- /dev/null +++ b/module-batch/src/main/java/com/kernel360/modulebatch/product/job/core/ImportProductFromConcernedProductJobConfig.java @@ -0,0 +1,118 @@ +package com.kernel360.modulebatch.product.job.core; + +import com.kernel360.brand.entity.Brand; +import com.kernel360.modulebatch.product.job.infra.JpaProductListWriter; +import com.kernel360.modulebatch.product.job.infra.ConcernedProductToProductListItemProcessor; +import com.kernel360.product.entity.Product; +import jakarta.persistence.EntityManagerFactory; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.batch.core.ExitStatus; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobExecutionListener; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.StepExecution; +import org.springframework.batch.core.StepExecutionListener; +import org.springframework.batch.core.configuration.annotation.JobScope; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.batch.item.database.JpaItemWriter; +import org.springframework.batch.item.database.JpaPagingItemReader; +import org.springframework.batch.item.database.builder.JpaPagingItemReaderBuilder; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.PlatformTransactionManager; + +@Slf4j +@Configuration +@RequiredArgsConstructor +//@ComponentScan("com.kernel360.product") +public class ImportProductFromConcernedProductJobConfig { + + private final ConcernedProductToProductListItemProcessor concernedProductToProductListItemProcessor; + + private final EntityManagerFactory emf; + + @Bean + public Job importProductFromConcernedProductJob(JobRepository jobRepository, + @Qualifier("importProductFromConcernedProductStep") Step importProductFromConcernedProductStep) { + + return new JobBuilder("importProductFromConcernedProductJob", jobRepository) + .start(importProductFromConcernedProductStep) + .listener(new importProductFromConcernedProductJobListener()) + .build(); + } + + @Bean + @JobScope + public Step importProductFromConcernedProductStep(JobRepository jobRepository, + PlatformTransactionManager transactionManager) { + + return new StepBuilder("importProductFromConcernedProductStep", jobRepository) + .>chunk(10, transactionManager) + .listener(new importProductFromConcernedProductStepListener()) + .reader(importProductFromConcernedProductJpaPagingItemReader()) + .processor(concernedProductToProductListItemProcessor) + .writer(productListWriter()) + .build(); + } + + @Bean + @StepScope + public JpaPagingItemReader importProductFromConcernedProductJpaPagingItemReader() { + String jpql = "SELECT b FROM Brand b"; + + return new JpaPagingItemReaderBuilder() + .pageSize(100) + .queryString(jpql) + .entityManagerFactory(emf) + .name("importProductFromConcernedProductJpaPagingItemReader") + .build(); + } + + private JpaProductListWriter productListWriter() { + JpaItemWriter writer = new JpaItemWriter<>(); + writer.setEntityManagerFactory(emf); + + return new JpaProductListWriter<>(writer); + } + + //-- Execution Listener --// + + public static class importProductFromConcernedProductJobListener implements JobExecutionListener { + @Override + public void beforeJob(JobExecution jobExecution) { + log.info("{} starts", jobExecution.getJobInstance().getJobName()); + } + + @Override + public void afterJob(JobExecution jobExecution) { + log.info("{} ends", jobExecution.getJobInstance().getJobName()); + } + } + + + public static class importProductFromConcernedProductStepListener implements StepExecutionListener { + @Override + public void beforeStep(StepExecution stepExecution) { + log.info("{} starts", stepExecution.getStepName()); + } + + @Override + public ExitStatus afterStep(StepExecution stepExecution) { + log.info("StepExecutionListener - afterStep, step name: {}, status: {}", stepExecution.getStepName(), + stepExecution.getStatus()); + log.info( + "StepExecutionListener - ReadCount: {}, WriteCount: {}, FilterCount: {}, ReadSkipCount: {}, ProcessSkipCount: {}, WriteSkipCount: {}", + stepExecution.getReadCount(), stepExecution.getWriteCount(), stepExecution.getFilterCount(), + stepExecution.getReadSkipCount(), stepExecution.getProcessSkipCount(), + stepExecution.getWriteSkipCount()); + return StepExecutionListener.super.afterStep(stepExecution); + } + } +} diff --git a/module-batch/src/main/java/com/kernel360/modulebatch/product/job/core/ImportProductFromReportedProductJobConfig.java b/module-batch/src/main/java/com/kernel360/modulebatch/product/job/core/ImportProductFromReportedProductJobConfig.java new file mode 100644 index 00000000..87b9cf00 --- /dev/null +++ b/module-batch/src/main/java/com/kernel360/modulebatch/product/job/core/ImportProductFromReportedProductJobConfig.java @@ -0,0 +1,146 @@ +package com.kernel360.modulebatch.product.job.core; + +import com.kernel360.ecolife.entity.ReportedProduct; +import com.kernel360.modulebatch.product.job.infra.FilterUnusedProductTasklet; +import com.kernel360.modulebatch.product.job.infra.ReportedProductToProductItemProcessor; +import com.kernel360.product.entity.Product; +import jakarta.persistence.EntityManagerFactory; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.batch.core.ExitStatus; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobExecutionListener; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.StepExecution; +import org.springframework.batch.core.StepExecutionListener; +import org.springframework.batch.core.configuration.annotation.JobScope; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.launch.support.RunIdIncrementer; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.batch.item.database.JpaItemWriter; +import org.springframework.batch.item.database.JpaPagingItemReader; +import org.springframework.batch.item.database.builder.JpaItemWriterBuilder; +import org.springframework.batch.item.database.builder.JpaPagingItemReaderBuilder; +import org.springframework.batch.item.database.orm.JpaNativeQueryProvider; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.web.client.ResourceAccessException; + +@Slf4j +@Configuration +@RequiredArgsConstructor +public class ImportProductFromReportedProductJobConfig { + + private final ReportedProductToProductItemProcessor reportedProductToProductItemProcessor; + + private final FilterUnusedProductTasklet filterUnusedProductTasklet; + + private final EntityManagerFactory emf; + + @Bean + public Job importProductFromReportedProductJob(JobRepository jobRepository, + @Qualifier("importProductFromReportedProductStep") Step importProductFromReportedProductStep, + @Qualifier("filterUnusedProductStep") Step filterUnusedProductStep) { + log.info("Import Product from ReportedProduct by Brand Job Build Configuration"); + + return new JobBuilder("ImportProductFromReportedProductJob", jobRepository) + .start(importProductFromReportedProductStep) + .next(filterUnusedProductStep) + .incrementer(new RunIdIncrementer()) + .listener(new ImportProductFromReportedProductJobListener()) + .build(); + } + + @Bean + @JobScope + public Step importProductFromReportedProductStep(JobRepository jobRepository, + PlatformTransactionManager transactionManager) throws Exception { + log.info("Import Product from ReportedProduct by Brand Step Build Configuration"); + + return new StepBuilder("ImportProductFromReportedProductStep", jobRepository) + .chunk(100, transactionManager) + .listener(new ImportProductFromReportedProductStepListener()) + .reader(importProductFromReportedProductItemReader()) + .processor(reportedProductToProductItemProcessor) + .writer(productJpaItemWriter()) + .faultTolerant() + .retryLimit(2) + .retry(ResourceAccessException.class) + .skipLimit(10) + .skip(DataIntegrityViolationException.class) + .build(); + } + + @Bean + @StepScope + public JpaPagingItemReader importProductFromReportedProductItemReader() throws Exception { + String sqlQuery = "SELECT rp.* FROM reported_product rp " + + "JOIN (SELECT prdt_nm, MAX(issu_date) AS max_issu_date " + + "FROM reported_product GROUP BY prdt_nm) max_dates " + + "ON rp.prdt_nm = max_dates.prdt_nm AND rp.issu_date = max_dates.max_issu_date " + + "ORDER BY max_dates.max_issu_date DESC"; + JpaNativeQueryProvider queryProvider = new JpaNativeQueryProvider<>(); + queryProvider.setSqlQuery(sqlQuery); + queryProvider.setEntityClass(ReportedProduct.class); + queryProvider.afterPropertiesSet(); + + return new JpaPagingItemReaderBuilder() + .name("importProductFromReportedProductItemReader") + .pageSize(100) + .queryProvider(queryProvider) + .entityManagerFactory(emf) + .build(); + } + + @Bean + @StepScope + public JpaItemWriter productJpaItemWriter() { + return new JpaItemWriterBuilder() + .entityManagerFactory(emf) + .build(); + } + + @Bean + public Step filterUnusedProductStep(JobRepository jobRepository, PlatformTransactionManager transactionManager) { + + return new StepBuilder("filterUnusedProductStep", jobRepository) + .tasklet(filterUnusedProductTasklet, transactionManager) + .listener(new ImportProductFromReportedProductStepListener()) + .build(); + } + +//-- Execution Listener --// + + public static class ImportProductFromReportedProductJobListener implements JobExecutionListener { + @Override + public void beforeJob(JobExecution jobExecution) { + log.info("{} starts", jobExecution.getJobInstance().getJobName()); + } + + @Override + public void afterJob(JobExecution jobExecution) { + log.info("{} ends", jobExecution.getJobInstance().getJobName()); + } + } + + public static class ImportProductFromReportedProductStepListener implements StepExecutionListener { + @Override + public void beforeStep(StepExecution stepExecution) { + log.info("{} starts", stepExecution.getStepName()); + } + + @Override + public ExitStatus afterStep(StepExecution stepExecution) { + log.info("{} ends", stepExecution.getStepName()); + return stepExecution.getExitStatus(); + } + } + +} + diff --git a/module-batch/src/main/java/com/kernel360/modulebatch/product/job/core/UpdateProductFromViolatedProductJobConfig.java b/module-batch/src/main/java/com/kernel360/modulebatch/product/job/core/UpdateProductFromViolatedProductJobConfig.java new file mode 100644 index 00000000..cf51596f --- /dev/null +++ b/module-batch/src/main/java/com/kernel360/modulebatch/product/job/core/UpdateProductFromViolatedProductJobConfig.java @@ -0,0 +1,126 @@ +package com.kernel360.modulebatch.product.job.core; + +import com.kernel360.modulebatch.product.dto.ProductJoinDto; +import com.kernel360.modulebatch.product.job.infra.UpdateProductFromViolatedProductItemProcessor; +import com.kernel360.product.entity.Product; +import jakarta.persistence.EntityManagerFactory; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.batch.core.ExitStatus; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobExecutionListener; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.StepExecution; +import org.springframework.batch.core.StepExecutionListener; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.batch.item.database.JpaCursorItemReader; +import org.springframework.batch.item.database.JpaItemWriter; +import org.springframework.batch.item.database.builder.JpaCursorItemReaderBuilder; +import org.springframework.batch.item.database.builder.JpaItemWriterBuilder; +import org.springframework.batch.item.database.orm.JpaNativeQueryProvider; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.PlatformTransactionManager; + +@Slf4j +@Configuration +@RequiredArgsConstructor +public class UpdateProductFromViolatedProductJobConfig { + + private final EntityManagerFactory emf; + + private final UpdateProductFromViolatedProductItemProcessor updateProductFromViolatedProductItemProcessor; + + @Bean + public Job updateProductFromViolatedProductJob(JobRepository jobRepository, + @Qualifier("updateProductFromViolatedProductStep") Step updateProductFromViolatedProductStep) { + + return new JobBuilder("updateProductFromViolatedProductJob", jobRepository) + .start(updateProductFromViolatedProductStep) + .listener(new UpdateProductFromViolatedProductJobListener()) + .build(); + } + + @Bean + public Step updateProductFromViolatedProductStep(JobRepository jobRepository, + PlatformTransactionManager transactionManager) throws Exception { + + return new StepBuilder("updateProductFromViolatedProductStep", jobRepository) + .chunk(100, transactionManager) + .reader(updateProductFromViolatedProductJpaPagingItemReader()) + .processor(updateProductFromViolatedProductItemProcessor) + .writer(updateProductFromViolatedProductJpaItemWriter()) + .listener(new UpdateProductFromViolatedProductStepListener()) + .build(); + } + + @Bean + @StepScope + public JpaCursorItemReader updateProductFromViolatedProductJpaPagingItemReader() throws Exception { + String query = + "SELECT p.product_name, p.company_name, vp.actioned_date, vp.violated_cn, vp.action_cn, vp.etc_info " + + "FROM product p " + + "INNER JOIN violated_product vp ON REGEXP_REPLACE(vp.company_name, ' ', '', 'g') LIKE" + + "'%' || REGEXP_REPLACE(REGEXP_REPLACE(REGEXP_REPLACE(REGEXP_REPLACE(p.company_name, '주식회사', '', 'g')," + + " '\\([^)]*\\)', '','g'), '\\[[^]]*\\]', '','g'), ' ', '', 'g') || '%'" + + "AND vp.product_name = p.product_name;"; + + JpaNativeQueryProvider queryProvider = new JpaNativeQueryProvider<>(); + queryProvider.setSqlQuery(query); + queryProvider.setEntityClass(ProductJoinDto.class); + queryProvider.afterPropertiesSet(); + + return new JpaCursorItemReaderBuilder() + .name("updateProductFromViolatedProductJpaPagingItemReader") + .queryProvider(queryProvider) + .entityManagerFactory(emf) + .build(); + } + + @Bean + @StepScope + public JpaItemWriter updateProductFromViolatedProductJpaItemWriter() { + + return new JpaItemWriterBuilder() + .entityManagerFactory(emf) + .build(); + } + + + //-- Execution Listener --// + public static class UpdateProductFromViolatedProductJobListener implements JobExecutionListener { + @Override + public void beforeJob(JobExecution jobExecution) { + log.info("{} starts", jobExecution.getJobInstance().getJobName()); + } + + @Override + public void afterJob(JobExecution jobExecution) { + log.info("{} ends", jobExecution.getJobInstance().getJobName()); + } + } + + public static class UpdateProductFromViolatedProductStepListener implements StepExecutionListener { + @Override + public void beforeStep(StepExecution stepExecution) { + log.info("{} starts", stepExecution.getStepName()); + } + + @Override + public ExitStatus afterStep(StepExecution stepExecution) { + log.info("StepExecutionListener - afterStep, step name: {}, status: {}", stepExecution.getStepName(), + stepExecution.getStatus()); + log.info( + "StepExecutionListener - ReadCount: {}, WriteCount: {}, FilterCount: {}, ReadSkipCount: {}, ProcessSkipCount: {}, WriteSkipCount: {}", + stepExecution.getReadCount(), stepExecution.getWriteCount(), stepExecution.getFilterCount(), + stepExecution.getReadSkipCount(), stepExecution.getProcessSkipCount(), + stepExecution.getWriteSkipCount()); + return StepExecutionListener.super.afterStep(stepExecution); + } + } +} diff --git a/module-batch/src/main/java/com/kernel360/modulebatch/product/job/infra/ConcernedProductToProductListItemProcessor.java b/module-batch/src/main/java/com/kernel360/modulebatch/product/job/infra/ConcernedProductToProductListItemProcessor.java new file mode 100644 index 00000000..980dad9b --- /dev/null +++ b/module-batch/src/main/java/com/kernel360/modulebatch/product/job/infra/ConcernedProductToProductListItemProcessor.java @@ -0,0 +1,158 @@ +package com.kernel360.modulebatch.product.job.infra; + +import com.kernel360.brand.entity.Brand; +import com.kernel360.brand.repository.BrandRepository; +import com.kernel360.ecolife.entity.ConcernedProduct; +import com.kernel360.ecolife.repository.ConcernedProductRepository; +import com.kernel360.modulebatch.product.dto.ProductDto; +import com.kernel360.product.entity.Product; +import com.kernel360.product.entity.SafetyStatus; +import com.kernel360.product.repository.ProductRepository; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.item.ItemProcessor; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@StepScope +@RequiredArgsConstructor +public class ConcernedProductToProductListItemProcessor implements ItemProcessor> { + + private final ConcernedProductRepository concernedProductRepository; + + private final ProductRepository productRepository; + + private final BrandRepository brandRepository; + + @Override + public List process(Brand brand) throws Exception { + List concernedProductList = concernedProductRepository + .findByBrandNameAndCompanyName( + "%" + brand.getBrandName().replaceAll(" ", "%") + "%", + "%" + brand.getCompanyName().replaceAll(" ", "%") + "%"); + + List productDtoList = concernedProductList.stream() + .filter(cp -> cp.getInspectedOrganization() != null) + .map(cp -> ProductDto.of(cp.getProductName(), + null, + null, + "취하".equals(cp.getProductType()) + ? SafetyStatus.DANGER + : SafetyStatus.CONCERN, + 0, + cp.getCompanyName(), + cp.getReportNumber(), + cp.getProductType(), + LocalDate.from(cp.getIssuedDate()), + cp.getSafetyInspectionStandard(), + cp.getUpperItem(), + cp.getItem(), + null, + null, + null, + null, null, null, null, + null, null, null, null, + cp.getManufacture(), null, + cp.getManufactureNation(), null)) + .toList(); + + return convertToProductList(productDtoList); + } + + private List convertToProductList(List productDtoList) { + List productList = new ArrayList<>(); + + for (ProductDto productDto : productDtoList) { + Optional foundProduct = productRepository.findProductByProductNameAndCompanyName( + productDto.productName(), "%" + getCompanyNameWithoutSlash(productDto.companyName()) + "%"); + + boolean foundInList = productList.stream().anyMatch( + product -> product.getProductName().equals(productDto.productName()) && + product.getCompanyName().equals(productDto.companyName())); + + if (foundInList) { // 현재 처리할 데이터에 이미 추가가 되어 있다면 무시 + continue; + } + + if (foundProduct.isEmpty()) { + Product newProduct = generateNewProduct(productDto); + productList.add(newProduct); + continue; + } + + if (productDto.issuedDate().isAfter(foundProduct.get().getIssuedDate())) { // 저장된 데이터보다 최신의 제품 데이터라면 + foundProduct.get().updateDetail(productDto.barcode(), + productDto.imageSource(), + productDto.reportNumber(), + String.valueOf(productDto.safetyStatus()), + productDto.issuedDate(), + productDto.safetyInspectionStandard(), + productDto.upperItem(), + productDto.item(), + productDto.propose(), + productDto.weight(), + productDto.usage(), + productDto.usagePrecaution(), + productDto.firstAid(), + productDto.mainSubstance(), + productDto.allergicSubstance(), + productDto.otherSubstance(), + productDto.preservative(), + productDto.surfactant(), + productDto.fluorescentWhitening(), + productDto.manufactureType(), + productDto.manufactureMethod(), + getNation(productDto), + productDto.violationInfo()); + } + } + + return productList; + } + + private Product generateNewProduct(ProductDto productDto) { + + return Product.of(productDto.productName(), productDto.barcode(), + productDto.imageSource(), productDto.reportNumber(), String.valueOf(productDto.safetyStatus()), + productDto.viewCount(), getCompanyNameWithoutSlash(productDto.companyName()), productDto.productType(), + productDto.issuedDate(), productDto.safetyInspectionStandard(), + productDto.upperItem(), + productDto.item(), productDto.propose(), productDto.weight(), productDto.usage(), + productDto.usagePrecaution(), productDto.firstAid(), productDto.mainSubstance(), + productDto.allergicSubstance(), productDto.otherSubstance(), productDto.preservative(), + productDto.surfactant(), productDto.fluorescentWhitening(), productDto.manufactureType(), + productDto.manufactureMethod(), getNation(productDto), productDto.violationInfo()); + } + + public static String getCompanyNameWithoutSlash(String companyName) { + int index = companyName.indexOf("/"); + if (index != -1) { + return companyName.substring(0, index); + } + return companyName; + + } + + private String getNation(ProductDto productDto) { + if (productDto.manufactureType().equals("수입")) { + if (!productDto.manufactureNation().isBlank()) { + return productDto.manufactureNation(); + } + List brandList = brandRepository.findByCompanyName(productDto.companyName()); + for (Brand b : brandList) { + if (productDto.productName().contains(b.getBrandName())) { + return b.getNationName(); + } + } + } + return "대한민국"; + } + + +} diff --git a/module-batch/src/main/java/com/kernel360/modulebatch/product/job/infra/FilterUnusedProductTasklet.java b/module-batch/src/main/java/com/kernel360/modulebatch/product/job/infra/FilterUnusedProductTasklet.java new file mode 100644 index 00000000..0aa99cd0 --- /dev/null +++ b/module-batch/src/main/java/com/kernel360/modulebatch/product/job/infra/FilterUnusedProductTasklet.java @@ -0,0 +1,26 @@ +package com.kernel360.modulebatch.product.job.infra; + +import lombok.RequiredArgsConstructor; +import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.batch.core.step.tasklet.Tasklet; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Component; + +@Component +@StepScope +@RequiredArgsConstructor +public class FilterUnusedProductTasklet implements Tasklet { + + private final JdbcTemplate jdbcTemplate; + + @Override + public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { + jdbcTemplate.update("delete from product where product.upper_item like '%세탁%'"); + jdbcTemplate.update("delete from product where product.product_name similar to '%(방향제|그라스|통풍구|선바이저|살라딘|세탁|체인)%'"); + + return RepeatStatus.FINISHED; + } +} diff --git a/module-batch/src/main/java/com/kernel360/modulebatch/product/job/JpaProductListWriter.java b/module-batch/src/main/java/com/kernel360/modulebatch/product/job/infra/JpaProductListWriter.java similarity index 92% rename from module-batch/src/main/java/com/kernel360/modulebatch/product/job/JpaProductListWriter.java rename to module-batch/src/main/java/com/kernel360/modulebatch/product/job/infra/JpaProductListWriter.java index 1afb6b33..1b32d702 100644 --- a/module-batch/src/main/java/com/kernel360/modulebatch/product/job/JpaProductListWriter.java +++ b/module-batch/src/main/java/com/kernel360/modulebatch/product/job/infra/JpaProductListWriter.java @@ -1,4 +1,4 @@ -package com.kernel360.modulebatch.product.job; +package com.kernel360.modulebatch.product.job.infra; import java.util.List; import org.springframework.batch.item.Chunk; diff --git a/module-batch/src/main/java/com/kernel360/modulebatch/product/job/infra/ReportedProductToProductItemProcessor.java b/module-batch/src/main/java/com/kernel360/modulebatch/product/job/infra/ReportedProductToProductItemProcessor.java new file mode 100644 index 00000000..12254e5f --- /dev/null +++ b/module-batch/src/main/java/com/kernel360/modulebatch/product/job/infra/ReportedProductToProductItemProcessor.java @@ -0,0 +1,143 @@ +package com.kernel360.modulebatch.product.job.infra; + +import com.kernel360.brand.entity.Brand; +import com.kernel360.brand.repository.BrandRepository; +import com.kernel360.ecolife.entity.ReportedProduct; +import com.kernel360.modulebatch.product.dto.ProductDto; +import com.kernel360.product.entity.Product; +import com.kernel360.product.entity.SafetyStatus; +import com.kernel360.product.repository.ProductRepository; +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.item.ItemProcessor; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@StepScope +@ComponentScan("com.kernel360.product") +@ComponentScan("com.kernel360.brand") +@RequiredArgsConstructor +public class ReportedProductToProductItemProcessor implements ItemProcessor { + private final ProductRepository productRepository; + + private final BrandRepository brandRepository; + + @Override + public Product process(ReportedProduct rp) throws Exception { + if (rp.getInspectedOrganization() == null) { + return null; + } + + ProductDto productDto = ProductDto.of(rp.getProductName(), null, null, + "취하".equals(rp.getRenewType()) + ? SafetyStatus.DANGER + : SafetyStatus.CONCERN, + 0, + rp.getCompanyName(), + rp.getSafetyReportNumber(), + rp.getProductType(), + LocalDate.from(rp.getIssuedDate()), + rp.getSafeStandard(), rp.getUpperItem(), + rp.getItem(), + rp.getProductPropose(), rp.getWeightAndBulk(), + rp.getUseMethod(), + rp.getUsageAttentionReport(), + rp.getFirstAid(), rp.getMainSubstance(), + rp.getAllergicSubstance(), + rp.getOtherSubstance(), rp.getPreservative(), + rp.getSurfactant(), + rp.getFluorescentWhiteningAgent(), + rp.getManufacture(), rp.getManufactureMethod(), + rp.getManufactureNation(), null); + + Optional foundProduct = productRepository + .findProductByProductNameAndCompanyName(productDto.productName(), + addWildCardToCompanyName(getCompanyNameWithoutSlash(productDto.companyName()))); + + if (foundProduct.isEmpty()) { + return generateNewProduct(productDto); + } + + if (productDto.issuedDate().isAfter(foundProduct.get().getIssuedDate())) { // 저장된 데이터보다 최신의 제품 데이터라면 + foundProduct.get().updateDetail(productDto.barcode(), + productDto.imageSource(), + productDto.reportNumber(), + String.valueOf(productDto.safetyStatus()), + productDto.issuedDate(), + productDto.safetyInspectionStandard(), + productDto.upperItem(), + productDto.item(), + productDto.propose(), + productDto.weight(), + productDto.usage(), + productDto.usagePrecaution(), + productDto.firstAid(), + productDto.mainSubstance(), + productDto.allergicSubstance(), + productDto.otherSubstance(), + productDto.preservative(), + productDto.surfactant(), + productDto.fluorescentWhitening(), + productDto.manufactureType(), + productDto.manufactureMethod(), + getNation(productDto), + productDto.violationInfo()); + + return foundProduct.get(); + } + + return null; + } + + private Product generateNewProduct(ProductDto productDto) { + + return Product.of(productDto.productName(), productDto.barcode(), + productDto.imageSource(), productDto.reportNumber(), String.valueOf(productDto.safetyStatus()), + productDto.viewCount(), getCompanyNameWithoutSlash(productDto.companyName()), productDto.productType(), + productDto.issuedDate(), productDto.safetyInspectionStandard(), + productDto.upperItem(), + productDto.item(), productDto.propose(), productDto.weight(), productDto.usage(), + productDto.usagePrecaution(), productDto.firstAid(), productDto.mainSubstance(), + productDto.allergicSubstance(), productDto.otherSubstance(), productDto.preservative(), + productDto.surfactant(), productDto.fluorescentWhitening(), productDto.manufactureType(), + productDto.manufactureMethod(), getNation(productDto), productDto.violationInfo()); + } + + public String getCompanyNameWithoutSlash(String companyName) { + int index = companyName.indexOf("/"); + if (index != -1) { + return companyName.substring(0, index); + } + return companyName; + } + + public String addWildCardToCompanyName(String companyName) { + return companyName.replaceAll(" ", "%"); + } + + private String getNation(ProductDto productDto) { + if (productDto.manufactureType().equals("수입")) { + if (!productDto.manufactureNation().isBlank()) { + return productDto.manufactureNation(); + } + String targetCompany = addWildCardToCompanyName(getCompanyNameWithoutSlash(productDto.companyName())); + List brandList = brandRepository.findByCompanyName(targetCompany); + for (Brand b : brandList) { + if (productDto.productName().contains(b.getBrandName())) { + return b.getNationName(); + } + } + if (!brandList.isEmpty()) { + return brandList.get(0).getNationName(); + } + } + return "대한민국"; + } + +} diff --git a/module-batch/src/main/java/com/kernel360/modulebatch/product/job/infra/UpdateProductFromViolatedProductItemProcessor.java b/module-batch/src/main/java/com/kernel360/modulebatch/product/job/infra/UpdateProductFromViolatedProductItemProcessor.java new file mode 100644 index 00000000..1fd4fcd5 --- /dev/null +++ b/module-batch/src/main/java/com/kernel360/modulebatch/product/job/infra/UpdateProductFromViolatedProductItemProcessor.java @@ -0,0 +1,35 @@ +package com.kernel360.modulebatch.product.job.infra; + +import com.kernel360.modulebatch.product.dto.ProductJoinDto; +import com.kernel360.product.entity.Product; +import com.kernel360.product.repository.ProductRepository; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.batch.item.ItemProcessor; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class UpdateProductFromViolatedProductItemProcessor implements ItemProcessor { + + private final ProductRepository productRepository; + + @Override + public Product process(ProductJoinDto item) throws Exception { + String productName = item.productName(); + String companyName = item.companyName(); + + Optional foundProduct = productRepository.findProductByProductNameAndCompanyName(productName, + companyName); + + if (foundProduct.isPresent()) { + foundProduct.get().updateViolatedInfo( + item.actionedDate() + "/" + item.violatedCn() + "/" + item.actionCn() + "/" + item.etcInfo()); + return foundProduct.get(); + } + + return null; + } +} diff --git a/module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/client/ReportedProductClient.java b/module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/client/ReportedProductClient.java index b0191340..e0fd3f8a 100644 --- a/module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/client/ReportedProductClient.java +++ b/module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/client/ReportedProductClient.java @@ -2,6 +2,7 @@ import java.nio.charset.StandardCharsets; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatusCode; import org.springframework.http.MediaType; import org.springframework.stereotype.Component; @@ -11,7 +12,12 @@ @Slf4j @Component public class ReportedProductClient implements ApiClient { - private static final String AUTH_KEY = System.getenv("API_AUTH_KEY"); + + @Value("${external.ecolife-api.path}") + private String BASE_PATH; + + @Value("${external.ecolife-api.service-key}") + private String AUTH_KEY; private final RestClient restClient; @@ -60,7 +66,7 @@ public String getXmlResponse(Integer pageNumber) { public String buildUri(Integer pageNumber) { - return UriComponentsBuilder.fromHttpUrl("https://ecolife.me.go.kr/openapi/ServiceSvl") + return UriComponentsBuilder.fromHttpUrl(BASE_PATH) .queryParam("AuthKey", AUTH_KEY) .queryParam("ServiceName", "slfsfcfst02List") .queryParam("PageCount", "20") diff --git a/module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/client/ReportedProductDetailClient.java b/module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/client/ReportedProductDetailClient.java index ff41c6d3..23fe046d 100644 --- a/module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/client/ReportedProductDetailClient.java +++ b/module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/client/ReportedProductDetailClient.java @@ -3,6 +3,7 @@ import com.kernel360.ecolife.entity.ReportedProduct; import java.nio.charset.StandardCharsets; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatusCode; import org.springframework.http.MediaType; import org.springframework.stereotype.Component; @@ -13,7 +14,11 @@ @Component public class ReportedProductDetailClient implements ApiClient { - private static final String AUTH_KEY = System.getenv("API_AUTH_KEY"); + @Value("${external.ecolife-api.path}") + private String BASE_PATH; + + @Value("${external.ecolife-api.service-key}") + private String AUTH_KEY; private final RestClient restClient; @@ -56,7 +61,7 @@ public String getXmlResponse(ReportedProduct reportedProduct) { @Override public String buildUri(ReportedProduct reportedProduct) { - return UriComponentsBuilder.fromHttpUrl("https://ecolife.me.go.kr/openapi/ServiceSvl") + return UriComponentsBuilder.fromHttpUrl(BASE_PATH) .queryParam("AuthKey", AUTH_KEY) .queryParam("ServiceName", "slfsfcfst02Detail") .queryParam("mstId", reportedProduct.getId().getProductMasterId()) diff --git a/module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/client/ReportedProductFromBrandClient.java b/module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/client/ReportedProductFromBrandClient.java index 96a2eb16..eb2382a2 100644 --- a/module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/client/ReportedProductFromBrandClient.java +++ b/module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/client/ReportedProductFromBrandClient.java @@ -3,6 +3,7 @@ import com.kernel360.brand.entity.Brand; import java.nio.charset.StandardCharsets; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatusCode; import org.springframework.http.MediaType; import org.springframework.stereotype.Component; @@ -13,7 +14,11 @@ @Component public class ReportedProductFromBrandClient{ - private static final String AUTH_KEY = System.getenv("API_AUTH_KEY"); + @Value("${external.ecolife-api.path}") + private String BASE_PATH; + + @Value("${external.ecolife-api.service-key}") + private String AUTH_KEY; private final RestClient restClient; @@ -55,7 +60,7 @@ public String getXmlResponse(Brand brand, int pageNumber) { public String buildUri(Brand brand, int pageNumber) { - return UriComponentsBuilder.fromHttpUrl("https://ecolife.me.go.kr/openapi/ServiceSvl") + return UriComponentsBuilder.fromHttpUrl(BASE_PATH) .queryParam("AuthKey", AUTH_KEY) .queryParam("ServiceName", "slfsfcfst02List") .queryParam("PageCount", "20") diff --git a/module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/job/ReportedProductDetailApiJobConfig.java b/module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/job/core/FetchReportedProductDetailJobConfig.java similarity index 88% rename from module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/job/ReportedProductDetailApiJobConfig.java rename to module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/job/core/FetchReportedProductDetailJobConfig.java index ef64cca7..f123178a 100644 --- a/module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/job/ReportedProductDetailApiJobConfig.java +++ b/module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/job/core/FetchReportedProductDetailJobConfig.java @@ -1,7 +1,7 @@ -package com.kernel360.modulebatch.reportedproduct.job; +package com.kernel360.modulebatch.reportedproduct.job.core; import com.kernel360.ecolife.entity.ReportedProduct; -import com.kernel360.modulebatch.reportedproduct.service.ReportedProductService; +import com.kernel360.modulebatch.reportedproduct.job.infra.ReportedProductDetailItemProcessor; import jakarta.persistence.EntityManagerFactory; import java.net.ConnectException; import java.nio.channels.ClosedChannelException; @@ -28,8 +28,9 @@ @Slf4j @Configuration @RequiredArgsConstructor -public class ReportedProductDetailApiJobConfig { - private final ReportedProductService service; +public class FetchReportedProductDetailJobConfig { + + private final ReportedProductDetailItemProcessor reportedProductDetailItemProcessor; private final EntityManagerFactory emf; @@ -51,7 +52,7 @@ public Step fetchReportedProductDetailStep(JobRepository jobRepository, return new StepBuilder("fetchReportedProductDetailStep", jobRepository) .chunk(10, transactionManager) .reader(productDetailItemReader()) // reported_product 테이블 읽어서 엔티티를 전달 - .processor(productDetailItemProcessor()) // 전달받은 엔티티로 detail 조회, 엔티티로 변환 + .processor(reportedProductDetailItemProcessor) // 전달받은 엔티티로 detail 조회, 엔티티로 변환 .writer(productDetailJpaItemWriter(emf)) // 엔티티를 테이블에 업데이트 .faultTolerant() .retryLimit(2) @@ -74,12 +75,6 @@ public JpaPagingItemReader productDetailItemReader() { return reader; } - @Bean - @StepScope - public ReportedProductDetailItemProcessor productDetailItemProcessor() { - return new ReportedProductDetailItemProcessor(service); - } - @Bean @StepScope public JpaItemWriter productDetailJpaItemWriter(EntityManagerFactory emf) { diff --git a/module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/job/ReportedProductApiJobConfig.java b/module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/job/core/FetchReportedProductListJobConfig.java similarity index 77% rename from module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/job/ReportedProductApiJobConfig.java rename to module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/job/core/FetchReportedProductListJobConfig.java index fe1791da..0734288c 100644 --- a/module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/job/ReportedProductApiJobConfig.java +++ b/module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/job/core/FetchReportedProductListJobConfig.java @@ -1,7 +1,8 @@ -package com.kernel360.modulebatch.reportedproduct.job; +package com.kernel360.modulebatch.reportedproduct.job.core; import com.kernel360.modulebatch.reportedproduct.dto.ReportedProductDto; -import com.kernel360.modulebatch.reportedproduct.service.ReportedProductService; +import com.kernel360.modulebatch.reportedproduct.job.infra.ReportedProductListItemReader; +import com.kernel360.modulebatch.reportedproduct.job.infra.ReportedProductListItemWriter; import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -9,7 +10,6 @@ import org.springframework.batch.core.JobExecution; import org.springframework.batch.core.JobExecutionListener; import org.springframework.batch.core.Step; -import org.springframework.batch.core.configuration.annotation.StepScope; import org.springframework.batch.core.job.builder.JobBuilder; import org.springframework.batch.core.launch.support.RunIdIncrementer; import org.springframework.batch.core.repository.JobRepository; @@ -24,9 +24,12 @@ @Slf4j @Configuration @RequiredArgsConstructor -public class ReportedProductApiJobConfig { +public class FetchReportedProductListJobConfig { + + private final ReportedProductListItemReader reportedProductListItemReader; + + private final ReportedProductListItemWriter reportedProductListItemWriter; - private final ReportedProductService service; @Bean public Job fetchReportedProductJob(JobRepository jobRepository, @@ -45,10 +48,11 @@ public Job fetchReportedProductJob(JobRepository jobRepository, public Step fetchReportedProductListStep(JobRepository jobRepository, PlatformTransactionManager transactionManager) { log.info("Fetch ReportedProduct List Step Build Configuration"); + return new StepBuilder("fetchReportedProductListStep", jobRepository) ., List>chunk(10, transactionManager) - .reader(productListItemReader()) // API 요청, 응답을 DTO 리스트로 반환 - .writer(productListItemWriter()) // DTO 리스트 입력, 저장 + .reader(reportedProductListItemReader) // API 요청, 응답을 DTO 리스트로 반환 + .writer(reportedProductListItemWriter) // DTO 리스트 입력, 저장 .faultTolerant() .retryLimit(2) .retry(ResourceAccessException.class) @@ -57,18 +61,6 @@ public Step fetchReportedProductListStep(JobRepository jobRepository, .build(); } - @Bean - @StepScope - public ReportedProductListItemReader productListItemReader() { - return new ReportedProductListItemReader(service); - } - - @Bean - @StepScope - public ReportedProductListItemWriter productListItemWriter() { - return new ReportedProductListItemWriter(service); - } - //-- Execution Listener --// public static class FetchReportedProductExecutionListener implements JobExecutionListener { @@ -83,5 +75,4 @@ public void afterJob(JobExecution jobExecution) { } } - } \ No newline at end of file diff --git a/module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/job/ReportedProductFromBrandJobConfig.java b/module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/job/core/ReportedProductFromBrandJobConfig.java similarity index 75% rename from module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/job/ReportedProductFromBrandJobConfig.java rename to module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/job/core/ReportedProductFromBrandJobConfig.java index 365acc09..691dc8c5 100644 --- a/module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/job/ReportedProductFromBrandJobConfig.java +++ b/module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/job/core/ReportedProductFromBrandJobConfig.java @@ -1,9 +1,9 @@ -package com.kernel360.modulebatch.reportedproduct.job; +package com.kernel360.modulebatch.reportedproduct.job.core; import com.kernel360.brand.entity.Brand; -import com.kernel360.modulebatch.reportedproduct.client.ReportedProductFromBrandClient; import com.kernel360.modulebatch.reportedproduct.dto.ReportedProductDto; -import com.kernel360.modulebatch.reportedproduct.service.ReportedProductService; +import com.kernel360.modulebatch.reportedproduct.job.infra.FetchReportedProductListFromBrandItemProcessor; +import com.kernel360.modulebatch.reportedproduct.job.infra.ReportedProductListItemWriter; import jakarta.persistence.EntityManagerFactory; import java.util.List; import lombok.RequiredArgsConstructor; @@ -27,9 +27,9 @@ @RequiredArgsConstructor public class ReportedProductFromBrandJobConfig { - private final ReportedProductFromBrandClient client; + private final ReportedProductListItemWriter reportedProductListItemWriter; - private final ReportedProductService service; + private final FetchReportedProductListFromBrandItemProcessor fetchReportedProductListFromBrandItemProcessor; private final EntityManagerFactory emf; @@ -51,8 +51,8 @@ public Step fetchReportedProductFromBrandStep(JobRepository jobRepository, return new StepBuilder("fetchReportedProductFromBrandStep", jobRepository) .>chunk(1, transactionManager) .reader(readBrand()) // brand 목록을 읽어와서 전달 - .processor(itemProcessor()) // 브랜드 정보를 통해서 API 요청, reportedProductDto 리스트 반환 - .writer(reportedProductListItemWriter()) + .processor(fetchReportedProductListFromBrandItemProcessor) // 브랜드 정보를 통해서 API 요청, reportedProductDto 리스트 반환 + .writer(reportedProductListItemWriter) .faultTolerant() .retryLimit(2) .retry(ResourceAccessException.class) @@ -70,16 +70,4 @@ public JpaPagingItemReader readBrand() throws Exception { return jpaPagingItemReader; } - - @Bean - @StepScope - public FetchReportedProductListFromBrandItemProcessor itemProcessor() { - return new FetchReportedProductListFromBrandItemProcessor(client, service); - } - - @Bean - @StepScope - public ReportedProductListItemWriter reportedProductListItemWriter() { - return new ReportedProductListItemWriter(service); - } } diff --git a/module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/job/FetchReportedProductListFromBrandItemProcessor.java b/module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/job/infra/FetchReportedProductListFromBrandItemProcessor.java similarity index 90% rename from module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/job/FetchReportedProductListFromBrandItemProcessor.java rename to module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/job/infra/FetchReportedProductListFromBrandItemProcessor.java index f5759490..7bba518b 100644 --- a/module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/job/FetchReportedProductListFromBrandItemProcessor.java +++ b/module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/job/infra/FetchReportedProductListFromBrandItemProcessor.java @@ -1,4 +1,4 @@ -package com.kernel360.modulebatch.reportedproduct.job; +package com.kernel360.modulebatch.reportedproduct.job.infra; import com.kernel360.brand.entity.Brand; import com.kernel360.modulebatch.reportedproduct.client.ReportedProductFromBrandClient; @@ -10,9 +10,13 @@ import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.batch.core.configuration.annotation.StepScope; import org.springframework.batch.item.ItemProcessor; +import org.springframework.stereotype.Component; @Slf4j +@Component +@StepScope @RequiredArgsConstructor public class FetchReportedProductListFromBrandItemProcessor implements ItemProcessor> { diff --git a/module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/job/ReportedProductDetailItemProcessor.java b/module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/job/infra/ReportedProductDetailItemProcessor.java similarity index 85% rename from module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/job/ReportedProductDetailItemProcessor.java rename to module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/job/infra/ReportedProductDetailItemProcessor.java index e1847b0e..554ebf03 100644 --- a/module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/job/ReportedProductDetailItemProcessor.java +++ b/module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/job/infra/ReportedProductDetailItemProcessor.java @@ -1,4 +1,4 @@ -package com.kernel360.modulebatch.reportedproduct.job; +package com.kernel360.modulebatch.reportedproduct.job.infra; import com.kernel360.ecolife.entity.ReportedProduct; import com.kernel360.modulebatch.reportedproduct.client.ReportedProductDetailClient; @@ -6,19 +6,20 @@ import com.kernel360.modulebatch.reportedproduct.dto.ReportedProductDto; import com.kernel360.modulebatch.reportedproduct.service.ReportedProductService; import java.time.format.DateTimeFormatter; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.batch.item.ItemProcessor; import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; @Slf4j +@Component +@RequiredArgsConstructor public class ReportedProductDetailItemProcessor implements ItemProcessor { + private final ReportedProductService service; - private final ReportedProductDetailClient client; - public ReportedProductDetailItemProcessor(ReportedProductService service) { - this.service = service; - this.client = new ReportedProductDetailClient(); - } + private final ReportedProductDetailClient client; /** * @param item ItemReader 에서 넘겨받은 엔티티 @@ -52,7 +53,8 @@ private String fetchXmlResponse(ReportedProduct reportedProduct) { return removeInvalidXmlCharacters(xmlResponse); } - public String removeInvalidXmlCharacters(String xmlString) { // XML 1.0 Specification 을 준수하는 ASCII printable characters (REPLACEMENT_CHARACTER) + public String removeInvalidXmlCharacters( + String xmlString) { // XML 1.0 Specification 을 준수하는 ASCII printable characters (REPLACEMENT_CHARACTER) String pattern = "[^\t\r\n -\uD7FF\uE000-\uFFFD\ud800\udc00-\udbff\udfff]"; return xmlString.replaceAll(pattern, ""); diff --git a/module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/job/ReportedProductListItemReader.java b/module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/job/infra/ReportedProductListItemReader.java similarity index 89% rename from module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/job/ReportedProductListItemReader.java rename to module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/job/infra/ReportedProductListItemReader.java index df7f16d4..16a71161 100644 --- a/module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/job/ReportedProductListItemReader.java +++ b/module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/job/infra/ReportedProductListItemReader.java @@ -1,4 +1,4 @@ -package com.kernel360.modulebatch.reportedproduct.job; +package com.kernel360.modulebatch.reportedproduct.job.infra; import com.fasterxml.jackson.core.JsonProcessingException; import com.kernel360.modulebatch.reportedproduct.client.ReportedProductClient; @@ -7,9 +7,13 @@ import java.util.List; import lombok.extern.slf4j.Slf4j; import org.springframework.batch.core.annotation.BeforeStep; +import org.springframework.batch.core.configuration.annotation.StepScope; import org.springframework.batch.item.ItemReader; +import org.springframework.stereotype.Component; @Slf4j +@Component +@StepScope public class ReportedProductListItemReader implements ItemReader> { private static int MAX_PAGES_PER_JOB; diff --git a/module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/job/ReportedProductListItemWriter.java b/module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/job/infra/ReportedProductListItemWriter.java similarity index 71% rename from module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/job/ReportedProductListItemWriter.java rename to module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/job/infra/ReportedProductListItemWriter.java index e0de3acd..1fc453ce 100644 --- a/module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/job/ReportedProductListItemWriter.java +++ b/module-batch/src/main/java/com/kernel360/modulebatch/reportedproduct/job/infra/ReportedProductListItemWriter.java @@ -1,17 +1,21 @@ -package com.kernel360.modulebatch.reportedproduct.job; +package com.kernel360.modulebatch.reportedproduct.job.infra; import com.kernel360.modulebatch.reportedproduct.dto.ReportedProductDto; import com.kernel360.modulebatch.reportedproduct.service.ReportedProductService; import java.util.List; +import org.springframework.batch.core.configuration.annotation.StepScope; import org.springframework.batch.item.Chunk; import org.springframework.batch.item.ItemWriter; +import org.springframework.stereotype.Component; +@Component +@StepScope public class ReportedProductListItemWriter implements ItemWriter> { - private final ReportedProductService apiService; + private final ReportedProductService service; - public ReportedProductListItemWriter(ReportedProductService apiService) { - this.apiService = apiService; + public ReportedProductListItemWriter(ReportedProductService service) { + this.service = service; } /** @@ -23,7 +27,7 @@ public ReportedProductListItemWriter(ReportedProductService apiService) { public void write(Chunk extends List> chunk) throws Exception { for (List c : chunk) { for (ReportedProductDto reportedProductDto : c) { - apiService.saveReportedProduct(reportedProductDto); + service.saveReportedProduct(reportedProductDto); } } } diff --git a/module-batch/src/main/java/com/kernel360/modulebatch/scheduler/ConcernedProductScheduler.java b/module-batch/src/main/java/com/kernel360/modulebatch/scheduler/ConcernedProductScheduler.java index 7ee92e22..5de2b8f2 100644 --- a/module-batch/src/main/java/com/kernel360/modulebatch/scheduler/ConcernedProductScheduler.java +++ b/module-batch/src/main/java/com/kernel360/modulebatch/scheduler/ConcernedProductScheduler.java @@ -18,16 +18,19 @@ public class ConcernedProductScheduler { private final Job fetchConcernedProductListFromBrandJob; private final Job fetchConcernedProductDetailJob; + private final Job importProductFromConcernedProductJob; private final JobLauncher jobLauncher; @Autowired public ConcernedProductScheduler( @Qualifier("fetchConcernedProductListFromBrandJob") Job fetchConcernedProductListJob, @Qualifier("fetchConcernedProductDetailJob") Job fetchConcernedProductDetailJob, + @Qualifier("importProductFromConcernedProductJob") Job importProductFromConcernedProductJob, JobLauncher jobLauncher) { this.fetchConcernedProductListFromBrandJob = fetchConcernedProductListJob; this.fetchConcernedProductDetailJob = fetchConcernedProductDetailJob; + this.importProductFromConcernedProductJob = importProductFromConcernedProductJob; this.jobLauncher = jobLauncher; } @@ -47,6 +50,14 @@ public void executeFetchConcernedProductDetailJob() { executeJob(fetchConcernedProductDetailJob); } + /** + * 매주 목요일 20시 00분 실행 + */ + @Scheduled(cron = "0 0 20 * * THU", zone = "Asia/Seoul") + public void executeImportProductFromConcernedProductJob() { + executeJob(importProductFromConcernedProductJob); + } + private synchronized void executeJob(Job job) { try { diff --git a/module-batch/src/main/java/com/kernel360/modulebatch/scheduler/ViolatedProductScheduler.java b/module-batch/src/main/java/com/kernel360/modulebatch/scheduler/ViolatedProductScheduler.java new file mode 100644 index 00000000..e5d3d9a2 --- /dev/null +++ b/module-batch/src/main/java/com/kernel360/modulebatch/scheduler/ViolatedProductScheduler.java @@ -0,0 +1,77 @@ +package com.kernel360.modulebatch.scheduler; + +import java.time.LocalDateTime; +import lombok.extern.slf4j.Slf4j; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecutionException; +import org.springframework.batch.core.JobParametersBuilder; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Profile; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@Profile({"local", "dev", "prod"}) +public class ViolatedProductScheduler { + private final Job fetchViolatedProductListJob; + private final Job fetchViolatedProductDetailJob; + + private final Job updateProductFromViolatedProductJob; + + private final JobLauncher jobLauncher; + + @Autowired + public ViolatedProductScheduler( + @Qualifier("fetchViolatedProductListJob") Job fetchViolatedProductListJob, + @Qualifier("fetchViolatedProductDetailJob") Job fetchViolatedProductDetailJob, + @Qualifier("updateProductFromViolatedProductJob") Job updateProductFromViolatedProductJob, + JobLauncher jobLauncher) { + this.fetchViolatedProductListJob = fetchViolatedProductListJob; + this.fetchViolatedProductDetailJob = fetchViolatedProductDetailJob; + this.updateProductFromViolatedProductJob = updateProductFromViolatedProductJob; + this.jobLauncher = jobLauncher; + } + + + /** + * 매주 목요일 새벽 1시 실행 + */ + @Scheduled(cron = "0 0 1 * * THU", zone = "Asia/Seoul") + public void executeFetchViolatedProductListJob() { + executeJob(fetchViolatedProductListJob); + } + + /** + * 매주 목요일 2시, 14시 실행 + */ + @Scheduled(cron = "0 0 2,14 * * THU", zone = "Asia/Seoul") + public void executeFetchViolatedProductDetailJob() { + executeJob(fetchViolatedProductDetailJob); + } + + /** + * 매주 목요일 2시 30분, 14시 30분 실행 + */ + @Scheduled(cron = "0 30 2,14 * * THU", zone = "Asia/Seoul") + public void executeUpdateProductFromViolatedProductJob() { + executeJob(updateProductFromViolatedProductJob); + } + + private synchronized void executeJob(Job job) { + + try { + jobLauncher.run( + job, + new JobParametersBuilder() + .addString("DATETIME", LocalDateTime.now().toString()) + .addString("PRODUCT_ARM_CODE", "07") + .toJobParameters() + ); + } catch (JobExecutionException je) { + log.error("JobExecutionException Occurred", je); + } + } +} diff --git a/module-batch/src/main/java/com/kernel360/modulebatch/violatedproduct/client/ViolatedProductDetailClient.java b/module-batch/src/main/java/com/kernel360/modulebatch/violatedproduct/client/ViolatedProductDetailClient.java new file mode 100644 index 00000000..0b5aeeda --- /dev/null +++ b/module-batch/src/main/java/com/kernel360/modulebatch/violatedproduct/client/ViolatedProductDetailClient.java @@ -0,0 +1,69 @@ +package com.kernel360.modulebatch.violatedproduct.client; + +import com.kernel360.ecolife.entity.ViolatedProductId; +import java.nio.charset.StandardCharsets; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClient; +import org.springframework.web.util.UriComponentsBuilder; + +@Slf4j +@Component +public class ViolatedProductDetailClient { + @Value("${external.ecolife-api.path}") + private String BASE_PATH; + + @Value("${external.ecolife-api.service-key}") + private String AUTH_KEY; + + private final RestClient restClient; + + public ViolatedProductDetailClient() { + this.restClient = RestClient.builder() + .build(); + } + + public String getXmlResponse(ViolatedProductId violatedProductId) { + + return restClient.post().uri(buildUri(violatedProductId)) + .accept(MediaType.APPLICATION_XML) + .acceptCharset(StandardCharsets.UTF_8) + .retrieve() + .onStatus(HttpStatusCode::is4xxClientError, + ((request, response) -> { + log.error("[ERROR] :: 4XX 에러 발생" + + response.getStatusText()); + throw new RuntimeException( + response.getStatusCode() + + response.getHeaders() + .toString()); + })) + .onStatus(HttpStatusCode::is5xxServerError, + (request, response) -> { + log.error("[ERROR] :: 5XX 에러 발생" + + response.getStatusText()); + throw new RuntimeException( + response.getStatusCode() + + response.getHeaders() + .toString()); + }) + .onStatus(HttpStatusCode::is2xxSuccessful, + ((request, response) -> log.info("[INFO] :: api 요청 성공" + + response.getBody()))) + .body(String.class); + } + + public String buildUri(ViolatedProductId violatedProductId) { + + return UriComponentsBuilder.fromHttpUrl(BASE_PATH) + .queryParam("AuthKey", AUTH_KEY) + .queryParam("ServiceName", "violtProductDetail") + .queryParam("prdtMstrNo", violatedProductId.getProductMasterNo()) + .queryParam("prdtarmCd", violatedProductId.getProductArmCode()) + .build() + .toUriString(); + } +} diff --git a/module-batch/src/main/java/com/kernel360/modulebatch/violatedproduct/client/ViolatedProductListClient.java b/module-batch/src/main/java/com/kernel360/modulebatch/violatedproduct/client/ViolatedProductListClient.java new file mode 100644 index 00000000..48aa7209 --- /dev/null +++ b/module-batch/src/main/java/com/kernel360/modulebatch/violatedproduct/client/ViolatedProductListClient.java @@ -0,0 +1,71 @@ +package com.kernel360.modulebatch.violatedproduct.client; + +import java.nio.charset.StandardCharsets; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClient; +import org.springframework.web.util.UriComponentsBuilder; + +@Slf4j +@Component +public class ViolatedProductListClient { + + @Value("${external.ecolife-api.path}") + private String BASE_PATH; + + @Value("${external.ecolife-api.service-key}") + private String AUTH_KEY; + + private final RestClient restClient; + + public ViolatedProductListClient() { + this.restClient = RestClient.builder() + .build(); + } + + public String getXmlResponse(String productArmCode, Integer pageNumber) { + + return restClient.post().uri(buildUri(productArmCode, pageNumber)) + .accept(MediaType.APPLICATION_XML) + .acceptCharset(StandardCharsets.UTF_8) + .retrieve() + .onStatus(HttpStatusCode::is4xxClientError, + ((request, response) -> { + log.error("[ERROR] :: 4XX 에러 발생" + + response.getStatusText()); + throw new RuntimeException( + response.getStatusCode() + + response.getHeaders() + .toString()); + })) + .onStatus(HttpStatusCode::is5xxServerError, + (request, response) -> { + log.error("[ERROR] :: 5XX 에러 발생" + + response.getStatusText()); + throw new RuntimeException( + response.getStatusCode() + + response.getHeaders() + .toString()); + }) + .onStatus(HttpStatusCode::is2xxSuccessful, + ((request, response) -> log.info("[INFO] :: api 요청 성공" + + response.getBody()))) + .body(String.class); + } + + public String buildUri(String productArmCode, Integer pageNumber) { + + return UriComponentsBuilder.fromHttpUrl(BASE_PATH) + .queryParam("AuthKey", AUTH_KEY) + .queryParam("ServiceName", "violtProductList") + .queryParam("prdtarmCd", productArmCode) + .queryParam("PageCount", "20") + .queryParam("PageNum", String.valueOf(pageNumber)) + .build() + .toUriString(); + } + +} diff --git a/module-batch/src/main/java/com/kernel360/modulebatch/violatedproduct/dto/ViolatedProductDetailDto.java b/module-batch/src/main/java/com/kernel360/modulebatch/violatedproduct/dto/ViolatedProductDetailDto.java new file mode 100644 index 00000000..0f8330ba --- /dev/null +++ b/module-batch/src/main/java/com/kernel360/modulebatch/violatedproduct/dto/ViolatedProductDetailDto.java @@ -0,0 +1,41 @@ +package com.kernel360.modulebatch.violatedproduct.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; +import com.kernel360.ecolife.entity.ViolatedProduct; +import com.kernel360.ecolife.entity.ViolatedProductId; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JacksonXmlRootElement(localName = "row") +public record ViolatedProductDetailDto( + @JacksonXmlProperty(localName = "prdt_mstr_no") String productMasterNo, + @JacksonXmlProperty(localName = "prdt_nm") String productName, + @JacksonXmlProperty(localName = "modl_nm") String modelName, + @JacksonXmlProperty(localName = "prdtarm") String productArm, + @JacksonXmlProperty(localName = "prdtarm_cd") String productArmCode, + @JacksonXmlProperty(localName = "prdtarm_cd_nm") String productArmCodeName, + @JacksonXmlProperty(localName = "mnfctur_nm") String companyName, + @JacksonXmlProperty(localName = "mnfctur_addr") String companyAddress, + @JacksonXmlProperty(localName = "violt_cn") String violatedCn, + @JacksonXmlProperty(localName = "origin_instt") String originInstitute, + @JacksonXmlProperty(localName = "prdtn_mnfctur") String productManufactureCountry, + @JacksonXmlProperty(localName = "act_org") String actOrganization, + @JacksonXmlProperty(localName = "action_de") String actionDate, + @JacksonXmlProperty(localName = "prdt_photo_url") String productPhotoUrl, + @JacksonXmlProperty(localName = "file_download_url") String fileDownloadUrl, + @JacksonXmlProperty(localName = "action_cn") String actionCn, + @JacksonXmlProperty(localName = "etc_info") String etcInfo + +) { + + public static ViolatedProduct toEntity(ViolatedProductDetailDto detailDto) { + + return ViolatedProduct.of(new ViolatedProductId(detailDto.productMasterNo, detailDto.productArmCode()), + detailDto.productName, detailDto.modelName, detailDto.productArm, detailDto.productArmCodeName, + detailDto.companyName, detailDto.originInstitute, detailDto.actionDate, detailDto.companyAddress, + detailDto.violatedCn, detailDto.productManufactureCountry, detailDto.actOrganization, + detailDto.productPhotoUrl, + detailDto.fileDownloadUrl, detailDto.actionCn, detailDto.etcInfo); + } +} diff --git a/module-batch/src/main/java/com/kernel360/modulebatch/violatedproduct/dto/ViolatedProductDetailListDto.java b/module-batch/src/main/java/com/kernel360/modulebatch/violatedproduct/dto/ViolatedProductDetailListDto.java new file mode 100644 index 00000000..173c301d --- /dev/null +++ b/module-batch/src/main/java/com/kernel360/modulebatch/violatedproduct/dto/ViolatedProductDetailListDto.java @@ -0,0 +1,20 @@ +package com.kernel360.modulebatch.violatedproduct.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; +import java.util.List; + +@JacksonXmlRootElement(localName = "rows") +@JsonIgnoreProperties(ignoreUnknown = true) +public record ViolatedProductDetailListDto( + @JacksonXmlProperty(localName = "count") int count, + @JacksonXmlProperty(localName = "resultcode") String resultCode, + @JacksonXmlProperty(localName = "pagenum") int pageNum, + @JacksonXmlProperty(localName = "pagesize") int pageSize, + @JacksonXmlProperty(localName = "row") + @JacksonXmlElementWrapper(useWrapping = false) + List violatedProductDetailDtoList +) { +} diff --git a/module-batch/src/main/java/com/kernel360/modulebatch/violatedproduct/dto/ViolatedProductDto.java b/module-batch/src/main/java/com/kernel360/modulebatch/violatedproduct/dto/ViolatedProductDto.java new file mode 100644 index 00000000..0ab7fcbd --- /dev/null +++ b/module-batch/src/main/java/com/kernel360/modulebatch/violatedproduct/dto/ViolatedProductDto.java @@ -0,0 +1,53 @@ +package com.kernel360.modulebatch.violatedproduct.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; +import com.kernel360.ecolife.entity.ViolatedProduct; +import com.kernel360.ecolife.entity.ViolatedProductId; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JacksonXmlRootElement(localName = "row") +public record ViolatedProductDto( + @JacksonXmlProperty(localName = "prdtarm") + String productArm, + + @JacksonXmlProperty(localName = "action_de") + String actionedDate, + + @JacksonXmlProperty(localName = "prdt_nm") + String productName, + + @JacksonXmlProperty(localName = "mnfctur_nm") + String companyName, + + @JacksonXmlProperty(localName = "prdtarm_cd") + String productArmCode, + + @JacksonXmlProperty(localName = "prdtarm_cd_nm") + String productArmCodeName, + + @JacksonXmlProperty(localName = "origin_instt") + String originInstitute, + + @JacksonXmlProperty(localName = "prdt_mstr_no") + String productMasterNo, + + @JacksonXmlProperty(localName = "modl_nm") + String modelName +) { + + public static ViolatedProduct toEntity(ViolatedProductDto dto) { + return ViolatedProduct.of(new ViolatedProductId(dto.productMasterNo, dto.productArmCode), + dto.productName, dto.modelName, dto.productArm, dto.productArmCodeName, dto.companyName, + dto.originInstitute, dto.actionedDate); + } + + public static ViolatedProductDto of(String productArm, String actionedDate, String productName, String companyName, + String productArmCode, String productArmCodeName, String originInstitute, + String productMasterNo, String modelName) { + + return new ViolatedProductDto(productArm, actionedDate, productName, companyName, productArmCode, + productArmCodeName, originInstitute, productMasterNo, modelName); + } +} diff --git a/module-batch/src/main/java/com/kernel360/modulebatch/violatedproduct/dto/ViolatedProductListDto.java b/module-batch/src/main/java/com/kernel360/modulebatch/violatedproduct/dto/ViolatedProductListDto.java new file mode 100644 index 00000000..9b56d354 --- /dev/null +++ b/module-batch/src/main/java/com/kernel360/modulebatch/violatedproduct/dto/ViolatedProductListDto.java @@ -0,0 +1,28 @@ +package com.kernel360.modulebatch.violatedproduct.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; +import java.util.List; + +@JacksonXmlRootElement(localName = "rows") +@JsonIgnoreProperties(ignoreUnknown = true) +public record ViolatedProductListDto( + @JacksonXmlProperty(localName = "count") + int count, + + @JacksonXmlProperty(localName = "resultcode") + String resultCode, + + @JacksonXmlProperty(localName = "pagenum") + int pageNum, + + @JacksonXmlProperty(localName = "pagesize") + int pageSize, + + @JacksonXmlProperty(localName = "row") + @JacksonXmlElementWrapper(useWrapping = false) + List violatedProductDtoList +) { +} diff --git a/module-batch/src/main/java/com/kernel360/modulebatch/violatedproduct/job/core/FetchViolatedProductDetailJobConfig.java b/module-batch/src/main/java/com/kernel360/modulebatch/violatedproduct/job/core/FetchViolatedProductDetailJobConfig.java new file mode 100644 index 00000000..0c0817c5 --- /dev/null +++ b/module-batch/src/main/java/com/kernel360/modulebatch/violatedproduct/job/core/FetchViolatedProductDetailJobConfig.java @@ -0,0 +1,117 @@ +package com.kernel360.modulebatch.violatedproduct.job.core; + +import com.kernel360.ecolife.entity.ViolatedProduct; +import com.kernel360.modulebatch.violatedproduct.job.infra.FetchViolatedProductDetailItemProcessor; +import jakarta.persistence.EntityManagerFactory; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.batch.core.ExitStatus; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobExecutionListener; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.StepExecution; +import org.springframework.batch.core.StepExecutionListener; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.launch.support.RunIdIncrementer; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.batch.item.database.JpaCursorItemReader; +import org.springframework.batch.item.database.JpaItemWriter; +import org.springframework.batch.item.database.builder.JpaCursorItemReaderBuilder; +import org.springframework.batch.item.database.builder.JpaItemWriterBuilder; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.PlatformTransactionManager; + +@Slf4j +@Configuration +@RequiredArgsConstructor +public class FetchViolatedProductDetailJobConfig { + + private final FetchViolatedProductDetailItemProcessor fetchViolatedProductDetailItemProcessor; + + private final EntityManagerFactory emf; + private final int chunkSize = 100; + + @Bean + public Job fetchViolatedProductDetailJob(JobRepository jobRepository, + @Qualifier("fetchViolatedProductDetailStep") Step fetchViolatedProductDetailStep) { + + return new JobBuilder("fetchViolatedProductDetailJob", jobRepository) + .start(fetchViolatedProductDetailStep) + .incrementer(new RunIdIncrementer()) + .listener(new FetchViolatedProductDetailJobListener()) + .build(); + } + + @Bean + public Step fetchViolatedProductDetailStep(JobRepository jobRepository, + PlatformTransactionManager transactionManager) { + + return new StepBuilder("fetchViolatedProductDetailStep", jobRepository) + .chunk(chunkSize, transactionManager) + .reader(fetchViolatedProductDetailJpaCursorItemReader()) + .processor(fetchViolatedProductDetailItemProcessor) + .writer(fetchViolatedProductDetailJpaItemWriter()) + .listener(new FetchViolatedProductDetailStepListener()) + .faultTolerant() + .build(); + } + + @Bean + @StepScope + public JpaCursorItemReader fetchViolatedProductDetailJpaCursorItemReader() { + String jpql = "SELECT vp FROM ViolatedProduct vp WHERE vp.actionCn is null"; + + return new JpaCursorItemReaderBuilder() + .entityManagerFactory(emf) + .name("fetchViolatedProductDetailJpaPagingItemReader") + .queryString(jpql) + .build(); + } + + @Bean + @StepScope + public JpaItemWriter fetchViolatedProductDetailJpaItemWriter() { + + return new JpaItemWriterBuilder() + .entityManagerFactory(emf) + .build(); + } + + //-- Execution Listener --// + public static class FetchViolatedProductDetailJobListener implements JobExecutionListener { + @Override + public void beforeJob(JobExecution jobExecution) { + log.info("{} starts", jobExecution.getJobInstance().getJobName()); + } + + @Override + public void afterJob(JobExecution jobExecution) { + log.info("{} ends", jobExecution.getJobInstance().getJobName()); + } + } + + public static class FetchViolatedProductDetailStepListener implements StepExecutionListener { + @Override + public void beforeStep(StepExecution stepExecution) { + log.info("{} starts", stepExecution.getStepName()); + } + + @Override + public ExitStatus afterStep(StepExecution stepExecution) { + log.info("StepExecutionListener - afterStep, step name: {}, status: {}", stepExecution.getStepName(), + stepExecution.getStatus()); + log.info( + "StepExecutionListener - ReadCount: {}, WriteCount: {}, FilterCount: {}, ReadSkipCount: {}, ProcessSkipCount: {}, WriteSkipCount: {}", + stepExecution.getReadCount(), stepExecution.getWriteCount(), stepExecution.getFilterCount(), + stepExecution.getReadSkipCount(), stepExecution.getProcessSkipCount(), + stepExecution.getWriteSkipCount()); + return StepExecutionListener.super.afterStep(stepExecution); + } + } + +} diff --git a/module-batch/src/main/java/com/kernel360/modulebatch/violatedproduct/job/core/FetchViolatedProductListJobConfig.java b/module-batch/src/main/java/com/kernel360/modulebatch/violatedproduct/job/core/FetchViolatedProductListJobConfig.java new file mode 100644 index 00000000..8b926e0f --- /dev/null +++ b/module-batch/src/main/java/com/kernel360/modulebatch/violatedproduct/job/core/FetchViolatedProductListJobConfig.java @@ -0,0 +1,87 @@ +package com.kernel360.modulebatch.violatedproduct.job.core; + +import com.kernel360.modulebatch.violatedproduct.dto.ViolatedProductDto; +import com.kernel360.modulebatch.violatedproduct.job.infra.FetchViolatedProductListItemReader; +import com.kernel360.modulebatch.violatedproduct.job.infra.FetchViolatedProductListItemWriter; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.batch.core.ExitStatus; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobExecutionListener; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.StepExecution; +import org.springframework.batch.core.StepExecutionListener; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.PlatformTransactionManager; + +@Slf4j +@Configuration +@RequiredArgsConstructor +public class FetchViolatedProductListJobConfig { + + private final FetchViolatedProductListItemReader fetchViolatedProductListItemReader; + + private final FetchViolatedProductListItemWriter fetchViolatedProductListItemWriter; + + @Bean + public Job fetchViolatedProductListJob(JobRepository jobRepository, + @Qualifier("fetchViolatedProductListStep") Step fetchViolatedProductListStep) { + return new JobBuilder("fetchViolatedProductListJob", jobRepository) + .start(fetchViolatedProductListStep) + .listener(new FetchViolatedProductListJobListener()) + .build(); + } + + @Bean + public Step fetchViolatedProductListStep(JobRepository jobRepository, + PlatformTransactionManager transactionManager) { + + return new StepBuilder("fetchViolatedProductListStep", jobRepository) + ., List>chunk(10, transactionManager) + .reader(fetchViolatedProductListItemReader) + .writer(fetchViolatedProductListItemWriter) + .listener(new FetchViolatedProductListStepListener()) + .faultTolerant() + .build(); + } + + //-- Execution Listener --// + public static class FetchViolatedProductListJobListener implements JobExecutionListener { + @Override + public void beforeJob(JobExecution jobExecution) { + log.info("{} starts", jobExecution.getJobInstance().getJobName()); + } + + @Override + public void afterJob(JobExecution jobExecution) { + log.info("{} ends", jobExecution.getJobInstance().getJobName()); + } + } + + public static class FetchViolatedProductListStepListener implements StepExecutionListener { + @Override + public void beforeStep(StepExecution stepExecution) { + log.info("{} starts", stepExecution.getStepName()); + } + + @Override + public ExitStatus afterStep(StepExecution stepExecution) { + log.info("StepExecutionListener - afterStep, step name: {}, status: {}", stepExecution.getStepName(), + stepExecution.getStatus()); + log.info( + "StepExecutionListener - ReadCount: {}, WriteCount: {}, FilterCount: {}, ReadSkipCount: {}, ProcessSkipCount: {}, WriteSkipCount: {}", + stepExecution.getReadCount(), stepExecution.getWriteCount(), stepExecution.getFilterCount(), + stepExecution.getReadSkipCount(), stepExecution.getProcessSkipCount(), + stepExecution.getWriteSkipCount()); + return StepExecutionListener.super.afterStep(stepExecution); + } + } + +} diff --git a/module-batch/src/main/java/com/kernel360/modulebatch/violatedproduct/job/infra/FetchViolatedProductDetailItemProcessor.java b/module-batch/src/main/java/com/kernel360/modulebatch/violatedproduct/job/infra/FetchViolatedProductDetailItemProcessor.java new file mode 100644 index 00000000..daf17b4a --- /dev/null +++ b/module-batch/src/main/java/com/kernel360/modulebatch/violatedproduct/job/infra/FetchViolatedProductDetailItemProcessor.java @@ -0,0 +1,54 @@ +package com.kernel360.modulebatch.violatedproduct.job.infra; + +import com.kernel360.ecolife.entity.ViolatedProduct; +import com.kernel360.modulebatch.violatedproduct.client.ViolatedProductDetailClient; +import com.kernel360.modulebatch.violatedproduct.dto.ViolatedProductDetailDto; +import com.kernel360.modulebatch.violatedproduct.dto.ViolatedProductDto; +import com.kernel360.modulebatch.violatedproduct.service.ViolatedProductService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.batch.item.ItemProcessor; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class FetchViolatedProductDetailItemProcessor implements ItemProcessor { + private final ViolatedProductService service; + private final ViolatedProductDetailClient client; + + @Override + public ViolatedProduct process(ViolatedProduct item) throws Exception { + if (item.getEtcInfo() != null) { + return null; + } + String response = fetchXmlResponse(item); + ViolatedProductDetailDto detailDto = service.deserializeXml2DetailDto(response).violatedProductDetailDtoList() + .get(0); + +// ViolatedProductDto productDto = ViolatedProductDto.of(item.getProductArm(), item.getActionedDate(), +// item.getProductName(), item.getCompanyName(), item.getId().getProductArmCode(), +// item.getProductArmCodeName(), item.getOriginInstitute(), +// item.getId().getProductMasterNo(), item.getModelName() +// ); + + return ViolatedProductDetailDto.toEntity(detailDto); + + } + + + private String fetchXmlResponse(ViolatedProduct violatedProduct) { + log.info("Fetch item Id = {}", violatedProduct); + String xmlResponse = client.getXmlResponse(violatedProduct.getId()); + log.info("Response accepted : {}", xmlResponse); + + return removeInvalidXmlCharacters(xmlResponse); + } + + public String removeInvalidXmlCharacters( + String xmlString) { // XML 1.0 Specification 을 준수하는 ASCII printable characters (REPLACEMENT_CHARACTER) + String pattern = "[^\t\r\n -\uD7FF\uE000-\uFFFD\ud800\udc00-\udbff\udfff]"; + + return xmlString.replaceAll(pattern, ""); + } +} diff --git a/module-batch/src/main/java/com/kernel360/modulebatch/violatedproduct/job/infra/FetchViolatedProductListItemReader.java b/module-batch/src/main/java/com/kernel360/modulebatch/violatedproduct/job/infra/FetchViolatedProductListItemReader.java new file mode 100644 index 00000000..8b06fcf9 --- /dev/null +++ b/module-batch/src/main/java/com/kernel360/modulebatch/violatedproduct/job/infra/FetchViolatedProductListItemReader.java @@ -0,0 +1,61 @@ +package com.kernel360.modulebatch.violatedproduct.job.infra; + +import com.kernel360.modulebatch.violatedproduct.client.ViolatedProductListClient; +import com.kernel360.modulebatch.violatedproduct.dto.ViolatedProductDto; +import com.kernel360.modulebatch.violatedproduct.service.ViolatedProductService; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.batch.core.annotation.BeforeStep; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.item.ItemReader; +import org.springframework.batch.item.NonTransientResourceException; +import org.springframework.batch.item.ParseException; +import org.springframework.batch.item.UnexpectedInputException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Slf4j +@StepScope +@Component +@RequiredArgsConstructor +public class FetchViolatedProductListItemReader implements ItemReader> { + private static int MAX_PAGES_PER_JOB; + + private final ViolatedProductListClient client; + + private final ViolatedProductService service; + + @Value("#{jobParameters['PRODUCT_ARM_CODE']}") + private String productArmCode; + private int maxPageNumber; + private int nextPage = -1; + + @BeforeStep + public void beforeStep() { + MAX_PAGES_PER_JOB = 100; + } + + @Override + public List read() + throws Exception { + nextPage++; + if (nextPage == maxPageNumber + 1 || MAX_PAGES_PER_JOB-- <= 0) { + return null; + } + + String xmlResponse = fetchXmlResponse(); + + maxPageNumber = service.getTotalPageCount(xmlResponse); + + return service.deserializeXml2ListDto(xmlResponse) + .violatedProductDtoList(); + } + + private String fetchXmlResponse() { + log.info("Fetch Page = {}", nextPage); + String xmlResponse = client.getXmlResponse(productArmCode, nextPage); + log.info("Response accepted : {}", xmlResponse); + return xmlResponse; + } +} diff --git a/module-batch/src/main/java/com/kernel360/modulebatch/violatedproduct/job/infra/FetchViolatedProductListItemWriter.java b/module-batch/src/main/java/com/kernel360/modulebatch/violatedproduct/job/infra/FetchViolatedProductListItemWriter.java new file mode 100644 index 00000000..f01fdabd --- /dev/null +++ b/module-batch/src/main/java/com/kernel360/modulebatch/violatedproduct/job/infra/FetchViolatedProductListItemWriter.java @@ -0,0 +1,27 @@ +package com.kernel360.modulebatch.violatedproduct.job.infra; + +import com.kernel360.modulebatch.violatedproduct.dto.ViolatedProductDto; +import com.kernel360.modulebatch.violatedproduct.service.ViolatedProductService; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.item.Chunk; +import org.springframework.batch.item.ItemWriter; +import org.springframework.stereotype.Component; + +@Component +@StepScope +@RequiredArgsConstructor +public class FetchViolatedProductListItemWriter implements ItemWriter> { + + private final ViolatedProductService service; + + @Override + public void write(Chunk extends List> chunk) throws Exception { + for (List list : chunk) { + for (ViolatedProductDto violatedProductDto : list) { + service.saveViolatedProduct(violatedProductDto); + } + } + } +} diff --git a/module-batch/src/main/java/com/kernel360/modulebatch/violatedproduct/service/ViolatedProductService.java b/module-batch/src/main/java/com/kernel360/modulebatch/violatedproduct/service/ViolatedProductService.java new file mode 100644 index 00000000..5936f816 --- /dev/null +++ b/module-batch/src/main/java/com/kernel360/modulebatch/violatedproduct/service/ViolatedProductService.java @@ -0,0 +1,54 @@ +package com.kernel360.modulebatch.violatedproduct.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import com.kernel360.ecolife.entity.ViolatedProduct; +import com.kernel360.ecolife.repository.ViolatedProductRepository; +import com.kernel360.modulebatch.violatedproduct.dto.ViolatedProductDetailDto; +import com.kernel360.modulebatch.violatedproduct.dto.ViolatedProductDetailListDto; +import com.kernel360.modulebatch.violatedproduct.dto.ViolatedProductDto; +import com.kernel360.modulebatch.violatedproduct.dto.ViolatedProductListDto; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.batch.core.JobExecutionException; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@ComponentScan("com.kernel360.ecolife") +@RequiredArgsConstructor +public class ViolatedProductService { + + private final ViolatedProductRepository violatedProductRepository; + + private final XmlMapper xmlMapper = new XmlMapper(); + + public ViolatedProductListDto deserializeXml2ListDto(String xml) throws JsonProcessingException { + return xmlMapper.readValue(xml, ViolatedProductListDto.class); + } + + public ViolatedProductDetailListDto deserializeXml2DetailDto(String xml) throws JsonProcessingException { + return xmlMapper.readValue(xml, ViolatedProductDetailListDto.class); + } + + public int getTotalPageCount(String response) throws JsonProcessingException { + return deserializeXml2ListDto(response).count() / 20 + 1; + } + + @Transactional + public void saveViolatedProduct(ViolatedProductDto dto) throws JobExecutionException { + ViolatedProduct violatedProduct = ViolatedProductDto.toEntity(dto); + Optional foundViolatedProduct = violatedProductRepository.findById(violatedProduct.getId()); + + if (foundViolatedProduct.isEmpty()) { + ViolatedProduct newViolatedProduct = violatedProductRepository.save(violatedProduct); + log.info("Saved entity : {} ", newViolatedProduct.getProductName()); + return; + } + log.info("Existing entity found: {}. Continuing without save", violatedProduct.getProductName()); + + } +} diff --git a/module-batch/src/main/resources/application-dev.yml b/module-batch/src/main/resources/application-dev.yml new file mode 100644 index 00000000..37673f76 --- /dev/null +++ b/module-batch/src/main/resources/application-dev.yml @@ -0,0 +1,47 @@ +spring: + jpa: + hibernate: + ddl-auto: validate + show-sql: true + properties: + hibernate: + generate_statistics: true + jdbc: + batch_size: 100 + format_sql: true + open-in-view: true + + datasource: + driver-class-name: org.postgresql.Driver + url: ${DB_URL} + username: ${DB_USER} + password: ${DB_PW} + hikari: + maximum-pool-size: 100 + idle-timeout: 20000 + validation-timeout: 5000 + minimum-idle: 5 + +external: + ecolife-api: + path: ${ECOLIFE_PATH} + service-key: ${ECOLIFE_KEY} + + +logging: + file: + path: ./logs/batch/ + name: dev-batch-log + logback: + rolling policy: + max-history: 14 + max-file-size: 50MB + total-size-cap: 5GB + level: + root: warn + com.kernel360: debug + org.hibernate.sql: debug + org.hibernate.type.descriptor.sql: trace + org: + springframework: + transaction=DEBUG: \ No newline at end of file diff --git a/module-batch/src/main/resources/logback-spring.xml b/module-batch/src/main/resources/logback-spring.xml index 9e5b2d41..b3549acb 100644 --- a/module-batch/src/main/resources/logback-spring.xml +++ b/module-batch/src/main/resources/logback-spring.xml @@ -34,6 +34,13 @@ + + +
기타 질문사항은 고객센터에 문의 주시길 바랍니다.
아래의 링크를 통해 패스워드를 재설정하세요.
평일 10:00~19:00 (주말 및 공휴일 제외/ 점심시간 13:00~14:00)
FAST CAMPUS
(주)데이원캠퍼니
서울특별시 강남구 테헤란로 231, 센트럴프라자 WEST 6층, 7층