웹 메인페이지 레이아웃 UI 추가

This commit is contained in:
sungro815 2026-01-08 14:35:14 +09:00
parent 95053c2e00
commit 8829bcd025
27 changed files with 3631 additions and 286 deletions

View File

@ -0,0 +1,53 @@
<?php
namespace App\Http\Controllers\Admin\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;
class AdminAuthController extends Controller
{
// 로그인 폼
public function create()
{
return view('admin.auth.login');
}
// 로그인 처리
public function store(Request $request)
{
$credentials = $request->validate([
'email' => ['required', 'email'],
'password' => ['required', 'string'],
]);
// remember 체크박스 지원 (선택)
$remember = $request->boolean('remember');
// 핵심: admin guard로 로그인 시도
if (! Auth::guard('admin')->attempt($credentials, $remember)) {
throw ValidationException::withMessages([
'email' => ['이메일 또는 비밀번호가 올바르지 않습니다.'],
]);
}
// 세션 고정 공격 방지
$request->session()->regenerate();
return redirect()->route('admin.home');
}
// 로그아웃 처리
public function destroy(Request $request)
{
Auth::guard('admin')->logout();
// 세션 무효화 + CSRF 토큰 재발급
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect()->route('admin.login');
}
}

View File

@ -14,7 +14,7 @@ return Application::configure(basePath: dirname(__DIR__))
then: function () {
Route::middleware('web')
->domain('four.syye.net')
->group(base_path('routes/site.php'));
->group(base_path('routes/web.php'));
Route::middleware('web')
->domain('shot.syye.net')

View File

@ -0,0 +1,28 @@
<?php
namespace Database\Seeders;
use App\Models\AdminUser;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;
class AdminUserSeeder extends Seeder
{
public function run(): void
{
AdminUser::updateOrCreate(
['email' => 'admin@syye.net'],
[
'password' => Hash::make('tjekdfl1234%^'),
'full_name' => '최고관리자',
'nickname' => 'superadmin',
'phone' => null,
'role' => 'super',
'status' => 'active',
'is_consult_available' => true,
'consult_types' => ['signup','login','payment','giftcard','event'],
'totp_enabled' => false,
]
);
}
}

2423
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

15
public/css/pretendard.css Normal file
View File

@ -0,0 +1,15 @@
@font-face {
font-family: "Pretendard";
font-style: normal;
font-weight: 45 920; /* variable font range */
font-display: swap;
src: url("/assets/fonts/pretendard/PretendardVariable.woff2") format("woff2");
}
:root {
--font-sans: "Pretendard", system-ui, -apple-system, "Segoe UI", Roboto, "Noto Sans KR", Arial, sans-serif;
}
body {
font-family: var(--font-sans);
}

528
resources/css/web.css Normal file
View File

@ -0,0 +1,528 @@
/* =========================================
Voucher Mall Design System
Theme: White base + Blue accent
========================================= */
:root {
/* Colors */
--color-bg-base: #FFFFFF;
--color-bg-section: #F7F8FA; /* Very light grey for separation */
--color-bg-tint: #EFF6FF; /* Blue tint for badges */
--color-text-primary: #111827;
--color-text-secondary: #6B7280;
--color-text-tertiary: #9CA3AF;
--color-text-white: #FFFFFF;
--color-border: #E5E7EB;
--color-accent-blue: #2563EB;
--color-accent-blue-hover: #1D4ED8;
--color-footer-bg: #0B1220;
--color-footer-text: #E5E7EB;
/* Semantic */
--color-danger: #EF4444;
/* Spacing & Layout */
--container-width: 1280px;
--header-height-desktop: 72px;
--header-height-scroll: 56px;
/* Radius */
--radius-card: 16px;
--radius-pill: 9999px;
--radius-sm: 8px;
/* Shadows */
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
/* Transitions */
--transition-base: 200ms ease-out;
}
/* Base & Reset */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, system-ui, Roboto, 'Helvetica Neue', 'Segoe UI', 'Apple SD Gothic Neo', 'Malgun Gothic', 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', sans-serif;
background-color: var(--color-bg-base);
color: var(--color-text-primary);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
a {
text-decoration: none;
color: inherit;
transition: color var(--transition-base);
}
ul, li {
list-style: none;
}
button {
font-family: inherit;
border: none;
background: none;
cursor: pointer;
padding: 0;
}
img {
max-width: 100%;
display: block;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
/* Typography */
h1, h2, h3, h4, h5, h6 {
font-weight: 700;
line-height: 1.2;
}
.text-h1 { font-size: 32px; line-height: 40px; }
.text-h2 { font-size: 22px; line-height: 28px; font-weight: 600; }
.text-body { font-size: 16px; }
.text-sm { font-size: 14px; }
.text-xs { font-size: 12px; }
/* Layout Utilities */
.container {
max-width: var(--container-width);
margin: 0 auto;
padding: 0 24px;
}
.grid-cols-12 {
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: 24px;
}
/* Flex Utilities */
.flex { display: flex; }
.flex-col { flex-direction: column; }
.items-center { align-items: center; }
.justify-center { justify-content: center; }
.justify-between { justify-content: space-between; }
.gap-1 { gap: 4px; }
.gap-2 { gap: 8px; }
.gap-3 { gap: 12px; }
.gap-4 { gap: 16px; }
/* Components */
/* Buttons */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-pill);
font-weight: 600;
transition: all var(--transition-base);
cursor: pointer;
}
.btn-primary {
background-color: var(--color-accent-blue);
color: white;
padding: 12px 24px;
}
.btn-primary:hover {
background-color: var(--color-accent-blue-hover);
transform: translateY(-1px);
box-shadow: var(--shadow-md);
}
.btn-ghost {
color: var(--color-text-primary);
padding: 8px 16px;
}
.btn-ghost:hover {
color: var(--color-accent-blue);
background-color: var(--color-bg-tint);
}
/* Header */
.site-header {
position: sticky;
top: 0;
z-index: 50;
background-color: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-bottom: 1px solid var(--color-border);
height: var(--header-height-desktop);
transition: height 0.3s ease;
}
.site-header.scrolled {
height: var(--header-height-scroll);
}
.nav-link {
font-size: 16px;
font-weight: 600;
color: var(--color-text-primary);
}
.nav-link:hover {
color: var(--color-accent-blue);
}
.search-bar {
position: relative;
max-width: 520px;
width: 100%;
}
.search-input {
width: 100%;
height: 44px;
padding: 0 48px 0 20px;
border: 1px solid var(--color-border);
border-radius: var(--radius-pill);
background-color: #F3F4F6;
font-size: 15px;
transition: all 0.2s;
}
.search-input:focus {
outline: none;
border-color: var(--color-accent-blue);
background-color: white;
box-shadow: 0 0 0 2px var(--color-bg-tint);
}
.search-icon {
position: absolute;
right: 16px;
top: 50%;
transform: translateY(-50%);
color: var(--color-text-secondary);
}
/* Hero Carousel */
.hero-slider {
position: relative;
overflow: hidden;
height: 400px;
background-color: var(--color-bg-section);
}
.hero-track {
display: flex;
height: 100%;
transition: transform 0.5s cubic-bezier(0.25, 1, 0.5, 1);
}
.hero-slide {
min-width: 100%;
display: flex;
align-items: center;
justify-content: center;
padding: 0 24px;
}
.hero-content {
max-width: 1000px;
width: 100%;
text-align: left;
}
.hero-title {
font-size: 40px;
font-weight: 800;
margin-bottom: 16px;
color: var(--color-text-primary);
}
.hero-desc {
font-size: 18px;
color: var(--color-text-secondary);
margin-bottom: 32px;
}
.dots-container {
position: absolute;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 8px;
}
.dot {
width: 10px;
height: 10px;
border-radius: 50%;
background-color: rgba(0,0,0,0.2);
cursor: pointer;
transition: all 0.2s;
}
.dot.active {
background-color: var(--color-accent-blue);
transform: scale(1.2);
}
/* Quick Categories */
.category-chips {
display: flex;
gap: 12px;
padding: 32px 0;
overflow-x: auto;
scrollbar-width: none; /* Firefox */
}
.category-chips::-webkit-scrollbar {
display: none;
}
.chip {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
border: 1px solid var(--color-border);
border-radius: var(--radius-pill);
white-space: nowrap;
font-weight: 500;
font-size: 15px;
color: var(--color-text-primary);
background-color: white;
transition: all 0.2s;
}
.chip:hover {
border-color: var(--color-accent-blue);
background-color: var(--color-bg-tint);
color: var(--color-accent-blue);
}
/* Product Card */
.product-card {
border: 1px solid var(--color-border);
border-radius: var(--radius-card);
overflow: hidden;
background: white;
transition: all 0.2s ease-out;
height: 100%;
display: flex;
flex-direction: column;
position: relative;
cursor: pointer;
}
.product-card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-lg);
border-color: #BFDBFE;
}
.card-thumb {
width: 100%;
aspect-ratio: 1/1;
background-color: #F3F4F6;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.card-body {
padding: 16px;
flex: 1;
display: flex;
flex-direction: column;
}
.card-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 8px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
color: var(--color-text-primary);
height: 48px; /* Fixed for 2 lines */
}
.card-price-area {
margin-top: auto;
}
.price-row {
display: flex;
align-items: baseline;
gap: 8px;
}
.discount {
color: var(--color-accent-blue);
font-weight: 700;
font-size: 18px;
}
.price {
font-size: 20px;
font-weight: 700;
}
.original-price {
text-decoration: line-through;
color: var(--color-text-tertiary);
font-size: 14px;
}
.payment-badge {
display: inline-block;
font-size: 11px;
padding: 2px 8px;
background-color: var(--color-bg-tint);
color: var(--color-accent-blue);
border: 1px solid #DBEAFE;
border-radius: var(--radius-pill);
margin-top: 8px;
font-weight: 600;
}
/* Sections */
.section {
padding: 60px 0;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: flex-end;
margin-bottom: 24px;
}
.section-title h2 {
font-size: 28px;
margin-bottom: 8px;
}
.section-title p {
color: var(--color-text-secondary);
}
/* Snap Carousel (Monthly Deals) */
.snap-carousel {
display: flex;
gap: 24px;
overflow-x: auto;
scroll-snap-type: x mandatory;
padding-bottom: 24px; /* for shadow */
scrollbar-width: none;
}
.snap-carousel::-webkit-scrollbar { display: none; }
.snap-item {
scroll-snap-align: start;
flex: 0 0 280px;
}
/* Filter Tabs */
.filter-tabs {
display: flex;
gap: 8px;
justify-content: center;
margin-bottom: 32px;
}
.tab-btn {
padding: 10px 20px;
border: 1px solid var(--color-border);
border-radius: var(--radius-pill);
background: white;
color: var(--color-text-secondary);
font-weight: 500;
transition: all 0.18s;
}
.tab-btn.active {
background-color: var(--color-accent-blue);
color: white;
border-color: var(--color-accent-blue);
box-shadow: 0 2px 4px rgba(37, 99, 235, 0.2);
}
.tab-btn:hover:not(.active) {
background-color: #F9FAFB;
border-color: #D1D5DB;
}
/* Footer */
.site-footer {
background-color: var(--color-footer-bg);
color: var(--color-footer-text);
padding: 60px 0 40px;
margin-top: 80px;
}
.footer-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 40px;
}
.footer-link {
display: block;
margin-bottom: 12px;
color: #9CA3AF;
}
.footer-link:hover {
color: white;
}
/* Mobile Breakpoint */
@media (max-width: 768px) {
:root {
--header-height-desktop: 60px;
}
.grid-cols-12 {
grid-template-columns: repeat(4, 1fr);
gap: 16px;
}
.hero-title { font-size: 28px; }
.search-bar {
display: none; /* Handle mobile search differently */
}
.footer-grid {
grid-template-columns: 1fr;
gap: 32px;
}
.category-chips, .snap-carousel {
margin-right: -24px;
padding-right: 24px;
}
}

