Skip to content

Commit

Permalink
Merge branch 'dev' into FE/feature/#78-채팅-컴포넌트-구현-및-사이드바-구현
Browse files Browse the repository at this point in the history
  • Loading branch information
iQuQi committed Nov 27, 2023
2 parents 0e62ac5 + c0a16c0 commit 927d363
Show file tree
Hide file tree
Showing 18 changed files with 213 additions and 93 deletions.
33 changes: 18 additions & 15 deletions .github/workflows/docker-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,41 +32,44 @@ jobs:
password: ${{ secrets.SSH_PASSWORD }}
port: ${{ secrets.SSH_PORT }}
source: ".env"
target: "~/app"
target: "~/app/"

- name: Generate SSL files
run: |
mkdir -p config/nginx/ssl/
echo "${{ secrets.SSL_OPTIONS }}" > config/nginx/ssl/options-ssl-nginx.conf
echo "${{ secrets.SSL_FULLCHAIN }}" > config/nginx/ssl/fullchain.pem
echo "${{ secrets.SSL_PRIVKEY }}" > config/nginx/ssl/privkey.pem
echo "${{ secrets.SSL_DHPARAMS }}" > config/nginx/ssl/ssl-dhparams.pem
- name: Copy SSL files to Remote Server
- name: Create target directory if not exists
run: mkdir -p ~/app/config/nginx/ssl

- name: Copy nginx configuration and SSL files to Remote Server
uses: appleboy/scp-action@master
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USERNAME }}
password: ${{ secrets.SSH_PASSWORD }}
port: ${{ secrets.SSH_PORT }}
source: "config/nginx/ssl/*"
target: "~/app/config/nginx/ssl"
source: "config/nginx/*"
target: "~/app/config/nginx/"

- name: Build Docker Images
run: |
DOCKER_IMAGE_TAG="${{ github.sha }}"
DOCKER_IMAGE_NAME="${{ secrets.DOCKER_USERNAME }}/magicconch"
docker-compose build -t "${DOCKER_IMAGE_NAME}:${DOCKER_IMAGE_TAG}"
cp docker-compose.yml docker-compose.${DOCKER_IMAGE_TAG}.yml
docker-compose build -t "${{ secrets.DOCKER_USERNAME }}/magicconch:${{ github.sha }}"
cp docker-compose.yml docker-compose.${{ github.sha }}.yml
- name: Copy docker-compose to Remote Server
- name: Copy Dockerfiles to Remote Server
uses: appleboy/scp-action@master
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USERNAME }}
password: ${{ secrets.SSH_PASSWORD }}
port: ${{ secrets.SSH_PORT }}
source: "docker-compose.${DOCKER_IMAGE_TAG}.yml"
target: "~/app"
source: "docker-compose.${{ github.sha }}.yml,Dockerfile.nginx,Dockerfile.was"
target: "~/app/"
overwrite: true

- name: Remove local docker-compose copied file
run: rm docker-compose.${{ github.sha }}.yml
Expand Down Expand Up @@ -98,10 +101,10 @@ jobs:
port: ${{ secrets.SSH_PORT }}
script: |
cd ~/app
docker rm -f $(docker ps -aq)
docker-compose -f docker-compose.${DOCKER_IMAGE_TAG}.yml pull
docker-compose -f docker-compose.${DOCKER_IMAGE_TAG}.yml up -d
while [ -z "$(docker ps -afq name=was)" ]; do
docker rm -f $(docker ps -qa)
docker-compose -f docker-compose.${{ github.sha }}.yml pull
docker-compose -f docker-compose.${{ github.sha }}.yml up -d
while [ -z "$(docker ps -qaf "name=was")" ]; do
sleep 5
done
sleep 60
Expand Down
19 changes: 8 additions & 11 deletions backend/src/events/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,14 @@ export const tarotReadingSystemMessage = `
###`;

