Merge remote-tracking branch 'origin/main' into board-0117

This commit is contained in:
kimdaae328 2025-01-17 14:13:59 +09:00
commit d2f7aed762
13 changed files with 725 additions and 26 deletions

View File

@ -84,6 +84,9 @@
<!-- Page JS -->
<!-- <script src="/js/dashboards-analytics.js"></script> -->
<!-- daum address -->
<script src="//t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js"></script>
<script type="module" src="/src/main.js"></script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,128 @@
<template>
<div class="mb-2">
<div class="d-flex align-items-center">
<label :for="name" class="col-md-2 col-form-label">
{{ title }}
<span :class="isEssential ? 'link-danger' : 'd-none'">*</span>
</label>
<button type="button" class="btn btn-sm btn-primary" @click="openAddressSearch">주소찾기</button>
</div>
<div class="col-md-12">
<div class="d-flex mb-3">
<input
:id="name"
class="form-control me-2 w-25"
type="text"
v-model="postcode"
placeholder="우편번호"
readonly
/>
<input
class="form-control"
type="text"
v-model="address"
placeholder="기본주소"
readonly
/>
</div>
<div>
<input
class="form-control"
type="text"
v-model="detailAddress"
placeholder="상세주소"
@input="updateDetailAddress"
:maxLength="maxlength"
/>
</div>
<div class="invalid-feedback" :class="isAlert ? 'd-block' : ''">{{ title }} 확인해주세요.</div>
</div>
</div>
</template>
<script setup>
import { ref, watch } from 'vue';
const props = defineProps({
title: {
type: String,
default: '라벨',
required: true,
},
name: {
type: String,
default: 'nameplz',
required: true,
},
isEssential: {
type: Boolean,
default: false,
required: false,
},
maxlength: {
type: Number,
default: 30,
required: false,
},
isAlert: {
type: Boolean,
default: false,
required: false,
},
});
const emits = defineEmits(['update:data', 'update:alert']);
//
const postcode = ref('');
const address = ref('');
const detailAddress = ref('');
//
const openAddressSearch = () => {
new window.daum.Postcode({
oncomplete: (data) => {
postcode.value = data.zonecode;
address.value = data.address;
//
emitAddressData();
//
setTimeout(() => {
const detailInput = document.querySelector('input[placeholder="상세주소"]');
if (detailInput) detailInput.focus();
}, 100);
}
}).open();
};
//
const updateDetailAddress = (event) => {
detailAddress.value = event.target.value;
emitAddressData();
};
//
const emitAddressData = () => {
const fullAddress = {
postcode: postcode.value,
address: address.value,
detailAddress: detailAddress.value,
fullAddress: `${address.value} ${detailAddress.value}`.trim()
};
emits('update:data', fullAddress);
};
// isAlert false
watch([postcode, address], ([newPostcode, newAddress]) => {
if (newPostcode && newAddress) {
emits('update:alert', false);
}
});
</script>

View File

@ -1,11 +1,11 @@
<template>
<div class="mb-4 row">
<label for="input-ss" class="col-md-2 col-form-label" :class="isLabel ? 'd-block' : 'd-none'">
<div class="mb-2" :class="isRow ?'row' : ''">
<label :for="name" class="col-md-2 col-form-label" :class="isLabel ? 'd-block' : 'd-none'">
{{ title }}
<span :class="isEssential ? 'text-red' : 'none'">*</span>
<span :class="isEssential ? 'link-danger' : 'none'">*</span>
</label>
<div class="col-md-10">
<select class="form-select" id="input-ss" v-model="selectData">
<div :class="isRow ?'col-md-10' : 'col-md-12'">
<select class="form-select" :id="name" v-model="selectData">
<option v-for="(item , i) in data" :key="item" :value="i" :selected="value == i">{{ item }}</option>
</select>
</div>
@ -50,7 +50,12 @@ const props = defineProps({
isLabel : {
type: Boolean,
default: true,
required: true,
required: false,
},
isRow : {
type: Boolean,
default: true,
required: false,
}
});

View File

