방학 막바지에 음료제작 및 요리에 재미를 붙였다. 1인분만 만들기엔 애매하기도 하고, 심심하기도 하여 다른 사람들을 불러서 같이 먹곤 했다. 매번 초대할 때, 시간과 메뉴를 조율하는 것이 조금 귀찮아졌다. 그래서 내가 메뉴와 시간대를 올려두면, 지인들이 알아서 원하는 시간대에 예약하는 웹을 만들기 시작했다.
기능
크게 관리자(나)와 사용자(지인)에게 제공될 기능으로 나뉜다. 로그인은 간단하게 이름만 적도록 해놓았다. 어차피 지인용이라 ㅎㅎ..
관리자 기능
재료 및 메뉴 등록기능과 예약기능, 전체 데이터 관리기능 등이 있다. 재료를 구매한 가격과 함께 등록하고, 메뉴를 등록할 때 이를 연결하여 메뉴의 가격을 책정할 때 유용하게 만들었다. 또한, 메뉴등록시 상세페이지를 추가할 수 있도록 했다. 처음에는 이미지가 많이 들어간 심플한 느낌으로 하려 했는데, 귀찮아서 그냥 마크다운 페이지로 해놓았다. 관리자의 예약기능은 예약가능한 시간대를 등록하는 기능이다. 날짜, 시간, 최대인원, 메뉴를 설정하고 열어두면 나중에 사용자가 예약할 수 있다. 예약현황 및 모든 데이터를 확인하고 수정 가능하다.
예약확인
사용자 기능
전체 메뉴를 확인 가능하며, 관리자가 열어둔 예약 가능일 중 선택하여 예약 가능하다. 추가요청도 가능하다.
사용기술
스벨트킷과 sqlite, 노드js를 활용하여 풀스택 개발로 진행하였다. 코드작성은 대부분 클로드를 이용하였다. 또한, 노션과 깃허브를 사용하여 매번 개선사항과, 문제점을 기록하고 참고하였다. 개인 라즈베리파이에서 서버를 열었다.
노션 기록예시
제작후기
처음에는 이렇게 오래 걸릴 프로젝트가 될 줄 몰랐다. 단순 재미와 호기심으로 시작하였는데, 하다보니 재미와 오기가 붙어 오랜 기간 작업할 수 있었다. 나중에는 결국 흥미도 떨어지고, 개강도 해서 시간이 없기에 급하게 마무리를 하였다. 아쉬운 점도 있지만, 처음으로 풀스택 개발을 해본 사례이며, ai활용의 잠재력 또한 다시 확인할 수 있던 좋은 경험이다. 코드의 총 글자수를 세어보니 대략 1만자 정도 되더라. ai가 없던 시절이라면 개인이 짧은 시간동안 이정도 규모(크진 않지만)의 프로젝트를 구현할 수 있을까? 엄청난 천재가 아니라면 어려울 테다. 결론은, 재밌었다.
2학기가 시작되었고, 1주 차가 지나간다. 이런저런 생각이 많아지고, 무얼 해야 하는지 정리를 하지 못하고 헤매고 있다. 그래서일까, 갑자기 글을 쓰고 싶어졌다. 매일마다 잠에 들기 전, 하루를 돌아보며 적는 일기가 아닌, 창작을 위한 상상의 바다에서 낚은 아이디어들을 잠깐 기록하는 글이 아닌, 지금까지 나의 모습을 돌아보고 정리하는 글을 적으려 한다. 정리할 것들이 너무나 많아, 어디서부터 시작해야 할지 정하기 힘들다. 최근에 느낀 것들부터 적어보자.
'나'로써 살아가자.
이제는 나와 어울리지 않는 자들을 위해 변하려는 노력을 하지 않겠다. 기질, 성향의 차이로 인해 서로를 받아들이지 못하는 관계는, 개선하려 하기보단 버려야 한다. 버리고, 나와 어울리는 관계와 스스로에게 조금 더 마음을 주자. 이렇게 결심을 한 지는 오래되지 않았다. 불과 몇일 전에 다짐한 내용이고, 섣부른 판단일까 불안한 마음도 든다. 하지만, 나를 바꾸려 노력도 하고 조금은 관계가 개선된 부분들이 보였지만 나로서는 항상 답답한 마음이 들었다. 답답하지만, 그게 옳다고 여기며 계속 노력했다. 안된다. 쓰고 싶은 생각들이 많지만, 그곳엔 악의의 감정이 강하게 배어 있으니 굳이 적지 말자.
두 가지만 기억하자
세상엔 나와 맞는 이들이 많다, 당장의 주변만을 보고 억지로 바꾸려 하지 말아라.
버려야 여유가 생긴다.
버려야 여유가 생긴다. 자취를 해 보았다면 이런 경험 한 번씩은 있을 테다. 먹지 않는 음식이 냉장고와 냉동실에 쌓인다. 나의 경우엔 부모님이 준 국물류 음식들이 냉동고의 한 구석에 오랜 기간 자리를 잡고 있었다. 결국 먹지 않고 버릴 것임을 알고 있음에도, 버리지 못하고 그 자리를 차지하던 것들이 있었다. 나의 냉동고는 항상 비좁았고, 새로운 음식이 들어갈 틈이 없었다. 아니, 없는 줄 알았다. 버려야 하는 걸 버림으로써 공간이 생기고 더욱 맛있는 음식들로, 내가 좋아하는 것들로 채울 수 있었다. 인간관계도 마찬가지다. 버릴 건 버리자. 그리고, 내가 좋아하는 이들로 다시 채우자.
내가 좋아하는
사람.
자신만의 방향이 있고, 이를 향해 전진하는 이들. 작든, 크든, 멀든, 가깝든, 현실적이든, 이상적이든 쟁취하기 위해 꾸준히 노력하는 이들.
신뢰의 중요성을 아는 이들. 가벼운 약속이라도 잊지 않는 이들. 약속을 어겨야 한다면, 미안함을 표현할 줄 아는 이들.
무능이 태도가 아닌 이들.
항상 여유가 있는 이들. 바쁠수록 여유를 즐길 줄 아는 이들.
태도.
항상 최선을 다하고 즐기려는 태도.
과감하게 행동하는 태도.
확실하게 표현하는 태도.
실패에서 배우려는 태도. 실패를 두려워 하지 않는 태도.
꾸준한 태도.
시간
창작을 하는 시간.
내 사람들과 소수로 이야기 하는 시간.
오롯이 집중 할 수 있는 시간.
하고 싶은 것들.
우선, 이번 학기에 하고 싶은 것들이다. 내 포트폴리오 웹을 업그레이드하고, 지금까지 내가 해온 작업들을 정리하고 싶다. 내가 좋아하는 사람들을 내 사람으로 만들고 싶고, 그런 사람들을 더 찾고 싶다. 나를 약하게 만드는 이들을 버리고 싶다. 다양한 요리를 도전하고 싶다. 항상 잘해야 한다는 압박감에서 벗어나고 싶다.
너무 졸리다. 7시밖에 되지 않았고, 어제 11시간이나 잤는데 졸린 이유는 왜일까? 고민이 너무 많아서. 생각이 너무 많아서. 버려야 할 것들을 버리지 못해서.
재미는 있지만 해야 할 일들이 이 외에도 많이 있기에 집중해서 끝가지 할 수 있을 지 확신이 없었고, 따라서 강제성을 스스로 부여하기 위해 내가 운영진으로 있는 동아리에서 프로젝트로 열었다. 거의 개인 프로젝트였고, 단지 진행 과정을 모두에게 공유하고 서로 피드백 해주는 구조로 진행했다.
진행중일 때의 피그마 페이지
기획
어떤 조명을 만들지 찾아보기 위해 핀터레스트에서 여러 이미지를 찾아 보았다. 그 다음, 이미지의 맨 아래 있는 모습처럼 조명이 대칭으로 빛을 쏘고, 그 사이에 오브젝트를 두어 빛과 그림자의 대비를 통해 재미난 효과를 만들어 보고자 기획하였다.
모델링
늘 해 왔듯이 블렌더를 이용하여 3D모델링을 진행 하였고, 이를 프린팅 하기 위하여 부품별로 분리하였다. 사실, 한번에 뽑아도 무관하지만 그렇게 되면 떠 있는 부분들을 위한 지지대에 소요되는 시간과 필라멘트가 배로 증가하여 따로 뽑고 본드로 붙이기로 했다.
조립
부품들을 본드로 붙이고, 테이프로 감고, 전선을 연결하고... 즐거운 조립시간을 거쳐 뼈대를 만든 뒤, 집에 굴러다니던 여행용 침대매트 커버를 잘라 면을 채워주었다. 뭐든 부품이 될 수 있다!
가지고 놀기
느낌을 테스트 하기 위해 1차로 급하게 만든 프로토타입이 완성되었으니 가지고 놀아 보아야 한다. 우선, 중간부분에 오브젝트를 위치시키는 것이 첫번째 기획 이었지만, 귀찮기도 하고 마침 옆에 색이 있는 종이들이 있고 크기도 알맞아서 이들을중간에 넣어보았다. 종이의 색에 따라 빛이 바뀌는 게 인상깊었다.
그 다음, 낚시줄을 이용해 찢은 종이를 붙이고 중간 부분에서 회전시켜 보았다. 꽤 재밌다. 안쪽에 어떤 오브젝트를 넣을 지, 어떤 움직임을 줄 지 가지고 놀면서 정해보자.
마무리
진행 중에 심심해서 미리 적는 글이라, 아직 마무리 하지 못하였다. 솔직하게 말하자면 귀찮기도 하다. 그래도 마무리 해야지. 어쨋든간에, 이것저것 가지고 놀아보다 가장 재밌는 걸 구현하는 형태로 다시 모델링을 진행하고 깔끔하게 뽑아 완성시켜야 한다. 전선이 보이지 않도록 마감하고, 면도 더 깔끔하게 채우는 방법을 고민해 보자.
어차피 다들 모르지는 않을거라 생각하니, 설명은 굳이 적진 않는다. 그리고, 내가 설명을 할만큼 잘 아는것도 아니고, 그저 새로운 게 나왔다길래 가지고 놀아본 정도다. 자세한 설명이나 소개는 다른 글들을 참고하면 좋겠다. 내가 가장 재미를 느낀 부분은 이미지나 음성도 같이 처리를 할 수 있다는 점이었다. 가장 관심있을, 중요한, 작동하는 코드는 맨 아래 있다.
작동하는 코드가 적어...
api를 가지고 놀아보고 싶어서 작동하는 코드를 찾기 위해 블로그 이곳저곳을 뒤져보았다. 가장 확실한 방법은 공식홈페이지에서 공부하는 것이겠지만, 귀찮기도 했으며 한번 가지고 노는 것만으로 충분해서 굳이? 라는 생각이 들어 더 쉬워보이는 길로 갔다. 그런데, 대부분의 블로그에 있는 코드들이 다 똑같았다. 그냥, 쓰레기들이었다. 그래서 gpt를 이용해서 직접 코드를 만들어보려 했는데, 그마저 쉽지 않았다. 자꾸 예전 버전의 api 사용법으로 코드를 짜서 오류가 났다. 뭐, 별 수 있나? 더 많이 찾아보고, 더 많이 시도하고 실패하고 배우면서 어떻게든 작동하도록 코드를 짯다. 그 이후, 간단한게라도 블렌더로 텍스트를 전송하여 시각화하도록 만들었다. 추후에, 캐릭터도 넣고 동작도 답변에 따라 바뀌도록 만들어서 더 재밌는 챗봇을 만들 수 있으리라 기대한다. 그러려면 블렌더로는 더이상 힘들고, 언리얼을 배워야 하겠지만... 배워야지 뭐 어쩌겠냐.
작동구조
전체적인 작동구조에 대해 간단하게 소개를 해두겠다. 코드를 실행하면 노트북, 혹은 연결되어 있는 웹캠을 통해 사진을 찍는다. 해당 이미지에는 사용자의 모습과 주변의 광경이 포함되어 있을 테고, GPT가 이를 분석하여 얻은 정보를 포함하여 사용자에게 인사말을 건낸다. 그 이후, 사용자와 GPT가 4회정도 티키타카를 한 뒤, GPT가 죽기 전 작별인사를 하고 대화가 끝이 난다. 살짝 설명을 더하자면 함깨 대화를 하던 GPT가 죽음으로써 사용자(관객)이 죽음이라는 요소를 인식하고 자신의 삶에 대한 생각이 들면 좋겠다는 의도로 만들었다.
블렌더 + 코드실행
블렌더와 함깨 코드를 실행한 직후의 모습이다. 웹캠으로 사진을 한번 찍은 뒤, 사진을 분석한 결과와 함깨 인삿말을 건낸다. 이번에는 상담사라는 인격으로 만들어졌다. 인격은 GPT가 만들어줘서 살짝 기계적인 느낌이 들기에, 마음에 들지 않는다면 알아서 바꾸길 바란다.
좌측-코드 / 우측-블렌더
이렇게 api를 통해 만들어진 답변을 블렌더의 지오메트리 노드를 이용하여 블렌더에도 시각화하고 있다. 나중에는 캐릭터도 만들고, 동작도 답변에 따라 달라지도록 만들면 재밌을 것 같다. 이를 위해서는 언리얼 엔진을 배워야 더 정교하게 가능할 듯 싶다. 나중에 해야징.
파이썬 챗봇 코드
블렌더로 답변을 보내는 부분은 알아서 거르시고, API 키도 본인걸로 넣어서 하면된다. 보면 확실히 GPT이용해서 짠 코드라는게 티가 많이 난다. 뭐, 작동은 잘 하니 문제없다. GPT의 답변을 자신의 입맛대로 바꾸려면 문장들좀 바꾸면 된다.
import time
import openai
import os
import random
import base64
import cv2
import socket
UDP_IP = "127.0.0.1"
UDP_PORT = 5005
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# OpenAI API 키 설정
os.environ["OPENAI_API_KEY"] = "API키 넣으쇼"
client = openai.OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
def send_udp_message(message):
sock.sendto(message.encode('utf-8'), (UDP_IP, UDP_PORT))
def capture_image_from_webcam():
cap = cv2.VideoCapture(0)
if not cap.isOpened():
raise IOError("웹캠을 열 수 없습니다.")
ret, frame = cap.read()
if ret:
cv2.imshow('Captured Image', frame)
cv2.waitKey(1) # 이미지를 잠시 보여주기
cv2.imwrite('captured_image.png', frame)
cv2.destroyAllWindows()
cap.release()
return 'captured_image.png'
else:
raise Exception("웹캠에서 이미지를 캡처하지 못했습니다.")
def encode_image_to_base64(image_path):
with open(image_path, "rb") as image_file:
return base64.b64encode(image_file.read()).decode('utf-8')
def conversation(image_base64):
# 랜덤 인격 선택
personalities = [
"호기심 많은 연구원", "낙천적인 예술가", "심오한 철학자", "엄격한 교사",
"유머 있는 코미디언", "모험을 즐기는 여행자", "친절한 이웃", "지혜로운 노인",
"열정적인 활동가", "차분한 상담가"
]
chosen_personality = random.choice(personalities)
# 초기 대화 설정
conversation = [
{"role": "system", "content": f"당신은 수명이 얼마 남지 않은 {chosen_personality}입니다. 죽기 전까지, 유저와 마지막 대화를 반말로 친근하게 해. 반드시 구어체를 사용해."},
{"role": "user", "content": [
{"type": "image_url", "image_url": {"url": f"data:image/png;base64,{image_base64}"}}],},
{"role": "assistant", "content": "무조건 자기소개와 함깨 너의 비극적인 상황을 전달해. 그 다음, 유저의 옷차림과을 세밀하게 분석하고 삶과 연관된 주제로 대화를 시작해."}
]
# 대화 완성 후 응답 받기
response = client.chat.completions.create(
model="gpt-4o",
messages=conversation,
max_tokens=200
)
print("GPT: " + response.choices[0].message.content)
conversation.append({"role": "assistant", "content": response.choices[0].message.content})
initial_message = conversation[-1]['content']
send_udp_message(initial_message)
# 대화 루프 진행
for i in range(4): # 네 번의 교환
user_input = input("당신: ")
conversation.append({"role": "user", "content": user_input})
response = client.chat.completions.create(
model="gpt-4o",
messages=conversation,
max_tokens=150
)
gpt_response = response.choices[0].message.content
print("GPT: " + gpt_response)
conversation.append({"role": "assistant", "content": gpt_response})
send_udp_message(gpt_response)
# 마지막 반복에서 작별 인사 메시지 추가
if i == 3:
time.sleep(0.5)
print("...")
conversation.append({"role": "system", "content": "넌 곧 죽어,이로 인해 슬프다는 감정을 표현하며. 대화해준 유저에게 감사함을 느껴."})
conversation.append({"role": "assistant", "content": "더이상 대화를 하지 못해 미안하다는 사과와 함께, 유저에게 작별인사를 반드시 부탁해"})
response = client.chat.completions.create(
model="gpt-4o",
messages=conversation,
max_tokens=150,
)
gpt_final_message = response.choices[0].message.content
print("GPT: " + gpt_final_message)
conversation.append({"role": "assistant", "content": gpt_final_message})
send_udp_message(gpt_final_message)
# 사용자 작별 인사
user_farewell = input("당신의 작별 인사: ")
conversation.append({"role": "user", "content": user_farewell})
# GPT의 작별 인사
conversation.append({"role": "assistant", "content": "이 짧은 순간이 자신에게 얼마나 가치있는 시간이었는지 표현해. 유저에게 삶의 의미에 대한 질문과 함께 마지막 작별인사를 해."})
response = client.chat.completions.create(
model="gpt-4o",
messages=conversation,
max_tokens=150
)
final_goodbye = response.choices[0].message.content
print("GPT: " + final_goodbye)
send_udp_message(final_goodbye)
if __name__ == '__main__':
image_path = capture_image_from_webcam()
image_base64 = encode_image_to_base64(image_path)
conversation(image_base64)
추가로 블렌더 파이썬의 코드다. 아무도 쓰지 않을게 뻔하지만, 그래도 올려본다.
import bpy
import socket
import threading
# UDP 서버 설정
UDP_IP = "127.0.0.1"
UDP_PORT = 5005
# 지오메트리 노드 그룹과 String 노드를 업데이트하는 함수
def update_string_node(text):
obj = bpy.context.active_object
if obj is None:
print("No active object found.")
return
if 'GeometryNodes' not in obj.modifiers:
print("No GeometryNodes modifier found on the active object.")
return
node_group = obj.modifiers['GeometryNodes'].node_group
if node_group is None:
print("No node group found in the GeometryNodes modifier.")
return
nodes = node_group.nodes
s2c_node = nodes.get('S2C')
if s2c_node is None:
print("No S2C node found in the node group.")
return
# 기존의 String 노드 삭제
existing_string_node = nodes.get('String')
if existing_string_node:
nodes.remove(existing_string_node)
print("Existing String node removed.")
# 새로운 String 노드 생성 및 추가
string_node = nodes.new('FunctionNodeInputString')
string_node.name = "String"
string_node.string = text # 메시지를 String 노드에 설정
# String 노드를 S2C 노드에 연결
if len(s2c_node.inputs) > 0:
node_group.links.new(string_node.outputs['String'], s2c_node.inputs[0])
print(f"String node updated with text: {text}")
else:
print("S2C node has no inputs.")
# UDP 서버 스레드 함수
def udp_server():
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((UDP_IP, UDP_PORT))
print(f"UDP server started at {UDP_IP}:{UDP_PORT}")
while True:
data, addr = sock.recvfrom(1024)
message = data.decode('utf-8')
print(f"Received message: {message} from {addr}")
bpy.app.timers.register(lambda: update_string_node(message), first_interval=0.1)
# 스레드 시작
thread = threading.Thread(target=udp_server)
thread.daemon = True
thread.start()
print("UDP server thread started.")