Merge PR #2: 라이트 고craft 재설계 (홈·외주·제품 3면)
Deep Field 다크 → 라이트 단일 시스템 재설계. 검증 통과(test 20/20, build 86/86).
This commit is contained in:
@@ -16,9 +16,9 @@ const KOR_TIGHT = { letterSpacing: '-0.02em' } as const;
|
||||
const KOR_BODY = { letterSpacing: '-0.01em' } as const;
|
||||
|
||||
const INPUT_STYLE = {
|
||||
background: 'var(--jsm-dark-surface)',
|
||||
border: '1px solid var(--jsm-dark-line)',
|
||||
color: 'var(--jsm-dark-ink)',
|
||||
background: 'var(--jsm-surface-alt)',
|
||||
border: '1px solid var(--jsm-line)',
|
||||
color: 'var(--jsm-ink)',
|
||||
} as const;
|
||||
|
||||
const PROJECT_TYPES = [
|
||||
@@ -195,13 +195,13 @@ export default function OutsourcingRequestForm() {
|
||||
ref={setHeadingRef}
|
||||
tabIndex={-1}
|
||||
className="text-xl font-bold break-keep outline-none"
|
||||
style={{ color: 'var(--jsm-dark-ink)', ...KOR_TIGHT }}
|
||||
style={{ color: 'var(--jsm-ink)', ...KOR_TIGHT }}
|
||||
>
|
||||
의뢰가 접수되었습니다
|
||||
</h3>
|
||||
<p
|
||||
className="mt-3 text-sm leading-relaxed break-keep"
|
||||
style={{ color: 'var(--jsm-dark-soft)', ...KOR_BODY }}
|
||||
style={{ color: 'var(--jsm-ink-soft)', ...KOR_BODY }}
|
||||
>
|
||||
영업일 2일 내 회신드립니다.
|
||||
</p>
|
||||
@@ -218,7 +218,7 @@ export default function OutsourcingRequestForm() {
|
||||
</Link>
|
||||
<p
|
||||
className="mt-3 text-xs leading-relaxed break-keep"
|
||||
style={{ color: 'var(--jsm-dark-soft)', ...KOR_BODY }}
|
||||
style={{ color: 'var(--jsm-ink-soft)', ...KOR_BODY }}
|
||||
>
|
||||
추적 링크를 이메일로도 보내드렸습니다.
|
||||
</p>
|
||||
@@ -232,7 +232,7 @@ export default function OutsourcingRequestForm() {
|
||||
const canAdvance = stepValid(step);
|
||||
|
||||
return (
|
||||
<div className="jsm-dark-form">
|
||||
<div>
|
||||
{/* 진행 표시기 */}
|
||||
<ol className="flex items-center gap-2 mb-7" aria-label="진행 단계">
|
||||
{STEPS.map((s, i) => {
|
||||
@@ -244,7 +244,7 @@ export default function OutsourcingRequestForm() {
|
||||
className="flex items-center justify-center w-6 h-6 rounded-full text-xs font-bold shrink-0 transition-colors"
|
||||
style={
|
||||
state === 'upcoming'
|
||||
? { background: 'var(--jsm-dark-surface)', color: 'var(--jsm-dark-soft)', boxShadow: 'inset 0 0 0 1px var(--jsm-dark-line)' }
|
||||
? { background: 'var(--jsm-surface-alt)', color: 'var(--jsm-ink-soft)', boxShadow: 'inset 0 0 0 1px var(--jsm-line)' }
|
||||
: { background: 'var(--jsm-accent)', color: '#ffffff' }
|
||||
}
|
||||
aria-current={state === 'current' ? 'step' : undefined}
|
||||
@@ -255,7 +255,7 @@ export default function OutsourcingRequestForm() {
|
||||
className="text-xs font-semibold truncate hidden sm:inline"
|
||||
style={{
|
||||
color:
|
||||
state === 'upcoming' ? 'var(--jsm-dark-soft)' : 'var(--jsm-dark-ink)',
|
||||
state === 'upcoming' ? 'var(--jsm-ink-soft)' : 'var(--jsm-ink)',
|
||||
...KOR_BODY,
|
||||
}}
|
||||
>
|
||||
@@ -264,7 +264,7 @@ export default function OutsourcingRequestForm() {
|
||||
{i < STEPS.length - 1 && (
|
||||
<span
|
||||
className="w-4 sm:w-6 h-px shrink-0"
|
||||
style={{ background: 'var(--jsm-dark-line)' }}
|
||||
style={{ background: 'var(--jsm-line)' }}
|
||||
aria-hidden
|
||||
/>
|
||||
)}
|
||||
@@ -281,13 +281,13 @@ export default function OutsourcingRequestForm() {
|
||||
ref={setHeadingRef}
|
||||
tabIndex={-1}
|
||||
className="text-lg font-bold break-keep outline-none mb-1"
|
||||
style={{ color: 'var(--jsm-dark-ink)', ...KOR_TIGHT }}
|
||||
style={{ color: 'var(--jsm-ink)', ...KOR_TIGHT }}
|
||||
>
|
||||
어떤 프로젝트인가요?
|
||||
</legend>
|
||||
<p
|
||||
className="text-sm leading-relaxed break-keep mb-5"
|
||||
style={{ color: 'var(--jsm-dark-soft)', ...KOR_BODY }}
|
||||
style={{ color: 'var(--jsm-ink-soft)', ...KOR_BODY }}
|
||||
>
|
||||
가장 가까운 유형을 하나 선택해주세요.
|
||||
</p>
|
||||
@@ -303,12 +303,12 @@ export default function OutsourcingRequestForm() {
|
||||
className="px-4 py-3.5 rounded-lg text-sm font-semibold text-center break-keep transition-colors outline-none focus-visible:ring-2 focus-visible:ring-[var(--jsm-accent)]"
|
||||
style={{
|
||||
border: selected
|
||||
? '1px solid var(--jsm-accent-bright)'
|
||||
: '1px solid var(--jsm-dark-line)',
|
||||
? '1px solid var(--jsm-accent)'
|
||||
: '1px solid var(--jsm-line)',
|
||||
background: selected
|
||||
? 'rgba(96,165,250,0.12)'
|
||||
: 'var(--jsm-dark-surface)',
|
||||
color: selected ? 'var(--jsm-accent-bright)' : 'var(--jsm-dark-ink)',
|
||||
? 'var(--jsm-accent-soft)'
|
||||
: 'var(--jsm-surface-alt)',
|
||||
color: selected ? 'var(--jsm-accent)' : 'var(--jsm-ink)',
|
||||
...KOR_BODY,
|
||||
}}
|
||||
>
|
||||
@@ -327,13 +327,13 @@ export default function OutsourcingRequestForm() {
|
||||
ref={setHeadingRef}
|
||||
tabIndex={-1}
|
||||
className="text-lg font-bold break-keep outline-none mb-1"
|
||||
style={{ color: 'var(--jsm-dark-ink)', ...KOR_TIGHT }}
|
||||
style={{ color: 'var(--jsm-ink)', ...KOR_TIGHT }}
|
||||
>
|
||||
예산과 일정을 알려주세요
|
||||
</h3>
|
||||
<p
|
||||
className="text-sm leading-relaxed break-keep mb-5"
|
||||
style={{ color: 'var(--jsm-dark-soft)', ...KOR_BODY }}
|
||||
style={{ color: 'var(--jsm-ink-soft)', ...KOR_BODY }}
|
||||
>
|
||||
대략적인 범위면 충분합니다. 정해지지 않았다면 미정을 선택하세요.
|
||||
</p>
|
||||
@@ -341,7 +341,7 @@ export default function OutsourcingRequestForm() {
|
||||
<fieldset className="mb-6">
|
||||
<legend
|
||||
className="text-sm font-semibold mb-2.5"
|
||||
style={{ color: 'var(--jsm-dark-ink)', ...KOR_BODY }}
|
||||
style={{ color: 'var(--jsm-ink)', ...KOR_BODY }}
|
||||
>
|
||||
예산
|
||||
</legend>
|
||||
@@ -360,7 +360,7 @@ export default function OutsourcingRequestForm() {
|
||||
<fieldset>
|
||||
<legend
|
||||
className="text-sm font-semibold mb-2.5"
|
||||
style={{ color: 'var(--jsm-dark-ink)', ...KOR_BODY }}
|
||||
style={{ color: 'var(--jsm-ink)', ...KOR_BODY }}
|
||||
>
|
||||
희망 일정
|
||||
</legend>
|
||||
@@ -385,13 +385,13 @@ export default function OutsourcingRequestForm() {
|
||||
ref={setHeadingRef}
|
||||
tabIndex={-1}
|
||||
className="text-lg font-bold break-keep outline-none mb-1"
|
||||
style={{ color: 'var(--jsm-dark-ink)', ...KOR_TIGHT }}
|
||||
style={{ color: 'var(--jsm-ink)', ...KOR_TIGHT }}
|
||||
>
|
||||
자세히 들려주세요
|
||||
</h3>
|
||||
<p
|
||||
className="text-sm leading-relaxed break-keep mb-5"
|
||||
style={{ color: 'var(--jsm-dark-soft)', ...KOR_BODY }}
|
||||
style={{ color: 'var(--jsm-ink-soft)', ...KOR_BODY }}
|
||||
>
|
||||
구체적일수록 정확한 견적이 가능합니다. 최소 10자 이상 작성해주세요.
|
||||
</p>
|
||||
@@ -413,7 +413,7 @@ export default function OutsourcingRequestForm() {
|
||||
/>
|
||||
<p
|
||||
className="mt-1.5 text-xs"
|
||||
style={{ color: 'var(--jsm-dark-soft)', ...KOR_BODY }}
|
||||
style={{ color: 'var(--jsm-ink-soft)', ...KOR_BODY }}
|
||||
>
|
||||
{trimmedMessage.length}/10자 이상
|
||||
</p>
|
||||
@@ -427,13 +427,13 @@ export default function OutsourcingRequestForm() {
|
||||
ref={setHeadingRef}
|
||||
tabIndex={-1}
|
||||
className="text-lg font-bold break-keep outline-none mb-1"
|
||||
style={{ color: 'var(--jsm-dark-ink)', ...KOR_TIGHT }}
|
||||
style={{ color: 'var(--jsm-ink)', ...KOR_TIGHT }}
|
||||
>
|
||||
어디로 회신드릴까요?
|
||||
</h3>
|
||||
<p
|
||||
className="text-sm leading-relaxed break-keep mb-5"
|
||||
style={{ color: 'var(--jsm-dark-soft)', ...KOR_BODY }}
|
||||
style={{ color: 'var(--jsm-ink-soft)', ...KOR_BODY }}
|
||||
>
|
||||
영업일 2일 내에 회신드립니다.
|
||||
</p>
|
||||
@@ -443,7 +443,7 @@ export default function OutsourcingRequestForm() {
|
||||
<label
|
||||
htmlFor="req-name"
|
||||
className="block text-sm font-medium mb-1.5"
|
||||
style={{ color: 'var(--jsm-dark-ink)', ...KOR_BODY }}
|
||||
style={{ color: 'var(--jsm-ink)', ...KOR_BODY }}
|
||||
>
|
||||
이름 <span style={{ color: 'var(--jsm-accent)' }}>*</span>
|
||||
</label>
|
||||
@@ -465,7 +465,7 @@ export default function OutsourcingRequestForm() {
|
||||
<label
|
||||
htmlFor="req-email"
|
||||
className="block text-sm font-medium mb-1.5"
|
||||
style={{ color: 'var(--jsm-dark-ink)', ...KOR_BODY }}
|
||||
style={{ color: 'var(--jsm-ink)', ...KOR_BODY }}
|
||||
>
|
||||
이메일 <span style={{ color: 'var(--jsm-accent)' }}>*</span>
|
||||
</label>
|
||||
@@ -487,7 +487,7 @@ export default function OutsourcingRequestForm() {
|
||||
<label
|
||||
htmlFor="req-phone"
|
||||
className="block text-sm font-medium mb-1.5"
|
||||
style={{ color: 'var(--jsm-dark-ink)', ...KOR_BODY }}
|
||||
style={{ color: 'var(--jsm-ink)', ...KOR_BODY }}
|
||||
>
|
||||
연락처
|
||||
</label>
|
||||
@@ -530,10 +530,10 @@ export default function OutsourcingRequestForm() {
|
||||
type="button"
|
||||
onClick={goPrev}
|
||||
disabled={submitting}
|
||||
className="px-5 py-3 rounded-lg text-sm font-semibold border transition-colors hover:bg-[var(--jsm-dark-surface)] disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
className="px-5 py-3 rounded-lg text-sm font-semibold border transition-colors hover:bg-[var(--jsm-surface-alt)] disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
style={{
|
||||
...INPUT_STYLE,
|
||||
borderColor: 'var(--jsm-dark-line)',
|
||||
borderColor: 'var(--jsm-line)',
|
||||
...KOR_BODY,
|
||||
}}
|
||||
>
|
||||
@@ -548,7 +548,7 @@ export default function OutsourcingRequestForm() {
|
||||
className="flex-1 py-3 rounded-lg text-sm font-semibold text-white transition-colors"
|
||||
style={{
|
||||
background: !canAdvance || submitting
|
||||
? 'var(--jsm-dark-line)'
|
||||
? 'var(--jsm-line)'
|
||||
: 'var(--jsm-accent)',
|
||||
cursor: !canAdvance || submitting ? 'not-allowed' : 'pointer',
|
||||
...KOR_BODY,
|
||||
@@ -563,7 +563,7 @@ export default function OutsourcingRequestForm() {
|
||||
disabled={!canAdvance}
|
||||
className="flex-1 inline-flex items-center justify-center gap-2 py-3 rounded-lg text-sm font-semibold text-white transition-colors"
|
||||
style={{
|
||||
background: canAdvance ? 'var(--jsm-accent)' : 'var(--jsm-dark-line)',
|
||||
background: canAdvance ? 'var(--jsm-accent)' : 'var(--jsm-line)',
|
||||
cursor: canAdvance ? 'pointer' : 'not-allowed',
|
||||
...KOR_BODY,
|
||||
}}
|
||||
@@ -595,9 +595,9 @@ function Chip({
|
||||
aria-pressed={selected}
|
||||
className="px-4 py-2.5 rounded-lg text-sm font-semibold break-keep transition-colors outline-none focus-visible:ring-2 focus-visible:ring-[var(--jsm-accent)]"
|
||||
style={{
|
||||
border: selected ? '1px solid var(--jsm-accent-bright)' : '1px solid var(--jsm-dark-line)',
|
||||
background: selected ? 'rgba(96,165,250,0.12)' : 'var(--jsm-dark-surface)',
|
||||
color: selected ? 'var(--jsm-accent-bright)' : 'var(--jsm-dark-ink)',
|
||||
border: selected ? '1px solid var(--jsm-accent)' : '1px solid var(--jsm-line)',
|
||||
background: selected ? 'var(--jsm-accent-soft)' : 'var(--jsm-surface-alt)',
|
||||
color: selected ? 'var(--jsm-accent)' : 'var(--jsm-ink)',
|
||||
...KOR_BODY,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -65,17 +65,13 @@ export default function TopNav() {
|
||||
return () => window.removeEventListener('keydown', onKey);
|
||||
}, [open]);
|
||||
|
||||
// 다크 라우트 판정
|
||||
const DARK_ROUTES = ['/', '/outsourcing'];
|
||||
const isDark = DARK_ROUTES.includes(pathname) || pathname.startsWith('/outsourcing/');
|
||||
|
||||
// 팔레트 헬퍼 — isDark 분기
|
||||
const ink = isDark ? 'var(--jsm-dark-ink)' : 'var(--jsm-ink)';
|
||||
const inkSoft = isDark ? 'var(--jsm-dark-soft)' : 'var(--jsm-ink-soft)';
|
||||
const surface = isDark ? 'var(--jsm-dark-bg)' : 'var(--jsm-surface)';
|
||||
const line = isDark ? 'var(--jsm-dark-line)' : 'var(--jsm-line)';
|
||||
const accent = isDark ? 'var(--jsm-accent-bright)' : 'var(--jsm-accent)';
|
||||
const accentBg = isDark ? 'rgba(96,165,250,0.12)' : 'var(--jsm-accent-soft)';
|
||||
// 단일 라이트 팔레트 (전 라우트 동일 — 라우트 분기 제거)
|
||||
const ink = 'var(--jsm-ink)';
|
||||
const inkSoft = 'var(--jsm-ink-soft)';
|
||||
const surface = 'var(--jsm-surface)';
|
||||
const line = 'var(--jsm-line)';
|
||||
const accent = 'var(--jsm-accent)';
|
||||
const accentBg = 'var(--jsm-accent-soft)';
|
||||
|
||||
const isActive = (href: string) => {
|
||||
if (href === '/') return pathname === '/';
|
||||
@@ -87,13 +83,11 @@ export default function TopNav() {
|
||||
<header
|
||||
className="fixed top-0 left-0 right-0 z-50 w-full transition-all duration-300"
|
||||
style={{
|
||||
background: scrolled
|
||||
? (isDark ? 'rgba(7,13,26,0.85)' : 'var(--jsm-surface)')
|
||||
: 'transparent',
|
||||
background: scrolled ? 'var(--jsm-surface)' : 'transparent',
|
||||
borderBottom: scrolled
|
||||
? `1px solid ${line}`
|
||||
: '1px solid transparent',
|
||||
boxShadow: scrolled && !isDark ? '0 1px 8px rgba(15,23,42,0.06)' : 'none',
|
||||
boxShadow: scrolled ? '0 1px 8px rgba(15,23,42,0.06)' : 'none',
|
||||
}}
|
||||
>
|
||||
<nav className="max-w-7xl mx-auto flex w-full items-center justify-between h-16 px-6 lg:px-8">
|
||||
|
||||
@@ -14,10 +14,13 @@ interface Props {
|
||||
export default function ScrollReveal({ children, delay = 0, variant = 'fade-up', className }: Props) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [shown, setShown] = useState(false);
|
||||
// reduced-motion: transition까지 생략하고 정적으로 표시
|
||||
const [instant, setInstant] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// reduced-motion: 즉시 표시 (연출 생략)
|
||||
// reduced-motion: 즉시 표시 (연출·전환 생략)
|
||||
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
||||
setInstant(true);
|
||||
setShown(true);
|
||||
return;
|
||||
}
|
||||
@@ -46,6 +49,15 @@ export default function ScrollReveal({ children, delay = 0, variant = 'fade-up',
|
||||
variant === 'fade' ? 'opacity-100' :
|
||||
'opacity-100 translate-y-0';
|
||||
|
||||
// reduced-motion이면 transition/transform 없이 정적 표시
|
||||
if (instant) {
|
||||
return (
|
||||
<div ref={ref} className={className}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import type { ShowcaseSlot } from '@/lib/showcase';
|
||||
import MockWindow from '@/app/components/mock/MockWindow';
|
||||
import { MOCK_REGISTRY } from '@/app/components/mock/registry';
|
||||
|
||||
interface Props {
|
||||
slot: ShowcaseSlot;
|
||||
@@ -11,316 +10,58 @@ interface Props {
|
||||
index: number;
|
||||
}
|
||||
|
||||
// ───────────────────────── 시드 PRNG (결정적) ─────────────────────────
|
||||
|
||||
/** slug → 32bit 정수 시드 (cyrb-ish 해시) */
|
||||
function hashSlug(s: string): number {
|
||||
let h = 2166136261 >>> 0;
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
h ^= s.charCodeAt(i);
|
||||
h = Math.imul(h, 16777619);
|
||||
}
|
||||
return h >>> 0;
|
||||
}
|
||||
|
||||
/** mulberry32 — 시드 하나로 결정적 난수열 생성 */
|
||||
function mulberry32(seed: number): () => number {
|
||||
let a = seed >>> 0;
|
||||
return () => {
|
||||
a |= 0;
|
||||
a = (a + 0x6d2b79f5) | 0;
|
||||
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
||||
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||||
};
|
||||
}
|
||||
|
||||
/** #rrggbb → {r,g,b} */
|
||||
function hexToRgb(hex: string): { r: number; g: number; b: number } {
|
||||
const h = hex.replace('#', '');
|
||||
return {
|
||||
r: parseInt(h.slice(0, 2), 16),
|
||||
g: parseInt(h.slice(2, 4), 16),
|
||||
b: parseInt(h.slice(4, 6), 16),
|
||||
};
|
||||
}
|
||||
|
||||
// ───────────────────────── 패턴 3종 (정적 텍스처) ─────────────────────────
|
||||
|
||||
type RGB = { r: number; g: number; b: number };
|
||||
|
||||
/** 1. 등고선 — accent 동심 곡선 흐름 */
|
||||
function drawContour(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
w: number,
|
||||
h: number,
|
||||
rng: () => number,
|
||||
c: RGB,
|
||||
) {
|
||||
const cx = w * (0.3 + rng() * 0.4);
|
||||
const cy = h * (0.3 + rng() * 0.4);
|
||||
const rings = 9 + Math.floor(rng() * 5);
|
||||
const step = Math.max(w, h) / rings;
|
||||
const wobble = 0.12 + rng() * 0.1;
|
||||
const phase = rng() * Math.PI * 2;
|
||||
ctx.lineWidth = 1.25;
|
||||
for (let i = 1; i <= rings; i++) {
|
||||
const baseR = i * step;
|
||||
const alpha = 0.1 + (1 - i / rings) * 0.08; // 0.10~0.18
|
||||
ctx.strokeStyle = `rgba(${c.r},${c.g},${c.b},${alpha})`;
|
||||
ctx.beginPath();
|
||||
const segs = 72;
|
||||
for (let s = 0; s <= segs; s++) {
|
||||
const a = (s / segs) * Math.PI * 2;
|
||||
const r = baseR * (1 + Math.sin(a * 3 + phase + i * 0.6) * wobble);
|
||||
const x = cx + Math.cos(a) * r;
|
||||
const y = cy + Math.sin(a) * r * 0.82;
|
||||
if (s === 0) ctx.moveTo(x, y);
|
||||
else ctx.lineTo(x, y);
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
/** 2. 격자 왜곡 — 미세하게 휘어진 그리드 라인 */
|
||||
function drawGrid(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
w: number,
|
||||
h: number,
|
||||
rng: () => number,
|
||||
c: RGB,
|
||||
) {
|
||||
const cols = 7 + Math.floor(rng() * 4);
|
||||
const rows = 5 + Math.floor(rng() * 4);
|
||||
const amp = 6 + rng() * 10;
|
||||
const fx = 1.5 + rng() * 2;
|
||||
const fy = 1.5 + rng() * 2;
|
||||
const phase = rng() * Math.PI * 2;
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeStyle = `rgba(${c.r},${c.g},${c.b},0.14)`;
|
||||
// 세로선
|
||||
for (let i = 0; i <= cols; i++) {
|
||||
const baseX = (i / cols) * w;
|
||||
ctx.beginPath();
|
||||
for (let j = 0; j <= 40; j++) {
|
||||
const ny = j / 40;
|
||||
const y = ny * h;
|
||||
const x = baseX + Math.sin(ny * Math.PI * fy + phase + i * 0.4) * amp;
|
||||
if (j === 0) ctx.moveTo(x, y);
|
||||
else ctx.lineTo(x, y);
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
// 가로선
|
||||
for (let i = 0; i <= rows; i++) {
|
||||
const baseY = (i / rows) * h;
|
||||
ctx.beginPath();
|
||||
for (let j = 0; j <= 40; j++) {
|
||||
const nx = j / 40;
|
||||
const x = nx * w;
|
||||
const y = baseY + Math.cos(nx * Math.PI * fx + phase + i * 0.4) * amp;
|
||||
if (j === 0) ctx.moveTo(x, y);
|
||||
else ctx.lineTo(x, y);
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
/** 3. 도트 필드 — 밀도 그라데이션 도트 */
|
||||
function drawDots(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
w: number,
|
||||
h: number,
|
||||
rng: () => number,
|
||||
c: RGB,
|
||||
) {
|
||||
const gap = 16 + rng() * 8;
|
||||
const ox = rng() * Math.PI * 2;
|
||||
const oy = rng() * Math.PI * 2;
|
||||
// 밀도 중심 (가장 진한 지점)
|
||||
const dcx = w * (0.2 + rng() * 0.6);
|
||||
const dcy = h * (0.2 + rng() * 0.6);
|
||||
const maxD = Math.hypot(w, h);
|
||||
for (let y = gap * 0.5; y < h; y += gap) {
|
||||
for (let x = gap * 0.5; x < w; x += gap) {
|
||||
const jx = Math.sin((y / gap) * 1.3 + ox) * 2.5;
|
||||
const jy = Math.cos((x / gap) * 1.3 + oy) * 2.5;
|
||||
const px = x + jx;
|
||||
const py = y + jy;
|
||||
const d = Math.hypot(px - dcx, py - dcy) / maxD; // 0~~1
|
||||
const density = 1 - d; // 중심부 1
|
||||
const alpha = 0.06 + density * 0.16; // 0.06~0.22
|
||||
const radius = 0.9 + density * 1.8;
|
||||
ctx.fillStyle = `rgba(${c.r},${c.g},${c.b},${alpha.toFixed(3)})`;
|
||||
ctx.beginPath();
|
||||
ctx.arc(px, py, radius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const PATTERNS = [drawContour, drawGrid, drawDots];
|
||||
|
||||
// ───────────────────────── 컴포넌트 ─────────────────────────
|
||||
|
||||
export default function ShowcaseCard({ slot, size = 'standard', index }: Props) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const wrapRef = useRef<HTMLDivElement>(null);
|
||||
const [hovered, setHovered] = useState(false);
|
||||
|
||||
// 슬러그 시드 — 결정적 패턴 선택/파라미터
|
||||
const seed = hashSlug(slot.slug);
|
||||
const patternType = seed % PATTERNS.length;
|
||||
|
||||
// 캔버스 1회 정적 렌더 (DPR 반영, 애니메이션 루프 없음)
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
|
||||
const draw = () => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const w = rect.width;
|
||||
const h = rect.height;
|
||||
if (w === 0 || h === 0) return;
|
||||
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
||||
canvas.width = Math.round(w * dpr);
|
||||
canvas.height = Math.round(h * dpr);
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
const rng = mulberry32(seed);
|
||||
const c = hexToRgb(slot.accent);
|
||||
PATTERNS[patternType](ctx, w, h, rng, c);
|
||||
};
|
||||
|
||||
draw();
|
||||
|
||||
// 컨테이너 리사이즈 시 재렌더 (정적 — 루프 아님)
|
||||
const ro = new ResizeObserver(() => draw());
|
||||
ro.observe(canvas);
|
||||
return () => ro.disconnect();
|
||||
}, [seed, patternType, slot.accent]);
|
||||
|
||||
// 호버 시차 — 리스너/rAF는 hover 중에만 가동 (상시 rAF 금지)
|
||||
useEffect(() => {
|
||||
if (!hovered) return;
|
||||
if (typeof window === 'undefined') return;
|
||||
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
|
||||
const canvas = canvasRef.current;
|
||||
const wrap = wrapRef.current;
|
||||
if (!canvas || !wrap) return;
|
||||
|
||||
let rafId = 0;
|
||||
let tx = 0;
|
||||
let ty = 0;
|
||||
|
||||
const apply = () => {
|
||||
rafId = 0;
|
||||
canvas.style.transform = `translate(${tx.toFixed(2)}px, ${ty.toFixed(2)}px)`;
|
||||
};
|
||||
|
||||
const onMove = (e: MouseEvent) => {
|
||||
const rect = wrap.getBoundingClientRect();
|
||||
const nx = (e.clientX - rect.left) / rect.width - 0.5; // -0.5~0.5
|
||||
const ny = (e.clientY - rect.top) / rect.height - 0.5;
|
||||
tx = nx * 12; // ±6px
|
||||
ty = ny * 12;
|
||||
if (!rafId) rafId = requestAnimationFrame(apply);
|
||||
};
|
||||
|
||||
wrap.addEventListener('mousemove', onMove);
|
||||
return () => {
|
||||
wrap.removeEventListener('mousemove', onMove);
|
||||
if (rafId) cancelAnimationFrame(rafId);
|
||||
canvas.style.transform = '';
|
||||
};
|
||||
}, [hovered]);
|
||||
|
||||
// 라이트 쇼케이스 카드 — surface-alt 스테이지 위에 흰 MockWindow가 떠 있는 "framed screen".
|
||||
// 서버 컴포넌트 (캔버스/시드/그래디언트 전량 제거).
|
||||
export default function ShowcaseCard({ slot, size = 'standard' }: Props) {
|
||||
const Mock = MOCK_REGISTRY[slot.mock];
|
||||
const isFeature = size === 'feature';
|
||||
const isLink = Boolean(slot.href);
|
||||
|
||||
// 타일 본체 (링크/div 공통)
|
||||
const tile = (
|
||||
const body = (
|
||||
<div
|
||||
ref={wrapRef}
|
||||
data-index={index}
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
className={[
|
||||
'group/card relative isolate h-full w-full overflow-hidden rounded-2xl',
|
||||
'transition-[transform,box-shadow] duration-500',
|
||||
'group/card flex h-full flex-col rounded-2xl border p-5 lg:p-6',
|
||||
'transition-[transform,box-shadow,border-color] duration-300',
|
||||
'[transition-timing-function:cubic-bezier(0.16,1,0.3,1)]',
|
||||
'motion-safe:hover:scale-[1.03]',
|
||||
isLink ? 'cursor-pointer' : 'cursor-default',
|
||||
isFeature ? 'aspect-[16/10]' : 'aspect-[4/3]',
|
||||
'motion-safe:hover:-translate-y-1 hover:shadow-[0_24px_60px_-32px_rgba(15,23,42,0.4)]',
|
||||
isLink ? 'cursor-pointer' : '',
|
||||
].join(' ')}
|
||||
style={
|
||||
{
|
||||
'--card-accent': slot.accent,
|
||||
backgroundImage: `linear-gradient(135deg, ${slot.palette[0]}, ${slot.palette[1]})`,
|
||||
// 기본 보더는 다크 라인, hover 시 accent 점등 + 코너 글로우 (인라인 hover는 className으로)
|
||||
boxShadow: hovered
|
||||
? `0 0 0 1px ${slot.accent}, 0 18px 50px -20px ${slot.accent}66, inset 0 0 60px -30px ${slot.accent}80`
|
||||
: '0 0 0 1px var(--jsm-dark-line, rgba(148,163,184,0.14))',
|
||||
} as React.CSSProperties
|
||||
}
|
||||
style={{ background: 'var(--jsm-surface-alt)', borderColor: 'var(--jsm-line)' }}
|
||||
>
|
||||
{/* 제너러티브 텍스처 (정적) */}
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute inset-0 h-full w-full will-change-transform"
|
||||
/>
|
||||
<MockWindow title={`${slot.slug}.app`} className="group-hover/card:border-[var(--jsm-accent-soft)]">
|
||||
<Mock />
|
||||
</MockWindow>
|
||||
|
||||
{/* 하단 스크림 — 텍스트 가독성 */}
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute inset-x-0 bottom-0 h-2/3"
|
||||
style={{
|
||||
background:
|
||||
'linear-gradient(to top, rgba(7,13,26,0.92) 0%, rgba(7,13,26,0.55) 45%, transparent 100%)',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 텍스트 레이어 */}
|
||||
<div className="absolute inset-x-0 bottom-0 flex flex-col gap-1.5 p-5 sm:p-6">
|
||||
<div className="mt-5">
|
||||
<span
|
||||
className="font-mono text-[11px] uppercase tracking-[0.18em]"
|
||||
style={{ color: slot.accent }}
|
||||
style={{ color: 'var(--jsm-accent)' }}
|
||||
>
|
||||
{slot.label}
|
||||
</span>
|
||||
<h3
|
||||
className={[
|
||||
'font-bold leading-snug [word-break:keep-all]',
|
||||
isFeature ? 'text-xl sm:text-2xl' : 'text-lg',
|
||||
'mt-1.5 font-bold [word-break:keep-all]',
|
||||
isFeature ? 'text-xl' : 'text-lg',
|
||||
].join(' ')}
|
||||
style={{ color: 'var(--jsm-dark-ink, #f8fafc)' }}
|
||||
style={{ color: 'var(--jsm-ink)', letterSpacing: '-0.02em' }}
|
||||
>
|
||||
{slot.title}
|
||||
</h3>
|
||||
<p
|
||||
className="line-clamp-1 text-sm [word-break:keep-all]"
|
||||
style={{ color: 'var(--jsm-dark-soft, #94a3b8)' }}
|
||||
className="mt-1.5 text-sm leading-relaxed [word-break:keep-all]"
|
||||
style={{ color: 'var(--jsm-ink-soft)', letterSpacing: '-0.01em' }}
|
||||
>
|
||||
{slot.desc}
|
||||
</p>
|
||||
|
||||
{isLink && (
|
||||
<span
|
||||
className="mt-1 inline-flex items-center gap-1.5 text-[13px] font-medium transition-transform duration-500 [transition-timing-function:cubic-bezier(0.16,1,0.3,1)] group-hover/card:translate-x-1"
|
||||
style={{ color: slot.accent }}
|
||||
className="mt-3 inline-flex items-center gap-1.5 text-[13px] font-semibold transition-transform duration-300 group-hover/card:translate-x-1"
|
||||
style={{ color: 'var(--jsm-accent)' }}
|
||||
>
|
||||
데모 보기
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
aria-hidden="true"
|
||||
className="transition-transform duration-500 [transition-timing-function:cubic-bezier(0.16,1,0.3,1)] group-hover/card:translate-x-0.5"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden>
|
||||
<path
|
||||
d="M5 12h14M13 6l6 6-6 6"
|
||||
stroke="currentColor"
|
||||
@@ -337,11 +78,11 @@ export default function ShowcaseCard({ slot, size = 'standard', index }: Props)
|
||||
|
||||
if (isLink) {
|
||||
return (
|
||||
<Link href={slot.href!} aria-label={slot.title} className="block h-full w-full">
|
||||
{tile}
|
||||
<Link href={slot.href!} aria-label={slot.title} className="block h-full">
|
||||
{body}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return tile;
|
||||
return body;
|
||||
}
|
||||
|
||||
51
app/components/mock/MockWindow.tsx
Normal file
51
app/components/mock/MockWindow.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
// 라이트 UI 목업의 공용 크롬 프레임 (서버 컴포넌트).
|
||||
// 실데이터 없이 "운영 중인 화면" 인상을 주는 craft 요소. --jsm-* 토큰만 사용.
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
interface MockWindowProps {
|
||||
/** 타이틀바 텍스트 — 파일/서비스명 느낌 (예: 'stock-report', 'realestate-match') */
|
||||
title: string;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function MockWindow({ title, children, className }: MockWindowProps) {
|
||||
return (
|
||||
<div
|
||||
className={`overflow-hidden rounded-xl border shadow-[0_24px_60px_-30px_rgba(15,23,42,0.35)] ${className ?? ''}`}
|
||||
style={{ background: 'var(--jsm-surface)', borderColor: 'var(--jsm-line)' }}
|
||||
>
|
||||
{/* 타이틀바 — 신호등 + 모노 파일명 + 라이브 점 */}
|
||||
<div
|
||||
className="flex items-center gap-2 border-b px-3.5 py-2.5"
|
||||
style={{ background: 'var(--jsm-surface-alt)', borderColor: 'var(--jsm-line)' }}
|
||||
>
|
||||
<span className="flex gap-1.5" aria-hidden>
|
||||
<span className="h-2.5 w-2.5 rounded-full" style={{ background: '#cbd5e1' }} />
|
||||
<span className="h-2.5 w-2.5 rounded-full" style={{ background: '#d8e0ea' }} />
|
||||
<span className="h-2.5 w-2.5 rounded-full" style={{ background: '#e2e8f0' }} />
|
||||
</span>
|
||||
<span
|
||||
className="ml-1 font-mono text-[11px]"
|
||||
style={{ color: 'var(--jsm-ink-faint)', letterSpacing: '-0.01em' }}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
<span className="ml-auto flex items-center gap-1.5" aria-hidden>
|
||||
<span
|
||||
className="h-1.5 w-1.5 rounded-full"
|
||||
style={{ background: 'var(--jsm-accent)' }}
|
||||
/>
|
||||
<span
|
||||
className="font-mono text-[10px] uppercase tracking-[0.14em]"
|
||||
style={{ color: 'var(--jsm-ink-faint)' }}
|
||||
>
|
||||
live
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
{/* 본문 슬롯 */}
|
||||
<div className="p-4">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
17
app/components/mock/keys.ts
Normal file
17
app/components/mock/keys.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
// 목업 키 — JSX를 끌어오지 않는 순수 모듈 (vitest/showcase가 안전하게 참조).
|
||||
export type MockKey =
|
||||
| 'dashboard'
|
||||
| 'feed'
|
||||
| 'match'
|
||||
| 'commerce'
|
||||
| 'site'
|
||||
| 'booking';
|
||||
|
||||
export const MOCK_KEYS: MockKey[] = [
|
||||
'dashboard',
|
||||
'feed',
|
||||
'match',
|
||||
'commerce',
|
||||
'site',
|
||||
'booking',
|
||||
];
|
||||
24
app/components/mock/registry.ts
Normal file
24
app/components/mock/registry.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
// 목업 스크린 레지스트리 — showcase 슬롯의 mock 키를 컴포넌트로 해석.
|
||||
import type { ComponentType } from 'react';
|
||||
|
||||
import type { MockKey } from './keys';
|
||||
import {
|
||||
DashboardMock,
|
||||
FeedMock,
|
||||
MatchMock,
|
||||
CommerceMock,
|
||||
SiteMock,
|
||||
BookingMock,
|
||||
} from './screens';
|
||||
|
||||
export type { MockKey } from './keys';
|
||||
export { MOCK_KEYS } from './keys';
|
||||
|
||||
export const MOCK_REGISTRY: Record<MockKey, ComponentType> = {
|
||||
dashboard: DashboardMock,
|
||||
feed: FeedMock,
|
||||
match: MatchMock,
|
||||
commerce: CommerceMock,
|
||||
site: SiteMock,
|
||||
booking: BookingMock,
|
||||
};
|
||||
250
app/components/mock/screens.tsx
Normal file
250
app/components/mock/screens.tsx
Normal file
@@ -0,0 +1,250 @@
|
||||
// 라이트 UI 목업 스크린 6종 (서버 컴포넌트, props 없음, 정적 마크업).
|
||||
// MockWindow 본문에 들어가 "운영 중인 화면" 인상을 만든다. 실데이터 0, --jsm-* 만.
|
||||
|
||||
const ACCENT = 'var(--jsm-accent)';
|
||||
const INK = 'var(--jsm-ink)';
|
||||
const SOFT = 'var(--jsm-ink-soft)';
|
||||
const FAINT = 'var(--jsm-ink-faint)';
|
||||
const LINE = 'var(--jsm-line)';
|
||||
const ALT = 'var(--jsm-surface-alt)';
|
||||
const SOFTBG = 'var(--jsm-accent-soft)';
|
||||
|
||||
/** 1. 대시보드 — 주식 리포트 톤: 스탯 3 + 막대 차트 */
|
||||
export function DashboardMock() {
|
||||
const bars = [38, 54, 30, 62, 46, 72, 58];
|
||||
return (
|
||||
<div className="space-y-3.5">
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<div className="rounded-lg p-2.5" style={{ background: ALT }}>
|
||||
<p className="font-mono text-[10px]" style={{ color: FAINT }}>
|
||||
오늘 손익
|
||||
</p>
|
||||
<p className="mt-1 text-sm font-bold" style={{ color: ACCENT, letterSpacing: '-0.02em' }}>
|
||||
+2.4%
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg p-2.5" style={{ background: ALT }}>
|
||||
<p className="font-mono text-[10px]" style={{ color: FAINT }}>
|
||||
체결
|
||||
</p>
|
||||
<p className="mt-1 text-sm font-bold" style={{ color: INK, letterSpacing: '-0.02em' }}>
|
||||
12건
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg p-2.5" style={{ background: ALT }}>
|
||||
<p className="font-mono text-[10px]" style={{ color: FAINT }}>
|
||||
승률
|
||||
</p>
|
||||
<p className="mt-1 text-sm font-bold" style={{ color: INK, letterSpacing: '-0.02em' }}>
|
||||
68%
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="flex h-20 items-end gap-1.5 rounded-lg border p-2.5"
|
||||
style={{ borderColor: LINE }}
|
||||
>
|
||||
{bars.map((h, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="flex-1 rounded-sm"
|
||||
style={{
|
||||
height: `${h}%`,
|
||||
background: i === 5 ? ACCENT : '#dbe3ee',
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** 2. 피드 — 텔레그램 봇 톤: 메시지 버블 3 */
|
||||
export function FeedMock() {
|
||||
const rows = [
|
||||
{ t: '09:01', m: '매수 체결 · 삼성전자 12,400', tag: '체결', on: true },
|
||||
{ t: '11:24', m: '목표가 도달 — 익절 알림', tag: '알림', on: false },
|
||||
{ t: '15:30', m: '일일 손익 리포트 전송 완료', tag: '리포트', on: false },
|
||||
];
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{rows.map((r) => (
|
||||
<div
|
||||
key={r.t}
|
||||
className="flex items-start gap-2.5 rounded-lg p-2.5"
|
||||
style={{ background: ALT }}
|
||||
>
|
||||
<span className="mt-0.5 font-mono text-[10px]" style={{ color: FAINT }}>
|
||||
{r.t}
|
||||
</span>
|
||||
<p className="flex-1 text-[12px] leading-snug" style={{ color: INK, letterSpacing: '-0.01em' }}>
|
||||
{r.m}
|
||||
</p>
|
||||
<span
|
||||
className="shrink-0 rounded px-1.5 py-0.5 text-[10px] font-semibold"
|
||||
style={
|
||||
r.on
|
||||
? { color: ACCENT, background: SOFTBG }
|
||||
: { color: SOFT, background: 'var(--jsm-surface)' }
|
||||
}
|
||||
>
|
||||
{r.tag}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** 3. 매칭 — 부동산 청약 톤: 필터칩 + 매칭률 리스트 3 */
|
||||
export function MatchMock() {
|
||||
const chips = ['강남구', '85㎡↑', '신축'];
|
||||
const rows = [
|
||||
{ n: '래미안 원베일리', s: '92%' },
|
||||
{ n: '디에이치 퍼스티어', s: '88%' },
|
||||
{ n: '아크로 포레스트', s: '81%' },
|
||||
];
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex gap-1.5">
|
||||
{chips.map((c, i) => (
|
||||
<span
|
||||
key={c}
|
||||
className="rounded-full px-2.5 py-1 text-[11px] font-medium"
|
||||
style={
|
||||
i === 0
|
||||
? { color: ACCENT, background: SOFTBG }
|
||||
: { color: SOFT, background: ALT }
|
||||
}
|
||||
>
|
||||
{c}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{rows.map((r) => (
|
||||
<div
|
||||
key={r.n}
|
||||
className="flex items-center justify-between rounded-lg border px-3 py-2.5"
|
||||
style={{ borderColor: LINE }}
|
||||
>
|
||||
<span className="text-[12px] font-medium" style={{ color: INK, letterSpacing: '-0.01em' }}>
|
||||
{r.n}
|
||||
</span>
|
||||
<span
|
||||
className="rounded px-1.5 py-0.5 font-mono text-[11px] font-bold"
|
||||
style={{ color: ACCENT, background: SOFTBG }}
|
||||
>
|
||||
{r.s}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** 4. 커머스 — 상품 그리드 4 + 장바구니 바 */
|
||||
export function CommerceMock() {
|
||||
const items = [
|
||||
{ p: '₩28,000' },
|
||||
{ p: '₩45,000' },
|
||||
{ p: '₩19,000' },
|
||||
{ p: '₩36,000' },
|
||||
];
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{items.map((it, i) => (
|
||||
<div key={i} className="rounded-lg border p-2" style={{ borderColor: LINE }}>
|
||||
<div className="h-9 rounded-md" style={{ background: ALT }} />
|
||||
<p className="mt-1.5 text-[11px] font-bold" style={{ color: INK, letterSpacing: '-0.02em' }}>
|
||||
{it.p}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div
|
||||
className="flex items-center justify-between rounded-lg px-3 py-2.5"
|
||||
style={{ background: INK }}
|
||||
>
|
||||
<span className="text-[11px] font-medium text-white/80">장바구니 3 · ₩128,000</span>
|
||||
<span
|
||||
className="rounded px-2 py-1 text-[11px] font-semibold"
|
||||
style={{ background: ACCENT, color: '#fff' }}
|
||||
>
|
||||
결제
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** 5. 사이트 — 기업/포트폴리오 와이어: 네비 + 헤드라인 + 카드 3 */
|
||||
export function SiteMock() {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="h-2.5 w-2.5 rounded-full" style={{ background: ACCENT }} />
|
||||
<div className="flex gap-3">
|
||||
<span className="h-1.5 w-6 rounded-full" style={{ background: LINE }} />
|
||||
<span className="h-1.5 w-6 rounded-full" style={{ background: LINE }} />
|
||||
<span className="h-1.5 w-6 rounded-full" style={{ background: LINE }} />
|
||||
</div>
|
||||
<span className="h-4 w-10 rounded" style={{ background: ALT }} />
|
||||
</div>
|
||||
<div className="space-y-1.5 py-1">
|
||||
<span className="block h-3 w-3/4 rounded" style={{ background: '#cbd5e1' }} />
|
||||
<span className="block h-3 w-1/2 rounded" style={{ background: ACCENT }} />
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{[0, 1, 2].map((i) => (
|
||||
<div key={i} className="rounded-lg border p-2" style={{ borderColor: LINE }}>
|
||||
<div className="h-6 rounded" style={{ background: ALT }} />
|
||||
<span className="mt-1.5 block h-1.5 w-full rounded-full" style={{ background: LINE }} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** 6. 예약 — 로컬 매장 톤: 주간 캘린더 + 슬롯 그리드 */
|
||||
export function BookingMock() {
|
||||
const days = ['월', '화', '수', '목', '금', '토', '일'];
|
||||
// 0=빈 1=예약됨(accent) 2=불가(alt)
|
||||
const slots = [
|
||||
1, 0, 0, 1, 0, 2, 2,
|
||||
0, 1, 0, 0, 1, 1, 2,
|
||||
0, 0, 1, 0, 0, 1, 0,
|
||||
];
|
||||
return (
|
||||
<div className="space-y-2.5">
|
||||
<div className="grid grid-cols-7 gap-1.5">
|
||||
{days.map((d) => (
|
||||
<span key={d} className="text-center font-mono text-[10px]" style={{ color: FAINT }}>
|
||||
{d}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid grid-cols-7 gap-1.5">
|
||||
{slots.map((s, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="aspect-square rounded"
|
||||
style={{
|
||||
background: s === 1 ? ACCENT : s === 2 ? ALT : 'var(--jsm-surface)',
|
||||
boxShadow: s === 0 ? `inset 0 0 0 1px ${LINE}` : undefined,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div
|
||||
className="rounded-lg py-2 text-center text-[11px] font-semibold"
|
||||
style={{ background: SOFTBG, color: ACCENT }}
|
||||
>
|
||||
예약 확정 · 금 19:00
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -49,15 +49,7 @@
|
||||
--jsm-accent-hover: #1e40af; /* blue-800 */
|
||||
--jsm-accent-soft: #dbeafe; /* blue-100 뱃지 배경 */
|
||||
|
||||
/* === Deep Field dark tokens (2026-06 랜딩 경험) — 라이트 토큰과 공존 === */
|
||||
--jsm-dark-bg: #070d1a;
|
||||
--jsm-dark-surface: rgba(255, 255, 255, 0.03);
|
||||
--jsm-dark-line: rgba(148, 163, 184, 0.14);
|
||||
--jsm-dark-ink: #f8fafc;
|
||||
--jsm-dark-soft: #94a3b8;
|
||||
--jsm-accent-bright: #60a5fa;
|
||||
|
||||
/* 기존 kx 변수 재매핑 (잔여 참조 호환용) */
|
||||
/* 기존 kx 변수 재매핑 (레거시·숨김 라우트 /packages·/work·/music 호환용) */
|
||||
--kx-surface: var(--jsm-bg);
|
||||
--kx-surface-low: var(--jsm-surface-alt);
|
||||
--kx-surface-mid: var(--jsm-surface);
|
||||
@@ -198,13 +190,6 @@ body {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 외주 의뢰 폼 — 다크 스킨 placeholder (Deep Field 재스킨) */
|
||||
.jsm-dark-form input::placeholder,
|
||||
.jsm-dark-form textarea::placeholder {
|
||||
color: var(--jsm-dark-soft);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Service card hover */
|
||||
.service-card {
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
@@ -260,20 +245,6 @@ body {
|
||||
mask-image: linear-gradient(to right, transparent, black 8%, black 92%, transparent);
|
||||
}
|
||||
|
||||
/* ─── Deep Field 히어로 스크롤 큐 (가는 세로선 안의 점 미세 바운스) ─── */
|
||||
@keyframes df-scroll-cue {
|
||||
0%, 100% { transform: translateY(0); opacity: 0.35; }
|
||||
50% { transform: translateY(8px); opacity: 1; }
|
||||
}
|
||||
.df-scroll-dot {
|
||||
animation: none;
|
||||
}
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.df-scroll-dot {
|
||||
animation: df-scroll-cue 1.8s cubic-bezier(0.16, 1, 0.3, 1) infinite;
|
||||
}
|
||||
}
|
||||
|
||||
/* Scrollbar styling */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
|
||||
@@ -2,15 +2,15 @@ import Link from 'next/link';
|
||||
import type { Metadata } from 'next';
|
||||
import OutsourcingRequestForm from '@/app/components/OutsourcingRequestForm';
|
||||
|
||||
import HeroField from '@/app/components/deepfield/HeroField';
|
||||
import ShowcaseGrid from '@/app/components/deepfield/ShowcaseGrid';
|
||||
import ScrollReveal from '@/app/components/deepfield/ScrollReveal';
|
||||
import MockWindow from '@/app/components/mock/MockWindow';
|
||||
import { FeedMock } from '@/app/components/mock/screens';
|
||||
import { SHOWCASE_SLOTS } from '@/lib/showcase';
|
||||
|
||||
// 외주 개발 의뢰 페이지 (서버 컴포넌트) — Deep Field 다크 캔버스.
|
||||
// PublicShell이 TopNav(h-16, /outsourcing 다크 인지)·푸터·main 배경(라이트)을 제공한다.
|
||||
// 이 페이지는 자기 풀-블리드 다크 배경을 소유해 main의 라이트 배경을 덮고,
|
||||
// 메인(/)과 동일한 비주얼 언어(다크 루트 div + -mt-16 hero + 섹션 border-t 리듬 + 모노 라벨 헤더)를 따른다.
|
||||
// 외주 개발 의뢰 페이지 (서버 컴포넌트) — 라이트 고craft.
|
||||
// PublicShell의 단일 라이트 셸을 따르며, 메인(/)과 동일한 비주얼 언어
|
||||
// (surface↔surface-alt 교차 + accent 모노 라벨 헤더 + 카드 스펙)를 공유한다.
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: '외주 개발',
|
||||
@@ -22,30 +22,12 @@ const KOR_TIGHT = { letterSpacing: '-0.02em' } as const;
|
||||
const KOR_BODY = { letterSpacing: '-0.01em' } as const;
|
||||
|
||||
const FIELDS = [
|
||||
{
|
||||
t: '웹 서비스 개발',
|
||||
d: '회원·결제·관리자까지, 실제로 굴러가는 서비스를 기획부터 배포까지 만들어 드립니다.',
|
||||
},
|
||||
{
|
||||
t: '웹사이트 제작',
|
||||
d: '기업 소개·포트폴리오·랜딩 페이지를 반응형·SEO까지 갖춰 제작합니다.',
|
||||
},
|
||||
{
|
||||
t: '업무 자동화',
|
||||
d: 'RPA·엑셀 집계·웹 크롤링으로 반복 업무를 사람 손에서 떼어냅니다.',
|
||||
},
|
||||
{
|
||||
t: 'API·백엔드',
|
||||
d: '데이터 모델 설계부터 인증·외부 연동까지 안정적인 서버를 구축합니다.',
|
||||
},
|
||||
{
|
||||
t: '텔레그램·디스코드 봇',
|
||||
d: '알림·명령·자동 응답 봇으로 운영과 커뮤니티 관리를 자동화합니다.',
|
||||
},
|
||||
{
|
||||
t: 'AI 연동 개발',
|
||||
d: 'LLM·생성형 AI를 업무 흐름에 붙여 초안 작성·분류·요약을 자동화합니다.',
|
||||
},
|
||||
{ t: '웹 서비스 개발', d: '회원·결제·관리자까지, 실제로 굴러가는 서비스를 기획부터 배포까지 만들어 드립니다.' },
|
||||
{ t: '웹사이트 제작', d: '기업 소개·포트폴리오·랜딩 페이지를 반응형·SEO까지 갖춰 제작합니다.' },
|
||||
{ t: '업무 자동화', d: 'RPA·엑셀 집계·웹 크롤링으로 반복 업무를 사람 손에서 떼어냅니다.' },
|
||||
{ t: 'API·백엔드', d: '데이터 모델 설계부터 인증·외부 연동까지 안정적인 서버를 구축합니다.' },
|
||||
{ t: '텔레그램·디스코드 봇', d: '알림·명령·자동 응답 봇으로 운영과 커뮤니티 관리를 자동화합니다.' },
|
||||
{ t: 'AI 연동 개발', d: 'LLM·생성형 AI를 업무 흐름에 붙여 초안 작성·분류·요약을 자동화합니다.' },
|
||||
];
|
||||
|
||||
const PROCESS = [
|
||||
@@ -57,143 +39,66 @@ const PROCESS = [
|
||||
{ n: '06', t: '무상 하자보수 30일', d: '납품 후 30일간 결함·수정을 무상으로 대응해 안정화까지 책임집니다.' },
|
||||
];
|
||||
|
||||
// 기존 work/freelance(lib/freelance-portfolio) 실사례를 다크 토큰 기준으로 재구성.
|
||||
const CASES = [
|
||||
{
|
||||
t: '주식 자동매매 시스템',
|
||||
cat: '실시간 트레이딩 · 직접 운영 중',
|
||||
live: true,
|
||||
d: '텔레그램과 연동해 실시간으로 주문을 집행하고 체결·손익 리포트를 자동 전송합니다.',
|
||||
tags: ['Python', 'Telegram Bot', '실시간 주문'],
|
||||
},
|
||||
{
|
||||
t: '부동산 청약 자동 수집·매칭',
|
||||
cat: '크롤링 · 직접 운영 중',
|
||||
live: true,
|
||||
d: '공고를 주기적으로 크롤링해 조건에 맞는 매물만 골라내고, 신규 매칭을 즉시 푸시합니다.',
|
||||
tags: ['Python', '크롤링', '조건 매칭'],
|
||||
},
|
||||
{
|
||||
t: 'AI 콘텐츠 자동화 파이프라인',
|
||||
cat: 'AI 연동 · 직접 운영 중',
|
||||
live: true,
|
||||
d: '생성부터 검수, 발행까지 사람이 개입할 지점만 남기고 전 과정을 자동으로 연결합니다.',
|
||||
tags: ['AI 연동', '검수 워크플로우', '자동 발행'],
|
||||
},
|
||||
{
|
||||
t: 'Gmail 자동화 RPA',
|
||||
cat: 'RPA · 납품 완료',
|
||||
live: false,
|
||||
d: '거래처 이메일 수신 시 자동 분류, 답장 초안 작성, 담당자 알림을 전송합니다.',
|
||||
tags: ['Python', 'Gmail API'],
|
||||
},
|
||||
{
|
||||
t: '쇼핑몰 가격 모니터링 봇',
|
||||
cat: '웹 스크래핑 · 납품 완료',
|
||||
live: false,
|
||||
d: '경쟁사 상품 가격을 매일 모니터링해 변동 시 텔레그램으로 즉시 알립니다.',
|
||||
tags: ['Python', 'Selenium', 'Telegram Bot'],
|
||||
},
|
||||
{
|
||||
t: '영업 일보 자동화 시스템',
|
||||
cat: '엑셀 자동화 · 납품 완료',
|
||||
live: false,
|
||||
d: '엑셀 데이터를 자동 집계해 일·주·월별 보고서 PDF를 생성하고 매일 09시 발송합니다.',
|
||||
tags: ['Python', 'OpenPyXL', 'ReportLab'],
|
||||
},
|
||||
{ t: '주식 자동매매 시스템', cat: '실시간 트레이딩 · 직접 운영 중', live: true, d: '텔레그램과 연동해 실시간으로 주문을 집행하고 체결·손익 리포트를 자동 전송합니다.', tags: ['Python', 'Telegram Bot', '실시간 주문'] },
|
||||
{ t: '부동산 청약 자동 수집·매칭', cat: '크롤링 · 직접 운영 중', live: true, d: '공고를 주기적으로 크롤링해 조건에 맞는 매물만 골라내고, 신규 매칭을 즉시 푸시합니다.', tags: ['Python', '크롤링', '조건 매칭'] },
|
||||
{ t: 'AI 콘텐츠 자동화 파이프라인', cat: 'AI 연동 · 직접 운영 중', live: true, d: '생성부터 검수, 발행까지 사람이 개입할 지점만 남기고 전 과정을 자동으로 연결합니다.', tags: ['AI 연동', '검수 워크플로우', '자동 발행'] },
|
||||
{ t: 'Gmail 자동화 RPA', cat: 'RPA · 납품 완료', live: false, d: '거래처 이메일 수신 시 자동 분류, 답장 초안 작성, 담당자 알림을 전송합니다.', tags: ['Python', 'Gmail API'] },
|
||||
{ t: '쇼핑몰 가격 모니터링 봇', cat: '웹 스크래핑 · 납품 완료', live: false, d: '경쟁사 상품 가격을 매일 모니터링해 변동 시 텔레그램으로 즉시 알립니다.', tags: ['Python', 'Selenium', 'Telegram Bot'] },
|
||||
{ t: '영업 일보 자동화 시스템', cat: '엑셀 자동화 · 납품 완료', live: false, d: '엑셀 데이터를 자동 집계해 일·주·월별 보고서 PDF를 생성하고 매일 09시 발송합니다.', tags: ['Python', 'OpenPyXL', 'ReportLab'] },
|
||||
];
|
||||
|
||||
const FAQ = [
|
||||
{
|
||||
q: '견적은 어떻게 산정되나요?',
|
||||
a: '기능 범위와 구현 난이도를 기준으로 산정합니다. 상담에서 필요한 기능을 함께 정리한 뒤, 영업일 2일 내에 범위·일정·금액을 명시한 견적으로 회신드립니다. 추측으로 부풀리지 않고 실제 작업량 기준으로 잡습니다.',
|
||||
},
|
||||
{
|
||||
q: '수정 요청은 몇 번까지 가능한가요?',
|
||||
a: '합의한 범위 안에서는 2회까지 무상으로 수정해 드립니다. 범위를 벗어나는 기능 추가나 방향 전환은 별도로 협의해 진행합니다. 무엇이 범위 안/밖인지는 착수 전 견적에 미리 명시합니다.',
|
||||
},
|
||||
{
|
||||
q: '소스코드도 제공되나요?',
|
||||
a: '제공됩니다. 잔금 완납 시 전체 소스코드와 배포·실행 문서를 함께 전달합니다. 직접 운영하시거나 다른 개발자에게 이어 맡기셔도 문제없도록 인도합니다.',
|
||||
},
|
||||
{
|
||||
q: '납품 후 유지보수는요?',
|
||||
a: '납품일로부터 30일간 결함·오류를 무상으로 하자보수합니다. 이후 기능 추가나 지속 운영이 필요하면 월 단위 유지보수 계약으로 이어갈 수 있습니다.',
|
||||
},
|
||||
{ q: '견적은 어떻게 산정되나요?', a: '기능 범위와 구현 난이도를 기준으로 산정합니다. 상담에서 필요한 기능을 함께 정리한 뒤, 영업일 2일 내에 범위·일정·금액을 명시한 견적으로 회신드립니다. 추측으로 부풀리지 않고 실제 작업량 기준으로 잡습니다.' },
|
||||
{ q: '수정 요청은 몇 번까지 가능한가요?', a: '합의한 범위 안에서는 2회까지 무상으로 수정해 드립니다. 범위를 벗어나는 기능 추가나 방향 전환은 별도로 협의해 진행합니다. 무엇이 범위 안/밖인지는 착수 전 견적에 미리 명시합니다.' },
|
||||
{ q: '소스코드도 제공되나요?', a: '제공됩니다. 잔금 완납 시 전체 소스코드와 배포·실행 문서를 함께 전달합니다. 직접 운영하시거나 다른 개발자에게 이어 맡기셔도 문제없도록 인도합니다.' },
|
||||
{ q: '납품 후 유지보수는요?', a: '납품일로부터 30일간 결함·오류를 무상으로 하자보수합니다. 이후 기능 추가나 지속 운영이 필요하면 월 단위 유지보수 계약으로 이어갈 수 있습니다.' },
|
||||
];
|
||||
|
||||
function ArrowRight() {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
|
||||
<path d="M5 12h14" />
|
||||
<path d="m13 5 7 7-7 7" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function Eyebrow({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<p className="mb-3 font-mono text-[11px] uppercase tracking-[0.22em]" style={{ color: 'var(--jsm-accent)' }}>
|
||||
{children}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
export default function OutsourcingPage() {
|
||||
return (
|
||||
// 풀-블리드 다크 캔버스 — main의 라이트 배경을 덮는다.
|
||||
<div style={{ background: 'var(--jsm-dark-bg)', color: 'var(--jsm-dark-ink)' }}>
|
||||
{/* ─────────────────── 1. HERO (축약 ~60vh) ─────────────────── */}
|
||||
{/* -mt-16 pt-16: 고정 헤더 아래로 끌어올려 상단 라이트 띠 제거 */}
|
||||
<section className="relative -mt-16 flex min-h-[60vh] items-center overflow-hidden pt-16">
|
||||
<HeroField className="absolute inset-0" />
|
||||
{/* 콘텐츠 가독성용 스크림 (radial 광원 위 텍스트 대비) */}
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-0"
|
||||
style={{
|
||||
background:
|
||||
'linear-gradient(to bottom, rgba(7,13,26,0.55) 0%, transparent 30%, transparent 62%, rgba(7,13,26,0.78) 100%)',
|
||||
}}
|
||||
/>
|
||||
<div className="relative z-10 mx-auto w-full max-w-6xl px-6 py-20 lg:px-8 lg:py-24">
|
||||
<div className="max-w-3xl">
|
||||
<span
|
||||
className="mb-7 inline-flex items-center gap-2 font-mono text-[11px] uppercase tracking-[0.22em]"
|
||||
style={{ color: 'var(--jsm-accent-bright)' }}
|
||||
>
|
||||
<span
|
||||
className="inline-block h-1 w-1 rounded-full"
|
||||
style={{ background: 'var(--jsm-accent-bright)' }}
|
||||
/>
|
||||
<>
|
||||
{/* ─────────────────── 1. HERO ─────────────────── */}
|
||||
<section style={{ background: 'var(--jsm-surface)' }}>
|
||||
<div className="mx-auto grid max-w-6xl items-center gap-12 px-6 pt-20 pb-16 lg:grid-cols-2 lg:gap-16 lg:px-8 lg:pt-28 lg:pb-24">
|
||||
<div>
|
||||
<span className="inline-flex items-center gap-2 font-mono text-[11px] uppercase tracking-[0.22em]" style={{ color: 'var(--jsm-accent)' }}>
|
||||
<span className="inline-block h-1 w-1 rounded-full" style={{ background: 'var(--jsm-accent)' }} />
|
||||
outsourcing
|
||||
</span>
|
||||
<h1
|
||||
className="font-bold break-keep"
|
||||
style={{
|
||||
color: 'var(--jsm-dark-ink)',
|
||||
fontSize: 'clamp(2.4rem, 7vw, 5rem)',
|
||||
lineHeight: 1.06,
|
||||
letterSpacing: '-0.04em',
|
||||
}}
|
||||
className="mt-6 font-extrabold break-keep"
|
||||
style={{ color: 'var(--jsm-ink)', fontSize: 'clamp(2.3rem, 6vw, 3.6rem)', lineHeight: 1.1, letterSpacing: '-0.035em' }}
|
||||
>
|
||||
맞춤 소프트웨어
|
||||
<br />
|
||||
외주 개발
|
||||
<span style={{ color: 'var(--jsm-accent-bright)' }}>.</span>
|
||||
<span style={{ color: 'var(--jsm-accent)' }}>.</span>
|
||||
</h1>
|
||||
<p
|
||||
className="mt-7 max-w-2xl break-keep text-lg leading-relaxed lg:text-xl"
|
||||
style={{ color: 'var(--jsm-dark-soft)', ...KOR_BODY }}
|
||||
>
|
||||
<p className="mt-7 max-w-xl break-keep text-lg leading-relaxed" style={{ color: 'var(--jsm-ink-soft)', ...KOR_BODY }}>
|
||||
기획 정리가 안 됐어도 괜찮습니다. 상담에서 함께 정리합니다.
|
||||
</p>
|
||||
<div className="mt-10 flex flex-col gap-3 sm:flex-row">
|
||||
<div className="mt-9 flex flex-col gap-3 sm:flex-row">
|
||||
<Link
|
||||
href="#contact"
|
||||
className="inline-flex items-center justify-center gap-2 rounded-lg px-6 py-3.5 font-semibold text-white transition-transform duration-200 hover:translate-y-[-1px]"
|
||||
className="inline-flex items-center justify-center gap-2 rounded-lg px-6 py-3.5 font-semibold text-white transition-colors duration-200 hover:bg-[var(--jsm-accent-hover)]"
|
||||
style={{ background: 'var(--jsm-accent)', ...KOR_BODY }}
|
||||
>
|
||||
의뢰 내용 보내기
|
||||
@@ -201,119 +106,72 @@ export default function OutsourcingPage() {
|
||||
</Link>
|
||||
<Link
|
||||
href="#showcase"
|
||||
className="inline-flex items-center justify-center gap-2 rounded-lg border px-6 py-3.5 font-semibold transition-colors duration-200 hover:bg-[var(--jsm-dark-surface)]"
|
||||
style={{
|
||||
color: 'var(--jsm-dark-ink)',
|
||||
borderColor: 'var(--jsm-dark-line)',
|
||||
...KOR_BODY,
|
||||
}}
|
||||
className="inline-flex items-center justify-center gap-2 rounded-lg border px-6 py-3.5 font-semibold transition-colors duration-200 hover:bg-[var(--jsm-surface-alt)]"
|
||||
style={{ color: 'var(--jsm-ink)', borderColor: 'var(--jsm-line)', ...KOR_BODY }}
|
||||
>
|
||||
작업 화면 보기
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="lg:pl-4">
|
||||
<MockWindow title="telegram-bot.log">
|
||||
<FeedMock />
|
||||
</MockWindow>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ─────────────────── 2. SHOWCASE (풀 그리드) ─────────────────── */}
|
||||
<section id="showcase" className="scroll-mt-20 border-t" style={{ borderColor: 'var(--jsm-dark-line)' }}>
|
||||
{/* 하위 호환: 기존 /outsourcing#portfolio 링크(메인 footer 등)용 앵커 유지 */}
|
||||
<section id="showcase" className="scroll-mt-20" style={{ background: 'var(--jsm-surface-alt)' }}>
|
||||
{/* 하위 호환: 기존 /outsourcing#portfolio 링크 앵커 유지 */}
|
||||
<div id="portfolio" className="scroll-mt-20" />
|
||||
<div className="mx-auto max-w-6xl px-6 py-24 lg:px-8 lg:py-32">
|
||||
<div className="mx-auto max-w-6xl px-6 py-20 lg:px-8 lg:py-28">
|
||||
<ScrollReveal>
|
||||
<p
|
||||
className="mb-3 font-mono text-[11px] uppercase tracking-[0.22em]"
|
||||
style={{ color: 'var(--jsm-accent-bright)' }}
|
||||
>
|
||||
showcase
|
||||
</p>
|
||||
<h2
|
||||
className="max-w-2xl break-keep text-3xl font-bold lg:text-[2.75rem] lg:leading-[1.12]"
|
||||
style={{ color: 'var(--jsm-dark-ink)', letterSpacing: '-0.03em' }}
|
||||
>
|
||||
<Eyebrow>showcase</Eyebrow>
|
||||
<h2 className="max-w-2xl break-keep text-3xl font-bold lg:text-[2.6rem] lg:leading-[1.12]" style={{ color: 'var(--jsm-ink)', letterSpacing: '-0.03em' }}>
|
||||
우리가 만드는 화면들
|
||||
</h2>
|
||||
</ScrollReveal>
|
||||
|
||||
<div className="mt-14">
|
||||
<div className="mt-12">
|
||||
<ShowcaseGrid slots={SHOWCASE_SLOTS} variant="full" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ─────────────────── 3. 운영 실사례 ─────────────────── */}
|
||||
<section className="border-t" style={{ borderColor: 'var(--jsm-dark-line)' }}>
|
||||
<div className="mx-auto max-w-6xl px-6 py-24 lg:px-8 lg:py-32">
|
||||
<section style={{ background: 'var(--jsm-surface)' }}>
|
||||
<div className="mx-auto max-w-6xl px-6 py-20 lg:px-8 lg:py-28">
|
||||
<ScrollReveal>
|
||||
<p
|
||||
className="mb-3 font-mono text-[11px] uppercase tracking-[0.22em]"
|
||||
style={{ color: 'var(--jsm-accent-bright)' }}
|
||||
>
|
||||
in production
|
||||
</p>
|
||||
<h2
|
||||
className="max-w-2xl break-keep text-3xl font-bold lg:text-[2.75rem] lg:leading-[1.12]"
|
||||
style={{ color: 'var(--jsm-dark-ink)', letterSpacing: '-0.03em' }}
|
||||
>
|
||||
<Eyebrow>in production</Eyebrow>
|
||||
<h2 className="max-w-2xl break-keep text-3xl font-bold lg:text-[2.6rem] lg:leading-[1.12]" style={{ color: 'var(--jsm-ink)', letterSpacing: '-0.03em' }}>
|
||||
직접 개발하고, 실제로 굴러가는 결과물
|
||||
</h2>
|
||||
<p
|
||||
className="mt-4 max-w-xl break-keep leading-relaxed"
|
||||
style={{ color: 'var(--jsm-dark-soft)', ...KOR_BODY }}
|
||||
>
|
||||
<p className="mt-4 max-w-xl break-keep leading-relaxed" style={{ color: 'var(--jsm-ink-soft)', ...KOR_BODY }}>
|
||||
운영 중인 서비스와 납품 완료 프로젝트입니다. 의뢰하신 프로젝트도 같은 깊이로 만듭니다.
|
||||
</p>
|
||||
</ScrollReveal>
|
||||
|
||||
<div className="mt-14 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<div className="mt-12 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{CASES.map((c, i) => (
|
||||
<ScrollReveal key={c.t} delay={i * 80}>
|
||||
<div
|
||||
className="flex h-full flex-col rounded-2xl border p-7"
|
||||
style={{
|
||||
background: 'var(--jsm-dark-surface)',
|
||||
borderColor: 'var(--jsm-dark-line)',
|
||||
}}
|
||||
>
|
||||
<div className="flex h-full flex-col rounded-2xl border p-7" style={{ background: 'var(--jsm-surface)', borderColor: 'var(--jsm-line)' }}>
|
||||
<span
|
||||
className="mb-5 inline-flex items-center gap-1.5 self-start rounded-full px-2.5 py-1 text-[11px] font-semibold"
|
||||
style={
|
||||
c.live
|
||||
? { color: 'var(--jsm-accent-bright)', background: 'rgba(96,165,250,0.12)' }
|
||||
: { color: 'var(--jsm-dark-soft)', background: 'rgba(148,163,184,0.08)' }
|
||||
}
|
||||
style={c.live ? { color: 'var(--jsm-accent)', background: 'var(--jsm-accent-soft)' } : { color: 'var(--jsm-ink-soft)', background: 'var(--jsm-surface-alt)' }}
|
||||
>
|
||||
{c.live && (
|
||||
<span
|
||||
className="h-1.5 w-1.5 rounded-full"
|
||||
style={{ background: 'var(--jsm-accent-bright)' }}
|
||||
/>
|
||||
)}
|
||||
{c.live && <span className="h-1.5 w-1.5 rounded-full" style={{ background: 'var(--jsm-accent)' }} />}
|
||||
{c.cat}
|
||||
</span>
|
||||
<h3
|
||||
className="break-keep text-lg font-bold"
|
||||
style={{ color: 'var(--jsm-dark-ink)', ...KOR_TIGHT }}
|
||||
>
|
||||
<h3 className="break-keep text-lg font-bold" style={{ color: 'var(--jsm-ink)', ...KOR_TIGHT }}>
|
||||
{c.t}
|
||||
</h3>
|
||||
<p
|
||||
className="mt-2.5 flex-1 break-keep text-sm leading-relaxed"
|
||||
style={{ color: 'var(--jsm-dark-soft)', ...KOR_BODY }}
|
||||
>
|
||||
<p className="mt-2.5 flex-1 break-keep text-sm leading-relaxed" style={{ color: 'var(--jsm-ink-soft)', ...KOR_BODY }}>
|
||||
{c.d}
|
||||
</p>
|
||||
<div className="mt-5 flex flex-wrap gap-1.5">
|
||||
{c.tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="rounded px-2.5 py-1 text-xs"
|
||||
style={{
|
||||
color: 'var(--jsm-dark-soft)',
|
||||
background: 'rgba(148,163,184,0.08)',
|
||||
...KOR_BODY,
|
||||
}}
|
||||
>
|
||||
<span key={tag} className="rounded px-2.5 py-1 text-xs" style={{ color: 'var(--jsm-ink-soft)', background: 'var(--jsm-surface-alt)', ...KOR_BODY }}>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
@@ -326,43 +184,23 @@ export default function OutsourcingPage() {
|
||||
</section>
|
||||
|
||||
{/* ─────────────────── 4a. 제공 분야 ─────────────────── */}
|
||||
<section className="border-t" style={{ borderColor: 'var(--jsm-dark-line)' }}>
|
||||
<div className="mx-auto max-w-6xl px-6 py-24 lg:px-8 lg:py-32">
|
||||
<section style={{ background: 'var(--jsm-surface-alt)' }}>
|
||||
<div className="mx-auto max-w-6xl px-6 py-20 lg:px-8 lg:py-28">
|
||||
<ScrollReveal>
|
||||
<p
|
||||
className="mb-3 font-mono text-[11px] uppercase tracking-[0.22em]"
|
||||
style={{ color: 'var(--jsm-accent-bright)' }}
|
||||
>
|
||||
scope
|
||||
</p>
|
||||
<h2
|
||||
className="max-w-2xl break-keep text-3xl font-bold lg:text-[2.75rem] lg:leading-[1.12]"
|
||||
style={{ color: 'var(--jsm-dark-ink)', letterSpacing: '-0.03em' }}
|
||||
>
|
||||
<Eyebrow>scope</Eyebrow>
|
||||
<h2 className="max-w-2xl break-keep text-3xl font-bold lg:text-[2.6rem] lg:leading-[1.12]" style={{ color: 'var(--jsm-ink)', letterSpacing: '-0.03em' }}>
|
||||
이런 것들을 만들어 드립니다
|
||||
</h2>
|
||||
</ScrollReveal>
|
||||
|
||||
<div className="mt-14 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<div className="mt-12 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{FIELDS.map((f, i) => (
|
||||
<ScrollReveal key={f.t} delay={i * 80}>
|
||||
<div
|
||||
className="h-full rounded-2xl border p-7"
|
||||
style={{
|
||||
background: 'var(--jsm-dark-surface)',
|
||||
borderColor: 'var(--jsm-dark-line)',
|
||||
}}
|
||||
>
|
||||
<h3
|
||||
className="break-keep text-lg font-bold"
|
||||
style={{ color: 'var(--jsm-dark-ink)', ...KOR_TIGHT }}
|
||||
>
|
||||
<div className="h-full rounded-2xl border p-7" style={{ background: 'var(--jsm-surface)', borderColor: 'var(--jsm-line)' }}>
|
||||
<h3 className="break-keep text-lg font-bold" style={{ color: 'var(--jsm-ink)', ...KOR_TIGHT }}>
|
||||
{f.t}
|
||||
</h3>
|
||||
<p
|
||||
className="mt-2.5 break-keep text-sm leading-relaxed"
|
||||
style={{ color: 'var(--jsm-dark-soft)', ...KOR_BODY }}
|
||||
>
|
||||
<p className="mt-2.5 break-keep text-sm leading-relaxed" style={{ color: 'var(--jsm-ink-soft)', ...KOR_BODY }}>
|
||||
{f.d}
|
||||
</p>
|
||||
</div>
|
||||
@@ -373,53 +211,29 @@ export default function OutsourcingPage() {
|
||||
</section>
|
||||
|
||||
{/* ─────────────────── 4b. 진행 프로세스 ─────────────────── */}
|
||||
<section id="process" className="scroll-mt-20 border-t" style={{ borderColor: 'var(--jsm-dark-line)' }}>
|
||||
<div className="mx-auto max-w-6xl px-6 py-24 lg:px-8 lg:py-32">
|
||||
<section id="process" className="scroll-mt-20" style={{ background: 'var(--jsm-surface)' }}>
|
||||
<div className="mx-auto max-w-6xl px-6 py-20 lg:px-8 lg:py-28">
|
||||
<ScrollReveal>
|
||||
<p
|
||||
className="mb-3 font-mono text-[11px] uppercase tracking-[0.22em]"
|
||||
style={{ color: 'var(--jsm-accent-bright)' }}
|
||||
>
|
||||
process
|
||||
</p>
|
||||
<h2
|
||||
className="max-w-2xl break-keep text-3xl font-bold lg:text-[2.75rem] lg:leading-[1.12]"
|
||||
style={{ color: 'var(--jsm-dark-ink)', letterSpacing: '-0.03em' }}
|
||||
>
|
||||
<Eyebrow>process</Eyebrow>
|
||||
<h2 className="max-w-2xl break-keep text-3xl font-bold lg:text-[2.6rem] lg:leading-[1.12]" style={{ color: 'var(--jsm-ink)', letterSpacing: '-0.03em' }}>
|
||||
상담부터 하자보수까지, 흐름이 분명합니다
|
||||
</h2>
|
||||
</ScrollReveal>
|
||||
|
||||
<div className="mt-14 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<div className="mt-12 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{PROCESS.map((s, i) => (
|
||||
<ScrollReveal key={s.n} delay={i * 80}>
|
||||
<div
|
||||
className="relative h-full rounded-2xl border p-7 lg:p-8"
|
||||
style={{
|
||||
background: 'var(--jsm-dark-surface)',
|
||||
borderColor: 'var(--jsm-dark-line)',
|
||||
}}
|
||||
>
|
||||
<div className="relative h-full rounded-2xl border p-7 lg:p-8" style={{ background: 'var(--jsm-surface)', borderColor: 'var(--jsm-line)' }}>
|
||||
<span
|
||||
className="relative z-10 inline-flex h-12 w-12 items-center justify-center rounded-full font-mono text-sm font-bold"
|
||||
style={{
|
||||
color: 'var(--jsm-accent-bright)',
|
||||
background: 'var(--jsm-dark-bg)',
|
||||
boxShadow: 'inset 0 0 0 1px var(--jsm-dark-line)',
|
||||
}}
|
||||
style={{ color: 'var(--jsm-accent)', background: 'var(--jsm-surface)', boxShadow: 'inset 0 0 0 1px var(--jsm-line)' }}
|
||||
>
|
||||
{s.n}
|
||||
</span>
|
||||
<h3
|
||||
className="mt-5 break-keep text-lg font-bold"
|
||||
style={{ color: 'var(--jsm-dark-ink)', ...KOR_TIGHT }}
|
||||
>
|
||||
<h3 className="mt-5 break-keep text-lg font-bold" style={{ color: 'var(--jsm-ink)', ...KOR_TIGHT }}>
|
||||
{s.t}
|
||||
</h3>
|
||||
<p
|
||||
className="mt-2 break-keep text-sm leading-relaxed"
|
||||
style={{ color: 'var(--jsm-dark-soft)', ...KOR_BODY }}
|
||||
>
|
||||
<p className="mt-2 break-keep text-sm leading-relaxed" style={{ color: 'var(--jsm-ink-soft)', ...KOR_BODY }}>
|
||||
{s.d}
|
||||
</p>
|
||||
</div>
|
||||
@@ -430,57 +244,26 @@ export default function OutsourcingPage() {
|
||||
</section>
|
||||
|
||||
{/* ─────────────────── 5. FAQ ─────────────────── */}
|
||||
<section className="border-t" style={{ borderColor: 'var(--jsm-dark-line)' }}>
|
||||
<div className="mx-auto max-w-3xl px-6 py-24 lg:px-8 lg:py-32">
|
||||
<section style={{ background: 'var(--jsm-surface-alt)' }}>
|
||||
<div className="mx-auto max-w-3xl px-6 py-20 lg:px-8 lg:py-28">
|
||||
<ScrollReveal>
|
||||
<p
|
||||
className="mb-3 font-mono text-[11px] uppercase tracking-[0.22em]"
|
||||
style={{ color: 'var(--jsm-accent-bright)' }}
|
||||
>
|
||||
faq
|
||||
</p>
|
||||
<h2
|
||||
className="break-keep text-3xl font-bold lg:text-[2.75rem] lg:leading-[1.12]"
|
||||
style={{ color: 'var(--jsm-dark-ink)', letterSpacing: '-0.03em' }}
|
||||
>
|
||||
<Eyebrow>faq</Eyebrow>
|
||||
<h2 className="break-keep text-3xl font-bold lg:text-[2.6rem] lg:leading-[1.12]" style={{ color: 'var(--jsm-ink)', letterSpacing: '-0.03em' }}>
|
||||
자주 묻는 질문
|
||||
</h2>
|
||||
</ScrollReveal>
|
||||
|
||||
<div className="mt-14 space-y-3">
|
||||
<div className="mt-12 space-y-3">
|
||||
{FAQ.map((item, i) => (
|
||||
<ScrollReveal key={item.q} delay={i * 80}>
|
||||
<details
|
||||
className="group overflow-hidden rounded-2xl border"
|
||||
style={{
|
||||
background: 'var(--jsm-dark-surface)',
|
||||
borderColor: 'var(--jsm-dark-line)',
|
||||
}}
|
||||
>
|
||||
<summary
|
||||
className="flex cursor-pointer list-none items-center justify-between gap-4 break-keep px-6 py-5 font-semibold"
|
||||
style={{ color: 'var(--jsm-dark-ink)', ...KOR_TIGHT }}
|
||||
>
|
||||
<details className="group overflow-hidden rounded-2xl border" style={{ background: 'var(--jsm-surface)', borderColor: 'var(--jsm-line)' }}>
|
||||
<summary className="flex cursor-pointer list-none items-center justify-between gap-4 break-keep px-6 py-5 font-semibold" style={{ color: 'var(--jsm-ink)', ...KOR_TIGHT }}>
|
||||
{item.q}
|
||||
<svg
|
||||
className="shrink-0 transition-transform duration-200 group-open:rotate-45"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
aria-hidden
|
||||
style={{ color: 'var(--jsm-dark-soft)' }}
|
||||
>
|
||||
<svg className="shrink-0 transition-transform duration-200 group-open:rotate-45" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden style={{ color: 'var(--jsm-ink-soft)' }}>
|
||||
<path d="M12 5v14M5 12h14" />
|
||||
</svg>
|
||||
</summary>
|
||||
<p
|
||||
className="break-keep px-6 pb-5 text-sm leading-relaxed"
|
||||
style={{ color: 'var(--jsm-dark-soft)', ...KOR_BODY }}
|
||||
>
|
||||
<p className="break-keep px-6 pb-5 text-sm leading-relaxed" style={{ color: 'var(--jsm-ink-soft)', ...KOR_BODY }}>
|
||||
{item.a}
|
||||
</p>
|
||||
</details>
|
||||
@@ -491,59 +274,26 @@ export default function OutsourcingPage() {
|
||||
</section>
|
||||
|
||||
{/* ─────────────────── 6. 의뢰 폼 ─────────────────── */}
|
||||
<section id="contact" className="scroll-mt-20 border-t" style={{ borderColor: 'var(--jsm-dark-line)' }}>
|
||||
<div className="mx-auto max-w-6xl px-6 py-24 lg:px-8 lg:py-32">
|
||||
<section id="contact" className="scroll-mt-20" style={{ background: 'var(--jsm-surface)' }}>
|
||||
<div className="mx-auto max-w-6xl px-6 py-20 lg:px-8 lg:py-28">
|
||||
<div className="grid gap-10 lg:grid-cols-5 lg:gap-12">
|
||||
{/* 안내 */}
|
||||
<div className="lg:col-span-2">
|
||||
<ScrollReveal>
|
||||
<p
|
||||
className="mb-3 font-mono text-[11px] uppercase tracking-[0.22em]"
|
||||
style={{ color: 'var(--jsm-accent-bright)' }}
|
||||
>
|
||||
contact
|
||||
</p>
|
||||
<h2
|
||||
className="break-keep text-3xl font-bold leading-tight lg:text-[2.5rem]"
|
||||
style={{ color: 'var(--jsm-dark-ink)', ...KOR_TIGHT }}
|
||||
>
|
||||
<Eyebrow>contact</Eyebrow>
|
||||
<h2 className="break-keep text-3xl font-bold leading-tight lg:text-[2.4rem]" style={{ color: 'var(--jsm-ink)', ...KOR_TIGHT }}>
|
||||
프로젝트 문의
|
||||
</h2>
|
||||
<p
|
||||
className="mt-5 break-keep text-lg leading-relaxed"
|
||||
style={{ color: 'var(--jsm-dark-soft)', ...KOR_BODY }}
|
||||
>
|
||||
영업일 2일 내에 회신드립니다. 아이디어 단계여도 괜찮습니다 — 상담에서 방향을
|
||||
함께 잡아드립니다.
|
||||
<p className="mt-5 break-keep text-lg leading-relaxed" style={{ color: 'var(--jsm-ink-soft)', ...KOR_BODY }}>
|
||||
영업일 2일 내에 회신드립니다. 아이디어 단계여도 괜찮습니다 — 상담에서 방향을 함께 잡아드립니다.
|
||||
</p>
|
||||
<div
|
||||
className="mt-8 space-y-3 border-t pt-8"
|
||||
style={{ borderColor: 'var(--jsm-dark-line)' }}
|
||||
>
|
||||
<a
|
||||
href="mailto:bgg8988@gmail.com"
|
||||
className="flex items-center gap-3 text-sm transition-colors hover:text-[var(--jsm-dark-ink)]"
|
||||
style={{ color: 'var(--jsm-dark-soft)', ...KOR_BODY }}
|
||||
>
|
||||
<span
|
||||
className="w-12 font-mono text-xs uppercase tracking-wider"
|
||||
style={{ color: 'var(--jsm-accent-bright)' }}
|
||||
>
|
||||
Mail
|
||||
</span>
|
||||
<div className="mt-8 space-y-3 border-t pt-8" style={{ borderColor: 'var(--jsm-line)' }}>
|
||||
<a href="mailto:bgg8988@gmail.com" className="flex items-center gap-3 text-sm transition-colors hover:text-[var(--jsm-ink)]" style={{ color: 'var(--jsm-ink-soft)', ...KOR_BODY }}>
|
||||
<span className="w-12 font-mono text-xs uppercase tracking-wider" style={{ color: 'var(--jsm-accent)' }}>Mail</span>
|
||||
bgg8988@gmail.com
|
||||
</a>
|
||||
<a
|
||||
href="tel:010-3907-1392"
|
||||
className="flex items-center gap-3 text-sm transition-colors hover:text-[var(--jsm-dark-ink)]"
|
||||
style={{ color: 'var(--jsm-dark-soft)', ...KOR_BODY }}
|
||||
>
|
||||
<span
|
||||
className="w-12 font-mono text-xs uppercase tracking-wider"
|
||||
style={{ color: 'var(--jsm-accent-bright)' }}
|
||||
>
|
||||
Tel
|
||||
</span>
|
||||
<a href="tel:010-3907-1392" className="flex items-center gap-3 text-sm transition-colors hover:text-[var(--jsm-ink)]" style={{ color: 'var(--jsm-ink-soft)', ...KOR_BODY }}>
|
||||
<span className="w-12 font-mono text-xs uppercase tracking-wider" style={{ color: 'var(--jsm-accent)' }}>Tel</span>
|
||||
010-3907-1392
|
||||
</a>
|
||||
</div>
|
||||
@@ -553,13 +303,7 @@ export default function OutsourcingPage() {
|
||||
{/* 폼 */}
|
||||
<div className="lg:col-span-3">
|
||||
<ScrollReveal delay={100}>
|
||||
<div
|
||||
className="rounded-2xl border p-6 lg:p-8"
|
||||
style={{
|
||||
background: 'var(--jsm-dark-surface)',
|
||||
borderColor: 'var(--jsm-dark-line)',
|
||||
}}
|
||||
>
|
||||
<div className="rounded-2xl border p-6 shadow-[0_24px_60px_-32px_rgba(15,23,42,0.3)] lg:p-8" style={{ background: 'var(--jsm-surface)', borderColor: 'var(--jsm-line)' }}>
|
||||
<OutsourcingRequestForm />
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
@@ -567,6 +311,6 @@ export default function OutsourcingPage() {
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
517
app/page.tsx
517
app/page.tsx
@@ -2,23 +2,28 @@ import Link from 'next/link';
|
||||
import { createAdminClient } from '@/lib/supabase/admin';
|
||||
import { getListedProducts, type ProductRow } from '@/lib/supabase/product-files';
|
||||
|
||||
import HeroField from './components/deepfield/HeroField';
|
||||
import ShowcaseGrid from './components/deepfield/ShowcaseGrid';
|
||||
import ScrollReveal from './components/deepfield/ScrollReveal';
|
||||
import CountUp from './components/deepfield/CountUp';
|
||||
import MockWindow from './components/mock/MockWindow';
|
||||
import { DashboardMock } from './components/mock/screens';
|
||||
import { SHOWCASE_SLOTS } from '@/lib/showcase';
|
||||
|
||||
// 쟁승메이드 메인 — Deep Field 다크 캔버스 (서버 컴포넌트)
|
||||
// PublicShell이 TopNav(h-16, 다크 인지)·푸터(navy)·main 배경(라이트)을 제공한다.
|
||||
// 이 페이지는 자기 풀-블리드 다크 배경을 소유하여 main의 라이트 배경을 덮는다.
|
||||
// 히어로를 -mt-16 + pt-16으로 끌어올려 pt-16로 인한 상단 16px 라이트 띠를 제거한다.
|
||||
// 쟁승메이드 메인 — 라이트 고craft (서버 컴포넌트).
|
||||
// PublicShell이 단일 라이트 TopNav(h-16)·navy 푸터·main(라이트 --jsm-bg, pt-16)을 제공한다.
|
||||
// 섹션은 surface(#fff) ↔ surface-alt(#f1f5f9) 교차로 구분하고, 히어로의 제품 목업이 유일한 강조면.
|
||||
|
||||
// 소프트웨어 진열 섹션이 DB 조회를 포함하므로 항상 최신 목록을 보여준다.
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
const KOR_TIGHT = { letterSpacing: '-0.02em' } as const;
|
||||
const KOR_BODY = { letterSpacing: '-0.01em' } as const;
|
||||
|
||||
const TRUST = [
|
||||
{ v: '15+', t: '직접 운영 중인 실서비스' },
|
||||
{ v: '24/7', t: '무중단 운영' },
|
||||
{ v: '원스톱', t: '기획 → 배포 단독 진행' },
|
||||
];
|
||||
|
||||
const PROCESS = [
|
||||
{ n: '01', t: '무료 상담', d: '요구사항을 함께 정리하고 실현 가능성을 점검합니다.' },
|
||||
{ n: '02', t: '견적·범위 확정', d: '영업일 2일 내 범위와 견적을 정리해 회신드립니다.' },
|
||||
@@ -63,6 +68,17 @@ function ArrowRight() {
|
||||
);
|
||||
}
|
||||
|
||||
function Eyebrow({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<p
|
||||
className="mb-3 font-mono text-[11px] uppercase tracking-[0.22em]"
|
||||
style={{ color: 'var(--jsm-accent)' }}
|
||||
>
|
||||
{children}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
async function loadFeaturedProducts(): Promise<ProductRow[]> {
|
||||
try {
|
||||
const all = await getListedProducts(createAdminClient());
|
||||
@@ -78,62 +94,47 @@ export default async function Home() {
|
||||
const hasProducts = featuredProducts.length > 0;
|
||||
|
||||
return (
|
||||
// 풀-블리드 다크 캔버스 — main의 라이트 배경을 덮는다.
|
||||
<div style={{ background: 'var(--jsm-dark-bg)', color: 'var(--jsm-dark-ink)' }}>
|
||||
<>
|
||||
{/* ─────────────────── 1. HERO ─────────────────── */}
|
||||
{/* -mt-16 pt-16: 고정 헤더 아래로 끌어올려 상단 라이트 띠 제거 + 풀 뷰포트 확보 */}
|
||||
<section className="relative -mt-16 flex min-h-[100svh] items-center overflow-hidden">
|
||||
<HeroField className="absolute inset-0" />
|
||||
{/* 콘텐츠 가독성용 스크림 — 좌측 앵커 다크(텍스트 컬럼 받침) + 상하 비네트.
|
||||
텍스트는 좌측 정렬(max-w-4xl)이므로 좌→우 어둠 그라데이션으로 글자 뒤를 항상 어둡게 깔고,
|
||||
우측은 파티클 필드가 비치게 둔다. 모바일(좁은 폭)에선 좌측 스크림이 거의 전체를 덮어 가독성 확보. */}
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-0"
|
||||
style={{
|
||||
background: [
|
||||
'linear-gradient(to right, rgba(7,13,26,0.94) 0%, rgba(7,13,26,0.80) 34%, rgba(7,13,26,0.34) 64%, rgba(7,13,26,0) 100%)',
|
||||
'linear-gradient(to bottom, rgba(7,13,26,0.50) 0%, rgba(7,13,26,0) 26%, rgba(7,13,26,0) 64%, rgba(7,13,26,0.82) 100%)',
|
||||
].join(','),
|
||||
}}
|
||||
/>
|
||||
<div className="relative z-10 mx-auto w-full max-w-6xl px-6 pt-28 pb-24 lg:px-8 lg:pt-32">
|
||||
<div className="max-w-4xl">
|
||||
<section style={{ background: 'var(--jsm-surface)' }}>
|
||||
<div className="mx-auto grid max-w-6xl items-center gap-12 px-6 pt-20 pb-16 lg:grid-cols-2 lg:gap-16 lg:px-8 lg:pt-28 lg:pb-24">
|
||||
{/* 좌 — 텍스트 */}
|
||||
<div>
|
||||
<span
|
||||
className="mb-7 inline-flex items-center gap-2 font-mono text-[11px] uppercase tracking-[0.22em]"
|
||||
style={{ color: 'var(--jsm-accent-bright)' }}
|
||||
className="inline-flex items-center gap-2 font-mono text-[11px] uppercase tracking-[0.22em]"
|
||||
style={{ color: 'var(--jsm-accent)' }}
|
||||
>
|
||||
<span
|
||||
className="inline-block h-1 w-1 rounded-full"
|
||||
style={{ background: 'var(--jsm-accent-bright)' }}
|
||||
style={{ background: 'var(--jsm-accent)' }}
|
||||
/>
|
||||
외주 개발 · 완성 소프트웨어
|
||||
outsourcing · software
|
||||
</span>
|
||||
<h1
|
||||
className="font-bold break-keep"
|
||||
className="mt-6 font-extrabold break-keep"
|
||||
style={{
|
||||
color: 'var(--jsm-dark-ink)',
|
||||
fontSize: 'clamp(2.6rem, 8vw, 5.75rem)',
|
||||
lineHeight: 1.04,
|
||||
letterSpacing: '-0.04em',
|
||||
color: 'var(--jsm-ink)',
|
||||
fontSize: 'clamp(2.4rem, 7vw, 4rem)',
|
||||
lineHeight: 1.08,
|
||||
letterSpacing: '-0.035em',
|
||||
}}
|
||||
>
|
||||
생각을
|
||||
<br />
|
||||
동작하는 소프트웨어로
|
||||
<span style={{ color: 'var(--jsm-accent-bright)' }}>.</span>
|
||||
<span style={{ color: 'var(--jsm-accent)' }}>.</span>
|
||||
</h1>
|
||||
<p
|
||||
className="mt-8 max-w-2xl break-keep text-lg leading-relaxed lg:text-xl"
|
||||
style={{ color: 'var(--jsm-dark-soft)', ...KOR_BODY }}
|
||||
className="mt-7 max-w-xl break-keep text-lg leading-relaxed"
|
||||
style={{ color: 'var(--jsm-ink-soft)', ...KOR_BODY }}
|
||||
>
|
||||
24시간 돌아가는 실서비스를 직접 설계하고 운영합니다. 외주 개발도, 완성
|
||||
소프트웨어도 — 같은 손으로.
|
||||
</p>
|
||||
<div className="mt-11 flex flex-col gap-3 sm:flex-row">
|
||||
<div className="mt-9 flex flex-col gap-3 sm:flex-row">
|
||||
<Link
|
||||
href="/outsourcing#contact"
|
||||
className="inline-flex items-center justify-center gap-2 rounded-lg px-6 py-3.5 font-semibold text-white transition-transform duration-200 hover:translate-y-[-1px]"
|
||||
className="inline-flex items-center justify-center gap-2 rounded-lg px-6 py-3.5 font-semibold text-white transition-colors duration-200 hover:bg-[var(--jsm-accent-hover)]"
|
||||
style={{ background: 'var(--jsm-accent)', ...KOR_BODY }}
|
||||
>
|
||||
프로젝트 문의
|
||||
@@ -141,10 +142,10 @@ export default async function Home() {
|
||||
</Link>
|
||||
<Link
|
||||
href="/products"
|
||||
className="inline-flex items-center justify-center gap-2 rounded-lg border px-6 py-3.5 font-semibold transition-colors duration-200 hover:bg-[var(--jsm-dark-surface)]"
|
||||
className="inline-flex items-center justify-center gap-2 rounded-lg border px-6 py-3.5 font-semibold transition-colors duration-200 hover:bg-[var(--jsm-surface-alt)]"
|
||||
style={{
|
||||
color: 'var(--jsm-dark-ink)',
|
||||
borderColor: 'var(--jsm-dark-line)',
|
||||
color: 'var(--jsm-ink)',
|
||||
borderColor: 'var(--jsm-line)',
|
||||
...KOR_BODY,
|
||||
}}
|
||||
>
|
||||
@@ -152,54 +153,134 @@ export default async function Home() {
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 우 — 제품 목업 (유일한 강조면) */}
|
||||
<div className="lg:pl-4">
|
||||
<MockWindow title="stock-report.app">
|
||||
<DashboardMock />
|
||||
</MockWindow>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 스크롤 큐 — 가는 세로선 + 점 미세 바운스 (motion-safe 가드는 CSS) */}
|
||||
{/* 신뢰 스트립 */}
|
||||
<div className="mx-auto max-w-6xl px-6 pb-16 lg:px-8 lg:pb-20">
|
||||
<div
|
||||
aria-hidden
|
||||
className="absolute bottom-7 left-1/2 z-10 flex -translate-x-1/2 flex-col items-center gap-2"
|
||||
className="grid grid-cols-1 gap-px overflow-hidden rounded-2xl border sm:grid-cols-3"
|
||||
style={{ borderColor: 'var(--jsm-line)', background: 'var(--jsm-line)' }}
|
||||
>
|
||||
{TRUST.map((s) => (
|
||||
<div
|
||||
key={s.v}
|
||||
className="flex items-baseline gap-3 px-6 py-5"
|
||||
style={{ background: 'var(--jsm-surface)' }}
|
||||
>
|
||||
<span
|
||||
className="block h-9 w-px"
|
||||
style={{
|
||||
background:
|
||||
'linear-gradient(to bottom, transparent, var(--jsm-dark-line))',
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
className="df-scroll-dot block h-1.5 w-1.5 rounded-full"
|
||||
style={{ background: 'var(--jsm-accent-bright)' }}
|
||||
/>
|
||||
className="text-2xl font-bold"
|
||||
style={{ color: 'var(--jsm-accent)', letterSpacing: '-0.03em' }}
|
||||
>
|
||||
{s.v}
|
||||
</span>
|
||||
<span className="break-keep text-sm" style={{ color: 'var(--jsm-ink-soft)', ...KOR_BODY }}>
|
||||
{s.t}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ─────────────────── 2. SHOWCASE ─────────────────── */}
|
||||
<section className="border-t" style={{ borderColor: 'var(--jsm-dark-line)' }}>
|
||||
<div className="mx-auto max-w-6xl px-6 py-24 lg:px-8 lg:py-32">
|
||||
{/* ─────────────────── 2. 2축 소개 ─────────────────── */}
|
||||
<section style={{ background: 'var(--jsm-surface-alt)' }}>
|
||||
<div className="mx-auto max-w-6xl px-6 py-20 lg:px-8 lg:py-28">
|
||||
<ScrollReveal>
|
||||
<p
|
||||
className="mb-3 font-mono text-[11px] uppercase tracking-[0.22em]"
|
||||
style={{ color: 'var(--jsm-accent-bright)' }}
|
||||
>
|
||||
showcase
|
||||
</p>
|
||||
<Eyebrow>what we do</Eyebrow>
|
||||
<h2
|
||||
className="max-w-2xl break-keep text-3xl font-bold lg:text-[2.75rem] lg:leading-[1.12]"
|
||||
style={{ color: 'var(--jsm-dark-ink)', letterSpacing: '-0.03em' }}
|
||||
className="max-w-2xl break-keep text-3xl font-bold lg:text-[2.6rem] lg:leading-[1.12]"
|
||||
style={{ color: 'var(--jsm-ink)', letterSpacing: '-0.03em' }}
|
||||
>
|
||||
두 가지 방식으로 도와드립니다
|
||||
</h2>
|
||||
</ScrollReveal>
|
||||
|
||||
<div className="mt-12 grid gap-6 md:grid-cols-2">
|
||||
{[
|
||||
{
|
||||
n: '01',
|
||||
k: 'outsourcing',
|
||||
t: '맞춤 외주 개발',
|
||||
d: '웹 서비스·업무 자동화·API·봇·AI 연동까지. 기획부터 납품과 30일 하자보수까지 단독으로 책임집니다.',
|
||||
href: '/outsourcing',
|
||||
cta: '의뢰 시작',
|
||||
},
|
||||
{
|
||||
n: '02',
|
||||
k: 'software',
|
||||
t: '완성 소프트웨어 구매',
|
||||
d: '직접 운영하며 검증한 도구를 계좌이체로 가져가세요. 입금 확인 즉시 마이페이지에서 다운로드합니다.',
|
||||
href: '/products',
|
||||
cta: '제품 보기',
|
||||
},
|
||||
].map((a, i) => (
|
||||
<ScrollReveal key={a.k} delay={i * 100}>
|
||||
<Link
|
||||
href={a.href}
|
||||
className="group flex h-full flex-col rounded-2xl border p-8 transition-[transform,box-shadow,border-color] duration-300 hover:-translate-y-1 hover:border-[var(--jsm-accent)] hover:shadow-[0_24px_60px_-32px_rgba(15,23,42,0.4)] lg:p-10"
|
||||
style={{ background: 'var(--jsm-surface)', borderColor: 'var(--jsm-line)' }}
|
||||
>
|
||||
<span
|
||||
className="font-mono text-[11px] uppercase tracking-[0.18em]"
|
||||
style={{ color: 'var(--jsm-accent)' }}
|
||||
>
|
||||
{a.n} · {a.k}
|
||||
</span>
|
||||
<h3
|
||||
className="mt-4 break-keep text-xl font-bold lg:text-2xl"
|
||||
style={{ color: 'var(--jsm-ink)', ...KOR_TIGHT }}
|
||||
>
|
||||
{a.t}
|
||||
</h3>
|
||||
<p
|
||||
className="mt-3 flex-1 break-keep leading-relaxed"
|
||||
style={{ color: 'var(--jsm-ink-soft)', ...KOR_BODY }}
|
||||
>
|
||||
{a.d}
|
||||
</p>
|
||||
<span
|
||||
className="mt-6 inline-flex items-center gap-1.5 font-semibold transition-transform duration-300 group-hover:translate-x-1"
|
||||
style={{ color: 'var(--jsm-accent)', ...KOR_BODY }}
|
||||
>
|
||||
{a.cta}
|
||||
<ArrowRight />
|
||||
</span>
|
||||
</Link>
|
||||
</ScrollReveal>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ─────────────────── 3. SHOWCASE ─────────────────── */}
|
||||
<section style={{ background: 'var(--jsm-surface)' }}>
|
||||
<div className="mx-auto max-w-6xl px-6 py-20 lg:px-8 lg:py-28">
|
||||
<ScrollReveal>
|
||||
<Eyebrow>showcase</Eyebrow>
|
||||
<h2
|
||||
className="max-w-2xl break-keep text-3xl font-bold lg:text-[2.6rem] lg:leading-[1.12]"
|
||||
style={{ color: 'var(--jsm-ink)', letterSpacing: '-0.03em' }}
|
||||
>
|
||||
이런 걸 만들어 드립니다
|
||||
</h2>
|
||||
</ScrollReveal>
|
||||
|
||||
<div className="mt-14">
|
||||
<div className="mt-12">
|
||||
<ShowcaseGrid slots={SHOWCASE_SLOTS} variant="home" />
|
||||
</div>
|
||||
|
||||
<div className="mt-10 flex justify-end">
|
||||
<Link
|
||||
href="/outsourcing#showcase"
|
||||
className="inline-flex items-center gap-1.5 text-sm font-semibold transition-colors duration-150 hover:opacity-80"
|
||||
style={{ color: 'var(--jsm-accent-bright)', ...KOR_BODY }}
|
||||
className="inline-flex items-center gap-1.5 text-sm font-semibold transition-colors duration-150 hover:text-[var(--jsm-accent-hover)]"
|
||||
style={{ color: 'var(--jsm-accent)', ...KOR_BODY }}
|
||||
>
|
||||
전체 레퍼런스
|
||||
<ArrowRight />
|
||||
@@ -208,135 +289,48 @@ export default async function Home() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ─────────────────── 3. PROCESS ─────────────────── */}
|
||||
<section className="border-t" style={{ borderColor: 'var(--jsm-dark-line)' }}>
|
||||
<div className="mx-auto max-w-6xl px-6 py-24 lg:px-8 lg:py-32">
|
||||
{/* ─────────────────── 4. 운영 실증 ─────────────────── */}
|
||||
<section style={{ background: 'var(--jsm-surface-alt)' }}>
|
||||
<div className="mx-auto max-w-6xl px-6 py-20 lg:px-8 lg:py-28">
|
||||
<ScrollReveal>
|
||||
<p
|
||||
className="mb-3 font-mono text-[11px] uppercase tracking-[0.22em]"
|
||||
style={{ color: 'var(--jsm-accent-bright)' }}
|
||||
>
|
||||
process
|
||||
</p>
|
||||
<Eyebrow>in production</Eyebrow>
|
||||
<h2
|
||||
className="max-w-2xl break-keep text-3xl font-bold lg:text-[2.75rem] lg:leading-[1.12]"
|
||||
style={{ color: 'var(--jsm-dark-ink)', letterSpacing: '-0.03em' }}
|
||||
>
|
||||
상담부터 납품까지, 흐름이 분명합니다
|
||||
</h2>
|
||||
</ScrollReveal>
|
||||
|
||||
<div className="relative mt-14">
|
||||
{/* 단계 연결선 — draw 라인 (데스크톱 가로 관통) */}
|
||||
<ScrollReveal
|
||||
variant="draw"
|
||||
className="absolute left-0 right-0 top-7 hidden lg:block"
|
||||
>
|
||||
<span
|
||||
className="block h-px w-full"
|
||||
style={{
|
||||
background:
|
||||
'linear-gradient(to right, transparent, var(--jsm-dark-line) 12%, var(--jsm-dark-line) 88%, transparent)',
|
||||
}}
|
||||
/>
|
||||
</ScrollReveal>
|
||||
|
||||
<div className="grid gap-px sm:grid-cols-2 lg:grid-cols-4 lg:gap-6 lg:bg-transparent">
|
||||
{PROCESS.map((s, i) => (
|
||||
<ScrollReveal key={s.n} delay={i * 100}>
|
||||
<div
|
||||
className="relative h-full rounded-2xl border p-7 lg:p-8"
|
||||
style={{
|
||||
background: 'var(--jsm-dark-surface)',
|
||||
borderColor: 'var(--jsm-dark-line)',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="relative z-10 inline-flex h-14 w-14 items-center justify-center rounded-full font-mono text-sm font-bold"
|
||||
style={{
|
||||
color: 'var(--jsm-accent-bright)',
|
||||
background: 'var(--jsm-dark-bg)',
|
||||
boxShadow: 'inset 0 0 0 1px var(--jsm-dark-line)',
|
||||
}}
|
||||
>
|
||||
{s.n}
|
||||
</span>
|
||||
<h3
|
||||
className="mt-5 break-keep text-lg font-bold"
|
||||
style={{ color: 'var(--jsm-dark-ink)', ...KOR_TIGHT }}
|
||||
>
|
||||
{s.t}
|
||||
</h3>
|
||||
<p
|
||||
className="mt-2 break-keep text-sm leading-relaxed"
|
||||
style={{ color: 'var(--jsm-dark-soft)', ...KOR_BODY }}
|
||||
>
|
||||
{s.d}
|
||||
</p>
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ─────────────────── 4. PROOF ─────────────────── */}
|
||||
<section className="border-t" style={{ borderColor: 'var(--jsm-dark-line)' }}>
|
||||
<div className="mx-auto max-w-6xl px-6 py-24 lg:px-8 lg:py-32">
|
||||
<ScrollReveal>
|
||||
<p
|
||||
className="mb-3 font-mono text-[11px] uppercase tracking-[0.22em]"
|
||||
style={{ color: 'var(--jsm-accent-bright)' }}
|
||||
>
|
||||
in production
|
||||
</p>
|
||||
<h2
|
||||
className="max-w-2xl break-keep text-3xl font-bold lg:text-[2.75rem] lg:leading-[1.12]"
|
||||
style={{ color: 'var(--jsm-dark-ink)', letterSpacing: '-0.03em' }}
|
||||
className="max-w-2xl break-keep text-3xl font-bold lg:text-[2.6rem] lg:leading-[1.12]"
|
||||
style={{ color: 'var(--jsm-ink)', letterSpacing: '-0.03em' }}
|
||||
>
|
||||
데모가 아니라 매일 돌아가는 시스템
|
||||
</h2>
|
||||
<p
|
||||
className="mt-4 max-w-xl break-keep leading-relaxed"
|
||||
style={{ color: 'var(--jsm-dark-soft)', ...KOR_BODY }}
|
||||
style={{ color: 'var(--jsm-ink-soft)', ...KOR_BODY }}
|
||||
>
|
||||
직접 개발하고 운영 중인 실서비스입니다. 같은 깊이로 의뢰하신 프로젝트를 만듭니다.
|
||||
</p>
|
||||
</ScrollReveal>
|
||||
|
||||
<div className="mt-14 grid gap-6 md:grid-cols-3">
|
||||
<div className="mt-12 grid gap-6 md:grid-cols-3">
|
||||
{PROOF.map((p, i) => (
|
||||
<ScrollReveal key={p.t} delay={i * 100}>
|
||||
<div
|
||||
className="flex h-full flex-col rounded-2xl border p-7"
|
||||
style={{
|
||||
background: 'var(--jsm-dark-surface)',
|
||||
borderColor: 'var(--jsm-dark-line)',
|
||||
}}
|
||||
style={{ background: 'var(--jsm-surface)', borderColor: 'var(--jsm-line)' }}
|
||||
>
|
||||
<span
|
||||
className="mb-5 inline-flex items-center gap-1.5 self-start rounded-full px-2.5 py-1 text-[11px] font-semibold"
|
||||
style={{
|
||||
color: 'var(--jsm-accent-bright)',
|
||||
background: 'rgba(96,165,250,0.12)',
|
||||
}}
|
||||
style={{ color: 'var(--jsm-accent)', background: 'var(--jsm-accent-soft)' }}
|
||||
>
|
||||
<span
|
||||
className="h-1.5 w-1.5 rounded-full"
|
||||
style={{ background: 'var(--jsm-accent-bright)' }}
|
||||
/>
|
||||
<span className="h-1.5 w-1.5 rounded-full" style={{ background: 'var(--jsm-accent)' }} />
|
||||
직접 개발·운영 중
|
||||
</span>
|
||||
<h3
|
||||
className="break-keep text-lg font-bold"
|
||||
style={{ color: 'var(--jsm-dark-ink)', ...KOR_TIGHT }}
|
||||
style={{ color: 'var(--jsm-ink)', ...KOR_TIGHT }}
|
||||
>
|
||||
{p.t}
|
||||
</h3>
|
||||
<p
|
||||
className="mt-2.5 flex-1 break-keep text-sm leading-relaxed"
|
||||
style={{ color: 'var(--jsm-dark-soft)', ...KOR_BODY }}
|
||||
style={{ color: 'var(--jsm-ink-soft)', ...KOR_BODY }}
|
||||
>
|
||||
{p.d}
|
||||
</p>
|
||||
@@ -345,11 +339,7 @@ export default async function Home() {
|
||||
<span
|
||||
key={tag}
|
||||
className="rounded px-2.5 py-1 text-xs"
|
||||
style={{
|
||||
color: 'var(--jsm-dark-soft)',
|
||||
background: 'rgba(148,163,184,0.08)',
|
||||
...KOR_BODY,
|
||||
}}
|
||||
style={{ color: 'var(--jsm-ink-soft)', background: 'var(--jsm-surface-alt)', ...KOR_BODY }}
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
@@ -361,50 +351,32 @@ export default async function Home() {
|
||||
</div>
|
||||
|
||||
{/* 스탯 3종 — 카운트업 */}
|
||||
<ScrollReveal className="mt-14">
|
||||
<ScrollReveal className="mt-12">
|
||||
<div
|
||||
className="grid grid-cols-1 gap-px overflow-hidden rounded-2xl border sm:grid-cols-3"
|
||||
style={{ borderColor: 'var(--jsm-dark-line)', background: 'var(--jsm-dark-line)' }}
|
||||
>
|
||||
<div className="px-8 py-10" style={{ background: 'var(--jsm-dark-bg)' }}>
|
||||
<p
|
||||
className="text-4xl font-bold lg:text-5xl"
|
||||
style={{ color: 'var(--jsm-dark-ink)', letterSpacing: '-0.03em' }}
|
||||
style={{ borderColor: 'var(--jsm-line)', background: 'var(--jsm-line)' }}
|
||||
>
|
||||
<div className="px-8 py-10" style={{ background: 'var(--jsm-surface)' }}>
|
||||
<p className="text-4xl font-bold lg:text-5xl" style={{ color: 'var(--jsm-ink)', letterSpacing: '-0.03em' }}>
|
||||
<CountUp to={15} suffix="+" />
|
||||
</p>
|
||||
<p
|
||||
className="mt-2 break-keep text-sm"
|
||||
style={{ color: 'var(--jsm-dark-soft)', ...KOR_BODY }}
|
||||
>
|
||||
<p className="mt-2 break-keep text-sm" style={{ color: 'var(--jsm-ink-soft)', ...KOR_BODY }}>
|
||||
직접 운영 중인 실서비스
|
||||
</p>
|
||||
</div>
|
||||
<div className="px-8 py-10" style={{ background: 'var(--jsm-dark-bg)' }}>
|
||||
<p
|
||||
className="text-4xl font-bold lg:text-5xl"
|
||||
style={{ color: 'var(--jsm-dark-ink)', letterSpacing: '-0.03em' }}
|
||||
>
|
||||
<div className="px-8 py-10" style={{ background: 'var(--jsm-surface)' }}>
|
||||
<p className="text-4xl font-bold lg:text-5xl" style={{ color: 'var(--jsm-ink)', letterSpacing: '-0.03em' }}>
|
||||
24/7
|
||||
</p>
|
||||
<p
|
||||
className="mt-2 break-keep text-sm"
|
||||
style={{ color: 'var(--jsm-dark-soft)', ...KOR_BODY }}
|
||||
>
|
||||
<p className="mt-2 break-keep text-sm" style={{ color: 'var(--jsm-ink-soft)', ...KOR_BODY }}>
|
||||
무중단 운영
|
||||
</p>
|
||||
</div>
|
||||
<div className="px-8 py-10" style={{ background: 'var(--jsm-dark-bg)' }}>
|
||||
<p
|
||||
className="text-4xl font-bold lg:text-5xl"
|
||||
style={{ color: 'var(--jsm-dark-ink)', letterSpacing: '-0.03em' }}
|
||||
>
|
||||
<div className="px-8 py-10" style={{ background: 'var(--jsm-surface)' }}>
|
||||
<p className="text-4xl font-bold lg:text-5xl" style={{ color: 'var(--jsm-ink)', letterSpacing: '-0.03em' }}>
|
||||
원스톱
|
||||
</p>
|
||||
<p
|
||||
className="mt-2 break-keep text-sm"
|
||||
style={{ color: 'var(--jsm-dark-soft)', ...KOR_BODY }}
|
||||
>
|
||||
<p className="mt-2 break-keep text-sm" style={{ color: 'var(--jsm-ink-soft)', ...KOR_BODY }}>
|
||||
기획 → 배포 단독 진행
|
||||
</p>
|
||||
</div>
|
||||
@@ -413,32 +385,83 @@ export default async function Home() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ─────────────────── 5. SOFTWARE + CTA ─────────────────── */}
|
||||
{/* Phase 2: products 테이블 기반 동적 진열. 0개이면 출시 준비 중 폴백. */}
|
||||
<section className="border-t" style={{ borderColor: 'var(--jsm-dark-line)' }}>
|
||||
<div className="mx-auto max-w-6xl px-6 py-24 lg:px-8 lg:py-32">
|
||||
{/* ─────────────────── 5. PROCESS ─────────────────── */}
|
||||
<section style={{ background: 'var(--jsm-surface)' }}>
|
||||
<div className="mx-auto max-w-6xl px-6 py-20 lg:px-8 lg:py-28">
|
||||
<ScrollReveal>
|
||||
<Eyebrow>process</Eyebrow>
|
||||
<h2
|
||||
className="max-w-2xl break-keep text-3xl font-bold lg:text-[2.6rem] lg:leading-[1.12]"
|
||||
style={{ color: 'var(--jsm-ink)', letterSpacing: '-0.03em' }}
|
||||
>
|
||||
상담부터 납품까지, 흐름이 분명합니다
|
||||
</h2>
|
||||
</ScrollReveal>
|
||||
|
||||
<div className="relative mt-12">
|
||||
{/* 단계 연결선 (데스크톱) */}
|
||||
<span
|
||||
aria-hidden
|
||||
className="absolute left-[12%] right-[12%] top-7 hidden h-px lg:block"
|
||||
style={{ background: 'var(--jsm-line)' }}
|
||||
/>
|
||||
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{PROCESS.map((s, i) => (
|
||||
<ScrollReveal key={s.n} delay={i * 100}>
|
||||
<div
|
||||
className="relative h-full rounded-2xl border p-7 lg:p-8"
|
||||
style={{ background: 'var(--jsm-surface)', borderColor: 'var(--jsm-line)' }}
|
||||
>
|
||||
<span
|
||||
className="relative z-10 inline-flex h-14 w-14 items-center justify-center rounded-full font-mono text-sm font-bold"
|
||||
style={{
|
||||
color: 'var(--jsm-accent)',
|
||||
background: 'var(--jsm-surface)',
|
||||
boxShadow: 'inset 0 0 0 1px var(--jsm-line)',
|
||||
}}
|
||||
>
|
||||
{s.n}
|
||||
</span>
|
||||
<h3
|
||||
className="mt-5 break-keep text-lg font-bold"
|
||||
style={{ color: 'var(--jsm-ink)', ...KOR_TIGHT }}
|
||||
>
|
||||
{s.t}
|
||||
</h3>
|
||||
<p
|
||||
className="mt-2 break-keep text-sm leading-relaxed"
|
||||
style={{ color: 'var(--jsm-ink-soft)', ...KOR_BODY }}
|
||||
>
|
||||
{s.d}
|
||||
</p>
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ─────────────────── 6. 완성 SW + CTA ─────────────────── */}
|
||||
<section style={{ background: 'var(--jsm-surface-alt)' }}>
|
||||
<div className="mx-auto max-w-6xl px-6 py-20 lg:px-8 lg:py-28">
|
||||
{hasProducts ? (
|
||||
<>
|
||||
<ScrollReveal>
|
||||
<div className="flex items-end justify-between">
|
||||
<div>
|
||||
<p
|
||||
className="mb-3 font-mono text-[11px] uppercase tracking-[0.22em]"
|
||||
style={{ color: 'var(--jsm-accent-bright)' }}
|
||||
>
|
||||
software
|
||||
</p>
|
||||
<Eyebrow>software</Eyebrow>
|
||||
<h2
|
||||
className="break-keep text-3xl font-bold lg:text-[2.75rem] lg:leading-[1.12]"
|
||||
style={{ color: 'var(--jsm-dark-ink)', letterSpacing: '-0.03em' }}
|
||||
className="break-keep text-3xl font-bold lg:text-[2.6rem] lg:leading-[1.12]"
|
||||
style={{ color: 'var(--jsm-ink)', letterSpacing: '-0.03em' }}
|
||||
>
|
||||
바로 쓰는 완성 소프트웨어
|
||||
</h2>
|
||||
</div>
|
||||
<Link
|
||||
href="/products"
|
||||
className="hidden shrink-0 items-center gap-1.5 text-sm font-semibold transition-colors duration-150 hover:opacity-80 sm:inline-flex"
|
||||
style={{ color: 'var(--jsm-accent-bright)', ...KOR_BODY }}
|
||||
className="hidden shrink-0 items-center gap-1.5 text-sm font-semibold transition-colors duration-150 hover:text-[var(--jsm-accent-hover)] sm:inline-flex"
|
||||
style={{ color: 'var(--jsm-accent)', ...KOR_BODY }}
|
||||
>
|
||||
전체 보기
|
||||
<ArrowRight />
|
||||
@@ -446,14 +469,13 @@ export default async function Home() {
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
|
||||
<div className="mt-14 grid gap-6 md:grid-cols-3">
|
||||
<div className="mt-12 grid gap-6 md:grid-cols-3">
|
||||
{featuredProducts.map((p, i) => (
|
||||
<ScrollReveal key={p.id} delay={i * 100}>
|
||||
{/* 라이트 카드가 다크 위에 떠 있는 대비 */}
|
||||
<Link
|
||||
href={`/products/${p.id}`}
|
||||
className="group flex h-full flex-col rounded-2xl p-7 shadow-[0_24px_60px_-24px_rgba(0,0,0,0.6)] transition-transform duration-300 hover:translate-y-[-2px]"
|
||||
style={{ background: 'var(--jsm-surface)' }}
|
||||
className="group flex h-full flex-col rounded-2xl border p-7 transition-[transform,box-shadow,border-color] duration-300 hover:-translate-y-1 hover:border-[var(--jsm-accent)] hover:shadow-[0_24px_60px_-32px_rgba(15,23,42,0.4)]"
|
||||
style={{ background: 'var(--jsm-surface)', borderColor: 'var(--jsm-line)' }}
|
||||
>
|
||||
<h3
|
||||
className="break-keep text-lg font-bold"
|
||||
@@ -495,7 +517,7 @@ export default async function Home() {
|
||||
<Link
|
||||
href="/products"
|
||||
className="inline-flex items-center gap-1.5 text-sm font-semibold"
|
||||
style={{ color: 'var(--jsm-accent-bright)', ...KOR_BODY }}
|
||||
style={{ color: 'var(--jsm-accent)', ...KOR_BODY }}
|
||||
>
|
||||
전체 보기
|
||||
<ArrowRight />
|
||||
@@ -506,38 +528,26 @@ export default async function Home() {
|
||||
<ScrollReveal>
|
||||
<div
|
||||
className="rounded-2xl border px-8 py-14 text-center lg:px-14 lg:py-16"
|
||||
style={{
|
||||
background: 'var(--jsm-dark-surface)',
|
||||
borderColor: 'var(--jsm-dark-line)',
|
||||
}}
|
||||
style={{ background: 'var(--jsm-surface)', borderColor: 'var(--jsm-line)' }}
|
||||
>
|
||||
<p
|
||||
className="mb-3 font-mono text-[11px] uppercase tracking-[0.22em]"
|
||||
style={{ color: 'var(--jsm-accent-bright)' }}
|
||||
>
|
||||
coming soon
|
||||
</p>
|
||||
<Eyebrow>coming soon</Eyebrow>
|
||||
<h2
|
||||
className="break-keep text-2xl font-bold lg:text-3xl"
|
||||
style={{ color: 'var(--jsm-dark-ink)', ...KOR_TIGHT }}
|
||||
style={{ color: 'var(--jsm-ink)', ...KOR_TIGHT }}
|
||||
>
|
||||
검증된 완성 소프트웨어를 준비하고 있습니다
|
||||
</h2>
|
||||
<p
|
||||
className="mx-auto mt-4 max-w-xl break-keep leading-relaxed"
|
||||
style={{ color: 'var(--jsm-dark-soft)', ...KOR_BODY }}
|
||||
style={{ color: 'var(--jsm-ink-soft)', ...KOR_BODY }}
|
||||
>
|
||||
직접 운영하며 다듬은 도구를 하나씩 다운로드 상품으로 공개할 예정입니다. 출시
|
||||
소식을 가장 먼저 받아보세요.
|
||||
</p>
|
||||
<Link
|
||||
href="/outsourcing#contact"
|
||||
className="mt-8 inline-flex items-center justify-center gap-2 rounded-lg border px-6 py-3.5 font-semibold transition-colors duration-200 hover:bg-[var(--jsm-dark-surface)]"
|
||||
style={{
|
||||
color: 'var(--jsm-dark-ink)',
|
||||
borderColor: 'var(--jsm-dark-line)',
|
||||
...KOR_BODY,
|
||||
}}
|
||||
className="mt-8 inline-flex items-center justify-center gap-2 rounded-lg border px-6 py-3.5 font-semibold transition-colors duration-200 hover:bg-[var(--jsm-surface-alt)]"
|
||||
style={{ color: 'var(--jsm-ink)', borderColor: 'var(--jsm-line)', ...KOR_BODY }}
|
||||
>
|
||||
출시 소식 받기
|
||||
<ArrowRight />
|
||||
@@ -546,22 +556,13 @@ export default async function Home() {
|
||||
</ScrollReveal>
|
||||
)}
|
||||
|
||||
{/* 최종 CTA 밴드 — accent bg */}
|
||||
<ScrollReveal className="mt-24 lg:mt-32">
|
||||
{/* 최종 CTA 밴드 — 평면 navy (사이트 유일 다크면) */}
|
||||
<ScrollReveal className="mt-20 lg:mt-28">
|
||||
<div
|
||||
className="relative overflow-hidden rounded-3xl px-8 py-16 lg:px-16 lg:py-20"
|
||||
style={{ background: 'var(--jsm-accent)' }}
|
||||
className="rounded-3xl px-8 py-16 lg:px-16 lg:py-20"
|
||||
style={{ background: 'var(--jsm-navy)' }}
|
||||
>
|
||||
{/* 광원 — radial 허용 */}
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-0"
|
||||
style={{
|
||||
backgroundImage:
|
||||
'radial-gradient(60% 80% at 85% 0%, rgba(96,165,250,0.45) 0%, transparent 55%)',
|
||||
}}
|
||||
/>
|
||||
<div className="relative max-w-3xl">
|
||||
<div className="max-w-3xl">
|
||||
<h2
|
||||
className="break-keep text-3xl font-bold leading-tight text-white lg:text-[2.5rem]"
|
||||
style={KOR_TIGHT}
|
||||
@@ -569,7 +570,7 @@ export default async function Home() {
|
||||
프로젝트, 이야기부터 시작하세요
|
||||
</h2>
|
||||
<p
|
||||
className="mt-5 max-w-2xl break-keep text-lg leading-relaxed text-white/80"
|
||||
className="mt-5 max-w-2xl break-keep text-lg leading-relaxed text-white/70"
|
||||
style={KOR_BODY}
|
||||
>
|
||||
아이디어 단계여도 괜찮습니다. 무료 상담에서 방향을 함께 잡아드립니다.
|
||||
@@ -577,7 +578,7 @@ export default async function Home() {
|
||||
<Link
|
||||
href="/outsourcing#contact"
|
||||
className="mt-9 inline-flex items-center justify-center gap-2 rounded-lg bg-white px-7 py-4 font-semibold transition-transform duration-200 hover:translate-y-[-1px]"
|
||||
style={{ color: 'var(--jsm-accent)', ...KOR_BODY }}
|
||||
style={{ color: 'var(--jsm-navy)', ...KOR_BODY }}
|
||||
>
|
||||
무료 상담 신청
|
||||
<ArrowRight />
|
||||
@@ -587,6 +588,6 @@ export default async function Home() {
|
||||
</ScrollReveal>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Metadata } from 'next';
|
||||
import { createAdminClient } from '@/lib/supabase/admin';
|
||||
import { getListedProducts, type ProductRow } from '@/lib/supabase/product-files';
|
||||
|
||||
// 완성 소프트웨어 동적 카탈로그 (서버 컴포넌트).
|
||||
// 완성 소프트웨어 동적 카탈로그 (서버 컴포넌트). 라이트 고craft — 홈·외주와 동일 언어.
|
||||
// DB 장애·마이그레이션 미적용 시 빈 배열로 폴백해 페이지가 항상 200으로 생존한다.
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -12,7 +12,6 @@ export const metadata: Metadata = {
|
||||
'쟁승메이드가 직접 운영하며 검증한 완성 소프트웨어 목록. 계좌이체 결제 후 입금 확인 즉시 마이페이지에서 다운로드할 수 있습니다.',
|
||||
};
|
||||
|
||||
// 카탈로그는 항상 최신 상품을 보여주도록 동적 렌더링.
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
const KOR_TIGHT = { letterSpacing: '-0.02em' } as const;
|
||||
@@ -26,17 +25,7 @@ const HOW = [
|
||||
|
||||
function ArrowRight() {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
|
||||
<path d="M5 12h14" />
|
||||
<path d="m13 5 7 7-7 7" />
|
||||
</svg>
|
||||
@@ -45,18 +34,7 @@ function ArrowRight() {
|
||||
|
||||
function CheckMark() {
|
||||
return (
|
||||
<svg
|
||||
width="15"
|
||||
height="15"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="shrink-0 mt-0.5"
|
||||
aria-hidden
|
||||
>
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" className="mt-0.5 shrink-0" aria-hidden>
|
||||
<path d="M20 6 9 17l-5-5" />
|
||||
</svg>
|
||||
);
|
||||
@@ -66,7 +44,6 @@ async function loadProducts(): Promise<ProductRow[]> {
|
||||
try {
|
||||
return await getListedProducts(createAdminClient());
|
||||
} catch (err) {
|
||||
// DB 장애·컬럼 미존재(마이그레이션 미적용) 등 — 페이지는 준비 중 폴백으로 생존
|
||||
console.error('[Products] getListedProducts failed, falling back to empty:', err);
|
||||
return [];
|
||||
}
|
||||
@@ -79,31 +56,23 @@ export default async function ProductsPage() {
|
||||
return (
|
||||
<>
|
||||
{/* ─── Hero ─── */}
|
||||
<section className="border-b" style={{ borderColor: 'var(--jsm-line)' }}>
|
||||
<div className="max-w-5xl mx-auto px-6 lg:px-8 py-20 lg:py-28">
|
||||
<section style={{ background: 'var(--jsm-surface)' }}>
|
||||
<div className="mx-auto max-w-6xl px-6 pt-20 pb-16 lg:px-8 lg:pt-28 lg:pb-20">
|
||||
<div className="max-w-2xl">
|
||||
<span
|
||||
className="inline-block text-xs font-semibold mb-6 px-2.5 py-1 rounded"
|
||||
style={{
|
||||
color: 'var(--jsm-accent)',
|
||||
background: 'var(--jsm-accent-soft)',
|
||||
...KOR_BODY,
|
||||
}}
|
||||
>
|
||||
완성 소프트웨어
|
||||
<span className="inline-flex items-center gap-2 font-mono text-[11px] uppercase tracking-[0.22em]" style={{ color: 'var(--jsm-accent)' }}>
|
||||
<span className="inline-block h-1 w-1 rounded-full" style={{ background: 'var(--jsm-accent)' }} />
|
||||
software
|
||||
</span>
|
||||
<h1
|
||||
className="text-3xl sm:text-4xl lg:text-5xl font-bold leading-[1.2] break-keep mb-5"
|
||||
style={{ color: 'var(--jsm-ink)', ...KOR_TIGHT }}
|
||||
className="mt-6 font-extrabold break-keep"
|
||||
style={{ color: 'var(--jsm-ink)', fontSize: 'clamp(2.3rem, 6vw, 3.6rem)', lineHeight: 1.1, letterSpacing: '-0.035em' }}
|
||||
>
|
||||
직접 운영하며 검증한 도구를
|
||||
<br />
|
||||
그대로 가져가세요.
|
||||
그대로 가져가세요
|
||||
<span style={{ color: 'var(--jsm-accent)' }}>.</span>
|
||||
</h1>
|
||||
<p
|
||||
className="text-base sm:text-lg leading-relaxed break-keep"
|
||||
style={{ color: 'var(--jsm-ink-soft)', ...KOR_BODY }}
|
||||
>
|
||||
<p className="mt-7 break-keep text-lg leading-relaxed" style={{ color: 'var(--jsm-ink-soft)', ...KOR_BODY }}>
|
||||
입금 확인 후 마이페이지에서 바로 다운로드할 수 있습니다.
|
||||
</p>
|
||||
</div>
|
||||
@@ -111,30 +80,24 @@ export default async function ProductsPage() {
|
||||
</section>
|
||||
|
||||
{/* ─── 카탈로그 / 준비 중 ─── */}
|
||||
<section style={{ background: 'var(--jsm-surface-alt)' }}>
|
||||
<div className="mx-auto max-w-6xl px-6 py-16 lg:px-8 lg:py-24">
|
||||
{hasProducts ? (
|
||||
<section className="border-b" style={{ borderColor: 'var(--jsm-line)' }}>
|
||||
<div className="max-w-5xl mx-auto px-6 lg:px-8 py-16 lg:py-20">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
{products.map((p) => {
|
||||
const features = (p.features ?? []).slice(0, 3);
|
||||
return (
|
||||
<Link
|
||||
key={p.id}
|
||||
href={`/products/${p.id}`}
|
||||
className="group flex flex-col rounded-2xl p-7 lg:p-8 border transition-colors duration-200 hover:border-[var(--jsm-accent)]"
|
||||
className="group flex flex-col rounded-2xl border p-7 transition-[transform,box-shadow,border-color] duration-300 hover:-translate-y-1 hover:border-[var(--jsm-accent)] hover:shadow-[0_24px_60px_-32px_rgba(15,23,42,0.4)] lg:p-8"
|
||||
style={{ background: 'var(--jsm-surface)', borderColor: 'var(--jsm-line)' }}
|
||||
>
|
||||
<h2
|
||||
className="text-xl font-bold break-keep"
|
||||
style={{ color: 'var(--jsm-ink)', ...KOR_TIGHT }}
|
||||
>
|
||||
<h2 className="break-keep text-xl font-bold" style={{ color: 'var(--jsm-ink)', ...KOR_TIGHT }}>
|
||||
{p.name}
|
||||
</h2>
|
||||
{p.description && (
|
||||
<p
|
||||
className="mt-2.5 text-sm leading-relaxed break-keep"
|
||||
style={{ color: 'var(--jsm-ink-soft)', ...KOR_BODY }}
|
||||
>
|
||||
<p className="mt-2.5 break-keep text-sm leading-relaxed" style={{ color: 'var(--jsm-ink-soft)', ...KOR_BODY }}>
|
||||
{p.description}
|
||||
</p>
|
||||
)}
|
||||
@@ -142,11 +105,7 @@ export default async function ProductsPage() {
|
||||
{features.length > 0 && (
|
||||
<ul className="mt-5 space-y-2">
|
||||
{features.map((f) => (
|
||||
<li
|
||||
key={f}
|
||||
className="flex items-start gap-2 text-sm break-keep"
|
||||
style={{ color: 'var(--jsm-ink)', ...KOR_BODY }}
|
||||
>
|
||||
<li key={f} className="flex items-start gap-2 break-keep text-sm" style={{ color: 'var(--jsm-ink)', ...KOR_BODY }}>
|
||||
<span style={{ color: 'var(--jsm-accent)' }}>
|
||||
<CheckMark />
|
||||
</span>
|
||||
@@ -156,17 +115,11 @@ export default async function ProductsPage() {
|
||||
</ul>
|
||||
)}
|
||||
|
||||
<div className="mt-6 pt-5 flex items-center justify-between border-t" style={{ borderColor: 'var(--jsm-line)' }}>
|
||||
<span
|
||||
className="text-lg font-bold"
|
||||
style={{ color: 'var(--jsm-ink)', ...KOR_TIGHT }}
|
||||
>
|
||||
₩{p.price.toLocaleString('ko-KR')}
|
||||
<div className="mt-6 flex items-center justify-between border-t pt-5" style={{ borderColor: 'var(--jsm-line)' }}>
|
||||
<span className="text-lg font-bold" style={{ color: 'var(--jsm-ink)', ...KOR_TIGHT }}>
|
||||
₩{p.price.toLocaleString('ko-KR')}
|
||||
</span>
|
||||
<span
|
||||
className="inline-flex items-center gap-1.5 text-sm font-semibold transition-colors duration-150 group-hover:text-[var(--jsm-accent-hover)]"
|
||||
style={{ color: 'var(--jsm-accent)', ...KOR_BODY }}
|
||||
>
|
||||
<span className="inline-flex items-center gap-1.5 text-sm font-semibold transition-colors duration-150 group-hover:text-[var(--jsm-accent-hover)]" style={{ color: 'var(--jsm-accent)', ...KOR_BODY }}>
|
||||
자세히 보기
|
||||
<ArrowRight />
|
||||
</span>
|
||||
@@ -175,75 +128,46 @@ export default async function ProductsPage() {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
) : (
|
||||
<section className="border-b" style={{ borderColor: 'var(--jsm-line)' }}>
|
||||
<div className="max-w-5xl mx-auto px-6 lg:px-8 py-16 lg:py-20">
|
||||
<div
|
||||
className="rounded-lg border p-8 text-center"
|
||||
style={{
|
||||
background: 'var(--jsm-surface-alt)',
|
||||
borderColor: 'var(--jsm-line)',
|
||||
}}
|
||||
>
|
||||
<p
|
||||
className="text-sm font-semibold mb-3"
|
||||
style={{ color: 'var(--jsm-accent)', ...KOR_BODY }}
|
||||
>
|
||||
출시 준비 중
|
||||
<div className="rounded-2xl border px-8 py-14 text-center lg:py-16" style={{ background: 'var(--jsm-surface)', borderColor: 'var(--jsm-line)' }}>
|
||||
<p className="mb-3 font-mono text-[11px] uppercase tracking-[0.22em]" style={{ color: 'var(--jsm-accent)' }}>
|
||||
coming soon
|
||||
</p>
|
||||
<p
|
||||
className="text-xl sm:text-2xl font-bold mb-4 break-keep"
|
||||
style={{ color: 'var(--jsm-ink)', ...KOR_TIGHT }}
|
||||
>
|
||||
현재 상품을 정비하고 있습니다.
|
||||
</p>
|
||||
<p
|
||||
className="text-sm sm:text-base leading-relaxed break-keep max-w-md mx-auto"
|
||||
style={{ color: 'var(--jsm-ink-soft)', ...KOR_BODY }}
|
||||
>
|
||||
로또 분석 도구, 주식 자동매매 유틸리티 등 실제로 운영 중인 도구들을
|
||||
구매 가능한 형태로 순차 공개할 예정입니다.
|
||||
출시 소식을 먼저 받고 싶다면 아래 링크로 문의해 주세요.
|
||||
<h2 className="break-keep text-2xl font-bold lg:text-3xl" style={{ color: 'var(--jsm-ink)', ...KOR_TIGHT }}>
|
||||
현재 상품을 정비하고 있습니다
|
||||
</h2>
|
||||
<p className="mx-auto mt-4 max-w-md break-keep leading-relaxed" style={{ color: 'var(--jsm-ink-soft)', ...KOR_BODY }}>
|
||||
로또 분석 도구, 주식 자동매매 유틸리티 등 실제로 운영 중인 도구들을 구매 가능한
|
||||
형태로 순차 공개할 예정입니다. 출시 소식을 먼저 받고 싶다면 아래 링크로 문의해
|
||||
주세요.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* ─── 구매 방식 안내 ─── */}
|
||||
<section style={{ background: 'var(--jsm-surface-alt)' }}>
|
||||
<div className="max-w-5xl mx-auto px-6 lg:px-8 py-16 lg:py-20">
|
||||
<h2
|
||||
className="text-xl sm:text-2xl font-bold mb-10 break-keep"
|
||||
style={{ color: 'var(--jsm-ink)', ...KOR_TIGHT }}
|
||||
>
|
||||
<section style={{ background: 'var(--jsm-surface)' }}>
|
||||
<div className="mx-auto max-w-6xl px-6 py-16 lg:px-8 lg:py-24">
|
||||
<p className="mb-3 font-mono text-[11px] uppercase tracking-[0.22em]" style={{ color: 'var(--jsm-accent)' }}>
|
||||
how to buy
|
||||
</p>
|
||||
<h2 className="break-keep text-3xl font-bold lg:text-[2.6rem] lg:leading-[1.12]" style={{ color: 'var(--jsm-ink)', letterSpacing: '-0.03em' }}>
|
||||
구매 방식
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-6">
|
||||
<div className="mt-12 grid grid-cols-1 gap-6 sm:grid-cols-3">
|
||||
{HOW.map((step) => (
|
||||
<div
|
||||
key={step.n}
|
||||
className="rounded-lg border p-6"
|
||||
style={{ background: 'var(--jsm-surface)', borderColor: 'var(--jsm-line)' }}
|
||||
>
|
||||
<div key={step.n} className="rounded-2xl border p-7" style={{ background: 'var(--jsm-surface)', borderColor: 'var(--jsm-line)' }}>
|
||||
<span
|
||||
className="text-xs font-semibold mb-3 block"
|
||||
style={{ color: 'var(--jsm-accent)', ...KOR_BODY }}
|
||||
className="inline-flex h-12 w-12 items-center justify-center rounded-full font-mono text-sm font-bold"
|
||||
style={{ color: 'var(--jsm-accent)', background: 'var(--jsm-surface)', boxShadow: 'inset 0 0 0 1px var(--jsm-line)' }}
|
||||
>
|
||||
{step.n}
|
||||
</span>
|
||||
<p
|
||||
className="font-bold mb-2 break-keep"
|
||||
style={{ color: 'var(--jsm-ink)', ...KOR_TIGHT }}
|
||||
>
|
||||
<p className="mt-5 break-keep font-bold" style={{ color: 'var(--jsm-ink)', ...KOR_TIGHT }}>
|
||||
{step.t}
|
||||
</p>
|
||||
<p
|
||||
className="text-sm leading-relaxed break-keep"
|
||||
style={{ color: 'var(--jsm-ink-soft)', ...KOR_BODY }}
|
||||
>
|
||||
<p className="mt-2 break-keep text-sm leading-relaxed" style={{ color: 'var(--jsm-ink-soft)', ...KOR_BODY }}>
|
||||
{step.d}
|
||||
</p>
|
||||
</div>
|
||||
@@ -253,29 +177,21 @@ export default async function ProductsPage() {
|
||||
</section>
|
||||
|
||||
{/* ─── CTA ─── */}
|
||||
<section className="border-t" style={{ borderColor: 'var(--jsm-line)' }}>
|
||||
<div className="max-w-5xl mx-auto px-6 lg:px-8 py-16 lg:py-20">
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<section style={{ background: 'var(--jsm-surface-alt)' }}>
|
||||
<div className="mx-auto max-w-6xl px-6 py-16 lg:px-8 lg:py-20">
|
||||
<div className="flex flex-col gap-4 sm:flex-row">
|
||||
<Link
|
||||
href="/outsourcing#contact"
|
||||
className="inline-flex items-center justify-center gap-2 px-6 py-3 rounded-lg text-sm font-semibold transition-colors"
|
||||
style={{
|
||||
background: 'var(--jsm-accent)',
|
||||
color: '#ffffff',
|
||||
...KOR_BODY,
|
||||
}}
|
||||
className="inline-flex items-center justify-center gap-2 rounded-lg px-6 py-3.5 text-sm font-semibold text-white transition-colors hover:bg-[var(--jsm-accent-hover)]"
|
||||
style={{ background: 'var(--jsm-accent)', ...KOR_BODY }}
|
||||
>
|
||||
{hasProducts ? '맞춤 개발 문의' : '출시 소식 받기'}
|
||||
<ArrowRight />
|
||||
</Link>
|
||||
<Link
|
||||
href="/outsourcing"
|
||||
className="inline-flex items-center justify-center gap-2 px-6 py-3 rounded-lg border text-sm font-semibold transition-colors"
|
||||
style={{
|
||||
borderColor: 'var(--jsm-line)',
|
||||
color: 'var(--jsm-ink-soft)',
|
||||
background: 'var(--jsm-surface)',
|
||||
...KOR_BODY,
|
||||
}}
|
||||
className="inline-flex items-center justify-center gap-2 rounded-lg border px-6 py-3.5 text-sm font-semibold transition-colors hover:bg-[var(--jsm-surface)]"
|
||||
style={{ borderColor: 'var(--jsm-line)', color: 'var(--jsm-ink)', background: 'var(--jsm-surface)', ...KOR_BODY }}
|
||||
>
|
||||
외주 개발 알아보기
|
||||
</Link>
|
||||
|
||||
235
docs/superpowers/plans/2026-06-30-jsm-light-redesign.md
Normal file
235
docs/superpowers/plans/2026-06-30-jsm-light-redesign.md
Normal file
@@ -0,0 +1,235 @@
|
||||
# 쟁승메이드 라이트 고craft 재설계 — Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 홈·외주·제품 3면을 라이트 `--jsm-*` 단일 시스템으로 통일하고, 히어로·쇼케이스를 코드 UI 목업(MockWindow)으로 재구성한다.
|
||||
|
||||
**Architecture:** 파티클(HeroField)·다크 토큰을 폐기하고, 재사용 가능한 라이트 `MockWindow` 목업 시스템을 craft의 핵심 비주얼로 삼는다. 3면이 동일한 컨테이너·타입 스케일·여백 리듬·카드 스펙을 공유한다. TopNav의 다크 라우트 분기를 제거해 전 페이지 단일 라이트 셸로 통일한다.
|
||||
|
||||
**Tech Stack:** Next.js 16 (App Router, 서버 컴포넌트 우선), TypeScript, Tailwind v4, Pretendard, vitest.
|
||||
|
||||
설계 문서: `docs/superpowers/specs/2026-06-30-jsm-light-redesign-design.md`
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- 색: `--jsm-*` 라이트 토큰만. **금지** — `--jsm-dark-*`, `--kx-*`, 보라/violet, gradient, blur, 이모지.
|
||||
- navy(`--jsm-navy`)는 푸터 + 홈 CTA 밴드 **2곳에서만** (평면, radial 없음).
|
||||
- 컨테이너: `max-w-6xl mx-auto px-6 lg:px-8` (3면 동일).
|
||||
- 한글: 헤딩·본문 `break-keep`. `KOR_TIGHT = letterSpacing -0.02em`, `KOR_BODY = -0.01em`.
|
||||
- 타이포: h1 `clamp(2.4rem,7vw,4rem)` w800 -0.03em / h2 `clamp(1.7rem,4vw,2.4rem)` w700 -0.02em / eyebrow 11px UPPER 0.2em accent / 본문 16–18px ink-soft.
|
||||
- 카피: 경력 어필("대기업 7년차" 류) 금지 → 운영 실증 표현 유지.
|
||||
- 모션: `ScrollReveal`·`.reveal` CSS 유지, `prefers-reduced-motion` 가드.
|
||||
- 각 Task 종료 시 `npm run build` 통과 + 커밋. 브랜치 `redesign/jsm-light-craft` (생성됨).
|
||||
- 빌드 명령(Windows): `npm run build`. 테스트: `npm test`.
|
||||
|
||||
> **계획 altitude 주석:** 본 계획은 *재사용 빌딩블록*(MockWindow API·showcase 타입·테스트)은 완전한 코드로, *페이지 재작성*은 섹션 구조 + 정확한 토큰/클래스 규약 + 검증 게이트로 명세한다. 페이지 JSX 전문을 계획에 박지 않는 것은 의도된 결정이다(시각 레이아웃은 토큰·구조 제약으로 충분히 결정되며, 전문 박제는 중복·열화를 유발).
|
||||
|
||||
---
|
||||
|
||||
### Task 1: MockWindow 목업 시스템
|
||||
|
||||
**Files:**
|
||||
- Create: `app/components/mock/MockWindow.tsx`
|
||||
- Create: `app/components/mock/screens.tsx` (6 스크린 목업 한 파일 — 함께 변경되므로 동거)
|
||||
- Create: `app/components/mock/registry.ts` (mock key → 컴포넌트 + 메타)
|
||||
|
||||
**Interfaces:**
|
||||
- Produces:
|
||||
- `MockWindow({ title, accent?, children, className? }): JSX` — 브라우저 크롬 프레임(● ● ● 신호등 + 타이틀바 + 본문 슬롯). 서버 컴포넌트. 라이트(surface) + navy 타이틀바 옵션.
|
||||
- 스크린 컴포넌트(전부 서버, props 없음, 정적 마크업): `DashboardMock`, `FeedMock`, `MatchMock`, `CommerceMock`, `SiteMock`, `BookingMock`.
|
||||
- `MOCK_REGISTRY: Record<MockKey, React.ComponentType>` 및 `type MockKey = 'dashboard'|'feed'|'match'|'commerce'|'site'|'booking'`.
|
||||
|
||||
**MockWindow 규약 (완전 코드):**
|
||||
```tsx
|
||||
// app/components/mock/MockWindow.tsx
|
||||
interface MockWindowProps {
|
||||
title: string; // 타이틀바 텍스트 (예: 'stock-report', 'realestate-match')
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
export default function MockWindow({ title, children, className }: MockWindowProps) {
|
||||
return (
|
||||
<div
|
||||
className={`overflow-hidden rounded-xl border shadow-[0_24px_60px_-30px_rgba(15,23,42,0.35)] ${className ?? ''}`}
|
||||
style={{ background: 'var(--jsm-surface)', borderColor: 'var(--jsm-line)' }}
|
||||
>
|
||||
{/* 타이틀바 */}
|
||||
<div
|
||||
className="flex items-center gap-2 px-3.5 py-2.5 border-b"
|
||||
style={{ background: 'var(--jsm-surface-alt)', borderColor: 'var(--jsm-line)' }}
|
||||
>
|
||||
<span className="flex gap-1.5" aria-hidden>
|
||||
<span className="h-2.5 w-2.5 rounded-full" style={{ background: '#e2e8f0' }} />
|
||||
<span className="h-2.5 w-2.5 rounded-full" style={{ background: '#e2e8f0' }} />
|
||||
<span className="h-2.5 w-2.5 rounded-full" style={{ background: '#e2e8f0' }} />
|
||||
</span>
|
||||
<span
|
||||
className="ml-1 font-mono text-[11px]"
|
||||
style={{ color: 'var(--jsm-ink-faint)', letterSpacing: '-0.01em' }}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
</div>
|
||||
{/* 본문 */}
|
||||
<div className="p-4">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**스크린 목업 시각 명세** (`screens.tsx` — 각 컴포넌트가 그릴 요소; 전부 `--jsm-*`, SVG/div, 실데이터 0):
|
||||
- `DashboardMock` — 상단 스탯 3칸(라벨+숫자, 1칸 accent 강조) + 막대 차트(div 높이 배열) 1개. "주식 리포트" 톤.
|
||||
- `FeedMock` — 메시지 버블 3~4개(좌측 정렬, 시각·텍스트·체결/알림 배지). "텔레그램 봇" 톤.
|
||||
- `MatchMock` — 리스트 행 3개(항목명 + 매칭률 배지 `92%` accent-soft) + 상단 필터칩. "부동산 청약" 톤.
|
||||
- `CommerceMock` — 상품 카드 그리드 4(썸네일 박스 + 가격) + 장바구니 바.
|
||||
- `SiteMock` — 기업 사이트 와이어(네비 바 + 큰 헤드라인 라인 2 + CTA 버튼 + 카드 3). "corporate/portfolio".
|
||||
- `BookingMock` — 주간 캘린더 헤더(요일 7) + 슬롯 그리드(일부 accent 채움) + 예약 버튼.
|
||||
|
||||
**Steps:**
|
||||
- [ ] **Step 1:** `MockWindow.tsx` 작성 (위 완전 코드).
|
||||
- [ ] **Step 2:** `screens.tsx`에 6개 스크린 컴포넌트 작성 (위 시각 명세 따름, 각 `<div className="space-y-3">...` 라이트 마크업).
|
||||
- [ ] **Step 3:** `registry.ts` 작성 — `MockKey` 타입 + `MOCK_REGISTRY` 매핑 export.
|
||||
- [ ] **Step 4:** 빌드 검증. Run: `npm run build` — Expected: 성공(타입 에러 0).
|
||||
- [ ] **Step 5:** 커밋. `git add app/components/mock && git commit -m "feat(redesign): MockWindow 라이트 목업 시스템(프레임+6스크린+레지스트리)"`
|
||||
|
||||
---
|
||||
|
||||
### Task 2: 쇼케이스 라이트 전환
|
||||
|
||||
**Files:**
|
||||
- Modify: `lib/showcase.ts` (슬롯 타입을 mock 기반으로 교체)
|
||||
- Modify: `app/components/deepfield/ShowcaseCard.tsx` (그래디언트/캔버스 → MockWindow 라이트 카드 재작성)
|
||||
- Keep: `app/components/deepfield/ShowcaseGrid.tsx` (레이아웃 로직 유지, 카드만 교체)
|
||||
- Test: `lib/__tests__/showcase.test.ts` (신규 — 가드레일 데이터 테스트)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 1의 `MockKey`, `MOCK_REGISTRY`.
|
||||
- Produces: `ShowcaseSlot { slug; label; title; desc; mock: MockKey; href? }` (palette/accent 제거). `SHOWCASE_SLOTS: ShowcaseSlot[]` (8슬롯, 보라 0).
|
||||
|
||||
**신규 슬롯 매핑** (보라 제거, mock 배정):
|
||||
```
|
||||
corporate → site | commerce → commerce | dashboard → dashboard | bakery → booking
|
||||
portfolio → site | game → site | interior → site | reading → site
|
||||
```
|
||||
> 메모: site 목업이 다수 → 시각 단조 방지 위해 `SiteMock`에 variant prop(헤드라인 색/레이아웃 미세 차이) 추가 가능(선택). 1차는 단일 SiteMock로 진행, Task 7 검증 시 단조하면 variant 보강.
|
||||
|
||||
**Steps:**
|
||||
- [ ] **Step 1 (테스트 먼저):** `lib/__tests__/showcase.test.ts` 작성 — 각 슬롯이 (a) `mock`이 유효한 MockKey, (b) `slug/title/desc` 비어있지 않음, (c) 어떤 필드에도 보라 hex(`#c4b5fd`,`#f0abfc`,`#341a4f`,`#4a1342`) 부재. (palette 필드 자체가 사라지므로 타입+값 검증.)
|
||||
- [ ] **Step 2:** Run `npm test` — Expected: FAIL (showcase 타입에 mock 없음 / palette 잔존).
|
||||
- [ ] **Step 3:** `lib/showcase.ts` 인터페이스·데이터를 mock 기반으로 교체.
|
||||
- [ ] **Step 4:** `ShowcaseCard.tsx` 재작성 — 카드 = `MockWindow`(상단) + 하단 텍스트(eyebrow label·title·desc, href면 "데모 보기"). 캔버스/시드/그래디언트/보라 전량 제거. 라이트 카드. `'use client'` 불필요면 서버 컴포넌트로.
|
||||
- [ ] **Step 5:** Run `npm test` — Expected: PASS. 이어서 `npm run build` — Expected: 성공.
|
||||
- [ ] **Step 6:** 커밋. `git commit -am "feat(redesign): 쇼케이스 그래디언트 타일 → 라이트 MockWindow 카드 + 가드레일 테스트"`
|
||||
|
||||
---
|
||||
|
||||
### Task 3: TopNav 라이트 단일화
|
||||
|
||||
**Files:**
|
||||
- Modify: `app/components/TopNav.tsx`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: 없음. Produces: 단일 라이트 네비(전 라우트 동일).
|
||||
|
||||
**변경:**
|
||||
- `DARK_ROUTES`/`isDark` 분기 + 다크 팔레트 헬퍼(`ink/inkSoft/surface/line/accent/accentBg`의 isDark 삼항) 전량 제거 → 라이트 고정값.
|
||||
- 최상단(미스크롤): 배경 transparent 유지(라이트 히어로 위 dark ink 텍스트로 가독) / 스크롤 시: `--jsm-surface` + `--jsm-line` border + 미세 shadow.
|
||||
- 모바일 드로어 `surface` = `--jsm-surface` 고정.
|
||||
|
||||
**Steps:**
|
||||
- [ ] **Step 1:** `isDark` 및 다크 분기 제거, 팔레트를 라이트 토큰 고정으로 치환.
|
||||
- [ ] **Step 2:** Run `npm run build` — Expected: 성공.
|
||||
- [ ] **Step 3:** 커밋. `git commit -am "feat(redesign): TopNav 다크 라우트 분기 제거 → 단일 라이트 네비"`
|
||||
|
||||
---
|
||||
|
||||
### Task 4: 홈 라이트 재구성 (`app/page.tsx`)
|
||||
|
||||
**Files:**
|
||||
- Modify: `app/page.tsx` (전면 재작성)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 1 `MockWindow`+스크린, Task 2 `ShowcaseGrid`/`SHOWCASE_SLOTS`, 기존 `getListedProducts`·`CountUp`·`ScrollReveal`.
|
||||
|
||||
**섹션 구조(배경 교차):**
|
||||
1. HERO (surface) — 비대칭 2단: 좌(eyebrow `OUTSOURCING · SOFTWARE` / h1 "생각을 / 동작하는 소프트웨어로." / sub / CTA 2개: filled accent `프로젝트 문의`→`/outsourcing#contact`, ghost `소프트웨어 보기`→`/products`) · 우(`MockWindow title="stock-report"` 안에 `DashboardMock`). `-mt-16`/스크림/HeroField 전량 제거. 하단 신뢰 스트립(15+ 실서비스 · 24/7 · 원스톱) border-y row.
|
||||
2. 2축 소개 (surface-alt) — `01 OUTSOURCING`/`02 SOFTWARE` 2카드(라벨·제목·요약·링크).
|
||||
3. SHOWCASE (surface) — `ShowcaseGrid slots variant="home"` (6).
|
||||
4. 운영 실증 (surface-alt) — PROOF 3카드 + 스탯(CountUp 15+/24·7/원스톱). 라이트 카드.
|
||||
5. PROCESS (surface) — 4단계 + 가로 연결선.
|
||||
6. 완성 SW (surface-alt) — featured 3(DB) / 0개 coming-soon 폴백, 라이트 카드.
|
||||
7. CTA 밴드 (navy 평면) — "프로젝트, 이야기부터 시작하세요" + 흰 버튼.
|
||||
|
||||
**Steps:**
|
||||
- [ ] **Step 1:** 다크 래퍼/HeroField/스크림 제거, 위 7섹션을 라이트 토큰으로 재작성. 모든 `--jsm-dark-*`/`accent-bright` → 라이트 대응(`--jsm-ink`/`ink-soft`/`accent`).
|
||||
- [ ] **Step 2:** Run `npm run build` — Expected: 성공. (DB 0개 폴백 경로도 타입 통과 확인.)
|
||||
- [ ] **Step 3:** 커밋. `git commit -am "feat(redesign): 홈 라이트 재구성 + 2축 복원 + 히어로 목업"`
|
||||
|
||||
---
|
||||
|
||||
### Task 5: 외주 라이트 전환 (`app/outsourcing/page.tsx` + 폼)
|
||||
|
||||
**Files:**
|
||||
- Modify: `app/outsourcing/page.tsx`
|
||||
- Modify: `app/components/OutsourcingRequestForm.tsx`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 1·2 컴포넌트, 기존 `ScrollReveal`.
|
||||
|
||||
**변경:**
|
||||
- 페이지: 다크 래퍼/HeroField/스크림 제거. 섹션 구조 유지(HERO·SHOWCASE 8·운영 실사례 6·제공분야 6·PROCESS 6·FAQ·CONTACT)를 라이트 토큰으로. 앵커(`#showcase`/`#portfolio`/`#process`/`#contact`) 유지. HERO 우측에 소형 `MockWindow`(`FeedMock` 등) 1개 추가(선택, 2단 비대칭).
|
||||
- 폼: `INPUT_STYLE`·각 `--jsm-dark-*`/`accent-bright`/`rgba(96,165,250,..)` → 라이트(`--jsm-surface`/`--jsm-line`/`--jsm-ink`/`--jsm-accent`/`--jsm-accent-soft`). 래퍼 `className="jsm-dark-form"` 제거. 에러 박스(이미 라이트 `#fef2f2`)는 유지.
|
||||
|
||||
**Steps:**
|
||||
- [ ] **Step 1:** `OutsourcingRequestForm.tsx`의 다크 토큰 전량 라이트 치환 + `jsm-dark-form` 제거.
|
||||
- [ ] **Step 2:** `outsourcing/page.tsx` 라이트 재작성(구조 유지).
|
||||
- [ ] **Step 3:** Run `npm run build` — Expected: 성공.
|
||||
- [ ] **Step 4:** 커밋. `git commit -am "feat(redesign): 외주 페이지 + 의뢰폼 라이트 전환"`
|
||||
|
||||
---
|
||||
|
||||
### Task 6: 제품 craft 정렬 (`app/products/page.tsx`)
|
||||
|
||||
**Files:**
|
||||
- Modify: `app/products/page.tsx`
|
||||
|
||||
**변경:** 이미 라이트 → `max-w-5xl`→`max-w-6xl`, 타입 스케일(h1 clamp·eyebrow·h2)·여백 리듬·카드(rounded-2xl·shadow-sm·hover) 를 홈과 동일 언어로 정렬. 교차 배경(surface↔surface-alt) 적용. 구조·카피 유지.
|
||||
|
||||
**Steps:**
|
||||
- [ ] **Step 1:** 컨테이너·타입·카드 스펙을 공통 언어로 정렬.
|
||||
- [ ] **Step 2:** Run `npm run build` — Expected: 성공.
|
||||
- [ ] **Step 3:** 커밋. `git commit -am "feat(redesign): 제품 페이지 craft 정렬(공통 언어)"`
|
||||
|
||||
---
|
||||
|
||||
### Task 7: 죽은 CSS 제거 + 전체 검증 + 문서 정리
|
||||
|
||||
**Files:**
|
||||
- Modify: `app/globals.css`
|
||||
- Modify: `CLAUDE.md` (다크 토큰 언급 정리 — 가드레일 본문 변경 없음)
|
||||
|
||||
**변경 (globals.css 제거 대상):** `--jsm-dark-*` 토큰, `--kx-*` 매핑, `.kx-section/.kx-display/.kx-label/.kx-folder/.kx-glass/.kx-glow/.kx-btn-*/.kx-gradient-text/.kx-orb`, `.gradient-text`(보라), `.jsm-dark-form`, `.df-scroll-dot` + `@keyframes df-scroll-cue`. **유지:** `--jsm-*` 라이트, `@font-face`, `.reveal*`, `.marquee*`(사용처 grep 후 미사용이면 제거), 스크롤바, `.scrollbar-hide`, `.service-card`.
|
||||
|
||||
**Steps:**
|
||||
- [ ] **Step 1:** `HeroField`/`useFieldMode` import 잔존 grep — Run: `grep -rn "HeroField\|useFieldMode\|jsm-dark\|--kx-\|gradient-text" app lib` — Expected: 코드(컴포넌트 파일 제외)에서 0건. 잔존 시 해당 파일 수정.
|
||||
- [ ] **Step 2:** `globals.css`에서 위 제거 대상 삭제.
|
||||
- [ ] **Step 3:** 가드레일 grep — Run: `grep -rn "jsm-dark\|--kx-\|#7c3aed\|#c4b5fd\|#f0abfc\|backdrop-filter\|blur(" app lib` — Expected: 0건(`globals.css` `.kx`/dark 제거 후).
|
||||
- [ ] **Step 4:** Run `npm test` — Expected: PASS. 이어서 `npm run build` — Expected: 성공.
|
||||
- [ ] **Step 5:** `CLAUDE.md` 디자인 시스템 섹션에서 다크 토큰 잔재 언급 정리(있다면).
|
||||
- [ ] **Step 6:** 커밋. `git commit -am "chore(redesign): 죽은 다크/kx/보라 CSS 제거 + 가드레일 검증 통과"`
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
|
||||
**Spec coverage:**
|
||||
- §3 시스템 기반 → Global Constraints + 각 Task. ✓
|
||||
- §4 MockWindow → Task 1. ✓
|
||||
- §5.1 홈 → Task 4. ✓ / §5.2 외주 → Task 5. ✓ / §5.3 제품 → Task 6. ✓
|
||||
- §6 셸(TopNav/Footer) → Task 3 (Footer는 이미 navy 유지, 변경 없음 명시). ✓
|
||||
- §7 정리 → Task 7. ✓
|
||||
- §9 검증 기준 → Task 2(테스트)·Task 7(grep/build/test). ✓
|
||||
|
||||
**Placeholder scan:** 페이지 JSX 전문 미기재는 의도(계획 altitude 주석). 스크린 목업은 시각 명세로 구체화. 빌딩블록(MockWindow)·테스트는 완전 코드. TBD 없음.
|
||||
|
||||
**Type consistency:** `MockKey`/`MOCK_REGISTRY`(Task1) → `ShowcaseSlot.mock`(Task2)에서 동일 사용. `ShowcaseGrid`의 `variant`/`size`(home|full / feature|standard) 기존 시그니처 유지. ✓
|
||||
158
docs/superpowers/specs/2026-06-30-jsm-light-redesign-design.md
Normal file
158
docs/superpowers/specs/2026-06-30-jsm-light-redesign-design.md
Normal file
@@ -0,0 +1,158 @@
|
||||
# 쟁승메이드 라이트 고craft 재설계 — 설계 문서
|
||||
|
||||
> 작성 2026-06-30 · brainstorming 산출물 (승인 완료)
|
||||
> 대상: `app/page.tsx`(홈) · `app/outsourcing/page.tsx` · `app/products/page.tsx` + 공통 시스템
|
||||
|
||||
---
|
||||
|
||||
## 1. 배경 / 문제 정의
|
||||
|
||||
최근 "Deep Field" 다크 캔버스 재스킨이 검증 없이 얹히면서 다음 문제가 발생했다.
|
||||
|
||||
1. **문서 ↔ 코드 충돌** — `CLAUDE.md` 가드레일(라이트·gradient/blur/보라 금지·`--jsm-*`)을 실제 메인/외주 코드가 정면으로 위반(다크 배경 + WebGL 파티클 + radial gradient + 보라 팔레트).
|
||||
2. **반복된 사후 패치** — 최근 커밋 2개가 전부 "히어로 텍스트 대비 복구" 류 → 다크 파티클 히어로가 픽셀 단위 튜닝에 실패.
|
||||
3. **톤 단절** — 홈·외주는 다크, `/products`는 라이트. 첫 클릭에서 톤이 깨진다.
|
||||
4. **가짜 포트폴리오** — 쇼케이스 8슬롯이 실작업 이미지가 아닌 그래디언트 타일(보라 포함). "AI가 뽑은 가짜" 인상.
|
||||
5. **사이트 정체성 누락** — CLAUDE.md가 규정한 "외주+완성SW 2축" 소개가 홈에 없고 바로 쇼케이스로 점프.
|
||||
6. **죽은 CSS** — `kx-*`(blur), `gradient-text`(보라), `kx-orb/glow`, `--jsm-dark-*`, `--kx-*` 잔존.
|
||||
|
||||
### 타깃·포지셔닝 (의사결정 근거)
|
||||
- 고객: 크몽·숨고·위시캣 트래픽 = 다수가 비개발자 소상공인/실무자.
|
||||
- 무기: "실서비스 15+ 직접 운영"이라는 **운영 실증** (경력 어필 금지 — `feedback_copy_no_career`).
|
||||
- 결론: 다크 스펙터클이 아니라 **라이트·명료 + 진짜 목업**이 신뢰·전환에 유리.
|
||||
|
||||
---
|
||||
|
||||
## 2. 확정된 방향 (승인됨)
|
||||
|
||||
| 결정 | 값 |
|
||||
|------|-----|
|
||||
| 비주얼 방향 | 라이트 기반 고(高)craft + 강조면 1곳 |
|
||||
| 강조면 위치 | **히어로의 코드 제품 목업** (운영 실증을 이미지로) |
|
||||
| 소재 확보 | **코드로 디자인한 UI 목업** (실데이터 0, `--jsm-*` 라이트/navy) |
|
||||
| 범위 | 홈 + 외주 + 제품 3면 통일 + 공통 시스템 정리 |
|
||||
| 가드레일 | 라이트 복귀 = **CLAUDE.md 컴플라이언스 회복** (개정 불필요, 다크 토큰 언급만 정리) |
|
||||
|
||||
---
|
||||
|
||||
## 3. 디자인 시스템 기반 (3면 공통)
|
||||
|
||||
### 색 (─ `--jsm-*` 만)
|
||||
```
|
||||
bg #f8fafc · surface #fff · surface-alt #f1f5f9
|
||||
ink #0f172a · ink-soft #475569 · ink-faint #94a3b8 · line #e2e8f0
|
||||
navy #0b1f3a (푸터 + CTA 1곳, 사이트 유일 다크면)
|
||||
accent #1d4ed8 (유일 포인트) · accent-hover #1e40af · accent-soft #dbeafe
|
||||
금지: 보라/violet · gradient · blur (navy CTA 밴드도 평면 navy로 — radial 광원 제거)
|
||||
```
|
||||
|
||||
### 타이포 (Pretendard)
|
||||
| 역할 | 스펙 |
|
||||
|------|------|
|
||||
| 디스플레이 h1 | `clamp(2.4rem, 7vw, 4rem)` · w800 · `-0.03em` · `break-keep` · lh 1.08 |
|
||||
| 섹션 h2 | `clamp(1.7rem, 4vw, 2.4rem)` · w700 · `-0.02em` |
|
||||
| 모노 라벨(eyebrow) | 11px · UPPER · `0.2em` · accent — 편집 디자인 시그니처 |
|
||||
| 본문 | 16–18px · ink-soft · `-0.01em` · leading-relaxed |
|
||||
|
||||
### 레이아웃·여백·리듬
|
||||
- 컨테이너 `max-w-6xl`(1152) · 패딩 `px-6 lg:px-8`. **3면 동일** (현재 제품은 max-w-5xl로 어긋남 → 통일).
|
||||
- **여백 변주**: 현재 전부 `py-24/32` 단조 → 히어로 큰 호흡, 이후 섹션 `py-20 / py-24 / py-28`로 리듬.
|
||||
- **교차 배경**: `surface`(#fff) ↔ `surface-alt`(#f1f5f9) 교차로 섹션 구분. `border-t` 단독 의존 탈피.
|
||||
- 카드: `rounded-2xl` · `border line` · `shadow-sm` · hover `translateY(-2px)` + border accent.
|
||||
|
||||
### 모션
|
||||
- `ScrollReveal`(fade+rise) 유지. `prefers-reduced-motion` 가드(기존 `.reveal` CSS 활용). 절제.
|
||||
- `CountUp` 유지 (운영 실증 스탯).
|
||||
|
||||
---
|
||||
|
||||
## 4. 핵심 신규 컴포넌트 — `MockWindow` 목업 시스템
|
||||
|
||||
파티클(HeroField)을 대체하는 craft의 핵심. **재사용 가능한 라이트 UI 목업.**
|
||||
|
||||
```
|
||||
app/components/mock/
|
||||
MockWindow.tsx 브라우저/앱 크롬 프레임 (● ● ● 신호등 + 타이틀바 + 본문 슬롯)
|
||||
screens/
|
||||
DashboardMock 스탯 카드 3 + 막대/라인 차트 (주식 리포트 톤)
|
||||
FeedMock 텔레그램풍 메시지 피드 (봇 알림)
|
||||
MatchMock 매물/항목 카드 + 매칭률 배지 (부동산 청약)
|
||||
CommerceMock 상품 그리드 + 장바구니/가격
|
||||
SiteMock 기업 사이트 히어로 와이어 (corporate/portfolio/editorial)
|
||||
BookingMock 예약 캘린더/슬롯 (로컬 매장)
|
||||
```
|
||||
- 전부 SVG/CSS, `--jsm-*` 라이트 + navy 헤더, accent 포인트. **실데이터 없음.**
|
||||
- 결정적 렌더(난수 시드 불필요 — 정적 마크업). SSR-safe(클라이언트 캔버스 의존 제거 → 서버 컴포넌트로 렌더 가능).
|
||||
- 용도: **히어로 1개**(대표 = DashboardMock/FeedMock) + **쇼케이스 N개**.
|
||||
|
||||
---
|
||||
|
||||
## 5. 페이지별 설계
|
||||
|
||||
### 5.1 홈 `/`
|
||||
섹션 순서 (배경 교차 표기):
|
||||
1. **HERO** (surface) — 비대칭 2단: 좌 텍스트(eyebrow·h1·sub·CTA 2) / 우 `MockWindow`(대표 목업). 하단 **신뢰 스트립**(15+ 실서비스 · 24/7 · 원스톱).
|
||||
2. **2축 소개** (surface-alt) — 신규. `01 OUTSOURCING` / `02 SOFTWARE` 2카드. 사이트 정체성 복원.
|
||||
3. **SHOWCASE** (surface) — `ShowcaseGrid` 재작성: 그래디언트 타일 → `MockWindow` 그리드. 홈 6장.
|
||||
4. **운영 실증** (surface-alt) — 3종 카드 + 스탯(CountUp 15+/24·7/원스톱). 라이트 카드 통일.
|
||||
5. **PROCESS** (surface) — 4단계 + 가로 연결선.
|
||||
6. **완성 SW** (surface-alt) — featured 3종(DB, `getListedProducts`). 0개면 coming-soon 폴백(라이트).
|
||||
7. **CTA 밴드** (navy) — 사이트 유일 다크면. 평면 navy(radial gradient 제거). "프로젝트, 이야기부터".
|
||||
|
||||
삭제: `HeroField` 사용, 좌측 스크림/비네트, `-mt-16` 다크 풀블리드 트릭(라이트라 불필요).
|
||||
|
||||
### 5.2 외주 `/outsourcing` — 다크→라이트 전환 (구조 유지)
|
||||
```
|
||||
HERO(라이트, 소형 MockWindow 1개) → 제공 분야 6 → 운영 실사례 6(라이트 카드)
|
||||
→ SHOWCASE 풀그리드 8(MockWindow) → PROCESS 6단계 → FAQ(아코디언) → 의뢰 폼
|
||||
```
|
||||
- 의뢰 폼: 라이트 스킨. `.jsm-dark-form` placeholder 규칙 제거/라이트화. `OutsourcingRequestForm` 입력 가독성 복구.
|
||||
- 앵커(`#showcase`/`#portfolio`/`#process`/`#contact`) 유지.
|
||||
|
||||
### 5.3 제품 `/products` — 이미 라이트, craft 격상
|
||||
```
|
||||
HERO → 카탈로그(2열 카드) → 구매방식 3단계 → CTA
|
||||
```
|
||||
- `max-w-5xl` → `max-w-6xl`, 타입 스케일·여백을 홈과 동일 언어로 정렬.
|
||||
- 카드 hover·라운드·그림자를 공통 카드 스펙에 맞춤.
|
||||
|
||||
---
|
||||
|
||||
## 6. 공통 셸
|
||||
|
||||
- **TopNav** — "다크 인지형" 라우트 분기 제거 → 단일 라이트 네비(흰 배경 + 하단 line, 스크롤 시 미세 shadow) + 우측 `프로젝트 문의` CTA. (구현 시 현 코드 확인 후 최소 수정.)
|
||||
- **Footer** — navy 유지. 사이트 유일 다크면(CTA 밴드와 함께).
|
||||
|
||||
---
|
||||
|
||||
## 7. 정리·마이그레이션
|
||||
|
||||
- `app/globals.css`:
|
||||
- 제거: `--jsm-dark-*` 토큰, `--kx-*` 매핑, `.kx-*`(glass/orb/glow/folder/...), `.gradient-text`(보라), `.kx-gradient-text`, `.jsm-dark-form`, `.df-scroll-dot`(파티클 전용).
|
||||
- 유지: `--jsm-*` 라이트, `.reveal*`, `.marquee*`(사용처 확인 후), 스크롤바, `.scrollbar-hide`.
|
||||
- `lib/showcase.ts`: `palette/accent` 그래디언트 스펙 → **목업 타입 스펙**(`mock: 'dashboard'|'commerce'|...`)으로 교체. 보라 4슬롯 제거/치환.
|
||||
- `app/components/deepfield/`:
|
||||
- `ShowcaseCard.tsx` → `MockWindow` 기반으로 재작성(또는 `app/components/mock/`로 이전).
|
||||
- `ShowcaseGrid.tsx` 유지(레이아웃 로직) — 카드만 교체.
|
||||
- `HeroField.tsx`·`useFieldMode.ts` — 홈/외주에서 import 제거. 파일은 보존만(미사용). three 의존 트리셰이킹 확인.
|
||||
- `CLAUDE.md`: 디자인 시스템 섹션에서 다크 토큰 언급 정리(가드레일 본문은 이미 라이트 → 변경 불필요).
|
||||
|
||||
---
|
||||
|
||||
## 8. 비목표 (YAGNI)
|
||||
- 다크 모드 토글/테마 시스템 (불필요).
|
||||
- 실제 스크린샷 수집·마스킹 파이프라인 (코드 목업으로 대체).
|
||||
- admin/mypage/legal 등 비공개·내부 페이지 재설계 (이번 범위 밖 — 이미 라이트).
|
||||
- 카피 전면 재작성 (기존 카피 유지, 구조·톤만 변경. 단 경력 어필 카피는 금지 유지).
|
||||
|
||||
---
|
||||
|
||||
## 9. 검증 기준
|
||||
- [ ] 3면 모두 라이트 `--jsm-*`만 사용, 다크 토큰/보라/blur/임의 색 0건 (grep).
|
||||
- [ ] 홈→외주→제품 클릭 시 톤 단절 없음.
|
||||
- [ ] 쇼케이스가 코드 목업(실화면 느낌)으로 렌더, 그래디언트 타일 0건.
|
||||
- [ ] 홈에 "2축 소개" 섹션 존재.
|
||||
- [ ] 의뢰 폼 입력 텍스트·placeholder 가독성 정상(라이트).
|
||||
- [ ] `npm run build` 성공 + `npm test`(lib 단위) 통과.
|
||||
- [ ] 죽은 CSS(`kx-*`/`gradient-text` 등) 제거 확인.
|
||||
- [ ] `prefers-reduced-motion` 시 모션 정지.
|
||||
40
lib/__tests__/showcase.test.ts
Normal file
40
lib/__tests__/showcase.test.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { SHOWCASE_SLOTS } from '@/lib/showcase';
|
||||
import { MOCK_KEYS } from '@/app/components/mock/keys';
|
||||
|
||||
// 가드레일: 쇼케이스 슬롯이 라이트 목업 기반이고 보라/그래디언트 잔재가 없어야 한다.
|
||||
const VIOLET_HEXES = ['#c4b5fd', '#f0abfc', '#341a4f', '#4a1342', '#7c3aed', '#9c48ea'];
|
||||
|
||||
describe('SHOWCASE_SLOTS 가드레일', () => {
|
||||
it('8슬롯이고 slug가 고유하다', () => {
|
||||
expect(SHOWCASE_SLOTS.length).toBe(8);
|
||||
const slugs = SHOWCASE_SLOTS.map((s) => s.slug);
|
||||
expect(new Set(slugs).size).toBe(slugs.length);
|
||||
});
|
||||
|
||||
it('각 슬롯의 mock이 유효한 MockKey이고 핵심 필드가 비어있지 않다', () => {
|
||||
for (const s of SHOWCASE_SLOTS) {
|
||||
expect(MOCK_KEYS).toContain(s.mock);
|
||||
expect(s.slug.length).toBeGreaterThan(0);
|
||||
expect(s.label.length).toBeGreaterThan(0);
|
||||
expect(s.title.length).toBeGreaterThan(0);
|
||||
expect(s.desc.length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
it('어떤 슬롯에도 보라/그래디언트 hex가 남아있지 않다', () => {
|
||||
const serialized = JSON.stringify(SHOWCASE_SLOTS).toLowerCase();
|
||||
for (const hex of VIOLET_HEXES) {
|
||||
expect(serialized).not.toContain(hex.toLowerCase());
|
||||
}
|
||||
// 더 이상 palette 필드를 갖지 않는다 (라이트 목업 전환).
|
||||
for (const s of SHOWCASE_SLOTS) {
|
||||
expect('palette' in s).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it('목업 종류가 최소 4가지 이상으로 다양하다 (단조 방지)', () => {
|
||||
const uniqueMocks = new Set(SHOWCASE_SLOTS.map((s) => s.mock));
|
||||
expect(uniqueMocks.size).toBeGreaterThanOrEqual(4);
|
||||
});
|
||||
});
|
||||
@@ -1,22 +1,23 @@
|
||||
/** Deep Field 쇼케이스 8슬롯 — 단일 소스.
|
||||
* href가 있는 슬롯만 클릭 가능 (샘플 리뉴얼 완료 시 href 추가). */
|
||||
/** Deep Field 쇼케이스 8슬롯 — 단일 소스 (라이트 MockWindow 목업 기반).
|
||||
* href가 있는 슬롯만 클릭 가능 (샘플 데모 완료 시 href 추가). */
|
||||
import type { MockKey } from '@/app/components/mock/keys';
|
||||
|
||||
export interface ShowcaseSlot {
|
||||
slug: string;
|
||||
label: string; // 모노스페이스 컨셉 라벨 (영문)
|
||||
title: string; // 카드 타이틀 (한글)
|
||||
desc: string; // 한 줄 설명
|
||||
palette: [string, string]; // 카드 고유 그래디언트 월드 [from, to]
|
||||
accent: string; // 카드 포인트 컬러
|
||||
href?: string; // 리뉴얼 완료된 샘플의 데모 링크
|
||||
mock: MockKey; // 카드에 렌더할 라이트 목업 화면
|
||||
href?: string; // 데모 링크 (있으면 클릭 가능)
|
||||
}
|
||||
|
||||
export const SHOWCASE_SLOTS: ShowcaseSlot[] = [
|
||||
{ slug: 'corporate', label: 'corporate', title: '기업 브랜드 사이트', desc: '신뢰를 첫인상으로 — 브랜드 스토리와 IR까지', palette: ['#13203a', '#0d2c54'], accent: '#60a5fa' },
|
||||
{ slug: 'shopping', label: 'commerce', title: '커머스 스토어', desc: '탐색부터 결제까지 끊김 없는 구매 동선', palette: ['#1a1430', '#341a4f'], accent: '#c4b5fd' },
|
||||
{ slug: 'dashboard', label: 'dashboard', title: '데이터 대시보드', desc: '실시간 지표를 한눈에 — 의사결정용 화면', palette: ['#0f2922', '#14503c'], accent: '#6ee7b7' },
|
||||
{ slug: 'bakery', label: 'local shop', title: '로컬 매장 사이트', desc: '예약·주문이 자연스러운 동네 가게의 얼굴', palette: ['#2b1a10', '#4f2d14'], accent: '#fdba74' },
|
||||
{ slug: 'portfolio', label: 'portfolio', title: '포트폴리오', desc: '작업물이 주인공이 되는 미니멀 갤러리', palette: ['#101418', '#23272d'], accent: '#e2e8f0' },
|
||||
{ slug: 'game', label: 'game', title: '게임 프로모션', desc: '세계관에 빠져들게 하는 런칭 페이지', palette: ['#250f23', '#4a1342'], accent: '#f0abfc' },
|
||||
{ slug: 'interior', label: 'interior', title: '인테리어 스튜디오', desc: '공간의 톤을 그대로 옮긴 쇼룸', palette: ['#1f2218', '#3a4028'], accent: '#d9f99d' },
|
||||
{ slug: 'reading', label: 'editorial', title: '에디토리얼·매거진', desc: '읽는 경험을 설계한 콘텐츠 사이트', palette: ['#101b2b', '#1f3a5f'], accent: '#93c5fd' },
|
||||
{ slug: 'corporate', label: 'corporate', title: '기업 브랜드 사이트', desc: '신뢰를 첫인상으로 — 브랜드 스토리와 회사 소개', mock: 'site' },
|
||||
{ slug: 'commerce', label: 'commerce', title: '커머스 스토어', desc: '탐색부터 결제까지 끊김 없는 구매 동선', mock: 'commerce' },
|
||||
{ slug: 'dashboard', label: 'dashboard', title: '데이터 대시보드', desc: '실시간 지표를 한눈에 — 의사결정용 화면', mock: 'dashboard' },
|
||||
{ slug: 'automation', label: 'automation', title: '봇·자동화 알림', desc: '체결·알림·리포트를 사람 손 없이 자동 전송', mock: 'feed' },
|
||||
{ slug: 'matching', label: 'matching', title: '조건 매칭 시스템', desc: '수집·필터·매칭으로 원하는 것만 골라내는 화면', mock: 'match' },
|
||||
{ slug: 'booking', label: 'local shop', title: '예약·매장 사이트', desc: '예약·주문이 자연스러운 동네 가게의 얼굴', mock: 'booking' },
|
||||
{ slug: 'portfolio', label: 'portfolio', title: '포트폴리오', desc: '작업물이 주인공이 되는 미니멀 갤러리', mock: 'site' },
|
||||
{ slug: 'editorial', label: 'editorial', title: '에디토리얼·매거진', desc: '읽는 경험을 설계한 콘텐츠 사이트', mock: 'site' },
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user