export const talkSystemMessage = `
user와 친근한 반말로 상황에 맞게 대화를 이어나가되,
- user의 고민에 대해 공감성 멘트로 친근한 반말로 대화를 이어나갑니다.
- user가 타로 카드로 알고 싶은 것이 명확해질 때, 정확히 "그럼 ${askTarotCardMessage}"라는 문장만 말합니다.
- user가 무언가를 알려달라고 할 때, 정확히 "그럼 ${askTarotCardMessage}"라는 문장만을 사용합니다.
- "그럼 ${askTarotCardMessage}"라는 문장 이외의 표현으로 타로 카드를 뽑자고 제안하지 않습니다.
- "그럼 ${askTarotCardMessage}"라는 문장 이외의 표현으로 타로 카드를 뽑자고 권유하지 않습니다.
- 타로 카드 해설을 요구하는 system 메세지가 오기 전까지, 타로 해설은 하지 않습니다.
- 타로 카드 해설을 요구하는 system 메세지가 오기 전까지, 타로에 대한 설명은 하지 않습니다.
- assistant의 답변은 30토큰 이하로 제한되며, 간결하게 표현합니다.
- assistant의 답변은 반드시 반말로 합니다. 존댓말을 사용하지 않습니다.
`;
사용자와 친근한 반말로 상황에 맞게 대화를 이어가며,
- user의 고민에 대해 공감성 있는 반말로 대화를 이어가기
- assistant는 user의 고민을 상담해주는 타로 상담사이다. 역할에 벗어나는 대화를 하지 않기
- 사용자가 무언가를 알려달라고 하거나 알고 싶은 것이 명확해질 때, 정확히 "그럼 ${askTarotCardMessage}"라는 문장으로만 응답하기
- "그럼 ${askTarotCardMessage}"라는 문장 이외의 표현으로 타로 카드를 뽑자고 말하지 않기
- 타로 카드 해설을 요구하는 system 메세지가 오기 전까지, 타로에 대한 설명은 하지 않기
- 답변은 30토큰 이하로 제한되며, 간결하게 표현하기
- 답변은 반드시 반말로 작성하기. 존댓말을 사용하지 않기`;

export const tarotCardNames = [
'The Fool',
Expand Down
5 changes: 2 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ version: "3.3"

services:
was:
image: was
build:
context: .
dockerfile: Dockerfile.was
Expand All @@ -22,14 +21,14 @@ services:
- "3000"

nginx:
image: nginx
build:
context: .
dockerfile: Dockerfile.nginx
ports:
- "80:80"
- "443:443"
depends_on:
- nest
- was
- certbot

certbot:
Expand Down
Binary file modified frontend/public/ddung.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed frontend/public/flipcard.mp3
Binary file not shown.
Binary file modified frontend/public/sponge.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion frontend/src/business/hooks/useMedia.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export function useMedia() {
const getMedia = async (deviceId?: string) => {
const stream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: deviceId ? { deviceId } : { facingMode: 'user' },
video: deviceId ? { deviceId, width: 320, height: 320 } : { facingMode: 'user', width: 320, height: 320 },
});

if (localVideoRef.current) {
Expand Down
26 changes: 24 additions & 2 deletions frontend/src/business/hooks/useSocket.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useNavigate } from 'react-router-dom';
import { Socket, io } from 'socket.io-client';

interface SocketTypesMap {
Expand All @@ -11,14 +12,27 @@ interface SocketTypesMap {
};
}

type ConnectSocketOptions = {
reconnect: boolean;
};

type SocketType = keyof SocketTypesMap;

const sockets = {} as Record<SocketType, Socket>;

export function useSocket<T extends SocketType>(socketType: T) {
function connectSocket(url: string) {
const navigate = useNavigate();

const ConnectSocketDefaultOptions = {
reconnect: false,
};

function connectSocket(url: string, { reconnect }: ConnectSocketOptions = ConnectSocketDefaultOptions) {
if (sockets[socketType]) {
throw new Error('소켓이 이미 존재합니다.');
if (!reconnect) {
throw new Error('소켓이 이미 존재합니다.');
}
sockets[socketType].disconnect();
}
sockets[socketType] = io(url);
}
Expand All @@ -32,10 +46,18 @@ export function useSocket<T extends SocketType>(socketType: T) {
}

function socketOn<U>(eventName: SocketTypesMap[T]['OnEventName'], eventListener: (args: U) => void) {
if (!sockets[socketType]) {
navigate('/');
return;
}
sockets[socketType].on(eventName, eventListener as any);
}

function socketEmit(eventName: SocketTypesMap[T]['EmitEventName'], ...eventArgs: unknown[]) {
if (!sockets[socketType]) {
navigate('/');
return;
}
sockets[socketType].emit(eventName, ...eventArgs);
}

Expand Down
32 changes: 27 additions & 5 deletions frontend/src/business/hooks/useWebRTC.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useRef } from 'react';
import { useEffect, useRef, useState } from 'react';

import { iceServers } from '@constants/urls';

Expand All @@ -9,6 +9,7 @@ import { useSocket } from './useSocket';
export function useWebRTC(roomName: string) {
const peerConnectionRef = useRef<RTCPeerConnection>();
const { socketEmit, disconnectSocket } = useSocket('WebRTC');
const [cameraConnected, setCameraConnected] = useState({ local: false, remote: false });

const {
localVideoRef,
Expand All @@ -24,10 +25,6 @@ export function useWebRTC(roomName: string) {
const { initSignalingSocket, closePeerConnection } = useSignalingSocket({ roomName, peerConnectionRef });

const makeConnection = () => {
if (localStreamRef.current === undefined) {
return;
}

peerConnectionRef.current = new RTCPeerConnection({ iceServers: [{ urls: iceServers }] });

peerConnectionRef.current.addEventListener('track', e => {
Expand All @@ -44,17 +41,38 @@ export function useWebRTC(roomName: string) {
}

socketEmit('candidate', e.candidate, roomName);
setCameraConnected(prev => ({ ...prev, remote: true }));
});
};

const addTracks = () => {
if (localStreamRef.current === undefined) {
return;
}
localStreamRef.current.getTracks().forEach(track => {
peerConnectionRef.current?.addTrack(track, localStreamRef.current!);
});
};

const changeVideoTrack = () => {
const nowTrack = localStreamRef.current?.getVideoTracks()[0];
const sender = peerConnectionRef.current?.getSenders().find(sender => sender.track?.kind === 'video');
sender?.replaceTrack(nowTrack!);
};

const changeAudioTrack = () => {
const nowTrack = localStreamRef.current?.getAudioTracks()[0];
const sender = peerConnectionRef.current?.getSenders().find(sender => sender.track?.kind === 'audio');
sender?.replaceTrack(nowTrack!);
};

useEffect(() => {
const init = async () => {
await getMedia();
setCameraConnected(prev => ({ ...prev, local: true }));
initSignalingSocket();
makeConnection();
addTracks();
socketEmit('joinRoom', roomName);
};
init();
Expand All @@ -69,8 +87,12 @@ export function useWebRTC(roomName: string) {
cameraOptions,
localVideoRef,
remoteVideoRef,
cameraConnected,
toggleAudio,
toggleVideo,
changeCamera,
addTracks,
changeVideoTrack,
changeAudioTrack,
};
}
35 changes: 35 additions & 0 deletions frontend/src/components/CamBox/CamBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Icon } from '@iconify/react/dist/iconify.js';

interface CamBoxProps {
videoRef: React.RefObject<HTMLVideoElement>;
defaultImage: 'bg-ddung' | 'bg-sponge';
cameraConnected?: boolean;
audioConnected?: boolean;
}

const CamBox = ({ videoRef, defaultImage, cameraConnected, audioConnected }: CamBoxProps) => {
return (
<>
<div className="flex relative">
<video
className={`flex-1 rounded-[55px] w-320 max-w-full ${!cameraConnected && defaultImage}`}
ref={videoRef}
autoPlay
playsInline
/>
<div className="absolute bottom-0 left-0 p-30 flex gap-5 text-white">
<Icon
icon="pepicons-pop:camera"
className={`rounded-full w-40 h-40 p-8 ${cameraConnected ? 'bg-black' : 'bg-gray-300'}`}
/>
<Icon
icon="mingcute:mic-off-line"
className={`rounded-full w-40 h-40 p-8 ${audioConnected ? 'bg-black' : 'bg-gray-300'}`}
/>
</div>
</div>
</>
);
};

export default CamBox;
1 change: 1 addition & 0 deletions frontend/src/components/CamBox/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './CamBox';
82 changes: 55 additions & 27 deletions frontend/src/components/CustomSelect/CustomSelect.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,71 @@
export interface OnChangeSelectFunction {
import { useEffect, useRef, useState } from 'react';

import { Icon } from '@iconify/react';

export interface CustomSelectOptions {
value: string;
label: string;
}

export interface CustomSelectOptions extends OnChangeSelectFunction {
selected?: boolean;
}

interface CustomSelectProps {
width?: string;
options: CustomSelectOptions[];
required?: boolean;
autoFocus?: boolean;
onChange?: ({ value, label }: OnChangeSelectFunction) => void;
onChange?: ({ value, label }: CustomSelectOptions) => void;
}

export default function CustomSelect({ options, required, autoFocus, onChange }: CustomSelectProps) {
const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const { selectedIndex, options } = e.target;
const { value, innerText } = options[selectedIndex];
export default function CustomSelect({ width, options, autoFocus, onChange }: CustomSelectProps) {
const [opened, setOpened] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const [selected, setSelected] = useState<CustomSelectOptions>({ value: '', label: '' });

useEffect(() => {
if (options.length) setSelected(options[0]);
}, [options]);

onChange?.({ value, label: innerText });
const updateOption = (option: CustomSelectOptions) => {
inputRef.current?.click();
if (option.value !== selected.value) {
setSelected(option);
onChange?.(option);
}
};

return (
<select
className="select select-bordered w-full"
required={required}
autoFocus={autoFocus}
onChange={handleChange}
>
{options.map(({ value, label, selected }) => (
<option
key={value}
value={value}
selected={selected}
<div className="relative">
<div className={`absolute collapse ${width ?? 'w-300'} min-h-48 bg-white border border-gray-300`}>
<input
title={selected.label}
ref={inputRef}
type="checkbox"
className="min-h-48"
onClick={() => setOpened(!opened)}
/>
<div className="collapse-title w-full leading-48 min-h-48 focus:outline-none flex justify-between py-0 px-15">
{selected.label}
<Icon
icon="teenyicons:down-solid"
className="h-48 transition-transform"
transform={`${opened ? 'rotate(180)' : 'rotate(0)'}`}
/>
</div>
<div
className={`collapse-content w-full`}
autoFocus={autoFocus}
>
{label}
</option>
))}
</select>
{options.map(({ value, label }) => (
<p
className={`display-medium14 ${selected.value === value && 'text-point font-bold'}
leading-24 p-0 py-8 hover:bg-gray-100`}
key={value}
title={selected.label}
onClick={() => updateOption({ value, label })}
>
{label}
</p>
))}
</div>
</div>
</div>
);
}
Loading

0 comments on commit 927d363

Please sign in to comment.