@ -1,11 +1,8 @@
<template>
<div class="mb-2">
<label
:for="name"
class="col-md-2 col-form-label"
>
<label :for="name" class="col-md-2 col-form-label">
{{ title }}
<span :class="isEssential ? 'text-red' : 'none'">*</span>
<span :class="isEssential ? 'link-danger' : 'd-none'">*</span>
</label>
<div class="col-md-12">
<div v-if="useInputGroup" class="input-group mb-3">
@ -17,10 +14,8 @@
:value="value"
:maxLength="maxlength"
:placeholder="title"
aria-label="Recipient's username"
:aria-describedby="`${name}-addon`"
/>
<span class="input-group-text" :id="`${name}-addon`">@ localhost.co.kr</span>
<span class="input-group-text">@ localhost.co.kr</span>
</div>
<div v-else>
<input
@ -34,7 +29,7 @@
/>
</div>
<div class="invalid-feedback" :class="isAlert ? 'display-block' : ''">{{ title }} 확인해주세요.</div>
<div class="invalid-feedback" :class="isAlert ? 'd-block' : ''">{{ title }} 확인해주세요.</div>
</div>
</div>
</template>
@ -85,7 +80,7 @@ const prop = defineProps({
},
});
const emits = defineEmits(['update:data'])
const emits = defineEmits(['update:data', 'update:alert'])
const updateInput = function (event) {
//Type Number maxlength
@ -93,11 +88,13 @@ const updateInput = function (event) {
event.target.value = event.target.value.slice(0, prop.maxlength);
}
emits('update:data', event.target.value);
// isAlert false
if (event.target.value.trim() !== '') { emits('update:alert', false); }
};
</script>
<style>
.none {
display: none;
}
</style>

View File

@ -0,0 +1,67 @@
<template>
<form @submit.prevent="handleSubmit">
<div class="col-xl-12">
<UserFormInput
title="아이디"
name="id"
:is-alert="idAlert"
:useInputGroup="true"
@update:data="handleIdChange"
:value="id"
/>
<UserFormInput
title="비밀번호"
name="pw"
type="password"
:is-alert="passwordAlert"
@update:data="handlePasswordChange"
:value="password"
/>
<div class="d-grid gap-2 mt-7 mb-5">
<button type="submit" class="btn btn-primary">로그인</button>
</div>
<div class="mb-3 d-flex justify-content-around">
<div>
<input type="checkbox" class="form-check-input" id="rememberCheck" />
<label class="form-check-label fw-bold" for="rememberCheck">&nbsp;자동로그인</label>
</div>
<RouterLink class="text-dark fw-bold" to="/register">등록신청</RouterLink>
<RouterLink class="text-dark fw-bold" to="/pw">비밀번호 찾기</RouterLink>
</div>
</div>
</form>
</template>
<script setup>
import { ref } from 'vue';
import UserFormInput from '@c/input/UserFormInput.vue';
const id = ref('');
const password = ref('');
const idAlert = ref(false);
const passwordAlert = ref(false);
const emit = defineEmits(['submit']);
const handleIdChange = value => {
id.value = value;
idAlert.value = false;
};
const handlePasswordChange = value => {
password.value = value;
passwordAlert.value = false;
};
const handleSubmit = () => {
idAlert.value = id.value.trim() === '';
passwordAlert.value = password.value.trim() === '';
if (!idAlert.value && !passwordAlert.value) {
emit('submit', { id: id.value, password: password.value });
}
};
</script>

View File

@ -0,0 +1,32 @@
<template>
<div class="text-center">
<span>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="100"
height="100"
viewBox="0 0 100 100"
>
<image
xlink:href="data:image/png;base64,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"
x="0"
y="0"
width="100"
height="100"
/>
</svg>
</span>
<h2>{{ title }}</h2>
</div>
</template>
<script setup>
const prop = defineProps({
title: {
type: String,
default: '라벨',
required: true,
},
});
</script>

View File

