feat: 프론트엔드 초기 스캐폴딩 (Vue3+TS+Quasar+Capacitor)
- 하늘색 파스텔 브랜드 테마(quasar-variables) - 스팟 지도/체크인/한줄평/메이트 홈 화면 - 강아지 프로필 + 댕비티아이 성향 태그 화면 - 구독 티어 스토어(Pinia) 및 FREE 티어 배너 광고(v-if 제어) - Capacitor(iOS/AOS) 설정, 빌드/타입체크 통과 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
# 백엔드 API 주소 (dognation_BT)
|
||||||
|
VITE_API_BASE_URL=http://localhost:8080
|
||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
# dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# build output
|
||||||
|
dist/
|
||||||
|
dist-ssr/
|
||||||
|
|
||||||
|
# env
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# capacitor native (별도 관리)
|
||||||
|
/ios/App/Pods/
|
||||||
|
/android/.gradle/
|
||||||
|
/android/app/build/
|
||||||
|
|
||||||
|
# logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
|
||||||
|
# editor / OS
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
.DS_Store
|
||||||
|
*.local
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# 산책갈개 · dognation_PT (Frontend)
|
||||||
|
|
||||||
|
우리 개 성향에 맞는 동네 산책 메이트를 안전하게 찾는 iOS/AOS 앱의 프론트엔드.
|
||||||
|
|
||||||
|
## 스택
|
||||||
|
- **Vue 3 + TypeScript**
|
||||||
|
- **Quasar** (UI, 하늘색 파스텔 테마)
|
||||||
|
- **Vite** (번들러)
|
||||||
|
- **Pinia** (상태관리 · 구독 티어)
|
||||||
|
- **Capacitor** (iOS/AOS 네이티브 패키징)
|
||||||
|
|
||||||
|
## 시작하기
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
cp .env.example .env # VITE_API_BASE_URL 설정
|
||||||
|
npm run dev # 웹 개발 서버 (http://localhost:9000)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 빌드
|
||||||
|
```bash
|
||||||
|
npm run build # 타입체크 + 웹 빌드
|
||||||
|
npm run build:app # Capacitor용 빌드 (상대경로)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 네이티브(추가 예정)
|
||||||
|
```bash
|
||||||
|
npx cap add ios
|
||||||
|
npx cap add android
|
||||||
|
npx cap sync
|
||||||
|
```
|
||||||
|
|
||||||
|
## 구조
|
||||||
|
- `src/pages/HomePage.vue` — 스팟 지도 / 체크인 / 한줄평 / 메이트
|
||||||
|
- `src/pages/ProfilePage.vue` — 강아지 프로필 / 댕비티아이 성향 태그 / 구독 티어
|
||||||
|
- `src/stores/subscription.ts` — 구독 티어 상태 (광고 노출·매칭 제한 파생)
|
||||||
|
- `src/components/AdBanner.vue` — FREE 티어 배너 광고 (v-if 제어)
|
||||||
|
- `src/quasar-variables.scss` — 하늘색 파스텔 브랜드 컬러
|
||||||
|
|
||||||
|
> 백엔드: [dognation_BT](https://gitlab.sblog.kr/sb/dognation_BT)
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import type { CapacitorConfig } from '@capacitor/cli'
|
||||||
|
|
||||||
|
const config: CapacitorConfig = {
|
||||||
|
appId: 'kr.sblog.dognation',
|
||||||
|
appName: '산책갈개',
|
||||||
|
webDir: 'dist',
|
||||||
|
ios: {
|
||||||
|
contentInset: 'always',
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
androidScheme: 'https',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||||
|
<meta name="theme-color" content="#6DB3E8" />
|
||||||
|
<title>산책갈개</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Generated
+3699
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"name": "dognation_pt",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"description": "산책갈개 - 우리 개 성향에 맞는 동네 산책 메이트 (iOS/AOS)",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vue-tsc --noEmit && vite build",
|
||||||
|
"build:app": "vite build --mode capacitor",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"typecheck": "vue-tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@capacitor/app": "^8.0.0",
|
||||||
|
"@capacitor/core": "^8.3.4",
|
||||||
|
"@quasar/extras": "^1.16.12",
|
||||||
|
"pinia": "^2.3.0",
|
||||||
|
"quasar": "^2.17.4",
|
||||||
|
"vue": "^3.5.13",
|
||||||
|
"vue-router": "^4.5.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@capacitor/android": "^8.3.4",
|
||||||
|
"@capacitor/cli": "^8.3.4",
|
||||||
|
"@capacitor/ios": "^8.3.4",
|
||||||
|
"@quasar/vite-plugin": "^1.9.0",
|
||||||
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
|
"sass-embedded": "^1.83.0",
|
||||||
|
"typescript": "^5.7.2",
|
||||||
|
"vite": "^6.0.5",
|
||||||
|
"vue-tsc": "^2.2.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<router-view />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// 루트 컴포넌트
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<template>
|
||||||
|
<!-- FREE 티어에서만 노출. AD_FREE/PREMIUM 은 hideAds 로 숨김 -->
|
||||||
|
<div v-if="!subscription.hideAds" class="ad-banner row items-center justify-center">
|
||||||
|
<q-icon name="campaign" size="20px" class="q-mr-sm" />
|
||||||
|
<span class="text-caption">광고 · 광고 없이 쓰려면 광고 제거 패스</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useSubscriptionStore } from '@/stores/subscription'
|
||||||
|
|
||||||
|
const subscription = useSubscriptionStore()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.ad-banner {
|
||||||
|
height: 56px;
|
||||||
|
background: #e8f4fd;
|
||||||
|
color: #3e92cc;
|
||||||
|
border-top: 1px solid #cfe8fb;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
// 전역 스타일 — 하늘색 파스텔 배경
|
||||||
|
body {
|
||||||
|
background: #F4FAFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-brand {
|
||||||
|
color: #3E92CC;
|
||||||
|
}
|
||||||
|
|
||||||
|
// iOS 노치/홈바 safe-area 대응
|
||||||
|
.safe-top {
|
||||||
|
padding-top: env(safe-area-inset-top);
|
||||||
|
}
|
||||||
|
.safe-bottom {
|
||||||
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
|
}
|
||||||
Vendored
+15
@@ -0,0 +1,15 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
declare module '*.vue' {
|
||||||
|
import type { DefineComponent } from 'vue'
|
||||||
|
const component: DefineComponent<Record<string, never>, Record<string, never>, unknown>
|
||||||
|
export default component
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
readonly VITE_API_BASE_URL: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<template>
|
||||||
|
<q-layout view="hHh lpR fFf">
|
||||||
|
<q-header class="bg-primary text-white safe-top">
|
||||||
|
<q-toolbar>
|
||||||
|
<q-toolbar-title class="text-weight-bold">
|
||||||
|
<q-icon name="pets" class="q-mr-sm" />산책갈개
|
||||||
|
</q-toolbar-title>
|
||||||
|
<q-chip dense color="white" text-color="primary" icon="bolt">
|
||||||
|
{{ tierLabel }}
|
||||||
|
</q-chip>
|
||||||
|
</q-toolbar>
|
||||||
|
</q-header>
|
||||||
|
|
||||||
|
<q-page-container>
|
||||||
|
<router-view />
|
||||||
|
</q-page-container>
|
||||||
|
|
||||||
|
<!-- FREE 티어 하단 배너 광고 -->
|
||||||
|
<AdBanner />
|
||||||
|
|
||||||
|
<q-footer class="bg-white text-grey-8 safe-bottom">
|
||||||
|
<q-tabs
|
||||||
|
v-model="tab"
|
||||||
|
active-color="primary"
|
||||||
|
indicator-color="primary"
|
||||||
|
class="text-grey-6"
|
||||||
|
>
|
||||||
|
<q-route-tab name="home" :to="{ name: 'home' }" icon="map" label="산책 스팟" />
|
||||||
|
<q-route-tab name="profile" :to="{ name: 'profile' }" icon="pets" label="우리 개" />
|
||||||
|
</q-tabs>
|
||||||
|
</q-footer>
|
||||||
|
</q-layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import AdBanner from '@/components/AdBanner.vue'
|
||||||
|
import { useSubscriptionStore } from '@/stores/subscription'
|
||||||
|
|
||||||
|
const tab = ref('home')
|
||||||
|
const subscription = useSubscriptionStore()
|
||||||
|
|
||||||
|
const tierLabel = computed(() => {
|
||||||
|
switch (subscription.tier) {
|
||||||
|
case 'PREMIUM':
|
||||||
|
return 'PREMIUM'
|
||||||
|
case 'AD_FREE':
|
||||||
|
return '광고제거'
|
||||||
|
default:
|
||||||
|
return 'FREE'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import { Quasar, Notify } from 'quasar'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
|
||||||
|
// Quasar 필수 스타일 & 아이콘
|
||||||
|
import '@quasar/extras/material-icons/material-icons.css'
|
||||||
|
import '@quasar/extras/mdi-v7/mdi-v7.css'
|
||||||
|
import 'quasar/src/css/index.sass'
|
||||||
|
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
import './css/app.scss'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
app.use(Quasar, {
|
||||||
|
plugins: { Notify },
|
||||||
|
config: {
|
||||||
|
brand: {
|
||||||
|
// quasar-variables.scss 와 일치 (런타임 참고용)
|
||||||
|
primary: '#6DB3E8',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
app.use(createPinia())
|
||||||
|
app.use(router)
|
||||||
|
|
||||||
|
app.mount('#app')
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
<template>
|
||||||
|
<q-page class="q-pa-md">
|
||||||
|
<!-- 지도 영역 (추후 네이버/카카오 지도 SDK 연동 자리) -->
|
||||||
|
<q-card flat bordered class="map-placeholder flex flex-center q-mb-md">
|
||||||
|
<div class="text-center text-grey-6">
|
||||||
|
<q-icon name="map" size="48px" color="secondary" />
|
||||||
|
<div class="q-mt-sm text-caption">지도 영역 (스팟 앵커 표시 예정)</div>
|
||||||
|
</div>
|
||||||
|
</q-card>
|
||||||
|
|
||||||
|
<!-- 체크인 -->
|
||||||
|
<q-btn
|
||||||
|
color="primary"
|
||||||
|
class="full-width q-py-sm"
|
||||||
|
unelevated
|
||||||
|
icon="place"
|
||||||
|
:label="checkedIn ? '체크인 완료 · 여기 있어요!' : '나 지금 여기 도착!'"
|
||||||
|
@click="onCheckIn"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 스팟 한줄평 (네이버 지도식 태그형) -->
|
||||||
|
<div class="text-subtitle1 text-weight-bold q-mt-lg q-mb-sm">
|
||||||
|
한강공원 산책로 · 오늘의 한줄평
|
||||||
|
</div>
|
||||||
|
<div class="row q-gutter-sm">
|
||||||
|
<q-chip
|
||||||
|
v-for="review in spotReviews"
|
||||||
|
:key="review"
|
||||||
|
color="secondary"
|
||||||
|
text-color="accent"
|
||||||
|
icon="tag"
|
||||||
|
>
|
||||||
|
{{ review }}
|
||||||
|
</q-chip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 현재 체크인 중인 메이트 -->
|
||||||
|
<div class="text-subtitle1 text-weight-bold q-mt-lg q-mb-sm">지금 이 스팟의 산책 메이트</div>
|
||||||
|
<q-card
|
||||||
|
v-for="mate in mates"
|
||||||
|
:key="mate.name"
|
||||||
|
flat
|
||||||
|
bordered
|
||||||
|
class="q-mb-sm"
|
||||||
|
>
|
||||||
|
<q-item>
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-avatar color="secondary" text-color="accent" icon="pets" />
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label class="text-weight-medium">{{ mate.name }}</q-item-label>
|
||||||
|
<q-item-label caption>{{ mate.traits.join(' · ') }}</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section side>
|
||||||
|
<q-btn dense flat round color="primary" icon="chat_bubble_outline" />
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-card>
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useQuasar } from 'quasar'
|
||||||
|
|
||||||
|
const $q = useQuasar()
|
||||||
|
const checkedIn = ref(false)
|
||||||
|
|
||||||
|
const spotReviews = ['진드기 많아요', '풀숲 무성함', '그늘 시원해요', '물그릇 있음']
|
||||||
|
|
||||||
|
const mates = [
|
||||||
|
{ name: '콩이 (말티즈)', traits: ['대형견 무서워함', '사회화 훈련 중'] },
|
||||||
|
{ name: '보리 (리트리버)', traits: ['터그놀이 매니아', '사람 좋아함'] },
|
||||||
|
]
|
||||||
|
|
||||||
|
function onCheckIn() {
|
||||||
|
checkedIn.value = !checkedIn.value
|
||||||
|
$q.notify({
|
||||||
|
color: checkedIn.value ? 'primary' : 'grey-6',
|
||||||
|
message: checkedIn.value ? '체크인 완료! 스팟 채팅이 열렸어요.' : '체크아웃 되었습니다.',
|
||||||
|
icon: checkedIn.value ? 'place' : 'logout',
|
||||||
|
position: 'top',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.map-placeholder {
|
||||||
|
height: 240px;
|
||||||
|
background: linear-gradient(160deg, #eaf5fe 0%, #d7ecfb 100%);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<template>
|
||||||
|
<q-page class="q-pa-md">
|
||||||
|
<!-- 강아지 프로필 카드 -->
|
||||||
|
<q-card flat bordered class="q-mb-md">
|
||||||
|
<q-card-section class="row items-center">
|
||||||
|
<q-avatar size="64px" color="secondary" text-color="accent" icon="pets" />
|
||||||
|
<div class="q-ml-md">
|
||||||
|
<div class="text-h6">몽이</div>
|
||||||
|
<div class="text-caption text-grey-7">푸들 · 3살 · 소형견</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
|
||||||
|
<!-- 댕비티아이(성향 태그) -->
|
||||||
|
<div class="text-subtitle1 text-weight-bold q-mb-sm">댕비티아이 · 성향 태그</div>
|
||||||
|
<div class="row q-gutter-sm q-mb-lg">
|
||||||
|
<q-chip
|
||||||
|
v-for="trait in traits"
|
||||||
|
:key="trait.code"
|
||||||
|
:color="trait.selected ? 'primary' : 'grey-3'"
|
||||||
|
:text-color="trait.selected ? 'white' : 'grey-7'"
|
||||||
|
clickable
|
||||||
|
@click="trait.selected = !trait.selected"
|
||||||
|
>
|
||||||
|
{{ trait.label }}
|
||||||
|
</q-chip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 구독 티어 전환 (개발용 데모) -->
|
||||||
|
<q-card flat bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-subtitle2 q-mb-sm">구독 티어 (데모)</div>
|
||||||
|
<q-btn-toggle
|
||||||
|
v-model="tier"
|
||||||
|
spread
|
||||||
|
no-caps
|
||||||
|
toggle-color="primary"
|
||||||
|
color="grey-3"
|
||||||
|
text-color="grey-8"
|
||||||
|
:options="[
|
||||||
|
{ label: 'FREE', value: 'FREE' },
|
||||||
|
{ label: '광고제거', value: 'AD_FREE' },
|
||||||
|
{ label: 'PREMIUM', value: 'PREMIUM' },
|
||||||
|
]"
|
||||||
|
@update:model-value="onTierChange"
|
||||||
|
/>
|
||||||
|
<div class="text-caption text-grey-6 q-mt-sm">
|
||||||
|
FREE 에서만 하단 배너 광고가 노출됩니다.
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useSubscriptionStore, type SubscriptionTier } from '@/stores/subscription'
|
||||||
|
|
||||||
|
const subscription = useSubscriptionStore()
|
||||||
|
const tier = ref<SubscriptionTier>(subscription.tier)
|
||||||
|
|
||||||
|
const traits = ref([
|
||||||
|
{ code: 'FEAR_BIG_DOG', label: '대형견 무서워함', selected: true },
|
||||||
|
{ code: 'TUG_LOVER', label: '터그놀이 매니아', selected: true },
|
||||||
|
{ code: 'SOCIALIZING', label: '사회화 훈련 중', selected: false },
|
||||||
|
{ code: 'LOVE_PEOPLE', label: '사람 좋아함', selected: true },
|
||||||
|
{ code: 'SHY', label: '낯가림 있음', selected: false },
|
||||||
|
])
|
||||||
|
|
||||||
|
function onTierChange(next: SubscriptionTier) {
|
||||||
|
subscription.setTier(next)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// ============================================================
|
||||||
|
// 산책갈개 브랜드 컬러 — 하늘색 파스텔톤
|
||||||
|
// ============================================================
|
||||||
|
$primary : #6DB3E8; // 메인 하늘색
|
||||||
|
$secondary : #BFE3FB; // 파스텔 라이트 블루
|
||||||
|
$accent : #3E92CC; // 포인트(진한 하늘색)
|
||||||
|
|
||||||
|
$dark : #1D1D1D;
|
||||||
|
$dark-page : #121212;
|
||||||
|
|
||||||
|
$positive : #7FD1AE;
|
||||||
|
$negative : #F2A6A6;
|
||||||
|
$info : #A9D6F5;
|
||||||
|
$warning : #FCE5A6;
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
|
||||||
|
import MainLayout from '@/layouts/MainLayout.vue'
|
||||||
|
|
||||||
|
const routes: RouteRecordRaw[] = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
component: MainLayout,
|
||||||
|
children: [
|
||||||
|
{ path: '', name: 'home', component: () => import('@/pages/HomePage.vue') },
|
||||||
|
{ path: 'profile', name: 'profile', component: () => import('@/pages/ProfilePage.vue') },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(),
|
||||||
|
routes,
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
|
export type SubscriptionTier = 'FREE' | 'AD_FREE' | 'PREMIUM'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 구독 상태 스토어.
|
||||||
|
* - 광고 노출 여부(hideAds) 와 매칭 무제한 여부(unlimitedMatching)를 파생.
|
||||||
|
* - AdBanner 등 광고 컴포넌트는 hideAds 로 v-if 제어.
|
||||||
|
*/
|
||||||
|
export const useSubscriptionStore = defineStore('subscription', () => {
|
||||||
|
const tier = ref<SubscriptionTier>('FREE')
|
||||||
|
|
||||||
|
// FREE 만 광고 노출, AD_FREE·PREMIUM 은 광고 제거
|
||||||
|
const hideAds = computed(() => tier.value !== 'FREE')
|
||||||
|
|
||||||
|
// PREMIUM 만 매칭 무제한
|
||||||
|
const unlimitedMatching = computed(() => tier.value === 'PREMIUM')
|
||||||
|
|
||||||
|
// 하루 무료 매칭 잔여(무제한이면 null)
|
||||||
|
const remainingMatches = ref<number | null>(3)
|
||||||
|
|
||||||
|
function setTier(next: SubscriptionTier) {
|
||||||
|
tier.value = next
|
||||||
|
}
|
||||||
|
|
||||||
|
return { tier, hideAds, unlimitedMatching, remainingMatches, setTier }
|
||||||
|
})
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"strict": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"types": ["vite/client", "quasar"],
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue", "vite.config.ts", "capacitor.config.ts"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import { quasar, transformAssetUrls } from '@quasar/vite-plugin'
|
||||||
|
|
||||||
|
// Capacitor(네이티브 앱) 빌드 시 상대 경로 사용
|
||||||
|
export default defineConfig(({ mode }) => ({
|
||||||
|
base: mode === 'capacitor' ? './' : '/',
|
||||||
|
plugins: [
|
||||||
|
vue({ template: { transformAssetUrls } }),
|
||||||
|
quasar({ sassVariables: fileURLToPath(new URL('./src/quasar-variables.scss', import.meta.url)) }),
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 9000,
|
||||||
|
},
|
||||||
|
}))
|
||||||
Reference in New Issue
Block a user