Skip to content

Commit eaf28bf

Browse files
authored
Merge pull request #33 from TaskFlow-CLAP/CLAP-143
CLAP-143 목록 페이지 UI 구현
2 parents b4b0252 + 8cb334c commit eaf28bf

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+1879
-108
lines changed

src/components/ModalView.vue

Lines changed: 44 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -3,42 +3,38 @@
33
v-if="isOpen"
44
class="fixed inset-0 bg-black bg-opacity-15 flex justify-center items-center z-50"
55
@click.self="closeModal">
6-
<div class="bg-white rounded-lg shadow-lg px-8 py-8 min-w-[364px]">
7-
<div class="mb-2 min-h-16">
8-
<div
9-
v-if="type == 'successType'"
10-
class="flex ic justify-center">
11-
<CommonIcons :name="successIcon" />
12-
</div>
13-
<div
14-
v-if="type == 'failType' || type == 'inputType'"
15-
class="flex ic justify-center">
16-
<CommonIcons :name="failIcon" />
17-
</div>
18-
<div
19-
v-if="type == 'warningType'"
20-
class="flex ic justify-center">
21-
<CommonIcons :name="warningIcon" />
22-
</div>
23-
</div>
6+
<div class="bg-white rounded-lg shadow-lg px-8 py-8">
7+
<div class="flex flex-col gap-8 w-[300px]">
8+
<div class="flex flex-col gap-6">
9+
<div class="flex flex-col items-center gap-2">
10+
<CommonIcons
11+
v-if="type == 'successType'"
12+
:name="successIcon" />
13+
<CommonIcons
14+
v-if="type == 'failType' || type == 'inputType'"
15+
:name="failIcon" />
16+
<CommonIcons
17+
v-if="type == 'warningType'"
18+
:name="warningIcon" />
2419

25-
<div>
26-
<div class="flex text-2xl font-bold justify-center mb-2">
27-
<slot name="header"></slot>
28-
</div>
29-
<div
30-
v-if="type != 'inputType'"
31-
class="flex text-sm font-bold text-body justify-center">
32-
<slot name="body"></slot>
20+
<div class="flex text-2xl font-bold justify-center">
21+
<slot name="header"></slot>
22+
</div>
23+
24+
<div
25+
v-if="type != 'inputType'"
26+
class="flex text-sm font-bold text-body justify-center">
27+
<slot name="body"></slot>
28+
</div>
29+
</div>
30+
31+
<textarea
32+
v-if="type == 'inputType'"
33+
v-model="textValue"
34+
placeholder="거부 사유를 입력해주세요"
35+
class="flex border w-full border-zinc-300 px-4 py-3 focus:outline-none resize-none h-[120px]" />
3336
</div>
34-
</div>
3537

36-
<textarea
37-
v-if="type == 'inputType'"
38-
v-model="textValue"
39-
placeholder="거부 사유를 입력해주세요"
40-
class="flex border w-full border-zinc-300 px-4 py-3 focus:outline-none resize-none mt-6 h-[120px]" />
41-
<div class="mt-8">
4238
<button
4339
class="button-large-primary"
4440
v-if="type == 'successType'"
@@ -54,22 +50,18 @@
5450
</button>
5551

