diff --git a/.DS_Store b/.DS_Store
deleted file mode 100644
index ccd76f0..0000000
Binary files a/.DS_Store and /dev/null differ
diff --git a/.gitignore b/.gitignore
index 21ca89f..c1ee09f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,6 +16,7 @@ out/
.jython_cache
+.vscode
# Local .terraform directories
@@ -23,6 +24,7 @@ out/
# .tfstate files
*.tfstate
*.tfstate.*
+terraform.exe
# Crash log files
crash.log
diff --git a/README.md b/README.md
index 3af3f87..3c2609d 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,134 @@
-# movie_chatbot
\ No newline at end of file
+# 무비빔밥
+사용자 선호 및 위치 기반 영화관을 추천해주는 **지능형 고객 지원 챗봇**
+
+
+
+
+- 🔗 서비스 링크 : http://d14hn9nv9zhgf7.cloudfront.net/
+- 📅 개발 기간 : 2024.07.17 ~ 2024.09.03 (7주)
+
+
+
+## 팀 구성 및 역할
+
+
+Alyssa - 인공지능 담당
+
+
+- ChatGPT API를 활용한 응답 생성
+ - 사용자 질문 데이터 전처리
+ - 프롬프트 엔지니어링을 통한 영화관 추천 생성
+- 프로젝트 협업 관리
+
+
+
+
+
+Ryan - 인공지능 담당
+
+
+- LLM ChatGPT api를 활용한 응답 생성
+ - 사용자 질문에서 NER을 이용하여 Entity 추출
+ - koBERT,kiwi를 이용한 RAG구축 후
+ - FAISS를 이용한 Semantic Search, 및 Levenshtein distance 기반 검색 기능 개발
+ - LLM 응답 정형화
+
+
+
+
+
+Yohan - 풀스택 담당
+
+
+- 백엔드
+ - Spring 애플리케이션 서버 개발
+ - AI 모델 구동을 위한 FastAPI 서버 개발
+ - OpenFeign을 사용해 서버 간 통신 구현
+ - MySQL DB 조회 기능 개발
+- 프론트엔드
+ - 날짜 선택, 지역 선택 화면 구현
+
+
+
+
+
+Mir - 풀스택 담당
+
+
+- 영화 상영정보 크롤링 및 DB에 저장
+- 프론트엔드
+ - 채팅 화면 구현
+ - 질문-답변 채팅형태 구현 및 엔티티 관리
+ - 매뉴얼/응답대기 메시지 및 재확인 버튼/체크박스 구현
+ - 스타일 적용
+ - 백엔드 연결
+
+
+
+
+
+Bryan - 클라우드 담당
+
+
+- AWS 인프라 설계
+- Terraform을 활용해 AWS 구현
+- Ansible을 활용한 EC2 인스턴스 개발 환경 구축
+- Prometheus 및 Grafana를 활용한 인스턴스 모니터링 환경 구축
+
+
+
+
+
+Eddy - 클라우드 담당
+
+
+- CI/CD 파이프라인 구축
+- Docker를 이용한 애플리케이션 이미지 만들기 및 배포
+- 전체적인 배포 환경에서의 애플리케이션 실행 테스트
+
+
+
+
+
+
+## 주요 기능
+- 채팅, 버튼을 통한, 의사소통 기능
+- 영화 이름, 지역, 날짜, 시간 정보를 통해 조건에 맞는 영화관의 상영 정보 제공 기능
+
+
+
+## Stack
+- Frontend :
+- Backend :
+- Cloud :
+- AI :
+
+
+
+## 아키텍처
+![Movie-chatbot1](https://github.com/user-attachments/assets/1e5d10f9-a22c-4588-90c4-65a51d475259)
+
+
+
+
+## 프로젝트 수행 결과
+
+- [풀스택](https://github.com/KTB-19/movie_chatbot/blob/main/docs/%EA%B2%B0%EA%B3%BC_%ED%92%80%EC%8A%A4%ED%83%9D.md)
+- [클라우드](https://github.com/KTB-19/movie_chatbot/blob/main/docs/%EA%B2%B0%EA%B3%BC_%ED%81%B4%EB%9D%BC%EC%9A%B0%EB%93%9C.md)
+- [인공지능](https://github.com/KTB-19/movie_chatbot/blob/main/docs/%EA%B2%B0%EA%B3%BC_%EC%9D%B8%EA%B3%B5%EC%A7%80%EB%8A%A5.md)
+
+
+
+## 트러블 슈팅
+
+- [풀스택](https://github.com/KTB-19/movie_chatbot/blob/main/docs/%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85_%ED%92%80%EC%8A%A4%ED%83%9D.md)
+- [클라우드](https://github.com/KTB-19/movie_chatbot/blob/main/docs/%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85_%ED%81%B4%EB%9D%BC%EC%9A%B0%EB%93%9C.md)
+- [인공지능](https://github.com/KTB-19/movie_chatbot/blob/main/docs/%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85_%EC%9D%B8%EA%B3%B5%EC%A7%80%EB%8A%A5.md)
+
+
+
+## 회고
+
+- [회고](https://github.com/KTB-19/movie_chatbot/blob/main/docs/%ED%9A%8C%EA%B3%A0.md)
+
+
diff --git a/backend/package-lock.json b/backend/package-lock.json
new file mode 100644
index 0000000..dfb18f1
--- /dev/null
+++ b/backend/package-lock.json
@@ -0,0 +1,6 @@
+{
+ "name": "backend",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {}
+}
diff --git a/cloud/ansible/crawling-docker-compose.yml b/cloud/ansible/crawling-docker-compose.yml
index bddca2b..a46f946 100644
--- a/cloud/ansible/crawling-docker-compose.yml
+++ b/cloud/ansible/crawling-docker-compose.yml
@@ -2,14 +2,18 @@ version: '3.8'
services:
movie-crawling:
- image: prunsoli/movie-crawling:2.0.7
+ image: prunsoli/movie-crawling:latest
container_name: movie-crawling
-
+ env_file:
+ - .env
+ environment:
+ - TZ=Asia/Seoul
+
cadvisor:
image: gcr.io/cadvisor/cadvisor:v0.49.1
container_name: cadvisor
ports:
- - "9100:8080"
+ - "9090:8080"
volumes:
- /:/rootfs:ro
- /var/run:/var/run:ro
diff --git a/cloud/ansible/backend-docker-compose.yml b/cloud/ansible/db-docker-compose.yml
similarity index 86%
rename from cloud/ansible/backend-docker-compose.yml
rename to cloud/ansible/db-docker-compose.yml
index 39e8978..727362a 100644
--- a/cloud/ansible/backend-docker-compose.yml
+++ b/cloud/ansible/db-docker-compose.yml
@@ -8,12 +8,14 @@ services:
- "3306:3306"
networks:
- export_network
+ volumes:
+ - /home/ec2-user/mysql:/var/lib/mysql
mysqld-exporter:
image: prom/mysqld-exporter:main
container_name: mysql_exporter
ports:
- - "9100:9104"
+ - "9090:9104"
volumes:
- /home/ec2-user/config.my-cnf:/cfg/config-my.cnf
command:
diff --git a/cloud/ansible/docker-install.yml b/cloud/ansible/docker-install.yml
index 8bc94d6..d967e37 100644
--- a/cloud/ansible/docker-install.yml
+++ b/cloud/ansible/docker-install.yml
@@ -30,7 +30,6 @@
- name: Install docker-compose
shell:
cmd: curl -L "https://github.com/docker/compose/releases/download/v2.3.3/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
- warn: false
- name: Chmod docker-compose
file:
@@ -42,7 +41,7 @@
src: /usr/local/bin/docker-compose
dest: /usr/bin/docker-compose
state: link
-
+ # 시간 바꾸기
- name: Set timezone to Asia/Seoul
ansible.builtin.command: timedatectl set-timezone Asia/Seoul
@@ -52,4 +51,23 @@
- name: Print the current timezone
ansible.builtin.debug:
- var: timedatectl_output.stdout
\ No newline at end of file
+ var: timedatectl_output.stdout
+
+ # Node Exporter 설치
+ - name: Pull Node Exporter Docker image
+ ansible.builtin.docker_image:
+ name: quay.io/prometheus/node-exporter
+ tag: latest
+ source: pull
+
+ - name: Run Node Exporter container
+ ansible.builtin.docker_container:
+ name: node_exporter
+ image: quay.io/prometheus/node-exporter:latest
+ state: started
+ restart_policy: always
+ network_mode: host
+ pid_mode: host
+ volumes:
+ - /:/host:ro,rslave
+ command: --path.rootfs=/host
\ No newline at end of file
diff --git a/cloud/ansible/work-instance.yml b/cloud/ansible/work-instance.yml
index 2327696..0aa7421 100644
--- a/cloud/ansible/work-instance.yml
+++ b/cloud/ansible/work-instance.yml
@@ -1,5 +1,5 @@
- name: Deploy and run docker-compose on different hosts
- hosts: backend, crawling
+ hosts: db, crawling
become: yes
tasks:
@@ -14,9 +14,9 @@
src: "{{ item.src }}"
dest: "{{ item.dest}}"
with_items:
- - { src: 'backend-docker-compose.yml', dest: '/home/ec2-user/docker-compose.yml'}
+ - { src: 'db-docker-compose.yml', dest: '/home/ec2-user/docker-compose.yml'}
- { src: 'config.my-cnf', dest: '/home/ec2-user/config.my-cnf'}
- when: "'backend' in group_names"
+ when: "'db' in group_names"
- name: Copy docker-compose.yml and .env to crawling servers
copy:
diff --git a/cloud/terraform/environment/dev/front/.terraform.lock.hcl b/cloud/terraform/environment/dev/front/.terraform.lock.hcl
deleted file mode 100644
index 0fa7c0c..0000000
--- a/cloud/terraform/environment/dev/front/.terraform.lock.hcl
+++ /dev/null
@@ -1,25 +0,0 @@
-# This file is maintained automatically by "terraform init".
-# Manual edits may be lost in future updates.
-
-provider "registry.terraform.io/hashicorp/aws" {
- version = "4.67.0"
- constraints = "~> 4.16"
- hashes = [
- "h1:5Zfo3GfRSWBaXs4TGQNOflr1XaYj6pRnVJLX5VAjFX4=",
- "zh:0843017ecc24385f2b45f2c5fce79dc25b258e50d516877b3affee3bef34f060",
- "zh:19876066cfa60de91834ec569a6448dab8c2518b8a71b5ca870b2444febddac6",
- "zh:24995686b2ad88c1ffaa242e36eee791fc6070e6144f418048c4ce24d0ba5183",
- "zh:4a002990b9f4d6d225d82cb2fb8805789ffef791999ee5d9cb1fef579aeff8f1",
- "zh:559a2b5ace06b878c6de3ecf19b94fbae3512562f7a51e930674b16c2f606e29",
- "zh:6a07da13b86b9753b95d4d8218f6dae874cf34699bca1470d6effbb4dee7f4b7",
- "zh:768b3bfd126c3b77dc975c7c0e5db3207e4f9997cf41aa3385c63206242ba043",
- "zh:7be5177e698d4b547083cc738b977742d70ed68487ce6f49ecd0c94dbf9d1362",
- "zh:8b562a818915fb0d85959257095251a05c76f3467caa3ba95c583ba5fe043f9b",
- "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425",
- "zh:9c385d03a958b54e2afd5279cd8c7cbdd2d6ca5c7d6a333e61092331f38af7cf",
- "zh:b3ca45f2821a89af417787df8289cb4314b273d29555ad3b2a5ab98bb4816b3b",
- "zh:da3c317f1db2469615ab40aa6baba63b5643bae7110ff855277a1fb9d8eb4f2c",
- "zh:dc6430622a8dc5cdab359a8704aec81d3825ea1d305bbb3bbd032b1c6adfae0c",
- "zh:fac0d2ddeadf9ec53da87922f666e1e73a603a611c57bcbc4b86ac2821619b1d",
- ]
-}
diff --git a/cloud/terraform/environment/dev/front/s3CD.tf b/cloud/terraform/environment/dev/front/s3CD.tf
index 4e2a514..1d70993 100644
--- a/cloud/terraform/environment/dev/front/s3CD.tf
+++ b/cloud/terraform/environment/dev/front/s3CD.tf
@@ -23,7 +23,7 @@ provider "aws" {
}
resource "aws_s3_bucket" "react_website" {
- bucket = "ktb-movie-bucket"
+ bucket = "ktb-movie-bucket-${workspace}"
}
# S3 버킷의 공용 접근 차단 설정
diff --git a/cloud/terraform/environment/dev/main.tf b/cloud/terraform/environment/dev/main.tf
index 3d46b95..c329f07 100644
--- a/cloud/terraform/environment/dev/main.tf
+++ b/cloud/terraform/environment/dev/main.tf
@@ -32,10 +32,10 @@ module "vpc" {
module "backend" {
source = "../../modules/instance"
ami_id = "ami-0c2acfcb2ac4d02a0"
- instance_type = "t3.small"
+ instance_type = "t3.large"
ssh_key_name = "kakao-tech-bootcamp"
subnet_id = module.vpc.private_subnet_ids[0]
- security_groups_id = [aws_security_group.ssh.id, aws_security_group.mysql.id]
+ security_groups_id = [aws_security_group.ssh.id, aws_security_group.backend.id]
workspace = "${terraform.workspace}-backend"
}
@@ -59,6 +59,86 @@ module "dev-host" {
workspace = "Dev-Host"
}
+# ALB
+
+# ALB를 위한 보안 그룹 생성
+resource "aws_security_group" "alb" {
+ vpc_id = module.vpc.vpc_id
+
+ ingress {
+ from_port = 80
+ to_port = 80
+ protocol = "tcp"
+ cidr_blocks = ["0.0.0.0/0"]
+ }
+
+ egress {
+ from_port = 0
+ to_port = 0
+ protocol = "-1"
+ cidr_blocks = ["0.0.0.0/0"]
+ }
+
+ tags = {
+ Name = "moive-${terraform.workspace}-sg-alb"
+ }
+}
+
+# ALB 생성
+resource "aws_lb" "alb" {
+ name = "${terraform.workspace}-alb"
+ internal = false
+ load_balancer_type = "application"
+ security_groups = [aws_security_group.alb.id]
+ subnets = module.vpc.public_subnet_ids
+
+ tags = {
+ Name = "moive-${terraform.workspace}-alb"
+ }
+}
+
+# ALB 리스너 생성
+resource "aws_lb_listener" "http" {
+ load_balancer_arn = aws_lb.alb.arn
+ port = "80"
+ protocol = "HTTP"
+
+ default_action {
+ type = "forward"
+ target_group_arn = aws_lb_target_group.backend.arn
+ }
+}
+
+# 타겟 그룹 생성
+resource "aws_lb_target_group" "backend" {
+ name = "${terraform.workspace}-tg-backend"
+ port = 8080
+ protocol = "HTTP"
+ vpc_id = module.vpc.vpc_id
+
+ health_check {
+ path = "/"
+ protocol = "HTTP"
+ matcher = "200-299"
+ interval = 30
+ timeout = 5
+ healthy_threshold = 3
+ unhealthy_threshold = 3
+ }
+
+ tags = {
+ Name = "moive-${terraform.workspace}-tg-backend"
+ }
+}
+
+# 백엔드 인스턴스 등록
+resource "aws_lb_target_group_attachment" "backend" {
+ target_group_arn = aws_lb_target_group.backend.arn
+ target_id = module.backend.instance_id
+ port = 8080
+}
+
+
# 보안 그룹
resource "aws_security_group" "ssh" {
vpc_id = module.vpc.vpc_id
@@ -88,7 +168,7 @@ resource "aws_security_group" "ssh" {
}
}
-resource "aws_security_group" "mysql" {
+resource "aws_security_group" "backend" {
vpc_id = module.vpc.vpc_id
ingress {
@@ -98,6 +178,7 @@ resource "aws_security_group" "mysql" {
cidr_blocks = ["0.0.0.0/0"]
}
+ #mysql
ingress {
from_port = 3306
to_port = 3306
@@ -105,6 +186,14 @@ resource "aws_security_group" "mysql" {
cidr_blocks = ["0.0.0.0/0"]
}
+ #alb
+ ingress {
+ from_port = 80
+ to_port = 8080
+ protocol = "tcp"
+ security_groups = [aws_security_group.alb.id]
+ }
+
egress {
from_port = 0
to_port = 0
diff --git a/cloud/terraform/environment/prod/main.tf b/cloud/terraform/environment/prod/main.tf
index 8aadae1..4106e66 100644
--- a/cloud/terraform/environment/prod/main.tf
+++ b/cloud/terraform/environment/prod/main.tf
@@ -1,6 +1,5 @@
####기본적인 terraform setup을 위한 tf 파일
# S3 버켓과 DynamoDB를 생성한다
-
terraform {
required_providers {
aws = {
@@ -20,118 +19,150 @@ terraform {
required_version = ">= 1.2.0"
}
+
module "vpc" {
- source = "../../modules/vpc"
- vpc_cidr = "192.168.0.0/16"
- public_subnet_cidr = "192.168.1.0/24"
- private_subnet_cidr = "192.168.2.0/24"
- environment = terraform.workspace
- public_subnet_count = 2
- private_subnet_count = 2
+ source = "../../modules/vpc_v2"
}
-
-## 인스턴스
-
-module "front" {
- source = "../../modules/instance"
- ami_id = "ami-0c2acfcb2ac4d02a0"
- instance_type = "t2.micro"
- ssh_key_name = "kakao-tech-bootcamp"
- subnet_id = module.vpc.public_subnet_ids[0]
- security_groups_id = [aws_security_group.ssh.id]
- workspace = "${terraform.workspace}-front"
+module "security_groups" {
+ source = "../../modules/security_groups"
+ vpc_id = module.vpc.vpc_id
}
-module "name" {
- source = "../../modules/instance"
- ami_id = "ami-0c2acfcb2ac4d02a0"
- instance_type = "t2.small"
- ssh_key_name = "kakao-tech-bootcamp"
- subnet_id = module.vpc.private_subnet_ids[0]
- security_groups_id = [aws_security_group.ssh.id]
- workspace = "${terraform.workspace}-backend"
+module "dev-host" {
+ source = "../../modules/ec2"
+ subnets = [module.vpc.public_subnets[0]]
+ instance_type = "t3.small"
+ ami_image = "ami-05d768df76a2b8bd8"
+ private_ips = ["192.168.1.23"]
+ instance_count = 1
+ security_groups = [module.security_groups.movie_default_sg_id]
+ tags = "dev-host"
}
module "crawling" {
- source = "../../modules/instance"
- ami_id = "ami-0c2acfcb2ac4d02a0"
- instance_type = "t2.xlarge"
- ssh_key_name = "kakao-tech-bootcamp"
- subnet_id = module.vpc.public_subnet_ids[1]
- security_groups_id = [aws_security_group.ssh.id]
- workspace = "${terraform.workspace}-crawling"
+ source = "../../modules/ec2"
+ subnets = [module.vpc.public_subnets[1]]
+ instance_type = "c6i.large"
+ private_ips = ["192.168.2.26"]
+ instance_count = 1
+ security_groups = [module.security_groups.movie_default_sg_id]
+ tags = "crawling"
+}
+
+module "backend" {
+ source = "../../modules/ec2"
+ subnets = module.vpc.private_subnets
+ instance_type = "r7i.large"
+ private_ips = ["192.168.3.233", "192.168.4.134"]
+ instance_count = 2
+ security_groups = [module.security_groups.movie_default_sg_id, module.security_groups.movie_backend_sg_id]
+ tags = "backend"
}
module "db" {
- source = "../../modules/instance"
- ami_id = "ami-0c2acfcb2ac4d02a0"
- instance_type = "t2.small"
- ssh_key_name = "kakao-tech-bootcamp"
- subnet_id = module.vpc.private_subnet_ids[1]
- security_groups_id = [aws_security_group.mysql.id,aws_security_group.ssh.id]
- workspace = "${terraform.workspace}-db"
+ source = "../../modules/ec2"
+ subnets = module.vpc.db_subnets
+ instance_type = "t3.small"
+ private_ips = ["192.168.5.116"]
+ instance_count = 1
+ security_groups = [module.security_groups.movie_default_sg_id, module.security_groups.movie_db_sg_id]
+ tags = "db"
}
-module "dev-host" {
- source = "../../modules/instance"
- ami_id = "ami-0c2acfcb2ac4d02a0"
- instance_type = "t2.xlarge"
- ssh_key_name = "kakao-tech-bootcamp"
- subnet_id = module.vpc.public_subnet_ids[1]
- security_groups_id = [aws_security_group.ssh.id]
- workspace = "${terraform.workspace}-db"
+module "alb" {
+ source = "../../modules/alb"
+ target_instances_ids = module.backend.instance_ids
+ vpc_id = module.vpc.vpc_id
+ alb-subnets_ids = module.vpc.public_subnets
+ security_groups_ids = [module.security_groups.movie_alb_sg_id]
}
+# Front 배포
+resource "aws_s3_bucket" "react_website" {
+ bucket = "ktb-movie-bucket-${terraform.workspace}"
+}
+# S3 버킷의 공용 접근 차단 설정
+resource "aws_s3_bucket_public_access_block" "react_website_public_access_block" {
+ bucket = aws_s3_bucket.react_website.id
-# 보안 그룹
-resource "aws_security_group" "ssh" {
- vpc_id = module.vpc.vpc_id
- ingress {
- from_port = 22
- to_port = 22
- protocol = "tcp"
- cidr_blocks = ["0.0.0.0/0"]
- }
+ block_public_acls = true
+ block_public_policy = true
+ ignore_public_acls = true
+ restrict_public_buckets = true
+}
- egress {
- from_port = 0
- to_port = 0
- protocol = "-1"
- cidr_blocks = ["0.0.0.0/0"]
- }
+resource "aws_cloudfront_origin_access_control" "s3_oac" {
+ name = "ktb-movie-oac"
+ description = "OAC for ktb-movie S3 bucket"
+ origin_access_control_origin_type = "s3"
- tags = {
- Name = "moive-${terraform.workspace}-sg-ssh"
- }
+ signing_behavior = "always"
+ signing_protocol = "sigv4"
}
-resource "aws_security_group" "mysql" {
- vpc_id = module.vpc.vpc_id
+resource "aws_cloudfront_distribution" "react_website_distribution" {
+ origin {
+ domain_name = aws_s3_bucket.react_website.bucket_regional_domain_name
+ origin_id = aws_s3_bucket.react_website.id
+ origin_access_control_id = aws_cloudfront_origin_access_control.s3_oac.id
+
+ }
+
+ enabled = true
+ is_ipv6_enabled = true
+ default_root_object = "index.html"
- ingress {
- from_port = 22
- to_port = 22
- protocol = "tcp"
- cidr_blocks = ["0.0.0.0/0"]
+ default_cache_behavior {
+ allowed_methods = ["GET", "HEAD"]
+ cached_methods = ["GET", "HEAD"]
+ target_origin_id = aws_s3_bucket.react_website.id
+
+ forwarded_values {
+ query_string = false
+ cookies {
+ forward = "none"
+ }
+ }
+
+ viewer_protocol_policy = "redirect-to-https"
}
-
- ingress {
- from_port = 3306
- to_port = 3306
- protocol = "tcp"
- cidr_blocks = ["0.0.0.0/0"]
+
+ viewer_certificate {
+ cloudfront_default_certificate = true
}
- egress {
- from_port = 0
- to_port = 0
- protocol = "-1"
- cidr_blocks = ["0.0.0.0/0"]
+ restrictions {
+ geo_restriction {
+ restriction_type = "none"
+ }
}
tags = {
- Name = "moive-${terraform.workspace}-sg-mysql"
+ Name = "React Website Distribution"
}
+}
+
+resource "aws_s3_bucket_policy" "react_website_policy" {
+ bucket = aws_s3_bucket.react_website.id
+
+ policy = jsonencode({
+ Version = "2012-10-17"
+ Statement = [
+ {
+ Effect = "Allow"
+ Principal = {
+ Service = "cloudfront.amazonaws.com"
+ }
+ Action = "s3:GetObject"
+ Resource = "${aws_s3_bucket.react_website.arn}/*"
+ Condition = {
+ StringEquals = {
+ "AWS:SourceArn" = aws_cloudfront_distribution.react_website_distribution.arn
+ }
+ }
+ }
+ ]
+ })
}
\ No newline at end of file
diff --git a/cloud/terraform/environment/prod/variables.tf b/cloud/terraform/environment/prod/variables.tf
index 1729329..c72a4ca 100644
--- a/cloud/terraform/environment/prod/variables.tf
+++ b/cloud/terraform/environment/prod/variables.tf
@@ -1,11 +1,11 @@
variable "aws_region" {
description = "AWS 리전"
- type = string
- default = "ap-northeast-2"
+ type = string
+ default = "ap-northeast-2"
}
variable "name" {
description = "프로젝트 이름"
- type = string
- default = "movie-chat"
+ type = string
+ default = "movie-chat"
}
\ No newline at end of file
diff --git a/cloud/terraform/modules/alb/alb.tf b/cloud/terraform/modules/alb/alb.tf
new file mode 100644
index 0000000..cfa9e6a
--- /dev/null
+++ b/cloud/terraform/modules/alb/alb.tf
@@ -0,0 +1,65 @@
+variable "alb-subnets_ids" {
+ description = "alb에 등록할 subnet id"
+ type = list(string)
+}
+
+variable "security_groups_ids" {
+ type = list(string)
+}
+
+variable "vpc_id" {
+ type = string
+}
+
+variable "target_instances_ids" {
+ type = list(string)
+}
+
+# ALB 생성
+resource "aws_lb" "backend_alb" {
+ name = "backend-alb"
+ internal = false
+ load_balancer_type = "application"
+ security_groups = var.security_groups_ids
+ subnets = var.alb-subnets_ids
+
+ tags = {
+ Name = "ktb-movie-backend-alb"
+ }
+}
+
+resource "aws_lb_target_group" "backend_tg" {
+ name = "backend-tg"
+ port = 8080
+ protocol = "HTTP"
+ vpc_id = var.vpc_id
+
+ health_check {
+ path = "/health"
+ protocol = "HTTP"
+ matcher = "200-299"
+ interval = 30
+ timeout = 5
+ healthy_threshold = 3
+ unhealthy_threshold = 3
+ }
+}
+
+resource "aws_lb_listener" "backend_listener" {
+ load_balancer_arn = aws_lb.backend_alb.arn
+ port = 80
+ protocol = "HTTP"
+
+ default_action {
+ type = "forward"
+ target_group_arn = aws_lb_target_group.backend_tg.arn
+ }
+}
+
+# 인스턴스 등록
+resource "aws_lb_target_group_attachment" "backend" {
+ count = length(var.target_instances_ids)
+ target_group_arn = aws_lb_target_group.backend_tg.arn
+ target_id = var.target_instances_ids[count.index]
+ port = 8080
+}
\ No newline at end of file
diff --git a/cloud/terraform/modules/ec2/ec2.tf b/cloud/terraform/modules/ec2/ec2.tf
new file mode 100644
index 0000000..9c66365
--- /dev/null
+++ b/cloud/terraform/modules/ec2/ec2.tf
@@ -0,0 +1,13 @@
+resource "aws_instance" "instances" {
+ count = var.instance_count
+ ami = var.ami_image
+ instance_type = var.instance_type
+ subnet_id = element(var.subnets, count.index)
+ key_name = "kakao-tech-bootcamp"
+ security_groups = var.security_groups
+ private_ip = var.private_ips[count.index]
+
+ tags = {
+ Name = "ktb-movie-${var.tags}-instance-${count.index}"
+ }
+}
diff --git a/cloud/terraform/modules/ec2/outputs.tf b/cloud/terraform/modules/ec2/outputs.tf
new file mode 100644
index 0000000..30ea4ae
--- /dev/null
+++ b/cloud/terraform/modules/ec2/outputs.tf
@@ -0,0 +1,4 @@
+output "instance_ids" {
+ description = "The IDs of all the instances created"
+ value = aws_instance.instances[*].id
+}
\ No newline at end of file
diff --git a/cloud/terraform/modules/ec2/variable.tf b/cloud/terraform/modules/ec2/variable.tf
new file mode 100644
index 0000000..3788ea3
--- /dev/null
+++ b/cloud/terraform/modules/ec2/variable.tf
@@ -0,0 +1,35 @@
+variable "subnets" {
+ description = "서브넷 주소"
+ type = list(string)
+ nullable = true
+}
+variable "tags" {
+ type = string
+}
+
+variable "instance_count" {
+ description = "생성할 인스턴스 개수"
+ type = number
+}
+
+variable "instance_type" {
+ description = "인스턴스 타입"
+ type = string
+ default = "t2.micro"
+}
+
+variable "security_groups" {
+ description = "Security Groups"
+ type = list(string)
+}
+
+variable "private_ips" {
+ description = "할당한 private subnet"
+ type = list(string)
+}
+
+variable "ami_image" {
+ description = "EC2 이미지"
+ type = string
+ default = "ami-0c2acfcb2ac4d02a0"
+}
\ No newline at end of file
diff --git a/cloud/terraform/modules/security_groups/main.tf b/cloud/terraform/modules/security_groups/main.tf
new file mode 100644
index 0000000..f6ef88d
--- /dev/null
+++ b/cloud/terraform/modules/security_groups/main.tf
@@ -0,0 +1,92 @@
+variable "vpc_id" {
+ type = string
+}
+
+resource "aws_security_group" "movie-default" {
+ vpc_id = var.vpc_id
+
+ ingress {
+ from_port = 22
+ to_port = 22
+ protocol = "tcp"
+ cidr_blocks = ["0.0.0.0/0"]
+ }
+
+ ingress {
+ from_port = 9100
+ to_port = 9100
+ protocol = "tcp"
+ cidr_blocks = ["0.0.0.0/0"]
+ }
+
+ ingress {
+ from_port = 9090
+ to_port = 9090
+ protocol = "tcp"
+ cidr_blocks = ["0.0.0.0/0"]
+ }
+
+ egress {
+ from_port = 0
+ to_port = 0
+ protocol = "-1"
+ cidr_blocks = ["0.0.0.0/0"]
+ }
+
+ tags = {
+ Name = "ktb-movie-default-sg"
+ }
+}
+
+resource "aws_security_group" "movie-db" {
+ vpc_id = var.vpc_id
+
+ ingress {
+ from_port = 3306
+ to_port = 3306
+ protocol = "tcp"
+ cidr_blocks = ["0.0.0.0/0"]
+ }
+
+ tags = {
+ Name = "ktb-movie-db-sg"
+ }
+}
+
+resource "aws_security_group" "movie-alb" {
+ vpc_id = var.vpc_id
+
+ ingress {
+ from_port = 80
+ to_port = 80
+ protocol = "tcp"
+ cidr_blocks = ["0.0.0.0/0"]
+ }
+
+ egress {
+ from_port = 0
+ to_port = 0
+ protocol = "-1"
+ cidr_blocks = ["0.0.0.0/0"]
+ }
+
+ tags = {
+ Name = "ktb-movie-alb-sg"
+ }
+}
+
+
+resource "aws_security_group" "movie-backend" {
+ vpc_id = var.vpc_id
+
+ ingress {
+ from_port = 8080
+ to_port = 8080
+ protocol = "tcp"
+ cidr_blocks = ["0.0.0.0/0"]
+ }
+
+ tags = {
+ Name = "ktb-movie-backend-sg"
+ }
+}
\ No newline at end of file
diff --git a/cloud/terraform/modules/security_groups/output.tf b/cloud/terraform/modules/security_groups/output.tf
new file mode 100644
index 0000000..71466d3
--- /dev/null
+++ b/cloud/terraform/modules/security_groups/output.tf
@@ -0,0 +1,23 @@
+# Output for movie-default security group
+output "movie_default_sg_id" {
+ description = "The ID of the movie-default security group"
+ value = aws_security_group.movie-default.id
+}
+
+# Output for movie-db security group
+output "movie_db_sg_id" {
+ description = "The ID of the movie-db security group"
+ value = aws_security_group.movie-db.id
+}
+
+# Output for movie-alb security group
+output "movie_alb_sg_id" {
+ description = "The ID of the movie-alb security group"
+ value = aws_security_group.movie-alb.id
+}
+
+# Output for movie-backend security group
+output "movie_backend_sg_id" {
+ description = "The ID of the movie-backend security group"
+ value = aws_security_group.movie-backend.id
+}
\ No newline at end of file
diff --git a/cloud/terraform/modules/vpc/variables.tf b/cloud/terraform/modules/vpc/variables.tf
index 791076c..85d4e2f 100644
--- a/cloud/terraform/modules/vpc/variables.tf
+++ b/cloud/terraform/modules/vpc/variables.tf
@@ -8,7 +8,6 @@ variable "environment" {
type = string
}
-
variable "public_subnet_count" {
description = "The number of public subnets"
type = number
@@ -22,5 +21,5 @@ variable "private_subnet_count" {
variable "availability_zone" {
description = "AZ"
type = list(string)
- default = ["ap-northeast-2a"]
+ default = ["ap-northeast-2a","ap-northeast-2c"]
}
\ No newline at end of file
diff --git a/cloud/terraform/modules/vpc/vpc.tf b/cloud/terraform/modules/vpc/vpc.tf
index bbf7ba4..d080811 100644
--- a/cloud/terraform/modules/vpc/vpc.tf
+++ b/cloud/terraform/modules/vpc/vpc.tf
@@ -12,7 +12,7 @@ resource "aws_subnet" "public" {
vpc_id = aws_vpc.this.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index)
map_public_ip_on_launch = true
- availability_zone = var.availability_zone[0]
+ availability_zone = var.availability_zone[count.index]
tags = {
Name = "movie-chat-${var.environment}-public-subnet-${count.index + 1}"
@@ -24,7 +24,7 @@ resource "aws_subnet" "private" {
vpc_id = aws_vpc.this.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, var.public_subnet_count + count.index)
- availability_zone = var.availability_zone[0]
+ availability_zone = var.availability_zone[count.index % length(var.availability_zone)]
tags = {
Name = "movie-chat-${var.environment}-private-subnet-${count.index + 1}"
@@ -86,6 +86,6 @@ resource "aws_eip" "movie-eip" {
# Nat Gateway
resource "aws_nat_gateway" "movie-nat-gw" {
allocation_id = aws_eip.movie-eip.id
- subnet_id = aws_subnet.public[1].id
+ subnet_id = aws_subnet.public[0].id
connectivity_type = "public"
}
\ No newline at end of file
diff --git a/cloud/terraform/modules/vpc_v2/output.tf b/cloud/terraform/modules/vpc_v2/output.tf
new file mode 100644
index 0000000..93153f6
--- /dev/null
+++ b/cloud/terraform/modules/vpc_v2/output.tf
@@ -0,0 +1,30 @@
+output "vpc_id" {
+ value = aws_vpc.main_vpc.id
+}
+
+# Public 서브넷 ID 출력
+output "public_subnets" {
+ description = "Public subnet IDs"
+ value = [
+ for key, subnet in aws_subnet.subnets : subnet.id
+ if substr(key, 0, 6) == "public"
+ ]
+}
+
+# Private 서브넷 ID 출력
+output "private_subnets" {
+ description = "Private subnet IDs"
+ value = [
+ for key, subnet in aws_subnet.subnets : subnet.id
+ if substr(key, 0, 7) == "private"
+ ]
+}
+
+# DB 서브넷 ID 출력
+output "db_subnets" {
+ description = "DB subnet IDs"
+ value = [
+ for key, subnet in aws_subnet.subnets : subnet.id
+ if substr(key, 0, 2) == "db"
+ ]
+}
\ No newline at end of file
diff --git a/cloud/terraform/modules/vpc_v2/vpc.tf b/cloud/terraform/modules/vpc_v2/vpc.tf
new file mode 100644
index 0000000..03de66e
--- /dev/null
+++ b/cloud/terraform/modules/vpc_v2/vpc.tf
@@ -0,0 +1,125 @@
+
+# 공통 태그 정의
+locals {
+ common_tags = {
+ Project = "ktb-movie"
+ Owner = "Cloud-Team-Bryan"
+ CreatedBy = "Terraform"
+ CreatedDate = formatdate("YYYY-MM-DD", timestamp())
+ }
+
+ # 서브넷 정의
+ subnets = {
+ public = {
+ cidr_blocks = ["192.168.1.0/24", "192.168.2.0/24"]
+ azs = ["ap-northeast-2a", "ap-northeast-2c"]
+ }
+ private = {
+ cidr_blocks = ["192.168.3.0/24", "192.168.4.0/24"]
+ azs = ["ap-northeast-2a", "ap-northeast-2c"]
+ }
+ db = {
+ cidr_blocks = ["192.168.5.0/24"]
+ azs = ["ap-northeast-2a"]
+ }
+ }
+
+ subnet_config = {
+ public = local.subnets.public
+ private = local.subnets.private
+ db = local.subnets.db
+ }
+
+ subnet_map = merge([
+ for subnet_type, config in local.subnet_config : {
+ for idx, cidr in config.cidr_blocks :
+ "${subnet_type}-${idx}" => {
+ cidr_block = cidr
+ availability_zone = config.azs[idx]
+ public = subnet_type == "public"
+ }
+ }
+ ]...)
+}
+
+# VPC 생성
+resource "aws_vpc" "main_vpc" {
+ cidr_block = "192.168.0.0/16"
+ enable_dns_support = true
+ enable_dns_hostnames = true
+
+ tags = merge(local.common_tags, {
+ Name = "ktb-movie-main-vpc"
+ })
+}
+
+# 인터넷 게이트웨이
+resource "aws_internet_gateway" "igw" {
+ vpc_id = aws_vpc.main_vpc.id
+
+ tags = merge(local.common_tags, {
+ Name = "ktb-movie-main-igw"
+ })
+}
+
+resource "aws_subnet" "subnets" {
+ for_each = local.subnet_map
+
+ vpc_id = aws_vpc.main_vpc.id
+ cidr_block = each.value.cidr_block
+ availability_zone = each.value.availability_zone
+ map_public_ip_on_launch = each.value.public
+
+ tags = merge(local.common_tags, {
+ Name = "ktb-movie-${each.key}-subnet"
+ })
+}
+
+# NAT Gateway
+resource "aws_eip" "nat_eip" {
+ vpc = true
+ tags = merge(local.common_tags, {
+ Name = "ktb-movie-nat-eip"
+ })
+}
+
+resource "aws_nat_gateway" "nat_gateway" {
+ allocation_id = aws_eip.nat_eip.id
+ subnet_id = aws_subnet.subnets["public-0"].id
+
+ tags = merge(local.common_tags, {
+ Name = "ktb-movie-nat-gateway"
+ })
+}
+
+# 라우팅 테이블
+resource "aws_route_table" "route_tables" {
+ for_each = toset(["public", "private"])
+
+ vpc_id = aws_vpc.main_vpc.id
+
+ tags = merge(local.common_tags, {
+ Name = "ktb-movie-${each.key}-route-table"
+ })
+}
+
+# 라우팅 규칙
+resource "aws_route" "public_internet_gateway" {
+ route_table_id = aws_route_table.route_tables["public"].id
+ destination_cidr_block = "0.0.0.0/0"
+ gateway_id = aws_internet_gateway.igw.id
+}
+
+resource "aws_route" "private_nat_gateway" {
+ route_table_id = aws_route_table.route_tables["private"].id
+ destination_cidr_block = "0.0.0.0/0"
+ nat_gateway_id = aws_nat_gateway.nat_gateway.id
+}
+
+# 서브넷 연결
+resource "aws_route_table_association" "subnet_routes" {
+ for_each = aws_subnet.subnets
+
+ subnet_id = each.value.id
+ route_table_id = aws_route_table.route_tables[startswith(each.key, "public") ? "public" : "private"].id
+}
\ No newline at end of file
diff --git a/crawling/main.py b/crawling/main.py
index 8b5ddb7..528391b 100644
--- a/crawling/main.py
+++ b/crawling/main.py
@@ -24,25 +24,25 @@ def job():
print("inserted into database", flush=True)
-def job_for7days():
- data_list = []
- divisions = [i for i in range(1, 18)]
+# def job_for7days():
+# data_list = []
+# divisions = [i for i in range(1, 18)]
- print("start crawling")
+# print("start crawling")
- for i in divisions:
- data_list.extend(process_division.process_division(i, initial=7))
+# for i in divisions:
+# data_list.extend(process_division.process_division(i, initial=7))
- print("final: ", len(data_list))
- database.insert_db.insert_data(data_list)
- print("inserted into database(for 7 days)", flush=True)
+# print("final: ", len(data_list))
+# database.insert_db.insert_data(data_list)
+# print("inserted into database(for 7 days)", flush=True)
if __name__ == '__main__':
- # 최초 1회 실행
- job_for7days()
+ # # 최초 1회 실행
+ # job_for7days()
- # 매일 오전 9시마다 실행
+ # # 매일 오전 9시마다 실행
schedule.every().day.at("09:00").do(job)
while True:
diff --git a/docs/img/1.png b/docs/img/1.png
new file mode 100644
index 0000000..2815adf
Binary files /dev/null and b/docs/img/1.png differ
diff --git a/docs/img/2.png b/docs/img/2.png
new file mode 100644
index 0000000..47d9ae3
Binary files /dev/null and b/docs/img/2.png differ
diff --git a/docs/img/3.png b/docs/img/3.png
new file mode 100644
index 0000000..68e6be7
Binary files /dev/null and b/docs/img/3.png differ
diff --git a/docs/img/4.png b/docs/img/4.png
new file mode 100644
index 0000000..514367d
Binary files /dev/null and b/docs/img/4.png differ
diff --git a/docs/img/Movie-chatbot-architecture.png b/docs/img/Movie-chatbot-architecture.png
new file mode 100644
index 0000000..5453c77
Binary files /dev/null and b/docs/img/Movie-chatbot-architecture.png differ
diff --git a/docs/img/c6i.large.png b/docs/img/c6i.large.png
new file mode 100644
index 0000000..c1ab836
Binary files /dev/null and b/docs/img/c6i.large.png differ
diff --git a/docs/img/c6i.xlarge.png b/docs/img/c6i.xlarge.png
new file mode 100644
index 0000000..546e0ce
Binary files /dev/null and b/docs/img/c6i.xlarge.png differ
diff --git a/docs/img/erd.png b/docs/img/erd.png
new file mode 100644
index 0000000..5b3f0a7
Binary files /dev/null and b/docs/img/erd.png differ
diff --git a/docs/img/exception.png b/docs/img/exception.png
new file mode 100644
index 0000000..8a9fc7a
Binary files /dev/null and b/docs/img/exception.png differ
diff --git a/docs/img/image (2).png b/docs/img/image (2).png
new file mode 100644
index 0000000..b82146c
Binary files /dev/null and b/docs/img/image (2).png differ
diff --git a/docs/img/lighthouse.png b/docs/img/lighthouse.png
new file mode 100644
index 0000000..d6ede92
Binary files /dev/null and b/docs/img/lighthouse.png differ
diff --git "a/docs/img/screen 2024-09-10 \354\230\244\355\233\204 2.47.28.png" "b/docs/img/screen 2024-09-10 \354\230\244\355\233\204 2.47.28.png"
new file mode 100644
index 0000000..0a0a7e6
Binary files /dev/null and "b/docs/img/screen 2024-09-10 \354\230\244\355\233\204 2.47.28.png" differ
diff --git a/docs/img/t2.large.png b/docs/img/t2.large.png
new file mode 100644
index 0000000..0b5ca08
Binary files /dev/null and b/docs/img/t2.large.png differ
diff --git a/docs/img/t2.xlarge.png b/docs/img/t2.xlarge.png
new file mode 100644
index 0000000..5f51a47
Binary files /dev/null and b/docs/img/t2.xlarge.png differ
diff --git a/docs/img/test.png b/docs/img/test.png
new file mode 100644
index 0000000..f341816
Binary files /dev/null and b/docs/img/test.png differ
diff --git a/docs/img/validate.png b/docs/img/validate.png
new file mode 100644
index 0000000..4f41e2c
Binary files /dev/null and b/docs/img/validate.png differ
diff --git "a/docs/\352\262\260\352\263\274_\354\235\270\352\263\265\354\247\200\353\212\245.md" "b/docs/\352\262\260\352\263\274_\354\235\270\352\263\265\354\247\200\353\212\245.md"
new file mode 100644
index 0000000..5aba83b
--- /dev/null
+++ "b/docs/\352\262\260\352\263\274_\354\235\270\352\263\265\354\247\200\353\212\245.md"
@@ -0,0 +1,31 @@
+## 1. RAG 기술 활용
+
+⇒ 영화관 추천 전문 고객지원 챗봇으로 커스텀
+
+- LLM ChatGPT api를 활용한 응답 생성
+ - 사용자 질문에서 NER을 이용하여 Entity 추출
+ - 9월 11일에 3시 강남에서 에일리언 보고싶어.
+ - {date}: 2024-09-11 {time}: 15:00 {region}: 강남{movieName}:에일리언
+ - koBERT,kiwi를 이용한 RAG구축 후
+ - FAISS를 이용한 Semantic Search, 및 Levenshtein distance 기반 검색 기능 개발
+ - {movieName}:에일리언 -> 에일리언:로물루스
+ - LLM 응답 정형화
+- LLM ChatGPT api를 활용한 응답 생성
+
+ex) 오늘 판교에서 에이리언 보고 싶어
+
+## 2. ChatGPT API를 활용한 응답 생성
+
+1. 사용자의 질문에서 추출한 정보가 올바른지 사용자에게 다시 확인하는 기능
+- ex) ‘2024-09-09 18:00에 성남시 분당구에서 에이리언:로물루스를 보고 싶은 게 맞으신가요?
+- {date} {time}에 {region}에서 {movieName}을 보고 싶은 게 맞으신가요?
+
+1. 사용자의 다양한 질문 형식을 자동으로 인식, 일관된 형식으로 변환해 답변하는 기능
+- 시간/날짜 형식 전처리(YYYY-MM-DD, HH:MM)
+- 지역명 데이터 추가해 영화관 조회를 용이하게 함 ex) ‘판교’ → ‘경기도 성남시 분당구’
+
+1. 사용자가 선택한 날짜, 장소, 영화명을 기반으로 최적의 영화관, 영화스케줄을 추천해주는 답변 생성
+- 사용자 맞춤 영화관 추천
+ - 사용자가 입력한 위치 근처에서 교통 접근성이 좋은 영화관, 사용자가 보고 싶은 영화를 많이 상영하는 영화관을 추천
+- 영화관 주소 데이터를 추가: 영화관 근처 교통정보 제공
+ - 추천된 영화관 근처 지하철역에서 이동경로(몇 번 출구에서 도보로 몇 분), 지하철역이 없을 경우 버스 정류장 정보 제공
\ No newline at end of file
diff --git "a/docs/\352\262\260\352\263\274_\355\201\264\353\235\274\354\232\260\353\223\234.md" "b/docs/\352\262\260\352\263\274_\355\201\264\353\235\274\354\232\260\353\223\234.md"
new file mode 100644
index 0000000..c2dc8b9
--- /dev/null
+++ "b/docs/\352\262\260\352\263\274_\355\201\264\353\235\274\354\232\260\353\223\234.md"
@@ -0,0 +1,245 @@
+# 프로젝트 아키텍처
+
+![Movie-chatbot-architecture.png](./img/Movie-chatbot-architecture.png)
+
+# AWS 아키텍쳐 관련
+
+## 인프라 구성 방법
+
+Terraform을 활용해 직접 인스턴스들을 제어하지 않고, 코드로 생성 및 관리함
+
+- Terraform 코드를 모듈화 시켜 보기 한눈에 알아 보기 쉽게 구성함
+
+ ![screen 2024-09-10 오후 2.47.28.png](/img%2Fscreen%202024-09-10%20%EC%98%A4%ED%9B%84%202.47.28.png)
+
+- 개발 환경과 프로덕션 환경을 나누어서 불필요한 리소스 소비를 줄였음
+
+## 개발환경 세팅
+
+모니터링 및 개발 인스턴스를 별도로 생성하였음
+
+Ansible를 활용하여 여러 인스턴스의 환경 설정을 효율적으로 하였음
+
+- Ansible을 활용하여 Docker, Docker-compose, Node_exporter를 세팅 하였다.
+
+## 모니터링
+
+각 인스턴스에 Node_exporter를 실행해 CPU, 메모리 등 주요 리소스를 모니터링 함
+
+인스턴스의 역할에 맞게 cAdvisor와 mysqld_exporter를 추가로 실행해, 역할에 맞는 리소스를 추가로 모니터링 함
+
+- CI/CD
+
+github actions를 이용한 배포 과정
+
+1. dev version workflows 작성
+
+ ```jsx
+ name: Deploy MovieChatBot to AWS
+
+ on:
+ push:
+ branches:
+ - develop
+
+ env:
+ DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }}
+ DOCKER_HUB_PASSWORD: ${{ secrets.DOCKER_HUB_PASSWORD }}
+ AWS_REGION: ap-northeast-2
+
+ jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v3
+
+ - 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_SECRET_ACCESS_KEY }}
+ aws-region: ${{ env.AWS_REGION }}
+
+ # 2. JDK 설치
+ - name: Set up JDK 21
+ uses: actions/setup-java@v2
+ with:
+ java-version: '21'
+ distribution: 'adopt'
+
+ # 3. Gradle 빌드
+ - name: Build with Gradle
+ run: |
+ cd backend # gradlew 파일이 있는 디렉토리로 이동
+ ./gradlew build
+
+ - name: Login to Docker Hub
+ run: echo $DOCKER_HUB_PASSWORD | docker login -u $DOCKER_HUB_USERNAME --password-stdin
+
+ # Frontend 빌드 및 S3 배포
+ - name: Build and deploy Frontend
+ run: |
+ cd frontend
+ npm ci
+ echo "REACT_APP_ENDPOINT=${{ secrets.REACT_APP_ENDPOINT }}" >> .env
+ CI=false npm run build
+ aws s3 sync build/ s3://${{ secrets.S3_BUCKET }} --delete
+ aws cloudfront create-invalidation --distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} --paths "/*"
+
+ # Backend, AI 이미지 빌드 및 푸시
+ - name: Build and push Docker images
+ run: |
+ cd backend
+ docker build -t $DOCKER_HUB_USERNAME/backend:latest .
+ docker push $DOCKER_HUB_USERNAME/backend:latest
+
+ cd ../ai
+ docker build --build-arg OPENAI_API_KEY="${{ secrets.OPENAI_API_KEY }}" -t $DOCKER_HUB_USERNAME/ai:latest .
+ docker push $DOCKER_HUB_USERNAME/ai:latest
+
+ # Backend 서비스 배포 (AWS Systems Manager 사용)
+ - name: Deploy Backend services
+ run: |
+ aws ssm send-command \
+ --instance-ids ${{ secrets.BACKEND_EC2_HOST }} \
+ --document-name "AWS-RunShellScript" \
+ --parameters '{
+ "commands": [
+ "sudo docker stop backend ai || true",
+ "sudo docker rm backend ai || true",
+ "sudo docker image rm ${{ secrets.DOCKER_HUB_USERNAME }}/ai:latest",
+ "sudo docker image rm ${{ secrets.DOCKER_HUB_USERNAME }}/backend:latest",
+ "sudo docker pull ${{ secrets.DOCKER_HUB_USERNAME }}/backend:latest",
+ "sudo docker pull ${{ secrets.DOCKER_HUB_USERNAME }}/ai:latest",
+ "sudo docker run -d --name backend --network ec2-user_export_network -p 8080:8080 -e SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/moviedatabase?serverTimezone=Asia/Seoul -e SPRING_DATASOURCE_USERNAME=root -e SPRING_DATASOURCE_PASSWORD=qlalfqjsgh486 -e AI_SERVICE_URL=http://ai:8000/api/v1 ${{ secrets.DOCKER_HUB_USERNAME }}/backend:latest",
+ "sudo docker run -d --name ai --network ec2-user_export_network -p 8000:8000 -e PROJECT_NAME=ParseAI -e DATABASE_URL=mysql+aiomysql://root:qlalfqjsgh486@mysql:3306/moviedatabase ${{ secrets.DOCKER_HUB_USERNAME }}/ai:latest"
+ ]
+ }'
+
+ - name: Cleanup
+ if: always()
+ run: |
+ docker logout
+ rm -f /tmp/ec2_key
+ ```
+
+2. git repositroy push → docker build and push → ec2 인스턴스에서 docker pull → docker run 으로 배포 완료
+3. 배포 환경(main) workflows 추가 작성
+
+```jsx
+name: Deploy MovieChatBot to Main AWS
+
+on:
+ push:
+ branches:
+ - main
+
+env:
+ DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }}
+ DOCKER_HUB_PASSWORD: ${{ secrets.DOCKER_HUB_PASSWORD }}
+ BACKEND_EC2_INSTANCE: ${{ secrets.BACKEND_EC2_INSTANCE }}
+ BACKEND_EC2_INSTANCE2: ${{ secrets.BACKEND_EC2_INSTANCE2 }}
+ AWS_REGION: ap-northeast-2
+
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v3
+
+ - 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_SECRET_ACCESS_KEY }}
+ aws-region: ${{ env.AWS_REGION }}
+
+ - name: Set up JDK 21
+ uses: actions/setup-java@v2
+ with:
+ java-version: '21'
+ distribution: 'adopt'
+
+ - name: Build with Gradle
+ run: |
+ cd backend
+ ./gradlew build
+
+ - name: Login to Docker Hub
+ run: echo $DOCKER_HUB_PASSWORD | docker login -u $DOCKER_HUB_USERNAME --password-stdin
+
+ - name: Build and deploy Frontend
+ run: |
+ cd frontend
+ npm ci
+ echo "REACT_APP_ENDPOINT=${{ secrets.REACT_APP_ENDPOINT }}" >> .env
+ CI=false npm run build
+ aws s3 sync build/ s3://${{ secrets.S3_BUCKET }} --delete
+ aws cloudfront create-invalidation --distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} --paths "/*"
+
+ - name: Build and push Docker images
+ run: |
+ cd backend
+ docker build -t $DOCKER_HUB_USERNAME/backend:latest .
+ docker push $DOCKER_HUB_USERNAME/backend:latest
+
+ cd ../ai
+ docker build --build-arg OPENAI_API_KEY="${{ secrets.OPENAI_API_KEY }}" -t $DOCKER_HUB_USERNAME/ai:latest .
+ docker push $DOCKER_HUB_USERNAME/ai:latest
+
+ - name: Deploy Backend services
+ run: |
+ aws ssm send-command \
+ --instance-ids ${{ secrets.BACKEND_EC2_INSTANCE }} \
+ --document-name "AWS-RunShellScript" \
+ --parameters '{
+ "commands": [
+ "sudo docker stop backend ai || true",
+ "sudo docker rm backend ai || true",
+ "sudo docker image rm ${{ secrets.DOCKER_HUB_USERNAME }}/ai:latest",
+ "sudo docker image rm ${{ secrets.DOCKER_HUB_USERNAME }}/backend:latest",
+ "sudo docker pull ${{ secrets.DOCKER_HUB_USERNAME }}/backend:latest",
+ "sudo docker pull ${{ secrets.DOCKER_HUB_USERNAME }}/ai:latest",
+ "sudo docker run -d --name backend --network ec2-user_export_network -p 8080:8080 -e SPRING_DATASOURCE_URL=jdbc:mysql://${{ secrets.DATABASE_EC2_PRIVATE_IP }}:3306/moviedatabase?serverTimezone=Asia/Seoul -e SPRING_DATASOURCE_USERNAME=root -e SPRING_DATASOURCE_PASSWORD=qlalfqjsgh486 -e AI_SERVICE_URL=http://ai:8000/api/v1 ${{ secrets.DOCKER_HUB_USERNAME }}/backend:latest",
+ "sudo docker run -d --name ai --network ec2-user_export_network -p 8000:8000 -e PROJECT_NAME=ParseAI -e DATABASE_URL=mysql+aiomysql://root:qlalfqjsgh486@${{ secrets.DATABASE_EC2_PRIVATE_IP }}:3306/moviedatabase ${{ secrets.DOCKER_HUB_USERNAME }}/ai:latest"
+ ]
+ }'
+ env:
+ DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }}
+ DATABASE_EC2_PRIVATE_IP: ${{ secrets.DATABASE_EC2_PRIVATE_IP }}
+
+ - name: Deploy Backend 2 services
+ run: |
+ aws ssm send-command \
+ --instance-ids ${{ secrets.BACKEND_EC2_INSTANCE2 }} \
+ --document-name "AWS-RunShellScript" \
+ --parameters '{
+ "commands": [
+ "sudo docker stop backend ai || true",
+ "sudo docker rm backend ai || true",
+ "sudo docker image rm ${{ secrets.DOCKER_HUB_USERNAME }}/ai:latest",
+ "sudo docker image rm ${{ secrets.DOCKER_HUB_USERNAME }}/backend:latest",
+ "sudo docker pull ${{ secrets.DOCKER_HUB_USERNAME }}/backend:latest",
+ "sudo docker pull ${{ secrets.DOCKER_HUB_USERNAME }}/ai:latest",
+ "sudo docker run -d --name backend --network ec2-user_export_network -p 8080:8080 -e SPRING_DATASOURCE_URL=jdbc:mysql://${{ secrets.DATABASE_EC2_PRIVATE_IP }}:3306/moviedatabase?serverTimezone=Asia/Seoul -e SPRING_DATASOURCE_USERNAME=root -e SPRING_DATASOURCE_PASSWORD=qlalfqjsgh486 -e AI_SERVICE_URL=http://ai:8000/api/v1 ${{ secrets.DOCKER_HUB_USERNAME }}/backend:latest",
+ "sudo docker run -d --name ai --network ec2-user_export_network -p 8000:8000 -e PROJECT_NAME=ParseAI -e DATABASE_URL=mysql+aiomysql://root:qlalfqjsgh486@${{ secrets.DATABASE_EC2_PRIVATE_IP }}:3306/moviedatabase ${{ secrets.DOCKER_HUB_USERNAME }}/ai:latest"
+ ]
+ }'
+ env:
+ DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }}
+ DATABASE_EC2_PRIVATE_IP: ${{ secrets.DATABASE_EC2_PRIVATE_IP }}
+
+ - name: Cleanup
+ if: always()
+ run: |
+ docker logout
+```
+
+- 컨테이너 배포 후 연결 및 배포환경 api 연결
+
+ ALB를 사용하여 프라이빗 서브넷에 배포한 backend api를 연결하는 것으로 진행하였음.
+
+ - health check를 이용한 ALB 연결 확인 유무
+![image (2).png](/img%2Fimage%20%282%29.png)
\ No newline at end of file
diff --git "a/docs/\352\262\260\352\263\274_\355\222\200\354\212\244\355\203\235.md" "b/docs/\352\262\260\352\263\274_\355\222\200\354\212\244\355\203\235.md"
new file mode 100644
index 0000000..727ff1d
--- /dev/null
+++ "b/docs/\352\262\260\352\263\274_\355\222\200\354\212\244\355\203\235.md"
@@ -0,0 +1,54 @@
+- 크롤링
+ - **Kobis에서 제공하는 지역별 및 날짜별 상영 스케줄 정보를 크롤링**
+ - 7일치의 정보를 크롤링 + 새롭게 올라온 날짜의 상영 스케줄 크롤링
+ - 선택된 조건에 맞는 영화 상영 스케줄 정보 수집
+ - 광역 선택 -> 기초 선택 -> 영화관 선택 -> 날짜 선택 순서로 크롤링 절차를 진행하여 사용자가 3~4회의 버튼 클릭으로 영화 상영 스케줄 정보를 수집하도록 구현
+ - **DB 설계 및 데이터 저장**
+ - 크롤링한 상영 스케줄 데이터를 효율적으로 조회할 수 있도록 DB 테이블 설계
+ - 상영 정보 조회 속도와 데이터 정합성을 고려하여 영화관, 영화 테이블의 정보를 기준으로 상영정보를 조회하도록 함
+ ![erd.png](./img/erd.png)
+ - **크롤링 속도 개선을 위해 멀티프로세싱 적용**
+ - 여러 영화관의 상영 정보를 동시에 수집하여 시간 최적화
+ - 시간 최적화
+- 프론트엔드
+ - React를 사용한 사용자 인터페이스 구축
+ - React 라이브러리를 사용한 컴포넌트 기반 UI 설계 및 구축
+ - UI 모듈화를 통해 각 컴포넌트의 독립적 개발 및 유지보수 가능성 향상과 코드 재사용성 극대화
+
+ ![4.png](./img/4.png)
+ - **상태 관리 및 전역 상태 관리**
+ - useState를 활용한 동적 데이터(사용자 입력 값, 서버로부터 받은 데이터 등) 관리
+ - useEffect를 통한 컴포넌트 생명주기 기반 데이터 페칭 및 DOM 업데이트 처리
+ - useRef를 사용하여 최신 상태 유지와 즉시 참조 가능성 확보
+ - 상태 변경에 따른 자동 렌더링 및 코드 간결화로 유지보수성 향상
+ - Context API를 통한 전역 상태 관리 도입
+ - 중복된 상태 전달 없이 필요한 데이터에 직접 접근할 수 있도록 개선하여 코드 가독성 향상
+ ![3.png](./img/3.png)
+ - **백엔드 API와의 통신**
+ - 사용자 입력 기반 영화 정보(영화 이름, 지역, 날짜 등)를 처리하는 비동기 통신 구현
+ - fetch API를 사용하여 백엔드 서버와 통신하며, 실시간 상영 스케줄 정보 및 응답값 반환
+ - 네트워크 지연 없는 사용자 경험 최적화
+ ![2.png](./img/2.png)
+ ![1.png](./img/1.png)
+ - 핵심 기능
+ - 실시간 영화 상영 정보 제공: 사용자의 질문에 대한 응답 제공 및 사용자가 입력한 영화, 지역, 날짜 정보를 바탕으로 상영 시간 정보를 실시간으로 제공
+ - 상태 기반 UI 업데이트: 사용자 입력 및 백엔드 응답에 따른 UI 실시간 변경 처리. 지역 선택 시 입력값과 응답값에 따른 동적 업데이트.
+ - 유연한 필터링 및 데이터 처리: 영화, 지역, 날짜 등의 선택에 따른 유동적인 데이터 필터링 및 항목 변경 시 즉각 처리.
+
+ - lighthouse 지표
+![lighthouse.png](./img/lighthouse.png)
+
+- 백엔드
+ - RESTful API 설계에 대한 이해 및 적용 (설계 및 API 명세서 작성)
+ - Swagger를 사용한 API 명세서 작성 (설계 및 API 명세서 작성)
+ - BDDMockito, JUnit5를 사용한 단위 테스트 작성 (테스트 작성)
+ ![test.png](./img/test.png)
+ - ExceptionHandler을 통한 공통 예외 처리 (예외 처리)
+ - 전역에서 발생하는 예외를 한 곳에서 처리함으로써, 예외 처리 로직을 모듈화하고 유지보수성 높임
+ - 로깅을 통해, 모니터링과 디버깅이 수월
+ ![exception.png](./img/exception.png)
+ - Validation 과정을 통해 데이터 유효성 검증 (검증 과정 추가)
+ - Pattern, Size 지정을 통해 요청 형식 제한
+ - 데이터 무결성 보장 및 보안 강화
+ ![validate.png](./img/validate.png)
+ - AI Vectorize 과정 스케줄링
\ No newline at end of file
diff --git "a/docs/\355\212\270\353\237\254\353\270\224\354\212\210\355\214\205_\354\235\270\352\263\265\354\247\200\353\212\245.md" "b/docs/\355\212\270\353\237\254\353\270\224\354\212\210\355\214\205_\354\235\270\352\263\265\354\247\200\353\212\245.md"
new file mode 100644
index 0000000..2093855
--- /dev/null
+++ "b/docs/\355\212\270\353\237\254\353\270\224\354\212\210\355\214\205_\354\235\270\352\263\265\354\247\200\353\212\245.md"
@@ -0,0 +1,26 @@
+## chatgpt api를 활용한 최적의 응답 생성 방법
+
+문제: 서비스 품질을 유지하려면 정해진 형식에 맞게 출력되어야 하는데 llm특성 상 매번 조금씩 다른 답변이 생성됨
+
+테스트: python 코드로 vs 프롬프트 엔제니어링으로
+
+- 프롬프트: 원하는 답변이 나오기는 하지만 세세한 것까지 자연어로 지시해야 하고, 수정이 용이하지 않음
+- python 코드: 프롬프트로 페르소나만 설정 + python 활용해서 응답 형식을 제한하면 형식에는 맞지만 이번 단계에서는 불필요한 답변(‘예매를 도와드릴까요?‘ 등)까지 생성됨
+
+해결: 하이브리드 형식
+
+- python 조건문으로 답변 틀 생성하고 / 프롬프트로 페르소나&답변 생성 방향&제한사항(절대 티켓 예매 관련해서는 언급하지 마세요)등 간단하게 설정
+
+문제2:
+
+## 가공을 위한 정형화된 아웃풋
+
+LLM을 활용한 응답에서 정형화의 문제가 있음.
+
+해결: 구체적인 프롬프트 엔지니어링을 통해 응답의 정형화 가능하였지만 아쉬운 점이 존재하였으며, 추후 structured outputs기능을 통해 고정된 답변을 생성하면 더 좋을 것으로 예상됨
+
+## chatgpt api를 활용한 엔티티 추출 방식
+
+엔티티 추출 후 RAG에서 검색 시 FAISS 검색만 하였을 때 의미 기반 Semantic Search만 사용할시 영화 제목은 의미 기반 검색이 효율이 떨어짐,
+
+해결: 따라서 Levenshtein distance를 이용한 검색을 추가하여 Hybride 방식의 검색 방식으로 변경하였을 때 효율이 급상승함.
\ No newline at end of file
diff --git "a/docs/\355\212\270\353\237\254\353\270\224\354\212\210\355\214\205_\355\201\264\353\235\274\354\232\260\353\223\234.md" "b/docs/\355\212\270\353\237\254\353\270\224\354\212\210\355\214\205_\355\201\264\353\235\274\354\232\260\353\223\234.md"
new file mode 100644
index 0000000..e843de0
--- /dev/null
+++ "b/docs/\355\212\270\353\237\254\353\270\224\354\212\210\355\214\205_\355\201\264\353\235\274\354\232\260\353\223\234.md"
@@ -0,0 +1,152 @@
+# 1. Mysql 도커 이미지로 EC2에서 Endtrypoint 에러
+
+### 문제
+
+- 도커 이미지로 만든 Mysql 이미지를 EC2 인스턴스에서 실행시 아래 에러가 났다.
+- DB 이미지에 초기 테이블과 스키마를 설정해주기 위해 넣은 init.sql 파일의 권한 에러가 났다.
+
+```java
+[Note] [Entrypoint]: /usr/local/bin/docker-entrypoint.sh: running /docker-entrypoint-initdb.d/init.sql
+/usr/local/bin/docker-entrypoint.sh: line 75: /docker-entrypoint-initdb.d/init.sql: Permission denied
+```
+
+### 시도
+
+1. 처음 에러를 보고 든 생각은 /usr/local/bin의 디렉토리만 보고 도커와 Mount된 Host 디렉토리에 대한 권한이 없는 줄 알고 해당 컨테이너와 Mount된 볼륨 권한을 설정해줬지만 에러가 났다.
+2. 다시 에러 메시지를 살펴보니 이미지 생성 할때 넣어준 init.sql 파일이였다.
+ - init.sql을 로컬에서 생성해서 파일 읽기 권한이 root에만 있어서 도커가 실행되고 나서 접근할 수 없었다.
+
+### 해결
+
+```docker
+chmod 755 명령어를 dockerfile에 추가해 주었더니 성공 했다.
+```
+
+# 2. Python Crawling 이미지 생성 중 chrome browser 설치 문제
+
+### 문제
+
+- 풀스텍 팀원이 작성한 Python 코드를 인스턴스에 도커 이미지로 생성하려고 했다.
+ - 이전에 미리 코드 작성하면서 사용한 의존성들을 작성해달라고 했다.
+ - 아래와 같은 의존성들이 필요했다.
+
+ ```docker
+ selenium
+ python-dotenv
+ coverage
+ pymysql
+ schedule
+ apscheduler
+ cryptography
+ ```
+
+- 해당 의존성을 다 포함해서 이미지를 만들었지만, 코드가 작동이 되지 않았다.
+- Selenium은 chrome broswer를 사용하는데, 당연히 로컬 환경에서는 깔려있어서 고려를 하지 못했다.
+
+### 시도한 것
+
+- 우선 인스턴스에서 실행되는 도커에 bash로 접속하여, linux용으로 만들어진 chrome을 공식 홈페이지에서 wget으로 다운받아 코드를 실행 했더니 잘되어서 Dockerfile에 포함 시켜서 이미지를 빌드했다.
+ - RUN 명령어를 사용하여 설치하려고 했는데 수많은 에러가 났다.
+- CMD 명령어로 시도를 해봤지만, python 실행 명령어를 사용할 수 없었다.
+- shell 스크립트를 이미지에 포함해서 이미지를 만들어 실행했지만, 해결 할 수 없는 에러가 났다.
+
+### 해결
+
+**결국 python이외의 아무것도 추가하지 않은 이미지를 실행시키고 bash에 붙어서 필요한 환경을 구축하고 사용한 명령어 그대로 Dockerfile에 작성했다.**
+
+- 그랬더니 성공했다!
+
+```docker
+FROM python:3.12
+
+# 작업 디렉토리 설정
+WORKDIR /app
+
+# 루트 디렉토리의 모든 파일을 컨테이너의 /app 디렉토리로 복사
+COPY . /app
+
+RUN apt-get update
+RUN pip install -r requirements.txt
+RUN wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
+RUN apt-get install -y ./google-chrome-stable_current_amd64.deb
+
+ENTRYPOINT [ "python", "main.py" ]
+```
+
+# 3. 크롤링 인스턴스의 적절한 type 설정
+
+### 문제
+
+- 크롤링이 멀티프로세싱을 이용함.
+- 로컬에서 apple m2 pro 프로세서로 테스트시 하루치 데이터를 가져오는데 약 20분이 걸림
+ - 로컬보다 cpu 개수가 낮은 인스턴스는 적합하지 않다고 판단.
+ - 별다른 테스트 없이 4 cpu 8gb 인스턴스인 c6i.xlarge를 사용
+- 하지만 가격이 너무 비싸 인스턴스 조정을 하고 싶었음
+
+### 시도 한 것
+
+- 모니터링을 공부하고 구축한 상황이라 여러 인스턴스를 만들고 동시에 실행 시켜 모니터링을 시도했음
+- Promethues를 활용하여 1일치 데이터를 크롤링에 걸리는 시간과 리소스를 비교
+
+### 결과
+
+**c6i.large (2core, 4gb, 0.096$)**
+
+![c6i.large.png](./img/c6i.large.png)
+
+**t2.large (2core, 8gb, 0.11$)**
+
+![c6i.xlarge.png](./img/c6i.xlarge.png)
+
+**t2.xlarge (4core , 8gb, 0.23$)**
+
+![t2.large.png](./img/t2.large.png)
+
+**c6i.xlarge (4core, 8gb, 0.19$)**
+
+![t2.xlarge.png](./img/t2.xlarge.png)
+
+**CPU Core 개수와 램의 스펙에 상관없이 일정하게 약 25분이 걸렸다. 예상과는 다른 결과에 놀랐지만, 가격이 제일 저렴하고, 효율적으로 리소스를 사용하는 `c6i.large` 인스턴스를 사용하기로 결정**
+
+# 4. 크롤링 이외의 시간에 사용되지 않는 인스턴스
+
+### 문제
+
+- c6i.large는 기본적인 인스턴스(t2.micro, t2.small)들에 비해 가격이 높은 편
+- 하지만 사용되는 시간보다 사용되지 않는 시간이 더 많음
+- 필요할 때만 리소스를 사용할 수 있는 방법을 찾아야함
+
+### 시도한 것
+
+- AWS lambda + EventBridge 를 이용한 방법
+ - 하지만 labmda 함수는 최대 15분까지만 작동되는 한계점이 있어, 크롤링이 이 시간보다 더 많이 작동 되어 사용하지 못함.
+- AWS Fargate와 EventBridge를 사용하는 방법
+ - Container Image로 만든 크롤링 앱을 ECS 클러스터를 통해 Task로 정의하면, 정해진 시간에만 인스턴스를 활용하여 실행시킬 수 있음
+
+### 결과
+
+- 관련 지식 기반이 부족 및 시간 부족으로 인해 아직 구현하지 못했음.
+
+# **5. 인스턴스와 서브넷 등의 네트워크 관계에 대한 공부의 필요성**
+
+- 각 파트의 도커 이미지를 만들어 배포를 하기 전 까지는 수월하고 도커 컴포즈를 통한 로컬 테스트까지 순조롭게 진행하였음.
+- 하지만 아키텍쳐에 맞게 배포를 하는 과정에서 프라이빗 서브넷, 퍼블릭 서브넷 등의 인스턴스와 서브넷 등의 정확한 의미를 깨닫지 못하고 배포를 하는 과정에서 IP주소 밑 같은 대역의 연결이 맞는건지 헷갈리기도 했었음
+- 결국 네트워크에 관한 서칭으로 각 서브넷에 대한 관계를 깨닫고 다시 배포를 실시하였고 성공하였음.
+
+# **6. CI/CD는 모든 상황에서 필요한 것인가?**
+
+- 항상 코드를 푸시할때마다 배포를 하게 된다면 과연 좋을까?
+- 변경이 자주되는 코드(이미지)가 있는 반면에, 한 번 올려두면 절대 변하지 않을 코드(이미지)들도 존재한다. 따라서 항상 모든 이미지를 새로 배포하는 것이 아닌 프로젝트 특성을 고려해서 변경이 자주 되는 부분과 한 번 올려두면 끝인 부분을 구분해둬서 workflows를 구성하려고 하였다.
+
+# 7. Docker container 배포시 각 컨테이너의 연결 방법에 대한 고민
+
+- 이번 프로젝트는 인스턴스를 2개로 나누어 각 컨테이너가 사용하는 리소스 량에 따라 인스턴스 스펙을 나누어 배포를 실시하였습니다.
+- 이때 한 인스턴스에 backend, ai의 연결 관계에서는 docker의 network를 이용하여 연결을 진행하였습니다.
+- Docker network를 사용하지 않으면은 매 배포마다 다른 IP를 부여받아서 연결이 항상 잘 되지 않았던 문제가 있었는데 같은 인스턴스 내에서 배포를 하여 연결하는 과정에서는 docker network를 사용하여 연결하는 방법을 이용하였습니다.
+- 그렇다면 다른 인스턴스에서 연결 할 때, 서브넷이 다를때의 연결 방식 또한 다르다는 걸 알았고 그런 상황들에 대비하여 pipeline를 짤때 고려해야할 점이 더 있다는 것 또한 알게 되었습니다.
+- 컨테이너들 간의 연결 시 인스턴스의 사용, 서브넷 등의 의해 연결 방식 또한 방법이 나뉘는 것을 알게 되었습니다.
+
+1. HTTPS, HTTP 도메인 관련 허용
+- 일반적으로 안전한 사이트를 위해 HTTPS를 사용합니다.
+- HTTPS는 SSL/TLS 프로토콜을 사용하여 데이터를 암호화를 하게 됩니다.
+- SSL 인증서를 구입하고, 웹 서버에 설치하는 과정이 필요합니다. SSL 인증서는 공인된 인증 기관(CA)으로부터 구입할 수 있으며, 인증서의 종류에 따라 가격과 보안 수준이 다릅니다.
\ No newline at end of file
diff --git "a/docs/\355\212\270\353\237\254\353\270\224\354\212\210\355\214\205_\355\222\200\354\212\244\355\203\235.md" "b/docs/\355\212\270\353\237\254\353\270\224\354\212\210\355\214\205_\355\222\200\354\212\244\355\203\235.md"
new file mode 100644
index 0000000..b8eb71e
--- /dev/null
+++ "b/docs/\355\212\270\353\237\254\353\270\224\354\212\210\355\214\205_\355\222\200\354\212\244\355\203\235.md"
@@ -0,0 +1,119 @@
+---
+
+## 영화 상영 스케줄 크롤링 시간을 어떻게 단축할 수 있을까?
+
+문제1
+
+- 영화 상영 정보에 대한 하루치 데이터를 크롤링 하는데 120분이 걸리는 상황
+
+해결1
+
+- 멀티 프로세싱을 사용해 40분으로 단축 (3배)
+
+문제 2
+
+- 일주일치 데이터를 크롤링 하는데, 40 x 7 = 280분이 걸리는 상황
+- 추가 시간 단축을 위해 멀티스레딩 시도 했으나 context switching 문제 발생
+
+해결 2
+
+- 멀티프로세싱만 사용하기로 결정
+- 프로세스 최적화
+ - 최대 가용한 cpu 수에 맞게 프로세스 수 설정
+ - 크롤링 대상의 크기에 따라 프로세스 별 사이즈 설정
+- 새롭게 업데이트되는 하루치 데이터만 크롤링하기로 결정
+- ⇒25분으로 단축 (11배)
+
+---
+
+## 인공지능 Python 코드를 구동하기 위한, 효율적인 아키텍처를 어떻게 설계할까?
+
+### 문제1
+
+- 인공지능 팀에서 Python 코드를 작성하기 때문에, 이를 구동시키기 위한 방법이 고민되는 상황
+- 선택지
+ - Spring 애플리케이션에서 Jython을 사용해 구동
+ - Python 코드를 구동시키기 위한 서버를 따로 분리
+
+### 해결
+
+- Spring 애플리케이션에서 Jython을 사용해 구동하기로 결정
+- 서버를 분리할 시, 서버 간 통신과정 추가로 인해 지연 시간 증가 및 유지보수 비용 증가가 예상되었기 때문
+
+### 문제2
+
+- Jython이 Python 2.7까지 지원해서, 최신 라이브러리와의 호환성 문제
+- 외부 패키지를 설치하기 어려운 문제
+
+### 해결
+
+- 서버를 두 개로 분리해, FastAPI에서 구동해서 해결
+- 효과
+ - 책임 분산 및 확장성 개선
+ - AI 코드 실행에 대한 책임을 FastAPI가 맡음으로써, Spring 애플리케이션은 비즈니스 로직에 집중 가능하고, 이로 인해 확장성 및 유지보수성이 상승함
+ - 장애 격리로 안정성 강화
+ - AI 코드 실행이 리소스를 많이 소모하기 때문에, 이로 인한 장애 발생 시 Spring 애플리케이션에 문제 전염되는 것을 차단
+ - 성능 및 처리 효율성 증가
+ - FastAPI는 비동기 처리를 잘 지원해서, AI 작업을 보다 빠르고 효율적으로 처리 가능
+
+---
+
+## 여러 객체에 흩어져있는 정보를 어떻게 한 번에 묶을 수 있을까?
+
+### 문제
+
+- 영화 상영 정보, 영화관 객체를 사용해 영화관 별 상영정보를 얻어야 하는 상황
+
+### 해결
+
+- stream의 groupingBy, mapping을 사용해 해결
+
+```java
+Map> timesPerTheaterNameMap = dto.stream()
+ .collect(
+ groupingBy(
+ d -> d.getTheater().getName(),
+ mapping(d -> d.getMovieInfo().getTime(), toList())
+ )
+ );
+```
+
+---
+
+## 영화 상영 스케줄 크롤링 시간을 어떻게 단축할 수 있을까?
+
+### 문제1
+
+- 영화 상영 정보에 대한 하루치 데이터를 크롤링 하는데 120분이 걸리는 상황
+
+### 해결1
+
+- 멀티 프로세싱을 사용해 40분으로 단축 (3배)
+
+### 문제 2
+
+- 일주일치 데이터를 크롤링 하는데, 40 x 7 = 280분이 걸리는 상황
+- 추가 시간 단축을 위해 멀티스레딩 시도 했으나 context switching 문제 발생
+
+### 해결 2
+
+- 멀티프로세싱만 사용하기로 결정
+- 프로세스 최적화
+ - 최대 가용한 cpu 수에 맞게 프로세스 수 설정
+ - 크롤링 대상의 크기에 따라 프로세스 별 사이즈 설정
+- 새롭게 업데이트되는 하루치 데이터만 크롤링하기로 결정
+- ⇒ 25분으로 단축 (11배)
+
+---
+
+## 상태의 변경 사항을 즉시 반영할 수 없을까?
+
+### 문제
+
+- React에서 상태 변경 시 useEffect만으로는 최신 상태 반영 불가
+- 변경된 값을 다른 곳에서 참조하면 최신 값이 아닌 이전 값이 참조되어 데이터 오류 발생
+
+### **해결**
+
+- useEffect는 상태 변경을 감지하지만, 다른 함수나 이벤트에서 최신 상태값을 바로 참조할 수 없는 문제가 발생함을 파악
+- 이를 해결하기 위해 useRef를 함께 사용하여 상태 변경 사항이 즉시 데이터에 반영되도록 구현
\ No newline at end of file
diff --git "a/docs/\355\232\214\352\263\240.md" "b/docs/\355\232\214\352\263\240.md"
new file mode 100644
index 0000000..33d9765
--- /dev/null
+++ "b/docs/\355\232\214\352\263\240.md"
@@ -0,0 +1,97 @@
+### ‘성과’ 측면 (개발 달성도, 완성도 등)
+
+- Eddy
+ - 아쉬운 점
+ - 뭔가 온전히 집중하는 프로젝트의 느낌이 아니었던 느낌.. 대면으로 했었으면 더 좋았을거같다.
+- Yohan
+ - 잘한 점
+ - 계획한 최소 기능 구현 완료
+ - 아쉬운 점
+ - 실 서비스 사용 테스트를 통해 예외 처리 개선 필요
+- Ryan
+ - 잘한 점
+ - 계획한 최소 기능 구현 완료
+ - 아쉬운 점
+ - 아직 부족한 기능 mvp단계의 초기에서 멈춰서 아쉽다.
+- Mir
+ - 잘한 점
+ - 처음으로 웹 크롤링을 도전하여, 데이터를 성공적으로 수집하고 DB에 저장하는 과정을 **완성**
+ - 새로운 기술을 학습하고 실제로 적용하여 결과를 낸 것에 대해 성취감
+ - 아쉬운 점
+ - 프로젝트 초기에 설계한 대로 진행했으나, 기능이 추가되거나 변경되는 과정에서 **설계 수정이 필요했음. 이로 인해 코드가 덧붙여지면서 최종적으로 구현된 구조가 다소 아쉬웠음.**
+ - 처음부터 최종적인 기능 요구 사항을 더 명확히 설계했다면 더욱 깔끔한 구조를 유지할 수 있었을 것이라 생각
+- Bryan
+ - 잘한 점
+ - 소규모 프로젝트에서도 최소한의 가용성을 가진 인프라를 구축한 것
+ - **개발 환경과 프로덕션 환경을 나누어 개발 한것**
+
+- Alyssa
+ - 😃 **ChatGPT API를 다양한 방식으로 활용, 매번 일정한 답변을 생성하는 데 성공 → 서비스 품질 향상**
+ - 🥲 기획 단계에 시간을 좀 더 쏟았다면 하는 아쉬움
+
+### ‘배움’ 측면 (배운 것, 향후 실무 활용 가능 정도 등)
+
+- Yohan
+ - 잘한 점
+ - 공통 예외 처리 및 Validation에 대한 학습 및 도입을 통해 보다 안정성 있는 서비스 구현
+ - 아쉬운 점
+ - Docker 등 클라우드에 대한 학습 및 이해 필요
+- Ryan
+ - 잘한 점
+ - AI-백엔드-프론트엔드/클라우드의 흐름과, Fast API와 같은 기술의 습득 및 langchain을 이용한 LLM가공 기술 획득
+ - 아쉬운점
+ - 백엔드와 프론트엔드 클라우드 기술의 흐름은 알았지만 아직 학습과 이해가 부족함.
+- Alyssa: 😃 백엔드-AI-프론트엔드 등 **서버 간 데이터의 흐름**과 협업 방식을 습득 → 향후 실무에 적용 가능
+- Eddy : CI/CD에 관한 설계 방식 및 효율성을 따지고 보게 됨
+- Mir
+ - 잘한 점
+ - 하나의 기능을 처음부터 끝까지 구현해 보는 경험
+ - 프로젝트에서 특정 기능이 전체 구조와 어떻게 연결되는지, 그리고 다른 파트와 어떻게 협력하는지에 대한 깊은 이해를 함
+ - 아쉬운 점
+ - 새로운 기술을 배우면서 구현했지만, 내가 선택한 방식이 최선인지에 대한 피드백을 받을 기회가 부족했음.
+ - 좀 더 다양한 시각에서 논의하고 검토할 수 있었다면, 기술적으로 더 나은 선택을 할 수 있었을 것이라는 아쉬움이 있음.
+- Bryan
+ - 잘한 점
+ - 전체적인 AWS 인프라를 설계하는 법, Ec2, Vpc, S3 등 기본적인 AWS 서비스들을 이해할 수 있었다.
+ - **Terraform 과 Ansible을 활용하여 효율적으로 인프라를 구성하고 환경을 만드는 것을 배웠다.**
+ - 아쉬운 점
+ - **모든 것이 처음이고 배우면서 했기에, 얇고 넓게 배운거 같아 아쉽다.**
+ - 실무에서는 이정도 규모가 아닌 몇배 몇십배는 큰 환경을 관리해야할텐데 어떻게 관리하는지에 대한 궁금증만 커져갔다.
+
+### ‘협업’ 측면 (팀 운영 관련 등)
+
+- Yohan
+ - 잘한 점
+ - MVP 도입을 통해, 작은 목표에 집중해 빠르게 개발한 점
+ - 매일 스크럼을 통해, 진행상황 공유한 점
+ - 아쉬운 점
+ - 다른 팀원이 간편하게 테스트할 수 있는 환경 구성 필요
+ - 테스크 관리 및 문서화 할 수 있는 환경 필요 - Jira
+- Ryan
+ - 잘한 점
+ - 매일 짧게 회의하고 진행 상황을 공유한 것은 전반적인 상황을 팔로우하기 좋았음.
+ - 첫 프로젝트라 PR 발표가 매우 좋았음.
+ - 아쉬운 점
+ - 첫 프로젝트라 자기 객관화가 부족하여 일정의 딜레이가 아쉬움.
+ - 문서화가 필요함.
+- Bryan
+ - 잘한점
+ - 팀 프로젝트를 처음 하는 팀원도 있고, 개발이 처음이 팀원도 있었지만, 서로 부족한 부분을 채워주며 함께 성장했고 완벽하지 않지만 프로젝트를 순탄하게 진행했음
+ - **매일 데일리 스크럼과 회의를 하며 서로의 상황을 개발 진척도나 트러블을 나누었던 것이 너무 좋았음.** 이를 통해 팀이 방향을 잃지 않고 한 방향으로 올 수 있었던거 같음
+ - 팀원 개인이 가진 능력(문서화, 내용 정리, 본질 잃지 않는 질문 등)이 매우 조화로웠음.
+ - 아쉬운점
+ - 처음 설계를 하고 시작했지만, 모두 낯선 환경에 세밀한 설계가 되지 않아 **프로젝트 진행 중간에 많이 변경했다는 점이 아쉽다.** 아무것도 모르고 시작했기에 당연한 결과였기에 마냥 아쉽지 않고 이를 통해 배운게 많아서 오히려 좋다.
+- eddy
+ - 잘한 점
+ - 매일 짧게라도 회의를 하여 진행 상황을 공유한 것
+ - 아쉬운 점
+ - Jira 등의 진행 상황에 대한 문서화가 필요했을 것 같다. (실제로 세세한 진행상황을 잘 정리해두지 않아서 나중에 정리할때 헷갈리지 않을까…)
+- Mir
+ - 잘한 점
+ - **매일 회의**를 통해 팀원들과 진행 상황을 공유하고, 프로젝트에서 변경사항이나 문제에 대해 즉각적으로 논의한 점
+ - 아쉬운 점
+ - 시간 제약으로 인해 다른 팀원들의 코드를 완벽하게 이해하지는 못한 점
+- Alyssa
+ - 😃 매일 스크럼을 진행해 서로 진행 상황과 문제점을 공유한 것
+ - 😃 애자일 방식을 도입해 개발/비개발 모든 측면에서 문제점이 있으면 → **바로 논의해 즉각적으로 수정하는 태도를 유지한 것**
+ - 🥲 각 스프린트가 원래 계획했던대로 기획→개발→테스트, 피드백→회고가 한 스프린트 내에 이뤄졌으면 좋았을 것 같다. 우리 팀에게 적절한 스프린트 주기를 찾는 과정이었다고 생각하는데, 이**번 프로젝트를 통해 찾은 적절한 주기를 앞으로 더 테스트해볼 수 없어 아쉽다.**
\ No newline at end of file