This commit is contained in:
khj0414 2025-03-06 09:50:56 +09:00
commit 708309f6a0
50 changed files with 826 additions and 575 deletions

View File

@ -1,6 +1,6 @@
VITE_DOMAIN = https://192.168.0.251:5173/ VITE_DOMAIN = https://192.168.0.251:5173/
# VITE_LOGIN_URL = http://localhost:10325/ms/ # VITE_LOGIN_URL = http://localhost:10325/ms/
# VITE_FILE_URL = http://localhost:10325/ms/ VITE_SERVER = https://192.168.0.251:10300/
VITE_API_URL = https://192.168.0.251:10325/api/ VITE_API_URL = https://192.168.0.251:10300/api/
VITE_TEST_URL = https://192.168.0.251:10325/test/ VITE_TEST_URL = https://192.168.0.251:10300/test/
VITE_KAKAO_MAP_KEY=6f092e8f45ee81186bb6d8408f66a492 VITE_KAKAO_MAP_KEY=6f092e8f45ee81186bb6d8408f66a492

View File

@ -1,6 +1,6 @@
VITE_DOMAIN = http://localhost:5173/ VITE_DOMAIN = http://localhost:5173/
# VITE_LOGIN_URL = http://localhost:10325/ms/ # VITE_LOGIN_URL = http://localhost:10325/ms/
# VITE_FILE_URL = http://localhost:10325/ms/ VITE_SERVER = http://localhost:10325/
VITE_API_URL = http://localhost:10325/api/ VITE_API_URL = http://localhost:10325/api/
VITE_TEST_URL = http://localhost:10325/test/ VITE_TEST_URL = http://localhost:10325/test/
VITE_KAKAO_MAP_KEY=6f092e8f45ee81186bb6d8408f66a492 VITE_KAKAO_MAP_KEY=6f092e8f45ee81186bb6d8408f66a492

View File

@ -32,7 +32,6 @@
<link rel="stylesheet" href="/css/font.css" /> <link rel="stylesheet" href="/css/font.css" />
<!-- Icons --> <!-- Icons -->
<link rel="stylesheet" href="/vendor/fonts/boxicons.css" /> <link rel="stylesheet" href="/vendor/fonts/boxicons.css" />
<link rel="stylesheet" href="/vendor/fonts/fontawesome.css" /> <link rel="stylesheet" href="/vendor/fonts/fontawesome.css" />

41
jenkinsfile Normal file
View File

@ -0,0 +1,41 @@
pipeline {
agent any
tools {
nodejs 'nodejs22'
}
stages {
stage('Build'){
steps {
bat 'npm install'
bat 'npm ci'
bat 'npm run build'
}
}
stage('Deploy') {
steps {
// 로컬 Nginx 서버에 빌드 파일 배포
bat '''
:: 루트경로 설정
set NGINX_ROOT=C:\\nginx\\html
:: 기존 빌드 삭제
if exist "%NGINX_ROOT%\\dist" rmdir /s /q "%NGINX_ROOT%\\dist"
:: 빌드 파일 복사
xcopy /s /y dist\\* "%NGINX_ROOT%\\dist\\"
'''
}
}
stage('Restart Server!') {
steps {
// 로컬 Nginx 서버 재실행
bat '''
net stop localnginx
ping -n 5 127.0.0.1 > nul
net start localnginx
ping -n 5 127.0.0.1 > nul
'''
}
}
}
}

View File

