Skip to content
Merged
2 changes: 1 addition & 1 deletion src/api/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export const patchChangeStatus = async (taskID: number, status: Status) => {
return response.data
}

export const changeLabel = async (taskID: number, labelId: number) => {
export const changeLabel = async (taskID: number, labelId: number | null) => {
const response = await axiosInstance.patch(`/api/tasks/${taskID}/label`, { labelId })
return response.data
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/request-approve/LabelDropdown.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
class="absolute w-full h-40 overflow-y-auto scrollbar-hide top-[52px] flex flex-col gap-2 p-2 bg-white rounded z-10 shadow border-t border-t-border-2">
<div
v-for="option in labelArr"
:key="option.labelId"
:key="option.labelId || option.labelName"
class="w-full flex items-center h-11 p-2 rounded hover:bg-background-2 cursor-pointer"
@click="selectOption(option)">
{{ option.labelName }}
Expand Down
7 changes: 4 additions & 3 deletions src/components/task-detail/TaskDetail.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,15 @@
</div>
<div class="w-[1px] bg-border-1"></div>
<TaskDetailRight
:data
:data="data"
:isProcessor="info.role !== 'ROLE_USER'" />
</div>
</div>
</template>

<script setup lang="ts">
import { getHistory, getTaskDetailManager, getTaskDetailUser } from '@/api/user'
import { useIsOverlayOpenStore } from '@/stores/isOverlayOpen'
import { useMemberStore } from '@/stores/member'
import type { TaskDetailDatas, TaskDetailHistoryData, TaskDetailProps } from '@/types/user'
import { useQuery } from '@tanstack/vue-query'
Expand All @@ -41,7 +42,6 @@ import TaskDetailHistory from './TaskDetailHistory.vue'
import TaskDetailLeft from './TaskDetailLeft.vue'
import TaskDetailRight from './TaskDetailRight.vue'
import TaskDetailTopBar from './TaskDetailTopBar.vue'
import { useIsOverlayOpenStore } from '@/stores/isOverlayOpen'

const { closeTaskDetail, selectedId } = defineProps<TaskDetailProps>()

Expand All @@ -54,7 +54,8 @@ const { data } = useQuery<TaskDetailDatas>({
info.value.role === 'ROLE_USER'
? () => getTaskDetailUser(selectedId)
: () => getTaskDetailManager(selectedId),
refetchOnMount: true
refetchOnMount: 'always',
staleTime: 0
})

const { data: historyData } = useQuery<TaskDetailHistoryData>({
Expand Down
9 changes: 7 additions & 2 deletions src/components/task-detail/TaskDetailLabelDropdown.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,14 @@
class="absolute w-full pb-6 top-12">
<div
class="w-full h-32 overflow-y-auto flex flex-col gap-2 p-2 bg-white rounded z-10 shadow-custom">
<div
class="w-full flex text-sm items-center h-10 p-1.5 rounded hover:bg-background-2 cursor-pointer text-disabled"
@click="selectOption({ labelId: null, labelName: '', labelColor: '' })">
구분 없음
</div>
<div
v-for="option in labelArr"
:key="option.labelId"
:key="option.labelId || option.labelName"
class="w-full flex text-sm items-center h-10 p-1.5 rounded hover:bg-background-2 cursor-pointer"
@click="selectOption(option)">
{{ option.labelName }}
Expand Down Expand Up @@ -55,7 +60,7 @@ const toggleDropdown = () => {

const selectOption = async (option: LabelDataTypes) => {
emit('update:modelValue', option)
await changeLabel(taskId || 0, option.labelId || 0)
await changeLabel(taskId || 0, option.labelId || null)
queryClient.invalidateQueries({ queryKey: ['taskDetailUser', taskId] })
dropdownOpen.value = false
}
Expand Down
1 change: 0 additions & 1 deletion src/components/task-detail/TaskDetailManagerDropdown.vue
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ import ImageContainer from '../common/ImageContainer.vue'

const { modelValue } = defineProps<{ modelValue: ManagerTypes; taskId: number }>()
const emit = defineEmits(['update:modelValue'])
console.log(modelValue, '현재 담당자')

const dropdownOpen = ref(false)
const managerArr = ref<ManagerTypes[]>([])
Expand Down
39 changes: 27 additions & 12 deletions src/components/task-detail/TaskDetailRight.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
class="w-fit">
<TaskStatus :status="data.taskStatus" />
</div>
<div v-else>
<div v-else-if="taskStatus">
<TaskStatusList
v-model="taskStatus"
:isProcessor="info.isReviewer || isProcessor"
Expand All @@ -40,7 +40,7 @@
</div>
<div>
<p class="task-detail">담당자</p>
<div v-if="data.taskStatus !== 'REQUESTED' && isProcessor">
<div v-if="data.taskStatus !== 'REQUESTED' && isProcessor && newManager">
<TaskDetailManagerDropdown
v-model="newManager"
:task-id="data.taskId" />
Expand All @@ -55,7 +55,7 @@
<p class="text-sm">{{ data.processorNickName || '-' }}</p>
</div>
</div>
<div v-if="data.taskStatus !== 'REQUESTED' && info.isReviewer">
<div v-if="data.taskStatus !== 'REQUESTED' && info.role === 'ROLE_MANAGER'">
<p class="task-detail">마감기한</p>
<div v-if="data.dueDate">
<div class="w-full flex justify-between items-center">
Expand All @@ -65,7 +65,7 @@
</div>
<div v-else>-</div>
</div>
<div v-if="data.taskStatus !== 'REQUESTED' && info.isReviewer">
<div v-if="data.taskStatus !== 'REQUESTED' && info.role === 'ROLE_MANAGER'">
<p class="task-detail">구분</p>
<TaskDetailLabelDropdown
v-model="taskLabel"
Expand All @@ -78,6 +78,7 @@
<script setup lang="ts">
import { changeProcessor } from '@/api/user'
import { useMemberStore } from '@/stores/member'
import type { Status } from '@/types/common'
import type { ManagerTypes } from '@/types/manager'
import type { TaskDetailDatas } from '@/types/user'
import { formatDateAndTime, formatDaysBefore, formatDueDate } from '@/utils/date'
Expand All @@ -92,27 +93,41 @@ import TaskStatusList from './TaskStatusList.vue'

const { data, isProcessor } = defineProps<{ data: TaskDetailDatas; isProcessor: boolean }>()

const selectedManager = ref({
const selectedManager = ref<ManagerTypes>({
memberId: -1,
nickname: data.processorNickName,
imageUrl: data.processorImageUrl,
nickname: '',
imageUrl: '',
remainingTasks: -1
} as ManagerTypes)
})

const taskStatus = ref(data.taskStatus)
const newManager = ref(selectedManager.value)
const taskStatus = ref<Status | null>(null)
const newManager = ref<ManagerTypes | null>(null)
const queryClient = useQueryClient()
const memberStore = useMemberStore()
const { info } = storeToRefs(memberStore)

const taskLabel = ref({
labelId: -1,
labelName: data.labelName || '',
labelName: '',
labelColor: ''
})

watchEffect(() => {
taskStatus.value = data.taskStatus
if (data) {
taskStatus.value = data.taskStatus
selectedManager.value = {
memberId: -1,
nickname: data.processorNickName || '',
imageUrl: data.processorImageUrl || '',
remainingTasks: -1
}
newManager.value = { ...selectedManager.value }
taskLabel.value = {
labelId: -1,
labelName: data.labelName || '',
labelColor: ''
}
}
})

watch(newManager, async newValue => {
Expand Down
37 changes: 25 additions & 12 deletions src/components/task-management/CategoryAdd.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,23 +26,34 @@
v-model="categoryForm.name"
placeholder-text="카테고리명을 입력해주세요"
:label-name="`${categoryStep}차 카테고리명`"
:is-invalidate="errorMessage.categoryName" />
:is-invalidate="errorMessage.categoryName"
:limitLength="30" />
<RequestTaskInput
v-model="categoryForm.code"
placeholder-text="카테고리의 작업코드를 입력해주세요"
label-name="작업코드 (대문자 영어 2글자까지)"
:is-invalidate="errorMessage.categoryCode === 'noCode' ? 'noCode' : isCodeInvalidate" />

<div
v-if="categoryStep === '2'"
class="flex flex-col gap-2">
<p class="text-body text-xs font-semibold">부가설명 템플릿</p>
class="flex flex-col gap-2 relative">
<div class="flex gap-1 text-xs">
<p class="text-body font-semibold">부가설명 템플릿</p>
<p
class="text-red-1"
v-if="errorMessage.description === 'tooLong'">
템플릿은 100자 이내로 적어주세요
</p>
</div>
<textarea
class="w-full h-32 border border-border-1 px-4 py-2 resize-none focus:outline-none rounded"
:value="categoryForm.descriptionExample"
:maxlength="100"
:placeholder="'부가설명 템플릿을 작성해주세요'"
@input="onValueChange">
</textarea>
<p class="absolute text-xs top-[calc(100%+4px)] w-full flex justify-end text-body">
{{ categoryForm.descriptionExample?.length || 0 }}/{{ 100 }}
</p>
</div>

<FormButtonContainer
Expand All @@ -54,17 +65,17 @@
</template>

<script lang="ts" setup>
import { getMainCategory } from '@/api/common'
import { CATEGORY_FORM } from '@/constants/admin'
import type { Category, CategoryForm } from '@/types/common'
import { axiosInstance } from '@/utils/axios'
import DOMPurify from 'dompurify'
import { computed, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import FormButtonContainer from '../common/FormButtonContainer.vue'
import ModalView from '../common/ModalView.vue'
import RequestTaskDropdown from '../request-task/RequestTaskDropdown.vue'
import RequestTaskInput from '../request-task/RequestTaskInput.vue'
import { axiosInstance } from '@/utils/axios'
import { getMainCategory } from '@/api/common'
import type { Category, CategoryForm } from '@/types/common'
import ModalView from '../common/ModalView.vue'
import DOMPurify from 'dompurify'

const router = useRouter()
const route = useRoute()
Expand All @@ -74,11 +85,10 @@ const { categoryStep } = defineProps<{
}>()

const isModalVisible = ref({ add: false, cancel: false, fail: false })
const errorMessage = ref({ categoryName: '', categoryCode: '' })
const errorMessage = ref({ categoryName: '', categoryCode: '', description: '' })
const hasMainCategory = ref(true)

const categoryForm = ref<CategoryForm>(CATEGORY_FORM)

const handleAddModal = () => {
isModalVisible.value.add = false
handleGoBack()
Expand All @@ -97,7 +107,7 @@ const handleGoBack = () => {

const handleSubmit = async () => {
hasMainCategory.value = true
errorMessage.value = { categoryCode: '', categoryName: '' }
errorMessage.value = { categoryCode: '', categoryName: '', description: '' }
if (!categoryForm.value.mainCategoryId && categoryStep === '2') {
hasMainCategory.value = false
return
Expand All @@ -110,6 +120,9 @@ const handleSubmit = async () => {
} else if (categoryForm.value.code.length === 0) {
errorMessage.value.categoryCode = 'noCode'
return
} else if ((categoryForm.value.descriptionExample ?? '').length > 100) {
errorMessage.value.description = 'tooLong'
return
}

categoryForm.value.name = DOMPurify.sanitize(categoryForm.value.name)
Expand Down
10 changes: 7 additions & 3 deletions src/components/user-manage/UserRegistration.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
? 'empty'
: isInvalidate === 'wrongNickname'
? isInvalidate
: ''
: isInvalidate === 'duplicate'
? isInvalidate
: ''
"
:placeholderText="'회원의 아이디를 입력해주세요'"
:labelName="'아이디'" />
Expand All @@ -34,7 +36,9 @@
</div>
<DepartmentDropDown
v-model="userRegistrationForm.department"
:is-invalidate="isInvalidate === 'departmentEmpty' ? isInvalidate : ''" />
:is-invalidate="
isInvalidate === 'departmentEmpty' || isInvalidate === 'reviewer' ? isInvalidate : ''
" />
<RequestTaskDropdown
v-model="userRegistrationForm.role"
:options="filteredRoleKeys"
Expand Down Expand Up @@ -69,6 +73,7 @@
<script lang="ts" setup>
import { addMemberAdmin } from '@/api/admin'
import { INITIAL_USER_REGISTRATION, RoleKeys, RoleTypeMapping } from '@/constants/admin'
import DOMPurify from 'dompurify'
import { computed, onMounted, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import FormButtonContainer from '../common/FormButtonContainer.vue'
Expand All @@ -77,7 +82,6 @@ import ModalView from '../common/ModalView.vue'
import RequestTaskDropdown from '../request-task/RequestTaskDropdown.vue'
import RequestTaskInput from '../request-task/RequestTaskInput.vue'
import DepartmentDropDown from './DepartmentDropDown.vue'
import DOMPurify from 'dompurify'

const router = useRouter()

Expand Down
4 changes: 3 additions & 1 deletion src/components/user-manage/UserUpdate.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
</div>
<DepartmentDropDown
v-model="userRegistrationForm.department"
:is-invalidate="isInvalidate === 'departmentEmpty' ? isInvalidate : ''" />
:is-invalidate="
isInvalidate === 'departmentEmpty' || isInvalidate === 'reviewer' ? isInvalidate : ''
" />
<RequestTaskDropdown
v-model="userRegistrationForm.role"
:options="filteredRoleKeys"
Expand Down
2 changes: 1 addition & 1 deletion src/types/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export interface CategoryDropdownProps {
}

export interface LabelDataTypes {
labelId: number
labelId: number | null
labelName: string
labelColor: string
}
Expand Down
2 changes: 2 additions & 0 deletions src/utils/axios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ const setInterceptors = (instance: AxiosInstance) => {
setError('중복된 카테고리명\n혹은 고유코드입니다')
} else if (error.response.data === 'MEMBER_006') {
setError('비밀번호가 일치하지 않습니다', '다시 시도해주세요')
} else if (error.response.data === 'LABEL_002') {
setError('중복된 구분 이름입니다')
} else if (error.response.data === 'MEMBER_007') {
return Promise.reject(new Error('WRONG_FILETYPE'))
} else if (error.response.data === 'MEMBER_012') {
Expand Down