Compare commits
6 Commits
c76c49d42f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 2661ec376b | |||
| c9f19e28ba | |||
|
|
1eba2a0a49 | ||
| e6d5f0436d | |||
| 0f8f0c2f51 | |||
| f7b2ba7f9c |
@@ -1,10 +1,33 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="relative py-8 px-4 bg-transparent">
|
<section class="relative py-8 px-4 bg-transparent">
|
||||||
<Carousel v-bind="config" class="mx-auto max-w-screen-xl">
|
<!-- 🔹 CTA Overlay -->
|
||||||
|
<div
|
||||||
|
class="absolute z-20 bottom-20 left-1/2 -translate-x-1/2 flex flex-col items-center gap-2 pointer-events-auto"
|
||||||
|
>
|
||||||
|
<!-- Red button -->
|
||||||
|
<NuxtLink
|
||||||
|
to="/shop"
|
||||||
|
class="rounded-full bg-red-500 text-white px-8 py-2.5 text-sm sm:text-base font-semibold shadow-lg hover:bg-red-600 transition"
|
||||||
|
>
|
||||||
|
지금 예약하기
|
||||||
|
</NuxtLink>
|
||||||
|
|
||||||
|
<!-- NPay -->
|
||||||
|
<NuxtLink to="/shop">
|
||||||
|
<img
|
||||||
|
:src="SOCIAL_IMAGES.npay"
|
||||||
|
alt="NPay"
|
||||||
|
class="h-6 sm:h-7 object-contain drop-shadow rounded-full"
|
||||||
|
/>
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 🔹 Carousel Section -->
|
||||||
|
<Carousel v-bind="config" class="mx-auto max-w-screen-xl relative">
|
||||||
<Slide
|
<Slide
|
||||||
v-for="(imgSrc, idx) in images"
|
v-for="(imgSrc, idx) in images"
|
||||||
:key="idx"
|
:key="idx"
|
||||||
class="relative h-64 sm:h-80 md:h-96 lg:h-[500px] overflow-hidden rounded-lg shadow-lg"
|
class="relative h-80 sm:h-96 md:h-[420px] lg:h-[800px] overflow-hidden rounded-lg shadow-lg"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
:src="imgSrc"
|
:src="imgSrc"
|
||||||
@@ -13,7 +36,6 @@
|
|||||||
/>
|
/>
|
||||||
</Slide>
|
</Slide>
|
||||||
|
|
||||||
<!-- Add navigation arrows and pagination indicators -->
|
|
||||||
<template #addons>
|
<template #addons>
|
||||||
<Navigation />
|
<Navigation />
|
||||||
<Pagination />
|
<Pagination />
|
||||||
@@ -21,54 +43,26 @@
|
|||||||
</Carousel>
|
</Carousel>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { Carousel, Slide, Navigation, Pagination } from 'vue3-carousel';
|
import { Carousel, Slide, Navigation, Pagination } from 'vue3-carousel';
|
||||||
import 'vue3-carousel/dist/carousel.css';
|
import 'vue3-carousel/dist/carousel.css';
|
||||||
import { MAIN_IMAGES } from '@/data/assets';
|
|
||||||
|
|
||||||
// Build a simple array of URLs from your MAIN_IMAGES object
|
import { MAIN_IMAGES, SOCIAL_IMAGES } from '@/data/assets';
|
||||||
|
|
||||||
const images = computed<string[]>(() => Object.values(MAIN_IMAGES));
|
const images = computed<string[]>(() => Object.values(MAIN_IMAGES));
|
||||||
|
|
||||||
// Carousel configuration
|
|
||||||
const config = {
|
const config = {
|
||||||
height: 500, // fixed height (px); adjust as needed (500px here)
|
height: 500,
|
||||||
itemsToShow: 1, // always show one slide at a time
|
itemsToShow: 1,
|
||||||
gap: 0, // no horizontal gap between slides
|
wrapAround: true,
|
||||||
wrapAround: true, // loop infinitely
|
autoplay: 4000,
|
||||||
mouseWheel: false, // disable mouse-wheel navigation
|
|
||||||
autoplay: 4000, // change slide every 4 seconds
|
|
||||||
pauseAutoplayOnHover: true,
|
pauseAutoplayOnHover: true,
|
||||||
breakpoints: {
|
breakpoints: {
|
||||||
1280: { itemsToShow: 1, height: 500 }, // ≥2xl
|
1280: { height: 500 },
|
||||||
1024: { itemsToShow: 1, height: 450 }, // ≥xl
|
1024: { height: 450 },
|
||||||
768: { itemsToShow: 1, height: 400 }, // ≥md
|
768: { height: 400 },
|
||||||
0: { itemsToShow: 1, height: 300 }, // mobile
|
0: { height: 300 },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
/* Customize arrow & pagination colors via CSS variables */
|
|
||||||
.carousel {
|
|
||||||
--vc-nav-background: rgba(255, 255, 255, 0.8);
|
|
||||||
--vc-nav-border-radius: 100%;
|
|
||||||
--vc-nav-icon-size: 1.25rem;
|
|
||||||
--vc-pgn-background-color: rgba(255, 255, 255, 0.5);
|
|
||||||
--vc-pgn-active-color: rgba(255, 255, 255, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Optional: subtle shadow behind navigation buttons */
|
|
||||||
.carousel__navigation-button {
|
|
||||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Ensure all images are rounded and cover the slide area */
|
|
||||||
img {
|
|
||||||
border-radius: 8px;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,54 +1,120 @@
|
|||||||
<template>
|
<template>
|
||||||
<header class="relative isolate z-10 bg-white dark:bg-gray-900 shadow-sm">
|
<header class="relative isolate z-10 bg-white dark:bg-gray-900 shadow-sm">
|
||||||
<nav
|
<nav
|
||||||
class="mx-auto flex max-w-7xl min-h-20 lg:min-h-32 items-end justify-between p-6 lg:px-8"
|
class="mx-auto max-w-7xl min-h-20 lg:min-h-32 p-6 lg:px-8 grid grid-cols-3 items-center"
|
||||||
aria-label="Global Navigation"
|
aria-label="Global Navigation"
|
||||||
>
|
>
|
||||||
<!-- MobileSidebar Open Button -->
|
<!-- Left: MobileSidebar Button -->
|
||||||
<div class="flex justify-start">
|
<div class="flex justify-start">
|
||||||
<!-- <button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="-m-2.5 inline-flex items-center justify-center rounded-md p-2.5 text-gray-700 dark:text-white"
|
class="-m-2.5 inline-flex items-center justify-center rounded-md p-2.5 text-gray-700 dark:text-white"
|
||||||
@click="mobileMenuOpen = true"
|
@click="mobileMenuOpen = true"
|
||||||
aria-label="Open main menu"
|
aria-label="Open main menu"
|
||||||
>
|
>
|
||||||
<Bars3Icon class="size-6" aria-hidden="true" />
|
<Bars3Icon class="size-6" aria-hidden="true" />
|
||||||
</button> -->
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Desktop Navigation -->
|
<!-- Center: Logo -->
|
||||||
<!-- Logo -->
|
<div class="flex justify-center">
|
||||||
<div class="flex">
|
|
||||||
<NuxtLink to="/" class="-m-1.5 p-1.5" aria-label="Bobu Home">
|
<NuxtLink to="/" class="-m-1.5 p-1.5" aria-label="Bobu Home">
|
||||||
<span class="sr-only">Bobu</span>
|
<span class="sr-only">Bobu</span>
|
||||||
<img
|
<img
|
||||||
class="h-14 w-auto block dark:hidden transition-all duration-300"
|
class="h-10 w-auto block dark:hidden transition-all duration-300"
|
||||||
:src="LOGOS.Red"
|
:src="LOGOS.Red"
|
||||||
alt="Bobu Logo Light"
|
alt="Bobu Logo Light"
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
class="h-14 w-auto hidden dark:block transition-all duration-300"
|
class="h-10 w-auto hidden dark:block transition-all duration-300"
|
||||||
:src="LOGOS.White"
|
:src="LOGOS.White"
|
||||||
alt="Bobu Logo Dark"
|
alt="Bobu Logo Dark"
|
||||||
/>
|
/>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
<!-- <PopoverGroup class="hidden lg:flex lg:gap-x-12">
|
|
||||||
<NuxtLink
|
|
||||||
v-for="item in navItems"
|
|
||||||
:key="item.name"
|
|
||||||
:to="item.href"
|
|
||||||
class="text-sm font-semibold leading-6 text-gray-900 dark:text-white hover:text-gray-700 dark:hover:text-gray-300"
|
|
||||||
>
|
|
||||||
{{ item.name }}
|
|
||||||
</NuxtLink>
|
|
||||||
</PopoverGroup> -->
|
|
||||||
|
|
||||||
<!-- Desktop HeaderActions -->
|
<!-- Right: Buttons -->
|
||||||
<div class="flex justify-end">
|
<div class="flex justify-end items-center">
|
||||||
<!-- <HeaderActions /> -->
|
<!-- Mobile: Icon Buttons -->
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-x-4 lg:hidden text-gray-700 dark:text-white"
|
||||||
|
>
|
||||||
|
<!-- If NOT logged in → Login Icon -->
|
||||||
|
<NuxtLink
|
||||||
|
v-if="!userStore.userLoggedIn"
|
||||||
|
to="/login"
|
||||||
|
class="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
|
||||||
|
>
|
||||||
|
<font-awesome-icon
|
||||||
|
:icon="['fas', 'circle-user']"
|
||||||
|
class="text-2xl"
|
||||||
|
/>
|
||||||
|
</NuxtLink>
|
||||||
|
|
||||||
|
<!-- If logged in → My Page Icon -->
|
||||||
|
<NuxtLink
|
||||||
|
v-else
|
||||||
|
to="/mypage"
|
||||||
|
class="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
|
||||||
|
>
|
||||||
|
<font-awesome-icon
|
||||||
|
:icon="['fas', 'circle-user']"
|
||||||
|
class="text-2xl"
|
||||||
|
/>
|
||||||
|
</NuxtLink>
|
||||||
|
|
||||||
|
<!-- Cart Icon -->
|
||||||
|
<NuxtLink
|
||||||
|
to="/shop"
|
||||||
|
class="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
|
||||||
|
>
|
||||||
|
<font-awesome-icon
|
||||||
|
:icon="['fas', 'shopping-cart']"
|
||||||
|
class="text-xl"
|
||||||
|
/>
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 🖥 Desktop: Text Buttons -->
|
||||||
|
<div class="hidden lg:flex items-center gap-x-3">
|
||||||
|
<!-- 로그인 / 회원가입 (red border) -->
|
||||||
|
<NuxtLink
|
||||||
|
v-if="!userStore.userLoggedIn"
|
||||||
|
to="/login"
|
||||||
|
class="rounded-full border border-red-500 text-red-500 bg-white px-3 py-1.5 text-xs font-semibold hover:bg-red-50 dark:hover:bg-red-900/20 transition"
|
||||||
|
>
|
||||||
|
로그인 / 회원가입
|
||||||
|
</NuxtLink>
|
||||||
|
|
||||||
|
<!-- 마이페이지 (red border) -->
|
||||||
|
<NuxtLink
|
||||||
|
v-else
|
||||||
|
to="/mypage"
|
||||||
|
class="rounded-full border border-red-500 text-red-500 bg-white px-3 py-1.5 text-xs font-semibold hover:bg-red-50 dark:hover:bg-blue-900/20 transition"
|
||||||
|
>
|
||||||
|
마이페이지
|
||||||
|
</NuxtLink>
|
||||||
|
|
||||||
|
<!-- 장바구니 (amber border) -->
|
||||||
|
<NuxtLink
|
||||||
|
to="/shop"
|
||||||
|
class="rounded-full border border-amber-500 text-amber-500 bg-white px-3 py-1.5 text-xs font-semibold hover:bg-amber-50 dark:hover:bg-amber-900/20 transition"
|
||||||
|
>
|
||||||
|
장바구니
|
||||||
|
</NuxtLink>
|
||||||
|
|
||||||
|
<!-- 로그아웃 (gray border, can be red if you prefer) -->
|
||||||
|
<button
|
||||||
|
v-if="userStore.userLoggedIn"
|
||||||
|
@click="userStore.signOut()"
|
||||||
|
class="rounded-full border border-gray-400 text-gray-600 bg-white px-3 py-1.5 text-xs font-semibold hover:bg-gray-100 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-gray-800 transition"
|
||||||
|
>
|
||||||
|
로그아웃
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- MobileSidebar -->
|
<!-- MobileSidebar -->
|
||||||
|
|
||||||
<Dialog
|
<Dialog
|
||||||
@@ -117,6 +183,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="py-6 space-y-2">
|
<div class="py-6 space-y-2">
|
||||||
|
<!-- Logout -->
|
||||||
|
<button
|
||||||
|
v-if="userStore.userLoggedIn"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
userStore.signOut();
|
||||||
|
mobileMenuOpen = false;
|
||||||
|
}
|
||||||
|
"
|
||||||
|
class="-mx-3 block rounded-lg px-3 py-2 text-base font-semibold leading-7 text-gray-500 dark:text-gray-300 hover:text-gray-50 dark:hover:text-gray-800 hover:bg-gray-100 dark:hover:bg-gray-800 transition"
|
||||||
|
>
|
||||||
|
로그아웃
|
||||||
|
</button>
|
||||||
<!-- Not logged in: show 로그인/회원가입 -->
|
<!-- Not logged in: show 로그인/회원가입 -->
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
v-if="!userStore.userLoggedIn"
|
v-if="!userStore.userLoggedIn"
|
||||||
|
|||||||
3436
bobu/app/components/PersonalInformation.vue
Normal file
3436
bobu/app/components/PersonalInformation.vue
Normal file
File diff suppressed because it is too large
Load Diff
2243
bobu/app/components/TermsofUse.vue
Normal file
2243
bobu/app/components/TermsofUse.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -8,20 +8,25 @@
|
|||||||
<p
|
<p
|
||||||
class="text-center text-2xl font-extrabold text-indigo-500 dark:text-indigo-400"
|
class="text-center text-2xl font-extrabold text-indigo-500 dark:text-indigo-400"
|
||||||
>
|
>
|
||||||
와디즈 펀딩 참여자 발송 페이지
|
가벼운 채비로 떠나는 정선 백패킹 포레스트
|
||||||
</p>
|
</p>
|
||||||
<figure class="mt-10">
|
<figure class="mt-10">
|
||||||
<blockquote
|
<blockquote
|
||||||
class="text-center text-lg font-normal text-gray-900 dark:text-gray-100"
|
class="text-center text-lg font-normal text-gray-900 dark:text-gray-100"
|
||||||
>
|
>
|
||||||
<p>안녕하세요, 메이커 보부입니다!</p>
|
<p>안녕하세요. 주식회사 보부입니다.</p>
|
||||||
<p>보부의 첫 여정에 마음을 더해주셔서 감사합니다.</p>
|
<p>정선 백패킹 포레스트 예약 시스템입니다.</p>
|
||||||
<p>여러분의 따뜻한 응원 덕분에,</p>
|
<br />
|
||||||
<p>정선의 자연속 1박 2일의 쉼을 준비할 수 있었습니다.</p>
|
<p>
|
||||||
<p>자연에 기대어, 잠시 천천히 머물러보는 시간을 선물해드릴게요.</p>
|
원활한 운영을 위해 아래 '참여자 정보 입력하기'를 클릭 후 문항을
|
||||||
<p class="mt-4 font-semibold">
|
작성해주시기 바랍니다.
|
||||||
“보부와 함께할 하루, 이제 예약으로 이어집니다."
|
|
||||||
</p>
|
</p>
|
||||||
|
<p>
|
||||||
|
정성스러운 답변은 더 나은 경험을 준비하는 데 큰 도움이 됩니다.
|
||||||
|
</p>
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<p>감사합니다.</p>
|
||||||
</blockquote>
|
</blockquote>
|
||||||
</figure>
|
</figure>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
44
bobu/app/components/about/AboutSection0.vue
Normal file
44
bobu/app/components/about/AboutSection0.vue
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<template>
|
||||||
|
<section class="bg-gray-50 dark:bg-gray-900 py-16 sm:py-20">
|
||||||
|
<div class="mx-auto max-w-4xl px-4">
|
||||||
|
<!-- Title -->
|
||||||
|
<div class="text-center">
|
||||||
|
<h2
|
||||||
|
class="text-sm font-semibold tracking-[0.2em] text-gray-500 dark:text-gray-400 uppercase"
|
||||||
|
>
|
||||||
|
BOBU – Nomad Social Club
|
||||||
|
</h2>
|
||||||
|
<p
|
||||||
|
class="mt-4 text-2xl sm:text-3xl font-bold text-gray-900 dark:text-gray-100 leading-snug"
|
||||||
|
>
|
||||||
|
자연 속, 자유롭지만 차분한 공간.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p
|
||||||
|
class="mt-2 text-base sm:text-lg font-medium text-gray-600 dark:text-gray-300 leading-relaxed"
|
||||||
|
>
|
||||||
|
정선의 유일한 공유 오피스에서 일과 쉼의 균형을 찾아보세요.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Body text -->
|
||||||
|
<div
|
||||||
|
class="mt-8 text-center sm:text-left text-gray-700 dark:text-gray-300 leading-relaxed"
|
||||||
|
>
|
||||||
|
<p class="whitespace-pre-line">
|
||||||
|
멀리 가지 않아도 곁에 있는 자연. 낮에는 머물고, 저녁엔 떠나고, 밤에는
|
||||||
|
자연 속에서 쉬어가는 일과 쉼의 순환. 보부는 그 한 사이의 연결을
|
||||||
|
제공합니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// Presentational only – no script logic needed for now
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Tailwind handles most styling; no extra CSS for now */
|
||||||
|
</style>
|
||||||
@@ -1,76 +1,84 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="bg-white dark:bg-gray-900 py-16 sm:py-24">
|
<section class="bg-gray-50 dark:bg-gray-900 py-16 sm:py-24">
|
||||||
<div class="mx-auto max-w-3xl px-4 text-center">
|
<div class="mx-auto max-w-3xl px-4 text-center">
|
||||||
<!-- Title -->
|
<!-- Title -->
|
||||||
<h2
|
<h2
|
||||||
class="text-3xl font-bold text-gray-900 dark:text-gray-100 sm:text-4xl"
|
class="text-3xl font-bold text-gray-900 dark:text-gray-100 sm:text-4xl"
|
||||||
>
|
>
|
||||||
노마드소셜클럽 보부<br />
|
노마드소셜클럽 보부<br />
|
||||||
<span class="text-xl font-medium text-gray-600 dark:text-gray-300"
|
<span class="text-xl font-medium text-gray-600 dark:text-gray-300">
|
||||||
>Nomad Social Club BOBU</span
|
Nomad Social Club BOBU
|
||||||
>
|
</span>
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Image Grid -->
|
<!-- Image Grid -->
|
||||||
<div
|
<div
|
||||||
class="mt-12 mx-auto grid max-w-4xl grid-cols-2 gap-6 px-4 sm:grid-cols-4"
|
class="mt-10 mx-auto grid max-w-3xl grid-cols-2 sm:grid-cols-3 gap-4 px-4"
|
||||||
>
|
>
|
||||||
<div
|
<img
|
||||||
v-for="(imgSrc, idx) in images"
|
v-for="(imgSrc, idx) in images"
|
||||||
:key="idx"
|
:key="idx"
|
||||||
class="flex justify-center"
|
:src="imgSrc"
|
||||||
>
|
alt="About image"
|
||||||
<img
|
class="aspect-square w-full rounded-xl object-cover shadow-md dark:shadow-none dark:bg-gray-800"
|
||||||
:src="imgSrc"
|
/>
|
||||||
alt="About image"
|
|
||||||
class="h-32 w-32 rounded-full object-cover shadow-md"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Description Text -->
|
<!-- Description -->
|
||||||
<div
|
<div
|
||||||
class="mt-12 mx-auto max-w-2xl px-4 text-center text-gray-700 dark:text-gray-300"
|
class="mt-12 mx-auto max-w-2xl px-4 text-center text-gray-700 dark:text-gray-300"
|
||||||
>
|
>
|
||||||
<p class="whitespace-pre-line leading-relaxed">
|
<!-- Description Text -->
|
||||||
강원도 정선군 정선읍 봉양리, 작은 마을에 자리한 워케이션 오피스입니다.
|
<div
|
||||||
다양한 방식으로 일하는 창작자들을 위해 공유 오피스, 로컬 콘텐츠,
|
class="mt-12 mx-auto max-w-2xl px-4 text-center text-gray-700 dark:text-gray-300"
|
||||||
투어(워크숍) 등을 운영하며 일과 일상, 여행이 자연스럽게 이어지는 유연한
|
>
|
||||||
삶의 방식을 제안합니다.
|
<!-- Lead / Highlight -->
|
||||||
</p>
|
<p
|
||||||
|
class="text-lg sm:text-xl font-semibold leading-relaxed text-gray-900 dark:text-gray-100"
|
||||||
|
>
|
||||||
|
자연 속, 차분하게 몰입하는 공간.<br />
|
||||||
|
정선의 유일한 공유 오피스, WORK & STOP.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Body paragraphs -->
|
||||||
|
<div class="mt-6 space-y-4 text-sm sm:text-base leading-relaxed">
|
||||||
|
<p>
|
||||||
|
일과 쉼이 자연스럽게 이어지는 공간을 만들었습니다. 틀에 갇힌
|
||||||
|
사무실이 아닌, 자유롭지만 차분한 분위기 속에서 몸과 마음이
|
||||||
|
편안해지고 집중이 깊어집니다.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
성과를 위한 몰입의 순간에도 쉼의 가치를 잃지 않도록. 여유와 놀이가
|
||||||
|
공존하는 공간에서 감정과 에너지를 부드럽게 환기하고, 나를 위한
|
||||||
|
충전의 시간을 마련하세요.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
정선의 맑은 공기와 자연이 함께하는 이곳, 보부에서 오늘보다 단단한
|
||||||
|
내일을 준비하는 당신을 기다립니다. 정선까지 온 당신에게, 쉬어갈 한
|
||||||
|
호흡을 선물하세요.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Link Buttons with Logos -->
|
<!-- Link Buttons -->
|
||||||
<div class="mt-8 flex justify-center space-x-6 px-4">
|
<div class="mt-10 flex justify-center px-4">
|
||||||
<!-- 네이버플레이스 -->
|
<!-- 네이버 예약 (Place or Reservation) -->
|
||||||
<a
|
<a
|
||||||
:href="socialLinks.naver"
|
:href="socialLinks.naver"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
class="flex items-center space-x-2 px-4 py-2 bg-green-600 text-white font-semibold rounded-lg hover:bg-green-700 transition"
|
class="flex items-center space-x-2 px-6 py-3 bg-green-600 text-white font-semibold rounded-lg hover:bg-green-700 transition"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
:src="socialImages.naver"
|
:src="socialImages.naver"
|
||||||
alt="네이버플레이스 로고"
|
alt="네이버 예약 로고"
|
||||||
class="h-6 w-6 object-contain"
|
class="h-6 w-6 object-contain"
|
||||||
/>
|
/>
|
||||||
<span>네이버플레이스</span>
|
<span>공유 오피스 예약하기</span>
|
||||||
</a>
|
|
||||||
|
|
||||||
<!-- 인스타그램 -->
|
|
||||||
<a
|
|
||||||
:href="socialLinks.instagram"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
class="flex items-center space-x-2 px-4 py-2 bg-pink-500 text-white font-semibold rounded-lg hover:bg-pink-600 transition"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
:src="socialImages.instagram"
|
|
||||||
alt="인스타그램 로고"
|
|
||||||
class="h-6 w-6 object-contain"
|
|
||||||
/>
|
|
||||||
<span>인스타그램</span>
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -81,14 +89,11 @@ import { computed } from 'vue';
|
|||||||
import { MAIN2_IMAGES, SOCIAL_IMAGES } from '@/data/assets';
|
import { MAIN2_IMAGES, SOCIAL_IMAGES } from '@/data/assets';
|
||||||
import { SOCIAL_LINKS } from '@/data/config';
|
import { SOCIAL_LINKS } from '@/data/config';
|
||||||
|
|
||||||
// 1) MAIN2_IMAGES → 배열
|
const images = [MAIN2_IMAGES.main1, MAIN2_IMAGES.main2, MAIN2_IMAGES.main3];
|
||||||
const images = computed<string[]>(() => Object.values(MAIN2_IMAGES));
|
|
||||||
|
|
||||||
// 2) SOCIAL_IMAGES (로고)와 SOCIAL_LINKS (URL) 노출
|
|
||||||
const socialImages = SOCIAL_IMAGES;
|
const socialImages = SOCIAL_IMAGES;
|
||||||
const socialLinks = SOCIAL_LINKS;
|
const socialLinks = SOCIAL_LINKS;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* Tailwind 유틸리티만으로 충분하므로 추가 CSS 불필요 */
|
/* Tailwind only */
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,97 +1,77 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="bg-white dark:bg-gray-900 py-24 sm:py-32">
|
<section
|
||||||
<div class="mx-auto max-w-7xl px-6 lg:px-8">
|
class="bg-white pt-20 pb-16 sm:pt-24 sm:pb-24 xl:pb-32 dark:bg-gray-900"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="pb-16 sm:pb-20 xl:pb-0 dark:bg-gray-800/60 dark:outline dark:outline-white/5"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class="mx-auto grid max-w-2xl grid-cols-1 items-start gap-x-8 gap-y-16 sm:gap-y-24 lg:mx-0 lg:max-w-none lg:grid-cols-2"
|
class="mx-auto flex max-w-7xl flex-col items-center gap-x-8 gap-y-10 px-6 sm:gap-y-8 lg:px-8 xl:flex-row xl:items-stretch"
|
||||||
>
|
>
|
||||||
<!-- Quote + Image Card -->
|
<!-- Left: Image -->
|
||||||
<div class="lg:pr-4">
|
<div class="-mt-8 w-full max-w-2xl xl:-mb-8 xl:w-96 xl:flex-none">
|
||||||
<div
|
<div
|
||||||
class="relative overflow-hidden rounded-3xl bg-gray-900 px-6 pt-64 pb-9 shadow-2xl sm:px-12 lg:max-w-lg lg:px-8 lg:pb-8 xl:px-10 xl:pb-10"
|
class="relative h-56 sm:h-72 md:h-80 lg:h-96 xl:h-full aspect-auto after:absolute after:inset-0 after:rounded-2xl after:inset-ring after:inset-ring-black/5 dark:after:inset-ring-white/15 md:-mx-8 xl:mx-0"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
class="absolute inset-0 w-full h-full object-cover brightness-105 saturate-100"
|
class="absolute inset-0 size-full rounded-2xl bg-gray-200 object-cover shadow-2xl dark:bg-gray-700 dark:shadow-none"
|
||||||
src="/assets/img/shoot.jpg"
|
:src="MAIN2_IMAGES.main3"
|
||||||
alt="Bobu team visual"
|
alt="BOBU Nomad Social Club"
|
||||||
/>
|
/>
|
||||||
<!-- dark overlay ↔ lighten in light mode -->
|
|
||||||
<div class="absolute inset-0 bg-black/40 dark:bg-black/60" />
|
|
||||||
<figure class="relative isolate">
|
|
||||||
<blockquote class="mt-6 text-xl font-semibold text-white">
|
|
||||||
<p>
|
|
||||||
{{ $t('about.quote') }}
|
|
||||||
</p>
|
|
||||||
</blockquote>
|
|
||||||
<figcaption class="mt-6 text-sm text-gray-300">
|
|
||||||
<strong class="font-semibold text-white">CEO,</strong>
|
|
||||||
{{ $t('about.ceo') }}
|
|
||||||
</figcaption>
|
|
||||||
</figure>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Right: Text block -->
|
||||||
<!-- Text Block -->
|
<div
|
||||||
<div>
|
class="w-full max-w-2xl xl:max-w-none xl:flex-auto xl:px-16 xl:py-20"
|
||||||
<div class="text-base text-gray-700 dark:text-gray-300 lg:max-w-lg">
|
>
|
||||||
|
<figure class="relative isolate pt-8 sm:pt-12">
|
||||||
|
<!-- Decorative quote SVG -->
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 162 128"
|
||||||
|
fill="none"
|
||||||
|
aria-hidden="true"
|
||||||
|
class="absolute top-0 left-0 -z-10 h-24 stroke-gray-300 dark:stroke-white/15"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
id="about-bobu-quote-shape"
|
||||||
|
d="M65.5697 118.507L65.8918 118.89C68.9503 116.314 71.367 113.253 73.1386 109.71C74.9162 106.155 75.8027 102.28 75.8027 98.0919C75.8027 94.237 75.16 90.6155 73.8708 87.2314C72.5851 83.8565 70.8137 80.9533 68.553 78.5292C66.4529 76.1079 63.9476 74.2482 61.0407 72.9536C58.2795 71.4949 55.276 70.767 52.0386 70.767C48.9935 70.767 46.4686 71.1668 44.4872 71.9924L44.4799 71.9955L44.4726 71.9988C42.7101 72.7999 41.1035 73.6831 39.6544 74.6492C38.2407 75.5916 36.8279 76.455 35.4159 77.2394L35.4047 77.2457L35.3938 77.2525C34.2318 77.9787 32.6713 78.3634 30.6736 78.3634C29.0405 78.3634 27.5131 77.2868 26.1274 74.8257C24.7483 72.2185 24.0519 69.2166 24.0519 65.8071C24.0519 60.0311 25.3782 54.4081 28.0373 48.9335C30.703 43.4454 34.3114 38.345 38.8667 33.6325C43.5812 28.761 49.0045 24.5159 55.1389 20.8979C60.1667 18.0071 65.4966 15.6179 71.1291 13.7305C73.8626 12.8145 75.8027 10.2968 75.8027 7.38572C75.8027 3.6497 72.6341 0.62247 68.8814 1.1527C61.1635 2.2432 53.7398 4.41426 46.6119 7.66522C37.5369 11.6459 29.5729 17.0612 22.7236 23.9105C16.0322 30.6019 10.618 38.4859 6.47981 47.558L6.47976 47.558L6.47682 47.5647C2.4901 56.6544 0.5 66.6148 0.5 77.4391C0.5 84.2996 1.61702 90.7679 3.85425 96.8404L3.8558 96.8445C6.08991 102.749 9.12394 108.02 12.959 112.654L12.959 112.654L12.9646 112.661C16.8027 117.138 21.2829 120.739 26.4034 123.459L26.4033 123.459L26.4144 123.465C31.5505 126.033 37.0873 127.316 43.0178 127.316C47.5035 127.316 51.6783 126.595 55.5376 125.148L55.5376 125.148L55.5477 125.144C59.5516 123.542 63.0052 121.456 65.9019 118.881L65.5697 118.507Z"
|
||||||
|
/>
|
||||||
|
<use href="#about-bobu-quote-shape" x="86" />
|
||||||
|
</svg>
|
||||||
|
<!-- Top: 3 키워드 -->
|
||||||
|
<blockquote
|
||||||
|
class="text-lg/8 sm:text-xl/8 font-semibold text-gray-900 dark:text-gray-100"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
‘노마드 (생활인구)’<br />
|
||||||
|
‘소셜 (관계 맺음)’<br />
|
||||||
|
‘클럽 (공간)’
|
||||||
|
</p>
|
||||||
|
</blockquote>
|
||||||
|
<!-- Middle: 첫 문단 -->
|
||||||
<p
|
<p
|
||||||
class="text-base font-semibold text-indigo-600 dark:text-indigo-400"
|
class="mt-6 text-base/7 sm:text-lg/8 text-gray-800 dark:text-gray-200"
|
||||||
>
|
>
|
||||||
{{ $t('about.sectionLabel') }}
|
세가지 주요 키워드로 다양한 사람들이 안전하고 즐거운 관계를 맺을
|
||||||
|
수 있도록, 노마드소셜클럽 보부(BOBU)는 지역 문화를 기반으로
|
||||||
|
활동하는 로컬 브랜드입니다.
|
||||||
</p>
|
</p>
|
||||||
<h1
|
<!-- Bottom: 두 번째 문단 -->
|
||||||
class="mt-2 text-4xl font-semibold tracking-tight text-gray-900 dark:text-white sm:text-5xl"
|
<p
|
||||||
|
class="mt-4 text-base/7 sm:text-lg/8 text-gray-800 dark:text-gray-200"
|
||||||
>
|
>
|
||||||
{{ $t('about.sectionTitle') }}
|
정선을 찾는 생활인구와 노마드 워커들을 위한 일(WORK)과 쉼(STOP)의
|
||||||
</h1>
|
공간을 운영하며 로컬을 경험하고 즐길 수 있는 콘텐츠를 제작합니다.
|
||||||
|
</p>
|
||||||
<div class="max-w-xl">
|
</figure>
|
||||||
<p class="mt-6">{{ $t('about.mission') }}</p>
|
|
||||||
<p class="mt-8">{{ $t('about.vision') }}</p>
|
|
||||||
<p class="mt-8">{{ $t('about.portfolio') }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Stats -->
|
|
||||||
<dl
|
|
||||||
class="mt-10 grid grid-cols-2 gap-8 border-t border-gray-900/10 dark:border-gray-100/10 pt-10 sm:grid-cols-4"
|
|
||||||
>
|
|
||||||
<div v-for="(stat, statIdx) in stats" :key="statIdx">
|
|
||||||
<dt
|
|
||||||
class="text-sm font-semibold text-gray-600 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
{{ stat.label }}
|
|
||||||
</dt>
|
|
||||||
<dd
|
|
||||||
class="mt-2 text-3xl font-bold tracking-tight text-gray-900 dark:text-white"
|
|
||||||
>
|
|
||||||
{{ stat.value }}
|
|
||||||
</dd>
|
|
||||||
</div>
|
|
||||||
</dl>
|
|
||||||
|
|
||||||
<!-- CTA -->
|
|
||||||
<div class="mt-10 flex">
|
|
||||||
<NuxtLink
|
|
||||||
to="/projects"
|
|
||||||
class="text-base font-semibold text-indigo-600 dark:text-indigo-400 hover:underline"
|
|
||||||
>
|
|
||||||
{{ $t('about.cta') }} <span aria-hidden="true">→</span>
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const stats = [
|
import { MAIN2_IMAGES } from '@/data/assets'; // Presentational only – no logic needed
|
||||||
{ label: 'Founded', value: '2018' },
|
|
||||||
{ label: 'Projects Delivered', value: '120+' },
|
|
||||||
{ label: 'Technologies', value: 'FILM / XR / 3D' },
|
|
||||||
{ label: 'Countries', value: '3' },
|
|
||||||
];
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* Tailwind utilities handle all dark/light styling */
|
/* Tailwind handles visuals */
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,94 +1,107 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="bg-white dark:bg-gray-900 py-24 sm:py-32">
|
<section class="bg-white dark:bg-gray-900 py-16 sm:py-24">
|
||||||
<div class="mx-auto max-w-7xl px-6 lg:px-8">
|
<div class="mx-auto max-w-4xl px-4">
|
||||||
<div class="mx-auto max-w-2xl text-center">
|
<!-- Heading -->
|
||||||
<h2
|
<header class="text-center">
|
||||||
class="text-3xl font-bold text-gray-900 dark:text-gray-100 sm:text-4xl"
|
<p
|
||||||
|
class="text-sm font-semibold tracking-[0.15em] text-gray-500 dark:text-gray-400 uppercase"
|
||||||
>
|
>
|
||||||
지난 프로그램 또는 후기
|
일을 마치고, 자연으로 떠나는 시간
|
||||||
|
</p>
|
||||||
|
<h2
|
||||||
|
class="mt-3 text-2xl sm:text-3xl font-bold text-gray-900 dark:text-gray-100"
|
||||||
|
>
|
||||||
|
백패킹 장비 대여
|
||||||
</h2>
|
</h2>
|
||||||
<p class="mt-2 text-lg/8 text-gray-600 dark:text-gray-400">
|
</header>
|
||||||
Learn how to grow your business with our expert advice.
|
<!-- 🌄 Image Grid (placed *after* content) -->
|
||||||
|
<div class="mt-10 grid grid-cols-2 gap-4 sm:grid-cols-4">
|
||||||
|
<img
|
||||||
|
v-for="(img, i) in imageList"
|
||||||
|
:key="i"
|
||||||
|
:src="img"
|
||||||
|
class="aspect-square w-full rounded-xl object-cover shadow-md dark:shadow-none dark:bg-gray-800"
|
||||||
|
alt="Backpacking image"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Body text -->
|
||||||
|
<div
|
||||||
|
class="mt-8 space-y-4 text-sm sm:text-base leading-relaxed text-gray-700 dark:text-gray-300"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
보부에서는 정선의 자연을 온전히 느낄 수 있도록 백패킹 장비 대여
|
||||||
|
서비스를 함께 운영하고 있습니다.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
가벼운 마음으로 떠날 수 있게 텐트부터 침낭, 랜턴까지 필요한 장비를
|
||||||
|
준비해드려요.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
일의 여운을 자연이 부드럽게 감싸주며, 오늘의 나를 위한 쉼은 보부에서
|
||||||
|
그렇게 완성됩니다.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
class="mx-auto mt-16 grid max-w-2xl auto-rows-fr grid-cols-1 gap-8 sm:mt-20 lg:mx-0 lg:max-w-none lg:grid-cols-3"
|
<!-- Gear list -->
|
||||||
>
|
<div class="mt-10 rounded-2xl bg-gray-50 px-5 py-6 dark:bg-gray-800/70">
|
||||||
<article
|
<p
|
||||||
v-for="post in posts"
|
class="text-xs font-semibold tracking-wide text-gray-600 dark:text-gray-300"
|
||||||
:key="post.id"
|
>
|
||||||
class="relative flex flex-col justify-end overflow-hidden rounded-2xl"
|
기본 대여 장비
|
||||||
|
</p>
|
||||||
|
<ul
|
||||||
|
class="mt-3 flex flex-wrap gap-2 text-xs sm:text-sm text-gray-700 dark:text-gray-200"
|
||||||
|
>
|
||||||
|
<li class="pill">배낭</li>
|
||||||
|
<li class="pill">텐트</li>
|
||||||
|
<li class="pill">자충매트</li>
|
||||||
|
<li class="pill">침낭</li>
|
||||||
|
<li class="pill">의자</li>
|
||||||
|
<li class="pill">테이블</li>
|
||||||
|
<li class="pill">D팩</li>
|
||||||
|
<li class="pill">랜턴</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CTA -->
|
||||||
|
<div class="mt-10 flex flex-col items-center gap-4 text-center">
|
||||||
|
<p class="text-sm sm:text-base text-gray-700 dark:text-gray-300">
|
||||||
|
<strong
|
||||||
|
>오롯이 나에게 집중할 수 있는 시간, 정선의 자연을
|
||||||
|
경험하세요.</strong
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<NuxtLink
|
||||||
|
to="/backpacking"
|
||||||
|
class="flex items-center space-x-2 px-6 py-3 bg-green-600 text-white font-semibold rounded-lg hover:bg-green-700 transition"
|
||||||
>
|
>
|
||||||
<!-- Background image -->
|
|
||||||
<img
|
<img
|
||||||
:src="post.imageUrl"
|
:src="SOCIAL_IMAGES.naver"
|
||||||
alt=""
|
alt="백패킹 예약 아이콘"
|
||||||
class="absolute inset-0 w-full h-full object-cover z-0"
|
class="h-6 w-6 object-contain"
|
||||||
/>
|
/>
|
||||||
<!-- 20% black overlay in light, 50% in dark -->
|
<span>백패킹 예약하기</span>
|
||||||
<div class="absolute inset-0 bg-black/20 dark:bg-black/50 z-10"></div>
|
</NuxtLink>
|
||||||
<!-- Gradient on top of overlay -->
|
|
||||||
<div
|
|
||||||
class="absolute inset-0 bg-gradient-to-t from-black/80 via-black/30 to-transparent z-20"
|
|
||||||
></div>
|
|
||||||
<!-- Content -->
|
|
||||||
<div class="relative z-30 px-8 pb-8 pt-48 sm:pt-32 lg:pt-48">
|
|
||||||
<div
|
|
||||||
class="flex flex-wrap items-center gap-y-1 overflow-hidden text-sm/6 text-gray-300"
|
|
||||||
>
|
|
||||||
<time :datetime="post.datetime" class="mr-8">{{
|
|
||||||
post.date
|
|
||||||
}}</time>
|
|
||||||
<div class="-ml-4 flex items-center gap-x-4">
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 2 2"
|
|
||||||
class="-ml-0.5 h-2 w-2 flex-none fill-white/50"
|
|
||||||
>
|
|
||||||
<circle cx="1" cy="1" r="1" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h3 class="mt-3 text-lg/6 font-semibold text-white">
|
|
||||||
<a :href="post.href" class="relative block">
|
|
||||||
<span class="absolute inset-0" />
|
|
||||||
{{ post.title }}
|
|
||||||
</a>
|
|
||||||
</h3>
|
|
||||||
<p
|
|
||||||
class="mt-2 text-sm text-gray-200 dark:text-gray-300 line-clamp-2"
|
|
||||||
>
|
|
||||||
{{ post.description }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup lang="ts">
|
||||||
const posts = [
|
import { MAIN2_IMAGES, SOCIAL_IMAGES } from '@/data/assets';
|
||||||
{
|
|
||||||
id: 1,
|
const imageList = [
|
||||||
title: '와디즈',
|
MAIN2_IMAGES.main1,
|
||||||
href: '#',
|
MAIN2_IMAGES.main2,
|
||||||
description:
|
MAIN2_IMAGES.main3,
|
||||||
'Illo sint voluptas. Error voluptates culpa eligendi. Hic vel totam vitae illo. Non aliquid explicabo necessitatibus unde. Sed exercitationem placeat consectetur nulla deserunt vel. Iusto corrupti dicta.',
|
MAIN2_IMAGES.main4,
|
||||||
imageUrl:
|
|
||||||
'https://images.unsplash.com/photo-1496128858413-b36217c2ce36?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=3603&q=80',
|
|
||||||
date: 'Mar 16, 2020',
|
|
||||||
datetime: '2020-03-16',
|
|
||||||
author: {
|
|
||||||
name: 'Michael Foster',
|
|
||||||
imageUrl:
|
|
||||||
'https://images.unsplash.com/photo-1519244703995-f4e0f30006d5?ixlib=rb-1.2.1&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/*
|
.pill {
|
||||||
Ensure you have the Tailwind line-clamp plugin enabled for “line-clamp-2”.
|
@apply rounded-full bg-white px-3 py-1 shadow-sm dark:bg-gray-900/60;
|
||||||
*/
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
92
bobu/app/components/about/AboutSection5.vue
Normal file
92
bobu/app/components/about/AboutSection5.vue
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
<template>
|
||||||
|
<section class="bg-white dark:bg-gray-900 py-24 sm:py-32">
|
||||||
|
<div class="mx-auto max-w-7xl px-6 lg:px-8">
|
||||||
|
<div class="mx-auto max-w-2xl text-center">
|
||||||
|
<h2
|
||||||
|
class="text-2xl font-bold text-gray-900 dark:text-gray-100 sm:text-3xl"
|
||||||
|
>
|
||||||
|
지난 프로그램 또는 후기
|
||||||
|
</h2>
|
||||||
|
<p class="mt-2 text-lg/8 text-gray-600 dark:text-gray-400"></p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx-auto mt-16 grid max-w-2xl auto-rows-fr grid-cols-1 gap-8 sm:mt-20 lg:mx-0 lg:max-w-none lg:grid-cols-3"
|
||||||
|
>
|
||||||
|
<article
|
||||||
|
v-for="post in posts"
|
||||||
|
:key="post.id"
|
||||||
|
class="relative flex flex-col justify-end overflow-hidden rounded-2xl"
|
||||||
|
>
|
||||||
|
<!-- Background image -->
|
||||||
|
<img
|
||||||
|
:src="post.imageUrl"
|
||||||
|
alt=""
|
||||||
|
class="absolute inset-0 w-full h-full object-cover z-0"
|
||||||
|
/>
|
||||||
|
<!-- 20% black overlay in light, 50% in dark -->
|
||||||
|
<div class="absolute inset-0 bg-black/20 dark:bg-black/50 z-10"></div>
|
||||||
|
<!-- Gradient on top of overlay -->
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 bg-gradient-to-t from-black/80 via-black/30 to-transparent z-20"
|
||||||
|
></div>
|
||||||
|
<!-- Content -->
|
||||||
|
<div class="relative z-30 px-8 pb-8 pt-48 sm:pt-32 lg:pt-48">
|
||||||
|
<div
|
||||||
|
class="flex flex-wrap items-center gap-y-1 overflow-hidden text-sm/6 text-gray-300"
|
||||||
|
>
|
||||||
|
<time :datetime="post.datetime" class="mr-8">{{
|
||||||
|
post.date
|
||||||
|
}}</time>
|
||||||
|
<div class="-ml-4 flex items-center gap-x-4">
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 2 2"
|
||||||
|
class="-ml-0.5 h-2 w-2 flex-none fill-white/50"
|
||||||
|
>
|
||||||
|
<circle cx="1" cy="1" r="1" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h3 class="mt-3 text-lg/6 font-semibold text-white">
|
||||||
|
<a :href="post.href" class="relative block">
|
||||||
|
<span class="absolute inset-0" />
|
||||||
|
{{ post.title }}
|
||||||
|
</a>
|
||||||
|
</h3>
|
||||||
|
<p
|
||||||
|
class="mt-2 text-sm text-gray-200 dark:text-gray-300 line-clamp-2"
|
||||||
|
>
|
||||||
|
{{ post.description }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const posts = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: '와디즈',
|
||||||
|
href: '#',
|
||||||
|
description:
|
||||||
|
'Illo sint voluptas. Error voluptates culpa eligendi. Hic vel totam vitae illo. Non aliquid explicabo necessitatibus unde. Sed exercitationem placeat consectetur nulla deserunt vel. Iusto corrupti dicta.',
|
||||||
|
imageUrl:
|
||||||
|
'https://images.unsplash.com/photo-1496128858413-b36217c2ce36?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=3603&q=80',
|
||||||
|
date: 'Mar 16, 2020',
|
||||||
|
datetime: '2020-03-16',
|
||||||
|
author: {
|
||||||
|
name: 'Michael Foster',
|
||||||
|
imageUrl:
|
||||||
|
'https://images.unsplash.com/photo-1519244703995-f4e0f30006d5?ixlib=rb-1.2.1&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/*
|
||||||
|
Ensure you have the Tailwind line-clamp plugin enabled for “line-clamp-2”.
|
||||||
|
*/
|
||||||
|
</style>
|
||||||
67
bobu/app/components/about/AboutSection6.vue
Normal file
67
bobu/app/components/about/AboutSection6.vue
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<template>
|
||||||
|
<section class="relative px-6 py-28 sm:px-10 sm:py-36 lg:px-16 bg-gray-900">
|
||||||
|
<!-- Background image -->
|
||||||
|
<div class="absolute inset-0 overflow-hidden">
|
||||||
|
<img
|
||||||
|
:src="bgImage"
|
||||||
|
alt="백패킹 커뮤니티 프로그램"
|
||||||
|
class="size-full object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dark overlay -->
|
||||||
|
<div aria-hidden="true" class="absolute inset-0 bg-black/50"></div>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<div
|
||||||
|
class="relative mx-auto flex max-w-3xl flex-col items-center text-center"
|
||||||
|
>
|
||||||
|
<!-- Small heading -->
|
||||||
|
<p
|
||||||
|
class="text-sm font-semibold tracking-[0.15em] text-gray-200 uppercase"
|
||||||
|
>
|
||||||
|
숨과 숨 사이, 서로의 배경이 되어 함께하는 느슨한 연결
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Main heading -->
|
||||||
|
<h2 class="mt-4 text-2xl font-bold tracking-tight text-white sm:text-3xl">
|
||||||
|
백패킹 커뮤니티 프로그램
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<!-- Body text -->
|
||||||
|
<p class="mt-5 text-base sm:text-lg text-gray-50 leading-relaxed">
|
||||||
|
보부에서는 혼자 떠나는 쉼도 아름답지만,<br />
|
||||||
|
때로는 조용히 함께 머무르는 순간이 더 깊은 울림을 준다고 생각합니다.<br />
|
||||||
|
<br />
|
||||||
|
그래서 우리는 ‘함께 걷고, 함께 쉬는’ 백패킹 커뮤니티 프로그램을
|
||||||
|
운영합니다.<br />
|
||||||
|
마음이 잠시 쉬어갈 자리를 찾는 사람들에게,<br />
|
||||||
|
자연 속에서 각자의 속도로 머무르며 서로의 온도를 확인하는 시간입니다.
|
||||||
|
</p>
|
||||||
|
<!-- Closing line -->
|
||||||
|
<p class="mt-10 text-base sm:text-lg text-gray-100">
|
||||||
|
정선의 밤공기를 마시며, 조용한 동행을 함께하세요.
|
||||||
|
</p>
|
||||||
|
<!-- CTA -->
|
||||||
|
<div class="mt-12">
|
||||||
|
<NuxtLink
|
||||||
|
to="/backpacking-community"
|
||||||
|
class="block w-full rounded-md bg-white px-8 py-3 text-base font-semibold text-gray-900 hover:bg-gray-100 sm:w-auto"
|
||||||
|
>
|
||||||
|
백패킹 커뮤니티 프로그램 참여하기
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { MAIN2_IMAGES } from '@/data/assets';
|
||||||
|
|
||||||
|
// Use any image you like here (main1~main4)
|
||||||
|
const bgImage = MAIN2_IMAGES.main4;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Tailwind handles most of it */
|
||||||
|
</style>
|
||||||
52
bobu/app/components/about/AboutSection7.vue
Normal file
52
bobu/app/components/about/AboutSection7.vue
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<template>
|
||||||
|
<section class="bg-white dark:bg-gray-900 py-20 sm:py-28">
|
||||||
|
<div class="mx-auto max-w-4xl px-6 text-center">
|
||||||
|
<!-- Small intro text -->
|
||||||
|
<p
|
||||||
|
class="text-sm font-semibold tracking-[0.15em] text-gray-500 dark:text-gray-400 uppercase"
|
||||||
|
>
|
||||||
|
일과 숲 사이, 일에 온도를 맞추고 쉼에 숨을 싣는 시간
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Main Heading -->
|
||||||
|
<h2
|
||||||
|
class="mt-3 text-2xl sm:text-3xl font-bold text-gray-900 dark:text-gray-100"
|
||||||
|
>
|
||||||
|
워케이션 프로그램
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<!-- Body Paragraph -->
|
||||||
|
<div
|
||||||
|
class="mt-6 text-base sm:text-lg leading-relaxed text-gray-700 dark:text-gray-300 space-y-4"
|
||||||
|
>
|
||||||
|
<p>자연 속에서 집중의 리듬을 회복하고 쉼의 온기를 되찾는 시간.</p>
|
||||||
|
<p>
|
||||||
|
도시의 속도에서 잠시 벗어나 나만의 속도로 일하고, 쉬고, 머무르세요.
|
||||||
|
낮에는 책상 위의 나로, 밤에는 자연 속의 나로.<br
|
||||||
|
class="hidden sm:block"
|
||||||
|
/>
|
||||||
|
하루 안에서 일과 쉼이 조용히 순환합니다.
|
||||||
|
</p>
|
||||||
|
<p>조용하지만 분명한 변화를 느껴보세요.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CTA Button -->
|
||||||
|
<div class="mt-10">
|
||||||
|
<NuxtLink
|
||||||
|
to="/workation"
|
||||||
|
class="inline-flex items-center space-x-2 px-6 py-3 bg-green-700 text-white font-semibold rounded-lg hover:bg-green-600 transition shadow-md dark:shadow-none"
|
||||||
|
>
|
||||||
|
<span>워케이션 프로그램 문의하기</span>
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// No images needed
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Tailwind handles visuals */
|
||||||
|
</style>
|
||||||
721
bobu/app/components/auth/register-form.vue
Normal file
721
bobu/app/components/auth/register-form.vue
Normal file
@@ -0,0 +1,721 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="min-h-screen bg-gray-50 dark:bg-gray-900 flex items-center justify-center px-4 py-12"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="w-full max-w-lg bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl shadow-sm px-6 py-8 sm:px-8"
|
||||||
|
>
|
||||||
|
<!-- Title -->
|
||||||
|
<h1 class="text-center text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
|
회원가입
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<!-- STEP 1: 약관 동의 -->
|
||||||
|
<section v-if="!termsProceed" class="mt-8 space-y-6">
|
||||||
|
<!-- 이용약관 -->
|
||||||
|
<div
|
||||||
|
class="border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 max-h-60 overflow-y-auto p-4"
|
||||||
|
>
|
||||||
|
<app-termsof-use />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 이용약관 동의 -->
|
||||||
|
<div class="space-y-3 text-sm text-gray-800 dark:text-gray-200">
|
||||||
|
<p>
|
||||||
|
<strong class="font-semibold text-red-500">[필수]</strong>
|
||||||
|
위의 이용약관에 동의하십니까?
|
||||||
|
</p>
|
||||||
|
<div class="flex items-center gap-6">
|
||||||
|
<label class="inline-flex items-center gap-1 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
v-model="agreedToTerms1"
|
||||||
|
value="true"
|
||||||
|
class="h-4 w-4 text-black focus:ring-black border-gray-300"
|
||||||
|
/>
|
||||||
|
<span>예</span>
|
||||||
|
</label>
|
||||||
|
<label class="inline-flex items-center gap-1 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
v-model="agreedToTerms1"
|
||||||
|
value="false"
|
||||||
|
class="h-4 w-4 text-black focus:ring-black border-gray-300"
|
||||||
|
/>
|
||||||
|
<span>아니오</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 개인정보 수집/이용 안내 -->
|
||||||
|
<div
|
||||||
|
class="overflow-x-auto border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900"
|
||||||
|
>
|
||||||
|
<table
|
||||||
|
class="min-w-full text-xs sm:text-sm text-gray-800 dark:text-gray-200"
|
||||||
|
>
|
||||||
|
<thead class="bg-gray-100 dark:bg-gray-800/70">
|
||||||
|
<tr class="text-left">
|
||||||
|
<th class="px-3 py-2">구분</th>
|
||||||
|
<th class="px-3 py-2">수집/이용 항목</th>
|
||||||
|
<th class="px-3 py-2">수집/이용 목적</th>
|
||||||
|
<th class="px-3 py-2">보유기간</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr class="border-t border-gray-200 dark:border-gray-700">
|
||||||
|
<td class="px-3 py-2">필수</td>
|
||||||
|
<td class="px-3 py-2">
|
||||||
|
성명, 아이디, 비밀번호, 휴대폰번호, 이메일, 회원유형
|
||||||
|
</td>
|
||||||
|
<td class="px-3 py-2" rowspan="2">
|
||||||
|
회원관리 및 서비스 제공 (공유 오피스·백패킹 등)
|
||||||
|
</td>
|
||||||
|
<td class="px-3 py-2" rowspan="2">
|
||||||
|
탈퇴 시 즉시 파기, 관련 법령에 따른 보관 (최대 5년)
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="border-t border-gray-200 dark:border-gray-700">
|
||||||
|
<td class="px-3 py-2">선택</td>
|
||||||
|
<td class="px-3 py-2">회원 프로필 사진</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="space-y-3 text-xs sm:text-sm text-gray-700 dark:text-gray-300"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
위의 개인정보 수집·이용에 대한 동의를 거부할 권리가 있습니다. 다만,
|
||||||
|
동의하지 않을 경우 일부 서비스 이용에 제한이 있을 수 있습니다.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong class="font-semibold text-red-500">[필수]</strong>
|
||||||
|
위와 같이 개인정보를 수집·이용하는 데 동의하십니까?
|
||||||
|
</p>
|
||||||
|
<div class="flex items-center gap-6">
|
||||||
|
<label class="inline-flex items-center gap-1 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
v-model="agreedToTerms2"
|
||||||
|
value="true"
|
||||||
|
class="h-4 w-4 text-black focus:ring-black border-gray-300"
|
||||||
|
/>
|
||||||
|
<span>예</span>
|
||||||
|
</label>
|
||||||
|
<label class="inline-flex items-center gap-1 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
v-model="agreedToTerms2"
|
||||||
|
value="false"
|
||||||
|
class="h-4 w-4 text-black focus:ring-black border-gray-300"
|
||||||
|
/>
|
||||||
|
<span>아니오</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 다음 버튼 -->
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="termsProceed = true"
|
||||||
|
:disabled="!(agreedToTerms1 === 'true' && agreedToTerms2 === 'true')"
|
||||||
|
class="mt-4 flex w-full justify-center rounded-md px-4 py-3 text-sm font-semibold text-white shadow-sm transition"
|
||||||
|
:class="{
|
||||||
|
'bg-gray-400 cursor-not-allowed': !(
|
||||||
|
agreedToTerms1 === 'true' && agreedToTerms2 === 'true'
|
||||||
|
),
|
||||||
|
'bg-black hover:bg-gray-800':
|
||||||
|
agreedToTerms1 === 'true' && agreedToTerms2 === 'true',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
다음
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- STEP 2: 회원정보 입력 -->
|
||||||
|
<section v-else class="mt-8">
|
||||||
|
<!-- Alert -->
|
||||||
|
<div
|
||||||
|
v-if="reg_show_alert"
|
||||||
|
:class="[
|
||||||
|
'mb-4 text-center text-sm font-semibold text-white rounded-md px-3 py-2',
|
||||||
|
reg_alert_variant,
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ reg_alert_msg }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<vee-form
|
||||||
|
:validation-schema="schema"
|
||||||
|
:initial-values="userData"
|
||||||
|
@submit="handleSubmit"
|
||||||
|
class="space-y-4"
|
||||||
|
>
|
||||||
|
<!-- 이메일 -->
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center justify-between mb-1.5">
|
||||||
|
<label class="text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
이메일
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="checkEmail"
|
||||||
|
:disabled="!abletoCheck || emailChecked"
|
||||||
|
class="inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-medium border transition"
|
||||||
|
:class="{
|
||||||
|
'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed':
|
||||||
|
!abletoCheck && !emailChecked,
|
||||||
|
'bg-yellow-100 text-yellow-700 border-yellow-400':
|
||||||
|
abletoCheck && !emailChecked,
|
||||||
|
'bg-green-100 text-green-700 border-green-500': emailChecked,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div v-if="emailState.checking">
|
||||||
|
<font-awesome-icon :icon="['fas', 'spinner']" spin />
|
||||||
|
</div>
|
||||||
|
<svg
|
||||||
|
v-else
|
||||||
|
class="h-1.5 w-1.5"
|
||||||
|
:class="{
|
||||||
|
'fill-gray-300': !abletoCheck && !emailChecked,
|
||||||
|
'fill-yellow-500': abletoCheck && !emailChecked,
|
||||||
|
'fill-green-500': emailChecked,
|
||||||
|
}"
|
||||||
|
viewBox="0 0 6 6"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<circle cx="3" cy="3" r="3" />
|
||||||
|
</svg>
|
||||||
|
<span>중복 확인</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<vee-field
|
||||||
|
v-model="emailInput"
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
class="block w-full rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-white/5 px-3 py-2 text-sm text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:outline-none focus:ring-2 focus:ring-black"
|
||||||
|
placeholder="example@normadbobu.com"
|
||||||
|
/>
|
||||||
|
<p class="mt-1 text-xs text-gray-600 dark:text-gray-400">
|
||||||
|
{{ emailMessage }}
|
||||||
|
</p>
|
||||||
|
<ErrorMessage class="mt-1 text-xs text-red-600" name="email" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 이름 -->
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
class="mb-1.5 block text-sm font-medium text-gray-900 dark:text-white"
|
||||||
|
>
|
||||||
|
이름
|
||||||
|
</label>
|
||||||
|
<vee-field
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
class="block w-full rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-white/5 px-3 py-2 text-sm text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:outline-none focus:ring-2 focus:ring-black"
|
||||||
|
placeholder="이름을 입력하세요"
|
||||||
|
/>
|
||||||
|
<ErrorMessage class="mt-1 text-xs text-red-600" name="name" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 비밀번호 -->
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
class="mb-1.5 block text-sm font-medium text-gray-900 dark:text-white"
|
||||||
|
>
|
||||||
|
비밀번호
|
||||||
|
</label>
|
||||||
|
<vee-field
|
||||||
|
name="password"
|
||||||
|
:bails="false"
|
||||||
|
v-slot="{ field, errors }"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-bind="field"
|
||||||
|
type="password"
|
||||||
|
class="block w-full rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-white/5 px-3 py-2 text-sm text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:outline-none focus:ring-2 focus:ring-black"
|
||||||
|
placeholder="비밀번호를 입력하세요"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-for="error in errors"
|
||||||
|
:key="error"
|
||||||
|
class="mt-1 text-xs text-red-600"
|
||||||
|
>
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
</vee-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 비밀번호 확인 -->
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
class="mb-1.5 block text-sm font-medium text-gray-900 dark:text-white"
|
||||||
|
>
|
||||||
|
비밀번호 확인
|
||||||
|
</label>
|
||||||
|
<vee-field
|
||||||
|
type="password"
|
||||||
|
name="confirm_password"
|
||||||
|
class="block w-full rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-white/5 px-3 py-2 text-sm text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:outline-none focus:ring-2 focus:ring-black"
|
||||||
|
placeholder="비밀번호를 다시 입력하세요"
|
||||||
|
/>
|
||||||
|
<ErrorMessage
|
||||||
|
class="mt-1 text-xs text-red-600"
|
||||||
|
name="confirm_password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 회원 유형 -->
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
class="mb-1.5 block text-sm font-medium text-gray-900 dark:text-white"
|
||||||
|
>
|
||||||
|
회원 유형
|
||||||
|
</label>
|
||||||
|
<vee-field
|
||||||
|
as="select"
|
||||||
|
name="membership"
|
||||||
|
class="block w-full rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-white/5 px-3 py-2 text-sm text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-black"
|
||||||
|
>
|
||||||
|
<option value="일반회원">일반회원</option>
|
||||||
|
<option value="노마드 워커">노마드 워커</option>
|
||||||
|
<option value="로컬 주민">로컬 주민</option>
|
||||||
|
<option value="기업/단체">기업/단체</option>
|
||||||
|
</vee-field>
|
||||||
|
<ErrorMessage class="mt-1 text-xs text-red-600" name="membership" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- reCAPTCHA -->
|
||||||
|
<div ref="recaptchaWrapperRef" class="mt-2">
|
||||||
|
<div id="recaptcha-container"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 휴대폰 번호 -->
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
class="flex justify-between mb-1.5 text-sm font-medium text-gray-900 dark:text-white"
|
||||||
|
>
|
||||||
|
<span>휴대폰 번호</span>
|
||||||
|
<button
|
||||||
|
v-if="startPhoneRegister"
|
||||||
|
type="button"
|
||||||
|
@click="resetPhonenumber"
|
||||||
|
class="text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
||||||
|
>
|
||||||
|
변경하기
|
||||||
|
</button>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
v-model="rawPhoneNumber"
|
||||||
|
placeholder="010-1234-5678"
|
||||||
|
class="block w-full rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-white/5 px-3 py-2 text-sm text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:outline-none focus:ring-2 focus:ring-black"
|
||||||
|
:disabled="startPhoneRegister"
|
||||||
|
:class="{ 'bg-gray-100 dark:bg-gray-800': startPhoneRegister }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 인증번호 입력 -->
|
||||||
|
<div v-if="startPhoneRegister">
|
||||||
|
<label
|
||||||
|
class="flex justify-between mb-1.5 text-sm font-medium text-gray-900 dark:text-white"
|
||||||
|
>
|
||||||
|
<span>인증번호</span>
|
||||||
|
<button
|
||||||
|
v-if="canResend"
|
||||||
|
type="button"
|
||||||
|
@click="resendSms"
|
||||||
|
class="text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
||||||
|
>
|
||||||
|
다시 전송하기
|
||||||
|
</button>
|
||||||
|
<div v-else class="text-xs text-gray-400 dark:text-gray-500">
|
||||||
|
<font-awesome-icon :icon="['fas', 'spinner']" spin />
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
v-model="smsCode"
|
||||||
|
placeholder="인증번호를 입력하세요"
|
||||||
|
class="block w-full rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-white/5 px-3 py-2 text-sm text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:outline-none focus:ring-2 focus:ring-black"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 버튼 영역 -->
|
||||||
|
<!-- 1) 이메일 미확인 -->
|
||||||
|
<button
|
||||||
|
v-if="!emailChecked"
|
||||||
|
type="button"
|
||||||
|
disabled
|
||||||
|
class="mt-6 flex w-full justify-center rounded-md bg-gray-400 px-4 py-3 text-sm font-semibold text-white shadow-sm cursor-not-allowed"
|
||||||
|
>
|
||||||
|
이메일 중복 확인을 완료해주세요
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- 2) 전화번호 인증 시작 버튼 -->
|
||||||
|
<button
|
||||||
|
v-if="!startPhoneRegister && emailChecked"
|
||||||
|
type="button"
|
||||||
|
@click="startPhoneNumberVerification"
|
||||||
|
:disabled="!isValidPhoneNumber"
|
||||||
|
class="mt-6 flex w-full justify-center rounded-md px-4 py-3 text-sm font-semibold text-white shadow-sm transition"
|
||||||
|
:class="{
|
||||||
|
'bg-gray-400 cursor-not-allowed': !isValidPhoneNumber,
|
||||||
|
'bg-black hover:bg-gray-800': isValidPhoneNumber,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<span v-if="!isValidPhoneNumber">휴대폰 번호를 확인해주세요</span>
|
||||||
|
<span v-else>인증번호 전송하기</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- 3) 최종 가입 버튼 -->
|
||||||
|
<button
|
||||||
|
v-if="startPhoneRegister"
|
||||||
|
type="submit"
|
||||||
|
:disabled="reg_in_submission || !emailChecked"
|
||||||
|
class="mt-6 flex w-full justify-center rounded-md px-4 py-3 text-sm font-semibold text-white shadow-sm transition"
|
||||||
|
:class="{
|
||||||
|
'bg-gray-500 cursor-not-allowed':
|
||||||
|
reg_in_submission || !emailChecked,
|
||||||
|
'bg-black hover:bg-gray-800': !reg_in_submission && emailChecked,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div v-if="onSmsfunction">
|
||||||
|
<font-awesome-icon :icon="['fas', 'spinner']" spin />
|
||||||
|
</div>
|
||||||
|
<span v-else>가입하기</span>
|
||||||
|
</button>
|
||||||
|
</vee-form>
|
||||||
|
|
||||||
|
<!-- Loading overlay -->
|
||||||
|
<app-loading-overlay
|
||||||
|
:isLoading="reg_in_submission"
|
||||||
|
:loadingMessage="loadingMessage"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch, reactive, toRefs, computed } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import type { ConfirmationResult } from 'firebase/auth';
|
||||||
|
import useUserStore from '@/stores/user';
|
||||||
|
import { checkEmailDuplicate } from '@/utils/firebaseUtils';
|
||||||
|
import AppTermsofUse from '~/components/TermsofUse.vue';
|
||||||
|
import AppLoadingOverlay from '~/components/LoadingOverlay.vue';
|
||||||
|
import { useNuxtApp } from '#app';
|
||||||
|
import { signInWithPhoneNumber } from 'firebase/auth';
|
||||||
|
const { $firebase, $createRecaptchaVerifier } = useNuxtApp();
|
||||||
|
|
||||||
|
const firebaseAuth = $firebase.auth;
|
||||||
|
const createRecaptchaVerifier =
|
||||||
|
$firebase.createRecaptchaVerifier ?? $createRecaptchaVerifier;
|
||||||
|
|
||||||
|
// ② If you did NOT touch the plugin, just use:
|
||||||
|
/// const createRecaptchaVerifier = $createRecaptchaVerifier;
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const emits = defineEmits(['register-success']);
|
||||||
|
|
||||||
|
/* ---------- STEP control ---------- */
|
||||||
|
const termsProceed = ref(false);
|
||||||
|
const agreedToTerms1 = ref<'true' | 'false' | ''>('');
|
||||||
|
const agreedToTerms2 = ref<'true' | 'false' | ''>('');
|
||||||
|
|
||||||
|
/* ---------- VeeValidate schema ---------- */
|
||||||
|
const schema = {
|
||||||
|
name: 'required|min:2|max:10',
|
||||||
|
email: 'required|min:2|max:100|email',
|
||||||
|
password: 'required|min:9|max:100|excluded:password',
|
||||||
|
confirm_password: 'passwords_mismatch:@password',
|
||||||
|
membership: 'required|membership_excluded',
|
||||||
|
};
|
||||||
|
|
||||||
|
interface RegisterValues {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
confirm_password: string;
|
||||||
|
name: string;
|
||||||
|
membership: string;
|
||||||
|
isActive: boolean;
|
||||||
|
profile_img: string;
|
||||||
|
created: any;
|
||||||
|
uid: string;
|
||||||
|
phone: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userData = ref({
|
||||||
|
membership: '일반회원',
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ---------- Email duplication check ---------- */
|
||||||
|
const loadingMessage = 'Uploading! 잠시만 기다려주세요...';
|
||||||
|
const emailState = reactive({
|
||||||
|
emailInput: '',
|
||||||
|
abletoCheck: false,
|
||||||
|
emailChecked: false,
|
||||||
|
emailMessage: '',
|
||||||
|
checking: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { emailInput, abletoCheck, emailChecked, emailMessage } =
|
||||||
|
toRefs(emailState);
|
||||||
|
|
||||||
|
const checkEmail = async () => {
|
||||||
|
emailState.checking = true;
|
||||||
|
if (!emailState.abletoCheck) {
|
||||||
|
emailState.checking = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const result = await checkEmailDuplicate(emailState.emailInput);
|
||||||
|
switch (result.status) {
|
||||||
|
case 'exists':
|
||||||
|
emailState.emailMessage = '이미 존재하는 이메일입니다.';
|
||||||
|
emailState.checking = false;
|
||||||
|
break;
|
||||||
|
case 'available':
|
||||||
|
emailState.emailMessage = '가입 가능한 이메일입니다.';
|
||||||
|
emailState.emailChecked = true;
|
||||||
|
emailState.checking = false;
|
||||||
|
break;
|
||||||
|
case 'error':
|
||||||
|
default:
|
||||||
|
emailState.abletoCheck = false;
|
||||||
|
emailState.emailMessage = result.message;
|
||||||
|
emailState.checking = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => emailState.emailInput,
|
||||||
|
(newValue) => {
|
||||||
|
emailState.abletoCheck = false;
|
||||||
|
emailState.emailChecked = false;
|
||||||
|
emailState.emailMessage = '';
|
||||||
|
|
||||||
|
const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/;
|
||||||
|
emailState.abletoCheck = emailRegex.test(newValue);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/* ---------- Phone verification ---------- */
|
||||||
|
const rawPhoneNumber = ref('');
|
||||||
|
const phoneNumber = ref('');
|
||||||
|
const smsCode = ref('');
|
||||||
|
const startPhoneRegister = ref(false);
|
||||||
|
const phoneVerified = ref(false);
|
||||||
|
const onSmsfunction = ref(false);
|
||||||
|
const canResend = ref(true);
|
||||||
|
const cooldownTime = ref(5);
|
||||||
|
const recaptchaWrapperRef = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
const isValidPhoneNumber = computed(() => {
|
||||||
|
if (typeof rawPhoneNumber.value !== 'string') return false;
|
||||||
|
const cleaned = rawPhoneNumber.value.replace(/-/g, '');
|
||||||
|
return (
|
||||||
|
(cleaned.startsWith('010') || cleaned.startsWith('070')) &&
|
||||||
|
cleaned.length === 11
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const resetPhonenumber = () => {
|
||||||
|
rawPhoneNumber.value = '';
|
||||||
|
startPhoneRegister.value = false;
|
||||||
|
recaptchaVerifier = null;
|
||||||
|
confirmationResult = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const processedPhoneNumber = computed(() => {
|
||||||
|
if (typeof rawPhoneNumber.value !== 'string') return '';
|
||||||
|
let cleaned = rawPhoneNumber.value.replace(/-/g, '');
|
||||||
|
if (cleaned.startsWith('0')) {
|
||||||
|
cleaned = '+82' + cleaned.substring(1);
|
||||||
|
}
|
||||||
|
return cleaned;
|
||||||
|
});
|
||||||
|
|
||||||
|
let recaptchaVerifier: any | null = null;
|
||||||
|
let confirmationResult: ConfirmationResult | null = null;
|
||||||
|
|
||||||
|
const startPhoneNumberVerification = async () => {
|
||||||
|
try {
|
||||||
|
phoneNumber.value = processedPhoneNumber.value;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
startPhoneRegister.value = true;
|
||||||
|
|
||||||
|
if (recaptchaVerifier) {
|
||||||
|
try {
|
||||||
|
await recaptchaVerifier.verify();
|
||||||
|
} catch (recaptchaError) {
|
||||||
|
console.error(
|
||||||
|
'Previous reCAPTCHA verification failed:',
|
||||||
|
recaptchaError
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
recaptchaVerifier.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recaptchaWrapperRef.value) {
|
||||||
|
recaptchaWrapperRef.value.innerHTML =
|
||||||
|
'<div id="recaptcha-container"></div>';
|
||||||
|
} else {
|
||||||
|
console.error('recaptchaWrapperRef is null');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
recaptchaVerifier = createRecaptchaVerifier('recaptcha-container');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await recaptchaVerifier.verify();
|
||||||
|
} catch (recaptchaError) {
|
||||||
|
console.error('Error during reCAPTCHA verification:', recaptchaError);
|
||||||
|
throw recaptchaError;
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmationResult = await signInWithPhoneNumber(
|
||||||
|
firebaseAuth,
|
||||||
|
phoneNumber.value,
|
||||||
|
recaptchaVerifier
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during phone number verification:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resendSms = async () => {
|
||||||
|
if (!confirmationResult) {
|
||||||
|
console.error('사이트에 문제가 있습니다, 관리자에게 문의하십시오.');
|
||||||
|
onSmsfunction.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
canResend.value = false;
|
||||||
|
|
||||||
|
if (recaptchaVerifier) {
|
||||||
|
recaptchaVerifier.clear();
|
||||||
|
if (recaptchaWrapperRef.value) {
|
||||||
|
recaptchaWrapperRef.value.innerHTML =
|
||||||
|
'<div id="recaptcha-container"></div>';
|
||||||
|
} else {
|
||||||
|
console.error('recaptchaWrapperRef is null');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
recaptchaVerifier = createRecaptchaVerifier('recaptcha-container');
|
||||||
|
|
||||||
|
confirmationResult = await signInWithPhoneNumber(
|
||||||
|
firebaseAuth,
|
||||||
|
phoneNumber.value,
|
||||||
|
recaptchaVerifier
|
||||||
|
);
|
||||||
|
|
||||||
|
let remaining = cooldownTime.value;
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
remaining--;
|
||||||
|
if (remaining <= 0) {
|
||||||
|
clearInterval(timer);
|
||||||
|
canResend.value = true;
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during SMS resend:', error);
|
||||||
|
canResend.value = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const verifySmsCode = async (): Promise<string | null> => {
|
||||||
|
try {
|
||||||
|
onSmsfunction.value = true;
|
||||||
|
if (confirmationResult) {
|
||||||
|
const result = await confirmationResult.confirm(smsCode.value);
|
||||||
|
phoneVerified.value = true;
|
||||||
|
await firebaseAuth.signOut();
|
||||||
|
onSmsfunction.value = false;
|
||||||
|
return result.user?.uid || null;
|
||||||
|
} else {
|
||||||
|
console.error('confirmationResult is null.');
|
||||||
|
onSmsfunction.value = false;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during SMS code verification:', error);
|
||||||
|
onSmsfunction.value = false;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ---------- Registration submit ---------- */
|
||||||
|
const reg_in_submission = ref(false);
|
||||||
|
const reg_show_alert = ref(false);
|
||||||
|
const reg_alert_variant = ref('bg-blue-500');
|
||||||
|
const reg_alert_msg = ref('계정 생성 중입니다. 잠시만 기다려주세요!');
|
||||||
|
|
||||||
|
const handleSubmit = async (_event: Event, values: Record<string, any>) => {
|
||||||
|
reg_show_alert.value = true;
|
||||||
|
reg_in_submission.value = true;
|
||||||
|
reg_alert_variant.value = 'bg-blue-500';
|
||||||
|
reg_alert_msg.value = '계정 생성 중입니다. 잠시만 기다려주세요!';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const userUID = await verifySmsCode();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (userUID) {
|
||||||
|
await visitorRegister({
|
||||||
|
...values.controlledValues,
|
||||||
|
uid: userUID,
|
||||||
|
phone: phoneNumber.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
reg_in_submission.value = false;
|
||||||
|
reg_alert_variant.value = 'bg-red-500';
|
||||||
|
reg_alert_msg.value = '계정 생성 실패!';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error Creating ID', error);
|
||||||
|
reg_in_submission.value = false;
|
||||||
|
reg_alert_variant.value = 'bg-red-500';
|
||||||
|
reg_alert_msg.value = '계정 생성 실패!';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
reg_in_submission.value = false;
|
||||||
|
reg_alert_variant.value = 'bg-green-500';
|
||||||
|
reg_alert_msg.value = '성공적으로 계정이 생성되었습니다!';
|
||||||
|
|
||||||
|
emits('register-success');
|
||||||
|
router.push('/');
|
||||||
|
};
|
||||||
|
|
||||||
|
async function visitorRegister(values: RegisterValues) {
|
||||||
|
try {
|
||||||
|
const { confirm_password, ...dataToSubmit } = values;
|
||||||
|
await userStore.visitorRegister(dataToSubmit);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('visitorRegisterError', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Chrome, Safari, Edge, Opera: remove number input arrows if any */
|
||||||
|
input::-webkit-outer-spin-button,
|
||||||
|
input::-webkit-inner-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
243
bobu/app/components/auth/user-login.vue
Normal file
243
bobu/app/components/auth/user-login.vue
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="min-h-screen bg-gray-50 dark:bg-gray-900 flex items-center justify-center px-4 py-12"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="w-full max-w-md bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl shadow-sm px-6 py-8 sm:px-8"
|
||||||
|
>
|
||||||
|
<!-- Title -->
|
||||||
|
<h1 class="text-center text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
|
로그인
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<!-- Social logins -->
|
||||||
|
<div class="mt-8 space-y-3">
|
||||||
|
<!-- Google -->
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="handleSocialLogin('google')"
|
||||||
|
class="w-full flex items-center gap-3 rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 px-3 py-2.5 text-sm font-medium text-gray-800 dark:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-800 transition"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-center h-6 w-6 rounded-full bg-white"
|
||||||
|
>
|
||||||
|
<!-- Placeholder icon: replace with real Google logo later -->
|
||||||
|
<font-awesome-icon
|
||||||
|
:icon="['fab', 'google']"
|
||||||
|
class="text-red-500 text-lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span class="flex-1 text-center">Google로 로그인하기</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Kakao -->
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="handleSocialLogin('kakao')"
|
||||||
|
class="w-full flex items-center gap-3 rounded-md border border-yellow-400 bg-[#FEE500] px-3 py-2.5 text-sm font-medium text-gray-900 hover:bg-[#FDE64B] transition"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-center h-6 w-6 rounded-full bg-black/5"
|
||||||
|
>
|
||||||
|
<!-- Placeholder icon: Kakao logo image or icon later -->
|
||||||
|
<img
|
||||||
|
v-if="socialImages.kakao"
|
||||||
|
:src="socialImages.kakao"
|
||||||
|
alt="Kakao"
|
||||||
|
class="h-5 w-5 object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span class="flex-1 text-center">Kakao로 로그인하기</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Naver -->
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="handleSocialLogin('naver')"
|
||||||
|
class="w-full flex items-center gap-3 rounded-md border border-green-600 bg-white dark:bg-gray-900 px-3 py-2.5 text-sm font-medium text-gray-800 dark:text-gray-100 hover:bg-green-50 dark:hover:bg-gray-800 transition"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-center h-6 w-6 rounded-full bg-green-600"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
v-if="socialImages.naver"
|
||||||
|
:src="socialImages.naver"
|
||||||
|
alt="Naver"
|
||||||
|
class="h-4 w-4 object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span class="flex-1 text-center">Naver로 로그인하기</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- separator -->
|
||||||
|
<div class="mt-6 flex items-center">
|
||||||
|
<div class="h-px flex-1 bg-gray-200 dark:bg-gray-700"></div>
|
||||||
|
<span class="mx-3 text-xs text-gray-400 dark:text-gray-500">또는</span>
|
||||||
|
<div class="h-px flex-1 bg-gray-200 dark:bg-gray-700"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ID / Email + Password form -->
|
||||||
|
<form class="mt-6 space-y-4" @submit.prevent="handleSubmit">
|
||||||
|
<!-- 아이디 또는 이메일 -->
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
for="email"
|
||||||
|
class="block text-sm font-medium text-gray-900 dark:text-white"
|
||||||
|
>
|
||||||
|
아이디 또는 이메일
|
||||||
|
</label>
|
||||||
|
<div class="mt-2">
|
||||||
|
<input
|
||||||
|
v-model="email"
|
||||||
|
type="text"
|
||||||
|
name="email"
|
||||||
|
id="email"
|
||||||
|
autocomplete="email"
|
||||||
|
required
|
||||||
|
class="block w-full rounded-md bg-white dark:bg-white/5 px-3 py-2 text-sm text-gray-900 dark:text-white outline-1 -outline-offset-1 outline-gray-300 dark:outline-white/10 placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:outline-2 focus:-outline-offset-2 focus:outline-black"
|
||||||
|
placeholder="example@normadbobu.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 비밀번호 -->
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
for="password"
|
||||||
|
class="block text-sm font-medium text-gray-900 dark:text-white"
|
||||||
|
>
|
||||||
|
비밀번호
|
||||||
|
</label>
|
||||||
|
<div class="mt-2">
|
||||||
|
<input
|
||||||
|
v-model="password"
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
id="password"
|
||||||
|
autocomplete="current-password"
|
||||||
|
required
|
||||||
|
class="block w-full rounded-md bg-white dark:bg-white/5 px-3 py-2 text-sm text-gray-900 dark:text-white outline-1 -outline-offset-1 outline-gray-300 dark:outline-white/10 placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:outline-2 focus:-outline-offset-2 focus:outline-black"
|
||||||
|
placeholder="●●●●●●●●"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 로그인 상태 유지 -->
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<label
|
||||||
|
class="flex items-center gap-2 text-xs sm:text-sm text-gray-700 dark:text-gray-300"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="rememberMe"
|
||||||
|
type="checkbox"
|
||||||
|
class="h-4 w-4 rounded border-gray-300 text-black focus:ring-black"
|
||||||
|
/>
|
||||||
|
<span>기억하기</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Alert -->
|
||||||
|
<div
|
||||||
|
v-if="login_show_alert"
|
||||||
|
:class="[
|
||||||
|
'text-white text-center text-sm font-semibold rounded-md p-3',
|
||||||
|
login_alert_variant,
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ login_alert_msg }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 로그인 button -->
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
:disabled="login_in_submission"
|
||||||
|
class="mt-2 flex w-full justify-center rounded-md bg-black px-3 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-gray-800 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-black disabled:opacity-60 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
로그인
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- 아래 링크들 -->
|
||||||
|
<div
|
||||||
|
class="mt-4 flex items-center justify-between text-xs sm:text-sm text-gray-700 dark:text-gray-300"
|
||||||
|
>
|
||||||
|
<NuxtLink to="/register" class="hover:underline"> 회원가입 </NuxtLink>
|
||||||
|
<NuxtLink to="/find-account" class="hover:underline">
|
||||||
|
아이디 · 비밀번호 찾기
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- small separator -->
|
||||||
|
<p class="mt-6 text-center text-xs text-gray-400 dark:text-gray-500">
|
||||||
|
또는
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- 비회원 예약 및 주문 조회 -->
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="mt-3 w-full rounded-md bg-slate-400 px-3 py-2.5 text-sm font-semibold text-white hover:bg-slate-500 transition"
|
||||||
|
>
|
||||||
|
비회원 예약 및 주문 조회
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { useRouter } from '#imports';
|
||||||
|
import useUserStore from '@/stores/user';
|
||||||
|
import { SOCIAL_IMAGES } from '@/data/assets';
|
||||||
|
import type { LoginValues } from '@/types';
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const email = ref('');
|
||||||
|
const password = ref('');
|
||||||
|
const rememberMe = ref(false); // currently not used in logic, placeholder for future
|
||||||
|
|
||||||
|
const login_in_submission = ref(false);
|
||||||
|
const login_show_alert = ref(false);
|
||||||
|
const login_alert_variant = ref('bg-blue-500');
|
||||||
|
const login_alert_msg = ref('로그인 중입니다. 잠시만 기다려주세요!');
|
||||||
|
|
||||||
|
const socialImages = SOCIAL_IMAGES;
|
||||||
|
|
||||||
|
// placeholder handler for future social login wiring
|
||||||
|
const handleSocialLogin = (provider: 'google' | 'kakao' | 'naver') => {
|
||||||
|
console.log(`Social login clicked: ${provider}`);
|
||||||
|
// TODO: implement real social login (Firebase Auth or other)
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
const values: LoginValues = {
|
||||||
|
email: email.value,
|
||||||
|
password: password.value,
|
||||||
|
};
|
||||||
|
|
||||||
|
login_in_submission.value = true;
|
||||||
|
login_show_alert.value = true;
|
||||||
|
login_alert_variant.value = 'bg-blue-500';
|
||||||
|
login_alert_msg.value = '로그인 중입니다. 잠시만 기다려주세요!';
|
||||||
|
|
||||||
|
// 도메인 자동 보정: @normadbobu.com
|
||||||
|
if (!values.email.includes('@')) {
|
||||||
|
values.email += '@normadbobu.com';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await userStore.authenticate(values);
|
||||||
|
} catch (error) {
|
||||||
|
login_in_submission.value = false;
|
||||||
|
login_alert_variant.value = 'bg-red-500';
|
||||||
|
login_alert_msg.value = '아이디 또는 비밀번호가 일치하지 않습니다.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
login_alert_variant.value = 'bg-green-500';
|
||||||
|
login_alert_msg.value = '로그인에 성공하였습니다!';
|
||||||
|
router.push('/');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -362,7 +362,7 @@
|
|||||||
<ul class="mt-4 list-disc pl-4 space-y-2">
|
<ul class="mt-4 list-disc pl-4 space-y-2">
|
||||||
<li>
|
<li>
|
||||||
<strong>수집 항목:</strong>
|
<strong>수집 항목:</strong>
|
||||||
소속(기관명/부서명), 성명, 생년월일, 연락처, 이메일, 주소
|
성명, 생년월일, 연락처, 이메일, 주소
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>수집 및 이용 목적:</strong>
|
<strong>수집 및 이용 목적:</strong>
|
||||||
|
|||||||
@@ -43,4 +43,5 @@ export const SOCIAL_IMAGES = {
|
|||||||
naver: '/assets/img/logo/naver_resized.jpg',
|
naver: '/assets/img/logo/naver_resized.jpg',
|
||||||
instagram: '/assets/img/logo/instagram.webp',
|
instagram: '/assets/img/logo/instagram.webp',
|
||||||
kakao: '/assets/img/logo/kakao_resized.jpg',
|
kakao: '/assets/img/logo/kakao_resized.jpg',
|
||||||
|
npay: '/assets/img/logo/npay.jpg',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
// @/data/config.ts
|
// @/data/config.ts
|
||||||
|
|
||||||
export const BASE_NAV_ITEMS = [
|
export const BASE_NAV_ITEMS = [
|
||||||
{ name: '소개', href: '/about', icon: ['fas', 'info-circle'] },
|
{ name: 'BOBU', href: '/about', icon: ['fas', 'info-circle'] },
|
||||||
{ name: '공유 오피스 예약', href: '/office', icon: ['fas', 'calendar-alt'] },
|
{ name: 'Backpacking', href: '/office', icon: ['fas', 'calendar-alt'] },
|
||||||
{
|
{
|
||||||
name: '워크케이션 프로그램',
|
name: 'Backpacking Community',
|
||||||
href: '/',
|
href: '/',
|
||||||
icon: ['fas', 'umbrella-beach'],
|
icon: ['fas', 'umbrella-beach'],
|
||||||
},
|
},
|
||||||
{ name: '쇼핑하기', href: '/shop', icon: ['fas', 'shopping-bag'] },
|
{ name: 'WORKATION Inquiry', href: '/contact', icon: ['fas', 'envelope'] },
|
||||||
{ name: '문의하기', href: '/contact', icon: ['fas', 'envelope'] },
|
|
||||||
|
{ name: 'SHOP', href: '/shop', icon: ['fas', 'shopping-bag'] },
|
||||||
{
|
{
|
||||||
name: '와디즈 펀딩 참여자',
|
name: '와디즈 펀딩 참여자',
|
||||||
href: '/wadiz',
|
href: '/wadiz',
|
||||||
|
|||||||
@@ -1,17 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
|
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
|
||||||
<!-- Featured Projects Section -->
|
<!-- Featured Projects Section -->
|
||||||
<!-- <FeaturesCarousel />
|
<FeaturesCarousel />
|
||||||
|
<AboutSection0 />
|
||||||
|
<AboutSection2 />
|
||||||
<AboutSection1 />
|
<AboutSection1 />
|
||||||
<AboutSection3 /> -->
|
<AboutSection3 />
|
||||||
|
<AboutSection6 />
|
||||||
|
<AboutSection7 />
|
||||||
|
<AboutSection5 />
|
||||||
|
|
||||||
<!-- <AppWadiz /> -->
|
<!-- <AppWadiz /> -->
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// import AppWadiz from '~/pages/wadiz/index.vue';
|
import AppWadiz from '~/pages/wadiz/index.vue';
|
||||||
import { MAIN } from '~/data/assets';
|
import { MAIN } from '~/data/assets';
|
||||||
definePageMeta({
|
// definePageMeta({
|
||||||
redirect: '/wadiz',
|
// redirect: '/wadiz',
|
||||||
});
|
// });
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -4,11 +4,11 @@
|
|||||||
<NuxtLink to="/wadiz/manage">TO manage</NuxtLink>
|
<NuxtLink to="/wadiz/manage">TO manage</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<AdminLogin />
|
<UserLogin />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import AdminLogin from '~/components/auth/admin-login.vue';
|
import UserLogin from '~/components/auth/user-login.vue';
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const userLoggedIn = computed(() => userStore.userLoggedIn);
|
const userLoggedIn = computed(() => userStore.userLoggedIn);
|
||||||
|
|||||||
20
bobu/app/pages/register.vue
Normal file
20
bobu/app/pages/register.vue
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="userLoggedIn">
|
||||||
|
<button @click="userStore.signOut">로그아웃</button>
|
||||||
|
<NuxtLink to="/wadiz/manage">TO manage</NuxtLink>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<RegisterForm />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import RegisterForm from '~/components/auth/register-form.vue';
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const userLoggedIn = computed(() => userStore.userLoggedIn);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (userLoggedIn.value) {
|
||||||
|
navigateTo('/');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
BIN
bobu/public/assets/img/logo/npay.jpg
Normal file
BIN
bobu/public/assets/img/logo/npay.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
Reference in New Issue
Block a user