129
resources/js/web.js Normal file
View File

@ -0,0 +1,129 @@
document.addEventListener('DOMContentLoaded', () => {
// --- Hero Carousel Logic ---
const heroTrack = document.querySelector('.hero-track');
const heroSlides = document.querySelectorAll('.hero-slide');
const preventBtn = document.querySelector('.slider-arrow.prev');
const nextBtn = document.querySelector('.slider-arrow.next');
const dots = document.querySelectorAll('.dot');
let currentSlide = 0;
const totalSlides = heroSlides.length;
let autoplayInterval;
function updateCarousel() {
heroTrack.style.transform = `translateX(-${currentSlide * 100}%)`;
dots.forEach((dot, index) => {
dot.classList.toggle('active', index === currentSlide);
});
}
function nextSlide() {
currentSlide = (currentSlide + 1) % totalSlides;
updateCarousel();
}
function prevSlide() {
currentSlide = (currentSlide - 1 + totalSlides) % totalSlides;
updateCarousel();
}
// Controls
if(nextBtn) nextBtn.addEventListener('click', () => {
nextSlide();
resetAutoplay();
});
if(preventBtn) preventBtn.addEventListener('click', () => {
prevSlide();
resetAutoplay();
});
dots.forEach((dot, index) => {
dot.addEventListener('click', () => {
currentSlide = index;
updateCarousel();
resetAutoplay();
});
});
// Autoplay
function startAutoplay() {
autoplayInterval = setInterval(nextSlide, 6000);
}
function resetAutoplay() {
clearInterval(autoplayInterval);
startAutoplay();
}
// Hover pause
const heroSlider = document.querySelector('.hero-slider');
if (heroSlider) {
heroSlider.addEventListener('mouseenter', () => clearInterval(autoplayInterval));
heroSlider.addEventListener('mouseleave', startAutoplay);
startAutoplay();
}
// --- Header Scroll Effect ---
const header = document.querySelector('.site-header');
window.addEventListener('scroll', () => {
if (window.scrollY > 50) {
header.classList.add('scrolled');
} else {
header.classList.remove('scrolled');
}
});
// --- Filter Tabs Logic (Client-side simulation) ---
const tabBtns = document.querySelectorAll('.tab-btn');
const productGrid = document.querySelector('.product-grid-container');
const productItems = document.querySelectorAll('.product-item');
tabBtns.forEach(btn => {
btn.addEventListener('click', () => {
// Active State
tabBtns.forEach(b => {
b.classList.remove('active');
b.setAttribute('aria-selected', 'false');
});
btn.classList.add('active');
btn.setAttribute('aria-selected', 'true');
// Filter Logic
const filter = btn.dataset.filter;
// Animation: Fade out
productGrid.style.opacity = '0';
productGrid.style.transform = 'translateY(10px)';
productGrid.style.transition = 'all 0.2s';
setTimeout(() => {
productItems.forEach(item => {
if (filter === 'all' || item.dataset.category === filter) {
item.style.display = 'block';
} else {
item.style.display = 'none';
}
});
// Fade in
productGrid.style.opacity = '1';
productGrid.style.transform = 'translateY(0)';
}, 200);
});
});
// --- Mobile Menu Toggle ---
const mobileMenuBtn = document.querySelector('.mobile-menu-btn');
// In a real implementation, we would toggle a drawer here.
// For this prototype, we'll just log it or alert.
if(mobileMenuBtn) {
mobileMenuBtn.addEventListener('click', () => {
alert('모바일 메뉴 드로어 열림 (구현 예정)');
});
}
});

