2024년 9월 20일 금요일

[quaser.dev][2024-09-14] 윈도우즈(10, 64bit)에 개발환경 설정하기

[quaser.dev][2024-09-14] 윈도우즈(10, 64bit)에 개발환경 설정하기

자세한 내용 보기 »

라벨: , , , , ,

2023년 9월 4일 월요일

[FastAPI] socket.io with fastapi_socketio (feat:python-socketio, saw: 톱 2종)

안녕하세요 클스 입니다.


팀에 하나의 제품을 개발할 때 인력이 약 15명 이상 있을 경우 react, vue, flutter등 

선택의 폭이 넓어집니다. 그런데 애매하게 5~8명이 back-end, front-end, db, infra 

전부 하면서 web, android, ios 앱을 개발하기엔 쉽지 않습니다.

우리는 무엇을 선택하면 좋을까요? 

현재 우리팀은 FastAPI, vuejs 를 기본으로 사용하고 있는데.. vuejs가 아직 react의 방대함

그리고 커뮤니티를 따라가지는 못하는  듯합니다.

확실히 react 보다는 사용하기는 제 경우는 쉬웠습니다. 사설이 길었네요~


여하튼 이번에는 웹에서 실시간으로 사용자에게 알림(시스템 변경, 작업 완료...)을 

실시간으로 전달하기위해 많이 사용하는 websocket과 socketio가 있는데

둘의 확실한 선택 기준은 서비스 환경에서 통신에 문제가 있는가 없는가에 따라 다릅니다.

socketio는 통신이 막히면 가능한 방식을 찾아서 연결해서 사용자에게 메시지를 전달합니다.

HTTP long polling 방식으로 전달을 합니다. 문제는 주기적으로  long polling를 합니다.

그리고 socketio client를 사용하면 서버가 재시작되면 자동으로 다시 연결해줍니다. 

개발자 입장에서는 확실히 좋은 기능입니다. 

그외 방기능, 브로드캐스팅, 속삭임등등 일반 채팅방을 쉽게 구현할 수 있습니다.

또한 일반적으로 kafka, redis 와 다양한 mq 서버들과 연동이 가능합니다.


1. 목표

  • 간단한 채팅방이 가능한 채팅 기능을 만들어 본다
  • fastapi에서 socketio를 많이 사용을 안 하는지? 업데이트가 잘 안되고 있어서
    github등의 예제로는 404 Not Found, 403 등의 오류가 나서 접속이 안되었다
    => 이것 때문에 쓴 글이기도 하다

2. 사용

  • socket.io 기반으로 python-socketio를 fastapi에 맞게 개발한 fastapi_socketio를 
    기반으로 한다.
  • client는 일단 html로 한다.
  • quasar에 socketio client를 추가해서 다시 만든다. 

3. 나의 선택

  • 나는 socketio를 사용하지 않을 듯하다. 이미 websocket + redis로 유사한 기능을
    개발했기도 하고
  • 하도 socketio가 좋다고 하지만, 사용성에 있어 생각보다 클리어하지 않았고
  • 무엇보다 nodejs 로 개발되었고..
  • 뭔가 뒤에서 무거운 작업을 많이 할 것도 같고..

4. 설치 및 소스

pnpm install fastapi_socketio 

FastAPI의 app.py   

app = createApp() # App 생성 이후에 socketio를 만들어 줌.

############################# socket.io fastapi-socketio BEGIN ####################################

from fastapi_socketio import SocketManager
sio = SocketManager(app=app)

app.add_route("/socket.io/", route=sio, methods=["GET", "POST"])
app.add_websocket_route("/ws/socket.io/", sio)


######### --------------------------
@app.sio.on('connect')
async def connect(sid, environ):
print('connect ', sid)

@app.sio.on('disconnect')
def disconnect(sid):
print('disconnect ', sid)

@app.sio.on('join')
async def handle_join(sid, *args, **kwargs):
print('join ', sid)

# 방 입장 이벤트 처리
@app.sio.on("enter_room")
async def handle_enter_room(sid, room):
print(f"{sid} entered room {room}")
sio.enter_room(sid, room) # 클라이언트를 지정한 방에 입장시킴

# 방에서 나가기 이벤트 처리
@app.sio.on("leave_room")
async def handle_leave_room(sid, room):
print(f"{sid} left room {room}")
sio.leave_room(sid, room) # 클라이언트를 지정한 방에서 나가게 함

@app.sio.on('message')
async def message(sid, data):
print('message ', data)
# 받은 메시지 다시 전송
    
