Merge branch 'feature/20251031_jo_FX_COUPON_01' into 'feature/202501107-all'
feat: 쿠폰등록 See merge request sgp-web-d/web-template-fe!5
This commit is contained in:
4
.env.dev
4
.env.dev
@@ -15,6 +15,10 @@ STOVE_M_API_URL=https://maintenance.gate8.com
|
||||
# STOVE - GNB
|
||||
STOVE_GNB=https://js-cdn-dev.onstove.com/libs/common-gnb/latest/stove-gnb.js
|
||||
|
||||
# STOVE - Coupon
|
||||
STOVE_COUPON_URL=https://bill-dev.onstove.com/History/UserCouponList.aspx
|
||||
STOVE_M_COUPON_URL=https://bill-dev.onstove.com/g-coupon/RegCouponM.aspx
|
||||
|
||||
# STOVE - Client Download
|
||||
STOVE_LAUNCHER_SCRIPT=https://js-cdn.gate8.com/libs/stove-js-service/latest/launcher-pack.js
|
||||
STOVE_CLIENT_DOWNLOAD_URL=https://sgs-gate8-dl.game.playstove.com/game/lcs/STOVESetup.exe
|
||||
|
||||
@@ -15,6 +15,10 @@ STOVE_M_API_URL=https://maintenance.onstove.com
|
||||
# STOVE - GNB
|
||||
STOVE_GNB=https://js-cdn.onstove.com/libs/common-gnb/latest/stove-gnb.js
|
||||
|
||||
# STOVE - Coupon
|
||||
STOVE_COUPON_URL=https://bill-dev.onstove.com/History/UserCouponList.aspx
|
||||
STOVE_M_COUPON_URL=https://bill-dev.onstove.com/g-coupon/RegCouponM.aspx
|
||||
|
||||
# STOVE - Client Download
|
||||
STOVE_LAUNCHER_SCRIPT=https://js-cdn.onstove.com/libs/stove-js-service/latest/launcher-pack.js
|
||||
STOVE_CLIENT_DOWNLOAD_URL=https://sgs-live-dl.game.playstove.com/game/lcs/STOVESetup.exe
|
||||
|
||||
4
.env.qa
4
.env.qa
@@ -15,6 +15,10 @@ STOVE_M_API_URL=https://maintenance.gate8.com
|
||||
# STOVE - GNB
|
||||
STOVE_GNB=https://js-cdn-qa.onstove.com/libs/common-gnb/latest/stove-gnb.js
|
||||
|
||||
# STOVE - Coupon
|
||||
STOVE_COUPON_URL=https://bill-qa.onstove.com/History/UserCouponList.aspx
|
||||
STOVE_M_COUPON_URL=https://bill-qa.onstove.com/g-coupon/RegCouponM.aspx
|
||||
|
||||
# STOVE - Client Download
|
||||
STOVE_LAUNCHER_SCRIPT=https://js-cdn.gate8.com/libs/stove-js-service/latest/launcher-pack.js
|
||||
STOVE_CLIENT_DOWNLOAD_URL=https://sgs-gate8-dl.game.playstove.com/game/lcs/STOVESetup.exe
|
||||
|
||||
@@ -15,6 +15,10 @@ STOVE_M_API_URL=https://maintenance.gate8.com
|
||||
# STOVE - GNB
|
||||
STOVE_GNB=https://js-cdn.gate8.com/libs/common-gnb/latest/stove-gnb.js
|
||||
|
||||
# STOVE - Coupon
|
||||
STOVE_COUPON_URL=https://bill-qa.onstove.com/History/UserCouponList.aspx
|
||||
STOVE_M_COUPON_URL=https://bill-qa.onstove.com/g-coupon/RegCouponM.aspx
|
||||
|
||||
# STOVE - Client Download
|
||||
STOVE_LAUNCHER_SCRIPT=https://js-cdn.gate8.com/libs/stove-js-service/latest/launcher-pack.js
|
||||
STOVE_CLIENT_DOWNLOAD_URL=https://sgs-gate8-dl.game.playstove.com/game/lcs/STOVESetup.exe
|
||||
|
||||
58
layers/components/atoms/Input.vue
Normal file
58
layers/components/atoms/Input.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
modelValue: string
|
||||
placeholder?: string
|
||||
useClearButton?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
useClearButton: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'input', 'keydown', 'clear'])
|
||||
|
||||
const localValue = computed({
|
||||
get: () => props.modelValue,
|
||||
set: value => emit('update:modelValue', value),
|
||||
})
|
||||
|
||||
const onClickClearButton = () => {
|
||||
localValue.value = ''
|
||||
emit('clear')
|
||||
}
|
||||
|
||||
const onInput = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement | null
|
||||
if (target) {
|
||||
localValue.value = target.value
|
||||
}
|
||||
emit('input')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="group relative w-full">
|
||||
<input
|
||||
v-model="localValue"
|
||||
:type="typeof $attrs.type === 'string' ? $attrs.type : 'text'"
|
||||
:placeholder="props.placeholder"
|
||||
v-bind="$attrs"
|
||||
class="relative w-full h-[48px] px-[12px] outline-none border border-solid border-[1px] border-[#D9D9D9] rounded-[8px] bg-white text-left text-[#333333] text-[14px] font-[400] leading-[20px] tracking-[-0.42px] placeholder:text-[#B2B2B2] md:h-[56px] md:px-[16px] md:text-[16px] md:leading-[26px] md:tracking-[-0.48px] hover:[&:not([readonly])]:border-[#999999] focus:border-[#999999]"
|
||||
@input="onInput"
|
||||
@keydown="emit('keydown', $event)"
|
||||
/>
|
||||
<AtomsButton
|
||||
v-if="props.useClearButton && localValue.length > 0"
|
||||
type="action"
|
||||
button-size="size-small"
|
||||
background-color="#00000000"
|
||||
text-color="transparent"
|
||||
class="!absolute top-[50%] right-[12px] translate-y-[-50%] flex items-center justify-center w-auto h-auto p-0 md:right-[16px]"
|
||||
@click="onClickClearButton"
|
||||
>
|
||||
<AtomsIconsCloseCircleFill :size="16" color="rgba(0,0,0,0.15)" />
|
||||
</AtomsButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
99
layers/components/atoms/Select.vue
Normal file
99
layers/components/atoms/Select.vue
Normal file
@@ -0,0 +1,99 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
options: Array<any>
|
||||
labelName: string | number
|
||||
modelValue?: object
|
||||
placeholder?: string
|
||||
defaultLabel?: string
|
||||
placement?: string | 'top' | 'bottom'
|
||||
className?: string
|
||||
selectedColor?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: null,
|
||||
placeholder: null,
|
||||
defaultLabel: null,
|
||||
placement: 'bottom',
|
||||
className: '',
|
||||
selectedColor: '#333333',
|
||||
})
|
||||
|
||||
const emits = defineEmits(['update:modelValue'])
|
||||
|
||||
const isActive = ref(false)
|
||||
const isSelected = ref(props.defaultLabel !== null)
|
||||
const selectedOption = ref<string>(props.placeholder || props.defaultLabel)
|
||||
|
||||
const onToggleOpen = () => {
|
||||
isActive.value = !isActive.value
|
||||
}
|
||||
|
||||
const onSelectOption = (option: { [key: string | number]: any }): void => {
|
||||
isActive.value = false
|
||||
isSelected.value = true
|
||||
selectedOption.value = option[props.labelName]
|
||||
emits('update:modelValue', option)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative w-full max-w-[432px]">
|
||||
<button
|
||||
type="button"
|
||||
class="relative flex items-center justify-between gap-[12px] w-full py-[12px] px-[16px] bg-white border border-solid border-[1px] border-[#D9D9D9] rounded-[8px]"
|
||||
@click="onToggleOpen"
|
||||
>
|
||||
<span
|
||||
class="inline-flex items-center justify-left text-left text-[14px] font-[400] leading-[20px] tracking-[-0.42px]"
|
||||
:class="isSelected ? 'text-[#333333]' : 'text-[#999999]'"
|
||||
>
|
||||
{{ selectedOption }}
|
||||
</span>
|
||||
<i
|
||||
class="inline-flex items-center justify-center w-[14px] h-[14px] shrink-0"
|
||||
>
|
||||
<AtomsIconsSelectArrowDownFill
|
||||
:size="12"
|
||||
color="#333333"
|
||||
:class="isActive ? 'rotate-180' : ''"
|
||||
/>
|
||||
</i>
|
||||
</button>
|
||||
<div
|
||||
v-if="isActive"
|
||||
:data-placement="props.placement"
|
||||
class="absolute z-[10] top-full left-0 translate-y-[4px] w-full py-[8px] border border-solid border-[1px] border-[rgba(0,0,0,0.3)] rounded-[8px] bg-white shadow-[0_4px_10px_0_rgba(0,0,0,0.10)]"
|
||||
>
|
||||
<ul class="relative flex flex-col items-center justify-start w-full">
|
||||
<li
|
||||
v-for="(option, index) in props.options"
|
||||
:key="String(option[props.labelName])"
|
||||
class="relative flex items-center justify-left w-full"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="relative flex items-center justify-left w-full py-[8px] pl-[40px] pr-[16px] bg-white text-left text-left text-[14px] font-[400] leading-[24px] tracking-[-0.42px] hover:bg-[rgba(0,0,0,0.04)]"
|
||||
:class="
|
||||
selectedColor ? `text-[${selectedColor}]` : 'text-[#333333]'
|
||||
"
|
||||
@click="onSelectOption(option)"
|
||||
>
|
||||
<template v-if="option[props.labelName] === selectedOption">
|
||||
<i
|
||||
class="absolute top-1/2 left-[12px] translate-y-[-50%] flex items-center justify-center w-[20px] h-[20px]"
|
||||
>
|
||||
<AtomsIconsCheckLine :size="16" :color="selectedColor" />
|
||||
</i>
|
||||
</template>
|
||||
<span>
|
||||
{{ option[props.labelName] }}
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
34
layers/components/atoms/icons/ArrowDoubleLeftLine.vue
Normal file
34
layers/components/atoms/icons/ArrowDoubleLeftLine.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
size?: number | string
|
||||
color?: string
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
size: 32,
|
||||
color: 'var(--foreground-gray-500)',
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
:width="size"
|
||||
:height="size"
|
||||
viewBox="0 0 11 11"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M5.80474 1.13807C6.06509 0.877722 6.06509 0.455612 5.80474 0.195262C5.54439 -0.0650874 5.12228 -0.0650874 4.86193 0.195262L0.195262 4.86193C-0.0650874 5.12228 -0.0650874 5.54439 0.195262 5.80474L4.86193 10.4714C5.12228 10.7318 5.54439 10.7318 5.80474 10.4714C6.06509 10.2111 6.06509 9.78894 5.80474 9.52859L1.60948 5.33333L5.80474 1.13807Z"
|
||||
:fill="color"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M10.4714 1.13807C10.7318 0.877722 10.7318 0.455612 10.4714 0.195262C10.2111 -0.0650874 9.78895 -0.0650874 9.5286 0.195262L4.86193 4.86193C4.60158 5.12228 4.60158 5.54439 4.86193 5.80474L9.5286 10.4714C9.78895 10.7318 10.2111 10.7318 10.4714 10.4714C10.7318 10.2111 10.7318 9.78894 10.4714 9.52859L6.27614 5.33333L10.4714 1.13807Z"
|
||||
:fill="color"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
32
layers/components/atoms/icons/CalendarLine.vue
Normal file
32
layers/components/atoms/icons/CalendarLine.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
size?: number | string
|
||||
color?: string
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
size: 32,
|
||||
color: '#EBEBEB',
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
:width="size"
|
||||
:height="size"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M5.83333 11.4583C5.83333 10.6529 5.18042 10 4.375 10C3.56958 10 2.91667 10.6529 2.91667 11.4583C2.91667 12.2637 3.56958 12.9167 4.375 12.9167C5.18041 12.9167 5.83333 12.2637 5.83333 11.4583Z"
|
||||
:fill="color"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M3.75 0C4.21024 0 4.58333 0.373096 4.58333 0.833333V1.66667H10.4167V0.833333C10.4167 0.373096 10.7898 0 11.25 0C11.7102 0 12.0833 0.373096 12.0833 0.833333V1.66667H12.9167C14.0673 1.66667 15 2.59941 15 3.75V13.75C15 14.9006 14.0673 15.8333 12.9167 15.8333H2.08333C0.93274 15.8333 0 14.9006 0 13.75V3.75C0 2.59941 0.93274 1.66667 2.08333 1.66667H2.91667V0.833333C2.91667 0.373096 3.28976 0 3.75 0ZM10.4167 3.33333C10.4167 3.79357 10.7898 4.16667 11.25 4.16667C11.7102 4.16667 12.0833 3.79357 12.0833 3.33333H12.9167C13.1468 3.33333 13.3333 3.51988 13.3333 3.75V5.83333H1.66667V3.75C1.66667 3.51988 1.85321 3.33333 2.08333 3.33333H2.91667C2.91667 3.79357 3.28976 4.16667 3.75 4.16667C4.21024 4.16667 4.58333 3.79357 4.58333 3.33333H10.4167ZM1.66667 7.5H13.3333V13.75C13.3333 13.9801 13.1468 14.1667 12.9167 14.1667H2.08333C1.85321 14.1667 1.66667 13.9801 1.66667 13.75V7.5Z"
|
||||
:fill="color"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
28
layers/components/atoms/icons/CheckLine.vue
Normal file
28
layers/components/atoms/icons/CheckLine.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
size?: number | string
|
||||
color?: string
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
size: 32,
|
||||
color: '#1F1F1F',
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
:width="size"
|
||||
:height="size"
|
||||
viewBox="0 0 15 10"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M13.9226 0.244078C14.248 0.569515 14.248 1.09715 13.9226 1.42259L5.58927 9.75592C5.42514 9.92006 5.19998 10.0083 4.96803 9.99939C4.73609 9.99048 4.51836 9.88523 4.3673 9.70899L0.200635 4.84788C-0.0988839 4.49844 -0.0584159 3.97236 0.291022 3.67284C0.64046 3.37332 1.16654 3.41379 1.46606 3.76323L5.04708 7.94109L12.7441 0.244078C13.0695 -0.0813592 13.5972 -0.0813592 13.9226 0.244078Z"
|
||||
:fill="color"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
28
layers/components/atoms/icons/CloseCircleFill.vue
Normal file
28
layers/components/atoms/icons/CloseCircleFill.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
size?: number | string
|
||||
color?: string
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
size: 32,
|
||||
color: '#1F1F1F',
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
:width="size"
|
||||
:height="size"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M12 1.5C17.799 1.5 22.5 6.20101 22.5 12C22.5 17.799 17.799 22.5 12 22.5C6.20101 22.5 1.5 17.799 1.5 12C1.50001 6.20101 6.20102 1.5 12 1.5ZM15.7071 15.7071C16.0976 15.3166 16.0976 14.6834 15.7071 14.2929L13.4142 12L15.7071 9.70711C16.0976 9.31659 16.0976 8.68342 15.7071 8.2929C15.3166 7.90237 14.6834 7.90237 14.2929 8.2929L12 10.5858L9.70711 8.2929C9.31659 7.90237 8.68342 7.90237 8.2929 8.2929C7.90238 8.68342 7.90238 9.31658 8.2929 9.70711L10.5858 12L8.2929 14.2929C7.90237 14.6834 7.90237 15.3166 8.2929 15.7071C8.68342 16.0976 9.31659 16.0976 9.70711 15.7071L12 13.4142L14.2929 15.7071C14.6834 16.0976 15.3166 16.0976 15.7071 15.7071Z"
|
||||
:fill="color"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
28
layers/components/atoms/icons/CouponFill.vue
Normal file
28
layers/components/atoms/icons/CouponFill.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
size?: number | string
|
||||
color?: string
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
size: 32,
|
||||
color: '#EBEBEB',
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg
|
||||
:width="size"
|
||||
:height="size"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M10.4413 0.644656C9.58368 -0.214056 8.18151 -0.214884 7.32284 0.64217L6.09332 1.85726L6.092 1.85863C6.08921 1.86072 6.08403 1.86443 6.07608 1.86947C6.05498 1.88286 6.02495 1.89889 5.98871 1.91393C5.95257 1.92893 5.91833 1.93961 5.89062 1.94597C5.8723 1.95017 5.86194 1.95131 5.8595 1.95158H4.15691C2.93936 1.95158 1.94911 2.9448 1.94911 4.16099V5.86353C1.9487 5.86679 1.94743 5.87584 1.94397 5.89078C1.93766 5.91805 1.92684 5.9528 1.91128 5.99038C1.89572 6.02795 1.87871 6.06042 1.86357 6.08467C1.85431 6.0995 1.84819 6.10737 1.8464 6.10964L0.643348 7.31422C-0.214449 8.17311 -0.214449 9.57689 0.643348 10.4358L1.8464 11.6404C1.84819 11.6426 1.85431 11.6505 1.86357 11.6653C1.87871 11.6896 1.89572 11.722 1.91128 11.7596C1.92684 11.7972 1.93766 11.8319 1.94397 11.8592C1.94743 11.8742 1.9487 11.8832 1.94911 11.8865V13.589C1.94911 14.8052 2.93936 15.7984 4.15691 15.7984H5.85707C5.86038 15.7988 5.86923 15.8001 5.88373 15.8035C5.91079 15.8098 5.94533 15.8205 5.98275 15.8361C6.02015 15.8516 6.05249 15.8686 6.07662 15.8837C6.09134 15.8929 6.09916 15.899 6.10143 15.9008L7.30447 17.1053C8.16292 17.9649 9.56698 17.9649 10.4254 17.1053L11.6285 15.9008C11.6307 15.899 11.6385 15.8929 11.6533 15.8837C11.6774 15.8686 11.7097 15.8516 11.7471 15.8361C11.7846 15.8205 11.8191 15.8098 11.8462 15.8035C11.8607 15.8001 11.8695 15.7988 11.8728 15.7984H13.573C14.7905 15.7984 15.7808 14.8052 15.7808 13.589V11.8865C15.7812 11.8832 15.7825 11.8742 15.7859 11.8592C15.7922 11.8319 15.8031 11.7972 15.8186 11.7596C15.8342 11.722 15.8512 11.6896 15.8663 11.6653C15.8756 11.6505 15.8817 11.6426 15.8835 11.6404L17.0774 10.4449C17.9753 9.58844 17.9649 8.17522 17.1001 7.31988L15.8994 6.11759C15.8976 6.11533 15.8915 6.10746 15.8822 6.09262C15.8671 6.06837 15.8501 6.0359 15.8345 5.99833C15.8189 5.96074 15.8081 5.926 15.8018 5.89872C15.7983 5.88378 15.7971 5.87473 15.7967 5.87147V4.16099C15.7967 2.94479 14.8064 1.95158 13.5889 1.95158H11.8887C11.8854 1.95116 11.8766 1.94989 11.8621 1.94653C11.835 1.94025 11.8004 1.92947 11.763 1.91393C11.7256 1.8984 11.6933 1.88142 11.6692 1.86632C11.6544 1.85711 11.6466 1.85102 11.6444 1.84922L10.4413 0.644656ZM12.3807 5.36843C12.7468 5.73455 12.7468 6.32814 12.3807 6.69425L6.69319 12.3818C6.32707 12.7479 5.73348 12.7479 5.36736 12.3818C5.00124 12.0156 5.00124 11.422 5.36736 11.0559L11.0549 5.36843C11.421 5.00231 12.0146 5.00231 12.3807 5.36843ZM6.23574 7.25014C6.79665 7.25014 7.25136 6.79543 7.25136 6.23451C7.25136 5.6736 6.79665 5.21889 6.23574 5.21889C5.67482 5.21889 5.22011 5.6736 5.22011 6.23451C5.22011 6.79543 5.67482 7.25014 6.23574 7.25014ZM11.5141 12.5312C12.075 12.5312 12.5297 12.0765 12.5297 11.5156C12.5297 10.9547 12.075 10.5 11.5141 10.5C10.9531 10.5 10.4984 10.9547 10.4984 11.5156C10.4984 12.0765 10.9531 12.5312 11.5141 12.5312Z"
|
||||
:fill="color"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
32
layers/components/atoms/icons/DesktopLine.vue
Normal file
32
layers/components/atoms/icons/DesktopLine.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
size?: number | string
|
||||
color?: string
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
size: 32,
|
||||
color: '#EBEBEB',
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg
|
||||
:width="size"
|
||||
:height="size"
|
||||
viewBox="0 0 17 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M5.83333 7.8125C5.3731 7.8125 5 8.1856 5 8.64583C5 9.10607 5.3731 9.47917 5.83333 9.47917H10.8333C11.2936 9.47917 11.6667 9.10607 11.6667 8.64583C11.6667 8.1856 11.2936 7.8125 10.8333 7.8125H5.83333Z"
|
||||
:fill="color"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M3.33333 0C1.49238 0 0 1.49238 0 3.33333V9.58333C0 11.4243 1.49238 12.9167 3.33333 12.9167H7.5V13.6458H5.20833C4.7481 13.6458 4.375 14.0189 4.375 14.4792C4.375 14.9394 4.7481 15.3125 5.20833 15.3125H11.4583C11.9186 15.3125 12.2917 14.9394 12.2917 14.4792C12.2917 14.0189 11.9186 13.6458 11.4583 13.6458H9.16667V12.9167H13.3333C15.1743 12.9167 16.6667 11.4243 16.6667 9.58333V3.33333C16.6667 1.49238 15.1743 0 13.3333 0H3.33333ZM1.66667 3.33333C1.66667 2.41286 2.41286 1.66667 3.33333 1.66667H13.3333C14.2538 1.66667 15 2.41286 15 3.33333V9.58333C15 10.5038 14.2538 11.25 13.3333 11.25H3.33333C2.41286 11.25 1.66667 10.5038 1.66667 9.58333V3.33333Z"
|
||||
:fill="color"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
28
layers/components/atoms/icons/SearchLine.vue
Normal file
28
layers/components/atoms/icons/SearchLine.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
size?: number | string
|
||||
color?: string
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
size: 32,
|
||||
color: '#EBEBEB',
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg
|
||||
:width="size"
|
||||
:height="size"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M9.74512 10.6879C8.71868 11.509 7.41668 12 6 12C2.68629 12 0 9.31371 0 6C0 2.68629 2.68629 0 6 0C9.31371 0 12 2.68629 12 6C12 7.41668 11.509 8.71868 10.6879 9.74512L13.1381 12.1953C13.3984 12.4556 13.3984 12.8777 13.1381 13.1381C12.8777 13.3984 12.4556 13.3984 12.1953 13.1381L9.74512 10.6879ZM10.6667 6C10.6667 8.57733 8.57733 10.6667 6 10.6667C3.42267 10.6667 1.33333 8.57733 1.33333 6C1.33333 3.42267 3.42267 1.33333 6 1.33333C8.57733 1.33333 10.6667 3.42267 10.6667 6Z"
|
||||
:fill="color"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
26
layers/components/atoms/icons/SelectArrowDownFill.vue
Normal file
26
layers/components/atoms/icons/SelectArrowDownFill.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
size?: number | string
|
||||
color?: string
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
size: 32,
|
||||
color: '#EBEBEB',
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg
|
||||
:width="size"
|
||||
:height="size"
|
||||
viewBox="0 0 10 6"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M4.06454 4.95333L0.204544 1.16667C-0.228789 0.74 0.0712106 0 0.684544 0L9.31787 0C9.9312 0 10.2312 0.74 9.79787 1.16667L5.93787 4.95333C5.41787 5.46 4.59121 5.46 4.07121 4.95333H4.06454Z"
|
||||
:fill="color"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
524
layers/components/blocks/DatePicker.vue
Normal file
524
layers/components/blocks/DatePicker.vue
Normal file
@@ -0,0 +1,524 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
getYear,
|
||||
getMonth,
|
||||
startOfDay,
|
||||
globalDateFormat,
|
||||
} from '@seed-next/date'
|
||||
import type { PublicMethods } from '@vuepic/vue-datepicker'
|
||||
|
||||
interface Props {
|
||||
date: Date | string | null
|
||||
minDate?: Date | null
|
||||
maxDate?: Date | null
|
||||
placeholder?: Date | string | null
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
minDate: null,
|
||||
maxDate: null,
|
||||
placeholder: null,
|
||||
})
|
||||
|
||||
const emits = defineEmits(['update:date'])
|
||||
|
||||
// Composabled
|
||||
const breakpoints = useResponsiveBreakpoints()
|
||||
|
||||
// Refs -----
|
||||
const { tm, locale } = useI18n()
|
||||
const datePickerRef = ref<any>(null)
|
||||
const resultDate = ref('')
|
||||
const formatDate = ref('')
|
||||
const currentYear = ref(new Date().getFullYear())
|
||||
const currentMonth = ref(new Date().getMonth())
|
||||
const currentDecade = ref<{ start: number; end: number } | null>(null)
|
||||
// 상태값
|
||||
const isMenuOpened = ref(false)
|
||||
const isShowYearPicker = ref(false)
|
||||
const isShowMonthPicker = ref(false)
|
||||
const isShowDecadePicker = ref(false)
|
||||
|
||||
// Computed
|
||||
const yearMonthText = computed(() => {
|
||||
return `${currentYear.value}년 ${currentMonth.value + 1}월`
|
||||
})
|
||||
const formatMinDate = computed(() => {
|
||||
if (props.minDate !== null) {
|
||||
return globalDateFormat(props.minDate, locale.value)
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
})
|
||||
const formatMaxDate = computed(() => {
|
||||
if (props.maxDate !== null) {
|
||||
return globalDateFormat(props.maxDate, locale.value)
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
})
|
||||
const formatPlaceholder = computed(() => {
|
||||
if (props.placeholder !== null) {
|
||||
return props.placeholder instanceof Date
|
||||
? (globalDateFormat(props.placeholder, locale.value) as string)
|
||||
: props.placeholder
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
})
|
||||
|
||||
// Functions
|
||||
const setFormatDate = () => {
|
||||
const toDay = startOfDay(new Date())
|
||||
resultDate.value =
|
||||
props.date === null ? toDay.toString() : props.date.toString()
|
||||
formatDate.value = globalDateFormat(resultDate.value, locale.value) as string
|
||||
|
||||
const dateObj = props.date === null ? toDay : new Date(props.date)
|
||||
currentYear.value = dateObj.getFullYear()
|
||||
currentMonth.value = dateObj.getMonth()
|
||||
}
|
||||
|
||||
const updateDate = (_date: Date | null) => {
|
||||
if (_date != null) {
|
||||
formatDate.value = globalDateFormat(_date, locale.value) as string
|
||||
currentYear.value = _date.getFullYear()
|
||||
currentMonth.value = _date.getMonth()
|
||||
emits('update:date', formatDate.value)
|
||||
}
|
||||
}
|
||||
|
||||
const format = (_date: Date) => {
|
||||
return globalDateFormat(_date, locale.value)
|
||||
}
|
||||
|
||||
const handleMenuOpen = () => {
|
||||
isMenuOpened.value = true
|
||||
}
|
||||
|
||||
const handleMenuClose = () => {
|
||||
isMenuOpened.value = false
|
||||
|
||||
isShowMonthPicker.value = false
|
||||
isShowYearPicker.value = false
|
||||
}
|
||||
|
||||
const handleOpenMonthPicker = () => {
|
||||
isShowMonthPicker.value = true
|
||||
isShowYearPicker.value = false
|
||||
}
|
||||
|
||||
const handleOpenYearPicker = () => {
|
||||
isShowYearPicker.value = true
|
||||
isShowMonthPicker.value = false
|
||||
}
|
||||
|
||||
const handleMonthSelect = (month: number) => {
|
||||
currentMonth.value = month
|
||||
isShowMonthPicker.value = false
|
||||
|
||||
// 선택한 년도/월로 날짜 업데이트
|
||||
const newDate = new Date(currentYear.value, currentMonth.value, 1)
|
||||
if (props.minDate && newDate < props.minDate) {
|
||||
newDate.setDate(props.minDate.getDate())
|
||||
}
|
||||
if (props.maxDate && newDate > props.maxDate) {
|
||||
newDate.setDate(props.maxDate.getDate())
|
||||
}
|
||||
|
||||
// DatePicker의 내부 상태 업데이트
|
||||
if (datePickerRef.value) {
|
||||
datePickerRef.value.selectDate(newDate)
|
||||
}
|
||||
|
||||
updateDate(newDate)
|
||||
}
|
||||
|
||||
const handleYearSelect = (year: number) => {
|
||||
currentYear.value = year
|
||||
isShowYearPicker.value = false
|
||||
|
||||
// 선택한 년도/월로 날짜 업데이트
|
||||
const newDate = new Date(currentYear.value, currentMonth.value, 1)
|
||||
if (props.minDate && newDate < props.minDate) {
|
||||
newDate.setDate(props.minDate.getDate())
|
||||
}
|
||||
if (props.maxDate && newDate > props.maxDate) {
|
||||
newDate.setDate(props.maxDate.getDate())
|
||||
}
|
||||
|
||||
// DatePicker의 내부 상태 업데이트
|
||||
if (datePickerRef.value) {
|
||||
datePickerRef.value.selectDate(newDate)
|
||||
}
|
||||
|
||||
updateDate(newDate)
|
||||
}
|
||||
|
||||
const generateYearList = () => {
|
||||
const years = []
|
||||
const minYear = props.minDate ? props.minDate.getFullYear() : 1900
|
||||
const maxYear = props.maxDate ? props.maxDate.getFullYear() : 2100
|
||||
|
||||
for (let year = minYear; year <= maxYear; year++) {
|
||||
years.push(year)
|
||||
}
|
||||
return years
|
||||
}
|
||||
|
||||
const generateMonthList = () => {
|
||||
const months = []
|
||||
const minMonth =
|
||||
props.minDate && props.minDate.getFullYear() === currentYear.value
|
||||
? props.minDate.getMonth()
|
||||
: 0
|
||||
const maxMonth =
|
||||
props.maxDate && props.maxDate.getFullYear() === currentYear.value
|
||||
? props.maxDate.getMonth()
|
||||
: 11
|
||||
|
||||
for (let month = minMonth; month <= maxMonth; month++) {
|
||||
months.push(month)
|
||||
}
|
||||
return months
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.date,
|
||||
() => {
|
||||
setFormatDate()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
setFormatDate()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VueDatePicker
|
||||
ref="datePickerRef"
|
||||
v-model="resultDate"
|
||||
:locale="locale"
|
||||
week-start="0"
|
||||
:year-first="true"
|
||||
:auto-apply="true"
|
||||
:hide-offset-dates="true"
|
||||
:enable-time-picker="false"
|
||||
:preview-format="format"
|
||||
:min-date="formatMinDate"
|
||||
:max-date="formatMaxDate"
|
||||
:prevent-min-max-navigation="true"
|
||||
:month-change-on-scroll="false"
|
||||
position="left"
|
||||
:auto-position="true"
|
||||
:offset="8"
|
||||
:config="{
|
||||
shadowDom: true,
|
||||
}"
|
||||
class="date-picker-wrap"
|
||||
@update:model-value="updateDate"
|
||||
@date-update="updateDate"
|
||||
@open="handleMenuOpen"
|
||||
@closed="handleMenuClose"
|
||||
>
|
||||
<template #trigger>
|
||||
<div
|
||||
class="relative flex items-center justify-between w-full h-[40px] px-[16px] border border-solid border-[1px] border-[#D9D9D9] rounded-[8px] bg-white cursor-pointer"
|
||||
>
|
||||
<AtomsInput
|
||||
:model-value="formatDate"
|
||||
:placeholder="formatPlaceholder"
|
||||
:value="formatDate"
|
||||
name="date-picker-text"
|
||||
label
|
||||
readonly
|
||||
class="inline-flex items-center justify-start !w-full !h-auto !p-0 m-0 bg-transparent border-none outline-none cursor-pointer text-left !text-[14px] text-[#333333] font-[400] !leading-[20px] !tracking-[-0.42px]"
|
||||
/>
|
||||
<AtomsIconsCalendarLine
|
||||
:size="breakpoints.isMobile ? 16 : 20"
|
||||
color="#333333"
|
||||
class="absolute top-[50%] right-[12px] translate-y-[-50%] md:right-[16px]"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #calendar-header="{ index, day }">
|
||||
<div :class="index === 0 ? 'sunday' : index === 6 ? 'saturday' : ''">
|
||||
{{ day }}
|
||||
</div>
|
||||
</template>
|
||||
<template #day="{ day, date }">
|
||||
<span
|
||||
:class="[
|
||||
date.getDay() === 0 ? 'text-[#FC4420]' : 'text-[#333333]',
|
||||
date > new Date(formatMaxDate) ? 'opacity-35' : 'opacity-100',
|
||||
]"
|
||||
>
|
||||
{{ day }}
|
||||
</span>
|
||||
</template>
|
||||
<template
|
||||
#month-year="{
|
||||
month,
|
||||
year,
|
||||
months,
|
||||
years,
|
||||
|
||||
updateMonthYear,
|
||||
handleMonthYearChange,
|
||||
isDisabled,
|
||||
}"
|
||||
>
|
||||
<div class="flex items-center justify-between w-full py-[16px]">
|
||||
<button
|
||||
type="button"
|
||||
class="relative inline-flex items-center justify-center w-[32px] h-[32px]"
|
||||
:disabled="isDisabled(false)"
|
||||
@click="
|
||||
updateMonthYear(
|
||||
month === 0 ? 11 : month - 1,
|
||||
month === 0 ? year - 1 : year,
|
||||
false
|
||||
)
|
||||
"
|
||||
>
|
||||
<AtomsIconsArrowRightLine
|
||||
:size="16"
|
||||
:color="isDisabled(false) ? '#CCCCCC' : '#333333'"
|
||||
class="relative rotate-180"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="relative inline-flex items-center justify-center gap-[8px] w-auto h-[32px]"
|
||||
@click="handleOpenMonthPicker"
|
||||
>
|
||||
<span
|
||||
class="inline-flex items-center justify-center text-[#1F1F1F] text-[18px] font-[500] leading-[26px] tracking-[-0.54px]"
|
||||
>
|
||||
{{ years.find(y => y.value === year)?.text }}.
|
||||
{{ months.find(m => m.value === month)?.text }}
|
||||
</span>
|
||||
<AtomsIconsSelectArrowDownFill :size="10" color="#333333" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="relative inline-flex items-center justify-center w-[32px] h-[32px]"
|
||||
:disabled="isDisabled(true)"
|
||||
@click="
|
||||
updateMonthYear(
|
||||
month === 11 ? 0 : month + 1,
|
||||
month === 11 ? year + 1 : year,
|
||||
false
|
||||
)
|
||||
"
|
||||
>
|
||||
<AtomsIconsArrowRightLine
|
||||
:size="16"
|
||||
:color="isDisabled(true) ? '#CCCCCC' : '#333333'"
|
||||
class="relative"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="isShowMonthPicker && isMenuOpened"
|
||||
class="absolute z-[2] top-0 left-0 flex flex-col items-center justify-start w-full h-full overflow-y-auto bg-white"
|
||||
>
|
||||
<div class="flex items-center justify-between w-full py-[16px]">
|
||||
<button
|
||||
type="button"
|
||||
class="relative inline-flex items-center justify-center w-[32px] h-[32px]"
|
||||
:disabled="isDisabled(false)"
|
||||
@click="
|
||||
updateMonthYear(
|
||||
month,
|
||||
year === getYear(minDate) ? year : year - 1,
|
||||
false
|
||||
)
|
||||
"
|
||||
>
|
||||
<AtomsIconsArrowRightLine
|
||||
:size="16"
|
||||
:color="isDisabled(false) ? '#CCCCCC' : '#333333'"
|
||||
class="relative rotate-180"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="relative inline-flex items-center justify-center gap-[8px] w-auto h-[32px]"
|
||||
@click="handleOpenYearPicker"
|
||||
>
|
||||
<span
|
||||
class="inline-flex items-center justify-center text-[#1F1F1F] text-[18px] font-[500] leading-[26px] tracking-[-0.54px]"
|
||||
>
|
||||
{{ years.find(y => y.value === year)?.text }}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="relative inline-flex items-center justify-center w-[32px] h-[32px]"
|
||||
:disabled="isDisabled(true)"
|
||||
@click="
|
||||
updateMonthYear(
|
||||
month,
|
||||
year === getYear(maxDate) ? year : year + 1,
|
||||
false
|
||||
)
|
||||
"
|
||||
>
|
||||
<AtomsIconsArrowRightLine
|
||||
:size="16"
|
||||
:color="isDisabled(true) ? '#CCCCCC' : '#333333'"
|
||||
class="relative"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-[4px] w-full">
|
||||
<button
|
||||
v-for="month in generateMonthList()"
|
||||
:key="month"
|
||||
type="button"
|
||||
:class="[
|
||||
'inline-flex items-center justify-center w-full h-[52px] px-[12px] py-[8px] text-center text-[14px] font-[400] leading-[20px] tracking-[-0.42px] rounded-[4px] transition-colors',
|
||||
year === currentYear && month === currentMonth
|
||||
? 'bg-[#FC4420] text-white'
|
||||
: 'text-[#333333] hover:bg-[#F5F5F5]',
|
||||
]"
|
||||
@click="handleMonthSelect(month)"
|
||||
>
|
||||
{{ months.find(m => m.value === month)?.text }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="isShowYearPicker && isMenuOpened"
|
||||
class="absolute z-[2] top-0 left-0 w-full h-full mt-[8px] overflow-y-auto bg-white"
|
||||
>
|
||||
<div class="grid grid-cols-4 gap-[4px]">
|
||||
<button
|
||||
v-for="year in generateYearList()"
|
||||
:key="year"
|
||||
type="button"
|
||||
:class="[
|
||||
'px-[12px] py-[8px] text-center text-[14px] font-[400] leading-[20px] tracking-[-0.42px] rounded-[4px] transition-colors',
|
||||
year === currentYear
|
||||
? 'bg-[#FC4420] text-white'
|
||||
: 'text-[#333333] hover:bg-[#F5F5F5]',
|
||||
]"
|
||||
@click="handleYearSelect(year)"
|
||||
>
|
||||
{{ years.find(y => y.value === year)?.text }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</VueDatePicker>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.date-picker-wrap :deep(.dp__calendar_header_separator) {
|
||||
display: none !important;
|
||||
}
|
||||
.date-picker-wrap :deep(.dp__menu) {
|
||||
padding: 0 12px 8px !important;
|
||||
}
|
||||
.date-picker-wrap :deep(.dp__menu_inner) {
|
||||
padding: 0 !important;
|
||||
}
|
||||
.date-picker-wrap :deep(.dp__calendar) {
|
||||
padding: 0 !important;
|
||||
}
|
||||
.date-picker-wrap :deep(.dp__calendar_row) {
|
||||
-webkit-gap: 4px !important;
|
||||
-moz-gap: 4px !important;
|
||||
-o-gap: 4px !important;
|
||||
-ms-gap: 4px !important;
|
||||
gap: 4px !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
.date-picker-wrap :deep(.dp__calendar_item) {
|
||||
padding: 2px 4px !important;
|
||||
}
|
||||
.date-picker-wrap :deep(.dp__cell_inner) {
|
||||
padding: 8px !important;
|
||||
border-radius: 100% !important;
|
||||
outline: none !important;
|
||||
border: none !important;
|
||||
}
|
||||
.date-picker-wrap :deep(.dp__cell_inner.dp__date_hover:hover) {
|
||||
background-color: rgba(0, 0, 0, 0.04) !important;
|
||||
}
|
||||
.date-picker-wrap :deep(.dp__cell_inner.dp__today) {
|
||||
border: 1px solid rgba(0, 0, 0, 0.15) !important;
|
||||
}
|
||||
.date-picker-wrap :deep(.dp__cell_inner.dp__active_date) {
|
||||
background-color: #fc4420 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
.date-picker-wrap :deep(.dp__cell_inner.dp__active_date span) {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
.date-picker-wrap :deep(.dp__cell_disabled:hover),
|
||||
.date-picker-wrap :deep(.dp__cell_disabled) {
|
||||
cursor: default !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
.date-picker-wrap :deep(.dp__menu),
|
||||
.date-picker-wrap :deep(.dp__menu_wrap),
|
||||
.date-picker-wrap :deep(.dp__overlay) {
|
||||
box-shadow: 0 4px 10px 0 rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
||||
.date-picker-wrap :deep(.dp__overlay_cell_pad) {
|
||||
color: #333333 !important;
|
||||
}
|
||||
.date-picker-wrap :deep(.dp__overlay_cell_pad:hover) {
|
||||
background-color: #f5f5f5 !important;
|
||||
}
|
||||
.date-picker-wrap :deep(.dp__overlay_cell_pad.dp__overlay_cell_active) {
|
||||
background-color: #fc4420 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
.date-picker-wrap :deep(.dp__overlay_cell_pad.dp__overlay_cell_disabled:hover),
|
||||
.date-picker-wrap :deep(.dp__overlay_cell_pad.dp__overlay_cell_disabled) {
|
||||
cursor: default !important;
|
||||
background-color: transparent !important;
|
||||
opacity: 0.35 !important;
|
||||
}
|
||||
.date-picker-wrap :deep(.dp__inner_nav) {
|
||||
color: #333333 !important;
|
||||
}
|
||||
.date-picker-wrap :deep(.dp__inner_nav:hover) {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
.date-picker-wrap :deep(.dp__inner_nav.dp__inner_nav_disabled) {
|
||||
background-color: transparent !important;
|
||||
cursor: default !important;
|
||||
opacity: 0.35 !important;
|
||||
}
|
||||
.date-picker-wrap :deep(.dp__month_year_select),
|
||||
.date-picker-wrap :deep(.dp__month_year_select:hover) {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
.date-picker-wrap :deep(.dp__arrow_bottom),
|
||||
.date-picker-wrap :deep(.dp__arrow_top),
|
||||
.date-picker-wrap :deep(.dp__arrow_left),
|
||||
.date-picker-wrap :deep(.dp__arrow_right) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.date-picker-wrap.date-picker-menu-absolute :deep(.dp__menu) {
|
||||
position: absolute !important;
|
||||
}
|
||||
|
||||
.date-picker-wrap.date-picker-menu-fixed :deep(.dp__menu) {
|
||||
position: fixed !important;
|
||||
}
|
||||
|
||||
.date-picker-wrap :deep(.dp__month_year_select) {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
150
layers/components/blocks/Pagination.vue
Normal file
150
layers/components/blocks/Pagination.vue
Normal file
@@ -0,0 +1,150 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
totalCount: number
|
||||
currentPage: number
|
||||
pageSize: number
|
||||
pageBlock?: number
|
||||
useMinMax?: boolean
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
currentPage: 1,
|
||||
pageBlock: 10,
|
||||
useMinMax: true,
|
||||
})
|
||||
|
||||
const emits = defineEmits(['update:page'])
|
||||
|
||||
const currentPage = ref(props.currentPage)
|
||||
const firstPage = ref(1)
|
||||
const lastPage = computed(() => Math.ceil(props.totalCount / props.pageSize))
|
||||
const prevPage = computed(() => currentPage.value - 1)
|
||||
const nextPage = computed(() => currentPage.value + 1)
|
||||
|
||||
const currentBlock = computed(() =>
|
||||
isShortThanBlock.value ? 1 : Math.ceil(currentPage.value / props.pageBlock)
|
||||
)
|
||||
// 블럭 크기가 전체 페이지 수보다 작은지 여부
|
||||
const isShortThanBlock = computed(() => lastPage.value < props.pageBlock)
|
||||
// 현재 블럭이 첫 블럭인지 여부
|
||||
const isFirstBlock = computed(() => currentBlock.value === 1)
|
||||
// 현재 블럭이 마지막 블럭인지 여부
|
||||
const isLastBlock = computed(
|
||||
() => currentBlock.value === Math.ceil(lastPage.value / props.pageBlock)
|
||||
)
|
||||
|
||||
const lastBlock = computed(() =>
|
||||
isShortThanBlock.value ? 1 : Math.ceil(lastPage.value / props.pageBlock)
|
||||
)
|
||||
const currentBlockFirstPage = computed(() =>
|
||||
isShortThanBlock.value ? 1 : (currentBlock.value - 1) * props.pageBlock + 1
|
||||
)
|
||||
const blocks = computed(() => {
|
||||
if (props.totalCount === 0) {
|
||||
return []
|
||||
}
|
||||
const blockSize =
|
||||
currentBlock.value === lastBlock.value &&
|
||||
lastPage.value % props.pageBlock !== 0
|
||||
? lastPage.value - currentBlockFirstPage.value + 1
|
||||
: props.pageBlock
|
||||
|
||||
return Array.from(
|
||||
{ length: blockSize },
|
||||
(_, i) => currentBlockFirstPage.value + i
|
||||
)
|
||||
})
|
||||
|
||||
const handlePagination = (page: number) => {
|
||||
currentPage.value = page
|
||||
emits('update:page', page)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
console.log(blocks.value)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="props.totalCount > 0"
|
||||
class="relative flex items-center justify-center w-full py-[32px]"
|
||||
>
|
||||
<template v-if="totalCount > pageSize">
|
||||
<template v-if="props.useMinMax">
|
||||
<button
|
||||
type="button"
|
||||
class="relative inline-flex items-center justify-center w-[32px] h-[32px]"
|
||||
:disabled="currentPage === firstPage"
|
||||
@click="handlePagination(firstPage)"
|
||||
>
|
||||
<AtomsIconsArrowDoubleLeftLine
|
||||
:size="11"
|
||||
:color="currentPage === firstPage ? '#CCCCCC' : '#333333'"
|
||||
/>
|
||||
</button>
|
||||
</template>
|
||||
<button
|
||||
type="button"
|
||||
class="relative inline-flex items-center justify-center w-[32px] h-[32px]"
|
||||
:disabled="currentPage === firstPage"
|
||||
@click="handlePagination(prevPage)"
|
||||
>
|
||||
<AtomsIconsArrowRightLine
|
||||
:size="16"
|
||||
:color="currentPage === firstPage ? '#CCCCCC' : '#333333'"
|
||||
class="rotate-180"
|
||||
/>
|
||||
</button>
|
||||
</template>
|
||||
<ol class="relative inline-flex items-center justify-center">
|
||||
<template v-for="page in blocks" :key="page">
|
||||
<li
|
||||
class="relative inline-flex items-center justify-center w-[32px] h-[32px]"
|
||||
>
|
||||
<AtomsButton
|
||||
type="action"
|
||||
button-size="size-small"
|
||||
background-color="transparent"
|
||||
text-color="#333333"
|
||||
:class="[
|
||||
'!w-full !h-full p-0 rounded-full text-center text-[14px] font-[500] leading-[24px] tracking-[-0.42px]',
|
||||
page === currentPage
|
||||
? '!bg-[#C7AE8B] !text-white cursor-default'
|
||||
: '',
|
||||
]"
|
||||
@click="handlePagination(page)"
|
||||
>
|
||||
<span>{{ page }}</span>
|
||||
</AtomsButton>
|
||||
</li>
|
||||
</template>
|
||||
</ol>
|
||||
<template v-if="totalCount > pageSize">
|
||||
<button
|
||||
type="button"
|
||||
class="relative inline-flex items-center justify-center w-[32px] h-[32px]"
|
||||
:disabled="currentPage === lastPage"
|
||||
@click="handlePagination(nextPage)"
|
||||
>
|
||||
<AtomsIconsArrowRightLine
|
||||
:size="16"
|
||||
:color="currentPage === lastPage ? '#CCCCCC' : '#333333'"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="relative inline-flex items-center justify-center w-[32px] h-[32px]"
|
||||
:disabled="currentPage === lastPage"
|
||||
@click="handlePagination(lastPage)"
|
||||
>
|
||||
<AtomsIconsArrowDoubleLeftLine
|
||||
:size="11"
|
||||
:color="currentPage === lastPage ? '#CCCCCC' : '#333333'"
|
||||
class="rotate-180"
|
||||
/>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -43,11 +43,11 @@ const componentProps = computed(() => {
|
||||
<template>
|
||||
<div class="flex flex-wrap items-end justify-between mb-[16px] md:mb-[24px]">
|
||||
<h3
|
||||
class="text-[#1F1F1F] text-[18px] font-bold leading-[26px] tracking-[-0.54px] md:text-[24px] md:leading-[34px] md:tracking-[0.72px]"
|
||||
class="text-[#1F1F1F] text-[18px] font-bold leading-[26px] tracking-[-0.54px] md:text-[24px] md:leading-[34px] md:tracking-[0.72px] shrink-0"
|
||||
>
|
||||
<span>{{ props.title }}</span>
|
||||
</h3>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<slot />
|
||||
<p
|
||||
v-if="props.description && !props.link"
|
||||
|
||||
175
layers/composables/uesGameLinkedData.ts
Normal file
175
layers/composables/uesGameLinkedData.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import type {
|
||||
ReqGetGuid,
|
||||
ResGetGuid,
|
||||
ReqGameCharacterList,
|
||||
CharacterInfo,
|
||||
ResGameCharacterList,
|
||||
} from '#layers/types/api/gameLinkedData'
|
||||
|
||||
/**
|
||||
* 게임 관련 항목
|
||||
*/
|
||||
const useGameLinkedData = () => {
|
||||
const logPrefix = {
|
||||
exception: '[Exception] /composables/useGameLinkedData',
|
||||
failure: '[Failure] /composables/useGameLinkedData',
|
||||
}
|
||||
const hasGuid = ref(false) // GUID 존재 여부
|
||||
const characterList = ref([] as CharacterInfo[]) // 보유 캐릭터 목록
|
||||
const mainCharacter = ref({} as CharacterInfo) // 대표 캐릭터
|
||||
const selectCharacter = ref(null) // (사용자가 )선택한 캐릭터
|
||||
|
||||
// [Setter] GUID 존재 여부 세팅
|
||||
const setHasGuid = (newHasGuid: boolean) => {
|
||||
hasGuid.value = newHasGuid
|
||||
}
|
||||
// [Setter] 보유 캐릭터 목록 세팅
|
||||
const setCharacterList = (newCharacters: CharacterInfo[]) => {
|
||||
characterList.value = newCharacters
|
||||
}
|
||||
// [Setter] 대표 캐릭터 세팅
|
||||
const setMainCharacter = (newCharacter: CharacterInfo) => {
|
||||
mainCharacter.value = newCharacter
|
||||
}
|
||||
// [Setter] 캐릭터 선택 세팅
|
||||
const setSelectCharacter = (newCharacter: CharacterInfo | null) => {
|
||||
selectCharacter.value = newCharacter as any
|
||||
}
|
||||
|
||||
// [Computed] 캐릭터 목록 존재 여부
|
||||
const hasNoCharacterList: ComputedRef<boolean> = computed(() => {
|
||||
return characterList.value == null || characterList.value.length === 0
|
||||
})
|
||||
|
||||
/**
|
||||
* GUID 조회
|
||||
*
|
||||
* @param {ReqGetGuid} req
|
||||
* @description https://developers-beta.onstove.com/ko/docs/web/member/api_member_game_id
|
||||
*/
|
||||
const getGuid = async (req: ReqGetGuid) => {
|
||||
let res: ResGetGuid = {} as ResGetGuid
|
||||
try {
|
||||
const baseApiUrl = req.baseApiUrl || ''
|
||||
|
||||
const url = `${baseApiUrl}/member/v3.0/${req.game_id}`
|
||||
const headers = {
|
||||
Authorization: `Bearer ${req.accessToken}`,
|
||||
}
|
||||
|
||||
res = (await commonFetch('GET', url, { headers })) as ResGetGuid
|
||||
|
||||
if (res != null) {
|
||||
if (res.code === 0 && res.value != null && res.value?.guid != null) {
|
||||
setHasGuid(res.value?.guid > 0)
|
||||
} else {
|
||||
res = { code: res.code, message: res.message || '' }
|
||||
console.log(`${logPrefix.failure}.getGuid: `, res)
|
||||
res.code = -99999 // else 알럿 띄우기 용 세팅
|
||||
setHasGuid(false)
|
||||
}
|
||||
} else {
|
||||
console.log(`${logPrefix.failure}.getGuid - res is null: `, res)
|
||||
res = { code: -99999, message: '' }
|
||||
setHasGuid(false)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`${logPrefix.exception}.getGuid: `, e)
|
||||
res = { code: -99999, message: `${e}` }
|
||||
setHasGuid(false)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* 보유 캐릭터 목록 조회
|
||||
*
|
||||
* @param {ReqGameCharacterList} req
|
||||
* @description https://wiki.smilegate.net/pages/viewpage.action?pageId=193659225
|
||||
*/
|
||||
const getCharacterList = async (req: ReqGameCharacterList) => {
|
||||
let res: ResGameCharacterList = {} as ResGameCharacterList
|
||||
try {
|
||||
const baseApiUrl = req.baseApiUrl || ''
|
||||
|
||||
const url = `${baseApiUrl}/game/v2.1/${req.game_id}/character`
|
||||
const headers = {
|
||||
Authorization: `Bearer ${req.accessToken}`,
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
}
|
||||
|
||||
res = (await commonFetch('GET', url, { headers })) as ResGameCharacterList
|
||||
|
||||
if (res != null) {
|
||||
if (res.code === 0) {
|
||||
const characterInfos =
|
||||
res.value?.character_infos || ([] as CharacterInfo[])
|
||||
|
||||
// '[서버명] 캐릭터명' 포맷의 라벨용 네이밍 세팅
|
||||
characterInfos.forEach(characterInfo => {
|
||||
const characterName = `${characterInfo.name}`
|
||||
let formattedNm = `${characterName}`
|
||||
|
||||
// 넘어온 월드 목록이 있는 경우 해당 목록에서 다국어로 된 정보 조회
|
||||
if (req.world_list != null && req.world_list.length > 0) {
|
||||
const worldNm =
|
||||
req.world_list.find(
|
||||
world => world.world_id === characterInfo?.world_id
|
||||
)?.world_nm || ''
|
||||
if (worldNm != null && worldNm !== '') {
|
||||
formattedNm = `[${worldNm}] ${characterName}`
|
||||
}
|
||||
}
|
||||
characterInfo.formatted_nm = formattedNm
|
||||
})
|
||||
|
||||
setCharacterList(characterInfos)
|
||||
setMainCharacter(
|
||||
res.value?.main_game_character || ({} as CharacterInfo)
|
||||
)
|
||||
} else if (res.code === 515) {
|
||||
// [515] AccessToken이 유효하지 않을 경우
|
||||
res = { code: res.code, message: res.message || '' }
|
||||
console.log(`${logPrefix.failure}.getCharacterList: `, res)
|
||||
setCharacterList([] as CharacterInfo[])
|
||||
setMainCharacter({} as CharacterInfo)
|
||||
} else {
|
||||
// [501] 캐릭터가 존재하지 않을 경우(member_no, game_no, character_id 기준)
|
||||
// [502] 유효하지 않거나 잘못된 파라미터로 호출할 경우
|
||||
// [701] 존재하지 않은 게임일 경우(game_no 기준)
|
||||
res = { code: res.code, message: res.message || '' }
|
||||
console.log(`${logPrefix.failure}.getCharacterList: `, res)
|
||||
res.code = -99999 // else 알럿 띄우기 용 세팅
|
||||
setCharacterList([] as CharacterInfo[])
|
||||
setMainCharacter({} as CharacterInfo)
|
||||
}
|
||||
} else {
|
||||
res = { code: -99999, message: '' }
|
||||
console.log(`${logPrefix.failure}.getCharacterList: `, res)
|
||||
setCharacterList([] as CharacterInfo[])
|
||||
setMainCharacter({} as CharacterInfo)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`${logPrefix.exception}.getCharacterList: `, e)
|
||||
res = { code: -99999, message: `${e}` }
|
||||
setCharacterList([] as CharacterInfo[])
|
||||
setMainCharacter({} as CharacterInfo)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
return {
|
||||
hasGuid,
|
||||
characterList,
|
||||
mainCharacter,
|
||||
selectCharacter,
|
||||
hasNoCharacterList,
|
||||
getGuid,
|
||||
setCharacterList,
|
||||
setMainCharacter,
|
||||
setSelectCharacter,
|
||||
getCharacterList,
|
||||
}
|
||||
}
|
||||
|
||||
export { useGameLinkedData }
|
||||
222
layers/composables/useCoupon.ts
Normal file
222
layers/composables/useCoupon.ts
Normal file
@@ -0,0 +1,222 @@
|
||||
import {
|
||||
getUnixTime,
|
||||
startOfDay,
|
||||
endOfDay,
|
||||
differenceInMonths,
|
||||
} from '@seed-next/date'
|
||||
|
||||
import type {
|
||||
ReqCouponUseParams,
|
||||
ReqCouponUse,
|
||||
ResCouponUse,
|
||||
ResCouponList,
|
||||
ResCouponListItem,
|
||||
ReqCouponList,
|
||||
ReqCouponListParams,
|
||||
} from '#layers/types/api/couponData'
|
||||
|
||||
/**
|
||||
* 쿠폰 관련 Composable 함수입니다.
|
||||
* @returns
|
||||
*/
|
||||
export const useCoupon = () => {
|
||||
// Refs
|
||||
const couponList = ref<Array<ResCouponListItem> | null>([])
|
||||
const totalCount = ref<number>(0)
|
||||
const hasNoCouponList = ref<boolean>(false)
|
||||
|
||||
/**
|
||||
* 쿠폰 등록 내역 데이터를 세팅합니다.
|
||||
* @param {ResCouponList} newValue - 쿠폰 등록 내역 데이터
|
||||
* @returns
|
||||
*/
|
||||
const setCouponList = (newValue: ResCouponList) => {
|
||||
if (newValue.value.total_count === 0) {
|
||||
hasNoCouponList.value = true
|
||||
couponList.value = null
|
||||
totalCount.value = 0
|
||||
return
|
||||
} else {
|
||||
hasNoCouponList.value = false
|
||||
couponList.value = newValue.value.list
|
||||
totalCount.value = newValue.value.total_count
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 쿠폰 등록 API 호출 함수입니다.
|
||||
* @param {ReqCouponUse} req - 쿠폰 등록 API 요청 데이터
|
||||
* @returns {ResCouponUse} res - 쿠폰 등록 API 응답 데이터
|
||||
*/
|
||||
const postCouponUse = async (req: ReqCouponUse) => {
|
||||
let res: ResCouponUse = {} as ResCouponUse
|
||||
try {
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const stoveApiUrl = runtimeConfig.public.stoveApiUrl
|
||||
const userTokenType = req.user_token_type || 'web'
|
||||
|
||||
const headers = {
|
||||
Authorization: `Bearer ${req.accessToken}`,
|
||||
}
|
||||
const query: ReqCouponUseParams = {
|
||||
game_code: req.game_code || '',
|
||||
coupon_no: req.coupon_no || '',
|
||||
client_ipaddr: req.client_ipaddr || '',
|
||||
world_no: req.world_no || '',
|
||||
character_no: req.character_no || '',
|
||||
}
|
||||
|
||||
const apiUrl = `${stoveApiUrl}/coupon/v3.0/${userTokenType}/coupon/use`
|
||||
|
||||
res = (await commonFetch('POST', apiUrl, {
|
||||
headers,
|
||||
query,
|
||||
})) as ResCouponUse
|
||||
|
||||
return res
|
||||
} catch (error) {
|
||||
console.error('[Exception] useCoupon.postCouponUse:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 쿠폰 등록 내역 API 호출 함수입니다.
|
||||
* @param {ReqCouponList} req - 쿠폰 등록 내역 API 요청 데이터
|
||||
* @returns {ResCouponList} res - 쿠폰 등록 내역 API 응답 데이터
|
||||
*/
|
||||
const getCouponList = async (req: ReqCouponList) => {
|
||||
let res: ResCouponList = {} as ResCouponList
|
||||
try {
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const stoveApiUrl = runtimeConfig.public.stoveApiUrl
|
||||
const userTokenType = req.user_token_type || 'web'
|
||||
|
||||
const headers = {
|
||||
Authorization: `Bearer ${req.accessToken}`,
|
||||
}
|
||||
const query: ReqCouponListParams = {
|
||||
game_code: req.game_code,
|
||||
start_date: req.start_date,
|
||||
end_date: req.end_date,
|
||||
use_state_code: req.use_state_code,
|
||||
page_size: req.page_size,
|
||||
page_no: req.page_no,
|
||||
lang_code: req.lang_code,
|
||||
}
|
||||
|
||||
const apiUrl = `${stoveApiUrl}/coupon/v3.0/${userTokenType}/couponbox/list`
|
||||
|
||||
res = (await commonFetch('GET', apiUrl, {
|
||||
headers,
|
||||
query,
|
||||
})) as ResCouponList
|
||||
|
||||
if (res.result === '000') {
|
||||
setCouponList(res)
|
||||
} else {
|
||||
console.error('[Exception] useCoupon.getCouponList:', res.message)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Exception] useCoupon.getCouponList:', error)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
return {
|
||||
couponList,
|
||||
totalCount,
|
||||
hasNoCouponList,
|
||||
|
||||
postCouponUse,
|
||||
getCouponList,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 쿠폰 조회 관련 Composable 함수입니다.
|
||||
* @returns
|
||||
*/
|
||||
export const useCouponDate = () => {
|
||||
// Refs
|
||||
const startDate = ref<Date | null>(null)
|
||||
const endDate = ref<Date | null>(null)
|
||||
const searchStatus = ref<number>(0) // 0: 전체, 1: 사용전, 2: 사용완료/기간만료/사용마감
|
||||
|
||||
// Computed
|
||||
const currentSearchPeriod = computed(() => {
|
||||
if (!startDate.value || !endDate.value) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return differenceInMonths(endDate.value, startDate.value)
|
||||
})
|
||||
|
||||
// Setting Functions
|
||||
const setCouponDate = (newDate: Date, type: 'start' | 'end') => {
|
||||
if (newDate === null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (type === 'start') {
|
||||
startDate.value = newDate
|
||||
} else {
|
||||
endDate.value = newDate
|
||||
}
|
||||
}
|
||||
|
||||
const setSearchStatus = (newStatus: number) => {
|
||||
searchStatus.value = newStatus
|
||||
}
|
||||
|
||||
// Unix(초) 단위 타임스탬프 변환 (해당 날짜의 시작/종료 시간)
|
||||
const toUnixTimestamp = (date: Date, type: 'start' | 'end') => {
|
||||
if (date === null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (type === 'start') {
|
||||
return getUnixTime(startOfDay(date))
|
||||
} else {
|
||||
return getUnixTime(endOfDay(date))
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
startDate,
|
||||
endDate,
|
||||
currentSearchPeriod,
|
||||
searchStatus,
|
||||
|
||||
toUnixTimestamp,
|
||||
setCouponDate,
|
||||
setSearchStatus,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 쿠폰 페이징 관련 Composable 함수입니다.
|
||||
* @returns
|
||||
*/
|
||||
export const useCouponPaging = () => {
|
||||
const pageNo = ref(1) // 현재 페이지 번호
|
||||
const pageSize = ref(20) // 페이지 사이즈
|
||||
const pageBlock = ref(10) // 페이지 블록 사이즈
|
||||
|
||||
const updatePagination = (no: number) => {
|
||||
pageNo.value = no
|
||||
}
|
||||
|
||||
const getPageBlock = (deviceMode: string) => {
|
||||
return deviceMode === 'desktop' ? pageBlock.value : 5
|
||||
}
|
||||
|
||||
return {
|
||||
pageNo,
|
||||
pageSize,
|
||||
pageBlock,
|
||||
|
||||
updatePagination,
|
||||
getPageBlock,
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,8 @@ import GrDetail02 from '#layers/templates/GrDetail02/index.vue'
|
||||
import GrDetail03 from '#layers/templates/GrDetail03/index.vue'
|
||||
import GrBoard01 from '#layers/templates/GrBoard01/index.vue'
|
||||
import GrContents01 from '#layers/templates/GrContents01/index.vue'
|
||||
import FxVideo01 from '#layers/templates/FxVideo01/index.vue'
|
||||
import FxDownload01 from '#layers/templates/FxDownload01/index.vue'
|
||||
import FxCoupon01 from '#layers/templates/FxCoupon01/index.vue'
|
||||
import FxSecure01 from '#layers/templates/FxSecure01/index.vue'
|
||||
import FxPreregist01 from '#layers/templates/FxPreregist01/index.vue'
|
||||
|
||||
@@ -26,8 +26,8 @@ const templateRegistry = {
|
||||
GR_DETAIL_02: { component: GrDetail02 },
|
||||
GR_DETAIL_03: { component: GrDetail03 },
|
||||
GR_CONTENTS_01: { component: GrContents01 },
|
||||
FX_VIDEO_01: { component: FxVideo01 },
|
||||
FX_DOWNLOAD_01: { component: FxDownload01 },
|
||||
FX_COUPON_01: { component: FxCoupon01 },
|
||||
FX_SECURE_01: { component: FxSecure01 },
|
||||
FX_PREREGIST_01: { component: FxPreregist01 },
|
||||
} as const
|
||||
|
||||
6
layers/plugins/datepicker.ts
Normal file
6
layers/plugins/datepicker.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import VueDatePicker from '@vuepic/vue-datepicker'
|
||||
import '@vuepic/vue-datepicker/dist/main.css'
|
||||
|
||||
export default defineNuxtPlugin(nuxtApp => {
|
||||
nuxtApp.vueApp.component('VueDatePicker', VueDatePicker)
|
||||
})
|
||||
111
layers/stores/useCouponStore.ts
Normal file
111
layers/stores/useCouponStore.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import type { WorldInfo, CharacterInfo } from '#layers/types/api/gameLinkedData'
|
||||
|
||||
export const useCouponStore = defineStore('couponStore', () => {
|
||||
// Refs
|
||||
const couponNo = ref('')
|
||||
const worldList = ref<Array<WorldInfo>>([] as Array<WorldInfo>)
|
||||
const memberNo = ref<number>(0)
|
||||
const guid = ref<number>(0)
|
||||
const selectCharacter = ref(null as CharacterInfo | null)
|
||||
const nickName = ref('')
|
||||
const selectWorld = ref({} as WorldInfo)
|
||||
|
||||
// Computed
|
||||
const isSelectCharacter = computed(() => {
|
||||
return (
|
||||
selectCharacter.value !== null &&
|
||||
selectCharacter.value.character_id !== null &&
|
||||
selectCharacter.value.character_id !== ''
|
||||
)
|
||||
})
|
||||
|
||||
/**
|
||||
* 월드 목록 업데이트 함수입니다.
|
||||
* @param {Array<WorldInfo>} newWorldList - 월드 목록
|
||||
*/
|
||||
const updateWorldList = (newWorldList: Array<WorldInfo>) => {
|
||||
worldList.value = newWorldList
|
||||
}
|
||||
|
||||
/**
|
||||
* 멤버번호 업데이트 함수입니다.
|
||||
* @param {number} newMemberNo - 멤버번호
|
||||
*/
|
||||
const updateMemberNo = (newMemberNo: number) => {
|
||||
memberNo.value = newMemberNo
|
||||
}
|
||||
|
||||
/**
|
||||
* GUID 업데이트 함수입니다.
|
||||
* @param {number} newGuid - GUID
|
||||
*/
|
||||
const updateGuid = (newGuid: number) => {
|
||||
guid.value = newGuid
|
||||
}
|
||||
|
||||
/**
|
||||
* 쿠폰 번호 업데이트 함수입니다.
|
||||
* @param {string} newCouponNo - 쿠폰 번호
|
||||
*/
|
||||
const updateCouponNo = (newCouponNo: string) => {
|
||||
couponNo.value = newCouponNo
|
||||
}
|
||||
|
||||
/**
|
||||
* 선택한 캐릭터 업데이트 함수입니다.
|
||||
* @param {CharacterInfo | null} newCharacter - 선택한 캐릭터 (null인 경우 초기화)
|
||||
*/
|
||||
const updateSelectCharacter = (newCharacter: CharacterInfo | null) => {
|
||||
selectCharacter.value = newCharacter
|
||||
}
|
||||
|
||||
/**
|
||||
* 닉네임 업데이트 함수입니다.
|
||||
* @param {string} newNickname - 닉네임
|
||||
*/
|
||||
const updateNickname = (newNickname: string) => {
|
||||
nickName.value = newNickname
|
||||
}
|
||||
|
||||
/**
|
||||
* 선택한 월드 업데이트 함수입니다.
|
||||
* @param {WorldInfo} newWorld - 선택한 월드
|
||||
*/
|
||||
const updateSelectWorld = (newWorld: WorldInfo) => {
|
||||
selectWorld.value = newWorld
|
||||
}
|
||||
|
||||
/**
|
||||
* 쿠폰 번호 빈값 여부 검증 함수입니다.
|
||||
* 빈값인 경우 true, 빈값이 아닌 경우 false를 반환합니다.
|
||||
* @param {string} value - 쿠폰 번호
|
||||
* @returns
|
||||
*/
|
||||
const isEmptyCouponNo = (value: string) => {
|
||||
if (value === '') {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
couponNo,
|
||||
worldList,
|
||||
memberNo,
|
||||
guid,
|
||||
selectCharacter,
|
||||
nickName,
|
||||
selectWorld,
|
||||
isSelectCharacter,
|
||||
|
||||
updateWorldList,
|
||||
updateMemberNo,
|
||||
updateGuid,
|
||||
updateSelectCharacter,
|
||||
updateNickname,
|
||||
updateSelectWorld,
|
||||
updateCouponNo,
|
||||
isEmptyCouponNo,
|
||||
}
|
||||
})
|
||||
855
layers/templates/FxCoupon01/index.vue
Normal file
855
layers/templates/FxCoupon01/index.vue
Normal file
@@ -0,0 +1,855 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
getTime,
|
||||
fromUnixTime,
|
||||
addMonths,
|
||||
differenceInDays,
|
||||
} from '@seed-next/date'
|
||||
import { getComponentGroup } from '#layers/utils/dataUtil'
|
||||
import {
|
||||
COUPON_NO_LENGTH_LIMIT,
|
||||
COUPON_RESULT,
|
||||
} from '#layers/types/api/couponData'
|
||||
import type { PageDataTemplateComponents } from '#layers/types/api/pageData'
|
||||
import type { ReqCouponList } from '#layers/types/api/couponData'
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
components: PageDataTemplateComponents
|
||||
pageVerTmplSeq: number
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
|
||||
// Configuration
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const runType = runtimeConfig.public.runType as string
|
||||
const staticUrl = runtimeConfig.public.staticUrl as string
|
||||
const stoveApiUrl = runtimeConfig.public.stoveApiUrl as string
|
||||
const stoveMaintenanceApiUrl = runtimeConfig.public
|
||||
.stoveMaintenanceApiUrl as string
|
||||
const multilingualBaseApiUrl = `${staticUrl}/${runType}/test`
|
||||
const multilingualFileName = 'test_homepage_brand_coupon.json'
|
||||
|
||||
// Multilingual
|
||||
const resultGetMultilingual = await useGetMultilingual({
|
||||
baseApiUrl: multilingualBaseApiUrl,
|
||||
fileName: multilingualFileName,
|
||||
})
|
||||
const { t, tm, locale }: any = useI18n({
|
||||
useScope: 'local',
|
||||
messages: Object(resultGetMultilingual?.value?.multilingual),
|
||||
})
|
||||
|
||||
// Composables
|
||||
const { isGameMaintenance, checkGameMaintenance } = useGetGameMaintenance()
|
||||
const { isWebInspection, getInspectionDataExternal } =
|
||||
useGetInspectionDataExternal()
|
||||
const breakpoints = useResponsiveBreakpoints()
|
||||
const { handleTokenValidation } = useTokenValidation()
|
||||
const { hasGuid, characterList, getGuid, getCharacterList } =
|
||||
useGameLinkedData()
|
||||
const {
|
||||
couponList,
|
||||
hasNoCouponList,
|
||||
totalCount,
|
||||
postCouponUse,
|
||||
getCouponList,
|
||||
} = useCoupon()
|
||||
const {
|
||||
startDate,
|
||||
endDate,
|
||||
currentSearchPeriod,
|
||||
searchStatus,
|
||||
setCouponDate,
|
||||
toUnixTimestamp,
|
||||
} = useCouponDate()
|
||||
const { pageNo, pageSize, pageBlock, updatePagination, getPageBlock } =
|
||||
useCouponPaging()
|
||||
|
||||
// Store
|
||||
const gameDataStore = useGameDataStore()
|
||||
const modalStore = useModalStore()
|
||||
const couponStore = useCouponStore()
|
||||
const { gameData } = storeToRefs(gameDataStore)
|
||||
const { handleOpenAlert, handleOpenConfirm } = modalStore
|
||||
const { couponNo, isSelectCharacter, selectCharacter } =
|
||||
storeToRefs(couponStore)
|
||||
const {
|
||||
updateMemberNo,
|
||||
updateCouponNo,
|
||||
updateSelectCharacter,
|
||||
isEmptyCouponNo,
|
||||
} = couponStore
|
||||
|
||||
// Data
|
||||
const backgroundData = computed(() =>
|
||||
getComponentGroup(props.components, 'background')
|
||||
)
|
||||
|
||||
// Refs
|
||||
const clientIp = ref('')
|
||||
const monthSelectList = ref<Array<number>>([1, 3, 6, 12])
|
||||
const isSelectCharacterModalOpen = ref(false)
|
||||
|
||||
// Computed
|
||||
const minDate = computed(() => {
|
||||
const date = new Date()
|
||||
date.setHours(0, 0, 0, 0)
|
||||
date.setFullYear(date.getFullYear() - 1)
|
||||
return date
|
||||
})
|
||||
const maxDate = computed(() => {
|
||||
const date = new Date()
|
||||
date.setHours(23, 59, 59, 999)
|
||||
return date
|
||||
})
|
||||
const sortedCharacterList = computed(() => {
|
||||
return characterList.value
|
||||
.map(characterInfo => {
|
||||
const worldId =
|
||||
characterInfo.world_id.split('_')[
|
||||
characterInfo.world_id?.split('_').length - 1
|
||||
]
|
||||
const arrWorldIdPriority = ['global', 'asia']
|
||||
const sortedIndex = arrWorldIdPriority.indexOf(worldId)
|
||||
return { ...characterInfo, sortedIndex }
|
||||
})
|
||||
.sort((a, b) => {
|
||||
return a.sortedIndex !== b.sortedIndex ? a.sortedIndex - b.sortedIndex : 0
|
||||
})
|
||||
})
|
||||
|
||||
// Functions
|
||||
/**
|
||||
* @description 기본 Alert 모달 팝업 함수입니다.
|
||||
* @param text - 모달 내용
|
||||
*/
|
||||
const openAlert = (text: string) => {
|
||||
handleOpenAlert({
|
||||
isShowDimmed: true,
|
||||
contentText: text,
|
||||
isOutsideClose: true,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 유저 IP 조회 함수입니다.
|
||||
*/
|
||||
const getClientIp = async () => {
|
||||
if (!clientIp.value) {
|
||||
try {
|
||||
const ipData = await $fetch('/api/clientIp')
|
||||
clientIp.value = ipData || ''
|
||||
} catch (e) {
|
||||
console.error('[Exception] FxCoupon01.getClientIp:', e)
|
||||
clientIp.value = ''
|
||||
}
|
||||
} // 이미 조회했으면 재사용
|
||||
return clientIp.value
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 쿠폰 사용 전 유효성 체크 함수입니다.
|
||||
* @returns {number} 쿠폰 사용 전 유효성 체크 결과
|
||||
*/
|
||||
const validationCheckBefore = async () => {
|
||||
// FIXME: 다른 체크 로직 추가 시 주석 해제 필요 ---------------------------------------------------
|
||||
|
||||
/**
|
||||
* @description 게임 점검 체크하는 로직입니다.
|
||||
* await checkGameMaintenance({
|
||||
* baseApiUrl: stoveMaintenanceApiUrl,
|
||||
* category: 'GAME',
|
||||
* service_id1: gameData.value.game_id,
|
||||
* gameId: gameData.value.game_id,
|
||||
* lang: locale.value,
|
||||
* })
|
||||
* if (isGameMaintenance.value) {
|
||||
* return COUPON_RESULT.GAME_MAINTENANCE
|
||||
* }
|
||||
*/
|
||||
|
||||
/**
|
||||
* @description 언어 코드 존재 여부 체크하는 로직입니다.
|
||||
* if (locale.value == null || locale.value === '') {
|
||||
* return COUPON_RESULT.SYSTEM_ERROR
|
||||
* }
|
||||
*/
|
||||
|
||||
/**
|
||||
* @description 서버(캐릭터) 선택 여부 체크하는 로직입니다.
|
||||
* if (!isSelectCharacter.value) {
|
||||
* return COUPON_RESULT.SELECT_CHARACTER_REQUIRED
|
||||
* }
|
||||
*/
|
||||
|
||||
/**
|
||||
* @description 선택한 캐릭터 정보 유효 여부 (접속 계정과 동일한 캐릭터 여부) 체크하는 로직입니다.
|
||||
* await getCharacterList({
|
||||
* baseApiUrl: stoveApiUrl,
|
||||
* accessToken: csrGetAccessToken(),
|
||||
* game_id: gameData.value.game_id,
|
||||
* gameId: gameData.value.game_id,
|
||||
* })
|
||||
* if (
|
||||
* !characterList.value.some(
|
||||
* character => character.character_id === selectCharacter.value.character_id
|
||||
* )
|
||||
* ) {
|
||||
* return COUPON_RESULT.SELECT_CHARACTER_REQUIRED
|
||||
* }
|
||||
*/
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @description 웹 점검 여부 체크
|
||||
*/
|
||||
await getInspectionDataExternal({
|
||||
baseApiUrl: stoveApiUrl,
|
||||
gameId: gameData.value.game_id,
|
||||
})
|
||||
if (isWebInspection.value) {
|
||||
return COUPON_RESULT.WEB_INSPECTION
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 로그인 여부 체크
|
||||
*/
|
||||
const accessToken = csrGetAccessToken()
|
||||
const validateTokenResult = await handleTokenValidation(accessToken || '')
|
||||
if (validateTokenResult === false) {
|
||||
return COUPON_RESULT.LOGIN_REQUIRED
|
||||
}
|
||||
|
||||
/**
|
||||
* @description GUID(게임 이용 동의) 체크
|
||||
*/
|
||||
await getGuid({
|
||||
baseApiUrl: stoveApiUrl,
|
||||
accessToken: csrGetAccessToken(),
|
||||
game_id: gameData.value.game_id,
|
||||
gameId: gameData.value.game_id,
|
||||
})
|
||||
if (!hasGuid.value) {
|
||||
return COUPON_RESULT.EMPTY_GUID
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 쿠폰번호 입력 여부 체크
|
||||
*/
|
||||
if (isEmptyCouponNo(couponNo.value)) {
|
||||
return COUPON_RESULT.NULL_COUPON_NO
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 쿠폰번호 유효성 체크 (최소 2자, 최대 20자)
|
||||
*/
|
||||
if (
|
||||
!isInRange(
|
||||
couponNo.value.length,
|
||||
COUPON_NO_LENGTH_LIMIT.MIN,
|
||||
COUPON_NO_LENGTH_LIMIT.MAX
|
||||
)
|
||||
) {
|
||||
return COUPON_RESULT.FAIL_COUPON_FORMAT
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 쿠폰 등록 버튼 함수입니다.
|
||||
* @description FE 유효성 체크 후 캐릭터 선택 모달 팝업 노출합니다.
|
||||
*/
|
||||
const handleCouponUse = async () => {
|
||||
const validationCheckBeforeResult = await validationCheckBefore()
|
||||
|
||||
if (validationCheckBeforeResult !== 0) {
|
||||
if (
|
||||
validationCheckBeforeResult === COUPON_RESULT.SELECT_CHARACTER_REQUIRED
|
||||
) {
|
||||
openSelectCharacterModal()
|
||||
return false
|
||||
} else if (validationCheckBeforeResult === COUPON_RESULT.SYSTEM_ERROR) {
|
||||
openAlert(t('Coupon_Error', { code: validationCheckBeforeResult }))
|
||||
return false
|
||||
} else {
|
||||
openAlert(tm(`Coupon_Alert_(${validationCheckBeforeResult})`))
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
openSelectCharacterModal()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 캐릭터 선택 후, 확인 버튼 클릭 시 쿠폰 등록 API 호출 함수입니다.
|
||||
*/
|
||||
const handleCouponRegister = async () => {
|
||||
closeSelectCharacterModal()
|
||||
|
||||
const validationCheckBeforeResult = await validationCheckBefore()
|
||||
if (validationCheckBeforeResult !== 0) {
|
||||
if (
|
||||
validationCheckBeforeResult === COUPON_RESULT.SELECT_CHARACTER_REQUIRED
|
||||
) {
|
||||
openSelectCharacterModal()
|
||||
return false
|
||||
} else if (validationCheckBeforeResult === COUPON_RESULT.SYSTEM_ERROR) {
|
||||
openAlert(t('Coupon_Error', { code: validationCheckBeforeResult }))
|
||||
return false
|
||||
} else {
|
||||
openAlert(tm(`Coupon_Alert_(${validationCheckBeforeResult})`))
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
await getClientIp()
|
||||
const res = await postCouponUse({
|
||||
accessToken: csrGetAccessToken(),
|
||||
user_token_type: 'web',
|
||||
game_code: gameData.value.game_code.toString(),
|
||||
coupon_no: couponNo.value,
|
||||
client_ipaddr: clientIp.value,
|
||||
world_no: selectCharacter.value.world_id,
|
||||
character_no: `${selectCharacter.value.character_id}`,
|
||||
lang_code: locale.value,
|
||||
})
|
||||
if (res.code === 0) {
|
||||
openAlert(tm('Coupon_Alert_Success'))
|
||||
} else {
|
||||
openAlert(tm(`Coupon_Alert_(${res.code})`))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 기간 선택 이벤트 함수입니다.
|
||||
* @param {number} month - 선택한 기간 개월수
|
||||
*/
|
||||
const handlePeriodSelect = async (month: number) => {
|
||||
const today = new Date()
|
||||
const startTimestamp = toUnixTimestamp(today, 'start')
|
||||
const endTimestamp = toUnixTimestamp(today, 'end')
|
||||
|
||||
const newStartDate = addMonths(fromUnixTime(startTimestamp), -month)
|
||||
const newEndDate = fromUnixTime(endTimestamp)
|
||||
|
||||
setCouponDate(newStartDate, 'start')
|
||||
setCouponDate(newEndDate, 'end')
|
||||
|
||||
handlePaging(1)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 조회하기 이벤트 함수입니다.
|
||||
*/
|
||||
const handlePeriodSearch = async () => {
|
||||
if (!startDate.value || !endDate.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const accessToken = csrGetAccessToken()
|
||||
const validateTokenResult = await handleTokenValidation(accessToken || '')
|
||||
if (validateTokenResult === false) {
|
||||
return
|
||||
}
|
||||
|
||||
const result = differenceInDays(endDate.value, startDate.value)
|
||||
if (result > 365) {
|
||||
openAlert(tm('Coupon_Msg_OverDays'))
|
||||
return
|
||||
}
|
||||
|
||||
const req: ReqCouponList = {
|
||||
accessToken: accessToken,
|
||||
user_token_type: 'web',
|
||||
game_code: gameData.value.game_code.toString(),
|
||||
start_date: getTime(startDate.value),
|
||||
end_date: getTime(endDate.value),
|
||||
use_state_code: searchStatus.value,
|
||||
page_size: pageSize.value,
|
||||
page_no: pageNo.value,
|
||||
lang_code: locale.value,
|
||||
}
|
||||
|
||||
await getCouponList(req)
|
||||
}
|
||||
|
||||
/**
|
||||
* @desciprion 페이지 변경 이벤트 함수입니다.
|
||||
* @param {number} newPageNo
|
||||
*/
|
||||
const handlePaging = async (newPageNo: number) => {
|
||||
updatePagination(newPageNo)
|
||||
await handlePeriodSearch()
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 게임별 스토브 쿠폰함 URL 조회 함수입니다.
|
||||
* @returns {string} 게임별 스토브 쿠폰함 URL
|
||||
*/
|
||||
const getCouponBoxUrl = () => {
|
||||
let url = ''
|
||||
|
||||
if (breakpoints.value.isMobile) {
|
||||
url = getStoveCouponUrl('mobile')
|
||||
} else {
|
||||
url = getStoveCouponUrl('desktop')
|
||||
}
|
||||
|
||||
return `${url}?game_id=${gameData.value.game_id}`
|
||||
}
|
||||
|
||||
/**
|
||||
* @description @coupon{문구} 형식의 문구를 게임별 스토브 쿠폰함 URL의 a태그로 변환합니다.
|
||||
* @param {string} text - 변환할 문구
|
||||
* @returns {string} 변환된 문구 HTML
|
||||
* @description string으로 return하므로 v-dompurify-html 사용 필요
|
||||
*/
|
||||
const getKeyToCouponUrl = (text: string) => {
|
||||
const couponBoxUrl = getCouponBoxUrl()
|
||||
|
||||
let result = ''
|
||||
|
||||
if (text.includes('@coupon')) {
|
||||
result = text.replace(/@coupon\{(.*?)\}/g, (_, key) => {
|
||||
return `<a href="${couponBoxUrl}" rel="noopener noreferrer" target="_blank" class="text-[#3C75FF] underline decoration-solid decoration-auto underline-offset-2 after:content-[''] after:absolute after:top-1/2 after:left-0 after:translate-y-[-50%] after:w-full after:h-fuill after:bg-white after:opacity-0 transition-opacity duration-300 ease-in-out hover:after:opacity-20">${key}</a>`
|
||||
})
|
||||
|
||||
return result
|
||||
} else {
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description STOVE 쿠폰함으로 이동합니다.
|
||||
*/
|
||||
const goToCouponBox = () => {
|
||||
let url = ''
|
||||
|
||||
if (breakpoints.value.isMobile) {
|
||||
url = getStoveCouponUrl('mobile')
|
||||
} else {
|
||||
url = getStoveCouponUrl('desktop')
|
||||
}
|
||||
|
||||
csrGoExternalLink(`${url}?game_id=${gameData.value.game_id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 쿠폰 등록 내역의 사용하기 버튼의 클릭 이벤트 함수입니다.
|
||||
*/
|
||||
const handleGoToCouponBox = async () => {
|
||||
const accessToken = csrGetAccessToken()
|
||||
const validateTokenResult = await handleTokenValidation(accessToken || '')
|
||||
|
||||
// 미로그인 상태일 경우 Return
|
||||
if (validateTokenResult === false) {
|
||||
return
|
||||
}
|
||||
|
||||
// 로그인 상태일 경우 모달 표시
|
||||
handleOpenConfirm({
|
||||
contentText: tm('Coupon_Alert_StoveCouponBox'),
|
||||
confirmButtonText: tm('Coupon_StoveCouponBox'),
|
||||
modalName: 'modal-coupon-use',
|
||||
confirmButtonEvent: () => {
|
||||
goToCouponBox()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 쿠폰 등록 관련 데이터를 초기화합니다.
|
||||
*/
|
||||
const initCouponData = () => {
|
||||
updateSelectCharacter(null)
|
||||
updateCouponNo('')
|
||||
updatePagination(1)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 캐릭터 선택 모달 팝업 열기
|
||||
*/
|
||||
const openSelectCharacterModal = () => {
|
||||
isSelectCharacterModalOpen.value = true
|
||||
}
|
||||
/**
|
||||
* @description 캐릭터 선택 모달 팝업 닫기
|
||||
*/
|
||||
const closeSelectCharacterModal = () => {
|
||||
isSelectCharacterModalOpen.value = false
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
initCouponData()
|
||||
|
||||
await handlePeriodSelect(1)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<WidgetsFixMainTitle
|
||||
:title="tm('Coupon_Page_Title')"
|
||||
:resourcesData="backgroundData"
|
||||
/>
|
||||
|
||||
<div class="section-container static">
|
||||
<section class="section-static">
|
||||
<WidgetsFixSubTitle :title="tm('Coupon_Section_Registration_Title')" />
|
||||
|
||||
<div
|
||||
class="relative flex flex-col items-center jutify-start w-full bg-white p-[20px] mt-[16px] rounded-[12px] sm:rounded-[16px] md:p-[32px] md:mt-[24px]"
|
||||
>
|
||||
<div
|
||||
class="relative flex flex-col items-center justify-start gap-[16px] w-full max-w-[880px] md:gap-[20px]"
|
||||
>
|
||||
<div
|
||||
novalidate
|
||||
class="relative flex flex-col items-center justify-start gap-[12px] w-full sm:flex-row"
|
||||
>
|
||||
<AtomsInput
|
||||
:model-value="couponNo"
|
||||
:name="tm('Coupon_Enter_Number')"
|
||||
:placeholder="tm('Coupon_Enter_Number_Please')"
|
||||
maxlength="20"
|
||||
@update:model-value="updateCouponNo"
|
||||
/>
|
||||
<AtomsButton
|
||||
type="action"
|
||||
button-size="size-small"
|
||||
background-color="#383838"
|
||||
text-color="#FFFFFF"
|
||||
class="relative flex items-center justify-center w-full gap-[4px] px-0 sm:w-[143px] sm:shrink-0 md:h-[56px] md:text-[16px] md:leading-[24px] md:tracking-[-0.48px]"
|
||||
@click="handleCouponUse"
|
||||
>
|
||||
<span>{{ tm('Coupon_Registration') }}</span>
|
||||
<AtomsIconsCouponFill
|
||||
:size="breakpoints.isMobile ? 16 : 20"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
</AtomsButton>
|
||||
</div>
|
||||
|
||||
<ul class="relative flex flex-col items-start justify-start w-full">
|
||||
<template v-for="notice in tm('Coupon_Notice_List')" :key="notice">
|
||||
<li
|
||||
class="relative flex items-start justify-start w-full pl-[20px] before:content-[''] before:absolute before:top-[10px] before:left-[9px] before:w-[3px] before:h-[3px] before:rounded-full before:bg-[#666666] text-left text-[#666666] text-[13px] font-[400] leading-[22px] tracking-[-0.325px] md:text-[14px] md:leading-[24px] md:tracking-[-0.42px] md:before:left-[8px] md:before:w-[4px] md:before:h-[4px]"
|
||||
>
|
||||
<span
|
||||
v-dompurify-html="
|
||||
getKeyToCouponUrl(tm(notice as string) as string)
|
||||
"
|
||||
></span>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section-static">
|
||||
<WidgetsFixSubTitle :title="tm('Coupon_Section_History_Title')">
|
||||
<div
|
||||
class="relative flex flex-col items-start justify-start gap-[12px] w-full mt-[16px] md:gap-[16px] md:mt-[24px] lg:flex-row lg:items-end lg:justify-between lg:gap-[0]"
|
||||
>
|
||||
<div
|
||||
class="relative flex items-center justify-start gap-[20px] w-full lg:w-auto lg:mr-auto"
|
||||
>
|
||||
<div
|
||||
v-if="breakpoints.isMd || breakpoints.isDesktop"
|
||||
class="relative flex items-center justify-start"
|
||||
>
|
||||
<template v-for="month in monthSelectList" :key="month">
|
||||
<AtomsButton
|
||||
type="action"
|
||||
button-size="size-small"
|
||||
background-color="#FAFAFA"
|
||||
text-color="#CCCCCC"
|
||||
:class="[
|
||||
'btn-period',
|
||||
{ 'btn-period-active': currentSearchPeriod === month },
|
||||
]"
|
||||
@click="handlePeriodSelect(month)"
|
||||
>
|
||||
<span>{{ tm(`Coupon_Month${month}`) }}</span>
|
||||
</AtomsButton>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="relative flex items-center justify-center gap-[8px] w-full md:w-auto"
|
||||
>
|
||||
<div
|
||||
class="relative flex items-center justify-center gap-[4px] md:gap-[8px] w-full md:w-auto"
|
||||
>
|
||||
<BlocksDatePicker
|
||||
:key="getTime(startDate)"
|
||||
:date="startDate"
|
||||
:min-date="minDate"
|
||||
:max-date="maxDate"
|
||||
@update:date="
|
||||
(date: string) => setCouponDate(new Date(date), 'start')
|
||||
"
|
||||
/>
|
||||
<span
|
||||
class="relative inline-flex items-center justify-center shrink-0 text-[#868078] text-[16px] font-[500] leading-[24px] tracking-[-0.48px] md:text-[18px] md:leading-[26px] md:tracking-[-0.54px]"
|
||||
>
|
||||
~
|
||||
</span>
|
||||
<BlocksDatePicker
|
||||
:key="getTime(endDate)"
|
||||
:date="endDate"
|
||||
:min-date="minDate"
|
||||
:max-date="maxDate"
|
||||
@update:date="
|
||||
(date: string) => setCouponDate(new Date(date), 'end')
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<AtomsButton
|
||||
type="action"
|
||||
button-size="size-small"
|
||||
background-color="#383838"
|
||||
text-color="#FFFFFF"
|
||||
class="shrink-0 w-[40px] h-[40px] p-0 md:w-auto md:px-[22px]"
|
||||
@click="handlePeriodSearch"
|
||||
>
|
||||
<span
|
||||
class="relative flex items-center justify-center gap-[2px]"
|
||||
>
|
||||
<span v-if="breakpoints.isMd || breakpoints.isDesktop">
|
||||
{{ tm('Coupon_Search') }}
|
||||
</span>
|
||||
<AtomsIconsSearchLine :size="16" color="#FFFFFF" />
|
||||
</span>
|
||||
</AtomsButton>
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
class="text-[#666666] text-[13px] font-[400] leading-[22px] tracking-[-0.325px] md:text-[14px] md:leading-[24px] md:tracking-[-0.421px]"
|
||||
>
|
||||
{{ tm('Coupon_Section_History_Description') }}
|
||||
</p>
|
||||
</div>
|
||||
</WidgetsFixSubTitle>
|
||||
|
||||
<div
|
||||
class="relative w-full border border-solid border-[#D9D9D9] rounded-[12px] overflow-hidden sm:rounded-[16px]"
|
||||
>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="!border-l-[0]">
|
||||
{{ tm('Coupon_Item_Name') }}
|
||||
</th>
|
||||
<th
|
||||
v-if="breakpoints.isMd || breakpoints.isDesktop"
|
||||
class="w-[260px]"
|
||||
>
|
||||
{{ tm('Coupon_Item_RegistDate') }}
|
||||
</th>
|
||||
<th class="w-[90px] md:w-[180px]">
|
||||
{{ tm('Coupon_Item_Status') }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-if="hasNoCouponList">
|
||||
<tr>
|
||||
<td
|
||||
:colspan="
|
||||
breakpoints.isMd || breakpoints.isDesktop ? '3' : '2'
|
||||
"
|
||||
class="!border-l-[0] !border-b-[0]"
|
||||
>
|
||||
<p>{{ tm('Coupon_Has_Not_Item') }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template
|
||||
v-for="(coupon, couponIdx) in couponList"
|
||||
:key="coupon.coupon_box_id"
|
||||
>
|
||||
<tr>
|
||||
<td
|
||||
:class="
|
||||
couponIdx === couponList.length - 1
|
||||
? '!border-l-[0] !border-b-[0]'
|
||||
: '!border-l-[0]'
|
||||
"
|
||||
>
|
||||
<p>{{ coupon.coupon_name }}</p>
|
||||
</td>
|
||||
<td
|
||||
v-if="breakpoints.isMd || breakpoints.isDesktop"
|
||||
:class="
|
||||
couponIdx === couponList.length - 1 ? '!border-b-[0]' : ''
|
||||
"
|
||||
>
|
||||
<p>{{ coupon.register_date }}</p>
|
||||
</td>
|
||||
<td
|
||||
:class="
|
||||
couponIdx === couponList.length - 1 ? '!border-b-[0]' : ''
|
||||
"
|
||||
>
|
||||
<template
|
||||
v-if="
|
||||
coupon.use_state === 1 &&
|
||||
coupon.reward_type_code.includes(2)
|
||||
"
|
||||
>
|
||||
<AtomsButton
|
||||
type="action"
|
||||
button-size="size-small"
|
||||
background-color="transparent"
|
||||
text-color="transparent"
|
||||
class="coupon-item coupon-item-use"
|
||||
@click="handleGoToCouponBox"
|
||||
>
|
||||
<span>{{ tm('Coupon_Item_Use') }}</span>
|
||||
</AtomsButton>
|
||||
</template>
|
||||
<template v-else-if="coupon.use_state === 1">
|
||||
<span class="coupon-item coupon-item-not-used">
|
||||
{{ tm('Coupon_Item_Not_Used') }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-else-if="coupon.use_state === 2">
|
||||
<span class="coupon-item coupon-item-used">
|
||||
{{ tm('Coupon_Item_Used') }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-else-if="coupon.use_state === 4">
|
||||
<span class="coupon-item coupon-item-expires">
|
||||
{{ tm('Coupon_Item_Expires') }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-else-if="coupon.use_state === 6">
|
||||
<span class="coupon-item coupon-item-deadline">
|
||||
{{ tm('Coupon_Item_Deadline') }}
|
||||
</span>
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<BlocksPagination
|
||||
:key="pageNo"
|
||||
:current-page="pageNo"
|
||||
:page-size="pageSize"
|
||||
:page-block="getPageBlock(breakpoints.isMobile ? 'mobile' : 'desktop')"
|
||||
:total-count="totalCount"
|
||||
:use-min-max="true"
|
||||
@update:page="handlePaging"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<ClientOnly>
|
||||
<Teleport to="body">
|
||||
<BlocksModalLayer
|
||||
:is-show-dimmed="true"
|
||||
:is-outside-close="true"
|
||||
modal-name="modal-coupon-character-select"
|
||||
:is-open="isSelectCharacterModalOpen"
|
||||
area-class="max-w-[480px] p-6 bg-white rounded-[20px]"
|
||||
close-class="hidden"
|
||||
>
|
||||
<div
|
||||
class="relative flex flex-col items-center justify-center gap-[24px] w-full"
|
||||
>
|
||||
<p
|
||||
class="relative flex items-start justify-center w-full text-center text-[#333333] text-[15px] font-[400] leading-[24px] tracking-[-0.45px]"
|
||||
v-dompurify-html="tm('Coupon_Alert_SelectCharacter')"
|
||||
></p>
|
||||
|
||||
<AtomsSelect
|
||||
:options="sortedCharacterList"
|
||||
label-name="formatted_nm"
|
||||
:placeholder="tm('Coupon_Alert_EnterCharacter')"
|
||||
@update:model-value="updateSelectCharacter"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="relative flex items-center justify-center gap-[8px] w-full"
|
||||
>
|
||||
<AtomsButtonVariant
|
||||
variant="outlined"
|
||||
class="max-w-[128px]"
|
||||
@click="closeSelectCharacterModal"
|
||||
>
|
||||
{{ tm('Text_Cancel') }}
|
||||
</AtomsButtonVariant>
|
||||
<AtomsButtonVariant
|
||||
:disabled="isSelectCharacter ? false : true"
|
||||
class="max-w-[128px]"
|
||||
@click="handleCouponRegister"
|
||||
>
|
||||
{{ tm('Text_Confirm') }}
|
||||
</AtomsButtonVariant>
|
||||
</div>
|
||||
</div>
|
||||
</BlocksModalLayer>
|
||||
</Teleport>
|
||||
</ClientOnly>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Button Style */
|
||||
.btn-period {
|
||||
@apply border border-solid border-[#D9D9D9] rounded-[0] w-auto h-[40px] px-[16px] before:content-none after:content-none
|
||||
transition-all duration-300 ease-in-out
|
||||
hover:z-[4] hover:!bg-[#FFFFFF] hover:border-[#404040] hover:!text-[#1F1F1F];
|
||||
}
|
||||
.btn-period + .btn-period {
|
||||
@apply ml-[-1px];
|
||||
}
|
||||
.btn-period:first-child {
|
||||
@apply rounded-l-[8px];
|
||||
}
|
||||
.btn-period:last-child {
|
||||
@apply rounded-r-[8px];
|
||||
}
|
||||
.btn-period.btn-period-active {
|
||||
@apply z-[4] !bg-[#FFFFFF] !border-[#404040] !text-[#1F1F1F];
|
||||
}
|
||||
|
||||
/* Table Style */
|
||||
table {
|
||||
@apply w-full h-auto border-collapse border-spacing-0 table-fixed;
|
||||
}
|
||||
table th {
|
||||
@apply py-[8px] px-[12px] border border-[#D9D9D9] border-t-[0] border-r-[0] bg-[#FAFAFA] text-[#1F1F1F] text-[14px] font-bold leading-[24px] tracking-[-0.42px]
|
||||
md:py-[11px] md:px-[20px] md:text-[16px] md:leading-[26px] md:tracking-[-0.48px];
|
||||
}
|
||||
table td {
|
||||
@apply h-[auto] p-[12px] border border-[#D9D9D9] border-t-[0] border-r-[0] bg-[#FFFFFF] text-center text-[#666666] text-[14px] font-[400] leading-[24px] tracking-[-0.42px]
|
||||
md:px-[20px] md:text-[16px] md:leading-[26px] md:tracking-[-0.48px];
|
||||
}
|
||||
table td p {
|
||||
@apply line-clamp-2 text-center;
|
||||
}
|
||||
/* Coupon Item Style */
|
||||
.coupon-item {
|
||||
@apply relative inline-flex items-center justify-center !w-auto !h-auto py-[2px] px-[6px] border-none rounded-full !bg-[#EBEBEB] text-center !text-[#999999] text-[12px] font-[500] leading-[18px] tracking-[-0.24px]
|
||||
md:py-[4px] md:px-[8px] md:text-[14px] md:leading-[20px] md:tracking-[-0.42px];
|
||||
}
|
||||
.coupon-item.coupon-item-use {
|
||||
@apply !bg-[#383838] !text-white;
|
||||
}
|
||||
.coupon-item.coupon-item-used {
|
||||
@apply !bg-[#E2EAFF] !text-[#3C75FF];
|
||||
}
|
||||
.coupon-item.coupon-item-not-used {
|
||||
@apply !bg-[#EEF6F1] !text-[#2B9450];
|
||||
}
|
||||
</style>
|
||||
@@ -315,18 +315,7 @@ const handleMoveFocus = (target: 'pc' | 'mobile') => {
|
||||
<span
|
||||
class="relative inline-flex items-center justify-center w-[20px] h-[20px]"
|
||||
>
|
||||
<img
|
||||
:src="
|
||||
getImageHost(
|
||||
'/images/common/ic-v2-hardware-desktop-line.svg',
|
||||
{ imageType: 'common' }
|
||||
)
|
||||
"
|
||||
alt="Desktop-Icon"
|
||||
loading="lazy"
|
||||
draggable="false"
|
||||
class="w-full object-contain"
|
||||
/>
|
||||
<AtomsIconsDesktopLine :size="16" color="#FFFFFF" />
|
||||
</span>
|
||||
<span>{{ tm('Download_Button_SpecCheck') }}</span>
|
||||
</em>
|
||||
|
||||
104
layers/types/api/couponData.ts
Normal file
104
layers/types/api/couponData.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
const COUPON_NO_LENGTH_LIMIT = {
|
||||
MIN: 2,
|
||||
MAX: 20,
|
||||
}
|
||||
|
||||
const COUPON_RESULT = {
|
||||
LOGIN_REQUIRED: 401, // 로그인 필요
|
||||
EMPTY_GUID: 6063, // 게임 이용 약관 미동의
|
||||
GAME_MAINTENANCE: 6065, // 게임 점검 중
|
||||
SELECT_CHARACTER_REQUIRED: 7014, // 캐릭터 미선택
|
||||
WEB_INSPECTION: -90003, // 웹 점검 중
|
||||
NULL_SERVER: -90004, // 서버(월드) 미선택
|
||||
NULL_MEMBERNO: -90005, // 스토브 회원 번호 미입력
|
||||
NULL_NICKNAME: -90006, // 캐릭터 닉네임 미입력
|
||||
NULL_COUPON_NO: -90007, // 쿠폰 코드 미입력
|
||||
FAIL_COUPON_FORMAT: -90008, // 자릿수 잘못된 쿠폰 코드
|
||||
CHARACTER_MATCH_FAIL: -90009, // 캐릭터 정보가 존재하나 접속 계정의 캐릭터 정보와 불일치
|
||||
SYSTEM_ERROR: 999, // 시스템 오류
|
||||
}
|
||||
|
||||
interface ReqCouponUseParams {
|
||||
game_code: string
|
||||
coupon_no: string
|
||||
client_ipaddr: string
|
||||
world_no?: string
|
||||
character_no?: string
|
||||
game_coupon_id?: number
|
||||
cbox_save_flag?: boolean
|
||||
lang_code?: string
|
||||
}
|
||||
|
||||
interface ReqCouponUse extends ReqCouponUseParams {
|
||||
accessToken: string
|
||||
user_token_type?: string | 'web' | 'ingame' | 'guid'
|
||||
}
|
||||
|
||||
interface ResCouponUseValue {
|
||||
coupon_box_id: number
|
||||
cbox_save_flag: boolean
|
||||
}
|
||||
|
||||
interface ResCouponUse {
|
||||
code?: number
|
||||
result?: string
|
||||
message?: string
|
||||
value?: ResCouponUseValue
|
||||
}
|
||||
|
||||
interface MyCharacterReqType {}
|
||||
|
||||
interface MyCharacterResType {}
|
||||
|
||||
interface ReqCouponListParams {
|
||||
game_code: string
|
||||
start_date: number
|
||||
end_date: number
|
||||
use_state_code?: number
|
||||
page_size: number
|
||||
page_no: number
|
||||
lang_code?: string
|
||||
}
|
||||
|
||||
interface ReqCouponList extends ReqCouponListParams {
|
||||
accessToken: string
|
||||
user_token_type?: string | 'web' | 'ingame' | 'guid'
|
||||
}
|
||||
|
||||
interface ResCouponListItem {
|
||||
coupon_box_id: number
|
||||
game_code: string
|
||||
coupon_name: string
|
||||
reward_type_code: Array<number>
|
||||
reward_type: Array<string>
|
||||
use_start_date: number
|
||||
use_end_date: number
|
||||
register_date: number
|
||||
use_date: number
|
||||
use_state: number
|
||||
}
|
||||
|
||||
interface ResCouponList {
|
||||
code?: number
|
||||
result?: string
|
||||
message?: string
|
||||
value?: {
|
||||
total_count: number
|
||||
list: Array<ResCouponListItem>
|
||||
}
|
||||
}
|
||||
|
||||
export { COUPON_NO_LENGTH_LIMIT, COUPON_RESULT }
|
||||
|
||||
export type {
|
||||
ReqCouponUseParams,
|
||||
ReqCouponUse,
|
||||
ResCouponUseValue,
|
||||
ResCouponUse,
|
||||
MyCharacterReqType,
|
||||
MyCharacterResType,
|
||||
ReqCouponListParams,
|
||||
ReqCouponList,
|
||||
ResCouponListItem,
|
||||
ResCouponList,
|
||||
}
|
||||
90
layers/types/api/gameLinkedData.ts
Normal file
90
layers/types/api/gameLinkedData.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import type {
|
||||
CommonRequestType,
|
||||
CommonResponseType,
|
||||
} from '#layers/types/Common'
|
||||
|
||||
/*************************************************************************
|
||||
* 스토브 회원
|
||||
************************************************************************/
|
||||
/**
|
||||
* GUID 조회
|
||||
*/
|
||||
interface ReqGetGuid extends CommonRequestType {
|
||||
// Header
|
||||
accessToken: string // 엑세스 토큰
|
||||
|
||||
// Path Variables
|
||||
game_id: string // 게임ID
|
||||
}
|
||||
interface DtoGuid {
|
||||
member_no: number // 회원번호
|
||||
guid: number // GUID
|
||||
}
|
||||
interface ResGetGuid extends CommonResponseType {
|
||||
value?: DtoGuid
|
||||
}
|
||||
|
||||
/*************************************************************************
|
||||
* 월드 & 캐릭터
|
||||
************************************************************************/
|
||||
/**
|
||||
* 월드 목록 조회
|
||||
*/
|
||||
interface WorldInfo {
|
||||
world_id: string
|
||||
world_nm: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 보유 캐릭터 목록 조회
|
||||
*/
|
||||
interface ReqGameCharacterList extends CommonRequestType {
|
||||
// Path Variables
|
||||
game_id: string
|
||||
|
||||
// Header
|
||||
accessToken: string
|
||||
|
||||
// Custom
|
||||
world_list?: WorldInfo[]
|
||||
}
|
||||
interface CharacterInfo {
|
||||
character_id: string
|
||||
world_id: string
|
||||
world_nm?: string // 서버명
|
||||
character_seq?: number
|
||||
name: string // E7 : 캐릭터명, L9 : [서버명]캐릭터명
|
||||
character_info?: string
|
||||
profile_image_url?: string
|
||||
is_main_character: string // Y/N
|
||||
level?: string
|
||||
reg_dt?: number
|
||||
upd_dt?: number
|
||||
exp?: number
|
||||
|
||||
// Custom
|
||||
formatted_nm?: string // [서버명] 캐릭터명
|
||||
}
|
||||
interface DtoGameCharacterList {
|
||||
id: string
|
||||
game_no: number
|
||||
game_id: string
|
||||
member_no: number
|
||||
nickname_flag: string
|
||||
reg_dt: number
|
||||
upd_dt: number
|
||||
character_infos: Array<CharacterInfo>
|
||||
main_game_character: CharacterInfo
|
||||
}
|
||||
interface ResGameCharacterList extends CommonResponseType {
|
||||
value?: DtoGameCharacterList
|
||||
}
|
||||
|
||||
export type {
|
||||
ReqGetGuid,
|
||||
ResGetGuid,
|
||||
WorldInfo,
|
||||
ReqGameCharacterList,
|
||||
CharacterInfo,
|
||||
ResGameCharacterList,
|
||||
}
|
||||
@@ -82,3 +82,15 @@ export const csrGetCountry = () => {
|
||||
}
|
||||
return countryCode
|
||||
}
|
||||
|
||||
/**
|
||||
* STOVE 쿠폰함 링크 조회
|
||||
*/
|
||||
export const getStoveCouponUrl = (device: string) => {
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
|
||||
const stoveCouponUrl = runtimeConfig.public.stoveCouponUrl as string
|
||||
const stoveMCouponUrl = runtimeConfig.public.stoveMCouponUrl as string
|
||||
|
||||
return device === 'desktop' ? stoveCouponUrl : stoveMCouponUrl
|
||||
}
|
||||
|
||||
@@ -88,6 +88,9 @@ export default defineNuxtConfig({
|
||||
|
||||
stoveGnb: process.env.STOVE_GNB,
|
||||
|
||||
stoveCouponUrl: process.env.STOVE_COUPON_URL,
|
||||
stoveMCouponUrl: process.env.STOVE_M_COUPON_URL,
|
||||
|
||||
stoveCs: process.env.STOVE_CS,
|
||||
|
||||
stoveLauncherScript: process.env.STOVE_LAUNCHER_SCRIPT,
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"@seed-next/date": "^0.0.0",
|
||||
"@splidejs/splide": "^4.1.4",
|
||||
"@splidejs/vue-splide": "^0.6.12",
|
||||
"@vuepic/vue-datepicker": "^11.0.2",
|
||||
"@vueuse/core": "^13.6.0",
|
||||
"@vueuse/nuxt": "^13.6.0",
|
||||
"h3": "^1.15.4",
|
||||
|
||||
14
pnpm-lock.yaml
generated
14
pnpm-lock.yaml
generated
@@ -32,6 +32,9 @@ importers:
|
||||
'@splidejs/vue-splide':
|
||||
specifier: ^0.6.12
|
||||
version: 0.6.12
|
||||
'@vuepic/vue-datepicker':
|
||||
specifier: ^11.0.2
|
||||
version: 11.0.3(vue@3.5.21(typescript@5.9.2))
|
||||
'@vueuse/core':
|
||||
specifier: ^13.6.0
|
||||
version: 13.9.0(vue@3.5.21(typescript@5.9.2))
|
||||
@@ -1872,6 +1875,12 @@ packages:
|
||||
'@vue/shared@3.5.21':
|
||||
resolution: {integrity: sha512-+2k1EQpnYuVuu3N7atWyG3/xoFWIVJZq4Mz8XNOdScFI0etES75fbny/oU4lKWk/577P1zmg0ioYvpGEDZ3DLw==}
|
||||
|
||||
'@vuepic/vue-datepicker@11.0.3':
|
||||
resolution: {integrity: sha512-sb2adwqwK2PizLQOpxCYps2SwhVT6/ic2HMIOqHJXuYa6iAJZWGL5YVlS7O4aW+sk6ZyxlDURLO7kDZPL4HB/w==}
|
||||
engines: {node: '>=18.12.0'}
|
||||
peerDependencies:
|
||||
vue: '>=3.3.0'
|
||||
|
||||
'@vueuse/core@13.9.0':
|
||||
resolution: {integrity: sha512-ts3regBQyURfCE2BcytLqzm8+MmLlo5Ln/KLoxDVcsZ2gzIwVNnQpQOL/UKV8alUqjSZOlpFZcRNsLRqj+OzyA==}
|
||||
peerDependencies:
|
||||
@@ -6741,6 +6750,11 @@ snapshots:
|
||||
|
||||
'@vue/shared@3.5.21': {}
|
||||
|
||||
'@vuepic/vue-datepicker@11.0.3(vue@3.5.21(typescript@5.9.2))':
|
||||
dependencies:
|
||||
date-fns: 4.1.0
|
||||
vue: 3.5.21(typescript@5.9.2)
|
||||
|
||||
'@vueuse/core@13.9.0(vue@3.5.21(typescript@5.9.2))':
|
||||
dependencies:
|
||||
'@types/web-bluetooth': 0.0.21
|
||||
|
||||
BIN
public/images/common/ic-v2-community-calendar-line.png
Normal file
BIN
public/images/common/ic-v2-community-calendar-line.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 448 B |
BIN
public/images/common/ic-v2-navigation-coupon-fill.png
Normal file
BIN
public/images/common/ic-v2-navigation-coupon-fill.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 336 B |
BIN
public/images/common/ic-v2-navigation-search-line.png
Normal file
BIN
public/images/common/ic-v2-navigation-search-line.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 294 B |
Reference in New Issue
Block a user