View File

@ -0,0 +1,46 @@
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>관리자 로그인</title>
</head>
<body style="font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; max-width: 360px; margin: 80px auto;">
<h2>관리자 로그인</h2>
@if ($errors->any())
<div style="background:#ffecec; border:1px solid #ffb3b3; padding:10px; margin:12px 0;">
<ul style="margin:0; padding-left:18px;">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<form method="POST" action="{{ route('admin.login.store') }}">
@csrf
<div style="margin: 12px 0;">
<label>Email</label><br>
<input type="email" name="email" value="{{ old('email') }}" required autofocus
style="width:100%; padding:10px; box-sizing:border-box;">
</div>
<div style="margin: 12px 0;">
<label>Password</label><br>
<input type="password" name="password" required
style="width:100%; padding:10px; box-sizing:border-box;">
</div>
<div style="margin: 12px 0;">
<label>
<input type="checkbox" name="remember" value="1">
로그인 유지
</label>
</div>
<button type="submit" style="width:100%; padding:10px;">로그인</button>
</form>
</body>
</html>

View File

@ -0,0 +1,20 @@
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>관리자 </title>
</head>
<body style="font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; max-width: 640px; margin: 60px auto;">
<h2>관리자 </h2>
<p>
로그인 사용자: {{ auth('admin')->user()->nickname }} ({{ auth('admin')->user()->email }})
</p>
<form method="POST" action="{{ route('admin.logout') }}">
@csrf
<button type="submit">로그아웃</button>
</form>
</body>
</html>