await sio.send(f'Re2 >>>>>>> :{data}')

chat_client_socketio.html

<!DOCTYPE html>

<html lang="ko">

<head>
<meta charset="UTF-8">
<title>Chat</title>
<script src="https://cdn.socket.io/4.7.0/socket.io.min.js"></script>
<script>
// Create a socket connection 아래 transports: ['websocket', 'polling'] } 두개를 써줘야 한다.
var socket = io("ws://localhost:8001", { path: "/ws/socket.io/", transports: ['websocket', 'polling'] });
// var socket = io("http://localhost:8001", { path: "/socket.io/", transports: ['websocket', 'polling'] }
</script>
</head>

<body>
<h1>FastAPI Socket.IO Chat</h1>
<div id="chat"></div>
<input type="text" id="message" value="내 메시지" placeholder="메시지 입력..." />
<button onclick="sendMessage()">전송</button>

<!-- 방 입장 및 퇴장 버튼 -->
<input type="text" id="room" value='7번방' placeholder="방 이름 입력..." />
<button onclick="enterRoom()">방 입장</button>
<button onclick="leaveRoom()">방 퇴장</button>
<button onclick="sendMessageToRoom()">방에만 전송</button>

<script>
socket.on('message', (data) => {
const chat = document.getElementById('chat');
chat.innerHTML += `<p>${data}</p>`;
});

function sendMessage() {
const message = document.getElementById('message').value;
socket.emit('message', message);
// document.getElementById('message').value = '';
}

function sendMessageToRoom() {
const room = document.getElementById('room').value; // 방 이름 가져오기
const message = document.getElementById('message').value;

const messageData = {
message: message,
room: room
};

socket.emit('message', { message, room }); // 메시지와 방 이름 함께 전송
}

function enterRoom() {
const room = document.getElementById('room').value;
socket.emit('enter_room', room);
// document.getElementById('room').value = '';
}

function leaveRoom() {
const room = document.getElementById('room').value;
socket.emit('leave_room', room);
// document.getElementById('room').value = '';
}
</script>
</body>

</html>

* 서버 재시작 후 client가 자동 연결되는 로그


INFO:     Application startup complete.

INFO:     ('127.0.0.1', 61816) - "WebSocket /socket.io/?EIO=4&transport=websocket" [accepted]

INFO:     ('127.0.0.1', 61824) - "WebSocket /socket.io/?EIO=4&transport=websocket" [accepted]

connect  tek0e16lm0TKD5FCAAAC

connect  QrqGhNp0FnIQiF9GAAAD


* 채팅 화면



* 참고

- FastAPI,socketio,python-socketio,Quasar,websocket,fastapi_socketio,long polling,

- https://channel.io/ko/blog/tech-socketio-redis-adapter-improvement 

- https://news.hada.io/topic?id=1124 websocket 재연결 문제 해결한 라이브러리
  https://sarus.anephenix.com/

- 웹소켓으로 GIS 작업 : https://progworks.tistory.com/65



추가적으로 톱은 왜? 샀나? 애들이 어릴때 2층 침대를 샀는데, 큰애가 대학을 가면서 나가서

둘째이 독립 침대를 만들어주려고 다리를 짤라야 했다.



자르는 동영상


https://www.youtube.com/shorts/VN-hdmi_gBc


아래 제품 소개는 제가 직접 사서 써본거에요~ 목적은 애들 2층 침대가 있는데,

다리를 잘라서 단층으로 만들거에요~

"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다." 


1. 길이가 좀 짧은것(20CM) : 좁은 공간 작업 + 휴대성 (캠핑+벌초)

          대건 국산 파워이지 접톱 210MM 1개, DE-903

2. 길이가 좀 긴것(30CM) : 빠른 작업

  대건 국산 파워이지 접톱 300MM 1개, DE-906

라벨: , , , , , ,

2023년 6월 14일 수요일

[2023-06-14] Quasar fileuploader 문제 해결 422 (Unprocessable Entity) performUpload @ quasar.esm.js:36051 runFactory

 안녕하세요. 클스 입니다.


Quasar는 정말 vue3로 구현된 좋은 프레임워크 입니다. 

아직 사용자가 많지 않아서 레퍼런스가 별로 없다는 것이 문제긴 합니다.

파일 업로더가 있는데 파일을 Drag & Drop 하면 서버에 올려줍니다. 물론 서버 rest api 가 있어야 합니다.

https://quasar.dev/vue-components/uploader

