프롤로그 - 1학기를 회고하며
2023년까지는 코딩에 코자도 모르는 비전공 문과생이었던 나였지만 우연히 싸피 공고를 본 후 11기로 입과하여 약 5개월 동안 코딩(프로그래밍)에 대해 배우게 되었다. 솔직히 말해서 싸피에 합격한 후에 입과할지 말지에 대해 정말 많은 고민이 있었고 심지어 입과 하루전까지도 잠을 제대로 못 잘만큼 고민을 많이 했었다. 왜냐하면 기존에 국제통상학, 경제학을 복수전공을 하여 졸업을 하게 되었는데 4년동안 배웠던 전공을 뿌리치고 다시 새로운 것에 도전하는 것에 대한 두려움이 가득했었기 때문이다.
어떤 두려움이었냐면 첫째, 남들은 바로 취업준비를 하고 있는데 나는 다시 새로 시작해야 한다는 두려움과 둘째, 코딩을 배워서 과연 내가 취업에 성공할 수 있을지에 대한 두려움들이었다. 하지만 이런 고민을 끝낼 수 있었던 이유가 첫째, 나중에 싸피를 입과하지 않은 것에 대한 후회와 둘째, 내가 싸피에 들어간다고 했을 때 응원해주었던 주변 사람들의 응원에 대한 기대를 저버리고 싶지 않았고 셋째, 도전에 대한 두려움을 없애고 싶다는 이러한 이유들 때문에 싸피에 입과하게 되었다.
싸피에 입과한 후 난생 처음 배우는 파이썬이라는 언어에 파이썬을 활용하여 문제 해결 능력을 위한 알고리즘 문제를 풀고 웹을 만드는 HTML, CSS, Django, Vue3에 대해 정신없이 배우니 어느덧 5월 16일 1학기 관통 프로젝트가 시작되었다.
본문 - 싸피 1학기 관통 프로젝트(금융)
싸피 1학기의 관통 프로젝트는 트랙이 2개로 나뉘어져 있는데 첫째, 영화 트랙과 둘째, 금융 트랙으로 나누어져 있다. 나는 영화보다는 금융쪽이 더 흥미가 있었고 은행권에 취업도 어느 정도 희망하고 있기 때문에 금융 트랙을 선택하였다. 프로젝트는 조별 컨셉에 따라 주어진 필수 요구사항을 진행한 후 자율적으로 추가 기능을 구현하는 프로젝트였다. 음 결론부터 말하자면 첫 프로젝트부터 안좋은 결과물을 내어서 기분이 좋지 않았다.
소통과 편의성에 중점을 맞춘 서비스
주로 프론트와 백을 나누기 보다는 기능별로 작업을 수행하였지만 실제 일정에 따라 역학 조율을 하여 프론트와 백을 나누어서 한 부분도 있었다.
하지만 기능별로 겹친 비율이 적기 때문에 주로 담당한 파트를 기준으로 작성하였다.
우선 사용한 기술에는 프론트 엔드에는 Vue3와 Bootstrap을 사용하였고 백엔드에는 Django와 DRF를 사용하였으며 챗봇 구현에는 openAI API를 프론트 엔드에서 백엔드로 요청을 보내는 것은 axios를 마지막으로 백엔드에서 가져온 데이터를 중앙에서 저장하고 관리하기 위해 pinia를 사용하였다.
class User(AbstractUser):
phone_number = models.CharField(max_length=30)
name = models.CharField(max_length=50)
gender = models.CharField(max_length=10)
birth_date = models.DateField(null=True)
assets = models.IntegerField(default=0)
def __str__(self):
return self.username
class CustomLoginSerializer(LoginSerializer):
username = serializers.CharField(allow_blank=True)
email = None
메인페이지에서는 총 3개의 블록으로 구성하였는데 우선 페이지의 중앙에는 프로젝트의 분위기를 나타내기 위하여 carousel를 이용해 3개의 이미지가 일정시간이 지나면 바뀌도록 설계를 하였고 오른쪽 하단의 블록에는 현재의 조회수 순으로 현재 인기글을 출력하도록 구성하였다. 마지막으로 왼쪽 하단 블록에는 챗봇 대화창을 넣었다.
보통의 사이트에는 오른쪽 하단에 고정적으로 작은 버튼을 펼쳐야 보이기 때문에 사용자의 눈에 잘 안보일 수 있기 때문에 메인페이지에 배치를 하여 메인 페이지에 들어오자마자 사용자들이 챗봇의 존재를 바로 확인할 수 있도록 설계를 하였다.
원래는 메인페이지에 공지사항 및 경제 기사 칸도 마련하려고 했으나 일정상 구현하지 못하겠다고 판단하여 아쉽게도 빼고 진행하였다. 이 부분에서 정말 아쉬웠다.
# 데이터 저장 뷰 함수
# 정기 예금 baseList와 optionList 저장
@api_view(['GET'])
def deposit_save(request):
URL = BASE_URL + 'depositProductsSearch.json'
params = {
'auth' : settings.API_KEY,
'topFinGrpNo' : '020000',
'pageNo' : 1,
}
response = requests.get(URL, params=params).json()
BaseList = response.get('result').get('baseList')
OptionList = response.get('result').get('optionList')
for product in BaseList:
fin_co_no = product.get('fin_co_no')
dcls_month = product.get('dcls_month')
fin_prdt_cd = product.get('fin_prdt_cd')
kor_co_nm = product.get('kor_co_nm')
fin_prdt_nm = product.get('fin_prdt_nm')
join_way = product.get('join_way')
mtrt_int = product.get('mtrt_int')
spcl_cnd = product.get('spcl_cnd')
join_deny = product.get('join_deny')
join_member = product.get('join_member')
etc_note = product.get('etc_note')
max_limit = product.get('max_limit')
dcls_strt_day = product.get('dcls_strt_day')
dcls_end_day = product.get('dcls_end_day')
fin_co_subm_day = product.get('fin_co_subm_day')
if max_limit == None:
max_limit = 0
if dcls_end_day == None:
dcls_end_day = '없음'
if not DepositProducts.objects.filter(
fin_prdt_cd = fin_prdt_cd
).exists():
product_data = {
'fin_co_no' : fin_co_no,
'dcls_month' : dcls_month,
'fin_prdt_cd' : fin_prdt_cd,
'kor_co_nm' : kor_co_nm,
'fin_prdt_nm' : fin_prdt_nm,
'join_way' : join_way,
'mtrt_int' : mtrt_int,
'spcl_cnd' : spcl_cnd,
'join_deny' : join_deny,
'join_member' : join_member,
'etc_note' : etc_note,
'max_limit' : max_limit,
'dcls_strt_day' : dcls_strt_day,
'dcls_end_day' : dcls_end_day,
'fin_co_subm_day' : fin_co_subm_day,
}
serializer = DepositProductListSerializers(data=product_data)
if serializer.is_valid(raise_exception=True):
serializer.save()
for option in OptionList:
dcls_month = option.get('dcls_month')
fin_co_no = option.get('fin_co_no')
fin_prdt_cd = option.get('fin_prdt_cd')
intr_rate_type = option.get('intr_rate_type')
intr_rate_type_nm = option.get('intr_rate_type_nm')
save_trm = option.get('save_trm')
intr_rate = option.get('intr_rate')
intr_rate2 = option.get('intr_rate2')
deposit_product = DepositProducts.objects.get(fin_prdt_cd=fin_prdt_cd)
if intr_rate == None:
intr_rate = 0
if intr_rate2 == None:
intr_rate2 = 0
option_data = {
'dcls_month' : dcls_month,
'fin_co_no' : fin_co_no,
'fin_prdt_cd' : fin_prdt_cd,
'intr_rate_type' : intr_rate_type,
'intr_rate_type_nm' : intr_rate_type_nm,
'save_trm' : save_trm,
'intr_rate' : intr_rate,
'intr_rate2' : intr_rate2,
}
serializer = DepositOptionsListSerializers(data=option_data)
if serializer.is_valid(raise_exception=True):
serializer.save(deposit_product=deposit_product)
return JsonResponse({'message' : '저장완료'})
이러한 원리를 이용해 프론트엔드에서 상품별 비교 페이지로 접속을 하면 onMounted를 이용해 store에 설정해둔 함수를 실행시켜 axios 요청을 통해 DB에 저장을 하도록 하였고 이렇게 저장된 데이터들을 다시 불러와 리스트에 저장을 시켰으며 이 리스트들을 상품별 비교 페이지로 불러와 출력을 하였다.
상품별 전체 조회는 데이터가 많기 때문에 스크롤 바를 구현하여 사용자가 굳이 페이지의 스크롤을 안 내리고 이용할 수 있도록 구성하였고 이자율의 6개월, 12개월, 24개월, 36개월에 해당하는 데이터를 가져올 때 이미 상품과 옵션을 따로 저장을 해놨기 때문에 역참조를 하여 가져오려고 했으나 옵션의 구조상 한 옵션 정보에 여러 기간의 이자율의 정보가 있는 것이 아닌 기간별로 따로 분리 되어 있기 때문에 이를 찾아서 가져오기가 까다로워서 직접 데이터를 가공하여 가져왔다.
from django.db.models.fields.related import ManyToManyField
def to_dict(instance):
opts = instance._meta
data = DotDict()
for f in opts.concrete_fields + opts.many_to_many:
if isinstance(f, ManyToManyField):
if instance.pk is None:
data[f.name] = []
else:
data[f.name] = list(f.value_from_object(instance).values_list('pk', flat=True))
else:
data[f.name] = f.value_from_object(instance)
return data
class DotDict(dict):
# obj.key
def __getattr__(self, key):
try:
return self[key]
except KeyError:
return self.key
def __setattr__(self, key, value):
self[key] = value
# 정기예금 base_list 보내기
@api_view(['GET'])
def deposit_base_list(request):
deposits = list(map(to_dict, DepositProducts.objects.all()))
options = list(map(to_dict, DepositOptions.objects.all()))
data = []
for deposit in deposits:
# 6개월, 12개월, 24개월, 36개월 이자율
trm = [0, 0, 0, 0]
for option in options:
if option.fin_prdt_cd == deposit.fin_prdt_cd:
intr_1 = 0
intr_2 = 0
if option.save_trm == "6":
if option.intr_rate != 0:
intr_1 = option.intr_rate
if option.intr_rate2 != 0:
intr_2 = option.intr_rate2
if intr_1 == 0 and intr_2 == 0:
trm[0] = 0
else:
trm[0] = round((intr_1 + intr_2) / 2, 2)
if option.save_trm == "12":
if option.intr_rate != 0:
intr_1 = option.intr_rate
if option.intr_rate2 != 0:
intr_2 = option.intr_rate2
if intr_1 == 0 and intr_2 == 0:
trm[1] = 0
else:
trm[1] = round((intr_1 + intr_2) / 2, 2)
if option.save_trm == "24":
if option.intr_rate != 0:
intr_1 = option.intr_rate
if option.intr_rate2 != 0:
intr_2 = option.intr_rate2
if intr_1 == 0 and intr_2 == 0:
trm[2] = 0
else:
trm[2] = round((intr_1 + intr_2) / 2, 2)
if option.save_trm == "36":
if option.intr_rate != 0:
intr_1 = option.intr_rate
if option.intr_rate2 != 0:
intr_2 = option.intr_rate2
if intr_1 == 0 and intr_2 == 0:
trm[3] = 0
else:
trm[3] = round((intr_1 + intr_2) / 2, 2)
for i in range(4):
if i == 0:
setattr(deposit, "6", trm[0])
elif i == 1:
setattr(deposit, "12", trm[1])
elif i == 2:
setattr(deposit, "24", trm[2])
else:
setattr(deposit, "36", trm[3])
data.append(deposit)
return Response(data=data, status=status.HTTP_200_OK)
이러한 방식으로 데이터를 가공하여 데이터를 보내니 내가 원하는 형태의 깔끔한 데이터가 프론트쪽으로 전달이 되었다.
상품 상세 조회에서는 ModelSerializer와 view 함수를 통해 상세 조회를 할 수 있도록 설계하였으며 프론트 엔드에서는 기존에 목록을 조회할 때 사용했던 store에 저장되어있는 각 상품별 리스트를 가져와 find를 통해 값을 찾아서 출력을 하였다. 그 다음 가입하기 기능을 구현하기 위해 백엔드에 포트폴리오 모델과 serializer를 만들어 사용자가 선택한 상품이 저장되도록 구현을 하였다. 그리고 중복 가입을 막기 위해 store에 포트폴리오 리스트를 만들어서 포트폴리오 데이터를 저장한 후 이 리스트를 다시 컴포넌트로 가져와 가입하려는 상품의 데이터가 리스트에 있으면 이미 가입된 상품이라고 alert를 띄우고 없으면 가입에 성공되었다고 alert를 띄웠다. 또한 가입하기 버튼은 store에 저장된 로그인 정보를 이용해 로그인이 되어 있을 경우에만 가입하기 버튼이 들 수 있도록 설계를 하였다.
시간이 조금 더 있었다면 도전과제인 메일 서비스를 구현할 수 있었을텐데 그것을 구현하지 못하여 아쉬웠다.
커뮤니티 기능으로는 기본적인 게시글 조회, 생성, 삭제 수정 및 댓글, 삭제 기능을 django와 vue3를 통해 구현하였으며 좋아요와 대댓글은 기본적으로 명세서에 주어진 과제들을 먼저 구현하고 이후에 진행하려고 하였으나 디테일한 부분을 같이 진행하려고 하는 바람에 일정이 밀리게 되어 구현하지 못했습니다.
커뮤니티의 카테고리로는 사용자들이 자유로운 주제로 작성할 수 있도록 자유 게시판, 사용자들끼리 재테크 정보를 공유할 수 있도록 하는 재테크 공유 게시판, 그리고 문의사항 등을 받기 위해 고객과의 소통 게시판으로 나누었습니다. 추가로 글을 쓰는데에 좀 더 편의성을 제공하기 위해 자유게시판 관련 글을 쓰다가 다시 주제를 바꾸어 올리고 싶을 때를 대비하여 카테고리 선택 항목을 추가로 배치해 주제를 쉽게 변경할 수 있도록 구현하였습니다.
그리고 본인이 작성한 게시글의 삭제와 수정 그리고 댓글 삭제를 구현하기 위해 store에 저장된 로그인 정보를 이용해 게시글의 user_id와 댓글의 user_id를 비교하여 같으면 띄우고 같지 않으면 보이지 않도록 하였습니다.
여기서부터는 차상곤 교육생이 진행한 파트입니다.
프로필 페이지에서는 기본 정보와 가입한 상품의 포트폴리오 정보를 볼 수 있도록 구현하였고 차트 라이브러리를 사용하려고 했으나 일정 이슈로 구현을 하지 못했습니다. 다만 회원가입을 할때에 입력 받은 자산정보를 바탕으로 포트폴리오에 담은 상품 별로 12개월 이자율의 정보를 가져와 현재 나의 자산에서 이자율을 통해 계산을 하여 1년 후 나의 자산 정보를 띄우도록 구현하였습니다.
그리고 회원 정보를 수정할 수 있도록 게시글 수정과 같은 로직을 통해 구현하였습니다.
기능 : 한국 화폐 <-> 외국 화폐로의 환전
onMounted 기능을 활용해서 환율 페이지로 접속시 환율 데이터 API요청을 보내어 DB에 저장하게 됩니다. 이 때, 이미 정보가 존재한다면 다시 요청을 보내지 않게 처리해주어야 오류가 발생하지 않습니다.
input 이벤트가 장착된 input 태그의 값에 변동이 생길 때마다 select의 value 값으로 지정된 국가화폐코드 (미국의 경우 USD)를 이용해서 get axios요청을 보내고, Back-End에서 Exchange 모델의 환율을 조회합니다.
환율과 variable routing을 통해 보내진 돈의 금액을 곱(또는 나눗셈)을 통해서 계산된 값을 반환해서 출력합니다.
어려웠던 점 : 한국 화폐 <-> 외국 화폐로의 환전 기능만을 구현했습니다. 위의 기능을 구현했다면 외국 화폐 -> 외국 화폐의 기능도 구현하기는 매우 쉽지만(그냥 한국 돈으로 환전하는 한 번의 과정만 더 거치면 됩니다.), 부정확도가 매우 높을 것 같아서 굳이 안 하는 게 낫다고 판단해서 하지 않았습니다. value 값은 실제로 "디르함 (AED)"와 같은 형태로 적혀 있었는데, 디르함은 한국어로 읽었을 때의 화폐단위인데, 달러,엔,유로 등과 같이 널리 알려진 화폐의 단위 외에는 대중들이 모를 가능성이 높기 때문에 단위에 같이 적어주었어야 했습니다.
그런데 이 부분이 조금 문제가 발생했습니다. 띄어쓰기를 같이 varial routing에는 활용이 바로 할 수 없었는데 인코딩(띄어쓰기->%20)-디코딩(%20->띄어쓰기)이 필요했습니다.
그러나 검색을 해보니 JS문법에서도 파이썬의 split과 비슷한 문법이 있었고, 그 덕분에 AED만을 추출해서 변수로 넘길 수 있었고, value 값은 그대로 화폐의 단위로 표시할 수 있었습니다.
기능 : 원하는 위치의 특정(또는 모든) 은행 검색 기능 & 현재 위치 근처의 특정(또는 모든) 은행 검색 기능
동작 방식
데이터 초기화 및 컴포넌트 마운트 : 데이터 초기화 및 컴포넌트 마운트될 때 카카오 맵 스크립트를 로드하여 지도를 초기화합니다.
시/도, 시/군/구 선택 시 목록 업데이트 : 사용자가 시/도를 선택하면 해당 시/도의 시/군/구 목록을 업데이트 합니다. 사용자가 시/군/구를 선택하면 해당 시/군/구의 읍/면/동 목록을 업데이트합니다.
카카오 스크립트 로드 : 카카오 맵 API 스크립트를 동적으로 로드하고, 로드가 완료되면 지도를 초기화합니다.
지도 초기화 : 지도를 초기화하고, 장소 검색 객체를 생성합니다.
지정한 위치의 은행 검색 : 사용자가 선택한 옵션들을 기반으로 검색 쿼리를 생성하고, 해당 위치의 은행을 검색합니다. 이 때, 검색 결과 중에서 은행 이름이 포함된 항목만 필터링하여 표시합니다.
현재 위치 근처의 은행 검색 : 사용자의 현재 위치를 가져와서 해당 위치를 중심으로 반경 2km이내의 은행을 검색합니다. 검색 결과를 지도에 마커로 표시하고, 마커를 클릭하면 인포윈도우(은행의 이름을 표시하는 창)를 추가합니다.
지도에 표시된 모든 마커와 인포윈도우 제거 : 새로운 검색을 하면 기존에 지도에 표시돼 있던 마커와 인포윈도우를 모두 제거합니다.
어려웠던 점
기능 : 메시지 입력 및 전송 & 메시지 저장 및 표시 & Open API 호출
동작 방식
메시지 입력 및 전송 : 사용자는 텍스트 입력란에 메시지를 입력하고, Enter 키를 누르거나 '전송' 버튼을 클릭하여 메시지를 전송합니다.
메시지 저장 및 표시 : 입력된 메시지는 messages 배열에 저장되고, addMessage 함수를 통해 화면에 표시됩니다.
OpenAI API 호출 : sendMessage 함수는 사용자가 입력한 메시지를 OpenAI API에 전달하여 챗봇의 응답을 가져옵니다. fetchAIResponse 함수고 API 호출을 처리하며, 응답 데이터를 받아와 addMessage 함수를 통해 챗봇의 응답을 저장하고 표시합니다.
채팅 인터페이스 : 메시지들을 스크롤 가능한 형식으로 표시했고, 구분이 쉽게 색깔을 주었습니다.
프롬프트 : system(챗봇의 동작 방식과 성격을 설정하는 시스템 메시지), user(사용자가 입력한 메시지), assistant(챗봇이 응답하는 메시지), 'role'과 'content'라는 두 가지 속성으로 챗봇이 어떻게 응답해야 할지를 결정(또는 학습 시키는 것), 챗봇의 역할과 GG Bank의 서비스 (투자 상품 비교, 환율 계산, 은행 찾기 등)를 설명하도록 설정
기능 : 사용자의 예금 및 적금 상품을 추천
동작 방식
사용자 입력 및 API 요청 전송 성별, 나이, 연봉, 자산, 저축 성향을 입력하고, 검색 버튼을 눌러서 django 서버로 API 요청을 보냅니다.
사용자의 위험 감수 성향 평가 : 사용자가 입력한 데이터를 바탕으로 점수를 계산하고, 그에 따라 등급을 평가합니다.
금융 상품의 위험 등급 평가 : DB에 저장된 예금 및 적금 데이터를 조회하여 점수를 계산하고, 그에 따라 등급을 평가합니다.
금융 상품 반환 : 사용자의 위험 감수 성향과 동일한 상품들 중 위험도가 낮은 순으로 데이터를 반환합니다.
사용자의 저축 성향에 따른 상품 추천 : 사용자가 선택한 저축 성향(단기, 중기, 장기)에 맞는 개월수의 상품들을 추천해줍니다.
커뮤니티에서 자유 게시판, 정보 공유 게시판, 고객과의 소통 페이지를 나누었지만 글을 작성할 때 하나의 카테고리에 갇혀 있지 않고 중간에 다른 카테고리로 바꾸고 싶으면 바꾸고 작성을 한다면 그 카테고리에 게시물이 post 됨으로써 사용자 편의성을 높혔습니다.
챗봇의 위치를 한눈에 알기 쉽도록 메인 페이지에 배치를 하여 저희의 서비스를 쉽게 접근하고 이해할 수 있도록 편의성을 제공하였습니다.
금융 상품 추천 서비스 : 사람을 성별에 따른 투자 성향 차이, 나이에 따른 투자 성향 차이, 연봉에 따른 투자 성향 차이, 자산에 따른 투자 성향 차이를 점수화 하여 위험도 감수 성향을 평가하였습니다. 그리고 상품들에 대하여 은행의 규모와 안정성, 금리에 따른 안정성, 기간에 따른 안정성을 점수화하여 상품의 위험성을 점수화하여 위험도를 평가하였습니다. 그에 따라 개인의 성향에 맞게 상품을 추천하도록 서비스를 구현하였습니다.
결론 - 후기
처음하는 프로젝트라서 그런지 의욕이 넘쳐서 필수 기능 구현 이외에 몇개의 기능 추가 그리고 디테일한 부분까지 모두 구현을 하려다가 일정이 꼬였으며 또한 이미 구현했던 기능이 다음날이 되면 작동하지 않는 경우가 발생했었기에 일정상 필수 부분만 진행하게 되어 다소 아쉬웠습니다. 그리고 역할을 기능별로 나누다보니 기능간의 연결점들에 의해 충돌이 발생하여 코드가 꼬이게 되어 작동이 안한 경우도 발생하였습니다. 이러한 상황을 잘 해결하기 위해서는 커밋 메시지를 활용을 했어야 했는데 커밋 메시지도 대충 작성한 흔적이 여러 곳에서 발생하였기에 이 부분도 아쉬웠습니다. 마지막으로 처음에 ERD를 어차피 나중에 가면 많이 바뀔수도 있으므로 설계를 제대로 하지 않고 진행하였다는 점에서 프로젝트를 진행하면서 어떤 부분을 진행하고 있는지에 대해 잘 모르게 되는 경우가 발생하였고 매 순간마다 하나씩 설계하면서 진행하다보니 오히려 시간이 많이 걸리게 되었고 비효율적인 ERD를 설계하게 되어 다시 갈아엎느라 시간을 많이 소모하게 되었습니다.
프로젝트를 진행하면서 배우게 된 점은 로그인과 회원가입을 커스텀하는 방법과 OpenAPI를 통해 데이터를 가져와서 저장하고 가공하는 법을 배우게 되었고 기존에 배웠던 게시판 기능을 다시 복습한 계기가 되어 한층 더 실력이 발전되었다고 생각합니다.
이렇게 첫 프로젝트를 진행하면서 아쉬웠던 점과 배우게 된 점을 잘 숙지하여 다음 프로젝트에서는 더욱 발전된 모습으로 진행하도록 하겠습니다.
멋져요 기먼규 교육생