Merge branch 'khj' of http://192.168.0.251:3000/localnet/localhost-front into khj
This commit is contained in:
commit
e764ea30af
@ -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>
|
||||
|
||||
BIN
public/img/avatars/default-Profile.jpg
Normal file
BIN
public/img/avatars/default-Profile.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
128
src/components/input/ArrInput.vue
Normal file
128
src/components/input/ArrInput.vue
Normal 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>
|
||||
@ -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>
|
||||
@ -51,6 +51,11 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: true,
|
||||
required: true,
|
||||
},
|
||||
isRow : {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
required: true,
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<div class="container mt-4">
|
||||
<div class="mt-4">
|
||||
<div v-if="posts.length === 0" class="text-center">
|
||||
게시물이 없습니다.
|
||||
</div>
|
||||
<div v-for="post in posts" :key="post.id">
|
||||
<div v-for="post in posts" :key="post.id" @click="handleClick(post.id)">
|
||||
<BoardCard
|
||||
:img="post.img"
|
||||
:category="post.category"
|
||||
@ -30,6 +30,12 @@ export default {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ['click'],
|
||||
methods: {
|
||||
handleClick(id) {
|
||||
this.$emit('click', id);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
67
src/components/user/LoginForm.vue
Normal file
67
src/components/user/LoginForm.vue
Normal 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"> 자동로그인</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>
|
||||
32
src/components/user/LogoHeader.vue
Normal file
32
src/components/user/LogoHeader.vue
Normal 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>
|
||||
216
src/components/user/RegisterForm.vue
Normal file
216
src/components/user/RegisterForm.vue
Normal 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>
|
||||
@ -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'),
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
<template>
|
||||
<div class="container-xxl flex-grow-1 container-p-y">
|
||||
<!-- 검색 -->
|
||||
<search-bar @update:data="search" />
|
||||
|
||||
<!-- 리스트 -->
|
||||
<div class="row g-3">
|
||||
<div class="mt-8">
|
||||
<router-link to="/board/write">
|
||||
@ -8,10 +11,15 @@
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<board-card :posts="filteredList" @click="goDetail" />
|
||||
<board-card :posts="paginatedList" @click="goDetail" />
|
||||
|
||||
<!-- 페이지네이션 -->
|
||||
<div class="mt-8">
|
||||
<pagination />
|
||||
<pagination
|
||||
:current-page="currentPage"
|
||||
:total-pages="totalPages"
|
||||
@update:page="changePage"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -32,6 +40,7 @@ const searchText = ref('');
|
||||
|
||||
// 상세 페이지 이동
|
||||
const goDetail = (id) => {
|
||||
console.log('Navigating to ID:', id)
|
||||
router.push({ name: 'BoardDetail', params: { id } });
|
||||
};
|
||||
|
||||
@ -47,13 +56,38 @@ const filteredList = computed(() =>
|
||||
)
|
||||
);
|
||||
|
||||
// 페이지네이션 상태
|
||||
const currentPage = ref(1); // 현재 페이지 번호
|
||||
const itemsPerPage = 5; // 한 페이지에 표시할 아이템 수
|
||||
|
||||
// 현재 페이지 데이터 계산
|
||||
const paginatedList = computed(() => {
|
||||
const start = (currentPage.value - 1) * itemsPerPage;
|
||||
const end = start + itemsPerPage;
|
||||
return filteredList.value.slice(start, end);
|
||||
});
|
||||
|
||||
// 총 페이지 수 계산
|
||||
const totalPages = computed(() => {
|
||||
return Math.ceil(filteredList.value.length / itemsPerPage);
|
||||
});
|
||||
|
||||
// 페이지 변경 함수
|
||||
const changePage = (page) => {
|
||||
if (page >= 1 && page <= totalPages.value) {
|
||||
currentPage.value = page;
|
||||
}
|
||||
};
|
||||
|
||||
// 게시물 데이터 로드
|
||||
const fetchPosts = async () => {
|
||||
const response = await axios.get("board/general");
|
||||
console.log(response.data.data.list)
|
||||
|
||||
if (response.data && response.data.data && Array.isArray(response.data.data.list)) {
|
||||
list.value = response.data.data.list.map((post) => ({
|
||||
list.value = response.data.data.list.map((post, index) => ({
|
||||
...post,
|
||||
id: post.id || index,
|
||||
img: post.img || null,
|
||||
likes: post.likes || 0,
|
||||
comments: post.comments || 0,
|
||||
|
||||
@ -83,6 +83,7 @@ const fetchBoardDetails = async () => {
|
||||
|
||||
// 컴포넌트 마운트 시 데이터 로드
|
||||
onMounted(() => {
|
||||
console.log('Route Params:', route.params);
|
||||
fetchBoardDetails();
|
||||
});
|
||||
</script>
|
||||
|
||||
19
src/views/user/TheLogin.vue
Normal file
19
src/views/user/TheLogin.vue
Normal 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>
|
||||
17
src/views/user/TheRegister.vue
Normal file
17
src/views/user/TheRegister.vue
Normal 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>
|
||||
@ -1,9 +1,11 @@
|
||||
<template>
|
||||
<div class="vacation-management">
|
||||
<div class="container-xxl flex-grow-1 container-p-y">
|
||||
<!-- 저장 버튼 -->
|
||||
<div class="save-button-container">
|
||||
<button class="btn btn-success" @click="addVacationRequest">✔</button>
|
||||
<button class="btn btn-success" @click="addVacationRequests">✔ 저장</button>
|
||||
</div>
|
||||
<!-- 캘린더 -->
|
||||
<div class="card app-calendar-wrapper">
|
||||
<div class="row g-0">
|
||||
<div class="col app-calendar-content">
|
||||
@ -15,13 +17,25 @@
|
||||
:options="calendarOptions"
|
||||
defaultView="dayGridMonth"
|
||||
class="flatpickr-calendar-only"
|
||||
>
|
||||
</full-calendar>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 오전/오후 반차 버튼 -->
|
||||
<div class="half-day-buttons">
|
||||
<button class="btn btn-info" :class="{ active: halfDayType === 'AM' }" @click="toggleHalfDay('AM')">☀️ 오전반차</button>
|
||||
<button class="btn btn-warning" :class="{ active: halfDayType === 'PM' }" @click="toggleHalfDay('PM')">🌙 오후반차</button>
|
||||
<button
|
||||
class="btn btn-info"
|
||||
:class="{ active: halfDayType === 'AM' }"
|
||||
@click="toggleHalfDay('AM')"
|
||||
>
|
||||
☀️ 오전반차
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-warning"
|
||||
:class="{ active: halfDayType === 'PM' }"
|
||||
@click="toggleHalfDay('PM')"
|
||||
>
|
||||
🌙 오후반차
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -36,12 +50,14 @@ import dayGridPlugin from '@fullcalendar/daygrid';
|
||||
import interactionPlugin from '@fullcalendar/interaction';
|
||||
import 'flatpickr/dist/flatpickr.min.css';
|
||||
import '@/assets/css/app-calendar.css';
|
||||
import { reactive, ref } from 'vue';
|
||||
import { reactive, ref, onMounted } from 'vue';
|
||||
import axios from '@api'; // Axios 추가
|
||||
|
||||
const fullCalendarRef = ref(null);
|
||||
const calendarEvents = ref([]);
|
||||
const selectedDates = ref([]);
|
||||
const halfDayType = ref(null); // 오전/오후 반차 선택
|
||||
const selectedDates = ref(new Map());
|
||||
const halfDayType = ref(null);
|
||||
const employeeId = ref(1);
|
||||
|
||||
const calendarOptions = reactive({
|
||||
plugins: [dayGridPlugin, interactionPlugin],
|
||||
@ -56,49 +72,153 @@ const calendarOptions = reactive({
|
||||
dateClick: handleDateClick,
|
||||
});
|
||||
|
||||
/**
|
||||
* 날짜 클릭 이벤트
|
||||
*/
|
||||
function handleDateClick(info) {
|
||||
const date = info.dateStr;
|
||||
const dayElement = info.dayEl;
|
||||
|
||||
if (!selectedDates.value.includes(date)) {
|
||||
selectedDates.value.push(date);
|
||||
if (halfDayType.value === 'AM') {
|
||||
if (!selectedDates.value.has(date)) {
|
||||
const type = halfDayType.value ? (halfDayType.value === 'AM' ? 'D' : 'N') : 'F';
|
||||
selectedDates.value.set(date, type);
|
||||
|
||||
if (type === 'D') {
|
||||
dayElement.style.backgroundImage = 'linear-gradient(to bottom, #ade3ff 50%, transparent 50%)';
|
||||
} else if (halfDayType.value === 'PM') {
|
||||
} else if (type === 'N') {
|
||||
dayElement.style.backgroundImage = 'linear-gradient(to top, #ade3ff 50%, transparent 50%)';
|
||||
} else {
|
||||
dayElement.style.backgroundColor = '#ade3ff';
|
||||
}
|
||||
} else {
|
||||
selectedDates.value = selectedDates.value.filter((d) => d !== date);
|
||||
selectedDates.value.delete(date);
|
||||
dayElement.style.backgroundColor = '';
|
||||
dayElement.style.backgroundImage = '';
|
||||
}
|
||||
|
||||
halfDayType.value = null; // 날짜 클릭 후 반차 선택 초기화
|
||||
halfDayType.value = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 오전/오후 반차 선택
|
||||
*/
|
||||
function toggleHalfDay(type) {
|
||||
halfDayType.value = halfDayType.value === type ? null : type;
|
||||
}
|
||||
|
||||
function addVacationRequest() {
|
||||
if (selectedDates.value.length === 0) {
|
||||
alert('Please select at least one date.');
|
||||
async function fetchVacationData() {
|
||||
try {
|
||||
const response = await axios.get('vacation/list');
|
||||
if (response.data.status === 'OK') {
|
||||
const vacationList = response.data.data;
|
||||
|
||||
// 사원별로 날짜를 그룹화
|
||||
const employeeVacations = new Map();
|
||||
|
||||
vacationList.forEach(({ employeeId, date, type }) => {
|
||||
if (!employeeVacations.has(employeeId)) {
|
||||
employeeVacations.set(employeeId, []);
|
||||
}
|
||||
employeeVacations.get(employeeId).push({ date, type });
|
||||
});
|
||||
|
||||
// 연속된 날짜 그룹화 및 스타일 적용
|
||||
employeeVacations.forEach((dates, employeeId) => {
|
||||
const sortedDates = dates.map(d => new Date(d.date)).sort((a, b) => a - b);
|
||||
const color = getColorByEmployeeId(employeeId); // 사원별 색상 함수
|
||||
|
||||
let previousDate = null;
|
||||
|
||||
sortedDates.forEach(currentDate => {
|
||||
const dateStr = currentDate.toISOString().split('T')[0];
|
||||
const dayElement = document.querySelector(`[data-date="${dateStr}"]`);
|
||||
|
||||
if (dayElement) {
|
||||
if (
|
||||
previousDate &&
|
||||
currentDate - previousDate === 86400000 // 하루 차이인지 확인
|
||||
) {
|
||||
// 연속된 날짜 스타일 설정
|
||||
dayElement.style.backgroundColor = color;
|
||||
} else {
|
||||
// 새로운 시작점
|
||||
dayElement.style.backgroundColor = color;
|
||||
dayElement.style.borderLeft = `3px solid ${color}`;
|
||||
}
|
||||
previousDate = currentDate;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching vacation data:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 사원 ID별 색상 반환 함수
|
||||
*/
|
||||
function getColorByEmployeeId(employeeId) {
|
||||
const colors = ['#ade3ff', '#ffade3', '#ade3ad', '#ffadad'];
|
||||
return colors[employeeId % colors.length];
|
||||
}
|
||||
|
||||
// 데이터 불러오기 호출
|
||||
fetchVacationData();
|
||||
|
||||
/**
|
||||
* 휴가 요청 추가
|
||||
*/
|
||||
async function addVacationRequests() {
|
||||
if (selectedDates.value.size === 0) {
|
||||
alert('휴가를 선택해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
const newEvents = selectedDates.value.map((date) => ({
|
||||
title: halfDayType.value ? `${halfDayType.value} Half Day Vacation` : 'Vacation',
|
||||
start: date,
|
||||
allDay: true,
|
||||
const vacationRequests = Array.from(selectedDates.value).map(([date, type]) => ({
|
||||
date,
|
||||
type,
|
||||
employeeId: employeeId.value,
|
||||
}));
|
||||
|
||||
calendarEvents.value = [...calendarEvents.value, ...newEvents];
|
||||
alert(`Vacation added for dates: ${selectedDates.value.join(', ')} as ${halfDayType.value || 'Full Day'}`);
|
||||
selectedDates.value = [];
|
||||
halfDayType.value = null;
|
||||
try {
|
||||
const response = await axios.post('vacation', vacationRequests);
|
||||
|
||||
if (response.data && response.data.status === 'OK') {
|
||||
alert('휴가가 저장되었습니다.');
|
||||
|
||||
const newEvents = vacationRequests.map(req => ({
|
||||
title: req.type === 'D' ? '오전반차' : req.type === 'N' ? '오후반차' : '종일 휴가',
|
||||
start: req.date,
|
||||
allDay: true,
|
||||
}));
|
||||
|
||||
calendarEvents.value = [...calendarEvents.value, ...newEvents];
|
||||
selectedDates.value.clear();
|
||||
resetCalendarStyles();
|
||||
} else {
|
||||
alert('휴가 저장 중 오류가 발생했습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
alert('휴가 저장에 실패했습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 초기화
|
||||
*/
|
||||
function resetCalendarStyles() {
|
||||
const calendarElements = document.querySelectorAll('.fc-daygrid-day');
|
||||
calendarElements.forEach(element => {
|
||||
element.style.backgroundColor = '';
|
||||
element.style.backgroundImage = '';
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchVacationData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -106,28 +226,6 @@ function addVacationRequest() {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.save-button-container {
|
||||
position: fixed;
|
||||
top: 900px; /* 탑바 아래로 간격 조정 */
|
||||
right: 400px;
|
||||
z-index: 1050; /* 탑바보다 높은 값 */
|
||||
background-color: white;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.save-button-container {
|
||||
top: 70px; /* 모바일에서 탑바 아래 간격 조정 */
|
||||
right: 5px;
|
||||
left: 5px;
|
||||
width: calc(100% - 10px); /* 모바일 화면에 맞게 크기 조정 */
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.half-day-buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@ -135,47 +233,7 @@ function addVacationRequest() {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.fc-day-sun .fc-col-header-cell-cushion,
|
||||
.fc-day-sun a {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.fc-day-sat .fc-col-header-cell-cushion,
|
||||
.fc-day-sat a {
|
||||
color: blue;
|
||||
}
|
||||
|
||||
.flatpickr-calendar-only input.flatpickr-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.flatpickr-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pt-2.px-3 {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.flatpickr-calendar {
|
||||
position: relative !important;
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.btn-info {
|
||||
background-color: #17a2b8;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background-color: #ffc107;
|
||||
color: white;
|
||||
}
|
||||
|
||||
button.active {
|
||||
opacity: 0.7;
|
||||
.half-day-buttons .btn.active {
|
||||
border: 2px solid black;
|
||||
}
|
||||
</style>
|
||||
|
||||
207
src/views/wordDict/wordDict.vue
Normal file
207
src/views/wordDict/wordDict.vue
Normal 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>
|
||||
Loading…
Reference in New Issue
Block a user