![]() |
|||||
|---|---|---|---|---|---|
박상현 |
연건창 |
윤지영 |
임광택 |
임서연 |
임채륜 |
- 프로젝트 개요
- 프로젝트 소개
- 프로젝트 배경 및 필요성
- 이벤트 스토밍 기반 DDD 설계 문서
- 요구사항 명세서
- Database 설계
- 화면 설계서
- 시스템 아키텍처
- 기능 수행 테스트 결과
- 빌드 및 배포 문서
- Jenkins CI/CD 테스트 결과
[입기 좋은 날]
위치, 날씨, 상황, 회원의 선호도에 따라 최적의 아웃핏(의상)을 추천해 주고, 사용자들 간의 소통을 지원하는 아웃핏 추천 웹 애플리케이션
"입기 좋은 날" 은 사용자의 위치, 날씨, 그리고 상황 (일상, 여행, 데이트 등)에 맞춘 아웃핏을 추천해 주는 웹 애플리케이션입니다. 웹을 이용하는 회원들에게 맞춤형 아웃핏 추천을 제공하며, 회원 간 소통을 돕는 리뷰 및 게시판 등의 다양한 기능을 포함하고 있습니다.
사용자는 자신의 실시간 위치나 설정한 위치를 바탕으로 날씨 정보를 확인하고, 원하는 시간대에 맞는 날씨 데이터를 얻을 수 있습니다. 그 후, 날씨와 상황에 맞는 아웃핏을 추천 받고, 추천된 아웃핏에 대한 리뷰를 남길 수 있습니다.
아웃핏 추천뿐만 아니라 회원은 리뷰와 게시판을 통해 자신만의 스타일 및 코디를 사진으로 공유하고, 다른 회원들과 소통할 수 있습니다.
최근 몇 년간 우리나라는 예측할 수 없는 기후 변화를 경험하고 있습니다. 무더운 여름과 갑작스러운 한파, 짧아진 봄과 가을 등 날씨가 빠르게 변하고 있습니다. 특히, 지역별 날씨 차이가 커짐에 따라, 동일한 지역 내에서도 날씨가 다른 경우가 빈번해지고 있습니다. 이러한 변화를 반영한 "입기 좋은 날" 은 사용자가 실시간으로 정확한 날씨 정보를 바탕으로 아웃핏을 선택할 수 있게 도와 줍니다.
미세먼지, 자외선, 무더위 등 기상 악화로 인해 우리는 매일 여러 가지 준비물을 챙겨야 합니다. 미세먼지가 심한 날엔 마스크, 자외선이 강한 날엔 선크림, 예상치 못한 비에는 우산 등이 필요합니다. 그러나 사용자는 종종 이런 다양한 요소들을 고려하지 못해 불편함을 겪습니다. "입기 좋은 날"은 이런 기상 상황에 맞춰 의상과 함께 준비물까지 추천하여 사용자들이 외출 시 필요한 모든 요소를 놓치지 않도록 도와 줍니다.
날씨에 대한 사람들의 반응은 개개인마다 다를 수 있습니다.
예를 들어, 30℃ 이상 더운 날에는 대부분의 사람들이 가벼운 차림으로 반팔, 반바지를 선택할 것입니다.
그렇다면 15℃ 또는 20℃는 어떨까요? 해당 기온은 사람에 따라 덥다고 혹은 적당하다고 혹은 춥다고 느낄 것입니다.
15℃는 추울 것이라 생각해서 겉옷을 준비했으나 더워서 짐만 되거나, 더울 것이라 생각해서 겉옷을 준비하지 않아 하루 종일 추위에 떨었던 경험도 있을 것입니다.
"입기 좋은 날"은 회원들의 체질과 스타일에 대한 선호 데이터를 수집하고, 이를 바탕으로 최적의 의상을 추천하여 불편한 경험을 최소화할 수 있도록 도와 줍니다.
사용자 맞춤형 의상 추천 시스템은 향후 지속적으로 개선되어 더욱 정확하고 개인화 된 아웃핏 추천을 제공할 것입니다.
Dockerfile
# 빌드 단계
FROM openjdk:17-alpine AS builder
WORKDIR /app
COPY . .
RUN ./gradlew bootjar
# 실행 단계
FROM openjdk:17-alpine
WORKDIR /app
EXPOSE 8080
# dockerize 설치
RUN apk add --no-cache curl && \
curl -sSL https://github.com/jwilder/dockerize/releases/download/v0.6.1/dockerize-alpine-linux-amd64-v0.6.1.tar.gz | tar -C /usr/local/bin -xzv
# 빌드된 JAR 파일을 복사
ARG JAR_FILE=build/libs/*.jar
COPY --from=builder /app/${JAR_FILE} app.jar
# ENTRYPOINT 설정
ENTRYPOINT ["java", "-jar", "app.jar"]backend-dep
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
maxSurge: 1
selector:
matchLabels:
app: backend
template:
metadata:
labels:
app: backend
spec:
containers:
- name: backend
image: dlacofbs/article1_spring:latest
imagePullPolicy: Always
ports:
- containerPort: 8080
env:
- name: SPRING_PROFILES_ACTIVE
value: aws
- name: db.host
valueFrom:
configMapKeyRef:
name: config
key: db.host
# ConfigMap에서 가져온 환경 변수들
- name: db.port
valueFrom:
configMapKeyRef:
name: config
key: db.port
- name: db.dbname
valueFrom:
configMapKeyRef:
name: config
key: db.dbname
- name: open.weather.api.key
valueFrom:
configMapKeyRef:
name: config
key: open.weather.api.key
- name: open.weather.current.api.url
valueFrom:
configMapKeyRef:
name: config
key: open.weather.current.api.url
- name: open.weather.5day.api.url
valueFrom:
configMapKeyRef:
name: config
key: open.weather.5day.api.url
- name: open.weather.air.api.url
valueFrom:
configMapKeyRef:
name: config
key: open.weather.air.api.url
- name: open.weather.air.forecast.url
valueFrom:
configMapKeyRef:
name: config
key: open.weather.air.forecast.url
- name: amazon.s3.access-key
valueFrom:
configMapKeyRef:
name: config
key: amazon.s3.access-key
- name: kakao.api.client_id
valueFrom:
configMapKeyRef:
name: config
key: kakao.api.client_id
- name: kakao.api.client_secret
valueFrom:
configMapKeyRef:
name: config
key: kakao.api.client_secret
- name: kakao.api.redirect_uri
valueFrom:
configMapKeyRef:
name: config
key: kakao.api.redirect_uri
# Secret에서 가져온 환경 변수들
- name: db.username
valueFrom:
configMapKeyRef:
name: config
key: db.username
- name: db.password
valueFrom:
configMapKeyRef:
name: config
key: db.password
- name: root_password
valueFrom:
secretKeyRef:
name: secret
key: root_password
- name: my_database
valueFrom:
secretKeyRef:
name: secret
key: my_database
- name: my_user
valueFrom:
secretKeyRef:
name: secret
key: my_user
- name: my_password
valueFrom:
secretKeyRef:
name: secret
key: my_password
- name: VITE_KAKAO_API_KEY
valueFrom:
secretKeyRef:
name: secret
key: VITE_KAKAO_API_KEY
- name: SECRET_KEY
valueFrom:
secretKeyRef:
name: secret
key: SECRET_KEY
args:
- "/usr/local/bin/dockerize"
- "-wait"
- "tcp://mariadb:3306" # 환경 변수로 포트 사용
- "-timeout"
- "30s" # ConfigMap에서 가져온 timeout 값 사용
- "java"
- "-jar"
- "app.jar"
backend-ser
apiVersion: v1
kind: Service
metadata:
name: backend
spec:
ports:
- port: 8080
targetPort: 8080
selector:
app: backend
type: ClusterIP
frontend-dep
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
spec:
replicas: 3
selector:
matchLabels:
app: frontend
template:
metadata:
labels:
app: frontend
spec:
containers:
- name: frontend-container
image: dlacofbs/article1_vue:latest
imagePullPolicy: Always
ports:
- containerPort: 80
env:
- name: VITE_KAKAO_API_KEY
valueFrom:
secretKeyRef:
name: secret
key: VITE_KAKAO_API_KEY
- name: VITE_KAKAO_REST_API_KEY
valueFrom:
secretKeyRef:
name: secret
key: VITE_KAKAO_REST_API_KEY
frontend-ser
apiVersion: v1
kind: Service
metadata:
name: frontend
spec:
type: ClusterIP
ports:
- port: 80
targetPort: 80
selector:
app: frontend
ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: article1-ingress
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "false"
nginx.ingress.kubernetes.io/rewrite-target: /$2
nginx.ingress.kubernetes.io/proxy-body-size: "3G" # 최대 요청 본문 크기 설정
nginx.ingress.kubernetes.io/client-body-buffer-size: "3G" # 클라이언트 요청 버퍼 크기 설정
nginx.ingress.kubernetes.io/affinity: "cookie" # 쿠키 기반 세션 스티키니스 활성화
nginx.ingress.kubernetes.io/session-cookie-name: "teenkiri-session" # 세션 쿠키 이름
nginx.ingress.kubernetes.io/session-cookie-hash: "sha1" # 쿠키 해시 알고리즘 (SHA-1)
kubernetes.io/ingress.class: nginx # Ingress 컨트롤러 지정
cert-manager.io/cluster-issuer: letsencrypt-prod # Let's Encrypt 인증서 발급
spec:
ingressClassName: nginx
rules:
- http:
paths:
- path: /()(.*)
pathType: ImplementationSpecific
backend:
service:
name: frontend
port:
number: 80
- path: /boot(/|$)(.*)
pathType: ImplementationSpecific
backend:
service:
name: backend
port:
number: 8080
mariadb-dep
apiVersion: apps/v1
kind: Deployment
metadata:
name: mariadb
spec:
selector:
matchLabels:
app: mariadb
replicas: 1
template:
metadata:
labels:
app: mariadb
spec:
containers:
- name: mariadb
image: mariadb:latest
env:
- name: MARIADB_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: secret
key: root_password
- name: MYSQL_DATABASE
valueFrom:
secretKeyRef:
name: secret
key: my_database
- name: MYSQL_USER
valueFrom:
secretKeyRef:
name: secret
key: my_user
- name: MYSQL_PASSWORD
valueFrom:
secretKeyRef:
name: secret
key: my_password
ports:
- containerPort: 3306
volumeMounts:
- name: mariadb-storage
mountPath: /var/lib/mysql
volumes:
- name: config-volume
configMap:
name: mariadb-config
- name: mariadb-storage
persistentVolumeClaim:
claimName: mariadb-pvc
mariadb-ser
apiVersion: v1
kind: Service
metadata:
name: mariadb
spec:
ports:
- port: 3306
targetPort: 3306
selector:
app: mariadb
type: ClusterIP
mariadb-pvc
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mariadb-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
Jenkins Pipeline Script
pipeline {
agent any
tools {
gradle 'gradle'
jdk 'openJDK17'
}
environment {
DOCKERHUB_CREDENTIALS = credentials('DOCKERHUB_PASSWORD')
GITHUB_URL = 'https://github.com/PBEM22/jenkins_article1'
}
stages {
stage('Check directory') {
steps {
script {
if (isUnix()) {
sh "ls -l ./article1_be/article1-be/"
} else {
bat "dir article1_be\\article1-be\\"
}
}
}
}
stage('Preparation') {
steps {
script {
if (isUnix()) {
sh 'docker --version'
} else {
bat 'docker --version'
}
}
}
}
stage('Add .env') {
steps {
script {
if (isUnix()) {
withCredentials([file(credentialsId: 'application-aws', variable: 'applicationAwsFile')]) {
sh 'cp ${applicationAwsFile} article1_be/article1-be/src/main/resources/application-aws.yml'
}
} else {
withCredentials([file(credentialsId: 'application-aws', variable: 'applicationAwsFile')]) {
bat "copy %applicationAwsFile% article1_be\\article1-be\\src\\main\\resources\\application-aws.yml"
}
}
}
}
}
stage('Source Build') {
steps {
git branch: 'develop', url: "${env.GITHUB_URL}"
script {
if (isUnix()) {
sh "chmod +x ./article1_be/article1-be/gradlew"
sh "./article1_be/article1-be/gradlew clean build -x test"
} else {
bat "cd article1_be\\article1-be && gradlew.bat clean build -x test"
}
}
}
}
stage('Container Build and Push') {
steps {
script {
withCredentials([usernamePassword(credentialsId: 'DOCKERHUB_PASSWORD', usernameVariable: 'DOCKER_USER', passwordVariable: 'DOCKER_PASS')]) {
if (isUnix()) {
sh "docker build -t ${DOCKER_USER}/article1_spring:latest ./article1_be/article1-be/"
sh "docker login -u ${DOCKER_USER} -p ${DOCKER_PASS}"
sh "docker push ${DOCKER_USER}/article1_spring:latest"
} else {
bat "docker build -t ${DOCKER_USER}/article1_spring:latest article1_be\\article1-be\\"
bat "docker login -u %DOCKER_USER% -p %DOCKER_PASS%"
bat "docker push ${DOCKER_USER}/article1_spring:latest"
}
}
}
}
}
}
post {
always {
script {
if (isUnix()) {
sh 'docker logout'
} else {
bat 'docker logout'
}
}
}
success {
echo 'Pipeline succeeded!'
}
failure {
echo 'Pipeline failed!'
}
}
}

































.gif)