@ -0,0 +1,216 @@
<template>
<form @submit.prevent="handleSubmit">
<div class="text-center">
<label
for="profilePic"
class="rounded-circle m-auto border-label-secondary ui-bg-cover position-relative cursor-pointer"
id="profileLabel"
style="width: 100px; height: 100px; background-image: url(public/img/avatars/default-Profile.jpg)"
>
</label>
<span class="link-danger position-absolute">*</span>
<input type="file" id="profilePic" class="d-none" @change="profileUpload" />
<span v-if="profilerr" class="invalid-feedback d-block">{{ profilerr }}</span>
</div>
<div class="col-xl-12">
<UserFormInput
title="아이디"
name="id"
:isEssential="true"
:is-alert="idAlert"
:useInputGroup="true"
@update:data="id = $event"
@update:alert="idAlert = $event"
:value="id"
/>
<UserFormInput
title="비밀번호"
name="pw"
type="password"
:isEssential="true"
:is-alert="passwordAlert"
@update:data="password = $event"
@update:alert="passwordAlert = $event"
:value="password"
/>
<UserFormInput
title="비밀번호 확인"
name="pwch"
type="password"
:isEssential="true"
:is-alert="passwordcheckAlert"
@update:data="passwordcheck = $event"
@update:alert="passwordcheckAlert = $event"
:value="passwordcheck"
/>
<UserFormInput
title="비밀번호 힌트"
name="pwhint"
:is-alert="passwordhintAlert"
@update:data="passwordhint = $event"
@update:alert="passwordhintAlert = $event"
:value="passwordhint"
/>
<div class="d-flex">
<UserFormInput
title="이름"
name="name"
:is-essential="true"
:is-alert="nameAlert"
@update:data="name = $event"
@update:alert="nameAlert = $event"
:value="name"
class="me-2 w-50"
/>
<FormSelect
title="컬러"
name="color"
:is-essential="true"
:is-row="false"
:is-label="true"
:data="colorList"
@update:data="color = $event"
class="w-50"
/>
</div>
<div class="d-flex">
<UserFormInput
title="생년월일"
name="birth"
:is-essential="true"
:is-alert="birthAlert"
@update:data="birth = $event"
@update:alert="birthAlert = $event"
:value="birth"
class="me-2 w-50"
/>
<FormSelect
title="MBTI"
name="mbti"
:is-essential="true"
:is-row="false"
:is-label="true"
:data="mbtiList"
@update:data="mbti = $event"
class="w-50"
/>
</div>
<ArrInput
title="주소"
name="arr"
:isEssential="true"
:is-alert="arrAlert"
@update:data="arr = $event"
@update:alert="arrAlert = $event"
:value="arr"
/>
<UserFormInput
title="휴대전화"
name="phone"
:isEssential="true"
:is-alert="phoneAlert"
@update:data="phone = $event"
@update:alert="phoneAlert = $event"
:maxlength="11"
:value="phone"
/>
<div class="d-flex mt-5">
<RouterLink type="button" class="btn btn-secondary me-2 w-50" to="/login">취소</RouterLink>
<button type="submit" class="btn btn-primary w-50">등록신청</button>
</div>
</div>
</form>
</template>
<script setup>
import { ref } from 'vue';
import UserFormInput from '@c/input/UserFormInput.vue';
import FormSelect from '@/components/input/FormSelect.vue';
import ArrInput from '@/components/input/ArrInput.vue';
const profile = ref(null);
const profilerr = ref('');
const id = ref('');
const password = ref('');
const passwordcheck = ref('');
const passwordhint = ref('');
const name = ref('');
const birth = ref('');
const arr = ref('');
const phone = ref('');
const color = ref('');
const mbti = ref('');
const idAlert = ref(false);
const passwordAlert = ref(false);
const passwordcheckAlert = ref(false);
const passwordhintAlert = ref(false);
const nameAlert = ref(false);
const birthAlert = ref(false);
const arrAlert = ref(false);
const phoneAlert = ref(false);
const handleSubmit = async () => {
idAlert.value = id.value.trim() === '';
passwordAlert.value = password.value.trim() === '';
passwordcheckAlert.value = passwordcheck.value.trim() === '';
passwordhintAlert.value = passwordhint.value.trim() === '';
nameAlert.value = name.value.trim() === '';
birthAlert.value = birth.value.trim() === '';
arrAlert.value = arr.value.trim() === '';
phoneAlert.value = phone.value.trim() === '';
if (!profile.value) {
profilerr.value = '프로필 이미지를 선택해주세요.';
return;
}
};
const profileValid = (size, type) => {
const maxSize = 5 * 1024 * 1024;
const validTypes = ['image/jpeg', 'image/png'];
if (size > maxSize) {
profilerr.value = '5MB 미만의 파일만 업로드할 수 있습니다.';
return false;
}
if (!validTypes.includes(type)) {
profilerr.value = '지원되지 않는 파일 형식입니다. JPEG 또는 PNG만 가능합니다.';
return false;
}
profilerr.value = '';
return true;
};
const profileUpload = e => {
const file = e.target.files[0];
const profileLabel = document.getElementById('profileLabel');
if (!profileValid(file.size, file.type)) {
e.target.value = '';
profileLabel.style.backgroundImage = 'url("public/img/avatars/default-Profile.jpg")';
return false;
}
const profilePic = e.target;
const url = URL.createObjectURL(profilePic.files[0]);
profileLabel.style.backgroundImage = `url(${url})`;
profile.value = file;
};
const colorList = ['blue', 'red', 'pink'];
const mbtiList = ['ISTP', 'ENFP', 'INTJ'];
</script>
<style></style>

