Compare commits

..

6 Commits

Author SHA1 Message Date
2661ec376b 251212_register 2025-12-12 03:45:52 +09:00
c9f19e28ba 251212_login 2025-12-12 03:38:56 +09:00
manojuno
1eba2a0a49 251211_Header_Main_Update 2025-12-11 07:22:11 +09:00
e6d5f0436d 251209 2025-12-09 05:43:50 +09:00
0f8f0c2f51 Merge remote-tracking branch 'refs/remotes/origin/main' 2025-12-09 03:17:53 +09:00
f7b2ba7f9c 2501209 2025-12-09 03:16:51 +09:00
21 changed files with 7285 additions and 283 deletions

View File

@@ -1,10 +1,33 @@
<template>
<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
v-for="(imgSrc, idx) in images"
: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
:src="imgSrc"
@@ -13,7 +36,6 @@
/>
</Slide>
<!-- Add navigation arrows and pagination indicators -->
<template #addons>
<Navigation />
<Pagination />
@@ -21,54 +43,26 @@
</Carousel>
</section>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { Carousel, Slide, Navigation, Pagination } from 'vue3-carousel';
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));
// Carousel configuration
const config = {
height: 500, // fixed height (px); adjust as needed (500px here)
itemsToShow: 1, // always show one slide at a time
gap: 0, // no horizontal gap between slides
wrapAround: true, // loop infinitely
mouseWheel: false, // disable mouse-wheel navigation
autoplay: 4000, // change slide every 4 seconds
height: 500,
itemsToShow: 1,
wrapAround: true,
autoplay: 4000,
pauseAutoplayOnHover: true,
breakpoints: {
1280: { itemsToShow: 1, height: 500 }, // ≥2xl
1024: { itemsToShow: 1, height: 450 }, // ≥xl
768: { itemsToShow: 1, height: 400 }, // ≥md
0: { itemsToShow: 1, height: 300 }, // mobile
1280: { height: 500 },
1024: { height: 450 },
768: { height: 400 },
0: { height: 300 },
},
};
</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>

View File

@@ -1,54 +1,120 @@
<template>
<header class="relative isolate z-10 bg-white dark:bg-gray-900 shadow-sm">
<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"
>
<!-- MobileSidebar Open Button -->
<!-- Left: MobileSidebar Button -->
<div class="flex justify-start">
<!-- <button
<button
type="button"
class="-m-2.5 inline-flex items-center justify-center rounded-md p-2.5 text-gray-700 dark:text-white"
@click="mobileMenuOpen = true"
aria-label="Open main menu"
>
<Bars3Icon class="size-6" aria-hidden="true" />
</button> -->
</button>
</div>
<!-- Desktop Navigation -->
<!-- Logo -->
<div class="flex">
<!-- Center: Logo -->
<div class="flex justify-center">
<NuxtLink to="/" class="-m-1.5 p-1.5" aria-label="Bobu Home">
<span class="sr-only">Bobu</span>
<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"
alt="Bobu Logo Light"
/>
<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"
alt="Bobu Logo Dark"
/>
</NuxtLink>
</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 -->
<div class="flex justify-end">
<!-- <HeaderActions /> -->
<!-- Right: Buttons -->
<div class="flex justify-end items-center">
<!-- 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>
</nav>
<!-- MobileSidebar -->
<Dialog
@@ -117,6 +183,19 @@
</div>
<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 로그인/회원가입 -->
<NuxtLink
v-if="!userStore.userLoggedIn"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -8,20 +8,25 @@
<p
class="text-center text-2xl font-extrabold text-indigo-500 dark:text-indigo-400"
>
와디즈 펀딩 참여자 발송 페이지
가벼운 채비로 떠나는 정선 백패킹 포레스트
</p>
<figure class="mt-10">
<blockquote
class="text-center text-lg font-normal text-gray-900 dark:text-gray-100"
>
<p>안녕하세요, 메이커 보부입니다!</p>
<p>보부의 여정에 마음을 더해주셔서 감사합니다.</p>
<p>여러분의 따뜻한 응원 덕분에,</p>
<p>정선의 자연속 1 2일의 쉼을 준비할 있었습니다.</p>
<p>자연에 기대어, 잠시 천천히 머물러보는 시간을 선물해드릴게요.</p>
<p class="mt-4 font-semibold">
보부와 함께할 하루, 이제 예약으로 이어집니다."
<p>안녕하세요. 주식회사 보부입니다.</p>
<p>정선 백패킹 포레스트 예약 시스템입니다.</p>
<br />
<p>
원활한 운영을 위해 아래 '참여자 정보 입력하기' 클릭 문항을
작성해주시기 바랍니다.
</p>
<p>
정성스러운 답변은 나은 경험을 준비하는 도움이 됩니다.
</p>
<br />
<p>감사합니다.</p>
</blockquote>
</figure>
</div>

View 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>

View File