그런데 메뉴얼 대로 했는데, 계속 오류가 납니다.

422 (Unprocessable Entity) performUpload @ quasar.esm.js:36051 runFactory


서버로 multi-part로 파일들을 보내는데, 서버쪽에서 files를 못받아서 문제 입니다.

RESTAPI 서버는 아래 오류를 계속 발생 시킵니다. swagger로 하면 문제없이 동작 합니다.

INFO:     127.0.0.1:59040 - "POST /api/upload/files HTTP/1.1" 422 Unprocessable Entity


fiileupload.py

@router.post("/upload/files")
async def create_upload_files(files: list[UploadFile]):
for file in files:
contents = await file.read()
with open(os.path.join(UPLOAD_DIR, file.filename), "wb") as fp:
fp.write(contents)
print(file.filename)

return {"filenames": [file.filename for file in files]}

fileupload.vue


<template>
<q-page class="bg-light-green window-height window-width row justify-center items-center">
<div class="q-pa-md">
<div class="q-gutter-sm row items-start">
<q-uploader
url="http://localhost:8001/api/upload/files"
label="Individual upload"
field-name="files" <!-- 서버에서 받을 변수명입니다. 아주 중요 -->
multiple
auto-upload
style="max-width: 300px"
/>
<q-uploader
url="http://localhost:8001/api/upload/files"
label="Batch upload"
multiple
field-name="files"
batch
style="max-width: 300px"
/>
</div>
</div>
</q-page>
</template>

오류의 원인이 이걸 안넣어줘서 그렇네요
field-name="files" <!-- 서버에서 받을 변수명입니다. 아주 중요 -->
async def create_upload_files(files: list[UploadFile]):
서버쪽에 넘겨줄 변수명이라 생각하면 됩니다. 그래서 맞춰주면 잘 올라갑니다.

아래 실행 화면에서 2개 타입이 있는데 하나는 파일을 선택하면 바로 올라가는 모드고, 다른 하나는
추가 후 upload 버튼으로 실행해줘야 합니다.

실행 화면



서버쪽에 올라간 파일 확인해보면 잘 올라가 있습니다.


이상 클스 였습니다.

라벨: , , , , ,

2023년 6월 8일 목요일

[2023-06-07] Quasar dotenv 로 multi profile 구축하기

 안녕하세요. 클스 입니다.


프로젝트를 하다보면 local, dev, test, staging, production 등 다양한 환경으로 API 주소등 환경을 다르게 가져가야 할 때가 있습니다.

이때 필요한 것이 env 파일을 이용해야 합니다.


Quasar 는 process.env 를 기본으로 제공합니다. quasar.config.js에 환경 변수를 넣어주면

process.env.변수 이렇게 사용하면 됩니다.

https://quasar.dev/quasar-cli-webpack/handling-process-env/

그러나 별도 파일로 구성하면 유지관리가 직관적이고 편리하기 때문에 dotenv 패키지를 많이 사용합니다.

1) dotenv 설치하기

$ yarn add dotenv

$ .env 폴더 생성 ==> project root에 생성합니다.



2) .env.local 파일 생성 후 아래 추가

APP_TITLE=':Admin'
APP_MODE='local-로컬'
APP_VERSION='0.6.5'
API_URL=http://localhost:8001

3) quasar.config.js 에 아래 추가

/* eslint-env node */

/*
* This file runs in a Node context (it's NOT transpiled by Babel), so use only
* the ES6 features that are supported by your Node version. https://node.green/
*/

// Configuration for your app
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js

const { configure } = require('quasar/wrappers');
const path = require('path');
const DotEnv = require('dotenv'); ==> 여기 추가

그리고 build 섹션에 아래와 같이 추가합니다.

build: {
env: DotEnv.config({ path: `./.env/.env.${process.env.ENVIRONMENT}` }).parsed,
}

4) App.vue 등 vue 파일에서 사용하기

<template>
<router-view />
</template>

<script lang="ts">
import { defineComponent, defineAsyncComponent } from 'vue';

export default defineComponent({
name: 'App',
components: {
// TODO:
},
setup() {
return {
//TODO:do something
};
},
mounted() {
// console.log('App Info:', process.env);
console.log('App Info:', process.env.APP_TITLE, '-', process.env.APP_MODE);
},

watch: {
$route: {
immediate: true,
handler(to) {
document.title = to.meta.title ? to.meta.title + ' ::: KELU' : 'KELU' + '::' + process.env.APP_MODE;
},
},
},
});
</script>