View File

@ -0,0 +1,38 @@
<footer class="site-footer">
<div class="container footer-grid">
<!-- Col 1: Brand -->
<div>
<div style="font-size: 20px; font-weight: 700; color: white; margin-bottom: 16px;">GIFTICON</div>
<p style="font-size: 14px; color: #9CA3AF; line-height: 1.6;">
대한민국 1 상품권 거래소.<br>
안전하고 빠른 거래를 약속드립니다.
</p>
<div style="margin-top: 24px; font-size: 12px; color: #6B7280;">
© 2024 Gifticon Corp. All rights reserved.
</div>
</div>
<!-- Col 2: Links -->
<div>
<h4 style="color: white; margin-bottom: 20px;">고객지원</h4>
<a href="#" class="footer-link">공지사항</a>
<a href="#" class="footer-link">자주 묻는 질문</a>
<a href="#" class="footer-link">1:1 문의</a>
<a href="#" class="footer-link">이용약관</a>
<a href="#" class="footer-link" style="font-weight: 600;">개인정보처리방침</a>
</div>
<!-- Col 3: Contact -->
<div>
<h4 style="color: white; margin-bottom: 20px;">Contact</h4>
<p style="margin-bottom: 8px;">고객센터: 1544-0000</p>
<p style="margin-bottom: 8px;">운영시간: 평일 09:00 - 18:00</p>
<p style="margin-bottom: 24px;">이메일: help@gifticon.com</p>
<div style="padding: 12px; background: rgba(255,255,255,0.05); border-radius: 8px;">
<p style="font-size: 12px; color: #9CA3AF; margin-bottom: 4px;">구매안전 서비스</p>
<p style="font-size: 13px; color: white;">KG Inicis 에스크로 적용됨</p>
</div>
</div>
</div>
</footer>