5652
<div
57-
class="flex mt-8 items-center"
53+
class="flex items-center gap-6"
5854
v-if="type == 'warningType' || type == 'inputType'">
59-
<div class="mr-6">
60-
<button
61-
class="button-large-default"
62-
@click="closeModal">
63-
취소
64-
</button>
65-
</div>
66-
<div>
67-
<button
68-
class="button-large-red"
69-
@click="closeModal">
70-
{{ type === 'inputType' ? '거부' : '삭제' }}
71-
</button>
72-
</div>
55+
<button
56+
class="button-large-default"
57+
@click="closeModal">
58+
취소
59+
</button>
60+
<button
61+
class="button-large-red"
62+
@click="confirmModal">
63+
{{ type === 'inputType' ? '거부' : '삭제' }}
64+
</button>
7365
</div>
7466
</div>
7567
</div>
@@ -102,4 +94,8 @@ watch(textValue, newValue => {
10294
const closeModal = () => {
10395
emit('close')
10496
}
97+
98+
const confirmModal = () => {
99+
emit('click')
100+
}
105101
</script>

src/components/TitleBar.vue

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,17 @@
11
<template>
22
<div class="flex justify-between items-center px-6 py-3 border-l-8 border-primary1">
3-
<span class="text-2xl font-bold text-black">{{ props.title }}</span>
4-
<button
5-
v-if="btn"
6-
class="flex items-center gap-1 text-xs font-bold text-primary1"
7-
@click="$emit('buttonClick')">
8-
<CommonIcons
9-
:name="plusIcon"
10-
:style="{ fill: '#7879EB' }" />
11-
{{ props.btn }}
12-
</button>
3+
<span class="text-2xl font-bold text-black">{{ title }}</span>
4+
<div
5+
v-if="$slots.button"
6+
class="flex gap-4">
7+
<slot
8+
name="button"
9+
class="flex gap-4" />
10+
</div>
1311
</div>
1412
</template>
1513

1614
<script setup lang="ts">
17-
import { plusIcon } from '@/constants/iconPath'
18-
import type { TitleBarProps } from '@/types/common'
19-
import CommonIcons from './common/CommonIcons.vue'
20-
21-
const props = defineProps<TitleBarProps>()
15+
const { title } = defineProps<{ title: string }>()
2216
defineEmits(['buttonClick'])
2317
</script>
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<template>
2+
<div class="flex gap-4">
3+
<FilterDropdown
4+
title="조회 기간"
5+
:option-list="TERM_LIST"
6+
:value="String(store.params.term)"
7+
@update:value="onParamsChange.onTermChange" />
8+
<FilterDropdown
9+
title="구분"
10+
:option-list="LOGIN_LOGS_DIVISION_LIST"
11+
:value="store.params.division"
12+
@update:value="onParamsChange.onDivisionChange" />
13+
<FilterInput
14+
title="아이디"
15+
width="full"
16+
:value="store.params.nickName"
17+
@update:value="onParamsChange.onNickNameChange" />
18+
<FilterIpAddress @update:value="onParamsChange.onIpAddressChange" />
19+
<FilterDropdown
20+
title="페이지 당 개수"
21+
:option-list="PAGE_SIZE_LIST"
22+
:value="String(store.params.pageSize)"
23+
@update:value="onParamsChange.onPageSizeChange" />
24+
</div>
25+
</template>
26+
27+
<script setup lang="ts">
28+
import FilterDropdown from '../filters/FilterDropdown.vue'
29+
import FilterInput from '../filters/FilterInput.vue'
30+
import { useLogsParamsStore } from '@/stores/params'
31+
import { PAGE_SIZE_LIST, TERM_LIST } from '@/constants/common'
32+
import { LOGIN_LOGS_DIVISION_LIST } from '@/constants/admin'
33+
import { useLogsParamsChange } from '../hooks/useLogsParamsChange'
34+
import FilterIpAddress from '../filters/FilterIpAddress.vue'
35+
36+
const store = useLogsParamsStore()
37+
store.$reset()
38+
39+
const onParamsChange = useLogsParamsChange()
40+
</script>
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<template>
2+
<ListContainer>
3+
<template #listBar>
4+
<ApiLogsListBar />
5+
</template>
6+
7+
<template #listCards>
8+
<ApiLogsListCard
9+
v-for="info in DUMMY_API_LOGS_LIST_DATA"
10+
:key="info.logId"
11+
:info="info" />
12+
</template>
13+
14+
<template #pagination>
15+
<ListPagination
16+
:page-number="params.page"
17+
:total-page="DUMMY_TOTAL_PAGE"
18+
@update:page-number="onPageChange" />
19+
</template>
20+
</ListContainer>
21+
</template>
22+
23+
<script setup lang="ts">
24+
import ListPagination from '../lists/ListPagination.vue'
25+
import ListContainer from '../lists/ListContainer.vue'
26+
import { useRequestParamsStore } from '@/stores/params'
27+
import ApiLogsListBar from './ApiLogsListBar.vue'
28+
import ApiLogsListCard from './ApiLogsListCard.vue'
29+
import { DUMMY_API_LOGS_LIST_DATA } from '@/datas/dummy'
30+
31+
const { params } = useRequestParamsStore()
32+
const DUMMY_TOTAL_PAGE = 18
33+
const onPageChange = (value: number) => {
34+
params.page = value
35+
}
36+
37+
// Data Handling
38+
</script>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<template>
2+
<div class="list-bar">
3+
<ListBarTab
4+
v-for="tab in LOGS_LIST_BAR_TAB"
5+
:key="tab.content"
6+
:content="tab.content"
7+
:width="tab.width"
8+
:sortBy="tab.sortBy"
9+
:current-order-request="params.orderRequest"
10+
@toggle-sort-by="toggleSortBy" />
11+
</div>
12+
</template>
13+
14+
<script setup lang="ts">
15+
import { useLogsParamsStore } from '@/stores/params'
16+
import ListBarTab from '../lists/ListBarTab.vue'
17+
import { LOGS_LIST_BAR_TAB } from '@/constants/admin'
18+
import { useLogsParamsChange } from '../hooks/useLogsParamsChange'
19+
20+
const { params } = useLogsParamsStore()
21+
22+
const { toggleSortBy } = useLogsParamsChange()
23+
</script>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<template>
2+
<div class="list-card">
3+
<ListCardTab
4+
v-for="tab in myRequestTabList"
5+
:key="tab.content"
6+
:content="tab.content"
7+
:width="tab.width"
8+
:is-text-xs="tab.isTextXs"
9+
:is-status="tab.isStatus"
10+
:is-status-code="tab.isStatusCode"
11+
:is-text-body="tab.isTextBody" />
12+
</div>
13+
</template>
14+
15+
<script setup lang="ts">
16+
import type { ListCardProps } from '@/types/common'
17+
import ListCardTab from '../lists/ListCardTab.vue'
18+
import type { LogsListData } from '@/types/admin'
19+
20+
const { info } = defineProps<{ info: LogsListData }>()
21+
const myRequestTabList: ListCardProps[] = [
22+
{ content: info.division, width: 80, isTextXs: true, isTextBody: true },
23+
{ content: info.createdAt, width: 180, isTextXs: true },
24+
{ content: info.nickName, width: 80 },
25+
{ content: info.ipAddress, width: 120, isTextXs: true },
26+
{ content: String(info.status), width: 40, isTextXs: true, isStatusCode: true },
27+
{ content: info.result }
28+
]
29+
</script>

src/components/filters/FilterInput.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<span class="filter-title">{{ title }}</span>
77
<input
88
@input="onValueChange"
9-
class="w-full h-8 border-b border-border-1 focus:outline-none text-xs text-black px-2" />
9+
class="w-full h-8 border-b border-border-1 outline-none text-xs text-black px-2" />
1010
</div>
1111
</template>
1212

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<template>
2+
<div class="filter-container w-36">
3+
<span class="filter-title">IP 주소</span>
4+
<div
5+
class="w-full h-8 border-b border-border-1 text-xs text-black flex items-center gap-1 px-2">
6+
<template
7+
v-for="(block, index) in blocks"
8+
:key="index">
9+
<input
10+
:ref="el => (inputRefs[index] = el as HTMLInputElement)"
11+
class="w-full grow outline-none text-center"
12+
v-model="blocks[index]"
13+
:maxlength="3"
14+
@input="onValueChange(index)"
15+
@keydown="event => onKeyDown(event, index)"
16+
@focus="onFocus(index)" />
17+
<span v-if="index !== 3">.</span>
18+
</template>
19+
</div>
20+
</div>
21+
</template>
22+
23+
<script setup lang="ts">
24+
import { ref } from 'vue'
25+
26+
const emit = defineEmits(['update:value'])
27+
28+
const blocks = ref(['', '', '', ''])
29+
const inputRefs = ref<(HTMLInputElement | null)[]>([])
30+
31+
const onValueChange = (index: number) => {
32+
const value = blocks.value[index].replace(/\D/g, '')
33+
blocks.value[index] = Number(value) > 255 ? '255' : value
34+
35+
if (value.length === 3 && index < 3) {
36+
inputRefs.value[index + 1]?.focus()
37+
}
38+
39+
if (value.length === 0) {
40+
for (let i = index + 1; i <= 3; i++) {
41+
blocks.value[i] = ''
42+
}
43+
}
44+
45+
setTimeout(() => {
46+
emit('update:value', blocks.value.filter(el => el !== '').join('.'))
47+
}, 500)
48+
}
49+
50+
const onKeyDown = (event: KeyboardEvent, index: number) => {
51+
if (event.key === 'Backspace' && blocks.value[index] === '' && index > 0) {
52+
event.preventDefault()
53+
blocks.value[index - 1] = blocks.value[index - 1].slice(0, -1)
54+
inputRefs.value[index - 1]?.focus()
55+
return
56+
}
57+
58+
if (event.key === 'ArrowLeft' && inputRefs.value[index]?.selectionStart === 0 && index > 0) {
59+
event.preventDefault()
60+
inputRefs.value[index - 1]?.focus()
61+
inputRefs.value[index - 1]?.setSelectionRange(
62+
blocks.value[index - 1].length,
63+
blocks.value[index - 1].length
64+
)
65+
return
66+
}
67+
68+
if (
69+
event.key === 'ArrowRight' &&
70+
inputRefs.value[index]?.selectionEnd === blocks.value[index].length &&
71+
index < 3
72+
) {
73+
event.preventDefault()
74+
inputRefs.value[index + 1]?.focus()
75+
inputRefs.value[index + 1]?.setSelectionRange(0, 0)
76+
}
77+
}
78+
79+
const onFocus = (index: number) => {
80+
for (let i = index - 1; i >= 0; i--) {
81+
if (blocks.value[i] === '') {
82+
inputRefs.value[i]?.focus()
83+
break
84+
}
85+
}
86+
}
87+
</script>

0 commit comments

Comments
 (0)