fix. 버튼 컴포넌트 링크 적용, 코드 리팩토링

This commit is contained in:
clkim
2025-09-25 18:28:02 +09:00
parent 0ef8c5bdf5
commit acea3418e3
10 changed files with 247 additions and 235 deletions

View File

@@ -1,45 +1,34 @@
<script setup lang="ts">
import type { GameDataKeyCodeCodes } from '#layers/types/api/gameData'
import type { ButtonType } from '#layers/types/components/button'
interface ButtonProps {
backgroundColor?: string
textColor?: string
type?: ButtonType
icon?: string
disabled?: boolean
target?: '_self' | '_blank'
href?: string
rel?: string
backgroundColor?: string
backgroundImage?: string
textColor?: string
disabled?: boolean
class?: string
}
const props = withDefaults(defineProps<ButtonProps>(), {
type: 'action',
backgroundColor: 'var(--primary)',
textColor: 'var(--alternative-02)',
icon: '',
disabled: false,
})
// 색상 코드 키 목록 key_code_codes
const PARSED_KEY_CODE_CODES_KEYS: (keyof GameDataKeyCodeCodes)[] = [
'primary',
'text-primary',
'text-secondary',
'alternative-01',
'alternative-02',
]
// 색상 값을 CSS 변수로 변환하는 헬퍼 함수
const getColorValue = (color: string) =>
PARSED_KEY_CODE_CODES_KEYS.includes(color as keyof GameDataKeyCodeCodes)
? `var(--${color})`
: color
const buttonClasses = computed(() => [
'btn-base group relative inline-flex items-center justify-center font-medium border border-gray-600/30 overflow-hidden',
props.disabled ? 'cursor-default' : 'cursor-pointer',
])
const buttonClasses = computed(() =>
['btn-base group', props.class].filter(Boolean)
)
const buttonStyles = computed(() => {
const styles: Record<string, string> = {
backgroundColor: getColorValue(props.backgroundColor),
color: getColorValue(props.textColor),
backgroundColor: props.backgroundColor,
color: props.textColor,
'--text-color': props.textColor,
}
if (props.backgroundImage) {
@@ -51,40 +40,50 @@ const buttonStyles = computed(() => {
return styles
})
const componentTag = computed((): string => {
switch (props.type) {
case 'download':
case 'external':
return 'a'
case 'internal':
return 'AtomsLocaleLink'
default:
return 'button'
}
})
const componentProps = computed(() => {
const baseProps = { disabled: props.disabled }
const overlayClasses = computed(() => [
'absolute inset-0 -m-px transition-opacity duration-200',
props.disabled
? 'opacity-20 z-10'
: 'bg-white opacity-0 group-hover:opacity-20',
])
const overlayDisabledStyles = computed(
() =>
props.disabled && {
backgroundColor: props.textColor,
if (props.type === 'external') {
return {
...baseProps,
href: props.href,
target: props.target,
rel: props.rel,
}
)
}
const contentDisabledStyles = computed(() => props.disabled && { opacity: 0.2 })
if (props.type === 'internal') {
return {
...baseProps,
to: props.href,
}
}
return baseProps
})
</script>
<template>
<button
<component
:is="componentTag"
v-bind="componentProps"
:class="buttonClasses"
:style="buttonStyles"
:disabled="props.disabled"
>
<!-- 호버 효과 / Disabled 오버레이 -->
<span :class="overlayClasses" :style="overlayDisabledStyles" />
<!-- 버튼 내용 -->
<span
class="relative flex items-center gap-2"
:style="contentDisabledStyles"
>
<span class="relative flex items-center gap-2 z-[1]">
<slot />
<span v-if="props.icon" class="flex-shrink-0" v-html="props.icon" />
</span>
</button>
</component>
</template>

View File

@@ -9,19 +9,29 @@ const props = withDefaults(defineProps<Props>(), {
target: '',
class: '',
})
const componentTag = computed(() => {
return props.target === '_blank' ? 'a' : 'AtomsLocaleLink'
})
const componentProps = computed(() => {
if (props.target === '_blank') {
return {
href: props.to,
target: props.target,
class: props.class,
}
}
return {
to: props.to,
class: props.class,
}
})
</script>
<template>
<a
v-if="props.target === '_blank'"
v-bind="$attrs"
:href="props.to"
:target="props.target"
:class="props.class"
>
<component :is="componentTag" v-bind="{ ...$attrs, ...componentProps }">
<slot />
</a>
<AtomsLocaleLink v-else v-bind="$attrs" :to="props.to" :class="props.class">
<slot />
</AtomsLocaleLink>
</component>
</template>

View File

@@ -1,5 +1,4 @@
<script setup lang="ts">
import { getResponsiveSrc } from '#layers/utils/dataUtil'
import type { PageDataResourceGroup } from '#layers/types/api/pageData'
interface Props {
@@ -13,39 +12,14 @@ const props = defineProps<Props>()
const displayText = computed(() => {
return props.resourcesData?.display?.txt || ''
})
// 이미지 소스 추출
const imageSrc = computed(() => {
return getResponsiveSrc(props.resourcesData?.res_path)
})
// 색상 코드 추출 (우선순위: color_code_txt > color_code)
const colorCode = computed(() => {
return (
props.resourcesData?.display?.color_code_txt ||
props.resourcesData?.display?.color_code
)
})
// 색상 이름 추출 (우선순위: color_name_txt > color_name)
const colorName = computed(() => {
return (
props.resourcesData?.display?.color_name_txt ||
props.resourcesData?.display?.color_name
)
return props.resourcesData?.display?.color_name
})
// 색상 스타일 계산
const textStyles = computed(() => {
const styles: Record<string, string> = {}
if (colorName.value) {
styles.color = `var(--${colorName.value})`
} else if (colorCode.value) {
styles.color = colorCode.value
}
return styles
const colorCode = computed(() => {
return props.resourcesData?.display?.color_code
})
// HTML 콘텐츠 정리 (줄바꿈 처리)
@@ -82,7 +56,7 @@ const hasImage = computed(() => {
<span
v-else-if="displayText"
v-dompurify-html="sanitizedContent"
:style="textStyles"
:style="{ color: getColorCode({ colorName, colorCode }) }"
class="block"
/>
</template>

View File

@@ -1,5 +1,4 @@
<script setup lang="ts">
import { getResponsiveClass, getResponsiveSrc } from '#layers/utils/dataUtil'
import type { PageDataResourceGroup } from '#layers/types/api/pageData'
interface Props {

View File

@@ -1,25 +1,69 @@
<script setup lang="ts">
import type { PageDataResourceGroup } from '#layers/types/api/pageData'
import type {
PageDataResourceGroup,
PageDataResourceGroupBtnInfo,
} from '#layers/types/api/pageData'
import type { ButtonType } from '#layers/types/components/button'
const props = defineProps<{
interface ButtonListProps {
resourcesData: PageDataResourceGroup[]
buttonType?: string
}>()
}
const props = defineProps<ButtonListProps>()
// 상수 정의
const BUTTON_TYPE_MAP = {
URL: {
_self: 'internal' as const,
_blank: 'external' as const,
},
DOWNLOAD: 'download' as const,
} as const
const DEFAULT_BUTTON_TYPE: ButtonType = 'action'
const getButtonType = (btnInfo: PageDataResourceGroupBtnInfo): ButtonType => {
const btnType = btnInfo?.detail?.btn_type
const btnTarget = btnInfo?.detail?.action?.link_target
if (btnType === 'URL' && btnTarget) {
return BUTTON_TYPE_MAP.URL[btnTarget] || DEFAULT_BUTTON_TYPE
}
if (btnType === 'DOWNLOAD') {
return BUTTON_TYPE_MAP.DOWNLOAD
}
return DEFAULT_BUTTON_TYPE
}
const getButtonProps = (button: PageDataResourceGroup) => ({
type: getButtonType(button.btn_info),
target: button.btn_info?.detail?.action?.link_target,
href: button.btn_info?.detail?.action?.url,
rel: button.btn_info?.detail?.action?.rel,
backgroundColor: getColorCode({
colorName: button.btn_info?.color_name_btn,
colorCode: button.btn_info?.color_code_btn,
}),
textColor: getColorCode({
colorName: button.btn_info?.color_name_txt,
colorCode: button.btn_info?.color_code_txt,
}),
disabled: button.btn_info?.disabled,
text: button.btn_info?.txt_btn_name,
})
</script>
<template>
<div
v-if="props.resourcesData"
v-if="props.resourcesData?.length"
class="flex flex-wrap justify-center gap-3 md:gap-4"
>
<AtomsButton
v-for="button in props.resourcesData"
:key="button.group_code"
:button-type="props.buttonType"
v-for="(button, index) in props.resourcesData"
:key="`${button.group_code}-${index}`"
v-bind="getButtonProps(button)"
class="size-extra-small md:size-medium"
:background-color="button.btn_info?.color_code_btn"
:text-color="button.btn_info?.color_code_txt"
:disabled="button.btn_info?.disabled"
>
{{ button.btn_info?.txt_btn_name }}
</AtomsButton>