여기서는 route 정보를 이용해서 app title을 변경하였습니다.


5) 실행하기

파일명이 .env.local 이므로 local를 환경변수로 받습니다.
실행할때 ENVIRONMENT 를 환경변수로 넣어주어 실행합니다.

$ ENVIRONMENT=local quasar dev .d88888b. d88P" "Y88b 888 888 888 888 888 888 8888b. .d8888b 8888b. 888d888 888 888 888 888 "88b 88K "88b 888P" 888 Y8b 888 888 888 .d888888 "Y8888b. .d888888 888 Y88b.Y8b88P Y88b 888 888 888 X88 888 888 888 "Y888888" "Y88888 "Y888888 88888P' "Y888888 888 Y8b


ENVIRONMENT 를 적절하게 변경하면 본인에게 맞는 환경을 쉽게 구축할 수 있습니다.

이상 클스 였습니다.



라벨: , , ,

2023년 5월 31일 수요일

[2023-05-31] Quasar + vite + vue 에서 raw-loader를 대체하기 vite-raw-plugin

안녕하세요. 클스 입니다.

오늘은 5월의 마지막 날이네요

Quasar는 vue3 + vite를 기반으로 합니다. 그런데 초반에 text 파일을 읽어서 처리하는데

raw-loader 를 사용했습니다. 이것은 webpack 기반이라 vite와는 맞지 않습니다.

그래서 yarn install 을 하면 아래와 같이 경고가 발생합니다.

yarn install v1.22.19
[1/5] 🔍  Validating package.json...
[2/5] 🔍  Resolving packages...
[3/5] 🚚  Fetching packages...
[4/5] 🔗  Linking dependencies...
warning " > raw-loader@4.0.2" has unmet peer dependency "webpack@^4.0.0 || ^5.0.0".
[5/5] 🔨  Building fresh packages...
✨  Done in 4.53s.

오늘은 똑 같은 기능을 하면서 vite 기반의 라이브러리로 대체해보고자 합니다.

먼저 raw-loader 삭제 합니다.
$ yarn remove raw-loader

그리고 vite-raw-plugin 을 설치합니다.
$ yarn add vite-raw-plugin

소스는 하나도 수정할 필요가 없습니다.

package.json 에 정상적으로 삭제되고 설치된 것을 확인 할 수 있습니다.
"vite-raw-plugin": "^1.0.1",

<script lang="ts">
import { useQuasar } from 'quasar';
import { defineComponent, defineAsyncComponent, ref } from 'vue';
import getVueTemplate from 'src/ztemplate/vue_gen_template.txt?raw';

... 생략 ...