View File

@ -32,16 +32,24 @@ const routes = [
}
]
},
// 레이아웃 필요없을 때 예시
// {
// path: '/login',
// component: () => import('@v/user/TheLogin.vue'),
// meta: { layout: 'NoLayout' },
// },
{
path: '/wordDict',
component: () => import('@v/wordDict/wordDict.vue'),
},
{
path: '/login',
component: () => import('@v/user/TheLogin.vue'),
meta: { layout: 'NoLayout' },
},
{
path: '/vacation',
component: () => import('@v/vacation/VacationManagement.vue'),
},
{
path: '/register',
component: () => import('@v/user/TheRegister.vue'),
meta: { layout: 'NoLayout' },
},
{
path: '/sample',
component: () => import('@c/calendar/SampleCalendar.vue'),

View File

@ -127,7 +127,7 @@ const write = async () => {
const formData = new FormData();
formData.append("file", file); //
formData.append("MEMBERSEQ",registrantId); //
formData.append("CMNFLEPAT", fileInfo.path); //
formData.append("CMNFLENAM", fileInfo.originalName); // ()
formData.append("CMNFLEORG", fileInfo.originalName); // ()

View File

@ -0,0 +1,19 @@
<template>
<div class="d-flex justify-content-center align-items-center vh-100">
<div class="container container-p-y rounded bg-white" style="max-width: 500px">
<LogoHeader title="LOCALNET" />
<LoginForm @submit="handleSubmit" />
</div>
</div>
</template>
<script setup>
import LoginForm from '@c/user/LoginForm.vue';
import LogoHeader from '@c/user/LogoHeader.vue';
const handleSubmit = async ({ id, password }) => {
console.log('Login');
};
</script>
<style></style>

View File

@ -0,0 +1,17 @@
<template>
<div class="d-flex justify-content-center align-items-center">
<div class="container rounded bg-white my-10 py-10" style="max-width: 500px">
<RegisterForm @submit="handleSubmit" />
</div>
</div>
</template>
<script setup>
import RegisterForm from '@c/user/RegisterForm.vue';
const handleSubmit = async (formData) => {
console.log('Register');
};
</script>
<style></style>

View File

@ -0,0 +1,207 @@
<template>
<div class="container-xxl flex-grow-1 container-p-y">
<div class="card">
<!-- 타이틀, 검색 -->
<div class="row mt-4">
<div class="col-6">
<h5 class="mb-0">용어집</h5>
</div>
<div class="col-6">
<SearchBar @update:data="search"/>
</div>
</div>
<!-- 단어 갯수, 작성하기 -->
<div class="mt-4">
단어 : {{ filteredList.length }}
<WriteButton />
</div>
<div>
<div class="nav-align-top">
<ul class="nav nav-pills" role="tablist">
<li class="nav-item" role="presentation">
<button type="button" class="nav-link"></button>
</li>
<li class="nav-item" role="presentation">
<button type="button" class="nav-link"></button>
</li>
<li class="nav-item" role="presentation">
<button type="button" class="nav-link"></button>
</li>
</ul>
<ul class="nav nav-pills" role="tablist">
<li class="nav-item" role="presentation">
<button type="button" class="nav-link">a</button>
</li>
<li class="nav-item" role="presentation">
<button type="button" class="nav-link">b</button>
</li>
<li class="nav-item" role="presentation">
<button type="button" class="nav-link">c</button>
</li>
</ul>
</div>
</div>
<!-- 카테고리 -->
<div>
<ul v-if="cateList.length">
<li v-for="item in cateList" :key="item.CMNCODVAL">
<button>{{ item.CMNCODNAM }}</button>
</li>
</ul>
</div>
<!-- 용어 리스트 -->
<div>
<!-- 로딩 중일 -->
<div v-if="loading">로딩 중...</div>
<!-- 에러 메시지 -->
<div v-if="error" class="error">{{ error }}</div>
<!-- 단어 목록 -->
<ul v-if="filteredList.length" class="word-list">
<li v-for="item in filteredList" :key="item.WRDDICSEQ">
<div class="d-flex">
<div>
({{ item.category }}) <strong>{{ item.WRDDICTTL }}</strong>
</div>
<WriteButton />
</div>
<p>{{ item.WRDDICCON }}</p>
<div>
<img :src="'/img/avatars/' + item.author.profileImage" alt="최초 작성자" />
최초 작성자 {{ item.author.name }}
작성일 {{ new Date(item.author.createdAt).toLocaleString() }}
</div>
<div>
<img :src="'/img/avatars/' + item.lastEditor.profileImage" alt="최근 수정자" />
최근 수정자 {{ item.lastEditor.name }}
수정일 {{ new Date(item.lastEditor.updatedAt).toLocaleString() }}
</div>
</li>
</ul>
<!-- 데이터가 없을 -->
<div v-else-if="!loading && !error">용어집의 용어가 없습니다.</div>
</div>
<!-- 작성 수정 -->
<div>
<div>
<FormSelect/>
<button>쇼핑몰</button>
<button>저장</button>
</div>
<div>
<FormInput/>
</div>
<div>
<QEditor/>
<button>저장</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, watchEffect, computed } from 'vue';
import axios from '@api';
import SearchBar from '@c/search/SearchBar.vue';
import WriteButton from '@c/button/WriteBtn.vue';
import QEditor from '@/components/editor/QEditor.vue';
import FormInput from '@/components/input/FormInput.vue';
import FormSelect from '@/components/input/FormSelect.vue';
// /** */
// const goDetail = idx => {
// router.push(`/board/get/${idx}`);
// };
//
const wordList = ref([]);
//
const cateList = ref([]);
const loading = ref(false);
const error = ref('');
//
const searchText = ref('');
// API
const fetchAllData = async () => {
loading.value = true;
error.value = '';
try {
//
const wordResponse = await axios.get('worddict/getWordList');
wordList.value = wordResponse.data.data.data;
console.log('단어 데이터:', wordList.value);
//
const categoryResponse = await axios.get('worddict/getWordCategory');
cateList.value = categoryResponse.data.data;
console.log('카테고리 데이터:', cateList.value);
} catch (err) {
error.value = '데이터를 가져오는 중 문제가 발생했습니다.';
} finally {
loading.value = false;
}
};
//
const search = (e) => {
const trimmedSearchText = e.trim();
if (trimmedSearchText.length === 0) {
alert('검색어를 입력해주세요.');
return;
}
searchText.value = trimmedSearchText;
};
//
const filteredList = computed(() =>
wordList.value.filter(item =>
item.WRDDICTTL.toLowerCase().includes(searchText.value.toLowerCase())
)
);
// `watchEffect` API
watchEffect(() => {
fetchAllData();
});
</script>
<style scoped>
.card > div {
padding: 0 30px
}
.word-list {
padding-left: 0;
}
.word-list li {
list-style: none;
}
.word-list li ~ li {
margin-top: 20px;
}
.error {
color: red;
font-weight: bold;
}
</style>