본문 바로가기
▶ Front-End/Vue.js

Vue3 기초 예제 - 입력 글자 수 표시(Computed)

by 오늘도 코딩 2023. 11. 21.
728x90
반응형

입력 글자 수 제한을 사용자가 알기 쉽게 화면에 동적 입력 글자 수 표현

*자세한 설명 생략

 

 

▷ Computed 란?

 

    - template의 data 표현을 간결하게 하는 속성

 

    - 가독성 ↑ 유지보수 ↑

 

*한눈에 알아보기

- 사용하기 전 코드

<h6 v-if="(inMsg.replaceAll('\n', '').length <= MAX)">
	( {{ inMsg.replaceAll('\n', '').length }} 자 / 최대 {{ MAX }} 자 
</h6>
<h6 class="maxInfo" v-else>
	( {{ inMsg.replaceAll('\n', '').length }} 자 / 최대 {{ MAX }} 자 )
</h6>

 

- 사용 후 코드

<h6 v-if="(inMsgSize <= MAX)">
	( {{ inMsgSize }} 자 / 최대 {{ MAX }} 자 )
</h6>
<h6 class="maxInfo" v-else>
	( {{ inMsgSize }} 자 / 최대 {{ MAX }} 자 )
</h6>

 

 

▷ 프로젝트 전체 구조

 

 

▷ 기초 예제 - 입력 글자 수 표시

*전체 코드

*주석 이외 설명 생략

 

- index.html

<!DOCTYPE html>
<html lang="">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <noscript>
      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

 

- Cmp.css

.MsgSendCmp {
    font-family: 'Spoqa Han Sans';
    margin-top: 10%;
    text-align: center;
}

.maxCheckDiv {
    margin-left: 200px;
}

.maxInfo {
    color: red;
}

.messageDiv {
    white-space: pre-wrap;
    text-align: left;
    margin-left: 5%;
    margin-right: 5%;
}

.toast {
    width: auto;
}

.smallButton {
    margin-left: 15px;
    height: 3px;
}

 

- main.js

/** Vue */
import { createApp } from 'vue'
import App from './App.vue'

/** PrimeVue */
import PrimeVue from 'primevue/config'
import 'primevue/resources/themes/lara-light-teal/theme.css'
import ToastService from 'primevue/toastservice'


createApp(App)
    /** 라이브러리 사용 등록 */
    .use(PrimeVue)
    .use(ToastService)
    
    /** 연결 */
    .mount('#app')

 

- App.vue

<style>
@import url('@/assets/css/Cmp.css');
</style>

<template>
  <div>
    <MsgSendCmp />
  </div>
</template>

<script>
import MsgSendCmp from '@/components/MsgSendCmp.vue'

export default {
  name: 'App',
  components: {
    MsgSendCmp
  }
}
</script>

 

- AxiosModule.js

import Axios from "axios"

const API_SERVER = "http://IP:PORT
const API_LIST = {

    "CMS-SEND-MSG": "/support/cmd",
}


function sendPost(API_URI, REQ_DATA) {
    return Axios
        .post(API_SERVER + API_URI, REQ_DATA)
        .then((res) => {
            return res.status
        })
        .catch((res) => {
            alert("잠시 후 다시 시도해주세요.")
            return res.status
        })
}

export {
    API_LIST,
    sendPost
}

 

- CmmUtile.js

/**
 * Toast 설정
 * 
 * @param {*} toast 
 * @param {*} code
 */
function setToast(toast, code) {

    const S = "success"
    const E = "error"
    const L = 1500

    if ("0000" == code) { toast.add({ detail: 'Message has been Send.', severity: S, summary: S, life: L }) }
    if ("0001" == code) { toast.add({ detail: 'Message has been Deleted.', severity: S, summary: S, life: L }) }
    if ("0002" == code) { toast.add({ detail: 'Message has been Copy.', severity: S, summary: S, life: L }) }

    if ("9001" == code) { toast.add({ detail: 'Please write your Message.', severity: E, summary: E, life: L }) }
    if ("9002" == code) { toast.add({ detail: 'Please write your ID.', severity: E, summary: E, life: L }) }
    if ("9003" == code) { toast.add({ detail: 'Message exceeds Size.', severity: E, summary: E, life: L }) }
    if ("9999" == code) { toast.add({ detail: 'Server Error. please try again.', severity: E, summary: E, life: L }) }
}

/**
 * Date Formater
 * 
 * @param {*} date 
 * @returns [YYYY-MM-dd hh:mm:ss]
 */
function getFormatDate(date) {
    const YYYY = String(date.getFullYear())
    const MM = String((date.getMonth() + 1) >= 10 ? (date.getMonth() + 1) : "0" + (date.getMonth() + 1))
    const dd = String(date.getDate() >= 10 ? date.getDate() : "0" + date.getDate())
    const hours = String(date.getHours() >= 10 ? date.getHours() : "0" + date.getHours())
    const minuts = String(date.getMinutes() >= 10 ? date.getMinutes() : "0" + date.getMinutes())
    const seconds = String(date.getSeconds() >= 10 ? date.getSeconds() : "0" + date.getSeconds())
    return "[" + YYYY + "-" + MM + "-" + dd + " " + hours + ":" + minuts + ":" + seconds + "]"
}

/**
 * Null Check
 * 
 * @param {*} str 
 * @returns default false
 */
function nullCheck(str) {
    if ("" == str) { return true }
    if (null == str) { return true }
    if (str.replace(/\s/g, '').length < 1) { return true }

    return false
}


export {
    setToast,
    getFormatDate,
    nullCheck,
}

 

- LocalStorageUtil.js

import * as CmmUtil from '@/module/CmmUtil'

/**
 * Local Storage Init
 * 
 * @param {*} key 
 * @param {*} list (ref)
 */
function initLastMsg(key, list) {
    localStorage.removeItem(key)
    list.value = []
}

/**
 * Local Storage GET Value
 * 
 * @param {*} key 
 * @param {*} list (ref)
 */
function getLastMsg(key, list) {
    if (null != localStorage.getItem(key)) {
        list.value = JSON.parse(localStorage.getItem(key))
    }
}

/**
 * Local Storage SET Value
 * 
 * @param {*} key 
 * @param {*} value (list in value)
 * @param {*} list (ref)
 */
function setLastMsg(key, value, list) {
    // 리스트 저장
    if (!CmmUtil.nullCheck(value)) {
        list.value.push({ "date": CmmUtil.getFormatDate(new Date()), "msg": value })
    }

    // 로컬 스토리지 저장
    localStorage.setItem(key, JSON.stringify(list.value))
}


export {
    getLastMsg,
    setLastMsg,
    initLastMsg,
}

 

- MsgSendCmp.vue

<template>
    <div class="MsgSendCmp">
        <h3>💌 Message</h3>
        <div>
            <Textarea v-model="inMsg" rows="5" cols="30" spellcheck="false" autoResize />
            <div class="maxCheckDiv">
                <h6 v-if="(inMsgSize <= MAX)">( {{ inMsgSize }} 자 / 최대 {{ MAX }} 자 )</h6>
                <h6 class="maxInfo" v-else>( {{ inMsgSize }} 자 / 최대 {{ MAX }} 자 )</h6>
            </div>
        </div>
        <div>
            <Button v-if="(inMsgSize <= MAX)" label="Send" severity="Primary" raised @click="sendMsg()" />
            <Button v-else label="Send" severity="danger" raised @click="sendMsg()" />
        </div><br /><br />
        <div class="messageDiv">
            <h3>💬 지난 메시지</h3>
            <div v-for="(obj, idx) in lastMsgList.slice().reverse()" :key="obj">
                <Panel toggleable>
                    <template #header>
                        <div>
                            {{ obj.date }}
                        </div>
                    </template>
                    <template #footer>
                        <Button class="smallButton" label="Copy" severity="secondary" text raised @click="selectMsg(idx)" />
                        <Button class="smallButton" label="Delete" severity="danger" text raised @click="deleteMsg(idx)" />
                    </template>
                    <p>{{ obj.msg }}</p>
                </Panel>
                <br />
            </div>
        </div>
    </div>
    <Toast class="toast" />
</template> 

<script setup>
/** Vue */
import { ref, computed, onMounted } from 'vue'

/** Util */
import * as CmmUtil from '@/module/CmmUtil'
import * as LocalStorageUtil from '@/module/LocalStorageUtil'

/** PrimeVue */
import Textarea from 'primevue/textarea'
import Button from 'primevue/button'
import Panel from 'primevue/panel'

/** PrimeVue Toast */
import Toast from 'primevue/toast';
import { useToast } from "primevue/usetoast"

/** Axios */
import * as AxiosModule from '@/module/AxiosModule'

onMounted(() => {
    /** Local Storage GET Value */
    LocalStorageUtil.getLastMsg(lastMsgKey, lastMsgList)

    /** 사용자 입력 메세지 초기화 */
    inMsgInit()
})

/** 입력 제한 글자수 */
const MAX = 20

/** 사용자 입력 메시지 */
const inMsg = ref("")
const inMsgSize = computed(() => inMsg.value.replaceAll("\n", "").length)

/** 지난 메시지 */
const lastMsgKey = "lastMsg"
const lastMsgList = ref([])

/** Toast */
const toast = useToast()

/** 사용자 입력 메세지 초기화 */
function inMsgInit() {
    if (0 == lastMsgList.value.length) {
        inMsg.value = ""
    } else {
        inMsg.value = lastMsgList.value[lastMsgList.value.length - 1].msg
    }
}

/** 지난 메시지 선택 복사 */
function selectMsg(idx) {
    inMsg.value = lastMsgList.value[lastMsgList.value.length - (idx + 1)].msg
    CmmUtil.setToast(toast, "0002")
    window.scrollTo({ top: 0, left: 0, behavior: 'smooth' }); // 스크롤 맨위 이동
}

/** 지난 메시지 선택 삭제 */
function deleteMsg(idx) {
    // 리스트에서 삭제
    lastMsgList.value.splice(lastMsgList.value.length - (idx + 1), 1)

    /** Local Storage SET Value */
    LocalStorageUtil.setLastMsg(lastMsgKey, null, lastMsgList)

    CmmUtil.setToast(toast, "0001")

    /** 사용자 입력 메세지 초기화 */
    inMsgInit()
}

/** Send message */
async function sendMsg() {

    /** Validation */
    if (CmmUtil.nullCheck(inMsg.value)) { return CmmUtil.setToast(toast, "9001") }
    if (CmmUtil.nullCheck(localStorage.getItem("ID"))) { return CmmUtil.setToast(toast, "9002") }
    if (inMsgSize.value > MAX) { return CmmUtil.setToast(toast, "9003") }

    /* 요청 값 설정 */
    const REQ_DATA = {
        "src": "WAS",
        "dest": localStorage.getItem("ID"),
        "cmd": "Ctrl",
        "params": [inMsg.value]
    }

    /* CMS POST Send message API */
    const resVO = await AxiosModule.sendPost(AxiosModule.API_LIST['CMS-SEND-MSG'], REQ_DATA)
    if (resVO == 200) {

        /** Local Storage SET Value */
        LocalStorageUtil.setLastMsg(lastMsgKey, inMsg.value, lastMsgList)

        CmmUtil.setToast(toast, "0000")
    } else {
        CmmUtil.setToast(toast, "9999")
    }
}
</script>

 

 

▷ 결과 확인

*줄 바꿈은 글자 수에 포함하지 않는다

*글자 수 관련 Test 이외 생략

 

① 메시지 입력 후 글자 수가 동적으로 변하고, Send 클릭 시 전송 완료 확인

 

② 글자 수 초과 시 글자 수와 버튼이 빨간색으로 변경되고, Send 클릭 시 Toast 팝업 확인

 

 

▷ 관련 글

 

Vue3 기초 예제 프로젝트 정리

Vue3 기초 예제 프로젝트 구조 및 소스 정리 *관련 글이 점점 늘어나 하나로 통합하기 위함 *이 글에서만 싱크 맞춤 *주석 이외 설명 생략 *자세한 설명 생략 ▷ 프로젝트 전체 구조 < 파일 따라가

coding-today.tistory.com

 

Vue3 기초 예제 - Toast 팝업(Primevue)

사용자로부터 입력받는 간단한 페이지와 Primevue ToastService 사용 예제 *새로운 프로젝트로 진행 *API 관련 설명 생략 *자세한 설명 생략 ▷ 프로젝트 전체 구조 ▷ Primevue + ToastService 설치 *vue-cli를

coding-today.tistory.com

 

 

728x90
728x90

댓글