export default defineComponent({
name: 'LoginPageEx',
components: {
    //TODO: Your components
},

setup() {
const $q = useQuasar();
return {
    //TODO:
};
},
data() {
return {
    //TODO:
};
},
methods: {
showFile() {
console.log('getVueTemplate > ', getVueTemplate);
this.$q
.dialog({
title: 'Source Code Here <클립보드로 들어가 있어요>!!!',
message: getVueTemplate,
style: 'min-width:50vw;min-height=50vh',
html: false,
})
.onOk(() => {
// console.log('OK')
})
.onCancel(() => {
// console.log('Cancel')
})
.onDismiss(() => {
// console.log('I am triggered on both OK and Cancel')
});
},
... 생략 ...

기존소스 변경없이 적용되니 좋네요~

이상입니다.

라벨: , , , ,

[2023-05-31] Quasar, Vue3 : Warning "Extraneous non-emits event listeners" shows up even when emits is set ...

안녕하세요. 클스 입니다.


Child.vue --> Parent.vue 와  데이터를 주고받을 때, emit을 사용합니다.

그런데 아래와 같은 오류가 발생합니다.

Warning "Extraneous non-emits event listeners" shows up even when emits is set

declare it using the "emits" option.


해결 방법1)

Parent.vue 에서 root 로 div를 감싸주는 된다. 그런데 UI 가 깨지는 문제가 발생한다.

<template> 
    <div>  : 추가
      .... 여러가지 컴포넌트 들....
    <MenuComponent msg="string input" @testChange="testChange"/>
   </div>   : 추가
</template>

Child.vue 에서 아래와 같이 보내준다.

<script lang="ts">

...

methods: {
emitTestChange(val) {
this.$emit('testChange', val);
},

</script>


해결 방법2) 

Child.vue에 emits 를 추가해준다.

Parent.vue 에서 root 로 div를 감싸주는 된다. 그런데 UI 가 깨지는 문제가 발생한다.

<template> 
      .... 여러가지 컴포넌트 들....
    <MenuComponent msg="string input" @testChange="testChange"/>
</template>

Child.vue 에서 아래와 같이 보내준다.

<script lang="ts">

export default defineComponent({
emits: ['testChange'], ==> 이거 한줄 추가
setup(props) {
return {
        //TODO:
    };
},
methods: {
emitTestChange(val) {
this.$emit('testChange', val);
},

</script>


이상 클스 였습니다.

테스트 버전 ; vue3   quasar 2.11.x  chrome, macos 13.4 입니다.

라벨: , , ,

2023년 5월 15일 월요일

[2023-05-15] Quasar QSelect 옵션 설명

 안녕하세요. 클스 입니다.


Quasar QSelect 옵션에 대한 설명을 하고자 합니다.


회사를 선택하거나, 여러 코드값을 선택할때 아래와 같은 화면이 필요합니다. 그리고 company_id 값으로

미리 선택되도록 해야 합니다. q-select 의 option 은 label, value 2개 속성이 기본 입니다. 

물론 option-value = 'yyy' , option-label = "xxx" 이렇게 해서 커스텀 가능합니다.

company_options: [
{
label: 'Google',
value: 'Google-value',
description: 'Search engine',
category: '1',
},


 
소스를 보면 아래와 같이 사용하면 됩니다. 그런데 map-options, emit-value 2개 옵션을 잘 사용해야 원하는 값을 얻게 됩니다.

<q-select
filled
v-model="company_model"
:options="company_options"
label="Standard"
map-options
emit-value />


map-options 은 value 값으로 선택을 할 수 있게 하는 옵션입니다.

emit-value 은 선택된 option의 value 값만 넘겨 줍니다.


앞에서 option을 object로 선언했기 때문에 map-option이 없으면 value 값이 화면에 표시됩니다.

아래 화면에서 보면 Apple-Value를 표시됩니다. 원하는건 Apple이 표시되길 원합니다.

이때 map-options 을 사용하면 value 값으로 매칭해서 Label을 출력해줍니다.

그리고 Object로 선언된 option이므로 선택하면 Object 가 값으로 넘어옵니다. company_id만 필요한데 불필요하게 Object 값이 넘어오니 company_model.value 이렇게 사용해야 됩니다.
바로 value를 string으로 받기 위해서 emit-value 사용하면 됩니다.

emit-value 를 사용하지 않으면 아래와 같이 값이 넘어옵니다.

최종적으로 우리가 원하는 화면을 위해서는 아래와 같이 전체 소스를 확인할 수 있습니다.

<!-- MainLayout.vue -->

<template>
<div class="q-pa-md" style="max-width: 300px">
<div class="q-gutter-md">
<q-badge color="secondary" multi-line> Model: "{{ company_model }}" </q-badge>
<p>Selected Value: {{ company_model }}</p>

<q-select filled v-model="company_model" :options="company_options" label="Standard" emit-value map-options />
</div>
</div>
</template>

<script>
import { ref } from 'vue';

export default {
setup() {
return {
company_model: ref(null),

company_options: [
{
label: 'Google',
value: 'Google-value',
description: 'Search engine',
category: '1',
},
{
label: 'Facebook',
value: 'Facebook-value',
description: 'Social media',
category: '1',
},
{
label: 'Twitter',
value: 'Twitter-value',
description: 'Quick updates',
category: '2',
},
{
label: 'Apple',
value: 'Apple-value',
description: 'iStuff',
category: '2',
},
{
label: 'Oracle',
value: 'Oracle-value',
disable: true,
description: 'Databases',
category: '3',
},
],
};
},
mounted() {
this.company_model = 'Apple-value';
},
};
</script>


라벨: , , ,

2022년 12월 30일 금요일

quasar + vite + vue + typescript

 quasar + vite + vue + typescript <Mac>

1. 사전 작업

  1. pyenv + virtualenv + poetry 설치
  2. python 3.11.1 설치
  3. virtualenv로 가상환경 생성 py3.11.1

2. 환경

$ pyenv activate py3.11.1
$ yarn global add @quasar/cli
$ npm install -g @vue/cli
$ mkdir -p ~/myprj/quasar && cd $_

* vue3js + vite + quasar + typescript 로 프로젝트가 만들어짐
$ yarn create vite my-vue-app --template vue

* 실행
$ vite dev

  VITE v4.0.2  ready in 377 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h to show help
  • 결과 <브라우저에서 확인>














라벨: , ,