View File

@ -0,0 +1,64 @@
<header class="site-header">
<div class="container" style="height: 100%; display: flex; align-items: center; justify-content: space-between;">
<!-- Left: Logo & Nav -->
<div class="flex items-center gap-4">
<a href="/" class="logo"
style="font-size: 24px; font-weight: 800; color: var(--color-accent-blue); margin-right: 32px;">
GIFTICON
</a>
<nav class="desktop-nav" style="display: flex; gap: 24px;">
<a href="/" class="nav-link">Home</a>
<a href="/shop" class="nav-link">Shop</a>
<a href="/exchange" class="nav-link">상품권현금교환</a>
<a href="/mypage" class="nav-link">마이페이지</a>
<a href="/cs" class="nav-link">고객센터</a>
</nav>
</div>
<!-- Center: Search (Desktop) -->
<div class="search-bar">
<form action="/shop" method="GET">
<input type="text" name="search" class="search-input" placeholder="상품권/브랜드 검색 (예: 문상, 해피, 구글플레이)">
<button type="submit" class="search-icon" aria-label="검색">
<svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
</button>
</form>
</div>
<!-- Right: Auth Buttons -->
<div class="auth-buttons flex items-center gap-2">
<a href="/login" class="btn btn-ghost">로그인</a>
<a href="/register" class="btn btn-primary" style="padding: 8px 20px;">회원가입</a>
</div>
<!-- Mobile Menu Toggle (Hidden on Desktop) -->
<button class="mobile-menu-btn" aria-label="메뉴 열기" style="display: none;">
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16">
</path>
</svg>
</button>
</div>
</header>
<!-- Mobile Breakpoint Adjustments will be handled in CSS media queries -->
<style>
@media (max-width: 1024px) {
.desktop-nav,
.search-bar,
.auth-buttons {
display: none !important;
}
.mobile-menu-btn {
display: block !important;
}
}
</style>

View File

@ -0,0 +1,45 @@
@extends('web.layouts.layout')
@section('content')
{{-- Hero --}}
@include('web.main.hero-carousel')
{{-- Quick Categories --}}
@include('web.main.quick-categories')
{{-- Monthly Deals --}}
<section class="section" id="monthly-deals">
<div class="container">
@include('web.main.section-title', [
'title' => '이달의 할인',
'desc' => '이번 달 인기 할인 상품을 모아봤어요.'
])
@include('web.main.product-carousel')
</div>
</section>
{{-- Popular --}}
<section class="section" id="popular">
<div class="container">
@include('web.main.section-title', [
'title' => '인기상품',
'desc' => '최근 24시간 기준'
])
@include('web.main.product-row-scroll')
</div>
</section>
{{-- All Products --}}
<section class="section" id="all-products">
<div class="container">
@include('web.main.section-title', ['title' => '전체상품'])
@include('web.main.filter-tabs')
@include('web.main.product-grid')
</div>
</section>
@endsection

View File

@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Voucher Mall - 상품권 최저가 쇼핑</title>
{{-- SEO --}}
<meta name="description" content="상품권, 모바일 교환권, 구글플레이, 문화상품권 등 다양한 모바일 쿠폰 최저가 할인 쇼핑몰">
<link rel="canonical" href="{{ rtrim(config('app.url'), '/') }}/">
{{-- CSRF (추후 /로그인 대비) --}}
<meta name="csrf-token" content="{{ csrf_token() }}">
{{-- Pretendard Font --}}
<link rel="stylesheet" href="{{ asset('css/pretendard.css') }}">
<link rel="preload" href="{{ asset('assets/fonts/pretendard/PretendardVariable.woff2') }}" as="font" type="font/woff2" crossorigin>
{{-- WEB 전용 번들 로딩 --}}
@vite(['resources/css/web.css', 'resources/js/web.js'])
</head>
<body>
{{-- Header --}}
@include('web.company.header')
<main>
{{-- 페이지에서 @section('content') 여기로 들어옴 --}}
@yield('content')
</main>
{{-- Footer --}}
@include('web.company.footer')
{{-- 페이지별 스크립트 추가용 --}}
@stack('scripts')
</body>
</html>

View File

@ -0,0 +1,7 @@
<div class="filter-tabs" role="tablist">
<button class="tab-btn active" role="tab" aria-selected="true" data-filter="all">전체</button>
<button class="tab-btn" role="tab" aria-selected="false" data-filter="card">카드결제</button>
<button class="tab-btn" role="tab" aria-selected="false" data-filter="phone">핸드폰결제</button>
<button class="tab-btn" role="tab" aria-selected="false" data-filter="kbank">케이뱅크</button>
<button class="tab-btn" role="tab" aria-selected="false" data-filter="sale">할인판매</button>
</div>

View File

@ -0,0 +1,52 @@
<section class="hero-slider" aria-label="프로모션 배너">
<div class="hero-track">
<!-- Slide 1 -->
<div class="hero-slide" style="background: linear-gradient(135deg, #EFF6FF 0%, #FFFFFF 100%);">
<div class="container hero-content">
<span style="color: var(--color-accent-blue); font-weight: 700; margin-bottom: 8px; display: block;">특별
프로모션</span>
<h1 class="hero-title">구글플레이 기프트카드<br>최대 12% 즉시 할인</h1>
<p class="hero-desc">인기 게임 아이템부터 영화까지, 저렴하게 즐기세요.</p>
<a href="/shop?category=google" class="btn btn-primary">지금 구매하기</a>
</div>
</div>
<!-- Slide 2 -->
<div class="hero-slide" style="background: linear-gradient(135deg, #F0FDFA 0%, #FFFFFF 100%);">
<div class="container hero-content">
<span style="color: #059669; font-weight: 700; margin-bottom: 8px; display: block;">신규 입점</span>
<h1 class="hero-title">문화상품권 24시간<br>자동 발송 시스템 오픈</h1>
<p class="hero-desc">기다림 없이 결제 즉시 문자로 핀번호를 받아보세요.</p>
<a href="/shop?category=paper" class="btn btn-primary" style="background-color: #059669;">상품 보러가기</a>
</div>
</div>
<!-- Slide 3 -->
<div class="hero-slide" style="background: linear-gradient(135deg, #FFF7ED 0%, #FFFFFF 100%);">
<div class="container hero-content">
<span style="color: #EA580C; font-weight: 700; margin-bottom: 8px; display: block;">한정 수량</span>
<h1 class="hero-title">편의점 모바일 금액권<br>5만원권 10% 핫딜</h1>
<p class="hero-desc">CU, GS25, 세븐일레븐 전국 어디서나 사용 가능</p>
<a href="/shop?category=convenience" class="btn btn-primary" style="background-color: #EA580C;">구매하기</a>
</div>
</div>
</div>
<button class="slider-arrow prev" aria-label="이전 슬라이드"
style="position: absolute; left: 24px; top: 50%; transform: translateY(-50%); width: 40px; height: 40px; border-radius: 50%; background: white; box-shadow: var(--shadow-md); display: flex; align-items: center; justify-content: center;">
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
</svg>
</button>
<button class="slider-arrow next" aria-label="다음 슬라이드"
style="position: absolute; right: 24px; top: 50%; transform: translateY(-50%); width: 40px; height: 40px; border-radius: 50%; background: white; box-shadow: var(--shadow-md); display: flex; align-items: center; justify-content: center;">
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
</svg>
</button>
<div class="dots-container">
<!-- Script will populate docs based on slide count -->
<div class="dot active"></div>
<div class="dot"></div>
<div class="dot"></div>
</div>
</section>

View File

@ -0,0 +1,26 @@
@props(['products' => []])
<div class="snap-carousel" id="monthly-deals-carousel">
<!-- Mock Items if empty -->
@for($i = 1; $i <= 5; $i++)
<div class="snap-item">
<div class="product-card">
<div class="card-thumb">
<span style="color: #9CA3AF; font-size: 14px;">IMG</span>
</div>
<div class="card-body">
<h3 class="card-title">구글플레이 기프트카드 5만원권 특가 할인 [{{$i}}]</h3>
<div class="card-price-area">
<div class="price-row">
<span class="discount">-8%</span>
<span class="price">46,000</span>
</div>
<span class="original-price">50,000</span>
<br>
<span class="payment-badge">Card</span>
</div>
</div>
</div>
</div>
@endfor
</div>

View File

@ -0,0 +1,36 @@
<div class="grid-cols-12 product-grid-container" id="all-products-grid">
<!-- Responsive sizing: Mobile 2col (spread), Tablet 4col (span 3), Desktop 4col (span 3) -->
<style>
.col-span-3 {
grid-column: span 3;
}
@media (max-width: 768px) {
.col-span-3 {
grid-column: span 6;
}
/* 2 columns on mobile */
}
</style>
@for($i = 1; $i <= 12; $i++)
<div class="col-span-3 product-item" data-category="{{ $i % 3 == 0 ? 'card' : 'phone' }}">
<div class="product-card">
<div class="card-thumb">
<span style="color: #9CA3AF;">IMG</span>
</div>
<div class="card-body">
<h3 class="card-title">문화상품권 {{$i}}만원권 즉시발송</h3>
<div class="card-price-area">
<div class="price-row">
<span class="discount">-{{$i}}%</span>
<span class="price">{{50 - $i}},000</span>
</div>
<span class="payment-badge">{{ $i % 3 == 0 ? 'Card' : 'Phone' }}</span>
</div>
</div>
</div>
</div>
@endfor
</div>

View File

@ -0,0 +1,20 @@
<div style="overflow-x: auto; padding-bottom: 24px;">
<div style="display: flex; gap: 16px;">
@for($i = 1; $i <= 8; $i++)
<div style="flex: 0 0 200px;">
<div class="product-card">
<div class="card-thumb" style="aspect-ratio: 1/1;">
<span style="color: #9CA3AF;">Icon</span>
</div>
<div class="card-body" style="padding: 12px;">
<h3 class="card-title" style="font-size: 14px; height: 40px;">해피머니 상품권 {{$i}}만원권</h3>
<div class="card-price-area">
<span class="discount" style="font-size: 16px;">-5%</span>
<span class="price" style="font-size: 16px;">{{$i}}9,000</span>
</div>
</div>
</div>
</div>
@endfor
</div>
</div>

View File

@ -0,0 +1,23 @@
<div class="container">
<div class="category-chips">
<a href="/shop?category=googleplay" class="chip">
<img src="https://via.placeholder.com/20/2563EB/FFFFFF?text=G" alt="" style="border-radius: 4px;"> Google
Play
</a>
<a href="/shop?category=paper" class="chip">
<img src="https://via.placeholder.com/20/purple/FFFFFF?text=C" alt="" style="border-radius: 4px;"> 문상/해피/도서
</a>
<a href="/shop?category=online" class="chip">
<img src="https://via.placeholder.com/20/orange/FFFFFF?text=O" alt="" style="border-radius: 4px;"> 온라인상품권
</a>
<a href="/shop?category=game" class="chip">
<img src="https://via.placeholder.com/20/green/FFFFFF?text=G" alt="" style="border-radius: 4px;"> 게임전용
</a>
<a href="/shop?category=convenience" class="chip">
<img src="https://via.placeholder.com/20/blue/FFFFFF?text=C" alt="" style="border-radius: 4px;"> 편의점
</a>
<a href="/shop?category=etc" class="chip">
<img src="https://via.placeholder.com/20/gray/FFFFFF?text=E" alt="" style="border-radius: 4px;"> 기타
</a>
</div>
</div>

View File

@ -0,0 +1,8 @@
<div class="section-header">
<div class="section-title">
<h2 class="text-h2">{{ $title }}</h2>
@if(!empty($desc))
<p class="text-body">{{ $desc }}</p>
@endif
</div>
</div>

File diff suppressed because one or more lines are too long

View File

@ -1,9 +1,23 @@
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Admin\Auth\AdminAuthController;
Route::middleware(['auth:admin'])->group(function () {
Route::get('/', function () {
return 'admin ok';
})->name('admin.home');
Route::middleware('guest:admin')->group(function () {
Route::get('/login', [AdminAuthController::class, 'create'])
->name('admin.login');
Route::post('/login', [AdminAuthController::class, 'store'])
->name('admin.login.store');
});
Route::middleware('auth:admin')->group(function () {
Route::get('/', function () {
return view('admin.home');
})->name('admin.home');
Route::post('/logout', [AdminAuthController::class, 'destroy'])
->name('admin.logout');
});
Route::fallback(fn () => abort(404));

View File

@ -1,7 +1,10 @@
<?php
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return view('welcome');
});
return view('web.home');
})->name('web.home');
Route::view('/', 'web.home')->name('web.home');
Route::fallback(fn () => abort(404));

View File

@ -5,7 +5,12 @@ import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
plugins: [
laravel({
input: ['resources/css/app.css', 'resources/js/app.js'],
input: [
'resources/css/web.css',
'resources/js/web.js',
'resources/css/admin.css',
'resources/js/admin.js',
],
refresh: true,
}),
tailwindcss(),