This commit is contained in:
root
2026-07-13 13:02:23 +00:00
parent ce111431c3
commit 2445995172
111 changed files with 11646 additions and 0 deletions
+328
View File
@@ -0,0 +1,328 @@
<script setup lang="ts">
import { ref } from 'vue'
import { RouterView, useRouter, useRoute } from 'vue-router'
import { Anchor, ShieldAlert, HeartHandshake, Menu, X } from 'lucide-vue-next'
const router = useRouter()
const route = useRoute()
const mobileMenuOpen = ref(false)
const navItems = [
{ name: '메인 홈', path: '/', icon: Anchor },
{ name: '캠페인 소개', path: '/about', icon: ShieldAlert },
{ name: '희망 기부', path: '/donation', icon: HeartHandshake }
]
const navigateTo = (path: string) => {
router.push(path)
mobileMenuOpen.value = false
}
</script>
<template>
<div class="app-layout">
<!-- Header -->
<header class="app-header">
<div class="header-logo" @click="navigateTo('/')">
<div class="logo-icon-wrapper">
<Anchor class="logo-icon" />
</div>
<div class="logo-text">
<span class="logo-title">희망의 바다</span>
<span class="logo-subtitle">수협중앙회</span>
</div>
</div>
<!-- Desktop Nav -->
<nav class="desktop-nav">
<button
v-for="item in navItems"
:key="item.path"
:class="['nav-link', { active: route.path === item.path }]"
@click="navigateTo(item.path)"
>
<component :is="item.icon" class="nav-icon" />
<span>{{ item.name }}</span>
</button>
</nav>
<!-- Mobile Nav Toggle -->
<button class="mobile-nav-toggle" @click="mobileMenuOpen = !mobileMenuOpen">
<Menu v-if="!mobileMenuOpen" />
<X v-else />
</button>
</header>
<!-- Mobile Nav Menu -->
<transition name="slide-fade">
<nav v-if="mobileMenuOpen" class="mobile-nav">
<button
v-for="item in navItems"
:key="item.path"
:class="['mobile-nav-link', { active: route.path === item.path }]"
@click="navigateTo(item.path)"
>
<component :is="item.icon" class="nav-icon" />
<span>{{ item.name }}</span>
</button>
</nav>
</transition>
<!-- Main Content Area -->
<main class="main-content">
<RouterView v-slot="{ Component }">
<transition name="fade" mode="out-in">
<component :is="Component" />
</transition>
</RouterView>
</main>
<!-- Footer -->
<footer class="app-footer">
<div class="footer-content">
<p class="footer-title">수협중앙회 희망의 바다 만들기 운동본부</p>
<p class="footer-text">© 2026 수협중앙회. All rights reserved. 깨끗하고 풍요로운 바다, 함께 만들어갑니다.</p>
</div>
</footer>
</div>
</template>
<style>
/* 전역 스코프 및 공통 레이아웃 스타일 정의 */
:root {
--primary-color: #00d2ff;
--primary-rgb: 0, 210, 255;
--secondary-color: #0066ff;
--accent-color: #ff6b6b;
--dark-bg: #030814;
--glass-bg: rgba(6, 17, 38, 0.65);
--glass-border: rgba(0, 210, 255, 0.15);
--text-color: #e0f2fe;
}
body {
background: radial-gradient(circle at center, #061530 0%, #030814 100%) !important;
color: var(--text-color);
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
margin: 0;
padding: 0 !important; /* Wails 기본 padding 덮어쓰기 */
}
/* Wails default background override */
.bg {
display: none !important;
}
.app-layout {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.app-header {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 70px;
background: var(--glass-bg);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-bottom: 1px solid var(--glass-border);
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 40px;
z-index: 100;
--wails-draggable: drag;
}
.header-logo {
display: flex;
align-items: center;
gap: 12px;
cursor: pointer;
--wails-draggable: no-drag;
}
.logo-icon-wrapper {
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
padding: 8px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 0 15px rgba(0, 210, 255, 0.4);
}
.logo-icon {
color: white;
width: 20px;
height: 20px;
animation: float 3s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-3px); }
}
.logo-text {
display: flex;
flex-direction: column;
}
.logo-title {
font-size: 1.15rem;
font-weight: 800;
letter-spacing: -0.5px;
background: linear-gradient(to right, #ffffff, var(--primary-color));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.logo-subtitle {
font-size: 0.75rem;
color: #94a3b8;
}
.desktop-nav {
display: flex;
gap: 8px;
--wails-draggable: no-drag;
}
.nav-link {
background: transparent;
border: none;
color: #94a3b8;
padding: 8px 16px;
border-radius: 8px;
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
font-size: 0.95rem;
cursor: pointer;
transition: all 0.3s ease;
}
.nav-link:hover {
color: var(--primary-color);
background: rgba(0, 210, 255, 0.08);
}
.nav-link.active {
color: white;
background: linear-gradient(135deg, rgba(0, 210, 255, 0.2), rgba(0, 102, 255, 0.2));
border: 1px solid var(--primary-color);
box-shadow: 0 0 15px rgba(0, 210, 255, 0.25);
}
.nav-icon {
width: 16px;
height: 16px;
}
.mobile-nav-toggle {
display: none;
background: transparent;
border: none;
color: white;
cursor: pointer;
--wails-draggable: no-drag;
}
.main-content {
flex: 1;
margin-top: 70px;
padding: 40px;
display: flex;
flex-direction: column;
}
.app-footer {
background: #020611;
border-top: 1px solid rgba(255, 255, 255, 0.05);
padding: 30px 40px;
text-align: center;
font-size: 0.85rem;
color: #64748b;
}
.footer-title {
font-weight: 700;
color: #94a3b8;
margin-bottom: 8px;
}
.footer-text {
margin: 0;
}
/* Transitions */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.slide-fade-enter-active,
.slide-fade-leave-active {
transition: all 0.3s ease;
}
.slide-fade-enter-from,
.slide-fade-leave-to {
transform: translateY(-20px);
opacity: 0;
}
/* Responsiveness */
@media (max-width: 768px) {
.desktop-nav {
display: none;
}
.mobile-nav-toggle {
display: block;
}
.main-content {
padding: 20px;
}
.mobile-nav {
position: fixed;
top: 70px;
left: 0;
right: 0;
background: var(--glass-bg);
backdrop-filter: blur(20px);
border-bottom: 1px solid var(--glass-border);
display: flex;
flex-direction: column;
padding: 15px;
gap: 8px;
z-index: 99;
}
.mobile-nav-link {
background: transparent;
border: none;
color: #94a3b8;
padding: 12px;
border-radius: 8px;
display: flex;
align-items: center;
gap: 12px;
font-weight: 600;
cursor: pointer;
text-align: left;
}
.mobile-nav-link.active {
color: white;
background: rgba(0, 210, 255, 0.15);
}
}
</style>
+114
View File
@@ -0,0 +1,114 @@
<script setup lang="ts">
import {ref, onMounted, useTemplateRef} from 'vue'
import {Events, WML} from "@wailsio/runtime";
import {GreetService} from "../../bindings/changeme";
// Show the actual Wails version this project was generated against.
const wailsVersion = "v3.0.0-alpha2.106";
const name = ref('');
const time = ref('Listening for Time event...');
const titleNameRef = useTemplateRef<HTMLElement>('titleNameRef');
const toastRef = useTemplateRef<HTMLElement>('toastRef');
const resultRef = useTemplateRef<HTMLElement>('resultRef');
let toastTimer: ReturnType<typeof setTimeout>;
// Crossfade the framework word in the heading ("Wails + Vue") to the name the
// user entered ("Wails + <name>"): the old word fades out while the new one
// fades in over the same spot.
function swapTitleName(newName: string) {
const titleNameElement = titleNameRef.value;
if (!titleNameElement) {
return;
}
const current = titleNameElement.querySelector('.title-name-text:not(.is-outgoing)');
if (!current || current.textContent === newName) {
return;
}
const incoming = document.createElement('span');
incoming.className = 'title-name-text is-entering';
incoming.textContent = newName;
current.classList.add('is-outgoing');
titleNameElement.appendChild(incoming);
// Force a reflow so the transitions run from the starting state.
void incoming.offsetWidth;
incoming.classList.remove('is-entering');
current.classList.add('is-leaving');
current.addEventListener('transitionend', () => current.remove(), {once: true});
}
// Pop the toast with the message Go returned, then auto-dismiss it.
function showToast(message: string) {
if (!resultRef.value || !toastRef.value) {
return;
}
resultRef.value.innerText = message;
toastRef.value.classList.add('is-visible');
clearTimeout(toastTimer);
toastTimer = setTimeout(() => toastRef.value?.classList.remove('is-visible'), 4000);
}
const doGreet = () => {
let n = name.value || 'anonymous';
swapTitleName(n);
GreetService.Greet(n).then(showToast).catch(console.error);
}
onMounted(() => {
Events.On('time', (timeValue: { data: string }) => {
// On a narrow screen the full RFC1123 stamp is too wide for the footer, so
// show just the clock time there (matching the CSS breakpoint).
const full = timeValue.data;
const compact = (full.match(/\d{1,2}:\d{2}:\d{2}/) || [full])[0];
time.value = window.matchMedia('(max-width: 640px)').matches ? compact : full;
});
// Wire up data-wml-openURL links (logos + footer "Docs" link).
WML.Reload();
})
</script>
<template>
<main class="container">
<header class="brand">
<a class="brand-mark" data-wml-openURL="https://v3.wails.io" aria-label="Wails website">
<img src="/wails.png" class="brand-logo" alt="Wails logo"/>
</a>
<a class="brand-badge" data-wml-openURL="https://vuejs.org/" aria-label="Vue">
<img src="/vue.svg" alt="Vue logo"/>
</a>
</header>
<h1 class="title"><span class="title-accent">Wails +</span> <span class="title-name" ref="titleNameRef"><span class="title-name-text">Vue</span></span></h1>
<p class="subtitle">Build beautiful cross-platform apps with Go and Vue.</p>
<div class="greet">
<div class="input-box" id="input">
<svg class="input-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
<input aria-label="input" class="input" id="name" v-model="name" type="text" placeholder="Your name" autocomplete="off"/>
<button aria-label="greet-btn" class="btn" @click="doGreet">Greet
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>
</button>
</div>
</div>
</main>
<hr class="footer-divider"/>
<footer class="footer">
<span class="footer-version">{{ wailsVersion }}</span>
<span class="footer-time">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
<span id="time">{{ time }}</span>
</span>
<a class="footer-docs" data-wml-openURL="https://v3.wails.io" aria-label="Wails documentation">Docs
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="7" y1="17" x2="17" y2="7"/><polyline points="7 7 17 7 17 17"/></svg>
</a>
</footer>
<!-- Toast: shows the greeting returned by the Go backend (overlay, so it never
reflows the layout). -->
<div class="toast" id="toast" ref="toastRef" role="status" aria-live="polite">
<span class="toast-label">From Go</span>
<span aria-label="result" class="toast-msg" id="result" ref="resultRef"></span>
</div>
</template>
+5
View File
@@ -0,0 +1,5 @@
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
createApp(App).use(router).mount('#app')
+29
View File
@@ -0,0 +1,29 @@
import { createRouter, createWebHashHistory } from 'vue-router';
import Home from '../views/Home.vue';
import About from '../views/About.vue';
import Donation from '../views/Donation.vue';
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: About
},
{
path: '/donation',
name: 'Donation',
component: Donation
}
];
const router = createRouter({
history: createWebHashHistory(),
routes
});
export default router;
+563
View File
@@ -0,0 +1,563 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { ArrowRight, Compass, Shield, Award, Users } from 'lucide-vue-next'
const router = useRouter()
const activeTab = ref('seed')
const activities = {
seed: {
title: '수산종자 방류 사업',
subtitle: '수산자원의 회복과 복원',
desc: '고갈되어 가는 우리 바다의 어족 자원을 늘리고 어업인의 소득을 보장하기 위해, 지역 해역 특성에 맞는 우수한 품종의 치어를 방류합니다.',
points: [
'방류 효과 분석을 바탕으로 한 경제성 높은 어종 선정',
'전국 연안 지자체 및 회원조합과 공동으로 매년 추진',
'감성돔, 조피볼락, 꽃게 등 대표 고부가가치 품종 방류',
'어업인의 수산 소득 증대 및 미래 생태 식량 확보'
],
infographic: [
{ step: '01', title: '우량 종자 확보', desc: '질병 검사를 마친 건강한 수산 치어 선별' },
{ step: '02', title: '해역 적합도 평가', desc: '지역별 염도 및 온도에 알맞은 어종 연구' },
{ step: '03', title: '체계적 방류', desc: '수산 자원 고갈 지점에서 집중 방류 실시' },
{ step: '04', title: '추적 및 관리', desc: '방류된 치어의 생존율 및 자원조성 효과 모니터링' }
]
},
cleanup: {
title: '해양 쓰레기 수거 사업',
subtitle: '조업 중 쓰레기 수매 및 수거 지원',
desc: '바다 밑에 침적된 폐어구, 플라스틱 등 어업 활동을 방해하고 해양 생태계를 오염시키는 해양 쓰레기를 신속히 수거하여 깨끗한 환경을 만듭니다.',
points: [
'어업인 참여 유도를 위한 조업 중 인양 쓰레기 수매 사업 진행',
'시민단체 및 연안 해안가 집중 정화 운동 전개',
'침적 폐그물 수거를 통해 해양 동물의 유령어업(Ghost Fishing) 방지',
'해양 플라스틱 오염 방지를 통한 미세 플라스틱 먹이사슬 차단'
],
infographic: [
{ step: '01', title: '인양 및 수집', desc: '조업 중 수거한 쓰레기를 항구로 반입' },
{ step: '02', title: '수협 수매 시스템', desc: '수협이 매입하여 쓰레기 회수율 극대화' },
{ step: '03', title: '친환경 처리', desc: '재활용 가능 플라스틱과 폐어구 분류 처리' },
{ step: '04', title: '바다 청정화', desc: '어장 환경의 개선과 선박 안전 운항 확보' }
]
},
restoration: {
title: '바닷속 환경 정화 사업',
subtitle: '갯녹음 치유 및 생태계 복원',
desc: '지구 온난화와 환경 오염으로 바다가 하얗게 사막화되는 갯녹음 현상(백화현상)을 방지하고, 해조류가 자라는 풍요로운 바다 숲을 만듭니다.',
points: [
'갯녹음 해역의 해적생물(성게 등) 집중 구제 및 유해 유기물 제거',
'어장 환경 개선을 위한 바닥갈이(경운) 및 물갈이 사업 지원',
'미생물 및 친환경 공법을 활용한 바다 해저 토양 정화',
'바다숲 조성을 통해 수산 생물의 산란장 및 서식지 제공'
],
infographic: [
{ step: '01', title: '현황 정밀 조사', desc: '갯녹음 및 바다 바닥 황폐화 상태 과학적 진단' },
{ step: '02', title: '어장 바닥갈이', desc: '경운 작업을 통해 암반의 퇴적물과 유해물질 세척' },
{ step: '03', title: '해조류 이식 및 관리', desc: '다시마, 감태 등 해조류를 안착시켜 바다숲 형성' },
{ step: '04', title: '풍요로운 어장 복원', desc: '어패류가 산란하고 성장할 수 있는 보금자리 완성' }
]
}
}
</script>
<template>
<div class="about-container">
<!-- Intro Hero -->
<section class="about-hero">
<div class="glow-bg"></div>
<div class="hero-content">
<h1 class="about-title">바다를 살리는 3가지 약속</h1>
<p class="about-subtitle-text">
수협중앙회는 2007년부터 전국의 어업인들과 힘을 합쳐,<br />
바다의 수산 자원을 회복하고 해양 환경을 맑게 가꾸는 공익적 실천을 이어가고 있습니다.
</p>
</div>
</section>
<!-- Tabbed Activities Grid -->
<section class="activities-section">
<div class="tab-buttons">
<button
:class="['tab-btn', { active: activeTab === 'seed' }]"
@click="activeTab = 'seed'"
>
<Compass class="tab-icon" />
<span>수산종자 방류</span>
</button>
<button
:class="['tab-btn', { active: activeTab === 'cleanup' }]"
@click="activeTab = 'cleanup'"
>
<Shield class="tab-icon" />
<span>쓰레기 수거</span>
</button>
<button
:class="['tab-btn', { active: activeTab === 'restoration' }]"
@click="activeTab = 'restoration'"
>
<Award class="tab-icon" />
<span>바닷속 환경 정화</span>
</button>
</div>
<!-- Tab Content Area -->
<transition name="fade" mode="out-in">
<div :key="activeTab" class="tab-content">
<div class="content-left">
<h2 class="content-title">{{ activities[activeTab].title }}</h2>
<h3 class="content-subtitle">{{ activities[activeTab].subtitle }}</h3>
<p class="content-desc">{{ activities[activeTab].desc }}</p>
<ul class="point-list">
<li v-for="(point, idx) in activities[activeTab].points" :key="idx" class="point-item">
<span class="bullet"></span>
<span>{{ point }}</span>
</li>
</ul>
</div>
<div class="content-right">
<h4 class="right-title">추진 프로세스</h4>
<div class="infographic-flow">
<div
v-for="step in activities[activeTab].infographic"
:key="step.step"
class="flow-step"
>
<div class="step-num">{{ step.step }}</div>
<div class="step-info">
<h5 class="step-title">{{ step.title }}</h5>
<p class="step-desc">{{ step.desc }}</p>
</div>
</div>
</div>
</div>
</div>
</transition>
</section>
<!-- Citizen Campaign Info -->
<section class="citizen-section">
<div class="citizen-card">
<div class="citizen-icon-wrapper">
<Users class="citizen-icon" />
</div>
<div class="citizen-info">
<span class="citizen-badge">국민 참여 프로그램</span>
<h2 class="citizen-title">모두의 바다, 함께海 캠페인</h2>
<p class="citizen-desc">
해양 환경 정화에 관심이 있는 학생, 일반 시민 모임, 동호회가 바닷가 정화 활동을 실천할 있도록 수협중앙회가 활동 경비(최대 100 ) 지원합니다.
</p>
<div class="citizen-benefits">
<div class="benefit-item">
<div class="benefit-dot"></div>
<span>전국 ·포구 해안가 정화 단체 지원</span>
</div>
<div class="benefit-item">
<div class="benefit-dot"></div>
<span>청소년 교육 환경 인식 제고</span>
</div>
<div class="benefit-item">
<div class="benefit-dot"></div>
<span>기금 기부를 통한 가상의 바다 정화 동참</span>
</div>
</div>
<button class="btn btn-primary" @click="router.push('/donation')">
<span>참여하고 바다 가꾸기</span>
<ArrowRight class="btn-icon" />
</button>
</div>
</div>
</section>
</div>
</template>
<style scoped>
.about-container {
display: flex;
flex-direction: column;
gap: 80px;
width: 100%;
max-width: 1200px;
margin: 0 auto;
}
/* About Hero */
.about-hero {
position: relative;
text-align: center;
padding: 60px 20px;
border-radius: 24px;
background: rgba(6, 17, 38, 0.4);
border: 1px solid rgba(0, 210, 255, 0.08);
overflow: hidden;
}
.glow-bg {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 300px;
height: 300px;
background: radial-gradient(circle, rgba(0, 210, 255, 0.15) 0%, transparent 70%);
z-index: 1;
pointer-events: none;
}
.hero-content {
position: relative;
z-index: 2;
}
.about-title {
font-size: 2.5rem;
font-weight: 800;
margin: 0 0 16px 0;
background: linear-gradient(to right, #ffffff, var(--primary-color));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.about-subtitle-text {
font-size: 1.1rem;
color: #94a3b8;
line-height: 1.6;
max-width: 750px;
margin: 0 auto;
}
/* Activities Section */
.activities-section {
display: flex;
flex-direction: column;
gap: 40px;
}
.tab-buttons {
display: flex;
justify-content: center;
gap: 15px;
}
.tab-btn {
background: rgba(6, 17, 38, 0.6);
border: 1px solid rgba(0, 210, 255, 0.1);
padding: 16px 30px;
border-radius: 16px;
color: #94a3b8;
font-size: 1.05rem;
font-weight: 700;
cursor: pointer;
display: flex;
align-items: center;
gap: 10px;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
}
.tab-btn:hover {
border-color: rgba(0, 210, 255, 0.3);
color: var(--primary-color);
transform: translateY(-2px);
}
.tab-btn.active {
background: linear-gradient(135deg, rgba(0, 210, 255, 0.15), rgba(0, 102, 255, 0.15));
border-color: var(--primary-color);
color: white;
box-shadow: 0 5px 20px rgba(0, 210, 255, 0.15);
}
.tab-icon {
width: 20px;
height: 20px;
}
.tab-content {
display: grid;
grid-template-columns: 1.2fr 1fr;
gap: 40px;
background: rgba(6, 17, 38, 0.4);
border: 1px solid rgba(0, 210, 255, 0.08);
border-radius: 24px;
padding: 40px;
backdrop-filter: blur(10px);
}
.content-left {
display: flex;
flex-direction: column;
justify-content: center;
}
.content-title {
font-size: 1.8rem;
font-weight: 800;
color: white;
margin: 0 0 8px 0;
}
.content-subtitle {
font-size: 1.1rem;
font-weight: 600;
color: var(--primary-color);
margin: 0 0 20px 0;
}
.content-desc {
font-size: 1rem;
line-height: 1.6;
color: #94a3b8;
margin-bottom: 25px;
}
.point-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 12px;
}
.point-item {
display: flex;
align-items: flex-start;
gap: 10px;
color: #cbd5e1;
font-size: 0.95rem;
line-height: 1.4;
}
.bullet {
width: 6px;
height: 6px;
background: var(--primary-color);
border-radius: 50%;
margin-top: 8px;
flex-shrink: 0;
box-shadow: 0 0 8px var(--primary-color);
}
/* Right Column Infographic Flow */
.content-right {
border-left: 1px solid rgba(255, 255, 255, 0.05);
padding-left: 40px;
display: flex;
flex-direction: column;
justify-content: center;
}
.right-title {
font-size: 1.1rem;
font-weight: 700;
color: #94a3b8;
margin: 0 0 20px 0;
letter-spacing: 0.5px;
}
.infographic-flow {
display: flex;
flex-direction: column;
gap: 20px;
}
.flow-step {
display: flex;
gap: 15px;
align-items: flex-start;
position: relative;
}
.flow-step:not(:last-child)::after {
content: '';
position: absolute;
top: 30px;
left: 17px;
width: 2px;
height: calc(100% - 10px);
background: linear-gradient(to bottom, var(--primary-color), rgba(0, 102, 255, 0.1));
}
.step-num {
width: 36px;
height: 36px;
border-radius: 50%;
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
color: white;
font-size: 0.95rem;
font-weight: 800;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
box-shadow: 0 0 10px rgba(0, 210, 255, 0.3);
}
.step-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.step-title {
font-size: 0.95rem;
font-weight: 700;
color: white;
margin: 0;
}
.step-desc {
font-size: 0.85rem;
color: #64748b;
margin: 0;
line-height: 1.4;
}
/* Citizen Campaign Info */
.citizen-section {
width: 100%;
}
.citizen-card {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 40px;
background: radial-gradient(circle at bottom right, rgba(0, 102, 255, 0.1) 0%, rgba(3, 8, 20, 0.8) 80%);
border: 1px solid rgba(0, 210, 255, 0.15);
border-radius: 24px;
padding: 50px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
}
.citizen-icon-wrapper {
background: linear-gradient(135deg, rgba(0, 210, 255, 0.1), rgba(0, 102, 255, 0.1));
border: 1px solid rgba(0, 210, 255, 0.2);
border-radius: 20px;
display: flex;
align-items: center;
justify-content: center;
padding: 40px;
}
.citizen-icon {
width: 80px;
height: 80px;
color: var(--primary-color);
animation: pulse 2s infinite alternate;
}
@keyframes pulse {
0% { transform: scale(1); filter: drop-shadow(0 0 10px rgba(0, 210, 255, 0.2)); }
100% { transform: scale(1.05); filter: drop-shadow(0 0 20px rgba(0, 210, 255, 0.5)); }
}
.citizen-info {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 15px;
}
.citizen-badge {
font-size: 0.8rem;
font-weight: 700;
color: #ff6b6b;
background: rgba(255, 107, 107, 0.12);
padding: 4px 10px;
border-radius: 6px;
border: 1px solid rgba(255, 107, 107, 0.3);
}
.citizen-title {
font-size: 1.8rem;
font-weight: 800;
color: white;
margin: 0;
}
.citizen-desc {
font-size: 1rem;
line-height: 1.6;
color: #94a3b8;
margin: 0;
}
.citizen-benefits {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 10px;
}
.benefit-item {
display: flex;
align-items: center;
gap: 8px;
color: #cbd5e1;
font-size: 0.95rem;
}
.benefit-dot {
width: 6px;
height: 6px;
background: #ff6b6b;
border-radius: 50%;
}
.btn-icon {
width: 16px;
height: 16px;
}
/* Animations */
.fade-enter-active,
.fade-leave-active {
transition: all 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
transform: translateY(10px);
}
/* Responsive */
@media (max-width: 992px) {
.tab-content {
grid-template-columns: 1fr;
gap: 30px;
}
.content-right {
border-left: none;
border-top: 1px solid rgba(255, 255, 255, 0.05);
padding-left: 0;
padding-top: 30px;
}
.citizen-card {
grid-template-columns: 1fr;
gap: 30px;
padding: 30px;
}
.citizen-icon-wrapper {
padding: 20px;
}
.citizen-icon {
width: 60px;
height: 60px;
}
}
@media (max-width: 768px) {
.tab-buttons {
flex-direction: column;
gap: 10px;
}
.tab-btn {
width: 100%;
justify-content: center;
}
}
</style>
+784
View File
@@ -0,0 +1,784 @@
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { Heart, Send, CheckCircle2, MessageSquare, Shield, Fish, Trash2, HelpCircle } from 'lucide-vue-next'
const stats = ref({
totalDonors: 12543,
totalAmount: 627150000,
releasedFish: 4200000,
collectedWaste: 185000,
purifiedArea: 95000
})
const recentDonations = ref<any[]>([])
const form = ref({
name: '',
amount: 30000,
message: ''
})
const showSuccess = ref(false)
const isSubmitting = ref(false)
const predefinedAmounts = [10000, 30000, 50000, 100000, 300000]
// Real-time impact simulation computed values
const simulatedImpact = computed(() => {
const amount = form.value.amount || 0
const multiplier = amount / 10000
return {
fish: Math.floor(multiplier * 2),
waste: (multiplier * 0.5).toFixed(1),
area: (multiplier * 0.2).toFixed(2)
}
})
const loadData = async () => {
try {
// @ts-ignore
const CampaignService = await import('../../bindings/changeme/campaignservice')
const currentStats = await CampaignService.GetStats()
if (currentStats) stats.value = currentStats
const list = await CampaignService.GetRecentDonations()
if (list) {
// Reverse list to show newest first
recentDonations.value = [...list].reverse()
}
} catch (e) {
console.warn('Wails bindings not available, using mock local data.', e)
// Local mock data fallback
recentDonations.value = [
{ name: '김어부', amount: 50000, message: '바다가 다시 풍요로워지길 바랍니다.', timestamp: new Date(Date.now() - 10 * 60000).toISOString() },
{ name: '이바다', amount: 30000, message: '깨끗한 바다를 우리 아이들에게!', timestamp: new Date(Date.now() - 60 * 60000).toISOString() },
{ name: '박수협', amount: 100000, message: '어업인 여러분 힘내세요!', timestamp: new Date(Date.now() - 180 * 60000).toISOString() }
]
}
}
onMounted(() => {
loadData()
})
const handleDonate = async () => {
if (form.value.amount <= 0) return
isSubmitting.value = true
try {
// @ts-ignore
const CampaignService = await import('../../bindings/changeme/campaignservice')
const newStats = await CampaignService.Donate(form.value.name, form.value.amount, form.value.message)
if (newStats) stats.value = newStats
// Refresh donation list
const list = await CampaignService.GetRecentDonations()
if (list) recentDonations.value = [...list].reverse()
showSuccessModal()
} catch (e) {
console.warn('Wails backend Donate call skipped. Simulating successfully on frontend.', e)
// Simulating locally if not running in Wails
stats.value.totalDonors += 1
stats.value.totalAmount += form.value.amount
const multiplier = form.value.amount / 10000
stats.value.releasedFish += Math.floor(multiplier * 2)
stats.value.collectedWaste += Math.floor(multiplier * 0.5)
stats.value.purifiedArea += Math.floor(multiplier * 0.2)
recentDonations.value.unshift({
name: form.value.name || '익명의 기부자',
amount: form.value.amount,
message: form.value.message || '푸른 바다를 함께 응원합니다!',
timestamp: new Date().toISOString()
})
showSuccessModal()
} finally {
isSubmitting.value = false
}
}
const showSuccessModal = () => {
showSuccess.value = true
// Reset form
form.value.name = ''
form.value.message = ''
}
const formatNumber = (num: number) => {
return new Intl.NumberFormat('ko-KR').format(num)
}
const formatDate = (dateStr: string) => {
const d = new Date(dateStr)
return `${d.getMonth() + 1}/${d.getDate()} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
}
</script>
<template>
<div class="donation-container">
<!-- Hero Header -->
<div class="donation-hero">
<h1 class="page-title">희망의 바다 기금 기부</h1>
<p class="page-subtitle">수협중앙회와 함께 맑은 바다 생태계를 가꾸는 가장 가치 있는 첫걸음입니다.</p>
</div>
<!-- Main Grid layout -->
<div class="donation-grid">
<!-- Left Column: Form and Impact simulation -->
<div class="column-left">
<!-- Interactive Impact Simulator Card -->
<div class="card simulator-card">
<h3 class="card-title"><Fish class="card-icon" /> 실시간 기부 효과 시뮬레이션</h3>
<p class="card-desc">기부하시는 금액에 따라 우리 바다에 생겨날 놀라운 변화를 확인해보세요.</p>
<div class="simulator-graphic">
<!-- Ocean environment visual feedback -->
<div class="mini-ocean">
<div class="sim-bubbles">
<div v-for="n in 6" :key="n" class="sim-bubble" :style="`--i:${n}`"></div>
</div>
<!-- Floating simulated fish -->
<div class="fish-pond">
<div
v-for="n in Math.min(simulatedImpact.fish, 10)"
:key="n"
class="sim-fish"
:style="`--speed:${1.5 + Math.random() * 2}s; --delay:${Math.random() * 2}s; --y:${15 + (n * 8)}%`"
>
🐟
</div>
<div v-if="simulatedImpact.fish === 0" class="no-fish-text">치어가 방류 대기 중입니다.</div>
</div>
</div>
</div>
<!-- Impact Metrics Grid -->
<div class="impact-metrics">
<div class="metric-box">
<span class="m-icon"><Fish /></span>
<span class="m-val">{{ simulatedImpact.fish }} 마리</span>
<span class="m-lbl">수산 치어 방류</span>
</div>
<div class="metric-box">
<span class="m-icon"><Trash2 /></span>
<span class="m-val">{{ simulatedImpact.waste }} kg</span>
<span class="m-lbl">해양 쓰레기 수거</span>
</div>
<div class="metric-box">
<span class="m-icon"><Shield /></span>
<span class="m-val">{{ simulatedImpact.area }} </span>
<span class="m-lbl">바닷속 갯녹음 정화</span>
</div>
</div>
</div>
<!-- Donation Form Card -->
<div class="card form-card">
<h3 class="card-title"><Heart class="card-icon form-icon" /> 가상 후원 참여 </h3>
<p class="card-desc">후원 금액과 응원 메시지는 실시간 캠페인 대시보드에 반영됩니다.</p>
<form @submit.prevent="handleDonate" class="donation-form">
<!-- Name Input -->
<div class="form-group">
<label for="donor-name" class="form-label">후원자명 (또는 단체명)</label>
<input
id="donor-name"
type="text"
v-model="form.name"
placeholder="익명의 기부자"
class="form-input"
/>
</div>
<!-- Amount Input & Preset Buttons -->
<div class="form-group">
<label class="form-label">후원 금액 선택</label>
<div class="preset-buttons">
<button
type="button"
v-for="amt in predefinedAmounts"
:key="amt"
:class="['preset-btn', { active: form.amount === amt }]"
@click="form.amount = amt"
>
{{ formatNumber(amt / 10000) }}
</button>
</div>
<div class="custom-amount-input">
<input
type="number"
v-model.number="form.amount"
placeholder="직접 입력 (원)"
min="1000"
step="1000"
class="form-input custom-input"
/>
<span class="currency-label"></span>
</div>
</div>
<!-- Message Input -->
<div class="form-group">
<label for="donor-msg" class="form-label">바다를 위한 응원 메시지</label>
<textarea
id="donor-msg"
v-model="form.message"
placeholder="깨끗한 바다를 향한 어업인과 수협에 전할 격려의 한마디를 작성해주세요."
class="form-textarea"
rows="3"
></textarea>
</div>
<button type="submit" class="btn btn-submit" :disabled="isSubmitting || form.amount < 1000">
<Heart class="submit-icon" />
<span>{{ isSubmitting ? '전송 중...' : '기금 후원하기' }}</span>
</button>
</form>
</div>
</div>
<!-- Right Column: Live dashboard stats and recent list -->
<div class="column-right">
<!-- Campaign Stats Box -->
<div class="card stats-box-card">
<h3 class="card-title">실시간 희망 바다 현황</h3>
<div class="simple-stat-row">
<span class="simple-lbl">누적 기금 조성액</span>
<span class="simple-val accent-text">{{ formatNumber(stats.totalAmount) }} </span>
</div>
<div class="simple-stat-row">
<span class="simple-lbl">누적 동참자</span>
<span class="simple-val">{{ formatNumber(stats.totalDonors) }} </span>
</div>
</div>
<!-- Recent Donations Feed -->
<div class="card feed-card">
<h3 class="card-title"><MessageSquare class="card-icon" /> 실시간 참여 피드</h3>
<div class="donation-feed">
<transition-group name="list">
<div v-for="(item, idx) in recentDonations" :key="idx" class="feed-item">
<div class="feed-header">
<span class="feed-name">{{ item.name }} </span>
<span class="feed-amt">{{ formatNumber(item.amount) }} 후원</span>
</div>
<p class="feed-msg">"{{ item.message || '풍요로운 바다의 미래를 힘차게 응원합니다!' }}"</p>
<span class="feed-time">{{ formatDate(item.timestamp) }}</span>
</div>
</transition-group>
<div v-if="recentDonations.length === 0" class="empty-feed">
번째 기부 참여를 기다리고 있습니다.
</div>
</div>
</div>
</div>
</div>
<!-- Success Modal popup -->
<div v-if="showSuccess" class="modal-overlay" @click.self="showSuccess = false">
<div class="modal-content">
<CheckCircle2 class="success-big-icon" />
<h2 class="modal-title">후원이 완료되었습니다!</h2>
<p class="modal-desc">
보내주신 따뜻한 희망의 씨앗이 건강한 바다 생태계 복원과 우리 어업인 지원 기금에 소중하게 사용됩니다. 감사합니다!
</p>
<button class="btn btn-primary btn-modal-close" @click="showSuccess = false">확인</button>
</div>
</div>
</div>
</template>
<style scoped>
.donation-container {
display: flex;
flex-direction: column;
gap: 40px;
width: 100%;
max-width: 1200px;
margin: 0 auto;
}
.donation-hero {
text-align: center;
}
.page-title {
font-size: 2.2rem;
font-weight: 800;
margin: 0 0 10px 0;
background: linear-gradient(to right, #ffffff, var(--primary-color));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.page-subtitle {
color: #64748b;
margin: 0;
}
.donation-grid {
display: grid;
grid-template-columns: 1.3fr 1fr;
gap: 30px;
}
.column-left, .column-right {
display: flex;
flex-direction: column;
gap: 30px;
}
/* Card Common Base */
.card {
background: rgba(6, 17, 38, 0.4);
border: 1px solid rgba(0, 210, 255, 0.08);
border-radius: 20px;
padding: 30px;
backdrop-filter: blur(10px);
}
.card-title {
font-size: 1.25rem;
font-weight: 800;
color: white;
margin: 0 0 8px 0;
display: flex;
align-items: center;
gap: 8px;
}
.card-icon {
width: 20px;
height: 20px;
color: var(--primary-color);
}
.card-desc {
font-size: 0.9rem;
color: #64748b;
margin: 0 0 20px 0;
}
/* Live Simulator Visual Graphic */
.simulator-card {
border-color: rgba(0, 210, 255, 0.15);
}
.simulator-graphic {
width: 100%;
margin-bottom: 24px;
}
.mini-ocean {
width: 100%;
height: 180px;
background: linear-gradient(to bottom, #072a56 0%, #03152d 100%);
border-radius: 12px;
border: 1px solid rgba(0, 210, 255, 0.2);
position: relative;
overflow: hidden;
}
.sim-bubbles {
position: absolute;
inset: 0;
}
.sim-bubble {
position: absolute;
bottom: -10px;
background: rgba(0, 210, 255, 0.15);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 50%;
width: 8px;
height: 8px;
left: calc(var(--i) * 15%);
animation: sim-bubble-up 3s linear infinite;
animation-delay: calc(var(--i) * 0.4s);
}
@keyframes sim-bubble-up {
0% { transform: translateY(0) scale(1); opacity: 0; }
20% { opacity: 0.6; }
90% { opacity: 0.6; }
100% { transform: translateY(-200px) scale(1.3); opacity: 0; }
}
.fish-pond {
position: absolute;
inset: 0;
}
.sim-fish {
position: absolute;
font-size: 1.3rem;
animation: swim var(--speed) linear infinite alternate;
animation-delay: var(--delay);
left: -20px;
top: var(--y);
}
@keyframes swim {
0% { left: -20px; transform: scaleX(1); }
45% { transform: scaleX(1); }
50% { transform: scaleX(-1); }
95% { transform: scaleX(-1); }
100% { left: calc(100% - 10px); transform: scaleX(1); }
}
.no-fish-text {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #475569;
font-weight: 600;
font-size: 0.9rem;
}
/* Impact Metrics */
.impact-metrics {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 15px;
}
.metric-box {
background: rgba(0, 0, 0, 0.2);
border: 1px solid rgba(0, 210, 255, 0.08);
border-radius: 12px;
padding: 15px 10px;
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
}
.m-icon {
color: var(--primary-color);
width: 20px;
height: 20px;
display: flex;
align-items: center;
}
.m-val {
font-size: 1.1rem;
font-weight: 800;
color: white;
}
.m-lbl {
font-size: 0.75rem;
color: #64748b;
font-weight: 600;
}
/* Donation Form */
.donation-form {
display: flex;
flex-direction: column;
gap: 20px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.form-label {
font-size: 0.9rem;
font-weight: 700;
color: #cbd5e1;
}
.form-input, .form-textarea {
background: rgba(0, 0, 0, 0.25);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 10px;
padding: 12px 16px;
color: white;
font-family: inherit;
font-size: 0.95rem;
transition: all 0.3s ease;
}
.form-input:focus, .form-textarea:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 10px rgba(0, 210, 255, 0.2);
}
.preset-buttons {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));
gap: 8px;
}
.preset-btn {
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 8px;
padding: 10px;
color: #94a3b8;
font-weight: 700;
font-size: 0.85rem;
cursor: pointer;
transition: all 0.2s ease;
}
.preset-btn:hover {
background: rgba(0, 210, 255, 0.08);
color: var(--primary-color);
}
.preset-btn.active {
background: rgba(0, 210, 255, 0.15);
border-color: var(--primary-color);
color: white;
}
.custom-amount-input {
display: flex;
align-items: center;
position: relative;
margin-top: 5px;
}
.custom-input {
width: 100%;
padding-right: 40px;
}
.currency-label {
position: absolute;
right: 15px;
color: #64748b;
font-weight: 700;
}
.btn-submit {
background: linear-gradient(135deg, #ff6b6b, #ff4747);
color: white;
justify-content: center;
padding: 15px;
border-radius: 12px;
font-size: 1.05rem;
font-weight: 700;
box-shadow: 0 5px 15px rgba(255, 107, 107, 0.3);
margin-top: 10px;
}
.btn-submit:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(255, 107, 107, 0.5);
}
.btn-submit:disabled {
background: #1e293b;
color: #475569;
box-shadow: none;
cursor: not-allowed;
}
.submit-icon {
width: 18px;
height: 18px;
}
/* Campaign Stats Box */
.stats-box-card {
background: linear-gradient(135deg, rgba(6, 17, 38, 0.7), rgba(3, 8, 20, 0.7));
border-color: rgba(0, 210, 255, 0.15);
}
.simple-stat-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
}
.simple-stat-row:not(:last-child) {
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.simple-lbl {
font-size: 0.9rem;
color: #94a3b8;
font-weight: 600;
}
.simple-val {
font-size: 1.15rem;
font-weight: 800;
color: white;
}
.accent-text {
color: var(--primary-color);
text-shadow: 0 0 10px rgba(0, 210, 255, 0.3);
}
/* Feed Card */
.feed-card {
flex: 1;
}
.donation-feed {
display: flex;
flex-direction: column;
gap: 15px;
max-height: 380px;
overflow-y: auto;
padding-right: 5px;
}
/* Scrollbar styling */
.donation-feed::-webkit-scrollbar {
width: 4px;
}
.donation-feed::-webkit-scrollbar-thumb {
background: rgba(0, 210, 255, 0.2);
border-radius: 2px;
}
.feed-item {
background: rgba(0, 0, 0, 0.15);
border: 1px solid rgba(255, 255, 255, 0.03);
border-radius: 12px;
padding: 15px;
display: flex;
flex-direction: column;
gap: 6px;
transition: all 0.3s ease;
}
.feed-item:hover {
border-color: rgba(0, 210, 255, 0.1);
background: rgba(0, 0, 0, 0.25);
}
.feed-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.feed-name {
font-size: 0.9rem;
font-weight: 800;
color: white;
}
.feed-amt {
font-size: 0.85rem;
font-weight: 700;
color: #ff6b6b;
}
.feed-msg {
font-size: 0.85rem;
color: #94a3b8;
margin: 0;
line-height: 1.4;
font-style: italic;
}
.feed-time {
font-size: 0.75rem;
color: #475569;
align-self: flex-end;
}
.empty-feed {
padding: 40px;
text-align: center;
color: #475569;
font-size: 0.9rem;
font-weight: 600;
}
/* Success Modal */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(2, 6, 17, 0.8);
backdrop-filter: blur(8px);
z-index: 110;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.modal-content {
background: radial-gradient(circle at center, #061530 0%, #030814 100%);
border: 1px solid rgba(0, 210, 255, 0.25);
max-width: 500px;
width: 100%;
border-radius: 24px;
padding: 40px;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
box-shadow: 0 20px 50px rgba(0, 210, 255, 0.3);
}
.success-big-icon {
width: 70px;
height: 70px;
color: var(--primary-color);
margin-bottom: 20px;
animation: scale-up-bounce 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
@keyframes scale-up-bounce {
0% { transform: scale(0.3); opacity: 0; }
70% { transform: scale(1.1); }
100% { transform: scale(1); opacity: 1; }
}
.modal-title {
font-size: 1.6rem;
font-weight: 800;
color: white;
margin: 0 0 10px 0;
}
.modal-desc {
font-size: 0.95rem;
color: #94a3b8;
line-height: 1.6;
margin: 0 0 30px 0;
}
.btn-modal-close {
width: 120px;
}
/* Transitions */
.list-enter-active,
.list-leave-active {
transition: all 0.5s ease;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateY(-30px);
}
/* Responsive */
@media (max-width: 992px) {
.donation-grid {
grid-template-columns: 1fr;
}
}
</style>
+585
View File
@@ -0,0 +1,585 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { Anchor, Users, Fish, Trash2, HelpCircle, ChevronRight, Waves } from 'lucide-vue-next'
const router = useRouter()
const stats = ref({
totalDonors: 12543,
totalAmount: 627150000,
releasedFish: 4200000,
collectedWaste: 185000,
purifiedArea: 95000
})
onMounted(async () => {
try {
// Dynamic import to handle non-Wails preview mode gracefully
// @ts-ignore
const CampaignService = await import('../../bindings/changeme/campaignservice')
const res = await CampaignService.GetStats()
if (res) {
stats.value = res
}
} catch (e) {
console.warn('Wails bindings not available, using mock statistics.', e)
}
})
const formatNumber = (num: number) => {
return new Intl.NumberFormat('ko-KR').format(num)
}
const formatKoreanAmount = (num: number) => {
if (num >= 100000000) {
const eok = Math.floor(num / 100000000)
const man = Math.floor((num % 100000000) / 10000)
return `${eok}${man > 0 ? man + '만' : ''}`
}
return formatNumber(num) + '원'
}
</script>
<template>
<div class="home-container">
<!-- Hero Section -->
<section class="hero-section">
<div class="ocean-bg">
<div class="wave wave1"></div>
<div class="wave wave2"></div>
<div class="wave wave3"></div>
<div class="light-ray"></div>
<div class="bubbles">
<div v-for="n in 15" :key="n" class="bubble" :style="`--i:${n}; --s:${Math.random() * 1.5 + 0.5}`"></div>
</div>
</div>
<div class="hero-content">
<span class="hero-badge"><Waves class="badge-icon" /> 수협중앙회 공익 캠페인</span>
<h1 class="hero-title">
생명의 바다,<br />
<span class="highlight">희망을 품다</span>
</h1>
<p class="hero-desc">
바다는 어업인의 일터이자 인류 생명의 젖줄입니다.<br />
수협중앙회 '희망의 바다 만들기 운동' 함께 깨끗하고 풍요로운 바다 생태계를 미래 세대에게 물려주세요.
</p>
<div class="hero-actions">
<button class="btn btn-primary" @click="router.push('/donation')">
<span>캠페인 동참하기</span>
<HeartHandshake class="btn-icon" />
</button>
<button class="btn btn-secondary" @click="router.push('/about')">
<span>자세히 알아보기</span>
<ChevronRight class="btn-icon" />
</button>
</div>
</div>
</section>
<!-- Stats Dashboard Section -->
<section class="stats-section">
<div class="section-header">
<h2 class="section-title">희망의 바다 누적 성과</h2>
<p class="section-subtitle">2007년부터 수협중앙회와 전국의 어업인이 흘려 가꿔온 생명의 기록입니다.</p>
</div>
<div class="stats-grid">
<!-- Card 1: Donors -->
<div class="stat-card">
<div class="stat-icon-wrapper donor-icon">
<Users class="stat-icon" />
</div>
<div class="stat-info">
<span class="stat-label">누적 참여 후원자</span>
<span class="stat-number">{{ formatNumber(stats.totalDonors) }} <span class="stat-unit"></span></span>
</div>
<div class="stat-progress">
<div class="progress-bar" style="width: 82%"></div>
</div>
<p class="stat-desc">많은 시민과 어업인이 한마음으로 동참하고 있습니다.</p>
</div>
<!-- Card 2: Released Fish -->
<div class="stat-card">
<div class="stat-icon-wrapper fish-icon">
<Fish class="stat-icon" />
</div>
<div class="stat-info">
<span class="stat-label">방류된 수산종자</span>
<span class="stat-number">{{ formatNumber(stats.releasedFish) }} <span class="stat-unit">마리</span></span>
</div>
<div class="stat-progress">
<div class="progress-bar" style="width: 75%"></div>
</div>
<p class="stat-desc">조피볼락, 감성돔 우수 수산종자를 바다로 돌려보냈습니다.</p>
</div>
<!-- Card 3: Waste -->
<div class="stat-card">
<div class="stat-icon-wrapper waste-icon">
<Trash2 class="stat-icon" />
</div>
<div class="stat-info">
<span class="stat-label">수거된 해양 쓰레기</span>
<span class="stat-number">{{ formatNumber(stats.collectedWaste) }} <span class="stat-unit">kg</span></span>
</div>
<div class="stat-progress">
<div class="progress-bar" style="width: 90%"></div>
</div>
<p class="stat-desc">조업 인양 폐기물과 침적 폐어구를 집중 정화했습니다.</p>
</div>
<!-- Card 4: Purified Area -->
<div class="stat-card">
<div class="stat-icon-wrapper area-icon">
<Anchor class="stat-icon" />
</div>
<div class="stat-info">
<span class="stat-label">정화된 해역 면적</span>
<span class="stat-number">{{ formatNumber(stats.purifiedArea) }} <span class="stat-unit"></span></span>
</div>
<div class="stat-progress">
<div class="progress-bar" style="width: 68%"></div>
</div>
<p class="stat-desc">갯녹음 해역 복원 바닥갈이 사업을 진행했습니다.</p>
</div>
</div>
<!-- General Info Alert -->
<div class="total-donation-badge">
<span class="badge-label">희망의 바다 조성 기금 누적액</span>
<span class="badge-amount">{{ formatKoreanAmount(stats.totalAmount) }}</span>
</div>
</section>
<!-- Call to action Section -->
<section class="cta-section">
<div class="cta-card">
<h3 class="cta-title">어업인과 국민이 함께 해양 생태계를 살립니다</h3>
<p class="cta-desc">
풍요로운 어장 복원과 맑은 바다 환경 조성은 우리 모두의 의무입니다. <br />
당신의 작은 실천이 푸른 바다의 희망찬 미래를 만들어 냅니다.
</p>
<button class="btn btn-primary" @click="router.push('/donation')">지금 바로 후원하기</button>
</div>
</section>
</div>
</template>
<style scoped>
.home-container {
display: flex;
flex-direction: column;
gap: 80px;
width: 100%;
max-width: 1200px;
margin: 0 auto;
}
/* Hero Section */
.hero-section {
position: relative;
display: flex;
align-items: center;
min-height: 550px;
padding: 40px;
border-radius: 24px;
overflow: hidden;
border: 1px solid var(--glass-border);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
}
.ocean-bg {
position: absolute;
inset: 0;
background: linear-gradient(180deg, #021a3a 0%, #030814 100%);
z-index: 1;
overflow: hidden;
}
/* Waves animation */
.wave {
position: absolute;
bottom: 0;
left: 0;
width: 200%;
height: 100px;
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1200 120' preserveAspectRatio='none'%3E%3Cpath d='M321.39,56.44c58-10.79,114.16-30.13,172-41.86,82.39-16.72,168.19-17.73,250.45-.39C823.78,31,906.67,72,985.66,92.83c70.05,18.48,146.53,26.09,214.34,3V120H0V0C26.9,8.75,55.05,18.06,83.4,26.8,145.42,46,212.41,60.11,321.39,56.44Z' fill='%2300d2ff' fill-opacity='0.08'/%3E%3C/svg%3E") repeat-x;
transform: translate3d(0, 0, 0);
}
.wave1 {
animation: wave 20s linear infinite;
z-index: 2;
}
.wave2 {
animation: wave 12s linear infinite reverse;
opacity: 0.5;
z-index: 3;
bottom: 10px;
}
.wave3 {
animation: wave 8s linear infinite;
opacity: 0.2;
z-index: 4;
bottom: 20px;
}
@keyframes wave {
0% { transform: translateX(0); }
100% { transform: translateX(-50%); }
}
/* Underwater Light Ray */
.light-ray {
position: absolute;
top: 0;
left: 30%;
width: 40%;
height: 100%;
background: linear-gradient(135deg, rgba(0, 210, 255, 0.15) 0%, transparent 70%);
transform: skewX(-25deg);
filter: blur(40px);
pointer-events: none;
animation: shimmer 6s ease-in-out infinite alternate;
}
@keyframes shimmer {
0% { opacity: 0.5; transform: skewX(-20deg) translateX(-10px); }
100% { opacity: 1; transform: skewX(-30deg) translateX(10px); }
}
/* Bubbles */
.bubble {
position: absolute;
bottom: -20px;
background: rgba(0, 210, 255, 0.2);
border: 1px solid rgba(255, 255, 255, 0.4);
border-radius: 50%;
width: calc(var(--s) * 15px);
height: calc(var(--s) * 15px);
left: calc(var(--i) * 6.5%);
animation: bubble-up calc(10s / var(--s)) linear infinite;
animation-delay: calc(var(--i) * 0.4s);
}
@keyframes bubble-up {
0% {
transform: translateY(0) scale(1) translateX(0);
opacity: 0;
}
10% {
opacity: 0.6;
}
90% {
opacity: 0.6;
}
100% {
transform: translateY(-600px) scale(1.2) translateX(30px);
opacity: 0;
}
}
.hero-content {
position: relative;
z-index: 10;
max-width: 600px;
text-align: left;
}
.hero-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 14px;
background: rgba(0, 210, 255, 0.12);
border: 1px solid rgba(0, 210, 255, 0.3);
border-radius: 99px;
font-size: 0.85rem;
font-weight: 700;
color: var(--primary-color);
margin-bottom: 24px;
}
.badge-icon {
width: 14px;
height: 14px;
}
.hero-title {
font-size: clamp(2.2rem, 5vw, 3.4rem);
line-height: 1.2;
font-weight: 800;
color: white;
margin: 0 0 20px 0;
}
.hero-title .highlight {
background: linear-gradient(to right, #00d2ff, #0066ff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.hero-desc {
font-size: 1.05rem;
line-height: 1.6;
color: #94a3b8;
margin-bottom: 35px;
}
.hero-actions {
display: flex;
gap: 15px;
}
.btn {
display: flex;
align-items: center;
gap: 8px;
padding: 14px 28px;
border-radius: 12px;
font-weight: 700;
font-size: 1rem;
cursor: pointer;
transition: all 0.3s ease;
border: none;
}
.btn-primary {
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
color: white;
box-shadow: 0 5px 15px rgba(0, 210, 255, 0.3);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 210, 255, 0.5);
}
.btn-secondary {
background: rgba(255, 255, 255, 0.05);
color: white;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.btn-secondary:hover {
background: rgba(255, 255, 255, 0.1);
transform: translateY(-2px);
}
.btn-icon {
width: 18px;
height: 18px;
}
/* Stats Section */
.stats-section {
display: flex;
flex-direction: column;
gap: 40px;
}
.section-header {
text-align: center;
}
.section-title {
font-size: 2rem;
font-weight: 800;
margin: 0 0 10px 0;
background: linear-gradient(to right, #ffffff, #94a3b8);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.section-subtitle {
color: #64748b;
margin: 0;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 24px;
}
.stat-card {
background: rgba(6, 17, 38, 0.4);
border: 1px solid rgba(0, 210, 255, 0.08);
border-radius: 20px;
padding: 30px;
display: flex;
flex-direction: column;
align-items: flex-start;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
}
.stat-card:hover {
transform: translateY(-5px);
border-color: rgba(0, 210, 255, 0.25);
box-shadow: 0 10px 30px rgba(0, 210, 255, 0.1);
}
.stat-icon-wrapper {
padding: 12px;
border-radius: 12px;
margin-bottom: 20px;
}
.stat-icon {
width: 24px;
height: 24px;
color: white;
}
.donor-icon {
background: rgba(0, 210, 255, 0.15);
}
.fish-icon {
background: rgba(0, 102, 255, 0.15);
}
.waste-icon {
background: rgba(255, 107, 107, 0.15);
}
.area-icon {
background: rgba(168, 85, 247, 0.15);
}
.stat-info {
display: flex;
flex-direction: column;
gap: 5px;
margin-bottom: 15px;
width: 100%;
}
.stat-label {
font-size: 0.9rem;
color: #64748b;
font-weight: 600;
}
.stat-number {
font-size: 1.8rem;
font-weight: 800;
color: white;
letter-spacing: -0.5px;
}
.stat-unit {
font-size: 1rem;
color: #94a3b8;
font-weight: 500;
}
.stat-progress {
width: 100%;
height: 6px;
background: rgba(255, 255, 255, 0.05);
border-radius: 3px;
margin-bottom: 15px;
overflow: hidden;
}
.progress-bar {
height: 100%;
background: linear-gradient(to right, var(--primary-color), var(--secondary-color));
border-radius: 3px;
}
.stat-desc {
font-size: 0.85rem;
color: #94a3b8;
margin: 0;
line-height: 1.4;
}
.total-donation-badge {
align-self: center;
display: flex;
align-items: center;
gap: 15px;
padding: 16px 30px;
background: linear-gradient(135deg, rgba(6, 17, 38, 0.8), rgba(3, 8, 20, 0.8));
border: 1px solid rgba(0, 210, 255, 0.2);
border-radius: 99px;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.3);
}
.badge-label {
font-size: 0.95rem;
color: #94a3b8;
font-weight: 600;
}
.badge-amount {
font-size: 1.4rem;
font-weight: 800;
color: var(--primary-color);
text-shadow: 0 0 10px rgba(0, 210, 255, 0.4);
}
/* CTA Section */
.cta-section {
width: 100%;
}
.cta-card {
background: radial-gradient(circle at top right, rgba(0, 210, 255, 0.15) 0%, rgba(3, 8, 20, 0.8) 70%);
border: 1px solid rgba(0, 210, 255, 0.15);
border-radius: 24px;
padding: 50px 40px;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.5);
}
.cta-title {
font-size: 1.8rem;
font-weight: 800;
color: white;
margin: 0;
}
.cta-desc {
font-size: 1.05rem;
line-height: 1.6;
color: #94a3b8;
max-width: 700px;
margin: 0;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.hero-section {
padding: 30px 20px;
min-height: auto;
}
.hero-actions {
flex-direction: column;
gap: 10px;
}
.btn {
width: 100%;
justify-content: center;
}
.total-donation-badge {
flex-direction: column;
gap: 5px;
border-radius: 20px;
width: 100%;
text-align: center;
}
.cta-card {
padding: 35px 20px;
}
}
</style>
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />