diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 0000000..4e616c9
Binary files /dev/null and b/.DS_Store differ
diff --git a/.ebextensions_dev/00-makeFiles.config b/.ebextensions_dev/00-makeFiles.config
new file mode 100644
index 0000000..8030404
--- /dev/null
+++ b/.ebextensions_dev/00-makeFiles.config
@@ -0,0 +1,12 @@
+files:
+ "/sbin/appstart":
+ mode: "000755"
+ owner: webapp
+ group: webapp
+ content: |
+ #!/usr/bin/env bash
+ JAR_PATH=/var/app/current/application.jar
+
+ # run app
+ killalljava
+ java -Dfile.encoding=UTF-8 -jar $JAR_PATH
\ No newline at end of file
diff --git a/.ebextensions_dev/01-set-timezone.config b/.ebextensions_dev/01-set-timezone.config
new file mode 100644
index 0000000..869275c
--- /dev/null
+++ b/.ebextensions_dev/01-set-timezone.config
@@ -0,0 +1,3 @@
+commands:
+ set_time_zone:
+ command: ln -f -s /usr/share/zoneinfo/Asia/Seoul /etc/localtime
\ No newline at end of file
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..8af972c
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,3 @@
+/gradlew text eol=lf
+*.bat text eol=crlf
+*.jar binary
diff --git a/.github/ISSUE_TEMPLATE/issue-template.md b/.github/ISSUE_TEMPLATE/issue-template.md
new file mode 100644
index 0000000..d5a0378
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/issue-template.md
@@ -0,0 +1,22 @@
+---
+name: Issue Template
+about: 이슈 템플릿
+title: ''
+labels: "✨ Feat"
+assignees: ''
+
+---
+
+## 📌 만들고자 하는 기능
+
+(여기에 기능 설명을 작성하세요)
+
+## ✅ 구현 내용
+
+- [ ] (구체적인 구현 항목을 작성하세요)
+- [ ] (구체적인 구현 항목을 작성하세요)
+- [ ] (구체적인 구현 항목을 작성하세요)
+
+## ⏰ 예상 기간
+
+0월 00일 ~ 0월 00일
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..c5d6b0d
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,20 @@
+## 💡 관련 이슈
+
+(관련있는 이슈 번호를 적어주세요.)
+
+## 📢 작업 내용
+
+- (작업한 내용 작성)
+- (작업한 내용 작성)
+
+## 🗨️ 리뷰 요구사항(선택)
+
+- (리뷰 요구사항 작성)
+
+## ✅ 체크리스트
+
+- [ ] 코드가 정상적으로 컴파일되나요?
+- [ ] 이슈 내용을 전부 구현했나요?
+- [ ] 작업 기간 내에 개발을 완료했나요?
+- [ ] 리뷰어를 선택했나요?
+- [ ] 라벨을 지정했나요?
diff --git a/.github/workflows/dev_deploy.yml b/.github/workflows/dev_deploy.yml
new file mode 100644
index 0000000..1ae41f3
--- /dev/null
+++ b/.github/workflows/dev_deploy.yml
@@ -0,0 +1,108 @@
+name: PerfumeonMe Dev CI/CD
+
+on:
+ push:
+ branches: [ "develop" ]
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+
+ - name: Set up JDK 21
+ uses: actions/setup-java@v3
+ with:
+ java-version: '21'
+ distribution: 'adopt'
+
+ - name: gradlew mod modify
+ run: chmod +x gradlew
+
+ # gradle 캐싱 (0) - 주석처리할수도있음
+ - name: Gradle Caching
+ uses: actions/cache@v3
+ with:
+ path: |
+ ~/.gradle/caches
+ ~/.gradle/wrapper
+ key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
+ restore-keys: |
+ ${{ runner.os }}-gradle-
+
+ - name: Spring Boot Build
+ run: ./gradlew clean build --exclude-task test
+
+ - name: Docker Image Build
+ run: docker build -t chanee29/perfumeonme .
+
+ - name: docker login
+ uses: docker/login-action@v2
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+
+ - name: docker Hub Push
+ run: docker push chanee29/perfumeonme
+
+ - name: get GitHub IP
+ id: ip
+ uses: haythem/public-ip@v1.2
+
+ - name: Configure AWS Credentials
+ uses: aws-actions/configure-aws-credentials@v1
+ with:
+ aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
+ aws-secret-access-key: ${{ secrets.AWS_ACCESS_KEY_PASSWORD }}
+ aws-region: ap-northeast-1
+
+ - name: Add GitHub IP to AWS
+ run: |
+ aws ec2 authorize-security-group-ingress --group-id ${{ secrets.AWS_SG_ID }} --protocol tcp --port 22 --cidr ${{ steps.ip.outputs.ipv4 }}/32
+
+ - name: AWS EC2 Connection & Deploy Spring (bind to loopback)
+ uses: appleboy/ssh-action@v0.1.6
+ with:
+ host: ${{ secrets.EC2_HOST }}
+ username: ubuntu
+ key: ${{ secrets.EC2_SSH_PRIVATE_KEY }}
+ port: ${{ secrets.EC2_SSH_PORT }}
+ timeout: 500s
+ script: |
+ set -e
+
+ sudo docker network create perfume-network || true
+
+ sudo docker stop perfumeonme || true
+ sudo docker rm perfumeonme || true
+ sudo docker rmi chanee29/perfumeonme || true
+ sudo docker pull chanee29/perfumeonme
+
+ echo "🚀 Starting new container with the following environment variables:"
+
+ sudo docker run -d --name perfumeonme \
+ --network perfume-network \
+ -p 127.0.0.1:8080:8080 \
+ -e SPRING_PROFILES_ACTIVE=dev \
+ -e DB_URL=${{ secrets.ENV_DB_URL }} \
+ -e DB_USERNAME=${{ secrets.ENV_DB_USERNAME }} \
+ -e DB_PASSWORD=${{ secrets.ENV_DB_PASSWORD }} \
+ -e SPRING_DATA_REDIS_HOST=${{ secrets.ENV_REDIS_HOST }} \
+ -e SPRING_DATA_REDIS_PORT=${{ secrets.ENV_REDIS_PORT }} \
+ -e JWT_SECRET=${{ secrets.ENV_JWT_SECRET }} \
+ -e OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }} \
+ -e KAKAO_REST_API_KEY=${{ secrets.KAKAO_REST_API_KEY }} \
+ -e KAKAO_CLIENT_SECRET=${{ secrets.KAKAO_CLIENT_SECRET }} \
+ -e AWS_S3_ACCESS_KEY_ID=${{ secrets.AWS_S3_ACCESS_KEY_ID }} \
+ -e AWS_S3_SECRET_ACCESS_KEY=${{ secrets.AWS_S3_SECRET_ACCESS_KEY }} \
+ -e AWS_S3_BUCKET_NAME=${{ secrets.AWS_S3_BUCKET_NAME }} \
+ -e EXTERNAL_FASTAPI_RECOMMEND_URL=${{ secrets.FASTAPI_RECOMMEND_URL }} \
+ -e EXTERNAL_FASTAPI_PBTI_URL=${{ secrets.FASTAPI_PBTI_URL }} \
+ -e IMAGE_KEYWORD_CHARACTER_BASE_PATH=${{ secrets.IMAGE_KEYWORD_CHARACTER_BASE_PATH }} \
+ chanee29/perfumeonme
+
+
+ - name: Remove GitHub IP FROM security group
+ run: |
+ aws ec2 revoke-security-group-ingress --group-id ${{ secrets.AWS_SG_ID }} --protocol tcp --port 22 --cidr ${{ steps.ip.outputs.ipv4 }}/32
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..2c46216
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,41 @@
+HELP.md
+.gradle
+build/
+!gradle/wrapper/gradle-wrapper.jar
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+bin/
+!**/src/main/**/bin/
+!**/src/test/**/bin/
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+out/
+!**/src/main/**/out/
+!**/src/test/**/out/
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+
+### VS Code ###
+.vscode/
+
+### application-local.yml ###
+src/main/resources/application-local.yml
+src/main/resources/application-secret.yml
\ No newline at end of file
diff --git a/.platform/conf.d/client_max_body_size.conf b/.platform/conf.d/client_max_body_size.conf
new file mode 100644
index 0000000..8e8277e
--- /dev/null
+++ b/.platform/conf.d/client_max_body_size.conf
@@ -0,0 +1 @@
+client_max_body_size 200M;
\ No newline at end of file
diff --git a/.platform/nginx.conf b/.platform/nginx.conf
new file mode 100644
index 0000000..612092e
--- /dev/null
+++ b/.platform/nginx.conf
@@ -0,0 +1,63 @@
+user nginx;
+error_log /var/log/nginx/error.log warn;
+pid /var/run/nginx.pid;
+worker_processes auto;
+worker_rlimit_nofile 33282;
+
+events {
+ use epoll;
+ worker_connections 1024;
+ multi_accept on;
+}
+
+http {
+ include /etc/nginx/mime.types;
+ default_type application/octet-stream;
+
+
+ log_format main '$remote_addr - $remote_user [$time_local] "$request" '
+ '$status $body_bytes_sent "$http_referer" '
+ '"$http_user_agent" "$http_x_forwarded_for"';
+
+ include conf.d/*.conf;
+
+ map $http_upgrade $connection_upgrade {
+ default "upgrade";
+ }
+
+ upstream springboot {
+ server 127.0.0.1:8080;
+ keepalive 1024;
+ }
+
+ server {
+ listen 80 default_server;
+ listen [::]:80 default_server;
+
+ location / {
+ proxy_pass http://springboot;
+ # CORS 관련 헤더 추가
+ add_header 'Access-Control-Allow-Origin' '*';
+ add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
+ add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type';
+ proxy_http_version 1.1;
+ proxy_set_header Connection $connection_upgrade;
+ proxy_set_header Upgrade $http_upgrade;
+
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ }
+
+ access_log /var/log/nginx/access.log main;
+
+ client_header_timeout 60;
+ client_body_timeout 60;
+ keepalive_timeout 60;
+ gzip off;
+ gzip_comp_level 4;
+
+ # Include the Elastic Beanstalk generated locations
+ include conf.d/elasticbeanstalk/healthd.conf;
+ }
+}
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..7cf97db
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,26 @@
+# 1단계: Gradle을 이용한 빌드용 이미지
+FROM gradle:8.4.0-jdk21 AS builder
+
+# 작업 디렉토리 설정
+WORKDIR /app
+
+# 전체 프로젝트 복사
+COPY . .
+
+# 종속성 캐시 및 빌드
+RUN gradle clean build -x test
+
+# ------------------------------------------------------
+
+# 2단계: 실제 애플리케이션 실행용 이미지
+FROM eclipse-temurin:21-jdk
+
+# JAR 복사 (빌드된 JAR 경로)
+ARG JAR_FILE=build/libs/*.jar
+COPY --from=builder /app/${JAR_FILE} app.jar
+
+# 8080 포트 오픈
+EXPOSE 8080
+
+# 앱 실행 명령어
+ENTRYPOINT ["java", "-jar", "app.jar"]
\ No newline at end of file
diff --git a/Procfile b/Procfile
new file mode 100644
index 0000000..58dab8d
--- /dev/null
+++ b/Procfile
@@ -0,0 +1 @@
+web: appstart
\ No newline at end of file
diff --git a/README.md b/README.md
index cab7da1..f28eddc 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,282 @@
-# Server
-PerFumeOnMe/Server(Spring Boot)
+
+
+# 🌸 퍼퓨온미 (Perfume On Me)
+
+**향수를 더 쉽고 즐겁게, 경험하다**
+향기와 경험을 담아내는 새로운 방식의 **향수 추천·경험 플랫폼**
+
+

+
+
+
+---
+
+## 📌 프로젝트 소개
+
+퍼퓨온미는 사용자가 자신에게 어울리는 향수를 쉽고 재미있게 찾을 수 있도록 돕는 향수 추천·경험 플랫폼입니다.
+GPT 기반 분석, 키워드 검색, 설문 등 다양한 방법을 통해 사용자의 취향을 파악하고,
+성격·기분·스타일에 맞춘 개인 맞춤형 향수 추천을 제공합니다.
+이를 통해 단순한 제품 구매를 넘어, 향수를 통해 추억과 감정을 담아내는 새로운 경험을 제안합니다.
+
+---
+
+## 🌱 프로젝트 배경
+
+수천 가지 향수가 존재하지만, 대부분의 사람들은 어떤 향이 자신에게 어울릴지 몰라 선택에 어려움을 겪습니다.
+또한 향에 대한 취향은 언어로 설명하기 어려워 기존의 검색·추천 방식에는 한계가 있습니다.
+퍼퓨온미는 이러한 문제를 해결하고자, 다양한 접근 방식과 개인화 추천을 결합한 플랫폼을 만들었습니다.
+향수를 비싸고 어려운 액세서리가 아닌, 누구나 즐길 수 있는 일상의 취미로 바꾸는 것이 우리의 목표입니다.
+
+---
+
+## 🔗 배포 주소
+
+> [🌐 퍼퓨온미 바로가기](https://perfumeonme.vercel.app)
+
+---
+
+## ✨ 주요 기능
+
+- 💡 **취향 맞춤 추천** : 취향 기반 개인 맞춤 향수 추천
+- 📚 **향수 아카이브** : 성별, 상황, 계절, 가격, 노트별 등 검색 및 필터
+- 🧾 **시향 기록** : 향에 대한 개인 다이어리 기록
+- 📱 **추천 컨텐츠** : 이미지 기반, 온라인 공방, PBTI 등 다양한 경로의 추천
+
+---
+
+## 🎥 데모 & 미리보기
+
+| 메인 화면 | 향수 상세 | 추천 화면 |
+|-------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------|
+|
|
|
|
+
+
+
+---
+
+---
+
+## 🌿 브랜치 전략
+
+본 프로젝트는 **Git Flow** 브랜치 전략을 기반으로 운영됩니다.
+
+- `main` : 실제 배포 버전이 반영되는 브랜치
+- `develop` : 개발이 진행되는 메인 브랜치
+- `feature` : 기능 단위 개발 브랜치
+- `fix` : 기능 단위 수정 브랜치
+- `refactor` : 코드 리팩터링 브랜치
+- `ci-cd` : CI/CD 브랜치
+- `style` : 기능에 영향을 주지 않는 수정 브랜치
+- `hotfix` : 배포 중 긴급 수정 브랜치
+
+> 모든 PR은 `develop` 브랜치로 머지되며, 코드 리뷰 후 승인 절차를 거칩니다.
+
+---
+
+## 🛠 기술 스택
+
+**Backend**
+
+- `Java`: 21
+- `JDK`: 21.0.2
+- `Build`: Gradle 8.14.2
+- `IDE`: IntelliJ IDEA 2024.1
+- `Framework`: Spring Boot 3.5.3, FastAPI
+- `Database`: MySQL (AWS RDS), Redis, AWS S3
+- `ORM`: Spring Data JPA
+- `CI/CD`: Github Actions (CI/CD) + Docker
+
+**협업 도구**
+
+- `Git/GitHub`
+- `Notion`
+- `Figma`
+- `Slack`
+- `Discord`
+
+---
+
+## 🧭 서버 아키텍처 다이어그램
+
+
+
+---
+
+## 📂 프로젝트 구조
+
+```
+└── 📁src
+ └── 📁main
+ └── 📁java
+ └── 📁PerfumeOnMe
+ └── 📁spring
+ └── 📁apiPayload
+ └── 📁code
+ └── 📁status
+ └── 📁exception
+ └── 📁chatbot
+ └── 📁converter
+ └── 📁domain
+ └── 📁repository
+ └── 📁service
+ └── 📁web
+ └── 📁controller
+ └── 📁dto
+ └── 📁common
+ └── 📁base
+ └── 📁config
+ └── 📁properties
+ └── 📁controller
+ └── 📁enums
+ └── 📁fragranceInit
+ └── 📁util
+ └── 📁validation
+ └── 📁annotation
+ └── 📁validator
+ └── 📁diary
+ └── 📁converter
+ └── 📁domain
+ └── 📁repository
+ └── 📁service
+ └── 📁web
+ └── 📁controller
+ └── 📁dto
+ └── 📁external
+ └── 📁fastapi
+ └── 📁dto
+ └── 📁openai
+ └── 📁fragrance
+ └── 📁converter
+ └── 📁domain
+ └── 📁mapping
+ └── 📁repository
+ └── 📁fragranceBaseNote
+ └── 📁fragranceLocation
+ └── 📁fragranceMiddleNote
+ └── 📁fragrancePrice
+ └── 📁fragranceSeason
+ └── 📁fragranceTopNote
+ └── 📁location
+ └── 📁note
+ └── 📁price
+ └── 📁season
+ └── 📁service
+ └── 📁validation
+ └── 📁annotation
+ └── 📁validator
+ └── 📁web
+ └── 📁controller
+ └── 📁dto
+ └── 📁imagekeyword
+ └── 📁converter
+ └── 📁domain
+ └── 📁redis
+ └── 📁repository
+ └── 📁imagekeyworddescription
+ └── 📁service
+ └── 📁util
+ └── 📁validation
+ └── 📁annotation
+ └── 📁validator
+ └── 📁web
+ └── 📁controller
+ └── 📁docs
+ └── 📁dto
+ └── 📁pbti
+ └── 📁converter
+ └── 📁domain
+ └── 📁repository
+ └── 📁service
+ └── 📁web
+ └── 📁controller
+ └── 📁dto
+ └── 📁s3file
+ └── 📁aws
+ └── 📁converter
+ └── 📁web
+ └── 📁controller
+ └── 📁dto
+ └── 📁security
+ └── 📁auth
+ └── 📁controller
+ └── 📁converter
+ └── 📁dto
+ └── 📁filter
+ └── 📁handler
+ └── 📁manager
+ └── 📁provider
+ └── 📁service
+ └── 📁token
+ └── 📁userDetails
+ └── 📁oauth
+ └── 📁controller
+ └── 📁converter
+ └── 📁dto
+ └── 📁service
+ └── 📁util
+ └── 📁user
+ └── 📁converter
+ └── 📁domain
+ └── 📁mapping
+ └── 📁repository
+ └── 📁userFragrance
+ └── 📁userNote
+ └── 📁service
+ └── 📁validation
+ └── 📁annotation
+ └── 📁validator
+ └── 📁web
+ └── 📁controller
+ └── 📁dto
+ └── 📁uuid
+ └── 📁domain
+ └── 📁repository
+ └── 📁workshop
+ └── 📁converter
+ └── 📁domain
+ └── 📁redis
+ └── 📁repository
+ └── 📁service
+ └── 📁validation
+ └── 📁annotation
+ └── 📁validator
+ └── 📁web
+ └── 📁controller
+ └── 📁docs
+ └── 📁dto
+ └── 📁resources
+ └── 📁data
+ └── 📁prompts
+ └── 📁test
+ └── 📁java
+ └── 📁PerfumeOnMe
+ └── 📁spring
+```
+
+## 📅 Roadmap
+
+- [ ] 향수 데이터 추가
+- [ ] 퍼퓸다이어리 공유 기능
+- [ ] 마이페이지 일부 기능 추가
+- [ ] 향수 추천 알고리즘 고도화
+- [ ] 모바일 앱 버전 출시
+
+---
+
+## 👥 팀원 정보
+
+| 이름 | 역할 | GitHub |
+|-----|---------|------------------------------------------------------|
+| 김은지 | Backend | [@hcg0127](https://github.com/hcg0127) |
+| 김찬우 | Backend | [@chanudevelop](https://github.com/chanudevelop) |
+| 이병웅 | Backend | [@bulee5328](https://github.com/bulee5328) |
+| 이원희 | Backend | [@leewonhee-3054](https://github.com/leewonhee-3054) |
+
+---
+
+## 📬 연락처
+
+인스타그램: perfu_on_me
+
+---
+
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..292a593
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,97 @@
+plugins {
+ id 'java'
+ id 'org.springframework.boot' version '3.5.3'
+ id 'io.spring.dependency-management' version '1.1.7'
+}
+
+group = 'PerfumeOnMe'
+version = '0.0.1-SNAPSHOT'
+
+java {
+ toolchain {
+ languageVersion = JavaLanguageVersion.of(21)
+ }
+}
+
+configurations {
+ compileOnly {
+ extendsFrom annotationProcessor
+ }
+}
+
+repositories {
+ mavenCentral()
+}
+
+dependencies {
+ implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
+ implementation 'org.springframework.boot:spring-boot-starter-web'
+ implementation 'org.springframework.boot:spring-boot-starter-validation'
+
+ // Lombok
+ compileOnly 'org.projectlombok:lombok'
+ annotationProcessor 'org.projectlombok:lombok'
+
+ // MySQL Driver
+ runtimeOnly 'com.mysql:mysql-connector-j'
+
+ // ✅ QueryDSL (5.1.0)
+ implementation 'com.querydsl:querydsl-jpa:5.1.0:jakarta'
+ annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
+ annotationProcessor "jakarta.annotation:jakarta.annotation-api"
+ annotationProcessor "jakarta.persistence:jakarta.persistence-api"
+
+ // 테스트 관련 의존성
+ testImplementation 'org.springframework.boot:spring-boot-starter-test'
+ testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
+
+ // Swagger
+ implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0'
+ // Validation
+ implementation 'org.springframework.boot:spring-boot-starter-validation'
+ // Security
+ implementation 'org.springframework.boot:spring-boot-starter-security'
+ testImplementation 'org.springframework.security:spring-security-test'
+ // Thymeleaf
+ implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
+ implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6:3.1.1.RELEASE'
+ // JWT
+ implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
+ runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
+ runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
+ // Redis
+ implementation 'org.springframework.boot:spring-boot-starter-data-redis'
+ implementation 'org.apache.commons:commons-pool2' // 커넥션 풀링
+ // OAuth
+ implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
+ // Query Parameter Log
+ implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.0'
+ // Redis
+ implementation 'org.springframework.boot:spring-boot-starter-data-redis'
+ implementation 'org.apache.commons:commons-pool2' // 커넥션 풀링
+ implementation 'com.fasterxml.jackson.core:jackson-databind'
+ // OAuth
+ implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
+ // Query Parameter Log
+ implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.0'
+ // Apache POI
+ implementation 'org.apache.poi:poi-ooxml:5.2.3'
+ // WebClient
+ implementation 'org.springframework.boot:spring-boot-starter-webflux'
+ // OAuth2.0
+ implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
+ //S3
+ implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
+}
+
+tasks.named('test') {
+ useJUnitPlatform()
+}
+
+clean {
+ delete file('src/main/generated')
+}
+
+jar {
+ enabled = false
+}
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..1b33c55
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..ff23a68
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..23d15a9
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,251 @@
+#!/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.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+##############################################################################
+#
+# 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/platforms/jvm/plugins-application/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 -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || 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="\\\"\\\""
+
+
+# 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, 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" \
+ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
+ "$@"
+
+# 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/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..db3a6ac
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,94 @@
+@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
+@rem SPDX-License-Identifier: Apache-2.0
+@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. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
+
+: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/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..ebf1ef8
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'spring'
diff --git a/src/.DS_Store b/src/.DS_Store
new file mode 100644
index 0000000..fc18281
Binary files /dev/null and b/src/.DS_Store differ
diff --git a/src/main/.DS_Store b/src/main/.DS_Store
new file mode 100644
index 0000000..fe4ae2f
Binary files /dev/null and b/src/main/.DS_Store differ
diff --git a/src/main/java/.DS_Store b/src/main/java/.DS_Store
new file mode 100644
index 0000000..525eba5
Binary files /dev/null and b/src/main/java/.DS_Store differ
diff --git a/src/main/java/PerfumeOnMe/.DS_Store b/src/main/java/PerfumeOnMe/.DS_Store
new file mode 100644
index 0000000..45121ee
Binary files /dev/null and b/src/main/java/PerfumeOnMe/.DS_Store differ
diff --git a/src/main/java/PerfumeOnMe/spring/Application.java b/src/main/java/PerfumeOnMe/spring/Application.java
new file mode 100644
index 0000000..7d5ece8
--- /dev/null
+++ b/src/main/java/PerfumeOnMe/spring/Application.java
@@ -0,0 +1,17 @@
+package PerfumeOnMe.spring;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+
+@SpringBootApplication
+@EnableJpaAuditing
+@EnableJpaRepositories
+public class Application {
+
+ public static void main(String[] args) {
+ SpringApplication.run(Application.class, args);
+ }
+
+}
diff --git a/src/main/java/PerfumeOnMe/spring/apiPayload/ApiResponse.java b/src/main/java/PerfumeOnMe/spring/apiPayload/ApiResponse.java
new file mode 100644
index 0000000..52f7af2
--- /dev/null
+++ b/src/main/java/PerfumeOnMe/spring/apiPayload/ApiResponse.java
@@ -0,0 +1,60 @@
+package PerfumeOnMe.spring.apiPayload;
+
+import java.io.IOException;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import PerfumeOnMe.spring.apiPayload.code.BaseCode;
+import PerfumeOnMe.spring.apiPayload.code.status.ErrorStatus;
+import PerfumeOnMe.spring.apiPayload.code.status.SuccessStatus;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+@JsonPropertyOrder({"isSuccess", "code", "message", "result"})
+public class ApiResponse {
+
+ private static final ObjectMapper mapper = new ObjectMapper();
+ @JsonProperty("isSuccess")
+ private final Boolean isSuccess;
+ private final String code;
+ private final String message;
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ private T result;
+
+ // 200 OK
+ public static ApiResponse onSuccess(T result) {
+ return new ApiResponse<>(true, SuccessStatus._OK.getCode(), SuccessStatus._OK.getMessage(), result);
+ }
+
+ // 201 CREATED, ...
+ public static ApiResponse of(BaseCode code, T result) {
+ return new ApiResponse<>(true, code.getReasonHttpStatus().getCode(), code.getReasonHttpStatus().getMessage(),
+ result);
+ }
+
+ // 400 CLIENT, 500 SERVER 등 실패한 경우 응답 생성
+ public static ApiResponse onFailure(String code, String message, T data) {
+ return new ApiResponse<>(false, code, message, data);
+ }
+
+ // Security Filter 레벨에서 사용하는 ErrorResponse 생성 메서드
+ public static void setErrorResponse(HttpServletResponse response,
+ ErrorStatus code, Throwable e) throws IOException {
+
+ // 응답 헤더 작성
+ response.setCharacterEncoding("UTF-8");
+ response.setContentType("application/json; charset=UTF-8");
+ response.setStatus(code.getReasonHttpStatus().getHttpStatus().value());
+
+ // 응답 데이터 생성 및 작성
+ ApiResponse res = ApiResponse
+ .onFailure(code.getCode(), code.getMessage(), null);
+ response.getWriter().write(mapper.writeValueAsString(res));
+ }
+}
diff --git a/src/main/java/PerfumeOnMe/spring/apiPayload/code/BaseCode.java b/src/main/java/PerfumeOnMe/spring/apiPayload/code/BaseCode.java
new file mode 100644
index 0000000..cb169dd
--- /dev/null
+++ b/src/main/java/PerfumeOnMe/spring/apiPayload/code/BaseCode.java
@@ -0,0 +1,8 @@
+package PerfumeOnMe.spring.apiPayload.code;
+
+public interface BaseCode {
+
+ ReasonDTO getReason();
+
+ ReasonDTO getReasonHttpStatus();
+}
diff --git a/src/main/java/PerfumeOnMe/spring/apiPayload/code/BaseErrorCode.java b/src/main/java/PerfumeOnMe/spring/apiPayload/code/BaseErrorCode.java
new file mode 100644
index 0000000..9acb016
--- /dev/null
+++ b/src/main/java/PerfumeOnMe/spring/apiPayload/code/BaseErrorCode.java
@@ -0,0 +1,8 @@
+package PerfumeOnMe.spring.apiPayload.code;
+
+public interface BaseErrorCode {
+
+ ErrorReasonDTO getReason();
+
+ ErrorReasonDTO getReasonHttpStatus();
+}
diff --git a/src/main/java/PerfumeOnMe/spring/apiPayload/code/ErrorReasonDTO.java b/src/main/java/PerfumeOnMe/spring/apiPayload/code/ErrorReasonDTO.java
new file mode 100644
index 0000000..fc26836
--- /dev/null
+++ b/src/main/java/PerfumeOnMe/spring/apiPayload/code/ErrorReasonDTO.java
@@ -0,0 +1,18 @@
+package PerfumeOnMe.spring.apiPayload.code;
+
+import lombok.Builder;
+import lombok.Getter;
+import org.springframework.http.HttpStatus;
+
+@Getter
+@Builder
+public class ErrorReasonDTO {
+
+ private HttpStatus httpStatus;
+
+ private final boolean isSuccess;
+ private final String code;
+ private final String message;
+
+ public boolean getIsSuccess(){return isSuccess;}
+}
diff --git a/src/main/java/PerfumeOnMe/spring/apiPayload/code/ReasonDTO.java b/src/main/java/PerfumeOnMe/spring/apiPayload/code/ReasonDTO.java
new file mode 100644
index 0000000..e4c9f20
--- /dev/null
+++ b/src/main/java/PerfumeOnMe/spring/apiPayload/code/ReasonDTO.java
@@ -0,0 +1,18 @@
+package PerfumeOnMe.spring.apiPayload.code;
+
+import lombok.Builder;
+import lombok.Getter;
+import org.springframework.http.HttpStatus;
+
+@Getter
+@Builder
+public class ReasonDTO {
+
+ private HttpStatus httpStatus;
+
+ private final boolean isSuccess;
+ private final String code;
+ private final String message;
+
+ public boolean getIsSuccess(){return isSuccess;}
+}
diff --git a/src/main/java/PerfumeOnMe/spring/apiPayload/code/status/ErrorStatus.java b/src/main/java/PerfumeOnMe/spring/apiPayload/code/status/ErrorStatus.java
new file mode 100644
index 0000000..64e1ee0
--- /dev/null
+++ b/src/main/java/PerfumeOnMe/spring/apiPayload/code/status/ErrorStatus.java
@@ -0,0 +1,136 @@
+package PerfumeOnMe.spring.apiPayload.code.status;
+
+import org.springframework.http.HttpStatus;
+
+import PerfumeOnMe.spring.apiPayload.code.BaseErrorCode;
+import PerfumeOnMe.spring.apiPayload.code.ErrorReasonDTO;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+public enum ErrorStatus implements BaseErrorCode {
+
+ //일반적인 에러
+ _INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON500", "서버 에러, 관리자에게 문의 바랍니다."),
+ _BAD_REQUEST(HttpStatus.BAD_REQUEST, "COMMON400", "잘못된 요청입니다."),
+ _UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "COMMON401", "인증이 필요합니다."),
+ _FORBIDDEN(HttpStatus.FORBIDDEN, "COMMON403", "금지된 요청입니다."),
+
+ // 사용자 에러
+ LOGIN_ID_DUPLICATE(HttpStatus.BAD_REQUEST, "MEMBER4001", "이미 사용된 아이디입니다."),
+ PASSWORD_NOT_MATCH(HttpStatus.BAD_REQUEST, "MEMBER4002", "비밀번호가 일치하지 않습니다."),
+ LOGIN_ID_NOT_FOUND(HttpStatus.BAD_REQUEST, "MEMBER4003", "해당 로그인 아이디를 가진 사용자가 존재하지 않습니다."),
+ LOGIN_PARSING_FAIL(HttpStatus.BAD_REQUEST, "MEMBER4004", "로그인 DTO 변환을 실패했습니다."),
+ LOGIN_UNKNOWN_ERROR(HttpStatus.BAD_REQUEST, "MEMBER4005", "로그인 중 알 수 없는 오류가 발생했습니다."),
+ NICKNAME_DUPLICATE(HttpStatus.BAD_REQUEST, "MEMBER4006", "이미 사용된 닉네임입니다."),
+ USER_ID_NULL(HttpStatus.UNAUTHORIZED, "MEMBER4007", "유저 정보가 존재 하지 않습니다. "),
+
+ // 토큰 에러
+ INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "TOKEN4001", "유효하지 않은 토큰입니다."),
+ REFRESH_TOKEN_NOT_FOUND(HttpStatus.UNAUTHORIZED, "TOKEN4002", "리프레시 토큰을 입력해야 합니다."),
+ EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, "TOKEN4003", "만료된 토큰입니다."),
+ LOGOUT_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED, "TOKEN4004", "로그아웃한 액세스 토큰입니다."),
+ MALFORMED_TOKEN(HttpStatus.UNAUTHORIZED, "TOKEN4005", "토큰 구조가 잘못됐습니다."),
+ UNSUPPORTED_TOKEN(HttpStatus.UNAUTHORIZED, "TOKEN4006", "지원하지 않는 토큰 형식입니다."),
+ INVALID_SIGNATURE(HttpStatus.UNAUTHORIZED, "TOKEN4007", "토큰의 서명이 잘못됐습니다."),
+ TOKEN_NOT_FOUND(HttpStatus.BAD_REQUEST, "TOKEN4008", "토큰이 없습니다."),
+
+ // OAuth 에러
+ UNSUPPORTED_SOCIAL(HttpStatus.BAD_REQUEST, "OAUTH4001", "지원하지 않는 소셜 로그인 방식입니다."),
+
+ // JSON Parsing 에러
+ PARSE_ERROR(HttpStatus.BAD_REQUEST, "OAUTH4002", "파싱 중 오류가 생겼습니다."),
+
+ // 데이터시트 에러
+ UNSUPPORTED_BRAND(HttpStatus.BAD_REQUEST, "DATA4001", "지원하지 않는 브랜드입니다."),
+ UNSUPPORTED_TYPE(HttpStatus.BAD_REQUEST, "DATA4002", "지원하지 않는 향수타입입니다."),
+ PRICE_PARSING_ERROR(HttpStatus.BAD_REQUEST, "DATA4003", "가격 정보를 숫자로 변환할 수 없습니다."),
+
+ // 향수 상세 페이지 에러
+ FRAGRANCE_NOT_FOUND(HttpStatus.BAD_REQUEST, "FRAGRANCE4001", "해당 ID에 해당하는 향수를 찾을 수 없습니다."),
+
+ // 향수 즐겨찾기 에러
+ ALREADY_FAVORITES_ERROR(HttpStatus.BAD_REQUEST, "FAVORITES4001", "이미 즐겨찾기에 등록한 향수입니다."),
+ FAVORITE_NOT_FOUND(HttpStatus.BAD_REQUEST, "FAVORITES4002", "즐겨찾기 목록에 존재하지 않는 향수입니다."),
+
+ // 향수 필터링 에러
+ INVALID_GENDER(HttpStatus.BAD_REQUEST, "FILTER4001", "유효하지 않은 성별입니다."),
+ INVALID_FRAGRANCE_TYPE(HttpStatus.BAD_REQUEST, "FILTER4002", "유효하지 않은 향수 타입입니다."),
+ INVALID_NOTE_ID(HttpStatus.BAD_REQUEST, "FILTER4003", "유효하지 않은 노트 ID 입니다."),
+ INVALID_SEASON_ID(HttpStatus.BAD_REQUEST, "FILTER4004", "유효하지 않은 계절 ID 입니다."),
+ INVALID_SITUATION_ID(HttpStatus.BAD_REQUEST, "FILTER4005", "유효하지 않은 장소 ID 입니다."),
+ INVALID_PRICE_RANGE(HttpStatus.BAD_REQUEST, "FILTER4006", "가격 범위가 올바르지 않습니다."),
+
+ // 챗봇 에러
+ FILE_NOT_FOUND(HttpStatus.BAD_REQUEST, "CHATBOT4001", "프롬프트 파일을 찾을 수 없습니다."),
+ PROMPT_LOADING_FAIL(HttpStatus.BAD_REQUEST, "CHATBOT4002", "프롬프트 로딩에 실패하였습니다."),
+ REQUIRED_MESSAGES(HttpStatus.BAD_REQUEST, "CHATBOT4003", "메세지를 입력하세요."),
+ OPENAI_RATE_LIMIT_EXCEEDED(HttpStatus.TOO_MANY_REQUESTS, "CHATBOT429", "OpenAI API 요청 한도를 초과했습니다. 잠시 후 다시 시도해주세요."),
+
+ // FastAPI 연동 에러
+ FASTAPI_COMMUNICATION_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "FASTAPI5001", "FastAPI 서버 통신 중 오류가 발생했습니다."),
+
+ //JSON 파싱 에러
+ JSON_PARSE_ERROR(HttpStatus.BAD_REQUEST, "JSON4001", "JSON 파싱에 실패했습니다."),
+
+ //
+
+ // 다이어리 에러
+ DIARY_NOT_FOUND(HttpStatus.BAD_REQUEST, "DIARY4001", "해당 다이어리를 찾을 수 없습니다."),
+ USER_DIARY_FORBIDDEN(HttpStatus.BAD_REQUEST, "DIARY4002", "다이어리 소유자의 요청이 아닙니다."),
+ USER_DIARY_NOT_FOUND(HttpStatus.BAD_REQUEST, "DIARY4003", "해당 날짜에 해당하는 다이어리를 찾을 수 없습니다."),
+ MONTH_DIARY_NOT_FOUND(HttpStatus.BAD_REQUEST, "DIARY4004", "해당 월에 작성된 다이어리가 없습니다."),
+
+ // PBTI 에러
+ CALL_WEBCLIENT_ERROR(HttpStatus.BAD_REQUEST, "PBTI4001", "WebClient 호출 과정에서 에러가 발생했습니다."),
+ JSON_PARSING_ERROR(HttpStatus.BAD_REQUEST, "PBTI4002", "GPT 응답 Json 파싱 과정에서 에러가 발생했습니다."),
+ SAVE_REDIS_ERROR(HttpStatus.BAD_REQUEST, "PBTI4003", "Redis 저장 중 직렬화 오류가 발생했습니다."),
+ PBTI_REDIS_KEY_EXPIRED(HttpStatus.BAD_REQUEST, "PBTI4004", "사용자의 PBTI 분석 결과가 Redis에서 만료되었거나 저장되어 있지 않습니다."),
+ PBTI_NOT_EXIST_ERROR(HttpStatus.BAD_REQUEST, "PBTI4005", "존재하지 않는 PBTI 입니다."),
+ PBTI_USER_NOT_MATCH(HttpStatus.BAD_REQUEST, "PBTI4006", "본인의 PBTI 결과만 조회할 수 있습니다."),
+
+ // S3 에러
+ INVALID_IMAGE_EXTENSION(HttpStatus.BAD_REQUEST, "S3IMAGE4001", "지원하지 않은 파일 확장자입니다."),
+
+ // 이미지 키워드 에러
+ EXPIRED_IMAGEKEYWORD_RESULT(HttpStatus.REQUEST_TIMEOUT, "IMAGEKEYWORD4001", "이미지 키워드 미리보기 결과가 만료되었습니다. 다시 시도해주세요."),
+ ALREADY_KEYWORD_NAME(HttpStatus.BAD_REQUEST, "IMAGEKEYWORD4002", "이미 같은 이름으로 저장된 이미지 키워드 결과가 있습니다."),
+ IMAGEKEYWORD_ID_NULL(HttpStatus.BAD_REQUEST, "IMAGEKEYWORD4003", "해당 이미지 키워드 결과 정보가 존재하지 않습니다."),
+ INVALID_IMAGEKEYWORD_ID(HttpStatus.BAD_REQUEST, "IMAGEKEYWORD4004", "해당 이미지 키워드 결과에 접근할 수 없습니다."),
+
+ // 향수공방 에러
+ WORKSHOP_TOTAL_VOLUME_OVERFLOW(HttpStatus.BAD_REQUEST, "WORKSHOP4001", "선택한 노트들의 총 용량은 10 초과할 수 없습니다."),
+ EXPIRED_WORKSHOP_RESULT(HttpStatus.BAD_REQUEST, "WORKSHOP4002", "향수공방 미리보기 결과가 만료되었습니다. 다시 시도해주세요."),
+ WORKSHOP_NAME_DUPLICATE(HttpStatus.BAD_REQUEST, "WORKSHOP4003", "이미 같은 이름으로 저장된 향수공방 결과가 있습니다."),
+ WORKSHOP_USER_NOT_MATCH(HttpStatus.BAD_REQUEST, "WORKSHOP4004", "해당 향수공방 결과에 접근할 수 없습니다."),
+ WORKSHOP_ID_NULL(HttpStatus.UNAUTHORIZED, "WORKSHOP4005", "해당 향수공방 결과 정보가 존재하지 않습니다."),
+
+ // 예시,,,
+ ARTICLE_NOT_FOUND(HttpStatus.NOT_FOUND, "ARTICLE4001", "게시글이 없습니다.");
+
+ private final HttpStatus httpStatus;
+ private final String code;
+ private final String message;
+
+ @Override
+ public ErrorReasonDTO getReason() {
+ return ErrorReasonDTO.builder()
+ .message(message)
+ .code(code)
+ .isSuccess(false)
+ .build();
+ }
+
+ @Override
+ public ErrorReasonDTO getReasonHttpStatus() {
+ return ErrorReasonDTO.builder()
+ .message(message)
+ .code(code)
+ .isSuccess(false)
+ .httpStatus(httpStatus)
+ .build()
+ ;
+ }
+}
+
diff --git a/src/main/java/PerfumeOnMe/spring/apiPayload/code/status/SuccessStatus.java b/src/main/java/PerfumeOnMe/spring/apiPayload/code/status/SuccessStatus.java
new file mode 100644
index 0000000..6668efb
--- /dev/null
+++ b/src/main/java/PerfumeOnMe/spring/apiPayload/code/status/SuccessStatus.java
@@ -0,0 +1,44 @@
+package PerfumeOnMe.spring.apiPayload.code.status;
+
+import org.springframework.http.HttpStatus;
+
+import PerfumeOnMe.spring.apiPayload.code.BaseCode;
+import PerfumeOnMe.spring.apiPayload.code.ReasonDTO;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+public enum SuccessStatus implements BaseCode {
+
+ // 일반적인 응답
+ _OK(HttpStatus.OK, "COMMON200", "성공입니다."),
+ _CREATED(HttpStatus.OK, "COMMON201", "리소스를 성공적으로 생성했습니다."),
+ DIARY_UPDATED(HttpStatus.OK, "DIARY200", "다이어리가 수정되었습니다."),
+ DIARY_DELETED(HttpStatus.OK, "DIARY201", "다이어리가 삭제되었습니다.");
+
+ private final HttpStatus httpStatus;
+ private final String code;
+ private final String message;
+
+ @Override
+ public ReasonDTO getReason() {
+ return ReasonDTO.builder()
+ .message(message)
+ .code(code)
+ .isSuccess(true)
+ .build();
+ }
+
+ @Override
+ public ReasonDTO getReasonHttpStatus() {
+ return ReasonDTO.builder()
+ .message(message)
+ .code(code)
+ .isSuccess(true)
+ .httpStatus(httpStatus)
+ .build()
+ ;
+ }
+}
+
diff --git a/src/main/java/PerfumeOnMe/spring/apiPayload/exception/ExceptionAdvice.java b/src/main/java/PerfumeOnMe/spring/apiPayload/exception/ExceptionAdvice.java
new file mode 100644
index 0000000..e0495ec
--- /dev/null
+++ b/src/main/java/PerfumeOnMe/spring/apiPayload/exception/ExceptionAdvice.java
@@ -0,0 +1,126 @@
+package PerfumeOnMe.spring.apiPayload.exception;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Optional;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.HttpStatusCode;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.context.request.ServletWebRequest;
+import org.springframework.web.context.request.WebRequest;
+import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
+
+import PerfumeOnMe.spring.apiPayload.ApiResponse;
+import PerfumeOnMe.spring.apiPayload.code.ErrorReasonDTO;
+import PerfumeOnMe.spring.apiPayload.code.status.ErrorStatus;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.validation.ConstraintViolationException;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+@RestControllerAdvice(annotations = {RestController.class})
+public class ExceptionAdvice extends ResponseEntityExceptionHandler {
+
+ @ExceptionHandler
+ public ResponseEntity