@ -6,7 +6,7 @@
"scripts": { "scripts": {
"dev": "vite --host 0.0.0.0 --mode dev", "dev": "vite --host 0.0.0.0 --mode dev",
"mine": "vite --host 0.0.0.0 --mode mine", "mine": "vite --host 0.0.0.0 --mode mine",
"build": "vite build --mode prod", "build": "vite build --mode dev",
"preview": "vite preview", "preview": "vite preview",
"lint": "eslint . --fix", "lint": "eslint . --fix",
"format": "prettier --write src/" "format": "prettier --write src/"

View File

@ -3,22 +3,33 @@
/* 휴가 */ /* 휴가 */
.fc-daygrid-day-events {
max-height: 100px !important;
overflow-y: auto !important;
}
/* 이벤트 선 없게 */ /* 이벤트 선 없게 */
.fc-event { .fc-event {
border: none; border: none;
} }
/* 오전전반차 그래프 */ /* 오전 반차 그래프 (왼쪽 절반) */
.fc-daygrid-event.half-day-am { .fc-daygrid-event.half-day-am {
width: calc(50% - 4px) !important; width: 50% !important;
height: 8px !important;
border-radius: 2px !important;
font-size: 0px !important;
} }
/* 오후반차 그래프프 */ /* 오후 반차 그래프 (오른쪽 절반) */
.fc-daygrid-event.half-day-pm { .fc-daygrid-event.half-day-pm {
width: calc(50% - 4px) !important; width: 50% !important;
margin-left: auto !important height: 8px !important;
margin-left: auto !important;
border-radius: 2px !important;
font-size: 0px !important;
}
/* 연차 그래프 (풀풀) */
.fc-daygrid-event.full-day {
width: 100% !important;
height: 8px !important;
margin-left: auto !important;
border-radius: 2px !important;
font-size: 0px !important;
} }
/* 공휴일,일요일 색상 */ /* 공휴일,일요일 색상 */
.fc-day-sun .fc-daygrid-day-number, .fc-day-sun .fc-daygrid-day-number,
@ -39,8 +50,8 @@
.flatpickr-calendar:after { .flatpickr-calendar:after {
display: none !important; display: none !important;
} }
/* 기본 스타일은 그대로 두고, 데이트피커 인풋의 추가 스타일 정의 */ /* 기본 스타일은 그대로 두고, 데이트피커 인풋의 추가 스타일 정의 */
.fc-toolbar-title { .fc-toolbar-title {
cursor: pointer; cursor: pointer;
} }
/* 클릭 가능한 날짜 (오늘 + 미래) */ /* 클릭 가능한 날짜 (오늘 + 미래) */
@ -84,7 +95,6 @@ opacity: 0.6; /* 흐려 보이게 */
/* 본인 모달 */ /* 본인 모달 */
/* 닫기 버튼 */ /* 닫기 버튼 */
.close-btn { .close-btn {
position: absolute; position: absolute;
@ -109,7 +119,6 @@ opacity: 0.6; /* 흐려 보이게 */
/* 선물하기 모달 */ /* 선물하기 모달 */
/* 연차 개수 버튼 */ /* 연차 개수 버튼 */
.count-btn { .count-btn {
font-size: 18px; font-size: 18px;
@ -127,7 +136,6 @@ opacity: 0.6; /* 흐려 보이게 */
background: #cccccc; background: #cccccc;
cursor: not-allowed; cursor: not-allowed;
} }
/* 버튼 컨테이너 (우측 정렬) */ /* 버튼 컨테이너 (우측 정렬) */
.custom-button-container { .custom-button-container {
display: flex; display: flex;
@ -141,18 +149,17 @@ opacity: 0.6; /* 흐려 보이게 */
padding: 10px; /* 크기 조정 */ padding: 10px; /* 크기 조정 */
cursor: pointer; /* 클릭 가능하도록 변경 */ cursor: pointer; /* 클릭 가능하도록 변경 */
} }
/* 아이콘 색상 변경 (기본) */ /* 아이콘 색상 변경 (기본) */
.custom-button i { .custom-button i {
color: #282538; /* 기본 아이콘 색상 */ color: #282538; /* 기본 아이콘 색상 */
font-size: 25px; /* 아이콘 크기 */ font-size: 25px; /* 아이콘 크기 */
} }
/* 버튼 호버 효과 */ /* 버튼 호버 효과 */
.custom-button:hover i { .custom-button:hover i {
color: #ff0800; /* 호버 시 아이콘 색상 변경 */ color: #ff0800; /* 호버 시 아이콘 색상 변경 */
} }
.grayscaleImg { .grayscaleImg {
filter: grayscale(100%); filter: grayscale(100%);
} }

BIN
public/img/mbti/enfj.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

BIN
public/img/mbti/enfp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
public/img/mbti/entj.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

BIN
public/img/mbti/entp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

BIN
public/img/mbti/esfj.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
public/img/mbti/esfp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
public/img/mbti/est.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

BIN
public/img/mbti/estp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
public/img/mbti/infj.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
public/img/mbti/infp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
public/img/mbti/intj.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

BIN
public/img/mbti/intp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
public/img/mbti/isfj.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

BIN
public/img/mbti/isfp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
public/img/mbti/istj.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

BIN
public/img/mbti/istp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -7,12 +7,6 @@
//----------------- //-----------------
let menu, animate; let menu, animate;
<<<<<<< HEAD
var menu, animate;
(function () {
// Initialize menu
//-----------------
=======
let layoutMenuEl = document.querySelectorAll('#layout-menu'); let layoutMenuEl = document.querySelectorAll('#layout-menu');
layoutMenuEl.forEach(function (element) { layoutMenuEl.forEach(function (element) {
menu = new Menu(element, { menu = new Menu(element, {
@ -23,7 +17,6 @@ var menu, animate;
window.Helpers.scrollToActive((animate = false)); window.Helpers.scrollToActive((animate = false));
window.Helpers.mainMenu = menu; window.Helpers.mainMenu = menu;
}); });
>>>>>>> board-comment
// Initialize menu togglers and bind click on each // Initialize menu togglers and bind click on each
let menuToggler = document.querySelectorAll('.layout-menu-toggle'); let menuToggler = document.querySelectorAll('.layout-menu-toggle');
@ -118,5 +111,7 @@ var menu, animate;
// If current layout is vertical and current window screen is > small // If current layout is vertical and current window screen is > small
// Auto update menu collapsed/expanded based on the themeConfig // Auto update menu collapsed/expanded based on the themeConfig
window.Helpers.setCollapsed(true, false); // 250304 pc 화면에서 메뉴바 고정을 위해 false 처리
window.Helpers.setCollapsed(false, false);
//window.Helpers.setCollapsed(true, false);
})(); })();

View File

@ -73,6 +73,14 @@ const common = {
seconds: zeroFormat(date.getSeconds()), seconds: zeroFormat(date.getSeconds()),
}; };
}, },
isNotEmpty(obj) {
if (obj === null || obj === undefined) return false;
if (typeof obj === 'string' && obj.trim() === '') return false;
if ((Array.isArray(obj) || obj === Object(obj)) && Object.keys(obj).length === 0) return false;
return true;
},
}; };
export default { export default {

View File

@ -11,43 +11,42 @@
:isLike="!isLike" :isLike="!isLike"
:isCommentPassword="isCommentPassword" :isCommentPassword="isCommentPassword"
:isCommentProfile="true" :isCommentProfile="true"
@editClick="aaaa" @editClick="handleEditClick"
@deleteClick="$emit('deleteClick', comment)" @deleteClick="$emit('deleteClick', comment)"
@updateReaction="handleUpdateReaction" @updateReaction="handleUpdateReaction"
/> />
<!-- <P>Commentpassssss: {{isCommentPassword}}</P> -->
<!-- :author="true" -->
<!-- 댓글 비밀번호 입력창 (익명일 경우) --> <!-- 댓글 비밀번호 입력창 (익명일 경우) -->
<div v-if="isCommentPassword === comment.commentId && unknown" class="mt-3 w-25 ms-auto"> <div v-if="currentPasswordCommentId === comment.commentId && unknown" class="mt-3 w-25 ms-auto">
<div class="input-group"> <div class="input-group">
<input <input
type="password" type="password"
class="form-control" class="form-control"
v-model="password" :value="password"
placeholder="비밀번호 입력" placeholder="비밀번호 입력"
@input="$emit('update:password', $event.target.value.trim())"
/> />
<button class="btn btn-primary" @click="logPasswordAndEmit">확인</button> <button class="btn btn-primary" @click="logPasswordAndEmit">확인</button>
</div> </div>
<span v-if="passwordCommentAlert" class="invalid-feedback d-block text-start">{{ passwordCommentAlert }}</span> <span v-if="passwordCommentAlert" class="invalid-feedback d-block text-start">{{ passwordCommentAlert }}</span>
</div> </div>
<!-- <p>authorId:{{ comment.authorId }}</p>
<p>코멘트 비교: {{comment.isCommentAuthor}}</p> -->
<div class="mt-6"> <div class="mt-6">
<template v-if="comment.isEditTextarea"> <template v-if="comment.isEditTextarea">
<textarea v-model="localEditedContent" class="form-control"></textarea> <textarea v-model="localEditedContent" class="form-control"></textarea>
<div class="mt-2 d-flex justify-content-end"> <div class="mt-2 d-flex justify-content-end">
<!-- <button class="btn btn-secondary me-2" @click="$emit('cancelEdit', comment)">취소</button> -->
<!-- <button class="btn btn-primary" @click="submitEdit">수정</button> -->
<SaveBtn class="btn btn-primary" @click="submitEdit"></SaveBtn> <SaveBtn class="btn btn-primary" @click="submitEdit"></SaveBtn>
</div> </div>
</template> </template>
<template v-else> <template v-else>
<p class="m-0">{{ comment.content }}</p> <p class="m-0">{{ comment.content }}</p>
</template> </template>
</div> </div>
<!-- <p>현재 isDeleted : {{ isDeleted }}</p> -->
<!-- <template v-if="isDeleted">
<p class="m-0 text-muted">댓글이 삭제되었습니다.</p>
</template> -->
<PlusButton v-if="isPlusButton" @click="toggleComment" class="mt-6"/> <PlusButton v-if="isPlusButton" @click="toggleComment" class="mt-6"/>
<BoardCommentArea v-if="isComment" :unknown="unknown" @submitComment="submitComment"/> <BoardCommentArea v-if="isComment" :unknown="unknown" @submitComment="submitComment"/>
@ -58,10 +57,6 @@
:key="child.commentId" :key="child.commentId"
class="mt-8 pt-6 ps-10 border-top" class="mt-8 pt-6 ps-10 border-top"
> >
<!-- <p>대댓글 데이터(JSON): {{ JSON.stringify(child, null, 2) }}</p> -->
<!-- <p>comment child: {{ comment.children }}</p> -->
<!-- :unknown="child.author === '익명'" -->
<p>child.isCommentPassword: {{ child.isCommentPassword }}</p>
<BoardComment <BoardComment
:comment="child" :comment="child"
:unknown="child.author === '익명'" :unknown="child.author === '익명'"
@ -70,12 +65,17 @@
:isCommentProfile="true" :isCommentProfile="true"
:isCommentAuthor="child.isCommentAuthor" :isCommentAuthor="child.isCommentAuthor"
:isCommentPassword="isCommentPassword" :isCommentPassword="isCommentPassword"
@editClick="$emit('editClick', $event)" :currentPasswordCommentId="currentPasswordCommentId"
:passwordCommentAlert="passwordCommentAlert"
:password="password"
@editClick="handleReplyEditClick"
@deleteClick="$emit('deleteClick', child)" @deleteClick="$emit('deleteClick', child)"
@submitEdit="(comment, editedContent) => $emit('submitEdit', comment, editedContent)" @submitEdit="(comment, editedContent) => $emit('submitEdit', comment, editedContent)"
@cancelEdit="$emit('cancelEdit', child)" @cancelEdit="$emit('cancelEdit', child)"
@submitComment="submitComment" @submitComment="submitComment"
@updateReaction="handleUpdateReaction" @updateReaction="handleUpdateReaction"
@submitPassword="$emit('submitPassword', child, password)"
@update:password="$emit('update:password', $event)"
/> />
</li> </li>
</ul> </ul>
@ -114,20 +114,29 @@ const props = defineProps({
type: Boolean, type: Boolean,
default: false default: false
}, },
isDeleted: {
type: Boolean,
default: false
},
isCommentPassword: { isCommentPassword: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
passwordCommentAlert: { passwordCommentAlert: {
type: String, type: String,
default: false default: ''
} },
currentPasswordCommentId: {
type: Number
},
password:{
type: String
},
}); });
// emits // emits
const emit = defineEmits(['submitComment', 'updateReaction', 'editClick', 'deleteClick', 'submitPassword', 'submitEdit', 'cancelEdit']); const emit = defineEmits(['submitComment', 'updateReaction', 'editClick', 'deleteClick', 'submitPassword', 'submitEdit', 'cancelEdit', 'update:password']);
const password = ref('');
const localEditedContent = ref(props.comment.content); const localEditedContent = ref(props.comment.content);
// //
@ -154,8 +163,7 @@ const handleUpdateReaction = (reactionData) => {
// //
const logPasswordAndEmit = () => { const logPasswordAndEmit = () => {
emit('submitPassword', props.comment, password.value); emit('submitPassword', props.comment, props.password);
password.value = "";
}; };
watch(() => props.comment.isEditTextarea, (newVal) => { watch(() => props.comment.isEditTextarea, (newVal) => {
@ -164,13 +172,26 @@ watch(() => props.comment.isEditTextarea, (newVal) => {
} }
}); });
// watch(() => props.comment.isDeleted, () => {
// console.log("BoardComment - isDeleted :", newVal);
// if (newVal) {
// localEditedContent.value = " ."; // UI
// props.comment.isEditTextarea = false;
// }
// });
// //
const submitEdit = () => { const submitEdit = () => {
emit('submitEdit', props.comment, localEditedContent.value); emit('submitEdit', props.comment, localEditedContent.value);
}; };
const aaaa = () => { const handleEditClick = () => {
emit('editClick', props.comment); emit('editClick', props.comment);
} }
const handleReplyEditClick = (comment) => {
emit('editClick', comment);
}
</script> </script>

View File

@ -11,13 +11,9 @@
</div> --> </div> -->
<!-- 텍스트박스 --> <!-- 텍스트박스 -->
<div class="w-100"> <div class="w-100">
<textarea <textarea class="form-control" placeholder="댓글 달기" rows="3" v-model="comment"></textarea>
class="form-control"
placeholder="댓글 달기"
rows="3"
v-model="comment"
></textarea>
<span v-if="commentAlert" class="invalid-feedback d-block text-start ms-2">{{ commentAlert }}</span> <span v-if="commentAlert" class="invalid-feedback d-block text-start ms-2">{{ commentAlert }}</span>
<span v-else class="invalid-feedback d-block text-start ms-2">{{ textAlert }}</span>
</div> </div>
</div> </div>
@ -26,12 +22,7 @@
<div class="d-flex flex-wrap align-items-center"> <div class="d-flex flex-wrap align-items-center">
<!-- 익명 체크박스 (익명게시판일 경우에만)--> <!-- 익명 체크박스 (익명게시판일 경우에만)-->
<div v-if="unknown" class="form-check form-check-inline mb-0 me-4"> <div v-if="unknown" class="form-check form-check-inline mb-0 me-4">
<input <input class="form-check-input" type="checkbox" id="inlineCheckbox1" v-model="isCheck" />
class="form-check-input"
type="checkbox"
id="inlineCheckbox1"
v-model="isCheck"
/>
<label class="form-check-label" for="inlineCheckbox1">익명</label> <label class="form-check-label" for="inlineCheckbox1">익명</label>
</div> </div>
@ -46,15 +37,13 @@
placeholder="비밀번호 입력" placeholder="비밀번호 입력"
/> />
<span v-if="passwordAlert" class="invalid-feedback d-block text-start ms-2">{{ passwordAlert }}</span> <span v-if="passwordAlert" class="invalid-feedback d-block text-start ms-2">{{ passwordAlert }}</span>
<span v-else class="invalid-feedback d-block text-start ms-2">{{ passwordAlert2 }}</span>
</div> </div>
</div> </div>
<!-- 답변 쓰기 버튼 --> <!-- 답변 쓰기 버튼 -->
<div class="ms-auto mt-3 mt-md-0"> <div class="ms-auto mt-3 mt-md-0">
<button class="btn btn-primary" @click="handleCommentSubmit"> <SaveBtn class="btn btn-primary" @click="handleCommentSubmit"></SaveBtn>
<!-- <i class="icon-base bx bx-check"></i> -->
확인
</button>
</div> </div>
</div> </div>
</div> </div>
@ -62,7 +51,8 @@
</template> </template>
<script setup> <script setup>
import { ref, defineEmits, defineProps, computed, watch } from 'vue'; import { ref, defineEmits, defineProps, watch, inject } from 'vue';
import SaveBtn from '../button/SaveBtn.vue';
const props = defineProps({ const props = defineProps({
unknown: { unknown: {
@ -71,39 +61,68 @@ const props = defineProps({
}, },
parentId: { parentId: {
type: Number, type: Number,
default: 0 default: 0,
}, },
passwordAlert: { passwordAlert: {
type: String, type: String,
default: false default: '',
}, },
commentAlert: { commentAlert: {
type: String, type: String,
default: false default: '',
} },
}); });
const $common = inject('common');
const comment = ref(''); const comment = ref('');
const password = ref(''); const password = ref('');
const isCheck = ref(props.unknown); const isCheck = ref(false);
const textAlert = ref('');
const passwordAlert2 = ref('');
const emit = defineEmits(['submitComment']); const emit = defineEmits(['submitComment']);
const LOCBRDTYP = isCheck.value ? '300102' : null;
function handleCommentSubmit() { const handleCommentSubmit = () => {
if (!$common.isNotEmpty(comment.value)) {
textAlert.value = '댓글을 입력하세요';
return false;
} else {
textAlert.value = '';
}
if (isCheck.value && !$common.isNotEmpty(password.value)) {
passwordAlert2.value = '비밀번호를 입력하세요';
return false;
} else {
passwordAlert2.value = '';
}
//
emit('submitComment', { emit('submitComment', {
comment: comment.value, comment: comment.value,
password: isCheck.value ? password.value : '', password: isCheck.value ? password.value : '',
isCheck: isCheck.value, isCheck: isCheck.value,
LOCBRDTYP, // '300102' LOCBRDTYP: isCheck.value ? '300102' : null, // '300102'
}); });
}
watch(() => props.passwordAlert, () => { //
if (!props.passwordAlert) { resetCommentForm();
};
//
const resetCommentForm = () => {
comment.value = ''; comment.value = '';
password.value = ''; password.value = '';
isCheck.value = false;
};
watch(
() => props.passwordAlert,
() => {
if (!props.passwordAlert) {
resetCommentForm();
} }
}); }
);
</script> </script>

View File

@ -10,16 +10,19 @@
:comment="comment" :comment="comment"
:isCommentAuthor="comment.isCommentAuthor" :isCommentAuthor="comment.isCommentAuthor"
:isEditTextarea="comment.isEditTextarea" :isEditTextarea="comment.isEditTextarea"
:isDeleted="isDeleted"
:isCommentPassword="isCommentPassword" :isCommentPassword="isCommentPassword"
:passwordCommentAlert="passwordCommentAlert" :passwordCommentAlert="passwordCommentAlert || ''"
:currentPasswordCommentId="currentPasswordCommentId"
:password="password"
@editClick="handleEditClick" @editClick="handleEditClick"
@deleteClick="handleDeleteClick" @deleteClick="handleDeleteClick"
@submitPassword="submitPassword" @submitPassword="submitPassword"
@submitComment="submitComment" @submitComment="submitComment"
@commentDeleted="handleCommentDeleted"
@submitEdit="handleSubmitEdit" @submitEdit="handleSubmitEdit"
@cancelEdit="handleCancelEdit" @cancelEdit="handleCancelEdit"
@updateReaction="(reactionData) => handleUpdateReaction(reactionData, comment.commentId, comment.boardId)" @updateReaction="(reactionData) => handleUpdateReaction(reactionData, comment.commentId, comment.boardId)"
@update:password="updatePassword"
/> />
</li> </li>
</ul> </ul>
@ -51,13 +54,23 @@ const props = defineProps({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
isDeleted: {
type: Boolean,
default: false,
},
passwordCommentAlert: { passwordCommentAlert: {
type: String, type: String,
default: false default: ''
} },
currentPasswordCommentId: {
type: Number
},
password:{
type: String
},
}); });
const emit = defineEmits(['submitComment', 'updateReaction', 'editClick', 'deleteClick', 'submitPassword', 'clearPassword']); const emit = defineEmits(['submitComment', 'updateReaction', 'editClick', 'deleteClick', 'submitPassword', 'clearPassword','submitEdit', 'update:password']);
const submitComment = (replyData) => { const submitComment = (replyData) => {
emit('submitComment', replyData); emit('submitComment', replyData);
@ -78,7 +91,11 @@ const submitPassword = (comment, password) => {
}; };
const handleEditClick = (comment) => { const handleEditClick = (comment) => {
emit('editClick', comment); if (comment.parentId) {
emit('editClick', comment); //
} else {
emit('editClick', comment); //
}
}; };
const handleSubmitEdit = (comment, editedContent) => { const handleSubmitEdit = (comment, editedContent) => {
@ -100,4 +117,8 @@ const handleCancelEdit = (comment) => {
emit('cancelEdit', comment); // emit('cancelEdit', comment); //
} }
}; };
const updatePassword = (newPassword) => {
emit('update:password', newPassword);
};
</script> </script>

View File

@ -2,8 +2,9 @@
<div class="d-flex align-items-center flex-wrap"> <div class="d-flex align-items-center flex-wrap">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<div v-if="!unknown" class="avatar me-2"> <div v-if="!unknown" class="avatar me-2">
<img src="/img/avatars/2.png" alt="Avatar" class="rounded-circle" /> <img :src="getProfileImage(profilePath)" alt="Avatar" class="rounded-circle" />
</div> </div>
<div class="me-2"> <div class="me-2">
<h6 class="mb-0">{{ profileName }}</h6> <h6 class="mb-0">{{ profileName }}</h6>
<div class="profile-detail"> <div class="profile-detail">
@ -22,8 +23,7 @@
<!-- 버튼 영역 --> <!-- 버튼 영역 -->
<div class="ms-auto text-end"> <div class="ms-auto text-end">
<!-- 수정, 삭제 버튼 --> <!-- 수정, 삭제 버튼 -->
<!-- <template v-if="isAuthor || showDetail"> --> <template v-if="!isDeletedComment && (unknown || isCommentAuthor || isAuthor)">
<template v-if="unknown || isCommentAuthor || isAuthor">
<EditButton @click.stop="editClick" /> <EditButton @click.stop="editClick" />
<DeleteButton @click.stop="deleteClick" /> <DeleteButton @click.stop="deleteClick" />
</template> </template>
@ -34,18 +34,23 @@
:boardId="boardId" :boardId="boardId"
:comment="comment" :comment="comment"
@updateReaction="handleUpdateReaction" @updateReaction="handleUpdateReaction"
> />
</BoardRecommendBtn>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, defineProps, defineEmits } from 'vue'; import { computed, defineProps, defineEmits } from 'vue';
import DeleteButton from '../button/DeleteBtn.vue'; import DeleteButton from '../button/DeleteBtn.vue';
import EditButton from '../button/EditBtn.vue'; import EditButton from '../button/EditBtn.vue';
import BoardRecommendBtn from '../button/BoardRecommendBtn.vue'; import BoardRecommendBtn from '../button/BoardRecommendBtn.vue';
//
const defaultProfile = "/img/icons/icon.png";
// (Vue )
const baseUrl = "http://localhost:10325/"; // API URL
// Props // Props
const props = defineProps({ const props = defineProps({
comment: { comment: {
@ -64,6 +69,10 @@ const props = defineProps({
type: String, type: String,
default: '', default: '',
}, },
profilePath: {
type: String,
default: '',
},
unknown: { unknown: {
type: Boolean, type: Boolean,
default: true, default: true,
@ -72,16 +81,15 @@ const props = defineProps({
type: Boolean, type: Boolean,
default: true, default: true,
}, },
// :
isAuthor: { isAuthor: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
isCommentAuthor: Boolean, // isCommentAuthor: Boolean,
isCommentProfile: Boolean, // isCommentProfile: Boolean,
date: { date: {
type: String, type: String,
required: true, required: '',
}, },
views: { views: {
type: Number, type: Number,
@ -99,6 +107,11 @@ const props = defineProps({
const emit = defineEmits(['updateReaction', 'editClick', 'deleteClick']); const emit = defineEmits(['updateReaction', 'editClick', 'deleteClick']);
const isDeletedComment = computed(() => {
return props.comment?.content === '삭제된 댓글입니다' &&
props.comment?.updateAtRaw !== props.comment?.createdAtRaw;
});
// //
const editClick = () => { const editClick = () => {
emit('editClick', { ...props.comment, unknown: props.unknown }); emit('editClick', { ...props.comment, unknown: props.unknown });
@ -109,6 +122,7 @@ const deleteClick = () => {
emit('deleteClick', { ...props.comment, unknown: props.unknown }); emit('deleteClick', { ...props.comment, unknown: props.unknown });
}; };
// /
const handleUpdateReaction = (reactionData) => { const handleUpdateReaction = (reactionData) => {
emit("updateReaction", { emit("updateReaction", {
boardId: props.boardId, boardId: props.boardId,
@ -117,30 +131,10 @@ const handleUpdateReaction = (reactionData) => {
}); });
}; };
//
const getProfileImage = (profilePath) => {
return profilePath && profilePath.trim()
? `${baseUrl}upload/img/profile/${profilePath}`
: defaultProfile;
};
</script> </script>
<style scoped>
.profile-detail span ~ span {
margin-left: 5px;
}
.ms-auto button + button {
margin-left: 5px;
}
.btn.author {
height: 30px;
}
@media screen and (max-width: 450px) {
.btn-area {
margin-top: 10px;
width: 100%;
}
.btn.author {
height: 30px;
}
}
</style>

View File

@ -13,7 +13,7 @@ import { ref, computed } from 'vue';
const props = defineProps({ const props = defineProps({
comment: { comment: {
type: Object, type: Object,
required: true, default: () => ({}),
}, },
likeClicked : { likeClicked : {
type : Boolean, type : Boolean,
@ -36,7 +36,7 @@ const props = defineProps({
required: true, required: true,
}, },
commentId: { commentId: {
type: Number, type: [Number, null],
default: null, default: null,
}, },
likeCount: { likeCount: {

View File

@ -1,13 +1,30 @@
<template> <template>
<button class="btn btn-label-primary btn-icon"> <button class="btn btn-label-primary btn-icon" @click="toggleText">
<i class="bx bx-edit-alt"></i> <i :class="buttonClass"></i>
</button> </button>
</template> </template>
<script> <script setup>
export default { import { ref, defineProps } from 'vue';
name: 'EditButton',
methods: { const props = defineProps({
isToggleEnabled: {
type: Boolean,
default: false,
}, },
}; });
const buttonClass = ref("bx bx-edit-alt");
const toggleText = () => {
if (props.isToggleEnabled) {
buttonClass.value = buttonClass.value === "bx bx-edit-alt" ? "bx bx-x" : "bx bx-edit-alt";
}
};
const resetButton = () => {
buttonClass.value = "bx bx-edit-alt";
};
defineExpose({ resetButton });
</script> </script>

View File

@ -20,7 +20,8 @@
<!-- 저장 버튼 --> <!-- 저장 버튼 -->
<div class="save-button-container"> <div class="save-button-container">
<button class="btn btn-success" @click="addVacationRequests" :disabled="isDisabled"> <button class="btn btn-success" @click="addVacationRequests"
:class="{ active: !isDisabled, disabled: isDisabled }">
</button> </button>
</div> </div>
@ -28,26 +29,28 @@
</template> </template>
<script setup> <script setup>
import { defineEmits, ref, defineProps } from "vue"; import { defineEmits, ref, defineProps, watch } from "vue";
const props = defineProps({ const props = defineProps({
isDisabled: Boolean isDisabled: Boolean,
selectedDate: String // props
}); });
const emit = defineEmits(["toggleHalfDay", "addVacationRequests", "resetHalfDay"]); const emit = defineEmits(["toggleHalfDay", "addVacationRequests", "resetHalfDay"]);
const halfDayType = ref(null); const halfDayType = ref(null);
const toggleHalfDay = (type) => { const toggleHalfDay = (type) => {
halfDayType.value = type; halfDayType.value = halfDayType.value === type ? null : type;
emit("toggleHalfDay", halfDayType.value); emit("toggleHalfDay", halfDayType.value);
// 1
setTimeout(() => {
halfDayType.value = null;
}, 1000);
}; };
// `selectedDate`
watch(() => props.selectedDate, (newDate) => {
if (newDate) {
resetHalfDay();
}
});
// //
const resetHalfDay = () => { const resetHalfDay = () => {
halfDayType.value = null; halfDayType.value = null;
@ -89,24 +92,24 @@ defineExpose({ resetHalfDay });
/* 선택된 (눌린) 버튼 */ /* 선택된 (눌린) 버튼 */
.btn.active { .btn.active {
border: 3px solid #fff; /* 흰색 테두리 강조 */ border: 3px solid #ff0000; /* 붉은색 테두리 적용 */
box-shadow: 0px 4px 15px rgba(0, 0, 0, 0.3); box-shadow: 0px 4px 15px rgba(0, 0, 0, 0.3);
transform: scale(1.1); transform: scale(1.1);
} }
/* AM 버튼 (선택된 상태) */ /* AM 버튼 (선택된 상태) */
.btn-warning.active { .btn-warning.active {
background-color: #ffca2c !important; /* 진한 노란색 */ background-color: #ffca2c !important;
color: black; color: black;
} }
/* PM 버튼 (선택된 상태) */ /* PM 버튼 (선택된 상태) */
.btn-info.active { .btn-info.active {
background-color: #0b5ed7 !important; /* 진한 파란색 */ background-color: #0b5ed7 !important;
color: white; color: white;
} }
/* ✔ 버튼 */ /* ✔ 버튼 기본 (비활성화일 때 기본 녹색) */
.btn-success { .btn-success {
font-size: 24px; font-size: 24px;
width: 50px; width: 50px;
@ -116,18 +119,26 @@ defineExpose({ resetHalfDay });
align-items: center; align-items: center;
justify-content: center; justify-content: center;
transition: all 0.2s ease-in-out; transition: all 0.2s ease-in-out;
background-color: #871919 !important;
color: white;
} }
/* ✔ 버튼 마우스 오버 */ /* ✔ 버튼 활성화 */
.btn-success:hover { .btn-success.active {
background-color: #198754; background-color: #ff0000 !important;
box-shadow: 0px 4px 10px rgba(25, 135, 84, 0.4); color: white !important;
border: 3px solid #eb9f9f !important;
box-shadow: 0px 4px 15px rgba(0, 0, 0, 0.3);
transform: scale(1.1); transform: scale(1.1);
} }
/* ✔ 버튼 클릭 */ /* ✔ 버튼 비활성화 */
.btn-success:active { .btn-success.disabled {
transform: scale(0.95); background-color: #bbb8b8 !important;
box-shadow: 0px 2px 5px rgba(25, 135, 84, 0.2); color: white !important;
cursor: not-allowed !important;
box-shadow: none;
transform: none;
opacity: 0.5;
} }
</style> </style>

View File

@ -1,9 +1,30 @@
<template> <template>
<button class="btn btn-label-primary btn-icon float-end"> <button class="btn btn-label-primary btn-icon float-end" @click="toggleText">
<i class="bx bx-edit"></i> <i :class="buttonClass"></i>
</button> </button>
</template> </template>
<script setup> <script setup>
import { ref, defineProps, defineExpose } from 'vue';
const props = defineProps({
isToggleEnabled: {
type: Boolean,
default: false,
},
});
const buttonClass = ref("bx bx-edit");
const toggleText = () => {
if (props.isToggleEnabled) {
buttonClass.value = buttonClass.value === "bx bx-edit" ? "bx bx-x" : "bx bx-edit";
}
};
const resetButton = () => {
buttonClass.value = "bx bx-edit";
};
defineExpose({ resetButton });
</script> </script>

View File

@ -1,11 +1,12 @@
<template> <template>
<div class="mb-4 row"> <div class="mb-4 row">
<label :for="name" class="col-md-2 col-form-label">{{ title }}</label> <label :for="inputId" class="col-md-2 col-form-label">{{ title }}</label>
<div class="col-md-10"> <div class="col-md-10">
<input <input
class="form-control" class="form-control"
type="file" type="file"
:id="name" :id="inputId"
ref="fileInput"
@change="changeHandler" @change="changeHandler"
multiple multiple
/> />
@ -21,7 +22,7 @@ import { ref ,computed} from 'vue';
import { fileMsg } from '@/common/msgEnum'; import { fileMsg } from '@/common/msgEnum';
// Props // Props
const prop = defineProps({ const props = defineProps({
title: { title: {
type: String, type: String,
default: '라벨', default: '라벨',
@ -29,7 +30,7 @@ const prop = defineProps({
}, },
name: { name: {
type: String, type: String,
default: 'nameplz', default: 'fileInput',
required: true, required: true,
}, },
isAlert: { isAlert: {
@ -38,12 +39,13 @@ const prop = defineProps({
required: false, required: false,
}, },
}); });
const inputId = computed(() => props.name || 'defaultFileInput');
const emits = defineEmits(['update:data', 'update:isValid']); const emits = defineEmits(['update:data', 'update:isValid']);
const MAX_TOTAL_SIZE = 5 * 1024 * 1024; // 5MB const MAX_TOTAL_SIZE = 5 * 1024 * 1024; // 5MB
const MAX_FILE_COUNT = 5; // const MAX_FILE_COUNT = 5; //
const ALLOWED_FILE_TYPES = ['image/jpeg', 'image/png', 'application/pdf']; // const ALLOWED_FILE_TYPES = []; //
const showError = ref(false); const showError = ref(false);
const fileMsgKey = ref(''); // const fileMsgKey = ref(''); //
@ -51,9 +53,12 @@ const fileMsgKey = ref(''); // 에러 메시지 키
const changeHandler = (event) => { const changeHandler = (event) => {
const files = Array.from(event.target.files); const files = Array.from(event.target.files);
const totalSize = files.reduce((sum, file) => sum + file.size, 0); const totalSize = files.reduce((sum, file) => sum + file.size, 0);
const invalidFiles = files.filter(file => !ALLOWED_FILE_TYPES.includes(file.type));
// // ALLOWED_FILE_TYPES
const invalidFiles = ALLOWED_FILE_TYPES.length > 0
? files.filter(file => !ALLOWED_FILE_TYPES.includes(file.type))
: [];
if (totalSize > MAX_TOTAL_SIZE) { if (totalSize > MAX_TOTAL_SIZE) {
showError.value = true; showError.value = true;
fileMsgKey.value = 'FileMaxSizeMsg'; fileMsgKey.value = 'FileMaxSizeMsg';

View File

@ -15,6 +15,7 @@
:disabled="disabled" :disabled="disabled"
:min="min" :min="min"
@focusout="$emit('focusout', modelValue)" @focusout="$emit('focusout', modelValue)"
@input="handleInput"
/> />
<div class="invalid-feedback" :class="isAlert ? 'd-block' : ''"> <div class="invalid-feedback" :class="isAlert ? 'd-block' : ''">
{{ title }} 확인해주세요. {{ title }} 확인해주세요.
@ -92,11 +93,6 @@ const inputValue = ref(props.modelValue);
// //
watch(inputValue, (newValue) => { watch(inputValue, (newValue) => {
emits('update:modelValue', newValue); emits('update:modelValue', newValue);
// `alert` false
if (newValue.trim() !== '') {
emits('update:alert', false);
}
}); });
// //
@ -106,6 +102,13 @@ watch(() => props.modelValue, (newValue) => {
} }
}); });
const handleInput = (event) => {
const newValue = event.target.value.slice(0, props.maxlength);
if (newValue.trim() !== '') {
emits('update:alert', false);
}
};
</script> </script>

View File

@ -4,19 +4,30 @@
{{ title }} {{ title }}
<span :class="isEssential ? 'link-danger' : 'none'">*</span> <span :class="isEssential ? 'link-danger' : 'none'">*</span>
</label> </label>
<div :class="isRow ? 'col-md-10' : 'col-md-12'"> <div :class="isRow ? 'col-md-10' : 'col-md-12'" class="d-flex gap-2 align-items-center">
<select class="form-select" :id="name" v-model="selectData" :disabled="disabled"> <select class="form-select" :id="name" v-model="selectData" :disabled="disabled" :style="isColor ? { color: selected } : {}">
<option v-for="(item, i) in data" :key="i" :value="isCommon ? item.value : i"> <option v-for="(item, i) in data" :key="i" :value="isCommon ? item.value : i" :style="isColor ? { color: item.label } : {}">
{{ isCommon ? item.label : item }} {{ isCommon ? item.label : item }}
</option> </option>
</select> </select>
<div v-if="isColor && selected"
class="w-px-40 h-px-30"
:style="{backgroundColor: selected}">
</div>
<img v-if="isMbti && selected"
role="img"
class="w-px-30 h-px-40"
:src="`/img/mbti/${selected.toLowerCase()}.png`"
alt="MBTI image"/>
</div> </div>
<div v-if="isAlert" class="invalid-feedback">{{ title }} 확인해주세요.</div> <div v-if="isAlert" class="invalid-feedback">{{ title }} 확인해주세요.</div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
const props = defineProps({ const props = defineProps({
title: { title: {
@ -67,7 +78,17 @@ const props = defineProps({
disabled: { disabled: {
type: Boolean, type: Boolean,
default: false default: false
} },
isColor: {
type: Boolean,
default: false,
required: false,
},
isMbti: {
type: Boolean,
default: false,
required: false,
},
}); });
const emit = defineEmits(['update:data']); const emit = defineEmits(['update:data']);
@ -93,4 +114,13 @@ watch(() => props.data, (newData) => {
watch(selectData, (newValue) => { watch(selectData, (newValue) => {
emit('update:data', newValue); emit('update:data', newValue);
}); });
const selected = computed(() => {
const selectedItem = props.data.find(item =>
props.isCommon ? item.value === selectData.value : props.data.indexOf(item) === selectData.value
);
return selectedItem ? selectedItem.label : null;
});
</script> </script>

View File

@ -38,7 +38,7 @@
<div class="ms-12 position-relative"> <div class="ms-12 position-relative">
{{ address }} {{ addressdtail }} {{ address }} {{ addressdtail }}
<!-- 팝오버 --> <!-- 팝오버 -->
<div v-if="isPopoverVisible" class="position-absolute w-100 map text-end"> <div v-if="isPopoverVisible" class="position-absolute map ">
<button type="button" class="btn-close popover-close" @click.stop="isPopoverVisible = !isPopoverVisible"></button> <button type="button" class="btn-close popover-close" @click.stop="isPopoverVisible = !isPopoverVisible"></button>
<div class="card"> <div class="card">
<div class="card-body p-1"> <div class="card-body p-1">
@ -46,8 +46,7 @@
v-if="coordinates" v-if="coordinates"
:lat="coordinates.lat" :lat="coordinates.lat"
:lng="coordinates.lng" :lng="coordinates.lng"
:draggable="false" class="w-px-200 h-px-200"
class="w-100 h-px-200"
> >
<KakaoMapMarker <KakaoMapMarker
:lat="coordinates.lat" :lat="coordinates.lat"
@ -310,6 +309,18 @@ const closeEditModal = () => {
isEditModalOpen.value = false; isEditModalOpen.value = false;
}; };
//
const hasChanges = computed(() => {
return selectedProject.value.PROJCTNAM !== props.title ||
selectedProject.value.PROJCTSTR !== props.strdate ||
selectedProject.value.PROJCTEND !== props.enddate ||
selectedProject.value.PROJCTZIP !== props.addressZip ||
selectedProject.value.PROJCTARR !== props.address ||
selectedProject.value.PROJCTDTL !== props.addressdtail ||
selectedProject.value.PROJCTDES !== props.description ||
selectedProject.value.PROJCTCOL !== props.projctCol;
});
// //
const handleUpdate = () => { const handleUpdate = () => {
nameAlert.value = selectedProject.value.PROJCTNAM.trim() === ''; nameAlert.value = selectedProject.value.PROJCTNAM.trim() === '';
@ -318,6 +329,11 @@ const handleUpdate = () => {
return; return;
} }
if (!hasChanges.value) {
toastStore.onToast('변경된 내용이 없습니다.', 'e');
return;
}
$api.patch('project/update', { $api.patch('project/update', {
projctSeq: selectedProject.value.PROJCTSEQ, projctSeq: selectedProject.value.PROJCTSEQ,
projctNam: selectedProject.value.PROJCTNAM, projctNam: selectedProject.value.PROJCTNAM,
@ -336,7 +352,6 @@ const handleUpdate = () => {
closeEditModal(); closeEditModal();
// //
emit('update'); emit('update');
window.location.reload()
} }
}); });
}; };

View File

@ -49,6 +49,7 @@
:is-essential="true" :is-essential="true"
:is-label="true" :is-label="true"
:is-common="true" :is-common="true"
:is-color="true"
:data="colorList" :data="colorList"
@update:data="color = $event" @update:data="color = $event"
/> />
@ -100,7 +101,7 @@
import { computed, ref, watch, onMounted, inject } from 'vue'; import { computed, ref, watch, onMounted, inject } from 'vue';
import SearchBar from '@c/search/SearchBar.vue'; import SearchBar from '@c/search/SearchBar.vue';
import ProjectCard from '@c/list/ProjectCard.vue'; import ProjectCard from '@c/list/ProjectCard.vue';
import CategoryBtn from '@c/category/CategoryBtn.vue'; import CategoryBtn from '@/components/category/CategoryBtn.vue';
import WriteBtn from '@c/button/WriteBtn.vue'; import WriteBtn from '@c/button/WriteBtn.vue';
import CenterModal from '@c/modal/CenterModal.vue'; import CenterModal from '@c/modal/CenterModal.vue';
import FormSelect from '@c/input/FormSelect.vue'; import FormSelect from '@c/input/FormSelect.vue';

View File

@ -68,20 +68,21 @@
:is-alert="passwordcheckAlert" :is-alert="passwordcheckAlert"
@update:data="passwordcheck = $event" @update:data="passwordcheck = $event"
@update:alert="passwordcheckAlert = $event" @update:alert="passwordcheckAlert = $event"
@blur="checkPw" @input="checkPw"
:value="passwordcheck" :value="passwordcheck"
/> />
<span v-if="passwordcheckError" class="invalid-feedback d-block">{{ passwordcheckError }}</span> <span v-if="passwordcheckError" class="invalid-feedback d-block">{{ passwordcheckError }}</span>
<div class="d-grid gap-2 mt-5 mb-5"> <div class="d-flex gap-2 mt-7 mb-3">
<SaveBtn @click="handleNewPassword" /> <BackBtn class=" w-50" @click="handleback"/>
<p v-if="pwErrMsg" class="invalid-feedback d-block mb-0">{{ pwErrMsg }}</p> <SaveBtn class="w-50" @click="handleNewPassword" />
</div> </div>
<p v-if="pwErrMsg" class="invalid-feedback d-block mb-0">{{ pwErrMsg }}</p>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref } from 'vue'; import { ref, watch } from 'vue';
import $api from '@api'; import $api from '@api';
import commonApi from '@/common/commonApi'; import commonApi from '@/common/commonApi';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
@ -112,7 +113,7 @@
const passwordAlert = ref(false); const passwordAlert = ref(false);
const passwordcheckAlert = ref(false); const passwordcheckAlert = ref(false);
const passwordcheckErrorAlert = ref(false); const passwordMismatch = ref(false);
const { pwhintList } = commonApi({ const { pwhintList } = commonApi({
loadPwhint: true, loadPwhint: true,
@ -127,6 +128,13 @@
router.push('/login'); router.push('/login');
} }
//
watch([password, passwordcheck], () => {
if (passwordcheck.value !== '') {
checkPw();
}
});
// , , , member input // , , , member input
const handleSubmit = async () => { const handleSubmit = async () => {
userCheckMsg.value = ''; userCheckMsg.value = '';
@ -153,13 +161,13 @@
} }
}; };
const checkPw = async () => { const checkPw = () => {
if (password.value !== passwordcheck.value) { if (password.value !== passwordcheck.value) {
passwordcheckError.value = '비밀번호가 일치하지 않습니다.'; passwordcheckError.value = '비밀번호가 일치하지 않습니다.';
passwordcheckErrorAlert.value = true; passwordMismatch.value = true;
} else { } else {
passwordcheckError.value = ''; passwordcheckError.value = '';
passwordcheckErrorAlert.value = false; passwordMismatch.value = false;
} }
}; };
@ -168,8 +176,10 @@
pwErrMsg.value = ''; pwErrMsg.value = '';
passwordAlert.value = password.value.trim() === ''; passwordAlert.value = password.value.trim() === '';
passwordcheckAlert.value = passwordcheck.value.trim() === ''; passwordcheckAlert.value = passwordcheck.value.trim() === '';
checkPw(); checkPw();
if (passwordAlert.value || passwordcheckAlert.value || passwordcheckErrorAlert.value) {
if (passwordAlert.value || passwordcheckAlert.value || passwordMismatch.value) {
return; return;
} }
@ -192,6 +202,5 @@
toastStore.onToast('비밀번호가 재설정 되었습니다.', 's'); toastStore.onToast('비밀번호가 재설정 되었습니다.', 's');
router.push('/login'); router.push('/login');
} }
}; };
</script> </script>

View File

@ -92,6 +92,7 @@
:is-row="false" :is-row="false"
:is-label="true" :is-label="true"
:is-common="true" :is-common="true"
:is-color="true"
:data="colorList" :data="colorList"
@update:data="color = $event" @update:data="color = $event"
class="w-50" class="w-50"
@ -118,6 +119,7 @@
:is-row="false" :is-row="false"
:is-label="true" :is-label="true"
:is-common="true" :is-common="true"
:is-mbti="true"
:data="mbtiList" :data="mbtiList"
@update:data="mbti = $event" @update:data="mbti = $event"
class="w-50" class="w-50"

View File

@ -1,7 +1,7 @@
<template> <template>
<ul class="list-unstyled users-list d-flex align-items-center gap-1"> <ul class="list-unstyled users-list d-flex align-items-center gap-1">
<li <li
v-for="(user, index) in userList" v-for="(user, index) in sortedUserList"
:key="index" :key="index"
class="avatar pull-up" class="avatar pull-up"
:class="{ 'opacity-100': isUserDisabled(user) }" :class="{ 'opacity-100': isUserDisabled(user) }"
@ -25,7 +25,7 @@
</template> </template>
<script setup> <script setup>
import { onMounted, ref, nextTick } from 'vue'; import { onMounted, ref, nextTick, computed } from 'vue';
import { useUserStore } from '@s/userList'; import { useUserStore } from '@s/userList';
import $api from '@api'; import $api from '@api';
@ -41,6 +41,19 @@ const props = defineProps({
} }
}); });
// computed
const sortedUserList = computed(() => {
return [...userList.value].sort((a, b) => {
const aDisabled = isUserDisabled(a);
const bDisabled = isUserDisabled(b);
//
if (!aDisabled && bDisabled) return -1;
if (aDisabled && !bDisabled) return 1;
return 0;
});
});
// //
const fetchProjectParticipation = async () => { const fetchProjectParticipation = async () => {
if (props.projctSeq) { if (props.projctSeq) {
@ -79,27 +92,33 @@ const isUserDisabled = (user) => {
// / DB // / DB
const toggleDisable = async (index) => { const toggleDisable = async (index) => {
const user = userList.value[index]; const user = sortedUserList.value[index];
if (user) { if (user) {
const newParticipationStatus = props.projctSeq const newParticipationStatus = props.projctSeq
? user.PROJCTYON === '1' ? user.PROJCTYON === '1'
: !user.disabled; : !user.disabled;
if (props.projctSeq) { if (props.projctSeq) {
const response = await $api.patch('project/updateYon', { const response = await $api.patch('project/updateYon', {
memberSeq: user.MEMBERSEQ, memberSeq: user.MEMBERSEQ,
projctSeq: props.projctSeq, projctSeq: props.projctSeq,
projctYon: newParticipationStatus ? '0' : '1' projctYon: newParticipationStatus ? '0' : '1'
}); });
if (response.status === 200) { if (response.status === 200) {
user.PROJCTYON = newParticipationStatus ? '0' : '1'; // userList
const originalIndex = userList.value.findIndex(u => u.MEMBERSEQ === user.MEMBERSEQ);
if (originalIndex !== -1) {
userList.value[originalIndex].PROJCTYON = newParticipationStatus ? '0' : '1';
}
} }
} else { } else {
user.disabled = newParticipationStatus; // userList
const originalIndex = userList.value.findIndex(u => u.MEMBERSEQ === user.MEMBERSEQ);
if (originalIndex !== -1) {
userList.value[originalIndex].disabled = newParticipationStatus;
emitUserListUpdate(); emitUserListUpdate();
} }
} }
}
}; };
// emit // emit

View File

@ -18,7 +18,6 @@
type="checkbox" type="checkbox"
class="form-check-input admin-chk" class="form-check-input admin-chk"
:name="item.WRDDICSEQ" :name="item.WRDDICSEQ"
id=""
@change="toggleCheck($event)" @change="toggleCheck($event)"
> >
<div class="d-flex align-ite-center"> <div class="d-flex align-ite-center">
@ -66,7 +65,7 @@
</div> </div>
<div class="edit-btn" v-if="userStore.user.role !== 'ROLE_ADMIN'"> <div class="edit-btn" v-if="userStore.user.role !== 'ROLE_ADMIN'">
<EditBtn @click="writeStore.toggleItem(item.WRDDICSEQ)" /> <EditBtn ref="writeButton" @click="writeStore.toggleItem(item.WRDDICSEQ)" :isToggleEnabled="true"/>
</div> </div>
</li> </li>
</template> </template>
@ -74,7 +73,7 @@
<script setup> <script setup>
import axios from "@api"; import axios from "@api";
import { useToastStore } from '@s/toastStore'; import { useToastStore } from '@s/toastStore';
import { ref, toRefs, getCurrentInstance, } from 'vue'; import { getCurrentInstance, ref } from 'vue';
import EditBtn from '@/components/button/EditBtn.vue'; import EditBtn from '@/components/button/EditBtn.vue';
import $api from '@api'; import $api from '@api';
import DictWrite from './DictWrite.vue'; import DictWrite from './DictWrite.vue';
@ -82,6 +81,7 @@ import { useUserInfoStore } from '@s/useUserInfoStore';
import { useWriteVisibleStore } from '@s/writeVisible'; import { useWriteVisibleStore } from '@s/writeVisible';
const writeStore = useWriteVisibleStore(); const writeStore = useWriteVisibleStore();
const writeButton = ref(null);
// //
const userStore = useUserInfoStore(); const userStore = useUserInfoStore();
@ -103,68 +103,12 @@ const props = defineProps({
} }
}); });
//
// const localCateList = ref([...props.cateList]);
//
// const selectedCategory = ref('');
// cateList emit // cateList emit
const emit = defineEmits(['update:cateList','refreshWordList', 'updateChecked']); const emit = defineEmits(['update:cateList','refreshWordList', 'updateChecked']);
//
// const isWriteVisible = ref(false);
// toggle
// const toggleWriteVisible = () => {
// isWriteVisible.value = !isWriteVisible.value;
// };
//
// const addCategory = (data) => {
// try {
// const lastCategory = localCateList.value.length > 0
// ? localCateList.value[localCateList.value.length - 1]
// : null;
// const newValue = lastCategory ? lastCategory.value + 1 : 600101;
// // // console.log('lastCategory', lastCategory);
// // // console.log('newValue', newValue);
// axios.post('worddict/insertCategory', {
// CMNCODNAM: data
// }).then(res => {
// if(res.data.data === 1){
// toastStore.onToast(' .', 's');
// const newCategory = { label: data, value: newValue };
// localCateList.value = [newCategory, ...localCateList.value];
// selectedCategory.value = newCategory.value;
// // // console.log('newCategory', newCategory);
// // // console.log('localCateList.value', localCateList.value);
// // // console.log('selectedCategory.value', selectedCategory.value);
// // //
// emit('update:cateList', localCateList.value);
// } else if(res.data.message == ' .') {
// toastStore.onToast(res.data.message, 'e');
// }
// }).catch(err => {
// console.error(' :', err);
// });
// } catch (err) {
// console.error(' :', err);
// }
// }
// //
const editWord = (data) => { const editWord = (data) => {
// console.log('📌 :', data);
// console.log('📌 :', data.id);
// console.log('📌 :', data.category);
// console.log('📌 :', data.title);
// console.log('📌 :', $common.deltaAsJson(data.content));
if (!data.id) { if (!data.id) {
console.error('❌ 수정할 데이터의 ID가 없습니다.'); console.error('❌ 수정할 데이터의 ID가 없습니다.');
@ -181,8 +125,10 @@ const editWord = (data) => {
.then((res) => { .then((res) => {
if (res.data.data === 1) { if (res.data.data === 1) {
toastStore.onToast('✅ 용어가 수정되었습니다.', 's'); toastStore.onToast('✅ 용어가 수정되었습니다.', 's');
// isWriteVisible.value = false;
writeStore.closeAll(); writeStore.closeAll();
if (writeButton.value) {
writeButton.value.resetButton();
}
emit('refreshWordList'); emit('refreshWordList');
} else { } else {
console.warn('⚠️ 서버 응답이 예상과 다릅니다:', res.data); console.warn('⚠️ 서버 응답이 예상과 다릅니다:', res.data);
@ -216,10 +162,13 @@ const toggleCheck = (event) => {
</script> </script>
<style scoped> <style scoped>
/* 동그란 테두리 설정 */ .avatar {
cursor: default;
}
.user-avatar { .user-avatar {
border: 3px solid; /* 테두리 */ border: 3px solid;
padding: 0.1px; /* 테두리와 이미지 사이의 간격 */ padding: 0.1px;
} }
.edit-btn { .edit-btn {

View File

@ -29,11 +29,6 @@
@focusout="handleCategoryFocusout(addCategory)" @focusout="handleCategoryFocusout(addCategory)"
/> />
</div> </div>
<!-- <div class="col-2 btn-margin">
<button class="btn btn-primary btn-icon" @click="saveInput">
<i class="bx bx-check"></i>
</button>
</div> -->
</div> </div>
</div> </div>
<div class="dict-w"> <div class="dict-w">
@ -65,14 +60,6 @@ import QEditor from '@/components/editor/QEditor.vue';
import FormInput from '@/components/input/FormInput.vue'; import FormInput from '@/components/input/FormInput.vue';
import FormSelect from '@/components/input/FormSelect.vue'; import FormSelect from '@/components/input/FormSelect.vue';
import PlusBtn from '../button/PlusBtn.vue'; import PlusBtn from '../button/PlusBtn.vue';
// import { clearConfig } from 'dompurify';
// import { useUserInfoStore } from '@s/useUserInfoStore';
//
// const userStore = useUserInfoStore();
// disabled
// const isDisabled = computed(() => userStore.user.role !== 'ROLE_ADMIN');
const emit = defineEmits(['close','addCategory','addWord']); const emit = defineEmits(['close','addCategory','addWord']);
@ -133,20 +120,6 @@ const toggleInput = () => {
showInput.value = !showInput.value; showInput.value = !showInput.value;
}; };
//
// const saveInput = () => {
// if(addCategory.value == ''){
// addCategoryAlert.value = true;
// return;
// }else {
// addCategoryAlert.value = false;
// }
// console.log(' !',addCategory.value);
// emit('addCategory', addCategory.value);
// showInput.value = false;
// };
const onChange = (newValue) => { const onChange = (newValue) => {
selectCategory.value = newValue.target.value; selectCategory.value = newValue.target.value;
}; };
@ -199,11 +172,9 @@ const handleCategoryFocusout = (value) => {
const valueTrim = value.trim(); const valueTrim = value.trim();
const existingCategory = props.dataList.find(item => item.label === valueTrim); const existingCategory = props.dataList.find(item => item.label === valueTrim);
// console.log('existingCategory', existingCategory);
// //
if(valueTrim == ''){ if(valueTrim == ''){
//alert(' ');
addCategoryAlert.value = true; addCategoryAlert.value = true;
// focus // focus

View File

@ -32,17 +32,17 @@
window.isDarkStyle = window.Helpers.isDarkStyle(); window.isDarkStyle = window.Helpers.isDarkStyle();
const loadScript = src => { const loadScript = src => {
const script = document.createElement('script'); const script = document.createElement('script');
script.src = src; script.src = src;
script.type = 'text/javascript'; script.type = 'text/javascript';
script.async = true; script.async = true;
document.body.appendChild(script); document.body.appendChild(script);
}; };
nextTick(async () => { nextTick(async () => {
await wait(200); await wait(200);
loadScript('/vendor/js/menu.js'); loadScript('/vendor/js/menu.js');
// loadScript('/js/main.js'); loadScript('/js/main.js');
}); });
</script> </script>
<style></style> <style></style>

View File

@ -23,9 +23,10 @@
<span class="app-brand-text demo menu-text fw-bold ms-2">LOCALNET</span> <span class="app-brand-text demo menu-text fw-bold ms-2">LOCALNET</span>
</a> </a>
<a href="javascript:void(0);" class="layout-menu-toggle menu-link text-large ms-auto"> <!-- 메뉴 토글바 -->
<!-- <a href="javascript:void(0);" class="layout-menu-toggle menu-link text-large ms-auto">
<i class="bx bx-chevron-left bx-sm d-flex align-items-center justify-content-center"></i> <i class="bx bx-chevron-left bx-sm d-flex align-items-center justify-content-center"></i>
</a> </a> -->
</div> </div>
<div class="menu-inner-shadow"></div> <div class="menu-inner-shadow"></div>

View File

@ -241,7 +241,7 @@
const user = ref(null); const user = ref(null);
//const baseUrl = $api.defaults.baseURL.replace(/api\/$/, ''); //const baseUrl = $api.defaults.baseURL.replace(/api\/$/, '');
const baseUrl = import.meta.env.BASE_URL; const baseUrl = import.meta.env.VITE_SERVER;
const authStore = useAuthStore(); const authStore = useAuthStore();
const userStore = useUserInfoStore(); const userStore = useUserInfoStore();

View File

@ -59,8 +59,9 @@
<td class="text-center">공지</td> <td class="text-center">공지</td>
<td class="cursor-pointer"> <td class="cursor-pointer">
📌 {{ notice.title }} 📌 {{ notice.title }}
<span v-if="notice.commentCount" class="comment-count">[ {{ notice.commentCount }} ]</span>
<i v-if="notice.img" class="bi bi-image me-1"></i> <i v-if="notice.img" class="bi bi-image me-1"></i>
<i v-if="notice.hasAttachment" class="bi bi-paperclip"></i> <i v-if="Array.isArray(notice.hasAttachment) && notice.hasAttachment.length > 0" class="bi bi-paperclip"></i>
<span v-if="isNewPost(notice.rawDate)" class="badge bg-danger text-white ms-2 fs-tiny">N</span> <span v-if="isNewPost(notice.rawDate)" class="badge bg-danger text-white ms-2 fs-tiny">N</span>
</td> </td>
<td class="text-center">{{ notice.author }}</td> <td class="text-center">{{ notice.author }}</td>
@ -76,8 +77,9 @@
<td class="text-center">{{ post.id }}</td> <td class="text-center">{{ post.id }}</td>
<td class="cursor-pointer"> <td class="cursor-pointer">
{{ post.title }} {{ post.title }}
<span v-if="post.commentCount" class="comment-count">[ {{ post.commentCount }} ]</span>
<i v-if="post.img" class="bi bi-image me-1"></i> <i v-if="post.img" class="bi bi-image me-1"></i>
<i v-if="post.hasAttachment" class="bi bi-paperclip"></i> <i v-if="Array.isArray(post.hasAttachment) && post.hasAttachment.length > 0" class="bi bi-paperclip"></i>
<span v-if="isNewPost(post.rawDate)" class="badge bg-danger text-white ms-2 fs-tiny">N</span> <span v-if="isNewPost(post.rawDate)" class="badge bg-danger text-white ms-2 fs-tiny">N</span>
</td> </td>
<td class="text-center">{{ post.author }}</td> <td class="text-center">{{ post.author }}</td>
@ -195,11 +197,7 @@ const fetchGeneralPosts = async (page = 1) => {
}); });
if (data?.data) { if (data?.data) {
// console.log(data) const totalPosts = data.data.total;
const totalPosts = data.data.total; //
// console.log('📌 API :', data.data);
generalList.value = data.data.list.map((post, index) => ({ generalList.value = data.data.list.map((post, index) => ({
realId: post.id, realId: post.id,
id: totalPosts - ((page - 1) * selectedSize.value) - index, id: totalPosts - ((page - 1) * selectedSize.value) - index,
@ -208,8 +206,9 @@ const fetchGeneralPosts = async (page = 1) => {
rawDate: post.date, rawDate: post.date,
date: formatDate(post.date), // date: formatDate(post.date), //
views: post.cnt || 0, views: post.cnt || 0,
hasAttachment: post.hasAttachment || false, hasAttachment: post.hasAttachment,
img: post.firstImageUrl || null img: post.firstImageUrl || null,
commentCount : post.commentCount
})); }));
pagination.value = { pagination.value = {
@ -248,8 +247,9 @@ const fetchNoticePosts = async () => {
date: formatDate(post.date), date: formatDate(post.date),
rawDate: post.date, rawDate: post.date,
views: post.cnt || 0, views: post.cnt || 0,
hasAttachment: post.hasAttachment || false, hasAttachment: post.hasAttachment,
img: post.firstImageUrl || null img: post.firstImageUrl || null,
commentCount : post.commentCount
})); }));
} }
} catch (error) { } catch (error) {
@ -289,4 +289,12 @@ onMounted(() => {
width: 100% !important; width: 100% !important;
} }
} }
/* 댓글 개수 스타일 */
.comment-count {
font-size: 0.9rem; /* 글씨 크기 증가 */
font-weight: bold;
color: #ff5733; /* 강조 색상 (붉은 계열) */
border-radius: 4px; /* 둥근 모서리 */
padding: 2px 6px; /* 내부 패딩 */
}
</style> </style>

View File

@ -18,45 +18,34 @@
@editClick="editClick" @editClick="editClick"
@deleteClick="deleteClick" @deleteClick="deleteClick"
/> />
</div>
</div>
<!-- 비밀번호 입력창 (익명일 경우) -->
<div v-if="isPassword && unknown" class="mt-3 w-25 ms-auto">
<div class="input-group">
<input
type="password"
class="form-control"
v-model="password"
placeholder="비밀번호 입력"
/>
<button class="btn btn-primary" @click="submitPassword">확인</button>
</div>
<span v-if="passwordAlert" class="invalid-feedback d-block text-start">{{ passwordAlert }}</span>
</div>
</div>
</div>
<!-- 게시글 내용 --> <!-- 게시글 내용 -->
<div class="card-body"> <div class="card-body">
<div class="d-flex justify-content-between align-items-center flex-wrap mb-6 gap-2"> <div class="d-flex justify-content-between align-items-center flex-wrap mb-6 gap-2">
<!-- 제목 섹션 --> <!-- 제목 섹션 -->
<div class="me-1"> <div class="me-1">
<h5 class="mb-4">{{ boardTitle }}</h5> <h5 class="mb-4">{{ boardTitle }}</h5>
</div> </div>
<!-- 첨부파일 섹션 --> <!-- 첨부파일 다운로드 버튼 -->
<div v-if="attachment" class="btn-group"> <div v-if="attachments.length" class="btn-group">
<button type="button" class="btn btn-label-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false"> <button type="button" class="btn btn-label-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa-solid fa-download me-2"></i> <i class="fa-solid fa-download me-2"></i>
첨부파일 첨부파일 ({{ attachments.length }})
<!-- (<span class="attachment-num">{{ dropdownItems.length }}</span>) -->
</button> </button>
<!-- <ul class="dropdown-menu"> <ul class="dropdown-menu">
<li v-for="(item, index) in dropdownItems" :key="index"> <li v-for="(attachment, index) in attachments" :key="index">
<a class="dropdown-item" href="javascript:void(0);"> <a
{{ item.label }} class="dropdown-item"
href="#"
@click.prevent="downloadFile(attachment)"
>
{{ attachment.originalName }}.{{ attachment.extension }}
</a> </a>
</li> </li>
</ul> --> </ul>
</div> </div>
</div> </div>
@ -76,19 +65,6 @@
@updateReaction="handleUpdateReaction" @updateReaction="handleUpdateReaction"
/> />
</div> </div>
<!-- <p>현재 로그인한 사용자 ID: {{ currentUserId }}</p>
<p>게시글 작성자: {{ authorId }}</p>
<p>isAuthor : {{ isAuthor }}</p> -->
<!-- <p>use이미지:{{userStore.user.img}}</p> -->
<!-- <img :src="`http://localhost:10325/upload/img/profile/${userStore.user.profile}`" alt="Profile Image" class="w-px-40 h-auto rounded-circle"/> -->
<!-- 첨부파일 목록 -->
<!-- <ul v-if="attachments.length" class="attachments mt-4 list-unstyled">
<li v-for="(attachment, index) in attachments" :key="index" class="mb-2">
<a :href="attachment.url" target="_blank" class="text-decoration-none">{{ attachment.name }}</a>
</li>
</ul> -->
<!-- 댓글 입력 영역 --> <!-- 댓글 입력 영역 -->
<BoardCommentArea <BoardCommentArea
@ -98,7 +74,6 @@
:passwordAlert="passwordAlert" :passwordAlert="passwordAlert"
@submitComment="handleCommentSubmit" @submitComment="handleCommentSubmit"
/> />
<!-- <BoardCommentArea :profileName="profileName" :unknown="unknown" /> -->
</div> </div>
<!-- 댓글 목록 --> <!-- 댓글 목록 -->
@ -106,9 +81,12 @@
<BoardCommentList <BoardCommentList
:unknown="unknown" :unknown="unknown"
:comments="commentsWithAuthStatus" :comments="commentsWithAuthStatus"
:isCommentPassword="Boolean(isCommentPassword)" :isCommentPassword="isCommentPassword"
:isEditTextarea="isEditTextarea" :isEditTextarea="isEditTextarea"
:isDeleted="isDeleted"
:passwordCommentAlert="passwordCommentAlert" :passwordCommentAlert="passwordCommentAlert"
:currentPasswordCommentId="currentPasswordCommentId"
:password="password"
@editClick="editComment" @editClick="editComment"
@deleteClick="deleteComment" @deleteClick="deleteComment"
@updateReaction="handleCommentReaction" @updateReaction="handleCommentReaction"
@ -117,6 +95,7 @@
@commentDeleted="handleCommentDeleted" @commentDeleted="handleCommentDeleted"
@cancelEdit="handleCancelEdit" @cancelEdit="handleCancelEdit"
@submitEdit="handleSubmitEdit" @submitEdit="handleSubmitEdit"
@update:password="updatePassword"
/> />
<Pagination <Pagination
v-if="pagination.pages" v-if="pagination.pages"
@ -173,20 +152,50 @@ const commentsWithAuthStatus = computed(() => {
isCommentAuthor: reply.authorId === currentUserId.value, isCommentAuthor: reply.authorId === currentUserId.value,
})) }))
})); }));
// console.log(" commentsWithAuthStatus :", updatedComments);
return updatedComments; return updatedComments;
}); });
const attachments = ref([]);
// URL
const downloadFile = async (attachment) => {
try {
const response = await axios.get(`board/download`, {
params: { path: attachment.path },
responseType: 'blob'
});
// Blob
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', attachment.originalName + '.' + attachment.extension);
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(url);
} catch (error) {
console.error('파일 다운로드 오류:', error);
alert('파일 다운로드 중 오류가 발생했습니다.');
}
};
const password = ref(''); const password = ref('');
const passwordAlert = ref(""); const passwordAlert = ref("");
const passwordCommentAlert = ref(""); const passwordCommentAlert = ref("");
const isPassword = ref(false); const isPassword = ref(false);
const isCommentPassword = ref(false); const isCommentPassword = ref(false);
const currentPasswordCommentId = ref(null);
const lastClickedButton = ref(""); const lastClickedButton = ref("");
const lastCommentClickedButton = ref(""); const lastCommentClickedButton = ref("");
const isEditTextarea = ref(false); const isEditTextarea = ref(false);
const commentAlert = ref('') const isDeleted = ref(true);
const commentAlert = ref('');
const updatePassword = (newPassword) => {
password.value = newPassword;
};
const pagination = ref({ const pagination = ref({
currentPage: 1, currentPage: 1,
@ -210,21 +219,10 @@ const fetchBoardDetails = async () => {
const response = await axios.get(`board/${currentBoardId.value}`); const response = await axios.get(`board/${currentBoardId.value}`);
const data = response.data.data; const data = response.data.data;
// console.log(data)
// API
// const boardDetail = data.boardDetail || {};
profileName.value = data.author || '익명'; profileName.value = data.author || '익명';
// authorId.value = data.authorId;
// profileName.value = '';
// console.log("📌 :", profileName.value); //
// console.log("🔍 (unknown.value):", unknown.value); //
authorId.value = data.authorId; // id
boardTitle.value = data.title || '제목 없음'; boardTitle.value = data.title || '제목 없음';
boardContent.value = data.content || ''; boardContent.value = data.content || '';
date.value = data.date || ''; date.value = data.date || '';
@ -233,6 +231,7 @@ const fetchBoardDetails = async () => {
dislikes.value = data.dislikeCount || 0; dislikes.value = data.dislikeCount || 0;
attachment.value = data.hasAttachment || null; attachment.value = data.hasAttachment || null;
commentNum.value = data.commentCount || 0; commentNum.value = data.commentCount || 0;
attachments.value = data.attachments || [];
} catch (error) { } catch (error) {
alert('게시물 데이터를 불러오는 중 오류가 발생했습니다.'); alert('게시물 데이터를 불러오는 중 오류가 발생했습니다.');
@ -245,7 +244,6 @@ const handleUpdateReaction = async ({ boardId, commentId, isLike, isDislike }) =
await axios.post(`/board/${boardId}/${commentId}/reaction`, { await axios.post(`/board/${boardId}/${commentId}/reaction`, {
LOCBRDSEQ: boardId, // id LOCBRDSEQ: boardId, // id
LOCCMTSEQ: commentId, // id LOCCMTSEQ: commentId, // id
// MEMBERSEQ: 1, // 1
LOCGOBGOD: isLike ? 'T' : 'F', LOCGOBGOD: isLike ? 'T' : 'F',
LOCGOBBAD: isDislike ? 'T' : 'F' LOCGOBBAD: isDislike ? 'T' : 'F'
}); });
@ -258,13 +256,9 @@ const handleUpdateReaction = async ({ boardId, commentId, isLike, isDislike }) =
likeClicked.value = isLike; likeClicked.value = isLike;
dislikeClicked.value = isDislike; dislikeClicked.value = isDislike;
// console.log(updatedData)
// console.log(" :", updatedData);
} catch (error) { } catch (error) {
alert('오류가 발생했습니다.'); alert('오류가 발생했습니다.');
// console.log(' .');
} }
}; };
@ -281,13 +275,10 @@ const handleCommentReaction = async ({ boardId, commentId, isLike, isDislike })
LOCGOBBAD: isDislike ? 'T' : 'F' LOCGOBBAD: isDislike ? 'T' : 'F'
}); });
// console.log(" API :", response.data);
await fetchComments(); await fetchComments();
} catch (error) { } catch (error) {
alert('오류가 발생했습니다.'); alert('오류가 발생했습니다.');
// console.log(' ');
} }
}; };
@ -301,9 +292,6 @@ const fetchComments = async (page = 1) => {
page page
} }
}); });
// console.log(response.data.data)
const commentsList = response.data.data.list.map(comment => ({ const commentsList = response.data.data.list.map(comment => ({
commentId: comment.LOCCMTSEQ, // ID commentId: comment.LOCCMTSEQ, // ID
boardId: comment.LOCBRDSEQ, boardId: comment.LOCBRDSEQ,
@ -318,8 +306,12 @@ const fetchComments = async (page = 1) => {
createdAtRaw: new Date(comment.LOCCMTRDT), // createdAtRaw: new Date(comment.LOCCMTRDT), //
createdAt: formattedDate(comment.LOCCMTRDT), // createdAt: formattedDate(comment.LOCCMTRDT), //
children: [], // children: [], //
updateAtRaw: comment.LOCCMTUDT,
})); }));
commentsList.sort((a, b) => b.createdAtRaw - a.createdAtRaw);
for (const comment of commentsList) { for (const comment of commentsList) {
if (!comment.commentId) continue; if (!comment.commentId) continue;
@ -327,8 +319,6 @@ const fetchComments = async (page = 1) => {
params: { LOCCMTPNT: comment.commentId } params: { LOCCMTPNT: comment.commentId }
}); });
// console.log(` (${comment.commentId} ):`, replyResponse.data);
if (replyResponse.data.data) { if (replyResponse.data.data) {
comment.children = replyResponse.data.data.map(reply => ({ comment.children = replyResponse.data.data.map(reply => ({
author: reply.author || '익명', author: reply.author || '익명',
@ -368,23 +358,19 @@ const fetchComments = async (page = 1) => {
navigateLastPage: response.data.data.navigateLastPage // navigateLastPage: response.data.data.navigateLastPage //
}; };
// console.log("📌 :", comments.value);
} catch (error) { } catch (error) {
alert('오류가 발생했습니다.'); alert('오류가 발생했습니다.');
// alert(' :', error);
} }
}; };
// //
const handleCommentSubmit = async (data, isCheck) => { const handleCommentSubmit = async (data) => {
if (!data) { if (!data) {
console.error("handleCommentSubmit: data가 undefined입니다.");
return; return;
} }
const { comment, password } = data; const { comment, password, isCheck } = data;
if (!comment || comment.trim() === "") { if (!comment || comment.trim() === "") {
commentAlert.value = '댓글을 입력해주세요.'; commentAlert.value = '댓글을 입력해주세요.';
@ -404,25 +390,22 @@ const handleCommentSubmit = async (data, isCheck) => {
LOCCMTRPY: comment, LOCCMTRPY: comment,
LOCCMTPWD: isCheck ? password : '', LOCCMTPWD: isCheck ? password : '',
LOCCMTPNT: 1, LOCCMTPNT: 1,
LOCBRDTYP: unknown.value ? "300102" : null LOCBRDTYP: isCheck ? "300102" : null
}); });
if (response.status === 200) { if (response.status === 200) {
// console.log(' :', response.data.message);
passwordAlert.value = ''; passwordAlert.value = '';
commentAlert.value = ''; commentAlert.value = '';
await fetchComments(); await fetchComments();
} else { } else {
// console.error(' :', response.data.message);
alert("댓글 작성을 실패했습니다.") alert("댓글 작성을 실패했습니다.")
} }
} catch (error) { } catch (error) {
// console.error(' :', error);
alert("오류가 발생했습니다.") alert("오류가 발생했습니다.")
} }
}; };
// ( `BoardCommentList` ) //
const handleCommentReply = async (reply) => { const handleCommentReply = async (reply) => {
try { try {
const response = await axios.post(`board/${currentBoardId.value}/comment`, { const response = await axios.post(`board/${currentBoardId.value}/comment`, {
@ -434,22 +417,17 @@ const handleCommentReply = async (reply) => {
}); });
if (response.status === 200) { if (response.status === 200) {
if (response.data.code === 200) { // if (response.data.code === 200) {
// console.log(' :', response.data); await fetchComments();
await fetchComments(); //
} else { } else {
// console.log(' - :', response.data);
alert('대댓글 작성을 실패했습니다.'); alert('대댓글 작성을 실패했습니다.');
} }
} }
} catch (error) { } catch (error) {
// console.error(' :', error);
if (error.response) { if (error.response) {
// console.error(' :', error.response.data);
alert("오류가 발생했습니다."); alert("오류가 발생했습니다.");
} }
alert("오류가 발생했습니다."); alert("오류가 발생했습니다.");
} }
} }
@ -486,45 +464,69 @@ const findCommentById = (commentId, commentsList) => {
return null; return null;
}; };
// // ( )
const editComment = (comment) => { const editComment = (comment) => {
password.value = '';
passwordCommentAlert.value = '';
currentPasswordCommentId.value = null;
//
const targetComment = findCommentById(comment.commentId, comments.value); const targetComment = findCommentById(comment.commentId, comments.value);
if (!targetComment) { if (!targetComment) {
return; return;
} }
//
const isMyComment = comment.authorId === currentUserId.value; const isMyComment = comment.authorId === currentUserId.value;
const isAnonymous = comment.author === "익명"; const isAnonymous = comment.author === "익명";
if (isMyComment) { if (isMyComment) {
// if (targetComment.isEditTextarea) {
targetComment.isEditTextarea = true; //
} else if (isAnonymous) { targetComment.isEditTextarea = false;
// currentPasswordCommentId.value = comment.commentId;
toggleCommentPassword(comment, "edit"); } else {
//
closeAllEditTextareas();
//
targetComment.isEditTextarea = true;
}
} else if (isAnonymous) {
if (currentPasswordCommentId.value === comment.commentId) {
//
return;
} else {
//
closeAllEditTextareas();
//
targetComment.isEditTextarea = false;
toggleCommentPassword(comment, "edit");
}
} else { } else {
// console.log(" - ");
alert("수정이 불가능합니다"); alert("수정이 불가능합니다");
} }
} }
//
const closeAllEditTextareas = () => {
comments.value.forEach(comment => {
comment.isEditTextarea = false;
comment.children.forEach(reply => {
reply.isEditTextarea = false;
});
});
};
// //
const deleteComment = async (comment) => { const deleteComment = async (comment) => {
//
const isMyComment = comment.authorId === currentUserId.value; const isMyComment = comment.authorId === currentUserId.value;
if (unknown.value && !isMyComment) { if (unknown.value && !isMyComment) {
if (comment.isEditTextarea) { if (comment.isEditTextarea) {
// ,
comment.isEditTextarea = false; comment.isEditTextarea = false;
comment.isCommentPassword = true; comment.isCommentPassword = true;
} else { } else {
//
toggleCommentPassword(comment, "delete"); toggleCommentPassword(comment, "delete");
} }
} else { } else {
@ -532,19 +534,21 @@ const deleteComment = async (comment) => {
} }
}; };
// //
const toggleCommentPassword = (comment, button) => { const toggleCommentPassword = (comment, button) => {
if (lastCommentClickedButton.value === button && isCommentPassword.value === comment.commentId) { if (lastCommentClickedButton.value === button && currentPasswordCommentId.value === comment.commentId) {
isCommentPassword.value = false; // currentPasswordCommentId.value = null; //
password.value = '';
passwordCommentAlert.value = '';
} else { } else {
isCommentPassword.value = comment.commentId; // currentPasswordCommentId.value = comment.commentId; //
password.value = '';
passwordCommentAlert.value = '';
} }
lastCommentClickedButton.value = button; lastCommentClickedButton.value = button;
}; };
const togglePassword = (button) => { const togglePassword = (button) => {
if (lastClickedButton.value === button) { if (lastClickedButton.value === button) {
isPassword.value = !isPassword.value; isPassword.value = !isPassword.value;
@ -556,16 +560,15 @@ const togglePassword = (button) => {
// //
const submitPassword = async () => { const submitPassword = async () => {
if (!password.value) { if (!password.value.trim()) {
passwordAlert.value = "비밀번호를 입력해주세요."; passwordAlert.value = "비밀번호를 입력해주세요.";
return; return;
} }
// console.log("📌 : submitPassword ");
try { try {
const response = await axios.post(`board/${currentBoardId.value}/password`, { const response = await axios.post(`board/${currentBoardId.value}/password`, {
LOCBRDPWD: password.value, LOCBRDPWD: password.value,
LOCBRDSEQ: 288, // ID LOCBRDSEQ: 288,
}); });
if (response.data.code === 200 && response.data.data === true) { if (response.data.code === 200 && response.data.data === true) {
@ -582,8 +585,6 @@ const submitPassword = async () => {
passwordAlert.value = "비밀번호가 일치하지 않습니다."; passwordAlert.value = "비밀번호가 일치하지 않습니다.";
} }
} catch (error) { } catch (error) {
// console.log("📌 :", error);
if (error.response) { if (error.response) {
if (error.response.status === 401) { if (error.response.status === 401) {
passwordAlert.value = "비밀번호가 일치하지 않습니다."; passwordAlert.value = "비밀번호가 일치하지 않습니다.";
@ -598,34 +599,40 @@ const submitPassword = async () => {
} }
}; };
// ( ) // ( )
const submitCommentPassword = async (comment, password) => { const submitCommentPassword = async (comment, password) => {
// console.log(" :", password);
// console.log(" ID:", comment.commentId);
if (!password) { if (!password) {
passwordCommentAlert.value = "비밀번호를 입력해주세요."; passwordCommentAlert.value = "비밀번호를 입력해주세요.";
return; return;
} }
const targetComment = findCommentById(comment.commentId, comments.value);
try { try {
// console.log(' ')
const response = await axios.post(`board/comment/${comment.commentId}/password`, { const response = await axios.post(`board/comment/${comment.commentId}/password`, {
LOCCMTPWD: password, LOCCMTPWD: password,
LOCCMTSEQ: comment.commentId, LOCCMTSEQ: comment.commentId,
}); });
// console.log(" :", response.data);
if (response.data.code === 200 && response.data.data === true) { if (response.data.code === 200 && response.data.data === true) {
passwordCommentAlert.value = ""; passwordCommentAlert.value = "";
comment.isCommentPassword = false; comment.isCommentPassword = false;
//
if (lastCommentClickedButton.value === "edit") { if (lastCommentClickedButton.value === "edit") {
comment.isEditTextarea = true;
passwordCommentAlert.value = "";
// handleSubmitEdit(comment, comment.content); if (targetComment) {
//
closeAllEditTextareas();
targetComment.isEditTextarea = true;
passwordCommentAlert.value = "";
currentPasswordCommentId.value = null;
} else {
alert("수정 취소를 실패했습니다.");
}
//
} else if (lastCommentClickedButton.value === "delete") { } else if (lastCommentClickedButton.value === "delete") {
passwordCommentAlert.value = ""; passwordCommentAlert.value = "";
@ -633,14 +640,12 @@ const submitCommentPassword = async (comment, password) => {
} }
lastCommentClickedButton.value = null; lastCommentClickedButton.value = null;
} else { } else {
// console.log(" ");
passwordCommentAlert.value = "비밀번호가 일치하지 않습니다."; passwordCommentAlert.value = "비밀번호가 일치하지 않습니다.";
} }
} catch (error) { } catch (error) {
// console.log("🚨 :", error.response?.data || error); if (error.response?.status === 401) {
// if (error.response?.status === 401) { passwordCommentAlert.value = "비밀번호가 일치하지 않습니다";
// console.log(" 401 : ( )"); }
// }
passwordCommentAlert.value = "비밀번호가 일치하지 않습니다"; passwordCommentAlert.value = "비밀번호가 일치하지 않습니다";
} }
}; };
@ -672,24 +677,30 @@ const deletePost = async () => {
// ( ) // ( )
const deleteReplyComment = async (comment) => { const deleteReplyComment = async (comment) => {
if (!confirm("정말 이 댓글을 삭제하시겠습니까?")) return; if (!confirm("정말 이 댓글을 삭제하시겠습니까?")) return;
// console.log(" ID:", comment);
const targetComment = findCommentById(comment.commentId, comments.value);
// console.log('?',comment)
try { try {
const response = await axios.delete(`board/comment/${comment.commentId}`, { const response = await axios.delete(`board/comment/${comment.commentId}`, {
data: { LOCCMTSEQ: comment.commentId } data: { LOCCMTSEQ: comment.commentId }
}); });
// console.log(" :", response.data);
if (response.data.code === 200) { if (response.data.code === 200) {
// console.log(" !");
await fetchComments(); await fetchComments();
if (targetComment) {
// console.log('',targetComment)
// " ." ,
targetComment.content = "댓글이 삭제되었습니다.";
targetComment.author = "알 수 없음"; //
targetComment.isDeleted = true; //
}
} else { } else {
// console.log(" :", response.data.message);
alert("댓글 삭제에 실패했습니다."); alert("댓글 삭제에 실패했습니다.");
} }
} catch (error) { } catch (error) {
// console.log(" :", error);
alert("댓글 삭제 중 오류가 발생했습니다."); alert("댓글 삭제 중 오류가 발생했습니다.");
} }
}; };
@ -702,9 +713,6 @@ const handleSubmitEdit = async (comment, editedContent) => {
LOCCMTRPY: editedContent LOCCMTRPY: editedContent
}); });
//
// comment.content = editedContent;
// comment.isEditTextarea = false; f
if (response.status === 200) { if (response.status === 200) {
const targetComment = findCommentById(comment.commentId, comments.value); const targetComment = findCommentById(comment.commentId, comments.value);
@ -712,15 +720,12 @@ const handleSubmitEdit = async (comment, editedContent) => {
targetComment.content = editedContent; // targetComment.content = editedContent; //
targetComment.isEditTextarea = false; // targetComment.isEditTextarea = false; //
} else { } else {
// console.warn(" ");
alert("수정할 댓글을 찾을 수 없습니다."); alert("수정할 댓글을 찾을 수 없습니다.");
} }
} else { } else {
// console.log(" :", response.data);
alert("댓글 수정 실패했습니다."); alert("댓글 수정 실패했습니다.");
} }
} catch (error) { } catch (error) {
// console.error(" :", error);
alert("댓글 수정 중 오류 발생했습니다."); alert("댓글 수정 중 오류 발생했습니다.");
} }
}; };
@ -730,10 +735,8 @@ const handleCancelEdit = (comment) => {
const targetComment = findCommentById(comment.commentId, comments.value); const targetComment = findCommentById(comment.commentId, comments.value);
if (targetComment) { if (targetComment) {
// console.log(" , :", targetComment);
targetComment.isEditTextarea = false; targetComment.isEditTextarea = false;
} else { } else {
// console.error(" , ");
alert("수정 취소를 실패했습니다."); alert("수정 취소를 실패했습니다.");
} }
}; };
@ -756,7 +759,7 @@ const handleCommentDeleted = (deletedCommentId) => {
return; return;
} }
// 2 //
for (let parent of comments.value) { for (let parent of comments.value) {
const childIndex = parent.children.findIndex(child => child.commentId === deletedCommentId); const childIndex = parent.children.findIndex(child => child.commentId === deletedCommentId);
if (childIndex !== -1) { if (childIndex !== -1) {
@ -764,11 +767,8 @@ const handleCommentDeleted = (deletedCommentId) => {
return; return;
} }
} }
// console.error(" :", deletedCommentId);
}; };
// //
const formattedDate = (dateString) => { const formattedDate = (dateString) => {
if (!dateString) return "날짜 없음"; if (!dateString) return "날짜 없음";

View File

@ -15,6 +15,8 @@
:is-essential="true" :is-essential="true"
:is-alert="titleAlert" :is-alert="titleAlert"
v-model="title" v-model="title"
@update:alert="titleAlert = $event"
@input="validateTitle"
/> />
<!-- 카테고리 선택 --> <!-- 카테고리 선택 -->
@ -32,16 +34,19 @@
:id="`category-${index}`" :id="`category-${index}`"
:value="category.CMNCODVAL" :value="category.CMNCODVAL"
v-model="categoryValue" v-model="categoryValue"
@change="categoryAlert = false"
/> />
<label class="form-check-label" :for="`category-${index}`"> <label class="form-check-label" :for="`category-${index}`">
{{ category.CMNCODNAM }} {{ category.CMNCODNAM }}
</label> </label>
</div> </div>
</div> </div>
<div class="invalid-feedback" :class="categoryAlert ? 'display-block' : ''">카테고리를 선택해주세요.</div> <div class="invalid-feedback" :class="categoryAlert ? 'd-block' : 'd-none'">
카테고리를 선택해주세요.
</div>
</div> </div>
<!-- 비밀번호 필드 --> <!-- 비밀번호 필드 (익명게시판 선택 활성화) -->
<div v-if="categoryValue === 300102" class="mb-4"> <div v-if="categoryValue === 300102" class="mb-4">
<FormInput <FormInput
title="비밀번호" title="비밀번호"
@ -50,26 +55,42 @@
:is-essential="true" :is-essential="true"
:is-alert="passwordAlert" :is-alert="passwordAlert"
v-model="password" v-model="password"
@update:alert="passwordAlert = $event"
@input="validatePassword"
/> />
</div> </div>
<!-- 첨부파일 업로드 -->
<FormFile <FormFile
title="첨부파일" title="첨부파일"
name="files" name="files"
:is-alert="attachFilesAlert" :is-alert="attachFilesAlert"
@update:data="attachFiles = $event" @update:data="handleFileUpload"
@update:isValid="isFileValid = $event" @update:isValid="isFileValid = $event"
/> />
<!-- 실시간 반영된 파일 개수 표시 -->
<p class="text-muted mt-1">첨부파일: {{ fileCount }} / 5</p>
<p v-if="fileError" class="text-danger">{{ fileError }}</p>
<ul class="list-group mt-2" v-if="attachFiles.length">
<li v-for="(file, index) in attachFiles" :key="index" class="list-group-item d-flex justify-content-between align-items-center">
{{ file.name }}
<button class="close-btn" @click="removeFile(index)"></button>
</li>
</ul>
<!-- 내용 입력 (에디터) -->
<div class="mb-4"> <div class="mb-4">
<label for="html5-tel-input" class="col-md-2 col-form-label"> <label class="col-md-2 col-form-label">
내용 내용 <span class="text-danger">*</span>
<span class="text-danger">*</span>
<div class="invalid-feedback" :class="contentAlert ? 'display-block' : ''">내용을 확인해주세요.</div>
</label> </label>
<div class="col-md-12"> <div class="col-md-12">
<QEditor @update:data="content = $event" /> <QEditor @update:data="content = $event" />
</div> </div>
<div class="invalid-feedback mt-1" :class="contentAlert ? 'd-block' : 'd-none'">
내용을 입력해주세요.
</div>
</div> </div>
<div class="mb-4 d-flex justify-content-end"> <div class="mb-4 d-flex justify-content-end">
@ -83,23 +104,22 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted, getCurrentInstance, watch, computed } from 'vue';
import QEditor from '@c/editor/QEditor.vue'; import QEditor from '@c/editor/QEditor.vue';
import FormInput from '@c/input/FormInput.vue'; import FormInput from '@c/input/FormInput.vue';
import FormFile from '@c/input/FormFile.vue'; import FormFile from '@c/input/FormFile.vue';
import { getCurrentInstance, ref, onMounted } from 'vue'; import SaveButton from '@c/button/SaveBtn.vue';
import BackButton from '@c/button/BackBtn.vue';
import { useToastStore } from '@s/toastStore';
import router from '@/router'; import router from '@/router';
import axios from '@api'; import axios from '@api';
import SaveButton from '@c/button/SaveBtn.vue';
import BackButton from '@c/button/BackBtn.vue'
import { useToastStore } from '@s/toastStore';
const toastStore = useToastStore(); const toastStore = useToastStore();
const categoryList = ref([]); const categoryList = ref([]);
const title = ref(''); const title = ref('');
const password = ref(''); const password = ref('');
const categoryValue = ref(null); const categoryValue = ref(null);
const content = ref(''); const content = ref({ ops: [] });
const attachFiles = ref(null);
const isFileValid = ref(true); const isFileValid = ref(true);
const titleAlert = ref(false); const titleAlert = ref(false);
@ -108,17 +128,18 @@ const contentAlert = ref(false);
const categoryAlert = ref(false); const categoryAlert = ref(false);
const attachFilesAlert = ref(false); const attachFilesAlert = ref(false);
const { appContext } = getCurrentInstance(); const attachFiles = ref([]);
const $common = appContext.config.globalProperties.$common; const maxFiles = 5;
const maxSize = 10 * 1024 * 1024;
const fileError = ref('');
const fetchCategories = async () => { const fetchCategories = async () => {
try { try {
const response = await axios.get('board/categories'); const response = await axios.get('board/categories');
categoryList.value = response.data.data; categoryList.value = response.data.data;
// "" (CMNCODNAM '' )
const freeCategory = categoryList.value.find(category => category.CMNCODNAM === '자유'); const freeCategory = categoryList.value.find(category => category.CMNCODNAM === '자유');
if (freeCategory) { if (freeCategory) {
categoryValue.value = freeCategory.CMNCODVAL; // categoryValue.value = freeCategory.CMNCODVAL;
} }
} catch (error) { } catch (error) {
console.error('카테고리 불러오기 오류:', error); console.error('카테고리 불러오기 오류:', error);
@ -129,15 +150,67 @@ onMounted(() => {
fetchCategories(); fetchCategories();
}); });
const goList = () => { const fileCount = computed(() => attachFiles.value.length);
router.push('/board');
const handleFileUpload = (files) => {
const validFiles = files.filter(file => file.size <= maxSize);
if (files.some(file => file.size > maxSize)) {
fileError.value = '파일 크기가 10MB를 초과할 수 없습니다.';
return;
}
if (attachFiles.value.length + validFiles.length > maxFiles) {
fileError.value = `최대 ${maxFiles}개의 파일만 업로드할 수 있습니다.`;
return;
}
fileError.value = '';
attachFiles.value = [...attachFiles.value, ...validFiles].slice(0, maxFiles);
}; };
const removeFile = (index) => {
attachFiles.value.splice(index, 1);
if (attachFiles.value.length <= maxFiles) {
fileError.value = '';
}
};
watch(attachFiles, () => {
isFileValid.value = attachFiles.value.length <= maxFiles;
});
const validateTitle = () => {
titleAlert.value = title.value.trim().length === 0;
};
const validatePassword = () => {
if (categoryValue.value === 300102) {
password.value = password.value.replace(/\s/g, ''); //
passwordAlert.value = password.value.length === 0;
} else {
passwordAlert.value = false;
}
};
const validateContent = () => {
if (!content.value?.ops?.length) {
contentAlert.value = true;
return;
}
//
const hasImage = content.value.ops.some(op => op.insert && typeof op.insert === 'object' && op.insert.image);
//
const hasText = content.value.ops.some(op => typeof op.insert === 'string' && op.insert.trim().length > 0);
//
contentAlert.value = !(hasText || hasImage);
};
/** 글쓰기 */
const write = async () => { const write = async () => {
titleAlert.value = !title.value; validateTitle();
passwordAlert.value = categoryValue.value === 300102 && !password.value; validatePassword();
contentAlert.value = !content.value; validateContent();
categoryAlert.value = !categoryValue.value; categoryAlert.value = categoryValue.value == null;
if (titleAlert.value || passwordAlert.value || contentAlert.value || categoryAlert.value || !isFileValid.value) { if (titleAlert.value || passwordAlert.value || contentAlert.value || categoryAlert.value || !isFileValid.value) {
return; return;
@ -146,16 +219,17 @@ const write = async () => {
try { try {
const boardData = { const boardData = {
LOCBRDTTL: title.value, LOCBRDTTL: title.value,
LOCBRDCON: $common.deltaAsJson(content.value), LOCBRDCON: JSON.stringify(content.value), // Delta JSON
LOCBRDPWD: categoryValue.value === 300102 ? password.value : null, LOCBRDPWD: categoryValue.value === 300102 ? password.value : null,
LOCBRDTYP: categoryValue.value LOCBRDTYP: categoryValue.value
}; };
const { data: boardResponse } = await axios.post('board', boardData); const { data: boardResponse } = await axios.post('board', boardData);
const boardId = boardResponse.data; const boardId = boardResponse.data;
// ( )
if (attachFiles.value && attachFiles.value.length > 0) { if (attachFiles.value && attachFiles.value.length > 0) {
for (const file of attachFiles.value) { await Promise.all(attachFiles.value.map(async (file) => {
console.log(file);
const formData = new FormData(); const formData = new FormData();
const fileNameWithoutExt = file.name.replace(/\.[^/.]+$/, ''); const fileNameWithoutExt = file.name.replace(/\.[^/.]+$/, '');
@ -163,15 +237,12 @@ const write = async () => {
formData.append('CMNFLEORG', fileNameWithoutExt); formData.append('CMNFLEORG', fileNameWithoutExt);
formData.append('CMNFLEEXT', file.name.split('.').pop()); formData.append('CMNFLEEXT', file.name.split('.').pop());
formData.append('CMNFLESIZ', file.size); formData.append('CMNFLESIZ', file.size);
formData.append('CMNFLEPAT', 'boardfile'); formData.append('file', file); // 📌
formData.append('file', file);
await axios.post(`board/${boardId}/attachments`, formData, { await axios.post(`board/${boardId}/attachments`, formData,
headers: { { isFormData : true }
'Content-Type': 'multipart/form-data', );
}, }));
});
}
} }
toastStore.onToast('게시물이 작성되었습니다.', 's'); toastStore.onToast('게시물이 작성되었습니다.', 's');
@ -181,4 +252,14 @@ const write = async () => {
toastStore.onToast('게시물 작성 중 오류가 발생했습니다.', 'e'); toastStore.onToast('게시물 작성 중 오류가 발생했습니다.', 'e');
} }
}; };
/** 목록으로 이동 */
const goList = () => {
router.push('/board');
};
/** `content` 변경 감지하여 자동 유효성 검사 실행 */
watch(content, () => {
validateContent();
});
</script> </script>

View File

@ -7,9 +7,11 @@
<div class="sidebar-content"> <div class="sidebar-content">
<div class="sidebar-actions text-center my-3"> <div class="sidebar-actions text-center my-3">
<HalfDayButtons <HalfDayButtons
ref="halfDayButtonsRef"
@toggleHalfDay="toggleHalfDay" @toggleHalfDay="toggleHalfDay"
@addVacationRequests="saveVacationChanges" @addVacationRequests="saveVacationChanges"
:isDisabled="!hasChanges" :isDisabled="!hasChanges"
:selectedDate="selectedDate"
/> />
</div> </div>
<ProfileList <ProfileList
@ -58,28 +60,31 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import { reactive, ref, onMounted, nextTick, computed, watch } from "vue"; import { reactive, ref, onMounted, nextTick, computed, watch, onBeforeUnmount } from "vue";
import axios from "@api"; import axios from "@api";
import FullCalendar from "@fullcalendar/vue3"; import FullCalendar from "@fullcalendar/vue3";
import dayGridPlugin from "@fullcalendar/daygrid"; import dayGridPlugin from "@fullcalendar/daygrid";
import interactionPlugin from "@fullcalendar/interaction"; import interactionPlugin from "@fullcalendar/interaction";
// Flatpickr MonthSelect // Flatpickr MonthSelect
import flatpickr from "flatpickr"; import flatpickr from "flatpickr";
import monthSelectPlugin from "flatpickr/dist/plugins/monthSelect/index"; import monthSelectPlugin from "flatpickr/dist/plugins/monthSelect/index";
import "flatpickr/dist/flatpickr.min.css"; import "flatpickr/dist/flatpickr.min.css";
import "flatpickr/dist/plugins/monthSelect/style.css"; import "flatpickr/dist/plugins/monthSelect/style.css";
import "@/assets/css/app-calendar.css"; import "@/assets/css/app-calendar.css";
import "bootstrap-icons/font/bootstrap-icons.css"; import "bootstrap-icons/font/bootstrap-icons.css";
import HalfDayButtons from "@c/button/HalfDayButtons.vue"; import HalfDayButtons from "@c/button/HalfDayButtons.vue";
import ProfileList from "@c/vacation/ProfileList.vue"; import ProfileList from "@c/vacation/ProfileList.vue";
import VacationModal from "@c/modal/VacationModal.vue"; import VacationModal from "@c/modal/VacationModal.vue";
import VacationGrantModal from "@c/modal/VacationGrantModal.vue"; import VacationGrantModal from "@c/modal/VacationGrantModal.vue";
import { useUserStore } from "@s/userList"; import { useUserStore } from "@s/userList";
import { useUserInfoStore } from "@s/useUserInfoStore"; import { useUserInfoStore } from "@s/useUserInfoStore";
import { fetchHolidays } from "@c/calendar/holiday.js"; import { fetchHolidays } from "@c/calendar/holiday.js";
import { useToastStore } from '@s/toastStore'; import { useToastStore } from '@s/toastStore';
import { useRouter } from "vue-router";
const router = useRouter();
const toastStore = useToastStore(); const toastStore = useToastStore();
const userStore = useUserInfoStore(); const userStore = useUserInfoStore();
@ -90,7 +95,7 @@
const receivedVacations = ref([]); const receivedVacations = ref([]);
const isModalOpen = ref(false); const isModalOpen = ref(false);
const remainingVacationData = ref({}); const remainingVacationData = ref({});
const selectedDate = ref(null);
const lastRemainingYear = ref(new Date().getFullYear()); const lastRemainingYear = ref(new Date().getFullYear());
const lastRemainingMonth = ref(String(new Date().getMonth() + 1).padStart(2, "0")); const lastRemainingMonth = ref(String(new Date().getMonth() + 1).padStart(2, "0"));
const isGrantModalOpen = ref(false); const isGrantModalOpen = ref(false);
@ -106,6 +111,39 @@
const fetchedEvents = ref([]); const fetchedEvents = ref([]);
const halfDayButtonsRef = ref(null); const halfDayButtonsRef = ref(null);
//
router.beforeEach((to, from, next) => {
if (hasChanges.value) {
const answer = window.confirm("저장하지 않은 변경 사항이 있습니다. 이동하시겠습니까?");
if (!answer) {
return next(false); //
}
}
next();
});
onBeforeUnmount(() => {
window.removeEventListener("beforeunload", preventUnsavedChanges);
});
function preventUnsavedChanges(event) {
if (hasChanges.value) {
event.preventDefault();
event.returnValue = ""; //
}
}
// `selectedDates`
watch(
() => Array.from(selectedDates.value.keys()), //
(newKeys) => {
if (halfDayButtonsRef.value) {
halfDayButtonsRef.value.resetHalfDay();
}
},
{ deep: true }
);
// ref // ref
const calendarDatepicker = ref(null); const calendarDatepicker = ref(null);
let fpInstance = null; let fpInstance = null;
@ -192,6 +230,7 @@ function handleDateClick(info) {
await fetchRemainingVacation(); await fetchRemainingVacation();
const currentYear = new Date().getFullYear(); const currentYear = new Date().getFullYear();
await fetchVacationHistory(currentYear); await fetchVacationHistory(currentYear);
window.addEventListener("beforeunload", preventUnsavedChanges);
// Flatpickr ( ) // Flatpickr ( )
fpInstance = flatpickr(calendarDatepicker.value, { fpInstance = flatpickr(calendarDatepicker.value, {

View File

@ -13,8 +13,7 @@
<!-- 단어 갯수, 작성하기 --> <!-- 단어 갯수, 작성하기 -->
<div class="mt-4"> <div class="mt-4">
단어 : {{ total }} <WriteButton ref="writeButton" @click="writeStore.toggleItem(999999)" :isToggleEnabled="true"/>
<WriteButton @click="writeStore.toggleItem(999999)" />
</div> </div>
<!-- --> <!-- -->
@ -67,7 +66,7 @@
</template> </template>
<script setup> <script setup>
import { ref, watchEffect, computed, onMounted, getCurrentInstance, toRaw } from 'vue'; import { ref, computed, onMounted, getCurrentInstance, toRaw } from 'vue';
import axios from '@api'; import axios from '@api';
import SearchBar from '@c/search/SearchBar.vue'; import SearchBar from '@c/search/SearchBar.vue';
import WriteButton from '@c/button/WriteBtn.vue'; import WriteButton from '@c/button/WriteBtn.vue';
@ -77,14 +76,11 @@
import DictAlphabetFilter from '@/components/wordDict/DictAlphabetFilter.vue'; import DictAlphabetFilter from '@/components/wordDict/DictAlphabetFilter.vue';
import commonApi from '@/common/commonApi'; import commonApi from '@/common/commonApi';
import { useToastStore } from '@s/toastStore'; import { useToastStore } from '@s/toastStore';
import { useUserInfoStore } from '@s/useUserInfoStore';
import { useWriteVisibleStore } from '@s/writeVisible'; import { useWriteVisibleStore } from '@s/writeVisible';
// //
const writeStore = useWriteVisibleStore(); const writeStore = useWriteVisibleStore();
const writeButton = ref(null);
//
// const userStore = useUserInfoStore();
const { appContext } = getCurrentInstance(); const { appContext } = getCurrentInstance();
const $common = appContext.config.globalProperties.$common; const $common = appContext.config.globalProperties.$common;
@ -107,7 +103,6 @@
}); });
const selectedCategory = ref(''); const selectedCategory = ref('');
const selectCategory = ref('');
// //
const checkedItems = ref([]); const checkedItems = ref([]);
@ -121,9 +116,6 @@
// //
const searchText = ref(''); const searchText = ref('');
//
// const isWriteVisible = ref(false);
// //
onMounted(() => { onMounted(() => {
getwordList(); getwordList();
@ -168,35 +160,9 @@
// //
const handleSelectedCategoryChange = (category) => { const handleSelectedCategoryChange = (category) => {
selectedCategory.value = category; selectedCategory.value = category;
getwordList(searchText.value, selectedAlphabet.value, selectedCategory.value) getwordList(searchText.value, selectedAlphabet.value, selectedCategory.value);
} }
// toggle
// const toggleWriteForm = () => {
// isWriteVisible.value = !isWriteVisible.value;
// };
//
// const addCategory = (data) =>{
// const lastCategory = cateList.value[cateList.value.length - 1];
// const newValue = lastCategory ? lastCategory.value + 1 : 600101;
// const newCategory = { label: data, value: newValue };
// cateList.value = [newCategory, ...cateList.value];
// selectedCategory.value = newCategory.value;
//// axios.post('worddict/insertCategory',{
//// CMNCODNAM: data
//// }).then(res => {
//// if(res.data.data == '1'){
//// toastStore.onToast(' .', 's');
//// const newCategory = { label: data, value: newValue };
//// cateList.value = [newCategory, ...cateList.value];
//// selectedCategory.value = newCategory.value;
//// } else if(res.data.message == ' .') {
//// toastStore.onToast(res.data.message, 'e');
//// }
//// })
// }
// //
const addWord = (wordData, data) => { const addWord = (wordData, data) => {
let category = null; let category = null;
@ -208,9 +174,9 @@
category = existingCategory.label == '' ? wordData.category : existingCategory.value; category = existingCategory.label == '' ? wordData.category : existingCategory.value;
} else { } else {
// //
// console.log(' ');
const lastCategory = cateList.value[cateList.value.length - 1]; const lastCategory = cateList.value[cateList.value.length - 1];
category = lastCategory ? lastCategory.value + 1 : 600101; category = lastCategory ? lastCategory.value + 1 : 600101;
} }
sendWordRequest(category, wordData, data, !existingCategory); sendWordRequest(category, wordData, data, !existingCategory);
}; };
@ -227,8 +193,10 @@
axios.post('worddict/insertWord', payload).then(res => { axios.post('worddict/insertWord', payload).then(res => {
if (res.data.status === 'OK') { if (res.data.status === 'OK') {
toastStore.onToast('용어가 등록 되었습니다.', 's'); toastStore.onToast('용어가 등록 되었습니다.', 's');
// isWriteVisible.value = false;
writeStore.closeAll(); writeStore.closeAll();
if (writeButton.value) {
writeButton.value.resetButton();
}
getwordList(); getwordList();
const newCategory = { label: data, value: category }; const newCategory = { label: data, value: category };
cateList.value = [newCategory, ...cateList.value]; cateList.value = [newCategory, ...cateList.value];
@ -238,8 +206,10 @@
axios.post('worddict/insertWord', payload).then(res => { axios.post('worddict/insertWord', payload).then(res => {
if (res.data.status === 'OK') { if (res.data.status === 'OK') {
toastStore.onToast('용어가 등록 되었습니다.', 's'); toastStore.onToast('용어가 등록 되었습니다.', 's');
// isWriteVisible.value = false;
writeStore.closeAll(); writeStore.closeAll();
if (writeButton.value) {
writeButton.value.resetButton();
}
getwordList(); getwordList();
} }
}); });
@ -256,16 +226,12 @@
checkedItems.value = checkedItems.value.filter(item => item !== id); checkedItems.value = checkedItems.value.filter(item => item !== id);
checkedNames.value = checkedNames.value.filter(item => item !== name); checkedNames.value = checkedNames.value.filter(item => item !== name);
} }
// name
// console.log(" name :", checkedNames.value);
}; };
const isAnyChecked = computed(() => checkedItems.value.length > 0); const isAnyChecked = computed(() => checkedItems.value.length > 0);
// //
const deleteCheckedItems = () => { const deleteCheckedItems = () => {
// console.log(" name :", Object.values(checkedNames.value));
axios.patch('worddict/deleteword', { axios.patch('worddict/deleteword', {
idList: Object.values(checkedNames.value) idList: Object.values(checkedNames.value)
@ -273,7 +239,6 @@
.then(res => { .then(res => {
if (res.data.status == 'OK') { if (res.data.status == 'OK') {
toastStore.onToast('용어 삭제가 완료되었습니다.', 's'); toastStore.onToast('용어 삭제가 완료되었습니다.', 's');
// isWriteVisible.value = false;
writeStore.closeAll(); writeStore.closeAll();
getwordList(); getwordList();
@ -283,7 +248,6 @@
} }
}) })
.catch(error => { .catch(error => {
// console.error(' :', error);
toastStore.onToast('오류가 발생했습니다. 다시 시도해주세요.', 'e'); toastStore.onToast('오류가 발생했습니다. 다시 시도해주세요.', 'e');
}); });