@@ -1,76 +1,84 @@
<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">
<!-- Title -->
<h2
class="text-3xl font-bold text-gray-900 dark:text-gray-100 sm:text-4xl"
>
노마드소셜클럽 보부<br />
<span class="text-xl font-medium text-gray-600 dark:text-gray-300"
>Nomad Social Club BOBU</span
>
<span class="text-xl font-medium text-gray-600 dark:text-gray-300">
Nomad Social Club BOBU
</span>
</h2>
</div>
<!-- Image Grid -->
<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"
:key="idx"
class="flex justify-center"
>
<img
:src="imgSrc"
alt="About image"
class="h-32 w-32 rounded-full object-cover shadow-md"
/>
</div>
:src="imgSrc"
alt="About image"
class="aspect-square w-full rounded-xl object-cover shadow-md dark:shadow-none dark:bg-gray-800"
/>
</div>
<!-- Description Text -->
<!-- Description -->
<div
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">
강원도 정선군 정선읍 봉양리, 작은 마을에 자리한 워케이션 오피스입니다.
다양한 방식으로 일하는 창작자들을 위해 공유 오피스, 로컬 콘텐츠,
투어(워크숍) 등을 운영하며 일과 일상, 여행이 자연스럽게 이어지는 유연한
삶의 방식을 제안합니다.
</p>
<!-- 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
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>
<!-- Link Buttons with Logos -->
<div class="mt-8 flex justify-center space-x-6 px-4">
<!-- 네이버플레이스 -->
<!-- Link Buttons -->
<div class="mt-10 flex justify-center px-4">
<!-- 네이버 예약 (Place or Reservation) -->
<a
:href="socialLinks.naver"
target="_blank"
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
:src="socialImages.naver"
alt="네이버플레이스 로고"
alt="네이버 예약 로고"
class="h-6 w-6 object-contain"
/>
<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>
<span>공유 오피스 예약하기</span>
</a>
</div>
</section>
@@ -81,14 +89,11 @@ import { computed } from 'vue';
import { MAIN2_IMAGES, SOCIAL_IMAGES } from '@/data/assets';
import { SOCIAL_LINKS } from '@/data/config';
// 1) MAIN2_IMAGES → 배열
const images = computed<string[]>(() => Object.values(MAIN2_IMAGES));
// 2) SOCIAL_IMAGES (로고)와 SOCIAL_LINKS (URL) 노출
const images = [MAIN2_IMAGES.main1, MAIN2_IMAGES.main2, MAIN2_IMAGES.main3];
const socialImages = SOCIAL_IMAGES;
const socialLinks = SOCIAL_LINKS;
</script>
<style scoped>
/* Tailwind 유틸리티만으로 충분하므로 추가 CSS 불필요 */
/* Tailwind only */
</style>

View File

@@ -1,97 +1,77 @@
<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">
<section
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
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 -->
<div class="lg:pr-4">
<!-- Left: Image -->
<div class="-mt-8 w-full max-w-2xl xl:-mb-8 xl:w-96 xl:flex-none">
<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
class="absolute inset-0 w-full h-full object-cover brightness-105 saturate-100"
src="/assets/img/shoot.jpg"
alt="Bobu team visual"
class="absolute inset-0 size-full rounded-2xl bg-gray-200 object-cover shadow-2xl dark:bg-gray-700 dark:shadow-none"
:src="MAIN2_IMAGES.main3"
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>
<!-- Text Block -->
<div>
<div class="text-base text-gray-700 dark:text-gray-300 lg:max-w-lg">
<!-- Right: Text block -->
<div
class="w-full max-w-2xl xl:max-w-none xl:flex-auto xl:px-16 xl:py-20"
>
<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
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>
<h1
class="mt-2 text-4xl font-semibold tracking-tight text-gray-900 dark:text-white sm:text-5xl"
<!-- Bottom: 번째 문단 -->
<p
class="mt-4 text-base/7 sm:text-lg/8 text-gray-800 dark:text-gray-200"
>
{{ $t('about.sectionTitle') }}
</h1>
<div class="max-w-xl">
<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">&rarr;</span>
</NuxtLink>
</div>
정선을 찾는 생활인구와 노마드 워커들을 위한 (WORK) (STOP)
공간을 운영하며 로컬을 경험하고 즐길 있는 콘텐츠를 제작합니다.
</p>
</figure>
</div>
</div>
</div>
</section>
</template>
<script setup lang="ts">
const stats = [
{ label: 'Founded', value: '2018' },
{ label: 'Projects Delivered', value: '120+' },
{ label: 'Technologies', value: 'FILM / XR / 3D' },
{ label: 'Countries', value: '3' },
];
import { MAIN2_IMAGES } from '@/data/assets'; // Presentational only no logic needed
</script>
<style scoped>
/* Tailwind utilities handle all dark/light styling */
/* Tailwind handles visuals */
</style>

View File

