데이터는 json 형식으로 만들어놓음
[
{"id: 1","name":"논노 01", "price": 289000, "color":"3", "imgUrl":""}
]

젠몬도 swiper.js를 사용해서 나도 사용했다
https://www.svgbackgrounds.com/tools/svg-to-css/
기존 svg 태그를 css data:image어쩌구 형식으로 바꿔줘서 background-image로 사용가능하게 해줌
<template>
<div class="top-banner">
<div class="wrap">
<swiper
:slidesPerView="1"
:spaceBetween="30"
:loop="true"
:navigation="true"
:modules="modules"
class="mySwiper"
:autoplay="{
delay: 1500,
disableOnInteraction: false
}"
>
<swiper-slide>나에게 어울리는 제품을 찾으세요? 실시간 라이브 채팅으로 제품 추천을 받아보세요.</swiper-slide>
<swiper-slide>무료 배송/반품 서비스로 젠틀몬스터를 경험해보세요.</swiper-slide>
<swiper-slide>선물하기 서비스를 통해 소중한 분에게 마음을 전하세요.</swiper-slide>
</swiper>
</div>
</div>
</template>
<script>
import { Swiper, SwiperSlide } from 'swiper/vue'
import 'swiper/css'
import 'swiper/css/navigation'
import { Autoplay, Navigation } from 'swiper/modules';
export default {
components: {
Swiper,
SwiperSlide
},
setup() {
return {
modules: [Navigation, Autoplay]
}
}
}
</script>
<style lang="scss">
.top-banner {
width: 100%;
height: 50px;
border-bottom: 1px solid #eee;
display: flex;
align-items: center;
.wrap {
width: 60%;
margin: 0 auto;
display: flex;
align-items: center;
.swiper {
.swiper-slide {
font-size: 13px;
text-align: center;
text-decoration: underline;
}
.swiper-button-next {
position: absolute;
z-index: 999;
background: url('data:image 어쩌구')
no-repeat;
background-size: 50% auto;
background-position: center;
&::after {
display: none;
}
}
.swiper-button-prev {
position: absolute;
right: 0;
background: url('')
no-repeat;
background-size: 50% auto;
background-position: center;
&::after {
display: none;
}
}
}
}
}
</style>
shop.js
pinia 사용
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useShopStore = defineStore('shop', () => {
const amount = ref(0)
const formattedAmount = computed(() => {
return amount.value.toLocaleString('ko-KR');
})
const setAmount = (newAmount) => {
amount.value = newAmount;
}
return { amount, formattedAmount, setAmount }
})
Product.js
import { ref, onMounted, reactive } from 'vue'
import { useShopStore } from '../stores/shop.js'
const store = useShopStore()
const products = ref(data)
const productsList = () => {
products.value.forEach((item) => {
store.setAmount(item.price)
item.price = store.formattedAmount
})
}
onMounted(() => {
productsList()
})

localstorage를 이용했다
svg태그는 길어서 지움
기존 배열을 있으면 가져와 내가 찜한 객체를 다시 배열에 추가하고 localstorage에 저장
<div class="heart-icon" @click="SaveWishList(item)">
<svg>
<path
:fill="item.isFavorite ? '#ff0000' : 'none'"
:stroke="item.isFavorite ? '#ff0000': 'black'"
stroke-width="2px"
stroke-miterlimit="20"></path>
</svg>
</div>
const store = useShopStore()
const products = ref(data)
const wishList = reactive([]) // 추가
const productsList = () => {
const wishListNames = wishList.value.map((item) => item.name) // 추가
products.value.forEach((item) => {
store.setAmount(item.price)
item.price = store.formattedAmount
item.isFavorite = !item.isFavorite && wishListNames.includes(item.name) ? true : false; // 추가
})
}
const SaveWishList = (item) => {
item.isFavorite = !item.isFavorite;
if (item.isFavorite) {
wishList.value.push(item);
} else {
wishList.value = wishList.value.filter(ele => ele.name !== item.name);
}
localStorage.setItem('wishlist', JSON.stringify(wishList.value))
} // 추가
onMounted(() => {
wishList.value = JSON.parse(localStorage.getItem('wishlist')) || [] // 추가
productsList()
})

info.json 파일을 만듦
[
{
"id": "1",
"name": "논노 01",
"price": 289000,
"content": "젠틀몬스터의 2023년 컬렉션을 통해 선보이는 논노 01 선글라스를 소개합니다. 깊이감이 돋보이는 스퀘어 실루엣에 심플한 블랙 컬러가 특징이며, 템플에 결합된 메탈 아이콘 디테일이 돋보이는 아이템입니다.",
"detail": [
"블랙 색상 아세테이트 프레임",
],
"imgUrl": [
"",
"",
]
},
]
-메인페이지
클릭시 params로 id값을 줌
<li v-for="(item, index) in products" :key="index">
<router-link :to="{ name: 'shopDetail', params: { id: `${item.id}` } }">
<div class="imageList">
<img />
</div>
</router-link>
</li>
shop.js / pinia
id 값을 받아와서 info.json 데이터와 비교 후 데이터 추출
import detailItem from '../../info.json'
const list = reactive(detailItem)
const setDetail = (newId) => {
filteredList.value = list.find((item) => item.id === newId);
const prefix = filteredList.value.name.indexOf(' ');
relatedLinksList.value = list.filter(item => item.name.substring(0, prefix) === filteredList.value.name.substring(0, prefix));
return { amount, formattedAmount, filteredList, setAmount, setDetail }
-상세페이지
shopDetailView.vue
store에서 불러와줌
<template>
<div class="shop-detail">
<div class="img-wrap">
<div class="img" v-for="(ele,index) in store.filteredList.imgUrl" :key="index">
<img :src="ele" lazy="loading"/>
</div>
</div>
</div>
</template>
<script setup>
onMounted(()=>{
store.setDetail(props.id);
store.setAmount(store.filteredList.price)
})
</script>

<div class="detail-info-box" v-for="(title,index) in infoList" :key="index" @click="toggle(index)" :style="[currentIdx === index ? {height: infoHeight} : {height: '60px'}]">
<div class="detail-info-top">
<span>{{ title.name }}</span> <span><svg width="15px" xmlns=""/></svg></span>
</div>
<div class="detail-info-content" ref="myElement">
<ul v-if="title.name === '제품 세부 정보'">
<li v-for="(detail,index) in store.filteredList.detail" :key="index">
{{ detail }}
</li>
</ul>
<ul v-else v-for="(ele,index) in title.content" :key="index">
<li v-html="ele.sub"></li>
</ul>
</div>
</div>
.detail-info-box{
overflow: hidden;
cursor: pointer;
height:60px;
transition: 0.3s ease-out;
.detail-info-top{
border-top:1px solid #eee;
display: flex;
justify-content:space-between;
height:60px;
align-items: center;
padding:0 10px;
font-size: 13px;
line-height: 18px;
span{
display: block;
height: 15px;
}
}
.detail-info-content{
ul{
padding:0 0 20px;
li{
font-size:13px;
list-style:disc;
margin: 0 30px;
}
}
}
}
<script setup>
const currentIdx = ref(0);
const myElement = ref([]);
const infoHeight = ref(0);
const infoList = ref([
{ name: '제품 세부 정보'},
{
name: '배송 & 반품',
content: [
{ sub: '<span>무료배송</span><span>오늘 주문한 상품을 영업일 기준 1-3일내에 받아보세요.</span>'},
{ sub: '<span>당일 배송</span><span>당일 배송 서비스를 이용하시면 오늘 상품을 받아볼 수 있습니다.<a>자세히 보기</a></span>'}
]
},
{ name: '기본 피팅 서비스',
content: [
{ sub: '<span>온라인에서 주문하시는 젠틀몬스터의 모든 제품은 기본 피팅 후 발송됩니다.<a>자세히 보기</a></span>'}
] },
{ name: '도움이 필요하신가요?', content: [
{ sub: '<span>온라인에서 주문하시는 젠틀몬스터의 모든 제품은 기본 피팅 후 발송됩니다.<a>자세히 보기</a></span>'}
] }
]);
const toggle = (index) => {
currentIdx.value = currentIdx.value === index ? -1 : index;
infoHeight.value = currentIdx.value === -1 ? '60px' : `${myElement.value[index].scrollHeight + 60}px`;
}
const getInfoHeight = (index) =>{
infoHeight.value = `${myElement.value[index].scrollHeight + 60}px`;
}
onMounted(()=>{
store.setDetail(props.id);
nextTick(() => {
getInfoHeight(0)
});
})
</script>

shop.js
이름 띄어쓰기 전만 가져와 비교해서 같은 값 가져옴
const setDetail = (newId) => {
const filteredItem = list.find((item) => item.id === newId);
const updatedItem = { ...filteredItem, price: filteredItem.price.toLocaleString('ko-KR') };
filteredList.value = updatedItem;
const prefix = filteredList.value.name.indexOf(' ');
const relatedLinksItem = list.filter(item => item.name.substring(0,prefix) === updatedItem.name.substring(0, prefix));
relatedLinksList.value = relatedLinksItem;
}
return { amount, formattedAmount, filteredList, relatedLinksList, setAmount, setDetail }
shopDetailView.vue
<template>
<div class="link">
<router-link v-for="(ele, index) in store.relatedLinksList" :key="index" :to="{ name: 'shopDetail', params: { id: `${ele.id}` } }" :style="{ backgroundImage: `url('${ele.imgUrl[0]}')`, border: props.id === ele.id ? '1px solid #000' : 'none' }">
</router-link>
</div>
</template>
<script setup>
const route = useRoute();
watch(route, () => {
store.setDetail(props.id);
});
</script>
정사각형 유지를 위해 background-image 사용
.link{
width: 60%;
margin: 0 auto 50px auto;
display: grid;
grid-template-columns: repeat(4,1fr);
gap:5px;
a{
display: block;
width: 0;
height: 0;
padding: 100% 100% 0 0;
background-repeat: no-repeat;
background-position: center;
background-size: 100% auto;
border-radius: 2px;
}
}

난 헤더 레이아웃을 app.vue에 해놔서 저 부분도 app.vue에 넣음
<button @click="store.toggleCart" class="cart-btn">
<span>{{ store.cartList.length }}</span>
아이콘
</button>
cartItem.vue
<div class="cart" :class="[store.isCartOpen ? 'open' : '']">
<div class="bg" @click="store.toggleCart"></div>
<div class="cart-wrap">
</div>
</div>
</div>
<style lang="scss">
.cart {
.bg {
position: fixed;
background-color: rgba(0, 0, 0, 0.4);
width: 100%;
height: 100%;
left: 0;
top: 0;
z-index: 998;
visibility: hidden;
}
&.open {
.cart-wrap {
transform: translateX(0);
}
.bg {
visibility: visible;
}
}
.cart-wrap {
width: 375px;
position: fixed;
right: 0;
top: 0;
min-height: 100%;
background: #fff;
transform: translateX(100vw);
transition: 0.3s ease-in-out;
z-index: 999;
height: 100%;
</style>
shop.js / pinia
장바구니 목록은 서버에서 관리한다고 하지만 서버 없어서 localstorage에 저장
각각 x아이콘 검은색 배경에도 store.toggleCart 넣어주면 끝
const isCartOpen = ref(false);
const cartList = ref([]);
const loadCartList = () =>{
cartList.value = JSON.parse(localStorage.getItem('cartList')) || [];
}
const toggleCart = () => {
isCartOpen.value = !(isCartOpen.value);
loadCartList();
document.body.style.overflow = isCartOpen.value ? 'hidden' : 'auto';
}

shpDetailView.vue
<button @click="store.setaddItemToCart(store.filteredList.id)">쇼핑백에 추가</button>
CartItem.vue
<span>합계</span><span>{{ store.getTotalPrice() }}원</span>
shop.js / pinia
수량도 같이 로컬스토리지 배열에 저장
const setaddItemToCart = (id) => {
let cartItem = cartList.value.find(item => item.id === id);
if (cartItem) {
cartItem.amount += 1;
} else {
cartItem = data.find(item => item.id === id);
cartItem = { ...cartItem, amount: 1 };
cartList.value.push(cartItem);
}
cartItem.total = (cartItem.price * cartItem.amount).toLocaleString('ko-KR');
getTotalPrice()
localStorage.setItem('cartList', JSON.stringify(cartList.value));
toggleCart();
}
const getTotalPrice = () => {
let total = 0;
for (const item of cartList.value) {
const itemTotal = item.amount * item.price;
total += itemTotal;
}
return total.toLocaleString('ko-KR');
}

shop.js / pinia
클릭한 값 빼고 재배열 후 저장
const removeItemFromCart = (id) => {
cartList.value = cartList.value.filter(item => item.id !== id);
localStorage.setItem('cartList', JSON.stringify(cartList.value));
}

shop.js / pinia
total은 특정 id 수량 * 가격 값
const modifyQuantity = (symbol, id) => {
let cartItem = cartList.value.find(item => item.id === id);
if (symbol === '+') {
if (cartItem.amount >= 10) {
alert('최대수량은 10개 까지 입니다.');
} else {
cartItem.amount += 1;
}
} else if (symbol === '-') {
if (cartItem.amount > 1) {
cartItem.amount -= 1;
} else {
cartItem.amount = 1;
}
}
cartItem.total = (cartItem.price * cartItem.amount).toLocaleString('ko-KR');
const index = cartList.value.findIndex(item => item.id === id);
if (index !== -1) {
cartList.value.splice(index, 1, cartItem);
}
localStorage.setItem('cartList', JSON.stringify(cartList.value));
};
CartItem.vue
<button @click="store.modifyQuantity('-', item.id)">-</button>

<template>
<div class="search" :class="[store.isSrhOpen ? 'open' : '']">
<div class="search-wrap">
<div class="x-icon" @click="store.toggleSearchBar">
<svg
width="15px"
height="15px"
>
</svg>
</div>
<div class="search-bar">
<form>
<input type="text" placeholder="검색어를 입력하세요" @input="getSearchList" />
</form>
</div>
<div class="search-list">
<div class="product">
<h5 v-if="searchResults.length > 10">인기 검색어</h5>
<h5 v-else>연관 제품</h5>
<template v-for="(item, index) in searchResults" :key="index">
<span v-if="index < 4">
{{ item.name }}
</span>
</template>
</div>
<ul class="img-product">
<template v-for="(item, index) in searchResults" :key="index">
<li v-if="index < 4">
<span><img :src="item.imgUrl" /></span>
<span>{{ item.name }}</span>
</li>
</template>
</ul>
</div>
</div>
<div class="bg"></div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useShopStore } from '../stores/shop.js'
const store = useShopStore()
const searchValue = ref('')
const searchResults = ref(store.allList)
const getSearchList = (e) => {
searchValue.value = e.target.value;
searchResults.value = store.allList.filter((item) => {
return item.name.includes(searchValue.value);
});
};
</script>
<style lang="scss">
.search {
z-index: -99;
position: fixed;
width: 100%;
.bg {
display: none;
z-index: -1;
}
.search-wrap {
visibility: hidden;
position: relative;
z-index: 999;
width: 100%;
.search-list {
width: 580px;
margin: 30px auto 50px auto;
display: flex;
.product{
h5{
font-size:14px;
color:#acacac;
margin:0 0 20px 0;
}
width:200px;
display: flex;
gap:10px;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
font-size:13px;
}
.img-product{
flex:1;
display: grid;
grid-template-columns: repeat(4,1fr);
gap:10px;
span{
font-size:13px;
display: block;
&:last-child{
margin:10px 0 0 0;
}
}
}
}
.search-bar {
width: 580px;
margin: 0 auto;
height: 80px;
position: relative;
form {
width: 181px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
input {
width: 100%;
border: 1px solid #000;
border-radius: 18px;
height: 36px;
padding: 2px 15px 0 15px;
}
}
}
.x-icon {
position: absolute;
right: 20px;
top: 40px;
transform: translate(0, -50%);
background: #000;
width: 25px;
height: 25px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
svg {
color: #fff;
}
}
}
&.open {
z-index: 10;
.bg {
display: block;
width: 100%;
position: fixed;
top: 0;
left: 0;
height: 100%;
background: rgba(0, 0, 0, 0.6);
z-index: 99;
}
.search-wrap {
visibility: visible;
position: fixed;
background: #fff;
left: 0;
top: 0;
.search-bar {
form {
width: 100%;
transition: 0.3s ease-in;
}
input {
width: 100%;
}
}
}
}
}
</style>
<li v-if="index < 4">
<router-link :to="{ name: 'shopDetail', params: { id: `${item.id}` } }" @click="store.toggleSearchBar">
<span><img :src="item.imgUrl" /></span>
<span>{{ item.name }}</span>
</router-link>
</li>
끗