@@ -1,94 +1,107 @@
<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-3xl font-bold text-gray-900 dark:text-gray-100 sm:text-4xl"
<section class="bg-white dark:bg-gray-900 py-16 sm:py-24">
<div class="mx-auto max-w-4xl px-4">
<!-- Heading -->
<header class="text-center">
<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>
<p class="mt-2 text-lg/8 text-gray-600 dark:text-gray-400">
Learn how to grow your business with our expert advice.
</header>
<!-- 🌄 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>
</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"
<!-- Gear list -->
<div class="mt-10 rounded-2xl bg-gray-50 px-5 py-6 dark:bg-gray-800/70">
<p
class="text-xs font-semibold tracking-wide text-gray-600 dark:text-gray-300"
>
기본 대여 장비
</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
:src="post.imageUrl"
alt=""
class="absolute inset-0 w-full h-full object-cover z-0"
:src="SOCIAL_IMAGES.naver"
alt="백패킹 예약 아이콘"
class="h-6 w-6 object-contain"
/>
<!-- 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>
<span>백패킹 예약하기</span>
</NuxtLink>
</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 setup lang="ts">
import { MAIN2_IMAGES, SOCIAL_IMAGES } from '@/data/assets';
const imageList = [
MAIN2_IMAGES.main1,
MAIN2_IMAGES.main2,
MAIN2_IMAGES.main3,
MAIN2_IMAGES.main4,
];
</script>
<style scoped>
/*
Ensure you have the Tailwind line-clamp plugin enabled for “line-clamp-2”.
*/
.pill {
@apply rounded-full bg-white px-3 py-1 shadow-sm dark:bg-gray-900/60;
}
</style>

View 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>

View 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>

View 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>

View 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>
&nbsp;위의 이용약관에 동의하십니까?
</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>
&nbsp;위와 같이 개인정보를 수집·이용하는 동의하십니까?
</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>

View 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>

View File

@@ -362,7 +362,7 @@
<ul class="mt-4 list-disc pl-4 space-y-2">
<li>
<strong>수집 항목:</strong>
소속(기관명/부서명), 성명, 생년월일, 연락처, 이메일, 주소
성명, 생년월일, 연락처, 이메일, 주소
</li>
<li>
<strong>수집 이용 목적:</strong>

View File

@@ -43,4 +43,5 @@ export const SOCIAL_IMAGES = {
naver: '/assets/img/logo/naver_resized.jpg',
instagram: '/assets/img/logo/instagram.webp',
kakao: '/assets/img/logo/kakao_resized.jpg',
npay: '/assets/img/logo/npay.jpg',
};

View File

@@ -1,15 +1,16 @@
// @/data/config.ts
export const BASE_NAV_ITEMS = [
{ name: '소개', href: '/about', icon: ['fas', 'info-circle'] },
{ name: '공유 오피스 예약', href: '/office', icon: ['fas', 'calendar-alt'] },
{ name: 'BOBU', href: '/about', icon: ['fas', 'info-circle'] },
{ name: 'Backpacking', href: '/office', icon: ['fas', 'calendar-alt'] },
{
name: '워크케이션 프로그램',
name: 'Backpacking Community',
href: '/',
icon: ['fas', 'umbrella-beach'],
},
{ name: '쇼핑하기', href: '/shop', icon: ['fas', 'shopping-bag'] },
{ name: '문의하기', href: '/contact', icon: ['fas', 'envelope'] },
{ name: 'WORKATION Inquiry', href: '/contact', icon: ['fas', 'envelope'] },
{ name: 'SHOP', href: '/shop', icon: ['fas', 'shopping-bag'] },
{
name: '와디즈 펀딩 참여자',
href: '/wadiz',

View File

@@ -1,17 +1,23 @@
<template>
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
<!-- Featured Projects Section -->
<!-- <FeaturesCarousel />
<FeaturesCarousel />
<AboutSection0 />
<AboutSection2 />
<AboutSection1 />
<AboutSection3 /> -->
<AboutSection3 />
<AboutSection6 />
<AboutSection7 />
<AboutSection5 />
<!-- <AppWadiz /> -->
</div>
</template>
<script setup lang="ts">
// import AppWadiz from '~/pages/wadiz/index.vue';
import AppWadiz from '~/pages/wadiz/index.vue';
import { MAIN } from '~/data/assets';
definePageMeta({
redirect: '/wadiz',
});
// definePageMeta({
// redirect: '/wadiz',
// });
</script>

View File

@@ -4,11 +4,11 @@
<NuxtLink to="/wadiz/manage">TO manage</NuxtLink>
</div>
<div v-else>
<AdminLogin />
<UserLogin />
</div>
</template>
<script setup lang="ts">
import AdminLogin from '~/components/auth/admin-login.vue';
import UserLogin from '~/components/auth/user-login.vue';
const userStore = useUserStore();
const userLoggedIn = computed(() => userStore.userLoggedIn);

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB